Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From trunk To v0.8.0
2024-04-22
| ||
15:01 | Rename package sxhtml to sxwebs/sxhtml; update some dependencies ... (Leaf check-in: 3e4f0da507 user: stern tags: trunk) | |
2024-04-18
| ||
13:30 | Adapt to client change: api.URLBuilder ... (check-in: 85cb9d749b user: stern tags: trunk) | |
2022-10-24
| ||
15:42 | Increase version to 0.9.0-dev to begin next development cycle ... (check-in: f38439155f user: stern tags: trunk) | |
2022-10-20
| ||
17:44 | Version 0.8.0 ... (check-in: 2790cb4cf1 user: stern tags: trunk, release, v0.8.0) | |
12:39 | Add manual to help when HTML content is not shown ... (check-in: f8a4ebc639 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 .fossil-settings/ignore-glob.
1 2 | bin/* releases/* | > | 1 2 3 | bin/* releases/* parser/pikchr/*.out |
Changes to LICENSE.txt.
|
| | | 1 2 3 4 5 6 7 8 | Copyright (c) 2020-2022 Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the EU. The English version is included here. Please see https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official |
︙ | ︙ |
Changes to Makefile.
1 |
| | | | | | | | | | | | 1 2 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 | ## 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 relcheck api build release clean check: go run tools/build.go check relcheck: go run tools/build.go relcheck api: go run tools/build.go testapi 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 README.md.
︙ | ︙ | |||
9 10 11 12 13 14 15 | gradually, one major focus is a long-term store of these notes, hence the name “Zettelstore”. To get an initial impression, take a look at the [manual](https://zettelstore.de/manual/). It is a live example of the zettelstore software, running in read-only mode. | | | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | gradually, one major focus is a long-term store of these notes, hence the name “Zettelstore”. To get an initial impression, take a look at the [manual](https://zettelstore.de/manual/). It is a live example of the zettelstore software, running in read-only mode. [Zettelstore Client](https://zettelstore.de/client) provides client software to access Zettelstore via its API more easily, [Zettelstore Contrib](https://zettelstore.de/contrib) contains contributed software, which often connects to Zettelstore via its API. Some of the software packages may be experimental. The software, including the manual, is licensed under the [European Union Public License 1.2 (or later)](https://zettelstore.de/home/file?name=LICENSE.txt&ci=trunk). [Stay tuned](https://twitter.com/zettelstore) … |
Changes to VERSION.
|
| | | 1 | 0.8.0 |
Changes to ast/ast.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree for parsed zettel content. package ast import ( "net/url" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ZettelNode is the root node of the abstract syntax tree. // It is *not* part of the visitor pattern. type ZettelNode struct { Meta *meta.Meta // Original metadata Content domain.Content // Original content Zid id.Zid // Zettel identification. InhMeta *meta.Meta // Metadata of the zettel, with inherited values. Ast BlockSlice // Zettel abstract syntax tree is a sequence of block nodes. Syntax string // Syntax / parser that produced the Ast } // Node is the interface, all nodes must implement. |
︙ | ︙ |
Changes to ast/block.go.
1 | //----------------------------------------------------------------------------- | | < < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast import "zettelstore.de/c/attrs" // Definition of Block nodes. // BlockSlice is a slice of BlockNodes. type BlockSlice []BlockNode func (*BlockSlice) blockNode() { /* Just a marker */ } |
︙ | ︙ | |||
88 89 90 91 92 93 94 95 96 97 98 99 100 101 | ) func (*VerbatimNode) blockNode() { /* Just a marker */ } func (*VerbatimNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // RegionNode encapsulates a region of block nodes. type RegionNode struct { Kind RegionKind Attrs attrs.Attributes | > > > > > | 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | ) func (*VerbatimNode) blockNode() { /* Just a marker */ } func (*VerbatimNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ } // Supported syntax values for VerbatimEval. const ( VerbatimEvalSyntaxDraw = "draw" ) //-------------------------------------------------------------------------- // RegionNode encapsulates a region of block nodes. type RegionNode struct { Kind RegionKind Attrs attrs.Attributes |
︙ | ︙ | |||
280 281 282 283 284 285 286 | func (*TranscludeNode) WalkChildren(Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // BLOBNode contains just binary data that must be interpreted according to // a syntax. type BLOBNode struct { | | | | | 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | func (*TranscludeNode) WalkChildren(Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // BLOBNode contains just binary data that must be interpreted according to // a syntax. type BLOBNode struct { Title string Syntax string Blob []byte } func (*BLOBNode) blockNode() { /* Just a marker */ } // WalkChildren does nothing. func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ } |
Changes to ast/inline.go.
1 | //----------------------------------------------------------------------------- | | < < < | | 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast import ( "unicode/utf8" "zettelstore.de/c/attrs" ) // Definitions of inline nodes. // InlineSlice is a list of BlockNodes. type InlineSlice []InlineNode |
︙ | ︙ | |||
194 195 196 197 198 199 200 | // FormatKind specifies the format that is applied to the inline nodes. type FormatKind int // Constants for FormatCode const ( _ FormatKind = iota | | | | | | | | < | | 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | // FormatKind specifies the format that is applied to the inline nodes. type FormatKind int // Constants for FormatCode const ( _ FormatKind = iota FormatEmph // Emphasized text. FormatStrong // Strongly emphasized text. FormatInsert // Inserted text. FormatDelete // Deleted text. FormatSuper // Superscripted text. FormatSub // SubscriptedText. FormatQuote // Quoted text. FormatSpan // Generic inline container. ) func (*FormatNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the formatted text. func (fn *FormatNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) } |
︙ | ︙ |
Changes to ast/ref.go.
1 | //----------------------------------------------------------------------------- | | < < < < | | | | < < < < < | 1 2 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast import ( "net/url" "strings" "zettelstore.de/z/domain/id" ) // QueryPrefix is the prefix that denotes a query expression. const QueryPrefix = "query:" // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { if s == "" || s == "00000000000000" { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } if strings.HasPrefix(s, QueryPrefix) { return &Reference{URL: nil, Value: s[len(QueryPrefix):], State: RefStateQuery} } if state, ok := localState(s); ok { if state == RefStateBased { s = s[1:] } u, err := url.Parse(s) if err == nil { return &Reference{URL: u, Value: s, State: state} } } u, err := url.Parse(s) if err != nil { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } if 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 } |
︙ | ︙ |
Changes to ast/ref_test.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast_test import ( "testing" |
︙ | ︙ |
Changes to ast/walk.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast // Visitor is a visitor for walking the AST. type Visitor interface { Visit(node Node) Visitor |
︙ | ︙ |
Changes to ast/walk_test.go.
1 | //----------------------------------------------------------------------------- | | < < < | | 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast_test import ( "testing" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" ) func BenchmarkWalk(b *testing.B) { root := ast.BlockSlice{ &ast.HeadingNode{ Inlines: ast.CreateInlineSliceFromWords("A", "Simple", "Heading"), |
︙ | ︙ | |||
60 61 62 63 64 65 66 | Ref: &ast.Reference{Value: "http://zettelstore.de"}, Inlines: ast.CreateInlineSliceFromWords("URL", "text."), }, ), } v := benchVisitor{} b.ResetTimer() | | | 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | Ref: &ast.Reference{Value: "http://zettelstore.de"}, Inlines: ast.CreateInlineSliceFromWords("URL", "text."), }, ), } v := benchVisitor{} b.ResetTimer() for n := 0; n < b.N; n++ { ast.Walk(&v, &root) } } type benchVisitor struct{} func (bv *benchVisitor) Visit(ast.Node) ast.Visitor { return bv } |
Changes to auth/auth.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | 1 2 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) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package auth provides services for authentification / authorization. package auth import ( "time" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // BaseManager allows to check some base auth modes. type BaseManager interface { // IsReadonly returns true, if the systems is configured to run in read-only-mode. IsReadonly() bool } |
︙ | ︙ | |||
41 42 43 44 45 46 47 | // TokenKind specifies for which application / usage a token is/was requested. type TokenKind int // Allowed values of token kind const ( _ TokenKind = iota | | | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // TokenKind specifies for which application / usage a token is/was requested. type TokenKind int // Allowed values of token kind const ( _ TokenKind = iota KindJSON KindHTML ) // TokenData contains some important elements from a token. type TokenData struct { Token []byte Now time.Time Issued time.Time |
︙ | ︙ |
Changes to auth/cred/cred.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | 1 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. //----------------------------------------------------------------------------- // 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 { |
︙ | ︙ | |||
43 44 45 46 47 48 49 | return false, nil } return false, err } func createFullCredential(zid id.Zid, ident, credential string) []byte { var buf bytes.Buffer | | | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | 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() } |
Deleted auth/impl/digest.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to auth/impl/impl.go.
1 | //----------------------------------------------------------------------------- | | < < < > | | < | | | | 1 2 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) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides services for authentification / authorization. package impl import ( "errors" "hash/fnv" "io" "time" "github.com/pascaldekloe/jwt" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) type myAuth struct { readonly bool owner id.Zid secret []byte } |
︙ | ︙ | |||
66 67 68 69 70 71 72 | } return h.Sum(nil) } // IsReadonly returns true, if the systems is configured to run in read-only-mode. func (a *myAuth) IsReadonly() bool { return a.readonly } | | < | | | < | > > > | | > > > > | > > | < | < | | < < < < < < < | > > > | < | | | | > > > > | > | | > > | < < > | | | < < < < < | | 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 | } return h.Sum(nil) } // IsReadonly returns true, if the systems is configured to run in read-only-mode. func (a *myAuth) IsReadonly() bool { return a.readonly } const reqHash = jwt.HS512 // ErrNoIdent signals that the 'ident' key is missing. var ErrNoIdent = errors.New("auth: missing ident") // ErrOtherKind signals that the token was defined for another token kind. var ErrOtherKind = errors.New("auth: wrong token kind") // ErrNoZid signals that the 'zid' key is missing. var ErrNoZid = errors.New("auth: missing zettel id") // GetToken returns a token to be used for authentification. func (a *myAuth) GetToken(ident *meta.Meta, d time.Duration, kind auth.TokenKind) ([]byte, error) { subject, ok := ident.Get(api.KeyUserID) if !ok || subject == "" { return nil, ErrNoIdent } now := time.Now().Round(time.Second) claims := jwt.Claims{ Registered: jwt.Registered{ Subject: subject, Expires: jwt.NewNumericTime(now.Add(d)), Issued: jwt.NewNumericTime(now), }, Set: map[string]interface{}{ "zid": ident.Zid.String(), "_tk": int(kind), }, } token, err := claims.HMACSign(reqHash, a.secret) if err != nil { return nil, err } return token, nil } // ErrTokenExpired signals an exired token var ErrTokenExpired = errors.New("auth: token expired") // CheckToken checks the validity of the token and returns relevant data. func (a *myAuth) CheckToken(token []byte, k auth.TokenKind) (auth.TokenData, error) { h, err := jwt.NewHMAC(reqHash, a.secret) if err != nil { return auth.TokenData{}, err } claims, err := h.Check(token) if err != nil { return auth.TokenData{}, err } now := time.Now().Round(time.Second) expires := claims.Expires.Time() if expires.Before(now) { return auth.TokenData{}, ErrTokenExpired } ident := claims.Subject if ident == "" { return auth.TokenData{}, ErrNoIdent } if zidS, ok := claims.Set["zid"].(string); ok { if zid, err2 := id.Parse(zidS); err2 == nil { if kind, ok2 := claims.Set["_tk"].(float64); ok2 { if auth.TokenKind(kind) == k { return auth.TokenData{ Token: token, Now: now, Issued: claims.Issued.Time(), Expires: expires, Ident: ident, Zid: zid, }, nil } } return auth.TokenData{}, ErrOtherKind } } return auth.TokenData{}, ErrNoZid } func (a *myAuth) Owner() id.Zid { return a.owner } func (a *myAuth) IsOwner(zid id.Zid) bool { return zid.IsValid() && zid == a.owner } |
︙ | ︙ |
Changes to auth/policy/anon.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | 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-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 import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" ) type anonPolicy struct { authConfig config.AuthConfig pre auth.Policy } |
︙ | ︙ |
Changes to auth/policy/box.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | | > > > > | 1 2 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package policy import ( "context" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" "zettelstore.de/z/web/server" ) // BoxWithPolicy wraps the given box inside a policy box. func BoxWithPolicy( manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig, ) (box.Box, auth.Policy) { pol := newPolicy(manager, authConfig) return newBox(box, pol), pol } // polBox implements a policy box. type polBox struct { box box.Box |
︙ | ︙ | |||
50 51 52 53 54 55 56 | return pp.box.Location() } func (pp *polBox) CanCreateZettel(ctx context.Context) bool { return pp.box.CanCreateZettel(ctx) } | | | | | | | | | < < < < > > > > > > > > | | | | | | | | | | | < < < < < < < < | 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 | return pp.box.Location() } func (pp *polBox) CanCreateZettel(ctx context.Context) bool { return pp.box.CanCreateZettel(ctx) } func (pp *polBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { user := server.GetUser(ctx) if pp.policy.CanCreate(user, zettel.Meta) { return pp.box.CreateZettel(ctx, zettel) } return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid) } func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { zettel, err := pp.box.GetZettel(ctx, zid) if err != nil { return domain.Zettel{}, err } user := server.GetUser(ctx) if pp.policy.CanRead(user, zettel.Meta) { return zettel, nil } return domain.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) } func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { return pp.box.GetAllZettel(ctx, zid) } func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.box.GetMeta(ctx, zid) if err != nil { return nil, err } user := server.GetUser(ctx) if pp.policy.CanRead(user, m) { return m, nil } return nil, box.NewErrNotAllowed("GetMeta", user, zid) } func (pp *polBox) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { return pp.box.GetAllMeta(ctx, zid) } func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) { return nil, box.NewErrNotAllowed("fetch-zids", server.GetUser(ctx), id.Invalid) } func (pp *polBox) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { user := server.GetUser(ctx) canRead := pp.policy.CanRead q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) return pp.box.SelectMeta(ctx, q) } func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return pp.box.CanUpdateZettel(ctx, zettel) } func (pp *polBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { zid := zettel.Meta.Zid user := server.GetUser(ctx) if !zid.IsValid() { return &box.ErrInvalidID{Zid: zid} } // Write existing zettel oldMeta, err := pp.box.GetMeta(ctx, zid) if err != nil { return err } if pp.policy.CanWrite(user, oldMeta, zettel.Meta) { return pp.box.UpdateZettel(ctx, zettel) } return box.NewErrNotAllowed("Write", user, zid) } func (pp *polBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return pp.box.AllowRenameZettel(ctx, zid) } func (pp *polBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { meta, err := pp.box.GetMeta(ctx, curZid) if err != nil { return err } user := server.GetUser(ctx) if pp.policy.CanRename(user, meta) { return pp.box.RenameZettel(ctx, curZid, newZid) } return box.NewErrNotAllowed("Rename", user, curZid) } func (pp *polBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return pp.box.CanDeleteZettel(ctx, zid) } func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error { meta, err := pp.box.GetMeta(ctx, zid) if err != nil { return err } user := server.GetUser(ctx) if pp.policy.CanDelete(user, meta) { return pp.box.DeleteZettel(ctx, zid) } return box.NewErrNotAllowed("Delete", user, zid) } func (pp *polBox) Refresh(ctx context.Context) error { user := server.GetUser(ctx) if pp.policy.CanRefresh(user) { return pp.box.Refresh(ctx) } return box.NewErrNotAllowed("Refresh", user, id.Invalid) } |
Changes to auth/policy/default.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | 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-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 import ( "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" ) type defaultPolicy struct { manager auth.AuthzManager } func (*defaultPolicy) CanCreate(_, _ *meta.Meta) bool { return true } |
︙ | ︙ |
Changes to auth/policy/owner.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | 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 policy import ( "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" ) type ownerPolicy struct { manager auth.AuthzManager authConfig config.AuthConfig pre auth.Policy } |
︙ | ︙ |
Changes to auth/policy/policy.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | 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 policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" ) // newPolicy creates a policy based on given constraints. func newPolicy(manager auth.AuthzManager, authConfig config.AuthConfig) auth.Policy { var pol auth.Policy if manager.IsReadonly() { pol = &roPolicy{} |
︙ | ︙ |
Changes to auth/policy/policy_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | 1 2 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-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 import ( "fmt" "testing" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestPolicies(t *testing.T) { t.Parallel() testScene := []struct { readonly bool withAuth bool |
︙ | ︙ |
Changes to auth/policy/readonly.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //----------------------------------------------------------------------------- // 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 import "zettelstore.de/z/domain/meta" type roPolicy struct{} func (*roPolicy) CanCreate(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanRead(_, _ *meta.Meta) bool { return true } func (*roPolicy) CanWrite(_, _, _ *meta.Meta) bool { return false } func (*roPolicy) CanRename(_, _ *meta.Meta) bool { return false } |
︙ | ︙ |
Changes to box/box.go.
1 | //----------------------------------------------------------------------------- | | < < < > > | | | | | > > > > > > > | > > > > > > > > > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < | | > > > < < < | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package box provides a generic interface to zettel boxes. package box import ( "context" "errors" "fmt" "io" "net/url" "strconv" "time" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" ) // BaseBox is implemented by all Zettel boxes. type BaseBox interface { // Location returns some information where the box is located. // Format is dependent of the box. Location() string // CanCreateZettel returns true, if box could possibly create a new zettel. CanCreateZettel(ctx context.Context) bool // CreateZettel creates a new zettel. // Returns the new zettel id (and an error indication). CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) // 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) // CanUpdateZettel returns true, if box could possibly update the given zettel. CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool // UpdateZettel updates an existing zettel. UpdateZettel(ctx context.Context, zettel domain.Zettel) error // AllowRenameZettel returns true, if box will not disallow renaming the zettel. AllowRenameZettel(ctx context.Context, zid id.Zid) bool // RenameZettel changes the current Zid to a new Zid. RenameZettel(ctx context.Context, curZid, newZid id.Zid) error // CanDeleteZettel returns true, if box could possibly delete the given zettel. CanDeleteZettel(ctx context.Context, zid id.Zid) bool // DeleteZettel removes the zettel from the box. DeleteZettel(ctx context.Context, zid id.Zid) error } // ZidFunc is a function that processes identifier of a zettel. type ZidFunc func(id.Zid) // MetaFunc is a function that processes metadata of a zettel. type MetaFunc func(*meta.Meta) // ManagedBox is the interface of managed boxes. type ManagedBox interface { BaseBox // Apply identifier of every zettel to the given function, if predicate returns true. ApplyZid(context.Context, ZidFunc, query.RetrievePredicate) error // Apply metadata of every zettel to the given function, if predicate returns true. ApplyMeta(context.Context, MetaFunc, query.RetrievePredicate) error // ReadStats populates st with box statistics ReadStats(st *ManagedBoxStats) } // ManagedBoxStats records statistics about the box. type ManagedBoxStats struct { // ReadOnly indicates that the content of a box cannot change. ReadOnly bool // Zettel is the number of zettel managed by the box. Zettel int } // StartStopper performs simple lifecycle management. type StartStopper interface { // Start the box. Now all other functions of the box are allowed. // Starting an already started box is not allowed. Start(ctx context.Context) error // Stop the started box. Now only the Start() function is allowed. Stop(ctx context.Context) } // Refresher allow to refresh their internal data. type Refresher interface { // Refresh the box data. Refresh(context.Context) } // Box is to be used outside the box package and its descendants. type Box interface { BaseBox // FetchZids returns the set of all zettel identifer managed by the box. FetchZids(ctx context.Context) (id.Set, error) // SelectMeta returns a list of metadata that comply to the given selection criteria. SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) // GetAllZettel retrieves a specific zettel from all managed boxes. GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) // GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) // Refresh the data from the box and from its managed sub-boxes. Refresh(context.Context) error } // Stats record stattistics about a box. type Stats struct { // ReadOnly indicates that boxes cannot be modified. ReadOnly bool |
︙ | ︙ | |||
206 207 208 209 210 211 212 | // UpdateReason gives an indication, why the ObserverFunc was called. type UpdateReason uint8 // Values for Reason const ( _ UpdateReason = iota | < | | 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | // UpdateReason gives an indication, why the ObserverFunc was called. type UpdateReason uint8 // Values for Reason const ( _ UpdateReason = iota OnReload // Box was reloaded OnZettel // Something with a zettel happened ) // UpdateInfo contains all the data about a changed zettel. type UpdateInfo struct { Box Box Reason UpdateReason Zid id.Zid } // UpdateFunc is a function to be called when a change is detected. type UpdateFunc func(UpdateInfo) |
︙ | ︙ | |||
252 253 254 255 256 257 258 | // DoNotEnrich determines if the context is marked to not enrich metadata. func DoNotEnrich(ctx context.Context) bool { _, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType) return ok } | < < < < < < < < | 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | // DoNotEnrich determines if the context is marked to not enrich metadata. func DoNotEnrich(ctx context.Context) bool { _, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType) return ok } // ErrNotAllowed is returned if the caller is not allowed to perform the operation. type ErrNotAllowed struct { Op string User *meta.Meta Zid id.Zid } |
︙ | ︙ | |||
307 308 309 310 311 312 313 | // ErrStopped is returned if calling methods on a box that was not started. var ErrStopped = errors.New("box is stopped") // ErrReadOnly is returned if there is an attepmt to write to a read-only box. var ErrReadOnly = errors.New("read-only box") | | < | < | | | > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | // ErrStopped is returned if calling methods on a box that was not started. var ErrStopped = errors.New("box is stopped") // ErrReadOnly is returned if there is an attepmt to write to a read-only box. var ErrReadOnly = errors.New("read-only box") // ErrNotFound is returned if a zettel was not found in the box. var ErrNotFound = errors.New("zettel not found") // ErrConflict is returned if a box operation detected a conflict.. // One example: if calculating a new zettel identifier takes too long. var ErrConflict = errors.New("conflict") // ErrCapacity is returned if a box has reached its capacity. var ErrCapacity = errors.New("capacity exceeded") // ErrInvalidID is returned if the zettel id is not appropriate for the box operation. type ErrInvalidID struct{ Zid id.Zid } func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() } // GetQueryBool is a helper function to extract bool values from a box URI. func GetQueryBool(u *url.URL, key string) bool { _, ok := u.Query()[key] return ok } // GetQueryInt is a helper function to extract int values of a specified range from a box URI. func GetQueryInt(u *url.URL, key string, min, def, max int) int { sVal := u.Query().Get(key) if sVal == "" { return def } iVal, err := strconv.Atoi(sVal) if err != nil { return def } if iVal < min { return min } if iVal > max { return max } return iVal } |
Changes to box/compbox/compbox.go.
1 | //----------------------------------------------------------------------------- | | < < < | > > > < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" ) func init() { manager.Register( " comp", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return getCompBox(cdata.Number, cdata.Enricher), nil |
︙ | ︙ | |||
48 49 50 51 52 53 54 | meta func(id.Zid) *meta.Meta content func(*meta.Meta) []byte }{ id.MustParse(api.ZidVersion): {genVersionBuildM, genVersionBuildC}, id.MustParse(api.ZidHost): {genVersionHostM, genVersionHostC}, id.MustParse(api.ZidOperatingSystem): {genVersionOSM, genVersionOSC}, id.MustParse(api.ZidLog): {genLogM, genLogC}, | < | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | meta func(id.Zid) *meta.Meta content func(*meta.Meta) []byte }{ id.MustParse(api.ZidVersion): {genVersionBuildM, genVersionBuildC}, id.MustParse(api.ZidHost): {genVersionHostM, genVersionHostC}, id.MustParse(api.ZidOperatingSystem): {genVersionOSM, genVersionOSC}, id.MustParse(api.ZidLog): {genLogM, genLogC}, id.MustParse(api.ZidBoxManager): {genManagerM, genManagerC}, id.MustParse(api.ZidMetadataKey): {genKeysM, genKeysC}, id.MustParse(api.ZidParser): {genParserM, genParserC}, id.MustParse(api.ZidStartupConfiguration): {genConfigZettelM, genConfigZettelC}, } // Get returns the one program box. |
︙ | ︙ | |||
70 71 72 73 74 75 76 | } // Setup remembers important values. func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } func (*compBox) Location() string { return "" } | > > > > > > > | | | | | | < | | | | > > > > | > > > > > | | 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 | } // Setup remembers important values. func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } func (*compBox) Location() string { return "" } func (*compBox) CanCreateZettel(context.Context) bool { return false } func (cb *compBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) { cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel") return id.Invalid, box.ErrReadOnly } func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { if gen, ok := myZettel[zid]; ok && gen.meta != nil { if m := gen.meta(zid); m != nil { updateMeta(m) if genContent := gen.content; genContent != nil { cb.log.Trace().Msg("GetMeta/Content") return domain.Zettel{ Meta: m, Content: domain.NewContent(genContent(m)), }, nil } cb.log.Trace().Msg("GetMeta/NoContent") return domain.Zettel{Meta: m}, nil } } cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel/Err") return domain.Zettel{}, box.ErrNotFound } func (cb *compBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { if gen, ok := myZettel[zid]; ok { if genMeta := gen.meta; genMeta != nil { if m := genMeta(zid); m != nil { updateMeta(m) cb.log.Trace().Msg("GetMeta") return m, nil } } } cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta/Err") return nil, box.ErrNotFound } func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta") for zid, gen := range myZettel { if !constraint(zid) { continue } if genMeta := gen.meta; genMeta != nil { if genMeta(zid) != nil { handle(zid) |
︙ | ︙ | |||
126 127 128 129 130 131 132 133 134 135 136 137 138 | cb.enricher.Enrich(ctx, m, cb.number) handle(m) } } } return nil } func (*compBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { _, ok := myZettel[zid] return !ok } | > > > > > > > | > < < | > < < | | 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 | cb.enricher.Enrich(ctx, m, cb.number) handle(m) } } } return nil } func (*compBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } func (cb *compBox) UpdateZettel(context.Context, domain.Zettel) error { cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel") return box.ErrReadOnly } func (*compBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { _, ok := myZettel[zid] return !ok } func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error { err := box.ErrNotFound if _, ok := myZettel[curZid]; ok { err = box.ErrReadOnly } cb.log.Trace().Err(err).Msg("RenameZettel") return err } func (*compBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) error { err := box.ErrNotFound if _, ok := myZettel[zid]; ok { err = box.ErrReadOnly } cb.log.Trace().Err(err).Msg("DeleteZettel") return err } func (cb *compBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(myZettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } func updateMeta(m *meta.Meta) { if _, ok := m.Get(api.KeySyntax); !ok { m.Set(api.KeySyntax, api.ValueSyntaxZmk) } m.Set(api.KeyRole, api.ValueRoleConfiguration) m.Set(api.KeyLang, api.ValueLangEN) m.Set(api.KeyReadOnly, api.ValueTrue) if _, ok := m.Get(api.KeyVisibility); !ok { m.Set(api.KeyVisibility, api.ValueVisibilityExpert) } } |
Changes to box/compbox/config.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | | 1 2 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-2022 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package compbox import ( "bytes" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myConfig == nil { return nil } m := meta.New(zid) |
︙ | ︙ |
Changes to box/compbox/keys.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | | | 1 2 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-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 compbox import ( "bytes" "fmt" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func genKeysM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Supported Metadata Keys") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } func genKeysC(*meta.Meta) []byte { keys := meta.GetSortedKeyDescriptions() var buf bytes.Buffer buf.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") for _, kd := range keys { fmt.Fprintf(&buf, "|%v|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) } return buf.Bytes() } |
Changes to box/compbox/log.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package compbox import ( "bytes" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func genLogM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Log") m.Set(api.KeySyntax, api.ValueSyntaxText) m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.ZidLayout)) return m } func genLogC(*meta.Meta) []byte { const tsFormat = "2006-01-02 15:04:05.999999" entries := kernel.Main.RetrieveLogEntries() var buf bytes.Buffer |
︙ | ︙ |
Changes to box/compbox/manager.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | | 1 2 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) 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 compbox import ( "bytes" "fmt" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func genManagerM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Box Manager") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) return m |
︙ | ︙ |
Deleted box/compbox/memory.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to box/compbox/parser.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed 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 compbox import ( "bytes" "fmt" "sort" "strings" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" ) func genParserM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Supported Parser") m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string)) m.Set(api.KeyVisibility, api.ValueVisibilityLogin) return m } func genParserC(*meta.Meta) []byte { var buf bytes.Buffer buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Image Format?:\n") syntaxes := parser.GetSyntaxes() sort.Strings(syntaxes) for _, syntax := range syntaxes { info := parser.Get(syntax) if info.Name != syntax { continue } altNames := info.AltNames sort.Strings(altNames) fmt.Fprintf( &buf, "|%v|%v|%v|%v\n", syntax, strings.Join(altNames, ", "), info.IsTextParser, info.IsImageFormat) } return buf.Bytes() } |
Changes to box/compbox/version.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | | 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 compbox import ( "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func getVersionMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, title) m.Set(api.KeyVisibility, api.ValueVisibilityExpert) return m |
︙ | ︙ |
Changes to box/constbox/base.css.
|
| < < < < < < < < < < < < < < | 1 2 3 4 5 6 7 | *,*::before,*::after { box-sizing: border-box; } html { font-size: 1rem; font-family: serif; scroll-behavior: smooth; |
︙ | ︙ | |||
93 94 95 96 97 98 99 | h1 { font-size:1.5rem; margin:.65rem 0 } h2 { font-size:1.25rem; margin:.70rem 0 } h3 { font-size:1.15rem; margin:.75rem 0 } h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold } h5 { font-size:1.05rem; margin:.8rem 0 } h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter } p { margin: .5rem 0 0 0 } | < | 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | h1 { font-size:1.5rem; margin:.65rem 0 } h2 { font-size:1.25rem; margin:.70rem 0 } h3 { font-size:1.15rem; margin:.75rem 0 } h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold } h5 { font-size:1.05rem; margin:.8rem 0 } h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter } p { margin: .5rem 0 0 0 } li,figure,figcaption,dl { margin: 0 } dt { margin: .5rem 0 0 0 } dt+dd { margin-top: 0 } dd { margin: .5rem 0 0 2rem } dd > p:first-child { margin: 0 0 0 0 } blockquote { border-left: 0.5rem solid lightgray; |
︙ | ︙ | |||
149 150 151 152 153 154 155 | display:block; border:none; border-bottom:1px solid #ccc; width:100%; } input.zs-primary { float:right } input.zs-secondary { float:left } | < < < < < | 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | display:block; border:none; border-bottom:1px solid #ccc; width:100%; } input.zs-primary { float:right } input.zs-secondary { float:left } a:not([class]) { text-decoration-skip-ink: auto } a.broken { text-decoration: line-through } img { max-width: 100% } img.right { float: right } ol.zs-endnotes { padding-top: .5rem; border-top: 1px solid; } kbd { font-family:monospace } |
︙ | ︙ |
Added box/constbox/base.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 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 | <!DOCTYPE html> <html{{#Lang}} lang="{{Lang}}"{{/Lang}}> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="generator" content="Zettelstore"> <meta name="format-detection" content="telephone=no"> {{{MetaHeader}}} <link rel="stylesheet" href="{{{CSSBaseURL}}}"> <link rel="stylesheet" href="{{{CSSUserURL}}}"> {{#CSSRoleURL}}<link rel="stylesheet" href="{{{CSSRoleURL}}}">{{/CSSRoleURL}} <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="{{{LogoutURL}}}">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> {{#CanRefresh}} <a href="{{{RefreshURL}}}">Refresh</a> {{/CanRefresh}} </nav> </div> {{#NewZettelLinks.Has}} <div class="zs-dropdown"> <button>New</button> <nav class="zs-dropdown-content"> {{#NewZettelLinks.Links}} <a href="{{{URL}}}">{{Text}}</a> {{/NewZettelLinks.Links}} </nav> </div> {{/NewZettelLinks.Has}} <form action="{{{SearchURL}}}"> <input type="text" placeholder="Search.." name="{{QueryKeyQuery}}"> </form> </nav> <main class="content"> {{{Content}}} </main> {{#FooterHTML}}<footer>{{{FooterHTML}}}</footer>{{/FooterHTML}} {{#DebugMode}}<div><b>WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!</b></div>{{/DebugMode}} </body> </html> |
Deleted box/constbox/base.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to box/constbox/constbox.go.
1 | //----------------------------------------------------------------------------- | | < < < | > > > < < < | > > > > > > > | | < | | | | > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package constbox puts zettel inside the executable. package constbox import ( "context" _ "embed" // Allow to embed file content "net/url" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" ) func init() { manager.Register( " const", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &constBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "const").Int("boxnum", int64(cdata.Number)).Child(), number: cdata.Number, zettel: constZettelMap, enricher: cdata.Enricher, }, nil }) } type constHeader map[string]string type constZettel struct { header constHeader content domain.Content } type constBox struct { log *logger.Logger number int zettel map[id.Zid]constZettel enricher box.Enricher } func (*constBox) Location() string { return "const:" } func (*constBox) CanCreateZettel(context.Context) bool { return false } func (cb *constBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) { cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel") return id.Invalid, box.ErrReadOnly } func (cb *constBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { if z, ok := cb.zettel[zid]; ok { cb.log.Trace().Msg("GetZettel") return domain.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil } cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel") return domain.Zettel{}, box.ErrNotFound } func (cb *constBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { if z, ok := cb.zettel[zid]; ok { cb.log.Trace().Msg("GetMeta") return meta.NewWithData(zid, z.header), nil } cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta") return nil, box.ErrNotFound } func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyZid") for zid := range cb.zettel { if constraint(zid) { handle(zid) |
︙ | ︙ | |||
92 93 94 95 96 97 98 99 100 101 102 103 104 | m := meta.NewWithData(zid, zettel.header) cb.enricher.Enrich(ctx, m, cb.number) handle(m) } } return nil } func (cb *constBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { _, ok := cb.zettel[zid] return !ok } | > > > > > > > | > < < | > < < > > | | | | | | | | | | | | < | | < | | | < | | > > > > > > > | > | | < | | < | | < | | | < | | < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < | | | > > > > > > > > > | | | < | | | | | < < | | < < < < < < < < < < < < < < < < < < < < < < | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | | | | | | > > > | | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < | 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 | m := meta.NewWithData(zid, zettel.header) cb.enricher.Enrich(ctx, m, cb.number) handle(m) } } return nil } func (*constBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } func (cb *constBox) UpdateZettel(context.Context, domain.Zettel) error { cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel") return box.ErrReadOnly } func (cb *constBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { _, ok := cb.zettel[zid] return !ok } func (cb *constBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error { err := box.ErrNotFound if _, ok := cb.zettel[curZid]; ok { err = box.ErrReadOnly } cb.log.Trace().Err(err).Msg("RenameZettel") return err } func (*constBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) error { err := box.ErrNotFound if _, ok := cb.zettel[zid]; ok { err = box.ErrReadOnly } cb.log.Trace().Err(err).Msg("DeleteZettel") return err } func (cb *constBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(cb.zettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } const syntaxTemplate = "mustache" var constZettelMap = map[id.Zid]constZettel{ id.ConfigurationZid: { constHeader{ api.KeyTitle: "Zettelstore Runtime Configuration", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxNone, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityOwner, }, domain.NewContent(nil)}, id.MustParse(api.ZidLicense): { constHeader{ api.KeyTitle: "Zettelstore License", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxText, api.KeyCreated: "20210504135842", api.KeyLang: api.ValueLangEN, api.KeyModified: "20220131153422", api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent(contentLicense)}, id.MustParse(api.ZidAuthors): { constHeader{ api.KeyTitle: "Zettelstore Contributors", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxZmk, api.KeyCreated: "20210504135842", api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityLogin, }, domain.NewContent(contentContributors)}, id.MustParse(api.ZidDependencies): { constHeader{ api.KeyTitle: "Zettelstore Dependencies", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityLogin, api.KeyCreated: "20210504135842", api.KeyModified: "20221013105100", }, domain.NewContent(contentDependencies)}, id.BaseTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Base HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, api.KeyCreated: "20210504135842", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentBaseMustache)}, id.LoginTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Login Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentLoginMustache)}, id.ZettelTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentZettelMustache)}, id.InfoTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Info HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentInfoMustache)}, id.ContextTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Context HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, api.KeyCreated: "20210218181140", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentContextMustache)}, id.FormTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentFormMustache)}, id.RenameTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Rename Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentRenameMustache)}, id.DeleteTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Delete HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentDeleteMustache)}, id.ListTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore List Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentListZettelMustache)}, id.ErrorTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Error HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: syntaxTemplate, api.KeyCreated: "20210305133215", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(contentErrorMustache)}, id.MustParse(api.ZidBaseCSS): { constHeader{ api.KeyTitle: "Zettelstore Base CSS", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: "css", api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent(contentBaseCSS)}, id.MustParse(api.ZidUserCSS): { constHeader{ api.KeyTitle: "Zettelstore User CSS", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: "css", api.KeyCreated: "20210622110143", api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent([]byte("/* User-defined CSS */"))}, id.RoleCSSMapZid: { constHeader{ api.KeyTitle: "Zettelstore Role to CSS Map", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxNone, api.KeyCreated: "20220321183214", api.KeyVisibility: api.ValueVisibilityExpert, }, domain.NewContent(nil)}, id.EmojiZid: { constHeader{ api.KeyTitle: "Zettelstore Generic Emoji", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxGif, api.KeyReadOnly: api.ValueTrue, api.KeyCreated: "20210504175807", api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent(contentEmoji)}, id.TOCNewTemplateZid: { constHeader{ api.KeyTitle: "New Menu", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyCreated: "20210217161829", api.KeyVisibility: api.ValueVisibilityCreator, }, domain.NewContent(contentNewTOCZettel)}, id.MustParse(api.ZidTemplateNewZettel): { constHeader{ api.KeyTitle: "New Zettel", api.KeyRole: api.ValueRoleZettel, api.KeySyntax: api.ValueSyntaxZmk, api.KeyCreated: "20201028185209", api.KeyVisibility: api.ValueVisibilityCreator, }, domain.NewContent(nil)}, id.MustParse(api.ZidTemplateNewUser): { constHeader{ api.KeyTitle: "New User", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: api.ValueSyntaxNone, api.KeyCreated: "20201028185209", meta.NewPrefix + api.KeyCredential: "", meta.NewPrefix + api.KeyUserID: "", meta.NewPrefix + api.KeyUserRole: api.ValueUserRoleReader, api.KeyVisibility: api.ValueVisibilityOwner, }, domain.NewContent(nil)}, id.DefaultHomeZid: { constHeader{ api.KeyTitle: "Home", api.KeyRole: api.ValueRoleZettel, api.KeySyntax: api.ValueSyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyCreated: "20210210190757", }, domain.NewContent(contentHomeZettel)}, } //go:embed license.txt var contentLicense []byte //go:embed contributors.zettel var contentContributors []byte //go:embed dependencies.zettel var contentDependencies []byte //go:embed base.mustache var contentBaseMustache []byte //go:embed login.mustache var contentLoginMustache []byte //go:embed zettel.mustache var contentZettelMustache []byte //go:embed info.mustache var contentInfoMustache []byte //go:embed context.mustache var contentContextMustache []byte //go:embed form.mustache var contentFormMustache []byte //go:embed rename.mustache var contentRenameMustache []byte //go:embed delete.mustache var contentDeleteMustache []byte //go:embed listzettel.mustache var contentListZettelMustache []byte //go:embed error.mustache var contentErrorMustache []byte //go:embed base.css var contentBaseCSS []byte //go:embed emoji_spin.gif var contentEmoji []byte //go:embed newtoc.zettel var contentNewTOCZettel []byte //go:embed home.zettel var contentHomeZettel []byte |
Added box/constbox/context.mustache.
> > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 | <header> <h1>{{Title}}</h1> <div class="zs-meta"> <a href="{{{InfoURL}}}">Info</a> · <a href="?dir=backward">Backward</a> · <a href="?dir=both">Both</a> · <a href="?dir=forward">Forward</a> · Depth:{{#Depths}} <a href="{{{URL}}}">{{{Text}}}</a>{{/Depths}} </div> </header> {{{Content}}} |
Added box/constbox/delete.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 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 | <article> <header> <h1>Delete Zettel {{Zid}}</h1> </header> <p>Do you really want to delete this zettel?</p> {{#HasShadows}} <div class="zs-info"> <h2>Infomation</h2> <p>If you delete this zettel, the previoulsy shadowed zettel from overlayed box {{ShadowedBox}} becomes available.</p> </div> {{/HasShadows}} {{#Incoming.Has}} <div class="zs-warning"> <h2>Warning!</h2> <p>If you delete this zettel, incoming references from the following zettel will become invalid.</p> <ul> {{#Incoming.Links}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/Incoming.Links}} </ul> </div> {{/Incoming.Has}} {{#HasUselessFiles}} <div class="zs-warning"> <h2>Warning!</h2> <p>Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.</p> <ul> {{#UselessFiles}} <li>{{{.}}}</li> {{/UselessFiles}} </ul> </div> {{/HasUselessFiles}} <dl> {{#MetaPairs}} <dt>{{Key}}:</dt><dd>{{Value}}</dd> {{/MetaPairs}} </dl> <form method="POST"> <input class="zs-primary" type="submit" value="Delete"> </form> </article> {{end}} |
Deleted box/constbox/delete.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to box/constbox/dependencies.zettel.
︙ | ︙ | |||
96 97 98 99 100 101 102 103 104 105 106 107 108 109 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` === yuin/goldmark ; URL & Source : [[https://github.com/yuin/goldmark]] ; License : MIT License ``` | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` === gopikchr ; URL & Source : [[https://github.com/gopikchr/gopikchr]] ; License : MIT License ; Remarks : Author is [[Zellyn Hunter|https://github.com/zellyn]], he wrote a blog post [[gopikchr: a yakshave|https://zellyn.com/2022/01/gopikchr-a-yakshave/]] about his work. : Gopikchr was incorporated into the source code of Zettelstore, moving it into package ''zettelstore.de/z/parser/pikchr''. Later, the source code was changed to adapt it to the needs of Zettelstore. For details, read README.txt in the appropriate source code folder. ``` MIT License Copyright (c) 2022 gopikchr Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` === hoisie/mustache / cbroglie/mustache ; URL & Source : [[https://github.com/hoisie/mustache]] / [[https://github.com/cbroglie/mustache]] ; License : MIT License ; Remarks : cbroglie/mustache is a fork from hoisie/mustache (starting with commit [f9b4cbf]). cbroglie/mustache does not claim a copyright and includes just the license file from hoisie/mustache. cbroglie/mustache obviously continues with the original license. ``` Copyright (c) 2009 Michael Hoisie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` === pascaldekloe/jwt ; URL & Source : [[https://github.com/pascaldekloe/jwt]] ; License : [[CC0 1.0 Universal|https://creativecommons.org/publicdomain/zero/1.0/legalcode]] ``` To the extent possible under law, Pascal S. de Kloe has waived all copyright and related or neighboring rights to JWT. This work is published from The Netherlands. https://creativecommons.org/publicdomain/zero/1.0/legalcode ``` === yuin/goldmark ; URL & Source : [[https://github.com/yuin/goldmark]] ; License : MIT License ``` |
︙ | ︙ | |||
125 126 127 128 129 130 131 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` | < < < < < < < < < < < < < < < | 204 205 206 207 208 209 210 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` |
Added box/constbox/error.mustache.
> > > > > > | 1 2 3 4 5 6 | <article> <header> <h1>{{ErrorTitle}}</h1> </header> {{ErrorText}} </article> |
Deleted box/constbox/error.sxn.
|
| < < < < < < < < < < < < < < < < < |
Added box/constbox/form.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 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 | <article> <header> <h1>{{Heading}}</h1> </header> <form method="POST"> <div> <label for="zs-title">Title <a title="Main heading of this zettel. You can use inline zettelmarkup.">ⓘ</a></label> <input class="zs-input" type="text" id="zs-title" name="title" placeholder="Title.." value="{{MetaTitle}}" autofocus> </div> <div> <div> <label for="zs-role">Role <a title="One word, without spaces, to set the main role of this zettel.">ⓘ</a></label> <input class="zs-input" type="text" id="zs-role" {{#HasRoleData}}list="zs-role-data"{{/HasRoleData}} name="role" placeholder="role.." value="{{MetaRole}}"> {{#HasRoleData}} <datalist id="zs-role-data"> {{#RoleData}} <option value="{{.}}"> {{/RoleData}} </datalist> {{/HasRoleData}} </div> <label for="zs-tags">Tags <a title="Tags must begin with an '#' sign. They are separated by spaces.">ⓘ</a></label> <input class="zs-input" type="text" id="zs-tags" name="tags" placeholder="#tag" value="{{MetaTags}}"> </div> <div> <label for="zs-meta">Metadata <a title="Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.">ⓘ</a></label> <textarea class="zs-input" id="zs-meta" name="meta" rows="4" placeholder="metakey: metavalue"> {{#MetaPairsRest}} {{Key}}: {{Value}} {{/MetaPairsRest}} </textarea> </div> <div> <label for="zs-syntax">Syntax <a title="Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).">ⓘ</a></label> <input class="zs-input" type="text" id="zs-syntax" {{#HasSyntaxData}}list="zs-syntax-data"{{/HasSyntaxData}} name="syntax" placeholder="syntax.." value="{{MetaSyntax}}"> {{#HasSyntaxData}} <datalist id="zs-syntax-data"> {{#SyntaxData}} <option value="{{.}}"> {{/SyntaxData}} </datalist> {{/HasSyntaxData}}</div> <div> {{#IsTextContent}} <label for="zs-content">Content <a title="Content for this zettel, according to above syntax.">ⓘ</a></label> <textarea class="zs-input zs-content" id="zs-content" name="content" rows="20" placeholder="Your content..">{{Content}}</textarea> {{/IsTextContent}} </div> <div> <input class="zs-primary" type="submit" value="Submit"> <input class="zs-secondary" type="submit" value="Save" formaction="?save"> </div> </form> </article> |
Deleted box/constbox/form.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added box/constbox/info.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 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 | <article> <header> <h1>Information for Zettel {{Zid}}</h1> <a href="{{{WebURL}}}">Web</a> · <a href="{{{ContextURL}}}">Context</a> {{#CanWrite}} · <a href="{{{EditURL}}}">Edit</a>{{/CanWrite}} {{#CanCopy}} · <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}} {{#CanVersion}} · <a href="{{{VersionURL}}}">Version</a>{{/CanVersion}} {{#CanFolge}} · <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}} {{#CanRename}}· <a href="{{{RenameURL}}}">Rename</a>{{/CanRename}} {{#CanDelete}}· <a href="{{{DeleteURL}}}">Delete</a>{{/CanDelete}} </header> <h2>Interpreted Metadata</h2> <table>{{#MetaData}}<tr><td>{{Key}}</td><td>{{{Value}}}</td></tr>{{/MetaData}}</table> <h2>References</h2> {{#HasLocLinks}} <h3>Local</h3> <ul> {{#LocLinks}} {{#Valid}}<li><a href="{{{Zid}}}">{{Zid}}</a></li>{{/Valid}} {{^Valid}}<li>{{Zid}}</li>{{/Valid}} {{/LocLinks}} </ul> {{/HasLocLinks}} {{#QueryLinks.Has}} <h3>Queries</h3> <ul> {{#QueryLinks.Links}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/QueryLinks.Links}} </ul> {{/QueryLinks.Has}} {{#HasExtLinks}} <h3>External</h3> <ul> {{#ExtLinks}} <li><a href="{{{.}}}"{{{ExtNewWindow}}}>{{.}}</a></li> {{/ExtLinks}} </ul> {{/HasExtLinks}} <h3>Unlinked</h3> {{{UnLinksContent}}} <form> <label for="phrase">Search Phrase</label> <input class="zs-input" type="text" id="phrase" name="{{QueryKeyPhrase}}" placeholder="Phrase.." value="{{UnLinksPhrase}}"> </form> <h2>Parts and encodings</h2> <table> {{#EvalMatrix}} <tr> <th>{{Header}}</th> {{#Elements}}<td><a href="{{{URL}}}">{{Text}}</td> {{/Elements}} </tr> {{/EvalMatrix}} </table> <h3>Parsed (not evaluated)</h3> <table> {{#ParseMatrix}} <tr> <th>{{Header}}</th> {{#Elements}}<td><a href="{{{URL}}}">{{Text}}</td> {{/Elements}} </tr> {{/ParseMatrix}} </table> {{#HasShadowLinks}} <h2>Shadowed Boxes</h2> <ul>{{#ShadowLinks}}<li>{{.}}</li>{{/ShadowLinks}}</ul> {{/HasShadowLinks}} {{#Endnotes}}{{{Endnotes}}}{{/Endnotes}} </article> |
Deleted box/constbox/info.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to box/constbox/license.txt.
|
| | | 1 2 3 4 5 6 7 8 | Copyright (c) 2020-2022 Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the EU. The English version is included here. Please see https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official |
︙ | ︙ |
Added box/constbox/listzettel.mustache.
> > > > > > > | 1 2 3 4 5 6 7 | <header> <h1>{{Title}}</h1> </header> <form action="{{{SearchURL}}}"> <input class="zs-input" type="text" placeholder="Search.." name="{{QueryKeyQuery}}" value="{{QueryValue}}"> </form> {{{Content}}} |
Deleted box/constbox/listzettel.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added box/constbox/login.mustache.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <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=""> <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> <div><input class="zs-primary" type="submit" value="Login"></div> </form> </article> |
Deleted box/constbox/login.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to box/constbox/newtoc.zettel.
1 2 3 | 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]] | < < | 1 2 3 4 | 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]] |
Deleted box/constbox/prelude.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added box/constbox/rename.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 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 | <article> <header> <h1>Rename Zettel {{Zid}}</h1> </header> <p>Do you really want to rename this zettel?</p> {{#Incoming.Has}} <div class="zs-warning"> <h2>Warning!</h2> <p>If you rename this zettel, incoming references from the following zettel will become invalid.</p> <ul> {{#Incoming.Links}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/Incoming.Links}} </ul> </div> {{/Incoming.Has}} {{#HasUselessFiles}} <div class="zs-warning"> <h2>Warning!</h2> <p>Renaming this zettel will also delete the following files, so that they will not be interpreted as content for a zettel with identifier {{Zid}}.</p> <ul> {{#UselessFiles}} <li>{{{.}}}</li> {{/UselessFiles}} </ul> </div> {{/HasUselessFiles}} <form method="POST"> <div> <label for="newid">New zettel id</label> <input class="zs-input" type="text" id="newzid" name="newzid" placeholder="ZID.." value="{{Zid}}" autofocus> </div> <input type="hidden" id="curzid" name="curzid" value="{{Zid}}"> <div><input class="zs-primary" type="submit" value="Rename"></div> </form> <dl> {{#MetaPairs}} <dt>{{Key}}:</dt><dd>{{Value}}</dd> {{/MetaPairs}} </dl> </article> |
Deleted box/constbox/rename.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/roleconfiguration.zettel.
|
| < < < < < < < < < < < < < < < < < < < < |
Deleted box/constbox/rolerole.zettel.
|
| < < < < < < < < < < |
Deleted box/constbox/roletag.zettel.
|
| < < < < < < |
Deleted box/constbox/rolezettel.zettel.
|
| < < < < < < < |
Deleted box/constbox/start.sxn.
|
| < < < < < < < < < < < < < < < < < |
Deleted box/constbox/wuicode.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added box/constbox/zettel.mustache.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 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 | <article> <header> <h1>{{{HTMLTitle}}}</h1> <div class="zs-meta"> {{#CanWrite}}<a href="{{{EditURL}}}">Edit</a> ·{{/CanWrite}} {{Zid}} · <a href="{{{InfoURL}}}">Info</a> · (<a href="{{{RoleURL}}}">{{RoleText}}</a>) {{#Tags.Has}}· {{#Tags.Links}} <a href="{{{URL}}}">{{Text}}</a>{{/Tags.Links}}{{/Tags.Has}} {{#CanCopy}}· <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}} {{#CanVersion}}· <a href="{{{VersionURL}}}">Version</a>{{/CanVersion}} {{#CanFolge}}· <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}} {{#PredecessorRefs}}<br>Predecessor: {{{PredecessorRefs}}}{{/PredecessorRefs}} {{#PrecursorRefs}}<br>Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}} {{#HasExtURL}}<br>URL: <a href="{{{ExtURL}}}"{{{ExtNewWindow}}}>{{ExtURL}}</a>{{/HasExtURL}} {{#Author}}<br>By {{Author}}{{/Author}} </div> </header> {{{Content}}} </article> {{#NeedBottomNav}}<nav>{{/NeedBottomNav}} {{#FolgeLinks.Has}} <details open> <summary>Folgezettel</summary> <ul> {{#FolgeLinks.Links}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/FolgeLinks.Links}} </ul> </details> {{/FolgeLinks.Has}} {{#BackLinks.Has}} <details open> <summary>Incoming</summary> <ul> {{#BackLinks.Links}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/BackLinks.Links}} </ul> </details> {{/BackLinks.Has}} {{#SuccessorLinks.Has}} <details open> <summary>Successors</summary> <ul> {{#SuccessorLinks.Links}} <li><a href="{{{URL}}}">{{Text}}</a></li> {{/SuccessorLinks.Links}} </ul> </details> {{/SuccessorLinks.Has}} {{#NeedBottomNav}}</nav>{{/NeedBottomNav}} |
Deleted box/constbox/zettel.sxn.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to box/dirbox/dirbox.go.
1 | //----------------------------------------------------------------------------- | | < < < > > > < < < | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package dirbox provides a directory-based zettel box. package dirbox import ( "context" "errors" "net/url" "os" "path/filepath" "sync" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/box/notify" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" ) func init() { manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { var log *logger.Logger if krnl := kernel.Main; krnl != nil { log = krnl.GetLogger(kernel.BoxService).Clone().Str("box", "dir").Int("boxnum", int64(cdata.Number)).Child() |
︙ | ︙ | |||
88 89 90 91 92 93 94 | _ notifyTypeSpec = iota dirNotifyAny dirNotifySimple dirNotifyFS ) func getDirSrvInfo(log *logger.Logger, notifyType string) notifyTypeSpec { | | | 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | _ notifyTypeSpec = iota dirNotifyAny dirNotifySimple dirNotifyFS ) func getDirSrvInfo(log *logger.Logger, notifyType string) notifyTypeSpec { for count := 0; count < 2; count++ { switch notifyType { case kernel.BoxDirTypeNotify: return dirNotifyFS case kernel.BoxDirTypeSimple: return dirNotifySimple default: notifyType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string) |
︙ | ︙ | |||
128 129 130 131 132 133 134 | mxCmds sync.RWMutex } func (dp *dirBox) Location() string { return dp.location } | < < < < < < < < < < < < < < < < < < | | < | 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 | mxCmds sync.RWMutex } func (dp *dirBox) Location() string { return dp.location } func (dp *dirBox) Start(context.Context) error { dp.mxCmds.Lock() defer dp.mxCmds.Unlock() dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) for i := uint32(0); i < dp.fSrvs; i++ { cc := make(chan fileCmd) go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc) dp.fCmds = append(dp.fCmds, cc) } var notifier notify.Notifier var err error switch dp.notifySpec { case dirNotifySimple: notifier, err = notify.NewSimpleDirNotifier(dp.log.Clone().Str("notify", "simple").Child(), dp.dir) default: notifier, err = notify.NewFSDirNotifier(dp.log.Clone().Str("notify", "fs").Child(), dp.dir) } if err != nil { dp.log.Fatal().Err(err).Msg("Unable to create directory supervisor") dp.stopFileServices() return err } dp.dirSrv = notify.NewDirService( dp.log.Clone().Str("sub", "dirsrv").Child(), notifier, dp.cdata.Notify, ) dp.dirSrv.Start() return nil } |
︙ | ︙ | |||
199 200 201 202 203 204 205 | func (dp *dirBox) stopFileServices() { for _, c := range dp.fCmds { close(c) } } | | | | | | | | | | | > > > > > | > > > > > > > > | 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 | func (dp *dirBox) stopFileServices() { for _, c := range dp.fCmds { close(c) } } func (dp *dirBox) notifyChanged(reason box.UpdateReason, zid id.Zid) { if chci := dp.cdata.Notify; chci != nil { dp.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChanged") chci <- box.UpdateInfo{Reason: reason, Zid: zid} } } func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd { // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function sum := 2166136261 ^ uint32(zid) sum *= 16777619 sum ^= uint32(zid >> 32) sum *= 16777619 dp.mxCmds.RLock() defer dp.mxCmds.RUnlock() return dp.fCmds[sum%dp.fSrvs] } func (dp *dirBox) CanCreateZettel(_ context.Context) bool { return !dp.readonly } func (dp *dirBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { if dp.readonly { return id.Invalid, box.ErrReadOnly } newZid, err := dp.dirSrv.SetNewDirEntry() if err != nil { return id.Invalid, err } meta := zettel.Meta meta.Zid = newZid entry := notify.DirEntry{Zid: newZid} dp.updateEntryFromMetaContent(&entry, meta, zettel.Content) err = dp.srvSetZettel(ctx, &entry, zettel) if err == nil { err = dp.dirSrv.UpdateDirEntry(&entry) } dp.notifyChanged(box.OnZettel, meta.Zid) dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel") return meta.Zid, err } func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return domain.Zettel{}, box.ErrNotFound } m, c, err := dp.srvGetMetaContent(ctx, entry, zid) if err != nil { return domain.Zettel{}, err } zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)} dp.log.Trace().Zid(zid).Msg("GetZettel") return zettel, nil } func (dp *dirBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := dp.doGetMeta(ctx, zid) dp.log.Trace().Zid(zid).Err(err).Msg("GetMeta") return m, err } func (dp *dirBox) doGetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return nil, box.ErrNotFound } m, err := dp.srvGetMeta(ctx, entry, zid) if err != nil { return nil, err } return m, nil } func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") for _, entry := range entries { handle(entry.Zid) |
︙ | ︙ | |||
289 290 291 292 293 294 295 | } dp.cdata.Enricher.Enrich(ctx, m, dp.number) handle(m) } return nil } | | | | | | | | | | | | | | | 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 | } dp.cdata.Enricher.Enrich(ctx, m, dp.number) handle(m) } return nil } func (dp *dirBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return !dp.readonly } func (dp *dirBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { if dp.readonly { return box.ErrReadOnly } meta := zettel.Meta zid := meta.Zid if !zid.IsValid() { return &box.ErrInvalidID{Zid: zid} } entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { // Existing zettel, but new in this box. entry = ¬ify.DirEntry{Zid: zid} } dp.updateEntryFromMetaContent(entry, meta, zettel.Content) dp.dirSrv.UpdateDirEntry(entry) err := dp.srvSetZettel(ctx, entry, zettel) if err == nil { dp.notifyChanged(box.OnZettel, zid) } dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel") return err } func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content domain.Content) { entry.SetupFromMetaContent(m, content, dp.cdata.Config.GetZettelFileSyntax) } func (dp *dirBox) AllowRenameZettel(context.Context, id.Zid) bool { return !dp.readonly } func (dp *dirBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { if curZid == newZid { return nil } curEntry := dp.dirSrv.GetDirEntry(curZid) if !curEntry.IsValid() { return box.ErrNotFound } if dp.readonly { return box.ErrReadOnly } // Check whether zettel with new ID already exists in this box. if _, err := dp.doGetMeta(ctx, newZid); err == nil { return &box.ErrInvalidID{Zid: newZid} } oldMeta, oldContent, err := dp.srvGetMetaContent(ctx, curEntry, curZid) if err != nil { return err } newEntry, err := dp.dirSrv.RenameDirEntry(curEntry, newZid) if err != nil { return err } oldMeta.Zid = newZid newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)} if err = dp.srvSetZettel(ctx, &newEntry, newZettel); err != nil { // "Rollback" rename. No error checking... dp.dirSrv.RenameDirEntry(&newEntry, curZid) return err } err = dp.srvDeleteZettel(ctx, curEntry, curZid) if err == nil { dp.notifyChanged(box.OnZettel, curZid) dp.notifyChanged(box.OnZettel, newZid) } dp.log.Trace().Zid(curZid).Zid(newZid).Err(err).Msg("RenameZettel") return err } func (dp *dirBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { if dp.readonly { return false } entry := dp.dirSrv.GetDirEntry(zid) return entry.IsValid() } func (dp *dirBox) DeleteZettel(ctx context.Context, zid id.Zid) error { if dp.readonly { return box.ErrReadOnly } entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return box.ErrNotFound } err := dp.dirSrv.DeleteDirEntry(zid) if err != nil { return nil } err = dp.srvDeleteZettel(ctx, entry, zid) if err == nil { dp.notifyChanged(box.OnZettel, zid) } dp.log.Trace().Zid(zid).Err(err).Msg("DeleteZettel") return err } func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = dp.readonly st.Zettel = dp.dirSrv.NumDirEntries() dp.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } |
Changes to box/dirbox/dirbox_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // 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 dirbox import "testing" func TestIsPrime(t *testing.T) { |
︙ | ︙ | |||
31 32 33 34 35 36 37 | if got != tc.exp { t.Errorf("isPrime(%d)=%v, but got %v", tc.n, tc.exp, got) } } } func TestMakePrime(t *testing.T) { | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | if got != tc.exp { t.Errorf("isPrime(%d)=%v, but got %v", tc.n, tc.exp, got) } } } func TestMakePrime(t *testing.T) { for i := uint32(0); i < 1500; i++ { np := makePrime(i) if np < i { t.Errorf("makePrime(%d) < %d", i, np) continue } if !isPrime(np) { t.Errorf("makePrime(%d) == %d is not prime", i, np) |
︙ | ︙ |
Changes to box/dirbox/service.go.
1 | //----------------------------------------------------------------------------- | | < < < < < | | | | | > | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package dirbox import ( "context" "io" "os" "path/filepath" "time" "zettelstore.de/z/box/filebox" "zettelstore.de/z/box/notify" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) func fileService(i uint32, log *logger.Logger, dirPath string, cmds <-chan fileCmd) { // Something may panic. Ensure a running service. defer func() { if r := recover(); r != nil { kernel.Main.LogRecover("FileService", r) go fileService(i, log, dirPath, cmds) } }() log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started") for cmd := range cmds { cmd.run(log, dirPath) } log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service stopped") } type fileCmd interface { run(*logger.Logger, string) } const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing. // COMMAND: srvGetMeta ---------------------------------------- // // Retrieves the meta data from a zettel. |
︙ | ︙ | |||
75 76 77 78 79 80 81 | rc chan<- resGetMeta } type resGetMeta struct { meta *meta.Meta err error } | | | > | | 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 | rc chan<- resGetMeta } type resGetMeta struct { meta *meta.Meta err error } func (cmd *fileGetMeta) run(log *logger.Logger, dirPath string) { var m *meta.Meta var err error entry := cmd.entry zid := entry.Zid if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt if contentName == "" || contentExt == "" { log.Panic().Zid(zid).Msg("No meta, no content in getMeta") } if entry.HasMetaInContent() { m, _, err = parseMetaContentFile(zid, filepath.Join(dirPath, contentName)) } else { m = filebox.CalcDefaultMeta(zid, contentExt) } } else { m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName)) } |
︙ | ︙ | |||
127 128 129 130 131 132 133 | } type resGetMetaContent struct { meta *meta.Meta content []byte err error } | | | > | | 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 | } type resGetMetaContent struct { meta *meta.Meta content []byte err error } func (cmd *fileGetMetaContent) run(log *logger.Logger, dirPath string) { var m *meta.Meta var content []byte var err error entry := cmd.entry zid := entry.Zid contentName := entry.ContentName contentExt := entry.ContentExt contentPath := filepath.Join(dirPath, contentName) if metaName := entry.MetaName; metaName == "" { if contentName == "" || contentExt == "" { log.Panic().Zid(zid).Msg("No meta, no content in getMetaContent") } if entry.HasMetaInContent() { m, content, err = parseMetaContentFile(zid, contentPath) } else { m = filebox.CalcDefaultMeta(zid, contentExt) content, err = os.ReadFile(contentPath) } } else { m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName)) |
︙ | ︙ | |||
166 167 168 169 170 171 172 | cmd.rc <- resGetMetaContent{m, content, err} } // COMMAND: srvSetZettel ---------------------------------------- // // Writes a new or exsting zettel. | | | | < | < > | | | | | | | < | | 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 | cmd.rc <- resGetMetaContent{m, content, err} } // COMMAND: srvSetZettel ---------------------------------------- // // Writes a new or exsting zettel. func (dp *dirBox) srvSetZettel(ctx context.Context, entry *notify.DirEntry, zettel domain.Zettel) error { rc := make(chan resSetZettel, 1) dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc} ctx, cancel := context.WithTimeout(ctx, serviceTimeout) defer cancel() select { case err := <-rc: return err case <-ctx.Done(): return ctx.Err() } } type fileSetZettel struct { entry *notify.DirEntry zettel domain.Zettel rc chan<- resSetZettel } type resSetZettel = error func (cmd *fileSetZettel) run(log *logger.Logger, dirPath string) { entry := cmd.entry zid := entry.Zid contentName := entry.ContentName m := cmd.zettel.Meta content := cmd.zettel.Content.AsBytes() metaName := entry.MetaName if metaName == "" { if contentName == "" { log.Panic().Zid(zid).Msg("No meta, no content in setZettel") } contentPath := filepath.Join(dirPath, contentName) if entry.HasMetaInContent() { err := writeZettelFile(contentPath, m, content) cmd.rc <- err return } err := writeFileContent(contentPath, content) cmd.rc <- err return } err := writeMetaFile(filepath.Join(dirPath, metaName), m) if err == nil && contentName != "" { err = writeFileContent(filepath.Join(dirPath, contentName), content) } cmd.rc <- err } func writeMetaFile(metaPath string, m *meta.Meta) error { |
︙ | ︙ | |||
237 238 239 240 241 242 243 | } func writeZettelFile(contentPath string, m *meta.Meta, content []byte) error { zettelFile, err := openFileWrite(contentPath) if err != nil { return err } | > | > | 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | } func writeZettelFile(contentPath string, m *meta.Meta, content []byte) error { zettelFile, err := openFileWrite(contentPath) if err != nil { return err } if err == nil { err = writeMetaHeader(zettelFile, m) } if err == nil { _, err = zettelFile.Write(content) } if err1 := zettelFile.Close(); err == nil { err = err1 } return err |
︙ | ︙ | |||
298 299 300 301 302 303 304 | type fileDeleteZettel struct { entry *notify.DirEntry rc chan<- resDeleteZettel } type resDeleteZettel = error | | | < < > | 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 | type fileDeleteZettel struct { entry *notify.DirEntry rc chan<- resDeleteZettel } type resDeleteZettel = error func (cmd *fileDeleteZettel) run(log *logger.Logger, dirPath string) { var err error entry := cmd.entry contentName := entry.ContentName contentPath := filepath.Join(dirPath, contentName) if metaName := entry.MetaName; metaName == "" { if contentName == "" { log.Panic().Zid(entry.Zid).Msg("No meta, no content in getMetaContent") } err = os.Remove(contentPath) } else { if contentName != "" { err = os.Remove(contentPath) } err1 := os.Remove(filepath.Join(dirPath, metaName)) if err == nil { err = err1 |
︙ | ︙ |
Changes to box/filebox/filebox.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package filebox provides boxes that are stored in a file. package filebox import ( "errors" "net/url" "path/filepath" "strings" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" ) func init() { manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { path := getFilepathFromURL(u) ext := strings.ToLower(filepath.Ext(path)) if ext != ".zip" { |
︙ | ︙ |
Changes to box/filebox/zipbox.go.
1 | //----------------------------------------------------------------------------- | | < < < < < | | | | | > < < < < < < < < < < < < < < < < < < | > > > | > | > | > > > > | > | | | | < | | | | | | > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package filebox import ( "archive/zip" "context" "io" "strings" "zettelstore.de/z/box" "zettelstore.de/z/box/notify" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/logger" "zettelstore.de/z/query" ) type zipBox struct { log *logger.Logger number int name string enricher box.Enricher notify chan<- box.UpdateInfo dirSrv *notify.DirService } func (zb *zipBox) Location() string { if strings.HasPrefix(zb.name, "/") { return "file://" + zb.name } return "file:" + zb.name } func (zb *zipBox) Start(context.Context) error { reader, err := zip.OpenReader(zb.name) if err != nil { return err } reader.Close() zipNotifier, err := notify.NewSimpleZipNotifier(zb.log, zb.name) if err != nil { return err } zb.dirSrv = notify.NewDirService(zb.log, zipNotifier, zb.notify) zb.dirSrv.Start() return nil } func (zb *zipBox) Refresh(_ context.Context) { zb.dirSrv.Refresh() zb.log.Trace().Msg("Refresh") } func (zb *zipBox) Stop(context.Context) { zb.dirSrv.Stop() } func (*zipBox) CanCreateZettel(context.Context) bool { return false } func (zb *zipBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) { err := box.ErrReadOnly zb.log.Trace().Err(err).Msg("CreateZettel") return id.Invalid, err } func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return domain.Zettel{}, box.ErrNotFound } reader, err := zip.OpenReader(zb.name) if err != nil { return domain.Zettel{}, err } defer reader.Close() var m *meta.Meta var src []byte var inMeta bool contentName := entry.ContentName if metaName := entry.MetaName; metaName == "" { if contentName == "" { zb.log.Panic().Zid(zid).Msg("No meta, no content in zipBox.GetZettel") } src, err = readZipFileContent(reader, entry.ContentName) if err != nil { return domain.Zettel{}, err } if entry.HasMetaInContent() { inp := input.NewInput(src) m = meta.NewFromInput(zid, inp) src = src[inp.Pos:] } else { m = CalcDefaultMeta(zid, entry.ContentExt) } } else { m, err = readZipMetaFile(reader, zid, metaName) if err != nil { return domain.Zettel{}, err } inMeta = true if contentName != "" { src, err = readZipFileContent(reader, entry.ContentName) if err != nil { return domain.Zettel{}, err } } } CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles) zb.log.Trace().Zid(zid).Msg("GetZettel") return domain.Zettel{Meta: m, Content: domain.NewContent(src)}, nil } func (zb *zipBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { return nil, box.ErrNotFound } reader, err := zip.OpenReader(zb.name) if err != nil { return nil, err } defer reader.Close() m, err := zb.readZipMeta(reader, zid, entry) zb.log.Trace().Err(err).Zid(zid).Msg("GetMeta") return m, err } func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := zb.dirSrv.GetDirEntries(constraint) zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") for _, entry := range entries { handle(entry.Zid) |
︙ | ︙ | |||
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | continue } zb.enricher.Enrich(ctx, m, zb.number) handle(m) } return nil } func (zb *zipBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { entry := zb.dirSrv.GetDirEntry(zid) return !entry.IsValid() } func (zb *zipBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error { err := box.ErrReadOnly if curZid == newZid { err = nil } curEntry := zb.dirSrv.GetDirEntry(curZid) if !curEntry.IsValid() { | > > > > > > > > | | | > | | 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 | continue } zb.enricher.Enrich(ctx, m, zb.number) handle(m) } return nil } func (*zipBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } func (zb *zipBox) UpdateZettel(context.Context, domain.Zettel) error { err := box.ErrReadOnly zb.log.Trace().Err(err).Msg("UpdateZettel") return err } func (zb *zipBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { entry := zb.dirSrv.GetDirEntry(zid) return !entry.IsValid() } func (zb *zipBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error { err := box.ErrReadOnly if curZid == newZid { err = nil } curEntry := zb.dirSrv.GetDirEntry(curZid) if !curEntry.IsValid() { err = box.ErrNotFound } zb.log.Trace().Err(err).Msg("RenameZettel") return err } func (*zipBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (zb *zipBox) DeleteZettel(_ context.Context, zid id.Zid) error { err := box.ErrReadOnly entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { err = box.ErrNotFound } zb.log.Trace().Err(err).Msg("DeleteZettel") return err } func (zb *zipBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = zb.dirSrv.NumDirEntries() zb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } func (zb *zipBox) readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *notify.DirEntry) (m *meta.Meta, err error) { var inMeta bool if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt if contentName == "" || contentExt == "" { zb.log.Panic().Zid(zid).Msg("No meta, no content in getMeta") } if entry.HasMetaInContent() { m, err = readZipMetaFile(reader, zid, contentName) } else { m = CalcDefaultMeta(zid, contentExt) } } else { m, err = readZipMetaFile(reader, zid, metaName) } |
︙ | ︙ |
Changes to box/helper.go.
1 | //----------------------------------------------------------------------------- | | | < < < < < | | < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 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) 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 box import ( "time" "zettelstore.de/z/domain/id" ) // GetNewZid calculates a new and unused zettel identifier, based on the current date and time. func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) { withSeconds := false for i := 0; i < 90; i++ { // Must be completed within 9 seconds (less than web/server.writeTimeout) zid := id.New(withSeconds) found, err := testZid(zid) if err != nil { return id.Invalid, err } if found { return zid, nil } // TODO: do not wait here unconditionally. time.Sleep(100 * time.Millisecond) withSeconds = true } return id.Invalid, ErrConflict } |
Changes to box/manager/anteroom.go.
1 | //----------------------------------------------------------------------------- | | < < < | > | | > | > | > | | | | | | > | | | > > | | | | > | > | > > | | | > | | > > > > > | > > > > | | | < < < | | | > | | > > | < | | < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package manager import ( "sync" "zettelstore.de/z/domain/id" ) type arAction int const ( arNothing arAction = iota arReload arZettel ) type anteroom struct { num uint64 next *anteroom waiting map[id.Zid]arAction curLoad int reload bool } type anterooms struct { mx sync.Mutex nextNum uint64 first *anteroom last *anteroom maxLoad int } func newAnterooms(maxLoad int) *anterooms { return &anterooms{maxLoad: maxLoad} } func (ar *anterooms) EnqueueZettel(zid id.Zid) { if !zid.IsValid() { return } ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { ar.first = ar.makeAnteroom(zid, arZettel) ar.last = ar.first return } for room := ar.first; room != nil; room = room.next { if room.reload { continue // Do not put zettel in reload room } if _, ok := room.waiting[zid]; ok { // Zettel is already waiting. return } } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { room.waiting[zid] = arZettel room.curLoad++ return } room := ar.makeAnteroom(zid, arZettel) 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 ar.nextNum++ return &anteroom{num: ar.nextNum, 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(newZids id.Set) uint64 { ar.mx.Lock() defer ar.mx.Unlock() newWaiting := createWaitingSet(newZids) ar.deleteReloadedRooms() if ns := len(newWaiting); ns > 0 { ar.nextNum++ ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newWaiting, curLoad: ns} if ar.first.next == nil { ar.last = ar.first } return ar.nextNum } ar.first = nil ar.last = nil return 0 } func createWaitingSet(zids id.Set) map[id.Zid]arAction { waitingSet := make(map[id.Zid]arAction, len(zids)) for zid := range zids { if zid.IsValid() { waitingSet[zid] = arZettel } } 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, uint64) { ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { return arNothing, id.Invalid, 0 } for zid, action := range ar.first.waiting { roomNo := ar.first.num 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, roomNo } return arNothing, id.Invalid, 0 } |
Changes to box/manager/anteroom_test.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package manager import ( "testing" "zettelstore.de/z/domain/id" ) func TestSimple(t *testing.T) { t.Parallel() ar := newAnterooms(2) ar.EnqueueZettel(id.Zid(1)) action, zid, rno := ar.Dequeue() if zid != id.Zid(1) || action != arZettel || rno != 1 { t.Errorf("Expected arZettel/1/1, but got %v/%v/%v", action, zid, rno) } _, zid, _ = ar.Dequeue() if zid != id.Invalid { t.Errorf("Expected invalid Zid, but got %v", zid) } ar.EnqueueZettel(id.Zid(1)) ar.EnqueueZettel(id.Zid(2)) |
︙ | ︙ | |||
51 52 53 54 55 56 57 | if count != 3 { t.Errorf("Expected 3 dequeues, but got %v", count) } } func TestReset(t *testing.T) { t.Parallel() | | | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | if count != 3 { t.Errorf("Expected 3 dequeues, but got %v", count) } } func TestReset(t *testing.T) { t.Parallel() ar := newAnterooms(1) ar.EnqueueZettel(id.Zid(1)) ar.Reset() action, zid, _ := ar.Dequeue() if action != arReload || zid != id.Invalid { t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) } ar.Reload(id.NewSet(3, 4)) |
︙ | ︙ | |||
84 85 86 87 88 89 90 | t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } | | | | 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 | t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.Reload(id.NewSet(id.Zid(6))) action, zid, _ = ar.Dequeue() if zid != id.Zid(6) || action != arZettel { t.Errorf("Expected 6/arZettel, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnterooms(1) ar.EnqueueZettel(id.Zid(8)) ar.Reload(nil) action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } } |
Changes to box/manager/box.go.
1 | //----------------------------------------------------------------------------- | | < < < > < | | | | | | | | | < < < < | < < | < < < < < < | < < | | | < < < > > > < | | | > > | < < | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | < < | < < < < < < < < < < < < < < | < < < < | < < < < < < < < < | > > | < < | < < < < | | | | | < < | | < < < < | < < < | > > | < > | | > | < < | > > | < < > > | < < < | | < > > | < < > > | < < < < | | < < < < < < < < < < < | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package manager import ( "bytes" "context" "errors" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" ) // Conatains all box.Box related functions // Location returns some information where the box is located. func (mgr *Manager) Location() string { if len(mgr.boxes) <= 2 { return "NONE" } var buf bytes.Buffer for i := 0; i < len(mgr.boxes)-2; i++ { if i > 0 { buf.WriteString(", ") } buf.WriteString(mgr.boxes[i].Location()) } return buf.String() } // CanCreateZettel returns true, if box could possibly create a new zettel. func (mgr *Manager) CanCreateZettel(ctx context.Context) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() return mgr.started && mgr.boxes[0].CanCreateZettel(ctx) } // CreateZettel creates a new zettel. func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { mgr.mgrLog.Debug().Msg("CreateZettel") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return id.Invalid, box.ErrStopped } return mgr.boxes[0].CreateZettel(ctx, zettel) } // GetZettel retrieves a specific zettel. func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetZettel") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return domain.Zettel{}, box.ErrStopped } for i, p := range mgr.boxes { if z, err := p.GetZettel(ctx, zid); err != box.ErrNotFound { if err == nil { mgr.Enrich(ctx, z.Meta, i+1) } return z, err } } return domain.Zettel{}, box.ErrNotFound } // GetAllZettel retrieves a specific zettel from all managed boxes. func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetAllZettel") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, box.ErrStopped } var result []domain.Zettel for i, p := range mgr.boxes { if z, err := p.GetZettel(ctx, zid); err == nil { mgr.Enrich(ctx, z.Meta, i+1) result = append(result, z) } } return result, nil } // GetMeta retrieves just the meta data of a specific zettel. func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, box.ErrStopped } return mgr.doGetMeta(ctx, zid) } func (mgr *Manager) doGetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { for i, p := range mgr.boxes { if m, err := p.GetMeta(ctx, zid); err != box.ErrNotFound { if err == nil { mgr.Enrich(ctx, m, i+1) } return m, err } } return nil, box.ErrNotFound } // GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. func (mgr *Manager) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetAllMeta") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, box.ErrStopped } var result []*meta.Meta for i, p := range mgr.boxes { if m, err := p.GetMeta(ctx, zid); err == nil { mgr.Enrich(ctx, m, i+1) result = append(result, m) } } return result, nil } // FetchZids returns the set of all zettel identifer managed by the box. func (mgr *Manager) FetchZids(ctx context.Context) (id.Set, error) { mgr.mgrLog.Debug().Msg("FetchZids") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, box.ErrStopped } result := id.Set{} for _, p := range mgr.boxes { err := p.ApplyZid(ctx, func(zid id.Zid) { result.Zid(zid) }, func(id.Zid) bool { return true }) if err != nil { return nil, err } } return result, nil } type metaMap map[id.Zid]*meta.Meta // SelectMeta returns all zettel meta data that match the selection // criteria. The result is ordered by descending zettel id. func (mgr *Manager) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { if msg := mgr.mgrLog.Debug(); msg.Enabled() { msg.Str("query", q.String()).Msg("SelectMeta") } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return nil, box.ErrStopped } compSearch := q.RetrieveAndCompile(mgr) selected := metaMap{} for _, term := range compSearch.Terms { rejected := id.Set{} handleMeta := func(m *meta.Meta) { zid := m.Zid if rejected.Contains(zid) { mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected") return } if _, ok := selected[zid]; ok { mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected") return } if compSearch.PreMatch(m) && term.Match(m) { selected[zid] = m mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match") } else { rejected.Zid(zid) mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject") } } for _, p := range mgr.boxes { if err := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err != nil { return nil, err } } } result := make([]*meta.Meta, 0, len(selected)) for _, m := range selected { result = append(result, m) } return q.Sort(result), nil } // CanUpdateZettel returns true, if box could possibly update the given zettel. func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() return mgr.started && mgr.boxes[0].CanUpdateZettel(ctx, zettel) } // UpdateZettel updates an existing zettel. func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return box.ErrStopped } // Remove all (computed) properties from metadata before storing the zettel. zettel.Meta = zettel.Meta.Clone() for _, p := range zettel.Meta.ComputedPairsRest() { if mgr.propertyKeys.Has(p.Key) { zettel.Meta.Delete(p.Key) } } return mgr.boxes[0].UpdateZettel(ctx, zettel) } // AllowRenameZettel returns true, if box will not disallow renaming the zettel. func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return false } for _, p := range mgr.boxes { if !p.AllowRenameZettel(ctx, zid) { return false } } return true } // RenameZettel changes the current zid to a new zid. func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { mgr.mgrLog.Debug().Zid(curZid).Zid(newZid).Msg("RenameZettel") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return box.ErrStopped } for i, p := range mgr.boxes { err := p.RenameZettel(ctx, curZid, newZid) if err != nil && !errors.Is(err, box.ErrNotFound) { for j := 0; j < i; j++ { mgr.boxes[j].RenameZettel(ctx, newZid, curZid) } return err } } return nil } // CanDeleteZettel returns true, if box could possibly delete the given zettel. func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return false } for _, p := range mgr.boxes { if p.CanDeleteZettel(ctx, zid) { return true } } return false } // DeleteZettel removes the zettel from the box. func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { mgr.mgrLog.Debug().Zid(zid).Msg("DeleteZettel") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if !mgr.started { return box.ErrStopped } for _, p := range mgr.boxes { err := p.DeleteZettel(ctx, zid) if err == nil { return nil } if !errors.Is(err, box.ErrNotFound) && !errors.Is(err, box.ErrReadOnly) { return err } } return box.ErrNotFound } |
Changes to box/manager/collect.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package manager import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain/id" "zettelstore.de/z/strfun" ) type collectData struct { refs id.Set words store.WordSet urls store.WordSet } |
︙ | ︙ | |||
75 76 77 78 79 80 81 | if ref.IsExternal() { data.urls.Add(strings.ToLower(ref.Value)) } if !ref.IsZettel() { return } if zid, err := id.Parse(ref.URL.Path); err == nil { | | | 72 73 74 75 76 77 78 79 80 81 | if ref.IsExternal() { data.urls.Add(strings.ToLower(ref.Value)) } if !ref.IsZettel() { return } if zid, err := id.Parse(ref.URL.Path); err == nil { data.refs.Zid(zid) } } |
Changes to box/manager/enrich.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | < | < | 1 2 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) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package manager import ( "context" "strconv" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Enrich computes additional properties and updates the given metadata. func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) { // Calculate computed, but stored values. if _, ok := m.Get(api.KeyCreated); !ok { m.Set(api.KeyCreated, computeCreated(m.Zid)) } if box.DoNotEnrich(ctx) { // Enrich is called indirectly via indexer or enrichment is not requested // because of other reasons -> ignore this call, do not update metadata return } computePublished(m) m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) mgr.idxStore.Enrich(ctx, m) } func computeCreated(zid id.Zid) string { if zid <= 10101000000 { // A year 0000 is not allowed and therefore an artificaial Zid. // In the year 0001, the month must be > 0. |
︙ | ︙ | |||
62 63 64 65 66 67 68 | zid /= 100 hours := zid % 100 if hours > 23 { hours = 23 } zid /= 100 day := zid % 100 | < < < < < < < < < > > | | > | | 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 | zid /= 100 hours := zid % 100 if hours > 23 { hours = 23 } zid /= 100 day := zid % 100 if day < 1 { day = 1 } zid /= 100 month := zid % 100 if month < 1 { month = 1 } if month > 12 { month = 12 } year := zid / 100 switch month { case 1, 3, 5, 7, 8, 10, 12: if day > 31 { day = 32 } case 4, 6, 9, 11: if day > 30 { day = 30 } case 2: if year%4 != 0 || (year%100 == 0 && year%400 != 0) { if day > 28 { day = 28 } } else { if day > 29 { day = 29 } } } created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds return created.String() } func computePublished(m *meta.Meta) { if _, ok := m.Get(api.KeyPublished); ok { return } if modified, ok := m.Get(api.KeyModified); ok { |
︙ | ︙ |
Changes to box/manager/indexer.go.
1 | //----------------------------------------------------------------------------- | | < < < > > > < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package manager import ( "context" "fmt" "net/url" "time" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" ) // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchEqual(word string) id.Set { found := mgr.idxStore.SearchEqual(word) mgr.idxLog.Debug().Str("word", word).Int("found", int64(len(found))).Msg("SearchEqual") |
︙ | ︙ | |||
74 75 76 77 78 79 80 | } // idxIndexer runs in the background and updates the index data structures. // This is the main service of the idxIndexer. func (mgr *Manager) idxIndexer() { // Something may panic. Ensure a running indexer. defer func() { | | | > | > | > > > > > | < | > | 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 | } // idxIndexer runs in the background and updates the index data structures. // This is the main service of the idxIndexer. func (mgr *Manager) idxIndexer() { // Something may panic. Ensure a running indexer. defer func() { if r := recover(); r != nil { kernel.Main.LogRecover("Indexer", r) go mgr.idxIndexer() } }() timerDuration := 15 * time.Second timer := time.NewTimer(timerDuration) ctx := box.NoEnrichContext(context.Background()) for { mgr.idxWorkService(ctx) if !mgr.idxSleepService(timer, timerDuration) { return } } } func (mgr *Manager) idxWorkService(ctx context.Context) { var roomNum uint64 var start time.Time for { switch action, zid, arRoomNum := mgr.idxAr.Dequeue(); action { case arNothing: return case arReload: mgr.idxLog.Debug().Msg("reload") roomNum = 0 zids, err := mgr.FetchZids(ctx) if err == nil { start = time.Now() if rno := mgr.idxAr.Reload(zids); rno > 0 { roomNum = rno } mgr.idxMx.Lock() mgr.idxLastReload = time.Now().Local() mgr.idxSinceReload = 0 mgr.idxMx.Unlock() } case arZettel: mgr.idxLog.Debug().Zid(zid).Msg("zettel") zettel, err := mgr.GetZettel(ctx, zid) if err != nil { // Zettel was deleted or is not accessible b/c of other reasons mgr.idxLog.Trace().Zid(zid).Msg("delete") mgr.idxMx.Lock() mgr.idxSinceReload++ mgr.idxMx.Unlock() mgr.idxDeleteZettel(zid) continue } mgr.idxLog.Trace().Zid(zid).Msg("update") mgr.idxMx.Lock() if arRoomNum == roomNum { mgr.idxDurReload = time.Since(start) } mgr.idxSinceReload++ mgr.idxMx.Unlock() mgr.idxUpdateZettel(ctx, zettel) } } } func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool { select { case _, ok := <-mgr.idxReady: |
︙ | ︙ | |||
149 150 151 152 153 154 155 | <-timer.C } return false } return true } | | | | 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | <-timer.C } return false } return true } func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) { var cData collectData cData.initialize() collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) m := zettel.Meta zi := store.NewZettelIndex(m.Zid) mgr.idxCollectFromMeta(ctx, m, zi, &cData) mgr.idxProcessData(ctx, zi, &cData) toCheck := mgr.idxStore.UpdateReferences(ctx, zi) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { |
︙ | ︙ | |||
183 184 185 186 187 188 189 | is := parser.ParseMetadata(pair.Value) collectInlineIndexData(&is, cData) case meta.TypeURL: if _, err := url.Parse(pair.Value); err == nil { cData.urls.Add(pair.Value) } default: | < | | < < < < < < < < < < < < < | | | < < < < < | | | 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 | is := parser.ParseMetadata(pair.Value) collectInlineIndexData(&is, cData) case meta.TypeURL: if _, err := url.Parse(pair.Value); err == nil { cData.urls.Add(pair.Value) } default: for _, word := range strfun.NormalizeWords(pair.Value) { cData.words.Add(word) } } } } func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) { for ref := range cData.refs { if _, err := mgr.GetMeta(ctx, ref); err == nil { zi.AddBackRef(ref) } else { zi.AddDeadRef(ref) } } zi.SetWords(cData.words) zi.SetUrls(cData.urls) } func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } if _, err = mgr.GetMeta(ctx, zid); err != nil { zi.AddDeadRef(zid) return } if inverseKey == "" { zi.AddBackRef(zid) return } zi.AddMetaRef(inverseKey, zid) } func (mgr *Manager) idxDeleteZettel(zid id.Zid) { toCheck := mgr.idxStore.DeleteZettel(context.Background(), zid) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCheckZettel(s id.Set) { for zid := range s { mgr.idxAr.EnqueueZettel(zid) } } |
Changes to box/manager/manager.go.
1 | //----------------------------------------------------------------------------- | | < < < > | > > < < | 1 2 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) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "io" "net/url" "sync" "time" "zettelstore.de/c/maps" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/memstore" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" ) // ConnectData contains all administration related values. type ConnectData struct { Number int // number of the box, starting with 1. Config config.Config Enricher box.Enricher |
︙ | ︙ | |||
77 78 79 80 81 82 83 84 85 86 | func Register(scheme string, create createFunc) { if _, ok := registry[scheme]; ok { panic(scheme) } registry[scheme] = create } // Manager is a coordinating box. type Manager struct { mgrLog *logger.Logger | > > > < < > | < < < < < < < < < < < < < | | | 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 | func Register(scheme string, create createFunc) { if _, ok := registry[scheme]; ok { panic(scheme) } registry[scheme] = create } // GetSchemes returns all registered scheme, ordered by scheme string. func GetSchemes() []string { return maps.Keys(registry) } // Manager is a coordinating box. type Manager struct { mgrLog *logger.Logger mgrMx sync.RWMutex started bool rtConfig config.Config boxes []box.ManagedBox observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} infos chan box.UpdateInfo propertyKeys strfun.Set // Set of property key names // Indexer data idxLog *logger.Logger idxStore store.Store idxAr *anterooms idxReady chan struct{} // Signal a non-empty anteroom to background task // Indexer stats data idxMx sync.RWMutex idxLastReload time.Time idxDurReload time.Duration idxSinceReload uint64 } // New creates a new managing box. func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) { descrs := meta.GetSortedKeyDescriptions() propertyKeys := make(strfun.Set, len(descrs)) for _, kd := range descrs { if kd.IsProperty() { propertyKeys.Set(kd.Name) } } boxLog := kernel.Main.GetLogger(kernel.BoxService) mgr := &Manager{ mgrLog: boxLog.Clone().Str("box", "manager").Child(), rtConfig: rtConfig, infos: make(chan box.UpdateInfo, len(boxURIs)*10), propertyKeys: propertyKeys, idxLog: boxLog.Clone().Str("box", "index").Child(), idxStore: memstore.New(), idxAr: newAnterooms(10), idxReady: make(chan struct{}, 1), } cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos} boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) for _, uri := range boxURIs { p, err := Connect(uri, authManager, &cdata) if err != nil { |
︙ | ︙ | |||
165 166 167 168 169 170 171 | } cdata.Number++ boxes = append(boxes, constbox, compbox) mgr.boxes = boxes return mgr, nil } | < < < < | | | 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 | } cdata.Number++ boxes = append(boxes, constbox, compbox) mgr.boxes = boxes return mgr, nil } // RegisterObserver registers an observer that will be notified // if a zettel was found to be changed. func (mgr *Manager) RegisterObserver(f box.UpdateFunc) { if f != nil { mgr.mxObserver.Lock() mgr.observers = append(mgr.observers, f) mgr.mxObserver.Unlock() } } func (mgr *Manager) notifier() { // The call to notify may panic. Ensure a running notifier. defer func() { if r := recover(); r != nil { kernel.Main.LogRecover("Notifier", r) go mgr.notifier() } }() tsLastEvent := time.Now() cache := destutterCache{} for { |
︙ | ︙ | |||
208 209 210 211 212 213 214 | reason, zid := ci.Reason, ci.Zid mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier") if ignoreUpdate(cache, now, reason, zid) { mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored") continue } | < < | < | 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | reason, zid := ci.Reason, ci.Zid mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier") if ignoreUpdate(cache, now, reason, zid) { mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored") continue } mgr.idxEnqueue(reason, zid) if ci.Box == nil { ci.Box = mgr } mgr.notifyObserver(&ci) } case <-mgr.done: return } } } |
︙ | ︙ | |||
244 245 246 247 248 249 250 | reason: reason, } return false } func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { switch reason { | < < < | 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | reason: reason, } return false } func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { switch reason { case box.OnReload: mgr.idxAr.Reset() case box.OnZettel: mgr.idxAr.EnqueueZettel(zid) default: return } select { case mgr.idxReady <- struct{}{}: default: } } |
︙ | ︙ | |||
273 274 275 276 277 278 279 | } } // Start the box. Now all other functions of the box are allowed. // Starting an already started box is not allowed. func (mgr *Manager) Start(ctx context.Context) error { mgr.mgrMx.Lock() | > | < < < | < < < < < < | | < < < < < < < < < < < < < < | < < < | < < < | < | > > | < < < < < < < < < < < < | 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 | } } // Start the box. Now all other functions of the box are allowed. // Starting an already started box is not allowed. func (mgr *Manager) Start(ctx context.Context) error { mgr.mgrMx.Lock() if mgr.started { mgr.mgrMx.Unlock() return box.ErrStarted } for i := len(mgr.boxes) - 1; i >= 0; i-- { ssi, ok := mgr.boxes[i].(box.StartStopper) if !ok { continue } err := ssi.Start(ctx) if err == nil { continue } for j := i + 1; j < len(mgr.boxes); j++ { if ssj, ok2 := mgr.boxes[j].(box.StartStopper); ok2 { ssj.Stop(ctx) } } mgr.mgrMx.Unlock() return err } mgr.idxAr.Reset() // Ensure an initial index run mgr.done = make(chan struct{}) go mgr.notifier() go mgr.idxIndexer() mgr.started = true mgr.mgrMx.Unlock() return nil } // Stop the started box. Now only the Start() function is allowed. func (mgr *Manager) Stop(ctx context.Context) { mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() if !mgr.started { return } close(mgr.done) for _, p := range mgr.boxes { if ss, ok := p.(box.StartStopper); ok { ss.Stop(ctx) } } mgr.started = false } // Refresh internal box data. func (mgr *Manager) Refresh(ctx context.Context) error { mgr.mgrLog.Debug().Msg("Refresh") mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() if !mgr.started { return box.ErrStopped } mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} for _, bx := range mgr.boxes { if rb, ok := bx.(box.Refresher); ok { rb.Refresh(ctx) } } return nil } // ReadStats populates st with box statistics. func (mgr *Manager) ReadStats(st *box.Stats) { mgr.mgrLog.Debug().Msg("ReadStats") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() subStats := make([]box.ManagedBoxStats, len(mgr.boxes)) for i, p := range mgr.boxes { |
︙ | ︙ |
Deleted box/manager/mapstore/mapstore.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/mapstore/refs.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted box/manager/mapstore/refs_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added box/manager/memstore/memstore.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package memstore stored the index in main memory. package memstore import ( "context" "fmt" "io" "sort" "strings" "sync" "zettelstore.de/c/api" "zettelstore.de/c/maps" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) 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 words []string urls []string } func (zi *zettelIndex) isEmpty() bool { if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 { return false } return len(zi.meta) == 0 } type stringRefs map[string]id.Slice type memStore struct { mx sync.RWMutex idx map[id.Zid]*zettelIndex dead map[id.Zid]id.Slice // map dead refs where they occur words stringRefs urls stringRefs // Stats updates uint64 } // New returns a new memory-based index store. func New() store.Store { return &memStore{ idx: make(map[id.Zid]*zettelIndex), dead: make(map[id.Zid]id.Slice), words: make(stringRefs), urls: make(stringRefs), } } func (ms *memStore) Enrich(_ context.Context, m *meta.Meta) { if ms.doEnrich(m) { ms.mx.Lock() ms.updates++ ms.mx.Unlock() } } func (ms *memStore) doEnrich(m *meta.Meta) bool { ms.mx.RLock() defer ms.mx.RUnlock() zi, ok := ms.idx[m.Zid] if !ok { return false } var updated bool if len(zi.dead) > 0 { m.Set(api.KeyDead, zi.dead.String()) updated = true } back := removeOtherMetaRefs(m, zi.backward.Copy()) if len(zi.backward) > 0 { m.Set(api.KeyBackward, zi.backward.String()) updated = true } if len(zi.forward) > 0 { m.Set(api.KeyForward, zi.forward.String()) back = remRefs(back, zi.forward) updated = true } 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(api.KeyBack, back.String()) updated = true } return updated } // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchEqual(word string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := id.NewSet() if refs, ok := ms.words[word]; ok { result.AddSlice(refs) } if refs, ok := ms.urls[word]; ok { result.AddSlice(refs) } zid, err := id.Parse(word) if err != nil { return result } zi, ok := ms.idx[zid] if !ok { return result } addBackwardZids(result, zid, zi) return result } // SearchPrefix returns all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchPrefix(prefix string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(prefix, strings.HasPrefix) l := len(prefix) if l > 14 { return result } maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) if err != nil { return result } var minZid id.Zid if l < 14 && prefix == "0000000000000"[:l] { minZid = id.Zid(1) } else { minZid, err = id.Parse(prefix + "00000000000000"[:14-l]) if err != nil { return result } } for zid, zi := range ms.idx { if minZid <= zid && zid <= maxZid { addBackwardZids(result, zid, zi) } } return result } // SearchSuffix returns all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchSuffix(suffix string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(suffix, strings.HasSuffix) l := len(suffix) if l > 14 { return result } val, err := id.ParseUint(suffix) if err != nil { return result } modulo := uint64(1) for i := 0; i < l; i++ { modulo *= 10 } for zid, zi := range ms.idx { if uint64(zid)%modulo == val { addBackwardZids(result, zid, zi) } } return result } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (ms *memStore) SearchContains(s string) id.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(s, strings.Contains) if len(s) > 14 { return result } if _, err := id.ParseUint(s); err != nil { return result } for zid, zi := range ms.idx { if strings.Contains(zid.String(), s) { addBackwardZids(result, zid, zi) } } return result } func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { // Must only be called if ms.mx is read-locked! result := id.NewSet() for word, refs := range ms.words { if !pred(word, s) { continue } result.AddSlice(refs) } for u, refs := range ms.urls { if !pred(u, s) { continue } result.AddSlice(refs) } return result } func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) { // Must only be called if ms.mx is read-locked! result.Zid(zid) result.AddSlice(zi.backward) for _, mref := range zi.meta { result.AddSlice(mref.backward) } } func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { for _, p := range m.PairsRest() { 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(_ context.Context, zidx *store.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) zi.words = updateWordSet(zidx.Zid, ms.words, zi.words, zidx.GetWords()) zi.urls = updateWordSet(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) // 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 *store.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 *store.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 *store.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 updateWordSet(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { // Must only be called if ms.mx is write-locked! newWords, removeWords := next.Diff(prev) for _, word := range newWords { if refs, ok := srefs[word]; ok { srefs[word] = addRef(refs, zid) continue } srefs[word] = id.Slice{zid} } for _, word := range removeWords { refs, ok := srefs[word] if !ok { continue } refs2 := remRef(refs, zid) if len(refs2) == 0 { delete(srefs, word) continue } srefs[word] = refs2 } return next.Words() } 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(_ 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) } } ms.deleteWords(zid, zi.words) 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.Zid(ref) } } 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) deleteWords(zid id.Zid, words []string) { // Must only be called if ms.mx is write-locked! for _, word := range words { refs, ok := ms.words[word] if !ok { continue } refs2 := remRef(refs, zid) if len(refs2) == 0 { delete(ms.words, word) continue } ms.words[word] = refs2 } } func (ms *memStore) ReadStats(st *store.Stats) { ms.mx.RLock() st.Zettel = len(ms.idx) st.Updates = ms.updates st.Words = uint64(len(ms.words)) st.Urls = uint64(len(ms.urls)) ms.mx.RUnlock() } func (ms *memStore) Dump(w io.Writer) { ms.mx.RLock() defer ms.mx.RUnlock() io.WriteString(w, "=== Dump\n") ms.dumpIndex(w) ms.dumpDead(w) dumpStringRefs(w, "Words", "", "", ms.words) dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) } func (ms *memStore) dumpIndex(w io.Writer) { if len(ms.idx) == 0 { return } io.WriteString(w, "==== Zettel Index\n") 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] if len(zi.dead) > 0 { fmt.Fprintln(w, "* Dead:", zi.dead) } dumpZids(w, "* Forward:", zi.forward) dumpZids(w, "* Backward:", zi.backward) for k, fb := range zi.meta { fmt.Fprintln(w, "* Meta", k) dumpZids(w, "** Forward:", fb.forward) dumpZids(w, "** Backward:", fb.backward) } dumpStrings(w, "* Words", "", "", zi.words) dumpStrings(w, "* URLs", "[[", "]]", zi.urls) } } func (ms *memStore) dumpDead(w io.Writer) { if len(ms.dead) == 0 { return } fmt.Fprintf(w, "==== Dead References\n") 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) fmt.Fprintln(w, ":", ms.dead[id]) } } func dumpZids(w io.Writer, prefix string, zids id.Slice) { if len(zids) > 0 { io.WriteString(w, prefix) for _, zid := range zids { io.WriteString(w, " ") w.Write(zid.Bytes()) } fmt.Fprintln(w) } } func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { if len(slice) > 0 { sl := make([]string, len(slice)) copy(sl, slice) sort.Strings(sl) fmt.Fprintln(w, title) for _, s := range sl { fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) } } } func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { if len(srefs) == 0 { return } fmt.Fprintln(w, "====", title) for _, s := range maps.Keys(srefs) { fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) fmt.Fprintln(w, ":", srefs[s]) } } |
Added box/manager/memstore/refs.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | //----------------------------------------------------------------------------- // Copyright (c) 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 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 } if rn < ro { newRefs = append(newRefs, rn) npos++ continue } remRefs = append(remRefs, ro) opos++ } if npos < len(refsN) { newRefs = append(newRefs, refsN[npos:]...) } if opos < len(refsO) { remRefs = append(remRefs, refsO[opos:]...) } return newRefs, remRefs } func addRef(refs id.Slice, ref id.Zid) id.Slice { hi := len(refs) for lo := 0; lo < hi; { m := lo + (hi-lo)/2 if r := refs[m]; r == ref { return refs } else if r < ref { lo = m + 1 } else { hi = m } } refs = append(refs, id.Invalid) copy(refs[hi+1:], refs[hi:]) refs[hi] = ref return refs } func remRefs(refs, 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 } if dr < rr { dpos++ continue } rpos++ dpos++ } if rpos < len(refs) { result = append(result, refs[rpos:]...) } return result } func remRef(refs id.Slice, ref id.Zid) id.Slice { hi := len(refs) for lo := 0; lo < hi; { m := lo + (hi-lo)/2 if r := refs[m]; r == ref { copy(refs[m:], refs[m+1:]) refs = refs[:len(refs)-1] return refs } else if r < ref { lo = m + 1 } else { hi = m } } return refs } |
Added box/manager/memstore/refs_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | //----------------------------------------------------------------------------- // 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 memstore 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) { t.Parallel() 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) { t.Parallel() 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) { t.Parallel() 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) { t.Parallel() 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 box/manager/store/store.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | 1 2 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) 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 store contains general index data for storing a zettel index. package store import ( "context" "io" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" ) // Stats records statistics about the store. type Stats struct { // Zettel is the number of zettel managed by the indexer. Zettel int |
︙ | ︙ | |||
39 40 41 42 43 44 45 | } // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { query.Searcher | < < < < < < < | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | } // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { query.Searcher // Entrich metadata with data from store. Enrich(ctx context.Context, m *meta.Meta) // UpdateReferences for a specific zettel. // Returns set of zettel identifier that must also be checked for changes. UpdateReferences(context.Context, *ZettelIndex) 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 *Stats) // Dump the content to a Writer. Dump(io.Writer) } |
Changes to box/manager/store/wordset.go.
1 | //----------------------------------------------------------------------------- | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // 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 store // WordSet contains the set of all words, with the count of their occurrences. type WordSet map[string]int |
︙ | ︙ |
Changes to box/manager/store/wordset_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // 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 store_test import ( "sort" "testing" |
︙ | ︙ |
Changes to box/manager/store/zettel.go.
1 | //----------------------------------------------------------------------------- | | < < < < | < < | < | | | | | | | < | | | | | | | | | | | > | < < | > | > | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package store 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 words WordSet urls WordSet } // 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(zid) } // 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(zid) 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(zid) } // SetWords sets the words to the given value. func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words } // SetUrls sets the words to the given value. func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls } // GetDeadRefs returns all dead references as a sorted list. func (zi *ZettelIndex) GetDeadRefs() id.Slice { return zi.deadrefs.Sorted() } // GetBackRefs returns all back references as a sorted list. func (zi *ZettelIndex) GetBackRefs() id.Slice { return zi.backrefs.Sorted() } // 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.Sorted() } return result } // GetWords returns a reference to the set of words. It must not be modified. func (zi *ZettelIndex) GetWords() WordSet { return zi.words } |
︙ | ︙ |
Changes to box/membox/membox.go.
1 | //----------------------------------------------------------------------------- | | < < < > > > < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package membox stores zettel volatile in main memory. package membox import ( "context" "net/url" "sync" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" ) func init() { manager.Register( "mem", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &memBox{ |
︙ | ︙ | |||
46 47 48 49 50 51 52 | type memBox struct { log *logger.Logger u *url.URL cdata manager.ConnectData maxZettel int maxBytes int mx sync.RWMutex // Protects the following fields | | | | < < < < < < < < < | | | | | | | | | | > | > > > | 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 | type memBox struct { log *logger.Logger u *url.URL cdata manager.ConnectData maxZettel int maxBytes int mx sync.RWMutex // Protects the following fields zettel map[id.Zid]domain.Zettel curBytes int } func (mb *memBox) notifyChanged(reason box.UpdateReason, zid id.Zid) { if chci := mb.cdata.Notify; chci != nil { chci <- box.UpdateInfo{Reason: reason, Zid: zid} } } func (mb *memBox) Location() string { return mb.u.String() } func (mb *memBox) Start(context.Context) error { mb.mx.Lock() mb.zettel = make(map[id.Zid]domain.Zettel) mb.curBytes = 0 mb.mx.Unlock() mb.log.Trace().Int("max-zettel", int64(mb.maxZettel)).Int("max-bytes", int64(mb.maxBytes)).Msg("Start Box") return nil } func (mb *memBox) Stop(context.Context) { mb.mx.Lock() mb.zettel = nil mb.mx.Unlock() } func (mb *memBox) CanCreateZettel(context.Context) bool { mb.mx.RLock() defer mb.mx.RUnlock() return len(mb.zettel) < mb.maxZettel } func (mb *memBox) CreateZettel(_ context.Context, zettel domain.Zettel) (id.Zid, error) { mb.mx.Lock() newBytes := mb.curBytes + zettel.Length() if mb.maxZettel < len(mb.zettel) || mb.maxBytes < newBytes { mb.mx.Unlock() return id.Invalid, box.ErrCapacity } zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { _, ok := mb.zettel[zid] return !ok, nil }) if err != nil { mb.mx.Unlock() return id.Invalid, err } meta := zettel.Meta.Clone() meta.Zid = zid zettel.Meta = meta mb.zettel[zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() mb.notifyChanged(box.OnZettel, zid) mb.log.Trace().Zid(zid).Msg("CreateZettel") return zid, nil } func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { mb.mx.RLock() zettel, ok := mb.zettel[zid] mb.mx.RUnlock() if !ok { return domain.Zettel{}, box.ErrNotFound } zettel.Meta = zettel.Meta.Clone() mb.log.Trace().Msg("GetZettel") return zettel, nil } func (mb *memBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { mb.mx.RLock() zettel, ok := mb.zettel[zid] mb.mx.RUnlock() if !ok { return nil, box.ErrNotFound } mb.log.Trace().Msg("GetMeta") return zettel.Meta.Clone(), nil } func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyZid") for zid := range mb.zettel { |
︙ | ︙ | |||
161 162 163 164 165 166 167 | mb.cdata.Enricher.Enrich(ctx, m, mb.cdata.Number) handle(m) } } return nil } | | | | | | | | | | | | 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 | mb.cdata.Enricher.Enrich(ctx, m, mb.cdata.Number) handle(m) } } return nil } func (mb *memBox) CanUpdateZettel(_ context.Context, zettel domain.Zettel) bool { mb.mx.RLock() defer mb.mx.RUnlock() zid := zettel.Meta.Zid if !zid.IsValid() { return false } newBytes := mb.curBytes + zettel.Length() if prevZettel, found := mb.zettel[zid]; found { newBytes -= prevZettel.Length() } return newBytes < mb.maxBytes } func (mb *memBox) UpdateZettel(_ context.Context, zettel domain.Zettel) error { m := zettel.Meta.Clone() if !m.Zid.IsValid() { return &box.ErrInvalidID{Zid: m.Zid} } mb.mx.Lock() newBytes := mb.curBytes + zettel.Length() if prevZettel, found := mb.zettel[m.Zid]; found { newBytes -= prevZettel.Length() } if mb.maxBytes < newBytes { mb.mx.Unlock() return box.ErrCapacity } zettel.Meta = m mb.zettel[m.Zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() mb.notifyChanged(box.OnZettel, m.Zid) mb.log.Trace().Msg("UpdateZettel") return nil } func (*memBox) AllowRenameZettel(context.Context, id.Zid) bool { return true } func (mb *memBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error { mb.mx.Lock() zettel, ok := mb.zettel[curZid] if !ok { mb.mx.Unlock() return box.ErrNotFound } // Check that there is no zettel with newZid if _, ok = mb.zettel[newZid]; ok { mb.mx.Unlock() return &box.ErrInvalidID{Zid: newZid} } meta := zettel.Meta.Clone() meta.Zid = newZid zettel.Meta = meta mb.zettel[newZid] = zettel delete(mb.zettel, curZid) mb.mx.Unlock() mb.notifyChanged(box.OnZettel, curZid) mb.notifyChanged(box.OnZettel, newZid) mb.log.Trace().Msg("RenameZettel") return nil } func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { mb.mx.RLock() _, ok := mb.zettel[zid] mb.mx.RUnlock() return ok } func (mb *memBox) DeleteZettel(_ context.Context, zid id.Zid) error { mb.mx.Lock() oldZettel, found := mb.zettel[zid] if !found { mb.mx.Unlock() return box.ErrNotFound } delete(mb.zettel, zid) mb.curBytes -= oldZettel.Length() mb.mx.Unlock() mb.notifyChanged(box.OnZettel, zid) mb.log.Trace().Msg("DeleteZettel") return nil } func (mb *memBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = false mb.mx.RLock() st.Zettel = len(mb.zettel) mb.mx.RUnlock() mb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } |
Changes to box/notify/directory.go.
1 | //----------------------------------------------------------------------------- | | < < < | < | | | | | | | < | | < | < < < < < < < < | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package notify import ( "errors" "fmt" "path/filepath" "regexp" "strings" "sync" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/logger" "zettelstore.de/z/parser" "zettelstore.de/z/query" "zettelstore.de/z/strfun" ) type entrySet map[id.Zid]*DirEntry // directoryState signal the internal state of the service. // // The following state transitions are possible: // --newDirService--> dsCreated // dsCreated --Start--> dsStarting // dsStarting --last list notification--> dsWorking // dsWorking --directory missing--> dsMissing // dsMissing --last list notification--> dsWorking // --Stop--> dsStopping type directoryState uint8 const ( dsCreated directoryState = iota dsStarting // Reading inital scan dsWorking // Initial scan complete, fully operational dsMissing // Directory is missing dsStopping // Service is shut down ) // DirService specifies a directory service for file based zettel. type DirService struct { log *logger.Logger dirPath string notifier Notifier infos chan<- box.UpdateInfo mx sync.RWMutex // protects status, entries state directoryState entries entrySet } // ErrNoDirectory signals missing directory data. var ErrNoDirectory = errors.New("unable to retrieve zettel directory information") // NewDirService creates a new directory service. func NewDirService(log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService { return &DirService{ log: log, notifier: notifier, infos: chci, state: dsCreated, } } // Start the directory service. func (ds *DirService) Start() { ds.mx.Lock() ds.state = dsStarting ds.mx.Unlock() go ds.updateEvents() } // Refresh the directory entries. func (ds *DirService) Refresh() { ds.notifier.Refresh() } // Stop the directory service. func (ds *DirService) Stop() { ds.mx.Lock() ds.state = dsStopping ds.mx.Unlock() ds.notifier.Close() } func (ds *DirService) logMissingEntry(action string) error { err := ErrNoDirectory ds.log.Info().Err(err).Str("action", action).Msg("Unable to get directory information") |
︙ | ︙ | |||
190 191 192 193 194 195 196 | func (ds *DirService) RenameDirEntry(oldEntry *DirEntry, newZid id.Zid) (DirEntry, error) { ds.mx.Lock() defer ds.mx.Unlock() if ds.entries == nil { return DirEntry{}, ds.logMissingEntry("rename") } if _, found := ds.entries[newZid]; found { | | | 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | func (ds *DirService) RenameDirEntry(oldEntry *DirEntry, newZid id.Zid) (DirEntry, error) { ds.mx.Lock() defer ds.mx.Unlock() if ds.entries == nil { return DirEntry{}, ds.logMissingEntry("rename") } if _, found := ds.entries[newZid]; found { return DirEntry{}, &box.ErrInvalidID{Zid: newZid} } oldZid := oldEntry.Zid newEntry := DirEntry{ Zid: newZid, MetaName: renameFilename(oldEntry.MetaName, oldZid, newZid), ContentName: renameFilename(oldEntry.ContentName, oldZid, newZid), ContentExt: oldEntry.ContentExt, |
︙ | ︙ | |||
223 224 225 226 227 228 229 | if ds.entries == nil { return ds.logMissingEntry("delete") } delete(ds.entries, zid) return nil } | | | | > | < | | | > | < < | < | < < < < < < < < < < < < < | | | | | | | | | | | | | | | | | > | | | | < < | | | | > | | < | | | | | | | | | | | | | | | | | < > | | | | | 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 | if ds.entries == nil { return ds.logMissingEntry("delete") } delete(ds.entries, zid) return nil } func (ds *DirService) updateEvents() { var newEntries entrySet for ev := range ds.notifier.Events() { ds.mx.RLock() state := ds.state ds.mx.RUnlock() if msg := ds.log.Trace(); msg.Enabled() { msg.Uint("state", uint64(state)).Str("op", ev.Op.String()).Str("name", ev.Name).Msg("notifyEvent") } if state == dsStopping { break } switch ev.Op { case Error: newEntries = nil if state != dsMissing { ds.log.Warn().Err(ev.Err).Msg("Notifier confused") } case Make: newEntries = make(entrySet) case List: if ev.Name == "" { zids := getNewZids(newEntries) ds.mx.Lock() fromMissing := ds.state == dsMissing prevEntries := ds.entries ds.entries = newEntries ds.state = dsWorking ds.mx.Unlock() newEntries = nil ds.onCreateDirectory(zids, prevEntries) if fromMissing { ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found") } } else if newEntries != nil { ds.onUpdateFileEvent(newEntries, ev.Name) } case Destroy: newEntries = nil ds.onDestroyDirectory() ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing") case Update: ds.mx.Lock() zid := ds.onUpdateFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { ds.notifyChange(box.OnZettel, zid) } case Delete: ds.mx.Lock() zid := ds.onDeleteFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { ds.notifyChange(box.OnZettel, zid) } default: ds.log.Warn().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") } } } func getNewZids(entries entrySet) id.Slice { zids := make(id.Slice, 0, len(entries)) for zid := range entries { zids = append(zids, zid) } return zids } func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) { for _, zid := range zids { ds.notifyChange(box.OnZettel, zid) delete(prevEntries, zid) } // These were previously stored, by are not found now. // Notify system that these were deleted, e.g. for updating the index. for zid := range prevEntries { ds.notifyChange(box.OnZettel, zid) } } func (ds *DirService) onDestroyDirectory() { ds.mx.Lock() entries := ds.entries ds.entries = nil ds.state = dsMissing ds.mx.Unlock() for zid := range entries { ds.notifyChange(box.OnZettel, zid) } } var validFileName = regexp.MustCompile(`^(\d{14})`) func matchValidFileName(name string) []string { return validFileName.FindStringSubmatch(name) |
︙ | ︙ | |||
372 373 374 375 376 377 378 | zid := seekZid(name) if zid == id.Invalid { return id.Invalid } entry := fetchdirEntry(entries, zid) dupName1, dupName2 := ds.updateEntry(entry, name) if dupName1 != "" { | | | | 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 | zid := seekZid(name) if zid == id.Invalid { return id.Invalid } entry := fetchdirEntry(entries, zid) dupName1, dupName2 := ds.updateEntry(entry, name) if dupName1 != "" { ds.log.Warn().Str("name", dupName1).Msg("Duplicate content (is ignored)") if dupName2 != "" { ds.log.Warn().Str("name", dupName2).Msg("Duplicate content (is ignored)") } return id.Invalid } return zid } func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) id.Zid { |
︙ | ︙ | |||
579 580 581 582 583 584 585 | return false } if newExt == "zmk" { return true } oldInfo := parser.Get(oldExt) newInfo := parser.Get(newExt) | | < < < | | | | | 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 | return false } if newExt == "zmk" { return true } oldInfo := parser.Get(oldExt) newInfo := parser.Get(newExt) if oldTextParser := oldInfo.IsTextParser; oldTextParser != newInfo.IsTextParser { return !oldTextParser } if oldImageFormat := oldInfo.IsImageFormat; oldImageFormat != newInfo.IsImageFormat { return oldImageFormat } if oldPrimary := primarySyntax.Has(oldExt); oldPrimary != primarySyntax.Has(newExt) { return !oldPrimary } } oldLen := len(oldExt) newLen := len(newExt) if oldLen != newLen { return newLen < oldLen } return newExt < oldExt } func (ds *DirService) notifyChange(reason box.UpdateReason, zid id.Zid) { if chci := ds.infos; chci != nil { ds.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChange") chci <- box.UpdateInfo{Reason: reason, Zid: zid} } } |
Changes to box/notify/directory_test.go.
1 | //----------------------------------------------------------------------------- | | < < < > > < > < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package notify import ( "testing" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/pikchr" // Allow to use pikchr parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) func TestSeekZid(t *testing.T) { testcases := []struct { name string zid id.Zid }{ |
︙ | ︙ | |||
48 49 50 51 52 53 54 | } } } func TestNewExtIsBetter(t *testing.T) { extVals := []string{ // Main Formats | | < | | < < < < | | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | } } } func TestNewExtIsBetter(t *testing.T) { extVals := []string{ // Main Formats api.ValueSyntaxZmk, "pikchr", "markdown", "md", // Other supported text formats "css", "txt", api.ValueSyntaxHTML, api.ValueSyntaxNone, "mustache", api.ValueSyntaxText, "plain", // Supported graphics formats api.ValueSyntaxGif, "png", api.ValueSyntaxSVG, "jpeg", "jpg", // Unsupported syntax values "gz", "cpp", "tar", "cppc", } for oldI, oldExt := range extVals { for newI, newExt := range extVals { if oldI <= newI { continue |
︙ | ︙ |
Changes to box/notify/entry.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | | 1 2 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package notify import ( "path/filepath" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/parser" ) const ( extZettel = "zettel" // file contains metadata and content extBin = "bin" // file contains binary content extTxt = "txt" // file contains non-binary content ) |
︙ | ︙ | |||
47 48 49 50 51 52 53 | // HasMetaInContent returns true, if metadata will be stored in the content file. func (e *DirEntry) HasMetaInContent() bool { return e.IsValid() && extIsMetaAndContent(e.ContentExt) } // SetupFromMetaContent fills entry data based on metadata and zettel content. | | | | | | 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 | // HasMetaInContent returns true, if metadata will be stored in the content file. func (e *DirEntry) HasMetaInContent() bool { return e.IsValid() && extIsMetaAndContent(e.ContentExt) } // SetupFromMetaContent fills entry data based on metadata and zettel content. func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content domain.Content, getZettelFileSyntax func() []string) { if e.Zid != m.Zid { panic("Zid differ") } if contentName := e.ContentName; contentName != "" { if !extIsMetaAndContent(e.ContentExt) && e.MetaName == "" { e.MetaName = e.calcBaseName(contentName) } return } syntax := m.GetDefault(api.KeySyntax, "") ext := calcContentExt(syntax, m.YamlSep, getZettelFileSyntax) metaName := e.MetaName eimc := extIsMetaAndContent(ext) if eimc { if metaName != "" { ext = contentExtWithMeta(syntax, content) } e.ContentName = e.calcBaseName(metaName) + "." + ext e.ContentExt = ext } else { if len(content.AsBytes()) > 0 { e.ContentName = e.calcBaseName(metaName) + "." + ext e.ContentExt = ext } if metaName == "" { e.MetaName = e.calcBaseName(e.ContentName) } } } func contentExtWithMeta(syntax string, content domain.Content) string { p := parser.Get(syntax) if content.IsBinary() { if p.IsImageFormat { return syntax } return extBin } if p.IsImageFormat { return extTxt } return syntax } func calcContentExt(syntax string, yamlSep bool, getZettelFileSyntax func() []string) string { if yamlSep { return extZettel } switch syntax { case api.ValueSyntaxNone, api.ValueSyntaxZmk: return extZettel } for _, s := range getZettelFileSyntax() { if s == syntax { return extZettel } } |
︙ | ︙ |
Changes to box/notify/fsdir.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package notify import ( "os" "path/filepath" |
︙ | ︙ | |||
54 55 56 57 58 59 60 | log.Error(). Str("parentDir", absParentDir).Err(errParent). Str("path", absPath).Err(err). Msg("Unable to access Zettel directory and its parent directory") watcher.Close() return nil, err } | > | | | | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | log.Error(). Str("parentDir", absParentDir).Err(errParent). Str("path", absPath).Err(err). Msg("Unable to access Zettel directory and its parent directory") watcher.Close() return nil, err } log.Warn(). Str("parentDir", absParentDir).Err(errParent). Msg("Parent of Zettel directory cannot be supervised") log.Warn().Str("path", absPath). Msg("Zettelstore might not detect a deletion or movement of the Zettel directory") } else if err != nil { // Not a problem, if container is not available. It might become available later. log.Warn().Err(err).Str("path", absPath).Msg("Zettel directory not available") } fsdn := &fsdirNotifier{ log: log, events: make(chan Event), refresh: make(chan struct{}), done: make(chan struct{}), |
︙ | ︙ | |||
92 93 94 95 96 97 98 | func (fsdn *fsdirNotifier) eventLoop() { defer fsdn.base.Close() defer close(fsdn.events) defer close(fsdn.refresh) if !listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) { return } | < | | | < < < < | 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 | func (fsdn *fsdirNotifier) eventLoop() { defer fsdn.base.Close() defer close(fsdn.events) defer close(fsdn.refresh) if !listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) { return } for fsdn.readAndProcessEvent() { } } func (fsdn *fsdirNotifier) readAndProcessEvent() bool { select { case <-fsdn.done: fsdn.log.Trace().Int("i", 1).Msg("done with read and process events") return false default: } select { case <-fsdn.done: fsdn.log.Trace().Int("i", 2).Msg("done with read and process events") return false case <-fsdn.refresh: fsdn.log.Trace().Msg("refresh") listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) case err, ok := <-fsdn.base.Errors: fsdn.log.Trace().Err(err).Bool("ok", ok).Msg("got errors") if !ok { return false } select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: fsdn.log.Trace().Int("i", 3).Msg("done with read and process events") return false } case ev, ok := <-fsdn.base.Events: fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Bool("ok", ok).Msg("file event") if !ok { return false } if !fsdn.processEvent(&ev) { return false } } return true } func (fsdn *fsdirNotifier) processEvent(ev *fsnotify.Event) bool { if strings.HasPrefix(ev.Name, fsdn.path) { if len(ev.Name) == len(fsdn.path) { return fsdn.processDirEvent(ev) } return fsdn.processFileEvent(ev) } |
︙ | ︙ | |||
165 166 167 168 169 170 171 | } return true } if ev.Has(fsnotify.Create) { err := fsdn.base.Add(fsdn.path) if err != nil { | | | 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | } return true } if ev.Has(fsnotify.Create) { err := fsdn.base.Add(fsdn.path) if err != nil { fsdn.log.IfErr(err).Str("name", fsdn.path).Msg("Unable to add directory") select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: fsdn.log.Trace().Int("i", 2).Msg("done dir event processing") return false } } |
︙ | ︙ |
Changes to box/notify/helper.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package notify import ( "archive/zip" "os" "zettelstore.de/z/logger" ) // MakeMetaFilename builds the name of the file containing metadata. func MakeMetaFilename(basename string) string { return basename //+ ".meta" } // EntryFetcher return a list of (file) names of an directory. type EntryFetcher interface { Fetch() ([]string, error) } type dirPathFetcher struct { |
︙ | ︙ |
Changes to box/notify/notify.go.
1 | //----------------------------------------------------------------------------- | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // 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 notify provides some notification services to be used by box services. package notify import "fmt" |
︙ | ︙ | |||
34 35 36 37 38 39 40 | // Valid constants for event operations. // // Error signals a detected error. Details are in Event.Err. // // Make signals that the container is detected. List events will follow. // // List signals a found file, if Event.Name is not empty. Otherwise it signals | | | | | | | 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // Valid constants for event operations. // // Error signals a detected error. Details are in Event.Err. // // Make signals that the container is detected. List events will follow. // // List signals a found file, if Event.Name is not empty. Otherwise it signals // the end of files within the container. // // Destroy signals that the container is not there any more. It might me Make later again. // // Update signals that file Event.Name was created/updated. File name is relative // to the container. // // Delete signals that file Event.Name was removed. File name is relative to // the container's name. const ( _ EventOp = iota Error // Error while operating Make // Make container List // List container Destroy // Destroy container Update // Update element |
︙ | ︙ |
Changes to box/notify/simpledir.go.
1 | //----------------------------------------------------------------------------- | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // 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 notify import ( "path/filepath" |
︙ | ︙ | |||
43 44 45 46 47 48 49 | } go sdn.eventLoop() return sdn, nil } // NewSimpleZipNotifier creates a zip-file based notifier that will not receive // any notifications from the operating system. | | | | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | } go sdn.eventLoop() return sdn, nil } // NewSimpleZipNotifier creates a zip-file based notifier that will not receive // any notifications from the operating system. func NewSimpleZipNotifier(log *logger.Logger, zipPath string) (Notifier, error) { sdn := &simpleDirNotifier{ log: log, events: make(chan Event), done: make(chan struct{}), refresh: make(chan struct{}), fetcher: newZipPathFetcher(zipPath), } go sdn.eventLoop() return sdn, nil } func (sdn *simpleDirNotifier) Events() <-chan Event { return sdn.events } func (sdn *simpleDirNotifier) Refresh() { |
︙ | ︙ |
Changes to cmd/cmd_file.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | | | | | | | | 1 2 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "context" "flag" "fmt" "io" "os" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) // ---------- Subcommand: file ----------------------------------------------- func cmdFile(fs *flag.FlagSet) (int, error) { enc := fs.Lookup("t").Value.String() m, inp, err := getInput(fs.Args()) if m == nil { return 2, err } z := parser.ParseZettel( context.Background(), domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, m.GetDefault(api.KeySyntax, api.ValueSyntaxZmk), nil, ) encdr := encoder.Create(api.Encoder(enc)) if encdr == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc) return 2, nil } _, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata) if err != nil { return 2, err |
︙ | ︙ |
Changes to cmd/cmd_password.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | 1 2 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 cmd import ( "flag" "fmt" "os" "golang.org/x/term" "zettelstore.de/c/api" "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" ) // ---------- Subcommand: password ------------------------------------------- func cmdPassword(fs *flag.FlagSet) (int, error) { if fs.NArg() == 0 { fmt.Fprintln(os.Stderr, "User name and user zettel identification missing") |
︙ | ︙ |
Changes to cmd/cmd_run.go.
1 | //----------------------------------------------------------------------------- | | < < < > < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "context" "flag" "net/http" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/server" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { fs.String("c", "", "configuration file") fs.Uint("a", 0, "port number kernel service (0=disable)") |
︙ | ︙ | |||
51 52 53 54 55 56 57 58 59 60 61 | return exitCode, err } func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig) kern := kernel.Main webLog := kern.GetLogger(kernel.WebService) var getUser getUserImpl logAuth := kern.GetLogger(kernel.AuthService) logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser) | > > > > > > < | | > | | < < < > > < < < < < < < < < | > < < | > | | > | > > > > > > > > > > > > > | | > > > > | | | 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 | return exitCode, err } func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig) kern := kernel.Main webLog := kern.GetLogger(kernel.WebService) a := api.New( webLog.Clone().Str("adapter", "api").Child(), webSrv, authManager, authManager, rtConfig, authPolicy) wui := webui.New( webLog.Clone().Str("adapter", "wui").Child(), webSrv, authManager, rtConfig, authManager, boxManager, authPolicy) var getUser getUserImpl logAuth := kern.GetLogger(kernel.AuthService) logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser) ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, authManager, boxManager) ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager) ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager) ucGetMeta := usecase.NewGetMeta(protectedBoxManager) ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) ucGetZettel := usecase.NewGetZettel(protectedBoxManager) ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) ucListMeta := usecase.NewListMeta(protectedBoxManager) ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta, ucListMeta) ucListSyntax := usecase.NewListSyntax(protectedBoxManager) ucListRoles := usecase.NewListRoles(protectedBoxManager) ucZettelContext := usecase.NewZettelContext(protectedBoxManager, rtConfig) ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager) ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager) ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager) ucUnlinkedRefs := usecase.NewUnlinkedReferences(protectedBoxManager, rtConfig) ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager) ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager)) if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" { const assetPrefix = "/assets/" webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir)))) } // Web user interface if !authManager.IsReadonly() { webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler( ucGetMeta, &ucEvaluate)) webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename)) webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler( ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax)) webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel)) webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler( ucGetMeta, ucGetAllMeta, &ucEvaluate)) webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete)) webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax)) webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate)) } webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh)) webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(ucListMeta, &ucEvaluate)) webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler( &ucEvaluate, ucGetMeta)) webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler()) webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler( ucParseZettel, &ucEvaluate, ucGetMeta, ucGetAllMeta, ucUnlinkedRefs)) webSrv.AddZettelRoute('k', server.MethodGet, wui.MakeZettelContextHandler( ucZettelContext, &ucEvaluate)) // API webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler()) webSrv.AddListRoute('j', server.MethodGet, a.MakeQueryHandler(ucListMeta)) webSrv.AddZettelRoute('j', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel)) webSrv.AddZettelRoute('m', server.MethodGet, a.MakeGetMetaHandler(ucGetMeta)) webSrv.AddZettelRoute('o', server.MethodGet, a.MakeGetOrderHandler( usecase.NewZettelOrder(protectedBoxManager, ucEvaluate))) webSrv.AddZettelRoute('p', server.MethodGet, a.MakeGetParsedZettelHandler(ucParseZettel)) webSrv.AddListRoute('q', server.MethodGet, a.MakeQueryHandler(ucListMeta)) webSrv.AddZettelRoute('u', server.MethodGet, a.MakeListUnlinkedMetaHandler( ucGetMeta, ucUnlinkedRefs, &ucEvaluate)) webSrv.AddZettelRoute('v', server.MethodGet, a.MakeGetEvalZettelHandler(ucEvaluate)) webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion)) webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh)) webSrv.AddZettelRoute('x', server.MethodGet, a.MakeZettelContextHandler(ucZettelContext)) webSrv.AddListRoute('z', server.MethodGet, a.MakeListPlainHandler(ucListMeta)) webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetPlainZettelHandler(ucGetZettel)) if !authManager.IsReadonly() { webSrv.AddListRoute('j', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel)) webSrv.AddZettelRoute('j', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate)) webSrv.AddZettelRoute('j', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) webSrv.AddZettelRoute('j', server.MethodMove, a.MakeRenameZettelHandler(&ucRename)) webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreatePlainZettelHandler(&ucCreateZettel)) webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdatePlainZettelHandler(&ucUpdate)) webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename)) } if authManager.WithAuth() { webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager)) } } type getUserImpl struct{} func (*getUserImpl) GetUser(ctx context.Context) *meta.Meta { return server.GetUser(ctx) } |
Changes to cmd/command.go.
1 | //----------------------------------------------------------------------------- | | < < < | | 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "flag" "zettelstore.de/c/maps" "zettelstore.de/z/logger" ) // Command stores information about commands / sub-commands. type Command struct { Name string // command name as it appears on the command line Func CommandFunc // function that executes a command |
︙ | ︙ | |||
47 48 49 50 51 52 53 | if cmd.Name == "" || cmd.Func == nil { panic("Required command values missing") } if _, ok := commands[cmd.Name]; ok { panic("Command already registered: " + cmd.Name) } cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError) | | | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | if cmd.Name == "" || cmd.Func == nil { panic("Required command values missing") } if _, ok := commands[cmd.Name]; ok { panic("Command already registered: " + cmd.Name) } cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError) cmd.flags.String("l", logger.InfoLevel.String(), "global log level") if cmd.SetFlags != nil { cmd.SetFlags(cmd.flags) } commands[cmd.Name] = cmd } |
︙ | ︙ |
Changes to cmd/main.go.
1 | //----------------------------------------------------------------------------- | | < < < | < > > > < < | 1 2 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package cmd import ( "crypto/sha256" "flag" "fmt" "net" "net/url" "os" "runtime/debug" "strconv" "strings" "time" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/box" "zettelstore.de/z/box/compbox" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) const strRunSimple = "run-simple" func init() { RegisterCommand(Command{ Name: "help", |
︙ | ︙ | |||
87 88 89 90 91 92 93 | }) RegisterCommand(Command{ Name: "password", Func: cmdPassword, }) } | | | | | | | | | | | | 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 | }) RegisterCommand(Command{ Name: "password", Func: cmdPassword, }) } func fetchStartupConfiguration(fs *flag.FlagSet) (cfg *meta.Meta) { if configFlag := fs.Lookup("c"); configFlag != nil { if filename := configFlag.Value.String(); filename != "" { content, err := readConfiguration(filename) return createConfiguration(content, err) } } content, err := searchAndReadConfiguration() return createConfiguration(content, err) } func createConfiguration(content []byte, err error) *meta.Meta { if err != nil { return meta.New(id.Invalid) } return meta.NewFromInput(id.Invalid, input.NewInput(content)) } func readConfiguration(filename string) ([]byte, error) { return os.ReadFile(filename) } func searchAndReadConfiguration() ([]byte, error) { for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg"} { if content, err := readConfiguration(filename); err == nil { return content, nil } } return readConfiguration(".zscfg") } func getConfig(fs *flag.FlagSet) *meta.Meta { cfg := fetchStartupConfiguration(fs) fs.Visit(func(flg *flag.Flag) { switch flg.Name { case "p": cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", flg.Value.String())) case "a": cfg.Set(keyAdminPort, flg.Value.String()) case "d": |
︙ | ︙ | |||
143 144 145 146 147 148 149 | cfg.Set(keyDebug, flg.Value.String()) case "r": cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } }) | | | 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | cfg.Set(keyDebug, flg.Value.String()) case "r": cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } }) return cfg } func deleteConfiguredBoxes(cfg *meta.Meta) { for _, p := range cfg.PairsRest() { if key := p.Key; strings.HasPrefix(key, kernel.BoxURIs) { cfg.Delete(key) } |
︙ | ︙ | |||
178 179 180 181 182 183 184 | keyURLPrefix = "url-prefix" keyVerbose = "verbose-mode" ) func setServiceConfig(cfg *meta.Meta) bool { debugMode := cfg.GetBool(keyDebug) if debugMode && kernel.Main.GetKernelLogger().Level() > logger.DebugLevel { | | | > | > | 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | keyURLPrefix = "url-prefix" keyVerbose = "verbose-mode" ) func setServiceConfig(cfg *meta.Meta) bool { debugMode := cfg.GetBool(keyDebug) if debugMode && kernel.Main.GetKernelLogger().Level() > logger.DebugLevel { kernel.Main.SetGlobalLogLevel(logger.DebugLevel) } if strLevel, found := cfg.Get(keyLogLevel); found { if level := logger.ParseLevel(strLevel); level.IsValid() { kernel.Main.SetGlobalLogLevel(level) } } err := setConfigValue(nil, kernel.CoreService, kernel.CoreDebug, debugMode) err = setConfigValue(err, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) if val, found := cfg.Get(keyAdminPort); found { err = setConfigValue(err, kernel.CoreService, kernel.CorePort, val) } |
︙ | ︙ | |||
233 234 235 236 237 238 239 | return err == nil } func setConfigValue(err error, subsys kernel.Service, key string, val any) error { if err == nil { err = kernel.Main.SetConfig(subsys, key, fmt.Sprint(val)) if err != nil { | | | | 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 | return err == nil } func setConfigValue(err error, subsys kernel.Service, key string, val any) error { if err == nil { err = kernel.Main.SetConfig(subsys, key, fmt.Sprint(val)) if err != nil { kernel.Main.GetKernelLogger().Fatal().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration") } } return err } func executeCommand(name string, args ...string) int { command, ok := Get(name) if !ok { fmt.Fprintf(os.Stderr, "Unknown command %q\n", name) return 1 } fs := command.GetFlags() if err := fs.Parse(args); err != nil { fmt.Fprintf(os.Stderr, "%s: unable to parse flags: %v %v\n", name, args, err) return 1 } cfg := getConfig(fs) if !setServiceConfig(cfg) { fs.Usage() return 2 } kern := kernel.Main var createManager kernel.CreateBoxManagerFunc |
︙ | ︙ | |||
289 290 291 292 293 294 295 | return nil }, ) if command.Simple { kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true") } | | | | 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 | return nil }, ) if command.Simple { kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true") } kern.Start(command.Header, command.LineServer) exitCode, err := command.Func(fs) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } kern.Shutdown(true) return exitCode } // 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() int { if _, err := searchAndReadConfiguration(); err == nil { return executeCommand(strRunSimple) } dir := "./zettel" if err := os.MkdirAll(dir, 0750); err != nil { fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err) return 1 } |
︙ | ︙ |
Changes to cmd/register.go.
1 | //----------------------------------------------------------------------------- | | < < < < < | > < > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package cmd provides command generic functions. package cmd // Mention all needed encoders, parsers and stores to have them registered. import ( _ "zettelstore.de/z/box/compbox" // Allow to use computed box. _ "zettelstore.de/z/box/constbox" // Allow to use global internal box. _ "zettelstore.de/z/box/dirbox" // Allow to use directory box. _ "zettelstore.de/z/box/filebox" // Allow to use file box. _ "zettelstore.de/z/box/membox" // Allow to use in-memory box. _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. _ "zettelstore.de/z/encoder/sexprenc" // Allow to use sexpr encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. _ "zettelstore.de/z/encoder/zjsonenc" // Allow to use ZJSON encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/kernel/impl" // Allow kernel implementation to create itself _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/pikchr" // Allow to use PIC/Pikchr parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) |
Changes to cmd/zettelstore/main.go.
1 | //----------------------------------------------------------------------------- | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // 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 main is the starting point for the zettelstore command. package main import ( "os" |
︙ | ︙ |
Changes to collect/collect.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/ast" |
︙ | ︙ |
Changes to collect/collect_test.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect_test provides some unit test for collectors. package collect_test import ( "testing" |
︙ | ︙ |
Changes to collect/order.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/ast" |
︙ | ︙ |
Added 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import ( "zettelstore.de/z/ast" "zettelstore.de/z/strfun" ) // DivideReferences divides the given list of rederences into zettel, local, and external References. func DivideReferences(all []*ast.Reference) (zettel, local, external []*ast.Reference) { if len(all) == 0 { return nil, nil, nil } mapZettel := make(strfun.Set) mapLocal := make(strfun.Set) mapExternal := make(strfun.Set) for _, ref := range all { if ref.State == ast.RefStateSelf { continue } if ref.IsZettel() { zettel = appendRefToList(zettel, mapZettel, ref) } else if ref.IsExternal() { external = appendRefToList(external, mapExternal, ref) } else { local = appendRefToList(local, mapLocal, ref) } } return zettel, local, external } func appendRefToList(reflist []*ast.Reference, refSet strfun.Set, ref *ast.Reference) []*ast.Reference { s := ref.String() if !refSet.Has(s) { reflist = append(reflist, ref) refSet.Set(s) } return reflist } |
Changes to config/config.go.
1 | //----------------------------------------------------------------------------- | | < < < > | > | < < < < < > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package config provides functions to retrieve runtime configuration data. package config import ( "context" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Key values that are supported by Config.Get const ( KeyFooterHTML = "footer-html" // api.KeyLang KeyMarkerExternal = "marker-external" ) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig // Get returns the value of the given key. It searches first in the given metadata, // then in the data of the current user, and at last in the system-wide data. Get(ctx context.Context, m *meta.Meta, key string) string // AddDefaultValues enriches the given meta data with its default values. AddDefaultValues(context.Context, *meta.Meta) *meta.Meta // GetSiteName returns the current value of the "site-name" key. GetSiteName() string // GetHomeZettel returns the value of the "home-zettel" key. GetHomeZettel() id.Zid // GetHTMLInsecurity returns the current GetHTMLInsecurity() HTMLInsecurity // GetMaxTransclusions returns the maximum number of indirect transclusions. GetMaxTransclusions() int |
︙ | ︙ | |||
94 95 96 97 98 99 100 | return "secure" } // AllowHTML returns true, if the given HTML insecurity level matches the given syntax value. func (hi HTMLInsecurity) AllowHTML(syntax string) bool { switch hi { case SyntaxHTML: | | | | < | 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | return "secure" } // AllowHTML returns true, if the given HTML insecurity level matches the given syntax value. func (hi HTMLInsecurity) AllowHTML(syntax string) bool { switch hi { case SyntaxHTML: return syntax == api.ValueSyntaxHTML case MarkdownHTML: return syntax == api.ValueSyntaxHTML || syntax == "markdown" || syntax == "md" case ZettelmarkupHTML: return syntax == api.ValueSyntaxZmk || syntax == api.ValueSyntaxHTML || syntax == "markdown" || syntax == "md" } return false } |
Changes to docs/development/00010000000000.zettel.
1 2 3 4 | id: 00010000000000 title: Developments Notes role: zettel syntax: zmk | < | < < | 1 2 3 4 5 6 7 8 | id: 00010000000000 title: Developments Notes role: zettel syntax: zmk modified: 20210916194954 * [[Required Software|20210916193200]] * [[Checklist for Release|20210916194900]] |
Changes to docs/development/20210916193200.zettel.
1 2 3 4 | id: 20210916193200 title: Required Software role: zettel syntax: zmk | < | | > | | > < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 20210916193200 title: Required Software role: zettel syntax: zmk modified: 20211213190428 The following software must be installed: * A current, supported [[release of Go|https://golang.org/doc/devel/release.html]], * [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``, * [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``, * [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest`` Make sure that the software is in your path, e.g. via: ```sh export PATH=$PATH:/usr/local/go/bin export PATH=$PATH:$(go env GOPATH)/bin ``` |
Changes to docs/development/20210916194900.zettel.
1 2 3 4 | id: 20210916194900 title: Checklist for Release role: zettel syntax: 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: 20210916194900 title: Checklist for Release role: zettel syntax: zmk modified: 20220309105459 # Sync with the official repository #* ``fossil sync -u`` # Make sure that there is no workspace defined. #* ``ls ..`` must not have a file ''go.work'', in no parent folder. # Make sure that all dependencies are up-to-date. #* ``cat go.mod`` # Clean up your Go workspace: #* ``go run tools/build.go clean`` (alternatively: ``make clean``). # All internal tests must succeed: #* ``go run tools/build.go relcheck`` (alternatively: ``make relcheck``). # The API tests must succeed on every development platform: #* ``go run tools/build.go testapi`` (alternatively: ``make api``). # Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual: #* ``go run -race cmd/zettelstore/main.go run -d docs/manual`` #* ``linkchecker http://127.0.0.1:23123 2>&1 | tee lc.txt`` #* Check all ""Error: 404 Not Found"" #* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/p'' with encoding ''html'' for those zettel that are accessible only in ''expert-mode''. #* Try to resolve other error messages and warnings #* Warnings about empty content can be ignored |
︙ | ︙ | |||
40 41 42 43 44 45 46 | # Disable Fossil autosync mode: #* ``fossil setting autosync off`` # Commit the new release version: #* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"`` #* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''. Otherwise client will not be able to import ''zettelkasten.de/z''. # Clean up your Go workspace: | | | | | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | # Disable Fossil autosync mode: #* ``fossil setting autosync off`` # Commit the new release version: #* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"`` #* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''. Otherwise client will not be able to import ''zettelkasten.de/z''. # Clean up your Go workspace: #* ``go run tools/build.go clean`` (alternatively: ``make clean``). # Create the release: #* ``go run tools/build.go release`` (alternatively: ``make release``). # Remove previous executables: #* ``fossil uv remove --glob '*-PREVVERSION*'`` # Add executables for release: #* ``cd release`` #* ``fossil uv add *.zip`` #* ``cd ..`` #* Synchronize with main repository: #* ``fossil sync -u`` # Enable autosync: #* ``fossil setting autosync on`` |
Deleted docs/development/20221026184300.zettel.
|
| < < < < < < < < < < < < < < |
Deleted docs/development/20231218181900.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00000000000100.zettel.
1 2 3 4 | id: 00000000000100 title: Zettelstore Runtime Configuration role: configuration syntax: none | < | | | | 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-2022 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> home-zettel: 00001000000000 modified: 20220215171041 site-name: Zettelstore Manual visibility: owner |
Changes to docs/manual/00001000000000.zettel.
1 2 3 4 5 | id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: 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: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk modified: 20220803183647 * [[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]] * [[Tips and Tricks|00001017000000]] * [[Troubleshooting|00001018000000]] * Frequently asked questions Licensed under the EUPL-1.2-or-later. |
Deleted docs/manual/00001000000001.zettel.
|
| < < < < < < < < |
Deleted docs/manual/00001000000100.zettel.
|
| < < < < < < < < |
Changes to docs/manual/00001002000000.zettel.
1 2 3 4 5 6 | id: 00001002000000 title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk created: 20210126175322 | | < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001002000000 title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20221018105415 Zettelstore supports the following design goals: ; Longevity of stored notes / zettel : Every zettel you create should be readable without the help of any tool, even without Zettelstore. : It should be not hard to write other software that works with your zettel. ; Single user : All zettel belong to you, only to you. Zettelstore provides its services only to one person: you. If the computer running Zettelstore is securely configured, there should be no risk that others are able to read or update your zettel. : If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel. ; Ease of installation : If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate file directory and start working. |
︙ | ︙ | |||
35 36 37 38 39 40 41 | ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : External software can be written to deeply analyze your zettel and the structures they form. ; Security by default : Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks. : If you know what use are doing, Zettelstore allows you to relax some security-related preferences. However, even in this case, the more secure way is chosen. | < < | 31 32 33 34 35 36 37 | ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : External software can be written to deeply analyze your zettel and the structures they form. ; Security by default : Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks. : If you know what use are doing, Zettelstore allows you to relax some security-related preferences. However, even in this case, the more secure way is chosen. |
Changes to docs/manual/00001004010000.zettel.
1 2 3 4 5 6 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20221018184208 The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup 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 stored. 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. |
︙ | ︙ | |||
80 81 82 83 84 85 86 | Default: ""secure"". ; [!listen-addr|''listen-addr''] : Configures the network address, where the Zettelstore service is listening for requests. Syntax is: ''[NETWORKIP]:PORT'', where ''NETWORKIP'' is the IP-address of the networking interface (or something like ""0.0.0.0"" if you want to listen on all network interfaces, and ''PORT'' is the TCP port. Default value: ""127.0.0.1:23123"" ; [!log-level|''log-level''] | | | < < < < < | | 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | Default: ""secure"". ; [!listen-addr|''listen-addr''] : Configures the network address, where the Zettelstore service is listening for requests. Syntax is: ''[NETWORKIP]:PORT'', where ''NETWORKIP'' is the IP-address of the networking interface (or something like ""0.0.0.0"" if you want to listen on all network interfaces, and ''PORT'' is the TCP port. Default value: ""127.0.0.1:23123"" ; [!log-level|''log-level''] : Specify the global [[logging level|00001004059700]] for the whole application, overwriting the level ""debug"" set by configuration [[''debug-mode''|#debug-mode]]. Can be changed at runtime, even for specific internal services, with the ''log-level'' command of the [[administrator console|00001004101000#log-level]]. Default: ""info"". When you are familiar to operate the Zettelstore, you might set the level to ""warn"" or ""error"" to receive less noisy messages from the Zettelstore. ; [!max-request-size|''max-request-size''] : Limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources. The minimum value is 1024. Default: 16777216 (16 MiB). ; [!owner|''owner''] : [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore. |
︙ | ︙ |
Changes to docs/manual/00001004020000.zettel.
1 2 3 4 5 6 | id: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | < | < < < < < | > | | < | < < < | | | < < < < < < < < < < < < < < < < < | 1 2 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 | id: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20220827180953 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. Can be overwritten in a zettel with [[meta key|00001006020000]] ''copyright''. Default: (the empty string). ; [!default-license|''default-license''] : License value to be used when rendering content. Can be overwritten in a zettel with [[meta key|00001006020000]] ''license''. Default: (the empty string). ; [!default-visibility|''default-visibility''] : Visibility to be used, if zettel does not specify a value for the [[''visibility''|00001006020000#visibility]] metadata key. Default: ""login"". ; [!expert-mode|''expert-mode''] : If set to a [[boolean true value|00001006030500]], all zettel with [[visibility ""expert""|00001010070200]] will be shown (to the owner, if [[authentication is enabled|00001010040100]]; to all, otherwise). 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|00001007040310]]. Default: ""&\#10138;"", to display a ""➚"" sign. ; [!lang|''lang''] : Language to be used when displaying content. Default: ""en"". This value is used as a default value, if it is not set in an user's zettel or in a zettel. It is also used to specify the language for all non-zettel content, e.g. lists or search results. Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]]. ; [!max-transclusions|''max-transclusions''] : Maximum number of indirect transclusion. This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]]. Default: ""1024"". ; [!site-name|''site-name''] : Name of the Zettelstore instance. Will be used when displaying some lists. Default: ""Zettelstore"". ; [!yaml-header|''yaml-header''] : If [[true|00001006030500]], 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. ; [!zettel-file-syntax|''zettel-file-syntax''] : If you create a new zettel with a syntax different to ""zmk"", Zettelstore will store the zettel as two files: one for the metadata (file without a filename extension) and another for the content (file extension based on the syntax value). If you want to specify alternative syntax values, for which you want new zettel to be stored in one file (file extension ''.zettel''), you can use this key. |
︙ | ︙ |
Deleted docs/manual/00001004020200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001004050000.zettel.
1 2 3 4 5 | id: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | < | | 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: 20220805174626 Zettelstore is not just a 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 ``` |
︙ | ︙ | |||
24 25 26 27 28 29 30 | * [[``zettelstore help``|00001004050200]] lists all available sub-commands. * [[``zettelstore version``|00001004050400]] to display version information of Zettelstore. * [[``zettelstore run``|00001004051000]] to start the Zettelstore service. * [[``zettelstore run-simple``|00001004051100]] is typically called, when you start Zettelstore by a double.click in your GUI. * [[``zettelstore file``|00001004051200]] to render files manually without activated/running Zettelstore services. * [[``zettelstore password``|00001004051400]] to calculate data for [[user authentication|00001010040200]]. | < < < < < < < | 23 24 25 26 27 28 29 30 | * [[``zettelstore help``|00001004050200]] lists all available sub-commands. * [[``zettelstore version``|00001004050400]] to display version information of Zettelstore. * [[``zettelstore run``|00001004051000]] to start the Zettelstore service. * [[``zettelstore run-simple``|00001004051100]] is typically called, when you start Zettelstore by a double.click in your GUI. * [[``zettelstore file``|00001004051200]] to render files manually without activated/running Zettelstore services. * [[``zettelstore password``|00001004051400]] to calculate data for [[user authentication|00001010040200]]. To measure potential bottlenecks within the software Zettelstore, there are some [[command line flags for profiling the application|00001004059900]]. |
Changes to docs/manual/00001004051100.zettel.
1 2 3 4 5 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk | < | | 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: 20220724162843 === ``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]]. First, this sub-command checks if it can read a [[Zettelstore startup configuration|00001004010000]] file by trying the [[default values|00001004051000#c]]. If this is the case, ''run-simple'' just continues as the [[''run'' sub-command|00001004051000]], but ignores any command line options (including ''-d DIR'').[^This allows a [[curious user|00001003000000]] to become an intermediate user.] |
︙ | ︙ |
Changes to docs/manual/00001004051200.zettel.
1 2 3 4 5 | id: 00001004051200 title: The ''file'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: 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 27 | id: 00001004051200 title: The ''file'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20220423131738 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]] ``` ; ''-t FORMAT'' : Specifies the output format. Supported values are: [[''html''|00001012920510]] (default), [[''sexpr''|00001012920516]], [[''text''|00001012920519]], [[''zjson''|00001012920503]], and [[''zmk''|00001012920522]]. ; ''file-1'' : Specifies the file name, where at least metadata is read. If ''file-2'' is not given, the zettel content is also read from here. ; ''file-2'' : File name where the zettel content is stored. |
︙ | ︙ |
Changes to docs/manual/00001004059700.zettel.
1 2 3 4 5 | id: 00001004059700 title: List of supported logging levels role: manual tags: #configuration #manual #zettelstore syntax: 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 27 | id: 00001004059700 title: List of supported logging levels role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20220113183606 Zettelstore supports various levels of logging output. This allows you to see the inner workings of Zettelstore, or to avoid it. Each level has an associated name and number. A lower number signals more logging output. |= Name | Number >| Description | Trace | 1 | Show most of the inner workings | Debug | 2 | Show many internal values that might be interesting for a [[Zettelstore developer|00000000000005]]. | Sense | 3 | Display sensing events, which are not essential information. | Info | 4 | Display information about an event. In most cases, there is no required action expected from you. | Warn | 5 | Show a warning, i.e. an event that might become an error or more. Mostly invalid data. | Error | 6 | Notify about an error, which was handled automatically. Something is broken. User intervention is not required, in most cases. Monitor the application. | Fatal | 7 | Notify about a significant error that cannot be handled automatically. At least some important functionality is disabled. | Panic | 8 | The application is in an uncertain state and notifies you about its panic. At least some part of the application is possibly restarted. | Mandatory | 9 | Important message will be shown, e.g. the Zettelstore version at startup time. | Disabled | 10 | No messages will be shown If you set the logging level to a certain value, only messages with the same or higher numerical value will be shown. E.g. if you set the logging level to ""warn"", no ""trace"", ""debug"", ""sense", and ""info"" messages are shown, but ""warn"", ""error"", ""fatal"", ""panic"", and ""mandatory"" messages. |
Changes to docs/manual/00001005090000.zettel.
1 2 3 4 5 6 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 | | < < < < > < < < < | | < < < | 1 2 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 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 modified: 20220909180240 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 | [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore | [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore | [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content | [[00000000000007]] | Zettelstore Log | Lists the last 8192 log messages | [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the the index process | [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more | [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore | [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of 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 Zettel 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 Template | 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 | [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000029000]] | Zettelstore Role to CSS Map | [[Maps|00001017000000#role-css]] [[role|00001006020000#role]] to a zettel identifier that is included by the [[Base HTML Template|00000000010100]] as an CSS file | [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid | [[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 [[user zettel|00001010040200]] | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. **Important:** All identifier may change until a stable version of the software is released. |
Changes to docs/manual/00001006000000.zettel.
1 2 3 4 5 | id: 00001006000000 title: Layout of a Zettel role: manual tags: #design #manual #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001006000000 title: Layout of a Zettel role: manual tags: #design #manual #zettelstore syntax: zmk modified: 20220724165931 A zettel consists of two parts: 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. The zettel content is, well, the actual content. In many cases, the content is in plain text form. Plain text is long-lasting. However, content in binary format is also possible. |
︙ | ︙ | |||
26 27 28 29 30 31 32 | Other text formats are also supported, like CSS and HTML templates. Plain text content is always Unicode, encoded as UTF-8. Other character encodings are not supported and will never be[^This is not a real problem, since every modern software should support UTF-8 as an encoding.]. There is support for a graphical format with a text representation: SVG. And there is support for some binary image formats, like GIF, PNG, and JPEG. === Plain, parsed, and evaluated zettel | | | | | | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | Other text formats are also supported, like CSS and HTML templates. Plain text content is always Unicode, encoded as UTF-8. Other character encodings are not supported and will never be[^This is not a real problem, since every modern software should support UTF-8 as an encoding.]. There is support for a graphical format with a text representation: SVG. And there is support for some binary image formats, like GIF, PNG, and JPEG. === Plain, parsed, and evaluated zettel Zettelstore may present your zettel in various forms. One way is to present the zettel as it was read by Zettelstore. This is called ""[[plain zettel|00001003000000#plain]]"", typically retrieved with the [[endpoint|00001012920000]] ''/z/{ID}''. The second way is to present the zettel as it was recognized by Zettelstore. This is called ""[[parsed zettel|00001012053600]]"", typically retrieved with the [[endpoint|00001012920000]] ''/p/{ID}''. Such a zettel was read and analyzed. It can be presented in various [[encodings|00001012920500]].[^The [[zmk encoding|00001012920522]] allows you to compare the plain, the parsed, and the evaluated form of a zettel.] However, a zettel such as this one you are currently reading, is a ""[[evaluated zettel|00001012053500]]"", typically retrieved with the [[endpoint|00001012920000]] ''/v/{ID}''. The biggest difference to a parsed zettel is the inclusion of [[block transclusions|00001007031100]] or [[inline transclusions|00001007040324]] for an evaluated zettel. It can also be presented in various encoding, including the ""zmk"" encoding. Evaluations also applies to metadata of a zettel, if appropriate. Please note, that searching for content is based on parsed zettel. Transcluded content will only be found in transcluded zettel, but not in the zettel that transcluded the content. However, you will easily pick up that zettel by follow the [[backward|00001006020000#backward]] metadata key of the transcluded zettel. |
Changes to docs/manual/00001006010000.zettel.
1 2 3 4 5 | id: 00001006010000 title: Syntax of Metadata role: manual tags: #manual #syntax #zettelstore syntax: 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: 00001006010000 title: Syntax of Metadata role: manual tags: #manual #syntax #zettelstore syntax: zmk modified: 20220218131923 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 (""''-''"", U+002D) 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. |
︙ | ︙ |
Changes to docs/manual/00001006020000.zettel.
1 2 3 4 5 6 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 modified: 20221004134841 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]]. ; [!author|''author''] |
︙ | ︙ | |||
40 41 42 43 44 45 46 | ; [!credential|''credential''] : Contains the hashed password, as it was emitted by [[``zettelstore password``|00001004051400]]. It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key. It is only used for zettel with a ''role'' value of ""user"". ; [!dead|''dead''] : Property that contains all references that does __not__ identify a zettel. | < < < < < < < < < | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | ; [!credential|''credential''] : Contains the hashed password, as it was emitted by [[``zettelstore password``|00001004051400]]. It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key. It is only used for zettel with a ''role'' value of ""user"". ; [!dead|''dead''] : Property that contains all references that does __not__ identify a zettel. ; [!folge|''folge''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''precursor''|#precursor]] value. ; [!forward|''forward''] : Property that contains all references that identify another zettel within the content of the zettel. ; [!id|''id''] : Contains the [[zettel identifier|00001006050000]], as given by the Zettelstore. It cannot be set manually, because it is a computed value. ; [!lang|''lang''] : Language for the zettel. |
︙ | ︙ | |||
90 91 92 93 94 95 96 | Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used. In all other cases, this property is not set. It can be used for [[sorting|00001007700000]] zettel based on their publication date. It is a computed value. There is no need to set it via Zettelstore. | < < < < < < < < < < < > > | 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 | Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used. In all other cases, this property is not set. It can be used for [[sorting|00001007700000]] zettel based on their publication date. It is a computed value. There is no need to set it via Zettelstore. ; [!read-only|''read-only''] : Marks a zettel as read-only. The interpretation of [[supported values|00001006020400]] for this key depends, whether authentication is [[enabled|00001010040100]] or not. ; [!role|''role''] : Defines the role of the zettel. Can be used for selecting zettel. See [[supported zettel roles|00001006020100]]. If not given, it is ignored. ; [!successors|''successors''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''predecessor''|#predecessor]] value. Therefore, it references all zettel that contain a new version of the content and/or metadata. In contrast to [[''folge''|#folge]], these are references because of technical reasons, not because of content-related reasons. In most cases, zettel referencing the current zettel should be updated to reference a successor zettel. The [[query reference|00001007040310]] [[query:backward? successors?]] lists all such zettel. ; [!syntax|''syntax''] : Specifies the syntax that should be used for interpreting the zettel. The zettel about [[other markup languages|00001008000000]] defines supported values. If it is not given, it defaults to ''plain''. ; [!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 of [[''id''|#id]] 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. One use case is to specify the document that the current zettel comments on. The URL will be rendered special in the [[web user interface|00001014000000]] if you use the default template. ; [!useless-files|''useless-files''] : Contains the file names that are rejected to serve the content of a zettel. Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]]. |
︙ | ︙ |
Changes to docs/manual/00001006020100.zettel.
1 2 3 4 5 | id: 00001006020100 title: Supported Zettel Roles role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: 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 27 28 29 30 31 32 33 | id: 00001006020100 title: Supported Zettel Roles role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk modified: 20220623183234 The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing. You are free to define your own roles. It is allowed to set an empty value or to omit the role. Some roles are defined for technical reasons: ; [!configuration|''configuration''] : A zettel that contains some configuration data for the Zettelstore. Most prominent is [[00000000000100]], as described in [[00001004020000]]. ; [!manual|''manual''] : All zettel that document the inner workings of the Zettelstore software. This role is only used in this specific Zettelstore. If you adhere to the process outlined by Niklas Luhmann, a zettel could have one of the following three roles: ; [!note|''note''] : A small note, to remember something. Notes are not real zettel, they just help to create a real zettel. Think of them as Post-it notes. ; [!literature|''literature''] : Contains some remarks about a book, a paper, a web page, etc. You should add a citation key for citing it. ; [!zettel|''zettel''] : A real zettel that contains your own thoughts. However, you are free to define additional roles, e.g. ''material'' for literature that is web-based only, ''slide'' for presentation slides, ''paper'' for the text of a scientific paper, ''project'' to define a project, ... |
Changes to docs/manual/00001006030000.zettel.
1 2 3 4 5 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk modified: 20220304114106 All [[supported metadata keys|00001006020000]] conform to a type. User-defined metadata keys conform also to a type, based on the suffix of the key. |=Suffix|Type | ''-number'' | [[Number|00001006033000]] | ''-role'' | [[Word|00001006035500]] | ''-set'' | [[WordSet|00001006036000]] | ''-title'' | [[Zettelmarkup|00001006036500]] | ''-url'' | [[URL|00001006035000]] | ''-zettel'' | [[Identifier|00001006032000]] | ''-zid'' | [[Identifier|00001006032000]] | ''-zids'' | [[IdentifierSet|00001006032500]] | any other suffix | [[EString|00001006031500]] 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. * [[Credential|00001006031000]] * [[EString|00001006031500]] * [[Identifier|00001006032000]] * [[IdentifierSet|00001006032500]] * [[Number|00001006033000]] * [[String|00001006033500]] * [[TagSet|00001006034000]] * [[Timestamp|00001006034500]] * [[URL|00001006035000]] * [[Word|00001006035500]] * [[WordSet|00001006036000]] * [[Zettelmarkup|00001006036500]] |
Changes to docs/manual/00001006031000.zettel.
1 2 3 4 5 6 | id: 00001006031000 title: Credential Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001006031000 title: Credential Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914130324 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. === Query operators A credential never compares to any other value. A comparison will never match in any way. === Sorting If a list of zettel should be sorted based on a credential value, the identifier of the respective zettel is used instead. |
Changes to docs/manual/00001006031500.zettel.
1 2 3 4 5 6 | id: 00001006031500 title: EString Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001006031500 title: EString Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914130448 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. === Query operator All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""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``. |
︙ | ︙ |
Changes to docs/manual/00001006032000.zettel.
1 2 3 4 5 6 | id: 00001006032000 title: Identifier Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | < < < < < < < | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001006032000 title: Identifier Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914134914 Values of this type denote a [[zettel identifier|00001006050000]]. === Allowed values Must be a sequence of 14 digits (""0""--""9""). === Query operator Comparison is done with the string representation of the identifiers. 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. |
Changes to docs/manual/00001006032500.zettel.
1 2 3 4 5 6 | id: 00001006032500 title: IdentifierSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001006032500 title: IdentifierSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914131354 Values of this type denote a (sorted) set of [[zettel identifier|00001006050000]]. A set is different to a list, as no duplicate values are allowed. === Allowed values Must be at least one sequence of 14 digits (""0""--""9""), separated by space characters. === Query operator A value matches an identifier set value, if the value matches any of the identifier set values. For example, ""000010060325"" is a prefix ""[[00001006032000]] [[00001006032500]]"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Changes to docs/manual/00001006033000.zettel.
1 2 3 4 5 6 | id: 00001006033000 title: Number Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | < < < < < | < | 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 created: 20210212135017 modified: 20220914131211 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. === Query operator All comparisons are done on the given string representation of the number, ""+12"" will be treated as a different number of ""12"". === Sorting Sorting is done by comparing the numeric values. |
Changes to docs/manual/00001006033500.zettel.
1 2 3 4 5 6 | id: 00001006033500 title: String Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001006033500 title: String Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914130505 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. === Query operator All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""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``. |
︙ | ︙ |
Changes to docs/manual/00001006034000.zettel.
1 2 3 4 5 6 | id: 00001006034000 title: TagSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001006034000 title: TagSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914131048 Values of this type denote a (sorted) set of tags. A set is different to a list, as no duplicate values 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. All characters are mapped to their lower case values. === Query operator All comparisons are done case-sensitive, i.e. ""#hell"" will not be the prefix of ""#Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Changes to docs/manual/00001006034500.zettel.
1 2 3 4 5 6 | id: 00001006034500 title: Timestamp Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | < < < < < < < < | < < | < < | > > | 1 2 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 created: 20210212135017 modified: 20220914130919 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. === Query operator All comparisons assume that up to 14 digits are given. === 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. |
Changes to docs/manual/00001006035000.zettel.
1 2 3 4 5 6 | id: 00001006035000 title: URL Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 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 created: 20210212135017 modified: 20220914130809 Values of this type denote an URL. === Allowed values All characters of an URL / URI are allowed. === Query operator All comparisons are done case-insensitive. For example, ""hello"" is the suffix of ""http://example.com/Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Changes to docs/manual/00001006035500.zettel.
1 2 3 4 5 6 | id: 00001006035500 title: Word Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001006035500 title: Word Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914130655 Values of this type denote a single word. === Allowed values Must be a non-empty sequence of characters, but without the space character. All characters are mapped to their lower case values. === Query operator All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"". === 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 19 20 | id: 00001006036000 title: WordSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914130725 Values of this type denote a (sorted) set of [[words|00001006035500]]. A set is different to a list, as no duplicate values are allowed. === Allowed values Must be a sequence of at least one word, separated by space characters. === Query operator All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""World, Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Changes to docs/manual/00001006036500.zettel.
1 2 3 4 5 6 | id: 00001006036500 title: Zettelmarkup Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001006036500 title: Zettelmarkup Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20220914135405 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. === Query operator Comparison is done similar to the full-text search: both the value to compare and the metadata value are normalized according to Unicode NKFD, ignoring everything except letters and numbers. Letters are mapped to the corresponding lower-case value. For example, ""Brücke"" will be the prefix of ""(Bruckenpfeiler,"". === Sorting To sort two values, the underlying encoding is used to determine which value is less than the other. |
︙ | ︙ |
Changes to docs/manual/00001007000000.zettel.
1 2 3 4 5 6 | id: 00001007000000 title: Zettelmarkup role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007000000 title: Zettelmarkup role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20221018104725 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. Zettelmarkup supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer. Zettelmarkup can be much easier parsed / consumed by a software compared to other markup languages. Writing a parser for [[Markdown|https://daringfireball.net/projects/markdown/syntax]] is quite challenging. |
︙ | ︙ | |||
35 36 37 38 39 40 41 | * [[Basic definitions|00001007020000]] * [[Block-structured elements|00001007030000]] * [[Inline-structured element|00001007040000]] * [[Attributes|00001007050000]] * [[Query expressions|00001007700000]] * [[Summary of formatting characters|00001007800000]] * [[Tutorial|00001007900000]] | < | 35 36 37 38 39 40 41 | * [[Basic definitions|00001007020000]] * [[Block-structured elements|00001007030000]] * [[Inline-structured element|00001007040000]] * [[Attributes|00001007050000]] * [[Query expressions|00001007700000]] * [[Summary of formatting characters|00001007800000]] * [[Tutorial|00001007900000]] |
Changes to docs/manual/00001007030900.zettel.
1 2 3 4 5 | id: 00001007030900 title: Zettelmarkup: Comment Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001007030900 title: Zettelmarkup: Comment Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218130330 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 renderer. 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. |
︙ | ︙ |
Changes to docs/manual/00001007031140.zettel.
1 2 3 4 5 6 | id: 00001007031140 title: Zettelmarkup: Query Transclusion role: manual tags: #manual #search #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007031140 title: Zettelmarkup: Query Transclusion role: manual tags: #manual #search #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 modified: 20221014164027 A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''. The line must literally start with the sequence ''{{{query:''. Everything after this prefix is interpreted as a [[query expression|00001007700000]]. When evaluated, the query expression is evaluated, often resulting in a list of [[links|00001007040310]] to zettel, matching the query expression. The result replaces the query transclusion element. |
︙ | ︙ | |||
43 44 45 46 47 48 49 | It is used for the ''ATOM'' and ''RSS'' action. ; ''ATOM'' (aggregate) : Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed. The document is embedded into the referencing zettel. ; ''RSS'' (aggregate) : Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed. The document is embedded into the referencing zettel. | < < < < < | | < < < < | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | It is used for the ''ATOM'' and ''RSS'' action. ; ''ATOM'' (aggregate) : Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed. The document is embedded into the referencing zettel. ; ''RSS'' (aggregate) : Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed. The document is embedded into the referencing zettel. ; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates) : Emit an aggregate of the given metadata key. The key can be given in any letter case. ```zmk {{{query:tags:#search | tags}}} ``` This is a tag cloud of all tags that are used together with the tag #search: :::example {{{query:tags:#search | tags}}} ::: |
Changes to docs/manual/00001007031300.zettel.
1 2 3 4 5 | id: 00001007031300 title: Zettelmarkup: Evaluation Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: 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: 00001007031300 title: Zettelmarkup: Evaluation Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220311120658 Evaluation blocks are used to enter text that could be evaluated by either Zettelstore or external software. They begin with at least three tilde characters (""''~''"", U+007E) at the first position of a line. You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters. The evaluation block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (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 [[syntax|00001008000000]] value to evaluate its content. Not all syntax values are supported by Zettelstore.[^Currently just ""[[draw|00001008050000]]"".] The main reason for an evaluation block is to be used with external software via the [[ZJSON encoding|00001012920503]]. 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 tilde characters in the text that should not be interpreted. For example: |
︙ | ︙ |
Changes to docs/manual/00001007031400.zettel.
1 2 3 4 5 | id: 00001007031400 title: Zettelmarkup: Math-mode Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: 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: 00001007031400 title: Zettelmarkup: Math-mode Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220311182505 Math-mode blocks are used to enter mathematical formulas / equations in a display style mode. Similar to a [[evaluation blocks|00001007031300]], the block content will be interpreted by either Zettelstore or an external software. They begin with at least three dollar sign characters (""''$''"", U+0024) at the first position of a line. You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters. A math-mode block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (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 [[syntax|00001008000000]] value to evaluate its content. Alternatively, you could provide an attribute with the key ""syntax"" and use the value to specify the syntax. Not all syntax values are supported by Zettelstore.[^Currently: none.] External software might support several values via the [[ZJSON encoding|00001012920503]]. 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 dollar-sign characters in the text that should not be interpreted. For example: |
︙ | ︙ |
Changes to docs/manual/00001007040100.zettel.
1 2 3 4 5 | id: 00001007040100 title: Zettelmarkup: Text Formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001007040100 title: Zettelmarkup: Text Formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218131003 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. |
︙ | ︙ | |||
26 27 28 29 30 31 32 | * The circumflex accent character (""''^''"", U+005E) allows to enter super-scripted text. ** Example: ``e=mc^^2^^`` is rendered in HTML as: ::e=mc^^2^^::{=example}. * The comma character (""'',''"", U+002C) produces sub-scripted text. ** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}. * The quotation mark character (""''"''"", U+0022) marks an inline quotation, according to the [[specified language|00001007050100]]. ** Example: ``""To be or not""`` is rendered in HTML as: ::""To be or not""::{=example}. ** Example: ``""Sein oder nicht""{lang=de}`` is rendered in HTML as: ::""Sein oder nicht""{lang=de}::{=example}. | < < < | 25 26 27 28 29 30 31 32 33 | * The circumflex accent character (""''^''"", U+005E) allows to enter super-scripted text. ** Example: ``e=mc^^2^^`` is rendered in HTML as: ::e=mc^^2^^::{=example}. * The comma character (""'',''"", U+002C) produces sub-scripted text. ** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}. * The quotation mark character (""''"''"", U+0022) marks an inline quotation, according to the [[specified language|00001007050100]]. ** Example: ``""To be or not""`` is rendered in HTML as: ::""To be or not""::{=example}. ** Example: ``""Sein oder nicht""{lang=de}`` is rendered in HTML as: ::""Sein oder nicht""{lang=de}::{=example}. * The colon character (""'':''"", U+003A) mark some text that should belong together. It fills a similar role as [[region blocks|00001007030800]], but just for inline elements. ** Example: ``abc ::def::{=example} ghi`` is rendered in HTML as: abc ::def::{=example} ghi. |
Changes to docs/manual/00001007040310.zettel.
1 2 3 4 5 6 | id: 00001007040310 title: Zettelmarkup: Links role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 00001007040310 title: Zettelmarkup: Links role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 modified: 20220922154843 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 text is a sequence of [[inline elements|00001007040000]]. However, it should not contain links itself. 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]]``. |
︙ | ︙ |
Changes to docs/manual/00001007040320.zettel.
1 2 3 4 5 | id: 00001007040320 title: Zettelmarkup: Inline Embedding / Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001007040320 title: Zettelmarkup: Inline Embedding / Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220803183936 To some degree, an specification for embedded material is conceptually not too far away from a specification for [[linked material|00001007040310]]. Both contain a reference specification and optionally some text. In contrast to a link, the specification of embedded material must currently resolve to some kind of real content. This content replaces the embed specification. An embed 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 reference 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 reference must point to some content, either zettel content or URL-referenced content. If the current user is not allowed to read the referenced zettel, the inline transclusion / embedding is ignored. If the referenced zettel does not exist, or is not readable because of other reasons, a [[spinning emoji|00000000040001]] is presented as a visual hint: |
︙ | ︙ |
Changes to docs/manual/00001007040322.zettel.
1 2 3 4 5 | id: 00001007040322 title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | < | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001007040322 title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220214180955 Image content is assumed, if an URL is used or if the referenced zettel contains an image. Supported formats are: * Portable Network Graphics (""PNG""), as defined by [[RFC\ 2083|https://tools.ietf.org/html/rfc2083]]. * Graphics Interchange Format (""GIF"), as defined by [[https://www.w3.org/Graphics/GIF/spec-gif89a.txt]]. * JPEG / JPG, defined by the __Joint Photographic Experts Group__. * Scalable Vector Graphics (SVG), defined by [[https://www.w3.org/Graphics/SVG/]] If the text is given, it will be interpreted as an alternative textual representation, to help persons with some visual disabilities. [[Attributes|00001007050000]] are supported. They must follow the last right curly bracket character immediately. One prominent example is to specify an explicit title attribute that is shown on certain web browsers when the zettel is rendered in HTML: |
︙ | ︙ |
Changes to docs/manual/00001007040324.zettel.
1 2 3 4 5 6 | id: 00001007040324 title: Zettelmarkup: Inline-mode Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007040324 title: Zettelmarkup: Inline-mode Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 modified: 20220926183509 Inline-mode transclusion applies to all zettel that are parsed in a non-trivial way, e.g. as structured textual content. For example, textual content is assumed if the [[syntax|00001006020000#syntax]] of a zettel is ""zmk"" ([[Zettelmarkup|00001007000000]]), or ""markdown"" / ""md"" ([[Markdown|00001008010000]]). Since this type of transclusion is at the level of [[inline-structured elements|00001007040000]], the transclude specification must be replaced with some inline-structured elements. First, the referenced zettel is read. |
︙ | ︙ | |||
34 35 36 37 38 39 40 | Example: ``{{00001007040322#spin}}`` is rendered as ::{{00001007040322#spin}}::{=example}. ** Just specifying the fragment identifier will reference something in the current page. This is not allowed, to prevent a possible endless recursion. * If the reference is a [[hosted or based|00001007040310#link-specifications]] link / URL to an image, that image will be rendered. | | | 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | Example: ``{{00001007040322#spin}}`` is rendered as ::{{00001007040322#spin}}::{=example}. ** Just specifying the fragment identifier will reference something in the current page. This is not allowed, to prevent a possible endless recursion. * If the reference is a [[hosted or based|00001007040310#link-specifications]] link / URL to an image, that image will be rendered. Example: ``{{/z/00000000040001}}`` is rendered as ::{{/z/00000000040001}}::{=example} If no inline-structured elements are found, the transclude specification is replaced by an error message. To avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]] (also known as ""XML bomb""), the total number of transclusions / expansions is limited. The limit can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel. === See also [[Full transclusion|00001007031100]] does not work inside some text, but is used for [[block-structured elements|00001007030000]]. |
Changes to docs/manual/00001007700000.zettel.
1 | id: 00001007700000 | | | | | | > | > > | > | | | | > > > | | | | < | > > > > > > > > > > > > > | 1 2 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 | id: 00001007700000 title: Query expression role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 modified: 20220913135434 A query expression allows you to search for specific zettel and to perform some actions on them. You may select zettel based on a full-text search, based on specific metadata values, or both. A query expression consists of a __search expression__ and of an optional __action list__. Both are separated by a vertical bar character (""''|''"", U+007C). A query expression follows a [[formal syntax|00001007780000]]. === Search expression In its simplest form, a search expression just contains a string to be search for with the help of a full-text search. For example, the string ''syntax'' will search for all zettel containing the word ""syntax"". If you want to search for all zettel with a title containing the word ""syntax"", you must specify ''title:syntax''. ""title"" names the [[metadata key|00001006010000]], in this case the [[supported metadata key ""title""|00001006020000#title]]. The colon character (""'':''"") is a [[search operator|00001007705000]], in this example to specify a match. ""syntax"" is the [[search value|00001007706000]] that must match to the value of the given metadata key, here ""title"". A search expression may contain more than one search term, such as ''title:syntax''. Search terms must be separated by one or more space characters, for example ''title:syntax title:search''. All terms of a select expression must be true so that a zettel is selected. * [[Search terms|00001007702000]] * [[Search operator|00001007705000]] * [[Search value|00001007706000]] Here are [[some examples|00001007790000]] of search expressions, which can be used to manage a Zettelstore: {{{00001007790000}}} === Action List With a search expression, a list of zettel is selected. Actions allow to modify this list to a certain degree. Which actions are allowed depends on the context. However, actions are further separated into __parameter action__ and __aggregate actions__. A parameter action just sets a parameter for an aggregate action. An aggregate action transforms the list of selected zettel into a different, aggregate form. Only the first aggregate form is executed, following aggregate actions are ignored. In most contexts, valid actions include the name of metadata keys, at least of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]]. |
Deleted docs/manual/00001007701000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001007702000.zettel.
1 2 3 4 5 | id: 00001007702000 title: Search term role: manual tags: #manual #search #zettelstore syntax: 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | id: 00001007702000 title: Search term role: manual tags: #manual #search #zettelstore syntax: zmk modified: 20220821163727 A search term allows you to specify one search restriction. The result [[search expression|00001007700000]], which contains more than one search term, will be the applications of all restrictions. A search term can be one of the following (the first three term are collectively called __search literals__): * A metadata-based search, by specifying the name of a [[metadata key|00001006010000]], followed by a [[search operator|00001007705000]], followed by an optional [[search value|00001007706000]]. All zettel containing the given metadata key with a allowed value (depending on the search operator) are selected. If no search value is given, then all zettel containing the given metadata key are selected (or ignored, for a negated search operator). * An optional [[search operator|00001007705000]], followed by a [[search value|00001007706000]]. This specifies a full-text search for the given search value. **Note:** the search value will be normalized according to Unicode NKFD, ignoring everything except letters and numbers. Therefore, the following search expression are essentially the same: ''"search syntax"'' and ''search syntax''. The first is a search expression with one search value, which is normalized to two strings to be searched for. The second is a search expression containing two search values, giving two string to be searched for. * A metadata key followed by ""''?''"" or ""''!?''"". Is true, if zettel metadata contains / does not contain the given key. * The string ''OR'' signals that following search literals may occur alternatively in the result. Since search literals may be negated, it is possible to form any boolean search expression. Any search expression will be in a [[disjunctive normal form|https://en.wikipedia.org/wiki/Disjunctive_normal_form]]. It has no effect on the following search terms initiated with a special uppercase word. * The string ''ORDER'', followed by a non-empty sequence of spaces and the name of a metadata key, will specify an ordering of the result list. If you include the string ''REVERSE'' after ''ORDER'' but before the metadata key, the ordering will be reversed. Example: ''ORDER published'' will order the resulting list based on the publishing data, while ''ORDER REVERSED published'' will return a reversed result order. An explicit order field will take precedence over the random order described below. If no random order is effective, a ``ORDER REVERSE id`` will be added. This makes the sort stable. Example: ``ORDER created`` will be interpreted as ``ORDER created ORDER REVERSE id``. |
︙ | ︙ | |||
84 85 86 87 88 89 90 | You may have noted that the specifications of first two items overlap somehow. This is resolved by the following rule: * A search term containing no [[search operator character|00001007705000]] is treated as a full-text search. * The first search operator character found in a search term divides the term into two pieces. If the first piece, from the beginning of the search term to the search operator character, is syntactically a metadata key, the search term is treated as a metadata-based search. * Otherwise, the search term is treated as a full-text search. | | < | 72 73 74 75 76 77 78 79 | You may have noted that the specifications of first two items overlap somehow. This is resolved by the following rule: * A search term containing no [[search operator character|00001007705000]] is treated as a full-text search. * The first search operator character found in a search term divides the term into two pieces. If the first piece, from the beginning of the search term to the search operator character, is syntactically a metadata key, the search term is treated as a metadata-based search. * Otherwise, the search term is treated as a full-text search. If a term like ''ORDER'', ''ORDER REVERSE'', ''OFFSET'', or ''LIMIT'' is not followed by an appropriate value, it is interpreted as a search value for a full-text search. For example, ''ORDER 123'' will search for a zettel conatining the strings ""ORDER"" (case-insensitive) and ""123"". |
Changes to docs/manual/00001007705000.zettel.
1 2 3 4 5 | id: 00001007705000 title: Search operator role: manual tags: #manual #search #zettelstore syntax: 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 27 28 29 30 31 32 | id: 00001007705000 title: Search operator role: manual tags: #manual #search #zettelstore syntax: zmk modified: 20220819194709 A search operator specifies how the comparison of a search value and a zettel should be executed. Every comparison is done case-insensitive, treating all uppercase letters the same as lowercase letters. The following are allowed search operator characters: * The exclamation mark character (""!"", U+0021) negates the meaning * The tilde character (""''~''"", U+007E) compares on matching (""match operator"") * The greater-than sign character (""''>''"", U+003E) matches if there is some prefix (""prefix operator"") * The less-than sign character (""''<''"", U+003C) compares a suffix relationship (""suffix operator"") * The colon character (""'':''"", U+003A) compares on equal words (""has operator"") * The question mark (""''?''"", U+003F) checks for an existing metadata key (""exist operator"") Since the exclamation mark character can be combined with the other, there are 10 possible combinations: # ""''!''"": is an abbreviation of the ""''!~''"" operator. # ""''~''"": is successful if the search value matched the value to be compared. # ""''!~''"": is successful if the search value does not match the value to be compared. # ""'':''"": is successful if the search value is equal to one word of the value to be compared. # ""''!:''"": is successful if the search value is not equal to any word of the value to be compared. # ""''>''"": is successful if the search value is a prefix of the value to be compared. # ""''!>''"": is successful if the search value is not a prefix of the value to be compared. # ""''<''"": is successful if the search value is a suffix of the value to be compared. # ""''!<''"": is successful if the search value is not a suffix of the value to be compared. # ""''?''"": is successful if the metadata contains the given key. # ""''!?''"": is successful if the metadata does not contain the given key. # ""''''"": a missing search operator can only occur for a full-text search. It is equal to the ""''~''"" operator. |
Deleted docs/manual/00001007710000.zettel.
|
| < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007720000.zettel.
|
| < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007720300.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007720600.zettel.
|
| < < < < < < < < < < < < |
Deleted docs/manual/00001007720900.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007721200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001007770000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001007780000.zettel.
1 2 3 4 5 6 | id: 00001007780000 title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 | | | < < < < < < < < < < < < < < < < | | | | | | < | | | | | | | | | | | | < < < < < < < < < < | | 1 2 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 | id: 00001007780000 title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 modified: 20220913134024 ``` QueryExpression := SearchExpression ActionExpression? SearchExpression := SearchTerm (SPACE+ SearchTerm)*. SearchTerm := SearchOperator? SearchValue | SearchKey SearchOperator SearchValue? | SearchKey ExistOperator | "OR" | "RANDOM" | "ORDER" SPACE+ ("REVERSE" SPACE+)? SearchKey | "OFFSET" SPACE+ PosInt | "LIMIT" SPACE+ PosInt. SearchValue := Word. SearchKey := MetadataKey. SearchOperator := '!' | ('!')? ('~' | ':' | '<' | '>'). ExistOperator := '?' | '!' '?'. PosInt := '0' | ('1' .. '9') DIGIT*. ActionExpression := '|' (Word (SPACE+ Word)*)? Word := NO-SPACE NO-SPACE* ``` |
Changes to docs/manual/00001007790000.zettel.
1 2 3 4 5 6 | id: 00001007790000 title: Useful query expressions role: manual tags: #example #manual #search #zettelstore syntax: zmk created: 20220810144539 | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001007790000 title: Useful query expressions role: manual tags: #example #manual #search #zettelstore syntax: zmk created: 20220810144539 modified: 20220917174956 |= Query Expression |= Meaning | [[query:role:configuration]] | Zettel that contains some configuration data for the Zettelstore | [[query:ORDER REVERSE created LIMIT 40]] | 40 recently created zettel | [[query:ORDER REVERSE published LIMIT 40]] | 40 recently updated zettel | [[query:RANDOM LIMIT 40]] | 40 random zettel | [[query:dead?]] | Zettel with invalid / dead links | [[query:backward!? precursor!?]] | Zettel that are not referenced by other zettel | [[query:tags!?]] | Zettel without tags |
Changes to docs/manual/00001007800000.zettel.
1 2 3 4 5 | id: 00001007800000 title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001007800000 title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk modified: 20220810095559 The following table gives an overview about the use of all characters that begin a markup element. |= Character :|= [[Blocks|00001007030000]] <|= [[Inlines|00001007040000]] < | ''!'' | (free) | (free) | ''"'' | [[Verse block|00001007030700]] | [[Short inline quote|00001007040100]] | ''#'' | [[Ordered list|00001007030200]] | [[Tag|00001007040000]] | ''$'' | (reserved) | (reserved) | ''%'' | [[Comment block|00001007030900]] | [[Comment|00001007040000]] | ''&'' | (free) | [[Entity|00001007040000]] | ''\''' | (free) | [[Computer input|00001007040200]] | ''('' | (free) | (free) | '')'' | (free) | (free) | ''*'' | [[Unordered list|00001007030200]] | [[strongly emphasized text|00001007040100]] |
︙ | ︙ |
Changes to docs/manual/00001007900000.zettel.
1 2 3 | id: 00001007900000 title: Zettelmarkup: Tutorial role: manual | | | | 1 2 3 4 5 6 7 8 9 10 | id: 00001007900000 title: Zettelmarkup: Tutorial role: manual tags: #manual #tutorial #zettelmarkup #zettelstore syntax: zmk created: 20220810182917 modified: 20220811135314 * [[First steps|00001007903000]]: learn something about paragraphs, emphasized text, and lists. * [[Second steps|00001007906000]]: know about links, thematic breaks, and headings. |
Changes to docs/manual/00001007903000.zettel.
1 2 3 4 5 6 | id: 00001007903000 title: Zettelmarkup: First Steps role: manual tags: #manual #tutorial #zettelmarkup #zettelstore syntax: zmk created: 20220810182917 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007903000 title: Zettelmarkup: First Steps role: manual tags: #manual #tutorial #zettelmarkup #zettelstore syntax: zmk created: 20220810182917 modified: 20220926183359 [[Zettelmarkup|00001007000000]] allows you to leave your text as it is, at least in many situations. Some characters have a special meaning, but you have to enter them is a defined way to see a visible change. Zettelmarkup is designed to be used for zettel, which are relatively short. It allows to produce longer texts, but you should probably use a different tool, if you want to produce an scientific paper, to name an example. === Paragraphs |
︙ | ︙ | |||
27 28 29 30 31 32 33 | | ''An __emphasized__ word'' | An __emphasized__ word | Put two underscore characters before and after the text you want to emphasize | ''Someone uses **bold** text'' | Someone uses **bold** text | Put two asterisks before and after the text you want to see bold | ''He says: ""I love you!""'' | Her says: ""I love you!"" | Put two quotation mark characters before and after the text you want to quote. You probably see a principle. One nice thing about the quotation mark characters: they are rendered according to the current language. | | | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | | ''An __emphasized__ word'' | An __emphasized__ word | Put two underscore characters before and after the text you want to emphasize | ''Someone uses **bold** text'' | Someone uses **bold** text | Put two asterisks before and after the text you want to see bold | ''He says: ""I love you!""'' | Her says: ""I love you!"" | Put two quotation mark characters before and after the text you want to quote. You probably see a principle. One nice thing about the quotation mark characters: they are rendered according to the current language. Examples: ""english""{lang=en}, ""french""{lang=fr}, ""german""{lang=de}, ""finnish""{lang=fi}. You will see later, how to change the current language. === Lists Quite often, text consists of lists. Zettelmarkup supports different types of lists. The most important lists are: * Unnumbered lists, |
︙ | ︙ |
Deleted docs/manual/00001007990000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001008000000.zettel.
1 2 3 4 5 6 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk created: 20210126175300 | | | < < | > > > > < < < > > > > > > > | 1 2 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 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk created: 20210126175300 modified: 20221018115054 [[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: * CSS * HTML template data * Image formats: GIF, PNG, JPEG, SVG * Markdown * Plain text, not further interpreted The [[metadata key|00001006020000#syntax]] ""''syntax''"" specifies which language should be used. If it is not given, it defaults to ''plain''. 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, which has the same name as the zettel identifier and has no file extension.[^Before version 0.2, the meta-file had the file extension ''.meta''] ; [!html|''html''] : Hypertext Markup Language, will not be parsed further. Instead, it is treated as [[text|#text]], but will be encoded differently for [[HTML format|00001012920510]] (same for the [[web user interface|00001014000000]]). Since HTML from unknown sources may contain security-related problems, zettel with this syntax are treated as an empty zettel, unless the startup configuration value for [[''insecure-html''|00001004010000#insecure-html]] is set to at least the value ""html"". For security reasons, equivocal elements will not be encoded in the HTML format / web user interface. The ``< script ...>`` tag is an example. See [[security aspects of Markdown|00001008010000#security-aspects]] for some details. ; [!markdown|''markdown''], [!md|''md''] : For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]]. Since the world of Markdown is so diverse, a [[CommonMark|00001008010500]] parser is used. See [[Use Markdown within Zettelstore|00001008010000]]. ; [!mustache|''mustache''] : A [[Mustache template|https://mustache.github.io/]], used when rendering a zettel as HTML for the [[web user interface|00001014000000]]. ; [!none|''none''] : Only the metadata of a zettel is ""parsed"". Useful for displaying the full metadata. The [[runtime configuration zettel|00000000000100]] uses this syntax. The zettel content is ignored. ; [!pikchr]''pikchr'' : A [[PIC|https://en.wikipedia.org/wiki/Pic_language]]-like [[markup language for diagrams|https://pikchr.org/]]. ; [!svg|''svg''] : [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]]. ; [!text|''text''], [!plain|''plain''], [!txt|''txt''] : Plain text that must not be interpreted further. ; [!zmk|''zmk''] : [[Zettelmarkup|00001007000000]]. The actual values are also listed in a zettel named [[Zettelstore Supported Parser|00000000000092]]. If you specify something else, your content will be interpreted as plain text. === Language for other elements of a zettel [[Zettelmarkup|00001007000000]] allows to specify [[evaluation blocks|00001007031300]], which also receive a syntax value. An evaluation blocks is typically interpreted by external software, for example [[Zettel Presenter|00001006055000#external-applications]]. However, some values are interpreted by Zettelstore during evaluation of a zettel: ; [!draw|''draw''] : A [[language|00001008050000]] to ""draw"" a graphic by using some simple Unicode characters. |
Changes to docs/manual/00001008050000.zettel.
1 | id: 00001008050000 | | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001008050000 title: The ""draw"" language role: manual tags: #graphic #manual #zettelstore syntax: zmk modified: 20220311120439 Sometimes, ""a picture is worth a thousand words"". To create some graphical representations, Zettelmarkup provides a simple mechanism. Characters like ""''|''"" or ""''-''"" already provide some visual feedback. For example, to create a picture containing two boxes that are connected via an arrow, the following representation is possible: ``` ~~~draw |
︙ | ︙ |
Changes to docs/manual/00001010040200.zettel.
1 2 3 4 5 | id: 00001010040200 title: Creating an user zettel role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: 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 27 28 29 30 31 32 33 34 | id: 00001010040200 title: Creating an user zettel role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk modified: 20211127174207 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 two metadata fields: ; ''user-id'' (""user identification"") : The unique identification to be specified for authentication. ; ''credential'' : A hashed password as generated by the [[``zettelstore password``{=sh}|00001004051400]] command. The title of the zettel typically specifies the real name of the user. The following metadata elements are optional: ; ''user-role'' : Associate the user with some basic privileges, e.g. a [[user role|00001010070300]] A user zettel can only be created by the owner of the Zettelstore. The owner should execute the following steps to create a new user zettel: # Create a new zettel. # Save the zettel to get a [[identifier|00001006050000]] for this zettel. # Choose a unique identification for the user. #* If the identifier is not unique, authentication will not work for this user. # Execute the [[``zettelstore password``|00001004051400]] command. #* You have to specify the user identification and the zettel identifier #* If you should not know the password of the new user, send her/him the user identification and the user zettel identifier, so that the person can create the hashed password herself. # Edit the user zettel and add the hashed password under the meta key ''credential'' and the user identification under the key ''user-id''. |
Changes to docs/manual/00001012000000.zettel.
1 2 3 4 5 6 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | < | < > > > | 1 2 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 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20220923105117 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 plain text and JSON as its main encoding format for exchanging messages between a Zettelstore and its client software. There is an [[overview zettel|00001012920000]] that shows the structure of the endpoints used by the API and gives an indication about its use. === Authentication If [[authentication is enabled|00001010040100]], most API calls must include an [[access token|00001010040700]] that proves the identity of the caller. * [[Authenticate an user|00001012050200]] to obtain an access token * [[Renew an access token|00001012050400]] without costly re-authentication * [[Provide an access token|00001012050600]] when doing an API call === Zettel lists * [[Query the list of all zettel|00001012051400]] * [[List plain text titles of all zettel|00001012051200]] === Working with zettel * [[Create a new zettel|00001012053200]] * [[Retrieve metadata and content of an existing zettel|00001012053300]] * [[Retrieve metadata of an existing zettel|00001012053400]] * [[Retrieve evaluated metadata and content of an existing zettel in various encodings|00001012053500]] * [[Retrieve parsed metadata and content of an existing zettel in various encodings|00001012053600]] * [[Retrieve context of an existing zettel|00001012053800]] * [[Retrieve unlinked references to an existing zettel|00001012053900]] * [[Retrieve zettel order within an existing zettel|00001012054000]] * [[Update metadata and content of a zettel|00001012054200]] * [[Rename a zettel|00001012054400]] * [[Delete a zettel|00001012054600]] === Various helper methods * [[Retrieve administrative data|00001012070500]] * [[Execute some commands|00001012080100]] |
︙ | ︙ |
Changes to docs/manual/00001012050200.zettel.
1 2 3 4 5 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: 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 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 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220107215844 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-lifetime-api'' of the [[startup configuration|00001004010000#token-lifetime-api]] (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 # curl -X POST http://IDENT:PASSWORD@127.0.0.1:23123/a {"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600} ``` If you do not want to use Basic Authentication, you can also send user identification and password as HTML form data: ```sh # curl -X POST -d 'username=IDENT&password=PASSWORD' http://127.0.0.1:23123/a {"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600} ``` In all cases, you will receive an JSON object will all [[relevant data|00001012921000]] to be used for further API calls. **Important:** obtaining a token is a time-intensive process. Zettelstore will delay every request to obtain a token for a certain amount of time. Please take into account that this request will take approximately 500 milliseconds, under certain circumstances more. However, if [[authentication is not enabled|00001010040100]] and you send an authentication request, no user identification/password checking is done and you receive an artificial token immediate, without any delay: ```sh # curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a {"token":"freeaccess","token_type":"Bearer","expires_in":316224000} ``` In this case, it is even possible to omit the user identification/password. === HTTP Status codes In all cases of successful authentication, a JSON object is returned, which contains the token under the key ''"token"''. A successful authentication is signaled with the HTTP status code 200, as usual. Other status codes possibly send by the Zettelstore: ; ''400'' : Unable to process the request. In most cases the form data was invalid. ; ''401'' |
︙ | ︙ |
Changes to docs/manual/00001012050400.zettel.
1 2 3 4 5 | id: 00001012050400 title: API: Renew an access token role: manual tags: #api #manual #zettelstore syntax: 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 27 28 29 30 31 32 33 34 35 36 37 | id: 00001012050400 title: API: Renew an access token role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220107215751 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. If [[authentication is not enabled|00001010040100]] and you send a renew request, no checking is done and you receive an artificial token immediate, without any delay: ```sh # curl -X PUT -H 'Authorization: Bearer freeaccess' http://127.0.0.1:23123/a {"token":"freeaccess","token_type":"Bearer","expires_in":316224000} ``` In this case, it is even possible to omit the access token. === HTTP Status codes ; ''200'' : Renew process was successful, the body contains an [[appropriate JSON object|00001012921000]]. ; ''400'' : The renew process was not successful. There are several reasons for this. Maybe access bearer token was not valid. Probably you should [[authenticate|00001012050200]] again with user identification and password. |
Changes to docs/manual/00001012051200.zettel.
1 | id: 00001012051200 | | | | < | < | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > < < < | < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | 1 2 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 | id: 00001012051200 title: API: List plain text titles of all or some zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20220923105412 To list the plain text titles 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]. A plain text document is returned, with one line per zettel. Each line contains in the first 14 characters the [[zettel identifier|00001006050000]]. Separated by a space character, the title of the zettel follows: ```sh # curl http://127.0.0.1:23123/z 00001012051200 API: Renew an access token 00001012050600 API: Provide an access token 00001012050400 API: Renew an access token 00001012050200 API: Authenticate a client 00001012000000 API ``` The query parameter ""''q''"" allows you to specify [[query expressions|00001007700000]] for a full-text search of all zettel content and/or restricting the search according to specific metadata, similar to endpoint ''/q'' to [[query zettel|00001012051400]]. You are allowed to specify this query parameter more than once. This parameter loosely resembles the search form of the [[web user interface|00001014000000]]. 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?q=title%3AAPI' 00001012921000 API: JSON structure of an access token 00001012920500 Formats available by the API 00001012920000 Endpoints used by the API ... ``` An implicit precondition is that the zettel must contain the given metadata key. For a metadata key like [[''title''|00001006020000#title]], which has a default value, this precondition should always be true. But the situation is different for a key like [[''url''|00001006020000#url]]. Both ``curl 'http://localhost:23123/z?q=url%3A'`` and ``curl 'http://localhost:23123/z?q=url%3A!'`` may result in an empty list. Alternatively, you also can use the [[endpoint|00001012920000]] ''/z'' for a simpler result format. The first example translates to: ```sh # curl 'http://127.0.0.1:23123/z?q=title%3AAPI' 00001012921000 API: JSON structure of an access token 00001012920500 Formats available by the API 00001012920000 Endpoints used by the API ... ``` === Other output formats If you want to get the list of metadata of all or some zettel in JSON format, use endpoint ''/q'' to [[query the list of zettel|00001012051400]]. === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an [[appropriate JSON object|00001012921000]]. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the access bearer token was not valid. |
Changes to docs/manual/00001012051400.zettel.
1 2 3 4 5 6 | id: 00001012051400 title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 | | < | < | < | < < | < < < < < | < < < < < < | < < < | < < < < < < < | < < < < | < < | < < < < | < < < < < < < < < < < < < < < < < < | < < | < < < < | < < < < < < < < < | > | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | < < < | 1 2 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 | id: 00001012051400 title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 modified: 20220923105405 The [[endpoint|00001012920000]] ''/q'' allows to query the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below). An empty search expression will select all zettel. An empty list of action, or no valid action, returns the list of all selected zettel metadata. Search expression and action list are separated by a vertical bar character (""''|''"", U+007C), and must be given with the query parameter ''q''. For example, to list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/q?q=|role''. If successful, the output is a JSON object: ```sh # curl http://127.0.0.1:23123/q?q=|role {"map":{"configuration":["00000000090002","00000000090000", ... ,"00000000000001"],"manual":["00001014000000", ... ,"00001000000000"],"zettel":["00010000000000", ... ,"00001012070500","00000000090001"]}} ``` The JSON object only contains the key ''"map"'' with the value of another object. This second object contains all role names as keys and the list of identifier of those zettel with this specific role as a value. Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/q?q=|tags''. If successful, the output is a JSON object: ```sh # curl http://127.0.0.1:23123/q?q=|tags {"map":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}} ``` The JSON object only contains the key ''"map"'' 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. If you want only those tags that occur at least 100 times, use the endpoint ''/q?q=|MIN100+tags''. You see from this that actions are separated by space characters. There are two types of actions: parameters and aggregates. The following actions are supported: ; ''MINn'' (parameter) : Emit only those values with at least __n__ aggregated values. __n__ must be a positive integer, ''MIN'' must be given in upper-case letters. ; ''MAXn'' (parameter) : Emit only those values with at most __n__ aggregated values. __n__ must be a positive integer, ''MAX'' must be given in upper-case letters. ; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates) : Emit an aggregate of the given metadata key. The key can be given in any letter case. Only the first aggregate action will be executed. If no valid aggregate action is given, the metadata of all selected zettel are returned.[^For this reason, a HTTP GET to the endpoint ''/j'' is an alias for the endpoint ''/q''.] ```sh # curl http://127.0.0.1:23123/q {"query":"","list":[{"id":"00001012051200","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62},{"id":"00001012050600","meta":{"title":"API: Provide an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62},{"id":"00001012050400","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62},{"id":"00001012050200","meta":{"title":"API: Authenticate a client","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62},{"id":"00001012000000","meta":{"title":"API","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62}]} ``` The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects. These zettel JSON objects themselves contains the keys ''"id"'' (value is a string containing the [[zettel identifier|00001006050000]]), ''"meta"'' (value as a JSON object), and ''"rights"'' (encodes the [[access rights|00001012921200]] for the given zettel). The value of key ''"meta"'' effectively contains all metadata of the identified zettel, where metadata keys are encoded as JSON object keys and metadata values encoded as JSON strings. Additionally, the JSON object contains the keys ''"query"'' and ''"human"'' with a string value. Both will contain a textual description of the underlying query if you select only some zettel with a [[query expression|00001007700000]]. Without a selection, the values are the empty string. ''"query"'' returns the normalized query expression itself, while ''"human"'' is the normalized query expression to be read by humans. If you reformat the JSON output from the ''GET /q'' call, you'll see its structure better: ```json { "query": "", "human": "", "list": [ { "id": "00001012051200", "meta": { "title": "API: List for all zettel some data", "tags": "#api #manual #zettelstore", "syntax": "zmk", "role": "manual" }, "rights":62 }, { "id": "00001012050600", "meta": { "title": "API: Provide an access token", "tags": "#api #manual #zettelstore", "syntax": "zmk", "role": "manual" }, "rights":62 }, { "id": "00001012050400", "meta": { "title": "API: Renew an access token", "tags": "#api #manual #zettelstore", "syntax": "zmk", "role": "manual" }, "rights":62 }, { "id": "00001012050200", "meta": { "title": "API: Authenticate a client", "tags": "#api #manual #zettelstore", "syntax": "zmk", "role": "manual" }, "rights":62 }, { "id": "00001012000000", "meta": { "title": "API", "tags": "#api #manual #zettelstore", "syntax": "zmk", "role": "manual" }, "rights":62 } ] } ``` In this special case, the metadata of each zettel just contains the four default keys ''title'', ''tags'', ''syntax'', and ''role''. === Note This request (and similar others) will always return a list of metadata, provided the request was syntactically correct. There will never be a HTTP status code 403 (Forbidden), even if [[authentication was enabled|00001010040100]] and you did not provide a valid access token. In this case, the resulting list might be quite short (some zettel will have [[public visibility|00001010070200]]) or the list might be empty. With this call, you cannot differentiate between an empty result list (e.g because your search did not found a zettel with the specified term) and an empty list because of missing authorization (e.g. an invalid access token). === HTTP Status codes ; ''200'' : Query was successful. ; ''204'' : Query was successful, but results in no content. Most likely, you specified no appropriate aggregator. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the access bearer token was not valid, or you forgot to specify a valid query. |
Deleted docs/manual/00001012051600.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012051800.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001012053200.zettel.
1 2 3 4 5 | id: 00001012053200 title: API: Create a new zettel role: manual tags: #api #manual #zettelstore syntax: 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 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 | id: 00001012053200 title: API: Create a new zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220628111320 A zettel is created by adding it to the [[list of zettel|00001012000000#zettel-lists]]. Therefore, the [[endpoint|00001012920000]] to create a new zettel is also ''/j'', but you must send the data of the new zettel via a HTTP POST request. The body of the POST request must contain a JSON object that specifies metadata and content of the zettel to be created. The following keys of the JSON object are used: ; ''"meta"'' : References an embedded JSON object with only string values. The name/value pairs of this objects are interpreted as the metadata of the new zettel. Please consider the [[list of supported metadata keys|00001006020000]] (and their value types). ; ''"encoding"'' : States how the content is encoded. Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]]. Other values will result in a HTTP response status code ''400''. ; ''"content"'' : Is a string value that contains the content of the zettel to be created. Typically, text content is not encoded, and binary content is encoded via Base64. Other keys will be ignored. Even these three keys are just optional. The body of the HTTP POST request must not be empty and it must contain a JSON object. Therefore, a body containing just ''{}'' is perfectly valid. The new zettel will have no content, and only an identifier as [[metadata|00001006020000]]: ``` # curl -X POST --data '{}' http://127.0.0.1:23123/j {"id":"20210713161000"} ``` If creating the zettel was successful, the HTTP response will contain a JSON object with one key: ; ''"id"'' : Contains the [[zettel identifier|00001006050000]] of the created zettel for further usage. In addition, the HTTP response header contains a key ''Location'' with a relative URL for the new zettel. A client must prepend the HTTP protocol scheme, the host name, and (optional, but often needed) the post number to make it an absolute URL. As an example, a zettel with title ""Note"" and content ""Important content."" can be created by issuing: ``` # curl -X POST --data '{"meta":{"title":"Note"},"content":"Important content."}' http://127.0.0.1:23123/j {"id":"20210713163100"} ``` [!plain]Alternatively, you can use the [[endpoint|00001012920000]] ''/z'' to create a new zettel. In this case, the zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line. This is the same format as used by storing zettel within a [[directory box|00001006010000]]. ``` # curl -X POST --data $'title: Note\n\nImportant content.' http://127.0.0.1:23123/z 20210903211500 ``` === HTTP Status codes ; ''201'' : Zettel creation was successful, the body contains its [[zettel identifier|00001006050000]] (JSON object or plain text). ; ''400'' : Request was not valid. There are several reasons for this. Most likely, the JSON was not formed according to above rules. ; ''403'' : You are not allowed to create a new zettel. |
Changes to docs/manual/00001012053300.zettel.
1 2 3 4 5 6 | id: 00001012053300 title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211004093206 | | | | | | | > > | > > | | > > > > > > > > > > > > > > > | | | < > > > > > > > > > > > > > > > > > > > > > > > > > > > > | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | 1 2 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 | id: 00001012053300 title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211004093206 modified: 20220908162927 The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053300''[^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/j/00001012053300 {"id":"00001012053300","meta":{"title":"API: Retrieve data for an existing 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 ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000}}.\n\nFor example, ... ``` Pretty-printed, this results in: ``` { "id": "00001012053300", "meta": { "back": "00001012000000 00001012053200 00001012054400", "backward": "00001012000000 00001012053200 00001012054400 00001012920000", "box-number": "1", "forward": "00001010040100 00001012050200 00001012920000 00001012920800", "modified": "20210726190012", "published": "20210726190012", "role": "manual", "syntax": "zmk", "tags": "#api #manual #zettelstore", "title": "API: Retrieve metadata and content of an existing zettel" }, "encoding": "", "content": "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' (...) "rights": 62 } ``` The following keys of the JSON object are used: ; ''"id"'' : The zettel identifier of the zettel you requested. ; ''"meta"'' : References an embedded JSON object with only string values. The name/value pairs of this objects are interpreted as the metadata of the new zettel. Please consider the [[list of supported metadata keys|00001006020000]] (and their value types). ; ''"encoding"'' : States how the content is encoded. Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]]. Other values will result in a HTTP response status code ''400''. ; ''"content"'' : Is a string value that contains the content of the zettel to be created. Typically, text content is not encoded, and binary content is encoded via Base64. ; ''"rights"'' : An integer number that describes the [[access rights|00001012921200]] for the zettel. === Plain zettel [!plain]Additionally, you can retrieve the plain zettel, without using JSON. Just change the [[endpoint|00001012920000]] to ''/z/{ID}'' Optionally, you may provide which parts of the zettel you are requesting. In this case, add an additional query parameter ''part=PART''. Valid values for [[''PART''|00001012920800]] are ""zettel"", ""[[meta|00001012053400]]"", and ""content"" (the default value). ````sh # curl 'http://127.0.0.1:23123/z/00001012053300' The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053300''[^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 ... ```` ````sh # curl 'http://127.0.0.1:23123/z/00001012053300?part=zettel' 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 ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ... ```` === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object / plain zettel data. ; ''204'' : Request was valid, but there is no data to be returned. Most likely, you specified the query parameter ''part=content'', but the zettel does not contain any content. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the [[zettel identifier|00001006050000]] did not consists of exactly 14 digits. |
︙ | ︙ |
Changes to docs/manual/00001012053400.zettel.
1 2 3 4 5 6 | id: 00001012053400 title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | 1 2 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 | id: 00001012053400 title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 modified: 20220917175233 The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/m/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/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/m/00001012053400 {"meta":{"back":"00001012000000 00001012053300","backward":"00001012000000 00001012053300 00001012920000","box-number":"1","forward":"00001010040100 00001012050200 00001012920000 00001012920800","modified":"20211004111240","published":"20211004111240","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata of an existing zettel"},"rights":62} ``` Pretty-printed, this results in: ``` { "meta": { "back": "00001012000000 00001012053300", "backward": "00001012000000 00001012053300 00001012920000", "box-number": "1", "forward": "00001010040100 00001012050200 00001012920000 00001012920800", "modified": "20211004111240", "published": "20211004111240", "role": "manual", "syntax": "zmk", "tags": "#api #manual #zettelstore", "title": "API: Retrieve metadata of an existing zettel" }, "rights": 62 } ``` The following keys of the JSON object are used: ; ''"meta"'' : References an embedded JSON object with only string values. The name/value pairs of this objects are interpreted as the metadata of the new zettel. Please consider the [[list of supported metadata keys|00001006020000]] (and their value types). ; ''"rights"'' : An integer number that describes the [[access rights|00001012921200]] for the zettel. [!plain]Additionally, you can retrieve the plain metadata of a zettel, without using JSON. Just change the [[endpoint|00001012920000]] to ''/z/{ID}?part=meta'' ````sh # curl 'http://127.0.0.1:23123/z/00001012053400?part=meta' title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk ```` === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the zettel identifier did not consist of exactly 14 digits. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' |
︙ | ︙ |
Changes to docs/manual/00001012053500.zettel.
1 2 3 4 5 6 | id: 00001012053500 title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 | | | | | | | | > | < | | > < | | | | < | < < < | < < | > > > > > > > > > > > > > > > > > | | 1 2 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 | id: 00001012053500 title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 modified: 20220908162843 The [[endpoint|00001012920000]] to work with evaluated metadata and content of a specific zettel is ''/v/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some evaluated data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/v/00001012053500''[^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/v/00001012053500 {"meta":{"title":[{"t":"Text","s":"API:"},{"t":"Space"},{"t":"Text","s":"Retrieve"},{"t":"Space"},{"t":"Text","s":"evaluated"},{"t":"Space"},{"t":"Text","s":"metadata"},{"t":"Space"},{"t":"Text","s":"and"},{"t":"Space"},{"t":"Text","s":"content"},{"t":"Space"},{"t":"Text","s":"of"},{"t":"Space"},{"t":"Text","s":"an"},{"t":"Space"},{"t":"Text","s":"existing"},{"t":"Space"},{"t":"Text","s":"zettel"},{"t":"Space"},{"t":"Text","s":"in"},{"t":"Space"}, ... ``` To select another encoding, you can provide a query parameter ''enc=ENCODING''. The default value for [[''ENCODING''|00001012920500]] is ""[[zjson|00001012920503]]"". Others are ""[[html|00001012920510]]"", ""[[text|00001012920519]]"", and some more. ```sh # curl 'http://127.0.0.1:23123/v/00001012053500?enc=html' <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>API: Retrieve evaluated metadata and content of an existing zettel in various encodings</title> <meta name="zs-role" content="manual"> <meta name="keywords" content="api, manual, zettelstore"> <meta name="zs-syntax" content="zmk"> <meta name="zs-back" content="00001012000000"> <meta name="zs-backward" content="00001012000000"> <meta name="zs-box-number" content="1"> <meta name="copyright" content="(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>"> <meta name="zs-forward" content="00001010040100 00001012050200 00001012920000 00001012920800"> <meta name="zs-published" content="00001012053500"> </head> <body> <p>The <a href="00001012920000">endpoint</a> to work with evaluated metadata and content of a specific zettel is <kbd>/v/{ID}</kbd>, where <kbd>{ID}</kbd> is a placeholder for the <a href="00001006050000">zettel identifier</a>.</p> ... ``` You also can use the query parameter ''part=PART'' to specify which [[parts|00001012920800]] of a zettel must be encoded. In this case, its default value is ''content''. ```sh # curl 'http://127.0.0.1:23123/v/00001012053500?enc=html&part=meta' <meta name="zs-title" content="API: Retrieve evaluated metadata and content of an existing zettel in various encodings"> <meta name="zs-role" content="manual"> <meta name="keywords" content="api, manual, zettelstore"> <meta name="zs-syntax" content="zmk"> <meta name="zs-back" content="00001012000000"> <meta name="zs-backward" content="00001012000000"> <meta name="zs-box-number" content="1"> <meta name="copyright" content="(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>"> <meta name="zs-forward" content="00001010040100 00001012050200 00001012920000 00001012920800"> <meta name="zs-lang" content="en"> <meta name="zs-published" content="00001012053500"> ``` === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' |
︙ | ︙ |
Changes to docs/manual/00001012053600.zettel.
1 2 3 4 5 6 | id: 00001012053600 title: API: Retrieve parsed metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | | | | 1 2 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 | id: 00001012053600 title: API: Retrieve parsed metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20220908163514 The [[endpoint|00001012920000]] to work with parsed metadata and content of a specific zettel is ''/p/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. A __parsed__ zettel is basically an [[unevaluated|00001012053500]] zettel: the zettel is read and analyzed, but its content is not __evaluated__. By using this endpoint, you are able to retrieve the structure of a zettel before it is evaluated. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/v/00001012053600''[^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/p/00001012053600 [{"t":"Para","i":[{"t":"Text","s":"The"},{"t":"Space"},{"t":"Link","q":"zettel","s":"00001012920000","i":[{"t":"Text","s":"endpoint"}]},{"t":"Space"},{"t":"Text","s":"to"},{"t":"Space"},{"t":"Text","s":"work"},{"t":"Space"},{"t":"Text","s":"with"},{"t":"Space"}, ... ``` Similar to [[retrieving an encoded zettel|00001012053500]], you can specify an [[encoding|00001012920500]] and state which [[part|00001012920800]] of a zettel you are interested in. The same default values applies to this endpoint. === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values. ; ''403'' : You are not allowed to retrieve data of the given zettel. ; ''404'' |
︙ | ︙ |
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 | id: 00001012053800 title: API: Retrieve context of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220202112607 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]]. 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 ''forward'' list all zettel to which the current zettel links to. 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. The search for the context of a zettel stops at the [[home zettel|00001004020000#home-zettel]]. This zettel is connected to all other zettel. If it is included, the context would become too big and therefore unusable. To retrieve the context of an existing zettel, use the [[endpoint|00001012920000]] ''/x/{ID}''[^Mnemonic: conte**X**t]. ```` # curl 'http://127.0.0.1:23123/x/00001012053800?limit=3&dir=forward&depth=2' {"id": "00001012053800","meta": {...},"rights":62,"list":[{"id": "00001012921000","meta": {...},"rights":62},{"id": "00001012920800","meta": {...},"rights":62},{"id": "00010000000000","meta": {...},"rights":62}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001012053800", "meta": {...}, "rights": 62, "list": [ { "id": "00001012921000", "meta": {...}, "rights":62 }, { "id": "00001012920800", "meta": {...}, "rights":62 }, { "id": "00010000000000", "meta": {...}, "rights":62 } ] } ```` === Keys The following top-level JSON keys are returned: ; ''id'' : The [[zettel identifier|00001006050000]] for which the context was requested. ; ''meta'': : The metadata of the zettel, encoded as a JSON object. ; ''rights'' : An integer number that describes the [[access rights|00001012921200]] for the given zettel. ; ''list'' : A list of JSON objects with keys ''id'', ''meta'', and ''rights'' 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/00001012053900.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: 00001012053900 title: API: Retrieve unlinked references to an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211119133357 modified: 20220913152019 The value of a personal Zettelstore is determined in part by explicit connections between related zettel. If the number of zettel grow, some of these connections are missing. There are various reasons for this. Maybe, you forgot that a zettel exists. Or you add a zettel later, but forgot that previous zettel already mention its title. __Unlinked references__ are phrases in a zettel that mention the title of another, currently unlinked zettel. To retrieve unlinked references to an existing zettel, use the [[endpoint|00001012920000]] ''/u/{ID}''. ```` # curl 'http://127.0.0.1:23123/u/00001007000000' {"id": "00001007000000","meta": {...},"rights":62,"list": [{"id": "00001012070500","meta": {...},"rights":62},...{"id": "00001006020000","meta": {...},"rights":62}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001007000000", "meta": {...}, "rights": 62, "list": [ { "id": "00001012070500", "meta": {...}, "rights": 62 }, ... { "id": "00001006020000", "meta": {...}, "rights": 62 } ] } ```` This call searches within all zettel whether the title of the specified zettel occurs there. The other zettel must not link to the specified zettel. The title must not occur within a link (e.g. to another zettel), in a [[heading|00001007030300]], in a [[citation|00001007040340]], and must have a uniform formatting. The match must be exact, but is case-insensitive. If the title of the specified zettel contains some extra character that probably reduce the number of found unlinked references, you can specify the title phase to be searched for as a query parameter ''phrase'': ```` # curl 'http://127.0.0.1:23123/u/00001007000000?phrase=markdown' {"id": "00001007000000","meta": {...},"list": [{"id": "00001008010000","meta": {...},"rights":62},{"id": "00001004020000","meta": {...},"rights":62}]} ```` %%TODO: In addition, you are allowed to limit the search by a [[query expression|00001012051840]], which may search for zettel content. === Keys The following top-level JSON keys are returned: ; ''id'' : The [[zettel identifier|00001006050000]] for which the unlinked references were requested. ; ''meta'': : The metadata of the zettel, encoded as a JSON object. ; ''rights'' : An integer number that describes the [[access rights|00001012921200]] for the given zettel. ; ''list'' : A list of JSON objects with keys ''id'', ''meta'', and ''rights'' that describe zettel with unlinked references. === 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 83 84 | id: 00001012054000 title: API: Retrieve zettel order within an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220202112451 Some zettel act as a ""table of contents"" for other zettel. The [[initial zettel|00001000000000]] 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/00001000000000 {"id":"00001000000000","meta":{...},"rights":62,"list":[{"id":"00001001000000","meta":{...},"rights":62},{"id":"00001002000000","meta":{...},"rights":62},{"id":"00001003000000","meta":{...},"rights":62},{"id":"00001004000000","meta":{...},"rights":62},...,{"id":"00001014000000","meta":{...},"rights":62}]} ```` Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] ````json { "id": "00001000000000", "meta": {...}, "rights": 62, "list": [ { "id": "00001001000000", "meta": {...}, "rights": 62 }, { "id": "00001002000000", "meta": {...}, "rights": 62 }, { "id": "00001003000000", "meta": {...}, "rights": 62 }, { "id": "00001004000000", "meta": {...}, "rights": 62 }, ... { "id": "00001014000000", "meta": {...}, "rights": 62 } ] } ```` The following top-level JSON keys are returned: ; ''id'' : The [[zettel identifier|00001006050000]] for which the references were requested. ; ''meta'': : The metadata of the zettel, encoded as a JSON object. ; ''rights'' : An integer number that describes the [[access rights|00001012921200]] for the given zettel. ; ''list'' : A list of JSON objects with keys ''id'', ''meta'', and ''rights'' 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/00001012054200.zettel.
1 2 3 4 5 | id: 00001012054200 title: API: Update a zettel role: manual tags: #api #manual #zettelstore syntax: 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 27 28 29 30 31 32 33 34 35 | id: 00001012054200 title: API: Update a zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20211124180943 Updating metadata and content of a zettel is technically quite similar to [[creating a new zettel|00001012053200]]. In both cases you must provide the data for the new or updated zettel in the body of the HTTP request. One difference is the endpoint. The [[endpoint|00001012920000]] to update a zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. You must send a HTTP PUT request to that endpoint: ``` # curl -X PUT --data '{}' http://127.0.0.1:23123/j/00001012054200 ``` This will put some empty content and metadata to the zettel you are currently reading. As usual, some metadata will be calculated if it is empty. The body of the HTTP response is empty, if the request was successful. [!plain]Alternatively, you can use the [[endpoint|00001012920000]] ''/z/{ID}'' to update a zettel. In this case, the zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line. This is the same format as used by storing zettel within a [[directory box|00001006010000]]. ``` # curl -X POST --data $'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200 ``` === HTTP Status codes ; ''204'' : Update was successful, there is no body in the response. ; ''400'' : Request was not valid. For example, the request body was not valid. |
︙ | ︙ |
Changes to docs/manual/00001012054400.zettel.
1 2 3 4 5 | id: 00001012054400 title: API: Rename a zettel role: manual tags: #api #manual #zettelstore syntax: 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 27 28 29 30 31 32 33 34 | id: 00001012054400 title: API: Rename a zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20211124181324 Renaming a zettel is effectively just specifying a new identifier for the zettel. Since more than one [[box|00001004011200]] might contain a zettel with the old identifier, the rename operation must success in every relevant box to be overall successful. If the rename operation fails in one box, Zettelstore tries to rollback previous successful operations. As a consequence, you cannot rename a zettel when its identifier is used in a read-only box. This applies to all [[predefined zettel|00001005090000]], for example. The [[endpoint|00001012920000]] to rename a zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. You must send a HTTP MOVE request to this endpoint, and you must specify the new zettel identifier as an URL, placed under the HTTP request header key ''Destination''. ``` # curl -X MOVE -H "Destination: 10000000000001" http://127.0.0.1:23123/j/00001000000000 ``` Only the last 14 characters of the value of ''Destination'' are taken into account and those must form an unused [[zettel identifier|00001006050000]]. If the value contains less than 14 characters that do not form an unused zettel identifier, the response will contain a HTTP status code ''400''. All other characters, besides those 14 digits, are effectively ignored. However, the value should form a valid URL that could be used later to [[read the content|00001012053300]] of the freshly renamed zettel. [!plain]Alternatively, you can also use the [[endpoint|00001012920000]] ''/z/{ID}''. Both endpoints behave identical. === HTTP Status codes ; ''204'' : Rename was successful, there is no body in the response. ; ''400'' : Request was not valid. For example, the HTTP header did not contain a valid ''Destination'' key, or the new identifier is already in use. |
︙ | ︙ |
Changes to docs/manual/00001012054600.zettel.
1 2 3 4 5 | id: 00001012054600 title: API: Delete a zettel role: manual tags: #api #manual #zettelstore syntax: 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: 00001012054600 title: API: Delete a zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20211124181041 Deleting a zettel within the Zettelstore is executed on the first [[box|00001004011200]] that contains that zettel. Zettel with the same identifier, but in subsequent boxes remain. If the first box containing the zettel is read-only, deleting that zettel will fail, as well for a Zettelstore in [[read-only mode|00001004010000#read-only-mode]] or if [[authentication is enabled|00001010040100]] and the user has no [[access right|00001010070600]] to do so. The [[endpoint|00001012920000]] to delete a zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. You must send a HTTP DELETE request to this endpoint: ``` # curl -X DELETE http://127.0.0.1:23123/j/00001000000000 ``` [!plain]Alternatively, you can also use the [[endpoint|00001012920000]] ''/z/{ID}''. Both endpoints behave identical. === HTTP Status codes ; ''204'' : Delete was successful, there is no body in the response. ; ''403'' : You are not allowed to delete the given zettel. Maybe you do not have enough access rights, or either the box or Zettelstore itself operate in read-only mode. |
︙ | ︙ |
Changes to docs/manual/00001012070500.zettel.
1 | id: 00001012070500 | | < | | | > | | < | < | | 1 2 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: 00001012070500 title: Retrieve administrative data role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220805174216 The [[endpoint|00001012920000]] ''/x'' allows you to retrieve some (administrative) data. Currently, you can only request Zettelstore version data. ```` # curl 'http://127.0.0.1:23123/x' {"major":0,"minor":4,"patch":0,"info":"dev","hash":"cb121cc980-dirty"} ```` Zettelstore conforms somehow to the Standard [[Semantic Versioning|https://semver.org/]]. The names ""major"", ""minor"", and ""patch"" are described in this standard. The name ""info"" contains sometimes some additional information, e.g. ""dev"" for a development version, or ""preview"" for a preview version. The name ""hash"" contains some data to identify the version from a developers perspective. === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate JSON object. |
Changes to docs/manual/00001012920000.zettel.
1 2 3 4 5 6 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 | | > > > > > > > > > > | | | | | | 1 2 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 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 modified: 20220923101703 All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where: ; ''PREFIX'' : is the URL prefix (default: ""/""), configured via the ''url-prefix'' [[startup configuration|00001004010000]], ; ''LETTER'' : is a single letter that specifies the resource 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]] | Mnemonic | ''a'' | POST: [[client authentication|00001012050200]] | | **A**uthenticate | | PUT: [[renew access token|00001012050400]] | | ''j'' | GET: [[query zettel list|00001012051400]] (alias of ''/q'') | GET: [[retrieve zettel AS JSON|00001012053300]] | **J**SON | | POST: [[create new zettel|00001012053200]] | PUT: [[update a zettel|00001012054200]] | | | DELETE: [[delete the zettel|00001012054600]] | | | MOVE: [[rename the zettel|00001012054400]] | ''m'' | | GET: [[retrieve metadata|00001012053400]] | **M**etadata | ''o'' | | GET: [[list zettel order|00001012054000]] | **O**rder | ''p'' | | GET: [[retrieve parsed zettel|00001012053600]]| **P**arsed | ''q'' | GET: [[query zettel list|00001012051400]] | | **Q**uery | ''u'' | | GET [[unlinked references|00001012053900]] | **U**nlinked | ''v'' | | GET: [[retrieve evaluated zettel|00001012053500]] | E**v**aluated | ''x'' | GET: [[retrieve administrative data|00001012070500]] | GET: [[list zettel context|00001012053800]] | Conte**x**t | | POST: [[execute command|00001012080100]] | ''z'' | GET: [[list zettel|00001012051200#plain]] | GET: [[retrieve zettel|00001012053300#plain]] | **Z**ettel | | POST: [[create new zettel|00001012053200#plain]] | PUT: [[update a zettel|00001012054200#plain]] | | | DELETE: [[delete zettel|00001012054600#plain]] | | | MOVE: [[rename zettel|00001012054400#plain]] 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 | id: 00001012920500 | | < | < < < | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012920500 title: Encodings available via the [[API|00001012000000]] role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20220423131535 A zettel representation can be encoded in various formats for further processing. * [[html|00001012920510]] * [[sexpr|00001012920516]] * [[text|00001012920519]] * [[zjson|00001012920503]] (default) * [[zmk|00001012920522]] |
Added docs/manual/00001012920503.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: 00001012920503 title: ZJSON Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 modified: 20220908163450 A zettel representation that allows to process the syntactic structure of a zettel. It is a JSON-based encoding format, but different to the structures returned by [[endpoint|00001012920000]] ''/j/{ID}''. For an example, take a look at the ZJSON encoding of this page, which is available via the ""Info"" sub-page of this zettel: * [[//v/00001012920503?enc=zjson&part=zettel]], * [[//v/00001012920503?enc=zjson&part=meta]], * [[//v/00001012920503?enc=zjson&part=content]]. If transferred via HTTP, the content type will be ''application/json''. A full zettel encoding results in a JSON object with two keys: ''"meta"'' and ''"content"''. Both values are the same as if you have requested just the appropriate [[part|00001012920800]]. === Encoding of metadata Metadata encoding results in a JSON object, where each metadata key is mapped to the same JSON object name. The associated value is itself a JSON object with two names. The first name ``""`` references the [[metadata key type|00001006030000]]. Depending on the key type, the other name denotes the value of the metadata element. The meaning of these names is [[well defined|00001012920582]], as well as the [[mapping of key types to used object names|00001012920584]]. === Encoding of zettel content The content encoding results in a JSON array of objects, where each objects represents a Zettelmarkup element. Every [!zettelmarkup|Zettelmarkup] element is encoded as a JSON object. These objects always contain the empty name ''""'' with a string value describing the type of Zettelmarkup element. Depending on the type, other one letter names denotes the details of the element. The meaning of these names is [[well defined|00001012920588]]. |
Deleted docs/manual/00001012920513.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001012920516.zettel.
1 | id: 00001012920516 | | | | > | > | | | | > | > > > > | > > > > > > > > > > > > > > > > > > | 1 2 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 | id: 00001012920516 title: Sexpr Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20220422181104 modified: 20220908163427 A zettel representation that is a [[s-expression|https://en.wikipedia.org/wiki/S-expression]] (also known as symbolic expression). It is an alternative to the [[ZJSON encoding|00001012920503]]. Both encodings are (relatively) easy to parse and contain all relevant information of a zettel, metadata and content. For example, take a look at the Sexpr encoding of this page, which is available via the ""Info"" sub-page of this zettel: * [[//v/00001012920516?enc=sexpr&part=zettel]], * [[//v/00001012920516?enc=sexpr&part=meta]], * [[//v/00001012920516?enc=sexpr&part=content]]. If transferred via HTTP, the content type will be ''text/plain''. === Syntax of s-expressions There are only two types of elements: atoms and lists. A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029). A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms. There are three syntactic forms for an atom: numbers, symbols and strings. A number is a non-empty sequence of digits (""0"" ... ""9""). The smallest number is ``0``, there are no negative numbers. A symbol is a non-empty sequence of printable characters, except left or right parenthesis. Unicode characters of the following categories contains printable characters in the above sense: letter (L), number (N), punctuation (P), symbol (S). Symbols are case-insensitive, i.e. ""''ZETTEL''"" and ""''zettel''"" denote the same symbol. A string starts with a quotation mark (""''"''"", U+0022), contains a possibly empty sequence of Unicode characters, and ends with a quotation mark. To allow a string to contain a quotations mark, it must be prefixed by one backslash (""''\\''"", U+005C). To allow a string to contain a backslash, it also must be prefixed by one backslash. Unicode characters with a code less than U+FF are encoded by by the sequence ""''\\xNM''"", where ''NM'' is the hex encoding of the character. Unicode characters with a code less than U+FFFF are encoded by by the sequence ""''\\uNMOP''"", where ''NMOP'' is the hex encoding of the character. Unicode characters with a code less than U+FFFFFF are encoded by by the sequence ""''\\UNMOPQR''"", where ''NMOPQR'' is the hex encoding of the character. In addition, the sequence ""''\\t''"" encodes a horizontal tab (U+0009), the sequence ""''\\n''"" encodes a line feed (U+000A). Atoms are separated by Unicode characters of category separator (Z). |
Deleted docs/manual/00001012920525.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added docs/manual/00001012920582.zettel.
> > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012920582 title: ZJSON Encoding: List of Valid Metadata Value Objects Names role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20220223184324 Every Metadata value element is mapped to a JSON object with some well defined names / keys. |=Name | JSON Value | Meaning | ''"\"'' | string | The type of the Zettelmarkup element. | ''"i"'' | array | A sequence of [[inline-structured|00001007040000]] elements. | ''"s"'' | string | The first / major string value of an element. | ''"y"'' | array | A set of string values. |
Added docs/manual/00001012920584.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 | id: 00001012920584 title: ZJSON Encoding: Mapping of Metadata Key Types to Object Names role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20220304114135 Every [[Metadata key|00001006030000]] is mapped to an [[object name|00001012920582]] where its value is encoded. |=Type | JSON Object Name | Remark | [[Credential|00001006031000]] | ''"s"'' | A string with the decrypted credential. | [[EString|00001006031500]] | ''"s"'' | A possibly empty string. | [[Identifier|00001006032000]] | ''"s"'' | A string containing a [[zettel identifier|00001006050000]]. | [[IdentifierSet|00001006032500]] | ''"y"'' | An array of strings containing [[zettel identifier|00001006050000]]. | [[Number|00001006033000]] | ''"s"'' | A string containing a numeric value. | [[String|00001006033500]] | ''"s"'' | A non-empty string. | [[TagSet|00001006034000]] | ''"y"'' | An array of string containing zettel tags. | [[Timestamp|00001006034500]] | ''"s"'' | A string containing a timestamp in the format YYYYMMDDHHmmSS. | [[URL|00001006035000]] | ''"s"'' | A string containing an URL. | [[Word|00001006035500]] | ''"s"'' | A string containing a word (no space characters) | [[WordSet|00001006036000]] | ''"y"'' | An array of strings containing words. | [[Zettelmarkup|00001006036500]] | ''"i"'' | A sequence of [[inline-structured|00001007040000]] elements. Please note, that metadata is weakly typed. Every metadata key expects a certain type. But the user is free to enter something different. For example, even if the metadata type is ""number"", its value could still be ""abc"". However, the mapping itself is always valid. |
Added docs/manual/00001012920588.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 | id: 00001012920588 title: ZJSON Encoding: List of Valid Zettelmarkup Element Objects Names role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20220301102447 Every [[Zettelmarkup|00001007000000]] element is mapped to a JSON object with some well defined names / keys. |=Name | JSON Value | Meaning | ''"\"'' | string | The type of the Zettelmarkup element. | ''"a"'' | object | Additional attributes of the element. | ''"b"'' | array | A sequence of [[block-structured|00001007030000]] elements. | ''"c"'' | array | A sequence of a sequence of (sub-) list elements or [[inline-structured|00001007040000]] elements. Used for nested lists. | ''"d"'' | array | A sequence of description list elements, where each element is an object of a definition term and a list of descriptions. | ''"e"'' | array | A sequence of descriptions: a JSON array of simple description, which is itself a JSON array of block structured elements. | ''"i"'' | array | A sequence of [[inline-structured|00001007040000]] elements. | ''"j"'' | object | An objects describing a BLOB element. | ''"n"'' | number | A numeric value, e.g. for specifying the [[heading|00001007030300]] level. | ''"o"'' | string | A base64 encoded binary value. Used in some BLOB elements. | ''"p"'' | array | A sequence of two elements: a sequence of [[table|00001007031000]] header value, followed by a sequence of sequence of table row values. | ''"q"'' | string | A second string value, if ''""s""'' is already used. | ''"s"'' | string | The first / major string value of an element. | ''"v"'' | string | A third string value, if ''""q""'' is already used. |
Changes to docs/manual/00001012921000.zettel.
1 | id: 00001012921000 | | < | < | > > | < < | < < < < < < < | 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 [[authentication 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/00001012921200.zettel.
1 2 3 4 5 | id: 00001012921200 title: API: Encoding of Zettel Access Rights role: manual tags: #api #manual #reference #zettelstore syntax: zmk | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001012921200 title: API: Encoding of Zettel Access Rights role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20220201171959 Various API calls return a JSON key ''"rights"'' that encodes the access rights the user currently has. It is an integer number between 0 and 62.[^Not all values in this range are used.] The value ""0"" signals that something went wrong internally while determining the access rights. A value of ""1"" says, that the current user has no access right for the given zettel. In most cases, this value will not occur, because only zettel are presented, which are at least readable by the current user. Values ""2"" to ""62"" are binary encoded values, where each bit signals a special right. |
︙ | ︙ |
Deleted docs/manual/00001012930000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012930500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012931000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012931200.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012931400.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012931600.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012931800.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012931900.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001017000000.zettel.
1 2 3 4 5 6 | id: 00001017000000 title: Tips and Tricks role: manual tags: #manual #zettelstore syntax: zmk created: 20220803170112 | | | < < < < | | | | | | | | | > | 1 2 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 | id: 00001017000000 title: Tips and Tricks role: manual tags: #manual #zettelstore syntax: zmk created: 20220803170112 modified: 20220916132030 === Welcome Zettel * **Problem:** You want to put your Zettelstore into the public and need a starting zettel for your users. In addition, you still want a ""home zettel"", with all your references to internal, non-public zettel. Zettelstore only allows to specify one [[''home-zettel''|00001004020000#home-zettel]]. * **Solution:** *# Create a new zettel with all your references to internal, non-public zettel. Let's assume this zettel receives the zettel identifier ''20220803182600''. *# Create the zettel that should serve as the starting zettel for your users. It must have syntax [[Zettelmarkup|00001008000000#zmk]], i.e. the syntax metadata must be set to ''zmk''. If needed, set the runtime configuration [[''home-zettel|00001004020000#home-zettel]] to the value of the identifier of this zettel. *# At the beginning of the start zettel, add the following [[Zettelmarkup|00001007000000]] text in a separate paragraph: ``{{{20220803182600}}}`` (you have to adapt to the actual value of the zettel identifier for your non-public home zettel). * **Discussion:** As stated in the description for a [[transclusion|00001007031100]], a transclusion will be ignored, if the transcluded zettel is not visible to the current user. In effect, the transclusion statement (above paragraph that contained ''{{{...}}}'') is ignored when rendering the zettel. === Role-specific Layout of Zettel in Web User Interface (WebUI) [!role-css] * **Problem:** You want to add some CSS when displaying zettel of a specific [[role|00001006020000#role]]. For example, you might want to add a yellow background color for all [[configuration|00001006020100#configuration]] zettel. Or you want a multi-column layout. * **Solution:** If you enable [[''expert-mode''|00001004020000#expert-mode]], you will have access to a zettel called ""[[Zettelstore Role to CSS Map|00000000029000]]"" (its identifier is ''00000000029000''). This zettel maps a role name to a zettel that must contain the role-specific CSS code. First, create a zettel containing the needed CSS: give it any title, its role is preferably ""configuration"" (but this is not a must). Set its [[''syntax''|00001006020000#syntax]]Â must be set to ""[[css|00001008000000#css]]"". The content must contain the role-specific CSS code, for example ``body {background-color: #FFFFD0}``for a background in a light yellow color. Let's assume, the newly created CSS zettel got the identifier ''20220825200100''. Now, you have to connect this zettel to the zettel called ""Zettelstore Role CSS Map"". Since you have enabled ''expert-mode'', you are allowed to modify it. Add the following metadata ''css-configuration-zid: 20220825200100'' to assign the role-specific CSS code for the role ""configuration"" to the CSS zettel containing that CSS. In general, its role-assigning metadata must be like this pattern: ''css-ROLE-zid: ID'', where ''ROLE'' is the placeholder for the role, and ''ID'' for the zettel identifier containing CSS code. It is allowed to assign more than one role to a specific CSS zettel. * **Discussion:** you have to ensure that the CSS zettel is allowed to be read by the intended audience of the zettel with that given role. For example, if you made zettel with a specific role public visible, the CSS zettel must also have a [[''visibility: public''|00001010070200]] metadata. * **Extension:** if you have already established a role-specific layout for zettel, but you additionally want just one another zettel with another role to be rendered with the same CSS, you have to add metadata to the one zettel: ''css-role: ROLE'', where ''ROLE'' is the placeholder for the role that already is assigned to a specific CSS-based layout. === Zettel synchronization with iCloud (Apple) * **Problem:** You use Zettelstore on various macOS computers and you want to use the sameset of zettel across all computers. * **Solution:** Place your zettel in an iCloud folder. To configure Zettelstore to use the folder, you must specify its location within you directory structure as [[''box-uri-X''|00001004010000#box-uri-x]] (replace ''X'' with an appropriate number). Your iCloud folder is typically placed in the folder ''~/Library/Mobile Documents/com~apple~CloudDocs''. |
︙ | ︙ |
Changes to docs/manual/00001018000000.zettel.
1 2 3 4 5 | id: 00001018000000 title: Troubleshooting role: manual tags: #manual #zettelstore syntax: zmk | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001018000000 title: Troubleshooting role: manual tags: #manual #zettelstore syntax: zmk created: 00010101000000 modified: 20221020132617 This page lists some problems and their solutions that may occur when using your Zettelstore. === Installation * **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer. Therefore, it will not start Zettelstore. ** **Solution:** Press the ''Ctrl'' key while opening the context menu of the Zettelstore executable with a right-click. |
︙ | ︙ | |||
24 25 26 27 28 29 30 | The difference between these two is the missing encryption of user name / password and for the answer of the Zettelstore if you use the ''http://'' schema. To be secure by default, the Zettelstore will not work in an insecure environment. ** **Solution 1:** If you are sure that your communication medium is safe, even if you use the ''http:/\/'' schema (for example, you are running the Zettelstore on the same computer you are working on, or if the Zettelstore is running on a computer in your protected local network), then you could add the entry ''insecure-cookie: true'' in you [[startup configuration|00001004010000#insecure-cookie]] file. ** **Solution 2:** If you are not sure about the security of your communication medium (for example, if unknown persons might use your local network), then you should run an [[external server|00001010090100]] in front of your Zettelstore to enable the use of the ''https://'' schema. === Working with Zettel Files * **Problem:** When you delete a zettel file by removing it from the ""disk"", e.g. by dropping it into the trash folder, by dragging into another folder, or by removing it from the command line, Zettelstore sometimes did not detect that change. | | | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | The difference between these two is the missing encryption of user name / password and for the answer of the Zettelstore if you use the ''http://'' schema. To be secure by default, the Zettelstore will not work in an insecure environment. ** **Solution 1:** If you are sure that your communication medium is safe, even if you use the ''http:/\/'' schema (for example, you are running the Zettelstore on the same computer you are working on, or if the Zettelstore is running on a computer in your protected local network), then you could add the entry ''insecure-cookie: true'' in you [[startup configuration|00001004010000#insecure-cookie]] file. ** **Solution 2:** If you are not sure about the security of your communication medium (for example, if unknown persons might use your local network), then you should run an [[external server|00001010090100]] in front of your Zettelstore to enable the use of the ''https://'' schema. === Working with Zettel Files * **Problem:** When you delete a zettel file by removing it from the ""disk"", e.g. by dropping it into the trash folder, by dragging into another folder, or by removing it from the command line, Zettelstore sometimes did not detect that change. If you access the zettel via Zettelstore, a fatal error is reported. ** **Explanation:** Sometimes, the operating system does not tell Zettelstore about the removed zettel. This occurs mostly under MacOS. ** **Solution 1:** If you are running Zettelstore in [[""simple-mode""|00001004051100]] or if you have enabled [[''expert-mode''|00001004020000#expert-mode]], you are allowed to refresh the internal data by selecting ""Refresh"" in the Web User Interface (you find it in the menu ""Lists""). ** **Solution 2:** There is an [[API|00001012080500]] call to make Zettelstore aware of this change. ** **Solution 3:** If you have an enabled [[Administrator Console|00001004100000]] you can use the command [[''refresh''|00001004101000#refresh]] to make your changes visible. ** **Solution 4:** You configure the zettel box as [[""simple""|00001004011400]]. |
︙ | ︙ |
Deleted docs/manual/00001019990010.zettel.
|
| < < < < < < < < |
Deleted docs/manual/20231128184200.zettel.
|
| < < < < < < < |
Changes to docs/readmezip.txt.
︙ | ︙ | |||
13 14 15 16 17 18 19 | https://zettelstore.de/manual/. It is a live example of the zettelstore software, running in read-only mode. You can download it separately and it is possible to make it directly available for your local Zettelstore. The software, including the manual, is licensed under the European Union Public License 1.2 (or later). See the separate file LICENSE.txt. | | > | 13 14 15 16 17 18 19 20 21 | https://zettelstore.de/manual/. It is a live example of the zettelstore software, running in read-only mode. You can download it separately and it is possible to make it directly available for your local Zettelstore. The software, including the manual, is licensed under the European Union Public License 1.2 (or later). See the separate file LICENSE.txt. To get in contact with the developer, send an email to ds@zettelstore.de or follow Zettelstore on Twitter: https://twitter.com/zettelstore. |
Added domain/content.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package domain provides domain specific types, constants, and functions. package domain import ( "bytes" "encoding/base64" "errors" "io" "unicode" "unicode/utf8" "zettelstore.de/z/input" ) // Content is just the content of a zettel. type Content struct { data []byte isBinary bool } // NewContent creates a new content from a string. func NewContent(data []byte) Content { return Content{data: data, isBinary: calcIsBinary(data)} } // Length returns the number of bytes stored. func (zc *Content) Length() int { return len(zc.data) } // Equal compares two content values. func (zc *Content) Equal(o *Content) bool { if zc == nil { return o == nil } if zc.isBinary != o.isBinary { return false } return bytes.Equal(zc.data, o.data) } // Set content to new string value. func (zc *Content) Set(data []byte) { zc.data = data zc.isBinary = calcIsBinary(data) } // Write it to a Writer func (zc *Content) Write(w io.Writer) (int, error) { return w.Write(zc.data) } // AsString returns the content itself is a string. func (zc *Content) AsString() string { return string(zc.data) } // AsBytes returns the content itself is a byte slice. func (zc *Content) AsBytes() []byte { return zc.data } // IsBinary returns true if the content contains non-unicode values or is, // interpreted a text, with a high probability binary content. func (zc *Content) IsBinary() bool { return zc.isBinary } // TrimSpace remove some space character in content, if it is not binary content. func (zc *Content) TrimSpace() { if zc.isBinary { return } inp := input.NewInput(zc.data) pos := inp.Pos for inp.Ch != input.EOS { if input.IsEOLEOS(inp.Ch) { inp.Next() pos = inp.Pos continue } if !input.IsSpace(inp.Ch) { break } inp.Next() } zc.data = bytes.TrimRightFunc(inp.Src[pos:], unicode.IsSpace) } // Encode content for future transmission. func (zc *Content) Encode() (data, encoding string) { if !zc.isBinary { return zc.AsString(), "" } return base64.StdEncoding.EncodeToString(zc.data), "base64" } // SetDecoded content to the decoded value of the given string. func (zc *Content) SetDecoded(data, encoding string) error { switch encoding { case "": zc.data = []byte(data) case "base64": decoded, err := base64.StdEncoding.DecodeString(data) if err != nil { return err } zc.data = decoded default: return errors.New("unknown encoding " + encoding) } zc.isBinary = calcIsBinary(zc.data) return nil } func calcIsBinary(data []byte) bool { if !utf8.Valid(data) { return true } l := len(data) for i := 0; i < l; i++ { if data[i] == 0 { return true } } return false } |
Added domain/content_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package domain_test import ( "testing" "zettelstore.de/z/domain" ) func TestContentIsBinary(t *testing.T) { t.Parallel() td := []struct { s string exp bool }{ {"abc", false}, {"äöü", false}, {"", false}, {string([]byte{0}), true}, } for i, tc := range td { content := domain.NewContent([]byte(tc.s)) got := content.IsBinary() if got != tc.exp { t.Errorf("TC=%d: expected %v, got %v", i, tc.exp, got) } } } func TestTrimSpace(t *testing.T) { t.Parallel() testcases := []struct { in, exp string }{ {"", ""}, {" ", ""}, {"abc", "abc"}, {" abc", " abc"}, {"abc ", "abc"}, {"abc \n", "abc"}, {"abc\n ", "abc"}, {"\nabc", "abc"}, {" \nabc", "abc"}, {" \n abc", " abc"}, {" \n\n abc", " abc"}, {" \n \n abc", " abc"}, {" \n \n abc \n \n ", " abc"}, } for _, tc := range testcases { c := domain.NewContent([]byte(tc.in)) c.TrimSpace() got := c.AsString() if got != tc.exp { t.Errorf("TrimSpace(%q) should be %q, but got %q", tc.in, tc.exp, got) } } } |
Added domain/id/id.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package id provides domain specific types, constants, and functions about // zettel identifier. package id import ( "strconv" "time" "zettelstore.de/c/api" ) // Zid is the internal identifier of a zettel. Typically, it is a // time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer. // A zettelstore implementation should try to set the last two digits to zero, // e.g. the seconds should be zero, type Zid uint64 // Some important ZettelIDs. const ( Invalid = Zid(0) // Invalid is a Zid that will never be valid ) // ZettelIDs that are used as Zid more than once. // // Note: if you change some values, ensure that you also change them in the // Constant box. They are mentioned there literally, because these // constants are not available there. var ( ConfigurationZid = MustParse(api.ZidConfiguration) BaseTemplateZid = MustParse(api.ZidBaseTemplate) LoginTemplateZid = MustParse(api.ZidLoginTemplate) ListTemplateZid = MustParse(api.ZidListTemplate) ZettelTemplateZid = MustParse(api.ZidZettelTemplate) InfoTemplateZid = MustParse(api.ZidInfoTemplate) FormTemplateZid = MustParse(api.ZidFormTemplate) RenameTemplateZid = MustParse(api.ZidRenameTemplate) DeleteTemplateZid = MustParse(api.ZidDeleteTemplate) ContextTemplateZid = MustParse(api.ZidContextTemplate) ErrorTemplateZid = MustParse(api.ZidErrorTemplate) RoleCSSMapZid = MustParse(api.ZidRoleCSSMap) EmojiZid = MustParse(api.ZidEmoji) TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate) DefaultHomeZid = MustParse(api.ZidDefaultHome) ) const maxZid = 99999999999999 // ParseUint interprets a string as a possible zettel identifier // and returns its integer value. func ParseUint(s string) (uint64, error) { res, err := strconv.ParseUint(s, 10, 47) if err != nil { return 0, err } if res == 0 || res > maxZid { return res, strconv.ErrRange } return res, nil } // Parse interprets a string as a zettel identification and // returns its value. func Parse(s string) (Zid, error) { if len(s) != 14 { return Invalid, strconv.ErrSyntax } res, err := ParseUint(s) if err != nil { return Invalid, err } return Zid(res), nil } // MustParse tries to interpret a string as a zettel identifier and returns // its value or panics otherwise. func MustParse(s api.ZettelID) Zid { zid, err := Parse(string(s)) if err == nil { return zid } panic(err) } // String converts the zettel identification to a string of 14 digits. // Only defined for valid ids. func (zid Zid) String() string { var result [14]byte zid.toByteArray(&result) return string(result[:]) } // Bytes converts the zettel identification to a byte slice of 14 digits. // Only defined for valid ids. func (zid Zid) Bytes() []byte { var result [14]byte zid.toByteArray(&result) return result[:] } // toByteArray converts the Zid into a fixed byte array, usable for printing. // // Based on idea by Daniel Lemire: "Converting integers to fix-digit representations quickly" // https://lemire.me/blog/2021/11/18/converting-integers-to-fix-digit-representations-quickly/ func (zid Zid) toByteArray(result *[14]byte) { date := uint64(zid) / 1000000 fullyear := date / 10000 century := fullyear / 100 year := fullyear % 100 monthday := date % 10000 month := monthday / 100 day := monthday % 100 time := uint64(zid) % 1000000 hmtime := time / 100 second := time % 100 hour := hmtime / 100 minute := hmtime % 100 result[0] = byte(century/10) + '0' result[1] = byte(century%10) + '0' result[2] = byte(year/10) + '0' result[3] = byte(year%10) + '0' result[4] = byte(month/10) + '0' result[5] = byte(month%10) + '0' result[6] = byte(day/10) + '0' result[7] = byte(day%10) + '0' result[8] = byte(hour/10) + '0' result[9] = byte(hour%10) + '0' result[10] = byte(minute/10) + '0' result[11] = byte(minute%10) + '0' result[12] = byte(second/10) + '0' result[13] = byte(second%10) + '0' } // IsValid determines if zettel id is a valid one, e.g. consists of max. 14 digits. func (zid Zid) IsValid() bool { return 0 < zid && zid <= maxZid } // ZidLayout to transform a date into a Zid and into other internal dates. const ZidLayout = "20060102150405" // New returns a new zettel id based on the current time. func New(withSeconds bool) Zid { now := time.Now().Local() var s string if withSeconds { s = now.Format(ZidLayout) } else { s = now.Format("20060102150400") } res, err := Parse(s) if err != nil { panic(err) } return res } |
Added 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | //----------------------------------------------------------------------------- // Copyright (c) 2020-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) { t.Parallel() validIDs := []string{ "00000000000001", "00000000000020", "00000000000300", "00000000004000", "00000000050000", "00000000600000", "00000007000000", "00000080000000", "00000900000000", "00001000000000", "00020000000000", "00300000000000", "04000000000000", "50000000000000", "99999999999999", "00001007030200", "20200310195100", } for i, sid := range validIDs { zid, err := id.Parse(sid) if err != nil { t.Errorf("i=%d: sid=%q is not valid, but should be. err=%v", i, sid, err) } s := zid.String() if s != sid { t.Errorf( "i=%d: zid=%v does not format to %q, but to %q", i, zid, sid, s) } } invalidIDs := []string{ "", "0", "a", "00000000000000", "0000000000000a", "000000000000000", "20200310T195100", } for i, sid := range invalidIDs { if zid, err := id.Parse(sid); err == nil { t.Errorf("i=%d: sid=%q is valid (zid=%s), but should not be", i, sid, zid) } } } var sResult string // to disable compiler optimization in loop below func BenchmarkString(b *testing.B) { var s string for n := 0; n < b.N; n++ { s = id.Zid(12345678901200).String() } sResult = s } var bResult []byte // to disable compiler optimization in loop below func BenchmarkBytes(b *testing.B) { var bs []byte for n := 0; n < b.N; n++ { bs = id.Zid(12345678901200).Bytes() } bResult = bs } |
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package id // Set is a set of zettel identifier type Set map[Zid]struct{} // NewSet returns a new set of identifier with the given initial values. func NewSet(zids ...Zid) Set { l := len(zids) if l < 8 { l = 8 } result := make(Set, l) result.AddSlice(zids) return result } // NewSetCap returns a new set of identifier with the given capacity and initial values. func NewSetCap(c int, zids ...Zid) Set { l := len(zids) if c < l { c = l } if c < 8 { c = 8 } result := make(Set, c) result.AddSlice(zids) return result } // Zid adds a Zid to the set. func (s Set) Zid(zid Zid) Set { if s == nil { return NewSet(zid) } s[zid] = struct{}{} return s } // Contains return true if the set is nil or if the set contains the given Zettel identifier. func (s Set) Contains(zid Zid) bool { if s != nil { _, found := s[zid] return found } return true } // Add all member from the other set. func (s Set) Add(other Set) Set { if s == nil { return other } for zid := range other { s[zid] = struct{}{} } return s } // AddSlice adds all identifier of the given slice to the set. func (s Set) AddSlice(sl Slice) { for _, zid := range sl { s[zid] = struct{}{} } } // Sorted returns the set as a sorted slice of zettel identifier. func (s Set) Sorted() Slice { if l := len(s); l > 0 { result := make(Slice, 0, l) for zid := range s { result = append(result, zid) } result.Sort() return result } return nil } // IntersectOrSet removes all zettel identifier that are not in the other set. // Both sets can be modified by this method. One of them is the set returned. // It contains the intersection of both, if s is not nil. // // If s == nil, then the other set is always returned. func (s Set) IntersectOrSet(other Set) Set { if s == nil { return other } if len(s) > len(other) { s, other = other, s } for zid := range s { _, otherOk := other[zid] if !otherOk { delete(s, zid) } } return s } // Remove all zettel identifier from 's' that are in the set 'other'. func (s Set) Remove(other Set) { if s == nil || other == nil { return } for zid := range other { delete(s, zid) } } |
Added domain/id/set_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package id_test import ( "testing" "zettelstore.de/z/domain/id" ) func TestSetContains(t *testing.T) { t.Parallel() testcases := []struct { s id.Set zid id.Zid exp bool }{ {nil, id.Invalid, true}, {nil, 14, true}, {id.NewSet(), id.Invalid, false}, {id.NewSet(), 1, false}, {id.NewSet(), id.Invalid, false}, {id.NewSet(1), 1, true}, } for i, tc := range testcases { got := tc.s.Contains(tc.zid) if got != tc.exp { t.Errorf("%d: %v.Contains(%v) == %v, but got %v", i, tc.s, tc.zid, tc.exp, got) } } } func TestSetAdd(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, {nil, id.NewSet(1), id.Slice{1}}, {id.NewSet(1), nil, id.Slice{1}}, {id.NewSet(1), id.NewSet(), id.Slice{1}}, {id.NewSet(1), id.NewSet(2), id.Slice{1, 2}}, {id.NewSet(1), id.NewSet(1), id.Slice{1}}, } for i, tc := range testcases { sl1 := tc.s1.Sorted() sl2 := tc.s2.Sorted() got := tc.s1.Add(tc.s2).Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Add(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) } } } func TestSetSorted(t *testing.T) { t.Parallel() testcases := []struct { set id.Set exp id.Slice }{ {nil, nil}, {id.NewSet(), nil}, {id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}}, } for i, tc := range testcases { got := tc.set.Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Sorted() should be %v, but got %v", i, tc.set, tc.exp, got) } } } func TestSetIntersectOrSet(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {nil, id.NewSet(), nil}, {id.NewSet(), id.NewSet(), nil}, {id.NewSet(1), nil, nil}, {nil, id.NewSet(1), id.Slice{1}}, {id.NewSet(1), id.NewSet(), nil}, {id.NewSet(), id.NewSet(1), nil}, {id.NewSet(1), id.NewSet(2), nil}, {id.NewSet(2), id.NewSet(1), nil}, {id.NewSet(1), id.NewSet(1), id.Slice{1}}, } for i, tc := range testcases { sl1 := tc.s1.Sorted() sl2 := tc.s2.Sorted() got := tc.s1.IntersectOrSet(tc.s2).Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.IntersectOrSet(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) } } } func TestSetRemove(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Set exp id.Slice }{ {nil, nil, nil}, {id.NewSet(), nil, nil}, {id.NewSet(), id.NewSet(), nil}, {id.NewSet(1), nil, id.Slice{1}}, {id.NewSet(1), id.NewSet(), id.Slice{1}}, {id.NewSet(1), id.NewSet(2), id.Slice{1}}, {id.NewSet(1), id.NewSet(1), id.Slice{}}, } for i, tc := range testcases { sl1 := tc.s1.Sorted() sl2 := tc.s2.Sorted() newS1 := id.NewSet(sl1...) newS1.Remove(tc.s2) got := newS1.Sorted() if !got.Equal(tc.exp) { t.Errorf("%d: %v.Remove(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) } } } // func BenchmarkSet(b *testing.B) { // s := id.Set{} // for i := 0; i < b.N; i++ { // s[id.Zid(i)] = true // } // } func BenchmarkSet(b *testing.B) { s := id.Set{} for i := 0; i < b.N; i++ { s[id.Zid(i)] = struct{}{} } } |
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 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 id provides domain specific types, constants, and functions about // zettel identifier. package id import ( "bytes" "sort" ) // 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 } // Equal reports whether zs and other are the same length and contain the samle zettel // identifier. A nil argument is equivalent to an empty slice. func (zs Slice) Equal(other Slice) bool { if len(zs) != len(other) { return false } if len(zs) == 0 { return true } for i, e := range zs { if e != other[i] { return false } } return true } func (zs Slice) String() string { if len(zs) == 0 { return "" } var buf bytes.Buffer for i, zid := range zs { if i > 0 { buf.WriteByte(' ') } buf.WriteString(zid.String()) } return buf.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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed 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 TestSliceSort(t *testing.T) { t.Parallel() zs := id.Slice{9, 4, 6, 1, 7} zs.Sort() exp := id.Slice{1, 4, 6, 7, 9} if !zs.Equal(exp) { t.Errorf("Slice.Sort did not work. Expected %v, got %v", exp, zs) } } func TestCopy(t *testing.T) { t.Parallel() var orig id.Slice got := orig.Copy() if got != nil { t.Errorf("Nil copy resulted in %v", got) } orig = id.Slice{9, 4, 6, 1, 7} got = orig.Copy() if !orig.Equal(got) { t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got) } } func TestSliceEqual(t *testing.T) { t.Parallel() testcases := []struct { s1, s2 id.Slice exp bool }{ {nil, nil, true}, {nil, id.Slice{}, true}, {nil, id.Slice{1}, false}, {id.Slice{1}, id.Slice{1}, true}, {id.Slice{1}, id.Slice{2}, false}, {id.Slice{1, 2}, id.Slice{2, 1}, false}, {id.Slice{1, 2}, id.Slice{1, 2}, true}, } for i, tc := range testcases { got := tc.s1.Equal(tc.s2) if got != tc.exp { t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s1, tc.s2, tc.exp, got) } got = tc.s2.Equal(tc.s1) if got != tc.exp { t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got) } } } func TestSliceString(t *testing.T) { t.Parallel() testcases := []struct { in id.Slice exp string }{ {nil, ""}, {id.Slice{}, ""}, {id.Slice{1}, "00000000000001"}, {id.Slice{1, 2}, "00000000000001 00000000000002"}, } for i, tc := range testcases { got := tc.in.String() if got != tc.exp { t.Errorf("%d/%v: expected %q, but got %q", i, tc.in, tc.exp, got) } } } |
Added domain/meta/collection.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 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package meta import "sort" // Arrangement stores metadata within its categories. // Typecally a category might be a tag name, a role name, a syntax value. type Arrangement map[string][]*Meta // CreateArrangement by inspecting a given key and use the found // value as a category. func CreateArrangement(metaList []*Meta, key string) Arrangement { if len(metaList) == 0 { return nil } descr := Type(key) if descr == nil { return nil } a := make(Arrangement) if descr.IsSet { for _, m := range metaList { if vals, ok := m.GetList(key); ok { for _, val := range vals { a[val] = append(a[val], m) } } } } else { for _, m := range metaList { if val, ok := m.Get(key); ok && val != "" { a[val] = append(a[val], m) } } } return a } // Counted returns the list of categories, together with the number of // metadata for each category. func (a Arrangement) Counted() CountedCategories { if len(a) == 0 { return nil } result := make(CountedCategories, 0, len(a)) for cat, metas := range a { result = append(result, CountedCategory{Name: cat, Count: len(metas)}) } return result } // CountedCategory contains of a name and the number how much this name occured // somewhere. type CountedCategory struct { Name string Count int } // CountedCategories is the list of CountedCategories. // Every name must occur only once. type CountedCategories []CountedCategory // SortByName sorts the list by the name attribute. // Since each name must occur only once, two CountedCategories cannot have // the same name. func (ccs CountedCategories) SortByName() { sort.Slice(ccs, func(i, j int) bool { return ccs[i].Name < ccs[j].Name }) } // SortByCount sorts the list by the count attribute, descending. // If two counts are equal, elements are sorted by name. func (ccs CountedCategories) SortByCount() { sort.Slice(ccs, func(i, j int) bool { iCount, jCount := ccs[i].Count, ccs[j].Count if iCount > jCount { return true } if iCount == jCount { return ccs[i].Name < ccs[j].Name } return false }) } // Categories returns just the category names. func (ccs CountedCategories) Categories() []string { result := make([]string, len(ccs)) for i, cc := range ccs { result[i] = cc.Name } return result } |
Added domain/meta/meta.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import ( "bytes" "regexp" "sort" "strings" "unicode" "unicode/utf8" "zettelstore.de/c/api" "zettelstore.de/c/maps" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" "zettelstore.de/z/strfun" ) type keyUsage int const ( _ keyUsage = iota usageUser // Key will be manipulated by the user usageComputed // Key is computed by zettelstore usageProperty // Key is computed and not stored by zettelstore ) // DescriptionKey formally describes each supported metadata key. type DescriptionKey struct { Name string Type *DescriptionType usage keyUsage Inverse string } // IsComputed returns true, if metadata is computed and not set by the user. func (kd *DescriptionKey) IsComputed() bool { return kd.usage >= usageComputed } // IsProperty returns true, if metadata is a computed property. func (kd *DescriptionKey) IsProperty() bool { return kd.usage >= usageProperty } // IsStoredComputed retruns true, if metadata is computed, but also stored. func (kd *DescriptionKey) IsStoredComputed() bool { return kd.usage == usageComputed } var registeredKeys = make(map[string]*DescriptionKey) func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) { if _, ok := registeredKeys[name]; ok { panic("Key '" + name + "' already defined") } if inverse != "" { if t != TypeID && t != TypeIDSet { panic("Inversable key '" + name + "' is not identifier type, but " + t.String()) } inv, ok := registeredKeys[inverse] if !ok { panic("Inverse Key '" + inverse + "' not found") } if !inv.IsComputed() { panic("Inverse Key '" + inverse + "' is not computed.") } if inv.Type != TypeIDSet { panic("Inverse Key '" + inverse + "' is not an identifier set, but " + inv.Type.String()) } } registeredKeys[name] = &DescriptionKey{name, t, usage, inverse} } // IsComputed returns true, if key denotes a computed metadata key. func IsComputed(name string) bool { if kd, ok := registeredKeys[name]; ok { return kd.IsComputed() } return false } // IsProperty returns true, if key denotes a property metadata value. func IsProperty(name string) bool { if kd, ok := registeredKeys[name]; ok { return kd.IsProperty() } return false } // IsStoredComputed returns true, if key denotes a computed metadata key that is stored. func IsStoredComputed(name string) bool { if kd, ok := registeredKeys[name]; ok { return kd.IsStoredComputed() } return false } // Inverse returns the name of the inverse key. func Inverse(name string) string { if kd, ok := registeredKeys[name]; ok { return kd.Inverse } return "" } // GetDescription returns the key description object of the given key name. func GetDescription(name string) DescriptionKey { if d, ok := registeredKeys[name]; ok { return *d } return DescriptionKey{Type: Type(name)} } // GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name. func GetSortedKeyDescriptions() []*DescriptionKey { keys := maps.Keys(registeredKeys) result := make([]*DescriptionKey, 0, len(keys)) for _, n := range keys { result = append(result, registeredKeys[n]) } return result } // Supported keys. func init() { registerKey(api.KeyID, TypeID, usageComputed, "") registerKey(api.KeyTitle, TypeZettelmarkup, usageUser, "") registerKey(api.KeyRole, TypeWord, usageUser, "") registerKey(api.KeyTags, TypeTagSet, usageUser, "") registerKey(api.KeySyntax, TypeWord, usageUser, "") // Properties that are inverse keys registerKey(api.KeyFolge, TypeIDSet, usageProperty, "") registerKey(api.KeySuccessors, TypeIDSet, usageProperty, "") registerKey(api.KeyAuthor, TypeString, usageUser, "") registerKey(api.KeyBack, TypeIDSet, usageProperty, "") registerKey(api.KeyBackward, TypeIDSet, usageProperty, "") registerKey(api.KeyBoxNumber, TypeNumber, usageProperty, "") registerKey(api.KeyCopyright, TypeString, usageUser, "") registerKey(api.KeyCreated, TypeTimestamp, usageComputed, "") registerKey(api.KeyCredential, TypeCredential, usageUser, "") registerKey(api.KeyDead, TypeIDSet, usageProperty, "") registerKey(api.KeyForward, TypeIDSet, usageProperty, "") registerKey(api.KeyLang, TypeWord, usageUser, "") registerKey(api.KeyLicense, TypeEmpty, usageUser, "") registerKey(api.KeyModified, TypeTimestamp, usageComputed, "") registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge) registerKey(api.KeyPredecessor, TypeID, usageUser, api.KeySuccessors) registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "") registerKey(api.KeyReadOnly, TypeWord, usageUser, "") registerKey(api.KeySummary, TypeZettelmarkup, usageUser, "") registerKey(api.KeyURL, TypeURL, usageUser, "") registerKey(api.KeyUselessFiles, TypeString, usageProperty, "") registerKey(api.KeyUserID, TypeWord, usageUser, "") registerKey(api.KeyUserRole, TypeWord, usageUser, "") registerKey(api.KeyVisibility, TypeWord, usageUser, "") } // NewPrefix is the prefix for metadata key in template zettel for creating new zettel. const NewPrefix = "new-" // Meta contains all meta-data of a zettel. type Meta struct { Zid id.Zid pairs map[string]string YamlSep bool } // New creates a new chunk for storing metadata. func New(zid id.Zid) *Meta { return &Meta{Zid: zid, pairs: make(map[string]string, 5)} } // NewWithData creates metadata object with given data. func NewWithData(zid id.Zid, data map[string]string) *Meta { pairs := make(map[string]string, len(data)) for k, v := range data { pairs[k] = v } return &Meta{Zid: zid, pairs: pairs} } // Length returns the number of bytes stored for the metadata. func (m *Meta) Length() int { if m == nil { return 0 } result := 6 // storage needed for Zid for k, v := range m.pairs { result += len(k) + len(v) + 1 // 1 because separator } return result } // Clone returns a new copy of the metadata. func (m *Meta) Clone() *Meta { return &Meta{ Zid: m.Zid, pairs: m.Map(), YamlSep: m.YamlSep, } } // Map returns a copy of the meta data as a string map. func (m *Meta) Map() map[string]string { pairs := make(map[string]string, len(m.pairs)) for k, v := range m.pairs { pairs[k] = v } return pairs } var reKey = regexp.MustCompile("^[0-9a-z][-0-9a-z]{0,254}$") // KeyIsValid returns true, the the key is a valid string. func KeyIsValid(key string) bool { return reKey.MatchString(key) } // Pair is one key-value-pair of a Zettel meta. type Pair struct { Key string Value string } var firstKeys = []string{api.KeyTitle, api.KeyRole, api.KeyTags, api.KeySyntax} var firstKeySet strfun.Set func init() { firstKeySet = strfun.NewSet(firstKeys...) } // Set stores the given string value under the given key. func (m *Meta) Set(key, value string) { if key != api.KeyID { m.pairs[key] = trimValue(value) } } // SetNonEmpty stores the given value under the given key, if the value is non-empty. // An empty value will delete the previous association. func (m *Meta) SetNonEmpty(key, value string) { if value == "" { delete(m.pairs, key) } else if key != api.KeyID { m.pairs[key] = trimValue(value) } } func trimValue(value string) string { return strings.TrimFunc(value, input.IsSpace) } // Get retrieves the string value of a given key. The bool value signals, // whether there was a value stored or not. func (m *Meta) Get(key string) (string, bool) { if key == api.KeyID { return m.Zid.String(), true } value, ok := m.pairs[key] return value, ok } // GetDefault retrieves the string value of the given key. If no value was // stored, the given default value is returned. func (m *Meta) GetDefault(key, def string) string { if value, ok := m.Get(key); ok { return value } return def } // GetTitle returns the title of the metadata. It is the only key that has a // defined default value: the string representation of the zettel identifier. func (m *Meta) GetTitle() string { if title, found := m.Get(api.KeyTitle); found { return title } return m.Zid.String() } // Pairs returns not computed key/values pairs stored, in a specific order. // First come the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey, // MetaContextKey. Then all other pairs are append to the list, ordered by key. func (m *Meta) Pairs() []Pair { return m.doPairs(m.getFirstKeys(), notComputedKey) } // ComputedPairs returns all key/values pairs stored, in a specific order. First come // the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey, // MetaContextKey. Then all other pairs are append to the list, ordered by key. func (m *Meta) ComputedPairs() []Pair { return m.doPairs(m.getFirstKeys(), anyKey) } // PairsRest returns not computed key/values pairs stored, except the values with // predefined keys. The pairs are ordered by key. func (m *Meta) PairsRest() []Pair { result := make([]Pair, 0, len(m.pairs)) return m.doPairs(result, notComputedKey) } // ComputedPairsRest returns all key/values pairs stored, except the values with // predefined keys. The pairs are ordered by key. func (m *Meta) ComputedPairsRest() []Pair { result := make([]Pair, 0, len(m.pairs)) return m.doPairs(result, anyKey) } func notComputedKey(key string) bool { return !IsComputed(key) } func anyKey(string) bool { return true } func (m *Meta) doPairs(firstKeys []Pair, addKeyPred func(string) bool) []Pair { keys := m.getKeysRest(addKeyPred) for _, k := range keys { firstKeys = append(firstKeys, Pair{k, m.pairs[k]}) } return firstKeys } func (m *Meta) getFirstKeys() []Pair { result := make([]Pair, 0, len(m.pairs)) for _, key := range firstKeys { if value, ok := m.pairs[key]; ok { result = append(result, Pair{key, value}) } } return result } func (m *Meta) getKeysRest(addKeyPred func(string) bool) []string { keys := make([]string, 0, len(m.pairs)) for k := range m.pairs { if !firstKeySet.Has(k) && addKeyPred(k) { keys = append(keys, k) } } sort.Strings(keys) return keys } // Delete removes a key from the data. func (m *Meta) Delete(key string) { if key != api.KeyID { delete(m.pairs, key) } } // Equal compares to metas for equality. func (m *Meta) Equal(o *Meta, allowComputed bool) bool { if m == nil && o == nil { return true } if m == nil || o == nil || m.Zid != o.Zid { return false } tested := make(strfun.Set, len(m.pairs)) for k, v := range m.pairs { tested.Set(k) if !equalValue(k, v, o, allowComputed) { return false } } for k, v := range o.pairs { if !tested.Has(k) && !equalValue(k, v, m, allowComputed) { return false } } return true } func equalValue(key, val string, other *Meta, allowComputed bool) bool { if allowComputed || !IsComputed(key) { if valO, ok := other.pairs[key]; !ok || val != valO { return false } } return true } // Sanitize all metadata keys and values, so that they can be written safely into a file. func (m *Meta) Sanitize() { if m == nil { return } for k, v := range m.pairs { m.pairs[RemoveNonGraphic(k)] = RemoveNonGraphic(v) } } // RemoveNonGraphic changes the given string not to include non-graphical characters. // It is needed to sanitize meta data. func RemoveNonGraphic(s string) string { if s == "" { return "" } pos := 0 var buf bytes.Buffer for pos < len(s) { nextPos := strings.IndexFunc(s[pos:], func(r rune) bool { return !unicode.IsGraphic(r) }) if nextPos < 0 { break } buf.WriteString(s[pos:nextPos]) buf.WriteByte(' ') _, size := utf8.DecodeRuneInString(s[nextPos:]) pos = nextPos + size } if pos == 0 { return strings.TrimSpace(s) } buf.WriteString(s[pos:]) return strings.TrimSpace(buf.String()) } |
Added domain/meta/meta_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import ( "strings" "testing" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" ) const testID = id.Zid(98765432101234) func TestKeyIsValid(t *testing.T) { t.Parallel() validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)} for _, key := range validKeys { if !KeyIsValid(key) { t.Errorf("Key %q wrongly identified as invalid key", key) } } invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)} for _, key := range invalidKeys { if KeyIsValid(key) { t.Errorf("Key %q wrongly identified as valid key", key) } } } func TestTitleHeader(t *testing.T) { t.Parallel() m := New(testID) if got, ok := m.Get(api.KeyTitle); ok && got != "" { t.Errorf("Title is not empty, but %q", got) } addToMeta(m, api.KeyTitle, " ") if got, ok := m.Get(api.KeyTitle); ok && got != "" { t.Errorf("Title is not empty, but %q", got) } const st = "A simple text" addToMeta(m, api.KeyTitle, " "+st+" ") if got, ok := m.Get(api.KeyTitle); !ok || got != st { t.Errorf("Title is not %q, but %q", st, got) } addToMeta(m, api.KeyTitle, " "+st+"\t") const exp = st + " " + st if got, ok := m.Get(api.KeyTitle); !ok || got != exp { t.Errorf("Title is not %q, but %q", exp, got) } m = New(testID) const at = "A Title" addToMeta(m, api.KeyTitle, at) addToMeta(m, api.KeyTitle, " ") if got, ok := m.Get(api.KeyTitle); !ok || got != at { t.Errorf("Title is not %q, but %q", at, got) } } func checkSet(t *testing.T, exp []string, m *Meta, key string) { t.Helper() got, _ := m.GetList(key) for i, tag := range exp { if i < len(got) { if tag != got[i] { t.Errorf("Pos=%d, expected %q, got %q", i, exp[i], got[i]) } } else { t.Errorf("Expected %q, but is missing", exp[i]) } } if len(exp) < len(got) { t.Errorf("Extra tags: %q", got[len(exp):]) } } func TestTagsHeader(t *testing.T) { t.Parallel() m := New(testID) checkSet(t, []string{}, m, api.KeyTags) addToMeta(m, api.KeyTags, "") checkSet(t, []string{}, m, api.KeyTags) addToMeta(m, api.KeyTags, " #t1 #t2 #t3 #t4 ") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, api.KeyTags) addToMeta(m, api.KeyTags, "#t5") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags) addToMeta(m, api.KeyTags, "t6") checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags) } func TestSyntax(t *testing.T) { t.Parallel() m := New(testID) if got, ok := m.Get(api.KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } addToMeta(m, api.KeySyntax, " ") if got, _ := m.Get(api.KeySyntax); got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } addToMeta(m, api.KeySyntax, "MarkDown") const exp = "markdown" if got, ok := m.Get(api.KeySyntax); !ok || got != exp { t.Errorf("Syntax is not %q, but %q", exp, got) } addToMeta(m, api.KeySyntax, " ") if got, _ := m.Get(api.KeySyntax); got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } } func checkHeader(t *testing.T, exp map[string]string, gotP []Pair) { t.Helper() got := make(map[string]string, len(gotP)) for _, p := range gotP { got[p.Key] = p.Value if _, ok := exp[p.Key]; !ok { t.Errorf("Key %q is not expected, but has value %q", p.Key, p.Value) } } for k, v := range exp { if gv, ok := got[k]; !ok || v != gv { if ok { t.Errorf("Key %q is not %q, but %q", k, v, got[k]) } else { t.Errorf("Key %q missing, should have value %q", k, v) } } } } func TestDefaultHeader(t *testing.T) { t.Parallel() m := New(testID) addToMeta(m, "h1", "d1") addToMeta(m, "H2", "D2") addToMeta(m, "H1", "D1.1") exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"} checkHeader(t, exp, m.Pairs()) addToMeta(m, "", "d0") checkHeader(t, exp, m.Pairs()) addToMeta(m, "h3", "") exp["h3"] = "" checkHeader(t, exp, m.Pairs()) addToMeta(m, "h3", " ") checkHeader(t, exp, m.Pairs()) addToMeta(m, "h4", " ") exp["h4"] = "" checkHeader(t, exp, m.Pairs()) } func TestDelete(t *testing.T) { t.Parallel() m := New(testID) m.Set("key", "val") if got, ok := m.Get("key"); !ok || got != "val" { t.Errorf("Value != %q, got: %v/%q", "val", ok, got) } m.Set("key", "") if got, ok := m.Get("key"); !ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } m.Delete("key") if got, ok := m.Get("key"); ok || got != "" { t.Errorf("Value != %q, got: %v/%q", "", ok, got) } } func TestEqual(t *testing.T) { t.Parallel() testcases := []struct { pairs1, pairs2 []string allowComputed bool exp bool }{ {nil, nil, true, true}, {nil, nil, false, true}, {[]string{"a", "a"}, nil, false, false}, {[]string{"a", "a"}, nil, true, false}, {[]string{api.KeyFolge, "0"}, nil, true, false}, {[]string{api.KeyFolge, "0"}, nil, false, true}, {[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, true, true}, {[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, false, true}, } for i, tc := range testcases { m1 := pairs2meta(tc.pairs1) m2 := pairs2meta(tc.pairs2) got := m1.Equal(m2, tc.allowComputed) if tc.exp != got { t.Errorf("%d: %v =?= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got) } got = m2.Equal(m1, tc.allowComputed) if tc.exp != got { t.Errorf("%d: %v =!= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got) } } // Pathologic cases var m1, m2 *Meta if !m1.Equal(m2, true) { t.Error("Nil metas should be treated equal") } m1 = New(testID) if m1.Equal(m2, true) { t.Error("Empty meta should not be equal to nil") } if m2.Equal(m1, true) { t.Error("Nil meta should should not be equal to empty") } m2 = New(testID + 1) if m1.Equal(m2, true) { t.Error("Different ID should differentiate") } if m2.Equal(m1, true) { t.Error("Different ID should differentiate") } } func pairs2meta(pairs []string) *Meta { m := New(testID) for i := 0; i < len(pairs); i = i + 2 { m.Set(pairs[i], pairs[i+1]) } return m } func TestRemoveNonGraphic(t *testing.T) { testCases := []struct { inp string exp string }{ {"", ""}, {" ", ""}, {"a", "a"}, {"a ", "a"}, {"a b", "a b"}, {"\n", ""}, {"a\n", "a"}, {"a\nb", "a b"}, {"a\tb", "a b"}, } for i, tc := range testCases { got := RemoveNonGraphic(tc.inp) if tc.exp != got { t.Errorf("%q/%d: expected %q, but got %q", tc.inp, i, tc.exp, got) } } } |
Added domain/meta/parse.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import ( "strings" "zettelstore.de/c/api" "zettelstore.de/c/maps" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" "zettelstore.de/z/strfun" ) // NewFromInput parses the meta data of a zettel. func NewFromInput(zid id.Zid, inp *input.Input) *Meta { if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' { skipToEOL(inp) inp.EatEOL() } meta := New(zid) for { skipSpace(inp) switch inp.Ch { case '\r': if inp.Peek() == '\n' { inp.Next() } fallthrough case '\n': inp.Next() return meta case input.EOS: return meta case '%': skipToEOL(inp) inp.EatEOL() continue } parseHeader(meta, inp) if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' { skipToEOL(inp) inp.EatEOL() meta.YamlSep = true return meta } } } func parseHeader(m *Meta, inp *input.Input) { pos := inp.Pos for isHeader(inp.Ch) { inp.Next() } key := inp.Src[pos:inp.Pos] skipSpace(inp) if inp.Ch == ':' { inp.Next() } var val []byte for { skipSpace(inp) pos = inp.Pos skipToEOL(inp) val = append(val, inp.Src[pos:inp.Pos]...) inp.EatEOL() if !input.IsSpace(inp.Ch) { break } val = append(val, ' ') } addToMeta(m, string(key), string(val)) } func skipSpace(inp *input.Input) { for input.IsSpace(inp.Ch) { inp.Next() } } func skipToEOL(inp *input.Input) { for { switch inp.Ch { case '\n', '\r', input.EOS: return } inp.Next() } } // Return true iff rune is valid for header key. func isHeader(ch rune) bool { return ('a' <= ch && ch <= 'z') || ('0' <= ch && ch <= '9') || ch == '-' || ('A' <= ch && ch <= 'Z') } type predValidElem func(string) bool func addToSet(set strfun.Set, elems []string, useElem predValidElem) { for _, s := range elems { if len(s) > 0 && useElem(s) { set.Set(s) } } } func addSet(m *Meta, key, val string, useElem predValidElem) { newElems := strings.Fields(val) oldElems, ok := m.GetList(key) if !ok { oldElems = nil } set := make(strfun.Set, len(newElems)+len(oldElems)) addToSet(set, newElems, useElem) if len(set) == 0 { // Nothing to add. Maybe because of rejected elements. return } addToSet(set, oldElems, useElem) m.SetList(key, maps.Keys(set)) } func addData(m *Meta, k, v string) { if o, ok := m.Get(k); !ok || o == "" { m.Set(k, v) } else if v != "" { m.Set(k, o+" "+v) } } func addToMeta(m *Meta, key, val string) { v := trimValue(val) key = strings.ToLower(key) if !KeyIsValid(key) { return } switch key { case "", api.KeyID: // Empty key and 'id' key will be ignored return } switch Type(key) { case TypeTagSet: addSet(m, key, strings.ToLower(v), func(s string) bool { return s[0] == '#' && len(s) > 1 }) case TypeWord: m.Set(key, strings.ToLower(v)) case TypeWordSet: addSet(m, key, strings.ToLower(v), func(s string) bool { return true }) case TypeID: if _, err := id.Parse(v); err == nil { m.Set(key, v) } case TypeIDSet: addSet(m, key, v, func(s string) bool { _, err := id.Parse(s) return err == nil }) case TypeTimestamp: if _, ok := TimeValue(v); ok { m.Set(key, v) } default: addData(m, key, v) } } |
Added domain/meta/parse_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta_test provides tests for the domain specific type 'meta'. package meta_test import ( "strings" "testing" "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) func parseMetaStr(src string) *meta.Meta { return meta.NewFromInput(testID, input.NewInput([]byte(src))) } func TestEmpty(t *testing.T) { t.Parallel() m := parseMetaStr("") if got, ok := m.Get(api.KeySyntax); ok || got != "" { t.Errorf("Syntax is not %q, but %q", "", got) } if got, ok := m.GetList(api.KeyTags); ok || len(got) > 0 { t.Errorf("Tags are not nil, but %v", got) } } func TestTitle(t *testing.T) { t.Parallel() td := []struct{ s, e string }{ {api.KeyTitle + ": a title", "a title"}, {api.KeyTitle + ": a\n\t title", "a title"}, {api.KeyTitle + ": a\n\t title\r\n x", "a title x"}, {api.KeyTitle + " AbC", "AbC"}, {api.KeyTitle + " AbC\n ded", "AbC ded"}, {api.KeyTitle + ": o\ntitle: p", "o p"}, {api.KeyTitle + ": O\n\ntitle: P", "O"}, {api.KeyTitle + ": b\r\ntitle: c", "b c"}, {api.KeyTitle + ": B\r\n\r\ntitle: C", "B"}, {api.KeyTitle + ": r\rtitle: q", "r q"}, {api.KeyTitle + ": R\r\rtitle: Q", "R"}, } for i, tc := range td { m := parseMetaStr(tc.s) if got, ok := m.Get(api.KeyTitle); !ok || got != tc.e { t.Log(m) t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got) } } } func TestTags(t *testing.T) { t.Parallel() testcases := []struct { src string exp string }{ {"", ""}, {api.KeyTags + ":", ""}, {api.KeyTags + ": c", ""}, {api.KeyTags + ": #", ""}, {api.KeyTags + ": #c", "c"}, {api.KeyTags + ": #c #", "c"}, {api.KeyTags + ": #c #b", "b c"}, {api.KeyTags + ": #c # #", "c"}, {api.KeyTags + ": #c # #b", "b c"}, } for i, tc := range testcases { m := parseMetaStr(tc.src) tags, found := m.GetTags(api.KeyTags) if !found { if tc.exp != "" { t.Errorf("%d / %q: no %s found", i, tc.src, api.KeyTags) } continue } if tc.exp == "" && len(tags) > 0 { t.Errorf("%d / %q: expected no %s, but got %v", i, tc.src, api.KeyTags, tags) continue } got := strings.Join(tags, " ") if tc.exp != got { t.Errorf("%d / %q: expected %q, got: %q", i, tc.src, tc.exp, got) } } } func TestNewFromInput(t *testing.T) { t.Parallel() testcases := []struct { input string exp []meta.Pair }{ {"", []meta.Pair{}}, {" a:b", []meta.Pair{{"a", "b"}}}, {"%a:b", []meta.Pair{}}, {"a:b\r\n\r\nc:d", []meta.Pair{{"a", "b"}}}, {"a:b\r\n%c:d", []meta.Pair{{"a", "b"}}}, {"% a:b\r\n c:d", []meta.Pair{{"c", "d"}}}, {"---\r\na:b\r\n", []meta.Pair{{"a", "b"}}}, {"---\r\na:b\r\n--\r\nc:d", []meta.Pair{{"a", "b"}, {"c", "d"}}}, {"---\r\na:b\r\n---\r\nc:d", []meta.Pair{{"a", "b"}}}, {"---\r\na:b\r\n----\r\nc:d", []meta.Pair{{"a", "b"}}}, {"new-title:\nnew-url:", []meta.Pair{{"new-title", ""}, {"new-url", ""}}}, } for i, tc := range testcases { meta := parseMetaStr(tc.input) if got := meta.Pairs(); !equalPairs(tc.exp, got) { t.Errorf("TC=%d: expected=%v, got=%v", i, tc.exp, got) } } // Test, whether input position is correct. inp := input.NewInput([]byte("---\na:b\n---\nX")) m := meta.NewFromInput(testID, inp) exp := []meta.Pair{{"a", "b"}} if got := m.Pairs(); !equalPairs(exp, got) { t.Errorf("Expected=%v, got=%v", exp, got) } expCh := 'X' if gotCh := inp.Ch; gotCh != expCh { t.Errorf("Expected=%v, got=%v", expCh, gotCh) } } func equalPairs(one, two []meta.Pair) bool { if len(one) != len(two) { return false } for i := 0; i < len(one); i++ { if one[i].Key != two[i].Key || one[i].Value != two[i].Value { return false } } return true } func TestPrecursorIDSet(t *testing.T) { t.Parallel() var testdata = []struct { inp string exp string }{ {"", ""}, {"123", ""}, {"12345678901234", "12345678901234"}, {"123 12345678901234", "12345678901234"}, {"12345678901234 123", "12345678901234"}, {"01234567890123 123 12345678901234", "01234567890123 12345678901234"}, {"12345678901234 01234567890123", "01234567890123 12345678901234"}, } for i, tc := range testdata { m := parseMetaStr(api.KeyPrecursor + ": " + tc.inp) if got, ok := m.Get(api.KeyPrecursor); (!ok && tc.exp != "") || tc.exp != got { t.Errorf("TC=%d: expected %q, but got %q when parsing %q", i, tc.exp, got, tc.inp) } } } |
Added domain/meta/type.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import ( "strconv" "strings" "sync" "time" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" ) // DescriptionType is a description of a specific key type. type DescriptionType struct { Name string IsSet bool } // String returns the string representation of the given type func (t DescriptionType) String() string { return t.Name } var registeredTypes = make(map[string]*DescriptionType) func registerType(name string, isSet bool) *DescriptionType { if _, ok := registeredTypes[name]; ok { panic("Type '" + name + "' already registered") } t := &DescriptionType{name, isSet} registeredTypes[name] = t return t } // Supported key types. var ( TypeCredential = registerType(api.MetaCredential, false) TypeEmpty = registerType(api.MetaEmpty, false) TypeID = registerType(api.MetaID, false) TypeIDSet = registerType(api.MetaIDSet, true) TypeNumber = registerType(api.MetaNumber, false) TypeString = registerType(api.MetaString, false) TypeTagSet = registerType(api.MetaTagSet, true) TypeTimestamp = registerType(api.MetaTimestamp, false) TypeURL = registerType(api.MetaURL, false) TypeWord = registerType(api.MetaWord, false) TypeWordSet = registerType(api.MetaWordSet, true) TypeZettelmarkup = registerType(api.MetaZettelmarkup, false) ) // Type returns a type hint for the given key. If no type hint is specified, // TypeUnknown is returned. func (*Meta) Type(key string) *DescriptionType { return Type(key) } var ( cachedTypedKeys = make(map[string]*DescriptionType) mxTypedKey sync.RWMutex suffixTypes = map[string]*DescriptionType{ "-number": TypeNumber, "-role": TypeWord, "-set": TypeWordSet, "-title": TypeZettelmarkup, "-url": TypeURL, "-zettel": TypeID, "-zid": TypeID, "-zids": TypeIDSet, } ) // Type returns a type hint for the given key. If no type hint is specified, // TypeEmpty is returned. func Type(key string) *DescriptionType { if k, ok := registeredKeys[key]; ok { return k.Type } mxTypedKey.RLock() k, ok := cachedTypedKeys[key] mxTypedKey.RUnlock() if ok { return k } for suffix, t := range suffixTypes { if strings.HasSuffix(key, suffix) { mxTypedKey.Lock() defer mxTypedKey.Unlock() cachedTypedKeys[key] = t return t } } return TypeEmpty } // SetList stores the given string list value under the given key. func (m *Meta) SetList(key string, values []string) { if key != api.KeyID { for i, val := range values { values[i] = trimValue(val) } m.pairs[key] = strings.Join(values, " ") } } // SetNow stores the current timestamp under the given key. func (m *Meta) SetNow(key string) { m.Set(key, time.Now().Local().Format(id.ZidLayout)) } // BoolValue returns the value interpreted as a bool. func BoolValue(value string) bool { if len(value) > 0 { switch value[0] { case '0', 'f', 'F', 'n', 'N': return false } } return true } // GetBool returns the boolean value of the given key. func (m *Meta) GetBool(key string) bool { if value, ok := m.Get(key); ok { return BoolValue(value) } return false } // TimeValue returns the time value of the given value. func TimeValue(value string) (time.Time, bool) { if t, err := time.Parse(id.ZidLayout, value); err == nil { return t, true } return time.Time{}, false } // GetTime returns the time value of the given key. func (m *Meta) GetTime(key string) (time.Time, bool) { if value, ok := m.Get(key); ok { return TimeValue(value) } return time.Time{}, false } // ListFromValue transforms a string value into a list value. func ListFromValue(value string) []string { return strings.Fields(value) } // GetList retrieves the string list value of a given key. The bool value // signals, whether there was a value stored or not. func (m *Meta) GetList(key string) ([]string, bool) { value, ok := m.Get(key) if !ok { return nil, false } return ListFromValue(value), true } // 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) { tagsValue, ok := m.Get(key) if !ok { return nil, false } tags := ListFromValue(strings.ToLower(tagsValue)) for i, tag := range tags { tags[i] = CleanTag(tag) } return tags, len(tags) > 0 } // CleanTag removes the number character ('#') from a tag value and lowercases it. func CleanTag(tag string) string { if len(tag) > 1 && tag[0] == '#' { return tag[1:] } return tag } // 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, def int64) int64 { if value, ok := m.Get(key); ok { if num, err := strconv.ParseInt(value, 10, 64); err == nil { return num } } return def } |
Added domain/meta/type_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | //----------------------------------------------------------------------------- // 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 meta_test provides tests for the domain specific type 'meta'. package meta_test import ( "strconv" "testing" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func TestNow(t *testing.T) { t.Parallel() m := meta.New(id.Invalid) m.SetNow("key") val, ok := m.Get("key") if !ok { t.Error("Unable to get value of key") } if len(val) != 14 { t.Errorf("Value is not 14 digits long: %q", val) } if _, err := strconv.ParseInt(val, 10, 64); err != nil { t.Errorf("Unable to parse %q as an int64: %v", val, err) } if _, ok = m.GetTime("key"); !ok { t.Errorf("Unable to get time from value %q", val) } } func TestGetTime(t *testing.T) { t.Parallel() testCases := []struct { value string valid bool exp time.Time }{ {"", false, time.Time{}}, {"1", false, time.Time{}}, {"00000000000000", false, time.Time{}}, {"98765432109876", false, time.Time{}}, {"20201221111905", true, time.Date(2020, time.December, 21, 11, 19, 5, 0, time.UTC)}, } for i, tc := range testCases { got, ok := meta.TimeValue(tc.value) if ok != tc.valid { t.Errorf("%d: parsing of %q should be %v, but got %v", i, tc.value, tc.valid, ok) continue } if got != tc.exp { t.Errorf("%d: parsing of %q should return %v, but got %v", i, tc.value, tc.exp, got) } } } |
Added domain/meta/values.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package meta import ( "fmt" "zettelstore.de/c/api" ) // Visibility enumerates the variations of the 'visibility' meta key. type Visibility int // Supported values for visibility. const ( _ Visibility = iota VisibilityUnknown VisibilityPublic VisibilityCreator VisibilityLogin VisibilityOwner VisibilityExpert ) var visMap = map[string]Visibility{ api.ValueVisibilityPublic: VisibilityPublic, api.ValueVisibilityCreator: VisibilityCreator, api.ValueVisibilityLogin: VisibilityLogin, api.ValueVisibilityOwner: VisibilityOwner, api.ValueVisibilityExpert: VisibilityExpert, } var revVisMap = map[Visibility]string{} func init() { for k, v := range visMap { revVisMap[v] = k } } // GetVisibility returns the visibility value of the given string func GetVisibility(val string) Visibility { if vis, ok := visMap[val]; ok { return vis } return VisibilityUnknown } func (v Visibility) String() string { if s, ok := revVisMap[v]; ok { return s } return fmt.Sprintf("Unknown (%d)", v) } // UserRole enumerates the supported values of meta key 'user-role'. type UserRole int // Supported values for user roles. const ( _ UserRole = iota UserRoleUnknown UserRoleCreator UserRoleReader UserRoleWriter UserRoleOwner ) var urMap = map[string]UserRole{ api.ValueUserRoleCreator: UserRoleCreator, api.ValueUserRoleReader: UserRoleReader, api.ValueUserRoleWriter: UserRoleWriter, api.ValueUserRoleOwner: UserRoleOwner, } // GetUserRole role returns the user role of the given string. func GetUserRole(val string) UserRole { if ur, ok := urMap[val]; ok { return ur } return UserRoleUnknown } |
Added domain/meta/write.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import "io" // Write writes metadata to a writer, excluding computed and propery values. func (m *Meta) Write(w io.Writer) (int, error) { return m.doWrite(w, IsComputed) } // WriteComputed writes metadata to a writer, including computed values, // but excluding property values. func (m *Meta) WriteComputed(w io.Writer) (int, error) { return m.doWrite(w, IsProperty) } func (m *Meta) doWrite(w io.Writer, ignoreKeyPred func(string) bool) (length int, err error) { for _, p := range m.ComputedPairs() { key := p.Key if ignoreKeyPred(key) { continue } if err != nil { break } var l int l, err = io.WriteString(w, key) length += l if err == nil { l, err = w.Write(colonSpace) length += l } if err == nil { l, err = io.WriteString(w, p.Value) length += l } if err == nil { l, err = w.Write(newline) length += l } } return length, err } var ( colonSpace = []byte{':', ' '} newline = []byte{'\n'} ) |
Added domain/meta/write_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package meta_test provides tests for the domain specific type 'meta'. package meta_test import ( "bytes" "strings" "testing" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) const testID = id.Zid(98765432101234) func newMeta(title string, tags []string, syntax string) *meta.Meta { m := meta.New(testID) if title != "" { m.Set(api.KeyTitle, title) } if tags != nil { m.Set(api.KeyTags, strings.Join(tags, " ")) } if syntax != "" { m.Set(api.KeySyntax, syntax) } return m } func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) { t.Helper() var buf bytes.Buffer m.Write(&buf) if got := buf.String(); got != expected { t.Errorf("\nExp: %q\ngot: %q", expected, got) } } func TestWriteMeta(t *testing.T) { t.Parallel() assertWriteMeta(t, newMeta("", nil, ""), "") m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax") assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n") m = newMeta("TITLE", nil, "") m.Set("user", "zettel") m.Set("auth", "basic") assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n") } |
Added domain/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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package domain provides domain specific types, constants, and functions. package domain import "zettelstore.de/z/domain/meta" // Zettel is the main data object of a zettelstore. type Zettel struct { Meta *meta.Meta // Some additional meta-data. Content Content // The content of the zettel itself. } // Length returns the number of bytes to store the zettel (in a domain view, // not in a technical view). func (z Zettel) Length() int { return z.Meta.Length() + z.Content.Length() } // Equal compares two zettel for equality. func (z Zettel) Equal(o Zettel, allowComputed bool) bool { return z.Meta.Equal(o.Meta, allowComputed) && z.Content.Equal(&o.Content) } |
Changes to encoder/encoder.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | 1 2 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package encoder provides a generic interface to encode the abstract syntax // tree into some text form. package encoder import ( "errors" "fmt" "io" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) // Encoder is an interface that allows to encode different parts of a zettel. type Encoder interface { WriteZettel(io.Writer, *ast.ZettelNode, EvalMetaFunc) (int, error) WriteMeta(io.Writer, *meta.Meta, EvalMetaFunc) (int, error) WriteContent(io.Writer, *ast.ZettelNode) (int, error) |
︙ | ︙ | |||
44 45 46 47 48 49 50 | 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") ) // Create builds a new encoder with the given options. | | | | < < < < < > > > > > > > > | 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 | 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") ) // Create builds a new encoder with the given options. func Create(enc api.EncodingEnum) Encoder { if create, ok := registry[enc]; ok { return create() } return nil } // CreateFunc produces a new encoder. type CreateFunc func() Encoder var registry = map[api.EncodingEnum]CreateFunc{} // Register the encoder for later retrieval. func Register(enc api.EncodingEnum, create CreateFunc) { if _, ok := registry[enc]; ok { panic(fmt.Sprintf("Encoder %q already registered", enc)) } registry[enc] = create } // GetEncodings returns all registered encodings, ordered by encoding value. func GetEncodings() []api.EncodingEnum { result := make([]api.EncodingEnum, 0, len(registry)) for enc := range registry { result = append(result, enc) } return result } // GetDefaultEncoding returns the encoding that should be used as default. func GetDefaultEncoding() api.EncodingEnum { if _, ok := registry[api.EncoderZJSON]; ok { return api.EncoderZJSON } panic("No ZJSON encoding registered") } |
Changes to encoder/encoder_blob_test.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | | | 1 2 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) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package encoder_test import ( "testing" "zettelstore.de/c/api" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. ) type blobTestCase struct { descr string blob []byte |
︙ | ︙ | |||
39 40 41 42 43 44 45 | 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x7e, 0x9b, 0x55, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x62, 0x00, 0x00, 0x00, 0x06, 0x00, 0x03, 0x36, 0x37, 0x7c, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, }, expect: expectMap{ | | | | | | 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 | 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x7e, 0x9b, 0x55, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x62, 0x00, 0x00, 0x00, 0x06, 0x00, 0x03, 0x36, 0x37, 0x7c, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, }, expect: expectMap{ encoderZJSON: `[{"":"BLOB","q":"PNG","s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}]`, encoderHTML: `<p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==" title="PNG"></p>`, encoderSexpr: `((BLOB "PNG" "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`, encoderText: "", encoderZmk: `%% Unable to display BLOB with title 'PNG' and syntax 'png'.`, }, }, } func TestBlob(t *testing.T) { m := meta.New(id.Invalid) m.Set(api.KeyTitle, "PNG") for testNum, tc := range pngTestCases { inp := input.NewInput(tc.blob) pe := &peBlocks{bs: parser.ParseBlocks(inp, m, "png", config.NoHTML)} checkEncodings(t, testNum, pe, tc.descr, tc.expect, "???") } } |
Changes to encoder/encoder_block_test.go.
1 | //----------------------------------------------------------------------------- | | < < < > < < | > < | < > < | < > | < | < | > | < | < | > | < < > < | < > < | < > < | < < < < < < < < < < < < < > < | < > < | < < < < < < < < < < < < < | > | < | < | > < | < > < | < > < | < > < | < > < | < > < | < > < | < | < < < < < < < < < < < | < | < < < < < < < < < < < < < > < | < > < | < | | < | > | < | < < < < < < < < < < < < > < | < < < < < < < < < < < < > < | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package encoder_test var tcsBlock = []zmkTestCase{ { descr: "Empty Zettelmarkup should produce near nothing", zmk: "", expect: expectMap{ encoderZJSON: `[]`, encoderHTML: "", encoderSexpr: `()`, encoderText: "", encoderZmk: useZmk, }, }, { descr: "Simple text: Hello, world", zmk: "Hello, world", expect: expectMap{ encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Hello,"},{"":"Space"},{"":"Text","s":"world"}]}]`, encoderHTML: "<p>Hello, world</p>", encoderSexpr: `((PARA (TEXT "Hello,") (SPACE) (TEXT "world")))`, encoderText: "Hello, world", encoderZmk: useZmk, }, }, { descr: "Simple block comment", zmk: "%%%\nNo\nrender\n%%%", expect: expectMap{ encoderZJSON: `[{"":"CommentBlock","s":"No\nrender"}]`, encoderHTML: ``, encoderSexpr: `((VERBATIM-COMMENT () "No\nrender"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Rendered block comment", zmk: "%%%{-}\nRender\n%%%", expect: expectMap{ encoderZJSON: `[{"":"CommentBlock","a":{"-":""},"s":"Render"}]`, encoderHTML: "<!--\nRender\n-->", encoderSexpr: `((VERBATIM-COMMENT (("-" "")) "Render"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Simple Heading", zmk: `=== Top`, expect: expectMap{ encoderZJSON: `[{"":"Heading","n":1,"s":"top","i":[{"":"Text","s":"Top"}]}]`, encoderHTML: "<h2 id=\"top\">Top</h2>", encoderSexpr: `((HEADING 1 () "top" "top" (TEXT "Top")))`, encoderText: `Top`, encoderZmk: useZmk, }, }, { descr: "Simple List", zmk: "* A\n* B\n* C", expect: expectMap{ encoderZJSON: `[{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"A"}]}],[{"":"Para","i":[{"":"Text","s":"B"}]}],[{"":"Para","i":[{"":"Text","s":"C"}]}]]}]`, encoderHTML: "<ul><li>A</li><li>B</li><li>C</li></ul>", encoderSexpr: `((UNORDERED ((TEXT "A")) ((TEXT "B")) ((TEXT "C"))))`, encoderText: "A\nB\nC", encoderZmk: useZmk, }, }, { descr: "Nested List", zmk: "* T1\n** T2\n* T3\n** T4\n** T5\n* T6", expect: expectMap{ encoderZJSON: `[{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"T1"}]},{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"T2"}]}]]}],[{"":"Para","i":[{"":"Text","s":"T3"}]},{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"T4"}]}],[{"":"Para","i":[{"":"Text","s":"T5"}]}]]}],[{"":"Para","i":[{"":"Text","s":"T6"}]}]]}]`, encoderHTML: `<ul><li><p>T1</p><ul><li>T2</li></ul></li><li><p>T3</p><ul><li>T4</li><li>T5</li></ul></li><li><p>T6</p></li></ul>`, encoderSexpr: `((UNORDERED ((PARA (TEXT "T1")) (UNORDERED ((TEXT "T2")))) ((PARA (TEXT "T3")) (UNORDERED ((TEXT "T4")) ((TEXT "T5")))) ((PARA (TEXT "T6")))))`, encoderText: "T1\nT2\nT3\nT4\nT5\nT6", encoderZmk: useZmk, }, }, { descr: "Sequence of two lists", zmk: "* Item1.1\n* Item1.2\n* Item1.3\n\n* Item2.1\n* Item2.2", expect: expectMap{ encoderZJSON: `[{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"Item1.1"}]}],[{"":"Para","i":[{"":"Text","s":"Item1.2"}]}],[{"":"Para","i":[{"":"Text","s":"Item1.3"}]}],[{"":"Para","i":[{"":"Text","s":"Item2.1"}]}],[{"":"Para","i":[{"":"Text","s":"Item2.2"}]}]]}]`, encoderHTML: "<ul><li>Item1.1</li><li>Item1.2</li><li>Item1.3</li><li>Item2.1</li><li>Item2.2</li></ul>", encoderSexpr: `((UNORDERED ((TEXT "Item1.1")) ((TEXT "Item1.2")) ((TEXT "Item1.3")) ((TEXT "Item2.1")) ((TEXT "Item2.2"))))`, encoderText: "Item1.1\nItem1.2\nItem1.3\nItem2.1\nItem2.2", encoderZmk: "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2", }, }, { descr: "Simple horizontal rule", zmk: `---`, expect: expectMap{ encoderZJSON: `[{"":"Thematic"}]`, encoderHTML: "<hr>", encoderSexpr: `((THEMATIC ()))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "No list after paragraph", zmk: "Text\n*abc", expect: expectMap{ encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"},{"":"Soft"},{"":"Text","s":"*abc"}]}]`, encoderHTML: "<p>Text *abc</p>", encoderSexpr: `((PARA (TEXT "Text") (SOFT) (TEXT "*abc")))`, encoderText: `Text *abc`, encoderZmk: useZmk, }, }, { descr: "A list after paragraph", zmk: "Text\n# abc", expect: expectMap{ encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"}]},{"":"Ordered","c":[[{"":"Para","i":[{"":"Text","s":"abc"}]}]]}]`, encoderHTML: "<p>Text</p><ol><li>abc</li></ol>", encoderSexpr: `((PARA (TEXT "Text")) (ORDERED ((TEXT "abc"))))`, encoderText: "Text\nabc", encoderZmk: useZmk, }, }, { descr: "Simple Quote Block", zmk: "<<<\nToBeOrNotToBe\n<<< Romeo", expect: expectMap{ encoderZJSON: `[{"":"Excerpt","b":[{"":"Para","i":[{"":"Text","s":"ToBeOrNotToBe"}]}],"i":[{"":"Text","s":"Romeo"}]}]`, encoderHTML: "<blockquote><p>ToBeOrNotToBe</p><cite>Romeo</cite></blockquote>", encoderSexpr: `((REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) ((TEXT "Romeo"))))`, encoderText: "ToBeOrNotToBe\nRomeo", encoderZmk: useZmk, }, }, { descr: "Quote Block with multiple paragraphs", zmk: "<<<\nToBeOr\n\nNotToBe\n<<< Romeo", expect: expectMap{ encoderZJSON: `[{"":"Excerpt","b":[{"":"Para","i":[{"":"Text","s":"ToBeOr"}]},{"":"Para","i":[{"":"Text","s":"NotToBe"}]}],"i":[{"":"Text","s":"Romeo"}]}]`, encoderHTML: "<blockquote><p>ToBeOr</p><p>NotToBe</p><cite>Romeo</cite></blockquote>", encoderSexpr: `((REGION-QUOTE () ((PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) ((TEXT "Romeo"))))`, encoderText: "ToBeOr\nNotToBe\nRomeo", encoderZmk: useZmk, }, }, { descr: "Verse block", zmk: `""" A line another line Back Paragraph Spacy Para """ Author`, expect: expectMap{ encoderZJSON: "[{\"\":\"Poem\",\"b\":[{\"\":\"Para\",\"i\":[{\"\":\"Text\",\"s\":\"A\"},{\"\":\"Space\",\"s\":\"\u00a0\"},{\"\":\"Text\",\"s\":\"line\"},{\"\":\"Hard\"},{\"\":\"Space\",\"s\":\"\u00a0\u00a0\"},{\"\":\"Text\",\"s\":\"another\"},{\"\":\"Space\",\"s\":\"\u00a0\"},{\"\":\"Text\",\"s\":\"line\"},{\"\":\"Hard\"},{\"\":\"Text\",\"s\":\"Back\"}]},{\"\":\"Para\",\"i\":[{\"\":\"Text\",\"s\":\"Paragraph\"}]},{\"\":\"Para\",\"i\":[{\"\":\"Space\",\"s\":\"\u00a0\u00a0\u00a0\u00a0\"},{\"\":\"Text\",\"s\":\"Spacy\"},{\"\":\"Space\",\"s\":\"\u00a0\u00a0\"},{\"\":\"Text\",\"s\":\"Para\"}]}],\"i\":[{\"\":\"Text\",\"s\":\"Author\"}]}]", encoderHTML: "<div><p>A\u00a0line<br>\u00a0\u00a0another\u00a0line<br>Back</p><p>Paragraph</p><p>\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para</p><cite>Author</cite></div>", encoderSexpr: "((REGION-VERSE () ((PARA (TEXT \"A\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (SPACE \"\u00a0\u00a0\") (TEXT \"another\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (SPACE \"\u00a0\u00a0\u00a0\u00a0\") (TEXT \"Spacy\") (SPACE \"\u00a0\u00a0\") (TEXT \"Para\"))) ((TEXT \"Author\"))))", encoderText: "A line\n another line\nBack\nParagraph\n Spacy Para\nAuthor", encoderZmk: "\"\"\"\nA\u00a0line\\\n\u00a0\u00a0another\u00a0line\\\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\n\"\"\" Author", }, }, { descr: "Span Block", zmk: `::: A simple span and much more :::`, expect: expectMap{ encoderZJSON: `[{"":"Block","b":[{"":"Para","i":[{"":"Text","s":"A"},{"":"Space"},{"":"Text","s":"simple"},{"":"Soft"},{"":"Space"},{"":"Text","s":"span"},{"":"Soft"},{"":"Text","s":"and"},{"":"Space"},{"":"Text","s":"much"},{"":"Space"},{"":"Text","s":"more"}]}]}]`, encoderHTML: "<div><p>A simple span and much more</p></div>", encoderSexpr: `((REGION-BLOCK () ((PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more"))) ()))`, encoderText: `A simple span and much more`, encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Code", zmk: "```\nHello\nWorld\n```", expect: expectMap{ encoderZJSON: `[{"":"CodeBlock","s":"Hello\nWorld"}]`, encoderHTML: "<pre><code>Hello\nWorld</code></pre>", encoderSexpr: `((VERBATIM-CODE () "Hello\nWorld"))`, encoderText: "Hello\nWorld", encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Code with visible spaces", zmk: "```{-}\nHello World\n```", expect: expectMap{ encoderZJSON: `[{"":"CodeBlock","a":{"-":""},"s":"Hello World"}]`, encoderHTML: "<pre><code>Hello\u2423World</code></pre>", encoderSexpr: `((VERBATIM-CODE (("-" "")) "Hello World"))`, encoderText: "Hello World", encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Eval", zmk: "~~~\nHello\nWorld\n~~~", expect: expectMap{ encoderZJSON: `[{"":"EvalBlock","s":"Hello\nWorld"}]`, encoderHTML: "<pre><code class=\"zs-eval\">Hello\nWorld</code></pre>", encoderSexpr: `((VERBATIM-EVAL () "Hello\nWorld"))`, encoderText: "Hello\nWorld", encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Math", zmk: "$$$\nHello\n\\LaTeX\n$$$", expect: expectMap{ encoderZJSON: `[{"":"MathBlock","s":"Hello\n\\LaTeX"}]`, encoderHTML: "<pre><code class=\"zs-math\">Hello\n\\LaTeX</code></pre>", encoderSexpr: `((VERBATIM-MATH () "Hello\n\\LaTeX"))`, encoderText: "Hello\n\\LaTeX", encoderZmk: useZmk, }, }, { descr: "Simple Description List", zmk: "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box", expect: expectMap{ encoderZJSON: `[{"":"Description","d":[{"i":[{"":"Text","s":"Zettel"}],"e":[[{"":"Para","i":[{"":"Text","s":"Paper"}]}],[{"":"Para","i":[{"":"Text","s":"Note"}]}]]},{"i":[{"":"Text","s":"Zettelkasten"}],"e":[[{"":"Para","i":[{"":"Text","s":"Slip"},{"":"Space"},{"":"Text","s":"box"}]}]]}]}]`, encoderHTML: "<dl><dt>Zettel</dt><dd>Paper</dd><dd>Note</dd><dt>Zettelkasten</dt><dd>Slip box</dd></dl>", encoderSexpr: `((DESCRIPTION ((TEXT "Zettel")) (((TEXT "Paper")) ((TEXT "Note"))) ((TEXT "Zettelkasten")) (((TEXT "Slip") (SPACE) (TEXT "box")))))`, encoderText: "Zettel\nPaper\nNote\nZettelkasten\nSlip box", encoderZmk: useZmk, }, }, { descr: "Simple Table", zmk: "|c1|c2|c3\n|d1||d3", expect: expectMap{ encoderZJSON: `[{"":"Table","p":[[],[[{"i":[{"":"Text","s":"c1"}]},{"i":[{"":"Text","s":"c2"}]},{"i":[{"":"Text","s":"c3"}]}],[{"i":[{"":"Text","s":"d1"}]},{"i":[]},{"i":[{"":"Text","s":"d3"}]}]]]}]`, encoderHTML: `<table><tbody><tr><td>c1</td><td>c2</td><td>c3</td></tr><tr><td>d1</td><td></td><td>d3</td></tr></tbody></table>`, encoderSexpr: `((TABLE () ((CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) ((CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`, encoderText: "c1 c2 c3\nd1 d3", encoderZmk: useZmk, }, }, { descr: "Table with alignment and comment", zmk: `|h1>|=h2|h3:| |%--+---+---+ |<c1|c2|:c3| |f1|f2|=f3`, expect: expectMap{ encoderZJSON: `[{"":"Table","p":[[{"s":">","i":[{"":"Text","s":"h1"}]},{"i":[{"":"Text","s":"h2"}]},{"s":":","i":[{"":"Text","s":"h3"}]}],[[{"s":"<","i":[{"":"Text","s":"c1"}]},{"i":[{"":"Text","s":"c2"}]},{"s":":","i":[{"":"Text","s":"c3"}]}],[{"s":">","i":[{"":"Text","s":"f1"}]},{"i":[{"":"Text","s":"f2"}]},{"s":":","i":[{"":"Text","s":"=f3"}]}]]]}]`, encoderHTML: `<table><thead><tr><td class="right">h1</td><td>h2</td><td class="center">h3</td></tr></thead><tbody><tr><td class="left">c1</td><td>c2</td><td class="center">c3</td></tr><tr><td class="right">f1</td><td>f2</td><td class="center">=f3</td></tr></tbody></table>`, encoderSexpr: `((TABLE ((CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) ((CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) ((CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`, encoderText: "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3", encoderZmk: `|=h1>|=h2|=h3: |<c1|c2|c3 |f1|f2|=f3`, }, }, { descr: "Simple Endnotes", zmk: `Text[^Footnote]`, expect: expectMap{ encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"},{"":"Footnote","i":[{"":"Text","s":"Footnote"}]}]}]`, encoderHTML: `<p>Text<sup id="fnref:1"><a class="zs-noteref" href="#fn:1" role="doc-noteref">1</a></sup></p><ol class="zs-endnotes"><li class="zs-endnote" id="fn:1" role="doc-endnote" value="1">Footnote <a class="zs-endnote-backref" href="#fnref:1" role="doc-backlink">↩︎</a></li></ol>`, encoderSexpr: `((PARA (TEXT "Text") (FOOTNOTE () (TEXT "Footnote"))))`, encoderText: "Text Footnote", encoderZmk: useZmk, }, }, { descr: "Transclusion", zmk: `{{{http://example.com/image}}}{width="100px"}`, expect: expectMap{ encoderZJSON: `[{"":"Transclude","a":{"width":"100px"},"q":"external","s":"http://example.com/image"}]`, encoderHTML: `<p><img class="external" src="http://example.com/image" width="100px"></p>`, encoderSexpr: `((TRANSCLUDE (("width" "100px")) (EXTERNAL "http://example.com/image")))`, encoderText: "", encoderZmk: useZmk, }, }, { descr: "", zmk: ``, expect: expectMap{ encoderZJSON: `[]`, encoderHTML: ``, encoderSexpr: `()`, encoderText: "", encoderZmk: useZmk, }, }, } // func TestEncoderBlock(t *testing.T) { // executeTestCases(t, tcsBlock) // } |
Changes to encoder/encoder_inline_test.go.
1 | //----------------------------------------------------------------------------- | | < < < > < < | > < | < < < < < < < < < < < < < < < < < < < < < < < < < > < | < > < | < > < | < > < | < > < | < > < | < > < | < | | | < > | < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < > < | < > < | < > < | < > < | < > < | < > < | < > < | < > | < | < > < | < < < < < < < < < < < < > < | < > < | < > < < | > < | < > | < | < | | > < | < | > < | < > < | < < < < < < < < < < < < < < < < < < < < < < < < < > < | < > < | < > < | < > < | < > < | < > < | < > < | < > < | < > < | < | > < | < > < | < > < | < > < | < > < | < > < < | > < | < > < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package encoder_test var tcsInline = []zmkTestCase{ { descr: "Empty Zettelmarkup should produce near nothing (inline)", zmk: "", expect: expectMap{ encoderZJSON: `[]`, encoderHTML: "", encoderSexpr: `()`, encoderText: "", encoderZmk: useZmk, }, }, { descr: "Simple text: Hello, world (inline)", zmk: `Hello, world`, expect: expectMap{ encoderZJSON: `[{"":"Text","s":"Hello,"},{"":"Space"},{"":"Text","s":"world"}]`, encoderHTML: "Hello, world", encoderSexpr: `((TEXT "Hello,") (SPACE) (TEXT "world"))`, encoderText: "Hello, world", encoderZmk: useZmk, }, }, { descr: "Emphasized formatting", zmk: "__emph__", expect: expectMap{ encoderZJSON: `[{"":"Emph","i":[{"":"Text","s":"emph"}]}]`, encoderHTML: "<em>emph</em>", encoderSexpr: `((FORMAT-EMPH () (TEXT "emph")))`, encoderText: "emph", encoderZmk: useZmk, }, }, { descr: "Strong formatting", zmk: "**strong**", expect: expectMap{ encoderZJSON: `[{"":"Strong","i":[{"":"Text","s":"strong"}]}]`, encoderHTML: "<strong>strong</strong>", encoderSexpr: `((FORMAT-STRONG () (TEXT "strong")))`, encoderText: "strong", encoderZmk: useZmk, }, }, { descr: "Insert formatting", zmk: ">>insert>>", expect: expectMap{ encoderZJSON: `[{"":"Insert","i":[{"":"Text","s":"insert"}]}]`, encoderHTML: "<ins>insert</ins>", encoderSexpr: `((FORMAT-INSERT () (TEXT "insert")))`, encoderText: "insert", encoderZmk: useZmk, }, }, { descr: "Delete formatting", zmk: "~~delete~~", expect: expectMap{ encoderZJSON: `[{"":"Delete","i":[{"":"Text","s":"delete"}]}]`, encoderHTML: "<del>delete</del>", encoderSexpr: `((FORMAT-DELETE () (TEXT "delete")))`, encoderText: "delete", encoderZmk: useZmk, }, }, { descr: "Update formatting", zmk: "~~old~~>>new>>", expect: expectMap{ encoderZJSON: `[{"":"Delete","i":[{"":"Text","s":"old"}]},{"":"Insert","i":[{"":"Text","s":"new"}]}]`, encoderHTML: "<del>old</del><ins>new</ins>", encoderSexpr: `((FORMAT-DELETE () (TEXT "old")) (FORMAT-INSERT () (TEXT "new")))`, encoderText: "oldnew", encoderZmk: useZmk, }, }, { descr: "Superscript formatting", zmk: "^^superscript^^", expect: expectMap{ encoderZJSON: `[{"":"Super","i":[{"":"Text","s":"superscript"}]}]`, encoderHTML: `<sup>superscript</sup>`, encoderSexpr: `((FORMAT-SUPER () (TEXT "superscript")))`, encoderText: `superscript`, encoderZmk: useZmk, }, }, { descr: "Subscript formatting", zmk: ",,subscript,,", expect: expectMap{ encoderZJSON: `[{"":"Sub","i":[{"":"Text","s":"subscript"}]}]`, encoderHTML: `<sub>subscript</sub>`, encoderSexpr: `((FORMAT-SUB () (TEXT "subscript")))`, encoderText: `subscript`, encoderZmk: useZmk, }, }, { descr: "Quotes formatting", zmk: `""quotes""`, expect: expectMap{ encoderZJSON: `[{"":"Quote","i":[{"":"Text","s":"quotes"}]}]`, encoderHTML: "<q>quotes</q>", encoderSexpr: `((FORMAT-QUOTE () (TEXT "quotes")))`, encoderText: `quotes`, encoderZmk: useZmk, }, }, { descr: "Quotes formatting (german)", zmk: `""quotes""{lang=de}`, expect: expectMap{ encoderZJSON: `[{"":"Quote","a":{"lang":"de"},"i":[{"":"Text","s":"quotes"}]}]`, encoderHTML: `<span lang="de"><q>quotes</q></span>`, encoderSexpr: `((FORMAT-QUOTE (("lang" "de")) (TEXT "quotes")))`, encoderText: `quotes`, encoderZmk: `""quotes""{lang="de"}`, }, }, { descr: "Span formatting", zmk: `::span::`, expect: expectMap{ encoderZJSON: `[{"":"Span","i":[{"":"Text","s":"span"}]}]`, encoderHTML: `<span>span</span>`, encoderSexpr: `((FORMAT-SPAN () (TEXT "span")))`, encoderText: `span`, encoderZmk: useZmk, }, }, { descr: "Code formatting", zmk: "``code``", expect: expectMap{ encoderZJSON: `[{"":"Code","s":"code"}]`, encoderHTML: `<code>code</code>`, encoderSexpr: `((LITERAL-CODE () "code"))`, encoderText: `code`, encoderZmk: useZmk, }, }, { descr: "Code formatting with visible space", zmk: "``x y``{-}", expect: expectMap{ encoderZJSON: `[{"":"Code","a":{"-":""},"s":"x y"}]`, encoderHTML: "<code>x\u2423y</code>", encoderSexpr: `((LITERAL-CODE (("-" "")) "x y"))`, encoderText: `x y`, encoderZmk: useZmk, }, }, { descr: "HTML in Code formatting", zmk: "``<script `` abc", expect: expectMap{ encoderZJSON: `[{"":"Code","s":"<script "},{"":"Space"},{"":"Text","s":"abc"}]`, encoderHTML: "<code><script </code> abc", encoderSexpr: `((LITERAL-CODE () "<script ") (SPACE) (TEXT "abc"))`, encoderText: `<script abc`, encoderZmk: useZmk, }, }, { descr: "Input formatting", zmk: `''input''`, expect: expectMap{ encoderZJSON: `[{"":"Input","s":"input"}]`, encoderHTML: `<kbd>input</kbd>`, encoderSexpr: `((LITERAL-INPUT () "input"))`, encoderText: `input`, encoderZmk: useZmk, }, }, { descr: "Output formatting", zmk: `==output==`, expect: expectMap{ encoderZJSON: `[{"":"Output","s":"output"}]`, encoderHTML: `<samp>output</samp>`, encoderSexpr: `((LITERAL-OUTPUT () "output"))`, encoderText: `output`, encoderZmk: useZmk, }, }, { descr: "Math formatting", zmk: `$$\TeX$$`, expect: expectMap{ encoderZJSON: `[{"":"Math","s":"\\TeX"}]`, encoderHTML: `<code class="zs-math">\TeX</code>`, encoderSexpr: `((LITERAL-MATH () "\\TeX"))`, encoderText: `\TeX`, encoderZmk: useZmk, }, }, { descr: "Nested Span Quote formatting", zmk: `::""abc""::{lang=fr}`, expect: expectMap{ encoderZJSON: `[{"":"Span","a":{"lang":"fr"},"i":[{"":"Quote","i":[{"":"Text","s":"abc"}]}]}]`, encoderHTML: `<span lang="fr"><q>abc</q></span>`, encoderSexpr: `((FORMAT-SPAN (("lang" "fr")) (FORMAT-QUOTE () (TEXT "abc"))))`, encoderText: `abc`, encoderZmk: `::""abc""::{lang="fr"}`, }, }, { descr: "Simple Citation", zmk: `[@Stern18]`, expect: expectMap{ encoderZJSON: `[{"":"Cite","s":"Stern18"}]`, encoderHTML: `<span>Stern18</span>`, // TODO encoderSexpr: `((CITE () "Stern18"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "No comment", zmk: `% comment`, expect: expectMap{ encoderZJSON: `[{"":"Text","s":"%"},{"":"Space"},{"":"Text","s":"comment"}]`, encoderHTML: `% comment`, encoderSexpr: `((TEXT "%") (SPACE) (TEXT "comment"))`, encoderText: `% comment`, encoderZmk: useZmk, }, }, { descr: "Line comment (nogen HTML)", zmk: `%% line comment`, expect: expectMap{ encoderZJSON: `[{"":"Comment","s":"line comment"}]`, encoderHTML: ``, encoderSexpr: `((LITERAL-COMMENT () "line comment"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Line comment", zmk: `%%{-} line comment`, expect: expectMap{ encoderZJSON: `[{"":"Comment","a":{"-":""},"s":"line comment"}]`, encoderHTML: `<!-- line comment -->`, encoderSexpr: `((LITERAL-COMMENT (("-" "")) "line comment"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Comment after text", zmk: `Text %%{-} comment`, expect: expectMap{ encoderZJSON: `[{"":"Text","s":"Text"},{"":"Comment","a":{"-":""},"s":"comment"}]`, encoderHTML: `Text<!-- comment -->`, encoderSexpr: `((TEXT "Text") (LITERAL-COMMENT (("-" "")) "comment"))`, encoderText: `Text`, encoderZmk: useZmk, }, }, { descr: "Comment after text and with -->", zmk: `Text %%{-} comment --> end`, expect: expectMap{ encoderZJSON: `[{"":"Text","s":"Text"},{"":"Comment","a":{"-":""},"s":"comment --> end"}]`, encoderHTML: `Text<!-- comment --> end -->`, encoderSexpr: `((TEXT "Text") (LITERAL-COMMENT (("-" "")) "comment --> end"))`, encoderText: `Text`, encoderZmk: useZmk, }, }, { descr: "Simple footnote", zmk: `[^footnote]`, expect: expectMap{ encoderZJSON: `[{"":"Footnote","i":[{"":"Text","s":"footnote"}]}]`, encoderHTML: `<sup id="fnref:1"><a class="zs-noteref" href="#fn:1" role="doc-noteref">1</a></sup>`, encoderSexpr: `((FOOTNOTE () (TEXT "footnote")))`, encoderText: `footnote`, encoderZmk: useZmk, }, }, { descr: "Simple mark", zmk: `[!mark]`, expect: expectMap{ encoderZJSON: `[{"":"Mark","s":"mark","q":"mark"}]`, encoderHTML: `<a id="mark"></a>`, encoderSexpr: `((MARK "mark" "mark" "mark"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Mark with text", zmk: `[!mark|with text]`, expect: expectMap{ encoderZJSON: `[{"":"Mark","s":"mark","q":"mark","i":[{"":"Text","s":"with"},{"":"Space"},{"":"Text","s":"text"}]}]`, encoderHTML: `<a id="mark">with text</a>`, encoderSexpr: `((MARK "mark" "mark" "mark" (TEXT "with") (SPACE) (TEXT "text")))`, encoderText: `with text`, encoderZmk: useZmk, }, }, { descr: "Dummy Link", zmk: `[[abc]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"external","s":"abc"}]`, encoderHTML: `<a class="external" href="abc">abc</a>`, encoderSexpr: `((LINK-EXTERNAL () "abc"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Simple URL", zmk: `[[https://zettelstore.de]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"external","s":"https://zettelstore.de"}]`, encoderHTML: `<a class="external" href="https://zettelstore.de">https://zettelstore.de</a>`, encoderSexpr: `((LINK-EXTERNAL () "https://zettelstore.de"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "URL with Text", zmk: `[[Home|https://zettelstore.de]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"external","s":"https://zettelstore.de","i":[{"":"Text","s":"Home"}]}]`, encoderHTML: `<a class="external" href="https://zettelstore.de">Home</a>`, encoderSexpr: `((LINK-EXTERNAL () "https://zettelstore.de" (TEXT "Home")))`, encoderText: `Home`, encoderZmk: useZmk, }, }, { descr: "Simple Zettel ID", zmk: `[[00000000000100]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"zettel","s":"00000000000100"}]`, encoderHTML: `<a href="00000000000100">00000000000100</a>`, encoderSexpr: `((LINK-ZETTEL () "00000000000100"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Zettel ID with Text", zmk: `[[Config|00000000000100]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"zettel","s":"00000000000100","i":[{"":"Text","s":"Config"}]}]`, encoderHTML: `<a href="00000000000100">Config</a>`, encoderSexpr: `((LINK-ZETTEL () "00000000000100" (TEXT "Config")))`, encoderText: `Config`, encoderZmk: useZmk, }, }, { descr: "Simple Zettel ID with fragment", zmk: `[[00000000000100#frag]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"zettel","s":"00000000000100#frag"}]`, encoderHTML: `<a href="00000000000100#frag">00000000000100#frag</a>`, encoderSexpr: `((LINK-ZETTEL () "00000000000100#frag"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Zettel ID with Text and fragment", zmk: `[[Config|00000000000100#frag]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"zettel","s":"00000000000100#frag","i":[{"":"Text","s":"Config"}]}]`, encoderHTML: `<a href="00000000000100#frag">Config</a>`, encoderSexpr: `((LINK-ZETTEL () "00000000000100#frag" (TEXT "Config")))`, encoderText: `Config`, encoderZmk: useZmk, }, }, { descr: "Fragment link to self", zmk: `[[#frag]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"self","s":"#frag"}]`, encoderHTML: `<a href="#frag">#frag</a>`, encoderSexpr: `((LINK-SELF () "#frag"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Hosted link", zmk: `[[H|/hosted]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"local","s":"/hosted","i":[{"":"Text","s":"H"}]}]`, encoderHTML: `<a href="/hosted">H</a>`, encoderSexpr: `((LINK-HOSTED () "/hosted" (TEXT "H")))`, encoderText: `H`, encoderZmk: useZmk, }, }, { descr: "Based link", zmk: `[[B|/based]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"local","s":"/based","i":[{"":"Text","s":"B"}]}]`, encoderHTML: `<a href="/based">B</a>`, encoderSexpr: `((LINK-HOSTED () "/based" (TEXT "B")))`, encoderText: `B`, encoderZmk: useZmk, }, }, { descr: "Relative link", zmk: `[[R|../relative]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"local","s":"../relative","i":[{"":"Text","s":"R"}]}]`, encoderHTML: `<a href="../relative">R</a>`, encoderSexpr: `((LINK-HOSTED () "../relative" (TEXT "R")))`, encoderText: `R`, encoderZmk: useZmk, }, }, { descr: "Query link w/o text", zmk: `[[query:title:syntax]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"query","s":"title:syntax"}]`, encoderHTML: `<a href="?q=title%3Asyntax">title:syntax</a>`, encoderSexpr: `((LINK-QUERY () "title:syntax"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Query link with text", zmk: `[[Q|query:title:syntax]]`, expect: expectMap{ encoderZJSON: `[{"":"Link","q":"query","s":"title:syntax","i":[{"":"Text","s":"Q"}]}]`, encoderHTML: `<a href="?q=title%3Asyntax">Q</a>`, encoderSexpr: `((LINK-QUERY () "title:syntax" (TEXT "Q")))`, encoderText: `Q`, encoderZmk: useZmk, }, }, { descr: "Dummy Embed", zmk: `{{abc}}`, expect: expectMap{ encoderZJSON: `[{"":"Embed","s":"abc"}]`, encoderHTML: `<img src="abc">`, encoderSexpr: `((EMBED () (EXTERNAL "abc") ""))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Inline HTML Zettel", zmk: `@@<hr>@@{="html"}`, expect: expectMap{ encoderZJSON: `[]`, encoderHTML: ``, encoderSexpr: `()`, encoderText: ``, encoderZmk: ``, }, }, { descr: "Inline Text Zettel", zmk: `@@<hr>@@{="text"}`, expect: expectMap{ encoderZJSON: `[{"":"Zettel","a":{"":"text"},"s":"<hr>"}]`, encoderHTML: ``, encoderSexpr: `((LITERAL-ZETTEL (("" "text")) "<hr>"))`, encoderText: `<hr>`, encoderZmk: useZmk, }, }, { descr: "", zmk: ``, expect: expectMap{ encoderZJSON: `[]`, encoderHTML: ``, encoderSexpr: `()`, encoderText: ``, encoderZmk: useZmk, }, }, } |
Changes to encoder/encoder_test.go.
1 | //----------------------------------------------------------------------------- | | < < < > < | | | | | < < | > > < | < | | | | < < < < < | | | | | > > > > | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package encoder_test import ( "bytes" "fmt" "testing" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/c/sexpr" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. _ "zettelstore.de/z/encoder/sexprenc" // Allow to use sexpr encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. _ "zettelstore.de/z/encoder/zjsonenc" // Allow to use ZJSON encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. "zettelstore.de/z/parser/cleaner" _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) type zmkTestCase struct { descr string zmk string inline bool expect expectMap } type expectMap map[api.EncodingEnum]string const useZmk = "\000" const ( encoderZJSON = api.EncoderZJSON encoderHTML = api.EncoderHTML encoderSexpr = api.EncoderSexpr encoderText = api.EncoderText encoderZmk = api.EncoderZmk ) func TestEncoder(t *testing.T) { for i := range tcsInline { tcsInline[i].inline = true } executeTestCases(t, append(tcsBlock, tcsInline...)) } func executeTestCases(t *testing.T, testCases []zmkTestCase) { for testNum, tc := range testCases { inp := input.NewInput([]byte(tc.zmk)) var pe parserEncoder if tc.inline { is := parser.ParseInlines(inp, api.ValueSyntaxZmk) cleaner.CleanInlineSlice(&is) pe = &peInlines{is: is} } else { pe = &peBlocks{bs: parser.ParseBlocks(inp, nil, api.ValueSyntaxZmk, config.NoHTML)} } checkEncodings(t, testNum, pe, tc.descr, tc.expect, tc.zmk) checkSexpr(t, testNum, pe, tc.descr) } } func checkEncodings(t *testing.T, testNum int, pe parserEncoder, descr string, expected expectMap, zmkDefault string) { for enc, exp := range expected { encdr := encoder.Create(enc) got, err := pe.encode(encdr) if err != nil { t.Error("pe.encode:", err) continue } if enc == api.EncoderZmk && exp == useZmk { exp = zmkDefault } if got != exp { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } prefix += "\nMode: " + pe.mode() t.Errorf("%s\nEncoder: %s\nExpected: %q\nGot: %q", prefix, enc, exp, got) } } } func checkSexpr(t *testing.T, testNum int, pe parserEncoder, descr string) { t.Helper() encdr := encoder.Create(encoderSexpr) exp, err := pe.encode(encdr) if err != nil { t.Error(err) return } val, err := sxpf.ParseString(sexpr.Smk, exp) if err != nil { t.Error(err) return } got, err := sxpf.Repr(val) if err != nil { t.Error(err) return } if exp != got { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } prefix += "\nMode: " + pe.mode() t.Errorf("%s\n\nExpected: %q\nGot: %q", prefix, exp, got) } } type parserEncoder interface { encode(encoder.Encoder) (string, error) mode() string } type peInlines struct { is ast.InlineSlice } func (in peInlines) encode(encdr encoder.Encoder) (string, error) { var buf bytes.Buffer if _, err := encdr.WriteInlines(&buf, &in.is); err != nil { return "", err } return buf.String(), nil } func (peInlines) mode() string { return "inline" } type peBlocks struct { bs ast.BlockSlice } func (bl peBlocks) encode(encdr encoder.Encoder) (string, error) { var buf bytes.Buffer if _, err := encdr.WriteBlocks(&buf, &bl.bs); err != nil { return "", err } return buf.String(), nil } func (peBlocks) mode() string { return "block" } |
Changes to encoder/htmlenc/htmlenc.go.
1 | //----------------------------------------------------------------------------- | | < < < | < | < | | | | | | | < < < < | < < < | < | < < < | | < < < < < | < < > | | | < < < | < < < < < | < | < < < > | > | < | | | < < < < < < < < | | | > | < < | < | | < | < | | < < | < | | | | < | < < < | < < < | | > > | > > > > > | | | | | | > | > > > > | > > > | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package htmlenc encodes the abstract syntax tree into HTML5 via zettelstore-client. package htmlenc import ( "io" "strings" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/c/html" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/sexprenc" "zettelstore.de/z/encoder/textenc" ) func init() { encoder.Register(api.EncoderHTML, func() encoder.Encoder { return &mySHE }) } type htmlEncoder struct { textEnc *textenc.Encoder } var mySHE = htmlEncoder{ textEnc: textenc.Create(), } // WriteZettel encodes a full zettel as HTML5. func (he *htmlEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { io.WriteString(w, "<html>\n<head>\n<meta charset=\"utf-8\">\n") plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle) if hasTitle { io.WriteString(w, "<title>") is := evalMeta(plainTitle) he.textEnc.WriteInlines(w, &is) io.WriteString(w, "</title>\n") } acceptMeta(w, he.textEnc, zn.InhMeta, evalMeta) io.WriteString(w, "</head>\n<body>\n") env := html.NewEncEnvironment(w, 1) if hasTitle { if isTitle := evalMeta(plainTitle); len(isTitle) > 0 { io.WriteString(w, "<h1>") if l, err := acceptInlines(env, &isTitle); err != nil { return l, err } io.WriteString(w, "</h1>\n") } } _, err := acceptBlocks(env, &zn.Ast) if err == nil { // env.WriteEndnotes() io.WriteString(w, "</body>\n</html>") } return 0, err } // WriteMeta encodes meta data as HTML5. func (he *htmlEncoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { acceptMeta(w, he.textEnc, m, evalMeta) return 0, nil } func (he *htmlEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return he.WriteBlocks(w, &zn.Ast) } // WriteBlocks encodes a block slice. func (*htmlEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { env := html.NewEncEnvironment(w, 1) _, err := acceptBlocks(env, bs) if err == nil { env.WriteEndnotes() err = env.GetError() } return 0, err } // WriteInlines writes an inline slice to the writer func (*htmlEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { env := html.NewEncEnvironment(w, 1) return acceptInlines(env, is) } func acceptMeta(w io.Writer, textEnc encoder.Encoder, m *meta.Meta, evalMeta encoder.EvalMetaFunc) { for _, p := range m.ComputedPairs() { io.WriteString(w, `<meta name="zs-`) io.WriteString(w, p.Key) io.WriteString(w, `" content="`) is := evalMeta(p.Value) var sb strings.Builder textEnc.WriteInlines(&sb, &is) html.AttributeEscape(w, sb.String()) io.WriteString(w, "\">\n") } } func acceptBlocks(env *html.EncEnvironment, bs *ast.BlockSlice) (int, error) { lst := sexprenc.GetSexpr(bs) sxpf.Eval(env, lst) return 0, env.GetError() } func acceptInlines(env *html.EncEnvironment, is *ast.InlineSlice) (int, error) { lst := sexprenc.GetSexpr(is) sxpf.Eval(env, lst) return 0, env.GetError() } |
Deleted encoder/mdenc/mdenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added encoder/sexprenc/sexprenc.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package sexprenc encodes the abstract syntax tree into a s-expr. package sexprenc import ( "io" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderSexpr, func() encoder.Encoder { return Create() }) } // Create a S-expr encoder func Create() *Encoder { return &mySE } type Encoder struct{} var mySE Encoder // WriteZettel writes the encoded zettel to the writer. func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { content := GetSexpr(&zn.Ast) meta := GetMeta(zn.InhMeta, evalMeta) return io.WriteString(w, sxpf.NewPair(meta, sxpf.NewPair(content, nil)).String()) } // WriteMeta encodes meta data as JSON. func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { return io.WriteString(w, GetMeta(m, evalMeta).String()) } func (se *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return se.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes a block slice to the writer func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { return io.WriteString(w, GetSexpr(bs).String()) } // WriteInlines writes an inline slice to the writer func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { return io.WriteString(w, GetSexpr(is).String()) } |
Added encoder/sexprenc/transform.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package sexprenc encodes the abstract syntax tree into a s-expr. package sexprenc import ( "bytes" "encoding/base64" "fmt" "log" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/c/sexpr" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) // GetSexpr returns the given node as a s-expression. func GetSexpr(node ast.Node) *sxpf.Pair { t := transformer{} return t.getSexpr(node) } type transformer struct { inVerse bool } func (t *transformer) getSexpr(node ast.Node) *sxpf.Pair { switch n := node.(type) { case *ast.BlockSlice: return t.getBlockSlice(n) case *ast.InlineSlice: return t.getInlineSlice(*n) case *ast.ParaNode: return sxpf.NewPair(sexpr.SymPara, t.getInlineSlice(n.Inlines)) case *ast.VerbatimNode: return sxpf.NewPairFromValues( mapGetS(mapVerbatimKindS, n.Kind), getAttributes(n.Attrs), sxpf.NewString(string(n.Content)), ) case *ast.RegionNode: return t.getRegion(n) case *ast.HeadingNode: return sxpf.NewPair( sexpr.SymHeading, sxpf.NewPair( sxpf.NewInteger(int64(n.Level)), sxpf.NewPair( getAttributes(n.Attrs), sxpf.NewPair( sxpf.NewString(n.Slug), sxpf.NewPair( sxpf.NewString(n.Fragment), t.getInlineSlice(n.Inlines), ), ), ), ), ) case *ast.HRuleNode: return sxpf.NewPairFromValues(sexpr.SymThematic, getAttributes(n.Attrs)) case *ast.NestedListNode: return t.getNestedList(n) case *ast.DescriptionListNode: return t.getDescriptionList(n) case *ast.TableNode: return t.getTable(n) case *ast.TranscludeNode: return sxpf.NewPairFromValues(sexpr.SymTransclude, getAttributes(n.Attrs), getReference(n.Ref)) case *ast.BLOBNode: return getBLOB(n) case *ast.TextNode: return sxpf.NewPairFromValues(sexpr.SymText, sxpf.NewString(n.Text)) case *ast.SpaceNode: if t.inVerse { return sxpf.NewPairFromValues(sexpr.SymSpace, sxpf.NewString(n.Lexeme)) } return sxpf.NewPairFromValues(sexpr.SymSpace) case *ast.BreakNode: if n.Hard { return sxpf.NewPairFromValues(sexpr.SymHard) } else { return sxpf.NewPairFromValues(sexpr.SymSoft) } case *ast.LinkNode: return t.getLink(n) case *ast.EmbedRefNode: return sxpf.NewPair( sexpr.SymEmbed, sxpf.NewPair( getAttributes(n.Attrs), sxpf.NewPair( getReference(n.Ref), sxpf.NewPair( sxpf.NewString(n.Syntax), t.getInlineSlice(n.Inlines), ), ), ), ) case *ast.EmbedBLOBNode: return t.getEmbedBLOB(n) case *ast.CiteNode: return sxpf.NewPair( sexpr.SymCite, sxpf.NewPair( getAttributes(n.Attrs), sxpf.NewPair( sxpf.NewString(n.Key), t.getInlineSlice(n.Inlines), ), ), ) case *ast.FootnoteNode: return sxpf.NewPair( sexpr.SymFootnote, sxpf.NewPair( getAttributes(n.Attrs), t.getInlineSlice(n.Inlines), ), ) case *ast.MarkNode: return sxpf.NewPair( sexpr.SymMark, sxpf.NewPair( sxpf.NewString(n.Mark), sxpf.NewPair( sxpf.NewString(n.Slug), sxpf.NewPair( sxpf.NewString(n.Fragment), t.getInlineSlice(n.Inlines), ), ), ), ) case *ast.FormatNode: return sxpf.NewPair( mapGetS(mapFormatKindS, n.Kind), sxpf.NewPair( getAttributes(n.Attrs), t.getInlineSlice(n.Inlines), ), ) case *ast.LiteralNode: return sxpf.NewPairFromValues( mapGetS(mapLiteralKindS, n.Kind), getAttributes(n.Attrs), sxpf.NewString(string(n.Content)), ) } log.Printf("SEXPR %T %v\n", node, node) return sxpf.NewPairFromValues(sexpr.SymUnknown, sxpf.NewString(fmt.Sprintf("%T %v", node, node))) } var mapVerbatimKindS = map[ast.VerbatimKind]*sxpf.Symbol{ ast.VerbatimZettel: sexpr.SymVerbatimZettel, ast.VerbatimProg: sexpr.SymVerbatimProg, ast.VerbatimEval: sexpr.SymVerbatimEval, ast.VerbatimMath: sexpr.SymVerbatimMath, ast.VerbatimComment: sexpr.SymVerbatimComment, ast.VerbatimHTML: sexpr.SymVerbatimHTML, } var mapRegionKindS = map[ast.RegionKind]*sxpf.Symbol{ ast.RegionSpan: sexpr.SymRegionBlock, ast.RegionQuote: sexpr.SymRegionQuote, ast.RegionVerse: sexpr.SymRegionVerse, } func (t *transformer) getRegion(rn *ast.RegionNode) *sxpf.Pair { saveInVerse := t.inVerse if rn.Kind == ast.RegionVerse { t.inVerse = true } symBlocks := t.getSexpr(&rn.Blocks) t.inVerse = saveInVerse return sxpf.NewPairFromValues( mapGetS(mapRegionKindS, rn.Kind), getAttributes(rn.Attrs), symBlocks, t.getSexpr(&rn.Inlines), ) } var mapNestedListKindS = map[ast.NestedListKind]*sxpf.Symbol{ ast.NestedListOrdered: sexpr.SymListOrdered, ast.NestedListUnordered: sexpr.SymListUnordered, ast.NestedListQuote: sexpr.SymListQuote, } func (t *transformer) getNestedList(ln *ast.NestedListNode) *sxpf.Pair { nlistVals := make([]sxpf.Value, len(ln.Items)+1) nlistVals[0] = mapGetS(mapNestedListKindS, ln.Kind) isCompact := isCompactList(ln.Items) for i, item := range ln.Items { if isCompact && len(item) > 0 { paragraph := t.getSexpr(item[0]) nlistVals[i+1] = paragraph.GetTail() continue } itemVals := make([]sxpf.Value, len(item)) for j, in := range item { itemVals[j] = t.getSexpr(in) } nlistVals[i+1] = sxpf.NewPairFromValues(itemVals...) } return sxpf.NewPairFromValues(nlistVals...) } func isCompactList(itemSlice []ast.ItemSlice) bool { for _, items := range itemSlice { if len(items) > 1 { return false } if len(items) == 1 { if _, ok := items[0].(*ast.ParaNode); !ok { return false } } } return true } func (t *transformer) getDescriptionList(dn *ast.DescriptionListNode) *sxpf.Pair { dlVals := make([]sxpf.Value, 2*len(dn.Descriptions)+1) dlVals[0] = sexpr.SymDescription for i, def := range dn.Descriptions { dlVals[2*i+1] = t.getInlineSlice(def.Term) descVals := make([]sxpf.Value, len(def.Descriptions)) for j, b := range def.Descriptions { if len(b) == 1 { descVals[j] = t.getSexpr(b[0]).GetTail() continue } dVal := make([]sxpf.Value, len(b)) for k, dn := range b { dVal[k] = t.getSexpr(dn) } descVals[j] = sxpf.NewPairFromValues(dVal...) } dlVals[2*i+2] = sxpf.NewPairFromValues(descVals...) } return sxpf.NewPairFromValues(dlVals...) } func (t *transformer) getTable(tn *ast.TableNode) *sxpf.Pair { tVals := make([]sxpf.Value, len(tn.Rows)+2) tVals[0] = sexpr.SymTable tVals[1] = t.getRow(tn.Header) for i, row := range tn.Rows { tVals[i+2] = t.getRow(row) } return sxpf.NewPairFromValues(tVals...) } func (t *transformer) getRow(row ast.TableRow) *sxpf.Pair { rVals := make([]sxpf.Value, len(row)) for i, cell := range row { rVals[i] = t.getCell(cell) } return sxpf.NewPairFromValues(rVals...) } var alignmentSymbolS = map[ast.Alignment]*sxpf.Symbol{ ast.AlignDefault: sexpr.SymCell, ast.AlignLeft: sexpr.SymCellLeft, ast.AlignCenter: sexpr.SymCellCenter, ast.AlignRight: sexpr.SymCellRight, } func (t *transformer) getCell(cell *ast.TableCell) *sxpf.Pair { return sxpf.NewPair(mapGetS(alignmentSymbolS, cell.Align), t.getInlineSlice(cell.Inlines)) } func getBLOB(bn *ast.BLOBNode) *sxpf.Pair { var lastValue sxpf.Value if bn.Syntax == api.ValueSyntaxSVG { lastValue = sxpf.NewString(string(bn.Blob)) } else { lastValue = getBase64String(bn.Blob) } return sxpf.NewPairFromValues( sexpr.SymBLOB, sxpf.NewString(bn.Title), sxpf.NewString(bn.Syntax), lastValue, ) } var mapRefStateLink = map[ast.RefState]*sxpf.Symbol{ ast.RefStateInvalid: sexpr.SymLinkInvalid, ast.RefStateZettel: sexpr.SymLinkZettel, ast.RefStateSelf: sexpr.SymLinkSelf, ast.RefStateFound: sexpr.SymLinkFound, ast.RefStateBroken: sexpr.SymLinkBroken, ast.RefStateHosted: sexpr.SymLinkHosted, ast.RefStateBased: sexpr.SymLinkBased, ast.RefStateQuery: sexpr.SymLinkQuery, ast.RefStateExternal: sexpr.SymLinkExternal, } func (t *transformer) getLink(ln *ast.LinkNode) *sxpf.Pair { return sxpf.NewPair( mapGetS(mapRefStateLink, ln.Ref.State), sxpf.NewPair( getAttributes(ln.Attrs), sxpf.NewPair( sxpf.NewString(ln.Ref.Value), t.getInlineSlice(ln.Inlines), ), ), ) } func (t *transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sxpf.Pair { tail := t.getInlineSlice(en.Inlines) if en.Syntax == api.ValueSyntaxSVG { tail = sxpf.NewPair(sxpf.NewString(string(en.Blob)), tail) } else { tail = sxpf.NewPair(getBase64String(en.Blob), tail) } return sxpf.NewPair( sexpr.SymEmbedBLOB, sxpf.NewPair( getAttributes(en.Attrs), sxpf.NewPair( sxpf.NewString(en.Syntax), tail, ), ), ) } var mapFormatKindS = map[ast.FormatKind]*sxpf.Symbol{ ast.FormatEmph: sexpr.SymFormatEmph, ast.FormatStrong: sexpr.SymFormatStrong, ast.FormatDelete: sexpr.SymFormatDelete, ast.FormatInsert: sexpr.SymFormatInsert, ast.FormatSuper: sexpr.SymFormatSuper, ast.FormatSub: sexpr.SymFormatSub, ast.FormatQuote: sexpr.SymFormatQuote, ast.FormatSpan: sexpr.SymFormatSpan, } var mapLiteralKindS = map[ast.LiteralKind]*sxpf.Symbol{ ast.LiteralZettel: sexpr.SymLiteralZettel, ast.LiteralProg: sexpr.SymLiteralProg, ast.LiteralInput: sexpr.SymLiteralInput, ast.LiteralOutput: sexpr.SymLiteralOutput, ast.LiteralComment: sexpr.SymLiteralComment, ast.LiteralHTML: sexpr.SymLiteralHTML, ast.LiteralMath: sexpr.SymLiteralMath, } func (t *transformer) getBlockSlice(bs *ast.BlockSlice) *sxpf.Pair { lstVals := make([]sxpf.Value, len(*bs)) for i, n := range *bs { lstVals[i] = t.getSexpr(n) } return sxpf.NewPairFromSlice(lstVals) } func (t *transformer) getInlineSlice(is ast.InlineSlice) *sxpf.Pair { lstVals := make([]sxpf.Value, len(is)) for i, n := range is { lstVals[i] = t.getSexpr(n) } return sxpf.NewPairFromSlice(lstVals) } func getAttributes(a attrs.Attributes) sxpf.Value { if a.IsEmpty() { return sxpf.Nil() } keys := a.Keys() lstVals := make([]sxpf.Value, 0, len(keys)) for _, k := range keys { lstVals = append(lstVals, sxpf.NewPair(sxpf.NewString(k), sxpf.NewPair(sxpf.NewString(a[k]), nil))) } return sxpf.NewPairFromSlice(lstVals) } var mapRefStateS = map[ast.RefState]*sxpf.Symbol{ ast.RefStateInvalid: sexpr.SymRefStateInvalid, ast.RefStateZettel: sexpr.SymRefStateZettel, ast.RefStateSelf: sexpr.SymRefStateSelf, ast.RefStateFound: sexpr.SymRefStateFound, ast.RefStateBroken: sexpr.SymRefStateBroken, ast.RefStateHosted: sexpr.SymRefStateHosted, ast.RefStateBased: sexpr.SymRefStateBased, ast.RefStateQuery: sexpr.SymRefStateQuery, ast.RefStateExternal: sexpr.SymRefStateExternal, } func getReference(ref *ast.Reference) *sxpf.Pair { return sxpf.NewPair( mapGetS(mapRefStateS, ref.State), sxpf.NewPair( sxpf.NewString(ref.Value), sxpf.Nil())) } var mapMetaTypeS = map[*meta.DescriptionType]*sxpf.Symbol{ meta.TypeCredential: sexpr.SymTypeCredential, meta.TypeEmpty: sexpr.SymTypeEmpty, meta.TypeID: sexpr.SymTypeID, meta.TypeIDSet: sexpr.SymTypeIDSet, meta.TypeNumber: sexpr.SymTypeNumber, meta.TypeString: sexpr.SymTypeString, meta.TypeTagSet: sexpr.SymTypeTagSet, meta.TypeTimestamp: sexpr.SymTypeTimestamp, meta.TypeURL: sexpr.SymTypeURL, meta.TypeWord: sexpr.SymTypeWord, meta.TypeWordSet: sexpr.SymTypeWordSet, meta.TypeZettelmarkup: sexpr.SymTypeZettelmarkup, } func GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sxpf.Pair { pairs := m.ComputedPairs() lstVals := make([]sxpf.Value, 0, len(pairs)) for _, p := range pairs { key := p.Key ty := m.Type(key) symType := mapGetS(mapMetaTypeS, ty) strKey := sxpf.NewString(key) var val sxpf.Value if ty.IsSet { setList := meta.ListFromValue(p.Value) setVals := make([]sxpf.Value, len(setList)) for i, val := range setList { setVals[i] = sxpf.NewString(val) } val = sxpf.NewPairFromSlice(setVals) } else if ty == meta.TypeZettelmarkup { is := evalMeta(p.Value) t := transformer{} val = t.getSexpr(&is) } else { val = sxpf.NewString(p.Value) } lstVals = append(lstVals, sxpf.NewPair(symType, sxpf.NewPair(strKey, sxpf.NewPair(val, nil)))) } return sxpf.NewPairFromSlice(lstVals) } func mapGetS[T comparable](m map[T]*sxpf.Symbol, k T) *sxpf.Symbol { if result, found := m[k]; found { return result } log.Println("MISS", k, m) return sexpr.Smk.MakeSymbol(fmt.Sprintf("**%v:NOT-FOUND**", k)) } func getBase64String(data []byte) *sxpf.String { var buf bytes.Buffer encoder := base64.NewEncoder(base64.StdEncoding, &buf) _, err := encoder.Write(data) if err == nil { err = encoder.Close() } if err == nil { return sxpf.NewString(buf.String()) } return nil } |
Deleted encoder/shtmlenc/shtmlenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/szenc/szenc.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/szenc/transform.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to encoder/textenc/textenc.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package textenc encodes the abstract syntax tree into its text. package textenc import ( "io" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderText, func() encoder.Encoder { return Create() }) } // Create an encoder. func Create() *Encoder { return &myTE } type Encoder struct{} |
︙ | ︙ |
Changes to encoder/write.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package encoder import ( "encoding/base64" "io" |
︙ | ︙ |
Added encoder/zjsonenc/zjsonenc.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zjsonenc encodes the abstract syntax tree into JSON. package zjsonenc import ( "fmt" "io" "strconv" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/c/zjson" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) func init() { encoder.Register(api.EncoderZJSON, func() encoder.Encoder { return Create() }) } // Create a ZJSON encoder func Create() *Encoder { return &myJE } type Encoder struct{} var myJE Encoder // WriteZettel writes the encoded zettel to the writer. func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { v := newDetailVisitor(w) v.b.WriteString(`{"meta":`) v.writeMeta(zn.InhMeta, evalMeta) v.b.WriteString(`,"content":`) ast.Walk(v, &zn.Ast) v.b.WriteByte('}') length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as JSON. func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { v := newDetailVisitor(w) v.writeMeta(m, evalMeta) length, err := v.b.Flush() return length, err } func (je *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return je.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes a block slice to the writer func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { v := newDetailVisitor(w) ast.Walk(v, bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { v := newDetailVisitor(w) ast.Walk(v, is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.EncWriter inVerse bool // Visiting a verse block: save spaces in ZJSON object } func newDetailVisitor(w io.Writer) *visitor { return &visitor{b: encoder.NewEncWriter(w)} } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: v.visitBlockSlice(n) return nil case *ast.InlineSlice: v.walkInlineSlice(n) return nil case *ast.ParaNode: v.writeNodeStart(zjson.TypeParagraph) v.writeContentStart(zjson.NameInline) ast.Walk(v, &n.Inlines) case *ast.VerbatimNode: v.visitVerbatim(n) case *ast.RegionNode: v.visitRegion(n) case *ast.HeadingNode: v.visitHeading(n) case *ast.HRuleNode: v.writeNodeStart(zjson.TypeBreakThematic) v.visitAttributes(n.Attrs) case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.TranscludeNode: v.writeNodeStart(zjson.TypeTransclude) v.visitAttributes(n.Attrs) v.writeContentStart(zjson.NameString2) writeEscaped(&v.b, mapRefState[n.Ref.State]) v.writeContentStart(zjson.NameString) writeEscaped(&v.b, n.Ref.String()) case *ast.BLOBNode: v.visitBLOB(n) case *ast.TextNode: v.writeNodeStart(zjson.TypeText) v.writeContentStart(zjson.NameString) writeEscaped(&v.b, n.Text) case *ast.SpaceNode: v.writeNodeStart(zjson.TypeSpace) if v.inVerse { v.writeContentStart(zjson.NameString) writeEscaped(&v.b, n.Lexeme) } case *ast.BreakNode: if n.Hard { v.writeNodeStart(zjson.TypeBreakHard) } else { v.writeNodeStart(zjson.TypeBreakSoft) } case *ast.LinkNode: v.visitLink(n) case *ast.EmbedRefNode: v.visitEmbedRef(n) case *ast.EmbedBLOBNode: v.visitEmbedBLOB(n) case *ast.CiteNode: v.writeNodeStart(zjson.TypeCitation) v.visitAttributes(n.Attrs) v.writeContentStart(zjson.NameString) writeEscaped(&v.b, n.Key) if len(n.Inlines) > 0 { v.writeContentStart(zjson.NameInline) ast.Walk(v, &n.Inlines) } case *ast.FootnoteNode: v.writeNodeStart(zjson.TypeFootnote) v.visitAttributes(n.Attrs) v.writeContentStart(zjson.NameInline) ast.Walk(v, &n.Inlines) case *ast.MarkNode: v.visitMark(n) case *ast.FormatNode: v.writeNodeStart(mapFormatKind[n.Kind]) v.visitAttributes(n.Attrs) v.writeContentStart(zjson.NameInline) ast.Walk(v, &n.Inlines) case *ast.LiteralNode: kind, ok := mapLiteralKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown literal kind %v", n.Kind)) } v.writeNodeStart(kind) v.visitAttributes(n.Attrs) v.writeContentStart(zjson.NameString) writeEscaped(&v.b, string(n.Content)) default: return v } v.b.WriteByte('}') return nil } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: zjson.TypeVerbatimZettel, ast.VerbatimProg: zjson.TypeVerbatimCode, ast.VerbatimEval: zjson.TypeVerbatimEval, ast.VerbatimMath: zjson.TypeVerbatimMath, ast.VerbatimComment: zjson.TypeVerbatimComment, ast.VerbatimHTML: zjson.TypeVerbatimHTML, } func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { kind, ok := mapVerbatimKind[vn.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim kind %v", vn.Kind)) } v.writeNodeStart(kind) v.visitAttributes(vn.Attrs) v.writeContentStart(zjson.NameString) writeEscaped(&v.b, string(vn.Content)) } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: zjson.TypeBlock, ast.RegionQuote: zjson.TypeExcerpt, ast.RegionVerse: zjson.TypePoem, } func (v *visitor) visitRegion(rn *ast.RegionNode) { kind, ok := mapRegionKind[rn.Kind] if !ok { panic(fmt.Sprintf("Unknown region kind %v", rn.Kind)) } saveInVerse := v.inVerse if rn.Kind == ast.RegionVerse { v.inVerse = true } v.writeNodeStart(kind) v.visitAttributes(rn.Attrs) v.writeContentStart(zjson.NameBlock) ast.Walk(v, &rn.Blocks) if len(rn.Inlines) > 0 { v.writeContentStart(zjson.NameInline) ast.Walk(v, &rn.Inlines) } v.inVerse = saveInVerse } func (v *visitor) visitHeading(hn *ast.HeadingNode) { v.writeNodeStart(zjson.TypeHeading) v.visitAttributes(hn.Attrs) v.writeContentStart(zjson.NameNumeric) v.b.WriteString(strconv.Itoa(hn.Level)) if fragment := hn.Fragment; fragment != "" { v.writeContentStart(zjson.NameString) v.b.WriteStrings(`"`, fragment, `"`) } v.writeContentStart(zjson.NameInline) ast.Walk(v, &hn.Inlines) } var mapNestedListKind = map[ast.NestedListKind]string{ ast.NestedListOrdered: zjson.TypeListOrdered, ast.NestedListUnordered: zjson.TypeListBullet, ast.NestedListQuote: zjson.TypeListQuotation, } func (v *visitor) visitNestedList(ln *ast.NestedListNode) { v.writeNodeStart(mapNestedListKind[ln.Kind]) v.writeContentStart(zjson.NameList) for i, item := range ln.Items { v.writeComma(i) v.b.WriteByte('[') for j, in := range item { v.writeComma(j) ast.Walk(v, in) } v.b.WriteByte(']') } v.b.WriteByte(']') } func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) { v.writeNodeStart(zjson.TypeDescrList) v.writeContentStart(zjson.NameDescrList) for i, def := range dn.Descriptions { v.writeComma(i) v.b.WriteStrings(`{"`, zjson.NameInline, `":`) ast.Walk(v, &def.Term) if len(def.Descriptions) > 0 { v.writeContentStart(zjson.NameDescription) for j, b := range def.Descriptions { v.writeComma(j) v.b.WriteByte('[') for k, dn := range b { v.writeComma(k) ast.Walk(v, dn) } v.b.WriteByte(']') } v.b.WriteByte(']') } v.b.WriteByte('}') } v.b.WriteByte(']') } func (v *visitor) visitTable(tn *ast.TableNode) { v.writeNodeStart(zjson.TypeTable) v.writeContentStart(zjson.NameTable) // Table header v.b.WriteByte('[') for i, cell := range tn.Header { v.writeComma(i) v.writeCell(cell) } v.b.WriteString("],") // Table rows v.b.WriteByte('[') for i, row := range tn.Rows { v.writeComma(i) v.b.WriteByte('[') for j, cell := range row { v.writeComma(j) v.writeCell(cell) } v.b.WriteByte(']') } v.b.WriteString("]]") } var alignmentCode = map[ast.Alignment]string{ ast.AlignDefault: "", ast.AlignLeft: "<", ast.AlignCenter: ":", ast.AlignRight: ">", } func (v *visitor) writeCell(cell *ast.TableCell) { if aCode := alignmentCode[cell.Align]; aCode != "" { v.b.WriteStrings(`{"`, zjson.NameString, `":"`, aCode, `","`, zjson.NameInline, `":`) } else { v.b.WriteStrings(`{"`, zjson.NameInline, `":`) } ast.Walk(v, &cell.Inlines) v.b.WriteByte('}') } func (v *visitor) visitBLOB(bn *ast.BLOBNode) { v.writeNodeStart(zjson.TypeBLOB) if bn.Title != "" { v.writeContentStart(zjson.NameString2) writeEscaped(&v.b, bn.Title) } v.writeContentStart(zjson.NameString) writeEscaped(&v.b, bn.Syntax) if bn.Syntax == api.ValueSyntaxSVG { v.writeContentStart(zjson.NameString3) writeEscaped(&v.b, string(bn.Blob)) } else { v.writeContentStart(zjson.NameBinary) v.b.WriteBase64(bn.Blob) v.b.WriteByte('"') } } var mapRefState = map[ast.RefState]string{ ast.RefStateInvalid: zjson.RefStateInvalid, ast.RefStateZettel: zjson.RefStateZettel, ast.RefStateSelf: zjson.RefStateSelf, ast.RefStateFound: zjson.RefStateFound, ast.RefStateBroken: zjson.RefStateBroken, ast.RefStateHosted: zjson.RefStateHosted, ast.RefStateBased: zjson.RefStateBased, ast.RefStateQuery: zjson.RefStateQuery, ast.RefStateExternal: zjson.RefStateExternal, } func (v *visitor) visitLink(ln *ast.LinkNode) { v.writeNodeStart(zjson.TypeLink) v.visitAttributes(ln.Attrs) v.writeContentStart(zjson.NameString2) writeEscaped(&v.b, mapRefState[ln.Ref.State]) v.writeContentStart(zjson.NameString) if ln.Ref.State == ast.RefStateQuery { writeEscaped(&v.b, ln.Ref.Value) } else { writeEscaped(&v.b, ln.Ref.String()) } if len(ln.Inlines) > 0 { v.writeContentStart(zjson.NameInline) ast.Walk(v, &ln.Inlines) } } func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) { v.writeNodeStart(zjson.TypeEmbed) v.visitAttributes(en.Attrs) v.writeContentStart(zjson.NameString) writeEscaped(&v.b, en.Ref.String()) if len(en.Inlines) > 0 { v.writeContentStart(zjson.NameInline) ast.Walk(v, &en.Inlines) } if en.Syntax != "" { v.writeContentStart(zjson.NameString2) writeEscaped(&v.b, en.Syntax) } } func (v *visitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) { v.writeNodeStart(zjson.TypeEmbedBLOB) v.visitAttributes(en.Attrs) v.writeContentStart(zjson.NameString) writeEscaped(&v.b, en.Syntax) if en.Syntax == api.ValueSyntaxSVG { v.writeContentStart(zjson.NameString3) writeEscaped(&v.b, string(en.Blob)) } else { v.writeContentStart(zjson.NameBinary) v.b.WriteBase64(en.Blob) v.b.WriteByte('"') } if len(en.Inlines) > 0 { v.writeContentStart(zjson.NameInline) ast.Walk(v, &en.Inlines) } } func (v *visitor) visitMark(mn *ast.MarkNode) { v.writeNodeStart(zjson.TypeMark) if text := mn.Mark; text != "" { v.writeContentStart(zjson.NameString) writeEscaped(&v.b, text) } if fragment := mn.Fragment; fragment != "" { v.writeContentStart(zjson.NameString2) v.b.WriteByte('"') v.b.WriteString(fragment) v.b.WriteByte('"') } if len(mn.Inlines) > 0 { v.writeContentStart(zjson.NameInline) ast.Walk(v, &mn.Inlines) } } var mapFormatKind = map[ast.FormatKind]string{ ast.FormatEmph: zjson.TypeFormatEmph, ast.FormatStrong: zjson.TypeFormatStrong, ast.FormatDelete: zjson.TypeFormatDelete, ast.FormatInsert: zjson.TypeFormatInsert, ast.FormatSuper: zjson.TypeFormatSuper, ast.FormatSub: zjson.TypeFormatSub, ast.FormatQuote: zjson.TypeFormatQuote, ast.FormatSpan: zjson.TypeFormatSpan, } var mapLiteralKind = map[ast.LiteralKind]string{ ast.LiteralZettel: zjson.TypeLiteralZettel, ast.LiteralProg: zjson.TypeLiteralCode, ast.LiteralInput: zjson.TypeLiteralInput, ast.LiteralOutput: zjson.TypeLiteralOutput, ast.LiteralComment: zjson.TypeLiteralComment, ast.LiteralHTML: zjson.TypeLiteralHTML, ast.LiteralMath: zjson.TypeLiteralMath, } func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) { v.b.WriteByte('[') for i, bn := range *bs { v.writeComma(i) ast.Walk(v, bn) } v.b.WriteByte(']') } func (v *visitor) walkInlineSlice(is *ast.InlineSlice) { v.b.WriteByte('[') for i, in := range *is { v.writeComma(i) ast.Walk(v, in) } v.b.WriteByte(']') } // visitAttributes write JSON attributes func (v *visitor) visitAttributes(a attrs.Attributes) { if a.IsEmpty() { return } v.writeContentStart(zjson.NameAttribute) for i, k := range a.Keys() { if i > 0 { v.b.WriteString(`","`) } strfun.JSONEscape(&v.b, k) v.b.WriteString(`":"`) strfun.JSONEscape(&v.b, a[k]) } v.b.WriteString(`"}`) } func (v *visitor) writeNodeStart(t string) { v.b.WriteStrings(`{"":"`, t, `"`) } var valueStart = map[string]string{ zjson.NameBlock: "", zjson.NameAttribute: `{"`, zjson.NameList: "[", zjson.NameDescrList: "[", zjson.NameDescription: "[", zjson.NameInline: "", zjson.NameBLOB: "{", zjson.NameNumeric: "", zjson.NameBinary: `"`, zjson.NameTable: "[", zjson.NameString2: "", zjson.NameString: "", zjson.NameString3: "", } func (v *visitor) writeContentStart(jsonName string) { s, ok := valueStart[jsonName] if !ok { panic("Unknown object name " + jsonName) } v.b.WriteStrings(`,"`, jsonName, `":`, s) } func (v *visitor) writeMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) { v.b.WriteByte('{') for i, p := range m.ComputedPairs() { if i > 0 { v.b.WriteByte(',') } v.b.WriteByte('"') key := p.Key strfun.JSONEscape(&v.b, key) t := m.Type(key) v.b.WriteStrings(`":{"`, zjson.NameType, `":"`, t.Name, `","`) if t.IsSet { v.b.WriteStrings(zjson.NameSet, `":`) v.writeSetValue(p.Value) } else if t == meta.TypeZettelmarkup { v.b.WriteStrings(zjson.NameInline, `":`) is := evalMeta(p.Value) ast.Walk(v, &is) } else { v.b.WriteStrings(zjson.NameString, `":`) writeEscaped(&v.b, p.Value) } v.b.WriteByte('}') } v.b.WriteByte('}') } func (v *visitor) writeSetValue(value string) { v.b.WriteByte('[') for i, val := range meta.ListFromValue(value) { v.writeComma(i) writeEscaped(&v.b, val) } v.b.WriteByte(']') } func (v *visitor) writeComma(pos int) { if pos > 0 { v.b.WriteByte(',') } } func writeEscaped(b *encoder.EncWriter, s string) { b.WriteByte('"') strfun.JSONEscape(b, s) b.WriteByte('"') } |
Changes to encoder/zmkenc/zmkenc.go.
1 | //----------------------------------------------------------------------------- | | < < < < | < | | | | | | < < < | | | | | | | < | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zmkenc encodes the abstract syntax tree back into Zettelmarkup. package zmkenc import ( "fmt" "io" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) func init() { encoder.Register(api.EncoderZmk, func() encoder.Encoder { return &myZE }) } type zmkEncoder struct{} var myZE zmkEncoder // WriteZettel writes the encoded zettel to the writer. func (*zmkEncoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { v := newVisitor(w) v.acceptMeta(zn.InhMeta, evalMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { v.b.WriteByte('\n') } ast.Walk(v, &zn.Ast) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as zmk. func (*zmkEncoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { v := newVisitor(w) v.acceptMeta(m, evalMeta) length, err := v.b.Flush() return length, err } func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) { for _, p := range m.ComputedPairs() { key := p.Key v.b.WriteStrings(key, ": ") if meta.Type(key) == meta.TypeZettelmarkup { is := evalMeta(p.Value) ast.Walk(v, &is) } else { v.b.WriteString(p.Value) } v.b.WriteByte('\n') } } func (ze *zmkEncoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return ze.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes the content of a block slice to the writer. func (*zmkEncoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { v := newVisitor(w) ast.Walk(v, bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer func (*zmkEncoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { v := newVisitor(w) ast.Walk(v, is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an io.Writer. type visitor struct { b encoder.EncWriter prefix []byte inVerse bool inlinePos int } func newVisitor(w io.Writer) *visitor { return &visitor{b: encoder.NewEncWriter(w)} } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: v.visitBlockSlice(n) case *ast.InlineSlice: |
︙ | ︙ | |||
271 272 273 274 275 276 277 | ast.Walk(v, in) } } v.prefix = v.prefix[:len(v.prefix)-1] } func (v *visitor) writePrefixSpaces() { | < | | < < < < < | < | 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 | ast.Walk(v, in) } } v.prefix = v.prefix[:len(v.prefix)-1] } func (v *visitor) writePrefixSpaces() { for i := 0; i <= len(v.prefix); i++ { v.b.WriteByte(' ') } } func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) { for i, descr := range dn.Descriptions { if i > 0 { v.b.WriteByte('\n') } v.b.WriteString("; ") ast.Walk(v, &descr.Term) for _, b := range descr.Descriptions { v.b.WriteString("\n: ") ast.WalkDescriptionSlice(v, b) } } } var alignCode = map[ast.Alignment]string{ ast.AlignDefault: "", ast.AlignLeft: "<", |
︙ | ︙ | |||
343 344 345 346 347 348 349 | v.b.WriteString(alignCode[cell.Align]) } ast.Walk(v, &cell.Inlines) } } func (v *visitor) visitBLOB(bn *ast.BLOBNode) { | | | < | | | > > | > > < < < | | | 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 | v.b.WriteString(alignCode[cell.Align]) } ast.Walk(v, &cell.Inlines) } } func (v *visitor) visitBLOB(bn *ast.BLOBNode) { if bn.Syntax == api.ValueSyntaxSVG { v.b.WriteStrings("@@@", bn.Syntax, "\n") v.b.Write(bn.Blob) v.b.WriteString("\n@@@\n") return } v.b.WriteStrings( "%% Unable to display BLOB with title '", bn.Title, "' and syntax '", bn.Syntax, "'.") } var escapeSeqs = strfun.NewSet( "\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", ) func (v *visitor) visitText(tn *ast.TextNode) { last := 0 for i := 0; i < len(tn.Text); i++ { if b := tn.Text[i]; b == '\\' { v.b.WriteString(tn.Text[last:i]) v.b.WriteBytes('\\', b) last = i + 1 continue } if i < len(tn.Text)-1 { s := tn.Text[i : i+2] if escapeSeqs.Has(s) { v.b.WriteString(tn.Text[last:i]) for j := 0; j < len(s); j++ { v.b.WriteBytes('\\', s[j]) } i++ last = i + 1 continue } } } v.b.WriteString(tn.Text[last:]) } func (v *visitor) visitBreak(bn *ast.BreakNode) { if bn.Hard { v.b.WriteString("\\\n") } else { v.b.WriteByte('\n') } if prefixLen := len(v.prefix); prefixLen > 0 { for i := 0; i <= prefixLen; i++ { v.b.WriteByte(' ') } } } func (v *visitor) visitLink(ln *ast.LinkNode) { v.b.WriteString("[[") if len(ln.Inlines) > 0 { ast.Walk(v, &ln.Inlines) v.b.WriteByte('|') } v.b.WriteStrings(ln.Ref.String(), "]]") } func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) { v.b.WriteString("{{") if len(en.Inlines) > 0 { ast.Walk(v, &en.Inlines) v.b.WriteByte('|') } v.b.WriteStrings(en.Ref.String(), "}}") } func (v *visitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) { if en.Syntax == api.ValueSyntaxSVG { v.b.WriteString("@@") v.b.Write(en.Blob) v.b.WriteStrings("@@{=", en.Syntax, "}") return } v.b.WriteString("{{TODO: display inline BLOB}}") } func (v *visitor) visitCite(cn *ast.CiteNode) { v.b.WriteStrings("[@", cn.Key) if len(cn.Inlines) > 0 { v.b.WriteString(", ") ast.Walk(v, &cn.Inlines) } v.b.WriteByte(']') v.visitAttributes(cn.Attrs) } func (v *visitor) visitMark(mn *ast.MarkNode) { |
︙ | ︙ | |||
451 452 453 454 455 456 457 | ast.FormatEmph: []byte("__"), ast.FormatStrong: []byte("**"), ast.FormatInsert: []byte(">>"), ast.FormatDelete: []byte("~~"), ast.FormatSuper: []byte("^^"), ast.FormatSub: []byte(",,"), ast.FormatQuote: []byte(`""`), | < | 435 436 437 438 439 440 441 442 443 444 445 446 447 448 | ast.FormatEmph: []byte("__"), ast.FormatStrong: []byte("**"), ast.FormatInsert: []byte(">>"), ast.FormatDelete: []byte("~~"), ast.FormatSuper: []byte("^^"), ast.FormatSub: []byte(",,"), ast.FormatQuote: []byte(`""`), ast.FormatSpan: []byte("::"), } func (v *visitor) visitFormat(fn *ast.FormatNode) { kind, ok := mapFormatKind[fn.Kind] if !ok { panic(fmt.Sprintf("Unknown format kind %d", fn.Kind)) |
︙ | ︙ | |||
526 527 528 529 530 531 532 | } } v.b.WriteByte('}') } func (v *visitor) writeEscaped(s string, toEscape byte) { last := 0 | | | | 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 | } } v.b.WriteByte('}') } func (v *visitor) writeEscaped(s string, toEscape byte) { last := 0 for i := 0; i < len(s); i++ { if b := s[i]; b == toEscape || b == '\\' { v.b.WriteString(s[last:i]) v.b.WriteBytes('\\', b) last = i + 1 } } v.b.WriteString(s[last:]) } func syntaxToHTML(a attrs.Attributes) attrs.Attributes { return a.Clone().Set("", api.ValueSyntaxHTML).Remove(api.KeySyntax) } |
Changes to encoding/atom/atom.go.
1 | //----------------------------------------------------------------------------- | | < < < > | > > < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package atom provides an Atom encoding. package atom import ( "bytes" "context" "time" "zettelstore.de/c/api" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoding" "zettelstore.de/z/encoding/xml" "zettelstore.de/z/kernel" "zettelstore.de/z/query" "zettelstore.de/z/strfun" ) const ContentType = "application/atom+xml" type Configuration struct { Title string Generator string NewURLBuilderAbs func() *api.URLBuilder } func (c *Configuration) Setup(ctx context.Context, cfg config.Config) { baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string) c.Title = cfg.GetSiteName() c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) + " " + kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') } |
︙ | ︙ | |||
65 66 67 68 69 70 71 | if atomUpdated != "" { xml.WriteTag(&buf, " ", "updated", atomUpdated) } xml.WriteTag(&buf, " ", "generator", c.Generator) buf.WriteString(" <author><name>Unknown</name></author>\n") for _, m := range ml { | > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 | if atomUpdated != "" { xml.WriteTag(&buf, " ", "updated", atomUpdated) } xml.WriteTag(&buf, " ", "generator", c.Generator) buf.WriteString(" <author><name>Unknown</name></author>\n") for _, m := range ml { entryUpdated := "" if val, found := m.Get(api.KeyPublished); found { if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil { entryUpdated = published.UTC().Format(time.RFC3339) } } link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String() buf.WriteString(" <entry>\n") xml.WriteTag(&buf, " ", "title", encoding.TitleAsText(m)) xml.WriteTag(&buf, " ", "id", link) buf.WriteString(` <link rel="self" href="`) strfun.XMLEscape(&buf, link) buf.WriteString(`"/>` + "\n") buf.WriteString(` <link rel="alternate" type="text/html" href="`) strfun.XMLEscape(&buf, link) buf.WriteString(`"/>` + "\n") if entryUpdated != "" { xml.WriteTag(&buf, " ", "updated", entryUpdated) } if tags, found := m.GetList(api.KeyTags); found && len(tags) > 0 { for _, tag := range tags { for len(tag) > 0 && tag[0] == '#' { tag = tag[1:] } if tag != "" { buf.WriteString(` <category term="`) strfun.XMLEscape(&buf, tag) buf.WriteString("\"/>\n") } } } buf.WriteString(" </entry>\n") } buf.WriteString("</feed>") return buf.Bytes() } |
Changes to encoding/encoding.go.
1 | //----------------------------------------------------------------------------- | | < < < > | | | > | | > > | > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed 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 encoding provides helper functions for encodings. package encoding import ( "bytes" "time" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/parser" ) // LastUpdated returns the formated time of the zettel which was updated at the latest time. func LastUpdated(ml []*meta.Meta, timeFormat string) string { maxPublished := time.Date(1, time.January, 1, 0, 0, 0, 0, time.Local) for _, m := range ml { if val, found := m.Get(api.KeyPublished); found { if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil { if maxPublished.Before(published) { maxPublished = published } } } } if maxPublished.Year() > 1 { return maxPublished.UTC().Format(timeFormat) } return "" } var textEnc = textenc.Create() // TitleAsText returns the title of a zettel as plain text func TitleAsText(m *meta.Meta) string { var title bytes.Buffer titleIns := parser.ParseMetadata(m.GetTitle()) if _, err := textEnc.WriteInlines(&title, &titleIns); err != nil { return m.GetTitle() } return title.String() } |
Changes to encoding/rss/rss.go.
1 | //----------------------------------------------------------------------------- | | < < < | > > < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package rss provides a RSS encoding. package rss import ( "bytes" "context" "time" "zettelstore.de/c/api" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoding" "zettelstore.de/z/encoding/xml" "zettelstore.de/z/kernel" "zettelstore.de/z/query" "zettelstore.de/z/strfun" ) const ContentType = "application/rss+xml" type Configuration struct { Title string Language string |
︙ | ︙ | |||
75 76 77 78 79 80 81 | buf.WriteString(" <docs>https://www.rssboard.org/rss-specification</docs>\n") if atomLink != "" { buf.WriteString(` <atom:link href="`) strfun.XMLEscape(&buf, atomLink) buf.WriteString(`" rel="self" type="application/rss+xml"></atom:link>` + "\n") } for _, m := range ml { | > > > > > > | > > > > > > > > > > > > > > > > > > > > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 | buf.WriteString(" <docs>https://www.rssboard.org/rss-specification</docs>\n") if atomLink != "" { buf.WriteString(` <atom:link href="`) strfun.XMLEscape(&buf, atomLink) buf.WriteString(`" rel="self" type="application/rss+xml"></atom:link>` + "\n") } for _, m := range ml { itemPublished := "" if val, found := m.Get(api.KeyPublished); found { if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil { itemPublished = published.UTC().Format(time.RFC1123Z) } } link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String() buf.WriteString(" <item>\n") xml.WriteTag(&buf, " ", "title", encoding.TitleAsText(m)) xml.WriteTag(&buf, " ", "link", link) xml.WriteTag(&buf, " ", "guid", link) if itemPublished != "" { xml.WriteTag(&buf, " ", "pubDate", itemPublished) } if tags, found := m.GetList(api.KeyTags); found && len(tags) > 0 { for _, tag := range tags { for len(tag) > 0 && tag[0] == '#' { tag = tag[1:] } if tag != "" { xml.WriteTag(&buf, " ", "category", tag) } } } buf.WriteString(" </item>\n") } buf.WriteString("</channel>\n</rss>") return buf.Bytes() } |
Changes to encoding/xml/xml.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package xml provides helper for a XML-based encoding. package xml import ( "bytes" |
︙ | ︙ |
Changes to evaluator/evaluator.go.
1 | //----------------------------------------------------------------------------- | | < < < < < < | | < > > > > < < < > | | | < < < < < < < < < < < < < < < < < < < < < < < < < < | | < < < < > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package evaluator interprets and evaluates the AST. package evaluator import ( "context" "errors" "fmt" "path" "strconv" "strings" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/parser/cleaner" "zettelstore.de/z/parser/draw" "zettelstore.de/z/query" ) // Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel. type Port interface { GetMeta(context.Context, id.Zid) (*meta.Meta, error) GetZettel(context.Context, id.Zid) (domain.Zettel, error) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // EvaluateZettel evaluates the given zettel in the given context, with the // given ports, and the given environment. func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) { if zn.Syntax == api.ValueSyntaxNone { // AST is empty, evaluate to a description list of metadata. zn.Ast = evaluateMetadata(zn.Meta) return } EvaluateBlock(ctx, port, rtConfig, &zn.Ast) } // EvaluateBlock evaluates the given block list in the given context, with // the given ports, and the given environment. func EvaluateBlock(ctx context.Context, port Port, rtConfig config.Config, bns *ast.BlockSlice) { evaluateNode(ctx, port, rtConfig, bns) cleaner.CleanBlockSlice(bns, true) |
︙ | ︙ | |||
187 188 189 190 191 192 193 | } func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode { switch vn.Kind { case ast.VerbatimZettel: return e.evalVerbatimZettel(vn) case ast.VerbatimEval: | | | | | | 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 | } func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode { switch vn.Kind { case ast.VerbatimZettel: return e.evalVerbatimZettel(vn) case ast.VerbatimEval: if syntax, found := vn.Attrs.Get(""); found && syntax == ast.VerbatimEvalSyntaxDraw { return draw.ParseDrawBlock(vn) } } return vn } func (e *evaluator) evalVerbatimZettel(vn *ast.VerbatimNode) ast.BlockNode { m := meta.New(id.Invalid) m.Set(api.KeySyntax, getSyntax(vn.Attrs, api.ValueSyntaxText)) zettel := domain.Zettel{ Meta: m, Content: domain.NewContent(vn.Content), } e.transcludeCount++ zn := e.evaluateEmbeddedZettel(zettel) return &zn.Ast } func getSyntax(a attrs.Attributes, defSyntax string) string { |
︙ | ︙ | |||
285 286 287 288 289 290 291 | e.transcludeCount += cost.ec } return &zn.Ast } func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode { q := query.Parse(expr) | | | | 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | e.transcludeCount += cost.ec } return &zn.Ast } func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode { q := query.Parse(expr) ml, err := e.port.SelectMeta(e.ctx, q) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return nil } return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel")) } result := QueryAction(e.ctx, q, ml, e.rtConfig) if result != nil { ast.Walk(e, result) } return result } func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode { |
︙ | ︙ | |||
375 376 377 378 379 380 381 | } ref := ln.Ref if ref == nil || ref.State != ast.RefStateZettel { return ln } zid := mustParseZid(ref) | | | 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 | } ref := ln.Ref if ref == nil || ref.State != ast.RefStateZettel { return ln } zid := mustParseZid(ref) _, err := e.port.GetMeta(box.NoEnrichContext(e.ctx), zid) if errors.Is(err, &box.ErrNotAllowed{}) { return &ast.FormatNode{ Kind: ast.FormatSpan, Attrs: ln.Attrs, Inlines: getLinkInline(ln), } } else if err != nil { |
︙ | ︙ | |||
438 439 440 441 442 443 444 | if errors.Is(err, &box.ErrNotAllowed{}) { return nil } e.transcludeCount++ return createInlineErrorImage(en) } | | | | | 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 | if errors.Is(err, &box.ErrNotAllowed{}) { return nil } e.transcludeCount++ return createInlineErrorImage(en) } if syntax := zettel.Meta.GetDefault(api.KeySyntax, ""); parser.IsImageFormat(syntax) { en.Syntax = syntax return en } else if !parser.IsTextParser(syntax) { // Not embeddable. e.transcludeCount++ return createInlineErrorText(ref, "Not", "embeddable (syntax="+syntax+")") } cost, ok := e.costMap[zid] zn := cost.zn |
︙ | ︙ | |||
490 491 492 493 494 495 496 | zid, err := id.Parse(ref.URL.Path) if err != nil { panic(fmt.Sprintf("%v: %q (state %v) -> %v", err, ref.URL.Path, ref.State, ref)) } return zid } | < < < < < < < < < < < < < | | 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 | zid, err := id.Parse(ref.URL.Path) if err != nil { panic(fmt.Sprintf("%v: %q (state %v) -> %v", err, ref.URL.Path, ref.State, ref)) } return zid } func (e *evaluator) evalLiteralNode(ln *ast.LiteralNode) ast.InlineNode { if ln.Kind != ast.LiteralZettel { return ln } e.transcludeCount++ result := e.evaluateEmbeddedInline(ln.Content, getSyntax(ln.Attrs, api.ValueSyntaxText)) if len(result) == 0 { return &ast.LiteralNode{ Kind: ast.LiteralComment, Attrs: map[string]string{"-": ""}, Content: []byte("Nothing to transclude"), } } |
︙ | ︙ | |||
566 567 568 569 570 571 572 | func (e *evaluator) evaluateEmbeddedInline(content []byte, syntax string) ast.InlineSlice { is := parser.ParseInlines(input.NewInput(content), syntax) ast.Walk(e, &is) return is } | | | | 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 | func (e *evaluator) evaluateEmbeddedInline(content []byte, syntax string) ast.InlineSlice { is := parser.ParseInlines(input.NewInput(content), syntax) ast.Walk(e, &is) return is } func (e *evaluator) evaluateEmbeddedZettel(zettel domain.Zettel) *ast.ZettelNode { zn := parser.ParseZettel(e.ctx, zettel, zettel.Meta.GetDefault(api.KeySyntax, ""), e.rtConfig) ast.Walk(e, &zn.Ast) return zn } func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice { if fragment == "" { return firstInlinesToEmbed(*bs) |
︙ | ︙ | |||
589 590 591 592 593 594 595 596 597 598 | if ins := bs.FirstParagraphInlines(); ins != nil { return ins } if len(bs) == 0 { return nil } if bn, ok := bs[0].(*ast.BLOBNode); ok { return ast.InlineSlice{&ast.EmbedBLOBNode{ Blob: bn.Blob, Syntax: bn.Syntax, | > > > > | | 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 | if ins := bs.FirstParagraphInlines(); ins != nil { return ins } if len(bs) == 0 { return nil } if bn, ok := bs[0].(*ast.BLOBNode); ok { var ins ast.InlineSlice if bn.Title != "" { ins = ast.CreateInlineSliceFromWords(strings.Fields(bn.Title)...) } return ast.InlineSlice{&ast.EmbedBLOBNode{ Blob: bn.Blob, Syntax: bn.Syntax, Inlines: ins, }} } return nil } type fragmentSearcher struct { fragment string |
︙ | ︙ |
Changes to evaluator/list.go.
1 | //----------------------------------------------------------------------------- | | < < < | | > < | | < < < < | | | | | | | | | | | | | | | | | | | | | | < < < | | < | | | | | | < < | | | | | | | | < < < < | < < | | < | | > | | > > > > > > > > > > > > > > | > > | > > | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < | | | | > | < | | < > < < < < < < < < < > > | > > | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package evaluator import ( "bytes" "context" "math" "sort" "strconv" "strings" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoding/atom" "zettelstore.de/z/encoding/rss" "zettelstore.de/z/parser" "zettelstore.de/z/query" ) // QueryAction transforms a list of metadata according to query actions into a AST nested list. func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta, rtConfig config.Config) ast.BlockNode { ap := actionPara{ ctx: ctx, q: q, ml: ml, kind: ast.NestedListUnordered, min: -1, max: -1, title: rtConfig.GetSiteName(), } if actions := q.Actions(); len(actions) > 0 { acts := make([]string, 0, len(actions)) for i, act := range actions { if strings.HasPrefix(act, "N") { ap.kind = ast.NestedListOrdered continue } if strings.HasPrefix(act, "MIN") { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.min = num continue } } if strings.HasPrefix(act, "MAX") { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.max = num continue } } if act == "TITLE" && i+1 < len(actions) { ap.title = strings.Join(actions[i+1:], " ") break } acts = append(acts, act) } for _, act := range acts { switch act { case "ATOM": return ap.createBlockNodeAtom(rtConfig) case "RSS": return ap.createBlockNodeRSS(rtConfig) } key := strings.ToLower(act) switch meta.Type(key) { case meta.TypeWord: return ap.createBlockNodeWord(key) case meta.TypeTagSet: return ap.createBlockNodeTagSet(key) } } } return ap.createBlockNodeMeta() } type actionPara struct { ctx context.Context q *query.Query ml []*meta.Meta kind ast.NestedListKind min int max int title string } func (ap *actionPara) createBlockNodeWord(key string) ast.BlockNode { var buf bytes.Buffer ccs, bufLen := ap.prepareCatAction(key, &buf) if len(ccs) == 0 { return nil } items := make([]ast.ItemSlice, 0, len(ccs)) ccs.SortByName() for _, cat := range ccs { buf.WriteString(cat.Name) items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(buf.String()), Inlines: ast.InlineSlice{&ast.TextNode{Text: cat.Name}}, })}) buf.Truncate(bufLen) } return &ast.NestedListNode{ Kind: ap.kind, Items: items, Attrs: nil, } } func (ap *actionPara) createBlockNodeTagSet(key string) ast.BlockNode { var buf bytes.Buffer ccs, bufLen := ap.prepareCatAction(key, &buf) if len(ccs) == 0 { return nil } ccs.SortByCount() if min, max := ap.min, ap.max; min > 0 || max > 0 { if min < 0 { min = ccs[len(ccs)-1].Count } if max < 0 { max = ccs[0].Count } if ccs[len(ccs)-1].Count < min || max < ccs[0].Count { temp := make(meta.CountedCategories, 0, len(ccs)) for _, cat := range ccs { if min <= cat.Count && cat.Count <= max { temp = append(temp, cat) } } ccs = temp } } countMap := ap.calcFontSizes(ccs) para := make(ast.InlineSlice, 0, len(ccs)) ccs.SortByName() for i, cat := range ccs { if i > 0 { para = append(para, &ast.SpaceNode{ Lexeme: " ", }) } buf.WriteString(cat.Name) para = append(para, &ast.LinkNode{ Attrs: countMap[cat.Count], Ref: ast.ParseReference(buf.String()), Inlines: ast.InlineSlice{ &ast.TextNode{Text: cat.Name}, }, }, &ast.FormatNode{ Kind: ast.FormatSuper, Attrs: nil, Inlines: ast.InlineSlice{&ast.TextNode{Text: strconv.Itoa(cat.Count)}}, }, ) buf.Truncate(bufLen) } return &ast.ParaNode{ Inlines: para, } } func (ap *actionPara) createBlockNodeMeta() ast.BlockNode { if len(ap.ml) == 0 { return nil } items := make([]ast.ItemSlice, 0, len(ap.ml)) for _, m := range ap.ml { zid := m.Zid.String() title, found := m.Get(api.KeyTitle) if !found { title = zid } items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(zid), Inlines: parser.ParseMetadataNoLink(title), })}) } return &ast.NestedListNode{ Kind: ap.kind, Items: items, Attrs: nil, } } func (ap *actionPara) prepareCatAction(key string, buf *bytes.Buffer) (meta.CountedCategories, int) { if len(ap.ml) == 0 { return nil, 0 } ccs := meta.CreateArrangement(ap.ml, key).Counted() if len(ccs) == 0 { return nil, 0 } sea := ap.q.Clone() sea.RemoveActions() buf.WriteString(ast.QueryPrefix) sea.Print(buf) if buf.Len() > len(ast.QueryPrefix) { buf.WriteByte(' ') } buf.WriteString(key) buf.WriteByte(':') bufLen := buf.Len() return ccs, bufLen } const fontSizes = 6 // Must be the number of CSS classes zs-font-size-* in base.css const fontSizes64 = float64(fontSizes) func (*actionPara) calcFontSizes(ccs meta.CountedCategories) map[int]attrs.Attributes { var fsAttrs [fontSizes]attrs.Attributes var a attrs.Attributes for i := 0; i < fontSizes; i++ { fsAttrs[i] = a.AddClass("zs-font-size-" + strconv.Itoa(i)) } countMap := make(map[int]int, len(ccs)) for _, cat := range ccs { countMap[cat.Count]++ } |
︙ | ︙ | |||
347 348 349 350 351 352 353 | } } return result } func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) } | | < | | > | | < | > | 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 | } } return result } func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) } func (ap *actionPara) createBlockNodeRSS(cfg config.Config) ast.BlockNode { var rssConfig rss.Configuration rssConfig.Setup(ap.ctx, cfg) rssConfig.Title = ap.title data := rssConfig.Marshal(ap.q, ap.ml) return &ast.VerbatimNode{ Kind: ast.VerbatimProg, Attrs: attrs.Attributes{"lang": "xml"}, Content: data, } } func (ap *actionPara) createBlockNodeAtom(cfg config.Config) ast.BlockNode { var atomConfig atom.Configuration atomConfig.Setup(ap.ctx, cfg) atomConfig.Title = ap.title data := atomConfig.Marshal(ap.q, ap.ml) return &ast.VerbatimNode{ Kind: ast.VerbatimProg, Attrs: attrs.Attributes{"lang": "xml"}, Content: data, } } |
Changes to evaluator/metadata.go.
1 | //----------------------------------------------------------------------------- | | < < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package evaluator import ( "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" ) func evaluateMetadata(m *meta.Meta) ast.BlockSlice { descrlist := &ast.DescriptionListNode{} for _, p := range m.Pairs() { descrlist.Descriptions = append( descrlist.Descriptions, getMetadataDescription(p.Key, p.Value)) |
︙ | ︙ | |||
41 42 43 44 45 46 47 | sliceData = meta.ListFromValue(value) if len(sliceData) == 0 { return nil } } else { sliceData = []string{value} } | > > > | > | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | sliceData = meta.ListFromValue(value) if len(sliceData) == 0 { return nil } } else { sliceData = []string{value} } var makeLink bool switch dt { case meta.TypeID, meta.TypeIDSet: makeLink = true } result := make(ast.InlineSlice, 0, 2*len(sliceData)-1) for i, val := range sliceData { if i > 0 { result = append(result, &ast.SpaceNode{Lexeme: " "}) } tn := &ast.TextNode{Text: val} |
︙ | ︙ |
Changes to go.mod.
1 2 | module zettelstore.de/z | | > | > | | | | < < | < | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | module zettelstore.de/z go 1.19 require ( codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0 github.com/fsnotify/fsnotify v1.6.0 github.com/pascaldekloe/jwt v1.12.0 github.com/yuin/goldmark v1.5.2 golang.org/x/crypto v0.0.0-20221012134737-56aed061732a golang.org/x/term v0.0.0-20221017184919-83659145692c golang.org/x/text v0.4.0 zettelstore.de/c v0.7.1-0.20220927073310-998ac1ba24c3 ) require golang.org/x/sys v0.1.0 // indirect |
Changes to go.sum.
|
| > > | | > > | | | | > | | | | | | < < < < < | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0 h1:viya/OgeF16+i8caBPJmcLQhGpZodPh+/nxtJzSSO1s= codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0/go.mod h1:4fAHEF3VH+ofbZkF6NzqiItTNy2X11tVCnZX99jXouA= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/pascaldekloe/jwt v1.12.0 h1:imQSkPOtAIBAXoKKjL9ZVJuF/rVqJ+ntiLGpLyeqMUQ= github.com/pascaldekloe/jwt v1.12.0/go.mod h1:LiIl7EwaglmH1hWThd/AmydNCnHf/mmfluBlNqHbk8U= github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg= golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20221017184919-83659145692c h1:dveknrit5futqEmXAvd2I1BbZIDhxRijsyWHM86NlcA= golang.org/x/term v0.0.0-20221017184919-83659145692c/go.mod h1:VTIZ7TEbF0BS9Sv9lPTvGbtW8i4z6GGbJBCM37uMCzY= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= zettelstore.de/c v0.7.1-0.20220927073310-998ac1ba24c3 h1:67MVyYuOVrzeriJlFkuZYLtFTw2YVqt8Xw16u7kiuOQ= zettelstore.de/c v0.7.1-0.20220927073310-998ac1ba24c3/go.mod h1:+SoneUhKQ81A2Id/bC6FdDYYQAHYfVryh7wHFnnklew= |
Added input/entity.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package input import ( "html" "unicode" ) // ScanEntity scans either a named or a numbered entity and returns it as a string. // // For numbered entities (like { or ģ) html.UnescapeString returns // sometimes other values as expected, if the number is not well-formed. This // may happen because of some strange HTML parsing rules. But these do not // apply to Zettelmarkup. Therefore, I parse the number here in the code. func (inp *Input) ScanEntity() (res string, success bool) { if inp.Ch != '&' { return "", false } pos := inp.Pos inp.Next() if inp.Ch == '#' { inp.Next() if inp.Ch == 'x' || inp.Ch == 'X' { return inp.scanEntityBase16() } return inp.scanEntityBase10() } return inp.scanEntityNamed(pos) } func (inp *Input) scanEntityBase16() (string, bool) { inp.Next() if inp.Ch == ';' { return "", false } code := 0 for { switch ch := inp.Ch; ch { case ';': inp.Next() if r := rune(code); isValidEntity(r) { return string(r), true } return "", false case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': code = 16*code + int(ch-'0') case 'a', 'b', 'c', 'd', 'e', 'f': code = 16*code + int(ch-'a'+10) case 'A', 'B', 'C', 'D', 'E', 'F': code = 16*code + int(ch-'A'+10) default: return "", false } if code > unicode.MaxRune { return "", false } inp.Next() } } func (inp *Input) scanEntityBase10() (string, bool) { // Base 10 code if inp.Ch == ';' { return "", false } code := 0 for { switch ch := inp.Ch; ch { case ';': inp.Next() if r := rune(code); isValidEntity(r) { return string(r), true } return "", false case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': code = 10*code + int(ch-'0') default: return "", false } if code > unicode.MaxRune { return "", false } inp.Next() } } func (inp *Input) scanEntityNamed(pos int) (string, bool) { for { switch inp.Ch { case EOS, '\n', '\r', '&': return "", false case ';': inp.Next() es := string(inp.Src[pos:inp.Pos]) ues := html.UnescapeString(es) if es == ues { return "", false } return ues, true default: inp.Next() } } } // isValidEntity checks if the given code is valid for an entity. // // According to https://html.spec.whatwg.org/multipage/syntax.html#character-references // ""The numeric character reference forms described above are allowed to reference any code point // excluding U+000D CR, noncharacters, and controls other than ASCII whitespace."" func isValidEntity(r rune) bool { // No C0 control and no "code point in the range U+007F DELETE to U+009F APPLICATION PROGRAM COMMAND, inclusive." if r < ' ' || ('\u007f' <= r && r <= '\u009f') { return false } // If below any noncharacter code point, return true // // See: https://infra.spec.whatwg.org/#noncharacter if r < '\ufdd0' { return true } // First range of noncharacter code points: "(...) in the range U+FDD0 to U+FDEF, inclusive" if r <= '\ufdef' { return false } // Other noncharacter code points: switch r { case '\uFFFE', '\uFFFF', '\U0001FFFE', '\U0001FFFF', '\U0002FFFE', '\U0002FFFF', '\U0003FFFE', '\U0003FFFF', '\U0004FFFE', '\U0004FFFF', '\U0005FFFE', '\U0005FFFF', '\U0006FFFE', '\U0006FFFF', '\U0007FFFE', '\U0007FFFF', '\U0008FFFE', '\U0008FFFF', '\U0009FFFE', '\U0009FFFF', '\U000AFFFE', '\U000AFFFF', '\U000BFFFE', '\U000BFFFF', '\U000CFFFE', '\U000CFFFF', '\U000DFFFE', '\U000DFFFF', '\U000EFFFE', '\U000EFFFF', '\U000FFFFE', '\U000FFFFF', '\U0010FFFE', '\U0010FFFF': return false } return true } |
Added input/entity_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package input_test import ( "testing" "zettelstore.de/z/input" ) func TestScanEntity(t *testing.T) { t.Parallel() var testcases = []struct { text string exp string }{ {"", ""}, {"a", ""}, {"&", "&"}, {"!", "!"}, {"3", "3"}, {""", "\""}, } for id, tc := range testcases { inp := input.NewInput([]byte(tc.text)) got, ok := inp.ScanEntity() if !ok { if tc.exp != "" { t.Errorf("ID=%d, text=%q: expected error, but got %q", id, tc.text, got) } if inp.Pos != 0 { t.Errorf("ID=%d, text=%q: input position advances to %d", id, tc.text, inp.Pos) } continue } if tc.exp != got { t.Errorf("ID=%d, text=%q: expected %q, but got %q", id, tc.text, tc.exp, got) } } } func TestScanIllegalEntity(t *testing.T) { t.Parallel() testcases := []string{"", "a", "& Input →", "	", ""} for i, tc := range testcases { inp := input.NewInput([]byte(tc)) got, ok := inp.ScanEntity() if ok { t.Errorf("%d: scanning %q was unexpected successful, got %q", i, tc, got) continue } } } |
Added input/input.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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package input provides an abstraction for data to be read. package input import ( "unicode/utf8" ) // Input is an abstract input source type Input struct { // Read-only, will never change Src []byte // The source string // Read-only, will change Ch rune // current character Pos int // character position in src readPos int // reading position (position after current character) } // NewInput creates a new input source. func NewInput(src []byte) *Input { inp := &Input{Src: src} inp.Next() return inp } // EOS = End of source const EOS = rune(-1) // Next reads the next rune into inp.Ch and returns it too. func (inp *Input) Next() rune { if inp.readPos >= len(inp.Src) { inp.Pos = len(inp.Src) inp.Ch = EOS return EOS } inp.Pos = inp.readPos r, w := rune(inp.Src[inp.readPos]), 1 if r >= utf8.RuneSelf { r, w = utf8.DecodeRune(inp.Src[inp.readPos:]) } inp.readPos += w inp.Ch = r return r } // Peek returns the rune following the most recently read rune without // advancing. If end-of-source was already found peek returns EOS. func (inp *Input) Peek() rune { return inp.PeekN(0) } // PeekN returns the n-th rune after the most recently read rune without // advancing. If end-of-source was already found peek returns EOS. func (inp *Input) PeekN(n int) rune { pos := inp.readPos + n if pos < len(inp.Src) { r := rune(inp.Src[pos]) if r >= utf8.RuneSelf { r, _ = utf8.DecodeRune(inp.Src[pos:]) } if r == '\t' { return ' ' } return r } return EOS } // Accept checks if the given string is a prefix of the text to be parsed. // If successful, advance position and current character. // String must only contain bytes < 128. // If not successful, everything remains as it is. func (inp *Input) Accept(s string) bool { pos := inp.Pos remaining := len(inp.Src) - pos if s == "" || len(s) > remaining { return false } // According to internal documentation of bytes.Equal, the string() will not allocate any memory. if readPos := pos + len(s); s == string(inp.Src[pos:readPos]) { inp.readPos = readPos inp.Next() return true } return false } // IsEOLEOS returns true if char is either EOS or EOL. func IsEOLEOS(ch rune) bool { switch ch { case EOS, '\n', '\r': return true } return false } // EatEOL transforms both "\r" and "\r\n" into "\n". func (inp *Input) EatEOL() { switch inp.Ch { case '\r': if inp.Peek() == '\n' { inp.Next() } inp.Ch = '\n' inp.Next() case '\n': inp.Next() } } // SetPos allows to reset the read position. func (inp *Input) SetPos(pos int) { if inp.Pos != pos { inp.readPos = pos inp.Next() } } // SkipToEOL reads until the next end-of-line. func (inp *Input) SkipToEOL() { for { switch inp.Ch { case EOS, '\n', '\r': return } inp.Next() } } // ScanLineContent reads the reaining input stream and interprets it as lines of text. func (inp *Input) ScanLineContent() []byte { result := make([]byte, 0, len(inp.Src)-inp.Pos+1) for { inp.EatEOL() posL := inp.Pos if inp.Ch == EOS { return result } inp.SkipToEOL() if len(result) > 0 { result = append(result, '\n') } result = append(result, inp.Src[posL:inp.Pos]...) } } |
Added input/input_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package input_test provides some unit-tests for reading data. package input_test import ( "testing" "zettelstore.de/z/input" ) func TestEatEOL(t *testing.T) { t.Parallel() inp := input.NewInput(nil) inp.EatEOL() if inp.Ch != input.EOS { t.Errorf("No EOS found: %q", inp.Ch) } if inp.Pos != 0 { t.Errorf("Pos != 0: %d", inp.Pos) } inp = input.NewInput([]byte("ABC")) if inp.Ch != 'A' { t.Errorf("First ch != 'A', got %q", inp.Ch) } inp.EatEOL() if inp.Ch != 'A' { t.Errorf("First ch != 'A', got %q", inp.Ch) } } func TestAccept(t *testing.T) { t.Parallel() testcases := []struct { accept string src string acc bool exp rune }{ {"", "", false, input.EOS}, {"AB", "abc", false, 'a'}, {"AB", "ABC", true, 'C'}, {"AB", "AB", true, input.EOS}, {"AB", "A", false, 'A'}, } for i, tc := range testcases { inp := input.NewInput([]byte(tc.src)) acc := inp.Accept(tc.accept) if acc != tc.acc { t.Errorf("%d: %q.Accept(%q) == %v, but got %v", i, tc.src, tc.accept, tc.acc, acc) } if got := inp.Ch; tc.exp != got { t.Errorf("%d: %q.Accept(%q) should result in run %v, but got %v", i, tc.src, tc.accept, tc.exp, got) } } } |
Added input/runes.go.
> > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package input import "unicode" // IsSpace returns true if rune is a whitespace. func IsSpace(ch rune) bool { switch ch { case ' ', '\t': return true case '\n', '\r', EOS: return false } return unicode.IsSpace(ch) } |
Changes to kernel/impl/auth.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | 1 2 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) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "errors" "sync" "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) type authService struct { srvConfig mxService sync.RWMutex manager auth.Manager createManager kernel.CreateAuthManagerFunc |
︙ | ︙ | |||
71 72 73 74 75 76 77 | func (as *authService) Start(*myKernel) error { as.mxService.Lock() defer as.mxService.Unlock() readonlyMode := as.GetNextConfig(kernel.AuthReadonly).(bool) owner := as.GetNextConfig(kernel.AuthOwner).(id.Zid) authMgr, err := as.createManager(readonlyMode, owner) if err != nil { | | | 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | func (as *authService) Start(*myKernel) error { as.mxService.Lock() defer as.mxService.Unlock() readonlyMode := as.GetNextConfig(kernel.AuthReadonly).(bool) owner := as.GetNextConfig(kernel.AuthOwner).(id.Zid) authMgr, err := as.createManager(readonlyMode, owner) if err != nil { as.logger.Fatal().Err(err).Msg("Unable to create manager") return err } as.logger.Info().Msg("Start Manager") as.manager = authMgr return nil } |
︙ | ︙ |
Changes to kernel/impl/box.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "context" "errors" |
︙ | ︙ | |||
81 82 83 84 85 86 87 | } boxURIs = append(boxURIs, u.(*url.URL)) } ps.mxService.Lock() defer ps.mxService.Unlock() mgr, err := ps.createManager(boxURIs, kern.auth.manager, &kern.cfg) if err != nil { | | | | 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | } boxURIs = append(boxURIs, u.(*url.URL)) } ps.mxService.Lock() defer ps.mxService.Unlock() mgr, err := ps.createManager(boxURIs, kern.auth.manager, &kern.cfg) if err != nil { ps.logger.Fatal().Err(err).Msg("Unable to create manager") return err } ps.logger.Info().Str("location", mgr.Location()).Msg("Start Manager") if err = mgr.Start(context.Background()); err != nil { ps.logger.Fatal().Err(err).Msg("Unable to start manager") return err } kern.cfg.setBox(mgr) ps.manager = mgr return nil } |
︙ | ︙ |
Changes to kernel/impl/cfg.go.
1 | //----------------------------------------------------------------------------- | | < < < | > > < < < > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "context" "errors" "fmt" "strconv" "strings" "sync" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) type configService struct { srvConfig mxService sync.RWMutex orig *meta.Meta } // Predefined Metadata keys for runtime configuration // See: https://zettelstore.de/manual/h/00001004020000 const ( keyDefaultCopyright = "default-copyright" keyDefaultLicense = "default-license" keyDefaultVisibility = "default-visibility" keyExpertMode = "expert-mode" keyHomeZettel = "home-zettel" keyMaxTransclusions = "max-transclusions" keySiteName = "site-name" keyYAMLHeader = "yaml-header" keyZettelFileSyntax = "zettel-file-syntax" ) var errUnknownVisibility = errors.New("unknown visibility") |
︙ | ︙ | |||
65 66 67 68 69 70 71 | if vis == meta.VisibilityUnknown { return nil, errUnknownVisibility } return vis, nil }, true, }, | | | | | > | | | | < < < < | | | | | | | | > | | | | | < < < < | 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 | if vis == meta.VisibilityUnknown { return nil, errUnknownVisibility } return vis, nil }, true, }, keyExpertMode: {"Expert mode", parseBool, true}, config.KeyFooterHTML: {"Footer HTML", parseString, true}, keyHomeZettel: {"Home zettel", parseZid, true}, kernel.ConfigInsecureHTML: { "Insecure HTML", cs.noFrozen(func(val string) (any, error) { switch val { case kernel.ConfigSyntaxHTML: return config.SyntaxHTML, nil case kernel.ConfigMarkdownHTML: return config.MarkdownHTML, nil case kernel.ConfigZmkHTML: return config.ZettelmarkupHTML, nil } return config.NoHTML, nil }), true, }, api.KeyLang: {"Language", parseString, true}, config.KeyMarkerExternal: {"Marker external URL", parseString, true}, keyMaxTransclusions: {"Maximum transclusions", parseInt64, true}, keySiteName: {"Site name", parseString, true}, keyYAMLHeader: {"YAML header", parseBool, true}, keyZettelFileSyntax: { "Zettel file syntax", func(val string) (any, error) { return strings.Fields(val), nil }, true, }, kernel.ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true}, } cs.next = interfaceMap{ keyDefaultCopyright: "", keyDefaultLicense: "", keyDefaultVisibility: meta.VisibilityLogin, keyExpertMode: false, config.KeyFooterHTML: "", keyHomeZettel: id.DefaultHomeZid, kernel.ConfigInsecureHTML: config.NoHTML, api.KeyLang: api.ValueLangEN, config.KeyMarkerExternal: "➚", keyMaxTransclusions: int64(1024), keySiteName: "Zettelstore", keyYAMLHeader: false, keyZettelFileSyntax: nil, kernel.ConfigSimpleMode: false, } } func (cs *configService) GetLogger() *logger.Logger { return cs.logger } func (cs *configService) Start(*myKernel) error { cs.logger.Info().Msg("Start Service") data := meta.New(id.ConfigurationZid) |
︙ | ︙ | |||
142 143 144 145 146 147 148 | return cs.orig != nil } func (cs *configService) Stop(*myKernel) { cs.logger.Info().Msg("Stop Service") cs.mxService.Lock() cs.orig = nil | < < < < | | | < | | | < < < < < | > | < < | | 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 | return cs.orig != nil } func (cs *configService) Stop(*myKernel) { cs.logger.Info().Msg("Stop Service") cs.mxService.Lock() cs.orig = nil cs.mxService.Unlock() } func (*configService) GetStatistics() []kernel.KeyValue { return nil } func (cs *configService) setBox(mgr box.Manager) { mgr.RegisterObserver(cs.observe) cs.doUpdate(mgr) } func (cs *configService) doUpdate(p box.Box) error { m, err := p.GetMeta(context.Background(), cs.orig.Zid) cs.logger.Trace().Err(err).Msg("got config meta") if err != nil { return err } cs.mxService.Lock() for _, pair := range cs.orig.Pairs() { key := pair.Key if val, ok := m.Get(key); ok { cs.SetConfig(key, val) } else if defVal, defFound := cs.orig.Get(key); defFound { cs.SetConfig(key, defVal) } } cs.mxService.Unlock() cs.SwitchNextToCur() // Poor man's restart return nil } func (cs *configService) observe(ci box.UpdateInfo) { if ci.Reason == box.OnReload { cs.logger.Debug().Msg("reload") go func() { cs.doUpdate(ci.Box) }() } else if ci.Zid == id.ConfigurationZid { cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe") go func() { cs.doUpdate(ci.Box) }() } } // --- config.Config func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string { if m != nil { if val, found := m.Get(key); found { return val } } if user := server.GetUser(ctx); user != nil { if val, found := user.Get(key); found { return val } } result := cs.GetConfig(key) if result == nil { return "" } switch val := result.(type) { case string: return val case bool: |
︙ | ︙ | |||
244 245 246 247 248 249 250 | func (cs *configService) AddDefaultValues(ctx context.Context, m *meta.Meta) *meta.Meta { if cs == nil { return m } result := m cs.mxService.RLock() if _, found := m.Get(api.KeyCopyright); !found { | | | | | | > > > > > > > > > > > > > | | | | | | | 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 | func (cs *configService) AddDefaultValues(ctx context.Context, m *meta.Meta) *meta.Meta { if cs == nil { return m } result := m cs.mxService.RLock() if _, found := m.Get(api.KeyCopyright); !found { result = updateMeta(result, m, api.KeyCopyright, cs.GetConfig(keyDefaultCopyright).(string)) } if _, found := m.Get(api.KeyLang); !found { result = updateMeta(result, m, api.KeyLang, cs.Get(ctx, nil, api.KeyLang)) } if _, found := m.Get(api.KeyLicense); !found { result = updateMeta(result, m, api.KeyLicense, cs.GetConfig(keyDefaultLicense).(string)) } if _, found := m.Get(api.KeyVisibility); !found { result = updateMeta(result, m, api.KeyVisibility, cs.GetConfig(keyDefaultVisibility).(meta.Visibility).String()) } cs.mxService.RUnlock() return result } func updateMeta(result, m *meta.Meta, key, val string) *meta.Meta { if result == m { result = m.Clone() } result.Set(key, val) return result } func (cs *configService) GetHTMLInsecurity() config.HTMLInsecurity { return cs.GetConfig(kernel.ConfigInsecureHTML).(config.HTMLInsecurity) } // GetSiteName returns the current value of the "site-name" key. func (cs *configService) GetSiteName() string { return cs.GetConfig(keySiteName).(string) } // GetHomeZettel returns the value of the "home-zettel" key. func (cs *configService) GetHomeZettel() id.Zid { homeZid := cs.GetConfig(keyHomeZettel).(id.Zid) if homeZid != id.Invalid { return homeZid } cs.mxService.RLock() val, _ := cs.orig.Get(keyHomeZettel) homeZid, _ = id.Parse(val) cs.mxService.RUnlock() return homeZid } // GetMaxTransclusions return the maximum number of indirect transclusions. func (cs *configService) GetMaxTransclusions() int { return int(cs.GetConfig(keyMaxTransclusions).(int64)) } // GetYAMLHeader returns the current value of the "yaml-header" key. func (cs *configService) GetYAMLHeader() bool { return cs.GetConfig(keyYAMLHeader).(bool) } // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. func (cs *configService) GetZettelFileSyntax() []string { if zfs := cs.GetConfig(keyZettelFileSyntax); zfs != nil { return zfs.([]string) } return nil } // --- config.AuthConfig // GetSimpleMode returns true if system tuns in simple-mode. func (cs *configService) GetSimpleMode() bool { return cs.GetConfig(kernel.ConfigSimpleMode).(bool) } // GetExpertMode returns the current value of the "expert-mode" key. func (cs *configService) GetExpertMode() bool { return cs.GetConfig(keyExpertMode).(bool) } // GetVisibility returns the visibility value, or "login" if none is given. func (cs *configService) GetVisibility(m *meta.Meta) meta.Visibility { if val, ok := m.Get(api.KeyVisibility); ok { if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } } vis := cs.GetConfig(keyDefaultVisibility).(meta.Visibility) if vis != meta.VisibilityUnknown { return vis } cs.mxService.RLock() val, _ := cs.orig.Get(keyDefaultVisibility) vis = meta.GetVisibility(val) cs.mxService.RUnlock() return vis } |
Changes to kernel/impl/cmd.go.
1 | //----------------------------------------------------------------------------- | | < < < | | 1 2 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) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "fmt" "io" "os" "runtime/metrics" "sort" "strconv" "strings" "zettelstore.de/c/maps" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" ) type cmdSession struct { w io.Writer |
︙ | ︙ | |||
222 223 224 225 226 227 228 | table = append(table, []string{kd.Key, kd.Descr}) } sess.printTable(table) return true } func cmdGetConfig(sess *cmdSession, _ string, args []string) bool { showConfig(sess, args, | | | 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | table = append(table, []string{kd.Key, kd.Descr}) } sess.printTable(table) return true } func cmdGetConfig(sess *cmdSession, _ string, args []string) bool { showConfig(sess, args, listCurConfig, func(srv service, key string) interface{} { return srv.GetConfig(key) }) return true } func cmdNextConfig(sess *cmdSession, _ string, args []string) bool { showConfig(sess, args, listNextConfig, func(srv service, key string) interface{} { return srv.GetNextConfig(key) }) return true } |
︙ | ︙ | |||
266 267 268 269 270 271 272 | if val == nil { sess.println("Unknown key", args[1], "for service", args[0]) return } sess.println(fmt.Sprintf("%v", val)) } func listCurConfig(sess *cmdSession, srv service) { | | | 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | if val == nil { sess.println("Unknown key", args[1], "for service", args[0]) return } sess.println(fmt.Sprintf("%v", val)) } func listCurConfig(sess *cmdSession, srv service) { listConfig(sess, func() []kernel.KeyDescrValue { return srv.GetConfigList(true) }) } func listNextConfig(sess *cmdSession, srv service) { listConfig(sess, srv.GetNextConfigList) } func listConfig(sess *cmdSession, getConfigList func() []kernel.KeyDescrValue) { l := getConfigList() table := [][]string{{"Key", "Value", "Description"}} |
︙ | ︙ |
Changes to kernel/impl/config.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "errors" "fmt" "sort" "strconv" "strings" "sync" "zettelstore.de/c/maps" "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) type parseFunc func(string) (any, error) type configDescription struct { text string parse parseFunc canList bool |
︙ | ︙ | |||
109 110 111 112 113 114 115 | if parse == nil { if cfg.frozen { return errAlreadyFrozen } cfg.next[key] = value return nil } | | | 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | if parse == nil { if cfg.frozen { return errAlreadyFrozen } cfg.next[key] = value return nil } iVal, err := parse(value) // TODO if err != nil { return err } cfg.next[key] = iVal return nil } |
︙ | ︙ | |||
134 135 136 137 138 139 140 | continue } return d, k, num } return configDescription{}, "", -1 } | | | | | 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 | continue } return d, k, num } return configDescription{}, "", -1 } func (cfg *srvConfig) GetConfig(key string) interface{} { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() if cfg.cur == nil { return cfg.next[key] } return cfg.cur[key] } func (cfg *srvConfig) GetNextConfig(key string) interface{} { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() return cfg.next[key] } func (cfg *srvConfig) GetConfigList(all bool) []kernel.KeyDescrValue { return cfg.getOneConfigList(all, cfg.GetConfig) } func (cfg *srvConfig) GetNextConfigList() []kernel.KeyDescrValue { return cfg.getOneConfigList(true, cfg.GetNextConfig) } func (cfg *srvConfig) getOneConfigList(all bool, getConfig func(string) interface{}) []kernel.KeyDescrValue { if len(cfg.descr) == 0 { return nil |
︙ | ︙ | |||
243 244 245 246 247 248 249 | func parseZid(val string) (any, error) { if zid, err := id.Parse(val); err == nil { return zid, nil } else { return id.Invalid, err } } | < < < < < | 240 241 242 243 244 245 246 | func parseZid(val string) (any, error) { if zid, err := id.Parse(val); err == nil { return zid, nil } else { return id.Invalid, err } } |
Changes to kernel/impl/core.go.
1 | //----------------------------------------------------------------------------- | | < < < | > < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "fmt" "net" "os" "runtime" "sync" "time" "zettelstore.de/c/maps" "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" ) type coreService struct { srvConfig started bool mxRecover sync.RWMutex |
︙ | ︙ | |||
80 81 82 83 84 85 86 | cs.next = interfaceMap{ kernel.CoreDebug: false, kernel.CoreGoArch: runtime.GOARCH, kernel.CoreGoOS: runtime.GOOS, kernel.CoreGoVersion: runtime.Version(), kernel.CoreHostname: "*unknown host*", kernel.CorePort: 0, | | | 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | cs.next = interfaceMap{ kernel.CoreDebug: false, kernel.CoreGoArch: runtime.GOARCH, kernel.CoreGoOS: runtime.GOOS, kernel.CoreGoVersion: runtime.Version(), kernel.CoreHostname: "*unknown host*", kernel.CorePort: 0, kernel.CoreStarted: time.Now().Local().Format(id.ZidLayout), kernel.CoreVerbose: false, } if hn, err := os.Hostname(); err == nil { cs.next[kernel.CoreHostname] = hn } } |
︙ | ︙ |
Changes to kernel/impl/impl.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the kernel implementation. package impl import ( "errors" |
︙ | ︙ | |||
26 27 28 29 30 31 32 | "runtime/pprof" "strconv" "strings" "sync" "syscall" "time" | | | | < | 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 | "runtime/pprof" "strconv" "strings" "sync" "syscall" "time" "zettelstore.de/z/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) // myKernel is the main internal kernel. type myKernel struct { logWriter *kernelLogWriter logger *logger.Logger wg sync.WaitGroup mx sync.RWMutex interrupt chan os.Signal profileName string fileName string profileFile *os.File profile *pprof.Profile core coreService cfg configService auth authService box boxService web webService srvs map[kernel.Service]serviceDescr |
︙ | ︙ | |||
70 71 72 73 74 75 76 | srv service srvnum kernel.Service } type serviceDependency map[kernel.Service][]kernel.Service const ( defaultNormalLogLevel = logger.InfoLevel | | < < | < < | | | | | | | | | | | < < < < < | | | | | | < | < | | | | < < < | < < | < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < | | 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 | srv service srvnum kernel.Service } type serviceDependency map[kernel.Service][]kernel.Service const ( defaultNormalLogLevel = logger.InfoLevel defaultSimpleLogLevel = logger.WarnLevel ) // create a new kernel. func init() { kernel.Main = createKernel() } // create a new kernel. func createKernel() kernel.Kernel { lw := newKernelLogWriter(8192) kern := &myKernel{ logWriter: lw, logger: logger.New(lw, "").SetLevel(defaultNormalLogLevel), interrupt: make(chan os.Signal, 5), } kern.srvs = map[kernel.Service]serviceDescr{ kernel.CoreService: {&kern.core, "core", defaultNormalLogLevel}, kernel.ConfigService: {&kern.cfg, "config", defaultNormalLogLevel}, kernel.AuthService: {&kern.auth, "auth", defaultNormalLogLevel}, kernel.BoxService: {&kern.box, "box", defaultNormalLogLevel}, kernel.WebService: {&kern.web, "web", defaultNormalLogLevel}, } kern.srvNames = make(map[string]serviceData, len(kern.srvs)) for key, srvD := range kern.srvs { if _, ok := kern.srvNames[srvD.name]; ok { kern.logger.Panic().Str("service", srvD.name).Msg("Service data already set") } kern.srvNames[srvD.name] = serviceData{srvD.srv, key} l := logger.New(lw, strings.ToUpper(srvD.name)).SetLevel(srvD.logLevel) srvD.srv.Initialize(l) } kern.depStart = serviceDependency{ kernel.CoreService: nil, kernel.ConfigService: {kernel.CoreService}, kernel.AuthService: {kernel.CoreService}, kernel.BoxService: {kernel.CoreService, kernel.ConfigService, kernel.AuthService}, kernel.WebService: {kernel.ConfigService, kernel.AuthService, kernel.BoxService}, } kern.depStop = make(serviceDependency, len(kern.depStart)) for srv, deps := range kern.depStart { for _, dep := range deps { kern.depStop[dep] = append(kern.depStop[dep], srv) } } return kern } func (kern *myKernel) Setup(progname, version string, versionTime time.Time) { kern.SetConfig(kernel.CoreService, kernel.CoreProgname, progname) kern.SetConfig(kernel.CoreService, kernel.CoreVersion, version) kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.ZidLayout)) } func (kern *myKernel) Start(headline, lineServer bool) { for _, srvD := range kern.srvs { srvD.srv.Freeze() } if kern.cfg.GetConfig(kernel.ConfigSimpleMode).(bool) { kern.SetGlobalLogLevel(defaultSimpleLogLevel) } kern.wg.Add(1) signal.Notify(kern.interrupt, os.Interrupt, syscall.SIGTERM) go func() { // Wait for interrupt. sig := <-kern.interrupt if strSig := sig.String(); strSig != "" { kern.logger.Info().Str("signal", strSig).Msg("Shut down Zettelstore") } kern.doShutdown() kern.wg.Done() }() kern.StartService(kernel.CoreService) if headline { logger := kern.logger logger.Mandatory().Msg(fmt.Sprintf( "%v %v (%v@%v/%v)", kern.core.GetConfig(kernel.CoreProgname), kern.core.GetConfig(kernel.CoreVersion), kern.core.GetConfig(kernel.CoreGoVersion), kern.core.GetConfig(kernel.CoreGoOS), kern.core.GetConfig(kernel.CoreGoArch), )) logger.Mandatory().Msg("Licensed under the latest version of the EUPL (European Union Public License)") if kern.core.GetConfig(kernel.CoreDebug).(bool) { logger.Warn().Msg("----------------------------------------") logger.Warn().Msg("DEBUG MODE, DO NO USE THIS IN PRODUCTION") logger.Warn().Msg("----------------------------------------") } if kern.auth.GetConfig(kernel.AuthReadonly).(bool) { logger.Info().Msg("Read-only mode") } } if lineServer { port := kern.core.GetNextConfig(kernel.CorePort).(int) if port > 0 { listenAddr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) startLineServer(kern, listenAddr) } } } func (kern *myKernel) doShutdown() { kern.StopService(kernel.CoreService) // Will stop all other services. } func (kern *myKernel) WaitForShutdown() { kern.wg.Wait() kern.doStopProfiling() } // --- Shutdown operation ---------------------------------------------------- // Shutdown the service. Waits for all concurrent activity to stop. func (kern *myKernel) Shutdown(silent bool) { kern.interrupt <- &shutdownSignal{silent: silent} } type shutdownSignal struct{ silent bool } func (s *shutdownSignal) String() string { if s.silent { return "" } return "shutdown" } func (*shutdownSignal) Signal() { /* Just a signal */ } // --- Log operation --------------------------------------------------------- func (kern *myKernel) GetKernelLogger() *logger.Logger { return kern.logger } func (kern *myKernel) SetGlobalLogLevel(level logger.Level) { if level.IsValid() { kern.mx.RLock() kern.logger.SetLevel(level) for _, srvD := range kern.srvs { srvD.srv.GetLogger().SetLevel(level) } kern.mx.RUnlock() } } func (kern *myKernel) RetrieveLogEntries() []kernel.LogEntry { return kern.logWriter.retrieveLogEntries() } func (kern *myKernel) GetLastLogTime() time.Time { return kern.logWriter.getLastLogTime() } // LogRecover outputs some information about the previous panic. func (kern *myKernel) LogRecover(name string, recoverInfo interface{}) bool { return kern.doLogRecover(name, recoverInfo) } func (kern *myKernel) doLogRecover(name string, recoverInfo interface{}) bool { stack := debug.Stack() kern.logger.Fatal().Str("recovered_from", fmt.Sprint(recoverInfo)).Bytes("stack", stack).Msg(name) kern.core.updateRecoverInfo(name, recoverInfo, stack) return true } // --- Profiling --------------------------------------------------------- var errProfileInWork = errors.New("already profiling") |
︙ | ︙ | |||
309 310 311 312 313 314 315 | if err != nil { f.Close() return err } kern.profileName = profileName kern.fileName = fileName kern.profileFile = f | | | 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | if err != nil { f.Close() return err } kern.profileName = profileName kern.fileName = fileName kern.profileFile = f return err } profile := pprof.Lookup(profileName) if profile == nil { return errProfileNotFound } f, err := os.Create(fileName) if err != nil { |
︙ | ︙ | |||
365 366 367 368 369 370 371 | return errUnknownService } func (kern *myKernel) GetConfig(srvnum kernel.Service, key string) interface{} { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { | | | | 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 | return errUnknownService } func (kern *myKernel) GetConfig(srvnum kernel.Service, key string) interface{} { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetConfig(key) } return nil } func (kern *myKernel) GetConfigList(srvnum kernel.Service) []kernel.KeyDescrValue { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetConfigList(false) } return nil } func (kern *myKernel) GetServiceStatistics(srvnum kernel.Service) []kernel.KeyValue { kern.mx.RLock() defer kern.mx.RUnlock() |
︙ | ︙ | |||
499 500 501 502 503 504 505 | // ConfigDescriptions returns a sorted list of configuration descriptions. ConfigDescriptions() []serviceConfigDescription // SetConfig stores a configuration value. SetConfig(key, value string) error | | | | | | 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 | // ConfigDescriptions returns a sorted list of configuration descriptions. ConfigDescriptions() []serviceConfigDescription // SetConfig stores a configuration value. SetConfig(key, value string) error // GetConfig returns the current configuration value. GetConfig(key string) interface{} // GetNextConfig returns the next configuration value. GetNextConfig(key string) interface{} // GetConfigList returns a sorted list of current configuration data. GetConfigList(all bool) []kernel.KeyDescrValue // GetNextConfigList returns a sorted list of next configuration data. GetNextConfigList() []kernel.KeyDescrValue // GetStatistics returns a key/value list of statistical data. GetStatistics() []kernel.KeyValue |
︙ | ︙ | |||
541 542 543 544 545 546 547 | createBoxManager kernel.CreateBoxManagerFunc, setupWebServer kernel.SetupWebServerFunc, ) { kern.auth.createManager = createAuthManager kern.box.createManager = createBoxManager kern.web.setupServer = setupWebServer } | < < < < < < < < < < < < < < < < < < < < < | 490 491 492 493 494 495 496 | createBoxManager kernel.CreateBoxManagerFunc, setupWebServer kernel.SetupWebServerFunc, ) { kern.auth.createManager = createAuthManager kern.box.createManager = createBoxManager kern.web.setupServer = setupWebServer } |
Changes to kernel/impl/log.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "os" "sync" |
︙ | ︙ | |||
75 76 77 78 79 80 81 82 83 84 85 86 87 88 | } buf = append(buf, msg...) buf = append(buf, details...) buf = append(buf, '\n') _, err := os.Stdout.Write(buf) klw.mx.Unlock() return err } func addTimestamp(buf *[]byte, ts time.Time) { year, month, day := ts.Date() itoa(buf, year, 4) *buf = append(*buf, '-') | > > > | 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | } buf = append(buf, msg...) buf = append(buf, details...) buf = append(buf, '\n') _, err := os.Stdout.Write(buf) klw.mx.Unlock() if level == logger.PanicLevel { panic(err) } return err } func addTimestamp(buf *[]byte, ts time.Time) { year, month, day := ts.Date() itoa(buf, year, 4) *buf = append(*buf, '-') |
︙ | ︙ | |||
122 123 124 125 126 127 128 | defer klw.mx.RUnlock() if !klw.full { if klw.writePos == 0 { return nil } result := make([]kernel.LogEntry, klw.writePos) | | | | 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 | defer klw.mx.RUnlock() if !klw.full { if klw.writePos == 0 { return nil } result := make([]kernel.LogEntry, klw.writePos) for i := 0; i < klw.writePos; i++ { copyE2E(&result[i], &klw.data[i]) } return result } result := make([]kernel.LogEntry, cap(klw.data)) pos := 0 for j := klw.writePos; j < cap(klw.data); j++ { copyE2E(&result[pos], &klw.data[j]) pos++ } for j := 0; j < klw.writePos; j++ { copyE2E(&result[pos], &klw.data[j]) pos++ } return result } func (klw *kernelLogWriter) getLastLogTime() time.Time { |
︙ | ︙ |
Changes to kernel/impl/server.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed 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 impl import ( "bufio" "net" ) func startLineServer(kern *myKernel, listenAddr string) error { ln, err := net.Listen("tcp", listenAddr) if err != nil { kern.logger.Fatal().Err(err).Msg("Unable to start administration console") return err } kern.logger.Mandatory().Str("listen", listenAddr).Msg("Start administration console") go func() { lineServer(ln, kern) }() return nil } func lineServer(ln net.Listener, kern *myKernel) { // Something may panic. Ensure a running line service. defer func() { if r := recover(); r != nil { kern.doLogRecover("Line", r) go lineServer(ln, kern) } }() for { conn, err := ln.Accept() if err != nil { // handle error kern.logger.IfErr(err).Msg("Unable to accept connection") break } go handleLineConnection(conn, kern) } ln.Close() } func handleLineConnection(conn net.Conn, kern *myKernel) { // Something may panic. Ensure a running connection. defer func() { if r := recover(); r != nil { kern.doLogRecover("LineConn", r) go handleLineConnection(conn, kern) } }() kern.logger.Mandatory().Str("from", conn.RemoteAddr().String()).Msg("Start session on administration console") cmds := cmdSession{} cmds.initialize(conn, kern) |
︙ | ︙ |
Changes to kernel/impl/web.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package impl import ( "errors" "net" |
︙ | ︙ | |||
145 146 147 148 149 150 151 | secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool) maxRequestSize := ws.GetNextConfig(kernel.WebMaxRequestSize).(int64) if maxRequestSize < 1024 { maxRequestSize = 1024 } if !strings.HasSuffix(baseURL, urlPrefix) { | | | | | | | 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 | secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool) maxRequestSize := ws.GetNextConfig(kernel.WebMaxRequestSize).(int64) if maxRequestSize < 1024 { maxRequestSize = 1024 } if !strings.HasSuffix(baseURL, urlPrefix) { ws.logger.Fatal().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg( "url-prefix is not a suffix of base-url") return errWrongBasePrefix } if lap := netip.MustParseAddrPort(listenAddr); !kern.auth.manager.WithAuth() && !lap.Addr().IsLoopback() { ws.logger.Warn().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled") } srvw := impl.New(ws.logger, listenAddr, baseURL, urlPrefix, persistentCookie, secureCookie, maxRequestSize, kern.auth.manager) err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg) if err != nil { ws.logger.Fatal().Err(err).Msg("Unable to create") return err } if kern.core.GetNextConfig(kernel.CoreDebug).(bool) { srvw.SetDebug() } if err = srvw.Run(); err != nil { ws.logger.Fatal().Err(err).Msg("Unable to start") return err } ws.logger.Info().Str("listen", listenAddr).Str("base-url", baseURL).Msg("Start Service") ws.mxService.Lock() ws.srvw = srvw ws.mxService.Unlock() if kern.cfg.GetConfig(kernel.ConfigSimpleMode).(bool) { listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string) if idx := strings.LastIndexByte(listenAddr, ':'); idx >= 0 { ws.logger.Mandatory().Msg(strings.Repeat("--------------------", 3)) ws.logger.Mandatory().Msg("Open your browser and enter the following URL:") ws.logger.Mandatory().Msg(" http://localhost" + listenAddr[idx:]) ws.logger.Mandatory().Msg("") ws.logger.Mandatory().Msg("If this does not work, try:") |
︙ | ︙ |
Changes to kernel/kernel.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package kernel provides the main kernel service. package kernel import ( "io" "net/url" "time" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) // Kernel is the main internal service. type Kernel interface { // Setup sets the most basic data of a software: its name, its version, // and when the version was created. Setup(progname, version string, versionTime time.Time) // Start the service. Start(headline bool, lineServer bool) // WaitForShutdown blocks the call until Shutdown is called. WaitForShutdown() // Shutdown the service. Waits for all concurrent activities to stop. Shutdown(silent bool) // GetKernelLogger returns the kernel logger. GetKernelLogger() *logger.Logger // SetGlobalLogLevel sets the level for all logger maintained by the kernel. SetGlobalLogLevel(logger.Level) // LogRecover outputs some information about the previous panic. LogRecover(name string, recoverInfo interface{}) bool // StartProfiling starts profiling the software according to a profile. // It is an error to start more than one profile. // |
︙ | ︙ | |||
122 123 124 125 126 127 128 | ) // Service specifies a service, e.g. web, ... type Service uint8 // Constants for type Service. const ( | | < | | | | | | 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | ) // Service specifies a service, e.g. web, ... type Service uint8 // Constants for type Service. const ( _ Service = iota CoreService ConfigService AuthService BoxService WebService ) // Constants for core service system keys. const ( CoreDebug = "debug" CoreGoArch = "go-arch" CoreGoOS = "go-os" |
︙ | ︙ |
Changes to logger/logger.go.
1 | //----------------------------------------------------------------------------- | | < < < | | > > > > > > > > > > > > | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package logger implements a logging package for use in the Zettelstore. package logger import ( "context" "strconv" "strings" "sync/atomic" "time" "zettelstore.de/z/domain/meta" ) // Level defines the possible log levels type Level uint8 // Constants for Level const ( noLevel Level = iota // the absent log level TraceLevel // Log most internal activities DebugLevel // Log most data updates SenseLevel // Log activities of minor interest InfoLevel // Log normal activities WarnLevel // Log event that can be easily recovered ErrorLevel // Log (persistent) errors FatalLevel // Log event that cannot be recovered within an internal acitivty PanicLevel // Log event that must stop the software MandatoryLevel // Log only mandatory events NeverLevel // Logging is disabled ) var logLevel = [...]string{ " ", "TRACE", "DEBUG", "SENSE", "INFO ", "WARN ", "ERROR", "FATAL", "PANIC", ">>>>>", "NEVER", } var strLevel = [...]string{ "", "trace", "debug", "sense", "info", "warn", "error", "fatal", "panic", "mandatory", "disabled", } // IsValid returns true, if the level is a valid level func (l Level) IsValid() bool { return TraceLevel <= l && l <= NeverLevel } |
︙ | ︙ | |||
79 80 81 82 83 84 85 | // ParseLevel returns the recognized level. func ParseLevel(text string) Level { for lv := TraceLevel; lv <= NeverLevel; lv++ { if len(text) > 2 && strings.HasPrefix(strLevel[lv], text) { return lv } } | | | 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | // ParseLevel returns the recognized level. func ParseLevel(text string) Level { for lv := TraceLevel; lv <= NeverLevel; lv++ { if len(text) > 2 && strings.HasPrefix(strLevel[lv], text) { return lv } } return noLevel } // Logger represents an objects that emits logging messages. type Logger struct { lw LogWriter levelVal uint32 prefix string |
︙ | ︙ | |||
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | // Trace creates a tracing message. func (l *Logger) Trace() *Message { return newMessage(l, TraceLevel) } // Debug creates a debug message. func (l *Logger) Debug() *Message { return newMessage(l, DebugLevel) } // Info creates a message suitable for information data. func (l *Logger) Info() *Message { return newMessage(l, InfoLevel) } // Error creates a message suitable for errors. func (l *Logger) Error() *Message { return newMessage(l, ErrorLevel) } // Mandatory creates a message that will always logged, except when logging // is disabled. func (l *Logger) Mandatory() *Message { return newMessage(l, MandatoryLevel) } // Clone creates a message to clone the logger. func (l *Logger) Clone() *Message { msg := newMessage(l, NeverLevel) if msg != nil { | > > > > > > > > > > > > > > > > > > > > | | 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 | // Trace creates a tracing message. func (l *Logger) Trace() *Message { return newMessage(l, TraceLevel) } // Debug creates a debug message. func (l *Logger) Debug() *Message { return newMessage(l, DebugLevel) } // Sense creates a message suitable for sensing data. func (l *Logger) Sense() *Message { return newMessage(l, SenseLevel) } // Info creates a message suitable for information data. func (l *Logger) Info() *Message { return newMessage(l, InfoLevel) } // Warn creates a message suitable for warning the user. func (l *Logger) Warn() *Message { return newMessage(l, WarnLevel) } // Error creates a message suitable for errors. func (l *Logger) Error() *Message { return newMessage(l, ErrorLevel) } // IfErr creates an error message and sets the go error, if there is an error. func (l *Logger) IfErr(err error) *Message { if err != nil { return newMessage(l, ErrorLevel).Err(err) } return nil } // Fatal creates a message suitable for fatal errors. func (l *Logger) Fatal() *Message { return newMessage(l, FatalLevel) } // Panic creates a message suitable for panicing. func (l *Logger) Panic() *Message { return newMessage(l, PanicLevel) } // Mandatory creates a message that will always logged, except when logging // is disabled. func (l *Logger) Mandatory() *Message { return newMessage(l, MandatoryLevel) } // Clone creates a message to clone the logger. func (l *Logger) Clone() *Message { msg := newMessage(l, NeverLevel) if msg != nil { msg.level = noLevel } return msg } // UserProvider allows to retrieve an user metadata from a context. type UserProvider interface { GetUser(ctx context.Context) *meta.Meta |
︙ | ︙ |
Changes to logger/logger_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // 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 logger_test import ( "fmt" "os" |
︙ | ︙ | |||
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | testcases := []struct { text string exp logger.Level }{ {"tra", logger.TraceLevel}, {"deb", logger.DebugLevel}, {"info", logger.InfoLevel}, {"err", logger.ErrorLevel}, {"manda", logger.MandatoryLevel}, {"dis", logger.NeverLevel}, {"d", logger.Level(0)}, } for i, tc := range testcases { got := logger.ParseLevel(tc.text) if got != tc.exp { t.Errorf("%d: ParseLevel(%q) == %q, but got %q", i, tc.text, tc.exp, got) } } } func BenchmarkDisabled(b *testing.B) { log := logger.New(&stderrLogWriter{}, "").SetLevel(logger.NeverLevel) | > > > | | | | | 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 | testcases := []struct { text string exp logger.Level }{ {"tra", logger.TraceLevel}, {"deb", logger.DebugLevel}, {"info", logger.InfoLevel}, {"warn", logger.WarnLevel}, {"err", logger.ErrorLevel}, {"fata", logger.FatalLevel}, {"pan", logger.PanicLevel}, {"manda", logger.MandatoryLevel}, {"dis", logger.NeverLevel}, {"d", logger.Level(0)}, } for i, tc := range testcases { got := logger.ParseLevel(tc.text) if got != tc.exp { t.Errorf("%d: ParseLevel(%q) == %q, but got %q", i, tc.text, tc.exp, got) } } } func BenchmarkDisabled(b *testing.B) { log := logger.New(&stderrLogWriter{}, "").SetLevel(logger.NeverLevel) for n := 0; n < b.N; n++ { log.Info().Str("key", "val").Msg("Benchmark") } } type stderrLogWriter struct{} func (*stderrLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error { fmt.Fprintf(os.Stderr, "%v %v %v %v %v\n", level.Format(), ts, prefix, msg, string(details)) return nil } type testLogWriter struct{} func (*testLogWriter) WriteMessage(logger.Level, time.Time, string, string, []byte) error { return nil } func BenchmarkStrMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "") for n := 0; n < b.N; n++ { log.Info().Str("key", "val").Msg("Benchmark") } } func BenchmarkMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "") for n := 0; n < b.N; n++ { log.Info().Msg("Benchmark") } } func BenchmarkCloneStrMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "").Clone().Str("sss", "ttt").Child() for n := 0; n < b.N; n++ { log.Info().Msg("123456789") } } |
Changes to logger/message.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | 1 2 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) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package logger import ( "context" "net/http" "strconv" "sync" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" ) // Message presents a message to log. type Message struct { logger *Logger level Level buf []byte |
︙ | ︙ |
Changes to parser/blob/blob.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | > | | < | | | < | | < < < < < < < < < < > < > | | | 1 2 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package blob provides a parser of binary data. package blob import ( "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: api.ValueSyntaxGif, AltNames: nil, IsTextParser: false, IsImageFormat: true, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: "jpeg", AltNames: []string{"jpg"}, IsTextParser: false, IsImageFormat: true, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: "png", AltNames: nil, IsTextParser: false, IsImageFormat: true, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice { if p := parser.Get(syntax); p != nil { syntax = p.Name } title := m.GetDefault(api.KeyTitle, "") return ast.BlockSlice{&ast.BLOBNode{ Title: title, Syntax: syntax, Blob: []byte(inp.Src), }} } func parseInlines(*input.Input, string) ast.InlineSlice { return nil } |
Changes to parser/cleaner/cleaner.go.
1 | //----------------------------------------------------------------------------- | | < < < > < | 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package cleaner provides functions to clean up the parsed AST. package cleaner import ( "bytes" "strconv" "zettelstore.de/z/ast" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/strfun" ) // CleanBlockSlice cleans the given block list. |
︙ | ︙ | |||
136 137 138 139 140 141 142 | } func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) { if cv.doMark || hn == nil || len(hn.Inlines) == 0 { return } if hn.Slug == "" { | | | | | 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | } func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) { if cv.doMark || hn == nil || len(hn.Inlines) == 0 { return } if hn.Slug == "" { var buf bytes.Buffer _, err := cv.textEnc.WriteInlines(&buf, &hn.Inlines) if err != nil { return } hn.Slug = strfun.Slugify(buf.String()) } if hn.Slug != "" { hn.Fragment = cv.addIdentifier(hn.Slug, hn) } } func (cv *cleanVisitor) visitMark(mn *ast.MarkNode) { |
︙ | ︙ |
Changes to parser/draw/canvas.go.
1 | //----------------------------------------------------------------------------- | | < < < | 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) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import ( "bytes" "fmt" |
︙ | ︙ | |||
96 97 98 99 100 101 102 | c.findTexts() sort.Sort(c.objs) } // findPaths by starting with a point that wasn't yet visited, beginning at the top // left of the grid. func (c *canvas) findPaths() { | | | | 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | c.findTexts() sort.Sort(c.objs) } // findPaths by starting with a point that wasn't yet visited, beginning at the top // left of the grid. func (c *canvas) findPaths() { for y := 0; y < c.siz.Y; y++ { p := point{y: y} for x := 0; x < c.siz.X; x++ { p.x = x if c.isVisited(p) { continue } ch := c.at(p) if !ch.isPathStart() { continue |
︙ | ︙ | |||
126 127 128 129 130 131 132 | c.objs = append(c.objs, objs...) } } } // findTexts with a second pass through the grid attempts to identify any text within the grid. func (c *canvas) findTexts() { | | | | 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | c.objs = append(c.objs, objs...) } } } // findTexts with a second pass through the grid attempts to identify any text within the grid. func (c *canvas) findTexts() { for y := 0; y < c.siz.Y; y++ { p := point{} p.y = y for x := 0; x < c.siz.X; x++ { p.x = x if c.isVisited(p) { continue } ch := c.at(p) if !ch.isTextStart() { continue |
︙ | ︙ |
Changes to parser/draw/canvas_test.go.
1 | //----------------------------------------------------------------------------- | | < < < | 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) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import ( "reflect" "strings" |
︙ | ︙ | |||
657 658 659 660 661 662 663 | " | | | | |", " +-----+-------+---------+---+", "", "", } chunk := []byte(strings.Join(data, "\n")) input := make([]byte, 0, len(chunk)*b.N) | | | 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 | " | | | | |", " +-----+-------+---------+---+", "", "", } chunk := []byte(strings.Join(data, "\n")) input := make([]byte, 0, len(chunk)*b.N) for i := 0; i < b.N; i++ { input = append(input, chunk...) } expected := 30 * b.N b.ResetTimer() c, err := newCanvas(input) if err != nil { b.Fatalf("Error creating canvas: %s", err) |
︙ | ︙ |
Changes to parser/draw/char.go.
1 | //----------------------------------------------------------------------------- | | < < < | 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) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import "unicode" type char rune |
︙ | ︙ |
Changes to parser/draw/draw.go.
1 | //----------------------------------------------------------------------------- | | < < < < < | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package draw provides a parser to create SVG from ASCII drawing. // // It is not a parser registered by the general parser framework (directed by // metadata "syntax" of a zettel). It will be used when a zettel is evaluated. package draw import ( "strconv" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" ) const ( defaultFont = "" defaultScaleX = 10 defaultScaleY = 20 ) // ParseDrawBlock parses the content of an eval verbatim node into an SVG image BLOB. func ParseDrawBlock(vn *ast.VerbatimNode) ast.BlockNode { font := defaultFont if val, found := vn.Attrs.Get("font"); found { font = val } scaleX := getScale(vn.Attrs, "x-scale", defaultScaleX) scaleY := getScale(vn.Attrs, "y-scale", defaultScaleY) canvas, err := newCanvas(vn.Content) if err != nil { return ast.CreateParaNode(canvasErrMsg(err)...) } if scaleX < 1 || 1000000 < scaleX { scaleX = defaultScaleX } if scaleY < 1 || 1000000 < scaleY { scaleY = defaultScaleY } svg := canvasToSVG(canvas, font, int(scaleX), int(scaleY)) if len(svg) == 0 { return ast.CreateParaNode(noSVGErrMsg()...) } return &ast.BLOBNode{ Title: "", Syntax: api.ValueSyntaxSVG, Blob: svg, } } func getScale(a attrs.Attributes, key string, defVal int) int { if val, found := a.Get(key); found { if n, err := strconv.Atoi(val); err == nil && 0 < n && n < 100000 { return n |
︙ | ︙ |
Deleted parser/draw/draw_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to parser/draw/object.go.
1 | //----------------------------------------------------------------------------- | | < < < | 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) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import "fmt" // object represents one of an open path, a closed path, or text. |
︙ | ︙ |
Changes to parser/draw/point.go.
1 | //----------------------------------------------------------------------------- | | < < < | 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) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import "fmt" // A renderHint suggests ways the SVG renderer may appropriately represent this point. |
︙ | ︙ |
Changes to parser/draw/svg.go.
1 | //----------------------------------------------------------------------------- | | < < < | 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) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import ( "bytes" "fmt" |
︙ | ︙ | |||
38 39 40 41 42 43 44 | } if font == "" { font = "monospace" } var b bytes.Buffer fmt.Fprintf(&b, | | | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | } if font == "" { font = "monospace" } var b bytes.Buffer fmt.Fprintf(&b, `<svg class="zs-draw" width="%dpx" height="%dpx" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">`, (c.size().X+1)*scaleX, (c.size().Y+1)*scaleY) writeMarkerDefs(&b, c, scaleX, scaleY) // 3 passes, first closed paths, then open paths, then text. writeClosedPaths(&b, c, scaleX, scaleY) writeOpenPaths(&b, c, scaleX, scaleY) writeTexts(&b, c, escape(font), scaleX, scaleY) |
︙ | ︙ | |||
162 163 164 165 166 167 168 | } if !first { io.WriteString(w, "</g>") } } func escape(s string) string { | | | | | 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | } if !first { io.WriteString(w, "</g>") } } func escape(s string) string { b := bytes.Buffer{} strfun.XMLEscape(&b, s) return b.String() } type scaledPoint struct { X float64 Y float64 Hint renderHint } |
︙ | ︙ |
Changes to parser/draw/svg_test.go.
1 | //----------------------------------------------------------------------------- | | < < < | 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) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was originally created by the ASCIIToSVG contributors under an MIT // license, but later changed to fulfil the needs of Zettelstore. The following // statements affects the original code as found on // https://github.com/asciitosvg/asciitosvg (Commit: // ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20): // // Copyright 2012 - 2018 The ASCIIToSVG Contributors // All rights reserved. //----------------------------------------------------------------------------- package draw import ( "strings" "testing" |
︙ | ︙ | |||
36 37 38 39 40 41 42 | // 0 Box with dashed corners and text { []string{ "+--.", "|Hi:", "+--+", }, | | | | | 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 | // 0 Box with dashed corners and text { []string{ "+--.", "|Hi:", "+--+", }, 486, }, // 2 Ticks and dots in lines. { []string{ " ------x----->", "", " <-----*------", }, 1088, }, // 3 Just text { []string{ " foo", }, 265, }, } for i, line := range data { canvas, err := newCanvas([]byte(strings.Join(line.input, "\n"))) if err != nil { t.Fatalf("Error creating canvas: %s", err) } |
︙ | ︙ |
Changes to parser/markdown/markdown.go.
1 | //----------------------------------------------------------------------------- | | < < < < | | | | | | | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package markdown provides a parser for markdown. package markdown import ( "bytes" "fmt" "strconv" gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: "markdown", AltNames: []string{"md"}, IsTextParser: true, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, _ *meta.Meta, _ string) ast.BlockSlice { |
︙ | ︙ | |||
148 149 150 151 152 153 154 | Content: p.acceptRawText(node), } } func (p *mdP) acceptRawText(node gmAst.Node) []byte { lines := node.Lines() result := make([]byte, 0, 512) | | | 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | Content: p.acceptRawText(node), } } func (p *mdP) acceptRawText(node gmAst.Node) []byte { lines := node.Lines() result := make([]byte, 0, 512) for i := 0; i < lines.Len(); i++ { s := lines.At(i) line := s.Value(p.source) if l := len(line); l > 0 { if l > 1 && line[l-2] == '\r' && line[l-1] == '\n' { line = line[0 : l-2] } else if line[l-1] == '\n' || line[l-1] == '\r' { line = line[0 : l-1] |
︙ | ︙ | |||
333 334 335 336 337 338 339 | ';': {}, '<': {}, '=': {}, '>': {}, '?': {}, '@': {}, '[': {}, '\\': {}, ']': {}, '^': {}, '_': {}, '`': {}, '{': {}, '|': {}, '}': {}, '~': {}, } // cleanText removes backslashes from TextNodes and expands entities func cleanText(text []byte, cleanBS bool) string { lastPos := 0 | | | | | | | | | 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 | ';': {}, '<': {}, '=': {}, '>': {}, '?': {}, '@': {}, '[': {}, '\\': {}, ']': {}, '^': {}, '_': {}, '`': {}, '{': {}, '|': {}, '}': {}, '~': {}, } // cleanText removes backslashes from TextNodes and expands entities func cleanText(text []byte, cleanBS bool) string { lastPos := 0 var buf bytes.Buffer for pos, ch := range text { if pos < lastPos { continue } if ch == '&' { inp := input.NewInput([]byte(text[pos:])) if s, ok := inp.ScanEntity(); ok { buf.Write(text[lastPos:pos]) buf.WriteString(s) lastPos = pos + inp.Pos } continue } if cleanBS && ch == '\\' && pos < len(text)-1 { if _, found := ignoreAfterBS[text[pos+1]]; found { buf.Write(text[lastPos:pos]) buf.WriteByte(text[pos+1]) lastPos = pos + 2 } } } if lastPos < len(text) { buf.Write(text[lastPos:]) } return buf.String() } func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice { return ast.InlineSlice{ &ast.LiteralNode{ Kind: ast.LiteralProg, Attrs: nil, //TODO |
︙ | ︙ | |||
436 437 438 439 440 441 442 | Attrs: a, }, } } func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice { is := p.acceptInlineChildren(node) | | | | | | | 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 | Attrs: a, }, } } func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice { is := p.acceptInlineChildren(node) var buf bytes.Buffer _, err := p.textEnc.WriteInlines(&buf, &is) if err != nil { panic(err) } if buf.Len() == 0 { return nil } return ast.InlineSlice{&ast.TextNode{Text: buf.String()}} } func (p *mdP) acceptAutoLink(node *gmAst.AutoLink) ast.InlineSlice { u := node.URL(p.source) if node.AutoLinkType == gmAst.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(u), []byte("mailto:")) { u = append([]byte("mailto:"), u...) } return ast.InlineSlice{ &ast.LinkNode{ Ref: ast.ParseReference(cleanText(u, false)), Inlines: nil, Attrs: nil, // TODO }, } } func (p *mdP) acceptRawHTML(node *gmAst.RawHTML) ast.InlineSlice { segs := make([][]byte, 0, node.Segments.Len()) for i := 0; i < node.Segments.Len(); i++ { segment := node.Segments.At(i) segs = append(segs, segment.Value(p.source)) } return ast.InlineSlice{ &ast.LiteralNode{ Kind: ast.LiteralHTML, Attrs: nil, // TODO: add HTML as language Content: bytes.Join(segs, nil), }, } } |
Changes to parser/markdown/markdown_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | | | | | | | | | 1 2 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 markdown provides a parser for markdown. package markdown import ( "bytes" "testing" "zettelstore.de/z/ast" ) func TestSplitText(t *testing.T) { t.Parallel() var testcases = []struct { text string exp string }{ {"", ""}, {"abc", "Tabc"}, {" ", "S "}, {"abc def", "TabcS Tdef"}, {"abc def ", "TabcS TdefS "}, {" abc def ", "S TabcS TdefS "}, } for i, tc := range testcases { var buf bytes.Buffer for _, in := range splitText(tc.text) { switch n := in.(type) { case *ast.TextNode: buf.WriteByte('T') buf.WriteString(n.Text) case *ast.SpaceNode: buf.WriteByte('S') buf.WriteString(n.Lexeme) default: buf.WriteByte('Q') } } got := buf.String() if tc.exp != got { t.Errorf("TC=%d, text=%q, exp=%q, got=%q", i, tc.text, tc.exp, got) } } } |
Changes to parser/none/none.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | > | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package none provides a none-parser, e.g. for zettel with just metadata. package none import ( "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: api.ValueSyntaxNone, AltNames: []string{}, IsTextParser: false, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(*input.Input, *meta.Meta, string) ast.BlockSlice { return nil } |
︙ | ︙ |
Changes to parser/parser.go.
1 | //----------------------------------------------------------------------------- | | < < < < | < | | | > | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package parser provides a generic interface to a range of different parsers. package parser import ( "context" "fmt" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser/cleaner" ) // Info describes a single parser. // // Before ParseBlocks() or ParseInlines() is called, ensure the input stream to // be valid. This can ce achieved on calling inp.Next() after the input stream // was created. type Info struct { Name string AltNames []string IsTextParser bool IsImageFormat bool ParseBlocks func(*input.Input, *meta.Meta, string) ast.BlockSlice ParseInlines func(*input.Input, string) ast.InlineSlice } var registry = map[string]*Info{} |
︙ | ︙ | |||
75 76 77 78 79 80 81 | } if pi := registry["plain"]; pi != nil { return pi } panic(fmt.Sprintf("No parser for %q found", name)) } | | | | > > > | < < < < < | < < | < < | < < < < | | | | < < | < < < | | | | | 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 | } if pi := registry["plain"]; pi != nil { return pi } panic(fmt.Sprintf("No parser for %q found", name)) } // IsTextParser returns whether the given syntax parses text into an AST or not. func IsTextParser(syntax string) bool { pi, ok := registry[syntax] if !ok { return false } return pi.IsTextParser } // IsImageFormat returns whether the given syntax is known to be an image format. func IsImageFormat(syntax string) bool { pi, ok := registry[syntax] if !ok { return false } return pi.IsImageFormat } // ParseBlocks parses some input and returns a slice of block nodes. func ParseBlocks(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice { return parseBlocksAndClean(inp, m, syntax, hi) } func parseBlocksAndClean(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice { bs := Get(syntax).ParseBlocks(inp, m, syntax) cleaner.CleanBlockSlice(&bs, hi.AllowHTML(syntax)) return bs } // ParseInlines parses some input and returns a slice of inline nodes. func ParseInlines(inp *input.Input, syntax string) ast.InlineSlice { // Do not clean, because we don't know the context where this function will be called. return Get(syntax).ParseInlines(inp, syntax) } // ParseMetadata parses a string as Zettelmarkup, resulting in an inline slice. // Typically used to parse the title or other metadata of type Zettelmarkup. func ParseMetadata(value string) ast.InlineSlice { return ParseInlines(input.NewInput([]byte(value)), api.ValueSyntaxZmk) } // ParseMetadataNoLink parses a string as Zettelmarkup, resulting in an inline slice. // All link and footnote nodes will be removed. func ParseMetadataNoLink(value string) ast.InlineSlice { in := ParseMetadata(value) cleaner.CleanInlineLinks(&in) return in } // ParseZettel parses the zettel based on the syntax. func ParseZettel(ctx context.Context, zettel domain.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode { m := zettel.Meta inhMeta := m if rtConfig != nil { inhMeta = rtConfig.AddDefaultValues(ctx, inhMeta) } if syntax == "" { syntax, _ = inhMeta.Get(api.KeySyntax) } parseMeta := inhMeta if syntax == api.ValueSyntaxNone { parseMeta = m } hi := config.NoHTML if rtConfig != nil { hi = rtConfig.GetHTMLInsecurity() } return &ast.ZettelNode{ Meta: m, Content: zettel.Content, Zid: m.Zid, InhMeta: inhMeta, Ast: parseBlocksAndClean(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, hi), Syntax: syntax, } } |
Changes to parser/parser_test.go.
1 | //----------------------------------------------------------------------------- | | < < < > | | | < | | | < | | | | | > | | | | < | | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package parser provides a generic interface to a range of different parsers. package parser_test import ( "testing" "zettelstore.de/c/api" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) func TestParserType(t *testing.T) { syntaxSet := strfun.NewSet(parser.GetSyntaxes()...) testCases := []struct { syntax string text bool image bool }{ {api.ValueSyntaxHTML, false, false}, {"css", false, false}, {api.ValueSyntaxGif, false, true}, {"jpeg", false, true}, {"jpg", false, true}, {"markdown", true, false}, {"md", true, false}, {"mustache", false, false}, {api.ValueSyntaxNone, false, false}, {"plain", false, false}, {"png", false, true}, {api.ValueSyntaxSVG, false, true}, {api.ValueSyntaxText, false, false}, {"txt", false, false}, {api.ValueSyntaxZmk, true, false}, } for _, tc := range testCases { delete(syntaxSet, tc.syntax) if got := parser.IsTextParser(tc.syntax); got != tc.text { t.Errorf("Syntax %q is text: %v, but got %v", tc.syntax, tc.text, got) } if got := parser.IsImageFormat(tc.syntax); got != tc.image { t.Errorf("Syntax %q is image: %v, but got %v", tc.syntax, tc.image, got) } } for syntax := range syntaxSet { t.Errorf("Forgot to test syntax %q", syntax) |
︙ | ︙ |
Added parser/pikchr/internal/ORIG_LICENSE.
> > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | MIT License Copyright (c) 2022 gopikchr Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
Added parser/pikchr/internal/README.txt.
> > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | This is a fork of gopikchr/gopikchr, adapted to the needs of Zettelstore. File gopikchr.go is generated by gopikchr.y You should not modify gopikchr.go, only gopikchr.Y To generate gopikchr.go you have to install gopikchr/golemon first: go install github.com/gopikchr/golemon@latest Invoke golemon: golemon gopikchr.y This will produce the files gopikchr.go and gopikchr.out You can safely remove gopikchr.out You probably should reformat the generated go file: gofmt -w gopikchr.go In the future, golemon might be incorporated too, to make generation easier and more self-hosted. |
Added parser/pikchr/internal/pikchr.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 3719 3720 3721 3722 3723 3724 3725 3726 3727 3728 3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754 3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780 3781 3782 3783 3784 3785 3786 3787 3788 3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 3799 3800 3801 3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 3812 3813 3814 3815 3816 3817 3818 3819 3820 3821 3822 3823 3824 3825 3826 3827 3828 3829 3830 3831 3832 3833 3834 3835 3836 3837 3838 3839 3840 3841 3842 3843 3844 3845 3846 3847 3848 3849 3850 3851 3852 3853 3854 3855 3856 3857 3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 3903 3904 3905 3906 3907 3908 3909 3910 3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 3958 3959 3960 3961 3962 3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 3992 3993 3994 3995 3996 3997 3998 3999 4000 4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 4048 4049 4050 4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078 4079 4080 4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 4179 4180 4181 4182 4183 4184 4185 4186 4187 4188 4189 4190 4191 4192 4193 4194 4195 4196 4197 4198 4199 4200 4201 4202 4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 4218 4219 4220 4221 4222 4223 4224 4225 4226 4227 4228 4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243 4244 4245 4246 4247 4248 4249 4250 4251 4252 4253 4254 4255 4256 4257 4258 4259 4260 4261 4262 4263 4264 4265 4266 4267 4268 4269 4270 4271 4272 4273 4274 4275 4276 4277 4278 4279 4280 4281 4282 4283 4284 4285 4286 4287 4288 4289 4290 4291 4292 4293 4294 4295 4296 4297 4298 4299 4300 4301 4302 4303 4304 4305 4306 4307 4308 4309 4310 4311 4312 4313 4314 4315 4316 4317 4318 4319 4320 4321 4322 4323 4324 4325 4326 4327 4328 4329 4330 4331 4332 4333 4334 4335 4336 4337 4338 4339 4340 4341 4342 4343 4344 4345 4346 4347 4348 4349 4350 4351 4352 4353 4354 4355 4356 4357 4358 4359 4360 4361 4362 4363 4364 4365 4366 4367 4368 4369 4370 4371 4372 4373 4374 4375 4376 4377 4378 4379 4380 4381 4382 4383 4384 4385 4386 4387 4388 4389 4390 4391 4392 4393 4394 4395 4396 4397 4398 4399 4400 4401 4402 4403 4404 4405 4406 4407 4408 4409 4410 4411 4412 4413 4414 4415 4416 4417 4418 4419 4420 4421 4422 4423 4424 4425 4426 4427 4428 4429 4430 4431 4432 4433 4434 4435 4436 4437 4438 4439 4440 4441 4442 4443 4444 4445 4446 4447 4448 4449 4450 4451 4452 4453 4454 4455 4456 4457 4458 4459 4460 4461 4462 4463 4464 4465 4466 4467 4468 4469 4470 4471 4472 4473 4474 4475 4476 4477 4478 4479 4480 4481 4482 4483 4484 4485 4486 4487 4488 4489 4490 4491 4492 4493 4494 4495 4496 4497 4498 4499 4500 4501 4502 4503 4504 4505 4506 4507 4508 4509 4510 4511 4512 4513 4514 4515 4516 4517 4518 4519 4520 4521 4522 4523 4524 4525 4526 4527 4528 4529 4530 4531 4532 4533 4534 4535 4536 4537 4538 4539 4540 4541 4542 4543 4544 4545 4546 4547 4548 4549 4550 4551 4552 4553 4554 4555 4556 4557 4558 4559 4560 4561 4562 4563 4564 4565 4566 4567 4568 4569 4570 4571 4572 4573 4574 4575 4576 4577 4578 4579 4580 4581 4582 4583 4584 4585 4586 4587 4588 4589 4590 4591 4592 4593 4594 4595 4596 4597 4598 4599 4600 4601 4602 4603 4604 4605 4606 4607 4608 4609 4610 4611 4612 4613 4614 4615 4616 4617 4618 4619 4620 4621 4622 4623 4624 4625 4626 4627 4628 4629 4630 4631 4632 4633 4634 4635 4636 4637 4638 4639 4640 4641 4642 4643 4644 4645 4646 4647 4648 4649 4650 4651 4652 4653 4654 4655 4656 4657 4658 4659 4660 4661 4662 4663 4664 4665 4666 4667 4668 4669 4670 4671 4672 4673 4674 4675 4676 4677 4678 4679 4680 4681 4682 4683 4684 4685 4686 4687 4688 4689 4690 4691 4692 4693 4694 4695 4696 4697 4698 4699 4700 4701 4702 4703 4704 4705 4706 4707 4708 4709 4710 4711 4712 4713 4714 4715 4716 4717 4718 4719 4720 4721 4722 4723 4724 4725 4726 4727 4728 4729 4730 4731 4732 4733 4734 4735 4736 4737 4738 4739 4740 4741 4742 4743 4744 4745 4746 4747 4748 4749 4750 4751 4752 4753 4754 4755 4756 4757 4758 4759 4760 4761 4762 4763 4764 4765 4766 4767 4768 4769 4770 4771 4772 4773 4774 4775 4776 4777 4778 4779 4780 4781 4782 4783 4784 4785 4786 4787 4788 4789 4790 4791 4792 4793 4794 4795 4796 4797 4798 4799 4800 4801 4802 4803 4804 4805 4806 4807 4808 4809 4810 4811 4812 4813 4814 4815 4816 4817 4818 4819 4820 4821 4822 4823 4824 4825 4826 4827 4828 4829 4830 4831 4832 4833 4834 4835 4836 4837 4838 4839 4840 4841 4842 4843 4844 4845 4846 4847 4848 4849 4850 4851 4852 4853 4854 4855 4856 4857 4858 4859 4860 4861 4862 4863 4864 4865 4866 4867 4868 4869 4870 4871 4872 4873 4874 4875 4876 4877 4878 4879 4880 4881 4882 4883 4884 4885 4886 4887 4888 4889 4890 4891 4892 4893 4894 4895 4896 4897 4898 4899 4900 4901 4902 4903 4904 4905 4906 4907 4908 4909 4910 4911 4912 4913 4914 4915 4916 4917 4918 4919 4920 4921 4922 4923 4924 4925 4926 4927 4928 4929 4930 4931 4932 4933 4934 4935 4936 4937 4938 4939 4940 4941 4942 4943 4944 4945 4946 4947 4948 4949 4950 4951 4952 4953 4954 4955 4956 4957 4958 4959 4960 4961 4962 4963 4964 4965 4966 4967 4968 4969 4970 4971 4972 4973 4974 4975 4976 4977 4978 4979 4980 4981 4982 4983 4984 4985 4986 4987 4988 4989 4990 4991 4992 4993 4994 4995 4996 4997 4998 4999 5000 5001 5002 5003 5004 5005 5006 5007 5008 5009 5010 5011 5012 5013 5014 5015 5016 5017 5018 5019 5020 5021 5022 5023 5024 5025 5026 5027 5028 5029 5030 5031 5032 5033 5034 5035 5036 5037 5038 5039 5040 5041 5042 5043 5044 5045 5046 5047 5048 5049 5050 5051 5052 5053 5054 5055 5056 5057 5058 5059 5060 5061 5062 5063 5064 5065 5066 5067 5068 5069 5070 5071 5072 5073 5074 5075 5076 5077 5078 5079 5080 5081 5082 5083 5084 5085 5086 5087 5088 5089 5090 5091 5092 5093 5094 5095 5096 5097 5098 5099 5100 5101 5102 5103 5104 5105 5106 5107 5108 5109 5110 5111 5112 5113 5114 5115 5116 5117 5118 5119 5120 5121 5122 5123 5124 5125 5126 5127 5128 5129 5130 5131 5132 5133 5134 5135 5136 5137 5138 5139 5140 5141 5142 5143 5144 5145 5146 5147 5148 5149 5150 5151 5152 5153 5154 5155 5156 5157 5158 5159 5160 5161 5162 5163 5164 5165 5166 5167 5168 5169 5170 5171 5172 5173 5174 5175 5176 5177 5178 5179 5180 5181 5182 5183 5184 5185 5186 5187 5188 5189 5190 5191 5192 5193 5194 5195 5196 5197 5198 5199 5200 5201 5202 5203 5204 5205 5206 5207 5208 5209 5210 5211 5212 5213 5214 5215 5216 5217 5218 5219 5220 5221 5222 5223 5224 5225 5226 5227 5228 5229 5230 5231 5232 5233 5234 5235 5236 5237 5238 5239 5240 5241 5242 5243 5244 5245 5246 5247 5248 5249 5250 5251 5252 5253 5254 5255 5256 5257 5258 5259 5260 5261 5262 5263 5264 5265 5266 5267 5268 5269 5270 5271 5272 5273 5274 5275 5276 5277 5278 5279 5280 5281 5282 5283 5284 5285 5286 5287 5288 5289 5290 5291 5292 5293 5294 5295 5296 5297 5298 5299 5300 5301 5302 5303 5304 5305 5306 5307 5308 5309 5310 5311 5312 5313 5314 5315 5316 5317 5318 5319 5320 5321 5322 5323 5324 5325 5326 5327 5328 5329 5330 5331 5332 5333 5334 5335 5336 5337 5338 5339 5340 5341 5342 5343 5344 5345 5346 5347 5348 5349 5350 5351 5352 5353 5354 5355 5356 5357 5358 5359 5360 5361 5362 5363 5364 5365 5366 5367 5368 5369 5370 5371 5372 5373 5374 5375 5376 5377 5378 5379 5380 5381 5382 5383 5384 5385 5386 5387 5388 5389 5390 5391 5392 5393 5394 5395 5396 5397 5398 5399 5400 5401 5402 5403 5404 5405 5406 5407 5408 5409 5410 5411 5412 5413 5414 5415 5416 5417 5418 5419 5420 5421 5422 5423 5424 5425 5426 5427 5428 5429 5430 5431 5432 5433 5434 5435 5436 5437 5438 5439 5440 5441 5442 5443 5444 5445 5446 5447 5448 5449 5450 5451 5452 5453 5454 5455 5456 5457 5458 5459 5460 5461 5462 5463 5464 5465 5466 5467 5468 5469 5470 5471 5472 5473 5474 5475 5476 5477 5478 5479 5480 5481 5482 5483 5484 5485 5486 5487 5488 5489 5490 5491 5492 5493 5494 5495 5496 5497 5498 5499 5500 5501 5502 5503 5504 5505 5506 5507 5508 5509 5510 5511 5512 5513 5514 5515 5516 5517 5518 5519 5520 5521 5522 5523 5524 5525 5526 5527 5528 5529 5530 5531 5532 5533 5534 5535 5536 5537 5538 5539 5540 5541 5542 5543 5544 5545 5546 5547 5548 5549 5550 5551 5552 5553 5554 5555 5556 5557 5558 5559 5560 5561 5562 5563 5564 5565 5566 5567 5568 5569 5570 5571 5572 5573 5574 5575 5576 5577 5578 5579 5580 5581 5582 5583 5584 5585 5586 5587 5588 5589 5590 5591 5592 5593 5594 5595 5596 5597 5598 5599 5600 5601 5602 5603 5604 5605 5606 5607 5608 5609 5610 5611 5612 5613 5614 5615 5616 5617 5618 5619 5620 5621 5622 5623 5624 5625 5626 5627 5628 5629 5630 5631 5632 5633 5634 5635 5636 5637 5638 5639 5640 5641 5642 5643 5644 5645 5646 5647 5648 5649 5650 5651 5652 5653 5654 5655 5656 5657 5658 5659 5660 5661 5662 5663 5664 5665 5666 5667 5668 5669 5670 5671 5672 5673 5674 5675 5676 5677 5678 5679 5680 5681 5682 5683 5684 5685 5686 5687 5688 5689 5690 5691 5692 5693 5694 5695 5696 5697 5698 5699 5700 5701 5702 5703 5704 5705 5706 5707 5708 5709 5710 5711 5712 5713 5714 5715 5716 5717 5718 5719 5720 5721 5722 5723 5724 5725 5726 5727 5728 5729 5730 5731 5732 5733 5734 5735 5736 5737 5738 5739 5740 5741 5742 5743 5744 5745 5746 5747 5748 5749 5750 5751 5752 5753 5754 5755 5756 5757 5758 5759 5760 5761 5762 5763 5764 5765 5766 5767 5768 5769 5770 5771 5772 5773 5774 5775 5776 5777 5778 5779 5780 5781 5782 5783 5784 5785 5786 5787 5788 5789 5790 5791 5792 5793 5794 5795 5796 5797 5798 5799 5800 5801 5802 5803 5804 5805 5806 5807 5808 5809 5810 5811 5812 5813 5814 5815 5816 5817 5818 5819 5820 5821 5822 5823 5824 5825 5826 5827 5828 5829 5830 5831 5832 5833 5834 5835 5836 5837 5838 5839 5840 5841 5842 5843 5844 5845 5846 5847 5848 5849 5850 5851 5852 5853 5854 5855 5856 5857 5858 5859 5860 5861 5862 5863 5864 5865 5866 5867 5868 5869 5870 5871 5872 5873 5874 5875 5876 5877 5878 5879 5880 5881 5882 5883 5884 5885 5886 5887 5888 5889 5890 5891 5892 5893 5894 5895 5896 5897 5898 5899 5900 5901 5902 5903 5904 5905 5906 5907 5908 5909 5910 5911 5912 5913 5914 5915 5916 5917 5918 5919 5920 5921 5922 5923 5924 5925 5926 5927 5928 5929 5930 5931 5932 5933 5934 5935 5936 5937 5938 5939 5940 5941 5942 5943 5944 5945 5946 5947 5948 5949 5950 5951 5952 5953 5954 5955 5956 5957 5958 5959 5960 5961 5962 5963 5964 5965 5966 5967 5968 5969 5970 5971 5972 5973 5974 5975 5976 5977 5978 5979 5980 5981 5982 5983 5984 5985 5986 5987 5988 5989 5990 5991 5992 5993 5994 5995 5996 5997 5998 5999 6000 6001 6002 6003 6004 6005 6006 6007 6008 6009 6010 6011 6012 6013 6014 6015 6016 6017 6018 6019 6020 6021 6022 6023 6024 6025 6026 6027 6028 6029 6030 6031 6032 6033 6034 6035 6036 6037 6038 6039 6040 6041 6042 6043 6044 6045 6046 6047 6048 6049 6050 6051 6052 6053 6054 6055 6056 6057 6058 6059 6060 6061 6062 6063 6064 6065 6066 6067 6068 6069 6070 6071 6072 6073 6074 6075 6076 6077 6078 6079 6080 6081 6082 6083 6084 6085 6086 6087 6088 6089 6090 6091 6092 6093 6094 6095 6096 6097 6098 6099 6100 6101 6102 6103 6104 6105 6106 6107 6108 6109 6110 6111 6112 6113 6114 6115 6116 6117 6118 6119 6120 6121 6122 6123 6124 6125 6126 6127 6128 6129 6130 6131 6132 6133 6134 6135 6136 6137 6138 6139 6140 6141 6142 6143 6144 6145 6146 6147 6148 6149 6150 6151 6152 6153 6154 6155 6156 6157 6158 6159 6160 6161 6162 6163 6164 6165 6166 6167 6168 6169 6170 6171 6172 6173 6174 6175 6176 6177 6178 6179 6180 6181 6182 6183 6184 6185 6186 6187 6188 6189 6190 6191 6192 6193 6194 6195 6196 6197 6198 6199 6200 6201 6202 6203 6204 6205 6206 6207 6208 6209 6210 6211 6212 6213 6214 6215 6216 6217 6218 6219 6220 6221 6222 6223 6224 6225 6226 6227 6228 6229 6230 6231 6232 6233 6234 6235 6236 6237 6238 6239 6240 6241 6242 6243 6244 6245 6246 6247 6248 6249 6250 6251 6252 6253 6254 6255 6256 6257 6258 6259 6260 6261 6262 6263 6264 6265 6266 6267 6268 6269 6270 6271 6272 6273 6274 6275 6276 6277 6278 6279 6280 6281 6282 6283 6284 6285 6286 6287 6288 6289 6290 6291 6292 6293 6294 6295 6296 6297 6298 6299 6300 6301 6302 6303 6304 6305 6306 6307 6308 6309 6310 6311 6312 6313 6314 6315 6316 6317 6318 6319 6320 6321 6322 6323 6324 6325 6326 6327 6328 6329 6330 6331 6332 6333 6334 6335 6336 6337 6338 6339 6340 6341 6342 6343 6344 6345 6346 6347 6348 6349 6350 6351 6352 6353 6354 6355 6356 6357 6358 6359 6360 6361 6362 6363 6364 6365 6366 6367 6368 6369 6370 6371 6372 6373 6374 6375 6376 6377 6378 6379 6380 6381 6382 6383 6384 6385 6386 6387 6388 6389 6390 6391 6392 6393 6394 6395 6396 6397 6398 6399 6400 6401 6402 6403 6404 6405 6406 6407 6408 6409 6410 6411 6412 6413 6414 6415 6416 6417 6418 6419 6420 6421 6422 6423 6424 6425 6426 6427 6428 6429 6430 6431 6432 6433 6434 6435 6436 6437 6438 6439 6440 6441 6442 6443 6444 6445 6446 6447 6448 6449 6450 6451 6452 6453 6454 6455 6456 6457 6458 6459 6460 6461 6462 6463 6464 6465 6466 6467 6468 6469 6470 6471 6472 6473 6474 6475 6476 6477 6478 6479 6480 6481 6482 6483 6484 6485 6486 6487 6488 6489 6490 6491 6492 6493 6494 6495 6496 6497 6498 6499 6500 6501 6502 6503 6504 6505 6506 6507 6508 6509 6510 6511 6512 6513 6514 6515 6516 6517 6518 6519 6520 6521 6522 6523 6524 6525 6526 6527 6528 6529 6530 6531 6532 6533 6534 6535 6536 6537 6538 6539 6540 6541 6542 6543 6544 6545 6546 6547 6548 6549 6550 6551 6552 6553 6554 6555 6556 6557 6558 6559 6560 6561 6562 6563 6564 6565 6566 6567 6568 6569 6570 6571 6572 6573 6574 6575 6576 6577 6578 6579 6580 6581 6582 6583 6584 6585 6586 6587 6588 6589 6590 6591 6592 6593 6594 6595 6596 6597 6598 6599 6600 6601 6602 6603 6604 6605 6606 6607 6608 6609 6610 6611 6612 6613 6614 6615 6616 6617 6618 6619 6620 6621 6622 6623 6624 6625 6626 6627 6628 6629 6630 6631 6632 6633 6634 6635 6636 6637 6638 6639 6640 6641 6642 6643 6644 6645 6646 6647 6648 6649 6650 6651 6652 6653 6654 6655 6656 6657 6658 6659 6660 6661 6662 6663 6664 6665 6666 6667 6668 6669 6670 6671 6672 6673 6674 6675 6676 6677 6678 6679 6680 6681 6682 6683 6684 6685 6686 6687 6688 6689 6690 6691 6692 6693 6694 6695 6696 6697 6698 6699 6700 6701 6702 6703 6704 6705 6706 6707 6708 6709 6710 6711 6712 6713 6714 6715 6716 6717 6718 6719 6720 6721 6722 6723 6724 6725 6726 6727 6728 6729 6730 6731 6732 6733 6734 6735 6736 6737 6738 6739 6740 6741 6742 6743 6744 6745 6746 6747 6748 6749 6750 6751 6752 6753 6754 6755 6756 6757 6758 6759 6760 6761 6762 6763 6764 6765 6766 6767 6768 6769 6770 6771 6772 6773 6774 6775 6776 6777 6778 6779 6780 6781 6782 6783 6784 6785 6786 6787 6788 6789 6790 6791 6792 6793 6794 6795 6796 6797 6798 6799 6800 6801 6802 6803 6804 6805 6806 6807 6808 6809 6810 6811 6812 6813 6814 6815 6816 6817 6818 6819 6820 6821 6822 6823 6824 6825 6826 6827 6828 6829 6830 6831 6832 6833 6834 6835 6836 6837 6838 6839 6840 6841 6842 6843 6844 6845 6846 6847 6848 6849 6850 6851 6852 6853 6854 6855 6856 6857 6858 6859 6860 6861 6862 6863 6864 6865 6866 6867 6868 6869 6870 6871 6872 6873 6874 6875 6876 6877 6878 6879 6880 6881 6882 6883 6884 6885 6886 6887 6888 6889 6890 6891 6892 6893 6894 6895 6896 6897 6898 6899 6900 6901 6902 6903 6904 6905 6906 6907 6908 6909 6910 6911 6912 6913 6914 6915 6916 6917 6918 6919 6920 6921 6922 6923 6924 6925 6926 6927 6928 6929 6930 6931 6932 6933 6934 6935 6936 6937 6938 6939 6940 6941 6942 6943 6944 6945 6946 6947 6948 6949 6950 6951 6952 6953 6954 6955 6956 6957 6958 6959 6960 6961 6962 6963 6964 6965 6966 6967 6968 6969 6970 6971 6972 6973 6974 6975 6976 6977 6978 6979 6980 6981 6982 6983 6984 6985 6986 6987 6988 6989 6990 6991 6992 6993 6994 6995 6996 6997 6998 6999 7000 7001 7002 7003 7004 7005 7006 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7017 7018 7019 7020 7021 7022 7023 7024 7025 7026 7027 7028 7029 7030 7031 7032 7033 7034 7035 7036 7037 7038 7039 7040 7041 7042 7043 7044 7045 7046 7047 7048 7049 7050 7051 7052 7053 7054 7055 7056 7057 7058 7059 7060 7061 7062 7063 7064 7065 7066 7067 7068 7069 7070 7071 7072 7073 7074 7075 7076 7077 7078 7079 7080 7081 7082 7083 7084 7085 7086 7087 7088 7089 7090 7091 7092 7093 7094 7095 7096 7097 7098 7099 7100 7101 7102 7103 7104 7105 7106 7107 7108 7109 7110 7111 7112 7113 7114 7115 7116 7117 7118 7119 7120 7121 7122 7123 7124 7125 7126 7127 7128 7129 7130 7131 7132 7133 7134 7135 7136 7137 7138 7139 7140 7141 7142 7143 7144 7145 7146 7147 7148 7149 7150 7151 7152 7153 7154 7155 7156 7157 7158 7159 7160 7161 7162 7163 7164 7165 7166 7167 7168 7169 7170 7171 7172 7173 7174 7175 7176 7177 7178 7179 7180 7181 7182 7183 7184 7185 7186 7187 7188 7189 7190 7191 7192 7193 7194 7195 7196 7197 7198 7199 7200 7201 7202 7203 7204 7205 7206 7207 7208 7209 7210 7211 7212 7213 7214 7215 7216 7217 7218 7219 7220 7221 7222 7223 7224 7225 7226 7227 7228 7229 7230 7231 7232 7233 7234 7235 7236 7237 7238 7239 7240 7241 7242 7243 7244 7245 7246 7247 7248 7249 7250 7251 7252 7253 7254 7255 7256 7257 7258 7259 7260 7261 7262 7263 7264 7265 7266 7267 7268 7269 7270 7271 7272 7273 7274 7275 7276 7277 7278 7279 7280 7281 7282 7283 7284 7285 7286 7287 7288 7289 7290 7291 7292 7293 7294 7295 7296 7297 7298 7299 7300 7301 7302 7303 7304 7305 7306 7307 7308 7309 7310 7311 7312 7313 7314 7315 7316 7317 7318 7319 7320 7321 7322 7323 7324 7325 7326 7327 7328 7329 7330 7331 7332 7333 7334 7335 7336 7337 7338 7339 7340 7341 7342 7343 7344 7345 7346 7347 7348 7349 7350 7351 7352 7353 7354 7355 7356 7357 7358 7359 7360 7361 7362 7363 7364 7365 7366 7367 7368 7369 7370 7371 7372 7373 7374 7375 7376 7377 7378 7379 7380 7381 7382 7383 7384 7385 7386 7387 7388 7389 7390 7391 7392 7393 7394 7395 7396 7397 7398 7399 7400 7401 7402 7403 7404 7405 7406 7407 7408 7409 7410 7411 7412 7413 7414 7415 7416 7417 7418 7419 7420 7421 7422 7423 7424 7425 7426 7427 7428 7429 7430 7431 7432 7433 7434 7435 7436 7437 7438 7439 7440 7441 7442 7443 7444 7445 7446 7447 7448 7449 7450 7451 7452 7453 7454 7455 7456 7457 7458 7459 7460 7461 7462 7463 7464 7465 7466 7467 7468 7469 7470 7471 7472 7473 7474 7475 7476 7477 7478 7479 7480 7481 7482 7483 7484 7485 7486 7487 7488 7489 7490 7491 7492 7493 7494 7495 7496 7497 7498 7499 7500 7501 7502 7503 7504 7505 7506 7507 7508 7509 7510 7511 7512 7513 7514 7515 7516 7517 7518 7519 7520 7521 7522 7523 7524 7525 7526 7527 7528 7529 7530 7531 7532 7533 7534 7535 7536 7537 7538 7539 7540 7541 7542 7543 7544 7545 7546 7547 7548 7549 7550 7551 7552 7553 7554 7555 7556 7557 7558 7559 7560 7561 7562 7563 7564 7565 7566 7567 7568 7569 7570 7571 7572 7573 7574 7575 7576 7577 7578 7579 7580 7581 7582 7583 7584 7585 7586 7587 7588 7589 7590 7591 7592 7593 7594 7595 7596 7597 7598 7599 7600 7601 7602 7603 7604 7605 7606 7607 7608 7609 7610 7611 7612 7613 7614 7615 7616 7617 7618 7619 7620 7621 7622 7623 7624 7625 7626 7627 7628 7629 7630 7631 7632 7633 7634 7635 7636 7637 7638 7639 7640 7641 7642 7643 7644 7645 7646 7647 7648 7649 7650 7651 7652 7653 7654 7655 7656 7657 7658 7659 7660 7661 7662 7663 7664 7665 7666 7667 7668 7669 7670 7671 7672 7673 7674 7675 7676 7677 7678 7679 7680 7681 7682 7683 7684 7685 7686 7687 7688 7689 7690 7691 7692 7693 7694 7695 7696 7697 7698 7699 7700 7701 7702 7703 7704 7705 7706 7707 7708 7709 7710 7711 7712 7713 7714 7715 7716 7717 7718 7719 7720 7721 7722 7723 7724 7725 7726 7727 7728 7729 7730 7731 7732 7733 7734 7735 7736 7737 7738 7739 7740 7741 7742 7743 7744 7745 7746 7747 7748 7749 7750 7751 7752 7753 7754 7755 7756 7757 7758 7759 7760 7761 7762 7763 7764 7765 7766 7767 7768 7769 7770 7771 7772 7773 7774 7775 7776 7777 7778 7779 7780 7781 7782 7783 7784 7785 7786 7787 7788 7789 7790 7791 7792 7793 7794 7795 7796 7797 7798 7799 7800 7801 7802 7803 7804 7805 7806 7807 7808 7809 7810 7811 7812 7813 7814 7815 7816 7817 7818 7819 7820 7821 7822 7823 7824 7825 7826 7827 7828 7829 7830 7831 7832 7833 7834 7835 7836 7837 7838 7839 7840 7841 7842 7843 7844 7845 7846 7847 7848 7849 7850 7851 7852 7853 7854 7855 7856 7857 7858 7859 7860 7861 7862 7863 7864 7865 7866 7867 7868 7869 7870 7871 7872 7873 7874 7875 7876 7877 7878 7879 7880 7881 7882 7883 7884 7885 7886 7887 7888 7889 7890 7891 7892 7893 7894 7895 7896 7897 7898 7899 7900 7901 7902 7903 7904 7905 7906 7907 7908 7909 7910 7911 7912 7913 7914 7915 7916 7917 7918 7919 7920 7921 7922 7923 7924 7925 7926 7927 7928 7929 7930 7931 7932 7933 7934 7935 7936 7937 7938 7939 7940 7941 7942 7943 7944 7945 7946 7947 7948 7949 7950 7951 7952 7953 7954 7955 7956 7957 7958 7959 7960 7961 7962 7963 7964 7965 7966 7967 7968 7969 7970 7971 7972 7973 7974 7975 7976 7977 7978 7979 7980 7981 7982 7983 7984 7985 7986 7987 7988 7989 7990 7991 7992 7993 7994 7995 7996 7997 7998 7999 8000 8001 8002 8003 8004 8005 8006 8007 8008 8009 8010 8011 8012 8013 8014 8015 8016 8017 8018 8019 8020 8021 8022 8023 8024 8025 8026 8027 8028 8029 8030 8031 8032 8033 8034 8035 8036 8037 8038 8039 8040 8041 8042 8043 8044 8045 8046 8047 8048 8049 8050 8051 8052 8053 8054 8055 8056 8057 8058 8059 8060 8061 8062 8063 8064 8065 8066 8067 8068 8069 8070 8071 8072 8073 8074 8075 8076 8077 8078 8079 8080 8081 8082 8083 8084 8085 8086 8087 8088 8089 8090 8091 8092 8093 8094 8095 8096 8097 8098 8099 8100 8101 8102 8103 8104 8105 8106 8107 8108 8109 8110 8111 8112 8113 8114 8115 8116 8117 8118 8119 8120 8121 8122 8123 8124 8125 8126 8127 8128 8129 8130 8131 8132 8133 8134 8135 8136 8137 8138 8139 8140 8141 8142 8143 8144 8145 8146 8147 8148 8149 8150 8151 8152 8153 8154 8155 8156 8157 8158 8159 8160 8161 8162 8163 8164 8165 8166 8167 8168 8169 8170 8171 8172 8173 8174 8175 8176 8177 8178 8179 8180 8181 8182 8183 8184 8185 8186 8187 8188 8189 8190 8191 8192 8193 8194 8195 8196 8197 8198 8199 8200 8201 8202 8203 8204 8205 8206 8207 8208 8209 8210 8211 8212 8213 8214 8215 8216 8217 8218 8219 8220 8221 8222 8223 8224 8225 8226 8227 8228 8229 8230 8231 8232 8233 8234 8235 8236 8237 8238 8239 8240 8241 8242 8243 8244 8245 8246 8247 8248 8249 8250 8251 8252 8253 8254 8255 8256 8257 8258 8259 8260 8261 8262 8263 8264 8265 8266 8267 8268 8269 8270 8271 8272 8273 8274 8275 8276 8277 8278 8279 8280 8281 8282 8283 8284 8285 8286 8287 8288 8289 8290 8291 8292 8293 8294 8295 8296 8297 8298 8299 8300 8301 8302 8303 8304 8305 8306 8307 8308 8309 8310 8311 8312 8313 8314 8315 8316 8317 8318 8319 8320 8321 8322 8323 8324 8325 8326 8327 8328 8329 8330 8331 8332 8333 8334 8335 8336 8337 8338 8339 8340 8341 8342 8343 8344 8345 8346 8347 8348 8349 8350 8351 8352 8353 8354 8355 8356 8357 8358 8359 8360 8361 8362 8363 8364 8365 8366 8367 8368 8369 8370 8371 8372 8373 8374 8375 8376 8377 8378 8379 8380 8381 8382 8383 8384 8385 8386 8387 8388 8389 8390 8391 8392 8393 8394 8395 8396 8397 8398 8399 8400 8401 8402 8403 8404 8405 8406 8407 8408 8409 8410 8411 8412 8413 8414 8415 8416 8417 8418 8419 8420 8421 8422 8423 8424 8425 8426 8427 8428 8429 8430 8431 8432 8433 8434 8435 8436 8437 8438 8439 8440 8441 8442 8443 8444 8445 8446 8447 8448 8449 8450 8451 8452 8453 8454 8455 8456 8457 8458 8459 8460 8461 8462 8463 8464 8465 8466 8467 8468 8469 8470 8471 8472 8473 8474 8475 8476 8477 8478 8479 8480 8481 8482 8483 8484 8485 8486 8487 8488 | /* This file is automatically generated by Lemon from input grammar ** source file "pikchr.y". */ //lint:file-ignore *,U1000 Ignore all unused code, it's generated /* ** Zero-Clause BSD license: ** ** Copyright (C) 2020-09-01 by D. Richard Hipp <drh@sqlite.org> ** ** Permission to use, copy, modify, and/or distribute this software for ** any purpose with or without fee is hereby granted. ** **************************************************************************** ** ** This software translates a PIC-inspired diagram language into SVG. ** ** PIKCHR (pronounced like "picture") is *mostly* backwards compatible ** with legacy PIC, though some features of legacy PIC are removed ** (for example, the "sh" command is removed for security) and ** many enhancements are added. ** ** PIKCHR is designed for use in an internet facing web environment. ** In particular, PIKCHR is designed to safely generate benign SVG from ** source text that provided by a hostile agent. ** ** This code was originally written by D. Richard Hipp using documentation ** from prior PIC implementations but without reference to prior code. ** All of the code in this project is original. ** ** This file implements a C-language subroutine that accepts a string ** of PIKCHR language text and generates a second string of SVG output that ** renders the drawing defined by the input. Space to hold the returned ** string is obtained from malloc() and should be freed by the caller. ** NULL might be returned if there is a memory allocation error. ** ** If there are errors in the PIKCHR input, the output will consist of an ** error message and the original PIKCHR input text (inside of <pre>...</pre>). ** ** The subroutine implemented by this file is intended to be stand-alone. ** It uses no external routines other than routines commonly found in ** the standard C library. ** **************************************************************************** ** COMPILING: ** ** The original source text is a mixture of C99 and "Lemon" ** (See https://sqlite.org/src/file/doc/lemon.html). Lemon is an LALR(1) ** parser generator program, similar to Yacc. The grammar of the ** input language is specified in Lemon. C-code is attached. Lemon ** runs to generate a single output file ("pikchr.c") which is then ** compiled to generate the Pikchr library. This header comment is ** preserved in the Lemon output, so you might be reading this in either ** the generated "pikchr.c" file that is output by Lemon, or in the ** "pikchr.y" source file that is input into Lemon. If you make changes, ** you should change the input source file "pikchr.y", not the ** Lemon-generated output file. ** ** Basic compilation steps: ** ** lemon pikchr.y ** cc pikchr.c -o pikchr.o ** ** Add -DPIKCHR_SHELL to add a main() routine that reads input files ** and sends them through Pikchr, for testing. Add -DPIKCHR_FUZZ for ** -fsanitizer=fuzzer testing. ** **************************************************************************** ** IMPLEMENTATION NOTES (for people who want to understand the internal ** operation of this software, perhaps to extend the code or to fix bugs): ** ** Each call to pikchr() uses a single instance of the Pik structure to ** track its internal state. The Pik structure lives for the duration ** of the pikchr() call. ** ** The input is a sequence of objects or "statements". Each statement is ** parsed into a PObj object. These are stored on an extensible array ** called PList. All parameters to each PObj are computed as the ** object is parsed. (Hence, the parameters to a PObj may only refer ** to prior statements.) Once the PObj is completely assembled, it is ** added to the end of a PList and never changes thereafter - except, ** PObj objects that are part of a "[...]" block might have their ** absolute position shifted when the outer [...] block is positioned. ** But apart from this repositioning, PObj objects are unchanged once ** they are added to the list. The order of statements on a PList does ** not change. ** ** After all input has been parsed, the top-level PList is walked to ** generate output. Sub-lists resulting from [...] blocks are scanned ** as they are encountered. All input must be collected and parsed ahead ** of output generation because the size and position of statements must be ** known in order to compute a bounding box on the output. ** ** Each PObj is on a "layer". (The common case is that all PObj's are ** on a single layer, but multiple layers are possible.) A separate pass ** is made through the list for each layer. ** ** After all output is generated, the Pik object and all the PList ** and PObj objects are deallocated and the generated output string is ** returned. Upon any error, the Pik.nErr flag is set, processing quickly ** stops, and the stack unwinds. No attempt is made to continue reading ** input after an error. ** ** Most statements begin with a class name like "box" or "arrow" or "move". ** There is a class named "text" which is used for statements that begin ** with a string literal. You can also specify the "text" class. ** A Sublist ("[...]") is a single object that contains a pointer to ** its substatements, all gathered onto a separate PList object. ** ** Variables go into PVar objects that form a linked list. ** ** Each PObj has zero or one names. Input constructs that attempt ** to assign a new name from an older name, for example: ** ** Abc: Abc + (0.5cm, 0) ** ** Statements like these generate a new "noop" object at the specified ** place and with the given name. As place-names are searched by scanning ** the list in reverse order, this has the effect of overriding the "Abc" ** name when referenced by subsequent objects. */ package internal import ( "bytes" "fmt" "io" "math" "os" "regexp" "strconv" "strings" ) // Limit the number of tokens in a single script to avoid run-away macro expansion attacks. // See forum post https://pikchr.org/home/forumpost/ef8684c6955a411a const PIKCHR_TOKEN_LIMIT = 100000 // Numeric value type PNum = float64 // Compass points const ( CP_N uint8 = iota + 1 CP_NE CP_E CP_SE CP_S CP_SW CP_W CP_NW CP_C /* .center or .c */ CP_END /* .end */ CP_START /* .start */ ) /* Heading angles corresponding to compass points */ var pik_hdg_angle = []PNum{ /* none */ 0.0, /* N */ 0.0, /* NE */ 45.0, /* E */ 90.0, /* SE */ 135.0, /* S */ 180.0, /* SW */ 225.0, /* W */ 270.0, /* NW */ 315.0, /* C */ 0.0, } /* Built-in functions */ const ( FN_ABS = 0 FN_COS = 1 FN_INT = 2 FN_MAX = 3 FN_MIN = 4 FN_SIN = 5 FN_SQRT = 6 ) /* Text position and style flags. Stored in PToken.eCode so limited ** to 15 bits. */ const ( TP_LJUST = 0x0001 /* left justify...... */ TP_RJUST = 0x0002 /* ...Right justify */ TP_JMASK = 0x0003 /* Mask for justification bits */ TP_ABOVE2 = 0x0004 /* Position text way above PObj.ptAt */ TP_ABOVE = 0x0008 /* Position text above PObj.ptAt */ TP_CENTER = 0x0010 /* On the line */ TP_BELOW = 0x0020 /* Position text below PObj.ptAt */ TP_BELOW2 = 0x0040 /* Position text way below PObj.ptAt */ TP_VMASK = 0x007c /* Mask for text positioning flags */ TP_BIG = 0x0100 /* Larger font */ TP_SMALL = 0x0200 /* Smaller font */ TP_XTRA = 0x0400 /* Amplify TP_BIG or TP_SMALL */ TP_SZMASK = 0x0700 /* Font size mask */ TP_ITALIC = 0x1000 /* Italic font */ TP_BOLD = 0x2000 /* Bold font */ TP_FMASK = 0x3000 /* Mask for font style */ TP_ALIGN = 0x4000 /* Rotate to align with the line */ ) /* An object to hold a position in 2-D space */ type PPoint struct { /* X and Y coordinates */ x PNum y PNum } /* A bounding box */ type PBox struct { /* Lower-left and top-right corners */ sw PPoint ne PPoint } /* An Absolute or a relative distance. The absolute distance ** is stored in rAbs and the relative distance is stored in rRel. ** Usually, one or the other will be 0.0. When using a PRel to ** update an existing value, the computation is usually something ** like this: ** ** value = PRel.rAbs + value*PRel.rRel ** */ type PRel struct { rAbs PNum /* Absolute value */ rRel PNum /* Value relative to current value */ } /* A variable created by the ID = EXPR construct of the PIKCHR script ** ** PIKCHR (and PIC) scripts do not use many varaibles, so it is reasonable ** to store them all on a linked list. */ type PVar struct { zName string /* Name of the variable */ val PNum /* Value of the variable */ pNext *PVar /* Next variable in a list of them all */ } /* A single token in the parser input stream */ type PToken struct { z []byte /* Pointer to the token text */ n int /* Length of the token in bytes */ eCode int16 /* Auxiliary code */ eType uint8 /* The numeric parser code */ eEdge uint8 /* Corner value for corner keywords */ } func (p PToken) String() string { return string(p.z[:p.n]) } /* Return negative, zero, or positive if pToken is less than, equal to ** or greater than the zero-terminated string z[] */ func pik_token_eq(pToken *PToken, z string) int { c := bytencmp(pToken.z, z, pToken.n) if c == 0 && len(z) > pToken.n && z[pToken.n] != 0 { c = -1 } return c } /* Extra token types not generated by LEMON but needed by the ** tokenizer */ const ( T_PARAMETER = 253 /* $1, $2, ..., $9 */ T_WHITESPACE = 254 /* Whitespace of comments */ T_ERROR = 255 /* Any text that is not a valid token */ ) /* Directions of movement */ const ( DIR_RIGHT = 0 DIR_DOWN = 1 DIR_LEFT = 2 DIR_UP = 3 ) func ValidDir(x uint8) bool { return x >= 0 && x <= 3 } func IsUpDown(x uint8) bool { return x&1 == 1 } func IsLeftRight(x uint8) bool { return x&1 == 0 } /* Bitmask for the various attributes for PObj. These bits are ** collected in PObj.mProp and PObj.mCalc to check for constraint ** errors. */ const ( A_WIDTH = 0x0001 A_HEIGHT = 0x0002 A_RADIUS = 0x0004 A_THICKNESS = 0x0008 A_DASHED = 0x0010 /* Includes "dotted" */ A_FILL = 0x0020 A_COLOR = 0x0040 A_ARROW = 0x0080 A_FROM = 0x0100 A_CW = 0x0200 A_AT = 0x0400 A_TO = 0x0800 /* one or more movement attributes */ A_FIT = 0x1000 ) /* A single graphics object */ type PObj struct { typ *PClass /* Object type or class */ errTok PToken /* Reference token for error messages */ ptAt PPoint /* Reference point for the object */ ptEnter PPoint /* Entry and exit points */ ptExit PPoint pSublist []*PObj /* Substructure for [...] objects */ zName string /* Name assigned to this statement */ w PNum /* "width" property */ h PNum /* "height" property */ rad PNum /* "radius" property */ sw PNum /* "thickness" property. (Mnemonic: "stroke width")*/ dotted PNum /* "dotted" property. <=0.0 for off */ dashed PNum /* "dashed" property. <=0.0 for off */ fill PNum /* "fill" property. Negative for off */ color PNum /* "color" property */ with PPoint /* Position constraint from WITH clause */ eWith uint8 /* Type of heading point on WITH clause */ cw bool /* True for clockwise arc */ larrow bool /* Arrow at beginning (<- or <->) */ rarrow bool /* Arrow at end (-> or <->) */ bClose bool /* True if "close" is seen */ bChop bool /* True if "chop" is seen */ nTxt uint8 /* Number of text values */ mProp uint /* Masks of properties set so far */ mCalc uint /* Values computed from other constraints */ aTxt [5]PToken /* Text with .eCode holding TP flags */ iLayer int /* Rendering order */ inDir uint8 /* Entry and exit directions */ outDir uint8 nPath int /* Number of path points */ aPath []PPoint /* Array of path points */ pFrom *PObj /* End-point objects of a path */ pTo *PObj bbox PBox /* Bounding box */ } // A list of graphics objects. type PList = []*PObj /* A macro definition */ type PMacro struct { pNext *PMacro /* Next in the list */ macroName PToken /* Name of the macro */ macroBody PToken /* Body of the macro */ inUse bool /* Do not allow recursion */ } /* Each call to the pikchr() subroutine uses an instance of the following ** object to pass around context to all of its subroutines. */ type Pik struct { nErr int /* Number of errors seen */ nToken int // Number of tokens parsed sIn PToken /* Input Pikchr-language text */ zOut bytes.Buffer /* Result accumulates here */ nOut uint /* Bytes written to zOut[] so far */ nOutAlloc uint /* Space allocated to zOut[] */ eDir uint8 /* Current direction */ mFlags uint /* Flags passed to pikchr() */ cur *PObj /* Object under construction */ lastRef *PObj /* Last object references by name */ list []*PObj /* Object list under construction */ pMacros *PMacro /* List of all defined macros */ pVar *PVar /* Application-defined variables */ bbox PBox /* Bounding box around all statements */ /* Cache of layout values. <=0.0 for unknown... */ rScale PNum /* Multiply to convert inches to pixels */ fontScale PNum /* Scale fonts by this percent */ charWidth PNum /* Character width */ charHeight PNum /* Character height */ wArrow PNum /* Width of arrowhead at the fat end */ hArrow PNum /* Ht of arrowhead - dist from tip to fat end */ bLayoutVars bool /* True if cache is valid */ thenFlag bool /* True if "then" seen */ samePath bool /* aTPath copied by "same" */ zClass string /* Class name for the <svg> */ wSVG int /* Width and height of the <svg> */ hSVG int fgcolor int /* foreground color value, or -1 for none */ bgcolor int /* background color value, or -1 for none */ /* Paths for lines are constructed here first, then transferred into ** the PObj object at the end: */ nTPath int /* Number of entries on aTPath[] */ mTPath int /* For last entry, 1: x set, 2: y set */ aTPath [1000]PPoint /* Path under construction */ /* Error contexts */ nCtx int /* Number of error contexts */ aCtx [10]PToken /* Nested error contexts */ svgWidth, svgHeight string // Explicit width/height, if not given by scale. svgFontScale PNum } /* Include PIKCHR_PLAINTEXT_ERRORS among the bits of mFlags on the 3rd ** argument to pikchr() in order to cause error message text to come out ** as text/plain instead of as text/html */ const PIKCHR_PLAINTEXT_ERRORS = 0x0001 /* Include PIKCHR_DARK_MODE among the mFlag bits to invert colors. */ const PIKCHR_DARK_MODE = 0x0002 /* ** The behavior of an object class is defined by an instance of ** this structure. This is the "virtual method" table. */ type PClass struct { zName string /* Name of class */ isLine bool /* True if a line class */ eJust int8 /* Use box-style text justification */ xInit func(*Pik, *PObj) /* Initializer */ xNumProp func(*Pik, *PObj, *PToken) /* Value change notification */ xCheck func(*Pik, *PObj) /* Checks to do after parsing */ xChop func(*Pik, *PObj, *PPoint) PPoint /* Chopper */ xOffset func(*Pik, *PObj, uint8) PPoint /* Offset from .c to edge point */ xFit func(pik *Pik, pobj *PObj, w PNum, h PNum) /* Size to fit text */ xRender func(*Pik, *PObj) /* Render */ } func yytestcase(condition bool) {} //line 480 "pikchr.go" /**************** End of %include directives **********************************/ /* These constants specify the various numeric values for terminal symbols. ***************** Begin token definitions *************************************/ const ( T_ID = 1 T_EDGEPT = 2 T_OF = 3 T_PLUS = 4 T_MINUS = 5 T_STAR = 6 T_SLASH = 7 T_PERCENT = 8 T_UMINUS = 9 T_EOL = 10 T_ASSIGN = 11 T_PLACENAME = 12 T_COLON = 13 T_ASSERT = 14 T_LP = 15 T_EQ = 16 T_RP = 17 T_DEFINE = 18 T_CODEBLOCK = 19 T_FILL = 20 T_COLOR = 21 T_THICKNESS = 22 T_PRINT = 23 T_STRING = 24 T_COMMA = 25 T_CLASSNAME = 26 T_LB = 27 T_RB = 28 T_UP = 29 T_DOWN = 30 T_LEFT = 31 T_RIGHT = 32 T_CLOSE = 33 T_CHOP = 34 T_FROM = 35 T_TO = 36 T_THEN = 37 T_HEADING = 38 T_GO = 39 T_AT = 40 T_WITH = 41 T_SAME = 42 T_AS = 43 T_FIT = 44 T_BEHIND = 45 T_UNTIL = 46 T_EVEN = 47 T_DOT_E = 48 T_HEIGHT = 49 T_WIDTH = 50 T_RADIUS = 51 T_DIAMETER = 52 T_DOTTED = 53 T_DASHED = 54 T_CW = 55 T_CCW = 56 T_LARROW = 57 T_RARROW = 58 T_LRARROW = 59 T_INVIS = 60 T_THICK = 61 T_THIN = 62 T_SOLID = 63 T_CENTER = 64 T_LJUST = 65 T_RJUST = 66 T_ABOVE = 67 T_BELOW = 68 T_ITALIC = 69 T_BOLD = 70 T_ALIGNED = 71 T_BIG = 72 T_SMALL = 73 T_AND = 74 T_LT = 75 T_GT = 76 T_ON = 77 T_WAY = 78 T_BETWEEN = 79 T_THE = 80 T_NTH = 81 T_VERTEX = 82 T_TOP = 83 T_BOTTOM = 84 T_START = 85 T_END = 86 T_IN = 87 T_THIS = 88 T_DOT_U = 89 T_LAST = 90 T_NUMBER = 91 T_FUNC1 = 92 T_FUNC2 = 93 T_DIST = 94 T_DOT_XY = 95 T_X = 96 T_Y = 97 T_DOT_L = 98 ) /**************** End token definitions ***************************************/ /* The next sections is a series of control #defines. ** various aspects of the generated parser. ** YYCODETYPE is the data type used to store the integer codes ** that represent terminal and non-terminal symbols. ** "unsigned char" is used if there are fewer than ** 256 symbols. Larger types otherwise. ** YYNOCODE is a number of type YYCODETYPE that is not used for ** any terminal or nonterminal symbol. ** YYFALLBACK If defined, this indicates that one or more tokens ** (also known as: "terminal symbols") have fall-back ** values which should be used if the original symbol ** would not parse. This permits keywords to sometimes ** be used as identifiers, for example. ** YYACTIONTYPE is the data type used for "action codes" - numbers ** that indicate what to do in response to the next ** token. ** pik_parserTOKENTYPE is the data type used for minor type for terminal ** symbols. Background: A "minor type" is a semantic ** value associated with a terminal or non-terminal ** symbols. For example, for an "ID" terminal symbol, ** the minor type might be the name of the identifier. ** Each non-terminal can have a different minor type. ** Terminal symbols all have the same minor type, though. ** This macros defines the minor type for terminal ** symbols. ** YYMINORTYPE is the data type used for all minor types. ** This is typically a union of many types, one of ** which is pik_parserTOKENTYPE. The entry in the union ** for terminal symbols is called "yy0". ** YYSTACKDEPTH is the maximum depth of the parser's stack. If ** zero the stack is dynamically sized using realloc() ** pik_parserARG_SDECL A static variable declaration for the %extra_argument ** pik_parserARG_PDECL A parameter declaration for the %extra_argument ** pik_parserARG_PARAM Code to pass %extra_argument as a subroutine parameter ** pik_parserARG_STORE Code to store %extra_argument into yypParser ** pik_parserARG_FETCH Code to extract %extra_argument from yypParser ** pik_parserCTX_* As pik_parserARG_ except for %extra_context ** YYERRORSYMBOL is the code number of the error symbol. If not ** defined, then do no error processing. ** YYNSTATE the combined number of states. ** YYNRULE the number of rules in the grammar ** YYNTOKEN Number of terminal symbols ** YY_MAX_SHIFT Maximum value for shift actions ** YY_MIN_SHIFTREDUCE Minimum value for shift-reduce actions ** YY_MAX_SHIFTREDUCE Maximum value for shift-reduce actions ** YY_ERROR_ACTION The yy_action[] code for syntax error ** YY_ACCEPT_ACTION The yy_action[] code for accept ** YY_NO_ACTION The yy_action[] code for no-op ** YY_MIN_REDUCE Minimum value for reduce actions ** YY_MAX_REDUCE Maximum value for reduce actions */ /************* Begin control #defines *****************************************/ const YYNOCODE = 135 type YYCODETYPE = uint8 type YYACTIONTYPE = uint16 type pik_parserTOKENTYPE = PToken type YYMINORTYPE struct { yyinit int yy0 pik_parserTOKENTYPE yy10 PRel yy79 PPoint yy104 *PObj yy112 int yy153 PNum yy186 []*PObj } const YYWILDCARD = 0 const YYSTACKDEPTH = 100 const YYNOERRORRECOVERY = false const YYCOVERAGE = false const YYTRACKMAXSTACKDEPTH = false const NDEBUG = false const YYERRORSYMBOL = 0 const YYFALLBACK = true const YYNSTATE = 164 const YYNRULE = 156 const YYNRULE_WITH_ACTION = 116 const YYNTOKEN = 99 const YY_MAX_SHIFT = 163 const YY_MIN_SHIFTREDUCE = 287 const YY_MAX_SHIFTREDUCE = 442 const YY_ERROR_ACTION = 443 const YY_ACCEPT_ACTION = 444 const YY_NO_ACTION = 445 const YY_MIN_REDUCE = 446 const YY_MAX_REDUCE = 601 /************* End control #defines *******************************************/ /* Applications can choose to define yytestcase() in the %include section ** to a macro that can assist in verifying code coverage. For production ** code the yytestcase() macro should be turned off. But it is useful ** for testing. */ /* Next are the tables used to determine what action to take based on the ** current state and lookahead token. These tables are used to implement ** functions that take a state number and lookahead value and return an ** action integer. ** ** Suppose the action integer is N. Then the action is determined as ** follows ** ** 0 <= N <= YY_MAX_SHIFT Shift N. That is, push the lookahead ** token onto the stack and goto state N. ** ** N between YY_MIN_SHIFTREDUCE Shift to an arbitrary state then ** and YY_MAX_SHIFTREDUCE reduce by rule N-YY_MIN_SHIFTREDUCE. ** ** N == YY_ERROR_ACTION A syntax error has occurred. ** ** N == YY_ACCEPT_ACTION The parser accepts its input. ** ** N == YY_NO_ACTION No such action. Denotes unused ** slots in the yy_action[] table. ** ** N between YY_MIN_REDUCE Reduce by rule N-YY_MIN_REDUCE ** and YY_MAX_REDUCE ** ** The action table is constructed as a single large table named yy_action[]. ** Given state S and lookahead X, the action is computed as either: ** ** (A) N = yy_action[ yy_shift_ofst[S] + X ] ** (B) N = yy_default[S] ** ** The (A) formula is preferred. The B formula is used instead if ** yy_lookahead[yy_shift_ofst[S]+X] is not equal to X. ** ** The formulas above are for computing the action when the lookahead is ** a terminal symbol. If the lookahead is a non-terminal (as occurs after ** a reduce action) then the yy_reduce_ofst[] array is used in place of ** the yy_shift_ofst[] array. ** ** The following are the tables generated in this section: ** ** yy_action[] A single table containing all actions. ** yy_lookahead[] A table containing the lookahead for each entry in ** yy_action. Used to detect hash collisions. ** yy_shift_ofst[] For each state, the offset into yy_action for ** shifting terminals. ** yy_reduce_ofst[] For each state, the offset into yy_action for ** shifting non-terminals after a reduce. ** yy_default[] Default action for each state. ** *********** Begin parsing tables **********************************************/ const YY_ACTTAB_COUNT = 1303 var yy_action = []YYACTIONTYPE{ /* 0 */ 575, 495, 161, 119, 25, 452, 29, 74, 129, 148, /* 10 */ 575, 492, 161, 119, 453, 113, 120, 161, 119, 530, /* 20 */ 427, 428, 339, 559, 81, 30, 560, 561, 575, 64, /* 30 */ 63, 62, 61, 322, 323, 9, 8, 33, 149, 32, /* 40 */ 7, 71, 127, 38, 335, 66, 48, 37, 28, 339, /* 50 */ 339, 339, 339, 425, 426, 340, 341, 342, 343, 344, /* 60 */ 345, 346, 347, 348, 474, 528, 161, 119, 577, 77, /* 70 */ 577, 73, 376, 148, 474, 533, 161, 119, 112, 113, /* 80 */ 120, 161, 119, 128, 427, 428, 339, 357, 81, 531, /* 90 */ 161, 119, 474, 36, 330, 13, 306, 322, 323, 9, /* 100 */ 8, 33, 149, 32, 7, 71, 127, 328, 335, 66, /* 110 */ 579, 310, 31, 339, 339, 339, 339, 425, 426, 340, /* 120 */ 341, 342, 343, 344, 345, 346, 347, 348, 394, 435, /* 130 */ 46, 59, 60, 64, 63, 62, 61, 54, 51, 376, /* 140 */ 69, 108, 2, 47, 403, 83, 297, 435, 375, 84, /* 150 */ 117, 80, 35, 308, 79, 133, 122, 126, 441, 440, /* 160 */ 299, 123, 3, 404, 405, 406, 408, 80, 298, 308, /* 170 */ 79, 4, 411, 412, 413, 414, 441, 440, 350, 350, /* 180 */ 350, 350, 350, 350, 350, 350, 350, 350, 62, 61, /* 190 */ 67, 434, 1, 75, 378, 158, 74, 76, 148, 411, /* 200 */ 412, 413, 414, 124, 113, 120, 161, 119, 106, 434, /* 210 */ 436, 437, 438, 439, 5, 375, 6, 117, 393, 155, /* 220 */ 154, 153, 394, 435, 69, 59, 60, 149, 436, 437, /* 230 */ 438, 439, 535, 376, 398, 399, 2, 424, 427, 428, /* 240 */ 339, 156, 156, 156, 423, 394, 435, 65, 59, 60, /* 250 */ 162, 131, 441, 440, 397, 72, 376, 148, 118, 2, /* 260 */ 380, 157, 125, 113, 120, 161, 119, 339, 339, 339, /* 270 */ 339, 425, 426, 535, 11, 441, 440, 394, 356, 535, /* 280 */ 59, 60, 535, 379, 159, 434, 149, 12, 102, 446, /* 290 */ 432, 42, 138, 14, 435, 139, 301, 302, 303, 36, /* 300 */ 305, 430, 106, 16, 436, 437, 438, 439, 434, 375, /* 310 */ 18, 117, 393, 155, 154, 153, 44, 142, 140, 64, /* 320 */ 63, 62, 61, 441, 440, 106, 19, 436, 437, 438, /* 330 */ 439, 45, 375, 20, 117, 393, 155, 154, 153, 68, /* 340 */ 55, 114, 64, 63, 62, 61, 147, 146, 394, 473, /* 350 */ 359, 59, 60, 43, 23, 391, 434, 106, 26, 376, /* 360 */ 57, 58, 42, 49, 375, 392, 117, 393, 155, 154, /* 370 */ 153, 64, 63, 62, 61, 436, 437, 438, 439, 384, /* 380 */ 382, 383, 22, 21, 377, 473, 160, 70, 39, 445, /* 390 */ 24, 445, 145, 141, 431, 142, 140, 64, 63, 62, /* 400 */ 61, 394, 15, 445, 59, 60, 64, 63, 62, 61, /* 410 */ 391, 445, 376, 445, 445, 42, 445, 445, 55, 391, /* 420 */ 156, 156, 156, 445, 147, 146, 445, 52, 106, 445, /* 430 */ 445, 43, 445, 445, 445, 375, 445, 117, 393, 155, /* 440 */ 154, 153, 445, 394, 143, 445, 59, 60, 64, 63, /* 450 */ 62, 61, 313, 445, 376, 378, 158, 42, 445, 445, /* 460 */ 22, 21, 121, 447, 454, 29, 445, 445, 24, 450, /* 470 */ 145, 141, 431, 142, 140, 64, 63, 62, 61, 445, /* 480 */ 163, 106, 445, 445, 444, 27, 445, 445, 375, 445, /* 490 */ 117, 393, 155, 154, 153, 445, 55, 74, 445, 148, /* 500 */ 445, 445, 147, 146, 497, 113, 120, 161, 119, 43, /* 510 */ 445, 394, 445, 445, 59, 60, 445, 445, 445, 118, /* 520 */ 445, 445, 376, 106, 445, 42, 445, 445, 149, 445, /* 530 */ 375, 445, 117, 393, 155, 154, 153, 445, 22, 21, /* 540 */ 394, 144, 445, 59, 60, 445, 24, 445, 145, 141, /* 550 */ 431, 376, 445, 445, 42, 445, 132, 130, 394, 445, /* 560 */ 445, 59, 60, 109, 447, 454, 29, 445, 445, 376, /* 570 */ 450, 445, 42, 445, 394, 445, 445, 59, 60, 445, /* 580 */ 445, 163, 445, 445, 445, 102, 27, 445, 42, 445, /* 590 */ 445, 106, 445, 64, 63, 62, 61, 445, 375, 445, /* 600 */ 117, 393, 155, 154, 153, 394, 355, 445, 59, 60, /* 610 */ 445, 445, 445, 445, 445, 74, 376, 148, 445, 40, /* 620 */ 106, 445, 496, 113, 120, 161, 119, 375, 445, 117, /* 630 */ 393, 155, 154, 153, 445, 448, 454, 29, 106, 445, /* 640 */ 445, 450, 445, 445, 445, 375, 149, 117, 393, 155, /* 650 */ 154, 153, 163, 445, 106, 445, 445, 27, 445, 445, /* 660 */ 445, 375, 445, 117, 393, 155, 154, 153, 394, 445, /* 670 */ 445, 59, 60, 64, 63, 62, 61, 445, 445, 376, /* 680 */ 445, 445, 41, 445, 445, 106, 354, 64, 63, 62, /* 690 */ 61, 445, 375, 445, 117, 393, 155, 154, 153, 445, /* 700 */ 445, 445, 74, 445, 148, 445, 88, 445, 445, 490, /* 710 */ 113, 120, 161, 119, 445, 120, 161, 119, 17, 74, /* 720 */ 445, 148, 110, 110, 445, 445, 484, 113, 120, 161, /* 730 */ 119, 445, 445, 149, 74, 445, 148, 152, 445, 445, /* 740 */ 445, 483, 113, 120, 161, 119, 445, 445, 106, 445, /* 750 */ 149, 445, 445, 107, 445, 375, 445, 117, 393, 155, /* 760 */ 154, 153, 120, 161, 119, 149, 478, 74, 445, 148, /* 770 */ 445, 88, 445, 445, 480, 113, 120, 161, 119, 445, /* 780 */ 120, 161, 119, 74, 152, 148, 10, 479, 479, 445, /* 790 */ 134, 113, 120, 161, 119, 445, 445, 445, 149, 74, /* 800 */ 445, 148, 152, 445, 445, 445, 517, 113, 120, 161, /* 810 */ 119, 445, 445, 74, 149, 148, 445, 445, 445, 445, /* 820 */ 137, 113, 120, 161, 119, 74, 445, 148, 445, 445, /* 830 */ 149, 445, 525, 113, 120, 161, 119, 445, 74, 445, /* 840 */ 148, 445, 445, 445, 149, 527, 113, 120, 161, 119, /* 850 */ 445, 445, 74, 445, 148, 445, 149, 445, 445, 524, /* 860 */ 113, 120, 161, 119, 74, 445, 148, 445, 445, 149, /* 870 */ 445, 526, 113, 120, 161, 119, 445, 445, 74, 445, /* 880 */ 148, 445, 88, 149, 445, 523, 113, 120, 161, 119, /* 890 */ 445, 120, 161, 119, 74, 149, 148, 85, 111, 111, /* 900 */ 445, 522, 113, 120, 161, 119, 120, 161, 119, 149, /* 910 */ 74, 445, 148, 152, 445, 445, 445, 521, 113, 120, /* 920 */ 161, 119, 445, 445, 74, 149, 148, 445, 152, 445, /* 930 */ 445, 520, 113, 120, 161, 119, 74, 445, 148, 445, /* 940 */ 445, 149, 445, 519, 113, 120, 161, 119, 445, 74, /* 950 */ 445, 148, 445, 445, 445, 149, 150, 113, 120, 161, /* 960 */ 119, 445, 445, 74, 445, 148, 445, 149, 445, 445, /* 970 */ 151, 113, 120, 161, 119, 74, 445, 148, 445, 445, /* 980 */ 149, 445, 136, 113, 120, 161, 119, 445, 445, 74, /* 990 */ 445, 148, 107, 445, 149, 445, 135, 113, 120, 161, /* 1000 */ 119, 120, 161, 119, 445, 463, 149, 445, 88, 445, /* 1010 */ 445, 445, 78, 78, 445, 445, 107, 120, 161, 119, /* 1020 */ 149, 445, 445, 152, 82, 120, 161, 119, 445, 463, /* 1030 */ 445, 466, 86, 34, 445, 88, 445, 569, 445, 152, /* 1040 */ 445, 120, 161, 119, 120, 161, 119, 152, 107, 445, /* 1050 */ 445, 475, 64, 63, 62, 61, 445, 120, 161, 119, /* 1060 */ 98, 451, 445, 152, 89, 396, 152, 90, 445, 120, /* 1070 */ 161, 119, 445, 120, 161, 119, 120, 161, 119, 152, /* 1080 */ 445, 64, 63, 62, 61, 445, 445, 445, 445, 445, /* 1090 */ 87, 152, 445, 99, 395, 152, 100, 445, 152, 120, /* 1100 */ 161, 119, 120, 161, 119, 120, 161, 119, 445, 101, /* 1110 */ 64, 63, 62, 61, 445, 445, 445, 445, 120, 161, /* 1120 */ 119, 152, 91, 391, 152, 445, 445, 152, 103, 445, /* 1130 */ 445, 120, 161, 119, 445, 92, 445, 120, 161, 119, /* 1140 */ 152, 93, 445, 445, 120, 161, 119, 104, 445, 445, /* 1150 */ 120, 161, 119, 152, 445, 445, 120, 161, 119, 152, /* 1160 */ 445, 445, 445, 445, 94, 445, 152, 445, 445, 445, /* 1170 */ 105, 445, 152, 120, 161, 119, 445, 95, 152, 120, /* 1180 */ 161, 119, 96, 445, 445, 445, 120, 161, 119, 445, /* 1190 */ 445, 120, 161, 119, 97, 152, 445, 445, 445, 445, /* 1200 */ 549, 152, 445, 120, 161, 119, 548, 445, 152, 120, /* 1210 */ 161, 119, 445, 152, 445, 120, 161, 119, 445, 445, /* 1220 */ 445, 445, 445, 547, 445, 152, 445, 445, 445, 445, /* 1230 */ 445, 152, 120, 161, 119, 546, 445, 152, 445, 115, /* 1240 */ 445, 445, 116, 445, 120, 161, 119, 445, 120, 161, /* 1250 */ 119, 120, 161, 119, 152, 64, 63, 62, 61, 64, /* 1260 */ 63, 62, 61, 445, 445, 445, 152, 445, 445, 445, /* 1270 */ 152, 445, 445, 152, 445, 445, 50, 445, 445, 445, /* 1280 */ 53, 64, 63, 62, 61, 445, 445, 445, 445, 445, /* 1290 */ 445, 445, 445, 445, 445, 445, 445, 445, 445, 445, /* 1300 */ 445, 445, 56, } var yy_lookahead = []YYCODETYPE{ /* 0 */ 0, 112, 113, 114, 133, 101, 102, 103, 105, 105, /* 10 */ 10, 112, 113, 114, 110, 111, 112, 113, 114, 105, /* 20 */ 20, 21, 22, 104, 24, 125, 107, 108, 28, 4, /* 30 */ 5, 6, 7, 33, 34, 35, 36, 37, 134, 39, /* 40 */ 40, 41, 42, 104, 44, 45, 107, 108, 106, 49, /* 50 */ 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, /* 60 */ 60, 61, 62, 63, 0, 112, 113, 114, 129, 130, /* 70 */ 131, 103, 12, 105, 10, 112, 113, 114, 110, 111, /* 80 */ 112, 113, 114, 105, 20, 21, 22, 17, 24, 112, /* 90 */ 113, 114, 28, 10, 2, 25, 25, 33, 34, 35, /* 100 */ 36, 37, 134, 39, 40, 41, 42, 2, 44, 45, /* 110 */ 132, 28, 127, 49, 50, 51, 52, 53, 54, 55, /* 120 */ 56, 57, 58, 59, 60, 61, 62, 63, 1, 2, /* 130 */ 38, 4, 5, 4, 5, 6, 7, 4, 5, 12, /* 140 */ 3, 81, 15, 38, 1, 115, 17, 2, 88, 115, /* 150 */ 90, 24, 128, 26, 27, 12, 1, 14, 31, 32, /* 160 */ 19, 18, 16, 20, 21, 22, 23, 24, 17, 26, /* 170 */ 27, 15, 29, 30, 31, 32, 31, 32, 64, 65, /* 180 */ 66, 67, 68, 69, 70, 71, 72, 73, 6, 7, /* 190 */ 43, 64, 13, 48, 26, 27, 103, 48, 105, 29, /* 200 */ 30, 31, 32, 110, 111, 112, 113, 114, 81, 64, /* 210 */ 83, 84, 85, 86, 40, 88, 40, 90, 91, 92, /* 220 */ 93, 94, 1, 2, 87, 4, 5, 134, 83, 84, /* 230 */ 85, 86, 48, 12, 96, 97, 15, 41, 20, 21, /* 240 */ 22, 20, 21, 22, 41, 1, 2, 98, 4, 5, /* 250 */ 82, 47, 31, 32, 17, 103, 12, 105, 90, 15, /* 260 */ 26, 27, 110, 111, 112, 113, 114, 49, 50, 51, /* 270 */ 52, 53, 54, 89, 25, 31, 32, 1, 17, 95, /* 280 */ 4, 5, 98, 26, 27, 64, 134, 74, 12, 0, /* 290 */ 79, 15, 78, 3, 2, 80, 20, 21, 22, 10, /* 300 */ 24, 79, 81, 3, 83, 84, 85, 86, 64, 88, /* 310 */ 3, 90, 91, 92, 93, 94, 38, 2, 3, 4, /* 320 */ 5, 6, 7, 31, 32, 81, 3, 83, 84, 85, /* 330 */ 86, 16, 88, 3, 90, 91, 92, 93, 94, 3, /* 340 */ 25, 95, 4, 5, 6, 7, 31, 32, 1, 2, /* 350 */ 76, 4, 5, 38, 25, 17, 64, 81, 15, 12, /* 360 */ 15, 15, 15, 25, 88, 17, 90, 91, 92, 93, /* 370 */ 94, 4, 5, 6, 7, 83, 84, 85, 86, 28, /* 380 */ 28, 28, 67, 68, 12, 38, 89, 3, 11, 135, /* 390 */ 75, 135, 77, 78, 79, 2, 3, 4, 5, 6, /* 400 */ 7, 1, 35, 135, 4, 5, 4, 5, 6, 7, /* 410 */ 17, 135, 12, 135, 135, 15, 135, 135, 25, 17, /* 420 */ 20, 21, 22, 135, 31, 32, 135, 25, 81, 135, /* 430 */ 135, 38, 135, 135, 135, 88, 135, 90, 91, 92, /* 440 */ 93, 94, 135, 1, 2, 135, 4, 5, 4, 5, /* 450 */ 6, 7, 8, 135, 12, 26, 27, 15, 135, 135, /* 460 */ 67, 68, 99, 100, 101, 102, 135, 135, 75, 106, /* 470 */ 77, 78, 79, 2, 3, 4, 5, 6, 7, 135, /* 480 */ 117, 81, 135, 135, 121, 122, 135, 135, 88, 135, /* 490 */ 90, 91, 92, 93, 94, 135, 25, 103, 135, 105, /* 500 */ 135, 135, 31, 32, 110, 111, 112, 113, 114, 38, /* 510 */ 135, 1, 135, 135, 4, 5, 135, 135, 135, 90, /* 520 */ 135, 135, 12, 81, 135, 15, 135, 135, 134, 135, /* 530 */ 88, 135, 90, 91, 92, 93, 94, 135, 67, 68, /* 540 */ 1, 2, 135, 4, 5, 135, 75, 135, 77, 78, /* 550 */ 79, 12, 135, 135, 15, 135, 46, 47, 1, 135, /* 560 */ 135, 4, 5, 99, 100, 101, 102, 135, 135, 12, /* 570 */ 106, 135, 15, 135, 1, 135, 135, 4, 5, 135, /* 580 */ 135, 117, 135, 135, 135, 12, 122, 135, 15, 135, /* 590 */ 135, 81, 135, 4, 5, 6, 7, 135, 88, 135, /* 600 */ 90, 91, 92, 93, 94, 1, 17, 135, 4, 5, /* 610 */ 135, 135, 135, 135, 135, 103, 12, 105, 135, 15, /* 620 */ 81, 135, 110, 111, 112, 113, 114, 88, 135, 90, /* 630 */ 91, 92, 93, 94, 135, 100, 101, 102, 81, 135, /* 640 */ 135, 106, 135, 135, 135, 88, 134, 90, 91, 92, /* 650 */ 93, 94, 117, 135, 81, 135, 135, 122, 135, 135, /* 660 */ 135, 88, 135, 90, 91, 92, 93, 94, 1, 135, /* 670 */ 135, 4, 5, 4, 5, 6, 7, 135, 135, 12, /* 680 */ 135, 135, 15, 135, 135, 81, 17, 4, 5, 6, /* 690 */ 7, 135, 88, 135, 90, 91, 92, 93, 94, 135, /* 700 */ 135, 135, 103, 135, 105, 135, 103, 135, 135, 110, /* 710 */ 111, 112, 113, 114, 135, 112, 113, 114, 35, 103, /* 720 */ 135, 105, 119, 120, 135, 135, 110, 111, 112, 113, /* 730 */ 114, 135, 135, 134, 103, 135, 105, 134, 135, 135, /* 740 */ 135, 110, 111, 112, 113, 114, 135, 135, 81, 135, /* 750 */ 134, 135, 135, 103, 135, 88, 135, 90, 91, 92, /* 760 */ 93, 94, 112, 113, 114, 134, 116, 103, 135, 105, /* 770 */ 135, 103, 135, 135, 110, 111, 112, 113, 114, 135, /* 780 */ 112, 113, 114, 103, 134, 105, 118, 119, 120, 135, /* 790 */ 110, 111, 112, 113, 114, 135, 135, 135, 134, 103, /* 800 */ 135, 105, 134, 135, 135, 135, 110, 111, 112, 113, /* 810 */ 114, 135, 135, 103, 134, 105, 135, 135, 135, 135, /* 820 */ 110, 111, 112, 113, 114, 103, 135, 105, 135, 135, /* 830 */ 134, 135, 110, 111, 112, 113, 114, 135, 103, 135, /* 840 */ 105, 135, 135, 135, 134, 110, 111, 112, 113, 114, /* 850 */ 135, 135, 103, 135, 105, 135, 134, 135, 135, 110, /* 860 */ 111, 112, 113, 114, 103, 135, 105, 135, 135, 134, /* 870 */ 135, 110, 111, 112, 113, 114, 135, 135, 103, 135, /* 880 */ 105, 135, 103, 134, 135, 110, 111, 112, 113, 114, /* 890 */ 135, 112, 113, 114, 103, 134, 105, 103, 119, 120, /* 900 */ 135, 110, 111, 112, 113, 114, 112, 113, 114, 134, /* 910 */ 103, 135, 105, 134, 135, 135, 135, 110, 111, 112, /* 920 */ 113, 114, 135, 135, 103, 134, 105, 135, 134, 135, /* 930 */ 135, 110, 111, 112, 113, 114, 103, 135, 105, 135, /* 940 */ 135, 134, 135, 110, 111, 112, 113, 114, 135, 103, /* 950 */ 135, 105, 135, 135, 135, 134, 110, 111, 112, 113, /* 960 */ 114, 135, 135, 103, 135, 105, 135, 134, 135, 135, /* 970 */ 110, 111, 112, 113, 114, 103, 135, 105, 135, 135, /* 980 */ 134, 135, 110, 111, 112, 113, 114, 135, 135, 103, /* 990 */ 135, 105, 103, 135, 134, 135, 110, 111, 112, 113, /* 1000 */ 114, 112, 113, 114, 135, 116, 134, 135, 103, 135, /* 1010 */ 135, 135, 123, 124, 135, 135, 103, 112, 113, 114, /* 1020 */ 134, 135, 135, 134, 119, 112, 113, 114, 135, 116, /* 1030 */ 135, 126, 103, 128, 135, 103, 135, 124, 135, 134, /* 1040 */ 135, 112, 113, 114, 112, 113, 114, 134, 103, 135, /* 1050 */ 135, 119, 4, 5, 6, 7, 135, 112, 113, 114, /* 1060 */ 103, 116, 135, 134, 103, 17, 134, 103, 135, 112, /* 1070 */ 113, 114, 135, 112, 113, 114, 112, 113, 114, 134, /* 1080 */ 135, 4, 5, 6, 7, 135, 135, 135, 135, 135, /* 1090 */ 103, 134, 135, 103, 17, 134, 103, 135, 134, 112, /* 1100 */ 113, 114, 112, 113, 114, 112, 113, 114, 135, 103, /* 1110 */ 4, 5, 6, 7, 135, 135, 135, 135, 112, 113, /* 1120 */ 114, 134, 103, 17, 134, 135, 135, 134, 103, 135, /* 1130 */ 135, 112, 113, 114, 135, 103, 135, 112, 113, 114, /* 1140 */ 134, 103, 135, 135, 112, 113, 114, 103, 135, 135, /* 1150 */ 112, 113, 114, 134, 135, 135, 112, 113, 114, 134, /* 1160 */ 135, 135, 135, 135, 103, 135, 134, 135, 135, 135, /* 1170 */ 103, 135, 134, 112, 113, 114, 135, 103, 134, 112, /* 1180 */ 113, 114, 103, 135, 135, 135, 112, 113, 114, 135, /* 1190 */ 135, 112, 113, 114, 103, 134, 135, 135, 135, 135, /* 1200 */ 103, 134, 135, 112, 113, 114, 103, 135, 134, 112, /* 1210 */ 113, 114, 135, 134, 135, 112, 113, 114, 135, 135, /* 1220 */ 135, 135, 135, 103, 135, 134, 135, 135, 135, 135, /* 1230 */ 135, 134, 112, 113, 114, 103, 135, 134, 135, 103, /* 1240 */ 135, 135, 103, 135, 112, 113, 114, 135, 112, 113, /* 1250 */ 114, 112, 113, 114, 134, 4, 5, 6, 7, 4, /* 1260 */ 5, 6, 7, 135, 135, 135, 134, 135, 135, 135, /* 1270 */ 134, 135, 135, 134, 135, 135, 25, 135, 135, 135, /* 1280 */ 25, 4, 5, 6, 7, 135, 135, 135, 135, 135, /* 1290 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, /* 1300 */ 135, 135, 25, 135, 135, 135, 135, 135, 135, 135, /* 1310 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, /* 1320 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, /* 1330 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, /* 1340 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, /* 1350 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, /* 1360 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, /* 1370 */ 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, /* 1380 */ 135, 99, 99, 99, 99, 99, 99, 99, 99, 99, /* 1390 */ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, /* 1400 */ 99, 99, } const YY_SHIFT_COUNT = 163 const YY_SHIFT_MIN = 0 const YY_SHIFT_MAX = 1277 var yy_shift_ofst = []uint16{ /* 0 */ 143, 127, 221, 244, 244, 244, 244, 244, 244, 244, /* 10 */ 244, 244, 244, 244, 244, 244, 244, 244, 244, 244, /* 20 */ 244, 244, 244, 244, 244, 244, 244, 276, 510, 557, /* 30 */ 276, 143, 347, 347, 0, 64, 143, 573, 557, 573, /* 40 */ 400, 400, 400, 442, 539, 557, 557, 557, 557, 557, /* 50 */ 557, 604, 557, 557, 667, 557, 557, 557, 557, 557, /* 60 */ 557, 557, 557, 557, 557, 218, 60, 60, 60, 60, /* 70 */ 60, 145, 315, 393, 471, 292, 292, 170, 71, 1303, /* 80 */ 1303, 1303, 1303, 114, 114, 338, 402, 129, 444, 367, /* 90 */ 683, 589, 1251, 669, 1255, 1048, 1277, 1077, 1106, 25, /* 100 */ 25, 25, 184, 25, 25, 25, 168, 25, 429, 83, /* 110 */ 92, 105, 70, 133, 138, 182, 182, 234, 257, 137, /* 120 */ 149, 289, 141, 155, 151, 146, 156, 147, 174, 176, /* 130 */ 196, 203, 204, 179, 237, 249, 213, 261, 211, 214, /* 140 */ 215, 222, 290, 300, 307, 278, 323, 330, 336, 246, /* 150 */ 274, 329, 246, 343, 345, 346, 348, 351, 352, 353, /* 160 */ 372, 297, 384, 377, } const YY_REDUCE_COUNT = 82 const YY_REDUCE_MIN = -129 const YY_REDUCE_MAX = 1139 var yy_reduce_ofst = []int16{ /* 0 */ 363, -96, -32, 93, 152, 394, 512, 599, 616, 631, /* 10 */ 664, 680, 696, 710, 722, 735, 749, 761, 775, 791, /* 20 */ 807, 821, 833, 846, 860, 872, 886, 889, 668, 905, /* 30 */ 913, 464, 603, 779, -61, -61, 535, 650, 932, 945, /* 40 */ 794, 929, 957, 961, 964, 987, 990, 993, 1006, 1019, /* 50 */ 1025, 1032, 1038, 1044, 1061, 1067, 1074, 1079, 1091, 1097, /* 60 */ 1103, 1120, 1132, 1136, 1139, -81, -111, -101, -47, -37, /* 70 */ -23, -22, -129, -129, -129, -97, -86, -58, -100, -15, /* 80 */ 30, 34, 24, } var yy_default = []YYACTIONTYPE{ /* 0 */ 449, 443, 443, 443, 443, 443, 443, 443, 443, 443, /* 10 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, /* 20 */ 443, 443, 443, 443, 443, 443, 443, 443, 473, 576, /* 30 */ 443, 449, 580, 485, 581, 581, 449, 443, 443, 443, /* 40 */ 443, 443, 443, 443, 443, 443, 443, 443, 477, 443, /* 50 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, /* 60 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, /* 70 */ 443, 443, 443, 443, 443, 443, 443, 443, 455, 470, /* 80 */ 508, 508, 576, 468, 493, 443, 443, 443, 471, 443, /* 90 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 488, /* 100 */ 486, 476, 459, 512, 511, 510, 443, 566, 443, 443, /* 110 */ 443, 443, 443, 588, 443, 545, 544, 540, 443, 532, /* 120 */ 529, 443, 443, 443, 443, 443, 443, 491, 443, 443, /* 130 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, /* 140 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 592, /* 150 */ 443, 443, 443, 443, 443, 443, 443, 443, 443, 443, /* 160 */ 443, 601, 443, 443, } /********** End of lemon-generated parsing tables *****************************/ /* The next table maps tokens (terminal symbols) into fallback tokens. ** If a construct like the following: ** ** %fallback ID X Y Z. ** ** appears in the grammar, then ID becomes a fallback token for X, Y, ** and Z. Whenever one of the tokens X, Y, or Z is input to the parser ** but it does not parse, the type of the token is changed to ID and ** the parse is retried before an error is thrown. ** ** This feature can be used, for example, to cause some keywords in a language ** to revert to identifiers if they keyword does not apply in the context where ** it appears. */ var yyFallback = []YYCODETYPE{ // 0, /* $ => nothing */ 0, /* ID => nothing */ 1, /* EDGEPT => ID */ 0, /* OF => nothing */ 0, /* PLUS => nothing */ 0, /* MINUS => nothing */ 0, /* STAR => nothing */ 0, /* SLASH => nothing */ 0, /* PERCENT => nothing */ 0, /* UMINUS => nothing */ 0, /* EOL => nothing */ 0, /* ASSIGN => nothing */ 0, /* PLACENAME => nothing */ 0, /* COLON => nothing */ 0, /* ASSERT => nothing */ 0, /* LP => nothing */ 0, /* EQ => nothing */ 0, /* RP => nothing */ 0, /* DEFINE => nothing */ 0, /* CODEBLOCK => nothing */ 0, /* FILL => nothing */ 0, /* COLOR => nothing */ 0, /* THICKNESS => nothing */ 0, /* PRINT => nothing */ 0, /* STRING => nothing */ 0, /* COMMA => nothing */ 0, /* CLASSNAME => nothing */ 0, /* LB => nothing */ 0, /* RB => nothing */ 0, /* UP => nothing */ 0, /* DOWN => nothing */ 0, /* LEFT => nothing */ 0, /* RIGHT => nothing */ 0, /* CLOSE => nothing */ 0, /* CHOP => nothing */ 0, /* FROM => nothing */ 0, /* TO => nothing */ 0, /* THEN => nothing */ 0, /* HEADING => nothing */ 0, /* GO => nothing */ 0, /* AT => nothing */ 0, /* WITH => nothing */ 0, /* SAME => nothing */ 0, /* AS => nothing */ 0, /* FIT => nothing */ 0, /* BEHIND => nothing */ 0, /* UNTIL => nothing */ 0, /* EVEN => nothing */ 0, /* DOT_E => nothing */ 0, /* HEIGHT => nothing */ 0, /* WIDTH => nothing */ 0, /* RADIUS => nothing */ 0, /* DIAMETER => nothing */ 0, /* DOTTED => nothing */ 0, /* DASHED => nothing */ 0, /* CW => nothing */ 0, /* CCW => nothing */ 0, /* LARROW => nothing */ 0, /* RARROW => nothing */ 0, /* LRARROW => nothing */ 0, /* INVIS => nothing */ 0, /* THICK => nothing */ 0, /* THIN => nothing */ 0, /* SOLID => nothing */ 0, /* CENTER => nothing */ 0, /* LJUST => nothing */ 0, /* RJUST => nothing */ 0, /* ABOVE => nothing */ 0, /* BELOW => nothing */ 0, /* ITALIC => nothing */ 0, /* BOLD => nothing */ 0, /* ALIGNED => nothing */ 0, /* BIG => nothing */ 0, /* SMALL => nothing */ 0, /* AND => nothing */ 0, /* LT => nothing */ 0, /* GT => nothing */ 0, /* ON => nothing */ 0, /* WAY => nothing */ 0, /* BETWEEN => nothing */ 0, /* THE => nothing */ 0, /* NTH => nothing */ 0, /* VERTEX => nothing */ 0, /* TOP => nothing */ 0, /* BOTTOM => nothing */ 0, /* START => nothing */ 0, /* END => nothing */ 0, /* IN => nothing */ 0, /* THIS => nothing */ 0, /* DOT_U => nothing */ 0, /* LAST => nothing */ 0, /* NUMBER => nothing */ 0, /* FUNC1 => nothing */ 0, /* FUNC2 => nothing */ 0, /* DIST => nothing */ 0, /* DOT_XY => nothing */ 0, /* X => nothing */ 0, /* Y => nothing */ 0, /* DOT_L => nothing */ } /* The following structure represents a single element of the ** parser's stack. Information stored includes: ** ** + The state number for the parser at this level of the stack. ** ** + The value of the token stored at this level of the stack. ** (In other words, the "major" token.) ** ** + The semantic value stored at this level of the stack. This is ** the information used by the action routines in the grammar. ** It is sometimes called the "minor" token. ** ** After the "shift" half of a SHIFTREDUCE action, the stateno field ** actually contains the reduce action for the second half of the ** SHIFTREDUCE. */ type yyStackEntry struct { stateno YYACTIONTYPE /* The state-number, or reduce action in SHIFTREDUCE */ major YYCODETYPE /* The major token value. This is the code ** number for the token at this stack level */ minor YYMINORTYPE /* The user-supplied minor token value. This ** is the value of the token */ } /* The state of the parser is completely contained in an instance of ** the following structure */ type yyParser struct { yytos int /* Index of top element on the stack */ // #ifdef YYTRACKMAXSTACKDEPTH yyhwm int /* High-water mark of the stack */ // #endif // #ifndef YYNOERRORRECOVERY yyerrcnt int /* Shifts left before out of the error */ // #endif /* A place to hold %extra_argument */ p *Pik /* A place to hold %extra_context */ yystack []yyStackEntry } var yyTraceFILE *os.File var yyTracePrompt string /* ** Turn parser tracing on by giving a stream to which to write the trace ** and a prompt to preface each trace message. Tracing is turned off ** by making either argument NULL ** ** Inputs: ** <ul> ** <li> A FILE* to which trace output should be written. ** If NULL, then tracing is turned off. ** <li> A prefix string written at the beginning of every ** line of trace output. If NULL, then tracing is ** turned off. ** </ul> ** ** Outputs: ** None. */ func pik_parserTrace(TraceFILE *os.File, zTracePrompt string) { yyTraceFILE = TraceFILE yyTracePrompt = zTracePrompt if yyTraceFILE == nil { yyTracePrompt = "" } else if yyTracePrompt == "" { yyTraceFILE = nil } } /* For tracing shifts, the names of all terminals and nonterminals ** are required. The following table supplies these names */ var yyTokenName = []string{ /* 0 */ "$", /* 1 */ "ID", /* 2 */ "EDGEPT", /* 3 */ "OF", /* 4 */ "PLUS", /* 5 */ "MINUS", /* 6 */ "STAR", /* 7 */ "SLASH", /* 8 */ "PERCENT", /* 9 */ "UMINUS", /* 10 */ "EOL", /* 11 */ "ASSIGN", /* 12 */ "PLACENAME", /* 13 */ "COLON", /* 14 */ "ASSERT", /* 15 */ "LP", /* 16 */ "EQ", /* 17 */ "RP", /* 18 */ "DEFINE", /* 19 */ "CODEBLOCK", /* 20 */ "FILL", /* 21 */ "COLOR", /* 22 */ "THICKNESS", /* 23 */ "PRINT", /* 24 */ "STRING", /* 25 */ "COMMA", /* 26 */ "CLASSNAME", /* 27 */ "LB", /* 28 */ "RB", /* 29 */ "UP", /* 30 */ "DOWN", /* 31 */ "LEFT", /* 32 */ "RIGHT", /* 33 */ "CLOSE", /* 34 */ "CHOP", /* 35 */ "FROM", /* 36 */ "TO", /* 37 */ "THEN", /* 38 */ "HEADING", /* 39 */ "GO", /* 40 */ "AT", /* 41 */ "WITH", /* 42 */ "SAME", /* 43 */ "AS", /* 44 */ "FIT", /* 45 */ "BEHIND", /* 46 */ "UNTIL", /* 47 */ "EVEN", /* 48 */ "DOT_E", /* 49 */ "HEIGHT", /* 50 */ "WIDTH", /* 51 */ "RADIUS", /* 52 */ "DIAMETER", /* 53 */ "DOTTED", /* 54 */ "DASHED", /* 55 */ "CW", /* 56 */ "CCW", /* 57 */ "LARROW", /* 58 */ "RARROW", /* 59 */ "LRARROW", /* 60 */ "INVIS", /* 61 */ "THICK", /* 62 */ "THIN", /* 63 */ "SOLID", /* 64 */ "CENTER", /* 65 */ "LJUST", /* 66 */ "RJUST", /* 67 */ "ABOVE", /* 68 */ "BELOW", /* 69 */ "ITALIC", /* 70 */ "BOLD", /* 71 */ "ALIGNED", /* 72 */ "BIG", /* 73 */ "SMALL", /* 74 */ "AND", /* 75 */ "LT", /* 76 */ "GT", /* 77 */ "ON", /* 78 */ "WAY", /* 79 */ "BETWEEN", /* 80 */ "THE", /* 81 */ "NTH", /* 82 */ "VERTEX", /* 83 */ "TOP", /* 84 */ "BOTTOM", /* 85 */ "START", /* 86 */ "END", /* 87 */ "IN", /* 88 */ "THIS", /* 89 */ "DOT_U", /* 90 */ "LAST", /* 91 */ "NUMBER", /* 92 */ "FUNC1", /* 93 */ "FUNC2", /* 94 */ "DIST", /* 95 */ "DOT_XY", /* 96 */ "X", /* 97 */ "Y", /* 98 */ "DOT_L", /* 99 */ "statement_list", /* 100 */ "statement", /* 101 */ "unnamed_statement", /* 102 */ "basetype", /* 103 */ "expr", /* 104 */ "numproperty", /* 105 */ "edge", /* 106 */ "direction", /* 107 */ "dashproperty", /* 108 */ "colorproperty", /* 109 */ "locproperty", /* 110 */ "position", /* 111 */ "place", /* 112 */ "object", /* 113 */ "objectname", /* 114 */ "nth", /* 115 */ "textposition", /* 116 */ "rvalue", /* 117 */ "lvalue", /* 118 */ "even", /* 119 */ "relexpr", /* 120 */ "optrelexpr", /* 121 */ "document", /* 122 */ "print", /* 123 */ "prlist", /* 124 */ "pritem", /* 125 */ "prsep", /* 126 */ "attribute_list", /* 127 */ "savelist", /* 128 */ "alist", /* 129 */ "attribute", /* 130 */ "go", /* 131 */ "boolproperty", /* 132 */ "withclause", /* 133 */ "between", /* 134 */ "place2", } /* For tracing reduce actions, the names of all rules are required. */ var yyRuleName = []string{ /* 0 */ "document ::= statement_list", /* 1 */ "statement_list ::= statement", /* 2 */ "statement_list ::= statement_list EOL statement", /* 3 */ "statement ::=", /* 4 */ "statement ::= direction", /* 5 */ "statement ::= lvalue ASSIGN rvalue", /* 6 */ "statement ::= PLACENAME COLON unnamed_statement", /* 7 */ "statement ::= PLACENAME COLON position", /* 8 */ "statement ::= unnamed_statement", /* 9 */ "statement ::= print prlist", /* 10 */ "statement ::= ASSERT LP expr EQ expr RP", /* 11 */ "statement ::= ASSERT LP position EQ position RP", /* 12 */ "statement ::= DEFINE ID CODEBLOCK", /* 13 */ "rvalue ::= PLACENAME", /* 14 */ "pritem ::= FILL", /* 15 */ "pritem ::= COLOR", /* 16 */ "pritem ::= THICKNESS", /* 17 */ "pritem ::= rvalue", /* 18 */ "pritem ::= STRING", /* 19 */ "prsep ::= COMMA", /* 20 */ "unnamed_statement ::= basetype attribute_list", /* 21 */ "basetype ::= CLASSNAME", /* 22 */ "basetype ::= STRING textposition", /* 23 */ "basetype ::= LB savelist statement_list RB", /* 24 */ "savelist ::=", /* 25 */ "relexpr ::= expr", /* 26 */ "relexpr ::= expr PERCENT", /* 27 */ "optrelexpr ::=", /* 28 */ "attribute_list ::= relexpr alist", /* 29 */ "attribute ::= numproperty relexpr", /* 30 */ "attribute ::= dashproperty expr", /* 31 */ "attribute ::= dashproperty", /* 32 */ "attribute ::= colorproperty rvalue", /* 33 */ "attribute ::= go direction optrelexpr", /* 34 */ "attribute ::= go direction even position", /* 35 */ "attribute ::= CLOSE", /* 36 */ "attribute ::= CHOP", /* 37 */ "attribute ::= FROM position", /* 38 */ "attribute ::= TO position", /* 39 */ "attribute ::= THEN", /* 40 */ "attribute ::= THEN optrelexpr HEADING expr", /* 41 */ "attribute ::= THEN optrelexpr EDGEPT", /* 42 */ "attribute ::= GO optrelexpr HEADING expr", /* 43 */ "attribute ::= GO optrelexpr EDGEPT", /* 44 */ "attribute ::= AT position", /* 45 */ "attribute ::= SAME", /* 46 */ "attribute ::= SAME AS object", /* 47 */ "attribute ::= STRING textposition", /* 48 */ "attribute ::= FIT", /* 49 */ "attribute ::= BEHIND object", /* 50 */ "withclause ::= DOT_E edge AT position", /* 51 */ "withclause ::= edge AT position", /* 52 */ "numproperty ::= HEIGHT|WIDTH|RADIUS|DIAMETER|THICKNESS", /* 53 */ "boolproperty ::= CW", /* 54 */ "boolproperty ::= CCW", /* 55 */ "boolproperty ::= LARROW", /* 56 */ "boolproperty ::= RARROW", /* 57 */ "boolproperty ::= LRARROW", /* 58 */ "boolproperty ::= INVIS", /* 59 */ "boolproperty ::= THICK", /* 60 */ "boolproperty ::= THIN", /* 61 */ "boolproperty ::= SOLID", /* 62 */ "textposition ::=", /* 63 */ "textposition ::= textposition CENTER|LJUST|RJUST|ABOVE|BELOW|ITALIC|BOLD|ALIGNED|BIG|SMALL", /* 64 */ "position ::= expr COMMA expr", /* 65 */ "position ::= place PLUS expr COMMA expr", /* 66 */ "position ::= place MINUS expr COMMA expr", /* 67 */ "position ::= place PLUS LP expr COMMA expr RP", /* 68 */ "position ::= place MINUS LP expr COMMA expr RP", /* 69 */ "position ::= LP position COMMA position RP", /* 70 */ "position ::= LP position RP", /* 71 */ "position ::= expr between position AND position", /* 72 */ "position ::= expr LT position COMMA position GT", /* 73 */ "position ::= expr ABOVE position", /* 74 */ "position ::= expr BELOW position", /* 75 */ "position ::= expr LEFT OF position", /* 76 */ "position ::= expr RIGHT OF position", /* 77 */ "position ::= expr ON HEADING EDGEPT OF position", /* 78 */ "position ::= expr HEADING EDGEPT OF position", /* 79 */ "position ::= expr EDGEPT OF position", /* 80 */ "position ::= expr ON HEADING expr FROM position", /* 81 */ "position ::= expr HEADING expr FROM position", /* 82 */ "place ::= edge OF object", /* 83 */ "place2 ::= object", /* 84 */ "place2 ::= object DOT_E edge", /* 85 */ "place2 ::= NTH VERTEX OF object", /* 86 */ "object ::= nth", /* 87 */ "object ::= nth OF|IN object", /* 88 */ "objectname ::= THIS", /* 89 */ "objectname ::= PLACENAME", /* 90 */ "objectname ::= objectname DOT_U PLACENAME", /* 91 */ "nth ::= NTH CLASSNAME", /* 92 */ "nth ::= NTH LAST CLASSNAME", /* 93 */ "nth ::= LAST CLASSNAME", /* 94 */ "nth ::= LAST", /* 95 */ "nth ::= NTH LB RB", /* 96 */ "nth ::= NTH LAST LB RB", /* 97 */ "nth ::= LAST LB RB", /* 98 */ "expr ::= expr PLUS expr", /* 99 */ "expr ::= expr MINUS expr", /* 100 */ "expr ::= expr STAR expr", /* 101 */ "expr ::= expr SLASH expr", /* 102 */ "expr ::= MINUS expr", /* 103 */ "expr ::= PLUS expr", /* 104 */ "expr ::= LP expr RP", /* 105 */ "expr ::= LP FILL|COLOR|THICKNESS RP", /* 106 */ "expr ::= NUMBER", /* 107 */ "expr ::= ID", /* 108 */ "expr ::= FUNC1 LP expr RP", /* 109 */ "expr ::= FUNC2 LP expr COMMA expr RP", /* 110 */ "expr ::= DIST LP position COMMA position RP", /* 111 */ "expr ::= place2 DOT_XY X", /* 112 */ "expr ::= place2 DOT_XY Y", /* 113 */ "expr ::= object DOT_L numproperty", /* 114 */ "expr ::= object DOT_L dashproperty", /* 115 */ "expr ::= object DOT_L colorproperty", /* 116 */ "lvalue ::= ID", /* 117 */ "lvalue ::= FILL", /* 118 */ "lvalue ::= COLOR", /* 119 */ "lvalue ::= THICKNESS", /* 120 */ "rvalue ::= expr", /* 121 */ "print ::= PRINT", /* 122 */ "prlist ::= pritem", /* 123 */ "prlist ::= prlist prsep pritem", /* 124 */ "direction ::= UP", /* 125 */ "direction ::= DOWN", /* 126 */ "direction ::= LEFT", /* 127 */ "direction ::= RIGHT", /* 128 */ "optrelexpr ::= relexpr", /* 129 */ "attribute_list ::= alist", /* 130 */ "alist ::=", /* 131 */ "alist ::= alist attribute", /* 132 */ "attribute ::= boolproperty", /* 133 */ "attribute ::= WITH withclause", /* 134 */ "go ::= GO", /* 135 */ "go ::=", /* 136 */ "even ::= UNTIL EVEN WITH", /* 137 */ "even ::= EVEN WITH", /* 138 */ "dashproperty ::= DOTTED", /* 139 */ "dashproperty ::= DASHED", /* 140 */ "colorproperty ::= FILL", /* 141 */ "colorproperty ::= COLOR", /* 142 */ "position ::= place", /* 143 */ "between ::= WAY BETWEEN", /* 144 */ "between ::= BETWEEN", /* 145 */ "between ::= OF THE WAY BETWEEN", /* 146 */ "place ::= place2", /* 147 */ "edge ::= CENTER", /* 148 */ "edge ::= EDGEPT", /* 149 */ "edge ::= TOP", /* 150 */ "edge ::= BOTTOM", /* 151 */ "edge ::= START", /* 152 */ "edge ::= END", /* 153 */ "edge ::= RIGHT", /* 154 */ "edge ::= LEFT", /* 155 */ "object ::= objectname", } /* ** Try to increase the size of the parser stack. Return the number ** of errors. Return 0 on success. */ func (p *yyParser) yyGrowStack() { oldSize := len(p.yystack) newSize := oldSize*2 + 100 pNew := make([]yyStackEntry, newSize) copy(pNew, p.yystack) p.yystack = pNew if !NDEBUG { // #ifndef NDEBUG if yyTraceFILE != nil { fmt.Fprintf(yyTraceFILE, "%sStack grows from %d to %d entries.\n", yyTracePrompt, oldSize, newSize) } } // #endif } /* Datatype of the argument to the memory allocated passed as the ** second argument to pik_parserAlloc() below. This can be changed by ** putting an appropriate #define in the %include section of the input ** grammar. */ // #ifndef YYMALLOCARGTYPE // # define YYMALLOCARGTYPE size_t // #endif /* Initialize a new parser that has already been allocated. */ func (yypParser *yyParser) pik_parserInit(p *Pik) { yypParser.p = p if !YYNOERRORRECOVERY { yypParser.yyerrcnt = -1 } if YYSTACKDEPTH > 0 { yypParser.yystack = make([]yyStackEntry, YYSTACKDEPTH) } else { yypParser.yystack = []yyStackEntry{{}} } yypParser.yytos = 0 } /* ** This function allocates a new parser. ** The only argument is a pointer to a function which works like ** malloc. ** ** Inputs: ** A pointer to the function used to allocate memory. ** ** Outputs: ** A pointer to a parser. This pointer is used in subsequent calls ** to pik_parser and pik_parserFree. */ func pik_parserAlloc(p *Pik) *yyParser { yypParser := &yyParser{} yypParser.p = p yypParser.pik_parserInit(p) return yypParser } /* The following function deletes the "minor type" or semantic value ** associated with a symbol. The symbol can be either a terminal ** or nonterminal. "yymajor" is the symbol code, and "yypminor" is ** a pointer to the value to be deleted. The code used to do the ** deletions is derived from the %destructor and/or %token_destructor ** directives of the input grammar. */ func (yypParser *yyParser) yy_destructor( yymajor YYCODETYPE, /* Type code for object to destroy */ yypminor *YYMINORTYPE, /* The object to be destroyed */ ) { p := yypParser.p _ = p switch yymajor { /* Here is inserted the actions which take place when a ** terminal or non-terminal is destroyed. This can happen ** when the symbol is popped from the stack during a ** reduce or during error processing or when a parser is ** being destroyed before it is finished parsing. ** ** Note: during a reduce, the only symbols destroyed are those ** which appear on the RHS of the rule, but which are *not* used ** inside the C code. */ /********* Begin destructor definitions ***************************************/ case 99: /* statement_list */ { //line 460 "pikchr.y" p.pik_elist_free(&(yypminor.yy186)) //line 1656 "pikchr.go" } break case 100: /* statement */ case 101: /* unnamed_statement */ case 102: /* basetype */ { //line 462 "pikchr.y" p.pik_elem_free((yypminor.yy104)) //line 1665 "pikchr.go" } break /********* End destructor definitions *****************************************/ default: break /* If no destructor action specified: do nothing */ } } /* ** Pop the parser's stack once. ** ** If there is a destructor routine associated with the token which ** is popped from the stack, then call it. */ func (pParser *yyParser) yy_pop_parser_stack() { assert(pParser.yytos > 0, "pParser.yytos>0") yytos := pParser.yystack[pParser.yytos] pParser.yytos-- if !NDEBUG { if yyTraceFILE != nil { fmt.Fprintf(yyTraceFILE, "%sPopping %s\n", yyTracePrompt, yyTokenName[yytos.major]) } } pParser.yy_destructor(yytos.major, &yytos.minor) } /* ** Clear all secondary memory allocations from the parser */ func (pParser *yyParser) pik_parserFinalize() { for pParser.yytos > 0 { pParser.yy_pop_parser_stack() } } /* ** Deallocate and destroy a parser. Destructors are called for ** all stack elements before shutting the parser down. ** ** If the YYPARSEFREENEVERNULL macro exists (for example because it ** is defined in a %include section of the input grammar) then it is ** assumed that the input pointer is never NULL. */ func (pParser *yyParser) pik_parserFree() { pParser.pik_parserFinalize() } /* ** Return the peak depth of the stack for a parser. */ func (pParser *yyParser) pik_parserStackPeak() int { return pParser.yyhwm } /* This array of booleans keeps track of the parser statement ** coverage. The element yycoverage[X][Y] is set when the parser ** is in state X and has a lookahead token Y. In a well-tested ** systems, every element of this matrix should end up being set. */ var yycoverage = [YYNSTATE][YYNTOKEN]bool{} /* ** Write into out a description of every state/lookahead combination that ** ** (1) has not been used by the parser, and ** (2) is not a syntax error. ** ** Return the number of missed state/lookahead combinations. */ func pik_parserCoverage(out io.Writer) int { nMissed := 0 for stateno := 0; stateno < YYNSTATE; stateno++ { i := yy_shift_ofst[stateno] for iLookAhead := 0; iLookAhead < YYNTOKEN; iLookAhead++ { if yy_lookahead[int(i)+iLookAhead] != YYCODETYPE(iLookAhead) { continue } if !yycoverage[stateno][iLookAhead] { nMissed++ } if out != nil { ok := "missed" if yycoverage[stateno][iLookAhead] { ok = "ok" } fmt.Fprintf(out, "State %d lookahead %s %s\n", stateno, yyTokenName[iLookAhead], ok) } } } return nMissed } /* ** Find the appropriate action for a parser given the terminal ** look-ahead token iLookAhead. */ func yy_find_shift_action( lookAhead YYCODETYPE, /* The look-ahead token */ stateno YYACTIONTYPE, /* Current state number */ ) YYACTIONTYPE { iLookAhead := int(lookAhead) if stateno > YY_MAX_SHIFT { return stateno } assert(stateno <= YY_SHIFT_COUNT, "stateno <= YY_SHIFT_COUNT") if YYCOVERAGE { yycoverage[stateno][iLookAhead] = true } for { i := int(yy_shift_ofst[stateno]) assert(i >= 0, "i>=0") assert(i <= YY_ACTTAB_COUNT, "i<=YY_ACTTAB_COUNT") assert(i+YYNTOKEN <= len(yy_lookahead), "i+YYNTOKEN<=len(yy_lookahead)") assert(iLookAhead != YYNOCODE, "iLookAhead!=YYNOCODE") assert(iLookAhead < YYNTOKEN, "iLookAhead < YYNTOKEN") i += iLookAhead assert(i < len(yy_lookahead), "i<len(yy_lookahead)") if int(yy_lookahead[i]) != iLookAhead { if YYFALLBACK { assert(iLookAhead < len(yyFallback), "iLookAhead<len(yyfallback)") iFallback := int(yyFallback[iLookAhead]) if iFallback != 0 { if !NDEBUG { if yyTraceFILE != nil { fmt.Fprintf(yyTraceFILE, "%sFALLBACK %s => %s\n", yyTracePrompt, yyTokenName[iLookAhead], yyTokenName[iFallback]) } } assert(yyFallback[iFallback] == 0, "yyFallback[iFallback]==0") /* Fallback loop must terminate */ iLookAhead = iFallback continue } } if YYWILDCARD > 0 { { j := i - iLookAhead + YYWILDCARD assert(j < len(yy_lookahead), "j < len(yy_lookahead)") if int(yy_lookahead[j]) == YYWILDCARD && iLookAhead > 0 { if !NDEBUG { if yyTraceFILE != nil { fmt.Fprintf(yyTraceFILE, "%sWILDCARD %s => %s\n", yyTracePrompt, yyTokenName[iLookAhead], yyTokenName[YYWILDCARD]) } } /* NDEBUG */ return yy_action[j] } } } /* YYWILDCARD */ return yy_default[stateno] } else { assert(i >= 0 && i < len(yy_action), "i >= 0 && i < len(yy_action)") return yy_action[i] } } } /* ** Find the appropriate action for a parser given the non-terminal ** look-ahead token iLookAhead. */ func yy_find_reduce_action( stateno YYACTIONTYPE, /* Current state number */ lookAhead YYCODETYPE, /* The look-ahead token */ ) YYACTIONTYPE { iLookAhead := int(lookAhead) if YYERRORSYMBOL > 0 { if stateno > YY_REDUCE_COUNT { return yy_default[stateno] } } else { assert(stateno <= YY_REDUCE_COUNT, "stateno <= YY_REDUCE_COUNT") } i := int(yy_reduce_ofst[stateno]) assert(iLookAhead != YYNOCODE, "iLookAhead != YYNOCODE") i += iLookAhead if YYERRORSYMBOL > 0 { if i < 0 || i >= YY_ACTTAB_COUNT || int(yy_lookahead[i]) != iLookAhead { return yy_default[stateno] } } else { assert(i >= 0 && i < YY_ACTTAB_COUNT, "i >= 0 && i < YY_ACTTAB_COUNT") assert(int(yy_lookahead[i]) == iLookAhead, "int(yy_lookahead[i]) == iLookAhead") } return yy_action[i] } /* ** The following routine is called if the stack overflows. */ func (yypParser *yyParser) yyStackOverflow() { p := yypParser.p _ = p if !NDEBUG { if yyTraceFILE != nil { fmt.Fprintf(yyTraceFILE, "%sStack Overflow!\n", yyTracePrompt) } } for yypParser.yytos > 0 { yypParser.yy_pop_parser_stack() } /* Here code is inserted which will execute if the parser ** stack every overflows */ /******** Begin %stack_overflow code ******************************************/ //line 493 "pikchr.y" p.pik_error(nil, "parser stack overflow") //line 1879 "pikchr.go" /******** End %stack_overflow code ********************************************/ /* Suppress warning about unused %extra_argument var */ yypParser.p = p } /* ** Print tracing information for a SHIFT action */ func (yypParser *yyParser) yyTraceShift(yyNewState int, zTag string) { if !NDEBUG { if yyTraceFILE != nil { if yyNewState < YYNSTATE { fmt.Fprintf(yyTraceFILE, "%s%s '%s', go to state %d\n", yyTracePrompt, zTag, yyTokenName[yypParser.yystack[yypParser.yytos].major], yyNewState) } else { fmt.Fprintf(yyTraceFILE, "%s%s '%s', pending reduce %d\n", yyTracePrompt, zTag, yyTokenName[yypParser.yystack[yypParser.yytos].major], yyNewState-YY_MIN_REDUCE) } } } } /* ** Perform a shift action. */ func (yypParser *yyParser) yy_shift( yyNewState YYACTIONTYPE, /* The new state to shift in */ yyMajor YYCODETYPE, /* The major token to shift in */ yyMinor pik_parserTOKENTYPE, /* The minor token to shift in */ ) { yypParser.yytos++ if YYTRACKMAXSTACKDEPTH { if yypParser.yytos > yypParser.yyhwm { yypParser.yyhwm++ assert(yypParser.yyhwm == yypParser.yytos, "yypParser.yyhwm == yypParser.yytos") } } if YYSTACKDEPTH > 0 { if yypParser.yytos >= YYSTACKDEPTH { yypParser.yyStackOverflow() return } } else { if yypParser.yytos+1 >= len(yypParser.yystack) { yypParser.yyGrowStack() } } if yyNewState > YY_MAX_SHIFT { yyNewState += YY_MIN_REDUCE - YY_MIN_SHIFTREDUCE } yytos := &yypParser.yystack[yypParser.yytos] yytos.stateno = yyNewState yytos.major = yyMajor yytos.minor.yy0 = yyMinor yypParser.yyTraceShift(int(yyNewState), "Shift") } /* For rule J, yyRuleInfoLhs[J] contains the symbol on the left-hand side ** of that rule */ var yyRuleInfoLhs = []YYCODETYPE{ 121, /* (0) document ::= statement_list */ 99, /* (1) statement_list ::= statement */ 99, /* (2) statement_list ::= statement_list EOL statement */ 100, /* (3) statement ::= */ 100, /* (4) statement ::= direction */ 100, /* (5) statement ::= lvalue ASSIGN rvalue */ 100, /* (6) statement ::= PLACENAME COLON unnamed_statement */ 100, /* (7) statement ::= PLACENAME COLON position */ 100, /* (8) statement ::= unnamed_statement */ 100, /* (9) statement ::= print prlist */ 100, /* (10) statement ::= ASSERT LP expr EQ expr RP */ 100, /* (11) statement ::= ASSERT LP position EQ position RP */ 100, /* (12) statement ::= DEFINE ID CODEBLOCK */ 116, /* (13) rvalue ::= PLACENAME */ 124, /* (14) pritem ::= FILL */ 124, /* (15) pritem ::= COLOR */ 124, /* (16) pritem ::= THICKNESS */ 124, /* (17) pritem ::= rvalue */ 124, /* (18) pritem ::= STRING */ 125, /* (19) prsep ::= COMMA */ 101, /* (20) unnamed_statement ::= basetype attribute_list */ 102, /* (21) basetype ::= CLASSNAME */ 102, /* (22) basetype ::= STRING textposition */ 102, /* (23) basetype ::= LB savelist statement_list RB */ 127, /* (24) savelist ::= */ 119, /* (25) relexpr ::= expr */ 119, /* (26) relexpr ::= expr PERCENT */ 120, /* (27) optrelexpr ::= */ 126, /* (28) attribute_list ::= relexpr alist */ 129, /* (29) attribute ::= numproperty relexpr */ 129, /* (30) attribute ::= dashproperty expr */ 129, /* (31) attribute ::= dashproperty */ 129, /* (32) attribute ::= colorproperty rvalue */ 129, /* (33) attribute ::= go direction optrelexpr */ 129, /* (34) attribute ::= go direction even position */ 129, /* (35) attribute ::= CLOSE */ 129, /* (36) attribute ::= CHOP */ 129, /* (37) attribute ::= FROM position */ 129, /* (38) attribute ::= TO position */ 129, /* (39) attribute ::= THEN */ 129, /* (40) attribute ::= THEN optrelexpr HEADING expr */ 129, /* (41) attribute ::= THEN optrelexpr EDGEPT */ 129, /* (42) attribute ::= GO optrelexpr HEADING expr */ 129, /* (43) attribute ::= GO optrelexpr EDGEPT */ 129, /* (44) attribute ::= AT position */ 129, /* (45) attribute ::= SAME */ 129, /* (46) attribute ::= SAME AS object */ 129, /* (47) attribute ::= STRING textposition */ 129, /* (48) attribute ::= FIT */ 129, /* (49) attribute ::= BEHIND object */ 132, /* (50) withclause ::= DOT_E edge AT position */ 132, /* (51) withclause ::= edge AT position */ 104, /* (52) numproperty ::= HEIGHT|WIDTH|RADIUS|DIAMETER|THICKNESS */ 131, /* (53) boolproperty ::= CW */ 131, /* (54) boolproperty ::= CCW */ 131, /* (55) boolproperty ::= LARROW */ 131, /* (56) boolproperty ::= RARROW */ 131, /* (57) boolproperty ::= LRARROW */ 131, /* (58) boolproperty ::= INVIS */ 131, /* (59) boolproperty ::= THICK */ 131, /* (60) boolproperty ::= THIN */ 131, /* (61) boolproperty ::= SOLID */ 115, /* (62) textposition ::= */ 115, /* (63) textposition ::= textposition CENTER|LJUST|RJUST|ABOVE|BELOW|ITALIC|BOLD|ALIGNED|BIG|SMALL */ 110, /* (64) position ::= expr COMMA expr */ 110, /* (65) position ::= place PLUS expr COMMA expr */ 110, /* (66) position ::= place MINUS expr COMMA expr */ 110, /* (67) position ::= place PLUS LP expr COMMA expr RP */ 110, /* (68) position ::= place MINUS LP expr COMMA expr RP */ 110, /* (69) position ::= LP position COMMA position RP */ 110, /* (70) position ::= LP position RP */ 110, /* (71) position ::= expr between position AND position */ 110, /* (72) position ::= expr LT position COMMA position GT */ 110, /* (73) position ::= expr ABOVE position */ 110, /* (74) position ::= expr BELOW position */ 110, /* (75) position ::= expr LEFT OF position */ 110, /* (76) position ::= expr RIGHT OF position */ 110, /* (77) position ::= expr ON HEADING EDGEPT OF position */ 110, /* (78) position ::= expr HEADING EDGEPT OF position */ 110, /* (79) position ::= expr EDGEPT OF position */ 110, /* (80) position ::= expr ON HEADING expr FROM position */ 110, /* (81) position ::= expr HEADING expr FROM position */ 111, /* (82) place ::= edge OF object */ 134, /* (83) place2 ::= object */ 134, /* (84) place2 ::= object DOT_E edge */ 134, /* (85) place2 ::= NTH VERTEX OF object */ 112, /* (86) object ::= nth */ 112, /* (87) object ::= nth OF|IN object */ 113, /* (88) objectname ::= THIS */ 113, /* (89) objectname ::= PLACENAME */ 113, /* (90) objectname ::= objectname DOT_U PLACENAME */ 114, /* (91) nth ::= NTH CLASSNAME */ 114, /* (92) nth ::= NTH LAST CLASSNAME */ 114, /* (93) nth ::= LAST CLASSNAME */ 114, /* (94) nth ::= LAST */ 114, /* (95) nth ::= NTH LB RB */ 114, /* (96) nth ::= NTH LAST LB RB */ 114, /* (97) nth ::= LAST LB RB */ 103, /* (98) expr ::= expr PLUS expr */ 103, /* (99) expr ::= expr MINUS expr */ 103, /* (100) expr ::= expr STAR expr */ 103, /* (101) expr ::= expr SLASH expr */ 103, /* (102) expr ::= MINUS expr */ 103, /* (103) expr ::= PLUS expr */ 103, /* (104) expr ::= LP expr RP */ 103, /* (105) expr ::= LP FILL|COLOR|THICKNESS RP */ 103, /* (106) expr ::= NUMBER */ 103, /* (107) expr ::= ID */ 103, /* (108) expr ::= FUNC1 LP expr RP */ 103, /* (109) expr ::= FUNC2 LP expr COMMA expr RP */ 103, /* (110) expr ::= DIST LP position COMMA position RP */ 103, /* (111) expr ::= place2 DOT_XY X */ 103, /* (112) expr ::= place2 DOT_XY Y */ 103, /* (113) expr ::= object DOT_L numproperty */ 103, /* (114) expr ::= object DOT_L dashproperty */ 103, /* (115) expr ::= object DOT_L colorproperty */ 117, /* (116) lvalue ::= ID */ 117, /* (117) lvalue ::= FILL */ 117, /* (118) lvalue ::= COLOR */ 117, /* (119) lvalue ::= THICKNESS */ 116, /* (120) rvalue ::= expr */ 122, /* (121) print ::= PRINT */ 123, /* (122) prlist ::= pritem */ 123, /* (123) prlist ::= prlist prsep pritem */ 106, /* (124) direction ::= UP */ 106, /* (125) direction ::= DOWN */ 106, /* (126) direction ::= LEFT */ 106, /* (127) direction ::= RIGHT */ 120, /* (128) optrelexpr ::= relexpr */ 126, /* (129) attribute_list ::= alist */ 128, /* (130) alist ::= */ 128, /* (131) alist ::= alist attribute */ 129, /* (132) attribute ::= boolproperty */ 129, /* (133) attribute ::= WITH withclause */ 130, /* (134) go ::= GO */ 130, /* (135) go ::= */ 118, /* (136) even ::= UNTIL EVEN WITH */ 118, /* (137) even ::= EVEN WITH */ 107, /* (138) dashproperty ::= DOTTED */ 107, /* (139) dashproperty ::= DASHED */ 108, /* (140) colorproperty ::= FILL */ 108, /* (141) colorproperty ::= COLOR */ 110, /* (142) position ::= place */ 133, /* (143) between ::= WAY BETWEEN */ 133, /* (144) between ::= BETWEEN */ 133, /* (145) between ::= OF THE WAY BETWEEN */ 111, /* (146) place ::= place2 */ 105, /* (147) edge ::= CENTER */ 105, /* (148) edge ::= EDGEPT */ 105, /* (149) edge ::= TOP */ 105, /* (150) edge ::= BOTTOM */ 105, /* (151) edge ::= START */ 105, /* (152) edge ::= END */ 105, /* (153) edge ::= RIGHT */ 105, /* (154) edge ::= LEFT */ 112, /* (155) object ::= objectname */ } /* For rule J, yyRuleInfoNRhs[J] contains the negative of the number ** of symbols on the right-hand side of that rule. */ var yyRuleInfoNRhs = []int8{ -1, /* (0) document ::= statement_list */ -1, /* (1) statement_list ::= statement */ -3, /* (2) statement_list ::= statement_list EOL statement */ 0, /* (3) statement ::= */ -1, /* (4) statement ::= direction */ -3, /* (5) statement ::= lvalue ASSIGN rvalue */ -3, /* (6) statement ::= PLACENAME COLON unnamed_statement */ -3, /* (7) statement ::= PLACENAME COLON position */ -1, /* (8) statement ::= unnamed_statement */ -2, /* (9) statement ::= print prlist */ -6, /* (10) statement ::= ASSERT LP expr EQ expr RP */ -6, /* (11) statement ::= ASSERT LP position EQ position RP */ -3, /* (12) statement ::= DEFINE ID CODEBLOCK */ -1, /* (13) rvalue ::= PLACENAME */ -1, /* (14) pritem ::= FILL */ -1, /* (15) pritem ::= COLOR */ -1, /* (16) pritem ::= THICKNESS */ -1, /* (17) pritem ::= rvalue */ -1, /* (18) pritem ::= STRING */ -1, /* (19) prsep ::= COMMA */ -2, /* (20) unnamed_statement ::= basetype attribute_list */ -1, /* (21) basetype ::= CLASSNAME */ -2, /* (22) basetype ::= STRING textposition */ -4, /* (23) basetype ::= LB savelist statement_list RB */ 0, /* (24) savelist ::= */ -1, /* (25) relexpr ::= expr */ -2, /* (26) relexpr ::= expr PERCENT */ 0, /* (27) optrelexpr ::= */ -2, /* (28) attribute_list ::= relexpr alist */ -2, /* (29) attribute ::= numproperty relexpr */ -2, /* (30) attribute ::= dashproperty expr */ -1, /* (31) attribute ::= dashproperty */ -2, /* (32) attribute ::= colorproperty rvalue */ -3, /* (33) attribute ::= go direction optrelexpr */ -4, /* (34) attribute ::= go direction even position */ -1, /* (35) attribute ::= CLOSE */ -1, /* (36) attribute ::= CHOP */ -2, /* (37) attribute ::= FROM position */ -2, /* (38) attribute ::= TO position */ -1, /* (39) attribute ::= THEN */ -4, /* (40) attribute ::= THEN optrelexpr HEADING expr */ -3, /* (41) attribute ::= THEN optrelexpr EDGEPT */ -4, /* (42) attribute ::= GO optrelexpr HEADING expr */ -3, /* (43) attribute ::= GO optrelexpr EDGEPT */ -2, /* (44) attribute ::= AT position */ -1, /* (45) attribute ::= SAME */ -3, /* (46) attribute ::= SAME AS object */ -2, /* (47) attribute ::= STRING textposition */ -1, /* (48) attribute ::= FIT */ -2, /* (49) attribute ::= BEHIND object */ -4, /* (50) withclause ::= DOT_E edge AT position */ -3, /* (51) withclause ::= edge AT position */ -1, /* (52) numproperty ::= HEIGHT|WIDTH|RADIUS|DIAMETER|THICKNESS */ -1, /* (53) boolproperty ::= CW */ -1, /* (54) boolproperty ::= CCW */ -1, /* (55) boolproperty ::= LARROW */ -1, /* (56) boolproperty ::= RARROW */ -1, /* (57) boolproperty ::= LRARROW */ -1, /* (58) boolproperty ::= INVIS */ -1, /* (59) boolproperty ::= THICK */ -1, /* (60) boolproperty ::= THIN */ -1, /* (61) boolproperty ::= SOLID */ 0, /* (62) textposition ::= */ -2, /* (63) textposition ::= textposition CENTER|LJUST|RJUST|ABOVE|BELOW|ITALIC|BOLD|ALIGNED|BIG|SMALL */ -3, /* (64) position ::= expr COMMA expr */ -5, /* (65) position ::= place PLUS expr COMMA expr */ -5, /* (66) position ::= place MINUS expr COMMA expr */ -7, /* (67) position ::= place PLUS LP expr COMMA expr RP */ -7, /* (68) position ::= place MINUS LP expr COMMA expr RP */ -5, /* (69) position ::= LP position COMMA position RP */ -3, /* (70) position ::= LP position RP */ -5, /* (71) position ::= expr between position AND position */ -6, /* (72) position ::= expr LT position COMMA position GT */ -3, /* (73) position ::= expr ABOVE position */ -3, /* (74) position ::= expr BELOW position */ -4, /* (75) position ::= expr LEFT OF position */ -4, /* (76) position ::= expr RIGHT OF position */ -6, /* (77) position ::= expr ON HEADING EDGEPT OF position */ -5, /* (78) position ::= expr HEADING EDGEPT OF position */ -4, /* (79) position ::= expr EDGEPT OF position */ -6, /* (80) position ::= expr ON HEADING expr FROM position */ -5, /* (81) position ::= expr HEADING expr FROM position */ -3, /* (82) place ::= edge OF object */ -1, /* (83) place2 ::= object */ -3, /* (84) place2 ::= object DOT_E edge */ -4, /* (85) place2 ::= NTH VERTEX OF object */ -1, /* (86) object ::= nth */ -3, /* (87) object ::= nth OF|IN object */ -1, /* (88) objectname ::= THIS */ -1, /* (89) objectname ::= PLACENAME */ -3, /* (90) objectname ::= objectname DOT_U PLACENAME */ -2, /* (91) nth ::= NTH CLASSNAME */ -3, /* (92) nth ::= NTH LAST CLASSNAME */ -2, /* (93) nth ::= LAST CLASSNAME */ -1, /* (94) nth ::= LAST */ -3, /* (95) nth ::= NTH LB RB */ -4, /* (96) nth ::= NTH LAST LB RB */ -3, /* (97) nth ::= LAST LB RB */ -3, /* (98) expr ::= expr PLUS expr */ -3, /* (99) expr ::= expr MINUS expr */ -3, /* (100) expr ::= expr STAR expr */ -3, /* (101) expr ::= expr SLASH expr */ -2, /* (102) expr ::= MINUS expr */ -2, /* (103) expr ::= PLUS expr */ -3, /* (104) expr ::= LP expr RP */ -3, /* (105) expr ::= LP FILL|COLOR|THICKNESS RP */ -1, /* (106) expr ::= NUMBER */ -1, /* (107) expr ::= ID */ -4, /* (108) expr ::= FUNC1 LP expr RP */ -6, /* (109) expr ::= FUNC2 LP expr COMMA expr RP */ -6, /* (110) expr ::= DIST LP position COMMA position RP */ -3, /* (111) expr ::= place2 DOT_XY X */ -3, /* (112) expr ::= place2 DOT_XY Y */ -3, /* (113) expr ::= object DOT_L numproperty */ -3, /* (114) expr ::= object DOT_L dashproperty */ -3, /* (115) expr ::= object DOT_L colorproperty */ -1, /* (116) lvalue ::= ID */ -1, /* (117) lvalue ::= FILL */ -1, /* (118) lvalue ::= COLOR */ -1, /* (119) lvalue ::= THICKNESS */ -1, /* (120) rvalue ::= expr */ -1, /* (121) print ::= PRINT */ -1, /* (122) prlist ::= pritem */ -3, /* (123) prlist ::= prlist prsep pritem */ -1, /* (124) direction ::= UP */ -1, /* (125) direction ::= DOWN */ -1, /* (126) direction ::= LEFT */ -1, /* (127) direction ::= RIGHT */ -1, /* (128) optrelexpr ::= relexpr */ -1, /* (129) attribute_list ::= alist */ 0, /* (130) alist ::= */ -2, /* (131) alist ::= alist attribute */ -1, /* (132) attribute ::= boolproperty */ -2, /* (133) attribute ::= WITH withclause */ -1, /* (134) go ::= GO */ 0, /* (135) go ::= */ -3, /* (136) even ::= UNTIL EVEN WITH */ -2, /* (137) even ::= EVEN WITH */ -1, /* (138) dashproperty ::= DOTTED */ -1, /* (139) dashproperty ::= DASHED */ -1, /* (140) colorproperty ::= FILL */ -1, /* (141) colorproperty ::= COLOR */ -1, /* (142) position ::= place */ -2, /* (143) between ::= WAY BETWEEN */ -1, /* (144) between ::= BETWEEN */ -4, /* (145) between ::= OF THE WAY BETWEEN */ -1, /* (146) place ::= place2 */ -1, /* (147) edge ::= CENTER */ -1, /* (148) edge ::= EDGEPT */ -1, /* (149) edge ::= TOP */ -1, /* (150) edge ::= BOTTOM */ -1, /* (151) edge ::= START */ -1, /* (152) edge ::= END */ -1, /* (153) edge ::= RIGHT */ -1, /* (154) edge ::= LEFT */ -1, /* (155) object ::= objectname */ } /* ** Perform a reduce action and the shift that must immediately ** follow the reduce. ** ** The yyLookahead and yyLookaheadToken parameters provide reduce actions ** access to the lookahead token (if any). The yyLookahead will be YYNOCODE ** if the lookahead token has already been consumed. As this procedure is ** only called from one place, optimizing compilers will in-line it, which ** means that the extra parameters have no performance impact. */ func (yypParser *yyParser) yy_reduce( yyruleno YYACTIONTYPE, /* Number of the rule by which to reduce */ yyLookahead YYCODETYPE, /* Lookahead token, or YYNOCODE if none */ yyLookaheadToken pik_parserTOKENTYPE, /* Value of the lookahead token */ p *Pik /* %extra_context */) YYACTIONTYPE { var ( yygoto YYCODETYPE /* The next state */ yyact YYACTIONTYPE /* The next action */ yymsp int /* The top of the parser's stack */ yysize int /* Amount to pop the stack */ yylhsminor YYMINORTYPE ) yymsp = yypParser.yytos _ = yylhsminor switch yyruleno { /* Beginning here are the reduction cases. A typical example ** follows: ** case 0: ** #line <lineno> <grammarfile> ** { ... } // User supplied code ** #line <lineno> <thisfile> ** break; */ /********** Begin reduce actions **********************************************/ case 0: /* document ::= statement_list */ //line 497 "pikchr.y" { p.pik_render(yypParser.yystack[yypParser.yytos+0].minor.yy186) } //line 2306 "pikchr.go" break case 1: /* statement_list ::= statement */ //line 500 "pikchr.y" { yylhsminor.yy186 = p.pik_elist_append(nil, yypParser.yystack[yypParser.yytos+0].minor.yy104) } //line 2311 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy186 = yylhsminor.yy186 break case 2: /* statement_list ::= statement_list EOL statement */ //line 502 "pikchr.y" { yylhsminor.yy186 = p.pik_elist_append(yypParser.yystack[yypParser.yytos+-2].minor.yy186, yypParser.yystack[yypParser.yytos+0].minor.yy104) } //line 2317 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy186 = yylhsminor.yy186 break case 3: /* statement ::= */ //line 505 "pikchr.y" { yypParser.yystack[yypParser.yytos+1].minor.yy104 = nil } //line 2323 "pikchr.go" break case 4: /* statement ::= direction */ //line 506 "pikchr.y" { p.pik_set_direction(uint8(yypParser.yystack[yypParser.yytos+0].minor.yy0.eCode)) yylhsminor.yy104 = nil } //line 2328 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy104 = yylhsminor.yy104 break case 5: /* statement ::= lvalue ASSIGN rvalue */ //line 507 "pikchr.y" { p.pik_set_var(&yypParser.yystack[yypParser.yytos+-2].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy153, &yypParser.yystack[yypParser.yytos+-1].minor.yy0) yylhsminor.yy104 = nil } //line 2334 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy104 = yylhsminor.yy104 break case 6: /* statement ::= PLACENAME COLON unnamed_statement */ //line 509 "pikchr.y" { yylhsminor.yy104 = yypParser.yystack[yypParser.yytos+0].minor.yy104 p.pik_elem_setname(yypParser.yystack[yypParser.yytos+0].minor.yy104, &yypParser.yystack[yypParser.yytos+-2].minor.yy0) } //line 2340 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy104 = yylhsminor.yy104 break case 7: /* statement ::= PLACENAME COLON position */ //line 511 "pikchr.y" { yylhsminor.yy104 = p.pik_elem_new(nil, nil, nil) if yylhsminor.yy104 != nil { yylhsminor.yy104.ptAt = yypParser.yystack[yypParser.yytos+0].minor.yy79 p.pik_elem_setname(yylhsminor.yy104, &yypParser.yystack[yypParser.yytos+-2].minor.yy0) } } //line 2347 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy104 = yylhsminor.yy104 break case 8: /* statement ::= unnamed_statement */ //line 513 "pikchr.y" { yylhsminor.yy104 = yypParser.yystack[yypParser.yytos+0].minor.yy104 } //line 2353 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy104 = yylhsminor.yy104 break case 9: /* statement ::= print prlist */ //line 514 "pikchr.y" { p.pik_append("<br>\n") yypParser.yystack[yypParser.yytos+-1].minor.yy104 = nil } //line 2359 "pikchr.go" break case 10: /* statement ::= ASSERT LP expr EQ expr RP */ //line 519 "pikchr.y" { yypParser.yystack[yypParser.yytos+-5].minor.yy104 = p.pik_assert(yypParser.yystack[yypParser.yytos+-3].minor.yy153, &yypParser.yystack[yypParser.yytos+-2].minor.yy0, yypParser.yystack[yypParser.yytos+-1].minor.yy153) } //line 2364 "pikchr.go" break case 11: /* statement ::= ASSERT LP position EQ position RP */ //line 521 "pikchr.y" { yypParser.yystack[yypParser.yytos+-5].minor.yy104 = p.pik_position_assert(&yypParser.yystack[yypParser.yytos+-3].minor.yy79, &yypParser.yystack[yypParser.yytos+-2].minor.yy0, &yypParser.yystack[yypParser.yytos+-1].minor.yy79) } //line 2369 "pikchr.go" break case 12: /* statement ::= DEFINE ID CODEBLOCK */ //line 522 "pikchr.y" { yypParser.yystack[yypParser.yytos+-2].minor.yy104 = nil p.pik_add_macro(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy0) } //line 2374 "pikchr.go" break case 13: /* rvalue ::= PLACENAME */ //line 533 "pikchr.y" { yylhsminor.yy153 = p.pik_lookup_color(&yypParser.yystack[yypParser.yytos+0].minor.yy0) } //line 2379 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy153 = yylhsminor.yy153 break case 14: /* pritem ::= FILL */ fallthrough case 15: /* pritem ::= COLOR */ yytestcase(yyruleno == 15) fallthrough case 16: /* pritem ::= THICKNESS */ yytestcase(yyruleno == 16) //line 538 "pikchr.y" { p.pik_append_num("", p.pik_value(yypParser.yystack[yypParser.yytos+0].minor.yy0.String(), nil)) } //line 2389 "pikchr.go" break case 17: /* pritem ::= rvalue */ //line 541 "pikchr.y" { p.pik_append_num("", yypParser.yystack[yypParser.yytos+0].minor.yy153) } //line 2394 "pikchr.go" break case 18: /* pritem ::= STRING */ //line 542 "pikchr.y" { p.pik_append_text(string(yypParser.yystack[yypParser.yytos+0].minor.yy0.z[1:yypParser.yystack[yypParser.yytos+0].minor.yy0.n-1]), 0) } //line 2399 "pikchr.go" break case 19: /* prsep ::= COMMA */ //line 543 "pikchr.y" { p.pik_append(" ") } //line 2404 "pikchr.go" break case 20: /* unnamed_statement ::= basetype attribute_list */ //line 546 "pikchr.y" { yylhsminor.yy104 = yypParser.yystack[yypParser.yytos+-1].minor.yy104 p.pik_after_adding_attributes(yylhsminor.yy104) } //line 2409 "pikchr.go" yypParser.yystack[yypParser.yytos+-1].minor.yy104 = yylhsminor.yy104 break case 21: /* basetype ::= CLASSNAME */ //line 548 "pikchr.y" { yylhsminor.yy104 = p.pik_elem_new(&yypParser.yystack[yypParser.yytos+0].minor.yy0, nil, nil) } //line 2415 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy104 = yylhsminor.yy104 break case 22: /* basetype ::= STRING textposition */ //line 550 "pikchr.y" { yypParser.yystack[yypParser.yytos+-1].minor.yy0.eCode = int16(yypParser.yystack[yypParser.yytos+0].minor.yy112) yylhsminor.yy104 = p.pik_elem_new(nil, &yypParser.yystack[yypParser.yytos+-1].minor.yy0, nil) } //line 2421 "pikchr.go" yypParser.yystack[yypParser.yytos+-1].minor.yy104 = yylhsminor.yy104 break case 23: /* basetype ::= LB savelist statement_list RB */ //line 552 "pikchr.y" { p.list = yypParser.yystack[yypParser.yytos+-2].minor.yy186 yypParser.yystack[yypParser.yytos+-3].minor.yy104 = p.pik_elem_new(nil, nil, yypParser.yystack[yypParser.yytos+-1].minor.yy186) if yypParser.yystack[yypParser.yytos+-3].minor.yy104 != nil { yypParser.yystack[yypParser.yytos+-3].minor.yy104.errTok = yypParser.yystack[yypParser.yytos+0].minor.yy0 } } //line 2427 "pikchr.go" break case 24: /* savelist ::= */ //line 557 "pikchr.y" { yypParser.yystack[yypParser.yytos+1].minor.yy186 = p.list p.list = nil } //line 2432 "pikchr.go" break case 25: /* relexpr ::= expr */ //line 564 "pikchr.y" { yylhsminor.yy10.rAbs = yypParser.yystack[yypParser.yytos+0].minor.yy153 yylhsminor.yy10.rRel = 0 } //line 2437 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy10 = yylhsminor.yy10 break case 26: /* relexpr ::= expr PERCENT */ //line 565 "pikchr.y" { yylhsminor.yy10.rAbs = 0 yylhsminor.yy10.rRel = yypParser.yystack[yypParser.yytos+-1].minor.yy153 / 100 } //line 2443 "pikchr.go" yypParser.yystack[yypParser.yytos+-1].minor.yy10 = yylhsminor.yy10 break case 27: /* optrelexpr ::= */ //line 567 "pikchr.y" { yypParser.yystack[yypParser.yytos+1].minor.yy10.rAbs = 0 yypParser.yystack[yypParser.yytos+1].minor.yy10.rRel = 1.0 } //line 2449 "pikchr.go" break case 28: /* attribute_list ::= relexpr alist */ //line 569 "pikchr.y" { p.pik_add_direction(nil, &yypParser.yystack[yypParser.yytos+-1].minor.yy10) } //line 2454 "pikchr.go" break case 29: /* attribute ::= numproperty relexpr */ //line 573 "pikchr.y" { p.pik_set_numprop(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy10) } //line 2459 "pikchr.go" break case 30: /* attribute ::= dashproperty expr */ //line 574 "pikchr.y" { p.pik_set_dashed(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy153) } //line 2464 "pikchr.go" break case 31: /* attribute ::= dashproperty */ //line 575 "pikchr.y" { p.pik_set_dashed(&yypParser.yystack[yypParser.yytos+0].minor.yy0, nil) } //line 2469 "pikchr.go" break case 32: /* attribute ::= colorproperty rvalue */ //line 576 "pikchr.y" { p.pik_set_clrprop(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy153) } //line 2474 "pikchr.go" break case 33: /* attribute ::= go direction optrelexpr */ //line 577 "pikchr.y" { p.pik_add_direction(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy10) } //line 2479 "pikchr.go" break case 34: /* attribute ::= go direction even position */ //line 578 "pikchr.y" { p.pik_evenwith(&yypParser.yystack[yypParser.yytos+-2].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy79) } //line 2484 "pikchr.go" break case 35: /* attribute ::= CLOSE */ //line 579 "pikchr.y" { p.pik_close_path(&yypParser.yystack[yypParser.yytos+0].minor.yy0) } //line 2489 "pikchr.go" break case 36: /* attribute ::= CHOP */ //line 580 "pikchr.y" { p.cur.bChop = true } //line 2494 "pikchr.go" break case 37: /* attribute ::= FROM position */ //line 581 "pikchr.y" { p.pik_set_from(p.cur, &yypParser.yystack[yypParser.yytos+-1].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy79) } //line 2499 "pikchr.go" break case 38: /* attribute ::= TO position */ //line 582 "pikchr.y" { p.pik_add_to(p.cur, &yypParser.yystack[yypParser.yytos+-1].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy79) } //line 2504 "pikchr.go" break case 39: /* attribute ::= THEN */ //line 583 "pikchr.y" { p.pik_then(&yypParser.yystack[yypParser.yytos+0].minor.yy0, p.cur) } //line 2509 "pikchr.go" break case 40: /* attribute ::= THEN optrelexpr HEADING expr */ fallthrough case 42: /* attribute ::= GO optrelexpr HEADING expr */ yytestcase(yyruleno == 42) //line 585 "pikchr.y" { p.pik_move_hdg(&yypParser.yystack[yypParser.yytos+-2].minor.yy10, &yypParser.yystack[yypParser.yytos+-1].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy153, nil, &yypParser.yystack[yypParser.yytos+-3].minor.yy0) } //line 2516 "pikchr.go" break case 41: /* attribute ::= THEN optrelexpr EDGEPT */ fallthrough case 43: /* attribute ::= GO optrelexpr EDGEPT */ yytestcase(yyruleno == 43) //line 586 "pikchr.y" { p.pik_move_hdg(&yypParser.yystack[yypParser.yytos+-1].minor.yy10, nil, 0, &yypParser.yystack[yypParser.yytos+0].minor.yy0, &yypParser.yystack[yypParser.yytos+-2].minor.yy0) } //line 2523 "pikchr.go" break case 44: /* attribute ::= AT position */ //line 591 "pikchr.y" { p.pik_set_at(nil, &yypParser.yystack[yypParser.yytos+0].minor.yy79, &yypParser.yystack[yypParser.yytos+-1].minor.yy0) } //line 2528 "pikchr.go" break case 45: /* attribute ::= SAME */ //line 593 "pikchr.y" { p.pik_same(nil, &yypParser.yystack[yypParser.yytos+0].minor.yy0) } //line 2533 "pikchr.go" break case 46: /* attribute ::= SAME AS object */ //line 594 "pikchr.y" { p.pik_same(yypParser.yystack[yypParser.yytos+0].minor.yy104, &yypParser.yystack[yypParser.yytos+-2].minor.yy0) } //line 2538 "pikchr.go" break case 47: /* attribute ::= STRING textposition */ //line 595 "pikchr.y" { p.pik_add_txt(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, int16(yypParser.yystack[yypParser.yytos+0].minor.yy112)) } //line 2543 "pikchr.go" break case 48: /* attribute ::= FIT */ //line 596 "pikchr.y" { p.pik_size_to_fit(&yypParser.yystack[yypParser.yytos+0].minor.yy0, 3) } //line 2548 "pikchr.go" break case 49: /* attribute ::= BEHIND object */ //line 597 "pikchr.y" { p.pik_behind(yypParser.yystack[yypParser.yytos+0].minor.yy104) } //line 2553 "pikchr.go" break case 50: /* withclause ::= DOT_E edge AT position */ fallthrough case 51: /* withclause ::= edge AT position */ yytestcase(yyruleno == 51) //line 605 "pikchr.y" { p.pik_set_at(&yypParser.yystack[yypParser.yytos+-2].minor.yy0, &yypParser.yystack[yypParser.yytos+0].minor.yy79, &yypParser.yystack[yypParser.yytos+-1].minor.yy0) } //line 2560 "pikchr.go" break case 52: /* numproperty ::= HEIGHT|WIDTH|RADIUS|DIAMETER|THICKNESS */ //line 609 "pikchr.y" { yylhsminor.yy0 = yypParser.yystack[yypParser.yytos+0].minor.yy0 } //line 2565 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy0 = yylhsminor.yy0 break case 53: /* boolproperty ::= CW */ //line 620 "pikchr.y" { p.cur.cw = true } //line 2571 "pikchr.go" break case 54: /* boolproperty ::= CCW */ //line 621 "pikchr.y" { p.cur.cw = false } //line 2576 "pikchr.go" break case 55: /* boolproperty ::= LARROW */ //line 622 "pikchr.y" { p.cur.larrow = true p.cur.rarrow = false } //line 2581 "pikchr.go" break case 56: /* boolproperty ::= RARROW */ //line 623 "pikchr.y" { p.cur.larrow = false p.cur.rarrow = true } //line 2586 "pikchr.go" break case 57: /* boolproperty ::= LRARROW */ //line 624 "pikchr.y" { p.cur.larrow = true p.cur.rarrow = true } //line 2591 "pikchr.go" break case 58: /* boolproperty ::= INVIS */ //line 625 "pikchr.y" { p.cur.sw = 0.0 } //line 2596 "pikchr.go" break case 59: /* boolproperty ::= THICK */ //line 626 "pikchr.y" { p.cur.sw *= 1.5 } //line 2601 "pikchr.go" break case 60: /* boolproperty ::= THIN */ //line 627 "pikchr.y" { p.cur.sw *= 0.67 } //line 2606 "pikchr.go" break case 61: /* boolproperty ::= SOLID */ //line 628 "pikchr.y" { p.cur.sw = p.pik_value("thickness", nil) p.cur.dotted = 0.0 p.cur.dashed = 0.0 } //line 2612 "pikchr.go" break case 62: /* textposition ::= */ //line 631 "pikchr.y" { yypParser.yystack[yypParser.yytos+1].minor.yy112 = 0 } //line 2617 "pikchr.go" break case 63: /* textposition ::= textposition CENTER|LJUST|RJUST|ABOVE|BELOW|ITALIC|BOLD|ALIGNED|BIG|SMALL */ //line 634 "pikchr.y" { yylhsminor.yy112 = pik_text_position(yypParser.yystack[yypParser.yytos+-1].minor.yy112, &yypParser.yystack[yypParser.yytos+0].minor.yy0) } //line 2622 "pikchr.go" yypParser.yystack[yypParser.yytos+-1].minor.yy112 = yylhsminor.yy112 break case 64: /* position ::= expr COMMA expr */ //line 637 "pikchr.y" { yylhsminor.yy79.x = yypParser.yystack[yypParser.yytos+-2].minor.yy153 yylhsminor.yy79.y = yypParser.yystack[yypParser.yytos+0].minor.yy153 } //line 2628 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy79 = yylhsminor.yy79 break case 65: /* position ::= place PLUS expr COMMA expr */ //line 639 "pikchr.y" { yylhsminor.yy79.x = yypParser.yystack[yypParser.yytos+-4].minor.yy79.x + yypParser.yystack[yypParser.yytos+-2].minor.yy153 yylhsminor.yy79.y = yypParser.yystack[yypParser.yytos+-4].minor.yy79.y + yypParser.yystack[yypParser.yytos+0].minor.yy153 } //line 2634 "pikchr.go" yypParser.yystack[yypParser.yytos+-4].minor.yy79 = yylhsminor.yy79 break case 66: /* position ::= place MINUS expr COMMA expr */ //line 640 "pikchr.y" { yylhsminor.yy79.x = yypParser.yystack[yypParser.yytos+-4].minor.yy79.x - yypParser.yystack[yypParser.yytos+-2].minor.yy153 yylhsminor.yy79.y = yypParser.yystack[yypParser.yytos+-4].minor.yy79.y - yypParser.yystack[yypParser.yytos+0].minor.yy153 } //line 2640 "pikchr.go" yypParser.yystack[yypParser.yytos+-4].minor.yy79 = yylhsminor.yy79 break case 67: /* position ::= place PLUS LP expr COMMA expr RP */ //line 642 "pikchr.y" { yylhsminor.yy79.x = yypParser.yystack[yypParser.yytos+-6].minor.yy79.x + yypParser.yystack[yypParser.yytos+-3].minor.yy153 yylhsminor.yy79.y = yypParser.yystack[yypParser.yytos+-6].minor.yy79.y + yypParser.yystack[yypParser.yytos+-1].minor.yy153 } //line 2646 "pikchr.go" yypParser.yystack[yypParser.yytos+-6].minor.yy79 = yylhsminor.yy79 break case 68: /* position ::= place MINUS LP expr COMMA expr RP */ //line 644 "pikchr.y" { yylhsminor.yy79.x = yypParser.yystack[yypParser.yytos+-6].minor.yy79.x - yypParser.yystack[yypParser.yytos+-3].minor.yy153 yylhsminor.yy79.y = yypParser.yystack[yypParser.yytos+-6].minor.yy79.y - yypParser.yystack[yypParser.yytos+-1].minor.yy153 } //line 2652 "pikchr.go" yypParser.yystack[yypParser.yytos+-6].minor.yy79 = yylhsminor.yy79 break case 69: /* position ::= LP position COMMA position RP */ //line 645 "pikchr.y" { yypParser.yystack[yypParser.yytos+-4].minor.yy79.x = yypParser.yystack[yypParser.yytos+-3].minor.yy79.x yypParser.yystack[yypParser.yytos+-4].minor.yy79.y = yypParser.yystack[yypParser.yytos+-1].minor.yy79.y } //line 2658 "pikchr.go" break case 70: /* position ::= LP position RP */ //line 646 "pikchr.y" { yypParser.yystack[yypParser.yytos+-2].minor.yy79 = yypParser.yystack[yypParser.yytos+-1].minor.yy79 } //line 2663 "pikchr.go" break case 71: /* position ::= expr between position AND position */ //line 648 "pikchr.y" { yylhsminor.yy79 = pik_position_between(yypParser.yystack[yypParser.yytos+-4].minor.yy153, yypParser.yystack[yypParser.yytos+-2].minor.yy79, yypParser.yystack[yypParser.yytos+0].minor.yy79) } //line 2668 "pikchr.go" yypParser.yystack[yypParser.yytos+-4].minor.yy79 = yylhsminor.yy79 break case 72: /* position ::= expr LT position COMMA position GT */ //line 650 "pikchr.y" { yylhsminor.yy79 = pik_position_between(yypParser.yystack[yypParser.yytos+-5].minor.yy153, yypParser.yystack[yypParser.yytos+-3].minor.yy79, yypParser.yystack[yypParser.yytos+-1].minor.yy79) } //line 2674 "pikchr.go" yypParser.yystack[yypParser.yytos+-5].minor.yy79 = yylhsminor.yy79 break case 73: /* position ::= expr ABOVE position */ //line 651 "pikchr.y" { yylhsminor.yy79 = yypParser.yystack[yypParser.yytos+0].minor.yy79 yylhsminor.yy79.y += yypParser.yystack[yypParser.yytos+-2].minor.yy153 } //line 2680 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy79 = yylhsminor.yy79 break case 74: /* position ::= expr BELOW position */ //line 652 "pikchr.y" { yylhsminor.yy79 = yypParser.yystack[yypParser.yytos+0].minor.yy79 yylhsminor.yy79.y -= yypParser.yystack[yypParser.yytos+-2].minor.yy153 } //line 2686 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy79 = yylhsminor.yy79 break case 75: /* position ::= expr LEFT OF position */ //line 653 "pikchr.y" { yylhsminor.yy79 = yypParser.yystack[yypParser.yytos+0].minor.yy79 yylhsminor.yy79.x -= yypParser.yystack[yypParser.yytos+-3].minor.yy153 } //line 2692 "pikchr.go" yypParser.yystack[yypParser.yytos+-3].minor.yy79 = yylhsminor.yy79 break case 76: /* position ::= expr RIGHT OF position */ //line 654 "pikchr.y" { yylhsminor.yy79 = yypParser.yystack[yypParser.yytos+0].minor.yy79 yylhsminor.yy79.x += yypParser.yystack[yypParser.yytos+-3].minor.yy153 } //line 2698 "pikchr.go" yypParser.yystack[yypParser.yytos+-3].minor.yy79 = yylhsminor.yy79 break case 77: /* position ::= expr ON HEADING EDGEPT OF position */ //line 656 "pikchr.y" { yylhsminor.yy79 = pik_position_at_hdg(yypParser.yystack[yypParser.yytos+-5].minor.yy153, &yypParser.yystack[yypParser.yytos+-2].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy79) } //line 2704 "pikchr.go" yypParser.yystack[yypParser.yytos+-5].minor.yy79 = yylhsminor.yy79 break case 78: /* position ::= expr HEADING EDGEPT OF position */ //line 658 "pikchr.y" { yylhsminor.yy79 = pik_position_at_hdg(yypParser.yystack[yypParser.yytos+-4].minor.yy153, &yypParser.yystack[yypParser.yytos+-2].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy79) } //line 2710 "pikchr.go" yypParser.yystack[yypParser.yytos+-4].minor.yy79 = yylhsminor.yy79 break case 79: /* position ::= expr EDGEPT OF position */ //line 660 "pikchr.y" { yylhsminor.yy79 = pik_position_at_hdg(yypParser.yystack[yypParser.yytos+-3].minor.yy153, &yypParser.yystack[yypParser.yytos+-2].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy79) } //line 2716 "pikchr.go" yypParser.yystack[yypParser.yytos+-3].minor.yy79 = yylhsminor.yy79 break case 80: /* position ::= expr ON HEADING expr FROM position */ //line 662 "pikchr.y" { yylhsminor.yy79 = pik_position_at_angle(yypParser.yystack[yypParser.yytos+-5].minor.yy153, yypParser.yystack[yypParser.yytos+-2].minor.yy153, yypParser.yystack[yypParser.yytos+0].minor.yy79) } //line 2722 "pikchr.go" yypParser.yystack[yypParser.yytos+-5].minor.yy79 = yylhsminor.yy79 break case 81: /* position ::= expr HEADING expr FROM position */ //line 664 "pikchr.y" { yylhsminor.yy79 = pik_position_at_angle(yypParser.yystack[yypParser.yytos+-4].minor.yy153, yypParser.yystack[yypParser.yytos+-2].minor.yy153, yypParser.yystack[yypParser.yytos+0].minor.yy79) } //line 2728 "pikchr.go" yypParser.yystack[yypParser.yytos+-4].minor.yy79 = yylhsminor.yy79 break case 82: /* place ::= edge OF object */ //line 676 "pikchr.y" { yylhsminor.yy79 = p.pik_place_of_elem(yypParser.yystack[yypParser.yytos+0].minor.yy104, &yypParser.yystack[yypParser.yytos+-2].minor.yy0) } //line 2734 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy79 = yylhsminor.yy79 break case 83: /* place2 ::= object */ //line 677 "pikchr.y" { yylhsminor.yy79 = p.pik_place_of_elem(yypParser.yystack[yypParser.yytos+0].minor.yy104, nil) } //line 2740 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy79 = yylhsminor.yy79 break case 84: /* place2 ::= object DOT_E edge */ //line 678 "pikchr.y" { yylhsminor.yy79 = p.pik_place_of_elem(yypParser.yystack[yypParser.yytos+-2].minor.yy104, &yypParser.yystack[yypParser.yytos+0].minor.yy0) } //line 2746 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy79 = yylhsminor.yy79 break case 85: /* place2 ::= NTH VERTEX OF object */ //line 679 "pikchr.y" { yylhsminor.yy79 = p.pik_nth_vertex(&yypParser.yystack[yypParser.yytos+-3].minor.yy0, &yypParser.yystack[yypParser.yytos+-2].minor.yy0, yypParser.yystack[yypParser.yytos+0].minor.yy104) } //line 2752 "pikchr.go" yypParser.yystack[yypParser.yytos+-3].minor.yy79 = yylhsminor.yy79 break case 86: /* object ::= nth */ //line 691 "pikchr.y" { yylhsminor.yy104 = p.pik_find_nth(nil, &yypParser.yystack[yypParser.yytos+0].minor.yy0) } //line 2758 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy104 = yylhsminor.yy104 break case 87: /* object ::= nth OF|IN object */ //line 692 "pikchr.y" { yylhsminor.yy104 = p.pik_find_nth(yypParser.yystack[yypParser.yytos+0].minor.yy104, &yypParser.yystack[yypParser.yytos+-2].minor.yy0) } //line 2764 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy104 = yylhsminor.yy104 break case 88: /* objectname ::= THIS */ //line 694 "pikchr.y" { yypParser.yystack[yypParser.yytos+0].minor.yy104 = p.cur } //line 2770 "pikchr.go" break case 89: /* objectname ::= PLACENAME */ //line 695 "pikchr.y" { yylhsminor.yy104 = p.pik_find_byname(nil, &yypParser.yystack[yypParser.yytos+0].minor.yy0) } //line 2775 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy104 = yylhsminor.yy104 break case 90: /* objectname ::= objectname DOT_U PLACENAME */ //line 697 "pikchr.y" { yylhsminor.yy104 = p.pik_find_byname(yypParser.yystack[yypParser.yytos+-2].minor.yy104, &yypParser.yystack[yypParser.yytos+0].minor.yy0) } //line 2781 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy104 = yylhsminor.yy104 break case 91: /* nth ::= NTH CLASSNAME */ //line 699 "pikchr.y" { yylhsminor.yy0 = yypParser.yystack[yypParser.yytos+0].minor.yy0 yylhsminor.yy0.eCode = p.pik_nth_value(&yypParser.yystack[yypParser.yytos+-1].minor.yy0) } //line 2787 "pikchr.go" yypParser.yystack[yypParser.yytos+-1].minor.yy0 = yylhsminor.yy0 break case 92: /* nth ::= NTH LAST CLASSNAME */ //line 700 "pikchr.y" { yylhsminor.yy0 = yypParser.yystack[yypParser.yytos+0].minor.yy0 yylhsminor.yy0.eCode = -p.pik_nth_value(&yypParser.yystack[yypParser.yytos+-2].minor.yy0) } //line 2793 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy0 = yylhsminor.yy0 break case 93: /* nth ::= LAST CLASSNAME */ //line 701 "pikchr.y" { yypParser.yystack[yypParser.yytos+-1].minor.yy0 = yypParser.yystack[yypParser.yytos+0].minor.yy0 yypParser.yystack[yypParser.yytos+-1].minor.yy0.eCode = -1 } //line 2799 "pikchr.go" break case 94: /* nth ::= LAST */ //line 702 "pikchr.y" { yylhsminor.yy0 = yypParser.yystack[yypParser.yytos+0].minor.yy0 yylhsminor.yy0.eCode = -1 } //line 2804 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy0 = yylhsminor.yy0 break case 95: /* nth ::= NTH LB RB */ //line 703 "pikchr.y" { yylhsminor.yy0 = yypParser.yystack[yypParser.yytos+-1].minor.yy0 yylhsminor.yy0.eCode = p.pik_nth_value(&yypParser.yystack[yypParser.yytos+-2].minor.yy0) } //line 2810 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy0 = yylhsminor.yy0 break case 96: /* nth ::= NTH LAST LB RB */ //line 704 "pikchr.y" { yylhsminor.yy0 = yypParser.yystack[yypParser.yytos+-1].minor.yy0 yylhsminor.yy0.eCode = -p.pik_nth_value(&yypParser.yystack[yypParser.yytos+-3].minor.yy0) } //line 2816 "pikchr.go" yypParser.yystack[yypParser.yytos+-3].minor.yy0 = yylhsminor.yy0 break case 97: /* nth ::= LAST LB RB */ //line 705 "pikchr.y" { yypParser.yystack[yypParser.yytos+-2].minor.yy0 = yypParser.yystack[yypParser.yytos+-1].minor.yy0 yypParser.yystack[yypParser.yytos+-2].minor.yy0.eCode = -1 } //line 2822 "pikchr.go" break case 98: /* expr ::= expr PLUS expr */ //line 707 "pikchr.y" { yylhsminor.yy153 = yypParser.yystack[yypParser.yytos+-2].minor.yy153 + yypParser.yystack[yypParser.yytos+0].minor.yy153 } //line 2827 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 break case 99: /* expr ::= expr MINUS expr */ //line 708 "pikchr.y" { yylhsminor.yy153 = yypParser.yystack[yypParser.yytos+-2].minor.yy153 - yypParser.yystack[yypParser.yytos+0].minor.yy153 } //line 2833 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 break case 100: /* expr ::= expr STAR expr */ //line 709 "pikchr.y" { yylhsminor.yy153 = yypParser.yystack[yypParser.yytos+-2].minor.yy153 * yypParser.yystack[yypParser.yytos+0].minor.yy153 } //line 2839 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 break case 101: /* expr ::= expr SLASH expr */ //line 710 "pikchr.y" { if yypParser.yystack[yypParser.yytos+0].minor.yy153 == 0.0 { p.pik_error(&yypParser.yystack[yypParser.yytos+-1].minor.yy0, "division by zero") yylhsminor.yy153 = 0.0 } else { yylhsminor.yy153 = yypParser.yystack[yypParser.yytos+-2].minor.yy153 / yypParser.yystack[yypParser.yytos+0].minor.yy153 } } //line 2847 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 break case 102: /* expr ::= MINUS expr */ //line 713 "pikchr.y" { yypParser.yystack[yypParser.yytos+-1].minor.yy153 = -yypParser.yystack[yypParser.yytos+0].minor.yy153 } //line 2853 "pikchr.go" break case 103: /* expr ::= PLUS expr */ //line 714 "pikchr.y" { yypParser.yystack[yypParser.yytos+-1].minor.yy153 = yypParser.yystack[yypParser.yytos+0].minor.yy153 } //line 2858 "pikchr.go" break case 104: /* expr ::= LP expr RP */ //line 715 "pikchr.y" { yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yypParser.yystack[yypParser.yytos+-1].minor.yy153 } //line 2863 "pikchr.go" break case 105: /* expr ::= LP FILL|COLOR|THICKNESS RP */ //line 716 "pikchr.y" { yypParser.yystack[yypParser.yytos+-2].minor.yy153 = p.pik_get_var(&yypParser.yystack[yypParser.yytos+-1].minor.yy0) } //line 2868 "pikchr.go" break case 106: /* expr ::= NUMBER */ //line 717 "pikchr.y" { yylhsminor.yy153 = pik_atof(&yypParser.yystack[yypParser.yytos+0].minor.yy0) } //line 2873 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy153 = yylhsminor.yy153 break case 107: /* expr ::= ID */ //line 718 "pikchr.y" { yylhsminor.yy153 = p.pik_get_var(&yypParser.yystack[yypParser.yytos+0].minor.yy0) } //line 2879 "pikchr.go" yypParser.yystack[yypParser.yytos+0].minor.yy153 = yylhsminor.yy153 break case 108: /* expr ::= FUNC1 LP expr RP */ //line 719 "pikchr.y" { yylhsminor.yy153 = p.pik_func(&yypParser.yystack[yypParser.yytos+-3].minor.yy0, yypParser.yystack[yypParser.yytos+-1].minor.yy153, 0.0) } //line 2885 "pikchr.go" yypParser.yystack[yypParser.yytos+-3].minor.yy153 = yylhsminor.yy153 break case 109: /* expr ::= FUNC2 LP expr COMMA expr RP */ //line 720 "pikchr.y" { yylhsminor.yy153 = p.pik_func(&yypParser.yystack[yypParser.yytos+-5].minor.yy0, yypParser.yystack[yypParser.yytos+-3].minor.yy153, yypParser.yystack[yypParser.yytos+-1].minor.yy153) } //line 2891 "pikchr.go" yypParser.yystack[yypParser.yytos+-5].minor.yy153 = yylhsminor.yy153 break case 110: /* expr ::= DIST LP position COMMA position RP */ //line 721 "pikchr.y" { yypParser.yystack[yypParser.yytos+-5].minor.yy153 = pik_dist(&yypParser.yystack[yypParser.yytos+-3].minor.yy79, &yypParser.yystack[yypParser.yytos+-1].minor.yy79) } //line 2897 "pikchr.go" break case 111: /* expr ::= place2 DOT_XY X */ //line 722 "pikchr.y" { yylhsminor.yy153 = yypParser.yystack[yypParser.yytos+-2].minor.yy79.x } //line 2902 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 break case 112: /* expr ::= place2 DOT_XY Y */ //line 723 "pikchr.y" { yylhsminor.yy153 = yypParser.yystack[yypParser.yytos+-2].minor.yy79.y } //line 2908 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 break case 113: /* expr ::= object DOT_L numproperty */ fallthrough case 114: /* expr ::= object DOT_L dashproperty */ yytestcase(yyruleno == 114) fallthrough case 115: /* expr ::= object DOT_L colorproperty */ yytestcase(yyruleno == 115) //line 724 "pikchr.y" { yylhsminor.yy153 = pik_property_of(yypParser.yystack[yypParser.yytos+-2].minor.yy104, &yypParser.yystack[yypParser.yytos+0].minor.yy0) } //line 2918 "pikchr.go" yypParser.yystack[yypParser.yytos+-2].minor.yy153 = yylhsminor.yy153 break default: /* (116) lvalue ::= ID */ yytestcase(yyruleno == 116) /* (117) lvalue ::= FILL */ yytestcase(yyruleno == 117) /* (118) lvalue ::= COLOR */ yytestcase(yyruleno == 118) /* (119) lvalue ::= THICKNESS */ yytestcase(yyruleno == 119) /* (120) rvalue ::= expr */ yytestcase(yyruleno == 120) /* (121) print ::= PRINT */ yytestcase(yyruleno == 121) /* (122) prlist ::= pritem (OPTIMIZED OUT) */ assert(yyruleno != 122, "yyruleno!=122") /* (123) prlist ::= prlist prsep pritem */ yytestcase(yyruleno == 123) /* (124) direction ::= UP */ yytestcase(yyruleno == 124) /* (125) direction ::= DOWN */ yytestcase(yyruleno == 125) /* (126) direction ::= LEFT */ yytestcase(yyruleno == 126) /* (127) direction ::= RIGHT */ yytestcase(yyruleno == 127) /* (128) optrelexpr ::= relexpr (OPTIMIZED OUT) */ assert(yyruleno != 128, "yyruleno!=128") /* (129) attribute_list ::= alist */ yytestcase(yyruleno == 129) /* (130) alist ::= */ yytestcase(yyruleno == 130) /* (131) alist ::= alist attribute */ yytestcase(yyruleno == 131) /* (132) attribute ::= boolproperty (OPTIMIZED OUT) */ assert(yyruleno != 132, "yyruleno!=132") /* (133) attribute ::= WITH withclause */ yytestcase(yyruleno == 133) /* (134) go ::= GO */ yytestcase(yyruleno == 134) /* (135) go ::= */ yytestcase(yyruleno == 135) /* (136) even ::= UNTIL EVEN WITH */ yytestcase(yyruleno == 136) /* (137) even ::= EVEN WITH */ yytestcase(yyruleno == 137) /* (138) dashproperty ::= DOTTED */ yytestcase(yyruleno == 138) /* (139) dashproperty ::= DASHED */ yytestcase(yyruleno == 139) /* (140) colorproperty ::= FILL */ yytestcase(yyruleno == 140) /* (141) colorproperty ::= COLOR */ yytestcase(yyruleno == 141) /* (142) position ::= place */ yytestcase(yyruleno == 142) /* (143) between ::= WAY BETWEEN */ yytestcase(yyruleno == 143) /* (144) between ::= BETWEEN */ yytestcase(yyruleno == 144) /* (145) between ::= OF THE WAY BETWEEN */ yytestcase(yyruleno == 145) /* (146) place ::= place2 */ yytestcase(yyruleno == 146) /* (147) edge ::= CENTER */ yytestcase(yyruleno == 147) /* (148) edge ::= EDGEPT */ yytestcase(yyruleno == 148) /* (149) edge ::= TOP */ yytestcase(yyruleno == 149) /* (150) edge ::= BOTTOM */ yytestcase(yyruleno == 150) /* (151) edge ::= START */ yytestcase(yyruleno == 151) /* (152) edge ::= END */ yytestcase(yyruleno == 152) /* (153) edge ::= RIGHT */ yytestcase(yyruleno == 153) /* (154) edge ::= LEFT */ yytestcase(yyruleno == 154) /* (155) object ::= objectname */ yytestcase(yyruleno == 155) break /********** End reduce actions ************************************************/ } assert(int(yyruleno) < len(yyRuleInfoLhs), "yyruleno < len(yyRuleInfoLhs)") yygoto = yyRuleInfoLhs[yyruleno] yysize = int(yyRuleInfoNRhs[yyruleno]) yyact = yy_find_reduce_action(yypParser.yystack[yymsp+yysize].stateno, yygoto) /* There are no SHIFTREDUCE actions on nonterminals because the table ** generator has simplified them to pure REDUCE actions. */ assert(!(yyact > YY_MAX_SHIFT && yyact <= YY_MAX_SHIFTREDUCE), "!(yyact > YY_MAX_SHIFT && yyact <= YY_MAX_SHIFTREDUCE)") /* It is not possible for a REDUCE to be followed by an error */ assert(yyact != YY_ERROR_ACTION, "yyact != YY_ERROR_ACTION") yymsp += yysize + 1 yypParser.yytos = yymsp yypParser.yystack[yymsp].stateno = yyact yypParser.yystack[yymsp].major = yygoto yypParser.yyTraceShift(int(yyact), "... then shift") return yyact } /* ** The following code executes when the parse fails */ func (yypParser *yyParser) yy_parse_failed() { p := yypParser.p _ = p if !NDEBUG { if yyTraceFILE != nil { fmt.Fprintf(yyTraceFILE, "%sFail!\n", yyTracePrompt) } } for yypParser.yytos > 0 { yypParser.yy_pop_parser_stack() } /* Here code is inserted which will be executed whenever the ** parser fails */ /************ Begin %parse_failure code ***************************************/ /************ End %parse_failure code *****************************************/ /* Suppress warning about unused %extra_argument variable */ yypParser.p = p } /* ** The following code executes when a syntax error first occurs. */ func (yypParser *yyParser) yy_syntax_error( yymajor YYCODETYPE, /* The major type of the error token */ yyminor pik_parserTOKENTYPE, /* The minor type of the error token */ ) { p := yypParser.p _ = p TOKEN := yyminor _ = TOKEN /************ Begin %syntax_error code ****************************************/ //line 486 "pikchr.y" if TOKEN.z != nil && TOKEN.z[0] != 0 { p.pik_error(&TOKEN, "syntax error") } else { p.pik_error(nil, "syntax error") } //line 3031 "pikchr.go" /************ End %syntax_error code ******************************************/ /* Suppress warning about unused %extra_argument variable */ yypParser.p = p } /* ** The following is executed when the parser accepts */ func (yypParser *yyParser) yy_accept() { p := yypParser.p _ = p if !NDEBUG { if yyTraceFILE != nil { fmt.Fprintf(yyTraceFILE, "%sAccept!\n", yyTracePrompt) } } if !YYNOERRORRECOVERY { yypParser.yyerrcnt = -1 } assert(yypParser.yytos == 0, fmt.Sprintf("want yypParser.yytos == 0; got %d", yypParser.yytos)) /* Here code is inserted which will be executed whenever the ** parser accepts */ /*********** Begin %parse_accept code *****************************************/ /*********** End %parse_accept code *******************************************/ /* Suppress warning about unused %extra_argument variable */ yypParser.p = p } /* The main parser program. ** The first argument is a pointer to a structure obtained from ** "pik_parserAlloc" which describes the current state of the parser. ** The second argument is the major token number. The third is ** the minor token. The fourth optional argument is whatever the ** user wants (and specified in the grammar) and is available for ** use by the action routines. ** ** Inputs: ** <ul> ** <li> A pointer to the parser (an opaque structure.) ** <li> The major token number. ** <li> The minor token number. ** <li> An option argument of a grammar-specified type. ** </ul> ** ** Outputs: ** None. */ func (yypParser *yyParser) pik_parser( yymajor YYCODETYPE, /* The major token code number */ yyminor pik_parserTOKENTYPE, /* The value for the token */ /* Optional %extra_argument parameter */ ) { var ( yyminorunion YYMINORTYPE yyact YYACTIONTYPE /* The parser action. */ yyendofinput bool /* True if we are at the end of input */ yyerrorhit bool /* True if yymajor has invoked an error */ ) p := yypParser.p _ = p assert(yypParser.yystack != nil, "yypParser.yystack != nil") if YYERRORSYMBOL == 0 && !YYNOERRORRECOVERY { yyendofinput = (yymajor == 0) } yyact = yypParser.yystack[yypParser.yytos].stateno if !NDEBUG { if yyTraceFILE != nil { if yyact < YY_MIN_REDUCE { fmt.Fprintf(yyTraceFILE, "%sInput '%s' in state %d\n", yyTracePrompt, yyTokenName[yymajor], yyact) } else { fmt.Fprintf(yyTraceFILE, "%sInput '%s' with pending reduce %d\n", yyTracePrompt, yyTokenName[yymajor], yyact-YY_MIN_REDUCE) } } } for { /* Exit by "break" */ assert(yypParser.yytos >= 0, "yypParser.yytos >= 0") assert(yyact == yypParser.yystack[yypParser.yytos].stateno, "yyact == yypParser.yystack[yypParser.yytos].stateno") yyact = yy_find_shift_action(yymajor, yyact) if yyact >= YY_MIN_REDUCE { yyruleno := yyact - YY_MIN_REDUCE /* Reduce by this rule */ if !NDEBUG { assert(int(yyruleno) < len(yyRuleName), "int(yyruleno) < len(yyRuleName)") if yyTraceFILE != nil { yysize := yyRuleInfoNRhs[yyruleno] wea := " without external action" if yyruleno < YYNRULE_WITH_ACTION { wea = "" } if yysize != 0 { fmt.Fprintf(yyTraceFILE, "%sReduce %d [%s]%s, pop back to state %d.\n", yyTracePrompt, yyruleno, yyRuleName[yyruleno], wea, yypParser.yystack[yypParser.yytos+int(yysize)].stateno) } else { fmt.Fprintf(yyTraceFILE, "%sReduce %d [%s]%s.\n", yyTracePrompt, yyruleno, yyRuleName[yyruleno], wea) } } } /* NDEBUG */ /* Check that the stack is large enough to grow by a single entry ** if the RHS of the rule is empty. This ensures that there is room ** enough on the stack to push the LHS value */ if yyRuleInfoNRhs[yyruleno] == 0 { if YYTRACKMAXSTACKDEPTH { if yypParser.yytos > yypParser.yyhwm { yypParser.yyhwm++ assert(yypParser.yyhwm == yypParser.yytos, "yypParser.yyhwm == yypParser.yytos") } } if YYSTACKDEPTH > 0 { if yypParser.yytos >= YYSTACKDEPTH-1 { yypParser.yyStackOverflow() break } } else { if yypParser.yytos+1 >= len(yypParser.yystack)-1 { yypParser.yyGrowStack() } } } yyact = yypParser.yy_reduce(yyruleno, yymajor, yyminor, p) } else if yyact <= YY_MAX_SHIFTREDUCE { yypParser.yy_shift(yyact, yymajor, yyminor) if !YYNOERRORRECOVERY { yypParser.yyerrcnt-- } break } else if yyact == YY_ACCEPT_ACTION { yypParser.yytos-- yypParser.yy_accept() return } else { assert(yyact == YY_ERROR_ACTION, "yyact == YY_ERROR_ACTION") yyminorunion.yy0 = yyminor if !NDEBUG { if yyTraceFILE != nil { fmt.Fprintf(yyTraceFILE, "%sSyntax Error!\n", yyTracePrompt) } } if YYERRORSYMBOL > 0 { /* A syntax error has occurred. ** The response to an error depends upon whether or not the ** grammar defines an error token "ERROR". ** ** This is what we do if the grammar does define ERROR: ** ** * Call the %syntax_error function. ** ** * Begin popping the stack until we enter a state where ** it is legal to shift the error symbol, then shift ** the error symbol. ** ** * Set the error count to three. ** ** * Begin accepting and shifting new tokens. No new error ** processing will occur until three tokens have been ** shifted successfully. ** */ if yypParser.yyerrcnt < 0 { yypParser.yy_syntax_error(yymajor, yyminor) } yymx := yypParser.yystack[yypParser.yytos].major if int(yymx) == YYERRORSYMBOL || yyerrorhit { if !NDEBUG { if yyTraceFILE != nil { fmt.Fprintf(yyTraceFILE, "%sDiscard input token %s\n", yyTracePrompt, yyTokenName[yymajor]) } } yypParser.yy_destructor(yymajor, &yyminorunion) yymajor = YYNOCODE } else { for yypParser.yytos > 0 { yyact = yy_find_reduce_action(yypParser.yystack[yypParser.yytos].stateno, YYERRORSYMBOL) if yyact <= YY_MAX_SHIFTREDUCE { break } yypParser.yy_pop_parser_stack() } if yypParser.yytos <= 0 || yymajor == 0 { yypParser.yy_destructor(yymajor, &yyminorunion) yypParser.yy_parse_failed() if !YYNOERRORRECOVERY { yypParser.yyerrcnt = -1 } yymajor = YYNOCODE } else if yymx != YYERRORSYMBOL { yypParser.yy_shift(yyact, YYERRORSYMBOL, yyminor) } } yypParser.yyerrcnt = 3 yyerrorhit = true if yymajor == YYNOCODE { break } yyact = yypParser.yystack[yypParser.yytos].stateno } else if YYNOERRORRECOVERY { /* If the YYNOERRORRECOVERY macro is defined, then do not attempt to ** do any kind of error recovery. Instead, simply invoke the syntax ** error routine and continue going as if nothing had happened. ** ** Applications can set this macro (for example inside %include) if ** they intend to abandon the parse upon the first syntax error seen. */ yypParser.yy_syntax_error(yymajor, yyminor) yypParser.yy_destructor(yymajor, &yyminorunion) break } else { /* YYERRORSYMBOL is not defined */ /* This is what we do if the grammar does not define ERROR: ** ** * Report an error message, and throw away the input token. ** ** * If the input token is $, then fail the parse. ** ** As before, subsequent error messages are suppressed until ** three input tokens have been successfully shifted. */ if yypParser.yyerrcnt <= 0 { yypParser.yy_syntax_error(yymajor, yyminor) } yypParser.yyerrcnt = 3 yypParser.yy_destructor(yymajor, &yyminorunion) if yyendofinput { yypParser.yy_parse_failed() if !YYNOERRORRECOVERY { yypParser.yyerrcnt = -1 } } break } } } if !NDEBUG { if yyTraceFILE != nil { cDiv := '[' fmt.Fprintf(yyTraceFILE, "%sReturn. Stack=", yyTracePrompt) for _, i := range yypParser.yystack[1 : yypParser.yytos+1] { fmt.Fprintf(yyTraceFILE, "%c%s", cDiv, yyTokenName[i.major]) cDiv = ' ' } fmt.Fprintf(yyTraceFILE, "]\n") } } return } /* ** Return the fallback token corresponding to canonical token iToken, or ** 0 if iToken has no fallback. */ func pik_parserFallback(iToken int) YYCODETYPE { if YYFALLBACK { assert(iToken < len(yyFallback), "iToken < len(yyFallback)") return yyFallback[iToken] } else { return 0 } } // assert is used in various places in the generated and template code // to check invariants. func assert(condition bool, message string) { if !condition { panic(message) } } //line 729 "pikchr.y" /* Chart of the 148 official CSS color names with their ** corresponding RGB values thru Color Module Level 4: ** https://developer.mozilla.org/en-US/docs/Web/CSS/color_value ** ** Two new names "None" and "Off" are added with a value ** of -1. */ var aColor = []struct { zName string /* Name of the color */ val int /* RGB value */ }{ {"AliceBlue", 0xf0f8ff}, {"AntiqueWhite", 0xfaebd7}, {"Aqua", 0x00ffff}, {"Aquamarine", 0x7fffd4}, {"Azure", 0xf0ffff}, {"Beige", 0xf5f5dc}, {"Bisque", 0xffe4c4}, {"Black", 0x000000}, {"BlanchedAlmond", 0xffebcd}, {"Blue", 0x0000ff}, {"BlueViolet", 0x8a2be2}, {"Brown", 0xa52a2a}, {"BurlyWood", 0xdeb887}, {"CadetBlue", 0x5f9ea0}, {"Chartreuse", 0x7fff00}, {"Chocolate", 0xd2691e}, {"Coral", 0xff7f50}, {"CornflowerBlue", 0x6495ed}, {"Cornsilk", 0xfff8dc}, {"Crimson", 0xdc143c}, {"Cyan", 0x00ffff}, {"DarkBlue", 0x00008b}, {"DarkCyan", 0x008b8b}, {"DarkGoldenrod", 0xb8860b}, {"DarkGray", 0xa9a9a9}, {"DarkGreen", 0x006400}, {"DarkGrey", 0xa9a9a9}, {"DarkKhaki", 0xbdb76b}, {"DarkMagenta", 0x8b008b}, {"DarkOliveGreen", 0x556b2f}, {"DarkOrange", 0xff8c00}, {"DarkOrchid", 0x9932cc}, {"DarkRed", 0x8b0000}, {"DarkSalmon", 0xe9967a}, {"DarkSeaGreen", 0x8fbc8f}, {"DarkSlateBlue", 0x483d8b}, {"DarkSlateGray", 0x2f4f4f}, {"DarkSlateGrey", 0x2f4f4f}, {"DarkTurquoise", 0x00ced1}, {"DarkViolet", 0x9400d3}, {"DeepPink", 0xff1493}, {"DeepSkyBlue", 0x00bfff}, {"DimGray", 0x696969}, {"DimGrey", 0x696969}, {"DodgerBlue", 0x1e90ff}, {"Firebrick", 0xb22222}, {"FloralWhite", 0xfffaf0}, {"ForestGreen", 0x228b22}, {"Fuchsia", 0xff00ff}, {"Gainsboro", 0xdcdcdc}, {"GhostWhite", 0xf8f8ff}, {"Gold", 0xffd700}, {"Goldenrod", 0xdaa520}, {"Gray", 0x808080}, {"Green", 0x008000}, {"GreenYellow", 0xadff2f}, {"Grey", 0x808080}, {"Honeydew", 0xf0fff0}, {"HotPink", 0xff69b4}, {"IndianRed", 0xcd5c5c}, {"Indigo", 0x4b0082}, {"Ivory", 0xfffff0}, {"Khaki", 0xf0e68c}, {"Lavender", 0xe6e6fa}, {"LavenderBlush", 0xfff0f5}, {"LawnGreen", 0x7cfc00}, {"LemonChiffon", 0xfffacd}, {"LightBlue", 0xadd8e6}, {"LightCoral", 0xf08080}, {"LightCyan", 0xe0ffff}, {"LightGoldenrodYellow", 0xfafad2}, {"LightGray", 0xd3d3d3}, {"LightGreen", 0x90ee90}, {"LightGrey", 0xd3d3d3}, {"LightPink", 0xffb6c1}, {"LightSalmon", 0xffa07a}, {"LightSeaGreen", 0x20b2aa}, {"LightSkyBlue", 0x87cefa}, {"LightSlateGray", 0x778899}, {"LightSlateGrey", 0x778899}, {"LightSteelBlue", 0xb0c4de}, {"LightYellow", 0xffffe0}, {"Lime", 0x00ff00}, {"LimeGreen", 0x32cd32}, {"Linen", 0xfaf0e6}, {"Magenta", 0xff00ff}, {"Maroon", 0x800000}, {"MediumAquamarine", 0x66cdaa}, {"MediumBlue", 0x0000cd}, {"MediumOrchid", 0xba55d3}, {"MediumPurple", 0x9370db}, {"MediumSeaGreen", 0x3cb371}, {"MediumSlateBlue", 0x7b68ee}, {"MediumSpringGreen", 0x00fa9a}, {"MediumTurquoise", 0x48d1cc}, {"MediumVioletRed", 0xc71585}, {"MidnightBlue", 0x191970}, {"MintCream", 0xf5fffa}, {"MistyRose", 0xffe4e1}, {"Moccasin", 0xffe4b5}, {"NavajoWhite", 0xffdead}, {"Navy", 0x000080}, {"None", -1}, /* Non-standard addition */ {"Off", -1}, /* Non-standard addition */ {"OldLace", 0xfdf5e6}, {"Olive", 0x808000}, {"OliveDrab", 0x6b8e23}, {"Orange", 0xffa500}, {"OrangeRed", 0xff4500}, {"Orchid", 0xda70d6}, {"PaleGoldenrod", 0xeee8aa}, {"PaleGreen", 0x98fb98}, {"PaleTurquoise", 0xafeeee}, {"PaleVioletRed", 0xdb7093}, {"PapayaWhip", 0xffefd5}, {"PeachPuff", 0xffdab9}, {"Peru", 0xcd853f}, {"Pink", 0xffc0cb}, {"Plum", 0xdda0dd}, {"PowderBlue", 0xb0e0e6}, {"Purple", 0x800080}, {"RebeccaPurple", 0x663399}, {"Red", 0xff0000}, {"RosyBrown", 0xbc8f8f}, {"RoyalBlue", 0x4169e1}, {"SaddleBrown", 0x8b4513}, {"Salmon", 0xfa8072}, {"SandyBrown", 0xf4a460}, {"SeaGreen", 0x2e8b57}, {"Seashell", 0xfff5ee}, {"Sienna", 0xa0522d}, {"Silver", 0xc0c0c0}, {"SkyBlue", 0x87ceeb}, {"SlateBlue", 0x6a5acd}, {"SlateGray", 0x708090}, {"SlateGrey", 0x708090}, {"Snow", 0xfffafa}, {"SpringGreen", 0x00ff7f}, {"SteelBlue", 0x4682b4}, {"Tan", 0xd2b48c}, {"Teal", 0x008080}, {"Thistle", 0xd8bfd8}, {"Tomato", 0xff6347}, {"Turquoise", 0x40e0d0}, {"Violet", 0xee82ee}, {"Wheat", 0xf5deb3}, {"White", 0xffffff}, {"WhiteSmoke", 0xf5f5f5}, {"Yellow", 0xffff00}, {"YellowGreen", 0x9acd32}, } /* Built-in variable names. ** ** This array is constant. When a script changes the value of one of ** these built-ins, a new PVar record is added at the head of ** the Pik.pVar list, which is searched first. Thus the new PVar entry ** will override this default value. ** ** Units are in inches, except for "color" and "fill" which are ** interpreted as 24-bit RGB values. ** ** Binary search used. Must be kept in sorted order. */ var aBuiltin = []struct { zName string val PNum }{ {"arcrad", 0.25}, {"arrowhead", 2.0}, {"arrowht", 0.08}, {"arrowwid", 0.06}, {"boxht", 0.5}, {"boxrad", 0.0}, {"boxwid", 0.75}, {"charht", 0.14}, {"charwid", 0.08}, {"circlerad", 0.25}, {"color", 0.0}, {"cylht", 0.5}, {"cylrad", 0.075}, {"cylwid", 0.75}, {"dashwid", 0.05}, {"dotrad", 0.015}, {"ellipseht", 0.5}, {"ellipsewid", 0.75}, {"fileht", 0.75}, {"filerad", 0.15}, {"filewid", 0.5}, {"fill", -1.0}, {"lineht", 0.5}, {"linewid", 0.5}, {"movewid", 0.5}, {"ovalht", 0.5}, {"ovalwid", 1.0}, {"scale", 1.0}, {"textht", 0.5}, {"textwid", 0.75}, {"thickness", 0.015}, } /* Methods for the "arc" class */ func arcInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("arcrad", nil) pObj.h = pObj.w } /* Hack: Arcs are here rendered as quadratic Bezier curves rather ** than true arcs. Multiple reasons: (1) the legacy-PIC parameters ** that control arcs are obscure and I could not figure out what they ** mean based on available documentation. (2) Arcs are rarely used, ** and so do not seem that important. */ func arcControlPoint(cw bool, f PPoint, t PPoint, rScale PNum) PPoint { var m PPoint var dx, dy PNum m.x = 0.5 * (f.x + t.x) m.y = 0.5 * (f.y + t.y) dx = t.x - f.x dy = t.y - f.y if cw { m.x -= 0.5 * rScale * dy m.y += 0.5 * rScale * dx } else { m.x += 0.5 * rScale * dy m.y -= 0.5 * rScale * dx } return m } func arcCheck(p *Pik, pObj *PObj) { if p.nTPath > 2 { p.pik_error(&pObj.errTok, "arc geometry error") return } m := arcControlPoint(pObj.cw, p.aTPath[0], p.aTPath[1], 0.5) pik_bbox_add_xy(&pObj.bbox, m.x, m.y) } func arcRender(p *Pik, pObj *PObj) { if pObj.nPath < 2 { return } if pObj.sw <= 0.0 { return } f := pObj.aPath[0] t := pObj.aPath[1] m := arcControlPoint(pObj.cw, f, t, 1.0) if pObj.larrow { p.pik_draw_arrowhead(&m, &f, pObj) } if pObj.rarrow { p.pik_draw_arrowhead(&m, &t, pObj) } p.pik_append_xy("<path d=\"M", f.x, f.y) p.pik_append_xy("Q", m.x, m.y) p.pik_append_xy(" ", t.x, t.y) p.pik_append("\" ") p.pik_append_style(pObj, 0) p.pik_append("\" />\n") p.pik_append_txt(pObj, nil) } /* Methods for the "arrow" class */ func arrowInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("linewid", nil) pObj.h = p.pik_value("lineht", nil) pObj.rad = p.pik_value("linerad", nil) pObj.rarrow = true } /* Methods for the "box" class */ func boxInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("boxwid", nil) pObj.h = p.pik_value("boxht", nil) pObj.rad = p.pik_value("boxrad", nil) } /* Return offset from the center of the box to the compass point ** given by parameter cp */ func boxOffset(p *Pik, pObj *PObj, cp uint8) PPoint { pt := PPoint{} var w2 PNum = 0.5 * pObj.w var h2 PNum = 0.5 * pObj.h var rad PNum = pObj.rad var rx PNum if rad <= 0.0 { rx = 0.0 } else { if rad > w2 { rad = w2 } if rad > h2 { rad = h2 } rx = 0.29289321881345252392 * rad } switch cp { case CP_C: case CP_N: pt.x = 0.0 pt.y = h2 case CP_NE: pt.x = w2 - rx pt.y = h2 - rx case CP_E: pt.x = w2 pt.y = 0.0 case CP_SE: pt.x = w2 - rx pt.y = rx - h2 case CP_S: pt.x = 0.0 pt.y = -h2 case CP_SW: pt.x = rx - w2 pt.y = rx - h2 case CP_W: pt.x = -w2 pt.y = 0.0 case CP_NW: pt.x = rx - w2 pt.y = h2 - rx default: assert(false, "false") } return pt } func boxChop(p *Pik, pObj *PObj, pPt *PPoint) PPoint { var dx, dy PNum cp := CP_C chop := pObj.ptAt if pObj.w <= 0.0 { return chop } if pObj.h <= 0.0 { return chop } dx = (pPt.x - pObj.ptAt.x) * pObj.h / pObj.w dy = (pPt.y - pObj.ptAt.y) if dx > 0.0 { if dy >= 2.414*dx { cp = CP_N } else if dy >= 0.414*dx { cp = CP_NE } else if dy >= -0.414*dx { cp = CP_E } else if dy > -2.414*dx { cp = CP_SE } else { cp = CP_S } } else { if dy >= -2.414*dx { cp = CP_N } else if dy >= -0.414*dx { cp = CP_NW } else if dy >= 0.414*dx { cp = CP_W } else if dy > 2.414*dx { cp = CP_SW } else { cp = CP_S } } chop = pObj.typ.xOffset(p, pObj, cp) chop.x += pObj.ptAt.x chop.y += pObj.ptAt.y return chop } func boxFit(p *Pik, pObj *PObj, w PNum, h PNum) { if w > 0 { pObj.w = w } if h > 0 { pObj.h = h } } func boxRender(p *Pik, pObj *PObj) { var w2 PNum = 0.5 * pObj.w var h2 PNum = 0.5 * pObj.h rad := pObj.rad pt := pObj.ptAt if pObj.sw > 0.0 { if rad <= 0.0 { p.pik_append_xy("<path d=\"M", pt.x-w2, pt.y-h2) p.pik_append_xy("L", pt.x+w2, pt.y-h2) p.pik_append_xy("L", pt.x+w2, pt.y+h2) p.pik_append_xy("L", pt.x-w2, pt.y+h2) p.pik_append("Z\" ") } else { /* ** ---- - y3 ** / \ ** / \ _ y2 ** | | ** | | _ y1 ** \ / ** \ / ** ---- _ y0 ** ** ' ' ' ' ** x0 x1 x2 x3 */ if rad > w2 { rad = w2 } if rad > h2 { rad = h2 } var x0 PNum = pt.x - w2 var x1 PNum = x0 + rad var x3 PNum = pt.x + w2 var x2 PNum = x3 - rad var y0 PNum = pt.y - h2 var y1 PNum = y0 + rad var y3 PNum = pt.y + h2 var y2 PNum = y3 - rad p.pik_append_xy("<path d=\"M", x1, y0) if x2 > x1 { p.pik_append_xy("L", x2, y0) } p.pik_append_arc(rad, rad, x3, y1) if y2 > y1 { p.pik_append_xy("L", x3, y2) } p.pik_append_arc(rad, rad, x2, y3) if x2 > x1 { p.pik_append_xy("L", x1, y3) } p.pik_append_arc(rad, rad, x0, y2) if y2 > y1 { p.pik_append_xy("L", x0, y1) } p.pik_append_arc(rad, rad, x1, y0) p.pik_append("Z\" ") } p.pik_append_style(pObj, 3) p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } /* Methods for the "circle" class */ func circleInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("circlerad", nil) * 2 pObj.h = pObj.w pObj.rad = 0.5 * pObj.w } func circleNumProp(p *Pik, pObj *PObj, pId *PToken) { /* For a circle, the width must equal the height and both must ** be twice the radius. Enforce those constraints. */ switch pId.eType { case T_DIAMETER, T_RADIUS: pObj.w = 2.0 * pObj.rad pObj.h = 2.0 * pObj.rad case T_WIDTH: pObj.h = pObj.w pObj.rad = 0.5 * pObj.w case T_HEIGHT: pObj.w = pObj.h pObj.rad = 0.5 * pObj.w } } func circleChop(p *Pik, pObj *PObj, pPt *PPoint) PPoint { var chop PPoint var dx PNum = pPt.x - pObj.ptAt.x var dy PNum = pPt.y - pObj.ptAt.y var dist PNum = math.Hypot(dx, dy) if dist < pObj.rad || dist <= 0 { return pObj.ptAt } chop.x = pObj.ptAt.x + dx*pObj.rad/dist chop.y = pObj.ptAt.y + dy*pObj.rad/dist return chop } func circleFit(p *Pik, pObj *PObj, w PNum, h PNum) { var mx PNum = 0.0 if w > 0 { mx = w } if h > mx { mx = h } if w*h > 0 && (w*w+h*h) > mx*mx { mx = math.Hypot(w, h) } if mx > 0.0 { pObj.rad = 0.5 * mx pObj.w = mx pObj.h = mx } } func circleRender(p *Pik, pObj *PObj) { r := pObj.rad pt := pObj.ptAt if pObj.sw > 0.0 { p.pik_append_x("<circle cx=\"", pt.x, "\"") p.pik_append_y(" cy=\"", pt.y, "\"") p.pik_append_dis(" r=\"", r, "\" ") p.pik_append_style(pObj, 3) p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } /* Methods for the "cylinder" class */ func cylinderInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("cylwid", nil) pObj.h = p.pik_value("cylht", nil) pObj.rad = p.pik_value("cylrad", nil) /* Minor radius of ellipses */ } func cylinderFit(p *Pik, pObj *PObj, w PNum, h PNum) { if w > 0 { pObj.w = w } if h > 0 { pObj.h = h + 0.25*pObj.rad + pObj.sw } } func cylinderRender(p *Pik, pObj *PObj) { var w2 PNum = 0.5 * pObj.w var h2 PNum = 0.5 * pObj.h rad := pObj.rad pt := pObj.ptAt if pObj.sw > 0.0 { if rad > h2 { rad = h2 } else if rad < 0 { rad = 0 } p.pik_append_xy("<path d=\"M", pt.x-w2, pt.y+h2-rad) p.pik_append_xy("L", pt.x-w2, pt.y-h2+rad) p.pik_append_arc(w2, rad, pt.x+w2, pt.y-h2+rad) p.pik_append_xy("L", pt.x+w2, pt.y+h2-rad) p.pik_append_arc(w2, rad, pt.x-w2, pt.y+h2-rad) p.pik_append_arc(w2, rad, pt.x+w2, pt.y+h2-rad) p.pik_append("\" ") p.pik_append_style(pObj, 3) p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } func cylinderOffset(p *Pik, pObj *PObj, cp uint8) PPoint { pt := PPoint{} var w2 PNum = pObj.w * 0.5 var h1 PNum = pObj.h * 0.5 var h2 PNum = h1 - pObj.rad switch cp { case CP_C: case CP_N: pt.x = 0.0 pt.y = h1 case CP_NE: pt.x = w2 pt.y = h2 case CP_E: pt.x = w2 pt.y = 0.0 case CP_SE: pt.x = w2 pt.y = -h2 case CP_S: pt.x = 0.0 pt.y = -h1 case CP_SW: pt.x = -w2 pt.y = -h2 case CP_W: pt.x = -w2 pt.y = 0.0 case CP_NW: pt.x = -w2 pt.y = h2 default: assert(false, "false") } return pt } /* Methods for the "dot" class */ func dotInit(p *Pik, pObj *PObj) { pObj.rad = p.pik_value("dotrad", nil) pObj.h = pObj.rad * 6 pObj.w = pObj.rad * 6 pObj.fill = pObj.color } func dotNumProp(p *Pik, pObj *PObj, pId *PToken) { switch pId.eType { case T_COLOR: pObj.fill = pObj.color case T_FILL: pObj.color = pObj.fill } } func dotCheck(p *Pik, pObj *PObj) { pObj.w = 0 pObj.h = 0 pik_bbox_addellipse(&pObj.bbox, pObj.ptAt.x, pObj.ptAt.y, pObj.rad, pObj.rad) } func dotOffset(p *Pik, pObj *PObj, cp uint8) PPoint { return PPoint{} } func dotRender(p *Pik, pObj *PObj) { r := pObj.rad pt := pObj.ptAt if pObj.sw > 0.0 { p.pik_append_x("<circle cx=\"", pt.x, "\"") p.pik_append_y(" cy=\"", pt.y, "\"") p.pik_append_dis(" r=\"", r, "\"") p.pik_append_style(pObj, 2) p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } /* Methods for the "ellipse" class */ func ellipseInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("ellipsewid", nil) pObj.h = p.pik_value("ellipseht", nil) } func ellipseChop(p *Pik, pObj *PObj, pPt *PPoint) PPoint { var chop PPoint var s, dq, dist PNum var dx PNum = pPt.x - pObj.ptAt.x var dy PNum = pPt.y - pObj.ptAt.y if pObj.w <= 0.0 { return pObj.ptAt } if pObj.h <= 0.0 { return pObj.ptAt } s = pObj.h / pObj.w dq = dx * s dist = math.Hypot(dq, dy) if dist < pObj.h { return pObj.ptAt } chop.x = pObj.ptAt.x + 0.5*dq*pObj.h/(dist*s) chop.y = pObj.ptAt.y + 0.5*dy*pObj.h/dist return chop } func ellipseOffset(p *Pik, pObj *PObj, cp uint8) PPoint { pt := PPoint{} var w PNum = pObj.w * 0.5 var w2 PNum = w * 0.70710678118654747608 var h PNum = pObj.h * 0.5 var h2 PNum = h * 0.70710678118654747608 switch cp { case CP_C: case CP_N: pt.x = 0.0 pt.y = h case CP_NE: pt.x = w2 pt.y = h2 case CP_E: pt.x = w pt.y = 0.0 case CP_SE: pt.x = w2 pt.y = -h2 case CP_S: pt.x = 0.0 pt.y = -h case CP_SW: pt.x = -w2 pt.y = -h2 case CP_W: pt.x = -w pt.y = 0.0 case CP_NW: pt.x = -w2 pt.y = h2 default: assert(false, "false") } return pt } func ellipseRender(p *Pik, pObj *PObj) { w := pObj.w h := pObj.h pt := pObj.ptAt if pObj.sw > 0.0 { p.pik_append_x("<ellipse cx=\"", pt.x, "\"") p.pik_append_y(" cy=\"", pt.y, "\"") p.pik_append_dis(" rx=\"", w/2.0, "\"") p.pik_append_dis(" ry=\"", h/2.0, "\" ") p.pik_append_style(pObj, 3) p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } /* Methods for the "file" object */ func fileInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("filewid", nil) pObj.h = p.pik_value("fileht", nil) pObj.rad = p.pik_value("filerad", nil) } /* Return offset from the center of the file to the compass point ** given by parameter cp */ func fileOffset(p *Pik, pObj *PObj, cp uint8) PPoint { pt := PPoint{} var w2 PNum = 0.5 * pObj.w var h2 PNum = 0.5 * pObj.h var rx PNum = pObj.rad mn := h2 if w2 < h2 { mn = w2 } if rx > mn { rx = mn } if rx < mn*0.25 { rx = mn * 0.25 } pt.x = 0.0 pt.y = 0.0 rx *= 0.5 switch cp { case CP_C: case CP_N: pt.x = 0.0 pt.y = h2 case CP_NE: pt.x = w2 - rx pt.y = h2 - rx case CP_E: pt.x = w2 pt.y = 0.0 case CP_SE: pt.x = w2 pt.y = -h2 case CP_S: pt.x = 0.0 pt.y = -h2 case CP_SW: pt.x = -w2 pt.y = -h2 case CP_W: pt.x = -w2 pt.y = 0.0 case CP_NW: pt.x = -w2 pt.y = h2 default: assert(false, "false") } return pt } func fileFit(p *Pik, pObj *PObj, w PNum, h PNum) { if w > 0 { pObj.w = w } if h > 0 { pObj.h = h + 2*pObj.rad } } func fileRender(p *Pik, pObj *PObj) { var w2 PNum = 0.5 * pObj.w var h2 PNum = 0.5 * pObj.h rad := pObj.rad pt := pObj.ptAt mn := h2 if w2 < h2 { mn = w2 } if rad > mn { rad = mn } if rad < mn*0.25 { rad = mn * 0.25 } if pObj.sw > 0.0 { p.pik_append_xy("<path d=\"M", pt.x-w2, pt.y-h2) p.pik_append_xy("L", pt.x+w2, pt.y-h2) p.pik_append_xy("L", pt.x+w2, pt.y+(h2-rad)) p.pik_append_xy("L", pt.x+(w2-rad), pt.y+h2) p.pik_append_xy("L", pt.x-w2, pt.y+h2) p.pik_append("Z\" ") p.pik_append_style(pObj, 1) p.pik_append("\" />\n") p.pik_append_xy("<path d=\"M", pt.x+(w2-rad), pt.y+h2) p.pik_append_xy("L", pt.x+(w2-rad), pt.y+(h2-rad)) p.pik_append_xy("L", pt.x+w2, pt.y+(h2-rad)) p.pik_append("\" ") p.pik_append_style(pObj, 0) p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } /* Methods for the "line" class */ func lineInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("linewid", nil) pObj.h = p.pik_value("lineht", nil) pObj.rad = p.pik_value("linerad", nil) } func lineOffset(p *Pik, pObj *PObj, cp uint8) PPoint { if false { // #if 0 /* In legacy PIC, the .center of an unclosed line is half way between ** its .start and .end. */ if cp == CP_C && !pObj.bClose { var out PPoint out.x = 0.5*(pObj.ptEnter.x+pObj.ptExit.x) - pObj.ptAt.x out.y = 0.5*(pObj.ptEnter.x+pObj.ptExit.y) - pObj.ptAt.y return out } } // #endif return boxOffset(p, pObj, cp) } func lineRender(p *Pik, pObj *PObj) { if pObj.sw > 0.0 { z := "<path d=\"M" n := pObj.nPath if pObj.larrow { p.pik_draw_arrowhead(&pObj.aPath[1], &pObj.aPath[0], pObj) } if pObj.rarrow { p.pik_draw_arrowhead(&pObj.aPath[n-2], &pObj.aPath[n-1], pObj) } for i := 0; i < pObj.nPath; i++ { p.pik_append_xy(z, pObj.aPath[i].x, pObj.aPath[i].y) z = "L" } if pObj.bClose { p.pik_append("Z") } else { pObj.fill = -1.0 } p.pik_append("\" ") if pObj.bClose { p.pik_append_style(pObj, 3) } else { p.pik_append_style(pObj, 0) } p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } /* Methods for the "move" class */ func moveInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("movewid", nil) pObj.h = pObj.w pObj.fill = -1.0 pObj.color = -1.0 pObj.sw = -1.0 } func moveRender(p *Pik, pObj *PObj) { /* No-op */ } /* Methods for the "oval" class */ func ovalInit(p *Pik, pObj *PObj) { pObj.h = p.pik_value("ovalht", nil) pObj.w = p.pik_value("ovalwid", nil) if pObj.h < pObj.w { pObj.rad = 0.5 * pObj.h } else { pObj.rad = 0.5 * pObj.w } } func ovalNumProp(p *Pik, pObj *PObj, pId *PToken) { /* Always adjust the radius to be half of the smaller of ** the width and height. */ if pObj.h < pObj.w { pObj.rad = 0.5 * pObj.h } else { pObj.rad = 0.5 * pObj.w } } func ovalFit(p *Pik, pObj *PObj, w PNum, h PNum) { if w > 0 { pObj.w = w } if h > 0 { pObj.h = h } if pObj.w < pObj.h { pObj.w = pObj.h } if pObj.h < pObj.w { pObj.rad = 0.5 * pObj.h } else { pObj.rad = 0.5 * pObj.w } } /* Methods for the "spline" class */ func splineInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("linewid", nil) pObj.h = p.pik_value("lineht", nil) pObj.rad = 1000 } /* Return a point along the path from "f" to "t" that is r units ** prior to reaching "t", except if the path is less than 2*r total, ** return the midpoint. */ func radiusMidpoint(f PPoint, t PPoint, r PNum, pbMid *bool) PPoint { var dx PNum = t.x - f.x var dy PNum = t.y - f.y var dist PNum = math.Hypot(dx, dy) if dist <= 0.0 { return t } dx /= dist dy /= dist if r > 0.5*dist { r = 0.5 * dist *pbMid = true } else { *pbMid = false } return PPoint{ x: t.x - r*dx, y: t.y - r*dy, } } func (p *Pik) radiusPath(pObj *PObj, r PNum) { n := pObj.nPath a := pObj.aPath an := a[n-1] isMid := false iLast := n - 1 if pObj.bClose { iLast = n } p.pik_append_xy("<path d=\"M", a[0].x, a[0].y) m := radiusMidpoint(a[0], a[1], r, &isMid) p.pik_append_xy(" L ", m.x, m.y) for i := 1; i < iLast; i++ { an = a[0] if i < n-1 { an = a[i+1] } m = radiusMidpoint(an, a[i], r, &isMid) p.pik_append_xy(" Q ", a[i].x, a[i].y) p.pik_append_xy(" ", m.x, m.y) if !isMid { m = radiusMidpoint(a[i], an, r, &isMid) p.pik_append_xy(" L ", m.x, m.y) } } p.pik_append_xy(" L ", an.x, an.y) if pObj.bClose { p.pik_append("Z") } else { pObj.fill = -1.0 } p.pik_append("\" ") if pObj.bClose { p.pik_append_style(pObj, 3) } else { p.pik_append_style(pObj, 0) } p.pik_append("\" />\n") } func splineRender(p *Pik, pObj *PObj) { if pObj.sw > 0.0 { n := pObj.nPath r := pObj.rad if n < 3 || r <= 0.0 { lineRender(p, pObj) return } if pObj.larrow { p.pik_draw_arrowhead(&pObj.aPath[1], &pObj.aPath[0], pObj) } if pObj.rarrow { p.pik_draw_arrowhead(&pObj.aPath[n-2], &pObj.aPath[n-1], pObj) } p.radiusPath(pObj, pObj.rad) } p.pik_append_txt(pObj, nil) } /* Methods for the "text" class */ func textInit(p *Pik, pObj *PObj) { p.pik_value("textwid", nil) p.pik_value("textht", nil) pObj.sw = 0.0 } func textOffset(p *Pik, pObj *PObj, cp uint8) PPoint { /* Automatically slim-down the width and height of text ** statements so that the bounding box tightly encloses the text, ** then get boxOffset() to do the offset computation. */ p.pik_size_to_fit(&pObj.errTok, 3) return boxOffset(p, pObj, cp) } /* Methods for the "sublist" class */ func sublistInit(p *Pik, pObj *PObj) { pList := pObj.pSublist pik_bbox_init(&pObj.bbox) for i := 0; i < len(pList); i++ { pik_bbox_addbox(&pObj.bbox, &pList[i].bbox) } pObj.w = pObj.bbox.ne.x - pObj.bbox.sw.x pObj.h = pObj.bbox.ne.y - pObj.bbox.sw.y pObj.ptAt.x = 0.5 * (pObj.bbox.ne.x + pObj.bbox.sw.x) pObj.ptAt.y = 0.5 * (pObj.bbox.ne.y + pObj.bbox.sw.y) pObj.mCalc |= A_WIDTH | A_HEIGHT | A_RADIUS } /* ** The following array holds all the different kinds of objects. ** The special [] object is separate. */ var aClass = []PClass{ { zName: "arc", isLine: true, eJust: 0, xInit: arcInit, xNumProp: nil, xCheck: arcCheck, xChop: nil, xOffset: boxOffset, xFit: nil, xRender: arcRender, }, { zName: "arrow", isLine: true, eJust: 0, xInit: arrowInit, xNumProp: nil, xCheck: nil, xChop: nil, xOffset: lineOffset, xFit: nil, xRender: splineRender, }, { zName: "box", isLine: false, eJust: 1, xInit: boxInit, xNumProp: nil, xCheck: nil, xChop: boxChop, xOffset: boxOffset, xFit: boxFit, xRender: boxRender, }, { zName: "circle", isLine: false, eJust: 0, xInit: circleInit, xNumProp: circleNumProp, xCheck: nil, xChop: circleChop, xOffset: ellipseOffset, xFit: circleFit, xRender: circleRender, }, { zName: "cylinder", isLine: false, eJust: 1, xInit: cylinderInit, xNumProp: nil, xCheck: nil, xChop: boxChop, xOffset: cylinderOffset, xFit: cylinderFit, xRender: cylinderRender, }, { zName: "dot", isLine: false, eJust: 0, xInit: dotInit, xNumProp: dotNumProp, xCheck: dotCheck, xChop: circleChop, xOffset: dotOffset, xFit: nil, xRender: dotRender, }, { zName: "ellipse", isLine: false, eJust: 0, xInit: ellipseInit, xNumProp: nil, xCheck: nil, xChop: ellipseChop, xOffset: ellipseOffset, xFit: boxFit, xRender: ellipseRender, }, { zName: "file", isLine: false, eJust: 1, xInit: fileInit, xNumProp: nil, xCheck: nil, xChop: boxChop, xOffset: fileOffset, xFit: fileFit, xRender: fileRender, }, { zName: "line", isLine: true, eJust: 0, xInit: lineInit, xNumProp: nil, xCheck: nil, xChop: nil, xOffset: lineOffset, xFit: nil, xRender: splineRender, }, { zName: "move", isLine: true, eJust: 0, xInit: moveInit, xNumProp: nil, xCheck: nil, xChop: nil, xOffset: boxOffset, xFit: nil, xRender: moveRender, }, { zName: "oval", isLine: false, eJust: 1, xInit: ovalInit, xNumProp: ovalNumProp, xCheck: nil, xChop: boxChop, xOffset: boxOffset, xFit: ovalFit, xRender: boxRender, }, { zName: "spline", isLine: true, eJust: 0, xInit: splineInit, xNumProp: nil, xCheck: nil, xChop: nil, xOffset: lineOffset, xFit: nil, xRender: splineRender, }, { zName: "text", isLine: false, eJust: 0, xInit: textInit, xNumProp: nil, xCheck: nil, xChop: boxChop, xOffset: textOffset, xFit: boxFit, xRender: boxRender, }, } var sublistClass = PClass{ zName: "[]", isLine: false, eJust: 0, xInit: sublistInit, xNumProp: nil, xCheck: nil, xChop: nil, xOffset: boxOffset, xFit: nil, xRender: nil, } var noopClass = PClass{ zName: "noop", isLine: false, eJust: 0, xInit: nil, xNumProp: nil, xCheck: nil, xChop: nil, xOffset: boxOffset, xFit: nil, xRender: nil, } /* ** Reduce the length of the line segment by amt (if possible) by ** modifying the location of *t. */ func pik_chop(f *PPoint, t *PPoint, amt PNum) { var dx PNum = t.x - f.x var dy PNum = t.y - f.y var dist PNum = math.Hypot(dx, dy) if dist <= amt { *t = *f return } var r PNum = 1.0 - amt/dist t.x = f.x + r*dx t.y = f.y + r*dy } /* ** Draw an arrowhead on the end of the line segment from pFrom to pTo. ** Also, shorten the line segment (by changing the value of pTo) so that ** the shaft of the arrow does not extend into the arrowhead. */ func (p *Pik) pik_draw_arrowhead(f *PPoint, t *PPoint, pObj *PObj) { var dx PNum = t.x - f.x var dy PNum = t.y - f.y var dist PNum = math.Hypot(dx, dy) var h PNum = p.hArrow * pObj.sw var w PNum = p.wArrow * pObj.sw if pObj.color < 0.0 { return } if pObj.sw <= 0.0 { return } if dist <= 0.0 { return } /* Unable */ dx /= dist dy /= dist var e1 PNum = dist - h if e1 < 0.0 { e1 = 0.0 h = dist } var ddx PNum = -w * dy var ddy PNum = w * dx var bx PNum = f.x + e1*dx var by PNum = f.y + e1*dy p.pik_append_xy("<polygon points=\"", t.x, t.y) p.pik_append_xy(" ", bx-ddx, by-ddy) p.pik_append_xy(" ", bx+ddx, by+ddy) p.pik_append_clr("\" style=\"fill:", pObj.color, "\"/>\n", false) pik_chop(f, t, h/2) } /* ** Compute the relative offset to an edge location from the reference for a ** an statement. */ func (p *Pik) pik_elem_offset(pObj *PObj, cp uint8) PPoint { return pObj.typ.xOffset(p, pObj, cp) } /* ** Append raw text to zOut */ func (p *Pik) pik_append(zText string) { p.zOut.WriteString(zText) } var re_entity = regexp.MustCompile(`&(#([0-9]{2,});)|([a-zA-Z][a-zA-Z0-9]+;)`) /* ** Append text to zOut with HTML characters escaped. ** ** * The space character is changed into non-breaking space (U+00a0) ** if mFlags has the 0x01 bit set. This is needed when outputting ** text to preserve leading and trailing whitespace. Turns out we ** cannot use as that is an HTML-ism and is not valid in XML. ** ** * The "&" character is changed into "&" if mFlags has the ** 0x02 bit set. This is needed when generating error message text. ** ** * Except for the above, only "<" and ">" are escaped. */ func (p *Pik) pik_append_text(zText string, mFlags int) { bQSpace := mFlags&1 > 0 bQAmp := mFlags&2 > 0 last := 0 var html string for i := 0; i < len(zText); i++ { switch zText[i] { case '<': html = "<" case '>': html = ">" case '&': if !bQAmp { continue } match := re_entity.FindStringSubmatch(zText[i:]) if len(match) > 0 { continue } html = "&" case ' ': if !bQSpace { continue } html = "\u00a0" default: continue } p.pik_append(zText[last:i]) p.pik_append(html) last = i + 1 } p.pik_append(zText[last:]) } /* ** Append error message text. This is either a raw append, or an append ** with HTML escapes, depending on whether the PIKCHR_PLAINTEXT_ERRORS flag ** is set. */ func (p *Pik) pik_append_errtxt(zText string) { if p.mFlags&PIKCHR_PLAINTEXT_ERRORS != 0 { p.pik_append(zText) } else { p.pik_append_text(zText, 0) } } /* Append a PNum value */ func (p *Pik) pik_append_num(z string, v PNum) { p.pik_append(z) p.pik_append(fmt.Sprintf("%.10g", v)) } /* Append a PPoint value (Used for debugging only) */ func (p *Pik) pik_append_point(z string, pPt *PPoint) { buf := fmt.Sprintf("%.10g,%.10g", pPt.x, pPt.y) p.pik_append(z) p.pik_append(buf) } /* ** Invert the RGB color so that it is appropriate for dark mode. ** Variable x hold the initial color. The color is intended for use ** as a background color if isBg is true, and as a foreground color ** if isBg is false. */ func pik_color_to_dark_mode(x int, isBg bool) int { x = 0xffffff - x r := (x >> 16) & 0xff g := (x >> 8) & 0xff b := x & 0xff mx := r if g > mx { mx = g } if b > mx { mx = b } mn := r if g < mn { mn = g } if b < mn { mn = b } r = mn + (mx - r) g = mn + (mx - g) b = mn + (mx - b) if isBg { if mx > 127 { r = (127 * r) / mx g = (127 * g) / mx b = (127 * b) / mx } } else { if mn < 128 && mx > mn { r = 127 + ((r-mn)*128)/(mx-mn) g = 127 + ((g-mn)*128)/(mx-mn) b = 127 + ((b-mn)*128)/(mx-mn) } } return r*0x10000 + g*0x100 + b } /* Append a PNum value surrounded by text. Do coordinate transformations ** on the value. */ func (p *Pik) pik_append_x(z1 string, v PNum, z2 string) { v -= p.bbox.sw.x p.pik_append(fmt.Sprintf("%s%d%s", z1, pik_round(p.rScale*v), z2)) } func (p *Pik) pik_append_y(z1 string, v PNum, z2 string) { v = p.bbox.ne.y - v p.pik_append(fmt.Sprintf("%s%d%s", z1, pik_round(p.rScale*v), z2)) } func (p *Pik) pik_append_xy(z1 string, x PNum, y PNum) { x = x - p.bbox.sw.x y = p.bbox.ne.y - y p.pik_append(fmt.Sprintf("%s%d,%d", z1, pik_round(p.rScale*x), pik_round(p.rScale*y))) } func (p *Pik) pik_append_dis(z1 string, v PNum, z2 string) { p.pik_append(fmt.Sprintf("%s%.6g%s", z1, p.rScale*v, z2)) } /* Append a color specification to the output. ** ** In PIKCHR_DARK_MODE, the color is inverted. The "bg" flags indicates that ** the color is intended for use as a background color if true, or as a ** foreground color if false. The distinction only matters for color ** inversions in PIKCHR_DARK_MODE. */ func (p *Pik) pik_append_clr(z1 string, v PNum, z2 string, bg bool) { x := pik_round(v) if x == 0 && p.fgcolor > 0 && !bg { x = p.fgcolor } else if bg && x >= 0xffffff && p.bgcolor > 0 { x = p.bgcolor } else if p.mFlags&PIKCHR_DARK_MODE != 0 { x = pik_color_to_dark_mode(x, bg) } r := (x >> 16) & 0xff g := (x >> 8) & 0xff b := x & 0xff buf := fmt.Sprintf("%srgb(%d,%d,%d)%s", z1, r, g, b, z2) p.pik_append(buf) } /* Append an SVG path A record: ** ** A r1 r2 0 0 0 x y */ func (p *Pik) pik_append_arc(r1 PNum, r2 PNum, x PNum, y PNum) { x = x - p.bbox.sw.x y = p.bbox.ne.y - y buf := fmt.Sprintf("A%d %d 0 0 0 %d %d", pik_round(p.rScale*r1), pik_round(p.rScale*r2), pik_round(p.rScale*x), pik_round(p.rScale*y)) p.pik_append(buf) } /* Append a style="..." text. But, leave the quote unterminated, in case ** the caller wants to add some more. ** ** eFill is non-zero to fill in the background, or 0 if no fill should ** occur. Non-zero values of eFill determine the "bg" flag to pik_append_clr() ** for cases when pObj.fill==pObj.color ** ** 1 fill is background, and color is foreground. ** 2 fill and color are both foreground. (Used by "dot" objects) ** 3 fill and color are both background. (Used by most other objs) */ func (p *Pik) pik_append_style(pObj *PObj, eFill int) { clrIsBg := false p.pik_append(" style=\"") if pObj.fill >= 0 && eFill != 0 { fillIsBg := true if pObj.fill == pObj.color { if eFill == 2 { fillIsBg = false } if eFill == 3 { clrIsBg = true } } p.pik_append_clr("fill:", pObj.fill, ";", fillIsBg) } else { p.pik_append("fill:none;") } if pObj.sw > 0.0 && pObj.color >= 0.0 { sw := pObj.sw p.pik_append_dis("stroke-width:", sw, ";") if pObj.nPath > 2 && pObj.rad <= pObj.sw { p.pik_append("stroke-linejoin:round;") } p.pik_append_clr("stroke:", pObj.color, ";", clrIsBg) if pObj.dotted > 0.0 { v := pObj.dotted if sw < 2.1/p.rScale { sw = 2.1 / p.rScale } p.pik_append_dis("stroke-dasharray:", sw, "") p.pik_append_dis(",", v, ";") } else if pObj.dashed > 0.0 { v := pObj.dashed p.pik_append_dis("stroke-dasharray:", v, "") p.pik_append_dis(",", v, ";") } } } /* ** Compute the vertical locations for all text items in the ** object pObj. In other words, set every pObj.aTxt[*].eCode ** value to contain exactly one of: TP_ABOVE2, TP_ABOVE, TP_CENTER, ** TP_BELOW, or TP_BELOW2 is set. */ func pik_txt_vertical_layout(pObj *PObj) { n := int(pObj.nTxt) if n == 0 { return } aTxt := pObj.aTxt[:] if n == 1 { if (aTxt[0].eCode & TP_VMASK) == 0 { aTxt[0].eCode |= TP_CENTER } } else { allSlots := int16(0) var aFree [5]int16 /* If there is more than one TP_ABOVE, change the first to TP_ABOVE2. */ for j, mJust, i := 0, int16(0), n-1; i >= 0; i-- { if aTxt[i].eCode&TP_ABOVE != 0 { if j == 0 { j++ mJust = aTxt[i].eCode & TP_JMASK } else if j == 1 && mJust != 0 && (aTxt[i].eCode&mJust) == 0 { j++ } else { aTxt[i].eCode = (aTxt[i].eCode &^ TP_VMASK) | TP_ABOVE2 break } } } /* If there is more than one TP_BELOW, change the last to TP_BELOW2 */ for j, mJust, i := 0, int16(0), 0; i < n; i++ { if aTxt[i].eCode&TP_BELOW != 0 { if j == 0 { j++ mJust = aTxt[i].eCode & TP_JMASK } else if j == 1 && mJust != 0 && (aTxt[i].eCode&mJust) == 0 { j++ } else { aTxt[i].eCode = (aTxt[i].eCode &^ TP_VMASK) | TP_BELOW2 break } } } /* Compute a mask of all slots used */ for i := 0; i < n; i++ { allSlots |= aTxt[i].eCode & TP_VMASK } /* Set of an array of available slots */ if n == 2 && ((aTxt[0].eCode|aTxt[1].eCode)&TP_JMASK) == (TP_LJUST|TP_RJUST) { /* Special case of two texts that have opposite justification: ** Allow them both to float to center. */ aFree[0] = TP_CENTER aFree[1] = TP_CENTER } else { /* Set up the arrow so that available slots are filled from top to ** bottom */ iSlot := 0 if n >= 4 && (allSlots&TP_ABOVE2) == 0 { aFree[iSlot] = TP_ABOVE2 iSlot++ } if (allSlots & TP_ABOVE) == 0 { aFree[iSlot] = TP_ABOVE iSlot++ } if (n & 1) != 0 { aFree[iSlot] = TP_CENTER iSlot++ } if (allSlots & TP_BELOW) == 0 { aFree[iSlot] = TP_BELOW iSlot++ } if n >= 4 && (allSlots&TP_BELOW2) == 0 { aFree[iSlot] = TP_BELOW2 iSlot++ } } /* Set the VMASK for all unassigned texts */ for i, iSlot := 0, 0; i < n; i++ { if (aTxt[i].eCode & TP_VMASK) == 0 { aTxt[i].eCode |= aFree[iSlot] iSlot++ } } } } /* Return the font scaling factor associated with the input text attribute. */ func (p *Pik) pik_font_scale(t PToken) PNum { scale := p.svgFontScale if t.eCode&TP_BIG != 0 { scale *= 1.25 } if t.eCode&TP_SMALL != 0 { scale *= 0.8 } if t.eCode&TP_XTRA != 0 { scale *= scale } return scale } /* Append multiple <text> SVG elements for the text fields of the PObj. ** Parameters: ** ** p The Pik object into which we are rendering ** ** pObj Object containing the text to be rendered ** ** pBox If not NULL, do no rendering at all. Instead ** expand the box object so that it will include all ** of the text. */ func (p *Pik) pik_append_txt(pObj *PObj, pBox *PBox) { var jw PNum /* Justification margin relative to center */ var ha2 PNum = 0.0 /* Height of the top row of text */ var ha1 PNum = 0.0 /* Height of the second "above" row */ var hc PNum = 0.0 /* Height of the center row */ var hb1 PNum = 0.0 /* Height of the first "below" row of text */ var hb2 PNum = 0.0 /* Height of the second "below" row */ var yBase PNum = 0.0 allMask := int16(0) if p.nErr != 0 { return } if pObj.nTxt == 0 { return } aTxt := pObj.aTxt[:] n := int(pObj.nTxt) pik_txt_vertical_layout(pObj) x := pObj.ptAt.x for i := 0; i < n; i++ { allMask |= pObj.aTxt[i].eCode } if pObj.typ.isLine { hc = pObj.sw * 1.5 } else if pObj.rad > 0.0 && pObj.typ.zName == "cylinder" { yBase = -0.75 * pObj.rad } if allMask&TP_CENTER != 0 { for i := 0; i < n; i++ { if pObj.aTxt[i].eCode&TP_CENTER != 0 { s := p.pik_font_scale(pObj.aTxt[i]) if hc < s*p.charHeight { hc = s * p.charHeight } } } } if allMask&TP_ABOVE != 0 { for i := 0; i < n; i++ { if pObj.aTxt[i].eCode&TP_ABOVE != 0 { s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight if ha1 < s { ha1 = s } } } if allMask&TP_ABOVE2 != 0 { for i := 0; i < n; i++ { if pObj.aTxt[i].eCode&TP_ABOVE2 != 0 { s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight if ha2 < s { ha2 = s } } } } } if allMask&TP_BELOW != 0 { for i := 0; i < n; i++ { if pObj.aTxt[i].eCode&TP_BELOW != 0 { s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight if hb1 < s { hb1 = s } } } if allMask&TP_BELOW2 != 0 { for i := 0; i < n; i++ { if pObj.aTxt[i].eCode&TP_BELOW2 != 0 { s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight if hb2 < s { hb2 = s } } } } } if pObj.typ.eJust == 1 { jw = 0.5 * (pObj.w - 0.5*(p.charWidth+pObj.sw)) } else { jw = 0.0 } for i := 0; i < n; i++ { t := aTxt[i] xtraFontScale := p.pik_font_scale(t) var nx PNum = 0 orig_y := pObj.ptAt.y y := yBase if t.eCode&TP_ABOVE2 != 0 { y += 0.5*hc + ha1 + 0.5*ha2 } if t.eCode&TP_ABOVE != 0 { y += 0.5*hc + 0.5*ha1 } if t.eCode&TP_BELOW != 0 { y -= 0.5*hc + 0.5*hb1 } if t.eCode&TP_BELOW2 != 0 { y -= 0.5*hc + hb1 + 0.5*hb2 } if t.eCode&TP_LJUST != 0 { nx -= jw } if t.eCode&TP_RJUST != 0 { nx += jw } if pBox != nil { /* If pBox is not NULL, do not draw any <text>. Instead, just expand ** pBox to include the text */ var cw PNum = PNum(pik_text_length(t)) * p.charWidth * xtraFontScale * 0.01 var ch PNum = p.charHeight * 0.5 * xtraFontScale var x0, y0, x1, y1 PNum /* Boundary of text relative to pObj.ptAt */ if t.eCode&TP_BOLD != 0 { cw *= 1.1 } if t.eCode&TP_RJUST != 0 { x0 = nx y0 = y - ch x1 = nx - cw y1 = y + ch } else if t.eCode&TP_LJUST != 0 { x0 = nx y0 = y - ch x1 = nx + cw y1 = y + ch } else { x0 = nx + cw/2 y0 = y + ch x1 = nx - cw/2 y1 = y - ch } if (t.eCode&TP_ALIGN) != 0 && pObj.nPath >= 2 { nn := pObj.nPath var dx PNum = pObj.aPath[nn-1].x - pObj.aPath[0].x var dy PNum = pObj.aPath[nn-1].y - pObj.aPath[0].y if dx != 0 || dy != 0 { var dist PNum = math.Hypot(dx, dy) var tt PNum dx /= dist dy /= dist tt = dx*x0 - dy*y0 y0 = dy*x0 - dx*y0 x0 = tt tt = dx*x1 - dy*y1 y1 = dy*x1 - dx*y1 x1 = tt } } pik_bbox_add_xy(pBox, x+x0, orig_y+y0) pik_bbox_add_xy(pBox, x+x1, orig_y+y1) continue } nx += x y += orig_y p.pik_append_x("<text x=\"", nx, "\"") p.pik_append_y(" y=\"", y, "\"") if t.eCode&TP_RJUST != 0 { p.pik_append(" text-anchor=\"end\"") } else if t.eCode&TP_LJUST != 0 { p.pik_append(" text-anchor=\"start\"") } else { p.pik_append(" text-anchor=\"middle\"") } if t.eCode&TP_ITALIC != 0 { p.pik_append(" font-style=\"italic\"") } if t.eCode&TP_BOLD != 0 { p.pik_append(" font-weight=\"bold\"") } if pObj.color >= 0.0 { p.pik_append_clr(" fill=\"", pObj.color, "\"", false) } xtraFontScale *= p.fontScale if xtraFontScale <= 0.99 || xtraFontScale >= 1.01 { p.pik_append_num(" font-size=\"", xtraFontScale*100.0) p.pik_append("%\"") } if (t.eCode&TP_ALIGN) != 0 && pObj.nPath >= 2 { nn := pObj.nPath var dx PNum = pObj.aPath[nn-1].x - pObj.aPath[0].x var dy PNum = pObj.aPath[nn-1].y - pObj.aPath[0].y if dx != 0 || dy != 0 { var ang PNum = math.Atan2(dy, dx) * -180 / math.Pi p.pik_append_num(" transform=\"rotate(", ang) p.pik_append_xy(" ", x, orig_y) p.pik_append(")\"") } } p.pik_append(" dominant-baseline=\"central\">") var z []byte var nz int if t.n >= 2 && t.z[0] == '"' { z = t.z[1:] nz = t.n - 2 } else { z = t.z nz = t.n } for nz > 0 { var j int for j = 0; j < nz && z[j] != '\\'; j++ { } if j != 0 { p.pik_append_text(string(z[:j]), 0x3) } if j < nz && (j+1 == nz || z[j+1] == '\\') { p.pik_append("\") j++ } nz -= j + 1 if nz > 0 { z = z[j+1:] } } p.pik_append("</text>\n") } } /* ** Append text (that will go inside of a <pre>...</pre>) that ** shows the context of an error token. */ func (p *Pik) pik_error_context(pErr *PToken, nContext int) { var ( iErrPt int /* Index of first byte of error from start of input */ iErrCol int /* Column of the error token on its line */ iStart int /* Start position of the error context */ iEnd int /* End position of the error context */ iLineno int /* Line number of the error */ iFirstLineno int /* Line number of start of error context */ i int /* Loop counter */ iBump = 0 /* Bump the location of the error cursor */ ) iErrPt = len(p.sIn.z) - len(pErr.z) // in C, uses pointer math: iErrPt = (int)(pErr->z - p->sIn.z); if iErrPt >= p.sIn.n { iErrPt = p.sIn.n - 1 iBump = 1 } else { for iErrPt > 0 && (p.sIn.z[iErrPt] == '\n' || p.sIn.z[iErrPt] == '\r') { iErrPt-- iBump = 1 } } iLineno = 1 for i = 0; i < iErrPt; i++ { if p.sIn.z[i] == '\n' { iLineno++ } } iStart = 0 iFirstLineno = 1 for iFirstLineno+nContext < iLineno { for p.sIn.z[iStart] != '\n' { iStart++ } iStart++ iFirstLineno++ } for iEnd = iErrPt; p.sIn.z[iEnd] != 0 && p.sIn.z[iEnd] != '\n'; iEnd++ { } i = iStart for iFirstLineno <= iLineno { zLineno := fmt.Sprintf("/* %4d */ ", iFirstLineno) iFirstLineno++ p.pik_append(zLineno) for i = iStart; p.sIn.z[i] != 0 && p.sIn.z[i] != '\n'; i++ { } p.pik_append_errtxt(string(p.sIn.z[iStart:i])) iStart = i + 1 p.pik_append("\n") } for iErrCol, i = 0, iErrPt; i > 0 && p.sIn.z[i] != '\n'; iErrCol, i = iErrCol+1, i-1 { } for i = 0; i < iErrCol+11+iBump; i++ { p.pik_append(" ") } for i = 0; i < pErr.n; i++ { p.pik_append("^") } p.pik_append("\n") } /* ** Generate an error message for the output. pErr is the token at which ** the error should point. zMsg is the text of the error message. If ** either pErr or zMsg is NULL, generate an out-of-memory error message. ** ** This routine is a no-op if there has already been an error reported. */ func (p *Pik) pik_error(pErr *PToken, zMsg string) { if p == nil { return } if p.nErr > 0 { return } p.nErr++ if zMsg == "" { if p.mFlags&PIKCHR_PLAINTEXT_ERRORS != 0 { p.pik_append("\nOut of memory\n") } else { p.pik_append("\n<div><p>Out of memory</p></div>\n") } return } if pErr == nil { p.pik_append("\n") p.pik_append_errtxt(zMsg) return } if (p.mFlags & PIKCHR_PLAINTEXT_ERRORS) == 0 { p.pik_append("<div><pre>\n") } p.pik_error_context(pErr, 5) p.pik_append("ERROR: ") p.pik_append_errtxt(zMsg) p.pik_append("\n") for i := p.nCtx - 1; i >= 0; i-- { p.pik_append("Called from:\n") p.pik_error_context(&p.aCtx[i], 0) } if (p.mFlags & PIKCHR_PLAINTEXT_ERRORS) == 0 { p.pik_append("</pre></div>\n") } } /* ** Process an "assert( e1 == e2 )" statement. Always return `nil`. */ func (p *Pik) pik_assert(e1 PNum, pEq *PToken, e2 PNum) *PObj { /* Convert the numbers to strings using %g for comparison. This ** limits the precision of the comparison to account for rounding error. */ zE1 := fmt.Sprintf("%g", e1) zE2 := fmt.Sprintf("%g", e2) if zE1 != zE2 { p.pik_error(pEq, fmt.Sprintf("%.50s != %.50s", zE1, zE2)) } return nil } /* ** Process an "assert( place1 == place2 )" statement. Always return `nil`. */ func (p *Pik) pik_position_assert(e1 *PPoint, pEq *PToken, e2 *PPoint) *PObj { /* Convert the numbers to strings using %g for comparison. This ** limits the precision of the comparison to account for rounding error. */ zE1 := fmt.Sprintf("(%g,%g)", e1.x, e1.y) zE2 := fmt.Sprintf("(%g,%g)", e2.x, e2.y) if zE1 != zE2 { p.pik_error(pEq, fmt.Sprintf("%s != %s", zE1, zE2)) } return nil } /* Free a complete list of objects */ func (p *Pik) pik_elist_free(pList *PList) { if pList == nil || *pList == nil { return } for i := 0; i < len(*pList); i++ { p.pik_elem_free((*pList)[i]) } } /* Free a single object, and its substructure */ func (p *Pik) pik_elem_free(pObj *PObj) { if pObj == nil { return } p.pik_elist_free(&pObj.pSublist) } /* Convert a numeric literal into a number. Return that number. ** There is no error handling because the tokenizer has already ** assured us that the numeric literal is valid. ** ** Allowed number forms: ** ** (1) Floating point literal ** (2) Same as (1) but followed by a unit: "cm", "mm", "in", ** "px", "pt", or "pc". ** (3) Hex integers: 0x000000 ** ** This routine returns the result in inches. If a different unit ** is specified, the conversion happens automatically. */ func pik_atof(num *PToken) PNum { if num.n >= 3 && num.z[0] == '0' && (num.z[1] == 'x' || num.z[1] == 'X') { i, err := strconv.ParseInt(string(num.z[2:num.n]), 16, 64) if err != nil { return 0 } return PNum(i) } factor := 1.0 z := num.String() if num.n > 2 { hasSuffix := true switch string(num.z[num.n-2 : num.n]) { case "cm": factor = 1 / 2.54 case "mm": factor = 1 / 25.4 case "px": factor = 1 / 96.0 case "pt": factor = 1 / 72.0 case "pc": factor = 1 / 6.0 case "in": factor = 1.0 default: hasSuffix = false } if hasSuffix { z = z[:len(z)-2] } } ans, err := strconv.ParseFloat(z, 64) ans *= factor if err != nil { return 0.0 } return PNum(ans) } /* ** Compute the distance between two points */ func pik_dist(pA *PPoint, pB *PPoint) PNum { dx := pB.x - pA.x dy := pB.y - pA.y return math.Hypot(dx, dy) } /* Return true if a bounding box is empty. */ func pik_bbox_isempty(p *PBox) bool { return p.sw.x > p.ne.x } /* Return true if point pPt is contained within the bounding box pBox */ func pik_bbox_contains_point(pBox *PBox, pPt *PPoint) bool { if pik_bbox_isempty(pBox) { return false } if pPt.x < pBox.sw.x { return false } if pPt.x > pBox.ne.x { return false } if pPt.y < pBox.sw.y { return false } if pPt.y > pBox.ne.y { return false } return true } /* Initialize a bounding box to an empty container */ func pik_bbox_init(p *PBox) { p.sw.x = 1.0 p.sw.y = 1.0 p.ne.x = 0.0 p.ne.y = 0.0 } /* Enlarge the PBox of the first argument so that it fully ** covers the second PBox */ func pik_bbox_addbox(pA *PBox, pB *PBox) { if pik_bbox_isempty(pA) { *pA = *pB } if pik_bbox_isempty(pB) { return } if pA.sw.x > pB.sw.x { pA.sw.x = pB.sw.x } if pA.sw.y > pB.sw.y { pA.sw.y = pB.sw.y } if pA.ne.x < pB.ne.x { pA.ne.x = pB.ne.x } if pA.ne.y < pB.ne.y { pA.ne.y = pB.ne.y } } /* Enlarge the PBox of the first argument, if necessary, so that ** it contains the point described by the 2nd and 3rd arguments. */ func pik_bbox_add_xy(pA *PBox, x PNum, y PNum) { if pik_bbox_isempty(pA) { pA.ne.x = x pA.ne.y = y pA.sw.x = x pA.sw.y = y return } if pA.sw.x > x { pA.sw.x = x } if pA.sw.y > y { pA.sw.y = y } if pA.ne.x < x { pA.ne.x = x } if pA.ne.y < y { pA.ne.y = y } } /* Enlarge the PBox so that it is able to contain an ellipse ** centered at x,y and with radiuses rx and ry. */ func pik_bbox_addellipse(pA *PBox, x PNum, y PNum, rx PNum, ry PNum) { if pik_bbox_isempty(pA) { pA.ne.x = x + rx pA.ne.y = y + ry pA.sw.x = x - rx pA.sw.y = y - ry return } if pA.sw.x > x-rx { pA.sw.x = x - rx } if pA.sw.y > y-ry { pA.sw.y = y - ry } if pA.ne.x < x+rx { pA.ne.x = x + rx } if pA.ne.y < y+ry { pA.ne.y = y + ry } } /* Append a new object onto the end of an object list. The ** object list is created if it does not already exist. Return ** the new object list. */ func (p *Pik) pik_elist_append(pList PList, pObj *PObj) PList { if pObj == nil { return pList } pList = append(pList, pObj) p.list = pList return pList } /* Convert an object class name into a PClass pointer */ func pik_find_class(pId *PToken) *PClass { zString := pId.String() first := 0 last := len(aClass) - 1 for { mid := (first + last) / 2 c := strings.Compare(aClass[mid].zName, zString) if c == 0 { return &aClass[mid] } if c < 0 { first = mid + 1 } else { last = mid - 1 } if first > last { return nil } } } /* Allocate and return a new PObj object. ** ** If pId!=0 then pId is an identifier that defines the object class. ** If pStr!=0 then it is a STRING literal that defines a text object. ** If pSublist!=0 then this is a [...] object. If all three parameters ** are NULL then this is a no-op object used to define a PLACENAME. */ func (p *Pik) pik_elem_new(pId *PToken, pStr *PToken, pSublist PList) *PObj { miss := false if p.nErr != 0 { return nil } pNew := &PObj{} p.cur = pNew p.nTPath = 1 p.thenFlag = false if len(p.list) == 0 { pNew.ptAt.x = 0.0 pNew.ptAt.y = 0.0 pNew.eWith = CP_C } else { pPrior := p.list[len(p.list)-1] pNew.ptAt = pPrior.ptExit switch p.eDir { default: pNew.eWith = CP_W case DIR_LEFT: pNew.eWith = CP_E case DIR_UP: pNew.eWith = CP_S case DIR_DOWN: pNew.eWith = CP_N } } p.aTPath[0] = pNew.ptAt pNew.with = pNew.ptAt pNew.outDir = p.eDir pNew.inDir = p.eDir pNew.iLayer = p.pik_value_int("layer", &miss) if miss { pNew.iLayer = 1000 } if pNew.iLayer < 0 { pNew.iLayer = 0 } if pSublist != nil { pNew.typ = &sublistClass pNew.pSublist = pSublist sublistClass.xInit(p, pNew) return pNew } if pStr != nil { n := PToken{ z: []byte("text"), n: 4, } pNew.typ = pik_find_class(&n) assert(pNew.typ != nil, "pNew.typ!=nil") pNew.errTok = *pStr pNew.typ.xInit(p, pNew) p.pik_add_txt(pStr, pStr.eCode) return pNew } if pId != nil { pNew.errTok = *pId pClass := pik_find_class(pId) if pClass != nil { pNew.typ = pClass pNew.sw = p.pik_value("thickness", nil) pNew.fill = p.pik_value("fill", nil) pNew.color = p.pik_value("color", nil) pClass.xInit(p, pNew) return pNew } p.pik_error(pId, "unknown object type") p.pik_elem_free(pNew) return nil } pNew.typ = &noopClass pNew.ptExit = pNew.ptAt pNew.ptEnter = pNew.ptAt return pNew } /* ** If the ID token in the argument is the name of a macro, return ** the PMacro object for that macro */ func (p *Pik) pik_find_macro(pId *PToken) *PMacro { for pMac := p.pMacros; pMac != nil; pMac = pMac.pNext { if pMac.macroName.n == pId.n && bytesEq(pMac.macroName.z[:pMac.macroName.n], pId.z[:pId.n]) { return pMac } } return nil } /* Add a new macro */ func (p *Pik) pik_add_macro( pId *PToken, /* The ID token that defines the macro name */ pCode *PToken, /* Macro body inside of {...} */ ) { pNew := p.pik_find_macro(pId) if pNew == nil { pNew = &PMacro{ pNext: p.pMacros, macroName: *pId, } p.pMacros = pNew } pNew.macroBody.z = pCode.z[1:] pNew.macroBody.n = pCode.n - 2 pNew.inUse = false } /* ** Set the output direction and exit point for an object */ func pik_elem_set_exit(pObj *PObj, eDir uint8) { assert(ValidDir(eDir), "ValidDir(eDir)") pObj.outDir = eDir if !pObj.typ.isLine || pObj.bClose { pObj.ptExit = pObj.ptAt switch pObj.outDir { default: pObj.ptExit.x += pObj.w * 0.5 case DIR_LEFT: pObj.ptExit.x -= pObj.w * 0.5 case DIR_UP: pObj.ptExit.y += pObj.h * 0.5 case DIR_DOWN: pObj.ptExit.y -= pObj.h * 0.5 } } } /* Change the layout direction. */ func (p *Pik) pik_set_direction(eDir uint8) { assert(ValidDir(eDir), "ValidDir(eDir)") p.eDir = eDir /* It seems to make sense to reach back into the last object and ** change its exit point (its ".end") to correspond to the new ** direction. Things just seem to work better this way. However, ** legacy PIC does *not* do this. ** ** The difference can be seen in a script like this: ** ** arrow; circle; down; arrow ** ** You can make pikchr render the above exactly like PIC ** by deleting the following three lines. But I (drh) think ** it works better with those lines in place. */ if len(p.list) > 0 { pik_elem_set_exit(p.list[len(p.list)-1], eDir) } } /* Move all coordinates contained within an object (and within its ** substructure) by dx, dy */ func pik_elem_move(pObj *PObj, dx PNum, dy PNum) { pObj.ptAt.x += dx pObj.ptAt.y += dy pObj.ptEnter.x += dx pObj.ptEnter.y += dy pObj.ptExit.x += dx pObj.ptExit.y += dy pObj.bbox.ne.x += dx pObj.bbox.ne.y += dy pObj.bbox.sw.x += dx pObj.bbox.sw.y += dy for i := 0; i < pObj.nPath; i++ { pObj.aPath[i].x += dx pObj.aPath[i].y += dy } if pObj.pSublist != nil { pik_elist_move(pObj.pSublist, dx, dy) } } func pik_elist_move(pList PList, dx PNum, dy PNum) { for i := 0; i < len(pList); i++ { pik_elem_move(pList[i], dx, dy) } } /* ** Check to see if it is ok to set the value of paraemeter mThis. ** Return 0 if it is ok. If it not ok, generate an appropriate ** error message and return non-zero. ** ** Flags are set in pObj so that the same object or conflicting ** objects may not be set again. ** ** To be ok, bit mThis must be clear and no more than one of ** the bits identified by mBlockers may be set. */ func (p *Pik) pik_param_ok( pObj *PObj, /* The object under construction */ pId *PToken, /* Make the error point to this token */ mThis uint, /* Value we are trying to set */ ) bool { if pObj.mProp&mThis != 0 { p.pik_error(pId, "value is already set") return true } if pObj.mCalc&mThis != 0 { p.pik_error(pId, "value already fixed by prior constraints") return true } pObj.mProp |= mThis return false } /* ** Set a numeric property like "width 7" or "radius 200%". ** ** The rAbs term is an absolute value to add in. rRel is ** a relative value by which to change the current value. */ func (p *Pik) pik_set_numprop(pId *PToken, pVal *PRel) { pObj := p.cur switch pId.eType { case T_HEIGHT: if p.pik_param_ok(pObj, pId, A_HEIGHT) { return } pObj.h = pObj.h*pVal.rRel + pVal.rAbs case T_WIDTH: if p.pik_param_ok(pObj, pId, A_WIDTH) { return } pObj.w = pObj.w*pVal.rRel + pVal.rAbs case T_RADIUS: if p.pik_param_ok(pObj, pId, A_RADIUS) { return } pObj.rad = pObj.rad*pVal.rRel + pVal.rAbs case T_DIAMETER: if p.pik_param_ok(pObj, pId, A_RADIUS) { return } pObj.rad = pObj.rad*pVal.rRel + 0.5*pVal.rAbs /* diam it 2x rad */ case T_THICKNESS: if p.pik_param_ok(pObj, pId, A_THICKNESS) { return } pObj.sw = pObj.sw*pVal.rRel + pVal.rAbs } if pObj.typ.xNumProp != nil { pObj.typ.xNumProp(p, pObj, pId) } } /* ** Set a color property. The argument is an RGB value. */ func (p *Pik) pik_set_clrprop(pId *PToken, rClr PNum) { pObj := p.cur switch pId.eType { case T_FILL: if p.pik_param_ok(pObj, pId, A_FILL) { return } pObj.fill = rClr case T_COLOR: if p.pik_param_ok(pObj, pId, A_COLOR) { return } pObj.color = rClr break } if pObj.typ.xNumProp != nil { pObj.typ.xNumProp(p, pObj, pId) } } /* ** Set a "dashed" property like "dash 0.05" ** ** Use the value supplied by pVal if available. If pVal==0, use ** a default. */ func (p *Pik) pik_set_dashed(pId *PToken, pVal *PNum) { pObj := p.cur switch pId.eType { case T_DOTTED: if pVal != nil { pObj.dotted = *pVal } else { pObj.dotted = p.pik_value("dashwid", nil) } pObj.dashed = 0.0 case T_DASHED: if pVal != nil { pObj.dashed = *pVal } else { pObj.dashed = p.pik_value("dashwid", nil) } pObj.dotted = 0.0 } } /* ** If the current path information came from a "same" or "same as" ** reset it. */ func (p *Pik) pik_reset_samepath() { if p.samePath { p.samePath = false p.nTPath = 1 } } /* Add a new term to the path for a line-oriented object by transferring ** the information in the ptTo field over onto the path and into ptFrom ** resetting the ptTo. */ func (p *Pik) pik_then(pToken *PToken, pObj *PObj) { if !pObj.typ.isLine { p.pik_error(pToken, "use with line-oriented objects only") return } n := p.nTPath - 1 if n < 1 && (pObj.mProp&A_FROM) == 0 { p.pik_error(pToken, "no prior path points") return } p.thenFlag = true } /* Advance to the next entry in p.aTPath. Return its index. */ func (p *Pik) pik_next_rpath(pErr *PToken) int { n := p.nTPath - 1 if n+1 >= len(p.aTPath) { (*Pik)(nil).pik_error(pErr, "too many path elements") return n } n++ p.nTPath++ p.aTPath[n] = p.aTPath[n-1] p.mTPath = 0 return n } /* Add a direction term to an object. "up 0.5", or "left 3", or "down" ** or "down 50%". */ func (p *Pik) pik_add_direction(pDir *PToken, pVal *PRel) { pObj := p.cur if !pObj.typ.isLine { if pDir != nil { p.pik_error(pDir, "use with line-oriented objects only") } else { x := pik_next_semantic_token(&pObj.errTok) p.pik_error(&x, "syntax error") } return } p.pik_reset_samepath() n := p.nTPath - 1 if p.thenFlag || p.mTPath == 3 || n == 0 { n = p.pik_next_rpath(pDir) p.thenFlag = false } dir := p.eDir if pDir != nil { dir = uint8(pDir.eCode) } switch dir { case DIR_UP: if p.mTPath&2 > 0 { n = p.pik_next_rpath(pDir) } p.aTPath[n].y += pVal.rAbs + pObj.h*pVal.rRel p.mTPath |= 2 case DIR_DOWN: if p.mTPath&2 > 0 { n = p.pik_next_rpath(pDir) } p.aTPath[n].y -= pVal.rAbs + pObj.h*pVal.rRel p.mTPath |= 2 case DIR_RIGHT: if p.mTPath&1 > 0 { n = p.pik_next_rpath(pDir) } p.aTPath[n].x += pVal.rAbs + pObj.w*pVal.rRel p.mTPath |= 1 case DIR_LEFT: if p.mTPath&1 > 0 { n = p.pik_next_rpath(pDir) } p.aTPath[n].x -= pVal.rAbs + pObj.w*pVal.rRel p.mTPath |= 1 } pObj.outDir = dir } /* Process a movement attribute of one of these forms: ** ** pDist pHdgKW rHdg pEdgept ** GO distance HEADING angle ** GO distance compasspoint */ func (p *Pik) pik_move_hdg( pDist *PRel, /* Distance to move */ pHeading *PToken, /* "heading" keyword if present */ rHdg PNum, /* Angle argument to "heading" keyword */ pEdgept *PToken, /* EDGEPT keyword "ne", "sw", etc... */ pErr *PToken, /* Token to use for error messages */ ) { pObj := p.cur var rDist PNum = pDist.rAbs + p.pik_value("linewid", nil)*pDist.rRel if !pObj.typ.isLine { p.pik_error(pErr, "use with line-oriented objects only") return } p.pik_reset_samepath() n := 0 for n < 1 { n = p.pik_next_rpath(pErr) } if pHeading != nil { rHdg = math.Mod(rHdg, 360.0) } else if pEdgept.eEdge == CP_C { p.pik_error(pEdgept, "syntax error") return } else { rHdg = pik_hdg_angle[pEdgept.eEdge] } if rHdg <= 45.0 { pObj.outDir = DIR_UP } else if rHdg <= 135.0 { pObj.outDir = DIR_RIGHT } else if rHdg <= 225.0 { pObj.outDir = DIR_DOWN } else if rHdg <= 315.0 { pObj.outDir = DIR_LEFT } else { pObj.outDir = DIR_UP } rHdg *= 0.017453292519943295769 /* degrees to radians */ p.aTPath[n].x += rDist * math.Sin(rHdg) p.aTPath[n].y += rDist * math.Cos(rHdg) p.mTPath = 2 } /* Process a movement attribute of the form "right until even with ..." ** ** pDir is the first keyword, "right" or "left" or "up" or "down". ** The movement is in that direction until its closest approach to ** the point specified by pPoint. */ func (p *Pik) pik_evenwith(pDir *PToken, pPlace *PPoint) { pObj := p.cur if !pObj.typ.isLine { p.pik_error(pDir, "use with line-oriented objects only") return } p.pik_reset_samepath() n := p.nTPath - 1 if p.thenFlag || p.mTPath == 3 || n == 0 { n = p.pik_next_rpath(pDir) p.thenFlag = false } switch pDir.eCode { case DIR_DOWN, DIR_UP: if p.mTPath&2 != 0 { n = p.pik_next_rpath(pDir) } p.aTPath[n].y = pPlace.y p.mTPath |= 2 case DIR_RIGHT, DIR_LEFT: if p.mTPath&1 != 0 { n = p.pik_next_rpath(pDir) } p.aTPath[n].x = pPlace.x p.mTPath |= 1 } pObj.outDir = uint8(pDir.eCode) } /* If the last referenced object is centered at point pPt then return ** a pointer to that object. If there is no prior object reference, ** or if the points are not the same, return NULL. ** ** This is a side-channel hack used to find the objects at which a ** line begins and ends. For example, in ** ** arrow from OBJ1 to OBJ2 chop ** ** The arrow object is normally just handed the coordinates of the ** centers for OBJ1 and OBJ2. But we also want to know the specific ** object named in case there are multiple objects centered at the ** same point. ** ** See forum post 1d46e3a0bc */ func (p *Pik) pik_last_ref_object(pPt *PPoint) *PObj { var pRes *PObj if p.lastRef == nil { return nil } if p.lastRef.ptAt.x == pPt.x && p.lastRef.ptAt.y == pPt.y { pRes = p.lastRef } p.lastRef = nil return pRes } /* Set the "from" of an object */ func (p *Pik) pik_set_from(pObj *PObj, pTk *PToken, pPt *PPoint) { if !pObj.typ.isLine { p.pik_error(pTk, "use \"at\" to position this object") return } if pObj.mProp&A_FROM != 0 { p.pik_error(pTk, "line start location already fixed") return } if pObj.bClose { p.pik_error(pTk, "polygon is closed") return } if p.nTPath > 1 { var dx PNum = pPt.x - p.aTPath[0].x var dy PNum = pPt.y - p.aTPath[0].y for i := 1; i < p.nTPath; i++ { p.aTPath[i].x += dx p.aTPath[i].y += dy } } p.aTPath[0] = *pPt p.mTPath = 3 pObj.mProp |= A_FROM pObj.pFrom = p.pik_last_ref_object(pPt) } /* Set the "to" of an object */ func (p *Pik) pik_add_to(pObj *PObj, pTk *PToken, pPt *PPoint) { n := p.nTPath - 1 if !pObj.typ.isLine { p.pik_error(pTk, "use \"at\" to position this object") return } if pObj.bClose { p.pik_error(pTk, "polygon is closed") return } p.pik_reset_samepath() if n == 0 || p.mTPath == 3 || p.thenFlag { n = p.pik_next_rpath(pTk) } p.aTPath[n] = *pPt p.mTPath = 3 pObj.pTo = p.pik_last_ref_object(pPt) } func (p *Pik) pik_close_path(pErr *PToken) { pObj := p.cur if p.nTPath < 3 { p.pik_error(pErr, "need at least 3 vertexes in order to close the polygon") return } if pObj.bClose { p.pik_error(pErr, "polygon already closed") return } pObj.bClose = true } /* Lower the layer of the current object so that it is behind the ** given object. */ func (p *Pik) pik_behind(pOther *PObj) { pObj := p.cur if p.nErr == 0 && pObj.iLayer >= pOther.iLayer { pObj.iLayer = pOther.iLayer - 1 } } /* Set the "at" of an object */ func (p *Pik) pik_set_at(pEdge *PToken, pAt *PPoint, pErrTok *PToken) { eDirToCp := []uint8{CP_E, CP_S, CP_W, CP_N} if p.nErr != 0 { return } pObj := p.cur if pObj.typ.isLine { p.pik_error(pErrTok, "use \"from\" and \"to\" to position this object") return } if pObj.mProp&A_AT != 0 { p.pik_error(pErrTok, "location fixed by prior \"at\"") return } pObj.mProp |= A_AT pObj.eWith = CP_C if pEdge != nil { pObj.eWith = pEdge.eEdge } if pObj.eWith >= CP_END { dir := pObj.inDir if pObj.eWith == CP_END { dir = pObj.outDir } pObj.eWith = eDirToCp[int(dir)] } pObj.with = *pAt } /* ** Try to add a text attribute to an object */ func (p *Pik) pik_add_txt(pTxt *PToken, iPos int16) { pObj := p.cur if int(pObj.nTxt) >= len(pObj.aTxt) { p.pik_error(pTxt, "too many text terms") return } pT := &pObj.aTxt[pObj.nTxt] pObj.nTxt++ *pT = *pTxt pT.eCode = iPos } /* Merge "text-position" flags */ func pik_text_position(iPrev int, pFlag *PToken) int { iRes := iPrev switch pFlag.eType { case T_LJUST: iRes = (iRes &^ TP_JMASK) | TP_LJUST case T_RJUST: iRes = (iRes &^ TP_JMASK) | TP_RJUST case T_ABOVE: iRes = (iRes &^ TP_VMASK) | TP_ABOVE case T_CENTER: iRes = (iRes &^ TP_VMASK) | TP_CENTER case T_BELOW: iRes = (iRes &^ TP_VMASK) | TP_BELOW case T_ITALIC: iRes |= TP_ITALIC case T_BOLD: iRes |= TP_BOLD case T_ALIGNED: iRes |= TP_ALIGN case T_BIG: if iRes&TP_BIG != 0 { iRes |= TP_XTRA } else { iRes = (iRes &^ TP_SZMASK) | TP_BIG } case T_SMALL: if iRes&TP_SMALL != 0 { iRes |= TP_XTRA } else { iRes = (iRes &^ TP_SZMASK) | TP_SMALL } } return iRes } /* ** Table of scale-factor estimates for variable-width characters. ** Actual character widths vary by font. These numbers are only ** guesses. And this table only provides data for ASCII. ** ** 100 means normal width. */ var awChar = []byte{ /* Skip initial 32 control characters */ /* ' ' */ 45, /* '!' */ 55, /* '"' */ 62, /* '#' */ 115, /* '$' */ 90, /* '%' */ 132, /* '&' */ 125, /* '\''*/ 40, /* '(' */ 55, /* ')' */ 55, /* '*' */ 71, /* '+' */ 115, /* ',' */ 45, /* '-' */ 48, /* '.' */ 45, /* '/' */ 50, /* '0' */ 91, /* '1' */ 91, /* '2' */ 91, /* '3' */ 91, /* '4' */ 91, /* '5' */ 91, /* '6' */ 91, /* '7' */ 91, /* '8' */ 91, /* '9' */ 91, /* ':' */ 50, /* ';' */ 50, /* '<' */ 120, /* '=' */ 120, /* '>' */ 120, /* '?' */ 78, /* '@' */ 142, /* 'A' */ 102, /* 'B' */ 105, /* 'C' */ 110, /* 'D' */ 115, /* 'E' */ 105, /* 'F' */ 98, /* 'G' */ 105, /* 'H' */ 125, /* 'I' */ 58, /* 'J' */ 58, /* 'K' */ 107, /* 'L' */ 95, /* 'M' */ 145, /* 'N' */ 125, /* 'O' */ 115, /* 'P' */ 95, /* 'Q' */ 115, /* 'R' */ 107, /* 'S' */ 95, /* 'T' */ 97, /* 'U' */ 118, /* 'V' */ 102, /* 'W' */ 150, /* 'X' */ 100, /* 'Y' */ 93, /* 'Z' */ 100, /* '[' */ 58, /* '\\'*/ 50, /* ']' */ 58, /* '^' */ 119, /* '_' */ 72, /* '`' */ 72, /* 'a' */ 86, /* 'b' */ 92, /* 'c' */ 80, /* 'd' */ 92, /* 'e' */ 85, /* 'f' */ 52, /* 'g' */ 92, /* 'h' */ 92, /* 'i' */ 47, /* 'j' */ 47, /* 'k' */ 88, /* 'l' */ 48, /* 'm' */ 135, /* 'n' */ 92, /* 'o' */ 86, /* 'p' */ 92, /* 'q' */ 92, /* 'r' */ 69, /* 's' */ 75, /* 't' */ 58, /* 'u' */ 92, /* 'v' */ 80, /* 'w' */ 121, /* 'x' */ 81, /* 'y' */ 80, /* 'z' */ 76, /* '{' */ 91, /* '|'*/ 49, /* '}' */ 91, /* '~' */ 118, } /* Return an estimate of the width of the displayed characters ** in a character string. The returned value is 100 times the ** average character width. ** ** Omit "\" used to escape characters. And count entities like ** "<" as a single character. Multi-byte UTF8 characters count ** as a single character. ** ** Attempt to scale the answer by the actual characters seen. Wide ** characters count more than narrow characters. But the widths are ** only guesses. */ func pik_text_length(pToken PToken) int { n := pToken.n z := pToken.z cnt := 0 for j := 1; j < n-1; j++ { c := z[j] if c == '\\' && z[j+1] != '&' { j++ c = z[j] } else if c == '&' { var k int for k = j + 1; k < j+7 && z[k] != 0 && z[k] != ';'; k++ { } if z[k] == ';' { j = k } cnt += 150 continue } if (c & 0xc0) == 0xc0 { for j+1 < n-1 && (z[j+1]&0xc0) == 0x80 { j++ } cnt += 100 continue } if c >= 0x20 && c <= 0x7e { cnt += int(awChar[int(c-0x20)]) } else { cnt += 100 } } return cnt } /* Adjust the width, height, and/or radius of the object so that ** it fits around the text that has been added so far. ** ** (1) Only text specified prior to this attribute is considered. ** (2) The text size is estimated based on the charht and charwid ** variable settings. ** (3) The fitted attributes can be changed again after this ** attribute, for example using "width 110%" if this auto-fit ** underestimates the text size. ** (4) Previously set attributes will not be altered. In other words, ** "width 1in fit" might cause the height to change, but the ** width is now set. ** (5) This only works for attributes that have an xFit method. ** ** The eWhich parameter is: ** ** 1: Fit horizontally only ** 2: Fit vertically only ** 3: Fit both ways */ func (p *Pik) pik_size_to_fit(pFit *PToken, eWhich int) { var w, h PNum var bbox PBox if p.nErr != 0 { return } pObj := p.cur if pObj.nTxt == 0 { (*Pik)(nil).pik_error(pFit, "no text to fit to") return } if pObj.typ.xFit == nil { return } pik_bbox_init(&bbox) p.pik_compute_layout_settings() p.pik_append_txt(pObj, &bbox) if eWhich&1 != 0 { w = (bbox.ne.x - bbox.sw.x) + p.charWidth } if eWhich&2 != 0 { var h1, h2 PNum h1 = bbox.ne.y - pObj.ptAt.y h2 = pObj.ptAt.y - bbox.sw.y hmax := h1 if h1 < h2 { hmax = h2 } h = 2.0*hmax + 0.5*p.charHeight } else { h = 0 } pObj.typ.xFit(p, pObj, w, h) pObj.mProp |= A_FIT } /* Set a local variable name to "val". ** ** The name might be a built-in variable or a color name. In either case, ** a new application-defined variable is set. Since app-defined variables ** are searched first, this will override any built-in variables. */ func (p *Pik) pik_set_var(pId *PToken, val PNum, pOp *PToken) { pVar := p.pVar for pVar != nil { if pik_token_eq(pId, pVar.zName) == 0 { break } pVar = pVar.pNext } if pVar == nil { pVar = &PVar{ zName: pId.String(), pNext: p.pVar, val: p.pik_value(pId.String(), nil), } p.pVar = pVar } switch pOp.eCode { case T_PLUS: pVar.val += val case T_STAR: pVar.val *= val case T_MINUS: pVar.val -= val case T_SLASH: if val == 0.0 { p.pik_error(pOp, "division by zero") } else { pVar.val /= val } default: pVar.val = val } p.bLayoutVars = false /* Clear the layout setting cache */ } /* ** Round a PNum into the nearest integer */ func pik_round(v PNum) int { switch { case math.IsNaN(v): return 0 case v < -2147483647: return (-2147483647 - 1) case v >= 2147483647: return 2147483647 default: return int(v + math.Copysign(1e-15, v)) } } /* ** Search for the variable named z[0..n-1] in: ** ** * Application defined variables ** * Built-in variables ** ** Return the value of the variable if found. If not found ** return 0.0. Also if pMiss is not NULL, then set it to 1 ** if not found. ** ** This routine is a subroutine to pik_get_var(). But it is also ** used by object implementations to look up (possibly overwritten) ** values for built-in variables like "boxwid". */ func (p *Pik) pik_value(z string, pMiss *bool) PNum { for pVar := p.pVar; pVar != nil; pVar = pVar.pNext { if pVar.zName == z { return pVar.val } } first := 0 last := len(aBuiltin) - 1 for first <= last { mid := (first + last) / 2 zName := aBuiltin[mid].zName if zName == z { return aBuiltin[mid].val } else if z > zName { first = mid + 1 } else { last = mid - 1 } } if pMiss != nil { *pMiss = true } return 0.0 } func (p *Pik) pik_value_int(z string, pMiss *bool) int { return pik_round(p.pik_value(z, pMiss)) } /* ** Look up a color-name. Unlike other names in this program, the ** color-names are not case sensitive. So "DarkBlue" and "darkblue" ** and "DARKBLUE" all find the same value (139). ** ** If not found, return -99.0. Also post an error if p!=NULL. ** ** Special color names "None" and "Off" return -1.0 without causing ** an error. */ func (p *Pik) pik_lookup_color(pId *PToken) PNum { first := 0 last := len(aColor) - 1 zId := strings.ToLower(pId.String()) for first <= last { mid := (first + last) / 2 zClr := strings.ToLower(aColor[mid].zName) c := strings.Compare(zId, zClr) if c == 0 { return PNum(aColor[mid].val) } if c > 0 { first = mid + 1 } else { last = mid - 1 } } if p != nil { p.pik_error(pId, "not a known color name") } return -99.0 } /* Get the value of a variable. ** ** Search in order: ** ** * Application defined variables ** * Built-in variables ** * Color names ** ** If no such variable is found, throw an error. */ func (p *Pik) pik_get_var(pId *PToken) PNum { miss := false v := p.pik_value(pId.String(), &miss) if !miss { return v } v = (*Pik)(nil).pik_lookup_color(pId) if v > -90.0 { return v } p.pik_error(pId, "no such variable") return 0.0 } /* Convert a T_NTH token (ex: "2nd", "5th"} into a numeric value and ** return that value. Throw an error if the value is too big. */ func (p *Pik) pik_nth_value(pNth *PToken) int16 { s := pNth.String() if s == "first" { return 1 } i, err := strconv.Atoi(s[:len(s)-2]) if err != nil { p.pik_error(pNth, "value can't be parsed as a number") } if i > 1000 { p.pik_error(pNth, "value too big - max '1000th'") i = 1 } return int16(i) } /* Search for the NTH object. ** ** If pBasis is not NULL then it should be a [] object. Use the ** sublist of that [] object for the search. If pBasis is not a [] ** object, then throw an error. ** ** The pNth token describes the N-th search. The pNth.eCode value ** is one more than the number of items to skip. It is negative ** to search backwards. If pNth.eType==T_ID, then it is the name ** of a class to search for. If pNth.eType==T_LB, then ** search for a [] object. If pNth.eType==T_LAST, then search for ** any type. ** ** Raise an error if the item is not found. */ func (p *Pik) pik_find_nth(pBasis *PObj, pNth *PToken) *PObj { var pList PList var pClass *PClass if pBasis == nil { pList = p.list } else { pList = pBasis.pSublist } if pList == nil { p.pik_error(pNth, "no such object") return nil } if pNth.eType == T_LAST { pClass = nil } else if pNth.eType == T_LB { pClass = &sublistClass } else { pClass = pik_find_class(pNth) if pClass == nil { (*Pik)(nil).pik_error(pNth, "no such object type") return nil } } n := pNth.eCode if n < 0 { for i := len(pList) - 1; i >= 0; i-- { pObj := pList[i] if pClass != nil && pObj.typ != pClass { continue } n++ if n == 0 { return pObj } } } else { for i := 0; i < len(pList); i++ { pObj := pList[i] if pClass != nil && pObj.typ != pClass { continue } n-- if n == 0 { return pObj } } } p.pik_error(pNth, "no such object") return nil } /* Search for an object by name. ** ** Search in pBasis.pSublist if pBasis is not NULL. If pBasis is NULL ** then search in p.list. */ func (p *Pik) pik_find_byname(pBasis *PObj, pName *PToken) *PObj { var pList PList if pBasis == nil { pList = p.list } else { pList = pBasis.pSublist } if pList == nil { p.pik_error(pName, "no such object") return nil } /* First look explicitly tagged objects */ for i := len(pList) - 1; i >= 0; i-- { pObj := pList[i] if pObj.zName != "" && pik_token_eq(pName, pObj.zName) == 0 { p.lastRef = pObj return pObj } } /* If not found, do a second pass looking for any object containing ** text which exactly matches pName */ for i := len(pList) - 1; i >= 0; i-- { pObj := pList[i] for j := 0; j < int(pObj.nTxt); j++ { t := pObj.aTxt[j].n if t == pName.n+2 && bytesEq(pObj.aTxt[j].z[1:t-1], pName.z[:pName.n]) { p.lastRef = pObj return pObj } } } p.pik_error(pName, "no such object") return nil } /* Change most of the settings for the current object to be the ** same as the pOther object, or the most recent object of the same ** type if pOther is NULL. */ func (p *Pik) pik_same(pOther *PObj, pErrTok *PToken) { pObj := p.cur if p.nErr != 0 { return } if pOther == nil { var i int for i = len(p.list) - 1; i >= 0; i-- { pOther = p.list[i] if pOther.typ == pObj.typ { break } } if i < 0 { p.pik_error(pErrTok, "no prior objects of the same type") return } } if pOther.nPath != 0 && pObj.typ.isLine { var dx, dy PNum dx = p.aTPath[0].x - pOther.aPath[0].x dy = p.aTPath[0].y - pOther.aPath[0].y for i := 1; i < pOther.nPath; i++ { p.aTPath[i].x = pOther.aPath[i].x + dx p.aTPath[i].y = pOther.aPath[i].y + dy } p.nTPath = pOther.nPath p.mTPath = 3 p.samePath = true } if !pObj.typ.isLine { pObj.w = pOther.w pObj.h = pOther.h } pObj.rad = pOther.rad pObj.sw = pOther.sw pObj.dashed = pOther.dashed pObj.dotted = pOther.dotted pObj.fill = pOther.fill pObj.color = pOther.color pObj.cw = pOther.cw pObj.larrow = pOther.larrow pObj.rarrow = pOther.rarrow pObj.bClose = pOther.bClose pObj.bChop = pOther.bChop pObj.inDir = pOther.inDir pObj.outDir = pOther.outDir pObj.iLayer = pOther.iLayer } /* Return a "Place" associated with object pObj. If pEdge is NULL ** return the center of the object. Otherwise, return the corner ** described by pEdge. */ func (p *Pik) pik_place_of_elem(pObj *PObj, pEdge *PToken) PPoint { pt := PPoint{} var pClass *PClass if pObj == nil { return pt } if pEdge == nil { return pObj.ptAt } pClass = pObj.typ if pEdge.eType == T_EDGEPT || (pEdge.eEdge > 0 && pEdge.eEdge < CP_END) { pt = pClass.xOffset(p, pObj, pEdge.eEdge) pt.x += pObj.ptAt.x pt.y += pObj.ptAt.y return pt } if pEdge.eType == T_START { return pObj.ptEnter } else { return pObj.ptExit } } /* Do a linear interpolation of two positions. */ func pik_position_between(x PNum, p1 PPoint, p2 PPoint) PPoint { var out PPoint out.x = p2.x*x + p1.x*(1.0-x) out.y = p2.y*x + p1.y*(1.0-x) return out } /* Compute the position that is dist away from pt at an heading angle of r ** ** The angle is a compass heading in degrees. North is 0 (or 360). ** East is 90. South is 180. West is 270. And so forth. */ func pik_position_at_angle(dist PNum, r PNum, pt PPoint) PPoint { r *= 0.017453292519943295769 /* degrees to radians */ pt.x += dist * math.Sin(r) pt.y += dist * math.Cos(r) return pt } /* Compute the position that is dist away at a compass point */ func pik_position_at_hdg(dist PNum, pD *PToken, pt PPoint) PPoint { return pik_position_at_angle(dist, pik_hdg_angle[pD.eEdge], pt) } /* Return the coordinates for the n-th vertex of a line. */ func (p *Pik) pik_nth_vertex(pNth *PToken, pErr *PToken, pObj *PObj) PPoint { var n int zero := PPoint{} if p.nErr != 0 || pObj == nil { return p.aTPath[0] } if !pObj.typ.isLine { p.pik_error(pErr, "object is not a line") return zero } n, err := strconv.Atoi(string(pNth.z[:pNth.n-2])) if err != nil || n < 1 || n > pObj.nPath { p.pik_error(pNth, "no such vertex") return zero } return pObj.aPath[n-1] } /* Return the value of a property of an object. */ func pik_property_of(pObj *PObj, pProp *PToken) PNum { var v PNum if pObj != nil { switch pProp.eType { case T_HEIGHT: v = pObj.h case T_WIDTH: v = pObj.w case T_RADIUS: v = pObj.rad case T_DIAMETER: v = pObj.rad * 2.0 case T_THICKNESS: v = pObj.sw case T_DASHED: v = pObj.dashed case T_DOTTED: v = pObj.dotted case T_FILL: v = pObj.fill case T_COLOR: v = pObj.color case T_X: v = pObj.ptAt.x case T_Y: v = pObj.ptAt.y case T_TOP: v = pObj.bbox.ne.y case T_BOTTOM: v = pObj.bbox.sw.y case T_LEFT: v = pObj.bbox.sw.x case T_RIGHT: v = pObj.bbox.ne.x } } return v } /* Compute one of the built-in functions */ func (p *Pik) pik_func(pFunc *PToken, x PNum, y PNum) PNum { var v PNum switch pFunc.eCode { case FN_ABS: if x < 0 { v = -x } else { v = x } case FN_COS: v = math.Cos(x) case FN_INT: v = math.Round(x) case FN_SIN: v = math.Sin(x) case FN_SQRT: if x < 0.0 { p.pik_error(pFunc, "sqrt of negative value") v = 0.0 } else { v = math.Sqrt(x) } case FN_MAX: if x > y { v = x } else { v = y } case FN_MIN: if x < y { v = x } else { v = y } default: v = 0.0 } return v } /* Attach a name to an object */ func (p *Pik) pik_elem_setname(pObj *PObj, pName *PToken) { if pObj == nil { return } if pName == nil { return } pObj.zName = pName.String() } /* ** Search for object located at *pCenter that has an xChop method and ** that does not enclose point pOther. ** ** Return a pointer to the object, or NULL if not found. */ func pik_find_chopper(pList PList, pCenter *PPoint, pOther *PPoint) *PObj { if pList == nil { return nil } for i := len(pList) - 1; i >= 0; i-- { pObj := pList[i] if pObj.typ.xChop != nil && pObj.ptAt.x == pCenter.x && pObj.ptAt.y == pCenter.y && !pik_bbox_contains_point(&pObj.bbox, pOther) { return pObj } else if pObj.pSublist != nil { pObj = pik_find_chopper(pObj.pSublist, pCenter, pOther) if pObj != nil { return pObj } } } return nil } /* ** There is a line traveling from pFrom to pTo. ** ** If pObj is not null and is a choppable object, then chop at ** the boundary of pObj - where the line crosses the boundary ** of pObj. ** ** If pObj is NULL or has no xChop method, then search for some ** other object centered at pTo that is choppable and use it ** instead. */ func (p *Pik) pik_autochop(pFrom *PPoint, pTo *PPoint, pObj *PObj) { if pObj == nil || pObj.typ.xChop == nil { pObj = pik_find_chopper(p.list, pTo, pFrom) } if pObj != nil { *pTo = pObj.typ.xChop(p, pObj, pFrom) } } /* This routine runs after all attributes have been received ** on an object. */ func (p *Pik) pik_after_adding_attributes(pObj *PObj) { if p.nErr != 0 { return } /* Position block objects */ if !pObj.typ.isLine { /* A height or width less than or equal to zero means "autofit". ** Change the height or width to be big enough to contain the text, */ if pObj.h <= 0.0 { if pObj.nTxt == 0 { pObj.h = 0.0 } else if pObj.w <= 0.0 { p.pik_size_to_fit(&pObj.errTok, 3) } else { p.pik_size_to_fit(&pObj.errTok, 2) } } if pObj.w <= 0.0 { if pObj.nTxt == 0 { pObj.w = 0.0 } else { p.pik_size_to_fit(&pObj.errTok, 1) } } ofst := p.pik_elem_offset(pObj, pObj.eWith) var dx PNum = (pObj.with.x - ofst.x) - pObj.ptAt.x var dy PNum = (pObj.with.y - ofst.y) - pObj.ptAt.y if dx != 0 || dy != 0 { pik_elem_move(pObj, dx, dy) } } /* For a line object with no movement specified, a single movement ** of the default length in the current direction */ if pObj.typ.isLine && p.nTPath < 2 { p.pik_next_rpath(nil) assert(p.nTPath == 2, fmt.Sprintf("want p.nTPath==2; got %d", p.nTPath)) switch pObj.inDir { default: p.aTPath[1].x += pObj.w case DIR_DOWN: p.aTPath[1].y -= pObj.h case DIR_LEFT: p.aTPath[1].x -= pObj.w case DIR_UP: p.aTPath[1].y += pObj.h } if pObj.typ.zName == "arc" { add := uint8(3) if pObj.cw { add = 1 } pObj.outDir = (pObj.inDir + add) % 4 p.eDir = pObj.outDir switch pObj.outDir { default: p.aTPath[1].x += pObj.w case DIR_DOWN: p.aTPath[1].y -= pObj.h case DIR_LEFT: p.aTPath[1].x -= pObj.w case DIR_UP: p.aTPath[1].y += pObj.h } } } /* Initialize the bounding box prior to running xCheck */ pik_bbox_init(&pObj.bbox) /* Run object-specific code */ if pObj.typ.xCheck != nil { pObj.typ.xCheck(p, pObj) if p.nErr != 0 { return } } /* Compute final bounding box, entry and exit points, center ** point (ptAt) and path for the object */ if pObj.typ.isLine { pObj.aPath = make([]PPoint, p.nTPath) pObj.nPath = p.nTPath copy(pObj.aPath, p.aTPath[:p.nTPath]) /* "chop" processing: ** If the line goes to the center of an object with an ** xChop method, then use the xChop method to trim the line. */ if pObj.bChop && pObj.nPath >= 2 { n := pObj.nPath p.pik_autochop(&pObj.aPath[n-2], &pObj.aPath[n-1], pObj.pTo) p.pik_autochop(&pObj.aPath[1], &pObj.aPath[0], pObj.pFrom) } pObj.ptEnter = pObj.aPath[0] pObj.ptExit = pObj.aPath[pObj.nPath-1] /* Compute the center of the line based on the bounding box over ** the vertexes. This is a difference from PIC. In Pikchr, the ** center of a line is the center of its bounding box. In PIC, the ** center of a line is halfway between its .start and .end. For ** straight lines, this is the same point, but for multi-segment ** lines the result is usually diferent */ for i := 0; i < pObj.nPath; i++ { pik_bbox_add_xy(&pObj.bbox, pObj.aPath[i].x, pObj.aPath[i].y) } pObj.ptAt.x = (pObj.bbox.ne.x + pObj.bbox.sw.x) / 2.0 pObj.ptAt.y = (pObj.bbox.ne.y + pObj.bbox.sw.y) / 2.0 /* Reset the width and height of the object to be the width and height ** of the bounding box over vertexes */ pObj.w = pObj.bbox.ne.x - pObj.bbox.sw.x pObj.h = pObj.bbox.ne.y - pObj.bbox.sw.y /* If this is a polygon (if it has the "close" attribute), then ** adjust the exit point */ if pObj.bClose { /* For "closed" lines, the .end is one of the .e, .s, .w, or .n ** points of the bounding box, as with block objects. */ pik_elem_set_exit(pObj, pObj.inDir) } } else { var w2 PNum = pObj.w / 2.0 var h2 PNum = pObj.h / 2.0 pObj.ptEnter = pObj.ptAt pObj.ptExit = pObj.ptAt switch pObj.inDir { default: pObj.ptEnter.x -= w2 case DIR_LEFT: pObj.ptEnter.x += w2 case DIR_UP: pObj.ptEnter.y -= h2 case DIR_DOWN: pObj.ptEnter.y += h2 } switch pObj.outDir { default: pObj.ptExit.x += w2 case DIR_LEFT: pObj.ptExit.x -= w2 case DIR_UP: pObj.ptExit.y += h2 case DIR_DOWN: pObj.ptExit.y -= h2 } pik_bbox_add_xy(&pObj.bbox, pObj.ptAt.x-w2, pObj.ptAt.y-h2) pik_bbox_add_xy(&pObj.bbox, pObj.ptAt.x+w2, pObj.ptAt.y+h2) } p.eDir = pObj.outDir } /* Show basic information about each object as a comment in the ** generated HTML. Used for testing and debugging. Activated ** by the (undocumented) "debug = 1;" ** command. */ func (p *Pik) pik_elem_render(pObj *PObj) { var zDir string if pObj == nil { return } p.pik_append("<!-- ") if pObj.zName != "" { p.pik_append_text(pObj.zName, 0) p.pik_append(": ") } p.pik_append_text(pObj.typ.zName, 0) if pObj.nTxt != 0 { p.pik_append(" \"") z := pObj.aTxt[0] p.pik_append_text(string(z.z[1:z.n-1]), 1) p.pik_append("\"") } p.pik_append_num(" w=", pObj.w) p.pik_append_num(" h=", pObj.h) p.pik_append_point(" center=", &pObj.ptAt) p.pik_append_point(" enter=", &pObj.ptEnter) switch pObj.outDir { default: zDir = " right" case DIR_LEFT: zDir = " left" case DIR_UP: zDir = " up" case DIR_DOWN: zDir = " down" } p.pik_append_point(" exit=", &pObj.ptExit) p.pik_append(zDir) p.pik_append(" -->\n") } /* Render a list of objects */ func (p *Pik) pik_elist_render(pList PList) { var iNextLayer, iThisLayer int bMoreToDo := true mDebug := p.pik_value_int("debug", nil) for bMoreToDo { bMoreToDo = false iThisLayer = iNextLayer iNextLayer = 0x7fffffff for i := 0; i < len(pList); i++ { pObj := pList[i] if pObj.iLayer > iThisLayer { if pObj.iLayer < iNextLayer { iNextLayer = pObj.iLayer } bMoreToDo = true continue /* Defer until another round */ } else if pObj.iLayer < iThisLayer { continue } if mDebug&1 != 0 { p.pik_elem_render(pObj) } xRender := pObj.typ.xRender if xRender != nil { xRender(p, pObj) } if pObj.pSublist != nil { p.pik_elist_render(pObj.pSublist) } } } /* If the color_debug_label value is defined, then go through ** and paint a dot at every label location */ miss := false var colorLabel PNum = p.pik_value("debug_label_color", &miss) if !miss && colorLabel >= 0.0 { dot := PObj{} dot.typ = &noopClass dot.rad = 0.015 dot.sw = 0.015 dot.fill = colorLabel dot.color = colorLabel dot.nTxt = 1 dot.aTxt[0].eCode = TP_ABOVE for i := 0; i < len(pList); i++ { pObj := pList[i] if pObj.zName == "" { continue } dot.ptAt = pObj.ptAt dot.aTxt[0].z = []byte(pObj.zName) dot.aTxt[0].n = len(dot.aTxt[0].z) dotRender(p, &dot) } } } /* Add all objects of the list pList to the bounding box */ func (p *Pik) pik_bbox_add_elist(pList PList, wArrow PNum) { for i := 0; i < len(pList); i++ { pObj := pList[i] if pObj.sw > 0.0 { pik_bbox_addbox(&p.bbox, &pObj.bbox) } p.pik_append_txt(pObj, &p.bbox) if pObj.pSublist != nil { p.pik_bbox_add_elist(pObj.pSublist, wArrow) } /* Expand the bounding box to account for arrowheads on lines */ if pObj.typ.isLine && pObj.nPath > 0 { if pObj.larrow { pik_bbox_addellipse(&p.bbox, pObj.aPath[0].x, pObj.aPath[0].y, wArrow, wArrow) } if pObj.rarrow { j := pObj.nPath - 1 pik_bbox_addellipse(&p.bbox, pObj.aPath[j].x, pObj.aPath[j].y, wArrow, wArrow) } } } } /* Recompute key layout parameters from variables. */ func (p *Pik) pik_compute_layout_settings() { var thickness PNum /* Line thickness */ var wArrow PNum /* Width of arrowheads */ /* Set up rendering parameters */ if p.bLayoutVars { return } thickness = p.pik_value("thickness", nil) if thickness <= 0.01 { thickness = 0.01 } wArrow = 0.5 * p.pik_value("arrowwid", nil) p.wArrow = wArrow / thickness p.hArrow = p.pik_value("arrowht", nil) / thickness p.fontScale = p.pik_value("fontscale", nil) if p.fontScale <= 0.0 { p.fontScale = 1.0 } p.rScale = 144.0 p.charWidth = p.pik_value("charwid", nil) * p.fontScale p.charHeight = p.pik_value("charht", nil) * p.fontScale p.bLayoutVars = true } /* Render a list of objects. Write the SVG into p.zOut. ** Delete the input object_list before returnning. */ func (p *Pik) pik_render(pList PList) { if pList == nil { return } if p.nErr == 0 { var ( thickness PNum /* Stroke width */ margin PNum /* Extra bounding box margin */ w, h PNum /* Drawing width and height */ wArrow PNum pikScale PNum /* Value of the "scale" variable */ ) /* Set up rendering parameters */ p.pik_compute_layout_settings() thickness = p.pik_value("thickness", nil) if thickness <= 0.01 { thickness = 0.01 } margin = p.pik_value("margin", nil) margin += thickness wArrow = p.wArrow * thickness miss := false p.fgcolor = p.pik_value_int("fgcolor", &miss) if miss { var t PToken t.z = []byte("fgcolor") t.n = 7 p.fgcolor = pik_round((*Pik)(nil).pik_lookup_color(&t)) } miss = false p.bgcolor = p.pik_value_int("bgcolor", &miss) if miss { var t PToken t.z = []byte("bgcolor") t.n = 7 p.bgcolor = pik_round((*Pik)(nil).pik_lookup_color(&t)) } /* Compute a bounding box over all objects so that we can know ** how big to declare the SVG canvas */ pik_bbox_init(&p.bbox) p.pik_bbox_add_elist(pList, wArrow) /* Expand the bounding box slightly to account for line thickness ** and the optional "margin = EXPR" setting. */ p.bbox.ne.x += margin + p.pik_value("rightmargin", nil) p.bbox.ne.y += margin + p.pik_value("topmargin", nil) p.bbox.sw.x -= margin + p.pik_value("leftmargin", nil) p.bbox.sw.y -= margin + p.pik_value("bottommargin", nil) /* Output the SVG */ p.pik_append("<svg xmlns='http://www.w3.org/2000/svg'") if p.zClass != "" { p.pik_append(" class=\"") p.pik_append(p.zClass) p.pik_append("\"") } w = p.bbox.ne.x - p.bbox.sw.x h = p.bbox.ne.y - p.bbox.sw.y p.wSVG = pik_round(p.rScale * w) p.hSVG = pik_round(p.rScale * h) pikScale = p.pik_value("scale", nil) if pikScale >= 0.001 && pikScale <= 1000.0 && (pikScale < 0.99 || pikScale > 1.01) { p.wSVG = pik_round(PNum(p.wSVG) * pikScale) p.hSVG = pik_round(PNum(p.hSVG) * pikScale) p.pik_append_num(" width=\"", PNum(p.wSVG)) p.pik_append_num("\" height=\"", PNum(p.hSVG)) p.pik_append("\"") } else { if p.svgWidth != "" { p.pik_append(` width="`) p.pik_append(p.svgWidth) p.pik_append(`"`) } if p.svgHeight != "" { p.pik_append(` height="`) p.pik_append(p.svgHeight) p.pik_append(`"`) } } p.pik_append_dis(" viewBox=\"0 0 ", w, "") p.pik_append_dis(" ", h, "\">\n") p.pik_elist_render(pList) p.pik_append("</svg>\n") } else { p.wSVG = -1 p.hSVG = -1 } p.pik_elist_free(&pList) } /* ** An array of this structure defines a list of keywords. */ type PikWord struct { zWord string /* Text of the keyword */ //TODO(zellyn): do we need this? nChar uint8 /* Length of keyword text in bytes */ eType uint8 /* Token code */ eCode uint8 /* Extra code for the token */ eEdge uint8 /* CP_* code for corner/edge keywords */ } /* ** Keywords */ var pik_keywords = []PikWord{ {"above", 5, T_ABOVE, 0, 0}, {"abs", 3, T_FUNC1, FN_ABS, 0}, {"aligned", 7, T_ALIGNED, 0, 0}, {"and", 3, T_AND, 0, 0}, {"as", 2, T_AS, 0, 0}, {"assert", 6, T_ASSERT, 0, 0}, {"at", 2, T_AT, 0, 0}, {"behind", 6, T_BEHIND, 0, 0}, {"below", 5, T_BELOW, 0, 0}, {"between", 7, T_BETWEEN, 0, 0}, {"big", 3, T_BIG, 0, 0}, {"bold", 4, T_BOLD, 0, 0}, {"bot", 3, T_EDGEPT, 0, CP_S}, {"bottom", 6, T_BOTTOM, 0, CP_S}, {"c", 1, T_EDGEPT, 0, CP_C}, {"ccw", 3, T_CCW, 0, 0}, {"center", 6, T_CENTER, 0, CP_C}, {"chop", 4, T_CHOP, 0, 0}, {"close", 5, T_CLOSE, 0, 0}, {"color", 5, T_COLOR, 0, 0}, {"cos", 3, T_FUNC1, FN_COS, 0}, {"cw", 2, T_CW, 0, 0}, {"dashed", 6, T_DASHED, 0, 0}, {"define", 6, T_DEFINE, 0, 0}, {"diameter", 8, T_DIAMETER, 0, 0}, {"dist", 4, T_DIST, 0, 0}, {"dotted", 6, T_DOTTED, 0, 0}, {"down", 4, T_DOWN, DIR_DOWN, 0}, {"e", 1, T_EDGEPT, 0, CP_E}, {"east", 4, T_EDGEPT, 0, CP_E}, {"end", 3, T_END, 0, CP_END}, {"even", 4, T_EVEN, 0, 0}, {"fill", 4, T_FILL, 0, 0}, {"first", 5, T_NTH, 0, 0}, {"fit", 3, T_FIT, 0, 0}, {"from", 4, T_FROM, 0, 0}, {"go", 2, T_GO, 0, 0}, {"heading", 7, T_HEADING, 0, 0}, {"height", 6, T_HEIGHT, 0, 0}, {"ht", 2, T_HEIGHT, 0, 0}, {"in", 2, T_IN, 0, 0}, {"int", 3, T_FUNC1, FN_INT, 0}, {"invis", 5, T_INVIS, 0, 0}, {"invisible", 9, T_INVIS, 0, 0}, {"italic", 6, T_ITALIC, 0, 0}, {"last", 4, T_LAST, 0, 0}, {"left", 4, T_LEFT, DIR_LEFT, CP_W}, {"ljust", 5, T_LJUST, 0, 0}, {"max", 3, T_FUNC2, FN_MAX, 0}, {"min", 3, T_FUNC2, FN_MIN, 0}, {"n", 1, T_EDGEPT, 0, CP_N}, {"ne", 2, T_EDGEPT, 0, CP_NE}, {"north", 5, T_EDGEPT, 0, CP_N}, {"nw", 2, T_EDGEPT, 0, CP_NW}, {"of", 2, T_OF, 0, 0}, {"previous", 8, T_LAST, 0, 0}, {"print", 5, T_PRINT, 0, 0}, {"rad", 3, T_RADIUS, 0, 0}, {"radius", 6, T_RADIUS, 0, 0}, {"right", 5, T_RIGHT, DIR_RIGHT, CP_E}, {"rjust", 5, T_RJUST, 0, 0}, {"s", 1, T_EDGEPT, 0, CP_S}, {"same", 4, T_SAME, 0, 0}, {"se", 2, T_EDGEPT, 0, CP_SE}, {"sin", 3, T_FUNC1, FN_SIN, 0}, {"small", 5, T_SMALL, 0, 0}, {"solid", 5, T_SOLID, 0, 0}, {"south", 5, T_EDGEPT, 0, CP_S}, {"sqrt", 4, T_FUNC1, FN_SQRT, 0}, {"start", 5, T_START, 0, CP_START}, {"sw", 2, T_EDGEPT, 0, CP_SW}, {"t", 1, T_TOP, 0, CP_N}, {"the", 3, T_THE, 0, 0}, {"then", 4, T_THEN, 0, 0}, {"thick", 5, T_THICK, 0, 0}, {"thickness", 9, T_THICKNESS, 0, 0}, {"thin", 4, T_THIN, 0, 0}, {"this", 4, T_THIS, 0, 0}, {"to", 2, T_TO, 0, 0}, {"top", 3, T_TOP, 0, CP_N}, {"until", 5, T_UNTIL, 0, 0}, {"up", 2, T_UP, DIR_UP, 0}, {"vertex", 6, T_VERTEX, 0, 0}, {"w", 1, T_EDGEPT, 0, CP_W}, {"way", 3, T_WAY, 0, 0}, {"west", 4, T_EDGEPT, 0, CP_W}, {"wid", 3, T_WIDTH, 0, 0}, {"width", 5, T_WIDTH, 0, 0}, {"with", 4, T_WITH, 0, 0}, {"x", 1, T_X, 0, 0}, {"y", 1, T_Y, 0, 0}, } /* ** Search a PikWordlist for the given keyword. Return a pointer to the ** keyword entry found. Or return 0 if not found. */ func pik_find_word( zIn string, /* Word to search for */ aList []PikWord, /* List to search */ ) *PikWord { first := 0 last := len(aList) - 1 for first <= last { mid := (first + last) / 2 c := strings.Compare(zIn, aList[mid].zWord) if c == 0 { return &aList[mid] } if c < 0 { last = mid - 1 } else { first = mid + 1 } } return nil } /* ** Set a symbolic debugger breakpoint on this routine to receive a ** breakpoint when the "#breakpoint" token is parsed. */ func pik_breakpoint(z []byte) { /* Prevent C compilers from optimizing out this routine. */ if z[2] == 'X' { os.Exit(1) } } var aEntity = []struct { eCode int /* Corresponding token code */ zEntity string /* Name of the HTML entity */ }{ {T_RARROW, "→"}, /* Same as . */ {T_RARROW, "→"}, /* Same as . */ {T_LARROW, "←"}, /* Same as <- */ {T_LARROW, "←"}, /* Same as <- */ {T_LRARROW, "↔"}, /* Same as <. */ } /* ** Return the length of next token. The token starts on ** the pToken->z character. Fill in other fields of the ** pToken object as appropriate. */ func pik_token_length(pToken *PToken, bAllowCodeBlock bool) int { z := pToken.z var i int switch z[0] { case '\\': pToken.eType = T_WHITESPACE for i = 1; z[i] == '\r' || z[i] == ' ' || z[i] == '\t'; i++ { } if z[i] == '\n' { return i + 1 } pToken.eType = T_ERROR return 1 case ';', '\n': pToken.eType = T_EOL return 1 case '"': for i = 1; z[i] != 0; i++ { c := z[i] if c == '\\' { if z[i+1] == 0 { break } i++ continue } if c == '"' { pToken.eType = T_STRING return i + 1 } } pToken.eType = T_ERROR return i case ' ', '\t', '\f', '\r': for i = 1; z[i] == ' ' || z[i] == '\t' || z[i] == '\r' || z[i] == '\f'; i++ { } pToken.eType = T_WHITESPACE return i case '#': for i = 1; z[i] != 0 && z[i] != '\n'; i++ { } pToken.eType = T_WHITESPACE /* If the comment is "#breakpoint" then invoke the pik_breakpoint() ** routine. The pik_breakpoint() routie is a no-op that serves as ** a convenient place to set a gdb breakpoint when debugging. */ if i >= 11 && string(z[:11]) == "#breakpoint" { pik_breakpoint(z) } return i case '/': if z[1] == '*' { for i = 2; z[i] != 0 && (z[i] != '*' || z[i+1] != '/'); i++ { } if z[i] == '*' { pToken.eType = T_WHITESPACE return i + 2 } else { pToken.eType = T_ERROR return i } } else if z[1] == '/' { for i = 2; z[i] != 0 && z[i] != '\n'; i++ { } pToken.eType = T_WHITESPACE return i } else if z[1] == '=' { pToken.eType = T_ASSIGN pToken.eCode = T_SLASH return 2 } else { pToken.eType = T_SLASH return 1 } case '+': if z[1] == '=' { pToken.eType = T_ASSIGN pToken.eCode = T_PLUS return 2 } pToken.eType = T_PLUS return 1 case '*': if z[1] == '=' { pToken.eType = T_ASSIGN pToken.eCode = T_STAR return 2 } pToken.eType = T_STAR return 1 case '%': pToken.eType = T_PERCENT return 1 case '(': pToken.eType = T_LP return 1 case ')': pToken.eType = T_RP return 1 case '[': pToken.eType = T_LB return 1 case ']': pToken.eType = T_RB return 1 case ',': pToken.eType = T_COMMA return 1 case ':': pToken.eType = T_COLON return 1 case '>': pToken.eType = T_GT return 1 case '=': if z[1] == '=' { pToken.eType = T_EQ return 2 } pToken.eType = T_ASSIGN pToken.eCode = T_ASSIGN return 1 case '-': if z[1] == '>' { pToken.eType = T_RARROW return 2 } else if z[1] == '=' { pToken.eType = T_ASSIGN pToken.eCode = T_MINUS return 2 } else { pToken.eType = T_MINUS return 1 } case '<': if z[1] == '-' { if z[2] == '>' { pToken.eType = T_LRARROW return 3 } else { pToken.eType = T_LARROW return 2 } } else { pToken.eType = T_LT return 1 } case 0xe2: if z[1] == 0x86 { if z[2] == 0x90 { pToken.eType = T_LARROW /* <- */ return 3 } if z[2] == 0x92 { pToken.eType = T_RARROW /* . */ return 3 } if z[2] == 0x94 { pToken.eType = T_LRARROW /* <. */ return 3 } } pToken.eType = T_ERROR return 1 case '{': var depth int i = 1 if bAllowCodeBlock { depth = 1 for z[i] != 0 && depth > 0 { var x PToken x.z = z[i:] len := pik_token_length(&x, false) if len == 1 { if z[i] == '{' { depth++ } if z[i] == '}' { depth-- } } i += len } } else { depth = 0 } if depth != 0 { pToken.eType = T_ERROR return 1 } pToken.eType = T_CODEBLOCK return i case '&': for i, ent := range aEntity { if bytencmp(z, aEntity[i].zEntity, len(aEntity[i].zEntity)) == 0 { pToken.eType = uint8(ent.eCode) return len(aEntity[i].zEntity) } } pToken.eType = T_ERROR return 1 default: c := z[0] if c == '.' { c1 := z[1] if islower(c1) { for i = 2; z[i] >= 'a' && z[i] <= 'z'; i++ { } pFound := pik_find_word(string(z[1:i]), pik_keywords) if pFound != nil && (pFound.eEdge > 0 || pFound.eType == T_EDGEPT || pFound.eType == T_START || pFound.eType == T_END) { /* Dot followed by something that is a 2-D place value */ pToken.eType = T_DOT_E } else if pFound != nil && (pFound.eType == T_X || pFound.eType == T_Y) { /* Dot followed by "x" or "y" */ pToken.eType = T_DOT_XY } else { /* Any other "dot" */ pToken.eType = T_DOT_L } return 1 } else if isdigit(c1) { i = 0 /* no-op. Fall through to number handling */ } else if isupper(c1) { for i = 2; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { } pToken.eType = T_DOT_U return 1 } else { pToken.eType = T_ERROR return 1 } } if (c >= '0' && c <= '9') || c == '.' { var nDigit int isInt := true if c != '.' { nDigit = 1 for i = 1; ; i++ { c = z[i] if c < '0' || c > '9' { break } nDigit++ } if i == 1 && (c == 'x' || c == 'X') { for i = 2; z[i] != 0 && isxdigit(z[i]); i++ { } pToken.eType = T_NUMBER return i } } else { isInt = false nDigit = 0 i = 0 } if c == '.' { isInt = false for i++; ; i++ { c = z[i] if c < '0' || c > '9' { break } nDigit++ } } if nDigit == 0 { pToken.eType = T_ERROR return i } if c == 'e' || c == 'E' { iBefore := i i++ c2 := z[i] if c2 == '+' || c2 == '-' { i++ c2 = z[i] } if c2 < '0' || c > '9' { /* This is not an exp */ i = iBefore } else { i++ isInt = false for { c = z[i] if c < '0' || c > '9' { break } i++ } } } var c2 byte if c != 0 { c2 = z[i+1] } if isInt { if (c == 't' && c2 == 'h') || (c == 'r' && c2 == 'd') || (c == 'n' && c2 == 'd') || (c == 's' && c2 == 't') { pToken.eType = T_NTH return i + 2 } } if (c == 'i' && c2 == 'n') || (c == 'c' && c2 == 'm') || (c == 'm' && c2 == 'm') || (c == 'p' && c2 == 't') || (c == 'p' && c2 == 'x') || (c == 'p' && c2 == 'c') { i += 2 } pToken.eType = T_NUMBER return i } else if islower(c) { for i = 1; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { } pFound := pik_find_word(string(z[:i]), pik_keywords) if pFound != nil { pToken.eType = pFound.eType pToken.eCode = int16(pFound.eCode) pToken.eEdge = pFound.eEdge return i } pToken.n = i if pik_find_class(pToken) != nil { pToken.eType = T_CLASSNAME } else { pToken.eType = T_ID } return i } else if c >= 'A' && c <= 'Z' { for i = 1; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { } pToken.eType = T_PLACENAME return i } else if c == '$' && z[1] >= '1' && z[1] <= '9' && !isdigit(z[2]) { pToken.eType = T_PARAMETER pToken.eCode = int16(z[1] - '1') return 2 } else if c == '_' || c == '$' || c == '@' { for i = 1; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { } pToken.eType = T_ID return i } else { pToken.eType = T_ERROR return 1 } } } /* ** Return a pointer to the next non-whitespace token after pThis. ** This is used to help form error messages. */ func pik_next_semantic_token(pThis *PToken) PToken { var x PToken i := pThis.n x.z = pThis.z for { x.z = pThis.z[i:] sz := pik_token_length(&x, true) if x.eType != T_WHITESPACE { x.n = sz return x } i += sz } } /* Parser arguments to a macro invocation ** ** (arg1, arg2, ...) ** ** Arguments are comma-separated, except that commas within string ** literals or with (...), {...}, or [...] do not count. The argument ** list begins and ends with parentheses. There can be at most 9 ** arguments. ** ** Return the number of bytes in the argument list. */ func (p *Pik) pik_parse_macro_args( z []byte, /* Start of the argument list */ n int, /* Available bytes */ args []PToken, /* Fill in with the arguments */ pOuter []PToken, /* Arguments of the next outer context, or NULL */ ) int { nArg := 0 var i, sz int depth := 0 var x PToken if z[0] != '(' { return 0 } args[0].z = z[1:] iStart := 1 for i = 1; i < n && z[i] != ')'; i += sz { x.z = z[i:] sz = pik_token_length(&x, false) if sz != 1 { continue } if z[i] == ',' && depth <= 0 { args[nArg].n = i - iStart if nArg == 8 { x.z = z x.n = 1 p.pik_error(&x, "too many macro arguments - max 9") return 0 } nArg++ args[nArg].z = z[i+1:] iStart = i + 1 depth = 0 } else if z[i] == '(' || z[i] == '{' || z[i] == '[' { depth++ } else if z[i] == ')' || z[i] == '}' || z[i] == ']' { depth-- } } if z[i] == ')' { args[nArg].n = i - iStart /* Remove leading and trailing whitespace from each argument. ** If what remains is one of $1, $2, ... $9 then transfer the ** corresponding argument from the outer context */ for j := 0; j <= nArg; j++ { t := &args[j] for t.n > 0 && isspace(t.z[0]) { t.n-- t.z = t.z[1:] } for t.n > 0 && isspace(t.z[t.n-1]) { t.n-- } if t.n == 2 && t.z[0] == '$' && t.z[1] >= '1' && t.z[1] <= '9' { if pOuter != nil { *t = pOuter[t.z[1]-'1'] } else { t.n = 0 } } } return i + 1 } x.z = z x.n = 1 p.pik_error(&x, "unterminated macro argument list") return 0 } /* ** Split up the content of a PToken into multiple tokens and ** send each to the parser. */ func (p *Pik) pik_tokenize(pIn *PToken, pParser *yyParser, aParam []PToken) { sz := 0 var token PToken for i := 0; i < pIn.n && pIn.z[i] != 0 && p.nErr == 0; i += sz { token.eCode = 0 token.eEdge = 0 token.z = pIn.z[i:] sz = pik_token_length(&token, true) if token.eType == T_WHITESPACE { continue /* no-op */ } if sz > 50000 { token.n = 1 p.pik_error(&token, "token is too long - max length 50000 bytes") break } if token.eType == T_ERROR { token.n = sz p.pik_error(&token, "unrecognized token") break } if sz+i > pIn.n { token.n = pIn.n - i p.pik_error(&token, "syntax error") break } if token.eType == T_PARAMETER { /* Substitute a parameter into the input stream */ if aParam == nil || aParam[token.eCode].n == 0 { continue } token.n = sz if p.nCtx >= len(p.aCtx) { p.pik_error(&token, "macros nested too deep") } else { p.aCtx[p.nCtx] = token p.nCtx++ p.pik_tokenize(&aParam[token.eCode], pParser, nil) p.nCtx-- } continue } if token.eType == T_ID { token.n = sz pMac := p.pik_find_macro(&token) if pMac != nil { args := make([]PToken, 9) j := i + sz if pMac.inUse { p.pik_error(&pMac.macroName, "recursive macro definition") break } token.n = sz if p.nCtx >= len(p.aCtx) { p.pik_error(&token, "macros nested too deep") break } pMac.inUse = true p.aCtx[p.nCtx] = token p.nCtx++ sz += p.pik_parse_macro_args(pIn.z[j:], pIn.n-j, args, aParam) p.pik_tokenize(&pMac.macroBody, pParser, args) p.nCtx-- pMac.inUse = false continue } } if false { // #if 0 n := sz if isspace(token.z[0]) { n = 0 } fmt.Printf("******** Token %s (%d): \"%s\" **************\n", yyTokenName[token.eType], token.eType, string(token.z[:n])) } // #endif token.n = sz p.nToken++ if p.nToken > PIKCHR_TOKEN_LIMIT { p.pik_error(&token, "script is too complex") break } pParser.pik_parser(token.eType, token) } } /* ** Parse the PIKCHR script contained in zText[]. Return a rendering. Or ** if an error is encountered, return the error text. The error message ** is HTML formatted. So regardless of what happens, the return text ** is safe to be insertd into an HTML output stream. ** ** If pnWidth and pnHeight are not NULL, then this routine writes the ** width and height of the <SVG> object into the integers that they ** point to. A value of -1 is written if an error is seen. ** ** If zClass is not NULL, then it is a class name to be included in ** the <SVG> markup. ** ** The returned string is contained in memory obtained from malloc() ** and should be released by the caller. */ func Pikchr( zText []byte, /* Input PIKCHR source text. zero-terminated */ zClass string, /* Add class="%s" to <svg> markup */ mFlags uint, /* Flags used to influence rendering behavior */ svgWidth, svgHeight string, svgFontScale PNum, pnWidth *int, /* Write width of <svg> here, if not NULL */ pnHeight *int, /* Write height here, if not NULL */ ) []byte { s := Pik{} var sParse yyParser s.sIn.n = len(zText) s.sIn.z = append(zText, 0) s.eDir = DIR_RIGHT s.zClass = zClass s.mFlags = mFlags s.svgWidth = svgWidth s.svgHeight = svgHeight s.svgFontScale = svgFontScale sParse.pik_parserInit(&s) if false { // #if 0 pik_parserTrace(os.Stdout, "parser: ") } // #endif s.pik_tokenize(&s.sIn, &sParse, nil) if s.nErr == 0 { var token PToken if s.sIn.n > 0 { token.z = zText[s.sIn.n-1:] } else { token.z = zText } token.n = 1 sParse.pik_parser(0, token) } sParse.pik_parserFinalize() if s.zOut.Len() == 0 && s.nErr == 0 { s.pik_append("<!-- empty pikchr diagram -->\n") } if pnWidth != nil { if s.nErr != 0 { *pnWidth = -1 } else { *pnWidth = s.wSVG } } if pnHeight != nil { if s.nErr != 0 { *pnHeight = -1 } else { *pnHeight = s.hSVG } } return s.zOut.Bytes() } // #if defined(PIKCHR_FUZZ) // #include <stdint.h> // int LLVMFuzzerTestOneInput(const uint8_t *aData, size_t nByte){ // int w,h; // char *zIn, *zOut; // unsigned int mFlags = nByte & 3; // zIn = malloc( nByte + 1 ); // if( zIn==0 ) return 0; // memcpy(zIn, aData, nByte); // zIn[nByte] = 0; // zOut = pikchr(zIn, "pikchr", mFlags, &w, &h); // free(zIn); // free(zOut); // return 0; // } // #endif /* PIKCHR_FUZZ */ // Helpers added for port to Go func isxdigit(b byte) bool { return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F') } func isalnum(b byte) bool { return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') } func isdigit(b byte) bool { return (b >= '0' && b <= '9') } func isspace(b byte) bool { return b == ' ' || b == '\n' || b == '\t' || b == '\f' } func isupper(b byte) bool { return (b >= 'A' && b <= 'Z') } func islower(b byte) bool { return (b >= 'a' && b <= 'z') } func bytencmp(a []byte, s string, n int) int { return strings.Compare(string(a[:n]), s) } func bytesEq(a, b []byte) bool { if len(a) != len(b) { return false } for i, bb := range a { if b[i] != bb { return false } } return true } //line 8253 "pikchr.go" |
Added parser/pikchr/internal/pikchr.y.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 3719 3720 3721 3722 3723 3724 3725 3726 3727 3728 3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754 3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780 3781 3782 3783 3784 3785 3786 3787 3788 3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 3799 3800 3801 3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 3812 3813 3814 3815 3816 3817 3818 3819 3820 3821 3822 3823 3824 3825 3826 3827 3828 3829 3830 3831 3832 3833 3834 3835 3836 3837 3838 3839 3840 3841 3842 3843 3844 3845 3846 3847 3848 3849 3850 3851 3852 3853 3854 3855 3856 3857 3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 3903 3904 3905 3906 3907 3908 3909 3910 3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 3958 3959 3960 3961 3962 3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 3992 3993 3994 3995 3996 3997 3998 3999 4000 4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 4048 4049 4050 4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078 4079 4080 4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 4179 4180 4181 4182 4183 4184 4185 4186 4187 4188 4189 4190 4191 4192 4193 4194 4195 4196 4197 4198 4199 4200 4201 4202 4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 4218 4219 4220 4221 4222 4223 4224 4225 4226 4227 4228 4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243 4244 4245 4246 4247 4248 4249 4250 4251 4252 4253 4254 4255 4256 4257 4258 4259 4260 4261 4262 4263 4264 4265 4266 4267 4268 4269 4270 4271 4272 4273 4274 4275 4276 4277 4278 4279 4280 4281 4282 4283 4284 4285 4286 4287 4288 4289 4290 4291 4292 4293 4294 4295 4296 4297 4298 4299 4300 4301 4302 4303 4304 4305 4306 4307 4308 4309 4310 4311 4312 4313 4314 4315 4316 4317 4318 4319 4320 4321 4322 4323 4324 4325 4326 4327 4328 4329 4330 4331 4332 4333 4334 4335 4336 4337 4338 4339 4340 4341 4342 4343 4344 4345 4346 4347 4348 4349 4350 4351 4352 4353 4354 4355 4356 4357 4358 4359 4360 4361 4362 4363 4364 4365 4366 4367 4368 4369 4370 4371 4372 4373 4374 4375 4376 4377 4378 4379 4380 4381 4382 4383 4384 4385 4386 4387 4388 4389 4390 4391 4392 4393 4394 4395 4396 4397 4398 4399 4400 4401 4402 4403 4404 4405 4406 4407 4408 4409 4410 4411 4412 4413 4414 4415 4416 4417 4418 4419 4420 4421 4422 4423 4424 4425 4426 4427 4428 4429 4430 4431 4432 4433 4434 4435 4436 4437 4438 4439 4440 4441 4442 4443 4444 4445 4446 4447 4448 4449 4450 4451 4452 4453 4454 4455 4456 4457 4458 4459 4460 4461 4462 4463 4464 4465 4466 4467 4468 4469 4470 4471 4472 4473 4474 4475 4476 4477 4478 4479 4480 4481 4482 4483 4484 4485 4486 4487 4488 4489 4490 4491 4492 4493 4494 4495 4496 4497 4498 4499 4500 4501 4502 4503 4504 4505 4506 4507 4508 4509 4510 4511 4512 4513 4514 4515 4516 4517 4518 4519 4520 4521 4522 4523 4524 4525 4526 4527 4528 4529 4530 4531 4532 4533 4534 4535 4536 4537 4538 4539 4540 4541 4542 4543 4544 4545 4546 4547 4548 4549 4550 4551 4552 4553 4554 4555 4556 4557 4558 4559 4560 4561 4562 4563 4564 4565 4566 4567 4568 4569 4570 4571 4572 4573 4574 4575 4576 4577 4578 4579 4580 4581 4582 4583 4584 4585 4586 4587 4588 4589 4590 4591 4592 4593 4594 4595 4596 4597 4598 4599 4600 4601 4602 4603 4604 4605 4606 4607 4608 4609 4610 4611 4612 4613 4614 4615 4616 4617 4618 4619 4620 4621 4622 4623 4624 4625 4626 4627 4628 4629 4630 4631 4632 4633 4634 4635 4636 4637 4638 4639 4640 4641 4642 4643 4644 4645 4646 4647 4648 4649 4650 4651 4652 4653 4654 4655 4656 4657 4658 4659 4660 4661 4662 4663 4664 4665 4666 4667 4668 4669 4670 4671 4672 4673 4674 4675 4676 4677 4678 4679 4680 4681 4682 4683 4684 4685 4686 4687 4688 4689 4690 4691 4692 4693 4694 4695 4696 4697 4698 4699 4700 4701 4702 4703 4704 4705 4706 4707 4708 4709 4710 4711 4712 4713 4714 4715 4716 4717 4718 4719 4720 4721 4722 4723 4724 4725 4726 4727 4728 4729 4730 4731 4732 4733 4734 4735 4736 4737 4738 4739 4740 4741 4742 4743 4744 4745 4746 4747 4748 4749 4750 4751 4752 4753 4754 4755 4756 4757 4758 4759 4760 4761 4762 4763 4764 4765 4766 4767 4768 4769 4770 4771 4772 4773 4774 4775 4776 4777 4778 4779 4780 4781 4782 4783 4784 4785 4786 4787 4788 4789 4790 4791 4792 4793 4794 4795 4796 4797 4798 4799 4800 4801 4802 4803 4804 4805 4806 4807 4808 4809 4810 4811 4812 4813 4814 4815 4816 4817 4818 4819 4820 4821 4822 4823 4824 4825 4826 4827 4828 4829 4830 4831 4832 4833 4834 4835 4836 4837 4838 4839 4840 4841 4842 4843 4844 4845 4846 4847 4848 4849 4850 4851 4852 4853 4854 4855 4856 4857 4858 4859 4860 4861 4862 4863 4864 4865 4866 4867 4868 4869 4870 4871 4872 4873 4874 4875 4876 4877 4878 4879 4880 4881 4882 4883 4884 4885 4886 4887 4888 4889 4890 4891 4892 4893 4894 4895 4896 4897 4898 4899 4900 4901 4902 4903 4904 4905 4906 4907 4908 4909 4910 4911 4912 4913 4914 4915 4916 4917 4918 4919 4920 4921 4922 4923 4924 4925 4926 4927 4928 4929 4930 4931 4932 4933 4934 4935 4936 4937 4938 4939 4940 4941 4942 4943 4944 4945 4946 4947 4948 4949 4950 4951 4952 4953 4954 4955 4956 4957 4958 4959 4960 4961 4962 4963 4964 4965 4966 4967 4968 4969 4970 4971 4972 4973 4974 4975 4976 4977 4978 4979 4980 4981 4982 4983 4984 4985 4986 4987 4988 4989 4990 4991 4992 4993 4994 4995 4996 4997 4998 4999 5000 5001 5002 5003 5004 5005 5006 5007 5008 5009 5010 5011 5012 5013 5014 5015 5016 5017 5018 5019 5020 5021 5022 5023 5024 5025 5026 5027 5028 5029 5030 5031 5032 5033 5034 5035 5036 5037 5038 5039 5040 5041 5042 5043 5044 5045 5046 5047 5048 5049 5050 5051 5052 5053 5054 5055 5056 5057 5058 5059 5060 5061 5062 5063 5064 5065 5066 5067 5068 5069 5070 5071 5072 5073 5074 5075 5076 5077 5078 5079 5080 5081 5082 5083 5084 5085 5086 5087 5088 5089 5090 5091 5092 5093 5094 5095 5096 5097 5098 5099 5100 5101 5102 5103 5104 5105 5106 5107 5108 5109 5110 5111 5112 5113 5114 5115 5116 5117 5118 5119 5120 5121 5122 5123 5124 5125 5126 5127 5128 5129 5130 5131 5132 5133 5134 5135 5136 5137 5138 5139 5140 5141 5142 5143 5144 5145 5146 5147 5148 5149 5150 5151 5152 5153 5154 5155 5156 5157 5158 5159 5160 5161 5162 5163 5164 5165 5166 5167 5168 5169 5170 5171 5172 5173 5174 5175 5176 5177 5178 5179 5180 5181 5182 5183 5184 5185 5186 5187 5188 5189 5190 5191 5192 5193 5194 5195 5196 5197 5198 5199 5200 5201 5202 5203 5204 5205 5206 5207 5208 5209 5210 5211 5212 5213 5214 5215 5216 5217 5218 5219 5220 5221 5222 5223 5224 5225 5226 5227 5228 5229 5230 5231 5232 5233 5234 5235 5236 5237 5238 5239 5240 5241 5242 5243 5244 5245 5246 5247 5248 5249 5250 5251 5252 5253 5254 5255 5256 5257 5258 5259 5260 5261 5262 5263 5264 5265 5266 5267 5268 5269 5270 5271 5272 5273 5274 5275 5276 5277 5278 5279 5280 5281 5282 5283 5284 5285 5286 5287 5288 5289 5290 5291 5292 5293 5294 5295 5296 5297 5298 5299 5300 5301 5302 5303 5304 5305 5306 5307 5308 5309 5310 5311 5312 5313 5314 5315 5316 5317 5318 5319 5320 5321 5322 5323 5324 5325 5326 5327 5328 5329 5330 5331 5332 5333 5334 5335 5336 5337 5338 5339 5340 5341 5342 5343 5344 5345 5346 5347 5348 5349 5350 5351 5352 5353 5354 5355 5356 5357 5358 5359 5360 5361 5362 5363 5364 5365 5366 5367 5368 5369 5370 5371 5372 5373 5374 5375 5376 5377 5378 5379 5380 5381 5382 5383 5384 5385 5386 5387 5388 5389 5390 5391 5392 5393 5394 5395 5396 5397 5398 5399 5400 5401 5402 5403 5404 5405 5406 5407 5408 5409 5410 5411 5412 5413 5414 5415 5416 5417 5418 5419 5420 5421 5422 5423 5424 5425 5426 5427 5428 5429 5430 5431 5432 5433 5434 5435 5436 5437 5438 5439 5440 5441 5442 5443 5444 5445 5446 5447 5448 5449 5450 5451 5452 5453 5454 5455 5456 5457 5458 5459 5460 5461 5462 5463 5464 5465 5466 5467 5468 5469 5470 5471 5472 5473 5474 5475 5476 5477 5478 5479 5480 5481 5482 5483 5484 5485 5486 5487 5488 5489 5490 5491 5492 5493 5494 5495 5496 5497 5498 5499 5500 5501 5502 5503 5504 5505 5506 5507 5508 5509 5510 5511 5512 5513 5514 5515 5516 5517 5518 5519 5520 5521 5522 5523 5524 5525 5526 5527 5528 5529 5530 5531 5532 5533 5534 5535 5536 5537 5538 5539 5540 5541 5542 5543 5544 5545 5546 5547 5548 5549 5550 5551 5552 5553 5554 5555 5556 5557 5558 5559 5560 5561 5562 5563 5564 5565 5566 5567 5568 5569 5570 5571 5572 5573 5574 5575 5576 5577 5578 5579 5580 5581 5582 5583 5584 5585 5586 5587 5588 5589 5590 5591 5592 5593 5594 5595 5596 5597 5598 5599 5600 5601 5602 5603 5604 5605 5606 5607 5608 5609 5610 5611 5612 5613 5614 5615 5616 5617 5618 5619 5620 5621 5622 5623 5624 5625 5626 5627 5628 5629 5630 5631 5632 5633 5634 5635 5636 5637 5638 5639 5640 5641 5642 5643 5644 5645 5646 5647 5648 5649 5650 5651 5652 5653 5654 5655 5656 5657 5658 5659 5660 5661 5662 5663 5664 | %include { //lint:file-ignore *,U1000 Ignore all unused code, it's generated /* ** Zero-Clause BSD license: ** ** Copyright (C) 2020-09-01 by D. Richard Hipp <drh@sqlite.org> ** ** Permission to use, copy, modify, and/or distribute this software for ** any purpose with or without fee is hereby granted. ** **************************************************************************** ** ** This software translates a PIC-inspired diagram language into SVG. ** ** PIKCHR (pronounced like "picture") is *mostly* backwards compatible ** with legacy PIC, though some features of legacy PIC are removed ** (for example, the "sh" command is removed for security) and ** many enhancements are added. ** ** PIKCHR is designed for use in an internet facing web environment. ** In particular, PIKCHR is designed to safely generate benign SVG from ** source text that provided by a hostile agent. ** ** This code was originally written by D. Richard Hipp using documentation ** from prior PIC implementations but without reference to prior code. ** All of the code in this project is original. ** ** This file implements a C-language subroutine that accepts a string ** of PIKCHR language text and generates a second string of SVG output that ** renders the drawing defined by the input. Space to hold the returned ** string is obtained from malloc() and should be freed by the caller. ** NULL might be returned if there is a memory allocation error. ** ** If there are errors in the PIKCHR input, the output will consist of an ** error message and the original PIKCHR input text (inside of <pre>...</pre>). ** ** The subroutine implemented by this file is intended to be stand-alone. ** It uses no external routines other than routines commonly found in ** the standard C library. ** **************************************************************************** ** COMPILING: ** ** The original source text is a mixture of C99 and "Lemon" ** (See https://sqlite.org/src/file/doc/lemon.html). Lemon is an LALR(1) ** parser generator program, similar to Yacc. The grammar of the ** input language is specified in Lemon. C-code is attached. Lemon ** runs to generate a single output file ("pikchr.c") which is then ** compiled to generate the Pikchr library. This header comment is ** preserved in the Lemon output, so you might be reading this in either ** the generated "pikchr.c" file that is output by Lemon, or in the ** "pikchr.y" source file that is input into Lemon. If you make changes, ** you should change the input source file "pikchr.y", not the ** Lemon-generated output file. ** ** Basic compilation steps: ** ** lemon pikchr.y ** cc pikchr.c -o pikchr.o ** ** Add -DPIKCHR_SHELL to add a main() routine that reads input files ** and sends them through Pikchr, for testing. Add -DPIKCHR_FUZZ for ** -fsanitizer=fuzzer testing. ** **************************************************************************** ** IMPLEMENTATION NOTES (for people who want to understand the internal ** operation of this software, perhaps to extend the code or to fix bugs): ** ** Each call to pikchr() uses a single instance of the Pik structure to ** track its internal state. The Pik structure lives for the duration ** of the pikchr() call. ** ** The input is a sequence of objects or "statements". Each statement is ** parsed into a PObj object. These are stored on an extensible array ** called PList. All parameters to each PObj are computed as the ** object is parsed. (Hence, the parameters to a PObj may only refer ** to prior statements.) Once the PObj is completely assembled, it is ** added to the end of a PList and never changes thereafter - except, ** PObj objects that are part of a "[...]" block might have their ** absolute position shifted when the outer [...] block is positioned. ** But apart from this repositioning, PObj objects are unchanged once ** they are added to the list. The order of statements on a PList does ** not change. ** ** After all input has been parsed, the top-level PList is walked to ** generate output. Sub-lists resulting from [...] blocks are scanned ** as they are encountered. All input must be collected and parsed ahead ** of output generation because the size and position of statements must be ** known in order to compute a bounding box on the output. ** ** Each PObj is on a "layer". (The common case is that all PObj's are ** on a single layer, but multiple layers are possible.) A separate pass ** is made through the list for each layer. ** ** After all output is generated, the Pik object and all the PList ** and PObj objects are deallocated and the generated output string is ** returned. Upon any error, the Pik.nErr flag is set, processing quickly ** stops, and the stack unwinds. No attempt is made to continue reading ** input after an error. ** ** Most statements begin with a class name like "box" or "arrow" or "move". ** There is a class named "text" which is used for statements that begin ** with a string literal. You can also specify the "text" class. ** A Sublist ("[...]") is a single object that contains a pointer to ** its substatements, all gathered onto a separate PList object. ** ** Variables go into PVar objects that form a linked list. ** ** Each PObj has zero or one names. Input constructs that attempt ** to assign a new name from an older name, for example: ** ** Abc: Abc + (0.5cm, 0) ** ** Statements like these generate a new "noop" object at the specified ** place and with the given name. As place-names are searched by scanning ** the list in reverse order, this has the effect of overriding the "Abc" ** name when referenced by subsequent objects. */ package internal import ( "bytes" "fmt" "io" "math" "os" "regexp" "strconv" "strings" ) // Limit the number of tokens in a single script to avoid run-away macro expansion attacks. // See forum post https://pikchr.org/home/forumpost/ef8684c6955a411a const PIKCHR_TOKEN_LIMIT = 100000 // Numeric value type PNum = float64 // Compass points const ( CP_N uint8 = iota + 1 CP_NE CP_E CP_SE CP_S CP_SW CP_W CP_NW CP_C /* .center or .c */ CP_END /* .end */ CP_START /* .start */ ) /* Heading angles corresponding to compass points */ var pik_hdg_angle = []PNum{ /* none */ 0.0, /* N */ 0.0, /* NE */ 45.0, /* E */ 90.0, /* SE */ 135.0, /* S */ 180.0, /* SW */ 225.0, /* W */ 270.0, /* NW */ 315.0, /* C */ 0.0, } /* Built-in functions */ const ( FN_ABS = 0 FN_COS = 1 FN_INT = 2 FN_MAX = 3 FN_MIN = 4 FN_SIN = 5 FN_SQRT = 6 ) /* Text position and style flags. Stored in PToken.eCode so limited ** to 15 bits. */ const ( TP_LJUST = 0x0001 /* left justify...... */ TP_RJUST = 0x0002 /* ...Right justify */ TP_JMASK = 0x0003 /* Mask for justification bits */ TP_ABOVE2 = 0x0004 /* Position text way above PObj.ptAt */ TP_ABOVE = 0x0008 /* Position text above PObj.ptAt */ TP_CENTER = 0x0010 /* On the line */ TP_BELOW = 0x0020 /* Position text below PObj.ptAt */ TP_BELOW2 = 0x0040 /* Position text way below PObj.ptAt */ TP_VMASK = 0x007c /* Mask for text positioning flags */ TP_BIG = 0x0100 /* Larger font */ TP_SMALL = 0x0200 /* Smaller font */ TP_XTRA = 0x0400 /* Amplify TP_BIG or TP_SMALL */ TP_SZMASK = 0x0700 /* Font size mask */ TP_ITALIC = 0x1000 /* Italic font */ TP_BOLD = 0x2000 /* Bold font */ TP_FMASK = 0x3000 /* Mask for font style */ TP_ALIGN = 0x4000 /* Rotate to align with the line */ ) /* An object to hold a position in 2-D space */ type PPoint struct { /* X and Y coordinates */ x PNum y PNum } /* A bounding box */ type PBox struct { /* Lower-left and top-right corners */ sw PPoint ne PPoint } /* An Absolute or a relative distance. The absolute distance ** is stored in rAbs and the relative distance is stored in rRel. ** Usually, one or the other will be 0.0. When using a PRel to ** update an existing value, the computation is usually something ** like this: ** ** value = PRel.rAbs + value*PRel.rRel ** */ type PRel struct { rAbs PNum /* Absolute value */ rRel PNum /* Value relative to current value */ } /* A variable created by the ID = EXPR construct of the PIKCHR script ** ** PIKCHR (and PIC) scripts do not use many varaibles, so it is reasonable ** to store them all on a linked list. */ type PVar struct { zName string /* Name of the variable */ val PNum /* Value of the variable */ pNext *PVar /* Next variable in a list of them all */ } /* A single token in the parser input stream */ type PToken struct { z []byte /* Pointer to the token text */ n int /* Length of the token in bytes */ eCode int16 /* Auxiliary code */ eType uint8 /* The numeric parser code */ eEdge uint8 /* Corner value for corner keywords */ } func (p PToken) String() string { return string(p.z[:p.n]) } /* Return negative, zero, or positive if pToken is less than, equal to ** or greater than the zero-terminated string z[] */ func pik_token_eq(pToken *PToken, z string) int { c := bytencmp(pToken.z, z, pToken.n) if c == 0 && len(z) > pToken.n && z[pToken.n] != 0 { c = -1 } return c } /* Extra token types not generated by LEMON but needed by the ** tokenizer */ const ( T_PARAMETER = 253 /* $1, $2, ..., $9 */ T_WHITESPACE = 254 /* Whitespace of comments */ T_ERROR = 255 /* Any text that is not a valid token */ ) /* Directions of movement */ const ( DIR_RIGHT = 0 DIR_DOWN = 1 DIR_LEFT = 2 DIR_UP = 3 ) func ValidDir(x uint8) bool { return x >= 0 && x <= 3 } func IsUpDown(x uint8) bool { return x&1 == 1 } func IsLeftRight(x uint8) bool { return x&1 == 0 } /* Bitmask for the various attributes for PObj. These bits are ** collected in PObj.mProp and PObj.mCalc to check for constraint ** errors. */ const ( A_WIDTH = 0x0001 A_HEIGHT = 0x0002 A_RADIUS = 0x0004 A_THICKNESS = 0x0008 A_DASHED = 0x0010 /* Includes "dotted" */ A_FILL = 0x0020 A_COLOR = 0x0040 A_ARROW = 0x0080 A_FROM = 0x0100 A_CW = 0x0200 A_AT = 0x0400 A_TO = 0x0800 /* one or more movement attributes */ A_FIT = 0x1000 ) /* A single graphics object */ type PObj struct { typ *PClass /* Object type or class */ errTok PToken /* Reference token for error messages */ ptAt PPoint /* Reference point for the object */ ptEnter PPoint /* Entry and exit points */ ptExit PPoint pSublist []*PObj /* Substructure for [...] objects */ zName string /* Name assigned to this statement */ w PNum /* "width" property */ h PNum /* "height" property */ rad PNum /* "radius" property */ sw PNum /* "thickness" property. (Mnemonic: "stroke width")*/ dotted PNum /* "dotted" property. <=0.0 for off */ dashed PNum /* "dashed" property. <=0.0 for off */ fill PNum /* "fill" property. Negative for off */ color PNum /* "color" property */ with PPoint /* Position constraint from WITH clause */ eWith uint8 /* Type of heading point on WITH clause */ cw bool /* True for clockwise arc */ larrow bool /* Arrow at beginning (<- or <->) */ rarrow bool /* Arrow at end (-> or <->) */ bClose bool /* True if "close" is seen */ bChop bool /* True if "chop" is seen */ nTxt uint8 /* Number of text values */ mProp uint /* Masks of properties set so far */ mCalc uint /* Values computed from other constraints */ aTxt [5]PToken /* Text with .eCode holding TP flags */ iLayer int /* Rendering order */ inDir uint8 /* Entry and exit directions */ outDir uint8 nPath int /* Number of path points */ aPath []PPoint /* Array of path points */ pFrom *PObj /* End-point objects of a path */ pTo *PObj bbox PBox /* Bounding box */ } // A list of graphics objects. type PList = []*PObj /* A macro definition */ type PMacro struct { pNext *PMacro /* Next in the list */ macroName PToken /* Name of the macro */ macroBody PToken /* Body of the macro */ inUse bool /* Do not allow recursion */ } /* Each call to the pikchr() subroutine uses an instance of the following ** object to pass around context to all of its subroutines. */ type Pik struct { nErr int /* Number of errors seen */ nToken int // Number of tokens parsed sIn PToken /* Input Pikchr-language text */ zOut bytes.Buffer /* Result accumulates here */ nOut uint /* Bytes written to zOut[] so far */ nOutAlloc uint /* Space allocated to zOut[] */ eDir uint8 /* Current direction */ mFlags uint /* Flags passed to pikchr() */ cur *PObj /* Object under construction */ lastRef *PObj /* Last object references by name */ list []*PObj /* Object list under construction */ pMacros *PMacro /* List of all defined macros */ pVar *PVar /* Application-defined variables */ bbox PBox /* Bounding box around all statements */ /* Cache of layout values. <=0.0 for unknown... */ rScale PNum /* Multiply to convert inches to pixels */ fontScale PNum /* Scale fonts by this percent */ charWidth PNum /* Character width */ charHeight PNum /* Character height */ wArrow PNum /* Width of arrowhead at the fat end */ hArrow PNum /* Ht of arrowhead - dist from tip to fat end */ bLayoutVars bool /* True if cache is valid */ thenFlag bool /* True if "then" seen */ samePath bool /* aTPath copied by "same" */ zClass string /* Class name for the <svg> */ wSVG int /* Width and height of the <svg> */ hSVG int fgcolor int /* foreground color value, or -1 for none */ bgcolor int /* background color value, or -1 for none */ /* Paths for lines are constructed here first, then transferred into ** the PObj object at the end: */ nTPath int /* Number of entries on aTPath[] */ mTPath int /* For last entry, 1: x set, 2: y set */ aTPath [1000]PPoint /* Path under construction */ /* Error contexts */ nCtx int /* Number of error contexts */ aCtx [10]PToken /* Nested error contexts */ svgWidth, svgHeight string // Explicit width/height, if not given by scale. svgFontScale PNum } /* Include PIKCHR_PLAINTEXT_ERRORS among the bits of mFlags on the 3rd ** argument to pikchr() in order to cause error message text to come out ** as text/plain instead of as text/html */ const PIKCHR_PLAINTEXT_ERRORS = 0x0001 /* Include PIKCHR_DARK_MODE among the mFlag bits to invert colors. */ const PIKCHR_DARK_MODE = 0x0002 /* ** The behavior of an object class is defined by an instance of ** this structure. This is the "virtual method" table. */ type PClass struct { zName string /* Name of class */ isLine bool /* True if a line class */ eJust int8 /* Use box-style text justification */ xInit func(*Pik, *PObj) /* Initializer */ xNumProp func(*Pik, *PObj, *PToken) /* Value change notification */ xCheck func(*Pik, *PObj) /* Checks to do after parsing */ xChop func(*Pik, *PObj, *PPoint) PPoint /* Chopper */ xOffset func(*Pik, *PObj, uint8) PPoint /* Offset from .c to edge point */ xFit func(pik *Pik, pobj *PObj, w PNum, h PNum) /* Size to fit text */ xRender func(*Pik, *PObj) /* Render */ } func yytestcase(condition bool) {} } // end %include %name pik_parser %token_prefix T_ %token_type {PToken} %extra_context {p *Pik} %fallback ID EDGEPT. // precedence rules. %left OF. %left PLUS MINUS. %left STAR SLASH PERCENT. %right UMINUS. %type statement_list {[]*PObj} %destructor statement_list {p.pik_elist_free(&$$)} %type statement {*PObj} %destructor statement {p.pik_elem_free($$)} %type unnamed_statement {*PObj} %destructor unnamed_statement {p.pik_elem_free($$)} %type basetype {*PObj} %destructor basetype {p.pik_elem_free($$)} %type expr {PNum} %type numproperty {PToken} %type edge {PToken} %type direction {PToken} %type dashproperty {PToken} %type colorproperty {PToken} %type locproperty {PToken} %type position {PPoint} %type place {PPoint} %type object {*PObj} %type objectname {*PObj} %type nth {PToken} %type textposition {int} %type rvalue {PNum} %type lvalue {PToken} %type even {PToken} %type relexpr {PRel} %type optrelexpr {PRel} %syntax_error { if TOKEN.z != nil && TOKEN.z[0] != 0 { p.pik_error(&TOKEN, "syntax error") }else{ p.pik_error(nil, "syntax error") } } %stack_overflow { p.pik_error(nil, "parser stack overflow") } document ::= statement_list(X). {p.pik_render(X)} statement_list(A) ::= statement(X). { A = p.pik_elist_append(nil,X) } statement_list(A) ::= statement_list(B) EOL statement(X). { A = p.pik_elist_append(B,X) } statement(A) ::= . { A = nil } statement(A) ::= direction(D). { p.pik_set_direction(uint8(D.eCode)); A=nil } statement(A) ::= lvalue(N) ASSIGN(OP) rvalue(X). {p.pik_set_var(&N,X,&OP); A=nil} statement(A) ::= PLACENAME(N) COLON unnamed_statement(X). { A = X; p.pik_elem_setname(X,&N) } statement(A) ::= PLACENAME(N) COLON position(P). { A = p.pik_elem_new(nil,nil,nil) if A!=nil { A.ptAt = P; p.pik_elem_setname(A,&N) }} statement(A) ::= unnamed_statement(X). {A = X} statement(A) ::= print prlist. {p.pik_append("<br>\n"); A=nil} // assert() statements are undocumented and are intended for testing and // debugging use only. If the equality comparison of the assert() fails // then an error message is generated. statement(A) ::= ASSERT LP expr(X) EQ(OP) expr(Y) RP. {A=p.pik_assert(X,&OP,Y)} statement(A) ::= ASSERT LP position(X) EQ(OP) position(Y) RP. {A=p.pik_position_assert(&X,&OP,&Y)} statement(A) ::= DEFINE ID(ID) CODEBLOCK(C). {A=nil; p.pik_add_macro(&ID,&C)} lvalue(A) ::= ID(A). lvalue(A) ::= FILL(A). lvalue(A) ::= COLOR(A). lvalue(A) ::= THICKNESS(A). // PLACENAME might actually be a color name (ex: DarkBlue). But we // cannot make it part of expr due to parsing ambiguities. The // rvalue non-terminal means "general expression or a colorname" rvalue(A) ::= expr(A). rvalue(A) ::= PLACENAME(C). {A = p.pik_lookup_color(&C)} print ::= PRINT. prlist ::= pritem. prlist ::= prlist prsep pritem. pritem ::= FILL(X). {p.pik_append_num("",p.pik_value(X.String(),nil))} pritem ::= COLOR(X). {p.pik_append_num("",p.pik_value(X.String(),nil))} pritem ::= THICKNESS(X). {p.pik_append_num("",p.pik_value(X.String(),nil))} pritem ::= rvalue(X). {p.pik_append_num("",X)} pritem ::= STRING(S). {p.pik_append_text(string(S.z[1:S.n-1]),0)} prsep ::= COMMA. {p.pik_append(" ")} unnamed_statement(A) ::= basetype(X) attribute_list. {A = X; p.pik_after_adding_attributes(A)} basetype(A) ::= CLASSNAME(N). {A = p.pik_elem_new(&N,nil,nil) } basetype(A) ::= STRING(N) textposition(P). {N.eCode = int16(P); A = p.pik_elem_new(nil,&N,nil) } basetype(A) ::= LB savelist(L) statement_list(X) RB(E). { p.list = L; A = p.pik_elem_new(nil,nil,X); if A!=nil {A.errTok = E} } %type savelist {[]*PObj} // No destructor required as this same PList is also held by // an "statement" non-terminal deeper on the stack. savelist(A) ::= . {A = p.list; p.list = nil} direction(A) ::= UP(A). direction(A) ::= DOWN(A). direction(A) ::= LEFT(A). direction(A) ::= RIGHT(A). relexpr(A) ::= expr(B). {A.rAbs = B; A.rRel = 0} relexpr(A) ::= expr(B) PERCENT. {A.rAbs = 0; A.rRel = B/100} optrelexpr(A) ::= relexpr(A). optrelexpr(A) ::= . {A.rAbs = 0; A.rRel = 1.0} attribute_list ::= relexpr(X) alist. {p.pik_add_direction(nil,&X)} attribute_list ::= alist. alist ::=. alist ::= alist attribute. attribute ::= numproperty(P) relexpr(X). { p.pik_set_numprop(&P,&X) } attribute ::= dashproperty(P) expr(X). { p.pik_set_dashed(&P,&X) } attribute ::= dashproperty(P). { p.pik_set_dashed(&P,nil); } attribute ::= colorproperty(P) rvalue(X). { p.pik_set_clrprop(&P,X) } attribute ::= go direction(D) optrelexpr(X). { p.pik_add_direction(&D,&X)} attribute ::= go direction(D) even position(P). {p.pik_evenwith(&D,&P)} attribute ::= CLOSE(E). { p.pik_close_path(&E) } attribute ::= CHOP. { p.cur.bChop = true } attribute ::= FROM(T) position(X). { p.pik_set_from(p.cur,&T,&X) } attribute ::= TO(T) position(X). { p.pik_add_to(p.cur,&T,&X) } attribute ::= THEN(T). { p.pik_then(&T, p.cur) } attribute ::= THEN(E) optrelexpr(D) HEADING(H) expr(A). {p.pik_move_hdg(&D,&H,A,nil,&E)} attribute ::= THEN(E) optrelexpr(D) EDGEPT(C). {p.pik_move_hdg(&D,nil,0,&C,&E)} attribute ::= GO(E) optrelexpr(D) HEADING(H) expr(A). {p.pik_move_hdg(&D,&H,A,nil,&E)} attribute ::= GO(E) optrelexpr(D) EDGEPT(C). {p.pik_move_hdg(&D,nil,0,&C,&E)} attribute ::= boolproperty. attribute ::= AT(A) position(P). { p.pik_set_at(nil,&P,&A) } attribute ::= WITH withclause. attribute ::= SAME(E). {p.pik_same(nil,&E)} attribute ::= SAME(E) AS object(X). {p.pik_same(X,&E)} attribute ::= STRING(T) textposition(P). {p.pik_add_txt(&T,int16(P))} attribute ::= FIT(E). {p.pik_size_to_fit(&E,3) } attribute ::= BEHIND object(X). {p.pik_behind(X)} go ::= GO. go ::= . even ::= UNTIL EVEN WITH. even ::= EVEN WITH. withclause ::= DOT_E edge(E) AT(A) position(P).{ p.pik_set_at(&E,&P,&A) } withclause ::= edge(E) AT(A) position(P). { p.pik_set_at(&E,&P,&A) } // Properties that require an argument numproperty(A) ::= HEIGHT|WIDTH|RADIUS|DIAMETER|THICKNESS(P). {A = P} // Properties with optional arguments dashproperty(A) ::= DOTTED(A). dashproperty(A) ::= DASHED(A). // Color properties colorproperty(A) ::= FILL(A). colorproperty(A) ::= COLOR(A). // Properties with no argument boolproperty ::= CW. {p.cur.cw = true} boolproperty ::= CCW. {p.cur.cw = false} boolproperty ::= LARROW. {p.cur.larrow=true; p.cur.rarrow=false } boolproperty ::= RARROW. {p.cur.larrow=false; p.cur.rarrow=true } boolproperty ::= LRARROW. {p.cur.larrow=true; p.cur.rarrow=true } boolproperty ::= INVIS. {p.cur.sw = 0.0} boolproperty ::= THICK. {p.cur.sw *= 1.5} boolproperty ::= THIN. {p.cur.sw *= 0.67} boolproperty ::= SOLID. {p.cur.sw = p.pik_value("thickness",nil) p.cur.dotted = 0.0; p.cur.dashed = 0.0} textposition(A) ::= . {A = 0} textposition(A) ::= textposition(B) CENTER|LJUST|RJUST|ABOVE|BELOW|ITALIC|BOLD|ALIGNED|BIG|SMALL(F). {A = pik_text_position(B,&F)} position(A) ::= expr(X) COMMA expr(Y). {A.x=X; A.y=Y} position(A) ::= place(A). position(A) ::= place(B) PLUS expr(X) COMMA expr(Y). {A.x=B.x+X; A.y=B.y+Y} position(A) ::= place(B) MINUS expr(X) COMMA expr(Y). {A.x=B.x-X; A.y=B.y-Y} position(A) ::= place(B) PLUS LP expr(X) COMMA expr(Y) RP. {A.x=B.x+X; A.y=B.y+Y} position(A) ::= place(B) MINUS LP expr(X) COMMA expr(Y) RP. {A.x=B.x-X; A.y=B.y-Y} position(A) ::= LP position(X) COMMA position(Y) RP. {A.x=X.x; A.y=Y.y} position(A) ::= LP position(X) RP. {A=X} position(A) ::= expr(X) between position(P1) AND position(P2). {A = pik_position_between(X,P1,P2)} position(A) ::= expr(X) LT position(P1) COMMA position(P2) GT. {A = pik_position_between(X,P1,P2)} position(A) ::= expr(X) ABOVE position(B). {A=B; A.y += X} position(A) ::= expr(X) BELOW position(B). {A=B; A.y -= X} position(A) ::= expr(X) LEFT OF position(B). {A=B; A.x -= X} position(A) ::= expr(X) RIGHT OF position(B). {A=B; A.x += X} position(A) ::= expr(D) ON HEADING EDGEPT(E) OF position(P). {A = pik_position_at_hdg(D,&E,P)} position(A) ::= expr(D) HEADING EDGEPT(E) OF position(P). {A = pik_position_at_hdg(D,&E,P)} position(A) ::= expr(D) EDGEPT(E) OF position(P). {A = pik_position_at_hdg(D,&E,P)} position(A) ::= expr(D) ON HEADING expr(G) FROM position(P). {A = pik_position_at_angle(D,G,P)} position(A) ::= expr(D) HEADING expr(G) FROM position(P). {A = pik_position_at_angle(D,G,P)} between ::= WAY BETWEEN. between ::= BETWEEN. between ::= OF THE WAY BETWEEN. // place2 is the same as place, but excludes the forms like // "RIGHT of object" to avoid a parsing ambiguity with "place .x" // and "place .y" expressions %type place2 {PPoint} place(A) ::= place2(A). place(A) ::= edge(X) OF object(O). {A = p.pik_place_of_elem(O,&X)} place2(A) ::= object(O). {A = p.pik_place_of_elem(O,nil)} place2(A) ::= object(O) DOT_E edge(X). {A = p.pik_place_of_elem(O,&X)} place2(A) ::= NTH(N) VERTEX(E) OF object(X). {A = p.pik_nth_vertex(&N,&E,X)} edge(A) ::= CENTER(A). edge(A) ::= EDGEPT(A). edge(A) ::= TOP(A). edge(A) ::= BOTTOM(A). edge(A) ::= START(A). edge(A) ::= END(A). edge(A) ::= RIGHT(A). edge(A) ::= LEFT(A). object(A) ::= objectname(A). object(A) ::= nth(N). {A = p.pik_find_nth(nil,&N)} object(A) ::= nth(N) OF|IN object(B). {A = p.pik_find_nth(B,&N)} objectname(A) ::= THIS. {A = p.cur} objectname(A) ::= PLACENAME(N). {A = p.pik_find_byname(nil,&N)} objectname(A) ::= objectname(B) DOT_U PLACENAME(N). {A = p.pik_find_byname(B,&N)} nth(A) ::= NTH(N) CLASSNAME(ID). {A=ID; A.eCode = p.pik_nth_value(&N) } nth(A) ::= NTH(N) LAST CLASSNAME(ID). {A=ID; A.eCode = -p.pik_nth_value(&N) } nth(A) ::= LAST CLASSNAME(ID). {A=ID; A.eCode = -1} nth(A) ::= LAST(ID). {A=ID; A.eCode = -1} nth(A) ::= NTH(N) LB(ID) RB. {A=ID; A.eCode = p.pik_nth_value(&N)} nth(A) ::= NTH(N) LAST LB(ID) RB. {A=ID; A.eCode = -p.pik_nth_value(&N)} nth(A) ::= LAST LB(ID) RB. {A=ID; A.eCode = -1 } expr(A) ::= expr(X) PLUS expr(Y). {A=X+Y} expr(A) ::= expr(X) MINUS expr(Y). {A=X-Y} expr(A) ::= expr(X) STAR expr(Y). {A=X*Y} expr(A) ::= expr(X) SLASH(E) expr(Y). { if Y==0.0 { p.pik_error(&E, "division by zero"); A = 0.0 } else{ A = X/Y } } expr(A) ::= MINUS expr(X). [UMINUS] {A=-X} expr(A) ::= PLUS expr(X). [UMINUS] {A=X} expr(A) ::= LP expr(X) RP. {A=X} expr(A) ::= LP FILL|COLOR|THICKNESS(X) RP. {A=p.pik_get_var(&X)} expr(A) ::= NUMBER(N). {A=pik_atof(&N)} expr(A) ::= ID(N). {A=p.pik_get_var(&N)} expr(A) ::= FUNC1(F) LP expr(X) RP. {A = p.pik_func(&F,X,0.0)} expr(A) ::= FUNC2(F) LP expr(X) COMMA expr(Y) RP. {A = p.pik_func(&F,X,Y)} expr(A) ::= DIST LP position(X) COMMA position(Y) RP. {A = pik_dist(&X,&Y)} expr(A) ::= place2(B) DOT_XY X. {A = B.x} expr(A) ::= place2(B) DOT_XY Y. {A = B.y} expr(A) ::= object(B) DOT_L numproperty(P). {A=pik_property_of(B,&P)} expr(A) ::= object(B) DOT_L dashproperty(P). {A=pik_property_of(B,&P)} expr(A) ::= object(B) DOT_L colorproperty(P). {A=pik_property_of(B,&P)} %code { /* Chart of the 148 official CSS color names with their ** corresponding RGB values thru Color Module Level 4: ** https://developer.mozilla.org/en-US/docs/Web/CSS/color_value ** ** Two new names "None" and "Off" are added with a value ** of -1. */ var aColor = []struct { zName string /* Name of the color */ val int /* RGB value */ }{ {"AliceBlue", 0xf0f8ff}, {"AntiqueWhite", 0xfaebd7}, {"Aqua", 0x00ffff}, {"Aquamarine", 0x7fffd4}, {"Azure", 0xf0ffff}, {"Beige", 0xf5f5dc}, {"Bisque", 0xffe4c4}, {"Black", 0x000000}, {"BlanchedAlmond", 0xffebcd}, {"Blue", 0x0000ff}, {"BlueViolet", 0x8a2be2}, {"Brown", 0xa52a2a}, {"BurlyWood", 0xdeb887}, {"CadetBlue", 0x5f9ea0}, {"Chartreuse", 0x7fff00}, {"Chocolate", 0xd2691e}, {"Coral", 0xff7f50}, {"CornflowerBlue", 0x6495ed}, {"Cornsilk", 0xfff8dc}, {"Crimson", 0xdc143c}, {"Cyan", 0x00ffff}, {"DarkBlue", 0x00008b}, {"DarkCyan", 0x008b8b}, {"DarkGoldenrod", 0xb8860b}, {"DarkGray", 0xa9a9a9}, {"DarkGreen", 0x006400}, {"DarkGrey", 0xa9a9a9}, {"DarkKhaki", 0xbdb76b}, {"DarkMagenta", 0x8b008b}, {"DarkOliveGreen", 0x556b2f}, {"DarkOrange", 0xff8c00}, {"DarkOrchid", 0x9932cc}, {"DarkRed", 0x8b0000}, {"DarkSalmon", 0xe9967a}, {"DarkSeaGreen", 0x8fbc8f}, {"DarkSlateBlue", 0x483d8b}, {"DarkSlateGray", 0x2f4f4f}, {"DarkSlateGrey", 0x2f4f4f}, {"DarkTurquoise", 0x00ced1}, {"DarkViolet", 0x9400d3}, {"DeepPink", 0xff1493}, {"DeepSkyBlue", 0x00bfff}, {"DimGray", 0x696969}, {"DimGrey", 0x696969}, {"DodgerBlue", 0x1e90ff}, {"Firebrick", 0xb22222}, {"FloralWhite", 0xfffaf0}, {"ForestGreen", 0x228b22}, {"Fuchsia", 0xff00ff}, {"Gainsboro", 0xdcdcdc}, {"GhostWhite", 0xf8f8ff}, {"Gold", 0xffd700}, {"Goldenrod", 0xdaa520}, {"Gray", 0x808080}, {"Green", 0x008000}, {"GreenYellow", 0xadff2f}, {"Grey", 0x808080}, {"Honeydew", 0xf0fff0}, {"HotPink", 0xff69b4}, {"IndianRed", 0xcd5c5c}, {"Indigo", 0x4b0082}, {"Ivory", 0xfffff0}, {"Khaki", 0xf0e68c}, {"Lavender", 0xe6e6fa}, {"LavenderBlush", 0xfff0f5}, {"LawnGreen", 0x7cfc00}, {"LemonChiffon", 0xfffacd}, {"LightBlue", 0xadd8e6}, {"LightCoral", 0xf08080}, {"LightCyan", 0xe0ffff}, {"LightGoldenrodYellow", 0xfafad2}, {"LightGray", 0xd3d3d3}, {"LightGreen", 0x90ee90}, {"LightGrey", 0xd3d3d3}, {"LightPink", 0xffb6c1}, {"LightSalmon", 0xffa07a}, {"LightSeaGreen", 0x20b2aa}, {"LightSkyBlue", 0x87cefa}, {"LightSlateGray", 0x778899}, {"LightSlateGrey", 0x778899}, {"LightSteelBlue", 0xb0c4de}, {"LightYellow", 0xffffe0}, {"Lime", 0x00ff00}, {"LimeGreen", 0x32cd32}, {"Linen", 0xfaf0e6}, {"Magenta", 0xff00ff}, {"Maroon", 0x800000}, {"MediumAquamarine", 0x66cdaa}, {"MediumBlue", 0x0000cd}, {"MediumOrchid", 0xba55d3}, {"MediumPurple", 0x9370db}, {"MediumSeaGreen", 0x3cb371}, {"MediumSlateBlue", 0x7b68ee}, {"MediumSpringGreen", 0x00fa9a}, {"MediumTurquoise", 0x48d1cc}, {"MediumVioletRed", 0xc71585}, {"MidnightBlue", 0x191970}, {"MintCream", 0xf5fffa}, {"MistyRose", 0xffe4e1}, {"Moccasin", 0xffe4b5}, {"NavajoWhite", 0xffdead}, {"Navy", 0x000080}, {"None", -1}, /* Non-standard addition */ {"Off", -1}, /* Non-standard addition */ {"OldLace", 0xfdf5e6}, {"Olive", 0x808000}, {"OliveDrab", 0x6b8e23}, {"Orange", 0xffa500}, {"OrangeRed", 0xff4500}, {"Orchid", 0xda70d6}, {"PaleGoldenrod", 0xeee8aa}, {"PaleGreen", 0x98fb98}, {"PaleTurquoise", 0xafeeee}, {"PaleVioletRed", 0xdb7093}, {"PapayaWhip", 0xffefd5}, {"PeachPuff", 0xffdab9}, {"Peru", 0xcd853f}, {"Pink", 0xffc0cb}, {"Plum", 0xdda0dd}, {"PowderBlue", 0xb0e0e6}, {"Purple", 0x800080}, {"RebeccaPurple", 0x663399}, {"Red", 0xff0000}, {"RosyBrown", 0xbc8f8f}, {"RoyalBlue", 0x4169e1}, {"SaddleBrown", 0x8b4513}, {"Salmon", 0xfa8072}, {"SandyBrown", 0xf4a460}, {"SeaGreen", 0x2e8b57}, {"Seashell", 0xfff5ee}, {"Sienna", 0xa0522d}, {"Silver", 0xc0c0c0}, {"SkyBlue", 0x87ceeb}, {"SlateBlue", 0x6a5acd}, {"SlateGray", 0x708090}, {"SlateGrey", 0x708090}, {"Snow", 0xfffafa}, {"SpringGreen", 0x00ff7f}, {"SteelBlue", 0x4682b4}, {"Tan", 0xd2b48c}, {"Teal", 0x008080}, {"Thistle", 0xd8bfd8}, {"Tomato", 0xff6347}, {"Turquoise", 0x40e0d0}, {"Violet", 0xee82ee}, {"Wheat", 0xf5deb3}, {"White", 0xffffff}, {"WhiteSmoke", 0xf5f5f5}, {"Yellow", 0xffff00}, {"YellowGreen", 0x9acd32}, } /* Built-in variable names. ** ** This array is constant. When a script changes the value of one of ** these built-ins, a new PVar record is added at the head of ** the Pik.pVar list, which is searched first. Thus the new PVar entry ** will override this default value. ** ** Units are in inches, except for "color" and "fill" which are ** interpreted as 24-bit RGB values. ** ** Binary search used. Must be kept in sorted order. */ var aBuiltin = []struct { zName string val PNum }{ {"arcrad", 0.25}, {"arrowhead", 2.0}, {"arrowht", 0.08}, {"arrowwid", 0.06}, {"boxht", 0.5}, {"boxrad", 0.0}, {"boxwid", 0.75}, {"charht", 0.14}, {"charwid", 0.08}, {"circlerad", 0.25}, {"color", 0.0}, {"cylht", 0.5}, {"cylrad", 0.075}, {"cylwid", 0.75}, {"dashwid", 0.05}, {"dotrad", 0.015}, {"ellipseht", 0.5}, {"ellipsewid", 0.75}, {"fileht", 0.75}, {"filerad", 0.15}, {"filewid", 0.5}, {"fill", -1.0}, {"lineht", 0.5}, {"linewid", 0.5}, {"movewid", 0.5}, {"ovalht", 0.5}, {"ovalwid", 1.0}, {"scale", 1.0}, {"textht", 0.5}, {"textwid", 0.75}, {"thickness", 0.015}, } /* Methods for the "arc" class */ func arcInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("arcrad", nil) pObj.h = pObj.w } /* Hack: Arcs are here rendered as quadratic Bezier curves rather ** than true arcs. Multiple reasons: (1) the legacy-PIC parameters ** that control arcs are obscure and I could not figure out what they ** mean based on available documentation. (2) Arcs are rarely used, ** and so do not seem that important. */ func arcControlPoint(cw bool, f PPoint, t PPoint, rScale PNum) PPoint { var m PPoint var dx, dy PNum m.x = 0.5 * (f.x + t.x) m.y = 0.5 * (f.y + t.y) dx = t.x - f.x dy = t.y - f.y if cw { m.x -= 0.5 * rScale * dy m.y += 0.5 * rScale * dx } else { m.x += 0.5 * rScale * dy m.y -= 0.5 * rScale * dx } return m } func arcCheck(p *Pik, pObj *PObj) { if p.nTPath > 2 { p.pik_error(&pObj.errTok, "arc geometry error") return } m := arcControlPoint(pObj.cw, p.aTPath[0], p.aTPath[1], 0.5) pik_bbox_add_xy(&pObj.bbox, m.x, m.y) } func arcRender(p *Pik, pObj *PObj) { if pObj.nPath < 2 { return } if pObj.sw <= 0.0 { return } f := pObj.aPath[0] t := pObj.aPath[1] m := arcControlPoint(pObj.cw, f, t, 1.0) if pObj.larrow { p.pik_draw_arrowhead(&m, &f, pObj) } if pObj.rarrow { p.pik_draw_arrowhead(&m, &t, pObj) } p.pik_append_xy("<path d=\"M", f.x, f.y) p.pik_append_xy("Q", m.x, m.y) p.pik_append_xy(" ", t.x, t.y) p.pik_append("\" ") p.pik_append_style(pObj, 0) p.pik_append("\" />\n") p.pik_append_txt(pObj, nil) } /* Methods for the "arrow" class */ func arrowInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("linewid", nil) pObj.h = p.pik_value("lineht", nil) pObj.rad = p.pik_value("linerad", nil) pObj.rarrow = true } /* Methods for the "box" class */ func boxInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("boxwid", nil) pObj.h = p.pik_value("boxht", nil) pObj.rad = p.pik_value("boxrad", nil) } /* Return offset from the center of the box to the compass point ** given by parameter cp */ func boxOffset(p *Pik, pObj *PObj, cp uint8) PPoint { pt := PPoint{} var w2 PNum = 0.5 * pObj.w var h2 PNum = 0.5 * pObj.h var rad PNum = pObj.rad var rx PNum if rad <= 0.0 { rx = 0.0 } else { if rad > w2 { rad = w2 } if rad > h2 { rad = h2 } rx = 0.29289321881345252392 * rad } switch cp { case CP_C: case CP_N: pt.x = 0.0 pt.y = h2 case CP_NE: pt.x = w2 - rx pt.y = h2 - rx case CP_E: pt.x = w2 pt.y = 0.0 case CP_SE: pt.x = w2 - rx pt.y = rx - h2 case CP_S: pt.x = 0.0 pt.y = -h2 case CP_SW: pt.x = rx - w2 pt.y = rx - h2 case CP_W: pt.x = -w2 pt.y = 0.0 case CP_NW: pt.x = rx - w2 pt.y = h2 - rx default: assert(false, "false") } return pt } func boxChop(p *Pik, pObj *PObj, pPt *PPoint) PPoint { var dx, dy PNum cp := CP_C chop := pObj.ptAt if pObj.w <= 0.0 { return chop } if pObj.h <= 0.0 { return chop } dx = (pPt.x - pObj.ptAt.x) * pObj.h / pObj.w dy = (pPt.y - pObj.ptAt.y) if dx > 0.0 { if dy >= 2.414*dx { cp = CP_N } else if dy >= 0.414*dx { cp = CP_NE } else if dy >= -0.414*dx { cp = CP_E } else if dy > -2.414*dx { cp = CP_SE } else { cp = CP_S } } else { if dy >= -2.414*dx { cp = CP_N } else if dy >= -0.414*dx { cp = CP_NW } else if dy >= 0.414*dx { cp = CP_W } else if dy > 2.414*dx { cp = CP_SW } else { cp = CP_S } } chop = pObj.typ.xOffset(p, pObj, cp) chop.x += pObj.ptAt.x chop.y += pObj.ptAt.y return chop } func boxFit(p *Pik, pObj *PObj, w PNum, h PNum) { if w > 0 { pObj.w = w } if h > 0 { pObj.h = h } } func boxRender(p *Pik, pObj *PObj) { var w2 PNum = 0.5 * pObj.w var h2 PNum = 0.5 * pObj.h rad := pObj.rad pt := pObj.ptAt if pObj.sw > 0.0 { if rad <= 0.0 { p.pik_append_xy("<path d=\"M", pt.x-w2, pt.y-h2) p.pik_append_xy("L", pt.x+w2, pt.y-h2) p.pik_append_xy("L", pt.x+w2, pt.y+h2) p.pik_append_xy("L", pt.x-w2, pt.y+h2) p.pik_append("Z\" ") } else { /* ** ---- - y3 ** / \ ** / \ _ y2 ** | | ** | | _ y1 ** \ / ** \ / ** ---- _ y0 ** ** ' ' ' ' ** x0 x1 x2 x3 */ if rad > w2 { rad = w2 } if rad > h2 { rad = h2 } var x0 PNum = pt.x - w2 var x1 PNum = x0 + rad var x3 PNum = pt.x + w2 var x2 PNum = x3 - rad var y0 PNum = pt.y - h2 var y1 PNum = y0 + rad var y3 PNum = pt.y + h2 var y2 PNum = y3 - rad p.pik_append_xy("<path d=\"M", x1, y0) if x2 > x1 { p.pik_append_xy("L", x2, y0) } p.pik_append_arc(rad, rad, x3, y1) if y2 > y1 { p.pik_append_xy("L", x3, y2) } p.pik_append_arc(rad, rad, x2, y3) if x2 > x1 { p.pik_append_xy("L", x1, y3) } p.pik_append_arc(rad, rad, x0, y2) if y2 > y1 { p.pik_append_xy("L", x0, y1) } p.pik_append_arc(rad, rad, x1, y0) p.pik_append("Z\" ") } p.pik_append_style(pObj, 3) p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } /* Methods for the "circle" class */ func circleInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("circlerad", nil) * 2 pObj.h = pObj.w pObj.rad = 0.5 * pObj.w } func circleNumProp(p *Pik, pObj *PObj, pId *PToken) { /* For a circle, the width must equal the height and both must ** be twice the radius. Enforce those constraints. */ switch pId.eType { case T_DIAMETER, T_RADIUS: pObj.w = 2.0 * pObj.rad pObj.h = 2.0 * pObj.rad case T_WIDTH: pObj.h = pObj.w pObj.rad = 0.5 * pObj.w case T_HEIGHT: pObj.w = pObj.h pObj.rad = 0.5 * pObj.w } } func circleChop(p *Pik, pObj *PObj, pPt *PPoint) PPoint { var chop PPoint var dx PNum = pPt.x - pObj.ptAt.x var dy PNum = pPt.y - pObj.ptAt.y var dist PNum = math.Hypot(dx, dy) if dist < pObj.rad || dist <= 0 { return pObj.ptAt } chop.x = pObj.ptAt.x + dx*pObj.rad/dist chop.y = pObj.ptAt.y + dy*pObj.rad/dist return chop } func circleFit(p *Pik, pObj *PObj, w PNum, h PNum) { var mx PNum = 0.0 if w > 0 { mx = w } if h > mx { mx = h } if w*h > 0 && (w*w+h*h) > mx*mx { mx = math.Hypot(w, h) } if mx > 0.0 { pObj.rad = 0.5 * mx pObj.w = mx pObj.h = mx } } func circleRender(p *Pik, pObj *PObj) { r := pObj.rad pt := pObj.ptAt if pObj.sw > 0.0 { p.pik_append_x("<circle cx=\"", pt.x, "\"") p.pik_append_y(" cy=\"", pt.y, "\"") p.pik_append_dis(" r=\"", r, "\" ") p.pik_append_style(pObj, 3) p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } /* Methods for the "cylinder" class */ func cylinderInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("cylwid", nil) pObj.h = p.pik_value("cylht", nil) pObj.rad = p.pik_value("cylrad", nil) /* Minor radius of ellipses */ } func cylinderFit(p *Pik, pObj *PObj, w PNum, h PNum) { if w > 0 { pObj.w = w } if h > 0 { pObj.h = h + 0.25*pObj.rad + pObj.sw } } func cylinderRender(p *Pik, pObj *PObj) { var w2 PNum = 0.5 * pObj.w var h2 PNum = 0.5 * pObj.h rad := pObj.rad pt := pObj.ptAt if pObj.sw > 0.0 { if rad > h2 { rad = h2 } else if rad < 0 { rad = 0 } p.pik_append_xy("<path d=\"M", pt.x-w2, pt.y+h2-rad) p.pik_append_xy("L", pt.x-w2, pt.y-h2+rad) p.pik_append_arc(w2, rad, pt.x+w2, pt.y-h2+rad) p.pik_append_xy("L", pt.x+w2, pt.y+h2-rad) p.pik_append_arc(w2, rad, pt.x-w2, pt.y+h2-rad) p.pik_append_arc(w2, rad, pt.x+w2, pt.y+h2-rad) p.pik_append("\" ") p.pik_append_style(pObj, 3) p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } func cylinderOffset(p *Pik, pObj *PObj, cp uint8) PPoint { pt := PPoint{} var w2 PNum = pObj.w * 0.5 var h1 PNum = pObj.h * 0.5 var h2 PNum = h1 - pObj.rad switch cp { case CP_C: case CP_N: pt.x = 0.0 pt.y = h1 case CP_NE: pt.x = w2 pt.y = h2 case CP_E: pt.x = w2 pt.y = 0.0 case CP_SE: pt.x = w2 pt.y = -h2 case CP_S: pt.x = 0.0 pt.y = -h1 case CP_SW: pt.x = -w2 pt.y = -h2 case CP_W: pt.x = -w2 pt.y = 0.0 case CP_NW: pt.x = -w2 pt.y = h2 default: assert(false, "false") } return pt } /* Methods for the "dot" class */ func dotInit(p *Pik, pObj *PObj) { pObj.rad = p.pik_value("dotrad", nil) pObj.h = pObj.rad * 6 pObj.w = pObj.rad * 6 pObj.fill = pObj.color } func dotNumProp(p *Pik, pObj *PObj, pId *PToken) { switch pId.eType { case T_COLOR: pObj.fill = pObj.color case T_FILL: pObj.color = pObj.fill } } func dotCheck(p *Pik, pObj *PObj) { pObj.w = 0 pObj.h = 0 pik_bbox_addellipse(&pObj.bbox, pObj.ptAt.x, pObj.ptAt.y, pObj.rad, pObj.rad) } func dotOffset(p *Pik, pObj *PObj, cp uint8) PPoint { return PPoint{} } func dotRender(p *Pik, pObj *PObj) { r := pObj.rad pt := pObj.ptAt if pObj.sw > 0.0 { p.pik_append_x("<circle cx=\"", pt.x, "\"") p.pik_append_y(" cy=\"", pt.y, "\"") p.pik_append_dis(" r=\"", r, "\"") p.pik_append_style(pObj, 2) p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } /* Methods for the "ellipse" class */ func ellipseInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("ellipsewid", nil) pObj.h = p.pik_value("ellipseht", nil) } func ellipseChop(p *Pik, pObj *PObj, pPt *PPoint) PPoint { var chop PPoint var s, dq, dist PNum var dx PNum = pPt.x - pObj.ptAt.x var dy PNum = pPt.y - pObj.ptAt.y if pObj.w <= 0.0 { return pObj.ptAt } if pObj.h <= 0.0 { return pObj.ptAt } s = pObj.h / pObj.w dq = dx * s dist = math.Hypot(dq, dy) if dist < pObj.h { return pObj.ptAt } chop.x = pObj.ptAt.x + 0.5*dq*pObj.h/(dist*s) chop.y = pObj.ptAt.y + 0.5*dy*pObj.h/dist return chop } func ellipseOffset(p *Pik, pObj *PObj, cp uint8) PPoint { pt := PPoint{} var w PNum = pObj.w * 0.5 var w2 PNum = w * 0.70710678118654747608 var h PNum = pObj.h * 0.5 var h2 PNum = h * 0.70710678118654747608 switch cp { case CP_C: case CP_N: pt.x = 0.0 pt.y = h case CP_NE: pt.x = w2 pt.y = h2 case CP_E: pt.x = w pt.y = 0.0 case CP_SE: pt.x = w2 pt.y = -h2 case CP_S: pt.x = 0.0 pt.y = -h case CP_SW: pt.x = -w2 pt.y = -h2 case CP_W: pt.x = -w pt.y = 0.0 case CP_NW: pt.x = -w2 pt.y = h2 default: assert(false, "false") } return pt } func ellipseRender(p *Pik, pObj *PObj) { w := pObj.w h := pObj.h pt := pObj.ptAt if pObj.sw > 0.0 { p.pik_append_x("<ellipse cx=\"", pt.x, "\"") p.pik_append_y(" cy=\"", pt.y, "\"") p.pik_append_dis(" rx=\"", w/2.0, "\"") p.pik_append_dis(" ry=\"", h/2.0, "\" ") p.pik_append_style(pObj, 3) p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } /* Methods for the "file" object */ func fileInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("filewid", nil) pObj.h = p.pik_value("fileht", nil) pObj.rad = p.pik_value("filerad", nil) } /* Return offset from the center of the file to the compass point ** given by parameter cp */ func fileOffset(p *Pik, pObj *PObj, cp uint8) PPoint { pt := PPoint{} var w2 PNum = 0.5 * pObj.w var h2 PNum = 0.5 * pObj.h var rx PNum = pObj.rad mn := h2 if w2 < h2 { mn = w2 } if rx > mn { rx = mn } if rx < mn*0.25 { rx = mn * 0.25 } pt.x = 0.0 pt.y = 0.0 rx *= 0.5 switch cp { case CP_C: case CP_N: pt.x = 0.0 pt.y = h2 case CP_NE: pt.x = w2 - rx pt.y = h2 - rx case CP_E: pt.x = w2 pt.y = 0.0 case CP_SE: pt.x = w2 pt.y = -h2 case CP_S: pt.x = 0.0 pt.y = -h2 case CP_SW: pt.x = -w2 pt.y = -h2 case CP_W: pt.x = -w2 pt.y = 0.0 case CP_NW: pt.x = -w2 pt.y = h2 default: assert(false, "false") } return pt } func fileFit(p *Pik, pObj *PObj, w PNum, h PNum) { if w > 0 { pObj.w = w } if h > 0 { pObj.h = h + 2*pObj.rad } } func fileRender(p *Pik, pObj *PObj) { var w2 PNum = 0.5 * pObj.w var h2 PNum = 0.5 * pObj.h rad := pObj.rad pt := pObj.ptAt mn := h2 if w2 < h2 { mn = w2 } if rad > mn { rad = mn } if rad < mn*0.25 { rad = mn * 0.25 } if pObj.sw > 0.0 { p.pik_append_xy("<path d=\"M", pt.x-w2, pt.y-h2) p.pik_append_xy("L", pt.x+w2, pt.y-h2) p.pik_append_xy("L", pt.x+w2, pt.y+(h2-rad)) p.pik_append_xy("L", pt.x+(w2-rad), pt.y+h2) p.pik_append_xy("L", pt.x-w2, pt.y+h2) p.pik_append("Z\" ") p.pik_append_style(pObj, 1) p.pik_append("\" />\n") p.pik_append_xy("<path d=\"M", pt.x+(w2-rad), pt.y+h2) p.pik_append_xy("L", pt.x+(w2-rad), pt.y+(h2-rad)) p.pik_append_xy("L", pt.x+w2, pt.y+(h2-rad)) p.pik_append("\" ") p.pik_append_style(pObj, 0) p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } /* Methods for the "line" class */ func lineInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("linewid", nil) pObj.h = p.pik_value("lineht", nil) pObj.rad = p.pik_value("linerad", nil) } func lineOffset(p *Pik, pObj *PObj, cp uint8) PPoint { if false { // #if 0 /* In legacy PIC, the .center of an unclosed line is half way between ** its .start and .end. */ if cp == CP_C && !pObj.bClose { var out PPoint out.x = 0.5*(pObj.ptEnter.x+pObj.ptExit.x) - pObj.ptAt.x out.y = 0.5*(pObj.ptEnter.x+pObj.ptExit.y) - pObj.ptAt.y return out } } // #endif return boxOffset(p, pObj, cp) } func lineRender(p *Pik, pObj *PObj) { if pObj.sw > 0.0 { z := "<path d=\"M" n := pObj.nPath if pObj.larrow { p.pik_draw_arrowhead(&pObj.aPath[1], &pObj.aPath[0], pObj) } if pObj.rarrow { p.pik_draw_arrowhead(&pObj.aPath[n-2], &pObj.aPath[n-1], pObj) } for i := 0; i < pObj.nPath; i++ { p.pik_append_xy(z, pObj.aPath[i].x, pObj.aPath[i].y) z = "L" } if pObj.bClose { p.pik_append("Z") } else { pObj.fill = -1.0 } p.pik_append("\" ") if pObj.bClose { p.pik_append_style(pObj, 3) } else { p.pik_append_style(pObj, 0) } p.pik_append("\" />\n") } p.pik_append_txt(pObj, nil) } /* Methods for the "move" class */ func moveInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("movewid", nil) pObj.h = pObj.w pObj.fill = -1.0 pObj.color = -1.0 pObj.sw = -1.0 } func moveRender(p *Pik, pObj *PObj) { /* No-op */ } /* Methods for the "oval" class */ func ovalInit(p *Pik, pObj *PObj) { pObj.h = p.pik_value("ovalht", nil) pObj.w = p.pik_value("ovalwid", nil) if pObj.h < pObj.w { pObj.rad = 0.5 * pObj.h } else { pObj.rad = 0.5 * pObj.w } } func ovalNumProp(p *Pik, pObj *PObj, pId *PToken) { /* Always adjust the radius to be half of the smaller of ** the width and height. */ if pObj.h < pObj.w { pObj.rad = 0.5 * pObj.h } else { pObj.rad = 0.5 * pObj.w } } func ovalFit(p *Pik, pObj *PObj, w PNum, h PNum) { if w > 0 { pObj.w = w } if h > 0 { pObj.h = h } if pObj.w < pObj.h { pObj.w = pObj.h } if pObj.h < pObj.w { pObj.rad = 0.5 * pObj.h } else { pObj.rad = 0.5 * pObj.w } } /* Methods for the "spline" class */ func splineInit(p *Pik, pObj *PObj) { pObj.w = p.pik_value("linewid", nil) pObj.h = p.pik_value("lineht", nil) pObj.rad = 1000 } /* Return a point along the path from "f" to "t" that is r units ** prior to reaching "t", except if the path is less than 2*r total, ** return the midpoint. */ func radiusMidpoint(f PPoint, t PPoint, r PNum, pbMid *bool) PPoint { var dx PNum = t.x - f.x var dy PNum = t.y - f.y var dist PNum = math.Hypot(dx, dy) if dist <= 0.0 { return t } dx /= dist dy /= dist if r > 0.5*dist { r = 0.5 * dist *pbMid = true } else { *pbMid = false } return PPoint{ x: t.x - r*dx, y: t.y - r*dy, } } func (p *Pik) radiusPath(pObj *PObj, r PNum) { n := pObj.nPath a := pObj.aPath an := a[n-1] isMid := false iLast := n - 1 if pObj.bClose { iLast = n } p.pik_append_xy("<path d=\"M", a[0].x, a[0].y) m := radiusMidpoint(a[0], a[1], r, &isMid) p.pik_append_xy(" L ", m.x, m.y) for i := 1; i < iLast; i++ { an = a[0] if i < n-1 { an = a[i+1] } m = radiusMidpoint(an, a[i], r, &isMid) p.pik_append_xy(" Q ", a[i].x, a[i].y) p.pik_append_xy(" ", m.x, m.y) if !isMid { m = radiusMidpoint(a[i], an, r, &isMid) p.pik_append_xy(" L ", m.x, m.y) } } p.pik_append_xy(" L ", an.x, an.y) if pObj.bClose { p.pik_append("Z") } else { pObj.fill = -1.0 } p.pik_append("\" ") if pObj.bClose { p.pik_append_style(pObj, 3) } else { p.pik_append_style(pObj, 0) } p.pik_append("\" />\n") } func splineRender(p *Pik, pObj *PObj) { if pObj.sw > 0.0 { n := pObj.nPath r := pObj.rad if n < 3 || r <= 0.0 { lineRender(p, pObj) return } if pObj.larrow { p.pik_draw_arrowhead(&pObj.aPath[1], &pObj.aPath[0], pObj) } if pObj.rarrow { p.pik_draw_arrowhead(&pObj.aPath[n-2], &pObj.aPath[n-1], pObj) } p.radiusPath(pObj, pObj.rad) } p.pik_append_txt(pObj, nil) } /* Methods for the "text" class */ func textInit(p *Pik, pObj *PObj) { p.pik_value("textwid", nil) p.pik_value("textht", nil) pObj.sw = 0.0 } func textOffset(p *Pik, pObj *PObj, cp uint8) PPoint { /* Automatically slim-down the width and height of text ** statements so that the bounding box tightly encloses the text, ** then get boxOffset() to do the offset computation. */ p.pik_size_to_fit(&pObj.errTok, 3) return boxOffset(p, pObj, cp) } /* Methods for the "sublist" class */ func sublistInit(p *Pik, pObj *PObj) { pList := pObj.pSublist pik_bbox_init(&pObj.bbox) for i := 0; i < len(pList); i++ { pik_bbox_addbox(&pObj.bbox, &pList[i].bbox) } pObj.w = pObj.bbox.ne.x - pObj.bbox.sw.x pObj.h = pObj.bbox.ne.y - pObj.bbox.sw.y pObj.ptAt.x = 0.5 * (pObj.bbox.ne.x + pObj.bbox.sw.x) pObj.ptAt.y = 0.5 * (pObj.bbox.ne.y + pObj.bbox.sw.y) pObj.mCalc |= A_WIDTH | A_HEIGHT | A_RADIUS } /* ** The following array holds all the different kinds of objects. ** The special [] object is separate. */ var aClass = []PClass{ { zName: "arc", isLine: true, eJust: 0, xInit: arcInit, xNumProp: nil, xCheck: arcCheck, xChop: nil, xOffset: boxOffset, xFit: nil, xRender: arcRender, }, { zName: "arrow", isLine: true, eJust: 0, xInit: arrowInit, xNumProp: nil, xCheck: nil, xChop: nil, xOffset: lineOffset, xFit: nil, xRender: splineRender, }, { zName: "box", isLine: false, eJust: 1, xInit: boxInit, xNumProp: nil, xCheck: nil, xChop: boxChop, xOffset: boxOffset, xFit: boxFit, xRender: boxRender, }, { zName: "circle", isLine: false, eJust: 0, xInit: circleInit, xNumProp: circleNumProp, xCheck: nil, xChop: circleChop, xOffset: ellipseOffset, xFit: circleFit, xRender: circleRender, }, { zName: "cylinder", isLine: false, eJust: 1, xInit: cylinderInit, xNumProp: nil, xCheck: nil, xChop: boxChop, xOffset: cylinderOffset, xFit: cylinderFit, xRender: cylinderRender, }, { zName: "dot", isLine: false, eJust: 0, xInit: dotInit, xNumProp: dotNumProp, xCheck: dotCheck, xChop: circleChop, xOffset: dotOffset, xFit: nil, xRender: dotRender, }, { zName: "ellipse", isLine: false, eJust: 0, xInit: ellipseInit, xNumProp: nil, xCheck: nil, xChop: ellipseChop, xOffset: ellipseOffset, xFit: boxFit, xRender: ellipseRender, }, { zName: "file", isLine: false, eJust: 1, xInit: fileInit, xNumProp: nil, xCheck: nil, xChop: boxChop, xOffset: fileOffset, xFit: fileFit, xRender: fileRender, }, { zName: "line", isLine: true, eJust: 0, xInit: lineInit, xNumProp: nil, xCheck: nil, xChop: nil, xOffset: lineOffset, xFit: nil, xRender: splineRender, }, { zName: "move", isLine: true, eJust: 0, xInit: moveInit, xNumProp: nil, xCheck: nil, xChop: nil, xOffset: boxOffset, xFit: nil, xRender: moveRender, }, { zName: "oval", isLine: false, eJust: 1, xInit: ovalInit, xNumProp: ovalNumProp, xCheck: nil, xChop: boxChop, xOffset: boxOffset, xFit: ovalFit, xRender: boxRender, }, { zName: "spline", isLine: true, eJust: 0, xInit: splineInit, xNumProp: nil, xCheck: nil, xChop: nil, xOffset: lineOffset, xFit: nil, xRender: splineRender, }, { zName: "text", isLine: false, eJust: 0, xInit: textInit, xNumProp: nil, xCheck: nil, xChop: boxChop, xOffset: textOffset, xFit: boxFit, xRender: boxRender, }, } var sublistClass = PClass{ zName: "[]", isLine: false, eJust: 0, xInit: sublistInit, xNumProp: nil, xCheck: nil, xChop: nil, xOffset: boxOffset, xFit: nil, xRender: nil, } var noopClass = PClass{ zName: "noop", isLine: false, eJust: 0, xInit: nil, xNumProp: nil, xCheck: nil, xChop: nil, xOffset: boxOffset, xFit: nil, xRender: nil, } /* ** Reduce the length of the line segment by amt (if possible) by ** modifying the location of *t. */ func pik_chop(f *PPoint, t *PPoint, amt PNum) { var dx PNum = t.x - f.x var dy PNum = t.y - f.y var dist PNum = math.Hypot(dx, dy) if dist <= amt { *t = *f return } var r PNum = 1.0 - amt/dist t.x = f.x + r*dx t.y = f.y + r*dy } /* ** Draw an arrowhead on the end of the line segment from pFrom to pTo. ** Also, shorten the line segment (by changing the value of pTo) so that ** the shaft of the arrow does not extend into the arrowhead. */ func (p *Pik) pik_draw_arrowhead(f *PPoint, t *PPoint, pObj *PObj) { var dx PNum = t.x - f.x var dy PNum = t.y - f.y var dist PNum = math.Hypot(dx, dy) var h PNum = p.hArrow * pObj.sw var w PNum = p.wArrow * pObj.sw if pObj.color < 0.0 { return } if pObj.sw <= 0.0 { return } if dist <= 0.0 { return } /* Unable */ dx /= dist dy /= dist var e1 PNum = dist - h if e1 < 0.0 { e1 = 0.0 h = dist } var ddx PNum = -w * dy var ddy PNum = w * dx var bx PNum = f.x + e1*dx var by PNum = f.y + e1*dy p.pik_append_xy("<polygon points=\"", t.x, t.y) p.pik_append_xy(" ", bx-ddx, by-ddy) p.pik_append_xy(" ", bx+ddx, by+ddy) p.pik_append_clr("\" style=\"fill:", pObj.color, "\"/>\n", false) pik_chop(f, t, h/2) } /* ** Compute the relative offset to an edge location from the reference for a ** an statement. */ func (p *Pik) pik_elem_offset(pObj *PObj, cp uint8) PPoint { return pObj.typ.xOffset(p, pObj, cp) } /* ** Append raw text to zOut */ func (p *Pik) pik_append(zText string) { p.zOut.WriteString(zText) } var re_entity = regexp.MustCompile(`&(#([0-9]{2,});)|([a-zA-Z][a-zA-Z0-9]+;)`) /* ** Append text to zOut with HTML characters escaped. ** ** * The space character is changed into non-breaking space (U+00a0) ** if mFlags has the 0x01 bit set. This is needed when outputting ** text to preserve leading and trailing whitespace. Turns out we ** cannot use as that is an HTML-ism and is not valid in XML. ** ** * The "&" character is changed into "&" if mFlags has the ** 0x02 bit set. This is needed when generating error message text. ** ** * Except for the above, only "<" and ">" are escaped. */ func (p *Pik) pik_append_text(zText string, mFlags int) { bQSpace := mFlags&1 > 0 bQAmp := mFlags&2 > 0 last := 0 var html string for i := 0; i < len(zText); i++ { switch zText[i] { case '<': html = "<" case '>': html = ">" case '&': if !bQAmp { continue } match := re_entity.FindStringSubmatch(zText[i:]) if len(match) > 0 { continue } html = "&" case ' ': if !bQSpace { continue } html = "\u00a0" default: continue } p.pik_append(zText[last:i]) p.pik_append(html) last = i + 1 } p.pik_append(zText[last:]) } /* ** Append error message text. This is either a raw append, or an append ** with HTML escapes, depending on whether the PIKCHR_PLAINTEXT_ERRORS flag ** is set. */ func (p *Pik) pik_append_errtxt(zText string) { if p.mFlags&PIKCHR_PLAINTEXT_ERRORS != 0 { p.pik_append(zText) } else { p.pik_append_text(zText, 0) } } /* Append a PNum value */ func (p *Pik) pik_append_num(z string, v PNum) { p.pik_append(z) p.pik_append(fmt.Sprintf("%.10g", v)) } /* Append a PPoint value (Used for debugging only) */ func (p *Pik) pik_append_point(z string, pPt *PPoint) { buf := fmt.Sprintf("%.10g,%.10g", pPt.x, pPt.y) p.pik_append(z) p.pik_append(buf) } /* ** Invert the RGB color so that it is appropriate for dark mode. ** Variable x hold the initial color. The color is intended for use ** as a background color if isBg is true, and as a foreground color ** if isBg is false. */ func pik_color_to_dark_mode(x int, isBg bool) int { x = 0xffffff - x r := (x >> 16) & 0xff g := (x >> 8) & 0xff b := x & 0xff mx := r if g > mx { mx = g } if b > mx { mx = b } mn := r if g < mn { mn = g } if b < mn { mn = b } r = mn + (mx - r) g = mn + (mx - g) b = mn + (mx - b) if isBg { if mx > 127 { r = (127 * r) / mx g = (127 * g) / mx b = (127 * b) / mx } } else { if mn < 128 && mx > mn { r = 127 + ((r-mn)*128)/(mx-mn) g = 127 + ((g-mn)*128)/(mx-mn) b = 127 + ((b-mn)*128)/(mx-mn) } } return r*0x10000 + g*0x100 + b } /* Append a PNum value surrounded by text. Do coordinate transformations ** on the value. */ func (p *Pik) pik_append_x(z1 string, v PNum, z2 string) { v -= p.bbox.sw.x p.pik_append(fmt.Sprintf("%s%d%s", z1, pik_round(p.rScale*v), z2)) } func (p *Pik) pik_append_y(z1 string, v PNum, z2 string) { v = p.bbox.ne.y - v p.pik_append(fmt.Sprintf("%s%d%s", z1, pik_round(p.rScale*v), z2)) } func (p *Pik) pik_append_xy(z1 string, x PNum, y PNum) { x = x - p.bbox.sw.x y = p.bbox.ne.y - y p.pik_append(fmt.Sprintf("%s%d,%d", z1, pik_round(p.rScale*x), pik_round(p.rScale*y))) } func (p *Pik) pik_append_dis(z1 string, v PNum, z2 string) { p.pik_append(fmt.Sprintf("%s%.6g%s", z1, p.rScale*v, z2)) } /* Append a color specification to the output. ** ** In PIKCHR_DARK_MODE, the color is inverted. The "bg" flags indicates that ** the color is intended for use as a background color if true, or as a ** foreground color if false. The distinction only matters for color ** inversions in PIKCHR_DARK_MODE. */ func (p *Pik) pik_append_clr(z1 string, v PNum, z2 string, bg bool) { x := pik_round(v) if x == 0 && p.fgcolor > 0 && !bg { x = p.fgcolor } else if bg && x >= 0xffffff && p.bgcolor > 0 { x = p.bgcolor } else if p.mFlags&PIKCHR_DARK_MODE != 0 { x = pik_color_to_dark_mode(x, bg) } r := (x >> 16) & 0xff g := (x >> 8) & 0xff b := x & 0xff buf := fmt.Sprintf("%srgb(%d,%d,%d)%s", z1, r, g, b, z2) p.pik_append(buf) } /* Append an SVG path A record: ** ** A r1 r2 0 0 0 x y */ func (p *Pik) pik_append_arc(r1 PNum, r2 PNum, x PNum, y PNum) { x = x - p.bbox.sw.x y = p.bbox.ne.y - y buf := fmt.Sprintf("A%d %d 0 0 0 %d %d", pik_round(p.rScale*r1), pik_round(p.rScale*r2), pik_round(p.rScale*x), pik_round(p.rScale*y)) p.pik_append(buf) } /* Append a style="..." text. But, leave the quote unterminated, in case ** the caller wants to add some more. ** ** eFill is non-zero to fill in the background, or 0 if no fill should ** occur. Non-zero values of eFill determine the "bg" flag to pik_append_clr() ** for cases when pObj.fill==pObj.color ** ** 1 fill is background, and color is foreground. ** 2 fill and color are both foreground. (Used by "dot" objects) ** 3 fill and color are both background. (Used by most other objs) */ func (p *Pik) pik_append_style(pObj *PObj, eFill int) { clrIsBg := false p.pik_append(" style=\"") if pObj.fill >= 0 && eFill != 0 { fillIsBg := true if pObj.fill == pObj.color { if eFill == 2 { fillIsBg = false } if eFill == 3 { clrIsBg = true } } p.pik_append_clr("fill:", pObj.fill, ";", fillIsBg) } else { p.pik_append("fill:none;") } if pObj.sw > 0.0 && pObj.color >= 0.0 { sw := pObj.sw p.pik_append_dis("stroke-width:", sw, ";") if pObj.nPath > 2 && pObj.rad <= pObj.sw { p.pik_append("stroke-linejoin:round;") } p.pik_append_clr("stroke:", pObj.color, ";", clrIsBg) if pObj.dotted > 0.0 { v := pObj.dotted if sw < 2.1/p.rScale { sw = 2.1 / p.rScale } p.pik_append_dis("stroke-dasharray:", sw, "") p.pik_append_dis(",", v, ";") } else if pObj.dashed > 0.0 { v := pObj.dashed p.pik_append_dis("stroke-dasharray:", v, "") p.pik_append_dis(",", v, ";") } } } /* ** Compute the vertical locations for all text items in the ** object pObj. In other words, set every pObj.aTxt[*].eCode ** value to contain exactly one of: TP_ABOVE2, TP_ABOVE, TP_CENTER, ** TP_BELOW, or TP_BELOW2 is set. */ func pik_txt_vertical_layout(pObj *PObj) { n := int(pObj.nTxt) if n == 0 { return } aTxt := pObj.aTxt[:] if n == 1 { if (aTxt[0].eCode & TP_VMASK) == 0 { aTxt[0].eCode |= TP_CENTER } } else { allSlots := int16(0) var aFree [5]int16 /* If there is more than one TP_ABOVE, change the first to TP_ABOVE2. */ for j, mJust, i := 0, int16(0), n-1; i >= 0; i-- { if aTxt[i].eCode&TP_ABOVE != 0 { if j == 0 { j++ mJust = aTxt[i].eCode & TP_JMASK } else if j == 1 && mJust != 0 && (aTxt[i].eCode&mJust) == 0 { j++ } else { aTxt[i].eCode = (aTxt[i].eCode &^ TP_VMASK) | TP_ABOVE2 break } } } /* If there is more than one TP_BELOW, change the last to TP_BELOW2 */ for j, mJust, i := 0, int16(0), 0; i < n; i++ { if aTxt[i].eCode&TP_BELOW != 0 { if j == 0 { j++ mJust = aTxt[i].eCode & TP_JMASK } else if j == 1 && mJust != 0 && (aTxt[i].eCode&mJust) == 0 { j++ } else { aTxt[i].eCode = (aTxt[i].eCode &^ TP_VMASK) | TP_BELOW2 break } } } /* Compute a mask of all slots used */ for i := 0; i < n; i++ { allSlots |= aTxt[i].eCode & TP_VMASK } /* Set of an array of available slots */ if n == 2 && ((aTxt[0].eCode|aTxt[1].eCode)&TP_JMASK) == (TP_LJUST|TP_RJUST) { /* Special case of two texts that have opposite justification: ** Allow them both to float to center. */ aFree[0] = TP_CENTER aFree[1] = TP_CENTER } else { /* Set up the arrow so that available slots are filled from top to ** bottom */ iSlot := 0 if n >= 4 && (allSlots&TP_ABOVE2) == 0 { aFree[iSlot] = TP_ABOVE2 iSlot++ } if (allSlots & TP_ABOVE) == 0 { aFree[iSlot] = TP_ABOVE iSlot++ } if (n & 1) != 0 { aFree[iSlot] = TP_CENTER iSlot++ } if (allSlots & TP_BELOW) == 0 { aFree[iSlot] = TP_BELOW iSlot++ } if n >= 4 && (allSlots&TP_BELOW2) == 0 { aFree[iSlot] = TP_BELOW2 iSlot++ } } /* Set the VMASK for all unassigned texts */ for i, iSlot := 0, 0; i < n; i++ { if (aTxt[i].eCode & TP_VMASK) == 0 { aTxt[i].eCode |= aFree[iSlot] iSlot++ } } } } /* Return the font scaling factor associated with the input text attribute. */ func (p* Pik) pik_font_scale(t PToken) PNum { scale := p.svgFontScale if t.eCode&TP_BIG != 0 { scale *= 1.25 } if t.eCode&TP_SMALL != 0 { scale *= 0.8 } if t.eCode&TP_XTRA != 0 { scale *= scale } return scale } /* Append multiple <text> SVG elements for the text fields of the PObj. ** Parameters: ** ** p The Pik object into which we are rendering ** ** pObj Object containing the text to be rendered ** ** pBox If not NULL, do no rendering at all. Instead ** expand the box object so that it will include all ** of the text. */ func (p *Pik) pik_append_txt(pObj *PObj, pBox *PBox) { var jw PNum /* Justification margin relative to center */ var ha2 PNum = 0.0 /* Height of the top row of text */ var ha1 PNum = 0.0 /* Height of the second "above" row */ var hc PNum = 0.0 /* Height of the center row */ var hb1 PNum = 0.0 /* Height of the first "below" row of text */ var hb2 PNum = 0.0 /* Height of the second "below" row */ var yBase PNum = 0.0 allMask := int16(0) if p.nErr != 0 { return } if pObj.nTxt == 0 { return } aTxt := pObj.aTxt[:] n := int(pObj.nTxt) pik_txt_vertical_layout(pObj) x := pObj.ptAt.x for i := 0; i < n; i++ { allMask |= pObj.aTxt[i].eCode } if pObj.typ.isLine { hc = pObj.sw * 1.5 } else if pObj.rad > 0.0 && pObj.typ.zName == "cylinder" { yBase = -0.75 * pObj.rad } if allMask&TP_CENTER != 0 { for i := 0; i < n; i++ { if pObj.aTxt[i].eCode&TP_CENTER != 0 { s := p.pik_font_scale(pObj.aTxt[i]) if hc < s*p.charHeight { hc = s * p.charHeight } } } } if allMask&TP_ABOVE != 0 { for i := 0; i < n; i++ { if pObj.aTxt[i].eCode&TP_ABOVE != 0 { s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight if ha1 < s { ha1 = s } } } if allMask&TP_ABOVE2 != 0 { for i := 0; i < n; i++ { if pObj.aTxt[i].eCode&TP_ABOVE2 != 0 { s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight if ha2 < s { ha2 = s } } } } } if allMask&TP_BELOW != 0 { for i := 0; i < n; i++ { if pObj.aTxt[i].eCode&TP_BELOW != 0 { s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight if hb1 < s { hb1 = s } } } if allMask&TP_BELOW2 != 0 { for i := 0; i < n; i++ { if pObj.aTxt[i].eCode&TP_BELOW2 != 0 { s := p.pik_font_scale(pObj.aTxt[i]) * p.charHeight if hb2 < s { hb2 = s } } } } } if pObj.typ.eJust == 1 { jw = 0.5 * (pObj.w - 0.5*(p.charWidth+pObj.sw)) } else { jw = 0.0 } for i := 0; i < n; i++ { t := aTxt[i] xtraFontScale := p.pik_font_scale(t) var nx PNum = 0 orig_y := pObj.ptAt.y y := yBase if t.eCode&TP_ABOVE2 != 0 { y += 0.5*hc + ha1 + 0.5*ha2 } if t.eCode&TP_ABOVE != 0 { y += 0.5*hc + 0.5*ha1 } if t.eCode&TP_BELOW != 0 { y -= 0.5*hc + 0.5*hb1 } if t.eCode&TP_BELOW2 != 0 { y -= 0.5*hc + hb1 + 0.5*hb2 } if t.eCode&TP_LJUST != 0 { nx -= jw } if t.eCode&TP_RJUST != 0 { nx += jw } if pBox != nil { /* If pBox is not NULL, do not draw any <text>. Instead, just expand ** pBox to include the text */ var cw PNum = PNum(pik_text_length(t)) * p.charWidth * xtraFontScale * 0.01 var ch PNum = p.charHeight * 0.5 * xtraFontScale var x0, y0, x1, y1 PNum /* Boundary of text relative to pObj.ptAt */ if t.eCode&TP_BOLD != 0 { cw *= 1.1 } if t.eCode&TP_RJUST != 0 { x0 = nx y0 = y - ch x1 = nx - cw y1 = y + ch } else if t.eCode&TP_LJUST != 0 { x0 = nx y0 = y - ch x1 = nx + cw y1 = y + ch } else { x0 = nx + cw/2 y0 = y + ch x1 = nx - cw/2 y1 = y - ch } if (t.eCode&TP_ALIGN) != 0 && pObj.nPath >= 2 { nn := pObj.nPath var dx PNum = pObj.aPath[nn-1].x - pObj.aPath[0].x var dy PNum = pObj.aPath[nn-1].y - pObj.aPath[0].y if dx != 0 || dy != 0 { var dist PNum = math.Hypot(dx, dy) var tt PNum dx /= dist dy /= dist tt = dx*x0 - dy*y0 y0 = dy*x0 - dx*y0 x0 = tt tt = dx*x1 - dy*y1 y1 = dy*x1 - dx*y1 x1 = tt } } pik_bbox_add_xy(pBox, x+x0, orig_y+y0) pik_bbox_add_xy(pBox, x+x1, orig_y+y1) continue } nx += x y += orig_y p.pik_append_x("<text x=\"", nx, "\"") p.pik_append_y(" y=\"", y, "\"") if t.eCode&TP_RJUST != 0 { p.pik_append(" text-anchor=\"end\"") } else if t.eCode&TP_LJUST != 0 { p.pik_append(" text-anchor=\"start\"") } else { p.pik_append(" text-anchor=\"middle\"") } if t.eCode&TP_ITALIC != 0 { p.pik_append(" font-style=\"italic\"") } if t.eCode&TP_BOLD != 0 { p.pik_append(" font-weight=\"bold\"") } if pObj.color >= 0.0 { p.pik_append_clr(" fill=\"", pObj.color, "\"", false) } xtraFontScale *= p.fontScale if xtraFontScale <= 0.99 || xtraFontScale >= 1.01 { p.pik_append_num(" font-size=\"", xtraFontScale*100.0) p.pik_append("%\"") } if (t.eCode&TP_ALIGN) != 0 && pObj.nPath >= 2 { nn := pObj.nPath var dx PNum = pObj.aPath[nn-1].x - pObj.aPath[0].x var dy PNum = pObj.aPath[nn-1].y - pObj.aPath[0].y if dx != 0 || dy != 0 { var ang PNum = math.Atan2(dy, dx) * -180 / math.Pi p.pik_append_num(" transform=\"rotate(", ang) p.pik_append_xy(" ", x, orig_y) p.pik_append(")\"") } } p.pik_append(" dominant-baseline=\"central\">") var z []byte var nz int if t.n >= 2 && t.z[0] == '"' { z = t.z[1:] nz = t.n - 2 } else { z = t.z nz = t.n } for nz > 0 { var j int for j = 0; j < nz && z[j] != '\\'; j++ { } if j != 0 { p.pik_append_text(string(z[:j]), 0x3) } if j < nz && (j+1 == nz || z[j+1] == '\\') { p.pik_append("\") j++ } nz -= j + 1 if nz > 0 { z = z[j+1:] } } p.pik_append("</text>\n") } } /* ** Append text (that will go inside of a <pre>...</pre>) that ** shows the context of an error token. */ func (p *Pik) pik_error_context(pErr *PToken, nContext int) { var ( iErrPt int /* Index of first byte of error from start of input */ iErrCol int /* Column of the error token on its line */ iStart int /* Start position of the error context */ iEnd int /* End position of the error context */ iLineno int /* Line number of the error */ iFirstLineno int /* Line number of start of error context */ i int /* Loop counter */ iBump = 0 /* Bump the location of the error cursor */ ) iErrPt = len(p.sIn.z) - len(pErr.z) // in C, uses pointer math: iErrPt = (int)(pErr->z - p->sIn.z); if iErrPt >= p.sIn.n { iErrPt = p.sIn.n - 1 iBump = 1 } else { for iErrPt > 0 && (p.sIn.z[iErrPt] == '\n' || p.sIn.z[iErrPt] == '\r') { iErrPt-- iBump = 1 } } iLineno = 1 for i = 0; i < iErrPt; i++ { if p.sIn.z[i] == '\n' { iLineno++ } } iStart = 0 iFirstLineno = 1 for iFirstLineno+nContext < iLineno { for p.sIn.z[iStart] != '\n' { iStart++ } iStart++ iFirstLineno++ } for iEnd = iErrPt; p.sIn.z[iEnd] != 0 && p.sIn.z[iEnd] != '\n'; iEnd++ { } i = iStart for iFirstLineno <= iLineno { zLineno := fmt.Sprintf("/* %4d */ ", iFirstLineno) iFirstLineno++ p.pik_append(zLineno) for i = iStart; p.sIn.z[i] != 0 && p.sIn.z[i] != '\n'; i++ { } p.pik_append_errtxt(string(p.sIn.z[iStart:i])) iStart = i + 1 p.pik_append("\n") } for iErrCol, i = 0, iErrPt; i > 0 && p.sIn.z[i] != '\n'; iErrCol, i = iErrCol+1, i-1 { } for i = 0; i < iErrCol+11+iBump; i++ { p.pik_append(" ") } for i = 0; i < pErr.n; i++ { p.pik_append("^") } p.pik_append("\n") } /* ** Generate an error message for the output. pErr is the token at which ** the error should point. zMsg is the text of the error message. If ** either pErr or zMsg is NULL, generate an out-of-memory error message. ** ** This routine is a no-op if there has already been an error reported. */ func (p *Pik) pik_error(pErr *PToken, zMsg string) { if p == nil { return } if p.nErr > 0 { return } p.nErr++ if zMsg == "" { if p.mFlags&PIKCHR_PLAINTEXT_ERRORS != 0 { p.pik_append("\nOut of memory\n") } else { p.pik_append("\n<div><p>Out of memory</p></div>\n") } return } if pErr == nil { p.pik_append("\n") p.pik_append_errtxt(zMsg) return } if (p.mFlags & PIKCHR_PLAINTEXT_ERRORS) == 0 { p.pik_append("<div><pre>\n") } p.pik_error_context(pErr, 5) p.pik_append("ERROR: ") p.pik_append_errtxt(zMsg) p.pik_append("\n") for i := p.nCtx - 1; i >= 0; i-- { p.pik_append("Called from:\n") p.pik_error_context(&p.aCtx[i], 0) } if (p.mFlags & PIKCHR_PLAINTEXT_ERRORS) == 0 { p.pik_append("</pre></div>\n") } } /* ** Process an "assert( e1 == e2 )" statement. Always return `nil`. */ func (p *Pik) pik_assert(e1 PNum, pEq *PToken, e2 PNum) *PObj { /* Convert the numbers to strings using %g for comparison. This ** limits the precision of the comparison to account for rounding error. */ zE1 := fmt.Sprintf("%g", e1) zE2 := fmt.Sprintf("%g", e2) if zE1 != zE2 { p.pik_error(pEq, fmt.Sprintf("%.50s != %.50s", zE1, zE2)) } return nil } /* ** Process an "assert( place1 == place2 )" statement. Always return `nil`. */ func (p *Pik) pik_position_assert(e1 *PPoint, pEq *PToken, e2 *PPoint) *PObj { /* Convert the numbers to strings using %g for comparison. This ** limits the precision of the comparison to account for rounding error. */ zE1 := fmt.Sprintf("(%g,%g)", e1.x, e1.y) zE2 := fmt.Sprintf("(%g,%g)", e2.x, e2.y) if zE1 != zE2 { p.pik_error(pEq, fmt.Sprintf("%s != %s", zE1, zE2)) } return nil } /* Free a complete list of objects */ func (p *Pik) pik_elist_free(pList *PList) { if pList == nil || *pList == nil { return } for i := 0; i < len(*pList); i++ { p.pik_elem_free((*pList)[i]) } } /* Free a single object, and its substructure */ func (p *Pik) pik_elem_free(pObj *PObj) { if pObj == nil { return } p.pik_elist_free(&pObj.pSublist) } /* Convert a numeric literal into a number. Return that number. ** There is no error handling because the tokenizer has already ** assured us that the numeric literal is valid. ** ** Allowed number forms: ** ** (1) Floating point literal ** (2) Same as (1) but followed by a unit: "cm", "mm", "in", ** "px", "pt", or "pc". ** (3) Hex integers: 0x000000 ** ** This routine returns the result in inches. If a different unit ** is specified, the conversion happens automatically. */ func pik_atof(num *PToken) PNum { if num.n >= 3 && num.z[0] == '0' && (num.z[1] == 'x' || num.z[1] == 'X') { i, err := strconv.ParseInt(string(num.z[2:num.n]), 16, 64) if err != nil { return 0 } return PNum(i) } factor := 1.0 z := num.String() if num.n > 2 { hasSuffix := true switch string(num.z[num.n-2 : num.n]) { case "cm": factor = 1 / 2.54 case "mm": factor = 1 / 25.4 case "px": factor = 1 / 96.0 case "pt": factor = 1 / 72.0 case "pc": factor = 1 / 6.0 case "in": factor = 1.0 default: hasSuffix = false } if hasSuffix { z = z[:len(z)-2] } } ans, err := strconv.ParseFloat(z, 64) ans *= factor if err != nil { return 0.0 } return PNum(ans) } /* ** Compute the distance between two points */ func pik_dist(pA *PPoint, pB *PPoint) PNum { dx := pB.x - pA.x dy := pB.y - pA.y return math.Hypot(dx, dy) } /* Return true if a bounding box is empty. */ func pik_bbox_isempty(p *PBox) bool { return p.sw.x > p.ne.x } /* Return true if point pPt is contained within the bounding box pBox */ func pik_bbox_contains_point(pBox *PBox, pPt *PPoint) bool { if pik_bbox_isempty(pBox) { return false } if pPt.x < pBox.sw.x { return false } if pPt.x > pBox.ne.x { return false } if pPt.y < pBox.sw.y { return false } if pPt.y > pBox.ne.y { return false } return true } /* Initialize a bounding box to an empty container */ func pik_bbox_init(p *PBox) { p.sw.x = 1.0 p.sw.y = 1.0 p.ne.x = 0.0 p.ne.y = 0.0 } /* Enlarge the PBox of the first argument so that it fully ** covers the second PBox */ func pik_bbox_addbox(pA *PBox, pB *PBox) { if pik_bbox_isempty(pA) { *pA = *pB } if pik_bbox_isempty(pB) { return } if pA.sw.x > pB.sw.x { pA.sw.x = pB.sw.x } if pA.sw.y > pB.sw.y { pA.sw.y = pB.sw.y } if pA.ne.x < pB.ne.x { pA.ne.x = pB.ne.x } if pA.ne.y < pB.ne.y { pA.ne.y = pB.ne.y } } /* Enlarge the PBox of the first argument, if necessary, so that ** it contains the point described by the 2nd and 3rd arguments. */ func pik_bbox_add_xy(pA *PBox, x PNum, y PNum) { if pik_bbox_isempty(pA) { pA.ne.x = x pA.ne.y = y pA.sw.x = x pA.sw.y = y return } if pA.sw.x > x { pA.sw.x = x } if pA.sw.y > y { pA.sw.y = y } if pA.ne.x < x { pA.ne.x = x } if pA.ne.y < y { pA.ne.y = y } } /* Enlarge the PBox so that it is able to contain an ellipse ** centered at x,y and with radiuses rx and ry. */ func pik_bbox_addellipse(pA *PBox, x PNum, y PNum, rx PNum, ry PNum) { if pik_bbox_isempty(pA) { pA.ne.x = x + rx pA.ne.y = y + ry pA.sw.x = x - rx pA.sw.y = y - ry return } if pA.sw.x > x-rx { pA.sw.x = x - rx } if pA.sw.y > y-ry { pA.sw.y = y - ry } if pA.ne.x < x+rx { pA.ne.x = x + rx } if pA.ne.y < y+ry { pA.ne.y = y + ry } } /* Append a new object onto the end of an object list. The ** object list is created if it does not already exist. Return ** the new object list. */ func (p *Pik) pik_elist_append(pList PList, pObj *PObj) PList { if pObj == nil { return pList } pList = append(pList, pObj) p.list = pList return pList } /* Convert an object class name into a PClass pointer */ func pik_find_class(pId *PToken) *PClass { zString := pId.String() first := 0 last := len(aClass) - 1 for { mid := (first + last) / 2 c := strings.Compare(aClass[mid].zName, zString) if c == 0 { return &aClass[mid] } if c < 0 { first = mid + 1 } else { last = mid - 1 } if first > last { return nil } } } /* Allocate and return a new PObj object. ** ** If pId!=0 then pId is an identifier that defines the object class. ** If pStr!=0 then it is a STRING literal that defines a text object. ** If pSublist!=0 then this is a [...] object. If all three parameters ** are NULL then this is a no-op object used to define a PLACENAME. */ func (p *Pik) pik_elem_new(pId *PToken, pStr *PToken, pSublist PList) *PObj { miss := false if p.nErr != 0 { return nil } pNew := &PObj{} p.cur = pNew p.nTPath = 1 p.thenFlag = false if len(p.list) == 0 { pNew.ptAt.x = 0.0 pNew.ptAt.y = 0.0 pNew.eWith = CP_C } else { pPrior := p.list[len(p.list)-1] pNew.ptAt = pPrior.ptExit switch p.eDir { default: pNew.eWith = CP_W case DIR_LEFT: pNew.eWith = CP_E case DIR_UP: pNew.eWith = CP_S case DIR_DOWN: pNew.eWith = CP_N } } p.aTPath[0] = pNew.ptAt pNew.with = pNew.ptAt pNew.outDir = p.eDir pNew.inDir = p.eDir pNew.iLayer = p.pik_value_int("layer", &miss) if miss { pNew.iLayer = 1000 } if pNew.iLayer < 0 { pNew.iLayer = 0 } if pSublist != nil { pNew.typ = &sublistClass pNew.pSublist = pSublist sublistClass.xInit(p, pNew) return pNew } if pStr != nil { n := PToken{ z: []byte("text"), n: 4, } pNew.typ = pik_find_class(&n) assert(pNew.typ != nil, "pNew.typ!=nil") pNew.errTok = *pStr pNew.typ.xInit(p, pNew) p.pik_add_txt(pStr, pStr.eCode) return pNew } if pId != nil { pNew.errTok = *pId pClass := pik_find_class(pId) if pClass != nil { pNew.typ = pClass pNew.sw = p.pik_value("thickness", nil) pNew.fill = p.pik_value("fill", nil) pNew.color = p.pik_value("color", nil) pClass.xInit(p, pNew) return pNew } p.pik_error(pId, "unknown object type") p.pik_elem_free(pNew) return nil } pNew.typ = &noopClass pNew.ptExit = pNew.ptAt pNew.ptEnter = pNew.ptAt return pNew } /* ** If the ID token in the argument is the name of a macro, return ** the PMacro object for that macro */ func (p *Pik) pik_find_macro(pId *PToken) *PMacro { for pMac := p.pMacros; pMac != nil; pMac = pMac.pNext { if pMac.macroName.n == pId.n && bytesEq(pMac.macroName.z[:pMac.macroName.n], pId.z[:pId.n]) { return pMac } } return nil } /* Add a new macro */ func (p *Pik) pik_add_macro( pId *PToken, /* The ID token that defines the macro name */ pCode *PToken, /* Macro body inside of {...} */ ) { pNew := p.pik_find_macro(pId) if pNew == nil { pNew = &PMacro{ pNext: p.pMacros, macroName: *pId, } p.pMacros = pNew } pNew.macroBody.z = pCode.z[1:] pNew.macroBody.n = pCode.n - 2 pNew.inUse = false } /* ** Set the output direction and exit point for an object */ func pik_elem_set_exit(pObj *PObj, eDir uint8) { assert(ValidDir(eDir), "ValidDir(eDir)") pObj.outDir = eDir if !pObj.typ.isLine || pObj.bClose { pObj.ptExit = pObj.ptAt switch pObj.outDir { default: pObj.ptExit.x += pObj.w * 0.5 case DIR_LEFT: pObj.ptExit.x -= pObj.w * 0.5 case DIR_UP: pObj.ptExit.y += pObj.h * 0.5 case DIR_DOWN: pObj.ptExit.y -= pObj.h * 0.5 } } } /* Change the layout direction. */ func (p *Pik) pik_set_direction(eDir uint8) { assert(ValidDir(eDir), "ValidDir(eDir)") p.eDir = eDir /* It seems to make sense to reach back into the last object and ** change its exit point (its ".end") to correspond to the new ** direction. Things just seem to work better this way. However, ** legacy PIC does *not* do this. ** ** The difference can be seen in a script like this: ** ** arrow; circle; down; arrow ** ** You can make pikchr render the above exactly like PIC ** by deleting the following three lines. But I (drh) think ** it works better with those lines in place. */ if len(p.list) > 0 { pik_elem_set_exit(p.list[len(p.list)-1], eDir) } } /* Move all coordinates contained within an object (and within its ** substructure) by dx, dy */ func pik_elem_move(pObj *PObj, dx PNum, dy PNum) { pObj.ptAt.x += dx pObj.ptAt.y += dy pObj.ptEnter.x += dx pObj.ptEnter.y += dy pObj.ptExit.x += dx pObj.ptExit.y += dy pObj.bbox.ne.x += dx pObj.bbox.ne.y += dy pObj.bbox.sw.x += dx pObj.bbox.sw.y += dy for i := 0; i < pObj.nPath; i++ { pObj.aPath[i].x += dx pObj.aPath[i].y += dy } if pObj.pSublist != nil { pik_elist_move(pObj.pSublist, dx, dy) } } func pik_elist_move(pList PList, dx PNum, dy PNum) { for i := 0; i < len(pList); i++ { pik_elem_move(pList[i], dx, dy) } } /* ** Check to see if it is ok to set the value of paraemeter mThis. ** Return 0 if it is ok. If it not ok, generate an appropriate ** error message and return non-zero. ** ** Flags are set in pObj so that the same object or conflicting ** objects may not be set again. ** ** To be ok, bit mThis must be clear and no more than one of ** the bits identified by mBlockers may be set. */ func (p *Pik) pik_param_ok( pObj *PObj, /* The object under construction */ pId *PToken, /* Make the error point to this token */ mThis uint, /* Value we are trying to set */ ) bool { if pObj.mProp&mThis != 0 { p.pik_error(pId, "value is already set") return true } if pObj.mCalc&mThis != 0 { p.pik_error(pId, "value already fixed by prior constraints") return true } pObj.mProp |= mThis return false } /* ** Set a numeric property like "width 7" or "radius 200%". ** ** The rAbs term is an absolute value to add in. rRel is ** a relative value by which to change the current value. */ func (p *Pik) pik_set_numprop(pId *PToken, pVal *PRel) { pObj := p.cur switch pId.eType { case T_HEIGHT: if p.pik_param_ok(pObj, pId, A_HEIGHT) { return } pObj.h = pObj.h*pVal.rRel + pVal.rAbs case T_WIDTH: if p.pik_param_ok(pObj, pId, A_WIDTH) { return } pObj.w = pObj.w*pVal.rRel + pVal.rAbs case T_RADIUS: if p.pik_param_ok(pObj, pId, A_RADIUS) { return } pObj.rad = pObj.rad*pVal.rRel + pVal.rAbs case T_DIAMETER: if p.pik_param_ok(pObj, pId, A_RADIUS) { return } pObj.rad = pObj.rad*pVal.rRel + 0.5*pVal.rAbs /* diam it 2x rad */ case T_THICKNESS: if p.pik_param_ok(pObj, pId, A_THICKNESS) { return } pObj.sw = pObj.sw*pVal.rRel + pVal.rAbs } if pObj.typ.xNumProp != nil { pObj.typ.xNumProp(p, pObj, pId) } } /* ** Set a color property. The argument is an RGB value. */ func (p *Pik) pik_set_clrprop(pId *PToken, rClr PNum) { pObj := p.cur switch pId.eType { case T_FILL: if p.pik_param_ok(pObj, pId, A_FILL) { return } pObj.fill = rClr case T_COLOR: if p.pik_param_ok(pObj, pId, A_COLOR) { return } pObj.color = rClr break } if pObj.typ.xNumProp != nil { pObj.typ.xNumProp(p, pObj, pId) } } /* ** Set a "dashed" property like "dash 0.05" ** ** Use the value supplied by pVal if available. If pVal==0, use ** a default. */ func (p *Pik) pik_set_dashed(pId *PToken, pVal *PNum) { pObj := p.cur switch pId.eType { case T_DOTTED: if pVal != nil { pObj.dotted = *pVal } else { pObj.dotted = p.pik_value("dashwid", nil) } pObj.dashed = 0.0 case T_DASHED: if pVal != nil { pObj.dashed = *pVal } else { pObj.dashed = p.pik_value("dashwid", nil) } pObj.dotted = 0.0 } } /* ** If the current path information came from a "same" or "same as" ** reset it. */ func (p *Pik) pik_reset_samepath() { if p.samePath { p.samePath = false p.nTPath = 1 } } /* Add a new term to the path for a line-oriented object by transferring ** the information in the ptTo field over onto the path and into ptFrom ** resetting the ptTo. */ func (p *Pik) pik_then(pToken *PToken, pObj *PObj) { if !pObj.typ.isLine { p.pik_error(pToken, "use with line-oriented objects only") return } n := p.nTPath - 1 if n < 1 && (pObj.mProp&A_FROM) == 0 { p.pik_error(pToken, "no prior path points") return } p.thenFlag = true } /* Advance to the next entry in p.aTPath. Return its index. */ func (p *Pik) pik_next_rpath(pErr *PToken) int { n := p.nTPath - 1 if n+1 >= len(p.aTPath) { (*Pik)(nil).pik_error(pErr, "too many path elements") return n } n++ p.nTPath++ p.aTPath[n] = p.aTPath[n-1] p.mTPath = 0 return n } /* Add a direction term to an object. "up 0.5", or "left 3", or "down" ** or "down 50%". */ func (p *Pik) pik_add_direction(pDir *PToken, pVal *PRel) { pObj := p.cur if !pObj.typ.isLine { if pDir != nil { p.pik_error(pDir, "use with line-oriented objects only") } else { x := pik_next_semantic_token(&pObj.errTok) p.pik_error(&x, "syntax error") } return } p.pik_reset_samepath() n := p.nTPath - 1 if p.thenFlag || p.mTPath == 3 || n == 0 { n = p.pik_next_rpath(pDir) p.thenFlag = false } dir := p.eDir if pDir != nil { dir = uint8(pDir.eCode) } switch dir { case DIR_UP: if p.mTPath&2 > 0 { n = p.pik_next_rpath(pDir) } p.aTPath[n].y += pVal.rAbs + pObj.h*pVal.rRel p.mTPath |= 2 case DIR_DOWN: if p.mTPath&2 > 0 { n = p.pik_next_rpath(pDir) } p.aTPath[n].y -= pVal.rAbs + pObj.h*pVal.rRel p.mTPath |= 2 case DIR_RIGHT: if p.mTPath&1 > 0 { n = p.pik_next_rpath(pDir) } p.aTPath[n].x += pVal.rAbs + pObj.w*pVal.rRel p.mTPath |= 1 case DIR_LEFT: if p.mTPath&1 > 0 { n = p.pik_next_rpath(pDir) } p.aTPath[n].x -= pVal.rAbs + pObj.w*pVal.rRel p.mTPath |= 1 } pObj.outDir = dir } /* Process a movement attribute of one of these forms: ** ** pDist pHdgKW rHdg pEdgept ** GO distance HEADING angle ** GO distance compasspoint */ func (p *Pik) pik_move_hdg( pDist *PRel, /* Distance to move */ pHeading *PToken, /* "heading" keyword if present */ rHdg PNum, /* Angle argument to "heading" keyword */ pEdgept *PToken, /* EDGEPT keyword "ne", "sw", etc... */ pErr *PToken, /* Token to use for error messages */ ) { pObj := p.cur var rDist PNum = pDist.rAbs + p.pik_value("linewid", nil)*pDist.rRel if !pObj.typ.isLine { p.pik_error(pErr, "use with line-oriented objects only") return } p.pik_reset_samepath() n := 0 for n < 1 { n = p.pik_next_rpath(pErr) } if pHeading != nil { rHdg = math.Mod(rHdg, 360.0) } else if pEdgept.eEdge == CP_C { p.pik_error(pEdgept, "syntax error") return } else { rHdg = pik_hdg_angle[pEdgept.eEdge] } if rHdg <= 45.0 { pObj.outDir = DIR_UP } else if rHdg <= 135.0 { pObj.outDir = DIR_RIGHT } else if rHdg <= 225.0 { pObj.outDir = DIR_DOWN } else if rHdg <= 315.0 { pObj.outDir = DIR_LEFT } else { pObj.outDir = DIR_UP } rHdg *= 0.017453292519943295769 /* degrees to radians */ p.aTPath[n].x += rDist * math.Sin(rHdg) p.aTPath[n].y += rDist * math.Cos(rHdg) p.mTPath = 2 } /* Process a movement attribute of the form "right until even with ..." ** ** pDir is the first keyword, "right" or "left" or "up" or "down". ** The movement is in that direction until its closest approach to ** the point specified by pPoint. */ func (p *Pik) pik_evenwith(pDir *PToken, pPlace *PPoint) { pObj := p.cur if !pObj.typ.isLine { p.pik_error(pDir, "use with line-oriented objects only") return } p.pik_reset_samepath() n := p.nTPath - 1 if p.thenFlag || p.mTPath == 3 || n == 0 { n = p.pik_next_rpath(pDir) p.thenFlag = false } switch pDir.eCode { case DIR_DOWN, DIR_UP: if p.mTPath&2 != 0 { n = p.pik_next_rpath(pDir) } p.aTPath[n].y = pPlace.y p.mTPath |= 2 case DIR_RIGHT, DIR_LEFT: if p.mTPath&1 != 0 { n = p.pik_next_rpath(pDir) } p.aTPath[n].x = pPlace.x p.mTPath |= 1 } pObj.outDir = uint8(pDir.eCode) } /* If the last referenced object is centered at point pPt then return ** a pointer to that object. If there is no prior object reference, ** or if the points are not the same, return NULL. ** ** This is a side-channel hack used to find the objects at which a ** line begins and ends. For example, in ** ** arrow from OBJ1 to OBJ2 chop ** ** The arrow object is normally just handed the coordinates of the ** centers for OBJ1 and OBJ2. But we also want to know the specific ** object named in case there are multiple objects centered at the ** same point. ** ** See forum post 1d46e3a0bc */ func (p *Pik) pik_last_ref_object(pPt *PPoint) *PObj { var pRes *PObj if p.lastRef == nil { return nil } if p.lastRef.ptAt.x == pPt.x && p.lastRef.ptAt.y == pPt.y { pRes = p.lastRef } p.lastRef = nil return pRes } /* Set the "from" of an object */ func (p *Pik) pik_set_from(pObj *PObj, pTk *PToken, pPt *PPoint) { if !pObj.typ.isLine { p.pik_error(pTk, "use \"at\" to position this object") return } if pObj.mProp&A_FROM != 0 { p.pik_error(pTk, "line start location already fixed") return } if pObj.bClose { p.pik_error(pTk, "polygon is closed") return } if p.nTPath > 1 { var dx PNum = pPt.x - p.aTPath[0].x var dy PNum = pPt.y - p.aTPath[0].y for i := 1; i < p.nTPath; i++ { p.aTPath[i].x += dx p.aTPath[i].y += dy } } p.aTPath[0] = *pPt p.mTPath = 3 pObj.mProp |= A_FROM pObj.pFrom = p.pik_last_ref_object(pPt) } /* Set the "to" of an object */ func (p *Pik) pik_add_to(pObj *PObj, pTk *PToken, pPt *PPoint) { n := p.nTPath - 1 if !pObj.typ.isLine { p.pik_error(pTk, "use \"at\" to position this object") return } if pObj.bClose { p.pik_error(pTk, "polygon is closed") return } p.pik_reset_samepath() if n == 0 || p.mTPath == 3 || p.thenFlag { n = p.pik_next_rpath(pTk) } p.aTPath[n] = *pPt p.mTPath = 3 pObj.pTo = p.pik_last_ref_object(pPt) } func (p *Pik) pik_close_path(pErr *PToken) { pObj := p.cur if p.nTPath < 3 { p.pik_error(pErr, "need at least 3 vertexes in order to close the polygon") return } if pObj.bClose { p.pik_error(pErr, "polygon already closed") return } pObj.bClose = true } /* Lower the layer of the current object so that it is behind the ** given object. */ func (p *Pik) pik_behind(pOther *PObj) { pObj := p.cur if p.nErr == 0 && pObj.iLayer >= pOther.iLayer { pObj.iLayer = pOther.iLayer - 1 } } /* Set the "at" of an object */ func (p *Pik) pik_set_at(pEdge *PToken, pAt *PPoint, pErrTok *PToken) { eDirToCp := []uint8{CP_E, CP_S, CP_W, CP_N} if p.nErr != 0 { return } pObj := p.cur if pObj.typ.isLine { p.pik_error(pErrTok, "use \"from\" and \"to\" to position this object") return } if pObj.mProp&A_AT != 0 { p.pik_error(pErrTok, "location fixed by prior \"at\"") return } pObj.mProp |= A_AT pObj.eWith = CP_C if pEdge != nil { pObj.eWith = pEdge.eEdge } if pObj.eWith >= CP_END { dir := pObj.inDir if pObj.eWith == CP_END { dir = pObj.outDir } pObj.eWith = eDirToCp[int(dir)] } pObj.with = *pAt } /* ** Try to add a text attribute to an object */ func (p *Pik) pik_add_txt(pTxt *PToken, iPos int16) { pObj := p.cur if int(pObj.nTxt) >= len(pObj.aTxt) { p.pik_error(pTxt, "too many text terms") return } pT := &pObj.aTxt[pObj.nTxt] pObj.nTxt++ *pT = *pTxt pT.eCode = iPos } /* Merge "text-position" flags */ func pik_text_position(iPrev int, pFlag *PToken) int { iRes := iPrev switch pFlag.eType { case T_LJUST: iRes = (iRes &^ TP_JMASK) | TP_LJUST case T_RJUST: iRes = (iRes &^ TP_JMASK) | TP_RJUST case T_ABOVE: iRes = (iRes &^ TP_VMASK) | TP_ABOVE case T_CENTER: iRes = (iRes &^ TP_VMASK) | TP_CENTER case T_BELOW: iRes = (iRes &^ TP_VMASK) | TP_BELOW case T_ITALIC: iRes |= TP_ITALIC case T_BOLD: iRes |= TP_BOLD case T_ALIGNED: iRes |= TP_ALIGN case T_BIG: if iRes&TP_BIG != 0 { iRes |= TP_XTRA } else { iRes = (iRes &^ TP_SZMASK) | TP_BIG } case T_SMALL: if iRes&TP_SMALL != 0 { iRes |= TP_XTRA } else { iRes = (iRes &^ TP_SZMASK) | TP_SMALL } } return iRes } /* ** Table of scale-factor estimates for variable-width characters. ** Actual character widths vary by font. These numbers are only ** guesses. And this table only provides data for ASCII. ** ** 100 means normal width. */ var awChar = []byte{ /* Skip initial 32 control characters */ /* ' ' */ 45, /* '!' */ 55, /* '"' */ 62, /* '#' */ 115, /* '$' */ 90, /* '%' */ 132, /* '&' */ 125, /* '\''*/ 40, /* '(' */ 55, /* ')' */ 55, /* '*' */ 71, /* '+' */ 115, /* ',' */ 45, /* '-' */ 48, /* '.' */ 45, /* '/' */ 50, /* '0' */ 91, /* '1' */ 91, /* '2' */ 91, /* '3' */ 91, /* '4' */ 91, /* '5' */ 91, /* '6' */ 91, /* '7' */ 91, /* '8' */ 91, /* '9' */ 91, /* ':' */ 50, /* ';' */ 50, /* '<' */ 120, /* '=' */ 120, /* '>' */ 120, /* '?' */ 78, /* '@' */ 142, /* 'A' */ 102, /* 'B' */ 105, /* 'C' */ 110, /* 'D' */ 115, /* 'E' */ 105, /* 'F' */ 98, /* 'G' */ 105, /* 'H' */ 125, /* 'I' */ 58, /* 'J' */ 58, /* 'K' */ 107, /* 'L' */ 95, /* 'M' */ 145, /* 'N' */ 125, /* 'O' */ 115, /* 'P' */ 95, /* 'Q' */ 115, /* 'R' */ 107, /* 'S' */ 95, /* 'T' */ 97, /* 'U' */ 118, /* 'V' */ 102, /* 'W' */ 150, /* 'X' */ 100, /* 'Y' */ 93, /* 'Z' */ 100, /* '[' */ 58, /* '\\'*/ 50, /* ']' */ 58, /* '^' */ 119, /* '_' */ 72, /* '`' */ 72, /* 'a' */ 86, /* 'b' */ 92, /* 'c' */ 80, /* 'd' */ 92, /* 'e' */ 85, /* 'f' */ 52, /* 'g' */ 92, /* 'h' */ 92, /* 'i' */ 47, /* 'j' */ 47, /* 'k' */ 88, /* 'l' */ 48, /* 'm' */ 135, /* 'n' */ 92, /* 'o' */ 86, /* 'p' */ 92, /* 'q' */ 92, /* 'r' */ 69, /* 's' */ 75, /* 't' */ 58, /* 'u' */ 92, /* 'v' */ 80, /* 'w' */ 121, /* 'x' */ 81, /* 'y' */ 80, /* 'z' */ 76, /* '{' */ 91, /* '|'*/ 49, /* '}' */ 91, /* '~' */ 118, } /* Return an estimate of the width of the displayed characters ** in a character string. The returned value is 100 times the ** average character width. ** ** Omit "\" used to escape characters. And count entities like ** "<" as a single character. Multi-byte UTF8 characters count ** as a single character. ** ** Attempt to scale the answer by the actual characters seen. Wide ** characters count more than narrow characters. But the widths are ** only guesses. */ func pik_text_length(pToken PToken) int { n := pToken.n z := pToken.z cnt := 0 for j := 1; j < n-1; j++ { c := z[j] if c == '\\' && z[j+1] != '&' { j++ c = z[j] } else if c == '&' { var k int for k = j + 1; k < j+7 && z[k] != 0 && z[k] != ';'; k++ { } if z[k] == ';' { j = k } cnt += 150 continue } if (c & 0xc0) == 0xc0 { for j+1 < n-1 && (z[j+1]&0xc0) == 0x80 { j++ } cnt += 100 continue } if c >= 0x20 && c <= 0x7e { cnt += int(awChar[int(c-0x20)]) } else { cnt += 100 } } return cnt } /* Adjust the width, height, and/or radius of the object so that ** it fits around the text that has been added so far. ** ** (1) Only text specified prior to this attribute is considered. ** (2) The text size is estimated based on the charht and charwid ** variable settings. ** (3) The fitted attributes can be changed again after this ** attribute, for example using "width 110%" if this auto-fit ** underestimates the text size. ** (4) Previously set attributes will not be altered. In other words, ** "width 1in fit" might cause the height to change, but the ** width is now set. ** (5) This only works for attributes that have an xFit method. ** ** The eWhich parameter is: ** ** 1: Fit horizontally only ** 2: Fit vertically only ** 3: Fit both ways */ func (p *Pik) pik_size_to_fit(pFit *PToken, eWhich int) { var w, h PNum var bbox PBox if p.nErr != 0 { return } pObj := p.cur if pObj.nTxt == 0 { (*Pik)(nil).pik_error(pFit, "no text to fit to") return } if pObj.typ.xFit == nil { return } pik_bbox_init(&bbox) p.pik_compute_layout_settings() p.pik_append_txt(pObj, &bbox) if eWhich&1 != 0 { w = (bbox.ne.x - bbox.sw.x) + p.charWidth } if eWhich&2 != 0 { var h1, h2 PNum h1 = bbox.ne.y - pObj.ptAt.y h2 = pObj.ptAt.y - bbox.sw.y hmax := h1 if h1 < h2 { hmax = h2 } h = 2.0*hmax + 0.5*p.charHeight } else { h = 0 } pObj.typ.xFit(p, pObj, w, h) pObj.mProp |= A_FIT } /* Set a local variable name to "val". ** ** The name might be a built-in variable or a color name. In either case, ** a new application-defined variable is set. Since app-defined variables ** are searched first, this will override any built-in variables. */ func (p *Pik) pik_set_var(pId *PToken, val PNum, pOp *PToken) { pVar := p.pVar for pVar != nil { if pik_token_eq(pId, pVar.zName) == 0 { break } pVar = pVar.pNext } if pVar == nil { pVar = &PVar{ zName: pId.String(), pNext: p.pVar, val: p.pik_value(pId.String(), nil), } p.pVar = pVar } switch pOp.eCode { case T_PLUS: pVar.val += val case T_STAR: pVar.val *= val case T_MINUS: pVar.val -= val case T_SLASH: if val == 0.0 { p.pik_error(pOp, "division by zero") } else { pVar.val /= val } default: pVar.val = val } p.bLayoutVars = false /* Clear the layout setting cache */ } /* ** Round a PNum into the nearest integer */ func pik_round(v PNum) int { switch { case math.IsNaN(v): return 0 case v < -2147483647: return (-2147483647 - 1) case v >= 2147483647: return 2147483647 default: return int(v + math.Copysign(1e-15, v)) } } /* ** Search for the variable named z[0..n-1] in: ** ** * Application defined variables ** * Built-in variables ** ** Return the value of the variable if found. If not found ** return 0.0. Also if pMiss is not NULL, then set it to 1 ** if not found. ** ** This routine is a subroutine to pik_get_var(). But it is also ** used by object implementations to look up (possibly overwritten) ** values for built-in variables like "boxwid". */ func (p *Pik) pik_value(z string, pMiss *bool) PNum { for pVar := p.pVar; pVar != nil; pVar = pVar.pNext { if pVar.zName == z { return pVar.val } } first := 0 last := len(aBuiltin) - 1 for first <= last { mid := (first + last) / 2 zName := aBuiltin[mid].zName if zName == z { return aBuiltin[mid].val } else if z > zName { first = mid + 1 } else { last = mid - 1 } } if pMiss != nil { *pMiss = true } return 0.0 } func (p *Pik) pik_value_int(z string, pMiss *bool) int { return pik_round(p.pik_value(z, pMiss)) } /* ** Look up a color-name. Unlike other names in this program, the ** color-names are not case sensitive. So "DarkBlue" and "darkblue" ** and "DARKBLUE" all find the same value (139). ** ** If not found, return -99.0. Also post an error if p!=NULL. ** ** Special color names "None" and "Off" return -1.0 without causing ** an error. */ func (p *Pik) pik_lookup_color(pId *PToken) PNum { first := 0 last := len(aColor) - 1 zId := strings.ToLower(pId.String()) for first <= last { mid := (first + last) / 2 zClr := strings.ToLower(aColor[mid].zName) c := strings.Compare(zId, zClr) if c == 0 { return PNum(aColor[mid].val) } if c > 0 { first = mid + 1 } else { last = mid - 1 } } if p != nil { p.pik_error(pId, "not a known color name") } return -99.0 } /* Get the value of a variable. ** ** Search in order: ** ** * Application defined variables ** * Built-in variables ** * Color names ** ** If no such variable is found, throw an error. */ func (p *Pik) pik_get_var(pId *PToken) PNum { miss := false v := p.pik_value(pId.String(), &miss) if !miss { return v } v = (*Pik)(nil).pik_lookup_color(pId) if v > -90.0 { return v } p.pik_error(pId, "no such variable") return 0.0 } /* Convert a T_NTH token (ex: "2nd", "5th"} into a numeric value and ** return that value. Throw an error if the value is too big. */ func (p *Pik) pik_nth_value(pNth *PToken) int16 { s := pNth.String() if s == "first" { return 1 } i, err := strconv.Atoi(s[:len(s)-2]) if err != nil { p.pik_error(pNth, "value can't be parsed as a number") } if i > 1000 { p.pik_error(pNth, "value too big - max '1000th'") i = 1 } return int16(i) } /* Search for the NTH object. ** ** If pBasis is not NULL then it should be a [] object. Use the ** sublist of that [] object for the search. If pBasis is not a [] ** object, then throw an error. ** ** The pNth token describes the N-th search. The pNth.eCode value ** is one more than the number of items to skip. It is negative ** to search backwards. If pNth.eType==T_ID, then it is the name ** of a class to search for. If pNth.eType==T_LB, then ** search for a [] object. If pNth.eType==T_LAST, then search for ** any type. ** ** Raise an error if the item is not found. */ func (p *Pik) pik_find_nth(pBasis *PObj, pNth *PToken) *PObj { var pList PList var pClass *PClass if pBasis == nil { pList = p.list } else { pList = pBasis.pSublist } if pList == nil { p.pik_error(pNth, "no such object") return nil } if pNth.eType == T_LAST { pClass = nil } else if pNth.eType == T_LB { pClass = &sublistClass } else { pClass = pik_find_class(pNth) if pClass == nil { (*Pik)(nil).pik_error(pNth, "no such object type") return nil } } n := pNth.eCode if n < 0 { for i := len(pList) - 1; i >= 0; i-- { pObj := pList[i] if pClass != nil && pObj.typ != pClass { continue } n++ if n == 0 { return pObj } } } else { for i := 0; i < len(pList); i++ { pObj := pList[i] if pClass != nil && pObj.typ != pClass { continue } n-- if n == 0 { return pObj } } } p.pik_error(pNth, "no such object") return nil } /* Search for an object by name. ** ** Search in pBasis.pSublist if pBasis is not NULL. If pBasis is NULL ** then search in p.list. */ func (p *Pik) pik_find_byname(pBasis *PObj, pName *PToken) *PObj { var pList PList if pBasis == nil { pList = p.list } else { pList = pBasis.pSublist } if pList == nil { p.pik_error(pName, "no such object") return nil } /* First look explicitly tagged objects */ for i := len(pList) - 1; i >= 0; i-- { pObj := pList[i] if pObj.zName != "" && pik_token_eq(pName, pObj.zName) == 0 { p.lastRef = pObj return pObj } } /* If not found, do a second pass looking for any object containing ** text which exactly matches pName */ for i := len(pList) - 1; i >= 0; i-- { pObj := pList[i] for j := 0; j < int(pObj.nTxt); j++ { t := pObj.aTxt[j].n if t == pName.n+2 && bytesEq(pObj.aTxt[j].z[1:t-1], pName.z[:pName.n]) { p.lastRef = pObj return pObj } } } p.pik_error(pName, "no such object") return nil } /* Change most of the settings for the current object to be the ** same as the pOther object, or the most recent object of the same ** type if pOther is NULL. */ func (p *Pik) pik_same(pOther *PObj, pErrTok *PToken) { pObj := p.cur if p.nErr != 0 { return } if pOther == nil { var i int for i = len(p.list) - 1; i >= 0; i-- { pOther = p.list[i] if pOther.typ == pObj.typ { break } } if i < 0 { p.pik_error(pErrTok, "no prior objects of the same type") return } } if pOther.nPath != 0 && pObj.typ.isLine { var dx, dy PNum dx = p.aTPath[0].x - pOther.aPath[0].x dy = p.aTPath[0].y - pOther.aPath[0].y for i := 1; i < pOther.nPath; i++ { p.aTPath[i].x = pOther.aPath[i].x + dx p.aTPath[i].y = pOther.aPath[i].y + dy } p.nTPath = pOther.nPath p.mTPath = 3 p.samePath = true } if !pObj.typ.isLine { pObj.w = pOther.w pObj.h = pOther.h } pObj.rad = pOther.rad pObj.sw = pOther.sw pObj.dashed = pOther.dashed pObj.dotted = pOther.dotted pObj.fill = pOther.fill pObj.color = pOther.color pObj.cw = pOther.cw pObj.larrow = pOther.larrow pObj.rarrow = pOther.rarrow pObj.bClose = pOther.bClose pObj.bChop = pOther.bChop pObj.inDir = pOther.inDir pObj.outDir = pOther.outDir pObj.iLayer = pOther.iLayer } /* Return a "Place" associated with object pObj. If pEdge is NULL ** return the center of the object. Otherwise, return the corner ** described by pEdge. */ func (p *Pik) pik_place_of_elem(pObj *PObj, pEdge *PToken) PPoint { pt := PPoint{} var pClass *PClass if pObj == nil { return pt } if pEdge == nil { return pObj.ptAt } pClass = pObj.typ if pEdge.eType == T_EDGEPT || (pEdge.eEdge > 0 && pEdge.eEdge < CP_END) { pt = pClass.xOffset(p, pObj, pEdge.eEdge) pt.x += pObj.ptAt.x pt.y += pObj.ptAt.y return pt } if pEdge.eType == T_START { return pObj.ptEnter } else { return pObj.ptExit } } /* Do a linear interpolation of two positions. */ func pik_position_between(x PNum, p1 PPoint, p2 PPoint) PPoint { var out PPoint out.x = p2.x*x + p1.x*(1.0-x) out.y = p2.y*x + p1.y*(1.0-x) return out } /* Compute the position that is dist away from pt at an heading angle of r ** ** The angle is a compass heading in degrees. North is 0 (or 360). ** East is 90. South is 180. West is 270. And so forth. */ func pik_position_at_angle(dist PNum, r PNum, pt PPoint) PPoint { r *= 0.017453292519943295769 /* degrees to radians */ pt.x += dist * math.Sin(r) pt.y += dist * math.Cos(r) return pt } /* Compute the position that is dist away at a compass point */ func pik_position_at_hdg(dist PNum, pD *PToken, pt PPoint) PPoint { return pik_position_at_angle(dist, pik_hdg_angle[pD.eEdge], pt) } /* Return the coordinates for the n-th vertex of a line. */ func (p *Pik) pik_nth_vertex(pNth *PToken, pErr *PToken, pObj *PObj) PPoint { var n int zero := PPoint{} if p.nErr != 0 || pObj == nil { return p.aTPath[0] } if !pObj.typ.isLine { p.pik_error(pErr, "object is not a line") return zero } n, err := strconv.Atoi(string(pNth.z[:pNth.n-2])) if err != nil || n < 1 || n > pObj.nPath { p.pik_error(pNth, "no such vertex") return zero } return pObj.aPath[n-1] } /* Return the value of a property of an object. */ func pik_property_of(pObj *PObj, pProp *PToken) PNum { var v PNum if pObj != nil { switch pProp.eType { case T_HEIGHT: v = pObj.h case T_WIDTH: v = pObj.w case T_RADIUS: v = pObj.rad case T_DIAMETER: v = pObj.rad * 2.0 case T_THICKNESS: v = pObj.sw case T_DASHED: v = pObj.dashed case T_DOTTED: v = pObj.dotted case T_FILL: v = pObj.fill case T_COLOR: v = pObj.color case T_X: v = pObj.ptAt.x case T_Y: v = pObj.ptAt.y case T_TOP: v = pObj.bbox.ne.y case T_BOTTOM: v = pObj.bbox.sw.y case T_LEFT: v = pObj.bbox.sw.x case T_RIGHT: v = pObj.bbox.ne.x } } return v } /* Compute one of the built-in functions */ func (p *Pik) pik_func(pFunc *PToken, x PNum, y PNum) PNum { var v PNum switch pFunc.eCode { case FN_ABS: if x < 0 { v = -x } else { v = x } case FN_COS: v = math.Cos(x) case FN_INT: v = math.Round(x) case FN_SIN: v = math.Sin(x) case FN_SQRT: if x < 0.0 { p.pik_error(pFunc, "sqrt of negative value") v = 0.0 } else { v = math.Sqrt(x) } case FN_MAX: if x > y { v = x } else { v = y } case FN_MIN: if x < y { v = x } else { v = y } default: v = 0.0 } return v } /* Attach a name to an object */ func (p *Pik) pik_elem_setname(pObj *PObj, pName *PToken) { if pObj == nil { return } if pName == nil { return } pObj.zName = pName.String() } /* ** Search for object located at *pCenter that has an xChop method and ** that does not enclose point pOther. ** ** Return a pointer to the object, or NULL if not found. */ func pik_find_chopper(pList PList, pCenter *PPoint, pOther *PPoint) *PObj { if pList == nil { return nil } for i := len(pList) - 1; i >= 0; i-- { pObj := pList[i] if pObj.typ.xChop != nil && pObj.ptAt.x == pCenter.x && pObj.ptAt.y == pCenter.y && !pik_bbox_contains_point(&pObj.bbox, pOther) { return pObj } else if pObj.pSublist != nil { pObj = pik_find_chopper(pObj.pSublist, pCenter, pOther) if pObj != nil { return pObj } } } return nil } /* ** There is a line traveling from pFrom to pTo. ** ** If pObj is not null and is a choppable object, then chop at ** the boundary of pObj - where the line crosses the boundary ** of pObj. ** ** If pObj is NULL or has no xChop method, then search for some ** other object centered at pTo that is choppable and use it ** instead. */ func (p *Pik) pik_autochop(pFrom *PPoint, pTo *PPoint, pObj *PObj) { if pObj == nil || pObj.typ.xChop == nil { pObj = pik_find_chopper(p.list, pTo, pFrom) } if pObj != nil { *pTo = pObj.typ.xChop(p, pObj, pFrom) } } /* This routine runs after all attributes have been received ** on an object. */ func (p *Pik) pik_after_adding_attributes(pObj *PObj) { if p.nErr != 0 { return } /* Position block objects */ if !pObj.typ.isLine { /* A height or width less than or equal to zero means "autofit". ** Change the height or width to be big enough to contain the text, */ if pObj.h <= 0.0 { if pObj.nTxt == 0 { pObj.h = 0.0 } else if pObj.w <= 0.0 { p.pik_size_to_fit(&pObj.errTok, 3) } else { p.pik_size_to_fit(&pObj.errTok, 2) } } if pObj.w <= 0.0 { if pObj.nTxt == 0 { pObj.w = 0.0 } else { p.pik_size_to_fit(&pObj.errTok, 1) } } ofst := p.pik_elem_offset(pObj, pObj.eWith) var dx PNum = (pObj.with.x - ofst.x) - pObj.ptAt.x var dy PNum = (pObj.with.y - ofst.y) - pObj.ptAt.y if dx != 0 || dy != 0 { pik_elem_move(pObj, dx, dy) } } /* For a line object with no movement specified, a single movement ** of the default length in the current direction */ if pObj.typ.isLine && p.nTPath < 2 { p.pik_next_rpath(nil) assert(p.nTPath == 2, fmt.Sprintf("want p.nTPath==2; got %d", p.nTPath)) switch pObj.inDir { default: p.aTPath[1].x += pObj.w case DIR_DOWN: p.aTPath[1].y -= pObj.h case DIR_LEFT: p.aTPath[1].x -= pObj.w case DIR_UP: p.aTPath[1].y += pObj.h } if pObj.typ.zName == "arc" { add := uint8(3) if pObj.cw { add = 1 } pObj.outDir = (pObj.inDir + add) % 4 p.eDir = pObj.outDir switch pObj.outDir { default: p.aTPath[1].x += pObj.w case DIR_DOWN: p.aTPath[1].y -= pObj.h case DIR_LEFT: p.aTPath[1].x -= pObj.w case DIR_UP: p.aTPath[1].y += pObj.h } } } /* Initialize the bounding box prior to running xCheck */ pik_bbox_init(&pObj.bbox) /* Run object-specific code */ if pObj.typ.xCheck != nil { pObj.typ.xCheck(p, pObj) if p.nErr != 0 { return } } /* Compute final bounding box, entry and exit points, center ** point (ptAt) and path for the object */ if pObj.typ.isLine { pObj.aPath = make([]PPoint, p.nTPath) pObj.nPath = p.nTPath copy(pObj.aPath, p.aTPath[:p.nTPath]) /* "chop" processing: ** If the line goes to the center of an object with an ** xChop method, then use the xChop method to trim the line. */ if pObj.bChop && pObj.nPath >= 2 { n := pObj.nPath p.pik_autochop(&pObj.aPath[n-2], &pObj.aPath[n-1], pObj.pTo) p.pik_autochop(&pObj.aPath[1], &pObj.aPath[0], pObj.pFrom) } pObj.ptEnter = pObj.aPath[0] pObj.ptExit = pObj.aPath[pObj.nPath-1] /* Compute the center of the line based on the bounding box over ** the vertexes. This is a difference from PIC. In Pikchr, the ** center of a line is the center of its bounding box. In PIC, the ** center of a line is halfway between its .start and .end. For ** straight lines, this is the same point, but for multi-segment ** lines the result is usually diferent */ for i := 0; i < pObj.nPath; i++ { pik_bbox_add_xy(&pObj.bbox, pObj.aPath[i].x, pObj.aPath[i].y) } pObj.ptAt.x = (pObj.bbox.ne.x + pObj.bbox.sw.x) / 2.0 pObj.ptAt.y = (pObj.bbox.ne.y + pObj.bbox.sw.y) / 2.0 /* Reset the width and height of the object to be the width and height ** of the bounding box over vertexes */ pObj.w = pObj.bbox.ne.x - pObj.bbox.sw.x pObj.h = pObj.bbox.ne.y - pObj.bbox.sw.y /* If this is a polygon (if it has the "close" attribute), then ** adjust the exit point */ if pObj.bClose { /* For "closed" lines, the .end is one of the .e, .s, .w, or .n ** points of the bounding box, as with block objects. */ pik_elem_set_exit(pObj, pObj.inDir) } } else { var w2 PNum = pObj.w / 2.0 var h2 PNum = pObj.h / 2.0 pObj.ptEnter = pObj.ptAt pObj.ptExit = pObj.ptAt switch pObj.inDir { default: pObj.ptEnter.x -= w2 case DIR_LEFT: pObj.ptEnter.x += w2 case DIR_UP: pObj.ptEnter.y -= h2 case DIR_DOWN: pObj.ptEnter.y += h2 } switch pObj.outDir { default: pObj.ptExit.x += w2 case DIR_LEFT: pObj.ptExit.x -= w2 case DIR_UP: pObj.ptExit.y += h2 case DIR_DOWN: pObj.ptExit.y -= h2 } pik_bbox_add_xy(&pObj.bbox, pObj.ptAt.x-w2, pObj.ptAt.y-h2) pik_bbox_add_xy(&pObj.bbox, pObj.ptAt.x+w2, pObj.ptAt.y+h2) } p.eDir = pObj.outDir } /* Show basic information about each object as a comment in the ** generated HTML. Used for testing and debugging. Activated ** by the (undocumented) "debug = 1;" ** command. */ func (p *Pik) pik_elem_render(pObj *PObj) { var zDir string if pObj == nil { return } p.pik_append("<!-- ") if pObj.zName != "" { p.pik_append_text(pObj.zName, 0) p.pik_append(": ") } p.pik_append_text(pObj.typ.zName, 0) if pObj.nTxt != 0 { p.pik_append(" \"") z := pObj.aTxt[0] p.pik_append_text(string(z.z[1:z.n-1]), 1) p.pik_append("\"") } p.pik_append_num(" w=", pObj.w) p.pik_append_num(" h=", pObj.h) p.pik_append_point(" center=", &pObj.ptAt) p.pik_append_point(" enter=", &pObj.ptEnter) switch pObj.outDir { default: zDir = " right" case DIR_LEFT: zDir = " left" case DIR_UP: zDir = " up" case DIR_DOWN: zDir = " down" } p.pik_append_point(" exit=", &pObj.ptExit) p.pik_append(zDir) p.pik_append(" -->\n") } /* Render a list of objects */ func (p *Pik) pik_elist_render(pList PList) { var iNextLayer, iThisLayer int bMoreToDo := true mDebug := p.pik_value_int("debug", nil) for bMoreToDo { bMoreToDo = false iThisLayer = iNextLayer iNextLayer = 0x7fffffff for i := 0; i < len(pList); i++ { pObj := pList[i] if pObj.iLayer > iThisLayer { if pObj.iLayer < iNextLayer { iNextLayer = pObj.iLayer } bMoreToDo = true continue /* Defer until another round */ } else if pObj.iLayer < iThisLayer { continue } if mDebug&1 != 0 { p.pik_elem_render(pObj) } xRender := pObj.typ.xRender if xRender != nil { xRender(p, pObj) } if pObj.pSublist != nil { p.pik_elist_render(pObj.pSublist) } } } /* If the color_debug_label value is defined, then go through ** and paint a dot at every label location */ miss := false var colorLabel PNum = p.pik_value("debug_label_color", &miss) if !miss && colorLabel >= 0.0 { dot := PObj{} dot.typ = &noopClass dot.rad = 0.015 dot.sw = 0.015 dot.fill = colorLabel dot.color = colorLabel dot.nTxt = 1 dot.aTxt[0].eCode = TP_ABOVE for i := 0; i < len(pList); i++ { pObj := pList[i] if pObj.zName == "" { continue } dot.ptAt = pObj.ptAt dot.aTxt[0].z = []byte(pObj.zName) dot.aTxt[0].n = len(dot.aTxt[0].z) dotRender(p, &dot) } } } /* Add all objects of the list pList to the bounding box */ func (p *Pik) pik_bbox_add_elist(pList PList, wArrow PNum) { for i := 0; i < len(pList); i++ { pObj := pList[i] if pObj.sw > 0.0 { pik_bbox_addbox(&p.bbox, &pObj.bbox) } p.pik_append_txt(pObj, &p.bbox) if pObj.pSublist != nil { p.pik_bbox_add_elist(pObj.pSublist, wArrow) } /* Expand the bounding box to account for arrowheads on lines */ if pObj.typ.isLine && pObj.nPath > 0 { if pObj.larrow { pik_bbox_addellipse(&p.bbox, pObj.aPath[0].x, pObj.aPath[0].y, wArrow, wArrow) } if pObj.rarrow { j := pObj.nPath - 1 pik_bbox_addellipse(&p.bbox, pObj.aPath[j].x, pObj.aPath[j].y, wArrow, wArrow) } } } } /* Recompute key layout parameters from variables. */ func (p *Pik) pik_compute_layout_settings() { var thickness PNum /* Line thickness */ var wArrow PNum /* Width of arrowheads */ /* Set up rendering parameters */ if p.bLayoutVars { return } thickness = p.pik_value("thickness", nil) if thickness <= 0.01 { thickness = 0.01 } wArrow = 0.5 * p.pik_value("arrowwid", nil) p.wArrow = wArrow / thickness p.hArrow = p.pik_value("arrowht", nil) / thickness p.fontScale = p.pik_value("fontscale", nil) if p.fontScale <= 0.0 { p.fontScale = 1.0 } p.rScale = 144.0 p.charWidth = p.pik_value("charwid", nil) * p.fontScale p.charHeight = p.pik_value("charht", nil) * p.fontScale p.bLayoutVars = true } /* Render a list of objects. Write the SVG into p.zOut. ** Delete the input object_list before returnning. */ func (p *Pik) pik_render(pList PList) { if pList == nil { return } if p.nErr == 0 { var ( thickness PNum /* Stroke width */ margin PNum /* Extra bounding box margin */ w, h PNum /* Drawing width and height */ wArrow PNum pikScale PNum /* Value of the "scale" variable */ ) /* Set up rendering parameters */ p.pik_compute_layout_settings() thickness = p.pik_value("thickness", nil) if thickness <= 0.01 { thickness = 0.01 } margin = p.pik_value("margin", nil) margin += thickness wArrow = p.wArrow * thickness miss := false p.fgcolor = p.pik_value_int("fgcolor", &miss) if miss { var t PToken t.z = []byte("fgcolor") t.n = 7 p.fgcolor = pik_round((*Pik)(nil).pik_lookup_color(&t)) } miss = false p.bgcolor = p.pik_value_int("bgcolor", &miss) if miss { var t PToken t.z = []byte("bgcolor") t.n = 7 p.bgcolor = pik_round((*Pik)(nil).pik_lookup_color(&t)) } /* Compute a bounding box over all objects so that we can know ** how big to declare the SVG canvas */ pik_bbox_init(&p.bbox) p.pik_bbox_add_elist(pList, wArrow) /* Expand the bounding box slightly to account for line thickness ** and the optional "margin = EXPR" setting. */ p.bbox.ne.x += margin + p.pik_value("rightmargin", nil) p.bbox.ne.y += margin + p.pik_value("topmargin", nil) p.bbox.sw.x -= margin + p.pik_value("leftmargin", nil) p.bbox.sw.y -= margin + p.pik_value("bottommargin", nil) /* Output the SVG */ p.pik_append("<svg xmlns='http://www.w3.org/2000/svg'") if p.zClass != "" { p.pik_append(" class=\"") p.pik_append(p.zClass) p.pik_append("\"") } w = p.bbox.ne.x - p.bbox.sw.x h = p.bbox.ne.y - p.bbox.sw.y p.wSVG = pik_round(p.rScale * w) p.hSVG = pik_round(p.rScale * h) pikScale = p.pik_value("scale", nil) if pikScale >= 0.001 && pikScale <= 1000.0 && (pikScale < 0.99 || pikScale > 1.01) { p.wSVG = pik_round(PNum(p.wSVG) * pikScale) p.hSVG = pik_round(PNum(p.hSVG) * pikScale) p.pik_append_num(" width=\"", PNum(p.wSVG)) p.pik_append_num("\" height=\"", PNum(p.hSVG)) p.pik_append("\"") } else { if p.svgWidth != "" { p.pik_append(` width="`) p.pik_append(p.svgWidth) p.pik_append(`"`) } if p.svgHeight != "" { p.pik_append(` height="`) p.pik_append(p.svgHeight) p.pik_append(`"`) } } p.pik_append_dis(" viewBox=\"0 0 ", w, "") p.pik_append_dis(" ", h, "\">\n") p.pik_elist_render(pList) p.pik_append("</svg>\n") } else { p.wSVG = -1 p.hSVG = -1 } p.pik_elist_free(&pList) } /* ** An array of this structure defines a list of keywords. */ type PikWord struct { zWord string /* Text of the keyword */ //TODO(zellyn): do we need this? nChar uint8 /* Length of keyword text in bytes */ eType uint8 /* Token code */ eCode uint8 /* Extra code for the token */ eEdge uint8 /* CP_* code for corner/edge keywords */ } /* ** Keywords */ var pik_keywords = []PikWord{ {"above", 5, T_ABOVE, 0, 0}, {"abs", 3, T_FUNC1, FN_ABS, 0}, {"aligned", 7, T_ALIGNED, 0, 0}, {"and", 3, T_AND, 0, 0}, {"as", 2, T_AS, 0, 0}, {"assert", 6, T_ASSERT, 0, 0}, {"at", 2, T_AT, 0, 0}, {"behind", 6, T_BEHIND, 0, 0}, {"below", 5, T_BELOW, 0, 0}, {"between", 7, T_BETWEEN, 0, 0}, {"big", 3, T_BIG, 0, 0}, {"bold", 4, T_BOLD, 0, 0}, {"bot", 3, T_EDGEPT, 0, CP_S}, {"bottom", 6, T_BOTTOM, 0, CP_S}, {"c", 1, T_EDGEPT, 0, CP_C}, {"ccw", 3, T_CCW, 0, 0}, {"center", 6, T_CENTER, 0, CP_C}, {"chop", 4, T_CHOP, 0, 0}, {"close", 5, T_CLOSE, 0, 0}, {"color", 5, T_COLOR, 0, 0}, {"cos", 3, T_FUNC1, FN_COS, 0}, {"cw", 2, T_CW, 0, 0}, {"dashed", 6, T_DASHED, 0, 0}, {"define", 6, T_DEFINE, 0, 0}, {"diameter", 8, T_DIAMETER, 0, 0}, {"dist", 4, T_DIST, 0, 0}, {"dotted", 6, T_DOTTED, 0, 0}, {"down", 4, T_DOWN, DIR_DOWN, 0}, {"e", 1, T_EDGEPT, 0, CP_E}, {"east", 4, T_EDGEPT, 0, CP_E}, {"end", 3, T_END, 0, CP_END}, {"even", 4, T_EVEN, 0, 0}, {"fill", 4, T_FILL, 0, 0}, {"first", 5, T_NTH, 0, 0}, {"fit", 3, T_FIT, 0, 0}, {"from", 4, T_FROM, 0, 0}, {"go", 2, T_GO, 0, 0}, {"heading", 7, T_HEADING, 0, 0}, {"height", 6, T_HEIGHT, 0, 0}, {"ht", 2, T_HEIGHT, 0, 0}, {"in", 2, T_IN, 0, 0}, {"int", 3, T_FUNC1, FN_INT, 0}, {"invis", 5, T_INVIS, 0, 0}, {"invisible", 9, T_INVIS, 0, 0}, {"italic", 6, T_ITALIC, 0, 0}, {"last", 4, T_LAST, 0, 0}, {"left", 4, T_LEFT, DIR_LEFT, CP_W}, {"ljust", 5, T_LJUST, 0, 0}, {"max", 3, T_FUNC2, FN_MAX, 0}, {"min", 3, T_FUNC2, FN_MIN, 0}, {"n", 1, T_EDGEPT, 0, CP_N}, {"ne", 2, T_EDGEPT, 0, CP_NE}, {"north", 5, T_EDGEPT, 0, CP_N}, {"nw", 2, T_EDGEPT, 0, CP_NW}, {"of", 2, T_OF, 0, 0}, {"previous", 8, T_LAST, 0, 0}, {"print", 5, T_PRINT, 0, 0}, {"rad", 3, T_RADIUS, 0, 0}, {"radius", 6, T_RADIUS, 0, 0}, {"right", 5, T_RIGHT, DIR_RIGHT, CP_E}, {"rjust", 5, T_RJUST, 0, 0}, {"s", 1, T_EDGEPT, 0, CP_S}, {"same", 4, T_SAME, 0, 0}, {"se", 2, T_EDGEPT, 0, CP_SE}, {"sin", 3, T_FUNC1, FN_SIN, 0}, {"small", 5, T_SMALL, 0, 0}, {"solid", 5, T_SOLID, 0, 0}, {"south", 5, T_EDGEPT, 0, CP_S}, {"sqrt", 4, T_FUNC1, FN_SQRT, 0}, {"start", 5, T_START, 0, CP_START}, {"sw", 2, T_EDGEPT, 0, CP_SW}, {"t", 1, T_TOP, 0, CP_N}, {"the", 3, T_THE, 0, 0}, {"then", 4, T_THEN, 0, 0}, {"thick", 5, T_THICK, 0, 0}, {"thickness", 9, T_THICKNESS, 0, 0}, {"thin", 4, T_THIN, 0, 0}, {"this", 4, T_THIS, 0, 0}, {"to", 2, T_TO, 0, 0}, {"top", 3, T_TOP, 0, CP_N}, {"until", 5, T_UNTIL, 0, 0}, {"up", 2, T_UP, DIR_UP, 0}, {"vertex", 6, T_VERTEX, 0, 0}, {"w", 1, T_EDGEPT, 0, CP_W}, {"way", 3, T_WAY, 0, 0}, {"west", 4, T_EDGEPT, 0, CP_W}, {"wid", 3, T_WIDTH, 0, 0}, {"width", 5, T_WIDTH, 0, 0}, {"with", 4, T_WITH, 0, 0}, {"x", 1, T_X, 0, 0}, {"y", 1, T_Y, 0, 0}, } /* ** Search a PikWordlist for the given keyword. Return a pointer to the ** keyword entry found. Or return 0 if not found. */ func pik_find_word( zIn string, /* Word to search for */ aList []PikWord, /* List to search */ ) *PikWord { first := 0 last := len(aList) - 1 for first <= last { mid := (first + last) / 2 c := strings.Compare(zIn, aList[mid].zWord) if c == 0 { return &aList[mid] } if c < 0 { last = mid - 1 } else { first = mid + 1 } } return nil } /* ** Set a symbolic debugger breakpoint on this routine to receive a ** breakpoint when the "#breakpoint" token is parsed. */ func pik_breakpoint(z []byte) { /* Prevent C compilers from optimizing out this routine. */ if z[2] == 'X' { os.Exit(1) } } var aEntity = []struct { eCode int /* Corresponding token code */ zEntity string /* Name of the HTML entity */ }{ {T_RARROW, "→"}, /* Same as . */ {T_RARROW, "→"}, /* Same as . */ {T_LARROW, "←"}, /* Same as <- */ {T_LARROW, "←"}, /* Same as <- */ {T_LRARROW, "↔"}, /* Same as <. */ } /* ** Return the length of next token. The token starts on ** the pToken->z character. Fill in other fields of the ** pToken object as appropriate. */ func pik_token_length(pToken *PToken, bAllowCodeBlock bool) int { z := pToken.z var i int switch z[0] { case '\\': pToken.eType = T_WHITESPACE for i = 1; z[i] == '\r' || z[i] == ' ' || z[i] == '\t'; i++ { } if z[i] == '\n' { return i + 1 } pToken.eType = T_ERROR return 1 case ';', '\n': pToken.eType = T_EOL return 1 case '"': for i = 1; z[i] != 0; i++ { c := z[i] if c == '\\' { if z[i+1] == 0 { break } i++ continue } if c == '"' { pToken.eType = T_STRING return i + 1 } } pToken.eType = T_ERROR return i case ' ', '\t', '\f', '\r': for i = 1; z[i] == ' ' || z[i] == '\t' || z[i] == '\r' || z[i] == '\f'; i++ { } pToken.eType = T_WHITESPACE return i case '#': for i = 1; z[i] != 0 && z[i] != '\n'; i++ { } pToken.eType = T_WHITESPACE /* If the comment is "#breakpoint" then invoke the pik_breakpoint() ** routine. The pik_breakpoint() routie is a no-op that serves as ** a convenient place to set a gdb breakpoint when debugging. */ if i >= 11 && string(z[:11]) == "#breakpoint" { pik_breakpoint(z) } return i case '/': if z[1] == '*' { for i = 2; z[i] != 0 && (z[i] != '*' || z[i+1] != '/'); i++ { } if z[i] == '*' { pToken.eType = T_WHITESPACE return i + 2 } else { pToken.eType = T_ERROR return i } } else if z[1] == '/' { for i = 2; z[i] != 0 && z[i] != '\n'; i++ { } pToken.eType = T_WHITESPACE return i } else if z[1] == '=' { pToken.eType = T_ASSIGN pToken.eCode = T_SLASH return 2 } else { pToken.eType = T_SLASH return 1 } case '+': if z[1] == '=' { pToken.eType = T_ASSIGN pToken.eCode = T_PLUS return 2 } pToken.eType = T_PLUS return 1 case '*': if z[1] == '=' { pToken.eType = T_ASSIGN pToken.eCode = T_STAR return 2 } pToken.eType = T_STAR return 1 case '%': pToken.eType = T_PERCENT return 1 case '(': pToken.eType = T_LP return 1 case ')': pToken.eType = T_RP return 1 case '[': pToken.eType = T_LB return 1 case ']': pToken.eType = T_RB return 1 case ',': pToken.eType = T_COMMA return 1 case ':': pToken.eType = T_COLON return 1 case '>': pToken.eType = T_GT return 1 case '=': if z[1] == '=' { pToken.eType = T_EQ return 2 } pToken.eType = T_ASSIGN pToken.eCode = T_ASSIGN return 1 case '-': if z[1] == '>' { pToken.eType = T_RARROW return 2 } else if z[1] == '=' { pToken.eType = T_ASSIGN pToken.eCode = T_MINUS return 2 } else { pToken.eType = T_MINUS return 1 } case '<': if z[1] == '-' { if z[2] == '>' { pToken.eType = T_LRARROW return 3 } else { pToken.eType = T_LARROW return 2 } } else { pToken.eType = T_LT return 1 } case 0xe2: if z[1] == 0x86 { if z[2] == 0x90 { pToken.eType = T_LARROW /* <- */ return 3 } if z[2] == 0x92 { pToken.eType = T_RARROW /* . */ return 3 } if z[2] == 0x94 { pToken.eType = T_LRARROW /* <. */ return 3 } } pToken.eType = T_ERROR return 1 case '{': var depth int i = 1 if bAllowCodeBlock { depth = 1 for z[i] != 0 && depth > 0 { var x PToken x.z = z[i:] len := pik_token_length(&x, false) if len == 1 { if z[i] == '{' { depth++ } if z[i] == '}' { depth-- } } i += len } } else { depth = 0 } if depth != 0 { pToken.eType = T_ERROR return 1 } pToken.eType = T_CODEBLOCK return i case '&': for i, ent := range aEntity { if bytencmp(z, aEntity[i].zEntity, len(aEntity[i].zEntity)) == 0 { pToken.eType = uint8(ent.eCode) return len(aEntity[i].zEntity) } } pToken.eType = T_ERROR return 1 default: c := z[0] if c == '.' { c1 := z[1] if islower(c1) { for i = 2; z[i] >= 'a' && z[i] <= 'z'; i++ { } pFound := pik_find_word(string(z[1:i]), pik_keywords) if pFound != nil && (pFound.eEdge > 0 || pFound.eType == T_EDGEPT || pFound.eType == T_START || pFound.eType == T_END) { /* Dot followed by something that is a 2-D place value */ pToken.eType = T_DOT_E } else if pFound != nil && (pFound.eType == T_X || pFound.eType == T_Y) { /* Dot followed by "x" or "y" */ pToken.eType = T_DOT_XY } else { /* Any other "dot" */ pToken.eType = T_DOT_L } return 1 } else if isdigit(c1) { i = 0 /* no-op. Fall through to number handling */ } else if isupper(c1) { for i = 2; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { } pToken.eType = T_DOT_U return 1 } else { pToken.eType = T_ERROR return 1 } } if (c >= '0' && c <= '9') || c == '.' { var nDigit int isInt := true if c != '.' { nDigit = 1 for i = 1; ; i++ { c = z[i] if c < '0' || c > '9' { break } nDigit++ } if i == 1 && (c == 'x' || c == 'X') { for i = 2; z[i] != 0 && isxdigit(z[i]); i++ { } pToken.eType = T_NUMBER return i } } else { isInt = false nDigit = 0 i = 0 } if c == '.' { isInt = false for i++; ; i++ { c = z[i] if c < '0' || c > '9' { break } nDigit++ } } if nDigit == 0 { pToken.eType = T_ERROR return i } if c == 'e' || c == 'E' { iBefore := i i++ c2 := z[i] if c2 == '+' || c2 == '-' { i++ c2 = z[i] } if c2 < '0' || c > '9' { /* This is not an exp */ i = iBefore } else { i++ isInt = false for { c = z[i] if c < '0' || c > '9' { break } i++ } } } var c2 byte if c != 0 { c2 = z[i+1] } if isInt { if (c == 't' && c2 == 'h') || (c == 'r' && c2 == 'd') || (c == 'n' && c2 == 'd') || (c == 's' && c2 == 't') { pToken.eType = T_NTH return i + 2 } } if (c == 'i' && c2 == 'n') || (c == 'c' && c2 == 'm') || (c == 'm' && c2 == 'm') || (c == 'p' && c2 == 't') || (c == 'p' && c2 == 'x') || (c == 'p' && c2 == 'c') { i += 2 } pToken.eType = T_NUMBER return i } else if islower(c) { for i = 1; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { } pFound := pik_find_word(string(z[:i]), pik_keywords) if pFound != nil { pToken.eType = pFound.eType pToken.eCode = int16(pFound.eCode) pToken.eEdge = pFound.eEdge return i } pToken.n = i if pik_find_class(pToken) != nil { pToken.eType = T_CLASSNAME } else { pToken.eType = T_ID } return i } else if c >= 'A' && c <= 'Z' { for i = 1; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { } pToken.eType = T_PLACENAME return i } else if c == '$' && z[1] >= '1' && z[1] <= '9' && !isdigit(z[2]) { pToken.eType = T_PARAMETER pToken.eCode = int16(z[1] - '1') return 2 } else if c == '_' || c == '$' || c == '@' { for i = 1; z[i] != 0 && (isalnum(z[i]) || z[i] == '_'); i++ { } pToken.eType = T_ID return i } else { pToken.eType = T_ERROR return 1 } } } /* ** Return a pointer to the next non-whitespace token after pThis. ** This is used to help form error messages. */ func pik_next_semantic_token(pThis *PToken) PToken { var x PToken i := pThis.n x.z = pThis.z for { x.z = pThis.z[i:] sz := pik_token_length(&x, true) if x.eType != T_WHITESPACE { x.n = sz return x } i += sz } } /* Parser arguments to a macro invocation ** ** (arg1, arg2, ...) ** ** Arguments are comma-separated, except that commas within string ** literals or with (...), {...}, or [...] do not count. The argument ** list begins and ends with parentheses. There can be at most 9 ** arguments. ** ** Return the number of bytes in the argument list. */ func (p *Pik) pik_parse_macro_args( z []byte, /* Start of the argument list */ n int, /* Available bytes */ args []PToken, /* Fill in with the arguments */ pOuter []PToken, /* Arguments of the next outer context, or NULL */ ) int { nArg := 0 var i, sz int depth := 0 var x PToken if z[0] != '(' { return 0 } args[0].z = z[1:] iStart := 1 for i = 1; i < n && z[i] != ')'; i += sz { x.z = z[i:] sz = pik_token_length(&x, false) if sz != 1 { continue } if z[i] == ',' && depth <= 0 { args[nArg].n = i - iStart if nArg == 8 { x.z = z x.n = 1 p.pik_error(&x, "too many macro arguments - max 9") return 0 } nArg++ args[nArg].z = z[i+1:] iStart = i + 1 depth = 0 } else if z[i] == '(' || z[i] == '{' || z[i] == '[' { depth++ } else if z[i] == ')' || z[i] == '}' || z[i] == ']' { depth-- } } if z[i] == ')' { args[nArg].n = i - iStart /* Remove leading and trailing whitespace from each argument. ** If what remains is one of $1, $2, ... $9 then transfer the ** corresponding argument from the outer context */ for j := 0; j <= nArg; j++ { t := &args[j] for t.n > 0 && isspace(t.z[0]) { t.n-- t.z = t.z[1:] } for t.n > 0 && isspace(t.z[t.n-1]) { t.n-- } if t.n == 2 && t.z[0] == '$' && t.z[1] >= '1' && t.z[1] <= '9' { if pOuter != nil { *t = pOuter[t.z[1]-'1'] } else { t.n = 0 } } } return i + 1 } x.z = z x.n = 1 p.pik_error(&x, "unterminated macro argument list") return 0 } /* ** Split up the content of a PToken into multiple tokens and ** send each to the parser. */ func (p *Pik) pik_tokenize(pIn *PToken, pParser *yyParser, aParam []PToken) { sz := 0 var token PToken for i := 0; i < pIn.n && pIn.z[i] != 0 && p.nErr == 0; i += sz { token.eCode = 0 token.eEdge = 0 token.z = pIn.z[i:] sz = pik_token_length(&token, true) if token.eType == T_WHITESPACE { continue /* no-op */ } if sz > 50000 { token.n = 1 p.pik_error(&token, "token is too long - max length 50000 bytes") break } if token.eType == T_ERROR { token.n = sz p.pik_error(&token, "unrecognized token") break } if sz+i > pIn.n { token.n = pIn.n - i p.pik_error(&token, "syntax error") break } if token.eType == T_PARAMETER { /* Substitute a parameter into the input stream */ if aParam == nil || aParam[token.eCode].n == 0 { continue } token.n = sz if p.nCtx >= len(p.aCtx) { p.pik_error(&token, "macros nested too deep") } else { p.aCtx[p.nCtx] = token p.nCtx++ p.pik_tokenize(&aParam[token.eCode], pParser, nil) p.nCtx-- } continue } if token.eType == T_ID { token.n = sz pMac := p.pik_find_macro(&token) if pMac != nil { args := make([]PToken, 9) j := i + sz if pMac.inUse { p.pik_error(&pMac.macroName, "recursive macro definition") break } token.n = sz if p.nCtx >= len(p.aCtx) { p.pik_error(&token, "macros nested too deep") break } pMac.inUse = true p.aCtx[p.nCtx] = token p.nCtx++ sz += p.pik_parse_macro_args(pIn.z[j:], pIn.n-j, args, aParam) p.pik_tokenize(&pMac.macroBody, pParser, args) p.nCtx-- pMac.inUse = false continue } } if false { // #if 0 n := sz if isspace(token.z[0]) { n = 0 } fmt.Printf("******** Token %s (%d): \"%s\" **************\n", yyTokenName[token.eType], token.eType, string(token.z[:n])) } // #endif token.n = sz p.nToken++ if p.nToken > PIKCHR_TOKEN_LIMIT { p.pik_error(&token, "script is too complex"); break; } pParser.pik_parser(token.eType, token) } } /* ** Parse the PIKCHR script contained in zText[]. Return a rendering. Or ** if an error is encountered, return the error text. The error message ** is HTML formatted. So regardless of what happens, the return text ** is safe to be insertd into an HTML output stream. ** ** If pnWidth and pnHeight are not NULL, then this routine writes the ** width and height of the <SVG> object into the integers that they ** point to. A value of -1 is written if an error is seen. ** ** If zClass is not NULL, then it is a class name to be included in ** the <SVG> markup. ** ** The returned string is contained in memory obtained from malloc() ** and should be released by the caller. */ func Pikchr( zText []byte, /* Input PIKCHR source text. zero-terminated */ zClass string, /* Add class="%s" to <svg> markup */ mFlags uint, /* Flags used to influence rendering behavior */ svgWidth, svgHeight string, svgFontScale PNum, pnWidth *int, /* Write width of <svg> here, if not NULL */ pnHeight *int, /* Write height here, if not NULL */ ) []byte { s := Pik{} var sParse yyParser s.sIn.n = len(zText) s.sIn.z = append(zText, 0) s.eDir = DIR_RIGHT s.zClass = zClass s.mFlags = mFlags s.svgWidth = svgWidth s.svgHeight = svgHeight s.svgFontScale = svgFontScale sParse.pik_parserInit(&s) if false { // #if 0 pik_parserTrace(os.Stdout, "parser: ") } // #endif s.pik_tokenize(&s.sIn, &sParse, nil) if s.nErr == 0 { var token PToken if s.sIn.n > 0 { token.z = zText[s.sIn.n-1:] } else { token.z = zText } token.n = 1 sParse.pik_parser(0, token) } sParse.pik_parserFinalize() if s.zOut.Len() == 0 && s.nErr == 0 { s.pik_append("<!-- empty pikchr diagram -->\n") } if pnWidth != nil { if s.nErr != 0 { *pnWidth = -1 } else { *pnWidth = s.wSVG } } if pnHeight != nil { if s.nErr != 0 { *pnHeight = -1 } else { *pnHeight = s.hSVG } } return s.zOut.Bytes() } // #if defined(PIKCHR_FUZZ) // #include <stdint.h> // int LLVMFuzzerTestOneInput(const uint8_t *aData, size_t nByte){ // int w,h; // char *zIn, *zOut; // unsigned int mFlags = nByte & 3; // zIn = malloc( nByte + 1 ); // if( zIn==0 ) return 0; // memcpy(zIn, aData, nByte); // zIn[nByte] = 0; // zOut = pikchr(zIn, "pikchr", mFlags, &w, &h); // free(zIn); // free(zOut); // return 0; // } // #endif /* PIKCHR_FUZZ */ // Helpers added for port to Go func isxdigit(b byte) bool { return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F') } func isalnum(b byte) bool { return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') } func isdigit(b byte) bool { return (b >= '0' && b <= '9') } func isspace(b byte) bool { return b == ' ' || b == '\n' || b == '\t' || b == '\f' } func isupper(b byte) bool { return (b >= 'A' && b <= 'Z') } func islower(b byte) bool { return (b >= 'a' && b <= 'z') } func bytencmp(a []byte, s string, n int) int { return strings.Compare(string(a[:n]), s) } func bytesEq(a, b []byte) bool { if len(a) != len(b) { return false } for i, bb := range a { if b[i] != bb { return false } } return true } } // end %code |
Added parser/pikchr/pikchr.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package pikchr provides a parser to create SVG from a textual PIC-like description. package pikchr import ( "strconv" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/parser/pikchr/internal" ) func init() { parser.Register(&parser.Info{ Name: "pikchr", AltNames: nil, IsTextParser: true, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, m *meta.Meta, _ string) ast.BlockSlice { var w, h int bsSVG := internal.Pikchr( inp.Src[inp.Pos:], "", 0, m.GetDefault("width", ""), m.GetDefault("height", ""), getScale(m, "font-scale"), &w, &h) if w == -1 { return ast.BlockSlice{ &ast.ParaNode{ Inlines: ast.CreateInlineSliceFromWords("Pikchr", "error:"), }, &ast.VerbatimNode{ Kind: ast.VerbatimHTML, Content: bsSVG, }, } } return ast.BlockSlice{&ast.BLOBNode{ Title: "", Syntax: api.ValueSyntaxSVG, Blob: bsSVG, }} } func parseInlines(_ *input.Input, syntax string) ast.InlineSlice { return ast.CreateInlineSliceFromWords("No", "inline", "code", "allowed", "for", "syntax:", syntax) } func getScale(m *meta.Meta, key string) internal.PNum { if val, found := m.Get(key); found { if scale, err := strconv.ParseFloat(val, 64); err == nil && scale > 0.001 && scale < 1000.0 { return internal.PNum(scale) } } return 1.0 } |
Changes to parser/plain/plain.go.
1 | //----------------------------------------------------------------------------- | | < < < < | | | | | | | | | < | | < | | < | | < | | < | | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package plain provides a parser for plain text data. package plain import ( "strings" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: "txt", AltNames: []string{"plain", api.ValueSyntaxText}, IsTextParser: false, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: api.ValueSyntaxHTML, AltNames: []string{}, IsTextParser: false, IsImageFormat: false, ParseBlocks: parseBlocksHTML, ParseInlines: parseInlinesHTML, }) parser.Register(&parser.Info{ Name: "css", AltNames: []string{}, IsTextParser: false, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) parser.Register(&parser.Info{ Name: api.ValueSyntaxSVG, AltNames: []string{}, IsTextParser: false, IsImageFormat: true, ParseBlocks: parseSVGBlocks, ParseInlines: parseSVGInlines, }) parser.Register(&parser.Info{ Name: "mustache", AltNames: []string{}, IsTextParser: false, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { return doParseBlocks(inp, syntax, ast.VerbatimProg) } func parseBlocksHTML(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { |
︙ | ︙ | |||
131 132 133 134 135 136 137 | svgSrc := string(inp.Src[inp.Pos:]) if !strings.HasPrefix(svgSrc, "<svg ") { return "" } // TODO: check proper end </svg> return svgSrc } | < < < < < < < < < < < < < < < < < < < < < < < < < < < | 122 123 124 125 126 127 128 | svgSrc := string(inp.Src[inp.Pos:]) if !strings.HasPrefix(svgSrc, "<svg ") { return "" } // TODO: check proper end </svg> return svgSrc } |
Changes to parser/zettelmark/block.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package zettelmark import ( "fmt" "zettelstore.de/z/ast" "zettelstore.de/z/input" ) // parseBlockSlice parses a sequence of blocks. func (cp *zmkP) parseBlockSlice() ast.BlockSlice { inp := cp.inp var lastPara *ast.ParaNode bs := ast.BlockSlice{} |
︙ | ︙ | |||
407 408 409 410 411 412 413 | } } return ln, newLnCount } func (cp *zmkP) cleanupParsedNestedList(newLnCount int) (res ast.BlockNode, success bool) { listDepth := len(cp.lists) | | | 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 | } } return ln, newLnCount } func (cp *zmkP) cleanupParsedNestedList(newLnCount int) (res ast.BlockNode, success bool) { listDepth := len(cp.lists) for i := 0; i < newLnCount; i++ { childPos := listDepth - i - 1 parentPos := childPos - 1 if parentPos < 0 { return cp.lists[0], true } if prevItems := cp.lists[parentPos].Items; len(prevItems) > 0 { lastItem := len(prevItems) - 1 |
︙ | ︙ |
Changes to parser/zettelmark/inline.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | 1 2 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package zettelmark import ( "bytes" "fmt" "strings" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/input" ) // parseInlineSlice parses a sequence of Inlines until EOS. func (cp *zmkP) parseInlineSlice() (ins ast.InlineSlice) { inp := cp.inp for inp.Ch != input.EOS { in := cp.parseInline() |
︙ | ︙ | |||
68 69 70 71 72 73 74 | case '{': inp.Next() if inp.Ch == '{' { in, success = cp.parseEmbed() } case '%': in, success = cp.parseComment() | | | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | case '{': inp.Next() if inp.Ch == '{' { in, success = cp.parseEmbed() } case '%': in, success = cp.parseComment() case '_', '*', '>', '~', '^', ',', '"', ':': in, success = cp.parseFormat() case '@', '\'', '`', '=', runeModGrave: in, success = cp.parseLiteral() case '$': in, success = cp.parseLiteralMath() case '\\': return cp.parseBackslash() |
︙ | ︙ | |||
101 102 103 104 105 106 107 | return cp.parseBackslashRest() } for { inp.Next() switch inp.Ch { // The following case must contain all runes that occur in parseInline! // Plus the closing brackets ] and } and ) and the middle | | | | 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | return cp.parseBackslashRest() } for { inp.Next() switch inp.Ch { // The following case must contain all runes that occur in parseInline! // Plus the closing brackets ] and } and ) and the middle | case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&': return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])} } } } func (cp *zmkP) parseBackslash() ast.InlineNode { inp := cp.inp |
︙ | ︙ | |||
152 153 154 155 156 157 158 | func (cp *zmkP) parseSoftBreak() *ast.BreakNode { cp.inp.EatEOL() return &ast.BreakNode{} } func (cp *zmkP) parseLink() (*ast.LinkNode, bool) { | | | < < < < | 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 (cp *zmkP) parseSoftBreak() *ast.BreakNode { cp.inp.EatEOL() return &ast.BreakNode{} } func (cp *zmkP) parseLink() (*ast.LinkNode, bool) { if ref, is, ok := cp.parseReference(']'); ok { attrs := cp.parseInlineAttributes() if len(ref) > 0 { return &ast.LinkNode{ Ref: ast.ParseReference(ref), Inlines: is, Attrs: attrs, }, true } } return nil, false } func hasQueryPrefix(src []byte) bool { return len(src) > len(ast.QueryPrefix) && string(src[:len(ast.QueryPrefix)]) == ast.QueryPrefix } func (cp *zmkP) parseReference(closeCh rune) (ref string, is ast.InlineSlice, _ bool) { inp := cp.inp inp.Next() cp.skipSpace() pos := inp.Pos if !hasQueryPrefix(inp.Src[pos:]) { hasSpace, ok := cp.readReferenceToSep(closeCh) if !ok { return "", nil, false } if inp.Ch == '|' { // First part must be inline text |
︙ | ︙ | |||
345 346 347 348 349 350 351 | if len(ins) == 0 { return nil, true } return ins, true } func (cp *zmkP) parseEmbed() (ast.InlineNode, bool) { | | | 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 | if len(ins) == 0 { return nil, true } return ins, true } func (cp *zmkP) parseEmbed() (ast.InlineNode, bool) { if ref, ins, ok := cp.parseReference('}'); ok { attrs := cp.parseInlineAttributes() if len(ref) > 0 { r := ast.ParseReference(ref) return &ast.EmbedRefNode{ Ref: r, Inlines: ins, Attrs: attrs, |
︙ | ︙ | |||
417 418 419 420 421 422 423 | '_': ast.FormatEmph, '*': ast.FormatStrong, '>': ast.FormatInsert, '~': ast.FormatDelete, '^': ast.FormatSuper, ',': ast.FormatSub, '"': ast.FormatQuote, | < | 410 411 412 413 414 415 416 417 418 419 420 421 422 423 | '_': ast.FormatEmph, '*': ast.FormatStrong, '>': ast.FormatInsert, '~': ast.FormatDelete, '^': ast.FormatSuper, ',': ast.FormatSub, '"': ast.FormatQuote, ':': ast.FormatSpan, } func (cp *zmkP) parseFormat() (res ast.InlineNode, success bool) { inp := cp.inp fch := inp.Ch kind, ok := mapRuneFormat[fch] |
︙ | ︙ | |||
498 499 500 501 502 503 504 | buf.WriteString(tn.Text) } } } func createLiteralNode(kind ast.LiteralKind, a attrs.Attributes, content []byte) *ast.LiteralNode { if kind == ast.LiteralZettel { | | | 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 | buf.WriteString(tn.Text) } } } func createLiteralNode(kind ast.LiteralKind, a attrs.Attributes, content []byte) *ast.LiteralNode { if kind == ast.LiteralZettel { if val, found := a.Get(""); found && val == api.ValueSyntaxHTML { kind = ast.LiteralHTML a = a.Remove("") } } return &ast.LiteralNode{ Kind: kind, Attrs: a, |
︙ | ︙ |
Changes to parser/zettelmark/node.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package zettelmark import "zettelstore.de/z/ast" // Internal nodes for parsing zettelmark. These will be removed in |
︙ | ︙ |
Changes to parser/zettelmark/post-processor.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package zettelmark import ( "strings" |
︙ | ︙ | |||
130 131 132 133 134 135 136 | } } } func (pp *postProcessor) visitTable(tn *ast.TableNode) { width := tableWidth(tn) tn.Align = make([]ast.Alignment, width) | | | 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | } } } func (pp *postProcessor) visitTable(tn *ast.TableNode) { width := tableWidth(tn) tn.Align = make([]ast.Alignment, width) for i := 0; i < width; i++ { tn.Align[i] = ast.AlignDefault } if len(tn.Rows) > 0 && isHeaderRow(tn.Rows[0]) { tn.Header = tn.Rows[0] tn.Rows = tn.Rows[1:] pp.visitTableHeader(tn) } |
︙ | ︙ |
Changes to parser/zettelmark/zettelmark.go.
1 | //----------------------------------------------------------------------------- | | < < < > | | | | | | | < | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( "strings" "unicode" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: api.ValueSyntaxZmk, AltNames: nil, IsTextParser: true, IsImageFormat: false, ParseBlocks: parseBlocks, ParseInlines: parseInlines, }) } func parseBlocks(inp *input.Input, _ *meta.Meta, _ string) ast.BlockSlice { |
︙ | ︙ |
Deleted parser/zettelmark/zettelmark_fuzz_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to parser/zettelmark/zettelmark_test.go.
1 | //----------------------------------------------------------------------------- | | < < < > | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package zettelmark_test provides some tests for the zettelmarkup parser. package zettelmark_test import ( "bytes" "fmt" "strings" "testing" "zettelstore.de/c/api" "zettelstore.de/c/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) type TestCase struct{ source, want string } type TestCases []TestCase func replace(s string, tcs TestCases) TestCases { var testCases TestCases |
︙ | ︙ | |||
44 45 46 47 48 49 50 | func checkTcs(t *testing.T, tcs TestCases) { t.Helper() for tcn, tc := range tcs { t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) { st.Helper() inp := input.NewInput([]byte(tc.source)) | | | 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | func checkTcs(t *testing.T, tcs TestCases) { t.Helper() for tcn, tc := range tcs { t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) { st.Helper() inp := input.NewInput([]byte(tc.source)) bns := parser.ParseBlocks(inp, nil, api.ValueSyntaxZmk, config.NoHTML) var tv TestVisitor ast.Walk(&tv, &bns) got := tv.String() if tc.want != got { st.Errorf("\nwant=%q\n got=%q", tc.want, got) } }) |
︙ | ︙ | |||
147 148 149 150 151 152 153 | {"[[b%c|a]]", "(PARA (LINK a b%c))"}, {"[[b%%c|a]]", "(PARA [[b {% c|a]]})"}, {"[[b|a]", "(PARA [[b|a])"}, {"[[b\nc|a]]", "(PARA (LINK a b SB c))"}, {"[[b c|a#n]]", "(PARA (LINK a#n b SP c))"}, {"[[a]]go", "(PARA (LINK a) go)"}, {"[[b|a]]{go}", "(PARA (LINK a b)[ATTR go])"}, | | | | | 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 | {"[[b%c|a]]", "(PARA (LINK a b%c))"}, {"[[b%%c|a]]", "(PARA [[b {% c|a]]})"}, {"[[b|a]", "(PARA [[b|a])"}, {"[[b\nc|a]]", "(PARA (LINK a b SB c))"}, {"[[b c|a#n]]", "(PARA (LINK a#n b SP c))"}, {"[[a]]go", "(PARA (LINK a) go)"}, {"[[b|a]]{go}", "(PARA (LINK a b)[ATTR go])"}, {"[[[[a]]|b]]", "(PARA (LINK [[a) |b]])"}, {"[[a[b]c|d]]", "(PARA (LINK d a[b]c))"}, {"[[[b]c|d]]", "(PARA (LINK d [b]c))"}, {"[[a[]c|d]]", "(PARA (LINK d a[]c))"}, {"[[a[b]|d]]", "(PARA (LINK d a[b]))"}, {"[[\\|]]", "(PARA (LINK %5C%7C))"}, {"[[\\||a]]", "(PARA (LINK a |))"}, {"[[b\\||a]]", "(PARA (LINK a b|))"}, {"[[b\\|c|a]]", "(PARA (LINK a b|c))"}, {"[[\\]]]", "(PARA (LINK %5C%5D))"}, {"[[\\]|a]]", "(PARA (LINK a ]))"}, {"[[b\\]|a]]", "(PARA (LINK a b]))"}, {"[[\\]\\||a]]", "(PARA (LINK a ]|))"}, {"[[http://a]]", "(PARA (LINK http://a))"}, {"[[http://a|http://a]]", "(PARA (LINK http://a http://a))"}, {"[[[[a]]]]", "(PARA (LINK [[a) ]])"}, {"[[query:title]]", "(PARA (LINK query:title))"}, {"[[query:title syntax]]", "(PARA (LINK query:title syntax))"}, {"[[query:title | action]]", "(PARA (LINK query:title | action))"}, {"[[Text|query:title]]", "(PARA (LINK query:title Text))"}, {"[[Text|query:title syntax]]", "(PARA (LINK query:title syntax Text))"}, {"[[Text|query:title | action]]", "(PARA (LINK query:title | action Text))"}, }) |
︙ | ︙ | |||
232 233 234 235 236 237 238 | {"{{b|}}", "(PARA {{b|}})"}, {"{{b|a}}", "(PARA (EMBED a b))"}, {"{{b| a}}", "(PARA (EMBED a b))"}, {"{{b|a}", "(PARA {{b|a})"}, {"{{b\nc|a}}", "(PARA (EMBED a b SB c))"}, {"{{b c|a#n}}", "(PARA (EMBED a#n b SP c))"}, {"{{a}}{go}", "(PARA (EMBED a)[ATTR go])"}, | | | | 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 | {"{{b|}}", "(PARA {{b|}})"}, {"{{b|a}}", "(PARA (EMBED a b))"}, {"{{b| a}}", "(PARA (EMBED a b))"}, {"{{b|a}", "(PARA {{b|a})"}, {"{{b\nc|a}}", "(PARA (EMBED a b SB c))"}, {"{{b c|a#n}}", "(PARA (EMBED a#n b SP c))"}, {"{{a}}{go}", "(PARA (EMBED a)[ATTR go])"}, {"{{{{a}}|b}}", "(PARA (EMBED %7B%7Ba) |b}})"}, {"{{\\|}}", "(PARA (EMBED %5C%7C))"}, {"{{\\||a}}", "(PARA (EMBED a |))"}, {"{{b\\||a}}", "(PARA (EMBED a b|))"}, {"{{b\\|c|a}}", "(PARA (EMBED a b|c))"}, {"{{\\}}}", "(PARA (EMBED %5C%7D))"}, {"{{\\}|a}}", "(PARA (EMBED a }))"}, {"{{b\\}|a}}", "(PARA (EMBED a b}))"}, {"{{\\}\\||a}}", "(PARA (EMBED a }|))"}, {"{{http://a}}", "(PARA (EMBED http://a))"}, {"{{http://a|http://a}}", "(PARA (EMBED http://a http://a))"}, {"{{{{a}}}}", "(PARA (EMBED %7B%7Ba) }})"}, }) } func TestMark(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"[!", "(PARA [!)"}, |
︙ | ︙ | |||
292 293 294 295 296 297 298 | {"100%", "(PARA 100%)"}, }) } func TestFormat(t *testing.T) { t.Parallel() // Not for Insert / '>', because collision with quoted list | | | | 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | {"100%", "(PARA 100%)"}, }) } func TestFormat(t *testing.T) { t.Parallel() // Not for Insert / '>', because collision with quoted list for _, ch := range []string{"_", "*", "~", "^", ",", "\"", ":"} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, })) } for _, ch := range []string{"_", "*", ">", "~", "^", ",", "\"", ":"} { checkTcs(t, replace(ch, TestCases{ {"$$a$$", "(PARA {$ a})"}, {"$$a$$$", "(PARA {$ a} $)"}, {"$$$a$$", "(PARA {$ $a})"}, {"$$$a$$$", "(PARA {$ $a} $)"}, {"$\\$", "(PARA $$)"}, {"$\\$$", "(PARA $$$)"}, |
︙ | ︙ | |||
679 680 681 682 683 684 685 | t.Parallel() checkTcs(t, TestCases{ {"{{{a}}}", "(TRANSCLUDE a)"}, {"{{{a}}}b", "(TRANSCLUDE a)[ATTR =b]"}, {"{{{a}}}}", "(TRANSCLUDE a)"}, {"{{{a\\}}}}", "(TRANSCLUDE a%5C%7D)"}, {"{{{a\\}}}}b", "(TRANSCLUDE a%5C%7D)[ATTR =b]"}, | | | 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 | t.Parallel() checkTcs(t, TestCases{ {"{{{a}}}", "(TRANSCLUDE a)"}, {"{{{a}}}b", "(TRANSCLUDE a)[ATTR =b]"}, {"{{{a}}}}", "(TRANSCLUDE a)"}, {"{{{a\\}}}}", "(TRANSCLUDE a%5C%7D)"}, {"{{{a\\}}}}b", "(TRANSCLUDE a%5C%7D)[ATTR =b]"}, {"{{{a}}", "(PARA (EMBED %7Ba))"}, {"{{{a}}}{go=b}", "(TRANSCLUDE a)[ATTR go=b]"}, }) } func TestBlockAttr(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ |
︙ | ︙ | |||
764 765 766 767 768 769 770 | }) } // -------------------------------------------------------------------------- // TestVisitor serializes the abstract syntax tree to a string. type TestVisitor struct { | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 | }) } // -------------------------------------------------------------------------- // TestVisitor serializes the abstract syntax tree to a string. type TestVisitor struct { buf bytes.Buffer } func (tv *TestVisitor) String() string { return tv.buf.String() } func (tv *TestVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.InlineSlice: tv.visitInlineSlice(n) case *ast.ParaNode: tv.buf.WriteString("(PARA") ast.Walk(tv, &n.Inlines) tv.buf.WriteByte(')') case *ast.VerbatimNode: code, ok := mapVerbatimKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim code %v", n.Kind)) } tv.buf.WriteString(code) if len(n.Content) > 0 { tv.buf.WriteByte('\n') tv.buf.Write(n.Content) } tv.buf.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.RegionNode: code, ok := mapRegionKind[n.Kind] if !ok { panic(fmt.Sprintf("Unknown region code %v", n.Kind)) } tv.buf.WriteString(code) if len(n.Blocks) > 0 { tv.buf.WriteByte(' ') ast.Walk(tv, &n.Blocks) } if len(n.Inlines) > 0 { tv.buf.WriteString(" (LINE") ast.Walk(tv, &n.Inlines) tv.buf.WriteByte(')') } tv.buf.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.HeadingNode: fmt.Fprintf(&tv.buf, "(H%d", n.Level) ast.Walk(tv, &n.Inlines) if n.Fragment != "" { tv.buf.WriteString(" #") tv.buf.WriteString(n.Fragment) } tv.buf.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.HRuleNode: tv.buf.WriteString("(HR)") tv.visitAttributes(n.Attrs) case *ast.NestedListNode: tv.buf.WriteString(mapNestedListKind[n.Kind]) for _, item := range n.Items { tv.buf.WriteString(" {") ast.WalkItemSlice(tv, item) tv.buf.WriteByte('}') } tv.buf.WriteByte(')') case *ast.DescriptionListNode: tv.buf.WriteString("(DL") for _, def := range n.Descriptions { tv.buf.WriteString(" (DT") ast.Walk(tv, &def.Term) tv.buf.WriteByte(')') for _, b := range def.Descriptions { tv.buf.WriteString(" (DD ") ast.WalkDescriptionSlice(tv, b) tv.buf.WriteByte(')') } } tv.buf.WriteByte(')') case *ast.TableNode: tv.buf.WriteString("(TAB") if len(n.Header) > 0 { tv.buf.WriteString(" (TR") for _, cell := range n.Header { tv.buf.WriteString(" (TH") tv.buf.WriteString(alignString[cell.Align]) ast.Walk(tv, &cell.Inlines) tv.buf.WriteString(")") } tv.buf.WriteString(")") } if len(n.Rows) > 0 { tv.buf.WriteString(" ") for _, row := range n.Rows { tv.buf.WriteString("(TR") for i, cell := range row { if i == 0 { tv.buf.WriteString(" ") } tv.buf.WriteString("(TD") tv.buf.WriteString(alignString[cell.Align]) ast.Walk(tv, &cell.Inlines) tv.buf.WriteString(")") } tv.buf.WriteString(")") } } tv.buf.WriteString(")") case *ast.TranscludeNode: fmt.Fprintf(&tv.buf, "(TRANSCLUDE %v)", n.Ref) tv.visitAttributes(n.Attrs) case *ast.BLOBNode: tv.buf.WriteString("(BLOB ") tv.buf.WriteString(n.Syntax) tv.buf.WriteString(")") case *ast.TextNode: tv.buf.WriteString(n.Text) case *ast.SpaceNode: if l := n.Count(); l == 1 { tv.buf.WriteString("SP") } else { fmt.Fprintf(&tv.buf, "SP%d", l) } case *ast.BreakNode: if n.Hard { tv.buf.WriteString("HB") } else { tv.buf.WriteString("SB") } case *ast.LinkNode: fmt.Fprintf(&tv.buf, "(LINK %v", n.Ref) ast.Walk(tv, &n.Inlines) tv.buf.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.EmbedRefNode: fmt.Fprintf(&tv.buf, "(EMBED %v", n.Ref) if len(n.Inlines) > 0 { ast.Walk(tv, &n.Inlines) } tv.buf.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.EmbedBLOBNode: panic("TODO: zmktest blob") case *ast.CiteNode: fmt.Fprintf(&tv.buf, "(CITE %s", n.Key) if len(n.Inlines) > 0 { ast.Walk(tv, &n.Inlines) } tv.buf.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.FootnoteNode: tv.buf.WriteString("(FN") ast.Walk(tv, &n.Inlines) tv.buf.WriteByte(')') tv.visitAttributes(n.Attrs) case *ast.MarkNode: tv.buf.WriteString("(MARK") if n.Mark != "" { tv.buf.WriteString(" \"") tv.buf.WriteString(n.Mark) tv.buf.WriteByte('"') } if n.Fragment != "" { tv.buf.WriteString(" #") tv.buf.WriteString(n.Fragment) } if len(n.Inlines) > 0 { ast.Walk(tv, &n.Inlines) } tv.buf.WriteByte(')') case *ast.FormatNode: fmt.Fprintf(&tv.buf, "{%c", mapFormatKind[n.Kind]) ast.Walk(tv, &n.Inlines) tv.buf.WriteByte('}') tv.visitAttributes(n.Attrs) case *ast.LiteralNode: code, ok := mapLiteralKind[n.Kind] if !ok { panic(fmt.Sprintf("No element for code %v", n.Kind)) } tv.buf.WriteByte('{') tv.buf.WriteRune(code) if len(n.Content) > 0 { tv.buf.WriteByte(' ') tv.buf.Write(n.Content) } tv.buf.WriteByte('}') tv.visitAttributes(n.Attrs) default: return tv } return nil } |
︙ | ︙ | |||
989 990 991 992 993 994 995 | ast.FormatEmph: '_', ast.FormatStrong: '*', ast.FormatInsert: '>', ast.FormatDelete: '~', ast.FormatSuper: '^', ast.FormatSub: ',', ast.FormatQuote: '"', | < | | | | | | | | | | | 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 | ast.FormatEmph: '_', ast.FormatStrong: '*', ast.FormatInsert: '>', ast.FormatDelete: '~', ast.FormatSuper: '^', ast.FormatSub: ',', ast.FormatQuote: '"', ast.FormatSpan: ':', } var mapLiteralKind = map[ast.LiteralKind]rune{ ast.LiteralZettel: '@', ast.LiteralProg: '`', ast.LiteralInput: '\'', ast.LiteralOutput: '=', ast.LiteralComment: '%', ast.LiteralMath: '$', } func (tv *TestVisitor) visitInlineSlice(is *ast.InlineSlice) { for _, in := range *is { tv.buf.WriteByte(' ') ast.Walk(tv, in) } } func (tv *TestVisitor) visitAttributes(a attrs.Attributes) { if a.IsEmpty() { return } tv.buf.WriteString("[ATTR") for _, k := range a.Keys() { tv.buf.WriteByte(' ') tv.buf.WriteString(k) v := a[k] if len(v) > 0 { tv.buf.WriteByte('=') if quoteString(v) { tv.buf.WriteByte('"') tv.buf.WriteString(v) tv.buf.WriteByte('"') } else { tv.buf.WriteString(v) } } } tv.buf.WriteByte(']') } func quoteString(s string) bool { for _, ch := range s { if ch <= ' ' { return true } } return false } |
Deleted query/compiled.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/context.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to query/parser.go.
1 | //----------------------------------------------------------------------------- | | < < < < < | | | 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) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query import ( "strconv" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) // Parse the query specification and return a Query object. func Parse(spec string) (q *Query) { return q.Parse(spec) } // Parse the query string and update the Query object. func (q *Query) Parse(spec string) *Query { |
︙ | ︙ | |||
41 42 43 44 45 46 47 | type parserState struct { inp *input.Input } func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS } func (ps *parserState) acceptSingleKw(s string) bool { | | | | | < | | | | | | > > > > > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | < < < < < < < | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | 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 | type parserState struct { inp *input.Input } func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS } func (ps *parserState) acceptSingleKw(s string) bool { if ps.inp.Accept(s) && (ps.isSpace() || ps.mustStop()) { return true } return false } func (ps *parserState) acceptKwArgs(s string) bool { if ps.inp.Accept(s) && ps.isSpace() { ps.skipSpace() return true } return false } const ( actionSeparatorChar = '|' existOperatorChar = '?' searchOperatorNotChar = '!' searchOperatorHasChar = ':' searchOperatorPrefixChar = '>' searchOperatorSuffixChar = '<' searchOperatorMatchChar = '~' kwLimit = "LIMIT" kwOffset = "OFFSET" kwOr = "OR" kwOrder = "ORDER" kwRandom = "RANDOM" kwReverse = "REVERSE" ) func (ps *parserState) parse(q *Query) *Query { inp := ps.inp for { ps.skipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.acceptSingleKw(kwOr) { q = createIfNeeded(q) if !q.terms[len(q.terms)-1].isEmpty() { q.terms = append(q.terms, conjTerms{}) } continue } inp.SetPos(pos) if ps.acceptSingleKw(kwRandom) { q = createIfNeeded(q) if len(q.order) == 0 { q.order = []sortOrder{{"", false}} } continue } inp.SetPos(pos) if ps.acceptKwArgs(kwOrder) { if s, ok := ps.parseOrder(q); ok { q = s continue } } inp.SetPos(pos) if ps.acceptKwArgs(kwOffset) { if s, ok := ps.parseOffset(q); ok { q = s continue } } inp.SetPos(pos) if ps.acceptKwArgs(kwLimit) { if s, ok := ps.parseLimit(q); ok { q = s continue } } inp.SetPos(pos) if isActionSep(inp.Ch) { q = ps.parseActions(q) break } q = ps.parseText(q) } return q } func (ps *parserState) parseOrder(q *Query) (*Query, bool) { reverse := false if ps.acceptKwArgs(kwReverse) { reverse = true } word := ps.scanWord() if len(word) == 0 { return q, false } if sWord := string(word); meta.KeyIsValid(sWord) { |
︙ | ︙ | |||
371 372 373 374 375 376 377 | } text, key := ps.scanSearchTextOrKey(hasOp) if len(key) > 0 { // Assert: hasOp == false op, hasOp = ps.scanSearchOp() // Assert hasOp == true if op == cmpExist || op == cmpNotExist { | | | 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | } text, key := ps.scanSearchTextOrKey(hasOp) if len(key) > 0 { // Assert: hasOp == false op, hasOp = ps.scanSearchOp() // Assert hasOp == true if op == cmpExist || op == cmpNotExist { if ps.isSpace() || isActionSep(inp.Ch) || ps.mustStop() { return q.addKey(string(key), op) } ps.inp.SetPos(pos) hasOp = false text = ps.scanWord() key = nil } else { |
︙ | ︙ | |||
410 411 412 413 414 415 416 | } func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) { inp := ps.inp pos := inp.Pos allowKey := !hasOp | | | < | < | > > > > > > > > > < < < < < < < < < < < < < < < < < < < < < < > > > > > > > > > > > > > > < < < < < < < < < < < < | < | 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 | } func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) { inp := ps.inp pos := inp.Pos allowKey := !hasOp for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() { if allowKey { switch inp.Ch { case searchOperatorNotChar, existOperatorChar, searchOperatorHasChar, searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar: allowKey = false if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) { return nil, key } } } inp.Next() } return inp.Src[pos:inp.Pos], nil } func (ps *parserState) scanWord() []byte { inp := ps.inp pos := inp.Pos for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() { inp.Next() } return inp.Src[pos:inp.Pos] } func (ps *parserState) scanPosInt() (int, bool) { inp := ps.inp ch := inp.Ch if ch == '0' { ch = inp.Next() if isSpace(ch) || isActionSep(inp.Ch) || ps.mustStop() { return 0, true } return 0, false } word := ps.scanWord() if len(word) == 0 { return 0, false } uval, err := strconv.ParseUint(string(word), 10, 63) if err != nil { return 0, false } return int(uval), true } func (ps *parserState) scanSearchOp() (compareOp, bool) { inp := ps.inp ch := inp.Ch negate := false if ch == searchOperatorNotChar { ch = inp.Next() negate = true } op := cmpUnknown switch ch { case existOperatorChar: inp.Next() op = cmpExist case searchOperatorHasChar: inp.Next() op = cmpHas case searchOperatorSuffixChar: inp.Next() op = cmpSuffix case searchOperatorPrefixChar: inp.Next() op = cmpPrefix case searchOperatorMatchChar: inp.Next() op = cmpMatch default: if negate { return cmpNoMatch, true } return cmpUnknown, false } if negate { return op.negate(), true } return op, true } func (ps *parserState) isSpace() bool { return isSpace(ps.inp.Ch) } func isSpace(ch rune) bool { switch ch { case input.EOS: return false case ' ', '\t', '\n', '\r': return true } return input.IsSpace(ch) } func (ps *parserState) skipSpace() { for ps.isSpace() { ps.inp.Next() } } func isActionSep(ch rune) bool { return ch == actionSeparatorChar } |
Changes to query/parser_test.go.
1 | //----------------------------------------------------------------------------- | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < | | < | | < < < | | < < < < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query_test import ( "testing" "zettelstore.de/z/query" ) func TestParser(t *testing.T) { t.Parallel() testcases := []struct { spec string exp string }{ {"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"}, {"key?", "key?"}, {"key!?", "key!?"}, {"b key?", "key? b"}, {"b key!?", "key!? b"}, {"key?a", "key?a"}, {"key!?a", "key!?a"}, {"", ""}, {"!", ""}, {":", ""}, {"!:", ""}, {">", ""}, {"!>", ""}, {"<", ""}, {"!<", ""}, {"~", ""}, {"!~", ""}, {`a`, `a`}, {`!a`, `!a`}, {`:a`, `:a`}, {`!:a`, `!:a`}, {`>a`, `>a`}, {`!>a`, `!>a`}, {`<a`, `<a`}, {`!<a`, `!<a`}, {`~a`, `a`}, {`!~a`, `!a`}, {`key:`, `key:`}, {`key!:`, `key!:`}, {`key>`, `key>`}, {`key!>`, `key!>`}, {`key<`, `key<`}, {`key!<`, `key!<`}, {`key~`, `key~`}, {`key!~`, `key!~`}, {`key:a`, `key:a`}, {`key!:a`, `key!:a`}, {`key>a`, `key>a`}, {`key!>a`, `key!>a`}, {`key<a`, `key<a`}, {`key!<a`, `key!<a`}, {`key~a`, `key~a`}, {`key!~a`, `key!~a`}, {`key1:a key2:b`, `key1:a key2:b`}, {`key1: key2:b`, `key1: key2:b`}, {"word key:a", "key:a word"}, {`RANDOM`, `RANDOM`}, {`RANDOM a`, `a RANDOM`}, {`a RANDOM`, `a RANDOM`}, {`RANDOM RANDOM a`, `a RANDOM`}, {`RANDOMRANDOM a`, `RANDOMRANDOM a`}, {`a RANDOMRANDOM`, `a RANDOMRANDOM`}, {`ORDER`, `ORDER`}, {"ORDER a b", "b ORDER a"}, {"a ORDER", "a ORDER"}, {"ORDER %", "ORDER %"}, {"ORDER a %", "% ORDER a"}, {"ORDER REVERSE", "ORDER REVERSE"}, {"ORDER REVERSE a b", "b ORDER REVERSE a"}, {"a RANDOM ORDER b", "a ORDER b"}, {"a ORDER b RANDOM", "a ORDER b"}, {"OFFSET", "OFFSET"}, {"OFFSET a", "OFFSET a"}, {"OFFSET 10 a", "a OFFSET 10"}, {"OFFSET 01 a", "OFFSET 01 a"}, {"OFFSET 0 a", "a"}, {"a OFFSET 0", "a"}, {"OFFSET 4 OFFSET 8", "OFFSET 8"}, {"OFFSET 8 OFFSET 4", "OFFSET 8"}, {"LIMIT", "LIMIT"}, {"LIMIT a", "LIMIT a"}, {"LIMIT 10 a", "a LIMIT 10"}, {"LIMIT 01 a", "LIMIT 01 a"}, {"LIMIT 0 a", "a"}, {"a LIMIT 0", "a"}, {"LIMIT 4 LIMIT 8", "LIMIT 4"}, {"LIMIT 8 LIMIT 4", "LIMIT 4"}, {"OR", ""}, {"OR OR", ""}, {"a OR", "a"}, {"OR b", "b"}, {"OR a OR", "a"}, {"a OR b", "a OR b"}, {"|", ""}, {" | RANDOM", "| RANDOM"}, {"| RANDOM", "| RANDOM"}, {"a|a b ", "a | a b"}, } for i, tc := range testcases { got := query.Parse(tc.spec).String() |
︙ | ︙ |
Changes to query/print.go.
1 | //----------------------------------------------------------------------------- | | < < < | < | | | < < | | | | | | | | < < < < | < < < < > > | > < | | < | | | | < < < | < < < < < < < < < < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query import ( "io" "strconv" "strings" "zettelstore.de/c/api" "zettelstore.de/c/maps" ) var op2string = map[compareOp]string{ cmpExist: api.ExistOperator, cmpNotExist: api.ExistNotOperator, cmpHas: api.SearchOperatorHas, cmpHasNot: api.SearchOperatorHasNot, cmpPrefix: api.SearchOperatorPrefix, cmpNoPrefix: api.SearchOperatorNoPrefix, cmpSuffix: api.SearchOperatorSuffix, cmpNoSuffix: api.SearchOperatorNoSuffix, cmpMatch: api.SearchOperatorMatch, cmpNoMatch: api.SearchOperatorNoMatch, } func (q *Query) String() string { var sb strings.Builder q.Print(&sb) return sb.String() } // Print the query in a parseable form. func (q *Query) Print(w io.Writer) { if q == nil { return } env := printEnv{w: w} for i, term := range q.terms { if i > 0 { env.writeString(" OR") } for _, name := range maps.Keys(term.keys) { env.printSpace() env.writeString(name) if op := term.keys[name]; op == cmpExist || op == cmpNotExist { env.writeString(op2string[op]) } else { env.writeString(api.ExistOperator) env.printSpace() env.writeString(name) env.writeString(api.ExistNotOperator) } } for _, name := range maps.Keys(term.mvals) { env.printExprValues(name, term.mvals[name]) } if len(term.search) > 0 { env.printExprValues("", term.search) } } env.printOrder(q.order) env.printPosInt(kwOffset, q.offset) env.printPosInt(kwLimit, q.limit) env.printActions(q.actions) } type printEnv struct { w io.Writer space bool } var bsSpace = []byte{' '} func (pe *printEnv) printSpace() { if pe.space { pe.w.Write(bsSpace) return } pe.space = true } func (pe *printEnv) write(ch byte) { pe.w.Write([]byte{ch}) } func (pe *printEnv) writeString(s string) { io.WriteString(pe.w, s) } func (pe *printEnv) printExprValues(key string, values []expValue) { for _, val := range values { pe.printSpace() pe.writeString(key) switch op := val.op; op { case cmpMatch: // An empty key signals a full-text search. Since "~" is the default op in this case, // it can be ignored. Therefore, print only "~" if there is a key. |
︙ | ︙ | |||
156 157 158 159 160 161 162 | } // PrintHuman the query to a writer in a human readable form. func (q *Query) PrintHuman(w io.Writer) { if q == nil { return } | | < < < < | 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | } // PrintHuman the query to a writer in a human readable form. func (q *Query) PrintHuman(w io.Writer) { if q == nil { return } env := printEnv{w: w} for i, term := range q.terms { if i > 0 { env.writeString(" OR ") env.space = false } for _, name := range maps.Keys(term.keys) { if env.space { |
︙ | ︙ | |||
199 200 201 202 203 204 205 | } env.writeString("ANY") env.printHumanSelectExprValues(term.search) env.space = true } } | < | | | < < < < < < < < < < < < | | > > | | | | > > | | 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 | } env.writeString("ANY") env.printHumanSelectExprValues(term.search) env.space = true } } env.printOrder(q.order) env.printPosInt(kwOffset, q.offset) env.printPosInt(kwLimit, q.limit) env.printActions(q.actions) } func (pe *printEnv) printHumanSelectExprValues(values []expValue) { if len(values) == 0 { pe.writeString(" MATCH ANY") return } for j, val := range values { if j > 0 { pe.writeString(" AND") } switch val.op { case cmpHas: pe.writeString(" HAS ") case cmpHasNot: pe.writeString(" HAS NOT ") case cmpPrefix: pe.writeString(" PREFIX ") case cmpNoPrefix: pe.writeString(" NOT PREFIX ") case cmpSuffix: pe.writeString(" SUFFIX ") case cmpNoSuffix: pe.writeString(" NOT SUFFIX ") case cmpMatch: pe.writeString(" MATCH ") case cmpNoMatch: pe.writeString(" NOT MATCH ") default: pe.writeString(" MaTcH ") } if val.value == "" { pe.writeString("NOTHING") } else { pe.writeString(val.value) } } } func (pe *printEnv) printOrder(order []sortOrder) { for _, o := range order { if o.isRandom() { pe.printSpace() pe.writeString(kwRandom) continue } else if o.key == api.KeyID && o.descending { continue } pe.printSpace() pe.writeString(kwOrder) if o.descending { pe.printSpace() pe.writeString(kwReverse) } pe.printSpace() pe.writeString(o.key) } } func (pe *printEnv) printPosInt(key string, val int) { if val > 0 { pe.printSpace() pe.writeString(key) pe.writeString(" ") pe.writeString(strconv.Itoa(val)) } } func (pe *printEnv) printActions(words []string) { if len(words) > 0 { pe.printSpace() pe.write(actionSeparatorChar) for _, word := range words { pe.printSpace() pe.writeString(word) } } } |
Changes to query/query.go.
1 | //----------------------------------------------------------------------------- | | < < < < | | | | | 1 2 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package query provides a query for zettel. package query import ( "math/rand" "sort" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // Searcher is used to select zettel identifier based on search criteria. type Searcher interface { // Select all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. SearchEqual(word string) id.Set |
︙ | ︙ | |||
40 41 42 43 44 45 46 | // Select all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. SearchContains(s string) id.Set } // Query specifies a mechanism for querying zettel. type Query struct { | < < < < < < < < < < < | | | | | < < < | | < | < | > > | < < | > > | < < < | > > | 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 | // Select all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. SearchContains(s string) id.Set } // Query specifies a mechanism for querying zettel. type Query struct { // Fields to be used for selecting preMatch MetaMatchFunc // Match that must be true terms []conjTerms // Fields to be used for sorting order []sortOrder offset int // <= 0: no offset limit int // <= 0: no limit // Execute specification actions []string } // Compiled is a compiled query, to be used in a Box type Compiled struct { PreMatch MetaMatchFunc // Precondition for Match and Retrieve Terms []CompiledTerm } // MetaMatchFunc is a function determine whethe some metadata should be selected or not. type MetaMatchFunc func(*meta.Meta) bool func matchAlways(*meta.Meta) bool { return true } func matchNever(*meta.Meta) bool { return false } // CompiledTerm is the preprocessed sequence of conjugated search terms. type CompiledTerm struct { Match MetaMatchFunc // Match on metadata Retrieve RetrievePredicate // Retrieve from full-text search } // RetrievePredicate returns true, if the given Zid is contained in the (full-text) search. type RetrievePredicate func(id.Zid) bool type keyExistMap map[string]compareOp type expMetaValues map[string][]expValue type conjTerms struct { keys keyExistMap mvals expMetaValues // Expected values for a meta datum |
︙ | ︙ | |||
138 139 140 141 142 143 144 | // Clone the query value. func (q *Query) Clone() *Query { if q == nil { return nil } c := new(Query) | < < < < < < < < < < < < < < < < < | | < < | | | | | | | | < < < < | < | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | > | > > | | | < < < < < < < < < | | 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 | // Clone the query value. func (q *Query) Clone() *Query { if q == nil { return nil } c := new(Query) c.preMatch = q.preMatch c.terms = make([]conjTerms, len(q.terms)) for i, term := range q.terms { if len(term.keys) > 0 { c.terms[i].keys = make(keyExistMap, len(term.keys)) for k, v := range term.keys { c.terms[i].keys[k] = v } } // if len(c.mvals) > 0 { c.terms[i].mvals = make(expMetaValues, len(term.mvals)) for k, v := range term.mvals { c.terms[i].mvals[k] = v } // } if len(term.search) > 0 { c.terms[i].search = append([]expValue{}, term.search...) } } if len(q.order) > 0 { c.order = append([]sortOrder{}, q.order...) } c.offset = q.offset c.limit = q.limit c.actions = q.actions return c } type compareOp uint8 const ( cmpUnknown compareOp = iota cmpExist cmpNotExist cmpHas cmpHasNot cmpPrefix cmpNoPrefix cmpSuffix cmpNoSuffix cmpMatch cmpNoMatch ) var negateMap = map[compareOp]compareOp{ cmpUnknown: cmpUnknown, cmpExist: cmpNotExist, cmpHas: cmpHasNot, cmpHasNot: cmpHas, cmpPrefix: cmpNoPrefix, cmpNoPrefix: cmpPrefix, cmpSuffix: cmpNoSuffix, cmpNoSuffix: cmpSuffix, cmpMatch: cmpNoMatch, cmpNoMatch: cmpMatch, } func (op compareOp) negate() compareOp { return negateMap[op] } var negativeMap = map[compareOp]bool{ cmpNotExist: true, cmpHasNot: true, cmpNoPrefix: true, cmpNoSuffix: true, cmpNoMatch: true, } func (op compareOp) isNegated() bool { return negativeMap[op] } type expValue struct { value string op compareOp } func (q *Query) addSearch(val expValue) { q.terms[len(q.terms)-1].addSearch(val) } func (q *Query) addKey(key string, op compareOp) *Query { q = createIfNeeded(q) q.terms[len(q.terms)-1].addKey(key, op) return q } // SetPreMatch sets the pre-selection predicate. func (q *Query) SetPreMatch(preMatch MetaMatchFunc) *Query { q = createIfNeeded(q) if q.preMatch != nil { panic("search PreMatch already set") } q.preMatch = preMatch return q } // SetLimit sets the given limit of the query object. func (q *Query) SetLimit(limit int) *Query { q = createIfNeeded(q) if limit < 0 { limit = 0 } q.limit = limit return q } // GetLimit returns the current offset value. func (q *Query) GetLimit() int { if q == nil { return 0 } return q.limit } // Actions returns the slice of action specifications func (q *Query) Actions() []string { if q == nil { return nil } |
︙ | ︙ | |||
328 329 330 331 332 333 334 | // EnrichNeeded returns true, if the query references a metadata key that // is calculated via metadata enrichments. func (q *Query) EnrichNeeded() bool { if q == nil { return false } | < < < | 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | // EnrichNeeded returns true, if the query references a metadata key that // is calculated via metadata enrichments. func (q *Query) EnrichNeeded() bool { if q == nil { return false } if len(q.actions) > 0 { // Unknown, what an action will use. Example: RSS needs api.KeyPublished. return true } for _, term := range q.terms { for key := range term.keys { if meta.IsProperty(key) { |
︙ | ︙ | |||
357 358 359 360 361 362 363 | } } return false } // RetrieveAndCompile queries the search index and returns a predicate // for its results and returns a matching predicate. | | | < < | < < < < < < < < < | < | > > | | | | < | < < < < < < < < < < < | < < < < < < < < < < < < < | | | | > | > > > > > | > > > > | > > > > > > > > > > > > > > > > > > | | 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 | } } return false } // RetrieveAndCompile queries the search index and returns a predicate // for its results and returns a matching predicate. func (q *Query) RetrieveAndCompile(searcher Searcher) Compiled { if q == nil { return Compiled{ PreMatch: matchAlways, Terms: []CompiledTerm{{ Match: matchAlways, Retrieve: alwaysIncluded, }}} } q = q.Clone() preMatch := q.preMatch if preMatch == nil { preMatch = matchAlways } result := Compiled{PreMatch: preMatch} for _, term := range q.terms { cTerm := term.retrievAndCompileTerm(searcher) if cTerm.Retrieve == nil { if cTerm.Match == nil { // no restriction on match/retrieve -> all will match return Compiled{ PreMatch: preMatch, Terms: []CompiledTerm{{ Match: matchAlways, Retrieve: alwaysIncluded, }}} } cTerm.Retrieve = alwaysIncluded } if cTerm.Match == nil { cTerm.Match = matchAlways } result.Terms = append(result.Terms, cTerm) } return result } func (ct *conjTerms) retrievAndCompileTerm(searcher Searcher) CompiledTerm { match := ct.compileMeta() // Match might add some searches var pred RetrievePredicate if searcher != nil { pred = ct.retrieveIndex(searcher) } return CompiledTerm{Match: match, Retrieve: pred} } // retrieveIndex and return a predicate to ask for results. func (ct *conjTerms) retrieveIndex(searcher Searcher) RetrievePredicate { if len(ct.search) == 0 { return nil } normCalls, plainCalls, negCalls := prepareRetrieveCalls(searcher, ct.search) if hasConflictingCalls(normCalls, plainCalls, negCalls) { return neverIncluded } positives := retrievePositives(normCalls, plainCalls) if positives == nil { // No positive search for words, must contain only words for a negative search. // Otherwise len(search) == 0 (see above) negatives := retrieveNegatives(negCalls) return func(zid id.Zid) bool { return !negatives.Contains(zid) } } if len(positives) == 0 { // Positive search didn't found anything. We can omit the negative search. return neverIncluded } if len(negCalls) == 0 { // Positive search found something, but there is no negative search. return positives.Contains } negatives := retrieveNegatives(negCalls) return func(zid id.Zid) bool { return positives.Contains(zid) && !negatives.Contains(zid) } } // Sort applies the sorter to the slice of meta data. func (q *Query) Sort(metaList []*meta.Meta) []*meta.Meta { if len(metaList) == 0 { return metaList } if q == nil || len(q.order) == 0 { sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid }) if q == nil { return metaList } } else if q.order[0].isRandom() { rand.Shuffle(len(metaList), func(i, j int) { metaList[i], metaList[j] = metaList[j], metaList[i] }) } else { sort.Slice(metaList, createSortFunc(q.order, metaList)) } if q.offset > 0 { if q.offset > len(metaList) { return nil } metaList = metaList[q.offset:] } return q.Limit(metaList) } // Limit returns only s.GetLimit() elements of the given list. func (q *Query) Limit(metaList []*meta.Meta) []*meta.Meta { if q == nil { return metaList } if q.limit > 0 && q.limit < len(metaList) { return metaList[:q.limit] } return metaList } |
Changes to query/retrieve.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query // This file contains helper functions to search within the index. import ( "fmt" "strings" "zettelstore.de/z/domain/id" "zettelstore.de/z/strfun" ) type searchOp struct { s string op compareOp } type searchFunc func(string) id.Set type searchCallMap map[searchOp]searchFunc var cmpPred = map[compareOp]func(string, string) bool{ cmpHas: func(s, t string) bool { return s == t }, cmpPrefix: strings.HasPrefix, cmpSuffix: strings.HasSuffix, cmpMatch: strings.Contains, } func (scm searchCallMap) addSearch(s string, op compareOp, sf searchFunc) { pred := cmpPred[op] for k := range scm { if op == cmpMatch { if strings.Contains(k.s, s) { |
︙ | ︙ | |||
61 62 63 64 65 66 67 68 69 70 71 72 73 74 | if pred(s, k.s) { delete(scm, k) } } scm[searchOp{s: s, op: op}] = sf } func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) { normCalls = make(searchCallMap, len(search)) negCalls = make(searchCallMap, len(search)) for _, val := range search { for _, word := range strfun.NormalizeWords(val.value) { if cmpOp := val.op; cmpOp.isNegated() { cmpOp = cmpOp.negate() | > > > | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | if pred(s, k.s) { delete(scm, k) } } scm[searchOp{s: s, op: op}] = sf } func alwaysIncluded(id.Zid) bool { return true } func neverIncluded(id.Zid) bool { return false } func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) { normCalls = make(searchCallMap, len(search)) negCalls = make(searchCallMap, len(search)) for _, val := range search { for _, word := range strfun.NormalizeWords(val.value) { if cmpOp := val.op; cmpOp.isNegated() { cmpOp = cmpOp.negate() |
︙ | ︙ | |||
132 133 134 135 136 137 138 | if result, found := cache[c]; found { normResult = normResult.IntersectOrSet(result) continue } } normResult = normResult.IntersectOrSet(sf(c.s)) } | | | | | | 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 | if result, found := cache[c]; found { normResult = normResult.IntersectOrSet(result) continue } } normResult = normResult.IntersectOrSet(sf(c.s)) } return normResult.Add(plainResult) } func isSuperset(normCalls, plainCalls searchCallMap) bool { for c := range plainCalls { if _, found := normCalls[c]; !found { return false } } return true } func retrieveNegatives(negCalls searchCallMap) id.Set { var negatives id.Set for val, sf := range negCalls { negatives = negatives.Add(sf(val.s)) } return negatives } func getSearchFunc(searcher Searcher, op compareOp) searchFunc { switch op { case cmpHas: return searcher.SearchEqual case cmpPrefix: return searcher.SearchPrefix case cmpSuffix: return searcher.SearchSuffix case cmpMatch: return searcher.SearchContains default: panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op)) } } |
Changes to query/select.go.
1 | //----------------------------------------------------------------------------- | | < < < > < > < | | < < < | < < < < < < < < < < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query import ( "bytes" "fmt" "strings" "unicode/utf8" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" ) type matchValueFunc func(value string) bool func matchValueNever(string) bool { return false } type matchSpec struct { key string match matchValueFunc } // compileMeta calculates a selection func based on the given select criteria. func (ct *conjTerms) compileMeta() MetaMatchFunc { for key := range ct.mvals { // All queried keys must exist ct.addKey(key, cmpExist) } for _, op := range ct.keys { if op != cmpExist && op != cmpNotExist { return matchNever } } posSpecs, negSpecs := ct.createSelectSpecs() if len(posSpecs) > 0 || len(negSpecs) > 0 || len(ct.keys) > 0 { return makeSearchMetaMatchFunc(posSpecs, negSpecs, ct.keys) } return nil } func (ct *conjTerms) createSelectSpecs() (posSpecs, negSpecs []matchSpec) { posSpecs = make([]matchSpec, 0, len(ct.mvals)) negSpecs = make([]matchSpec, 0, len(ct.mvals)) for key, values := range ct.mvals { if !meta.KeyIsValid(key) { continue } posMatch, negMatch := createPosNegMatchFunc(key, values, ct.addSearch) if posMatch != nil { posSpecs = append(posSpecs, matchSpec{key, posMatch}) } if negMatch != nil { negSpecs = append(negSpecs, matchSpec{key, negMatch}) } } return posSpecs, negSpecs } type addSearchFunc func(val expValue) func noAddSearch(expValue) {} func createPosNegMatchFunc(key string, values []expValue, addSearch addSearchFunc) (posMatch, negMatch matchValueFunc) { posValues := make([]expValue, 0, len(values)) negValues := make([]expValue, 0, len(values)) for _, val := range values { if val.op.isNegated() { negValues = append(negValues, val) |
︙ | ︙ | |||
108 109 110 111 112 113 114 | func createMatchFunc(key string, values []expValue, addSearch addSearchFunc) matchValueFunc { if len(values) == 0 { return nil } switch meta.Type(key) { case meta.TypeCredential: return matchValueNever | | < < < < > > | | < < < < < < < | < < < < < | | > > | < | | < < < < < < < < < | 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 | func createMatchFunc(key string, values []expValue, addSearch addSearchFunc) matchValueFunc { if len(values) == 0 { return nil } switch meta.Type(key) { case meta.TypeCredential: return matchValueNever case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout return createMatchIDFunc(values, addSearch) case meta.TypeIDSet: return createMatchIDSetFunc(values, addSearch) case meta.TypeTagSet: return createMatchTagSetFunc(values, addSearch) case meta.TypeWord: return createMatchWordFunc(values, addSearch) case meta.TypeWordSet: return createMatchWordSetFunc(values, addSearch) case meta.TypeZettelmarkup: return createMatchZmkFunc(values, addSearch) } return createMatchStringFunc(values, addSearch) } func createMatchIDFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToStringPredicates(values, addSearch) return func(value string) bool { for _, pred := range preds { if !pred(value) { return false } } return true } } func createMatchIDSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { predList := valuesToStringSetPredicates(preprocessSet(values), addSearch) return func(value string) bool { ids := meta.ListFromValue(value) for _, preds := range predList { for _, pred := range preds { if !pred(ids) { return false } } } return true } } func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { predList := valuesToStringSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch) 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 _, preds := range predList { for _, pred := range preds { if !pred(tags) { return false } } } |
︙ | ︙ | |||
210 211 212 213 214 215 216 | } result[i] = tags } return result } func createMatchWordFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { | | > > > > > > > > > > > > > > > | 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 | } result[i] = tags } return result } func createMatchWordFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToStringPredicates(sliceToLower(values), addSearch) return func(value string) bool { value = strings.ToLower(value) for _, pred := range preds { if !pred(value) { return false } } return true } } func createMatchWordSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { predsList := valuesToStringSetPredicates(preprocessSet(sliceToLower(values)), addSearch) return func(value string) bool { words := meta.ListFromValue(value) for _, preds := range predsList { for _, pred := range preds { if !pred(words) { return false } } } return true } } func createMatchStringFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToStringPredicates(sliceToLower(values), addSearch) |
︙ | ︙ | |||
253 254 255 256 257 258 259 | func createMatchZmkFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { normPreds := make([]stringPredicate, 0, len(values)) negPreds := make([]stringPredicate, 0, len(values)) for _, v := range values { for _, word := range strfun.NormalizeWords(v.value) { if cmpOp := v.op; cmpOp.isNegated() { cmpOp = cmpOp.negate() | | | | 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | func createMatchZmkFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { normPreds := make([]stringPredicate, 0, len(values)) negPreds := make([]stringPredicate, 0, len(values)) for _, v := range values { for _, word := range strfun.NormalizeWords(v.value) { if cmpOp := v.op; cmpOp.isNegated() { cmpOp = cmpOp.negate() negPreds = append(negPreds, createStringCompareFunc(word, cmpOp)) } else { normPreds = append(normPreds, createStringCompareFunc(word, cmpOp)) addSearch(expValue{word, cmpOp}) // addSearch only for positive selections } } } return func(metaValue string) bool { temp := strings.Fields(zmk2text(metaValue)) values := make([]string, 0, len(temp)) |
︙ | ︙ | |||
293 294 295 296 297 298 299 | } } return true } func zmk2text(zmk string) string { isASCII, hasUpper, needParse := true, false, false | | | | | | 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 | } } return true } func zmk2text(zmk string) string { isASCII, hasUpper, needParse := true, false, false for i := 0; i < len(zmk); i++ { ch := zmk[i] if ch >= utf8.RuneSelf { isASCII = false break } hasUpper = hasUpper || ('A' <= ch && ch <= 'Z') needParse = needParse || !(('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z') || ('0' <= ch && ch <= '9') || ch == ' ') } if isASCII { if !needParse { if !hasUpper { return zmk } return strings.ToLower(zmk) } } is := parser.ParseMetadata(zmk) var buf bytes.Buffer if _, err := textenc.Create().WriteInlines(&buf, &is); err != nil { return strings.ToLower(zmk) } return strings.ToLower(buf.String()) } func preprocessSet(set []expValue) [][]expValue { result := make([][]expValue, 0, len(set)) for _, elem := range set { splitElems := strings.Split(elem.value, ",") valueElems := make([]expValue, 0, len(splitElems)) |
︙ | ︙ | |||
338 339 340 341 342 343 344 | } } return result } type stringPredicate func(string) bool | | < < < < < < < < < < < < < < < < < | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < | | | | | | | | | | < < | | | < < < < | | < < | 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 | } } return result } type stringPredicate func(string) bool func valuesToStringPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { result := make([]stringPredicate, len(values)) for i, v := range values { if !v.op.isNegated() { addSearch(v) // addSearch only for positive selections } result[i] = createStringCompareFunc(v.value, v.op) } return result } func createStringCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate { switch cmpOp { case cmpHas: return func(metaVal string) bool { return metaVal == cmpVal } case cmpHasNot: return func(metaVal string) bool { return metaVal != cmpVal } case cmpPrefix: return func(metaVal string) bool { return strings.HasPrefix(metaVal, cmpVal) } case cmpNoPrefix: return func(metaVal string) bool { return !strings.HasPrefix(metaVal, cmpVal) } case cmpSuffix: return func(metaVal string) bool { return strings.HasSuffix(metaVal, cmpVal) } case cmpNoSuffix: return func(metaVal string) bool { return !strings.HasSuffix(metaVal, cmpVal) } case cmpMatch: return func(metaVal string) bool { return strings.Contains(metaVal, cmpVal) } case cmpNoMatch: return func(metaVal string) bool { return !strings.Contains(metaVal, cmpVal) } default: panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal)) } } type stringSetPredicate func(value []string) bool func valuesToStringSetPredicates(values [][]expValue, addSearch addSearchFunc) [][]stringSetPredicate { result := make([][]stringSetPredicate, len(values)) for i, val := range values { elemPreds := make([]stringSetPredicate, len(val)) for j, v := range val { opVal := v.value // loop variable is used in closure --> save needed value switch v.op { case cmpHas: addSearch(v) // addSearch only for positive selections elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, true) case cmpHasNot: elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, false) case cmpPrefix: addSearch(v) elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, true) case cmpNoPrefix: elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, false) case cmpSuffix: addSearch(v) elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, true) case cmpNoSuffix: elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, false) case cmpMatch: addSearch(v) elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, true) case cmpNoMatch: elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, false) default: panic(fmt.Sprintf("Unknown compare operation %d with value %q", v.op, opVal)) } } result[i] = elemPreds } return result } func stringEqual(val1, val2 string) bool { return val1 == val2 } type compareStringFunc func(val1, val2 string) bool func makeStringSetPredicate(neededValue string, compare compareStringFunc, foundResult bool) stringSetPredicate { return func(metaVals []string) bool { for _, metaVal := range metaVals { if compare(metaVal, neededValue) { |
︙ | ︙ |
Changes to query/select_test.go.
1 | //----------------------------------------------------------------------------- | | < < < < | | | | | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query_test import ( "testing" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" ) func TestMatchZidNegate(t *testing.T) { q := query.Parse(api.KeyID + api.SearchOperatorHasNot + string(api.ZidVersion) + " " + api.KeyID + api.SearchOperatorHasNot + string(api.ZidLicense)) compiled := q.RetrieveAndCompile(nil) testCases := []struct { zid api.ZettelID exp bool }{ {api.ZidVersion, false}, {api.ZidLicense, false}, |
︙ | ︙ |
Changes to query/sorter.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package query import ( "strconv" "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" ) type sortFunc func(i, j int) bool func createSortFunc(order []sortOrder, ml []*meta.Meta) sortFunc { hasID := false sortFuncs := make([]sortFunc, 0, len(order)+1) |
︙ | ︙ | |||
56 57 58 59 60 61 62 | keyType := meta.Type(key) if key == api.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 } } | < < < < < < < < < < < < < < < < < < < < < < < < < < < > > > > > > > > > | 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 | keyType := meta.Type(key) if key == api.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.TypeNumber { return createSortNumberFunc(ml, key, descending) } return createSortStringFunc(ml, key, descending) } 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 } } return func(i, j int) bool { iVal, iOk := ml[i].Get(key) jVal, jOk := ml[j].Get(key) return (iOk && (!jOk || iVal < jVal)) || !jOk } } func getNum(m *meta.Meta, key string) (int64, bool) { if s, ok := m.Get(key); ok { if i, err := strconv.ParseInt(s, 10, 64); err == nil { return i, true } } return 0, false } |
Deleted query/specs.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted query/unlinked.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to strfun/escape.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package strfun import "io" var ( |
︙ | ︙ | |||
51 52 53 54 55 56 57 | } io.WriteString(w, s[last:i]) w.Write(esc) last = i + 1 } io.WriteString(w, s[last:]) } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | } io.WriteString(w, s[last:i]) w.Write(esc) last = i + 1 } io.WriteString(w, s[last:]) } var ( jsBackslash = []byte{'\\', '\\'} jsDoubleQuote = []byte{'\\', '"'} jsNewline = []byte{'\\', 'n'} jsTab = []byte{'\\', 't'} jsCr = []byte{'\\', 'r'} jsUnicode = []byte{'\\', 'u', '0', '0', '0', '0'} jsHex = []byte("0123456789ABCDEF") ) // JSONEscape returns the given string as a byte slice, where every non-printable // rune is made printable. func JSONEscape(w io.Writer, s string) (int, error) { length := 0 last := 0 for i, ch := range s { var b []byte switch ch { case '\t': b = jsTab case '\r': b = jsCr case '\n': b = jsNewline case '"': b = jsDoubleQuote case '\\': b = jsBackslash default: if ch < ' ' { b = jsUnicode b[2] = '0' b[3] = '0' b[4] = jsHex[ch>>4] b[5] = jsHex[ch&0xF] } else { continue } } l1, err := io.WriteString(w, s[last:i]) if err != nil { return 0, err } l2, err := w.Write(b) if err != nil { return 0, err } length += l1 + l2 last = i + 1 } l, err := io.WriteString(w, s[last:]) if err != nil { return 0, err } return length + l, nil } |
Changes to strfun/set.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package strfun // Set ist a set of strings. type Set map[string]struct{} |
︙ | ︙ |
Changes to strfun/slugify.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package strfun import ( "strings" "unicode" |
︙ | ︙ |
Changes to strfun/slugify_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- // 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 strfun provides some string functions. package strfun_test import ( "testing" "zettelstore.de/z/strfun" ) |
︙ | ︙ |
Changes to strfun/strfun.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < | 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 ( "bytes" "strings" "unicode/utf8" ) // Length returns the number of runes in the given string. func Length(s string) int { return utf8.RuneCountInString(s) } |
︙ | ︙ | |||
35 36 37 38 39 40 41 | runes = append(runes, r) } if len(runes) > maxLen { runes = runes[:maxLen] runes[maxLen-1] = '\u2025' } | | | | | | < < < < < < < < | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | runes = append(runes, r) } if len(runes) > maxLen { runes = runes[:maxLen] runes[maxLen-1] = '\u2025' } var buf bytes.Buffer for _, r := range runes { buf.WriteRune(r) } for i := 0; i < maxLen-len(runes); i++ { buf.WriteRune(pad) } return buf.String() } // SplitLines splits the given string into a list of lines. func SplitLines(s string) []string { return strings.FieldsFunc(s, func(r rune) bool { return r == '\n' || r == '\r' }) } |
Changes to strfun/strfun_test.go.
1 | //----------------------------------------------------------------------------- | | | < < < > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- // 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" ) |
︙ | ︙ |
Added template/LICENSE.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | Copyright (c) 2009 Michael Hoisie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
Added template/mustache.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // This file was derived from previous work: // - https://github.com/hoisie/mustache (License: MIT) // Copyright (c) 2009 Michael Hoisie // - https://github.com/cbroglie/mustache (a fork from above code) // Starting with commit [f9b4cbf] // Does not have an explicit copyright and obviously continues with // above MIT license. // The license text is included in the same directory where this file is // located. See file LICENSE. //----------------------------------------------------------------------------- // Package template implements the Mustache templating language. package template import ( "fmt" "io" "reflect" "regexp" "strings" "zettelstore.de/c/html" ) // Node represents a node in the parse tree. // It is either a Tag or a textNode. type node interface { node() } // Tag represents the different mustache tag types. // // Not all methods apply to all kinds of tags. Restrictions, if any, are noted // in the documentation for each method. Use the Type method to find out the // type of tag before calling type-specific methods. Calling a method // inappropriate to the type of tag causes a run time panic. type Tag interface { node // Type returns the type of the tag. Type() TagType // Name returns the name of the tag. Name() string // Tags returns any child tags. It panics for tag types which cannot contain // child tags (i.e. variable tags). Tags() []Tag } // A TagType represents the specific type of mustache tag that a Tag // represents. The zero TagType is not a valid type. type TagType uint // Defines representing the possible Tag types const ( Invalid TagType = iota Variable Section InvertedSection Partial ) type varNode struct { name string raw bool } func (*varNode) node() {} func (*varNode) Type() TagType { return Variable } func (e *varNode) Name() string { return e.name } func (*varNode) Tags() []Tag { panic("mustache: Tags on Variable type") } type sectionNode struct { name string inverted bool startline int nodes []node } func (*sectionNode) node() {} func (e *sectionNode) Type() TagType { if e.inverted { return InvertedSection } return Section } func (e *sectionNode) Name() string { return e.name } func (e *sectionNode) Tags() []Tag { return extractTags(e.nodes) } type partialNode struct { name string indent string prov PartialProvider } func (*partialNode) node() {} func (*partialNode) Type() TagType { return Partial } func (e *partialNode) Name() string { return e.name } func (*partialNode) Tags() []Tag { return nil } type textNode struct { text []byte } func (*textNode) node() {} // Template represents a compiled mustache template type Template struct { data string otag string ctag string p int curline int nodes []node partial PartialProvider errmiss bool // Error when variable is not found? } type parseError struct { line int message string } func (p parseError) Error() string { return fmt.Sprintf("line %d: %s", p.line, p.message) } // Tags returns the mustache tags for the given template func (tmpl *Template) Tags() []Tag { return extractTags(tmpl.nodes) } func extractTags(nodes []node) []Tag { tags := make([]Tag, 0, len(nodes)) for _, elem := range nodes { switch elem := elem.(type) { case *varNode: tags = append(tags, elem) case *sectionNode: tags = append(tags, elem) case *partialNode: tags = append(tags, elem) } } return tags } func (tmpl *Template) readString(s string) (string, error) { newlines := 0 for i := tmpl.p; ; i++ { //are we at the end of the string? if i+len(s) > len(tmpl.data) { return tmpl.data[tmpl.p:], io.EOF } if tmpl.data[i] == '\n' { newlines++ } if tmpl.data[i] != s[0] { continue } match := true for j := 1; j < len(s); j++ { if s[j] != tmpl.data[i+j] { match = false break } } if match { e := i + len(s) text := tmpl.data[tmpl.p:e] tmpl.p = e tmpl.curline += newlines return text, nil } } } type textReadingResult struct { text string padding string mayStandalone bool } func (tmpl *Template) readText() (*textReadingResult, error) { pPrev := tmpl.p text, err := tmpl.readString(tmpl.otag) if err == io.EOF { return &textReadingResult{ text: text, padding: "", mayStandalone: false, }, err } i := tmpl.p - len(tmpl.otag) for ; i > pPrev; i-- { if tmpl.data[i-1] != ' ' && tmpl.data[i-1] != '\t' { break } } if i == 0 || tmpl.data[i-1] == '\n' { return &textReadingResult{ text: tmpl.data[pPrev:i], padding: tmpl.data[i : tmpl.p-len(tmpl.otag)], mayStandalone: true, }, nil } return &textReadingResult{ text: tmpl.data[pPrev : tmpl.p-len(tmpl.otag)], padding: "", mayStandalone: false, }, nil } type tagReadingResult struct { tag string standalone bool } var skipWhitespaceTagTypes = map[byte]struct{}{ '#': {}, '^': {}, '/': {}, '<': {}, '>': {}, '=': {}, '!': {}, } 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 } } standalone := tmpl.skipWhitespaceTag(tag, eow, mayStandalone) return &tagReadingResult{ tag: tag, standalone: standalone, }, nil } func (tmpl *Template) skipWhitespaceTag(tag string, eow int, mayStandalone bool) bool { if !mayStandalone { return true } // Skip all whitespaces apeared after these types of tags until end of line if // the line only contains a tag and whitespaces. if _, ok := skipWhitespaceTagTypes[tag[0]]; !ok { return false } if eow == len(tmpl.data) { tmpl.p = eow return true } if eow < len(tmpl.data) && tmpl.data[eow] == '\n' { tmpl.p = eow + 1 tmpl.curline++ return true } if eow+1 < len(tmpl.data) && tmpl.data[eow] == '\r' && tmpl.data[eow+1] == '\n' { tmpl.p = eow + 2 tmpl.curline++ return true } return false } func (tmpl *Template) parsePartial(name, indent string) *partialNode { return &partialNode{ name: name, indent: indent, prov: tmpl.partial, } } func (tmpl *Template) parseSection(section *sectionNode) error { for { textResult, err := tmpl.readText() text := textResult.text padding := textResult.padding mayStandalone := textResult.mayStandalone if err == io.EOF { //put the remaining text in a block return parseError{section.startline, "Section " + section.name + " has no closing tag"} } // put text into an item section.nodes = append(section.nodes, &textNode{[]byte(text)}) tagResult, err := tmpl.readTag(mayStandalone) if err != nil { return err } if !tagResult.standalone { section.nodes = append(section.nodes, &textNode{[]byte(padding)}) } tag := tagResult.tag switch tag[0] { case '!': //ignore comment case '#', '^': name := strings.TrimSpace(tag[1:]) sn := §ionNode{name, tag[0] == '^', tmpl.curline, []node{}} err = tmpl.parseSection(sn) if err != nil { return err } section.nodes = append(section.nodes, sn) case '/': name := strings.TrimSpace(tag[1:]) if name != section.name { return parseError{tmpl.curline, "interleaved closing tag: " + name} } return nil case '>': name := strings.TrimSpace(tag[1:]) partial := tmpl.parsePartial(name, textResult.padding) section.nodes = append(section.nodes, partial) case '=': if tag[len(tag)-1] != '=' { return parseError{tmpl.curline, "Invalid meta tag"} } tag = strings.TrimSpace(tag[1 : len(tag)-1]) newtags := strings.SplitN(tag, " ", 2) if len(newtags) == 2 { tmpl.otag = newtags[0] tmpl.ctag = newtags[1] } case '{': if tag[len(tag)-1] == '}' { //use a raw tag name := strings.TrimSpace(tag[1 : len(tag)-1]) section.nodes = append(section.nodes, &varNode{name, true}) } case '&': name := strings.TrimSpace(tag[1:]) section.nodes = append(section.nodes, &varNode{name, true}) default: section.nodes = append(section.nodes, &varNode{tag, false}) } } } func (tmpl *Template) parse() error { for { textResult, err := tmpl.readText() text := textResult.text padding := textResult.padding mayStandalone := textResult.mayStandalone if err == io.EOF { //put the remaining text in a block tmpl.nodes = append(tmpl.nodes, &textNode{[]byte(text)}) return nil } // put text into an item tmpl.nodes = append(tmpl.nodes, &textNode{[]byte(text)}) tagResult, err := tmpl.readTag(mayStandalone) if err != nil { return err } if !tagResult.standalone { tmpl.nodes = append(tmpl.nodes, &textNode{[]byte(padding)}) } tag := tagResult.tag switch tag[0] { case '!': //ignore comment case '#', '^': name := strings.TrimSpace(tag[1:]) sn := §ionNode{name, tag[0] == '^', tmpl.curline, []node{}} err = tmpl.parseSection(sn) if err != nil { return err } tmpl.nodes = append(tmpl.nodes, sn) case '/': return parseError{tmpl.curline, "unmatched close tag"} case '>': name := strings.TrimSpace(tag[1:]) partial := tmpl.parsePartial(name, textResult.padding) tmpl.nodes = append(tmpl.nodes, partial) case '=': if tag[len(tag)-1] != '=' { return parseError{tmpl.curline, "Invalid meta tag"} } tag = strings.TrimSpace(tag[1 : len(tag)-1]) newtags := strings.SplitN(tag, " ", 2) if len(newtags) == 2 { tmpl.otag = newtags[0] tmpl.ctag = newtags[1] } case '{': //use a raw tag if tag[len(tag)-1] == '}' { name := strings.TrimSpace(tag[1 : len(tag)-1]) tmpl.nodes = append(tmpl.nodes, &varNode{name, true}) } case '&': name := strings.TrimSpace(tag[1:]) tmpl.nodes = append(tmpl.nodes, &varNode{name, true}) default: tmpl.nodes = append(tmpl.nodes, &varNode{tag, false}) } } } // Evaluate interfaces and pointers looking for a value that can look up the // name, via a struct field, method, or map key, and return the result of the // lookup. func lookup(stack []reflect.Value, name string, errMissing bool) (reflect.Value, error) { // dot notation if pos := strings.IndexByte(name, '.'); pos > 0 && pos < len(name)-1 { v, err := lookup(stack, name[:pos], errMissing) if err != nil { return v, err } return lookup([]reflect.Value{v}, name[pos+1:], errMissing) } for i := len(stack) - 1; i >= 0; i-- { if val, ok := lookupValue(stack[i], name); ok { return val, nil } } if errMissing { return reflect.Value{}, fmt.Errorf("missing variable %q", name) } return reflect.Value{}, nil } func lookupValue(v reflect.Value, name string) (reflect.Value, bool) { for v.IsValid() { typ := v.Type() if n := v.Type().NumMethod(); n > 0 { for i := 0; i < n; i++ { m := typ.Method(i) mtyp := m.Type if m.Name == name && mtyp.NumIn() == 1 { return v.Method(i).Call(nil)[0], true } } } if name == "." { return v, true } switch av := v; av.Kind() { case reflect.Ptr: v = av.Elem() case reflect.Interface: v = av.Elem() case reflect.Struct: return sanitizeValue(av.FieldByName(name)) case reflect.Map: return sanitizeValue(av.MapIndex(reflect.ValueOf(name))) default: return reflect.Value{}, false } } return reflect.Value{}, false } func sanitizeValue(v reflect.Value) (reflect.Value, bool) { if v.IsValid() { return v, true } return reflect.Value{}, false } 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: for v.IsValid() { switch av := v; av.Kind() { case reflect.Ptr: v = av.Elem() case reflect.Interface: v = av.Elem() default: break loop } } return v } func (tmpl *Template) renderSection(w io.Writer, section *sectionNode, stack []reflect.Value) error { value, err := lookup(stack, section.name, false) if err != nil { return err } // if the value is empty, check if it's an inverted section if isEmpty(value) != section.inverted { return nil } if !section.inverted { switch val := indirect(value); val.Kind() { case reflect.Slice, reflect.Array: valLen := val.Len() enumeration := make([]reflect.Value, valLen) for i := 0; i < valLen; i++ { enumeration[i] = val.Index(i) } topStack := len(stack) stack = append(stack, enumeration[0]) for _, elem := range enumeration { stack[topStack] = elem if err = tmpl.renderNodes(w, section.nodes, stack); err != nil { return err } } return nil case reflect.Map, reflect.Struct: return tmpl.renderNodes(w, section.nodes, append(stack, value)) } return tmpl.renderNodes(w, section.nodes, stack) } return tmpl.renderNodes(w, section.nodes, stack) } func (tmpl *Template) renderNodes(w io.Writer, nodes []node, stack []reflect.Value) error { for _, n := range nodes { if err := tmpl.renderNode(w, n, stack); err != nil { return err } } return nil } func (tmpl *Template) renderNode(w io.Writer, node node, stack []reflect.Value) error { switch n := node.(type) { case *textNode: _, err := w.Write(n.text) return err case *varNode: val, err := lookup(stack, n.name, tmpl.errmiss) if err != nil { return err } if val.IsValid() { if n.raw { fmt.Fprint(w, val.Interface()) } else { html.Escape(w, fmt.Sprint(val.Interface())) } } case *sectionNode: if err := tmpl.renderSection(w, n, stack); err != nil { return err } case *partialNode: partial, err := getPartials(n.prov, n.name, n.indent) if err != nil { return err } if err = partial.renderTemplate(w, stack); err != nil { return err } } return nil } func (tmpl *Template) renderTemplate(w io.Writer, stack []reflect.Value) error { for _, n := range tmpl.nodes { if err := tmpl.renderNode(w, n, stack); err != nil { return err } } return nil } // Render uses the given data source - generally a map or struct - to render // the compiled template to an io.Writer. func (tmpl *Template) Render(w io.Writer, data interface{}) error { return tmpl.renderTemplate(w, []reflect.Value{reflect.ValueOf(data)}) } // ParseString compiles a mustache template string, retrieving any // required partials from the given provider. The resulting output can be used // to efficiently render the template multiple times with different data // sources. func ParseString(data string, partials PartialProvider) (*Template, error) { if partials == nil { partials = &EmptyProvider } tmpl := Template{data, "{{", "}}", 0, 1, []node{}, partials, false} err := tmpl.parse() if err != nil { return nil, err } return &tmpl, err } // SetErrorOnMissing will produce an error is a variable is not found. func (tmpl *Template) SetErrorOnMissing() { tmpl.errmiss = true } // PartialProvider comprises the behaviors required of a struct to be able to // provide partials to the mustache rendering engine. type PartialProvider interface { // Get accepts the name of a partial and returns the parsed partial, if it // could be found; a valid but empty template, if it could not be found; or // nil and error if an error occurred (other than an inability to find the // partial). Get(name string) (string, error) } // ErrPartialNotFound is returned if a partial was not found. type ErrPartialNotFound struct { Name string } func (err *ErrPartialNotFound) Error() string { return "Partial '" + err.Name + "' not found" } // StaticProvider implements the PartialProvider interface by providing // partials drawn from a map, which maps partial name to template contents. type StaticProvider struct { Partials map[string]string } // Get accepts the name of a partial and returns the parsed partial. func (sp *StaticProvider) Get(name string) (string, error) { if sp.Partials != nil { if data, ok := sp.Partials[name]; ok { return data, nil } } return "", &ErrPartialNotFound{name} } // emptyProvider will always returns an empty string. type emptyProvider struct{} // Get accepts the name of a partial and returns the parsed partial. func (*emptyProvider) Get(string) (string, error) { return "", nil } // EmptyProvider is a partial provider that will always return an empty string. var EmptyProvider emptyProvider var nonEmptyLine = regexp.MustCompile(`(?m:^(.+)$)`) func getPartials(partials PartialProvider, name, indent string) (*Template, error) { data, err := partials.Get(name) if err != nil { return nil, err } // indent non empty lines data = nonEmptyLine.ReplaceAllString(data, indent+"$1") return ParseString(data, partials) } |
Added template/mustache_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 | //----------------------------------------------------------------------------- // 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. // // This file was derived from previous work: // - https://github.com/hoisie/mustache (License: MIT) // Copyright (c) 2009 Michael Hoisie // - https://github.com/cbroglie/mustache (a fork from above code) // Starting with commit [f9b4cbf] // Does not have an explicit copyright and obviously continues with // above MIT license. // The license text is included in the same directory where this file is // located. See file LICENSE. //----------------------------------------------------------------------------- package template_test import ( "bytes" "fmt" "strconv" "strings" "testing" "zettelstore.de/z/template" ) type Test struct { tmpl string context interface{} expected string err error } type Data struct { A bool B string } type User struct { Name string ID int64 } type Settings struct { Allow bool } func (u User) Func1() string { return u.Name } func (u *User) Func2() string { return u.Name } func (u *User) Func3() (map[string]string, error) { return map[string]string{"name": u.Name}, nil } func (u *User) Func4() (map[string]string, error) { return nil, nil } func (u *User) Func5() (*Settings, error) { return &Settings{true}, nil } func (u *User) Func6() ([]interface{}, error) { var v []interface{} v = append(v, &Settings{true}) return v, nil } func (u User) Truefunc1() bool { return true } func (u *User) Truefunc2() bool { return true } func makeVector(n int) []interface{} { var v []interface{} for i := 0; i < n; i++ { v = append(v, &User{"Mike", 1}) } return v } type Category struct { Tag string Description string } func (c Category) DisplayName() string { return c.Tag + " - " + c.Description } var tests = []Test{ {`hello world`, nil, "hello world", nil}, {`hello {{name}}`, map[string]string{"name": "world"}, "hello world", nil}, {`{{var}}`, map[string]string{"var": "5 > 2"}, "5 > 2", nil}, {`{{{var}}}`, map[string]string{"var": "5 > 2"}, "5 > 2", nil}, // {`{{var}}`, map[string]string{"var": "& \" < >"}, "& " < >", nil}, {`{{{var}}}`, map[string]string{"var": "& \" < >"}, "& \" < >", nil}, {`{{a}}{{b}}{{c}}{{d}}`, map[string]string{"a": "a", "b": "b", "c": "c", "d": "d"}, "abcd", nil}, {`0{{a}}1{{b}}23{{c}}456{{d}}89`, map[string]string{"a": "a", "b": "b", "c": "c", "d": "d"}, "0a1b23c456d89", nil}, {`hello {{! comment }}world`, map[string]string{}, "hello world", nil}, {`{{ a }}{{=<% %>=}}<%b %><%={{ }}=%>{{ c }}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc", nil}, {`{{ a }}{{= <% %> =}}<%b %><%= {{ }}=%>{{c}}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc", nil}, //section tests {`{{#A}}{{B}}{{/A}}`, Data{true, "hello"}, "hello", nil}, {`{{#A}}{{{B}}}{{/A}}`, Data{true, "5 > 2"}, "5 > 2", nil}, {`{{#A}}{{B}}{{/A}}`, Data{true, "5 > 2"}, "5 > 2", nil}, {`{{#A}}{{B}}{{/A}}`, Data{false, "hello"}, "", nil}, {`{{a}}{{#b}}{{b}}{{/b}}{{c}}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc", nil}, {`{{#A}}{{B}}{{/A}}`, struct { A []struct { B string } }{[]struct { B string }{{"a"}, {"b"}, {"c"}}}, "abc", nil, }, {`{{#A}}{{b}}{{/A}}`, struct{ A []map[string]string }{[]map[string]string{{"b": "a"}, {"b": "b"}, {"b": "c"}}}, "abc", nil}, {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []User{{"Mike", 1}}}, "Mike", nil}, {`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": nil}, "", nil}, {`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": (*User)(nil)}, "", nil}, {`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": []User{}}, "", nil}, {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil}, {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []interface{}{&User{"Mike", 12}}}, "Mike", nil}, {`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": makeVector(1)}, "Mike", nil}, {`{{Name}}`, User{"Mike", 1}, "Mike", nil}, {`{{Name}}`, &User{"Mike", 1}, "Mike", nil}, {"{{#users}}\n{{Name}}\n{{/users}}", map[string]interface{}{"users": makeVector(2)}, "Mike\nMike\n", nil}, {"{{#users}}\r\n{{Name}}\r\n{{/users}}", map[string]interface{}{"users": makeVector(2)}, "Mike\r\nMike\r\n", nil}, //falsy: golang zero values {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": nil}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": false}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": 0}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": 0.0}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": ""}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": Data{}}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": []interface{}{}}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": [0]interface{}{}}, "", nil}, //falsy: special cases we disagree with golang {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": "\t"}, "", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": []interface{}{0}}, "Hi 0", nil}, {"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": [1]interface{}{0}}, "Hi 0", nil}, //section does not exist {`{{#has}}{{/has}}`, &User{"Mike", 1}, "", nil}, // implicit iterator tests {`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []string{"a", "b", "c", "d", "e"}}, "\"(a)(b)(c)(d)(e)\"", nil}, {`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []int{1, 2, 3, 4, 5}}, "\"(1)(2)(3)(4)(5)\"", nil}, {`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []float64{1.10, 2.20, 3.30, 4.40, 5.50}}, "\"(1.1)(2.2)(3.3)(4.4)(5.5)\"", nil}, //inverted section tests {`{{a}}{{^b}}b{{/b}}{{c}}`, map[string]interface{}{"a": "a", "b": false, "c": "c"}, "abc", nil}, {`{{^a}}b{{/a}}`, map[string]interface{}{"a": false}, "b", nil}, {`{{^a}}b{{/a}}`, map[string]interface{}{"a": true}, "", nil}, {`{{^a}}b{{/a}}`, map[string]interface{}{"a": "nonempty string"}, "", nil}, {`{{^a}}b{{/a}}`, map[string]interface{}{"a": []string{}}, "b", nil}, {`{{a}}{{^b}}b{{/b}}{{c}}`, map[string]string{"a": "a", "c": "c"}, "abc", nil}, //function tests {`{{#users}}{{Func1}}{{/users}}`, map[string]interface{}{"users": []User{{"Mike", 1}}}, "Mike", nil}, {`{{#users}}{{Func1}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil}, {`{{#users}}{{Func2}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil}, {`{{#users}}{{#Func3}}{{name}}{{/Func3}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil}, {`{{#users}}{{#Func4}}{{name}}{{/Func4}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "", nil}, {`{{#Truefunc1}}abcd{{/Truefunc1}}`, User{"Mike", 1}, "abcd", nil}, {`{{#Truefunc1}}abcd{{/Truefunc1}}`, &User{"Mike", 1}, "abcd", nil}, {`{{#Truefunc2}}abcd{{/Truefunc2}}`, &User{"Mike", 1}, "abcd", nil}, {`{{#Func5}}{{#Allow}}abcd{{/Allow}}{{/Func5}}`, &User{"Mike", 1}, "abcd", nil}, {`{{#user}}{{#Func5}}{{#Allow}}abcd{{/Allow}}{{/Func5}}{{/user}}`, map[string]interface{}{"user": &User{"Mike", 1}}, "abcd", nil}, {`{{#user}}{{#Func6}}{{#Allow}}abcd{{/Allow}}{{/Func6}}{{/user}}`, map[string]interface{}{"user": &User{"Mike", 1}}, "abcd", nil}, //context chaining {`hello {{#section}}{{name}}{{/section}}`, map[string]interface{}{"section": map[string]string{"name": "world"}}, "hello world", nil}, {`hello {{#section}}{{name}}{{/section}}`, map[string]interface{}{"name": "bob", "section": map[string]string{"name": "world"}}, "hello world", nil}, {`hello {{#bool}}{{#section}}{{name}}{{/section}}{{/bool}}`, map[string]interface{}{"bool": true, "section": map[string]string{"name": "world"}}, "hello world", nil}, {`{{#users}}{{canvas}}{{/users}}`, map[string]interface{}{"canvas": "hello", "users": []User{{"Mike", 1}}}, "hello", nil}, {`{{#categories}}{{DisplayName}}{{/categories}}`, map[string][]*Category{ "categories": {&Category{"a", "b"}}, }, "a - b", nil}, //dotted names(dot notation) {`"{{person.name}}" == "{{#person}}{{name}}{{/person}}"`, map[string]interface{}{"person": map[string]string{"name": "Joe"}}, `"Joe" == "Joe"`, nil}, {`"{{{person.name}}}" == "{{#person}}{{{name}}}{{/person}}"`, map[string]interface{}{"person": map[string]string{"name": "Joe"}}, `"Joe" == "Joe"`, nil}, {`"{{a.b.c.d.e.name}}" == "Phil"`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": map[string]interface{}{"d": map[string]interface{}{"e": map[string]string{"name": "Phil"}}}}}}, `"Phil" == "Phil"`, nil}, {`"{{#a}}{{b.c.d.e.name}}{{/a}}" == "Phil"`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": map[string]interface{}{"d": map[string]interface{}{"e": map[string]string{"name": "Phil"}}}}}, "b": map[string]interface{}{"c": map[string]interface{}{"d": map[string]interface{}{"e": map[string]string{"name": "Wrong"}}}}}, `"Phil" == "Phil"`, nil}, } func parseString(data string) (*template.Template, error) { return template.ParseString(data, nil) } func render(tmpl *template.Template, data interface{}) (string, error) { var buf bytes.Buffer err := tmpl.Render(&buf, data) return buf.String(), err } func renderString(data string, errMissing bool, value interface{}) (string, error) { tmpl, err := parseString(data) if err != nil { return "", err } if errMissing { tmpl.SetErrorOnMissing() } return render(tmpl, value) } func TestBasic(t *testing.T) { t.Parallel() for _, test := range tests { output, err := renderString(test.tmpl, false, test.context) if err != nil { t.Errorf("%q expected %q but got error: %v", test.tmpl, test.expected, err) } else if output != test.expected { t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) } } // Now set "error on missing variable" and test again for _, test := range tests { output, err := renderString(test.tmpl, true, test.context) if err != nil { t.Errorf("%q expected %q but got error: %v", test.tmpl, test.expected, err) } else if output != test.expected { t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) } } } var missing = []Test{ //does not exist {`{{dne}}`, map[string]string{"name": "world"}, "", nil}, {`{{dne}}`, User{"Mike", 1}, "", nil}, {`{{dne}}`, &User{"Mike", 1}, "", nil}, //dotted names(dot notation) {`"{{a.b.c}}" == ""`, map[string]interface{}{}, `"" == ""`, nil}, {`"{{a.b.c.name}}" == ""`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "c": map[string]string{"name": "Jim"}}, `"" == ""`, nil}, {`{{#a}}{{b.c}}{{/a}}`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "b": map[string]string{"c": "ERROR"}}, "", nil}, } func TestMissing(t *testing.T) { t.Parallel() for _, test := range missing { output, err := renderString(test.tmpl, false, test.context) if err != nil { t.Error(err) } else if output != test.expected { t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) } } // 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")}, {`{{}}`, nil, "", fmt.Errorf("line 1: empty tag")}, {`{{}`, nil, "", fmt.Errorf("line 1: unmatched open tag")}, {`{{`, nil, "", fmt.Errorf("line 1: unmatched open tag")}, //invalid syntax - https://github.com/hoisie/mustache/issues/10 {`{{#a}}{{#b}}{{/a}}{{/b}}}`, map[string]interface{}{}, "", fmt.Errorf("line 1: interleaved closing tag: a")}, } func TestMalformed(t *testing.T) { t.Parallel() for _, test := range malformed { output, err := renderString(test.tmpl, false, test.context) if err != nil { if test.err == nil { t.Error(err) } else if test.err.Error() != err.Error() { t.Errorf("%q expected error %q but got error %q", test.tmpl, test.err.Error(), err.Error()) } } else { if test.err == nil { t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output) } else { t.Errorf("%q expected error %q but got %q", test.tmpl, test.err.Error(), output) } } } } type Person struct { FirstName string LastName string } func (p *Person) Name1() string { return p.FirstName + " " + p.LastName } func (p Person) Name2() string { return p.FirstName + " " + p.LastName } func TestPointerReceiver(t *testing.T) { t.Parallel() p := Person{"John", "Smith"} tests := []struct { tmpl string context interface{} expected string }{ { tmpl: "{{Name1}}", context: &p, expected: "John Smith", }, { tmpl: "{{Name2}}", context: &p, expected: "John Smith", }, { tmpl: "{{Name1}}", context: p, expected: "", }, { tmpl: "{{Name2}}", context: p, expected: "John Smith", }, } for _, test := range tests { output, err := renderString(test.tmpl, false, test.context) if err != nil { t.Error(err) } else if output != test.expected { t.Errorf("expected %q got %q", test.expected, output) } } } type tag struct { Type template.TagType Name string Tags []tag } type tagsTest struct { tmpl string tags []tag } var tagTests = []tagsTest{ { tmpl: `hello world`, tags: nil, }, { tmpl: `hello {{name}}`, tags: []tag{ { Type: template.Variable, Name: "name", }, }, }, { tmpl: `{{#name}}hello {{name}}{{/name}}{{^name}}hello {{name2}}{{/name}}`, tags: []tag{ { Type: template.Section, Name: "name", Tags: []tag{ { Type: template.Variable, Name: "name", }, }, }, { Type: template.InvertedSection, Name: "name", Tags: []tag{ { Type: template.Variable, Name: "name2", }, }, }, }, }, } func TestTags(t *testing.T) { t.Parallel() for _, test := range tagTests { testTags(t, &test) } } func testTags(t *testing.T, test *tagsTest) { tmpl, err := parseString(test.tmpl) if err != nil { t.Error(err) return } compareTags(t, tmpl.Tags(), test.tags) } func compareTags(t *testing.T, actual []template.Tag, expected []tag) { if len(actual) != len(expected) { t.Errorf("expected %d tags, got %d", len(expected), len(actual)) return } for i, tag := range actual { if tag.Type() != expected[i].Type { t.Errorf("expected %s, got %s", tagString(expected[i].Type), tagString(tag.Type())) return } if tag.Name() != expected[i].Name { t.Errorf("expected %s, got %s", expected[i].Name, tag.Name()) return } switch tag.Type() { case template.Variable: if len(expected[i].Tags) != 0 { 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 } } } func tagString(t template.TagType) string { if int(t) < len(tagNames) { return tagNames[t] } return "type" + strconv.Itoa(int(t)) } var tagNames = []string{ template.Invalid: "Invalid", template.Variable: "Variable", template.Section: "Section", template.InvertedSection: "InvertedSection", template.Partial: "Partial", } |
Added template/spec_test.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | //----------------------------------------------------------------------------- // 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. // // This file was derived from previous work: // - https://github.com/hoisie/mustache (License: MIT) // Copyright (c) 2009 Michael Hoisie // - https://github.com/cbroglie/mustache (a fork from above code) // Starting with commit [f9b4cbf] // Does not have an explicit copyright and obviously continues with // above MIT license. // The license text is included in the same directory where this file is // located. See file LICENSE. //----------------------------------------------------------------------------- package template_test import ( "encoding/json" "errors" "os" "path/filepath" "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, "Ampersand Integer Interpolation": true, "Basic Decimal Interpolation": true, "Triple Mustache Decimal Interpolation": true, "Ampersand Decimal Interpolation": true, "Basic Context Miss Interpolation": true, "Triple Mustache Context Miss Interpolation": true, "Ampersand Context Miss Interpolation": true, "Dotted Names - Basic Interpolation": true, "Dotted Names - Triple Mustache Interpolation": true, "Dotted Names - Ampersand Interpolation": true, "Dotted Names - Arbitrary Depth": true, "Dotted Names - Broken Chains": true, "Dotted Names - Broken Chain Resolution": true, "Dotted Names - Initial Resolution": true, "Interpolation - Surrounding Whitespace": true, "Triple Mustache - Surrounding Whitespace": true, "Ampersand - Surrounding Whitespace": true, "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, "Nested (Truthy)": true, "Context Misses": true, "Dotted Names - Truthy": true, "Dotted Names - Falsey": true, "Internal Whitespace": true, "Indented Inline Sections": true, "Standalone Lines": true, "Standalone Indented Lines": true, "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, "Nested (Truthy)": true, "Nested (Falsey)": true, "Context Misses": true, "Implicit Iterator - String": true, "Implicit Iterator - Integer": true, "Implicit Iterator - Decimal": true, "Implicit Iterator - Array": true, "Dotted Names - Truthy": true, "Dotted Names - Falsey": true, "Dotted Names - Broken Chains": true, "Surrounding Whitespace": true, "Internal Whitespace": true, "Indented Inline Sections": true, "Standalone Lines": true, "Indented Standalone Lines": true, "Standalone Line Endings": true, "Standalone Without Previous Line": true, "Standalone Without Newline": true, "Padding": true, }, "~lambdas.json": nil, // not implemented } type specTest struct { Name string `json:"name"` Data interface{} `json:"data"` Expected string `json:"expected"` Template string `json:"template"` Description string `json:"desc"` Partials map[string]string `json:"partials"` } type specTestSuite struct { Tests []specTest `json:"tests"` } func getRoot() string { curDir, err := os.Getwd() if err != nil { curDir = os.Getenv("PWD") } return filepath.Join(curDir, "..", "testdata", "mustache") } func TestSpec(t *testing.T) { t.Parallel() root := getRoot() if _, err := os.Stat(root); err != nil { if errors.Is(err, os.ErrNotExist) { t.Fatalf("Could not find the mustache testdata folder at %s'", root) } t.Fatal(err) } paths, err := filepath.Glob(filepath.Join(root, "*.json")) if err != nil { t.Fatal(err) } sort.Strings(paths) for _, path := range paths { _, file := filepath.Split(path) enabled, ok := enabledTests[file] if !ok { t.Errorf("Unexpected file %s, consider adding to enabledFiles", file) continue } if enabled == nil { continue } b, err2 := os.ReadFile(path) if err2 != nil { t.Fatal(err2) } var suite specTestSuite err2 = json.Unmarshal(b, &suite) if err2 != nil { t.Fatal(err2) } for _, test := range suite.Tests { runTest(t, file, &test) } } } func selectProvider(partials map[string]string) template.PartialProvider { if len(partials) == 0 { return &template.EmptyProvider } return &template.StaticProvider{partials} } func runTest(t *testing.T, file string, test *specTest) { enabled, ok := enabledTests[file][test.Name] if !ok { t.Errorf("[%s %s]: Unexpected test, add to enabledTests", file, test.Name) } if !enabled { t.Logf("[%s %s]: Skipped", file, test.Name) return } tmpl, err := template.ParseString(test.Template, selectProvider(test.Partials)) if err != nil { t.Errorf("[%s %s]: %s", file, test.Name, err.Error()) return } out, err := render(tmpl, test.Data) if err != nil { t.Errorf("[%s %s]: %s", file, test.Name, err.Error()) return } if out != test.Expected { t.Errorf("[%s %s]: Expected %q, got %q", file, test.Name, test.Expected, out) return } t.Logf("[%s %s]: Passed", file, test.Name) } |
Added testdata/mustache/comments.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Comment tags represent content that should never appear in the resulting\noutput.\n\nThe tag's content may contain any substring (including newlines) EXCEPT the\nclosing delimiter.\n\nComment tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Inline","data":{},"expected":"1234567890","template":"12345{{! Comment Block! }}67890","desc":"Comment blocks should be removed from the template."},{"name":"Multiline","data":{},"expected":"1234567890\n","template":"12345{{!\n This is a\n multi-line comment...\n}}67890\n","desc":"Multiline comments should be permitted."},{"name":"Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{! Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{! Indented Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{! Standalone Comment }}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"!","template":" {{! I'm Still Standalone }}\n!","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"!\n","template":"!\n {{! I'm Still Standalone }}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{!\nSomething's going on here...\n}}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{!\n Something's going on here...\n }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Inline","data":{},"expected":" 12 \n","template":" 12 {{! 34 }}\n","desc":"Inline comments should not strip whitespace"},{"name":"Surrounding Whitespace","data":{},"expected":"12345 67890","template":"12345 {{! Comment Block! }} 67890","desc":"Comment removal should preserve surrounding whitespace."}]} |
Added testdata/mustache/delimiters.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Set Delimiter tags are used to change the tag delimiters for all content\nfollowing the tag in the current compilation unit.\n\nThe tag's content MUST be any two non-whitespace sequences (separated by\nwhitespace) EXCEPT an equals sign ('=') followed by the current closing\ndelimiter.\n\nSet Delimiter tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Pair Behavior","data":{"text":"Hey!"},"expected":"(Hey!)","template":"{{=<% %>=}}(<%text%>)","desc":"The equals sign (used on both sides) should permit delimiter changes."},{"name":"Special Characters","data":{"text":"It worked!"},"expected":"(It worked!)","template":"({{=[ ]=}}[text])","desc":"Characters with special meaning regexen should be valid delimiters."},{"name":"Sections","data":{"section":true,"data":"I got interpolated."},"expected":"[\n I got interpolated.\n |data|\n\n {{data}}\n I got interpolated.\n]\n","template":"[\n{{#section}}\n {{data}}\n |data|\n{{/section}}\n\n{{= | | =}}\n|#section|\n {{data}}\n |data|\n|/section|\n]\n","desc":"Delimiters set outside sections should persist."},{"name":"Inverted Sections","data":{"section":false,"data":"I got interpolated."},"expected":"[\n I got interpolated.\n |data|\n\n {{data}}\n I got interpolated.\n]\n","template":"[\n{{^section}}\n {{data}}\n |data|\n{{/section}}\n\n{{= | | =}}\n|^section|\n {{data}}\n |data|\n|/section|\n]\n","desc":"Delimiters set outside inverted sections should persist."},{"name":"Partial Inheritence","data":{"value":"yes"},"expected":"[ .yes. ]\n[ .yes. ]\n","template":"[ {{>include}} ]\n{{= | | =}}\n[ |>include| ]\n","desc":"Delimiters set in a parent template should not affect a partial.","partials":{"include":".{{value}}."}},{"name":"Post-Partial Behavior","data":{"value":"yes"},"expected":"[ .yes. .yes. ]\n[ .yes. .|value|. ]\n","template":"[ {{>include}} ]\n[ .{{value}}. .|value|. ]\n","desc":"Delimiters set in a partial should not affect the parent template.","partials":{"include":".{{value}}. {{= | | =}} .|value|."}},{"name":"Surrounding Whitespace","data":{},"expected":"| |","template":"| {{=@ @=}} |","desc":"Surrounding whitespace should be left untouched."},{"name":"Outlying Whitespace (Inline)","data":{},"expected":" | \n","template":" | {{=@ @=}}\n","desc":"Whitespace should be left untouched."},{"name":"Standalone Tag","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{=@ @=}}\nEnd.\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Tag","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{=@ @=}}\nEnd.\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{= @ @ =}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"=","template":" {{=@ @=}}\n=","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"=\n","template":"=\n {{=@ @=}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Pair with Padding","data":{},"expected":"||","template":"|{{= @ @ =}}|","desc":"Superfluous in-tag whitespace should be ignored."}]} |
Added testdata/mustache/interpolation.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Interpolation tags are used to integrate dynamic content into the template.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the data to replace the tag. A single period (`.`)\nindicates that the item currently sitting atop the context stack should be\nused; otherwise, name resolution is as follows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object, the data is the value returned by the\n method with the given name.\n 5) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nData should be coerced into a string (and escaped, if appropriate) before\ninterpolation.\n\nThe Interpolation tags MUST NOT be treated as standalone.\n","tests":[{"name":"No Interpolation","data":{},"expected":"Hello from {Mustache}!\n","template":"Hello from {Mustache}!\n","desc":"Mustache-free templates should render as-is."},{"name":"Basic Interpolation","data":{"subject":"world"},"expected":"Hello, world!\n","template":"Hello, {{subject}}!\n","desc":"Unadorned tags should interpolate content into the template."},{"name":"HTML Escaping","data":{"forbidden":"& \" < >"},"expected":"These characters should be HTML escaped: & " < >\n","template":"These characters should be HTML escaped: {{forbidden}}\n","desc":"Basic interpolation should be HTML escaped."},{"name":"Triple Mustache","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{{forbidden}}}\n","desc":"Triple mustaches should interpolate without HTML escaping."},{"name":"Ampersand","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{&forbidden}}\n","desc":"Ampersand should interpolate without HTML escaping."},{"name":"Basic Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Triple Mustache Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{{mph}}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Ampersand Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{&mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Basic Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Triple Mustache Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{{power}}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Ampersand Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{&power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Basic Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Triple Mustache Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{{cannot}}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Ampersand Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{&cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Dotted Names - Basic Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{person.name}}\" == \"{{#person}}{{name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Triple Mustache Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{{person.name}}}\" == \"{{#person}}{{{name}}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Ampersand Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{&person.name}}\" == \"{{#person}}{{&name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Arbitrary Depth","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{a.b.c.d.e.name}}\" == \"Phil\"","desc":"Dotted names should be functional to any level of nesting."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{a.b.c}}\" == \"\"","desc":"Any falsey value prior to the last part of the name should yield ''."},{"name":"Dotted Names - Broken Chain Resolution","data":{"a":{"b":{}},"c":{"name":"Jim"}},"expected":"\"\" == \"\"","template":"\"{{a.b.c.name}}\" == \"\"","desc":"Each part of a dotted name should resolve only against its parent."},{"name":"Dotted Names - Initial Resolution","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}},"b":{"c":{"d":{"e":{"name":"Wrong"}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{#a}}{{b.c.d.e.name}}{{/a}}\" == \"Phil\"","desc":"The first part of a dotted name should resolve as any other name."},{"name":"Interpolation - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{{string}}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{&string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Interpolation - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{{string}}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{&string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Interpolation With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{ string }}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Triple Mustache With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{{ string }}}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Ampersand With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{& string }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} |
Added testdata/mustache/inverted.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Inverted Section tags and End Section tags are used in combination to wrap a\nsection of the template.\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Inverted Section tag MUST be\nfollowed by an End Section tag with the same content within the same\nsection.\n\nThis tag's content names the data to replace the tag. Name resolution is as\nfollows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object and the method with the given name has an\n arity of 1, the method SHOULD be called with a String containing the\n unprocessed contents of the sections; the data is the value returned.\n 5) Otherwise, the data is the value returned by calling the method with\n the given name.\n 6) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nThis section MUST NOT be rendered unless the data list is empty.\n\nInverted Section and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Falsey","data":{"boolean":false},"expected":"\"This should be rendered.\"","template":"\"{{^boolean}}This should be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents rendered."},{"name":"Truthy","data":{"boolean":true},"expected":"\"\"","template":"\"{{^boolean}}This should not be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"\"","template":"\"{{^context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should behave like truthy values."},{"name":"List","data":{"list":[{"n":1},{"n":2},{"n":3}]},"expected":"\"\"","template":"\"{{^list}}{{n}}{{/list}}\"","desc":"Lists should behave like truthy values."},{"name":"Empty List","data":{"list":[]},"expected":"\"Yay lists!\"","template":"\"{{^list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":false},"expected":"* first\n* second\n* third\n","template":"{{^bool}}\n* first\n{{/bool}}\n* {{two}}\n{{^bool}}\n* third\n{{/bool}}\n","desc":"Multiple inverted sections per template should be permitted."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A B C D E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should have their contents rendered."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[Cannot find key 'missing'!]","template":"[{{^missing}}Cannot find key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"\" == \"\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":false},"expected":" | \t|\t | \n","template":" | {{^boolean}}\t|\t{{/boolean}} | \n","desc":"Inverted sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":false},"expected":" | \n | \n","template":" | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Inverted should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":false},"expected":" NO\n WAY\n","template":" {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{^boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Standalone Indented Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n {{^boolean}}\n|\n {{/boolean}}\n| A Line\n","desc":"Standalone indented lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":false},"expected":"|\r\n|","template":"|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":false},"expected":"^\n/","template":" {{^boolean}}\n^{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":false},"expected":"^\n/\n","template":"^{{^boolean}}\n/\n {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":false},"expected":"|=|","template":"|{{^ boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} |
Added testdata/mustache/partials.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Partial tags are used to expand an external template into the current\ntemplate.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the partial to inject. Set Delimiter tags MUST NOT\naffect the parsing of a partial. The partial MUST be rendered against the\ncontext stack local to the tag. If the named partial cannot be found, the\nempty string SHOULD be used instead, as in interpolations.\n\nPartial tags SHOULD be treated as standalone when appropriate. If this tag\nis used standalone, any whitespace preceding the tag should treated as\nindentation, and prepended to each line of the partial before rendering.\n","tests":[{"name":"Basic Behavior","data":{},"expected":"\"from partial\"","template":"\"{{>text}}\"","desc":"The greater-than operator should expand to the named partial.","partials":{"text":"from partial"}},{"name":"Failed Lookup","data":{},"expected":"\"\"","template":"\"{{>text}}\"","desc":"The empty string should be used when the named partial is not found.","partials":{}},{"name":"Context","data":{"text":"content"},"expected":"\"*content*\"","template":"\"{{>partial}}\"","desc":"The greater-than operator should operate within the current context.","partials":{"partial":"*{{text}}*"}},{"name":"Recursion","data":{"content":"X","nodes":[{"content":"Y","nodes":[]}]},"expected":"X<Y<>>","template":"{{>node}}","desc":"The greater-than operator should properly recurse.","partials":{"node":"{{content}}<{{#nodes}}{{>node}}{{/nodes}}>"}},{"name":"Surrounding Whitespace","data":{},"expected":"| \t|\t |","template":"| {{>partial}} |","desc":"The greater-than operator should not alter surrounding whitespace.","partials":{"partial":"\t|\t"}},{"name":"Inline Indentation","data":{"data":"|"},"expected":" | >\n>\n","template":" {{data}} {{> partial}}\n","desc":"Whitespace should be left untouched.","partials":{"partial":">\n>"}},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n>|","template":"|\r\n{{>partial}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags.","partials":{"partial":">"}},{"name":"Standalone Without Previous Line","data":{},"expected":" >\n >>","template":" {{>partial}}\n>","desc":"Standalone tags should not require a newline to precede them.","partials":{"partial":">\n>"}},{"name":"Standalone Without Newline","data":{},"expected":">\n >\n >","template":">\n {{>partial}}","desc":"Standalone tags should not require a newline to follow them.","partials":{"partial":">\n>"}},{"name":"Standalone Indentation","data":{"content":"<\n->"},"expected":"\\\n |\n <\n->\n |\n/\n","template":"\\\n {{>partial}}\n/\n","desc":"Each line of the partial should be indented before rendering.","partials":{"partial":"|\n{{{content}}}\n|\n"}},{"name":"Padding Whitespace","data":{"boolean":true},"expected":"|[]|","template":"|{{> partial }}|","desc":"Superfluous in-tag whitespace should be ignored.","partials":{"partial":"[]"}}]} |
Added testdata/mustache/sections.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Section tags and End Section tags are used in combination to wrap a section\nof the template for iteration\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Section tag MUST be followed\nby an End Section tag with the same content within the same section.\n\nThis tag's content names the data to replace the tag. Name resolution is as\nfollows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object and the method with the given name has an\n arity of 1, the method SHOULD be called with a String containing the\n unprocessed contents of the sections; the data is the value returned.\n 5) Otherwise, the data is the value returned by calling the method with\n the given name.\n 6) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nFor each element in the data list, the element MUST be pushed onto the\ncontext stack, the section MUST be rendered, and the element MUST be popped\noff the context stack.\n\nSection and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Truthy","data":{"boolean":true},"expected":"\"This should be rendered.\"","template":"\"{{#boolean}}This should be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents rendered."},{"name":"Falsey","data":{"boolean":false},"expected":"\"\"","template":"\"{{#boolean}}This should not be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"Hi Joe.\"","template":"\"{{#context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should be pushed onto the context stack."},{"name":"Deeply Nested Contexts","data":{"a":{"one":1},"b":{"two":2},"c":{"three":3},"d":{"four":4},"e":{"five":5}},"expected":"1\n121\n12321\n1234321\n123454321\n1234321\n12321\n121\n1\n","template":"{{#a}}\n{{one}}\n{{#b}}\n{{one}}{{two}}{{one}}\n{{#c}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{#d}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{#e}}\n{{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}\n{{/e}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{/d}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{/c}}\n{{one}}{{two}}{{one}}\n{{/b}}\n{{one}}\n{{/a}}\n","desc":"All elements on the context stack should be accessible."},{"name":"List","data":{"list":[{"item":1},{"item":2},{"item":3}]},"expected":"\"123\"","template":"\"{{#list}}{{item}}{{/list}}\"","desc":"Lists should be iterated; list items should visit the context stack."},{"name":"Empty List","data":{"list":[]},"expected":"\"\"","template":"\"{{#list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":true},"expected":"* first\n* second\n* third\n","template":"{{#bool}}\n* first\n{{/bool}}\n* {{two}}\n{{#bool}}\n* third\n{{/bool}}\n","desc":"Multiple sections per template should be permitted."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A B C D E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should have their contents rendered."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[]","template":"[{{#missing}}Found key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Implicit Iterator - String","data":{"list":["a","b","c","d","e"]},"expected":"\"(a)(b)(c)(d)(e)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should directly interpolate strings."},{"name":"Implicit Iterator - Integer","data":{"list":[1,2,3,4,5]},"expected":"\"(1)(2)(3)(4)(5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast integers to strings and interpolate."},{"name":"Implicit Iterator - Decimal","data":{"list":[1.1,2.2,3.3,4.4,5.5]},"expected":"\"(1.1)(2.2)(3.3)(4.4)(5.5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast decimals to strings and interpolate."},{"name":"Implicit Iterator - Array","desc":"Implicit iterators should allow iterating over nested arrays.","data":{"list":[[1,2,3],["a","b","c"]]},"template":"\"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}\"","expected":"\"(123)(abc)\""},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"Here\" == \"Here\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"Here\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":true},"expected":" | \t|\t | \n","template":" | {{#boolean}}\t|\t{{/boolean}} | \n","desc":"Sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":true},"expected":" | \n | \n","template":" | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Sections should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":true},"expected":" YES\n GOOD\n","template":" {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{#boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n {{#boolean}}\n|\n {{/boolean}}\n| A Line\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":true},"expected":"|\r\n|","template":"|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":true},"expected":"#\n/","template":" {{#boolean}}\n#{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":true},"expected":"#\n/\n","template":"#{{#boolean}}\n/\n {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":true},"expected":"|=|","template":"|{{# boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} |
Added testdata/mustache/~lambdas.json.
> | 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Lambdas are a special-cased data type for use in interpolations and\nsections.\n\nWhen used as the data value for an Interpolation tag, the lambda MUST be\ntreatable as an arity 0 function, and invoked as such. The returned value\nMUST be rendered against the default delimiters, then interpolated in place\nof the lambda.\n\nWhen used as the data value for a Section tag, the lambda MUST be treatable\nas an arity 1 function, and invoked as such (passing a String containing the\nunprocessed section contents). The returned value MUST be rendered against\nthe current delimiters, then interpolated in place of the section.\n","tests":[{"name":"Interpolation","data":{"lambda":{"php":"return \"world\";","clojure":"(fn [] \"world\")","__tag__":"code","perl":"sub { \"world\" }","python":"lambda: \"world\"","ruby":"proc { \"world\" }","js":"function() { return \"world\" }"}},"expected":"Hello, world!","template":"Hello, {{lambda}}!","desc":"A lambda's return value should be interpolated."},{"name":"Interpolation - Expansion","data":{"planet":"world","lambda":{"php":"return \"{{planet}}\";","clojure":"(fn [] \"{{planet}}\")","__tag__":"code","perl":"sub { \"{{planet}}\" }","python":"lambda: \"{{planet}}\"","ruby":"proc { \"{{planet}}\" }","js":"function() { return \"{{planet}}\" }"}},"expected":"Hello, world!","template":"Hello, {{lambda}}!","desc":"A lambda's return value should be parsed."},{"name":"Interpolation - Alternate Delimiters","data":{"planet":"world","lambda":{"php":"return \"|planet| => {{planet}}\";","clojure":"(fn [] \"|planet| => {{planet}}\")","__tag__":"code","perl":"sub { \"|planet| => {{planet}}\" }","python":"lambda: \"|planet| => {{planet}}\"","ruby":"proc { \"|planet| => {{planet}}\" }","js":"function() { return \"|planet| => {{planet}}\" }"}},"expected":"Hello, (|planet| => world)!","template":"{{= | | =}}\nHello, (|&lambda|)!","desc":"A lambda's return value should parse with the default delimiters."},{"name":"Interpolation - Multiple Calls","data":{"lambda":{"php":"global $calls; return ++$calls;","clojure":"(def g (atom 0)) (fn [] (swap! g inc))","__tag__":"code","perl":"sub { no strict; $calls += 1 }","python":"lambda: globals().update(calls=globals().get(\"calls\",0)+1) or calls","ruby":"proc { $calls ||= 0; $calls += 1 }","js":"function() { return (g=(function(){return this})()).calls=(g.calls||0)+1 }"}},"expected":"1 == 2 == 3","template":"{{lambda}} == {{{lambda}}} == {{lambda}}","desc":"Interpolated lambdas should not be cached."},{"name":"Escaping","data":{"lambda":{"php":"return \">\";","clojure":"(fn [] \">\")","__tag__":"code","perl":"sub { \">\" }","python":"lambda: \">\"","ruby":"proc { \">\" }","js":"function() { return \">\" }"}},"expected":"<>>","template":"<{{lambda}}{{{lambda}}}","desc":"Lambda results should be appropriately escaped."},{"name":"Section","data":{"x":"Error!","lambda":{"php":"return ($text == \"{{x}}\") ? \"yes\" : \"no\";","clojure":"(fn [text] (if (= text \"{{x}}\") \"yes\" \"no\"))","__tag__":"code","perl":"sub { $_[0] eq \"{{x}}\" ? \"yes\" : \"no\" }","python":"lambda text: text == \"{{x}}\" and \"yes\" or \"no\"","ruby":"proc { |text| text == \"{{x}}\" ? \"yes\" : \"no\" }","js":"function(txt) { return (txt == \"{{x}}\" ? \"yes\" : \"no\") }"}},"expected":"<yes>","template":"<{{#lambda}}{{x}}{{/lambda}}>","desc":"Lambdas used for sections should receive the raw section string."},{"name":"Section - Expansion","data":{"planet":"Earth","lambda":{"php":"return $text . \"{{planet}}\" . $text;","clojure":"(fn [text] (str text \"{{planet}}\" text))","__tag__":"code","perl":"sub { $_[0] . \"{{planet}}\" . $_[0] }","python":"lambda text: \"%s{{planet}}%s\" % (text, text)","ruby":"proc { |text| \"#{text}{{planet}}#{text}\" }","js":"function(txt) { return txt + \"{{planet}}\" + txt }"}},"expected":"<-Earth->","template":"<{{#lambda}}-{{/lambda}}>","desc":"Lambdas used for sections should have their results parsed."},{"name":"Section - Alternate Delimiters","data":{"planet":"Earth","lambda":{"php":"return $text . \"{{planet}} => |planet|\" . $text;","clojure":"(fn [text] (str text \"{{planet}} => |planet|\" text))","__tag__":"code","perl":"sub { $_[0] . \"{{planet}} => |planet|\" . $_[0] }","python":"lambda text: \"%s{{planet}} => |planet|%s\" % (text, text)","ruby":"proc { |text| \"#{text}{{planet}} => |planet|#{text}\" }","js":"function(txt) { return txt + \"{{planet}} => |planet|\" + txt }"}},"expected":"<-{{planet}} => Earth->","template":"{{= | | =}}<|#lambda|-|/lambda|>","desc":"Lambdas used for sections should parse with the current delimiters."},{"name":"Section - Multiple Calls","data":{"lambda":{"php":"return \"__\" . $text . \"__\";","clojure":"(fn [text] (str \"__\" text \"__\"))","__tag__":"code","perl":"sub { \"__\" . $_[0] . \"__\" }","python":"lambda text: \"__%s__\" % (text)","ruby":"proc { |text| \"__#{text}__\" }","js":"function(txt) { return \"__\" + txt + \"__\" }"}},"expected":"__FILE__ != __LINE__","template":"{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}","desc":"Lambdas used for sections should not be cached."},{"name":"Inverted Section","data":{"static":"static","lambda":{"php":"return false;","clojure":"(fn [text] false)","__tag__":"code","perl":"sub { 0 }","python":"lambda text: 0","ruby":"proc { |text| false }","js":"function(txt) { return false }"}},"expected":"<>","template":"<{{^lambda}}{{static}}{{/lambda}}>","desc":"Lambdas used for inverted sections should be considered truthy."}]} |
Deleted testdata/testbox/20230929102100.zettel.
|
| < < < < < < < |
Changes to tests/client/client_test.go.
1 | //----------------------------------------------------------------------------- | | < < < < < | | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. //----------------------------------------------------------------------------- // Package client provides a client for accessing the Zettelstore via its API. package client_test import ( "context" "flag" "fmt" "net/http" "net/url" "strconv" "testing" "zettelstore.de/c/api" "zettelstore.de/c/client" "zettelstore.de/z/kernel" ) func nextZid(zid api.ZettelID) api.ZettelID { numVal, err := strconv.ParseUint(string(zid), 10, 64) if err != nil { panic(err) |
︙ | ︙ | |||
50 51 52 53 54 55 56 | } } } func TestListZettel(t *testing.T) { const ( | | | | | | | | < | | | | | | | | | | < < < | | | > | | 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 | } } } func TestListZettel(t *testing.T) { const ( ownerZettel = 47 configRoleZettel = 29 writerZettel = ownerZettel - 23 readerZettel = ownerZettel - 23 creatorZettel = 7 publicZettel = 4 ) testdata := []struct { user string exp int }{ {"", publicZettel}, {"creator", creatorZettel}, {"reader", readerZettel}, {"writer", writerZettel}, {"owner", ownerZettel}, } t.Parallel() c := getClient() for i, tc := range testdata { t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) { c.SetAuth(tc.user, tc.user) q, h, l, err := c.ListZettelJSON(context.Background(), "") if err != nil { tt.Error(err) return } if q != "" { tt.Errorf("Query should be empty, but is %q", q) } if h != "" { tt.Errorf("Human should be empty, but is %q", q) } got := len(l) if got != tc.exp { tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l) } }) } q, h, l, err := c.ListZettelJSON(context.Background(), api.KeyRole+api.SearchOperatorHas+api.ValueRoleConfiguration) if err != nil { t.Error(err) return } expQ := "role:configuration" if q != expQ { t.Errorf("Query should be %q, but is %q", expQ, q) } expH := "role HAS configuration" if h != expH { t.Errorf("Human should be %q, but is %q", expH, h) } got := len(l) if got != configRoleZettel { t.Errorf("List of length %d expected, but got %d\n%v", configRoleZettel, got, l) } pl, err := c.ListZettel(context.Background(), api.KeyRole+api.SearchOperatorHas+api.ValueRoleConfiguration) if err != nil { t.Error(err) return } compareZettelList(t, pl, l) } func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaJSON) { t.Helper() if len(pl) != len(l) { t.Errorf("Different list lenght: Plain=%d, JSON=%d", len(pl), len(l)) } else { for i, line := range pl { if got := api.ZettelID(line[:14]); got != l[i].ID { t.Errorf("%d: JSON=%q, got=%q", i, l[i].ID, got) } } } } func TestGetZettelJSON(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") z, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome) if err != nil { t.Error(err) return } if m := z.Meta; len(m) == 0 { t.Errorf("Exptected non-empty meta, but got %v", z.Meta) } if z.Content == "" || z.Encoding != "" { t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding) } m, err := c.GetMeta(context.Background(), api.ZidDefaultHome) if err != nil { t.Error(err) return } if len(m) != len(z.Meta) { t.Errorf("Pure meta differs from zettel meta: %s vs %s", m, z.Meta) return } for k, v := range z.Meta { got, ok := m[k] if !ok { t.Errorf("Pure meta has no key %q", k) continue } if got != v { t.Errorf("Pure meta has different value for key %q: %q vs %q", k, got, v) } } } func TestGetParsedEvaluatedZettel(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") encodings := []api.EncodingEnum{ api.EncoderZJSON, api.EncoderHTML, api.EncoderSexpr, api.EncoderText, } for _, enc := range encodings { content, err := c.GetParsedZettel(context.Background(), api.ZidDefaultHome, enc) if err != nil { t.Error(err) continue |
︙ | ︙ | |||
200 201 202 203 204 205 206 | } if len(content) == 0 { t.Errorf("Empty content for evaluated encoding %v", enc) } } } | > > > > > > > > > | > > > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < | < < < < < < < < < < < < < < < | > > > > | | | 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 | } if len(content) == 0 { t.Errorf("Empty content for evaluated encoding %v", enc) } } } func checkZid(t *testing.T, expected, got api.ZettelID) bool { t.Helper() if expected != got { t.Errorf("Expected a Zid %q, but got %q", expected, got) return false } return true } func checkListZid(t *testing.T, l []api.ZidMetaJSON, pos int, expected api.ZettelID) { t.Helper() if got := api.ZettelID(l[pos].ID); got != expected { t.Errorf("Expected result[%d]=%v, but got %v", pos, expected, got) } } func TestGetZettelOrder(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.GetZettelOrder(context.Background(), api.ZidTOCNewTemplate) if err != nil { t.Error(err) return } if !checkZid(t, api.ZidTOCNewTemplate, rl.ID) { return } l := rl.List if got := len(l); got != 2 { t.Errorf("Expected list of length 2, got %d", got) return } checkListZid(t, l, 0, api.ZidTemplateNewZettel) checkListZid(t, l, 1, api.ZidTemplateNewUser) } func TestGetZettelContext(t *testing.T) { const ( allUserZid = api.ZettelID("20211019200500") ownerZid = api.ZettelID("20210629163300") writerZid = api.ZettelID("20210629165000") readerZid = api.ZettelID("20210629165024") creatorZid = api.ZettelID("20210629165050") limitAll = 3 ) t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.GetZettelContext(context.Background(), ownerZid, client.DirBoth, 0, limitAll) if err != nil { t.Error(err) return } if !checkZid(t, ownerZid, rl.ID) { return } l := rl.List if got := len(l); got != limitAll { t.Errorf("Expected list of length %d, got %d", limitAll, got) t.Error(rl) return } checkListZid(t, l, 0, allUserZid) checkListZid(t, l, 1, writerZid) checkListZid(t, l, 2, readerZid) // checkListZid(t, l, 3, creatorZid) rl, err = c.GetZettelContext(context.Background(), ownerZid, client.DirBackward, 0, 0) if err != nil { t.Error(err) return } if !checkZid(t, ownerZid, rl.ID) { return } l = rl.List if got := len(l); got != 1 { t.Errorf("Expected list of length 1, got %d", got) return } checkListZid(t, l, 0, allUserZid) } func TestGetUnlinkedReferences(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") zl, err := c.GetUnlinkedReferences(context.Background(), api.ZidDefaultHome, nil) if err != nil { t.Error(err) return } if !checkZid(t, api.ZidDefaultHome, zl.ID) { return } l := zl.List if got := len(l); got != 1 { t.Errorf("Expected list of length 1, got %d", got) return } } func failNoErrorOrNoCode(t *testing.T, err error, goodCode int) bool { if err != nil { if cErr, ok := err.(*client.Error); ok { |
︙ | ︙ | |||
328 329 330 331 332 333 334 | } } func TestListTags(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < | | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | 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 | } } func TestListTags(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") tm, err := c.QueryMapMeta(context.Background(), api.ActionSeparator+api.KeyTags) if err != nil { t.Error(err) return } tags := []struct { key string size int }{ {"#invisible", 1}, {"#user", 4}, {"#test", 4}, } if len(tm) != len(tags) { t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(tm), tm) } for _, tag := range tags { if zl, ok := tm[tag.key]; !ok { t.Errorf("No tag %v: %v", tag.key, tm) } else if len(zl) != tag.size { t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl) } } for i, id := range tm["#user"] { if id != tm["#test"][i] { t.Errorf("Tags #user and #test have different content: %v vs %v", tm["#user"], tm["#test"]) } } } func TestListRoles(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.QueryMapMeta(context.Background(), api.ActionSeparator+api.KeyRole) if err != nil { t.Error(err) return } exp := []string{"configuration", "user", "zettel"} if len(rl) != len(exp) { t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(rl), rl) } for _, id := range exp { if _, found := rl[id]; !found { t.Errorf("Role map expected key %q", id) } } } func TestVersion(t *testing.T) { t.Parallel() c := getClient() ver, err := c.GetVersionJSON(context.Background()) if err != nil { t.Error(err) return } if ver.Major != -1 || ver.Minor != -1 || ver.Patch != -1 || ver.Info != kernel.CoreDefaultVersion || ver.Hash != "" { t.Error(ver) } |
︙ | ︙ |
Changes to tests/client/crud_test.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights // and obligations under this license. //----------------------------------------------------------------------------- package client_test import ( "context" "strings" "testing" "zettelstore.de/c/api" "zettelstore.de/c/client" ) // --------------------------------------------------------------------------- // Tests that change the Zettelstore must nor run parallel to other tests. func TestCreateGetRenameDeleteZettel(t *testing.T) { // Is not to be allowed to run in parallel with other tests. |
︙ | ︙ | |||
58 59 60 61 62 63 64 | t.Error("Cannot rename", zid, ":", err) newZid = zid } doDelete(t, c, newZid) } | | | | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | t.Error("Cannot rename", zid, ":", err) newZid = zid } doDelete(t, c, newZid) } func TestCreateGetRenameDeleteZettelJSON(t *testing.T) { // Is not to be allowed to run in parallel with other tests. c := getClient() c.SetAuth("creator", "creator") zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{ Meta: nil, Encoding: "", Content: "Example", }) if err != nil { t.Error("Cannot create zettel:", err) return |
︙ | ︙ | |||
87 88 89 90 91 92 93 | newZid = zid } c.SetAuth("owner", "owner") doDelete(t, c, newZid) } | | | | | 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 | newZid = zid } c.SetAuth("owner", "owner") doDelete(t, c, newZid) } func TestCreateGetDeleteZettelJSON(t *testing.T) { // Is not to be allowed to run in parallel with other tests. c := getClient() c.SetAuth("owner", "owner") wrongModified := "19691231115959" zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{ Meta: api.ZettelMeta{ api.KeyTitle: "A\nTitle", // \n must be converted into a space api.KeyModified: wrongModified, }, }) if err != nil { t.Error("Cannot create zettel:", err) return } z, err := c.GetZettelJSON(context.Background(), zid) if err != nil { t.Error("Cannot get zettel:", zid, err) } else { exp := "A Title" if got := z.Meta[api.KeyTitle]; got != exp { t.Errorf("Expected title %q, but got %q", exp, got) } |
︙ | ︙ | |||
151 152 153 154 155 156 157 | if string(zt) != newZettel { t.Errorf("Expected zettel %q, got %q", newZettel, zt) } // Must delete to clean up for next tests doDelete(t, c, api.ZidDefaultHome) } | | | | | | 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 | if string(zt) != newZettel { t.Errorf("Expected zettel %q, got %q", newZettel, zt) } // Must delete to clean up for next tests doDelete(t, c, api.ZidDefaultHome) } func TestUpdateZettelJSON(t *testing.T) { c := getClient() c.SetAuth("writer", "writer") z, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome) if err != nil { t.Error(err) return } if got := z.Meta[api.KeyTitle]; got != "Home" { t.Errorf("Title of zettel is not \"Home\", but %q", got) return } newTitle := "New Home" z.Meta[api.KeyTitle] = newTitle wrongModified := "19691231235959" z.Meta[api.KeyModified] = wrongModified err = c.UpdateZettelJSON(context.Background(), api.ZidDefaultHome, z) if err != nil { t.Error(err) return } zt, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome) if err != nil { t.Error(err) return } if got := zt.Meta[api.KeyTitle]; got != newTitle { t.Errorf("Title of zettel is not %q, but %q", newTitle, got) } |
︙ | ︙ |
Changes to tests/client/embed_test.go.
1 | //----------------------------------------------------------------------------- | | < < < | | 1 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) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package client_test import ( "context" "strings" "testing" "zettelstore.de/c/api" ) const ( abcZid = api.ZettelID("20211020121000") abc10Zid = api.ZettelID("20211020121100") ) |
︙ | ︙ | |||
76 77 78 79 80 81 82 | } func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("reader", "reader") | | | | 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 | } func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("reader", "reader") zettelData, err := c.GetZettelJSON(context.Background(), api.ZidEmoji) if err != nil { t.Error(err) return } expectedEnc := "base64" if got := zettelData.Encoding; expectedEnc != got { t.Errorf("Zettel %q: encoding %q expected, but got %q", abcZid, expectedEnc, got) } content, err := c.GetEvaluatedZettel(context.Background(), abc10Zid, api.EncoderHTML) if err != nil { t.Error(err) return } if exp, got := "<p></p>", string(content); exp != got { t.Errorf("Zettel %q must contain %q, but got %q", abc10Zid, exp, got) } } func stringHead(s string) string { const maxLen = 40 if len(s) <= maxLen { |
︙ | ︙ |
Changes to tests/markdown_test.go.
1 | //----------------------------------------------------------------------------- | | < < < < | < | | | | | < | | 1 2 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) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package tests import ( "bytes" "encoding/json" "fmt" "os" "testing" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoder" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/sexprenc" _ "zettelstore.de/z/encoder/textenc" _ "zettelstore.de/z/encoder/zjsonenc" _ "zettelstore.de/z/encoder/zmkenc" "zettelstore.de/z/input" "zettelstore.de/z/parser" _ "zettelstore.de/z/parser/markdown" _ "zettelstore.de/z/parser/zettelmark" ) type markdownTestCase struct { Markdown string `json:"markdown"` HTML string `json:"html"` Example int `json:"example"` StartLine int `json:"start_line"` EndLine int `json:"end_line"` Section string `json:"section"` } func TestEncoderAvailability(t *testing.T) { t.Parallel() encoderMissing := false for _, enc := range encodings { enc := encoder.Create(enc) if enc == nil { t.Errorf("No encoder for %q found", enc) encoderMissing = true } } if encoderMissing { panic("At least one encoder is missing. See test log") |
︙ | ︙ | |||
77 78 79 80 81 82 83 | ast := createMDBlockSlice(tc.Markdown, config.NoHTML) testAllEncodings(t, tc, &ast) testZmkEncoding(t, tc, &ast) } } func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice { | | | | | | | | | | | | | | 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 | ast := createMDBlockSlice(tc.Markdown, config.NoHTML) testAllEncodings(t, tc, &ast) testZmkEncoding(t, tc, &ast) } } func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice { return parser.ParseBlocks(input.NewInput([]byte(markdown)), nil, "markdown", hi) } func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { var buf bytes.Buffer testID := tc.Example*100 + 1 for _, enc := range encodings { t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) { encoder.Create(enc).WriteBlocks(&buf, ast) buf.Reset() }) } } func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { zmkEncoder := encoder.Create(api.EncoderZmk) var buf bytes.Buffer testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { buf.Reset() zmkEncoder.WriteBlocks(&buf, ast) // gotFirst := buf.String() testID = tc.Example*100 + 2 secondAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, api.ValueSyntaxZmk, config.NoHTML) buf.Reset() zmkEncoder.WriteBlocks(&buf, &secondAst) gotSecond := buf.String() // if gotFirst != gotSecond { // st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond) // } testID = tc.Example*100 + 3 thirdAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, api.ValueSyntaxZmk, config.NoHTML) buf.Reset() zmkEncoder.WriteBlocks(&buf, &thirdAst) gotThird := buf.String() if gotSecond != gotThird { st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird) } }) } func TestAdditionalMarkdown(t *testing.T) { testcases := []struct { md string exp string }{ {`abc<br>def`, `abc@@<br>@@{="html"}def`}, } zmkEncoder := encoder.Create(api.EncoderZmk) var buf bytes.Buffer for i, tc := range testcases { ast := createMDBlockSlice(tc.md, config.MarkdownHTML) buf.Reset() zmkEncoder.WriteBlocks(&buf, &ast) got := buf.String() if got != tc.exp { t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got) } } } |
Changes to tests/naughtystrings_test.go.
1 | //----------------------------------------------------------------------------- | | < < < < < > | | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package tests import ( "bufio" "io" "os" "path/filepath" "testing" _ "zettelstore.de/z/cmd" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" ) // Test all parser / encoder with a list of "naughty strings", i.e. unusual strings // that often crash software. func getNaughtyStrings() (result []string, err error) { fpath := filepath.Join("..", "testdata", "naughty", "blns.txt") |
︙ | ︙ | |||
55 56 57 58 59 60 61 | } } return result } func getAllEncoder() (result []encoder.Encoder) { for _, enc := range encoder.GetEncodings() { | | | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | } } return result } func getAllEncoder() (result []encoder.Encoder) { for _, enc := range encoder.GetEncodings() { e := encoder.Create(enc) result = append(result, e) } return result } func TestNaughtyStringParser(t *testing.T) { blns, err := getNaughtyStrings() |
︙ | ︙ |
Changes to tests/regression_test.go.
1 | //----------------------------------------------------------------------------- | | < < < > < | > > < < | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package tests provides some higher-level tests. package tests import ( "bytes" "context" "fmt" "io" "net/url" "os" "path/filepath" "testing" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" _ "zettelstore.de/z/box/dirbox" ) var encodings = []api.EncodingEnum{ api.EncoderHTML, api.EncoderSexpr, api.EncoderText, api.EncoderZJSON, } func getFileBoxes(wd, kind string) (root string, boxes []box.ManagedBox) { root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind)) entries, err := os.ReadDir(root) if err != nil { panic(err) |
︙ | ︙ | |||
120 121 122 123 124 125 126 | } return u.Path[len(root):] } func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { t.Helper() | | | | | < | | | | | > | 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 | } return u.Path[len(root):] } func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { t.Helper() if enc := encoder.Create(enc); enc != nil { var buf bytes.Buffer enc.WriteMeta(&buf, zn.Meta, parser.ParseMetadata) checkFileContent(t, resultName, buf.String()) return } panic(fmt.Sprintf("Unknown writer encoding %q", enc)) } func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) { ss := p.(box.StartStopper) if err := ss.Start(context.Background()); err != nil { panic(err) } metaList := []*meta.Meta{} err := p.ApplyMeta(context.Background(), func(m *meta.Meta) { metaList = append(metaList, m) }, nil) if err != nil { panic(err) } for _, meta := range metaList { zettel, err2 := p.GetZettel(context.Background(), meta.Zid) if err2 != nil { panic(err2) } z := parser.ParseZettel(context.Background(), zettel, "", testConfig) for _, enc := range encodings { t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, enc), func(st *testing.T) { resultName := filepath.Join(wd, "result", "meta", boxName, z.Zid.String()+"."+enc.String()) checkMetaFile(st, resultName, z, enc) }) } } ss.Stop(context.Background()) } type myConfig struct{} func (*myConfig) Get(context.Context, *meta.Meta, string) string { return "" } func (*myConfig) AddDefaultValues(_ context.Context, m *meta.Meta) *meta.Meta { return m } func (*myConfig) GetHTMLInsecurity() config.HTMLInsecurity { return config.NoHTML } func (*myConfig) GetHomeZettel() id.Zid { return id.Invalid } func (*myConfig) GetListPageSize() int { return 0 } func (*myConfig) GetSiteName() string { return "" } func (*myConfig) GetYAMLHeader() bool { return false } func (*myConfig) GetZettelFileSyntax() []string { return nil } func (*myConfig) GetSimpleMode() bool { return false } func (*myConfig) GetExpertMode() bool { return false } |
︙ | ︙ |
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 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package main provides a command to build and run the software. package main import ( "archive/zip" "bytes" "errors" "flag" "fmt" "io" "io/fs" "net" "os" "os/exec" "path/filepath" "strings" "time" "zettelstore.de/z/strfun" ) var directProxy = []string{"GOPROXY=direct"} func executeCommand(env []string, name string, arg ...string) (string, error) { logCommand("EXEC", env, name, arg) var out bytes.Buffer cmd := prepareCommand(env, name, arg, &out) err := cmd.Run() return out.String(), err } func prepareCommand(env []string, name string, arg []string, out io.Writer) *exec.Cmd { if len(env) > 0 { env = append(env, os.Environ()...) } cmd := exec.Command(name, arg...) cmd.Env = env cmd.Stdin = nil cmd.Stdout = out cmd.Stderr = os.Stderr return cmd } func logCommand(exec string, env []string, name string, arg []string) { if verbose { if len(env) > 0 { for i, e := range env { fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e) } } fmt.Fprintln(os.Stderr, exec, name, arg) } } func readVersionFile() (string, error) { content, err := os.ReadFile("VERSION") if err != nil { return "", err } return strings.TrimFunc(string(content), func(r rune) bool { return r <= ' ' }), nil } func getVersion() string { base, err := readVersionFile() if err != nil { base = "dev" } return base } var dirtyPrefixes = []string{ "DELETED ", "ADDED ", "UPDATED ", "CONFLICT ", "EDITED ", "RENAMED ", "EXTRA "} const dirtySuffix = "-dirty" func readFossilDirty() (string, error) { s, err := executeCommand(nil, "fossil", "status", "--differ") if err != nil { return "", err } for _, line := range strfun.SplitLines(s) { for _, prefix := range dirtyPrefixes { if strings.HasPrefix(line, prefix) { return dirtySuffix, nil } } } return "", nil } func getFossilDirty() string { fossil, err := readFossilDirty() if err != nil { return "" } return fossil } func findExec(cmd string) string { if path, err := executeCommand(nil, "which", cmd); err == nil && path != "" { return strings.TrimSpace(path) } return "" } func cmdCheck(forRelease bool) error { if err := checkGoTest("./..."); err != nil { return err } if err := checkGoVet(); err != nil { return err } if err := checkShadow(forRelease); err != nil { return err } if err := checkStaticcheck(); err != nil { return err } if err := checkUnparam(forRelease); err != nil { return err } return checkFossilExtra() } func checkGoTest(pkg string, testParams ...string) error { args := []string{"test", pkg} args = append(args, testParams...) out, err := executeCommand(directProxy, "go", args...) if err != nil { for _, line := range strfun.SplitLines(out) { if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") { continue } fmt.Fprintln(os.Stderr, line) } } return err } func checkGoVet() error { out, err := executeCommand(nil, "go", "vet", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some checks failed") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func checkShadow(forRelease bool) error { path, err := findExecStrict("shadow", forRelease) if path == "" { return err } out, err := executeCommand(nil, path, "-strict", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some shadowed variables found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func checkStaticcheck() error { out, err := executeCommand(nil, "staticcheck", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some staticcheck problems found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } } return err } func checkUnparam(forRelease bool) error { path, err := findExecStrict("unparam", forRelease) if path == "" { return err } // out, err := executeCommand(nil, path, "./...") // if err != nil { // fmt.Fprintln(os.Stderr, "Some unparam problems found") // if len(out) > 0 { // fmt.Fprintln(os.Stderr, out) // } // } // if forRelease { // if out2, err2 := executeCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil { // fmt.Fprintln(os.Stderr, "Some optional unparam problems found") // if len(out2) > 0 { // fmt.Fprintln(os.Stderr, out2) // } // } // } return err } func findExecStrict(cmd string, forRelease bool) (string, error) { path := findExec(cmd) if path != "" || !forRelease { return path, nil } return "", errors.New("Command '" + cmd + "' not installed, but required for release") } func checkFossilExtra() error { out, err := executeCommand(nil, "fossil", "extra") if err != nil { fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'") return err } if len(out) > 0 { fmt.Fprint(os.Stderr, "Warning: unversioned file(s):") for i, extra := range strfun.SplitLines(out) { if i > 0 { fmt.Fprint(os.Stderr, ",") } fmt.Fprintf(os.Stderr, " %q", extra) } fmt.Fprintln(os.Stderr) } return nil } type zsInfo struct { cmd *exec.Cmd out bytes.Buffer adminAddress string } func cmdTestAPI() error { var err error var info zsInfo needServer := !addressInUse(":23123") if needServer { err = startZettelstore(&info) } if err != nil { return err } err = checkGoTest("zettelstore.de/z/tests/client", "-base-url", "http://127.0.0.1:23123") if needServer { err1 := stopZettelstore(&info) if err == nil { err = err1 } } return err } func startZettelstore(info *zsInfo) error { info.adminAddress = ":2323" name, arg := "go", []string{ "run", "cmd/zettelstore/main.go", "run", "-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]} logCommand("FORK", nil, name, arg) cmd := prepareCommand(nil, name, arg, &info.out) if !verbose { cmd.Stderr = nil } err := cmd.Start() for i := 0; i < 100; i++ { time.Sleep(time.Millisecond * 100) if addressInUse(info.adminAddress) { info.cmd = cmd return err } } return errors.New("zettelstore did not start") } func stopZettelstore(i *zsInfo) error { conn, err := net.Dial("tcp", i.adminAddress) if err != nil { fmt.Println("Unable to stop Zettelstore") return err } io.WriteString(conn, "shutdown\n") conn.Close() err = i.cmd.Wait() return err } func addressInUse(address string) bool { conn, err := net.Dial("tcp", address) if err != nil { return false } conn.Close() return true } func cmdBuild() error { return doBuild(directProxy, getVersion(), "bin/zettelstore") } func doBuild(env []string, version, target string) error { out, err := executeCommand( env, "go", "build", "-tags", "osusergo,netgo", "-trimpath", "-ldflags", fmt.Sprintf("-X main.version=%v -w", version), "-o", target, "zettelstore.de/z/cmd/zettelstore", ) if err != nil { return err } if len(out) > 0 { fmt.Println(out) } return nil } func cmdManual() error { base := getReleaseVersionData() return createManualZip(".", base) } func createManualZip(path, base string) error { manualPath := filepath.Join("docs", "manual") entries, err := os.ReadDir(manualPath) if err != nil { return err } zipName := filepath.Join(path, "manual-"+base+".zip") zipFile, err := os.OpenFile(zipName, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } defer zipFile.Close() zipWriter := zip.NewWriter(zipFile) defer zipWriter.Close() for _, entry := range entries { if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil { return err } } return nil } func createManualZipEntry(path string, entry fs.DirEntry, zipWriter *zip.Writer) error { info, err := entry.Info() if err != nil { return err } fh, err := zip.FileInfoHeader(info) if err != nil { return err } fh.Name = entry.Name() fh.Method = zip.Deflate w, err := zipWriter.CreateHeader(fh) if err != nil { return err } manualFile, err := os.Open(filepath.Join(path, entry.Name())) if err != nil { return err } defer manualFile.Close() _, err = io.Copy(w, manualFile) return err } func getReleaseVersionData() string { if fossil := getFossilDirty(); fossil != "" { fmt.Fprintln(os.Stderr, "Warning: releasing a dirty version") } base := getVersion() if strings.HasSuffix(base, "dev") { return base[:len(base)-3] + "preview-" + time.Now().Local().Format("20060102") } return base } func cmdRelease() error { if err := cmdCheck(true); err != nil { return err } base := getReleaseVersionData() releases := []struct { arch string os string env []string name string }{ {"amd64", "linux", nil, "zettelstore"}, {"arm", "linux", []string{"GOARM=6"}, "zettelstore"}, {"amd64", "darwin", nil, "zettelstore"}, {"arm64", "darwin", nil, "zettelstore"}, {"amd64", "windows", nil, "zettelstore.exe"}, } for _, rel := range releases { env := append([]string{}, rel.env...) env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os) env = append(env, directProxy...) zsName := filepath.Join("releases", rel.name) if err := doBuild(env, base, zsName); err != nil { return err } zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch) if err := createReleaseZip(zsName, zipName, rel.name); err != nil { return err } if err := os.Remove(zsName); err != nil { return err } } return createManualZip("releases", base) } func createReleaseZip(zsName, zipName, fileName string) error { zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } defer zipFile.Close() zw := zip.NewWriter(zipFile) defer zw.Close() err = addFileToZip(zw, zsName, fileName) if err != nil { return err } err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt") if err != nil { return err } err = addFileToZip(zw, "docs/readmezip.txt", "README.txt") return err } func addFileToZip(zipFile *zip.Writer, filepath, filename string) error { zsFile, err := os.Open(filepath) if err != nil { return err } defer zsFile.Close() stat, err := zsFile.Stat() if err != nil { return err } fh, err := zip.FileInfoHeader(stat) if err != nil { return err } fh.Name = filename fh.Method = zip.Deflate w, err := zipFile.CreateHeader(fh) if err != nil { return err } _, err = io.Copy(w, zsFile) return err } func cmdClean() error { for _, dir := range []string{"bin", "releases"} { err := os.RemoveAll(dir) if err != nil { return err } } out, err := executeCommand(nil, "go", "clean", "./...") if err != nil { return err } if len(out) > 0 { fmt.Println(out) } out, err = executeCommand(nil, "go", "clean", "-cache", "-modcache", "-testcache") if err != nil { return err } if len(out) > 0 { fmt.Println(out) } return nil } func cmdHelp() { fmt.Println(`Usage: go run tools/build.go [-v] COMMAND Options: -v Verbose output. Commands: build Build the software for local computer. check Check current working state: execute tests, static analysis tools, extra files, ... Is automatically done when releasing the software. clean Remove all build and release directories. help Output this text. manual Create a ZIP file with all manual zettel relcheck Check current working state for release. release Create the software for various platforms and put them in appropriate named ZIP files. testapi Start a Zettelstore and execute API tests. version Print the current version of the software. All commands can be abbreviated as long as they remain unique.`) } var ( verbose bool ) func main() { flag.BoolVar(&verbose, "v", false, "Verbose output") flag.Parse() var err error args := flag.Args() if len(args) < 1 { cmdHelp() } else { switch args[0] { case "b", "bu", "bui", "buil", "build": err = cmdBuild() case "m", "ma", "man", "manu", "manua", "manual": err = cmdManual() case "r", "re", "rel", "rele", "relea", "releas", "release": err = cmdRelease() case "cl", "cle", "clea", "clean": err = cmdClean() case "v", "ve", "ver", "vers", "versi", "versio", "version": fmt.Print(getVersion()) case "ch", "che", "chec", "check": err = cmdCheck(false) case "relc", "relch", "relche", "relchec", "relcheck": err = cmdCheck(true) case "t", "te", "tes", "test", "testa", "testap", "testapi": cmdTestAPI() case "h", "he", "hel", "help": cmdHelp() default: fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0]) cmdHelp() os.Exit(1) } } if err != nil { fmt.Fprintln(os.Stderr, err) } } |
Deleted tools/build/build.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/check/check.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/clean/clean.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/devtools/devtools.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/htmllint/htmllint.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/testapi/testapi.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted tools/tools.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to usecase/authenticate.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | > > > > > > > > | | > | | 1 2 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) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "math/rand" "net/http" "time" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/cred" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/logger" "zettelstore.de/z/query" ) // AuthenticatePort is the interface used by this use case. type AuthenticatePort interface { GetMeta(context.Context, id.Zid) (*meta.Meta, error) SelectMeta(context.Context, *query.Query) ([]*meta.Meta, error) } // Authenticate is the data for this use case. type Authenticate struct { log *logger.Logger token auth.TokenManager port AuthenticatePort ucGetUser GetUser } // NewAuthenticate creates a new use case. func NewAuthenticate(log *logger.Logger, token auth.TokenManager, authz auth.AuthzManager, port AuthenticatePort) Authenticate { return Authenticate{ log: log, token: token, port: port, ucGetUser: NewGetUser(authz, port), } } // Run executes the use case. // // Parameter "r" is just included to produce better logging messages. It may be nil. Do not use it // for other purposes. |
︙ | ︙ | |||
86 87 88 89 90 91 92 | "$2a$10$WHcSO3G9afJ3zlOYQR1suuf83bCXED2jmzjti/MH4YH4l2mivDuze", id.Invalid, "", "") } // addDelay after credential checking to allow some CPU time for other tasks. // durDelay is the normal delay, if time spend for checking is smaller than // the minimum delay minDelay. In addition some jitter (+/- 50 ms) is added. func addDelay(start time.Time, durDelay, minDelay time.Duration) { | | | 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | "$2a$10$WHcSO3G9afJ3zlOYQR1suuf83bCXED2jmzjti/MH4YH4l2mivDuze", id.Invalid, "", "") } // addDelay after credential checking to allow some CPU time for other tasks. // durDelay is the normal delay, if time spend for checking is smaller than // the minimum delay minDelay. In addition some jitter (+/- 50 ms) is added. func addDelay(start time.Time, durDelay, minDelay time.Duration) { jitter := time.Duration(rand.Intn(100)-50) * time.Millisecond if elapsed := time.Since(start); elapsed+minDelay < durDelay { time.Sleep(durDelay - elapsed + jitter) } else { time.Sleep(minDelay + jitter) } } |
︙ | ︙ | |||
129 130 131 132 133 134 135 | IsAuthenticatedAndValid IsAuthenticatedAndInvalid ) // Run executes the use case. func (uc *IsAuthenticated) Run(ctx context.Context) IsAuthenticatedResult { if !uc.authz.WithAuth() { | | | | | 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | IsAuthenticatedAndValid IsAuthenticatedAndInvalid ) // Run executes the use case. func (uc *IsAuthenticated) Run(ctx context.Context) IsAuthenticatedResult { if !uc.authz.WithAuth() { uc.log.Sense().Str("auth", "disabled").Msg("IsAuthenticated") return IsAuthenticatedDisabled } if uc.port.GetUser(ctx) == nil { uc.log.Sense().Msg("IsAuthenticated is false") return IsAuthenticatedAndInvalid } uc.log.Sense().Msg("IsAuthenticated is true") return IsAuthenticatedAndValid } |
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 190 191 192 193 194 195 196 197 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ZettelContextPort is the interface used by this use case. type ZettelContextPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // ZettelContextConfig is the interface to allow the usecase to read some config data. type ZettelContextConfig interface { // GetHomeZettel returns the value of the "home-zettel" key. GetHomeZettel() id.Zid } // ZettelContext is the data for this use case. type ZettelContext struct { port ZettelContextPort config ZettelContextConfig } // NewZettelContext creates a new use case. func NewZettelContext(port ZettelContextPort, config ZettelContextConfig) ZettelContext { return ZettelContext{port: port, config: config} } // ZettelContextDirection determines the way, the context is calculated. type ZettelContextDirection int // Constant values for ZettelContextDirection const ( _ ZettelContextDirection = iota ZettelContextForward // Traverse all forwarding links ZettelContextBackward // Traverse all backwaring links ZettelContextBoth // Traverse both directions ) // Run executes the use case. func (uc ZettelContext) Run(ctx context.Context, zid id.Zid, dir ZettelContextDirection, depth, limit int) (result []*meta.Meta, err error) { start, err := uc.port.GetMeta(ctx, zid) if err != nil { return nil, err } tasks := newQueue(start, depth, limit, uc.config.GetHomeZettel()) isBackward := dir == ZettelContextBoth || dir == ZettelContextBackward isForward := dir == ZettelContextBoth || dir == ZettelContextForward for { m, curDepth, found := tasks.next() if !found { break } result = append(result, m) for _, p := range m.ComputedPairsRest() { tasks.addPair(ctx, uc.port, p.Key, p.Value, curDepth+1, isBackward, isForward) } } return result, nil } type ztlCtxTask struct { next *ztlCtxTask meta *meta.Meta depth int } type contextQueue struct { home id.Zid seen id.Set first *ztlCtxTask last *ztlCtxTask maxDepth int limit int } func newQueue(m *meta.Meta, maxDepth, limit int, home id.Zid) *contextQueue { task := &ztlCtxTask{ next: nil, meta: m, depth: 0, } result := &contextQueue{ home: home, seen: id.NewSet(), first: task, last: task, maxDepth: maxDepth, limit: limit, } return result } func (zc *contextQueue) addPair( ctx context.Context, port ZettelContextPort, key, value string, curDepth int, isBackward, isForward bool, ) { if key == api.KeyBackward { if isBackward { zc.addIDSet(ctx, port, curDepth, value) } return } if key == api.KeyForward { if isForward { zc.addIDSet(ctx, port, curDepth, value) } return } if key == api.KeyBack { return } hasInverse := meta.Inverse(key) != "" if (!hasInverse || !isBackward) && (hasInverse || !isForward) { return } if t := meta.Type(key); t == meta.TypeID { zc.addID(ctx, port, curDepth, value) } else if t == meta.TypeIDSet { zc.addIDSet(ctx, port, curDepth, value) } } func (zc *contextQueue) addID(ctx context.Context, port ZettelContextPort, depth int, value string) { if (zc.maxDepth > 0 && depth > zc.maxDepth) || zc.hasLimit() { return } zid, err := id.Parse(value) if err != nil || zid == zc.home { return } m, err := port.GetMeta(ctx, zid) if err != nil { return } task := &ztlCtxTask{next: nil, meta: m, depth: depth} if zc.first == nil { zc.first = task zc.last = task } else { zc.last.next = task zc.last = task } } func (zc *contextQueue) addIDSet(ctx context.Context, port ZettelContextPort, curDepth int, value string) { for _, val := range meta.ListFromValue(value) { zc.addID(ctx, port, curDepth, val) } } func (zc *contextQueue) next() (*meta.Meta, int, bool) { if zc.hasLimit() { return nil, -1, false } for zc.first != nil { task := zc.first zc.first = task.next if zc.first == nil { zc.last = nil } m := task.meta zid := m.Zid _, found := zc.seen[zid] if found { continue } zc.seen.Zid(zid) return m, task.depth, true } return nil, -1, false } func (zc *contextQueue) hasLimit() bool { limit := zc.limit return limit > 0 && len(zc.seen) > limit } |
Changes to usecase/create_zettel.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | | | | < | > | > | | > | > | | | < < < | < < < < | < < < | | | | > > < < < | < < < < < < | < | | | | | | < | | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "time" "zettelstore.de/c/api" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/logger" ) // CreateZettelPort is the interface used by this use case. type CreateZettelPort interface { // CreateZettel creates a new zettel. CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) } // CreateZettel is the data for this use case. type CreateZettel struct { log *logger.Logger rtConfig config.Config port CreateZettelPort } // NewCreateZettel creates a new use case. func NewCreateZettel(log *logger.Logger, rtConfig config.Config, port CreateZettelPort) CreateZettel { return CreateZettel{ log: log, rtConfig: rtConfig, port: port, } } // PrepareCopy the zettel for further modification. func (*CreateZettel) PrepareCopy(origZettel domain.Zettel) domain.Zettel { m := origZettel.Meta.Clone() if title, found := m.Get(api.KeyTitle); found { m.Set(api.KeyTitle, prependTitle(title, "Copy", "Copy of ")) } if readonly, found := m.Get(api.KeyReadOnly); found { m.Set(api.KeyReadOnly, copyReadonly(readonly)) } content := origZettel.Content content.TrimSpace() return domain.Zettel{Meta: m, Content: content} } // PrepareVersion the zettel for further modification. func (*CreateZettel) PrepareVersion(origZettel domain.Zettel) domain.Zettel { origMeta := origZettel.Meta m := origMeta.Clone() m.Set(api.KeyPredecessor, origMeta.Zid.String()) if readonly, found := m.Get(api.KeyReadOnly); found { m.Set(api.KeyReadOnly, copyReadonly(readonly)) } content := origZettel.Content content.TrimSpace() return domain.Zettel{Meta: m, Content: content} } // PrepareFolge the zettel for further modification. func (*CreateZettel) PrepareFolge(origZettel domain.Zettel) domain.Zettel { origMeta := origZettel.Meta m := meta.New(id.Invalid) if title, found := origMeta.Get(api.KeyTitle); found { m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of ")) } m.SetNonEmpty(api.KeyRole, origMeta.GetDefault(api.KeyRole, "")) m.SetNonEmpty(api.KeyTags, origMeta.GetDefault(api.KeyTags, "")) m.SetNonEmpty(api.KeySyntax, origMeta.GetDefault(api.KeySyntax, "")) m.Set(api.KeyPrecursor, origMeta.Zid.String()) return domain.Zettel{Meta: m, Content: domain.NewContent(nil)} } // PrepareNew the zettel for further modification. func (*CreateZettel) PrepareNew(origZettel domain.Zettel) domain.Zettel { m := meta.New(id.Invalid) om := origZettel.Meta m.SetNonEmpty(api.KeyTitle, om.GetDefault(api.KeyTitle, "")) m.SetNonEmpty(api.KeyRole, om.GetDefault(api.KeyRole, "")) m.SetNonEmpty(api.KeyTags, om.GetDefault(api.KeyTags, "")) m.SetNonEmpty(api.KeySyntax, om.GetDefault(api.KeySyntax, "")) const prefixLen = len(meta.NewPrefix) for _, pair := range om.PairsRest() { if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix { m.Set(key[prefixLen:], pair.Value) } } content := origZettel.Content content.TrimSpace() return domain.Zettel{Meta: m, Content: content} } func prependTitle(title, s0, s1 string) string { if len(title) > 0 { return s1 + title } return s0 } func copyReadonly(string) string { // TODO: Currently, "false" is a safe value. // // If the current user and its role is known, a more elaborative calculation // could be done: set it to a value, so that the current user will be able // to modify it later. return api.ValueFalse } // Run executes the use case. func (uc *CreateZettel) Run(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { m := zettel.Meta if m.Zid.IsValid() { return m.Zid, nil // TODO: new error: already exists } m.Set(api.KeyCreated, time.Now().Local().Format(id.ZidLayout)) m.Delete(api.KeyModified) m.YamlSep = uc.rtConfig.GetYAMLHeader() zettel.Content.TrimSpace() zid, err := uc.port.CreateZettel(ctx, zettel) uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Create zettel") return zid, err } |
Changes to usecase/delete_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | 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 import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/logger" ) // DeleteZettelPort is the interface used by this use case. type DeleteZettelPort interface { // DeleteZettel removes the zettel from the box. DeleteZettel(ctx context.Context, zid id.Zid) error } |
︙ | ︙ |
Changes to usecase/evaluate.go.
1 | //----------------------------------------------------------------------------- | | < < < > > > < < < | | | > | | | | > | > > > > < | < < < < | > > > > > > > > > > > > | | | | | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/query" ) // Evaluate is the data for this use case. type Evaluate struct { rtConfig config.Config getZettel GetZettel getMeta GetMeta listMeta ListMeta } // NewEvaluate creates a new use case. func NewEvaluate(rtConfig config.Config, getZettel GetZettel, getMeta GetMeta, listMeta ListMeta) Evaluate { return Evaluate{ rtConfig: rtConfig, getZettel: getZettel, getMeta: getMeta, listMeta: listMeta, } } // Run executes the use case. func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) { zettel, err := uc.getZettel.Run(ctx, zid) if err != nil { return nil, err } zn, err := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil if err != nil { return nil, err } evaluator.EvaluateZettel(ctx, uc, uc.rtConfig, zn) return zn, nil } // RunBlockNode executes the use case for a metadata list. func (uc *Evaluate) RunBlockNode(ctx context.Context, bn ast.BlockNode) ast.BlockSlice { if bn == nil { return nil } bns := ast.BlockSlice{bn} evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns) return bns } // RunMetadata executes the use case for a metadata value. func (uc *Evaluate) RunMetadata(ctx context.Context, value string) ast.InlineSlice { is := parser.ParseMetadata(value) evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is) return is } // RunMetadataNoLink executes the use case for a metadata value, but ignores link and footnote nodes. func (uc *Evaluate) RunMetadataNoLink(ctx context.Context, value string) ast.InlineSlice { is := parser.ParseMetadataNoLink(value) evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is) return is } // GetMeta retrieves the metadata of a given zettel identifier. func (uc *Evaluate) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { return uc.getMeta.Run(ctx, zid) } // GetZettel retrieves the full zettel of a given zettel identifier. func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { return uc.getZettel.Run(ctx, zid) } // SelectMeta returns a list of metadata that comply to the given selection criteria. func (uc *Evaluate) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { return uc.listMeta.Run(ctx, q) } |
Added usecase/get_all_meta.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // GetAllMetaPort is the interface used by this use case. type GetAllMetaPort interface { // GetAllMeta retrieves just the meta data of a specific zettel. GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) } // GetAllMeta is the data for this use case. type GetAllMeta struct { port GetAllMetaPort } // NewGetAllMeta creates a new use case. func NewGetAllMeta(port GetAllMetaPort) GetAllMeta { return GetAllMeta{port: port} } // Run executes the use case. func (uc GetAllMeta) Run(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { return uc.port.GetAllMeta(ctx, zid) } |
Deleted usecase/get_all_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added usecase/get_meta.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // GetMetaPort is the interface used by this use case. type GetMetaPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // GetMeta is the data for this use case. type GetMeta struct { port GetMetaPort } // NewGetMeta creates a new use case. func NewGetMeta(port GetMetaPort) GetMeta { return GetMeta{port: port} } // Run executes the use case. func (uc GetMeta) Run(ctx context.Context, zid id.Zid) (*meta.Meta, error) { return uc.port.GetMeta(ctx, zid) } |
Deleted usecase/get_special_zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to usecase/get_user.go.
1 | //----------------------------------------------------------------------------- | | < < < < | | | | | | | | | | | | | | < | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" ) // Use case: return user identified by meta key ident. // --------------------------------------------------- // GetUserPort is the interface used by this use case. type GetUserPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // GetUser is the data for this use case. type GetUser struct { authz auth.AuthzManager port GetUserPort } // NewGetUser creates a new use case. func NewGetUser(authz auth.AuthzManager, port GetUserPort) GetUser { return GetUser{authz: authz, port: port} } // Run executes the use case. func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) { ctx = box.NoEnrichContext(ctx) // It is important to try first with the owner. First, because another user // could give herself the same ''ident''. Second, in most cases the owner // will authenticate. identMeta, err := uc.port.GetMeta(ctx, uc.authz.Owner()) if err == nil && identMeta.GetDefault(api.KeyUserID, "") == ident { return identMeta, nil } // Owner was not found or has another ident. Try via list search. q := query.Parse(api.KeyUserID + api.SearchOperatorHas + ident + " " + api.SearchOperatorHas + ident) metaList, err := uc.port.SelectMeta(ctx, q) if err != nil { return nil, err } if len(metaList) < 1 { return nil, nil } return metaList[len(metaList)-1], nil } // Use case: return an user identified by zettel id and assert given ident value. // ------------------------------------------------------------------------------ // GetUserByZidPort is the interface used by this use case. type GetUserByZidPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // GetUserByZid is the data for this use case. type GetUserByZid struct { port GetUserByZidPort } // NewGetUserByZid creates a new use case. func NewGetUserByZid(port GetUserByZidPort) GetUserByZid { return GetUserByZid{port: port} } // GetUser executes the use case. func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { userMeta, err := uc.port.GetMeta(box.NoEnrichContext(ctx), zid) if err != nil { return nil, err } if val, ok := userMeta.Get(api.KeyUserID); !ok || val != ident { return nil, nil } return userMeta, nil } |
Changes to usecase/get_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | | 1 2 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 import ( "context" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" ) // GetZettelPort is the interface used by this use case. type GetZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) } // GetZettel is the data for this use case. type GetZettel struct { port GetZettelPort } // NewGetZettel creates a new use case. func NewGetZettel(port GetZettelPort) GetZettel { return GetZettel{port: port} } // Run executes the use case. func (uc GetZettel) Run(ctx context.Context, zid id.Zid) (domain.Zettel, error) { return uc.port.GetZettel(ctx, zid) } |
Changes to usecase/lists.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | > > > > > | > > > | > > > > > > > > > > > > > > | | > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/meta" "zettelstore.de/z/parser" "zettelstore.de/z/query" ) // ListMetaPort is the interface used by this use case. type ListMetaPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // ListMeta is the data for this use case. type ListMeta struct { port ListMetaPort } // NewListMeta creates a new use case. func NewListMeta(port ListMetaPort) ListMeta { return ListMeta{port: port} } // Run executes the use case. func (uc ListMeta) Run(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { return uc.port.SelectMeta(ctx, q) } // -------- List roles ------------------------------------------------------- // ListSyntaxPort is the interface used by this use case. type ListSyntaxPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // ListSyntax is the data for this use case. type ListSyntax struct { port ListSyntaxPort } // NewListSyntax creates a new use case. func NewListSyntax(port ListSyntaxPort) ListSyntax { return ListSyntax{port: port} } // Run executes the use case. func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) { q := query.Parse(api.KeySyntax + api.ExistOperator) // We look for all metadata with a syntax key metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), q) if err != nil { return nil, err } result := meta.CreateArrangement(metas, api.KeySyntax) for _, syn := range parser.GetSyntaxes() { if _, found := result[syn]; !found { delete(result, syn) } } return result, nil } // -------- List roles ------------------------------------------------------- // ListRolesPort is the interface used by this use case. type ListRolesPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // ListRoles is the data for this use case. type ListRoles struct { port ListRolesPort } // NewListRoles creates a new use case. func NewListRoles(port ListRolesPort) ListRoles { return ListRoles{port: port} } // Run executes the use case. func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) { q := query.Parse(api.KeyRole + api.ExistOperator) // We look for all metadata with an existing role key metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), q) if err != nil { return nil, err } return meta.CreateArrangement(metas, api.KeyRole), nil } |
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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/collect" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // ZettelOrderPort is the interface used by this use case. type ZettelOrderPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) } // ZettelOrder is the data for this use case. type ZettelOrder struct { port ZettelOrderPort evaluate Evaluate } // NewZettelOrder creates a new use case. func NewZettelOrder(port ZettelOrderPort, evaluate Evaluate) ZettelOrder { return ZettelOrder{port: port, evaluate: evaluate} } // Run executes the use case. func (uc ZettelOrder) Run(ctx context.Context, zid id.Zid, syntax string) ( start *meta.Meta, result []*meta.Meta, err error, ) { zn, err := uc.evaluate.Run(ctx, zid, syntax) if err != nil { return nil, nil, err } for _, ref := range collect.Order(zn) { if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil { if m, err3 := uc.port.GetMeta(ctx, collectedZid); err3 == nil { result = append(result, m) } } } return zn.Meta, result, nil } |
Changes to usecase/parse_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | 1 2 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 usecase import ( "context" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/parser" ) // ParseZettel is the data for this use case. type ParseZettel struct { rtConfig config.Config getZettel GetZettel } |
︙ | ︙ |
Deleted usecase/query.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to usecase/refresh.go.
1 | //----------------------------------------------------------------------------- | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" |
︙ | ︙ |
Deleted usecase/reindex.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to usecase/rename_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | > | > > | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/logger" ) // RenameZettelPort is the interface used by this use case. type RenameZettelPort interface { // GetMeta retrieves just the meta data of a specific zettel. GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) // Rename changes the current id to a new id. RenameZettel(ctx context.Context, curZid, newZid id.Zid) error } // RenameZettel is the data for this use case. type RenameZettel struct { log *logger.Logger port RenameZettelPort } // ErrZidInUse is returned if the zettel id is not appropriate for the box operation. type ErrZidInUse struct{ Zid id.Zid } func (err *ErrZidInUse) Error() string { return "Zettel id already in use: " + err.Zid.String() } // NewRenameZettel creates a new use case. func NewRenameZettel(log *logger.Logger, port RenameZettelPort) RenameZettel { return RenameZettel{log: log, port: port} } // Run executes the use case. func (uc *RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error { noEnrichCtx := box.NoEnrichContext(ctx) if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil { return err } if newZid == curZid { // Nothing to do return nil } if _, err := uc.port.GetMeta(noEnrichCtx, newZid); err == nil { return &ErrZidInUse{Zid: newZid} } err := uc.port.RenameZettel(ctx, curZid, newZid) uc.log.Info().User(ctx).Zid(curZid).Err(err).Zid(newZid).Msg("Rename zettel") return err } |
Added usecase/unlinked_refs.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 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "strings" "unicode" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/query" ) // UnlinkedReferencesPort is the interface used by this use case. type UnlinkedReferencesPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // UnlinkedReferences is the data for this use case. type UnlinkedReferences struct { port UnlinkedReferencesPort rtConfig config.Config encText *textenc.Encoder } // NewUnlinkedReferences creates a new use case. func NewUnlinkedReferences(port UnlinkedReferencesPort, rtConfig config.Config) UnlinkedReferences { return UnlinkedReferences{ port: port, rtConfig: rtConfig, encText: textenc.Create(), } } // Run executes the usecase with already evaluated title value. func (uc *UnlinkedReferences) Run(ctx context.Context, phrase string, q *query.Query) ([]*meta.Meta, error) { words := makeWords(phrase) if len(words) == 0 { return nil, nil } var sb strings.Builder for _, word := range words { sb.WriteString(" :") sb.WriteString(word) } q = q.Parse(sb.String()) // Limit applies to the filtering process, not to SelectMeta limit := q.GetLimit() q = q.SetLimit(0) candidates, err := uc.port.SelectMeta(ctx, q) if err != nil { return nil, err } q = q.SetLimit(limit) // Restore limit return q.Limit(uc.filterCandidates(ctx, candidates, words)), nil } func makeWords(text string) []string { return strings.FieldsFunc(text, func(r rune) bool { return unicode.In(r, unicode.C, unicode.P, unicode.Z) }) } func (uc *UnlinkedReferences) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta { result := make([]*meta.Meta, 0, len(candidates)) candLoop: for _, cand := range candidates { zettel, err := uc.port.GetZettel(ctx, cand.Zid) if err != nil { continue } v := unlinkedVisitor{ words: words, found: false, } v.text = v.joinWords(words) for _, pair := range zettel.Meta.Pairs() { if meta.Type(pair.Key) != meta.TypeZettelmarkup { continue } is := parser.ParseMetadata(pair.Value) evaluator.EvaluateInline(ctx, uc.port, uc.rtConfig, &is) ast.Walk(&v, &is) if v.found { result = append(result, cand) continue candLoop } } syntax := zettel.Meta.GetDefault(api.KeySyntax, "") if !parser.IsTextParser(syntax) { continue } zn, err := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil if err != nil { continue } evaluator.EvaluateZettel(ctx, uc.port, uc.rtConfig, zn) ast.Walk(&v, &zn.Ast) if v.found { result = append(result, cand) } } return result } func (*unlinkedVisitor) joinWords(words []string) string { return " " + strings.ToLower(strings.Join(words, " ")) + " " } type unlinkedVisitor struct { words []string text string found bool } func (v *unlinkedVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.InlineSlice: v.checkWords(n) return nil case *ast.HeadingNode: return nil case *ast.LinkNode, *ast.EmbedRefNode, *ast.EmbedBLOBNode, *ast.CiteNode: return nil } return v } func (v *unlinkedVisitor) checkWords(is *ast.InlineSlice) { if len(*is) < 2*len(v.words)-1 { return } for _, text := range v.splitInlineTextList(is) { if strings.Contains(text, v.text) { v.found = true } } } func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string { var result []string var curList []string for _, in := range *is { switch n := in.(type) { case *ast.TextNode: curList = append(curList, makeWords(n.Text)...) case *ast.SpaceNode: default: if curList != nil { result = append(result, v.joinWords(curList)) curList = nil } } } if curList != nil { result = append(result, v.joinWords(curList)) } return result } |
Changes to usecase/update_zettel.go.
1 | //----------------------------------------------------------------------------- | | < < < < | | | | | | | | | | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/logger" ) // 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) // UpdateZettel updates an existing zettel. UpdateZettel(ctx context.Context, zettel domain.Zettel) error } // UpdateZettel is the data for this use case. type UpdateZettel struct { log *logger.Logger port UpdateZettelPort } // NewUpdateZettel creates a new use case. func NewUpdateZettel(log *logger.Logger, port UpdateZettelPort) UpdateZettel { return UpdateZettel{log: log, port: port} } // Run executes the use case. func (uc *UpdateZettel) Run(ctx context.Context, zettel domain.Zettel, hasContent bool) error { m := zettel.Meta oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid) if err != nil { return err } if zettel.Equal(oldZettel, false) { return nil } // Update relevant computed, but stored values. if _, found := m.Get(api.KeyCreated); !found { if val, crFound := oldZettel.Meta.Get(api.KeyCreated); crFound { m.Set(api.KeyCreated, val) } } m.SetNow(api.KeyModified) m.YamlSep = oldZettel.Meta.YamlSep if m.Zid == id.ConfigurationZid { m.Set(api.KeySyntax, api.ValueSyntaxNone) } if !hasContent { zettel.Content = oldZettel.Content } zettel.Content.TrimSpace() err = uc.port.UpdateZettel(ctx, zettel) uc.log.Sense().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel") return err } |
Changes to usecase/usecase.go.
1 | //----------------------------------------------------------------------------- | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 | //----------------------------------------------------------------------------- // 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 |
Changes to usecase/version.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package usecase import ( "regexp" "strconv" |
︙ | ︙ |
Deleted web/adapter/adapter.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/api/api.go.
1 | //----------------------------------------------------------------------------- | | < < < | > < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "bytes" "context" "net/http" "time" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // API holds all data and methods for delivering API call results. type API struct { log *logger.Logger b server.Builder authz auth.AuthzManager |
︙ | ︙ | |||
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | rtConfig: rtConfig, policy: pol, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration), } return a } // NewURLBuilder creates a new URL builder object with the given key. func (a *API) NewURLBuilder(key byte) *api.URLBuilder { return a.b.NewURLBuilder(key) } func (a *API) getAuthData(ctx context.Context) *server.AuthData { return server.GetAuthData(ctx) } func (a *API) withAuth() bool { return a.authz.WithAuth() } func (a *API) getToken(ident *meta.Meta) ([]byte, error) { | > > > | | > > | > > > > > | 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 | rtConfig: rtConfig, policy: pol, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration), } return a } // GetURLPrefix returns the configured URL prefix of the web server. func (a *API) GetURLPrefix() string { return a.b.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. func (a *API) NewURLBuilder(key byte) *api.URLBuilder { return a.b.NewURLBuilder(key) } func (a *API) getAuthData(ctx context.Context) *server.AuthData { return server.GetAuthData(ctx) } func (a *API) withAuth() bool { return a.authz.WithAuth() } func (a *API) getToken(ident *meta.Meta) ([]byte, error) { return a.token.GetToken(ident, a.tokenLifetime, auth.KindJSON) } func (a *API) reportUsecaseError(w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { a.log.IfErr(err).Msg(text) http.Error(w, http.StatusText(code), code) return } // TODO: must call PrepareHeader somehow http.Error(w, text, code) } func writeBuffer(w http.ResponseWriter, buf *bytes.Buffer, contentType string) error { if buf.Len() == 0 { w.WriteHeader(http.StatusNoContent) return nil } adapter.PrepareHeader(w, contentType) w.WriteHeader(http.StatusOK) _, err := w.Write(buf.Bytes()) return err } func (a *API) getRights(ctx context.Context, m *meta.Meta) (result api.ZettelRights) { pol := a.policy user := server.GetUser(ctx) if pol.CanCreate(user, m) { result |= api.ZettelCanCreate |
︙ | ︙ |
Changes to web/adapter/api/command.go.
1 | //----------------------------------------------------------------------------- | | < < < | | 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) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "context" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/usecase" ) // MakePostCommandHandler creates a new HTTP handler to execute certain commands. func (a *API) MakePostCommandHandler( ucIsAuth *usecase.IsAuthenticated, ucRefresh *usecase.Refresh, |
︙ | ︙ |
Added web/adapter/api/content_type.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import "zettelstore.de/c/api" const ( ctHTML = "text/html; charset=utf-8" ctJSON = "application/json" ctPlainText = "text/plain; charset=utf-8" ctSVG = "image/svg+xml" ) var mapEncoding2CT = map[api.EncodingEnum]string{ api.EncoderHTML: ctHTML, api.EncoderSexpr: ctPlainText, api.EncoderText: ctPlainText, api.EncoderZJSON: ctJSON, api.EncoderZmk: ctPlainText, } func encoding2ContentType(enc api.EncodingEnum) string { if ct, ok := mapEncoding2CT[enc]; ok { return ct } return "application/octet-stream" } var mapSyntax2CT = map[string]string{ "css": "text/css; charset=utf-8", api.ValueSyntaxGif: "image/gif", api.ValueSyntaxHTML: "text/html; charset=utf-8", "jpeg": "image/jpeg", "jpg": "image/jpeg", "js": "text/javascript; charset=utf-8", "pdf": "application/pdf", "png": "image/png", api.ValueSyntaxSVG: ctSVG, "xml": "text/xml; charset=utf-8", api.ValueSyntaxZmk: "text/x-zmk; charset=utf-8", "plain": ctPlainText, api.ValueSyntaxText: ctPlainText, "markdown": "text/markdown; charset=utf-8", "md": "text/markdown; charset=utf-8", "mustache": ctPlainText, } func syntax2contentType(syntax string) (string, bool) { contentType, ok := mapSyntax2CT[syntax] return contentType, ok } |
Changes to web/adapter/api/create_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < > < < | | | < | | | | < < < < < | < | > > > | > > | > > > > > > > > > > > > > > > < | < < < < | | < < | < > | < | | | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakePostCreatePlainZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (a *API) MakePostCreatePlainZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zettel, err := buildZettelFromPlainData(r, id.Invalid) if err != nil { a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } newZid, err := createZettel.Run(ctx, zettel) if err != nil { a.reportUsecaseError(w, err) return } u := a.NewURLBuilder('z').SetZid(api.ZettelID(newZid.String())).String() h := adapter.PrepareHeader(w, ctPlainText) h.Set(api.HeaderLocation, u) w.WriteHeader(http.StatusCreated) _, err = w.Write(newZid.Bytes()) a.log.IfErr(err).Zid(newZid).Msg("Create Plain Zettel") } } // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zettel, err := buildZettelFromJSONData(r, id.Invalid) if err != nil { a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } newZid, err := createZettel.Run(ctx, zettel) if err != nil { a.reportUsecaseError(w, err) return } var buf bytes.Buffer err = encodeJSONData(&buf, api.ZidJSON{ID: api.ZettelID(newZid.String())}) if err != nil { a.log.Fatal().Err(err).Zid(newZid).Msg("Unable to store new Zid in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } h := adapter.PrepareHeader(w, ctJSON) h.Set(api.HeaderLocation, a.NewURLBuilder('j').SetZid(api.ZettelID(newZid.String())).String()) w.WriteHeader(http.StatusCreated) _, err = w.Write(buf.Bytes()) a.log.IfErr(err).Zid(newZid).Msg("Create JSON Zettel") } } |
Changes to web/adapter/api/delete_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | 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) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "net/http" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" ) // MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { |
︙ | ︙ |
Changes to web/adapter/api/get_data.go.
1 | //----------------------------------------------------------------------------- | | < < < > < | | | | | | | | < > > > | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/usecase" ) // MakeGetDataHandler creates a new HTTP handler to return zettelstore data. func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { version := ucVersion.Run() result := api.VersionJSON{ Major: version.Major, Minor: version.Minor, Patch: version.Patch, Info: version.Info, Hash: version.Hash, } var buf bytes.Buffer err := encodeJSONData(&buf, result) if err != nil { a.log.Fatal().Err(err).Msg("Unable to version info in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = writeBuffer(w, &buf, ctJSON) a.log.IfErr(err).Msg("Write Version Info") } } |
Added web/adapter/api/get_eval_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "net/http" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/id" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" ) // MakeGetEvalZettelHandler creates a new HTTP handler to return a evaluated zettel. func (a *API) MakeGetEvalZettelHandler(evaluate usecase.Evaluate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() enc, encStr := getEncoding(r, q, encoder.GetDefaultEncoding()) part := getPart(q, partContent) zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax)) if err != nil { a.reportUsecaseError(w, err) return } evalMeta := func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) } a.writeEncodedZettelPart(w, zn, evalMeta, enc, encStr, part) } } |
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 40 41 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "net/http" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" ) // MakeGetOrderHandler creates a new API handler to return zettel references // of a given zettel. func (a *API) MakeGetOrderHandler(zettelOrder usecase.ZettelOrder) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() q := r.URL.Query() start, metas, err := zettelOrder.Run(ctx, zid, q.Get(api.KeySyntax)) if err != nil { a.reportUsecaseError(w, err) return } err = a.writeMetaList(ctx, w, start, metas) a.log.IfErr(err).Zid(zid).Msg("Write Zettel Order") } } |
Added web/adapter/api/get_parsed_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "bytes" "fmt" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/id" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetParsedZettelHandler creates a new HTTP handler to return a parsed zettel. func (a *API) MakeGetParsedZettelHandler(parseZettel usecase.ParseZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() enc, encStr := getEncoding(r, q, encoder.GetDefaultEncoding()) part := getPart(q, partContent) zn, err := parseZettel.Run(r.Context(), zid, q.Get(api.KeySyntax)) if err != nil { a.reportUsecaseError(w, err) return } a.writeEncodedZettelPart(w, zn, parser.ParseMetadata, enc, encStr, part) } } func (a *API) writeEncodedZettelPart( w http.ResponseWriter, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc, enc api.EncodingEnum, encStr string, part partType, ) { encdr := encoder.Create(enc) if encdr == nil { adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid, encStr)) return } var err error var buf bytes.Buffer switch part { case partZettel: _, err = encdr.WriteZettel(&buf, zn, evalMeta) case partMeta: _, err = encdr.WriteMeta(&buf, zn.InhMeta, evalMeta) case partContent: _, err = encdr.WriteContent(&buf, zn) } if err != nil { a.log.Fatal().Err(err).Zid(zn.Zid).Msg("Unable to store data in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if buf.Len() == 0 { w.WriteHeader(http.StatusNoContent) return } err = writeBuffer(w, &buf, encoding2ContentType(enc)) a.log.IfErr(err).Zid(zn.Zid).Msg("Write Encoded Zettel") } |
Added web/adapter/api/get_unlinked_refs.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "net/http" "strings" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListUnlinkedMetaHandler creates a new HTTP handler for the use case "list unlinked references". func (a *API) MakeListUnlinkedMetaHandler( getMeta usecase.GetMeta, unlinkedRefs usecase.UnlinkedReferences, evaluate *usecase.Evaluate, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() zm, err := getMeta.Run(ctx, zid) if err != nil { a.reportUsecaseError(w, err) return } que := r.URL.Query() phrase := que.Get(api.QueryKeyPhrase) if phrase == "" { if zmkTitle, found := zm.Get(api.KeyTitle); found { isTitle := evaluate.RunMetadata(ctx, zmkTitle) encdr := textenc.Create() var b strings.Builder _, err = encdr.WriteInlines(&b, &isTitle) if err == nil { phrase = b.String() } } } metaList, err := unlinkedRefs.Run(ctx, phrase, adapter.AddUnlinkedRefsToQuery(adapter.GetQuery(que), zm)) if err != nil { a.reportUsecaseError(w, err) return } result := api.ZidMetaRelatedList{ ID: api.ZettelID(zid.String()), Meta: zm.Map(), Rights: a.getRights(ctx, zm), List: make([]api.ZidMetaJSON, 0, len(metaList)), } for _, m := range metaList { result.List = append(result.List, api.ZidMetaJSON{ ID: api.ZettelID(m.Zid.String()), Meta: m.Map(), Rights: a.getRights(ctx, m), }) } var buf bytes.Buffer err = encodeJSONData(&buf, result) if err != nil { a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store unlinked references in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = writeBuffer(w, &buf, ctJSON) a.log.IfErr(err).Zid(zid).Msg("Write Unlinked References") } } |
Changes to web/adapter/api/get_zettel.go.
1 | //----------------------------------------------------------------------------- | | < < < > < < < < | | | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > | > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > | > > > > | > > > | | < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | < < < < < < < < < | < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | < < < | < | | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "bytes" "context" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" ) // MakeGetZettelHandler creates a new HTTP handler to return a zettel. func (a *API) MakeGetZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() z, err := a.getZettelFromPath(ctx, w, r, getZettel) if err != nil { return } m := z.Meta var buf bytes.Buffer content, encoding := z.Content.Encode() err = encodeJSONData(&buf, api.ZettelJSON{ ID: api.ZettelID(m.Zid.String()), Meta: m.Map(), Encoding: encoding, Content: content, Rights: a.getRights(ctx, m), }) if err != nil { a.log.Fatal().Err(err).Zid(m.Zid).Msg("Unable to store zettel in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = writeBuffer(w, &buf, ctJSON) a.log.IfErr(err).Zid(m.Zid).Msg("Write JSON Zettel") } } // MakeGetPlainZettelHandler creates a new HTTP handler to return a zettel in plain formar func (a *API) MakeGetPlainZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { z, err := a.getZettelFromPath(box.NoEnrichContext(r.Context()), w, r, getZettel) if err != nil { return } var buf bytes.Buffer var contentType string switch getPart(r.URL.Query(), partContent) { case partZettel: _, err = z.Meta.Write(&buf) if err == nil { err = buf.WriteByte('\n') } if err == nil { _, err = z.Content.Write(&buf) } case partMeta: contentType = ctPlainText _, err = z.Meta.Write(&buf) case partContent: if ct, ok := syntax2contentType(z.Meta.GetDefault(api.KeySyntax, "")); ok { contentType = ct } _, err = z.Content.Write(&buf) } if err != nil { a.log.Fatal().Err(err).Zid(z.Meta.Zid).Msg("Unable to store plain zettel/part in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = writeBuffer(w, &buf, contentType) a.log.IfErr(err).Zid(z.Meta.Zid).Msg("Write Plain Zettel") } } func (a *API) getZettelFromPath(ctx context.Context, w http.ResponseWriter, r *http.Request, getZettel usecase.GetZettel) (domain.Zettel, error) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return domain.Zettel{}, err } z, err := getZettel.Run(ctx, zid) if err != nil { a.reportUsecaseError(w, err) return domain.Zettel{}, err } return z, nil } // MakeGetMetaHandler creates a new HTTP handler to return metadata of a zettel. func (a *API) MakeGetMetaHandler(getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } ctx := r.Context() m, err := getMeta.Run(ctx, zid) if err != nil { a.reportUsecaseError(w, err) return } var buf bytes.Buffer err = encodeJSONData(&buf, api.MetaJSON{ Meta: m.Map(), Rights: a.getRights(ctx, m), }) if err != nil { a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store metadata in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = writeBuffer(w, &buf, ctJSON) a.log.IfErr(err).Zid(zid).Msg("Write JSON Meta") } } |
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 49 50 51 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "net/http" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func (a *API) MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } q := r.URL.Query() dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir)) depth, ok := adapter.GetInteger(q, api.QueryKeyDepth) if !ok || depth < 0 { depth = 5 } limit, ok := adapter.GetInteger(q, api.QueryKeyLimit) if !ok || limit < 0 { limit = 200 } ctx := r.Context() metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { a.reportUsecaseError(w, err) return } err = a.writeMetaList(ctx, w, metaList[0], metaList[1:]) a.log.IfErr(err).Zid(zid).Msg("Write Context") } } |
Added web/adapter/api/get_zettel_list.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "fmt" "net/http" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListPlainHandler creates a new HTTP handler for the use case "list some zettel". func (a *API) MakeListPlainHandler(listMeta usecase.ListMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() metaList, err := listMeta.Run(ctx, adapter.GetQuery(r.URL.Query())) if err != nil { a.reportUsecaseError(w, err) return } var buf bytes.Buffer for _, m := range metaList { _, err = fmt.Fprintln(&buf, m.Zid.String(), m.GetTitle()) if err != nil { a.log.Fatal().Err(err).Msg("Unable to store plain list in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } err = writeBuffer(w, &buf, ctPlainText) a.log.IfErr(err).Msg("Write Plain List") } } |
Added web/adapter/api/json.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "bytes" "context" "encoding/json" "io" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) func encodeJSONData(w io.Writer, data interface{}) error { enc := json.NewEncoder(w) enc.SetEscapeHTML(false) return enc.Encode(data) } func (a *API) writeMetaList(ctx context.Context, w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error { outList := make([]api.ZidMetaJSON, len(metaList)) for i, m := range metaList { outList[i].ID = api.ZettelID(m.Zid.String()) outList[i].Meta = m.Map() outList[i].Rights = a.getRights(ctx, m) } var buf bytes.Buffer err := encodeJSONData(&buf, api.ZidMetaRelatedList{ ID: api.ZettelID(m.Zid.String()), Meta: m.Map(), Rights: a.getRights(ctx, m), List: outList, }) if err != nil { a.log.Fatal().Err(err).Zid(m.Zid).Msg("Unable to store meta list in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return nil } return writeBuffer(w, &buf, ctJSON) } func buildZettelFromJSONData(r *http.Request, zid id.Zid) (domain.Zettel, error) { var zettel domain.Zettel dec := json.NewDecoder(r.Body) var zettelData api.ZettelDataJSON if err := dec.Decode(&zettelData); err != nil { return zettel, err } m := meta.New(zid) for k, v := range zettelData.Meta { m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v)) } zettel.Meta = m if err := zettel.Content.SetDecoded(zettelData.Content, zettelData.Encoding); err != nil { return zettel, err } return zettel, nil } |
Changes to web/adapter/api/login.go.
1 | //----------------------------------------------------------------------------- | | < < < > > | < | | < | | | < > > > > > > > > > > > > > > > > > | | < | | < | | | < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "encoding/json" "net/http" "time" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !a.withAuth() { err := a.writeJSONToken(w, "freeaccess", 24*366*10*time.Hour) a.log.IfErr(err).Msg("Login/free") return } var token []byte if ident, cred := retrieveIdentCred(r); ident != "" { var err error token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindJSON) if err != nil { a.reportUsecaseError(w, err) return } } if len(token) == 0 { w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } err := a.writeJSONToken(w, string(token), a.tokenLifetime) a.log.IfErr(err).Msg("Login") } } func retrieveIdentCred(r *http.Request) (string, string) { if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok { return ident, cred } if ident, cred, ok := r.BasicAuth(); ok { return ident, cred } return "", "" } func (a *API) writeJSONToken(w http.ResponseWriter, token string, lifetime time.Duration) error { var buf bytes.Buffer je := json.NewEncoder(&buf) err := je.Encode(api.AuthJSON{ Token: token, Type: "Bearer", Expires: int(lifetime / time.Second), }) if err != nil { a.log.Fatal().Err(err).Msg("Unable to store token in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return nil } return writeBuffer(w, &buf, ctJSON) } // MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user. func (a *API) MakeRenewAuthHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if !a.withAuth() { err := a.writeJSONToken(w, "freeaccess", 24*366*10*time.Hour) a.log.IfErr(err).Msg("Refresh/free") return } authData := a.getAuthData(ctx) if authData == nil || len(authData.Token) == 0 || authData.User == nil { adapter.BadRequest(w, "Not authenticated") return } totalLifetime := authData.Expires.Sub(authData.Issued) currentLifetime := authData.Now.Sub(authData.Issued) // If we are in the first quarter of the tokens lifetime, return the token if currentLifetime*4 < totalLifetime { err := a.writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime) a.log.IfErr(err).Msg("Write old token") return } // Token is a little bit aged. Create a new one token, err := a.getToken(authData.User) if err != nil { a.reportUsecaseError(w, err) return } err = a.writeJSONToken(w, string(token), a.tokenLifetime) a.log.IfErr(err).Msg("Write renewed token") } } |
Changes to web/adapter/api/query.go.
1 | //----------------------------------------------------------------------------- | | < < < | < < < < | | | | | < | | < < | < < | > | < < < < < < < < < < | < < < < | < < < < | < < < < | < | < < < < < < < < < < | | > > > > | | | < < < < | | | | | < < < | > | | < | < < < < < | | < < | | < < < < | < | < < < | | < < < | < < < | | | < > > | | < > > | < < > | > | < < < < < < < < | < < | < < < < < < < < | < < < | < < < < < < < < < < < < | | < < < < < < | < < < < < < | < < < < < < < < < < < < | < | < < < | > | < < < < | | < < < | < < | | < < < | < | < < < | < < < | | < < < < < < < < < < < | < < < < < < < < | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "bytes" "context" "io" "net/http" "strconv" "strings" "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeQueryHandler creates a new HTTP handler to perform a query. func (a *API) MakeQueryHandler(listMeta usecase.ListMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := adapter.GetQuery(r.URL.Query()) metaList, err := listMeta.Run(ctx, q) if err != nil { a.reportUsecaseError(w, err) return } var buf bytes.Buffer contentType, err := a.queryAction(ctx, &buf, q, metaList) if err != nil { a.reportUsecaseError(w, err) return } err = writeBuffer(w, &buf, contentType) a.log.IfErr(err).Msg("write action") } } func (a *API) queryAction(ctx context.Context, w io.Writer, q *query.Query, ml []*meta.Meta) (string, error) { ap := actionPara{ w: w, q: q, ml: ml, min: -1, max: -1, } if actions := q.Actions(); len(actions) > 0 { acts := make([]string, 0, len(actions)) for _, act := range actions { if strings.HasPrefix(act, "MIN") { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.min = num continue } } if strings.HasPrefix(act, "MAX") { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.max = num continue } } acts = append(acts, act) } for _, act := range acts { key := strings.ToLower(act) switch meta.Type(key) { case meta.TypeWord, meta.TypeTagSet: return ap.createMapMeta(key) } } } err := a.writeQueryMetaList(ctx, w, q, ml) return ctJSON, err } type actionPara struct { w io.Writer q *query.Query ml []*meta.Meta min int max int } func (ap *actionPara) createMapMeta(key string) (string, error) { if len(ap.ml) == 0 { return "", nil } arr := meta.CreateArrangement(ap.ml, key) if len(arr) == 0 { return "", nil } min, max := ap.min, ap.max mm := make(api.MapMeta, len(arr)) for tag, metaList := range arr { if len(metaList) < min || (max > 0 && len(metaList) > max) { continue } zidList := make([]api.ZettelID, 0, len(metaList)) for _, m := range metaList { zidList = append(zidList, api.ZettelID(m.Zid.String())) } mm[tag] = zidList } err := encodeJSONData(ap.w, api.MapListJSON{Map: mm}) return ctJSON, err } func (a *API) writeQueryMetaList(ctx context.Context, w io.Writer, q *query.Query, ml []*meta.Meta) error { result := make([]api.ZidMetaJSON, 0, len(ml)) for _, m := range ml { result = append(result, api.ZidMetaJSON{ ID: api.ZettelID(m.Zid.String()), Meta: m.Map(), Rights: a.getRights(ctx, m), }) } err := encodeJSONData(w, api.ZettelListJSON{ Query: q.String(), Human: q.Human(), List: result, }) return err } |
Changes to web/adapter/api/rename_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //----------------------------------------------------------------------------- // Copyright (c) 2021 Detlef Stern // // This file is part of zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "net/http" "net/url" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" ) // MakeRenameZettelHandler creates a new HTTP handler to update a zettel. func (a *API) MakeRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { |
︙ | ︙ |
Changes to web/adapter/api/request.go.
1 | //----------------------------------------------------------------------------- | | < < < < | < | | | | | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( "io" "net/http" "net/url" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) // getEncoding returns the data encoding selected by the caller. func getEncoding(r *http.Request, q url.Values, defEncoding api.EncodingEnum) (api.EncodingEnum, string) { encoding := q.Get(api.QueryKeyEncoding) if encoding != "" { return api.Encoder(encoding), encoding } if enc, ok := getOneEncoding(r, api.HeaderAccept); ok { return api.Encoder(enc), enc } if enc, ok := getOneEncoding(r, api.HeaderContentType); ok { return api.Encoder(enc), enc } return defEncoding, defEncoding.String() } func getOneEncoding(r *http.Request, key string) (string, bool) { if values, ok := r.Header[key]; ok { for _, value := range values { if enc, ok2 := contentType2encoding(value); ok2 { return enc, true } } } return "", false } var mapCT2encoding = map[string]string{ "application/json": "json", "text/html": api.EncodingHTML, } func contentType2encoding(contentType string) (string, bool) { // TODO: only check before first ';' enc, ok := mapCT2encoding[contentType] return enc, ok } |
︙ | ︙ | |||
100 101 102 103 104 105 106 | func (p partType) DefString(defPart partType) string { if p == defPart { return "" } return p.String() } | | < | | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | func (p partType) DefString(defPart partType) string { if p == defPart { return "" } return p.String() } func buildZettelFromPlainData(r *http.Request, zid id.Zid) (domain.Zettel, error) { b, err := io.ReadAll(r.Body) if err != nil { return domain.Zettel{}, err } inp := input.NewInput(b) m := meta.NewFromInput(zid, inp) return domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, nil } |
Deleted web/adapter/api/response.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/api/update_zettel.go.
1 | //----------------------------------------------------------------------------- | | | < < < < | | < | > > > > > > > > > > > > > > > > > > > > > < < < < < < < | < < < < < | 1 2 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 api import ( "net/http" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeUpdatePlainZettelHandler creates a new HTTP handler to update a zettel. func (a *API) MakeUpdatePlainZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } zettel, err := buildZettelFromPlainData(r, zid) if err != nil { a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } if err = updateZettel.Run(r.Context(), zettel, true); err != nil { a.reportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) } } // MakeUpdateZettelHandler creates a new HTTP handler to update a zettel. func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } zettel, err := buildZettelFromJSONData(r, zid) if err != nil { a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error())) return } if err = updateZettel.Run(r.Context(), zettel, true); err != nil { a.reportUsecaseError(w, err) return |
︙ | ︙ |
Changes to web/adapter/errors.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > > > > | | > > | < | 1 2 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 adapter provides handlers for web requests. package adapter import "net/http" // BadRequest signals HTTP status code 400. func BadRequest(w http.ResponseWriter, text string) { http.Error(w, text, http.StatusBadRequest) } // Forbidden signals HTTP status code 403. func Forbidden(w http.ResponseWriter, text string) { http.Error(w, text, http.StatusForbidden) } // NotFound signals HTTP status code 404. func NotFound(w http.ResponseWriter, text string) { http.Error(w, text, http.StatusNotFound) } |
Changes to web/adapter/request.go.
1 | //----------------------------------------------------------------------------- | | < < < | > > > > > > > > > > > > > | | > > | > > | > > > > > > > | > > > > > > > > > > > > > > | < > > > > > | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package adapter import ( "net/http" "net/url" "strconv" "strings" "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/query" "zettelstore.de/z/usecase" ) // GetCredentialsViaForm retrieves the authentication credentions from a form. func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { err := r.ParseForm() if err != nil { kernel.Main.GetLogger(kernel.WebService).Info().Err(err).Msg("Unable to parse form") return "", "", false } ident = strings.TrimSpace(r.PostFormValue("username")) cred = r.PostFormValue("password") if ident == "" { return "", "", false } return ident, cred, true } // 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 } // GetQuery retrieves the specified options from a query. func GetQuery(vals url.Values) *query.Query { if exprs, found := vals[api.QueryKeyQuery]; found { return query.Parse(strings.Join(exprs, " ")) } return nil } // GetZCDirection returns a direction value for a given string. func GetZCDirection(s string) usecase.ZettelContextDirection { switch s { case api.DirBackward: return usecase.ZettelContextBackward case api.DirForward: return usecase.ZettelContextForward } return usecase.ZettelContextBoth } // AddUnlinkedRefsToQuery inspects metadata and enhances the given query to ignore // some zettel identifier. func AddUnlinkedRefsToQuery(q *query.Query, m *meta.Meta) *query.Query { var sb strings.Builder sb.WriteString(api.KeyID) sb.WriteString("!:") sb.WriteString(m.Zid.String()) for _, pair := range m.ComputedPairsRest() { switch meta.Type(pair.Key) { case meta.TypeID: sb.WriteByte(' ') sb.WriteString(api.KeyID) sb.WriteString("!:") sb.WriteString(pair.Value) case meta.TypeIDSet: for _, value := range meta.ListFromValue(pair.Value) { sb.WriteByte(' ') sb.WriteString(api.KeyID) sb.WriteString("!:") sb.WriteString(value) } } } return q.Parse(sb.String()) } |
Changes to web/adapter/response.go.
1 | //----------------------------------------------------------------------------- | | < < < < | < < < < < < < < < < < < | | | < | | < < | | < | | < | < < < < < < < < | < | < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package adapter import ( "errors" "fmt" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/usecase" ) // PrepareHeader sets the HTTP header to defined values. func PrepareHeader(w http.ResponseWriter, contentType string) http.Header { h := w.Header() if contentType != "" { h.Set(api.HeaderContentType, contentType) } return h } // ErrBadRequest is returned if the caller made an invalid HTTP request. type ErrBadRequest struct { Text string } // NewErrBadRequest creates an new bad request error. func NewErrBadRequest(text string) error { return &ErrBadRequest{Text: text} } func (err *ErrBadRequest) Error() string { return err.Text } // CodeMessageFromError returns an appropriate HTTP status code and text from a given error. func CodeMessageFromError(err error) (int, string) { if err == box.ErrNotFound { return http.StatusNotFound, http.StatusText(http.StatusNotFound) } if err1, ok := err.(*box.ErrNotAllowed); ok { return http.StatusForbidden, err1.Error() } if err1, ok := err.(*box.ErrInvalidID); ok { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", err1.Zid) } if err1, ok := err.(*usecase.ErrZidInUse); ok { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", err1.Zid) } if err1, ok := err.(*ErrBadRequest); ok { return http.StatusBadRequest, err1.Text } if errors.Is(err, box.ErrStopped) { return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err) } if errors.Is(err, box.ErrConflict) { return http.StatusConflict, "Zettelstore operations conflicted" } if errors.Is(err, box.ErrCapacity) { return http.StatusInsufficientStorage, "Zettelstore reached one of its storage limits" } return http.StatusInternalServerError, err.Error() } |
Changes to web/adapter/webui/const.go.
1 | //----------------------------------------------------------------------------- | | < < < | < < | | > | | | > | > | < | < < > | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed 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 // WebUI related constants. const queryKeyAction = "action" // Values for queryKeyAction const ( valueActionCopy = "copy" valueActionFolge = "folge" valueActionNew = "new" valueActionVersion = "version" ) // Enumeration for queryKeyAction type createAction uint8 const ( actionCopy createAction = iota actionFolge actionNew actionVersion ) func getCreateAction(s string) createAction { switch s { case valueActionCopy: return actionCopy case valueActionFolge: return actionFolge case valueActionNew: return actionNew case valueActionVersion: return actionVersion default: return actionCopy } } |
Changes to web/adapter/webui/create_zettel.go.
1 | //----------------------------------------------------------------------------- | | < < < < < < | | | > < < < < | | | | | | | | | | > > > > > > > > > > | < < | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "context" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // MakeGetCreateZettelHandler creates a new HTTP handler to display the // HTML edit view for the various zettel creation methods. func (wui *WebUI) MakeGetCreateZettelHandler( getZettel usecase.GetZettel, createZettel *usecase.CreateZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() op := getCreateAction(q.Get(queryKeyAction)) zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) switch op { case actionCopy: wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "Copy Zettel", roleData, syntaxData) case actionVersion: wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "Versionzettel", roleData, syntaxData) case actionFolge: wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "Folgezettel", roleData, syntaxData) case actionNew: m := origZettel.Meta title := parser.ParseMetadata(m.GetTitle()) textTitle, err2 := encodeInlinesText(&title, wui.gentext) if err2 != nil { wui.reportError(ctx, w, err2) return } htmlTitle, err2 := wui.getSimpleHTMLEncoder().InlinesString(&title) if err2 != nil { wui.reportError(ctx, w, err2) return } wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel), textTitle, htmlTitle, roleData, syntaxData) } } } func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) { roleData := dataListFromArrangement(ucListRoles.Run(ctx)) syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx)) |
︙ | ︙ | |||
86 87 88 89 90 91 92 | } return nil } func (wui *WebUI) renderZettelForm( ctx context.Context, w http.ResponseWriter, | | | < | | < < < < < < < | > | < > > > | > | < > > > | | | < < < < < < < | | | | < < | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 | } return nil } func (wui *WebUI) renderZettelForm( ctx context.Context, w http.ResponseWriter, zettel domain.Zettel, title, heading string, roleData []string, syntaxData []string, ) { user := server.GetUser(ctx) m := zettel.Meta var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), title, "", user, &base) wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ Heading: heading, MetaTitle: m.GetDefault(api.KeyTitle, ""), MetaTags: m.GetDefault(api.KeyTags, ""), MetaRole: m.GetDefault(api.KeyRole, ""), HasRoleData: len(roleData) > 0, RoleData: roleData, HasSyntaxData: len(syntaxData) > 0, SyntaxData: syntaxData, MetaSyntax: m.GetDefault(api.KeySyntax, ""), MetaPairsRest: m.PairsRest(), IsTextContent: !zettel.Content.IsBinary(), Content: zettel.Content.AsString(), }) } // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reEdit, zettel, hasContent, err := parseZettelForm(r, id.Invalid) if err != nil { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read form data")) return } if !hasContent { wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing")) return } newZid, err := createZettel.Run(ctx, zettel) if err != nil { wui.reportError(ctx, w, err) return } if reEdit { wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(api.ZettelID(newZid.String()))) } else { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String()))) } } } |
Changes to web/adapter/webui/delete_zettel.go.
1 | //----------------------------------------------------------------------------- | | < < < < | | > > < < | > > > > < | | | | > > > > > > > > > > > > > > > > > > > > | < < | > | > | | | > > | < < | > | > > > | | < < < | > > > > > > > > > > > > > | | < < < < < < < < < < < < < < < < < < < | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "net/http" "zettelstore.de/c/api" "zettelstore.de/c/maps" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. func (wui *WebUI) MakeGetDeleteZettelHandler( getMeta usecase.GetMeta, getAllMeta usecase.GetAllMeta, evaluate *usecase.Evaluate, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } ms, err := getAllMeta.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } m := ms[0] var shadowedBox string var incomingLinks simpleLinks if len(ms) > 1 { shadowedBox = ms[1].GetDefault(api.KeyBoxNumber, "???") } else { getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), createEvalMetadataFunc(ctx, evaluate)) incomingLinks = wui.encodeIncoming(m, getTextTitle) } uselessFiles := retrieveUselessFiles(m) user := server.GetUser(ctx) var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), "Delete Zettel "+m.Zid.String(), "", user, &base) wui.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct { Zid string MetaPairs []meta.Pair HasShadows bool ShadowedBox string Incoming simpleLinks HasUselessFiles bool UselessFiles []string }{ Zid: zid.String(), MetaPairs: m.ComputedPairs(), HasShadows: shadowedBox != "", ShadowedBox: shadowedBox, Incoming: incomingLinks, HasUselessFiles: len(uselessFiles) > 0, UselessFiles: uselessFiles, }) } } func retrieveUselessFiles(m *meta.Meta) []string { if val, found := m.Get(api.KeyUselessFiles); found { return []string{val} } return nil } // MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } if err = deleteZettel.Run(r.Context(), zid); err != nil { wui.reportError(ctx, w, err) return } wui.redirectFound(w, r, wui.NewURLBuilder('/')) } } func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) simpleLinks { zidMap := make(strfun.Set) addListValues(zidMap, m, api.KeyBackward) for _, kd := range meta.GetSortedKeyDescriptions() { inverseKey := kd.Inverse if inverseKey == "" { continue } ikd := meta.GetDescription(inverseKey) switch ikd.Type { case meta.TypeID: if val, ok := m.Get(inverseKey); ok { zidMap.Set(val) } case meta.TypeIDSet: addListValues(zidMap, m, inverseKey) } } return createSimpleLinks(wui.encodeZidLinks(maps.Keys(zidMap), getTextTitle)) } func addListValues(zidMap strfun.Set, m *meta.Meta, key string) { if values, ok := m.GetList(key); ok { for _, val := range values { zidMap.Set(val) } } } |
Changes to web/adapter/webui/edit_zettel.go.
1 | //----------------------------------------------------------------------------- | | < < < > | | | < | | | < | | | < < < < | | | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "net/http" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeEditGetZettelHandler creates a new HTTP handler to display the // HTML edit view of a zettel. func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { wui.reportError(ctx, w, err) return } roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "Edit Zettel", roleData, syntaxData) } } // MakeEditSetZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } reEdit, zettel, hasContent, err := parseZettelForm(r, zid) if err != nil { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read zettel form")) return } if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil { wui.reportError(ctx, w, err) return } if reEdit { wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(api.ZettelID(zid.String()))) } else { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String()))) } } } |
Deleted web/adapter/webui/favicon.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/webui/forms.go.
1 | //----------------------------------------------------------------------------- | | < < < < < < < < | | | < | | > > > > > > > > > > > > > > > < < | < | | | < < < | | | < | > > > < | < | < | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "bytes" "net/http" "regexp" "strings" "zettelstore.de/c/api" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/input" ) type formZettelData struct { Heading string MetaTitle string MetaRole string HasRoleData bool RoleData []string HasSyntaxData bool SyntaxData []string MetaTags string MetaSyntax string MetaPairsRest []meta.Pair IsTextContent bool Content string } var ( bsCRLF = []byte{'\r', '\n'} bsLF = []byte{'\n'} ) func parseZettelForm(r *http.Request, zid id.Zid) (bool, domain.Zettel, bool, error) { err := r.ParseForm() if err != nil { return false, domain.Zettel{}, false, err } _, doSave := r.Form["save"] var m *meta.Meta if postMeta, ok := trimmedFormValue(r, "meta"); ok { m = meta.NewFromInput(zid, input.NewInput(removeEmptyLines([]byte(postMeta)))) m.Sanitize() } else { m = meta.New(zid) } if postTitle, ok := trimmedFormValue(r, "title"); ok { m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle)) } if postTags, ok := trimmedFormValue(r, "tags"); ok { if tags := strings.Fields(meta.RemoveNonGraphic(postTags)); len(tags) > 0 { m.SetList(api.KeyTags, tags) } } if postRole, ok := trimmedFormValue(r, "role"); ok { m.Set(api.KeyRole, meta.RemoveNonGraphic(postRole)) } if postSyntax, ok := trimmedFormValue(r, "syntax"); ok { m.Set(api.KeySyntax, meta.RemoveNonGraphic(postSyntax)) } if values, ok := r.PostForm["content"]; ok && len(values) > 0 { return doSave, domain.Zettel{ Meta: m, Content: domain.NewContent(bytes.ReplaceAll([]byte(values[0]), bsCRLF, bsLF)), }, true, nil } return doSave, domain.Zettel{ Meta: m, Content: domain.NewContent(nil), }, false, nil } func trimmedFormValue(r *http.Request, key string) (string, bool) { if values, ok := r.PostForm[key]; ok && len(values) > 0 { value := strings.TrimSpace(values[0]) if len(value) > 0 { return value, true } } return "", false } var reEmptyLines = regexp.MustCompile(`(\n|\r)+\s*(\n|\r)+`) func removeEmptyLines(s []byte) []byte { b := bytes.TrimSpace(s) return reEmptyLines.ReplaceAllLiteral(b, []byte{'\n'}) } |
Changes to web/adapter/webui/forms_test.go.
1 | //----------------------------------------------------------------------------- | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import "testing" func TestRemoveEmptyLines(t *testing.T) { |
︙ | ︙ |
Changes to web/adapter/webui/get_info.go.
1 | //----------------------------------------------------------------------------- | | < < < > < < | | | | | | | | | > | > > > > | > > > | > | | | | | < | | | > > > > > > > > > > > > > > | | < < < | > | | | < | < < | < | > | > > > > > > > < | | < | | > | > > > > > > > > > > > > > > > > > > | < < | | > | | > > | | | | | | < | | < < | > > | | < < < > > > > > | > > > > > > > | > | | < > | < > | < < < < > > > | > | < < < | < | < | > > > | | < < | | > > | < < | | | | > | > | | > > | | > | | > < < | < | < | | < | | < | > | | < < | | | | | < < < | < | | < < < < < | | | > > > > | | | > > | > > > > | < | | | < | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "bytes" "context" "net/http" "sort" "strings" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/domain/id" "zettelstore.de/z/encoder" "zettelstore.de/z/evaluator" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) type metaDataInfo struct { Key string Value string } type matrixLine struct { Header string Elements []simpleLink } // MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetInfoHandler( parseZettel usecase.ParseZettel, evaluate *usecase.Evaluate, getMeta usecase.GetMeta, getAllMeta usecase.GetAllMeta, unlinkedRefs usecase.UnlinkedReferences, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } zn, err := parseZettel.Run(ctx, zid, q.Get(api.KeySyntax)) if err != nil { wui.reportError(ctx, w, err) return } evalMetadata := createEvalMetadataFunc(ctx, evaluate) enc := wui.getSimpleHTMLEncoder() pairs := zn.Meta.ComputedPairs() metaData := make([]metaDataInfo, len(pairs)) getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), evalMetadata) for i, p := range pairs { var buf bytes.Buffer wui.writeHTMLMetaValue( &buf, p.Key, p.Value, getTextTitle, func(val string) ast.InlineSlice { return evaluate.RunMetadata(ctx, val) }, enc) metaData[i] = metaDataInfo{p.Key, buf.String()} } summary := collect.References(zn) locLinks, qLinks, extLinks := splitLocSeaExtLinks(append(summary.Links, summary.Embeds...)) queryLinks := make([]simpleLink, len(qLinks)) for i, sq := range qLinks { queryLinks[i].Text = sq queryLinks[i].URL = wui.NewURLBuilder('h').AppendQuery(sq).String() } textTitle := encodeEvaluatedTitleText(zn.InhMeta, evalMetadata, wui.gentext) phrase := q.Get(api.QueryKeyPhrase) if phrase == "" { phrase = textTitle } phrase = strings.TrimSpace(phrase) unlinkedMeta, err := unlinkedRefs.Run( ctx, phrase, adapter.AddUnlinkedRefsToQuery(nil, zn.InhMeta)) if err != nil { wui.reportError(ctx, w, err) return } bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, nil, unlinkedMeta, wui.rtConfig)) unlinkedContent, err := enc.BlocksString(&bns) if err != nil { wui.reportError(ctx, w, err) return } shadowLinks := getShadowLinks(ctx, zid, getAllMeta) endnotes, err := enc.BlocksString(&ast.BlockSlice{}) if err != nil { endnotes = "" } user := server.GetUser(ctx) canCreate := wui.canCreate(ctx, user) apiZid := api.ZettelID(zid.String()) var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), textTitle, "", user, &base) wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct { Zid string WebURL string ContextURL string CanWrite bool EditURL string CanCopy bool CopyURL string CanVersion bool VersionURL string CanFolge bool FolgeURL string CanRename bool RenameURL string CanDelete bool DeleteURL string MetaData []metaDataInfo HasLocLinks bool LocLinks []localLink QueryLinks simpleLinks HasExtLinks bool ExtLinks []string ExtNewWindow string UnLinksContent string UnLinksPhrase string QueryKeyPhrase string EvalMatrix []matrixLine ParseMatrix []matrixLine HasShadowLinks bool ShadowLinks []string Endnotes string }{ Zid: zid.String(), WebURL: wui.NewURLBuilder('h').SetZid(apiZid).String(), ContextURL: wui.NewURLBuilder('k').SetZid(apiZid).String(), CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: wui.NewURLBuilder('e').SetZid(apiZid).String(), CanCopy: canCreate && !zn.Content.IsBinary(), CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String(), CanVersion: canCreate, VersionURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String(), CanFolge: canCreate, FolgeURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String(), CanRename: wui.canRename(ctx, user, zn.Meta), RenameURL: wui.NewURLBuilder('b').SetZid(apiZid).String(), CanDelete: wui.canDelete(ctx, user, zn.Meta), DeleteURL: wui.NewURLBuilder('d').SetZid(apiZid).String(), MetaData: metaData, HasLocLinks: len(locLinks) > 0, LocLinks: locLinks, QueryLinks: createSimpleLinks(queryLinks), HasExtLinks: len(extLinks) > 0, ExtLinks: extLinks, ExtNewWindow: htmlAttrNewWindow(len(extLinks) > 0), UnLinksContent: unlinkedContent, UnLinksPhrase: phrase, QueryKeyPhrase: api.QueryKeyPhrase, EvalMatrix: wui.infoAPIMatrix('v', zid), ParseMatrix: wui.infoAPIMatrixPlain('p', zid), HasShadowLinks: len(shadowLinks) > 0, ShadowLinks: shadowLinks, Endnotes: endnotes, }) } } type localLink struct { Valid bool Zid string } func splitLocSeaExtLinks(links []*ast.Reference) (locLinks []localLink, queries, extLinks []string) { if len(links) == 0 { return nil, nil, nil } for _, ref := range links { if ref.State == ast.RefStateSelf || ref.IsZettel() { continue } if ref.State == ast.RefStateQuery { queries = append(queries, ref.Value) continue } if ref.IsExternal() { extLinks = append(extLinks, ref.String()) continue } locLinks = append(locLinks, localLink{ref.IsValid(), ref.String()}) } return locLinks, queries, extLinks } func (wui *WebUI) infoAPIMatrix(key byte, zid id.Zid) []matrixLine { encodings := encoder.GetEncodings() encTexts := make([]string, 0, len(encodings)) for _, f := range encodings { encTexts = append(encTexts, f.String()) } sort.Strings(encTexts) defEncoding := encoder.GetDefaultEncoding().String() parts := getParts() matrix := make([]matrixLine, 0, len(parts)) u := wui.NewURLBuilder(key).SetZid(api.ZettelID(zid.String())) for _, part := range parts { row := make([]simpleLink, len(encTexts)) for j, enc := range encTexts { u.AppendKVQuery(api.QueryKeyPart, part) if enc != defEncoding { u.AppendKVQuery(api.QueryKeyEncoding, enc) } row[j] = simpleLink{enc, u.String()} u.ClearQuery() } matrix = append(matrix, matrixLine{part, row}) } return matrix } func (wui *WebUI) infoAPIMatrixPlain(key byte, zid id.Zid) []matrixLine { matrix := wui.infoAPIMatrix(key, zid) apiZid := api.ZettelID(zid.String()) // Append plain and JSON format u := wui.NewURLBuilder('z').SetZid(apiZid) for i, part := range getParts() { u.AppendKVQuery(api.QueryKeyPart, part) matrix[i].Elements = append(matrix[i].Elements, simpleLink{"plain", u.String()}) u.ClearQuery() } u = wui.NewURLBuilder('j').SetZid(apiZid) matrix[0].Elements = append(matrix[0].Elements, simpleLink{"json", u.String()}) u = wui.NewURLBuilder('m').SetZid(apiZid) matrix[1].Elements = append(matrix[1].Elements, simpleLink{"json", u.String()}) return matrix } func getParts() []string { return []string{api.PartZettel, api.PartMeta, api.PartContent} } func getShadowLinks(ctx context.Context, zid id.Zid, getAllMeta usecase.GetAllMeta) []string { ml, err := getAllMeta.Run(ctx, zid) if err != nil || len(ml) < 2 { return nil } result := make([]string, 0, len(ml)-1) for _, m := range ml[1:] { if boxNo, ok := m.Get(api.KeyBoxNumber); ok { result = append(result, boxNo) } } return result } |
Changes to web/adapter/webui/get_zettel.go.
1 | //----------------------------------------------------------------------------- | | < < < | < < | < | | | | | | | | < | | < < < | < < < < | > | > > > > > | < < < | | < > > > < > | > | > > > | | | > > > | > | | | | | | | | | < < > > | | < | > > > > > > | > > > > > | < > > | < < < > > > | > > > > > > > > | < | > > > > > > > > > | < | | < < < < < < | > > > | | > > > | | < < < < < < < | < < < | | | | > > > > > > > > | | | > > > > > | > | > | | | | | | | | | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "bytes" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getMeta usecase.GetMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } q := r.URL.Query() zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax)) if err != nil { wui.reportError(ctx, w, err) return } enc := wui.createZettelEncoder(ctx, zn.InhMeta) evalMetadata := createEvalMetadataFunc(ctx, evaluate) textTitle := encodeEvaluatedTitleText(zn.InhMeta, evalMetadata, wui.gentext) htmlTitle := encodeEvaluatedTitleHTML(zn.InhMeta, evalMetadata, enc) htmlContent, err := enc.BlocksString(&zn.Ast) if err != nil { wui.reportError(ctx, w, err) return } var roleCSSURL string cssZid, err := wui.retrieveCSSZidFromRole(ctx, *zn.InhMeta) if err != nil { wui.reportError(ctx, w, err) return } if cssZid != id.Invalid { roleCSSURL = wui.NewURLBuilder('z').SetZid(api.ZettelID(cssZid.String())).String() } user := server.GetUser(ctx) roleText := zn.Meta.GetDefault(api.KeyRole, "") canCreate := wui.canCreate(ctx, user) getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), evalMetadata) extURL, hasExtURL := zn.Meta.Get(api.KeyURL) folgeLinks := createSimpleLinks(wui.encodeZettelLinks(zn.InhMeta, api.KeyFolge, getTextTitle)) backLinks := createSimpleLinks(wui.encodeZettelLinks(zn.InhMeta, api.KeyBack, getTextTitle)) successorLinks := createSimpleLinks(wui.encodeZettelLinks(zn.InhMeta, api.KeySuccessors, getTextTitle)) apiZid := api.ZettelID(zid.String()) var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), textTitle, roleCSSURL, user, &base) base.MetaHeader = enc.MetaString(zn.InhMeta, evalMetadata) wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct { HTMLTitle string RoleCSS string CanWrite bool EditURL string Zid string InfoURL string RoleText string RoleURL string Tags simpleLinks CanCopy bool CopyURL string CanVersion bool VersionURL string CanFolge bool FolgeURL string PredecessorRefs string PrecursorRefs string HasExtURL bool ExtURL string ExtNewWindow string Author string Content string NeedBottomNav bool FolgeLinks simpleLinks BackLinks simpleLinks SuccessorLinks simpleLinks }{ HTMLTitle: htmlTitle, RoleCSS: roleCSSURL, CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: wui.NewURLBuilder('e').SetZid(apiZid).String(), Zid: zid.String(), InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(), RoleText: roleText, RoleURL: wui.NewURLBuilder('h').AppendQuery(api.KeyRole + api.SearchOperatorHas + roleText).String(), Tags: createSimpleLinks(wui.buildTagInfos(zn.Meta)), CanCopy: canCreate && !zn.Content.IsBinary(), CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String(), CanVersion: canCreate, VersionURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String(), CanFolge: canCreate, FolgeURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String(), PredecessorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPredecessor, getTextTitle), PrecursorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPrecursor, getTextTitle), ExtURL: extURL, HasExtURL: hasExtURL, ExtNewWindow: htmlAttrNewWindow(hasExtURL), Author: zn.Meta.GetDefault(api.KeyAuthor, ""), Content: htmlContent, NeedBottomNav: folgeLinks.Has || backLinks.Has || successorLinks.Has, FolgeLinks: folgeLinks, BackLinks: backLinks, SuccessorLinks: successorLinks, }) } } func encodeInlinesText(is *ast.InlineSlice, enc *textenc.Encoder) (string, error) { if is == nil || len(*is) == 0 { return "", nil } var buf bytes.Buffer _, err := enc.WriteInlines(&buf, is) if err != nil { return "", err } return buf.String(), nil } func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink { var tagInfos []simpleLink if tags, ok := m.GetList(api.KeyTags); ok { ub := wui.NewURLBuilder('h') tagInfos = make([]simpleLink, len(tags)) for i, tag := range tags { tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery(api.KeyTags + api.SearchOperatorHas + tag).String()} ub.ClearQuery() } } return tagInfos } func (wui *WebUI) encodeIdentifierSet(m *meta.Meta, key string, getTextTitle getTextTitleFunc) string { if value, ok := m.Get(key); ok { var buf bytes.Buffer wui.writeIdentifierSet(&buf, meta.ListFromValue(value), getTextTitle) return buf.String() } return "" } func (wui *WebUI) encodeZettelLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) []simpleLink { values, ok := m.GetList(key) if !ok || len(values) == 0 { return nil } return wui.encodeZidLinks(values, getTextTitle) } func (wui *WebUI) encodeZidLinks(values []string, getTextTitle getTextTitleFunc) []simpleLink { result := make([]simpleLink, 0, len(values)) for _, val := range values { zid, err := id.Parse(val) if err != nil { continue } if title, found := getTextTitle(zid); found > 0 { url := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())).String() if title == "" { result = append(result, simpleLink{Text: val, URL: url}) } else { result = append(result, simpleLink{Text: title, URL: url}) } } } return result } |
Changes to web/adapter/webui/goaction.go.
1 | //----------------------------------------------------------------------------- | | | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // 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 import ( "net/http" |
︙ | ︙ |
Changes to web/adapter/webui/home.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | < | > | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "context" "errors" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/web/server" ) 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 (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if r.URL.Path != "/" { wui.reportError(ctx, w, box.ErrNotFound) return } homeZid := wui.rtConfig.GetHomeZettel() apiHomeZid := api.ZettelID(homeZid.String()) if homeZid != id.DefaultHomeZid { if _, err := s.GetMeta(ctx, homeZid); err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid)) return } homeZid = id.DefaultHomeZid } _, err := s.GetMeta(ctx, homeZid) if err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid)) return } if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil { wui.redirectFound(w, r, wui.NewURLBuilder('i')) return |
︙ | ︙ |
Changes to web/adapter/webui/htmlgen.go.
1 | //----------------------------------------------------------------------------- | | < < < | | < | | | | | | | | | | | | | | | | | < < < < < < | < < < < < < < < < < < < | < | < < < > | < < < | < < < < < < < < | < | | < < < > > | < < < < < < < < | < < < < < | | | | < > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | < < < < < < | < < < < < < < > | | < < | < < < < < < < < < < < < | < < < < < < < < | < | < > | | < | > | < > | < < > > > > | | < | | | | > | > > > | > > > | > > | > > | | > > | | > > | > | > > | | > > > > | | < | > > > > > | > > | | > > > > > > > > | > > > > > > > > > | | | > > | > > | | | | > > | > | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "bytes" "strings" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/c/html" "zettelstore.de/c/sexpr" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/sexprenc" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/query" "zettelstore.de/z/strfun" ) // Builder allows to build new URLs for the web service. type urlBuilder interface { GetURLPrefix() string NewURLBuilder(key byte) *api.URLBuilder } type htmlGenerator struct { builder urlBuilder textEnc *textenc.Encoder extMarker string env *html.EncEnvironment } func createGenerator(builder urlBuilder, extMarker string) *htmlGenerator { env := html.NewEncEnvironment(nil, 1) gen := &htmlGenerator{ builder: builder, textEnc: textenc.Create(), extMarker: extMarker, env: env, } env.Builtins.Set(sexpr.SymLinkZettel, sxpf.NewBuiltin("linkZ", true, 2, -1, gen.generateLinkZettel)) env.Builtins.Set(sexpr.SymLinkFound, sxpf.NewBuiltin("linkZ", true, 2, -1, gen.generateLinkZettel)) env.Builtins.Set(sexpr.SymLinkBased, sxpf.NewBuiltin("linkB", true, 2, -1, gen.generateLinkBased)) env.Builtins.Set(sexpr.SymLinkQuery, sxpf.NewBuiltin("linkQ", true, 2, -1, gen.generateLinkQuery)) env.Builtins.Set(sexpr.SymLinkExternal, sxpf.NewBuiltin("linkE", true, 2, -1, gen.generateLinkExternal)) f, err := env.Builtins.LookupForm(sexpr.SymEmbed) if err != nil { panic(err) } b := f.(*sxpf.Builtin) env.Builtins.Set(sexpr.SymEmbed, sxpf.NewBuiltin(b.Name(), true, 3, -1, gen.makeGenerateEmbed(b.GetValue()))) return gen } var mapMetaKey = map[string]string{ api.KeyCopyright: "copyright", api.KeyLicense: "license", } func (g *htmlGenerator) MetaString(m *meta.Meta, evalMeta encoder.EvalMetaFunc) string { ignore := strfun.NewSet(api.KeyTitle, api.KeyLang) var buf bytes.Buffer if tags, ok := m.Get(api.KeyTags); ok { writeMetaTags(&buf, tags) ignore.Set(api.KeyTags) } for _, p := range m.ComputedPairs() { key := p.Key if ignore.Has(key) { continue } if altKey, found := mapMetaKey[key]; found { buf.WriteString(`<meta name="`) buf.WriteString(altKey) } else { buf.WriteString(`<meta name="zs-`) buf.WriteString(key) } buf.WriteString(`" content="`) is := evalMeta(p.Value) var sb strings.Builder g.textEnc.WriteInlines(&sb, &is) html.AttributeEscape(&buf, sb.String()) buf.WriteString("\">\n") } return buf.String() } func writeMetaTags(buf *bytes.Buffer, tags string) { buf.WriteString(`<meta name="keywords" content="`) for i, val := range meta.ListFromValue(tags) { if i > 0 { buf.WriteString(", ") } html.AttributeEscape(buf, strings.TrimPrefix(val, "#")) } buf.WriteString("\">\n") } // BlocksString encodes a block slice. func (g *htmlGenerator) BlocksString(bs *ast.BlockSlice) (string, error) { if bs == nil || len(*bs) == 0 { return "", nil } lst := sexprenc.GetSexpr(bs) var buf bytes.Buffer g.env.ReplaceWriter(&buf) sxpf.Eval(g.env, lst) if g.env.GetError() == nil { g.env.WriteEndnotes() } g.env.ReplaceWriter(nil) return buf.String(), g.env.GetError() } // InlinesString writes an inline slice to the writer func (g *htmlGenerator) InlinesString(is *ast.InlineSlice) (string, error) { if is == nil || len(*is) == 0 { return "", nil } return html.EvaluateInline(g.env, sexprenc.GetSexpr(is), true, false), nil } func (g *htmlGenerator) generateLinkZettel(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) { env := senv.(*html.EncEnvironment) if a, refValue, ok := html.PrepareLink(env, args); ok { zid, fragment, hasFragment := strings.Cut(refValue, "#") u := g.builder.NewURLBuilder('h').SetZid(api.ZettelID(zid)) if hasFragment { u = u.SetFragment(fragment) } html.WriteLink(env, args, a.Set("href", u.String()), refValue, "") } return nil, nil } func (g *htmlGenerator) generateLinkBased(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) { env := senv.(*html.EncEnvironment) if a, refValue, ok := html.PrepareLink(env, args); ok { u := g.builder.NewURLBuilder('/').SetRawLocal(refValue) html.WriteLink(env, args, a.Set("href", u.String()), refValue, "") } return nil, nil } func (g *htmlGenerator) generateLinkQuery(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) { env := senv.(*html.EncEnvironment) if a, refValue, ok := html.PrepareLink(env, args); ok { queryExpr := query.Parse(refValue).String() u := g.builder.NewURLBuilder('h').AppendQuery(queryExpr) html.WriteLink(env, args, a.Set("href", u.String()), refValue, "") } return nil, nil } func (g *htmlGenerator) generateLinkExternal(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) { env := senv.(*html.EncEnvironment) if a, refValue, ok := html.PrepareLink(env, args); ok { a = a.Set("href", refValue). AddClass("external"). Set("target", "_blank"). Set("rel", "noopener noreferrer") html.WriteLink(env, args, a, refValue, g.extMarker) } return nil, nil } func (g *htmlGenerator) makeGenerateEmbed(oldFn sxpf.BuiltinFn) sxpf.BuiltinFn { return func(senv sxpf.Environment, args *sxpf.Pair, arity int) (sxpf.Value, error) { env := senv.(*html.EncEnvironment) ref := env.GetPair(args.GetTail()) refValue := env.GetString(ref.GetTail()) zid := api.ZettelID(refValue) if !zid.IsValid() { return oldFn(senv, args, arity) } u := g.builder.NewURLBuilder('z').SetZid(zid) env.WriteImageWithSource(args, u.String()) return nil, nil } } |
Changes to web/adapter/webui/htmlmeta.go.
1 | //----------------------------------------------------------------------------- | | < < < > > > > < < | < | | | | | | > > > | | | | | | | | < | < < < < < < < | | > > | > | > > > | < > | | < | > | > < < < > | | | > | > > > > > | > > | | > | > | > | | < > | > > > > | | > > | > | > > | < < < | < < | < > > < < < < > > > > | < | < < < | | | < < | > | < > | > > | | | | > > > > | | 1 2 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) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "context" "errors" "fmt" "io" "net/url" "time" "zettelstore.de/c/api" "zettelstore.de/c/html" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" ) var space = []byte{' '} func (wui *WebUI) writeHTMLMetaValue( w io.Writer, key, value string, getTextTitle getTextTitleFunc, evalMetadata evalMetadataFunc, gen *htmlGenerator, ) { switch kt := meta.Type(key); kt { case meta.TypeCredential: writeCredential(w, value) case meta.TypeEmpty: writeEmpty(w, value) case meta.TypeID: wui.writeIdentifier(w, value, getTextTitle) case meta.TypeIDSet: wui.writeIdentifierSet(w, meta.ListFromValue(value), getTextTitle) case meta.TypeNumber: wui.writeNumber(w, key, value) case meta.TypeString: writeString(w, value) case meta.TypeTagSet: wui.writeTagSet(w, key, meta.ListFromValue(value)) case meta.TypeTimestamp: if ts, ok := meta.TimeValue(value); ok { writeTimestamp(w, ts) } case meta.TypeURL: writeURL(w, value) case meta.TypeWord: wui.writeWord(w, key, value) case meta.TypeWordSet: wui.writeWordSet(w, key, meta.ListFromValue(value)) case meta.TypeZettelmarkup: io.WriteString(w, encodeZmkMetadata(value, evalMetadata, gen)) default: html.Escape(w, value) fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key) } } func writeCredential(w io.Writer, val string) { html.Escape(w, val) } func writeEmpty(w io.Writer, val string) { html.Escape(w, val) } func (wui *WebUI) writeIdentifier(w io.Writer, val string, getTextTitle getTextTitleFunc) { zid, err := id.Parse(val) if err != nil { html.Escape(w, val) return } title, found := getTextTitle(zid) switch { case found > 0: ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())) if title == "" { fmt.Fprintf(w, "<a href=\"%v\">%v</a>", ub, zid) } else { fmt.Fprintf(w, "<a href=\"%v\" title=\"%v\">%v</a>", ub, title, zid) } case found == 0: fmt.Fprintf(w, "<s>%v</s>", val) case found < 0: io.WriteString(w, val) } } func (wui *WebUI) writeIdentifierSet(w io.Writer, vals []string, getTextTitle getTextTitleFunc) { for i, val := range vals { if i > 0 { w.Write(space) } wui.writeIdentifier(w, val, getTextTitle) } } func (wui *WebUI) writeNumber(w io.Writer, key, val string) { wui.writeLink(w, key, val, val) } func writeString(w io.Writer, val string) { html.Escape(w, val) } func (wui *WebUI) writeTagSet(w io.Writer, key string, tags []string) { for i, tag := range tags { if i > 0 { w.Write(space) } wui.writeLink(w, key, tag, tag) } } func writeTimestamp(w io.Writer, ts time.Time) { io.WriteString(w, ts.Format("2006-01-02 15:04:05")) } func writeURL(w io.Writer, val string) { u, err := url.Parse(val) if err != nil { html.Escape(w, val) return } fmt.Fprintf(w, "<a href=\"%v\"%v>", u, htmlAttrNewWindow(true)) html.Escape(w, val) io.WriteString(w, "</a>") } func (wui *WebUI) writeWord(w io.Writer, key, word string) { wui.writeLink(w, key, word, word) } func (wui *WebUI) writeWordSet(w io.Writer, key string, words []string) { for i, word := range words { if i > 0 { w.Write(space) } wui.writeWord(w, key, word) } } func (wui *WebUI) writeLink(w io.Writer, key, value, text string) { fmt.Fprintf(w, `<a href="%v">`, wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value)) html.Escape(w, text) io.WriteString(w, "</a>") } type getMetadataFunc func(id.Zid) (*meta.Meta, error) func createGetMetadataFunc(ctx context.Context, getMeta usecase.GetMeta) getMetadataFunc { return func(zid id.Zid) (*meta.Meta, error) { return getMeta.Run(box.NoEnrichContext(ctx), zid) } } type evalMetadataFunc = func(string) ast.InlineSlice func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc { return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) } } type getTextTitleFunc func(id.Zid) (string, int) func (wui *WebUI) makeGetTextTitle(getMetadata getMetadataFunc, evalMetadata evalMetadataFunc) getTextTitleFunc { return func(zid id.Zid) (string, int) { m, err := getMetadata(zid) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return "", -1 } return "", 0 } return encodeEvaluatedTitleText(m, evalMetadata, wui.gentext), 1 } } func encodeZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) string { is := evalMetadata(value) result, err := gen.InlinesString(&is) if err != nil { return err.Error() } return result } |
Changes to web/adapter/webui/lists.go.
1 | //----------------------------------------------------------------------------- | | < < < > < < | < | | | < < | < < < < < | < < | | < | | < < < < < < < < < | | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < | < < < < < < < > < < | < | > > | | | | | < < < | < < < | < < < < > | < | < < > | < < < < < < < < < < < < < < < < | | | | | | > > | | > | > | | | > > > | | | | | > > > > | | | > > > > > > > > > > > | < | > > > > > > > > > > > > > > > > > > > | > > > > > | | | < > | < | < > > | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "bytes" "context" "io" "net/http" "net/url" "strings" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoding/atom" "zettelstore.de/z/encoding/rss" "zettelstore.de/z/encoding/xml" "zettelstore.de/z/evaluator" "zettelstore.de/z/query" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML. func (wui *WebUI) MakeListHTMLMetaHandler(listMeta usecase.ListMeta, evaluate *usecase.Evaluate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := adapter.GetQuery(r.URL.Query()) ctx := r.Context() if !q.EnrichNeeded() { ctx = box.NoEnrichContext(ctx) } metaList, err := listMeta.Run(ctx, q) if err != nil { wui.reportError(ctx, w, err) return } if actions := q.Actions(); len(actions) > 0 { switch actions[0] { case "ATOM": wui.renderAtom(ctx, w, q, metaList) return case "RSS": wui.renderRSS(ctx, w, q, metaList) return } } bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, q, metaList, wui.rtConfig)) enc := wui.getSimpleHTMLEncoder() htmlContent, err := enc.BlocksString(&bns) if err != nil { wui.reportError(ctx, w, err) return } user := server.GetUser(ctx) var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), wui.rtConfig.GetSiteName(), "", user, &base) wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct { Title string SearchURL string QueryValue string QueryKeyQuery string Content string }{ Title: wui.listTitleQuery(q), SearchURL: base.SearchURL, QueryValue: q.String(), QueryKeyQuery: base.QueryKeyQuery, Content: htmlContent, }) } } func (wui *WebUI) renderRSS(ctx context.Context, w http.ResponseWriter, q *query.Query, ml []*meta.Meta) { var rssConfig rss.Configuration rssConfig.Setup(ctx, wui.rtConfig) if actions := q.Actions(); len(actions) > 2 && actions[1] == "TITLE" { rssConfig.Title = strings.Join(actions[2:], " ") } data := rssConfig.Marshal(q, ml) adapter.PrepareHeader(w, rss.ContentType) w.WriteHeader(http.StatusOK) var err error if _, err = io.WriteString(w, xml.Header); err == nil { _, err = w.Write(data) } if err != nil { wui.log.IfErr(err).Msg("unable to write RSS data") } } func (wui *WebUI) renderAtom(ctx context.Context, w http.ResponseWriter, q *query.Query, ml []*meta.Meta) { var atomConfig atom.Configuration atomConfig.Setup(ctx, wui.rtConfig) if actions := q.Actions(); len(actions) > 2 && actions[1] == "TITLE" { atomConfig.Title = strings.Join(actions[2:], " ") } data := atomConfig.Marshal(q, ml) adapter.PrepareHeader(w, atom.ContentType) w.WriteHeader(http.StatusOK) var err error if _, err = io.WriteString(w, xml.Header); err == nil { _, err = w.Write(data) } if err != nil { wui.log.IfErr(err).Msg("unable to write Atom data") } } // MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context". func (wui *WebUI) MakeZettelContextHandler(getContext usecase.ZettelContext, evaluate *usecase.Evaluate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } q := r.URL.Query() dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir)) depth := getIntParameter(q, api.QueryKeyDepth, 5) limit := getIntParameter(q, api.QueryKeyLimit, 200) metaList, err := getContext.Run(ctx, zid, dir, depth, limit) if err != nil { wui.reportError(ctx, w, err) return } bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, nil, metaList, wui.rtConfig)) enc := wui.getSimpleHTMLEncoder() htmlContent, err := enc.BlocksString(&bns) if err != nil { wui.reportError(ctx, w, err) return } apiZid := api.ZettelID(zid.String()) depths := []string{"2", "3", "4", "5", "6", "7", "8", "9", "10"} depthLinks := make([]simpleLink, len(depths)) depthURL := wui.NewURLBuilder('k').SetZid(apiZid) for i, depth := range depths { depthURL.ClearQuery() switch dir { case usecase.ZettelContextBackward: depthURL.AppendKVQuery(api.QueryKeyDir, api.DirBackward) case usecase.ZettelContextForward: depthURL.AppendKVQuery(api.QueryKeyDir, api.DirForward) } depthURL.AppendKVQuery(api.QueryKeyDepth, depth) depthLinks[i].Text = depth depthLinks[i].URL = depthURL.String() } var base baseData user := server.GetUser(ctx) wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), wui.rtConfig.GetSiteName(), "", user, &base) wui.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct { Title string InfoURL string Depths []simpleLink Content string }{ Title: "Zettel Context", InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(), Depths: depthLinks, Content: htmlContent, }) } } func getIntParameter(q url.Values, key string, minValue int) int { val, ok := adapter.GetInteger(q, key) if !ok || val < 0 { return minValue } return val } func (wui *WebUI) listTitleQuery(q *query.Query) string { if q == nil { return wui.rtConfig.GetSiteName() } var buf bytes.Buffer q.PrintHuman(&buf) return buf.String() } |
Changes to web/adapter/webui/login.go.
1 | //----------------------------------------------------------------------------- | | < < < < | | | | > | < < | > > | > | < | | | 1 2 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 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "context" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetLoginOutHandler creates a new HTTP handler to display the HTML login view, // or to execute a logout. func (wui *WebUI) MakeGetLoginOutHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() if query.Has("logout") { wui.clearToken(r.Context(), w) wui.redirectFound(w, r, wui.NewURLBuilder('/')) return } wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false) } } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), "Login", "", nil, &base) wui.renderTemplate(ctx, w, id.LoginTemplateZid, &base, struct { Title string Retry bool }{ Title: base.Title, Retry: retry, }) } // MakePostLoginHandler creates a new HTTP handler to authenticate the given user. func (wui *WebUI) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !wui.authz.WithAuth() { wui.redirectFound(w, r, wui.NewURLBuilder('/')) return } ctx := r.Context() ident, cred, ok := adapter.GetCredentialsViaForm(r) if !ok { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read login form")) return } token, err := ucAuth.Run(ctx, r, ident, cred, wui.tokenLifetime, auth.KindHTML) if err != nil { wui.reportError(ctx, w, err) return } if token == nil { wui.renderLoginForm(wui.clearToken(ctx, w), w, true) return } wui.setToken(w, token) wui.redirectFound(w, r, wui.NewURLBuilder('/')) } } |
Deleted web/adapter/webui/meta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/meta_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/webui/rename_zettel.go.
1 | //----------------------------------------------------------------------------- | | < < < | > | | | | | < | | | | > > < < > | < < < | > > > | < > | < > > | > > | < | | < < | < < | < < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "fmt" "net/http" "strings" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // MakeGetRenameZettelHandler creates a new HTTP handler to display the // HTML rename view of a zettel. func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta, evaluate *usecase.Evaluate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } m, err := getMeta.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), createEvalMetadataFunc(ctx, evaluate)) uselessFiles := retrieveUselessFiles(m) user := server.GetUser(ctx) var base baseData wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), "Rename Zettel "+zid.String(), "", user, &base) wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct { Zid string MetaPairs []meta.Pair Incoming simpleLinks HasUselessFiles bool UselessFiles []string }{ Zid: zid.String(), MetaPairs: m.ComputedPairs(), Incoming: wui.encodeIncoming(m, getTextTitle), HasUselessFiles: len(uselessFiles) > 0, UselessFiles: uselessFiles, }) } } // MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel. func (wui *WebUI) MakePostRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() curZid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } if err = r.ParseForm(); err != nil { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form")) return } if formCurZid, err1 := id.Parse( r.PostFormValue("curzid")); err1 != nil || formCurZid != curZid { wui.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form")) return } formNewZid := strings.TrimSpace(r.PostFormValue("newzid")) newZid, err := id.Parse(formNewZid) if err != nil { wui.reportError( ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", formNewZid))) return } if err = renameZettel.Run(r.Context(), curZid, newZid); err != nil { wui.reportError(ctx, w, err) return } wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String()))) } } |
Changes to web/adapter/webui/response.go.
1 | //----------------------------------------------------------------------------- | | < < < | | 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "net/http" "zettelstore.de/c/api" ) func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) { us := ub.String() wui.log.Debug().Str("uri", us).Msg("redirect") http.Redirect(w, r, us, http.StatusFound) } |
Deleted web/adapter/webui/sxn_code.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/webui/template.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added web/adapter/webui/titleenc.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package webui import ( "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" ) func encodeEvaluatedTitleHTML(m *meta.Meta, evalMetadata evalMetadataFunc, gen *htmlGenerator) string { return encodeZmkMetadata(m.GetTitle(), evalMetadata, gen) } func encodeEvaluatedTitleText(m *meta.Meta, evalMetadata evalMetadataFunc, enc *textenc.Encoder) string { is := evalMetadata(m.GetTitle()) result, err := encodeInlinesText(&is, enc) if err != nil { return err.Error() } return result } |
Changes to web/adapter/webui/webui.go.
1 | //----------------------------------------------------------------------------- | | < < < > > | | | | | | | | | | | | | | | | | > > > < | < < < < < | < < < < | | | | | | | < < | < < | < < < < < < < < < | | | < | < | | | | | > | > > | > > > > > > > > > > > > > > > > > > > | | > > > > > > > > > > > > > > > > > > > > > > | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package webui provides web-UI handlers for web requests. package webui import ( "bytes" "context" "net/http" "strings" "sync" "time" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/parser" "zettelstore.de/z/template" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // WebUI holds all data for delivering the web ui. type WebUI struct { log *logger.Logger debug bool ab server.AuthBuilder authz auth.AuthzManager rtConfig config.Config token auth.TokenManager box webuiBox policy auth.Policy gentext *textenc.Encoder mxCache sync.RWMutex templateCache map[id.Zid]*template.Template mxRoleCSSMap sync.RWMutex roleCSSMap map[string]id.Zid tokenLifetime time.Duration cssBaseURL string cssUserURL string homeURL string listZettelURL string listRolesURL string listTagsURL string refreshURL string withAuth bool loginURL string logoutURL string searchURL string } type webuiBox interface { CanCreateZettel(ctx context.Context) bool GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool AllowRenameZettel(ctx context.Context, zid id.Zid) bool CanDeleteZettel(ctx context.Context, zid id.Zid) bool } // New creates a new WebUI struct. func New(log *logger.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager, mgr box.Manager, pol auth.Policy) *WebUI { loginoutBase := ab.NewURLBuilder('i') wui := &WebUI{ log: log, debug: kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool), ab: ab, rtConfig: rtConfig, authz: authz, token: token, box: mgr, policy: pol, gentext: textenc.Create(), tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration), cssBaseURL: ab.NewURLBuilder('z').SetZid(api.ZidBaseCSS).String(), cssUserURL: ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(), homeURL: ab.NewURLBuilder('/').String(), listZettelURL: ab.NewURLBuilder('h').String(), listRolesURL: ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyRole).String(), listTagsURL: ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyTags).String(), refreshURL: ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(), withAuth: authz.WithAuth(), loginURL: loginoutBase.String(), logoutURL: loginoutBase.AppendKVQuery("logout", "").String(), searchURL: ab.NewURLBuilder('h').String(), } wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(wui.observe) return wui } func (wui *WebUI) observe(ci box.UpdateInfo) { wui.mxCache.Lock() if ci.Reason == box.OnReload || ci.Zid == id.BaseTemplateZid { wui.templateCache = make(map[id.Zid]*template.Template, len(wui.templateCache)) } else { delete(wui.templateCache, ci.Zid) } wui.mxCache.Unlock() wui.mxRoleCSSMap.Lock() if ci.Reason == box.OnReload || ci.Zid == id.RoleCSSMapZid { wui.roleCSSMap = nil } wui.mxRoleCSSMap.Unlock() } func (wui *WebUI) cacheSetTemplate(zid id.Zid, t *template.Template) { wui.mxCache.Lock() wui.templateCache[zid] = t wui.mxCache.Unlock() } func (wui *WebUI) cacheGetTemplate(zid id.Zid) (*template.Template, bool) { wui.mxCache.RLock() t, ok := wui.templateCache[zid] wui.mxCache.RUnlock() return t, ok } func (wui *WebUI) retrieveCSSZidFromRole(ctx context.Context, m meta.Meta) (id.Zid, error) { wui.mxRoleCSSMap.RLock() if wui.roleCSSMap == nil { wui.mxRoleCSSMap.RUnlock() wui.mxRoleCSSMap.Lock() mMap, err := wui.box.GetMeta(ctx, id.RoleCSSMapZid) if err == nil { wui.roleCSSMap = createRoleCSSMap(mMap) } wui.mxRoleCSSMap.Unlock() if err != nil { return id.Invalid, err } wui.mxRoleCSSMap.RLock() } defer wui.mxRoleCSSMap.RUnlock() if role, found := m.Get("css-role"); found { if result, found2 := wui.roleCSSMap[role]; found2 { return result, nil } } if role, found := m.Get(api.KeyRole); found { if result, found2 := wui.roleCSSMap[role]; found2 { return result, nil } } return id.Invalid, nil } func createRoleCSSMap(mMap *meta.Meta) map[string]id.Zid { result := make(map[string]id.Zid) for _, p := range mMap.PairsRest() { key := p.Key if len(key) < 9 || !strings.HasPrefix(key, "css-") || !strings.HasSuffix(key, "-zid") { continue } zid, err2 := id.Parse(p.Value) if err2 != nil { continue } result[key[4:len(key)-4]] = zid } return result } func (wui *WebUI) canCreate(ctx context.Context, user *meta.Meta) bool { m := meta.New(id.Invalid) return wui.policy.CanCreate(user, m) && wui.box.CanCreateZettel(ctx) } func (wui *WebUI) canWrite( ctx context.Context, user, meta *meta.Meta, content domain.Content) bool { return wui.policy.CanWrite(user, meta, meta) && wui.box.CanUpdateZettel(ctx, domain.Zettel{Meta: meta, Content: content}) } func (wui *WebUI) canRename(ctx context.Context, user, m *meta.Meta) bool { return wui.policy.CanRename(user, m) && wui.box.AllowRenameZettel(ctx, m.Zid) } func (wui *WebUI) canDelete(ctx context.Context, user, m *meta.Meta) bool { return wui.policy.CanDelete(user, m) && wui.box.CanDeleteZettel(ctx, m.Zid) } func (wui *WebUI) canRefresh(user *meta.Meta) bool { return wui.policy.CanRefresh(user) } func (wui *WebUI) getTemplate( ctx context.Context, templateID id.Zid) (*template.Template, error) { if t, ok := wui.cacheGetTemplate(templateID); ok { return t, nil } realTemplateZettel, err := wui.box.GetZettel(ctx, templateID) if err != nil { return nil, err } t, err := template.ParseString(realTemplateZettel.Content.AsString(), nil) if err == nil { // t.SetErrorOnMissing() wui.cacheSetTemplate(templateID, t) } return t, err } type simpleLink struct { Text string URL string } type simpleLinks struct { Has bool Links []simpleLink } func createSimpleLinks(ls []simpleLink) simpleLinks { return simpleLinks{ Has: len(ls) > 0, Links: ls, } } type baseData struct { Lang string MetaHeader string CSSBaseURL string CSSUserURL string CSSRoleURL string Title string HomeURL string WithUser bool WithAuth bool UserIsValid bool UserZettelURL string UserIdent string LoginURL string LogoutURL string ListZettelURL string ListRolesURL string ListTagsURL string CanRefresh bool RefreshURL string NewZettelLinks simpleLinks SearchURL string QueryKeyQuery string Content string FooterHTML string DebugMode bool } func (wui *WebUI) makeBaseData(ctx context.Context, lang, title, roleCSSURL string, user *meta.Meta, data *baseData) { var userZettelURL string var userIdent string userIsValid := user != nil if userIsValid { userZettelURL = wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String() userIdent = user.GetDefault(api.KeyUserID, "") } data.Lang = lang data.CSSBaseURL = wui.cssBaseURL data.CSSUserURL = wui.cssUserURL data.CSSRoleURL = roleCSSURL data.Title = title data.HomeURL = wui.homeURL data.WithAuth = wui.withAuth data.WithUser = data.WithAuth data.UserIsValid = userIsValid data.UserZettelURL = userZettelURL data.UserIdent = userIdent data.LoginURL = wui.loginURL data.LogoutURL = wui.logoutURL data.ListZettelURL = wui.listZettelURL data.ListRolesURL = wui.listRolesURL data.ListTagsURL = wui.listTagsURL data.CanRefresh = wui.canRefresh(user) data.RefreshURL = wui.refreshURL data.NewZettelLinks = createSimpleLinks(wui.fetchNewTemplates(ctx, user)) data.SearchURL = wui.searchURL data.QueryKeyQuery = api.QueryKeyQuery data.FooterHTML = wui.rtConfig.Get(ctx, nil, config.KeyFooterHTML) data.DebugMode = wui.debug } func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator { return createGenerator(wui, "") } func (wui *WebUI) createZettelEncoder(ctx context.Context, m *meta.Meta) *htmlGenerator { return createGenerator(wui, wui.rtConfig.Get(ctx, m, config.KeyMarkerExternal)) } // htmlAttrNewWindow returns HTML attribute string for opening a link in a new window. // If hasURL is false an empty string is returned. func htmlAttrNewWindow(hasURL bool) string { if hasURL { return " target=\"_blank\" ref=\"noopener noreferrer\"" } return "" } func (wui *WebUI) fetchNewTemplates(ctx context.Context, user *meta.Meta) (result []simpleLink) { ctx = box.NoEnrichContext(ctx) if !wui.canCreate(ctx, user) { return nil } menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid) if err != nil { return nil } refs := collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig)) for _, ref := range refs { zid, err2 := id.Parse(ref.URL.Path) if err2 != nil { continue } m, err2 := wui.box.GetMeta(ctx, zid) if err2 != nil { continue } if !wui.policy.CanRead(user, m) { continue } title := m.GetTitle() astTitle := parser.ParseMetadataNoLink(title) menuTitle, err2 := wui.getSimpleHTMLEncoder().InlinesString(&astTitle) if err2 != nil { menuTitle, err2 = encodeInlinesText(&astTitle, wui.gentext) if err2 != nil { menuTitle = title } } result = append(result, simpleLink{ Text: menuTitle, URL: wui.NewURLBuilder('c').SetZid(api.ZettelID(m.Zid.String())). AppendKVQuery(queryKeyAction, valueActionNew).String(), }) } return result } func (wui *WebUI) renderTemplate( ctx context.Context, w http.ResponseWriter, templateID id.Zid, base *baseData, data interface{}) { wui.renderTemplateStatus(ctx, w, http.StatusOK, templateID, base, data) } func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { wui.log.Error().Msg(err.Error()) } user := server.GetUser(ctx) var base baseData wui.makeBaseData(ctx, api.ValueLangEN, "Error", "", user, &base) wui.renderTemplateStatus(ctx, w, code, id.ErrorTemplateZid, &base, struct { ErrorTitle string ErrorText string }{ ErrorTitle: http.StatusText(code), ErrorText: text, }) } func (wui *WebUI) renderTemplateStatus( ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, base *baseData, data interface{}) { bt, err := wui.getTemplate(ctx, id.BaseTemplateZid) if err != nil { wui.log.IfErr(err).Zid(id.BaseTemplateZid).Msg("Unable to get template") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } t, err := wui.getTemplate(ctx, templateID) if err != nil { wui.log.IfErr(err).Zid(templateID).Msg("Unable to get template") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if user := server.GetUser(ctx); user != nil { if tok, err1 := wui.token.GetToken(user, wui.tokenLifetime, auth.KindHTML); err1 == nil { wui.setToken(w, tok) } } var content bytes.Buffer err = t.Render(&content, data) if err == nil { wui.prepareAndWriteHeader(w, code) base.Content = content.String() err = bt.Render(w, base) } if err != nil { wui.log.IfErr(err).Msg("Unable to write HTML via template") } } // GetURLPrefix returns the configured URL prefix of the web server. func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. func (wui *WebUI) NewURLBuilder(key byte) *api.URLBuilder { return wui.ab.NewURLBuilder(key) } |
︙ | ︙ |
Deleted web/content/content.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/content/content_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/server/impl/http.go.
1 | //----------------------------------------------------------------------------- | | | < < < > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | //----------------------------------------------------------------------------- // Copyright (c) 2020-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 impl provides the Zettelstore web service. package impl import ( "context" "net" "net/http" "time" ) // Server timeout values const ( shutdownTimeout = 5 * time.Second readTimeout = 5 * time.Second writeTimeout = 10 * time.Second idleTimeout = 120 * time.Second ) // httpServer is a HTTP server. type httpServer struct { http.Server waitStop chan struct{} } // initializeHTTPServer creates a new HTTP server object. func (srv *httpServer) initializeHTTPServer(addr string, handler http.Handler) { if addr == "" { addr = ":http" } srv.Server = http.Server{ Addr: addr, Handler: handler, // See: https://blog.cloudflare.com/exposing-go-on-the-internet/ ReadTimeout: readTimeout, WriteTimeout: writeTimeout, IdleTimeout: idleTimeout, } srv.waitStop = make(chan struct{}) } // SetDebug enables debugging goroutines that are started by the server. // Basically, just the timeout values are reset. This method should be called // before running the server. func (srv *httpServer) SetDebug() { srv.ReadTimeout = 0 |
︙ | ︙ |
Changes to web/server/impl/impl.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the Zettelstore web service. package impl import ( "context" "net/http" "time" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/domain/meta" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) type myServer struct { log *logger.Logger baseURL string server httpServer router httpRouter |
︙ | ︙ |
Changes to web/server/impl/router.go.
1 | //----------------------------------------------------------------------------- | | < < < > | | 1 2 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package impl provides the Zettelstore web service. package impl import ( "io" "net/http" "regexp" "strings" "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) type ( |
︙ | ︙ | |||
107 108 109 110 111 112 113 | func (rt *httpRouter) Handle(pattern string, handler http.Handler) { rt.mux.Handle(pattern, handler) } func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Something may panic. Ensure a kernel log. defer func() { | | | | 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | func (rt *httpRouter) Handle(pattern string, handler http.Handler) { rt.mux.Handle(pattern, handler) } func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Something may panic. Ensure a kernel log. defer func() { if reco := recover(); reco != nil { rt.log.Error().Str("Method", r.Method).Str("URL", r.URL.String()).HTTPIP(r).Msg("Recover context") kernel.Main.LogRecover("Web", reco) } }() var withDebug bool if msg := rt.log.Debug(); msg.Enabled() { withDebug = true w = &traceResponseWriter{original: w} |
︙ | ︙ | |||
172 173 174 175 176 177 178 | } func (rt *httpRouter) addUserContext(r *http.Request) *http.Request { if rt.ur == nil { // No auth needed return r } | | | | | | 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 | } func (rt *httpRouter) addUserContext(r *http.Request) *http.Request { if rt.ur == nil { // No auth needed return r } k := auth.KindJSON t := getHeaderToken(r) if len(t) == 0 { rt.log.Debug().Msg("no jwt token found") // IP already logged: ServeHTTP k = auth.KindHTML t = getSessionToken(r) } if len(t) == 0 { rt.log.Debug().Msg("no auth token found in request") // IP already logged: ServeHTTP return r } tokenData, err := rt.auth.CheckToken(t, k) if err != nil { rt.log.Sense().Err(err).HTTPIP(r).Msg("invalid auth token") return r } ctx := r.Context() user, err := rt.ur.GetUser(ctx, tokenData.Zid, tokenData.Ident) if err != nil { rt.log.Sense().Zid(tokenData.Zid).Str("ident", tokenData.Ident).Err(err).HTTPIP(r).Msg("auth user not found") return r } return r.WithContext(updateContext(ctx, user, &tokenData)) } func getSessionToken(r *http.Request) []byte { cookie, err := r.Cookie(sessionName) |
︙ | ︙ |
Changes to web/server/server.go.
1 | //----------------------------------------------------------------------------- | | < < < | | | | 1 2 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-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package server provides the Zettelstore web service. package server import ( "context" "net/http" "time" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) // UserRetriever allows to retrieve user data based on a given zettel identifier. type UserRetriever interface { GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) } |
︙ | ︙ |
Changes to 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 | # 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), * [staticcheck](https://staticcheck.io/), * [Fossil](https://fossil-scm.org/), * [Git](https://git-scm.org) (so that Go can download some dependencies). ## Clone the repository Most of this is covered by the excellent Fossil [documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki). 1. Create a directory to store your Fossil repositories. Let's assume, you have created <tt>$HOME/fossils</tt>. 1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossils/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/fossils/zettelstore.fossil`. (If you are not able to use Fossil, you could try the GitHub 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. Some important `COMMAND`s are: * `build`: builds the software with correct version information and puts 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). * `clean`: removes the build directories and cleans the Go cache. * `version`: prints the current version information. Therefore, the easiest way to build your own version of the Zettelstore software is to execute the command ``` go run tools/build.go build ``` 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 | <title>Change Log</title> | < < < < < < < < < < < < < | < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | | | | | | 1 2 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>Change Log</title> <a name="0_9"></a> <h2>Changes for Version 0.9.0 (pending)</h2> <a name="0_8"></a> <h2>Changes for Version 0.8.0 (2022-10-20)</h2> * Remove support for tags within zettel content. Removes also property metadata keys <tt>all-tags</tt> and <tt>computed-tags</tt>. Deprecated in version 0.7.0. (breaking: zettelmarkup, api, webui) * Remove API endpoint <tt>/m</tt>, which retrieve aggregated (tags, roles) zettel identifier. Deprecated in version 0.7.0. (breaking: api) * Remove support for URL query parameter starting with an underscore. Deprecated in version 0.7.0. (breaking: api, webui) * Ignore HTML content by default, and allow HTML gradually by setting startup value <tt>insecure-html</tt>. (breaking: markup) * Endpoint <tt>/q</tt> returns list of full metadata, if no query action is specified. A HTTP call <tt>GET /z</tt> (retrieving metadata of all or some zettel) is now an alias for <tt>GET /q</tt>. (major: api) * Allow to create a zettel that acts as the new version of an existing zettel. Useful if you want to have access to older, outdated content. (minor: webui) * Allow transclusion to reference local image via URL. (minor: zettelmarkup, webui) * Add categories in RSS feed, based on zettel tags. |
︙ | ︙ | |||
433 434 435 436 437 438 439 | enabled. (minor: server) * Fix error that a manual zettel deletion was not always detected. (bug: dirbox) * Some smaller bug fixes and improvements, to the software and to the documentation. | | | | | | | | | | < | | | | | | | | | | | | < | | | | | | | | | | | | | | 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 | enabled. (minor: server) * Fix error that a manual zettel deletion was not always detected. (bug: dirbox) * Some smaller bug fixes and improvements, to the software and to the documentation. <a name="0_7"></a> <h2>Changes for Version 0.7.1 (2022-09-18)</h2> * Produce a RSS feed compatible to Miniflux. (minor) * Make sure to always produce a pubdata in RSS feed. (bug) * Prefix search for data that looks like a zettel identifier may end with a <tt>0</tt>. (bug) * Fix glitch on manual zettel. (bug) <h2>Changes for Version 0.7.0 (2022-09-17)</h2> * Removes support for URL query parameter to search for metadata values, sorting, offset, and limit a zettel list. Deprecated in version 0.6.0 (breaking: api, webui) * Allow to search for the existence / non-existence of a metadata key with the "?" operator: <tt>key?</tt> and <tt>key!?</tt>. Previously, the ":" operator was used for this by specifying an empty search value. Now you can use the ":" operator to find empty / non-empty metadata values. If you specify a search operator for metadata, the specified key is assumed to exist. (breaking: api, webui) * Rename “search expression” into “query expressions”. Similar, the reference prefix <tt>search:</tt> to specify a query link or a query transclusion is renamed to <tt>query:</tt> (breaking: zettelmarkup) * Rename query parameter for query expression from <tt>_s</tt> to <tt>q</tt>. (breaking: api, webui) * Cleanup names for HTTP query parameters in WebUI. Update your bookmarks if you used them. (For API: see below) (breaking: webui) * Allow search terms to be OR-ed. This allows to specify any search expression in disjunctive normal form. Therefore, the NEGATE term is not needed any more. (breaking: api, webui) * Replace runtime configuration <tt>default-lang</tt> with <tt>lang</tt>. Additionally, <tt>lang</tt> set at the zettel of the current user, will provide a default value for the current user, overwriting the global default value. (breaking) * Add new syntax <tt>pikchr</tt>, a markup language for diagrams in technical documentation. (major) * Add endpoint <tt>/q</tt> to query the zettelstore and aggregate resulting values. This is done by extending the query syntax. (major: api) * Add support for query actions. Actions may aggregate w.r.t. some metadata keys, or produce an RSS feed. (major: api, webui) * Query results can be ordered for more than one metadata key. Ordering by zettel identifier is an implicit last order expression to produce stable results. (minor: api, webui) * Add support for an asset directory, accessible via URL prefix <tt>/assests/</tt>. (minor: server) * Add support for metadata key <tt>created</tt>, a timestamp when the zettel was created. Since key <tt>published</tt> is now either <tt>created</tt> or <tt>modified</tt>, it will now always contains a valid time stamp. (minor) * Add support for metadata key <tt>author</tt>. It will be displayed on a zettel, if set. (minor: webui) * Remove CSS for lists. The browsers default value for <tt>padding-left</tt> will be used. (minor: webui) * Removed templates for rendering roles and tags lists. This is now done by query actions. (minor: webui) * Tags within zettel content are deprecated in version 0.8. This affects the computed metadata keys <tt>content-tags</tt> and <tt>all-tags</tt>. They will be removed. The number sign of a content tag introduces unintended tags, esp. in the english language; content tags may occur within links → links within links, when rendered as HTML; content tags may occur in the title of a zettel; naming of content tags, zettel tags, and their union is confusing for many. Migration: use zettel tags or replace content tag with a search. (deprecated: zettelmarkup) * Cleanup names for HTTP query parameter for API calls. Essentially, underscore characters in front are removed. Please use new names, old names will be deprecated in version 0.8. (deprecated: api) * Some smaller bug fixes and improvements, to the software and to the documentation. <a name="0_6"></a> <h2>Changes for Version 0.6.2 (2022-08-22)</h2> * Recognize renaming of zettel file external to Zettelstore. (bug) <h2>Changes for Version 0.6.1 (2022-08-22)</h2> * Ignore empty tags when reading metadata. (bug) |
︙ | ︙ | |||
563 564 565 566 567 568 569 | in version 0.7. Replace these with the more useable search expressions. Please be aware that the = search operator is also deprecated. It was only introduced to help the migration. (deprecated: api, webui) * Some smaller bug fixes and improvements, to the software and to the documentation. | | | 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | in version 0.7. Replace these with the more useable search expressions. Please be aware that the = search operator is also deprecated. It was only introduced to help the migration. (deprecated: api, webui) * Some smaller bug fixes and improvements, to the software and to the documentation. <a name="0_5_0"></a> <h2>Changes for Version 0.5.1 (2022-08-02)</h2> * Log missing authentication tokens in debug level (was: sense level) (major) * Allow to use empty metadata values of string and zmk types. (minor) * Add IP address to some log messages, esp. when authentication fails. (minor) |
︙ | ︙ | |||
585 586 587 588 589 590 591 | * If authentication is enabled, a secret of at least 16 bytes must be set in the startup configuration. (breaking) * “Sexpr” encoding replaces “Native” encoding. Sexpr encoding is much easier to parse, compared with native and ZJSON encoding. In most cases it is smaller than ZJSON. (breaking: api) | | | | | | 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | * If authentication is enabled, a secret of at least 16 bytes must be set in the startup configuration. (breaking) * “Sexpr” encoding replaces “Native” encoding. Sexpr encoding is much easier to parse, compared with native and ZJSON encoding. In most cases it is smaller than ZJSON. (breaking: api) * Endpoint <tt>/r</tt> is changed to <tt>/m?_key=role</tt> and returns now a map of role names to the list of zettel having this role. Endpoint <tt>/t</tt> is changed to <tt>/m?_key=tags</tt>. It already returned mapping described before. (breaking: api) * Remove support for a default value for metadata key title, role, and syntax. Title and role are now allowed to be empty, an empty syntax value defaults to “plain”. (breaking) * Add support for an “evaluation block” syntax in Zettelmarkup to allow interpretation of content by external software. |
︙ | ︙ | |||
628 629 630 631 632 633 634 | (minor: web server) * New startup configuration key <kbd>max-request-size</kbd> to limit a web request body to prevent client sending too large requests. (minor: web server) * Many smaller bug fixes and improvements, to the software and to the documentation. | | | | | 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | (minor: web server) * New startup configuration key <kbd>max-request-size</kbd> to limit a web request body to prevent client sending too large requests. (minor: web server) * Many smaller bug fixes and improvements, to the software and to the documentation. <a name="0_4"></a> <h2>Changes for Version 0.4 (2022-03-08)</h2> * Encoding “djson” renamed to “zjson” (<em>zettel json</em>). (breaking: api; minor: webui) * Remove inline quotation syntax <tt><<...<<</tt>. Now, <tt>""...""</tt> generates the equivalent code. Typographical quotes are generated by the browser, not by Zettelstore. (breaking: Zettelmarkup) * Remove inline formatting for monospace. Its syntax is now used by the similar syntax element of literal computer input. Monospace was just a visual element with no semantic association. Now, the syntax <kbd>++...++</kbd> is obsolete. (breaking: Zettelmarkup). |
︙ | ︙ | |||
665 666 667 668 669 670 671 | interpreted as Zettelmarkup. Similar, the suffix <kbd>-set</kbd> denotes a set/list of words and the suffix <kbd>-zids</kbd> a set/list of zettel identifier. (minor: api, webui) * Change generated URLs for zettel-creation forms. If you have bookmarked them, e.g. to create a new zettel, you should update. (minor: webui) | | | | | | | | | | | | | | | | < | | | | | | | | | | < | | | | | | | | | > | | < | | | | 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 | interpreted as Zettelmarkup. Similar, the suffix <kbd>-set</kbd> denotes a set/list of words and the suffix <kbd>-zids</kbd> a set/list of zettel identifier. (minor: api, webui) * Change generated URLs for zettel-creation forms. If you have bookmarked them, e.g. to create a new zettel, you should update. (minor: webui) * Remove support for metadata key <tt>no-index</tt> to suppress indexing selected zettel. It was introduced in <a href="#0_0_11">v0.0.11</a>, but disallows some future optimizations for searching zettel. (minor: api, webui) * Make some metadata-based searches a little bit faster by executing a (in-memory-based) full-text search first. Now only those zettel are loaded from file that contain the metdata value. (minor: api, webui) * Add an API call to retrieve the version of the Zettelstore. (minor: api) * Limit the amount of zettel and bytes to be stored in a memory box. Allows to use it with public access. (minor: box) * Disallow to cache the authentication cookie. Will remove most unexpected log-outs when using a mobile device. (minor: webui) * Many smaller bug fixes and improvements, to the software and to the documentation. <a name="0_3"></a> <h2>Changes for Version 0.3 (2022-02-09)</h2> * Zettel files with extension <tt>.meta</tt> are now treated as content files. Previoulsy, they were interpreted as metadata files. The interpretation as metadata files was deprecated in version 0.2. (breaking: directory and file/zip box) * Add syntax “draw” to produce some graphical representations. (major) * Add Zettelmarkup syntax to specify full transclusion of other zettel. (major: Zettelmarkup) * Add Zettelmarkup syntax to specify inline-zettel, both for block-structured and for inline-structured elements. (major: Zettelmarkup) * Metadata-returning API calls additionally return an indication about access rights for the given zettel. (minor: api) * A previously duplicate file that is now useful (because another file was deleted) is now logged as such. (minor: directory and file/zip box) * Many smaller bug fixes and improvements, to the software and to the documentation. <a name="0_2"></a> <h2>Changes for Version 0.2 (2022-01-19)</h2> * v0.2.1 (2021-02-01) updates the license year in some documents * Remove support for <tt>;;small text;;</tt> Zettelmarkup. (breaking: Zettelmarkup) * On macOS, the downloadable executable program is now called “zettelstore”, as on all other Unix-like platforms. (possibly breaking: macOS) * External metadata (e.g. for zettel with file extension other than <tt>.zettel</tt>) are stored in files without an extension. Metadata files with extension <tt>.meta</tt> are still recognized, but result in a warning message. In a future version (probably v0.3), <tt>.meta</tt> files will be treated as ordinary content files, possibly resulting in duplicate content. In other words: usage of <tt>.meta</tt> files for storing metadata is deprecated. (possibly breaking: directory and file box) * Show unlinked references in info page of each zettel. Unlinked references are phrases within zettel content that might reference another zettel with the same title as the phase. (major: webui) * Add endpoint <tt>/u/{ID}</tt> to retrieve unlinked references. (major: api) * Provide a logging facility. Log messages are written to standard output. Messages with level “information” are also written to a circular buffer (of length 8192) which can be retrieved via a computed zettel. There is a command line flag <tt>-l LEVEL</tt> to specify an application global logging level on startup (default: “information”). Logging level can also be changed via the administrator console, even for specific (sub-) services. (major) * The internal handling of zettel files is rewritten. This allows less reloads ands detects when the directory containing the zettel files is removed. The API, WebUI, and the admin console allow to manually refresh the internal state on demand. (major: box, webui) * <tt>.zettel</tt> files with YAML header are now correctly written. (bug) * Selecting zettel based on their metadata allows the same syntax as searching for zettel content. For example, you can list all zettel that have an identifier not ending with <tt>00</tt> by using the query <tt>id=!<00</tt>. (minor: api, webui) * Remove support for <tt>//deprecated emphasized//</tt> Zettelmarkup. (minor: Zettelmarkup) * Add options to profile the software. Profiling can be enabled at the command line or via the administrator console. (minor) * Add computed zettel that lists all supported parser / recognized zettel syntaxes. (minor) * Add API call to check for enabled authentication. (minor: api) * Renewing an API access token works even if authentication is not enabled. This corresponds to the behaviour of optaining an access token. (minor: api) * If there is nothing to return, use HTTP status code 204, instead of 200 + <tt>Content-Length: 0</tt>. (minor: api) * Metadata key <tt>duplicates</tt> stores the duplicate file names, instead of just a boolean value that there were duplicate file names. (minor) * Document autostarting Zettelstore on Windows, macOS, and Linux. (minor) * Many smaller bug fixes and improvements, to the software and to the documentation. <a name="0_1"></a><a name="0_1_0"></a> <h2>Changes for Version 0.1 (2021-11-11)</h2> * v0.1.3 (2021-12-15) fixes a bug where the modification date could be set when a new zettel is created. * v0.1.2 (2021-11-18) fixes a bug when selecting zettel from a list when more than one comparison is negated. * v0.1.1 (2021-11-12) updates the documentation, mostly related to the deprecation of the <tt>//</tt> markup. * Remove visual Zettelmarkup (italic, underline). Semantic Zettelmarkup (emphasize, insert) is still allowed, but got a different syntax. The new syntax for <ins>inserted text</ins> is <tt>>>inserted>></tt>, while its previous syntax now denotes <em>emphasized text</em>: <tt>__emphasized__</tt>. The previous syntax for emphasized text is now deprecated: <tt>//deprecated emphasized//</tt>. Starting with Version 0.2.0, the deprecated syntax will not be supported. The reason is the collision with URLs that also contain the characters <tt>//</tt>. The ZMK encoding of a zettel may help with the transition (<tt>/v/{ZettelID}?_part=zettel&_enc=zmk</tt>, on the Info page of each zettel in the WebUI). Additionally, all deprecated uses of <tt>//</tt> will be rendered with a dashed box within the WebUI. (breaking: Zettelmarkup). * API client software is now a [https://zettelstore.de/client/|separate] project. (breaking) * Initial support for HTTP security headers (Content-Security-Policy, Permissions-Policy, Referrer-Policy, X-Content-Type-Options, X-Frame-Options). Header values are currently some constant values. (possibly breaking: api, webui) * Remove visual Zettelmarkup (bold, striketrough). Semantic Zettelmarkup (strong, delete) is still allowed and replaces the visual elements syntactically. The visual appearance should not change (depends on your changes / additions to CSS zettel). (possibly breaking: Zettelmarkup). * Add API endpoint <tt>POST /v</tt> to retrieve HTMl and text encoded strings from given ZettelMarkup encoded values. This will be used to render a HTML page from a given zettel: in many cases the title of a zettel must be treated separately. (minor: api) * Add API endpoint <tt>/m</tt> to retrieve only the metadata of a zettel. (minor: api) * New metadata value <tt>content-tags</tt> contains the tags that were given in the zettel content. To put it simply, <tt>all-tags</tt> = <tt>tags</tt> + <tt>content-tags</tt>. (minor) * Calculating the context of a zettel stops at the home zettel. (minor: api, webui) * When renaming or deleting a zettel, a warning will be given, if other zettel references the given zettel, or when “deleting” will uncover zettel in overlay box. (minor: webui) |
︙ | ︙ | |||
839 840 841 842 843 844 845 | * Separate repository for [https://zettelstore.de/contrib/|contributed] software. First entry is a software for creating a presentation by using zettel. (info) * Many smaller bug fixes and improvements, to the software and to the documentation. | | | < | | < > | > | | | | | | | | | | | | | | | | | | | | | | | | | | | < | | | | | | | | | | | | | | | < | | | | | | | | | < | < | | | | < | | | < | | | | | | | | | | < | | < | | | < | < | < | | < | < | < | | | | | | | | | | | | | | | | | | < | | | | | | | | | | | | | | | | | | | | | | | < | | | | | | | | | | | < | | | | | | | | | | 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 | * Separate repository for [https://zettelstore.de/contrib/|contributed] software. First entry is a software for creating a presentation by using zettel. (info) * Many smaller bug fixes and improvements, to the software and to the documentation. <a name="0_0_15"></a> <h2>Changes for Version 0.0.15 (2021-09-17)</h2> * Move again endpoint characters for authentication to make room for future features. WebUI authentication moves from <tt>/a</tt> to <tt>/i</tt> (login) and <tt>/i?logout</tt> (logout). API authentication moves from <tt>/v</tt> to </tt>/a</tt>. JSON-based basic zettel handling moves from <tt>/z</tt> to <tt>/j</tt> and <tt>/z/{ID}</tt> to <tt>/j/{ID}</tt>. Since the API client is updated too, this should not be a breaking change for most users. (minor: api, webui; possibly breaking) * Add API endpoint <tt>/v/{ID}</tt> to retrieve an evaluated zettel in various encodings. Mostly replaces endpoint <tt>/z/{ID}</tt> for other encodings except “json” and “raw”. Endpoint <tt>/j/{ID}</tt> now only returns JSON data, endpoint <tt>/z/{ID}</tt> is used to retrieve plain zettel data (previously called “raw”). See documentation for details. (major: api; breaking) * Metadata values of type <em>tag set</em> (the metadata with key <tt>tags</tt> is its most prominent example), are now compared in a case-insensitive manner. Tags that only differ in upper / lower case character are now treated identical. This might break your workflow, if you depend on case-sensitive comparison of tag values. Tag values are translated to their lower case equivalent before comparing them and when you edit a zettel through Zettelstore. If you just modify the zettel files, your tag values remain unchanged. (major; breaking) * Endpoint <tt>/z/{ID}</tt> allows the same methods as endpoint <tt>/j/{ID}</tt>: <tt>GET</tt> retrieves zettel (see above), <tt>PUT</tt> updates a zettel, <tt>DELETE</tt> deletes a zettel, <tt>MOVE</tt> renames a zettel. In addtion, <tt>POST /z</tt> will create a new zettel. When zettel data must be given, the format is plain text, with metadata separated from content by an empty line. See documentation for more details. (major: api (plus WebUI for some details)) * Allows to transclude / expand the content of another zettel into a target zettel when the zettel is rendered. By using the syntax of embedding an image (which is some kind of expansion too), the first top-level paragraph of a zettel may be transcluded into the target zettel. Endless recursion is checked, as well as a possible “transclusion bomb ” (similar to a XML bomb). See manual for details. (major: zettelmarkup) * The endpoint <tt>/z</tt> allows to list zettel in a simpler format than endpoint <tt>/j</tt>: one line per zettel, and only zettel identifier plus zettel title. (minor: api) * Folgezettel are now displayed with full title at the bottom of a page. (minor: webui) * Add API endpoint <tt>/p/{ID}</tt> to retrieve a parsed, but not evaluated zettel in various encodings. (minor: api) * Fix: do not list a shadowed zettel that matches the select criteria. (minor) * Many smaller bug fixes and improvements, to the software and to the documentation. <a name="0_0_14"></a> <h2>Changes for Version 0.0.14 (2021-07-23)</h2> * Rename “place” into “box”. This also affects the configuration keys to specify boxes <tt>box-uri<em>X</em></tt> (previously <tt>place-uri-<em>X</em></tt>. Older changes documented here are renamed too. (breaking) * Add API for creating, updating, renaming, and deleting zettel. (major: api) * Initial API client for Go. (major: api) * Remove support for paging of WebUI list. Runtime configuration key <tt>list-page-size</tt> is removed. If you still specify it, it will be ignored. (major: webui) * Use endpoint <tt>/v</tt> for user authentication via API. Endpoint <tt>/a</tt> is now used for the web user interface only. Similar, endpoint <tt>/y</tt> (“zettel context”) is renamed to <tt>/x</tt>. (minor, possibly breaking) * Type of used-defined metadata is determined by suffix of key: <tt>-number</tt>, <tt>-url</tt>, <tt>-zid</tt> will result the values to be interpreted as a number, an URL, or a zettel identifier. (minor, but possibly breaking if you already used a metadata key with above suffixes, but as a string type) * New <tt>user-role</tt> “creator”, which is only allowed to create new zettel (except user zettel). This role may only read and update public zettel or its own user zettel. Added to support future client software (e.g. on a mobile device) that automatically creates new zettel but, in case of a password loss, should not allow to read existing zettel. (minor, possibly breaking, because new zettel template zettel must always prepend the string <tt>new-</tt> before metdata keys that should be transferred to the new zettel) * New suported metadata key <tt>box-number</tt>, which gives an indication from which box the zettel was loaded. (minor) * New supported syntax <tt>html</tt>. (minor) * New predefined zettel “User CSS” that can be used to redefine some predefined CSS (without modifying the base CSS zettel). (minor: webui) * When a user moves a zettel file with additional characters into the box directory, these characters are preserved when zettel is updated. (bug) * The phase “filtering a zettel list” is more precise “selecting zettel” (documentation) * Many smaller bug fixes and improvements, to the software and to the documentation. <a name="0_0_13"></a> <h2>Changes for Version 0.0.13 (2021-06-01)</h2> * Startup configuration <tt>box-<em>X</em>-uri</tt> (where <em>X</em> is a number greater than zero) has been renamed to <tt>box-uri-<em>X</em></tt>. (breaking) * Web server processes startup configuration <tt>url-prefix</tt>. There is no need for stripping the prefix by a front-end web server any more. (breaking: webui, api) * Administrator console (only optional accessible locally). Enable it only on systems with a single user or with trusted users. It is disabled by default. (major: core) * Remove visibility value “simple-expert” introduced in [#0_0_8|version 0.0.8]. It was too complicated, esp. authorization. There was a name collision with the “simple” directory box sub-type. (major) * For security reasons, HTML blocks are not encoded as HTML if they contain certain snippets, such as <tt><script</tt> or <tt><iframe</tt>. These may be caused by using CommonMark as a zettel syntax. (major) * Full-text search can be a prefix search or a search for equal words, in addition to the search whether a word just contains word of the search term. (minor: api, webui) * Full-text search for URLs, with above additional operators. (minor: api, webui) * Add system zettel about license, contributors, and dependencies (and their license). For a nicer layout of zettel identifier, the zettel about environment values and about runtime metrics got new zettel identifier. This affects only user that referenced those zettel. (minor) * Local images that cannot be read (not found or no access rights) are substituted with the new default image, a spinning emoji. See [/file?name=box/constbox/emoji_spin.gif]. (minor: webui) * Add zettelmarkup syntax for a table row that should be ignored: <tt>|%</tt>. This allows to paste output of the administrator console into a zettel. (minor: zmk) * Many smaller bug fixes and improvements, to the software and to the documentation. <a name="0_0_12"></a> <h2>Changes for Version 0.0.12 (2021-04-16)</h2> * Raise the per-process limit of open files on macOS to 1.048.576. This allows most macOS users to use at least 500.000 zettel. That should be enough for the near future. (major) * Mitigate the shortcomings of the macOS version by introducing types of directory boxes. The original directory box type is now called "notify" (the default value). There is a new type called "simple". This new type does not notify Zettelstore when some of the underlying Zettel files change. (major) * Add new startup configuration <tt>default-dir-box-type</tt>, which gives the default value for specifying a directory box type. The default value is “notify”. On macOS, the default value may be changed “simple” if some errors occur while raising the per-process limit of open files. (minor) <a name="0_0_11"></a> <h2>Changes for Version 0.0.11 (2021-04-05)</h2> * New box schema "file" allows to read zettel from a ZIP file. A zettel collection can now be packaged and distributed easier. (major: server) * Non-restricted search is a full-text search. The search string will be normalized according to Unicode NFKD. Every character that is not a letter or a number will be ignored for the search. It is sufficient if the words to be searched are part of words inside a zettel, both content and metadata. (major: api, webui) * A zettel can be excluded from being indexed (and excluded from being found in a search) if it contains the metadata <tt>no-index: true</tt>. (minor: api, webui) * Menu bar is shown when displaying error messages. (minor: webui) * When selecting zettel, it can be specified that a given value should <em>not</em> match. Previously, only the whole select criteria could be negated (which is still possible). (minor: api, webui) * You can select a zettel by specifying that specific metadata keys must (or must not) be present. (minor: api, webui) * Context of a zettel (introduced in version 0.0.10) does not take tags into account any more. Using some tags for determining the context resulted into erratic, non-deterministic context lists. (minor: api, webui) * Selecting zettel depending on tag values can be both by comparing only the prefix or the whole string. If a search value begins with '#', only zettel with the exact tag will be returned. Otherwise a zettel will be returned if the search string just matches the prefix of only one of its tags. (minor: api, webui) * Many smaller bug fixes and improvements, to the software and to the documentation. A note for users of macOS: in the current release and with macOS's default values, a zettel directory must not contain more than approx. 250 files. There are three options to mitigate this limitation temporarily: # You update the per-process limit of open files on macOS. # You setup a virtualization environment to run Zettelstore on Linux or Windows. # You wait for version 0.0.12 which addresses this issue. <a name="0_0_10"></a> <h2>Changes for Version 0.0.10 (2021-02-26)</h2> * Menu item “Home” 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 “table of contents” 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 “New Menu” 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 ”New“ 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 (“<code>//</code>”) 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>[[Zettel list|//h]]</code> will render as <code><a href="/manual/h">Zettel list</a></code>. (minor: syntax) * Searching/selecting ignores the leading '#' character of tags. (minor: api, webui) * When result of selecting or searching is presented, the query is written as the page heading. (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 “&#10138;” (“➚”). It is more beautiful than the previous “&#8599;&#xfe0e;” (“↗︎”), which also needed the additional “&#xfe0e;” 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 improvements, 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 “Identifier” or “IdentifierSet” 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 “smart backlinks”. * (minor) If Zettelstore is being stopped, an appropriate message is written in the console log. * (minor) New computed zettel with environmental data, the list of supported meta data keys, and statistics about all configured zettel boxes. Some other computed zettel got a new identifier (to make room for other variant). * (minor) Remove zettel <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 “Zettelmarkup”. It is a non-empty string, that will be formatted with Zettelmarkup. <tt>title</tt> and <tt>default-title</tt> have this type. * (major) Rename zettel syntax “meta” to “none”. Please update the <i>Zettelstore Runtime Configuration</i> and all other zettel that previously used the value “meta”. Other zettel are typically user zettel, used for authentication. However, there is no real harm, if you do not update these zettel. In this case, the metadata is just not presented when rendered. Zettelstore will still work. * (minor) Login will take at least 500 milliseconds to mitigate login attacks. This affects both the API and the WebUI. * (minor) Add a sort option “_random” to produce a zettel list in random order. <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 real backlinks, they are shown at the botton of the page (“Additional links to this zettel”). * (minor) All property metadata, even computed metadata is shown in the info page of a zettel. * (minor) Rendering of metadata keys <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> <h2>Changes for Version 0.0.8 (2020-12-23)</h2> <h3>Server / API</h3> * (bug) Zettel files with extension <tt>.jpg</tt> and without metadata will get a <tt>syntax</tt> value “jpg”. The internal data structure got the same value internally, instead of “jpeg”. This has been fixed for all possible alternative syntax values. * (bug) If a file, e.g. an image file like <tt>20201130190200.jpg</tt>, is added to the directory box, its metadata are just calculated from the information available. Updated metadata did not find its way into the zettel box, because the <tt>.meta</tt> file was not written. * (bug) If just the <tt>.meta</tt> file was deleted manually, the zettel was assumed to be missing. A workaround is to restart the software. If the <tt>.meta</tt> file is deleted, metadata is now calculated in the same way when the <tt>.meta</tt> file is non-existing at the start of the software. * (bug) A link to the current zettel, only using a fragment (e.g. <code>[[Title|#title]]</code>) is now handled correctly as a zettel link (and not as a link to external material). * (minor) Allow zettel to be marked as “read only”. This is done through the metadata key <tt>read-only</tt>. * (bug) When renaming a zettel, check all boxes for the new zettel identifier, not just the first one. Otherwise it will be possible to shadow a read-only zettel from a next box, effectively modifying it. * (minor) Add support for a configurable default value for metadata key <tt>visibility</tt>. * (bug) If <tt>list-page-size</tt> is set to a relatively small value and the authenticated user is <i>not</i> the owner, some zettel were not shown in the list of zettel or were not returned by the API. * (minor) Add support for new visibility “expert”. An owner becomes an expert, if the runtime configuration key <tt>expert-mode</tt> is set to true. * (major) Add support for computed zettel. These zettel have an identifier less than <tt>0000000000100</tt>. Most of them are only visible, if <tt>expert-mode</tt> is enabled. * (bug) Fixes a memory leak that results in too many open files after approx. 125 reload operations. * (major) Predefined templates for new zettel got an explicit value for visibility: “login”. Please update these zettel if you modified them. * (major) Rename key <tt>readonly</tt> of <i>Zettelstore Startup Configuration</i> to <tt>read-only-mode</tt>. This was done to avoid some confusion with the the zettel metadata key <tt>read-only</tt>. <b>Please adapt your startup configuration. Otherwise your Zettelstore will be accidentally writable.</b> * (minor) References starting with “./” and “../” are treated as a local reference. Previously, only the prefix “/” was treated as a local reference. * (major) Metadata key <tt>modified</tt> will be set automatically to the current local time if a zettel is updated through Zettelstore. <b>If you used that key previously for your own, you should rename it before you upgrade.</b> * (minor) The new visibility value “simple-expert” ensures that many computed zettel are shown for new users. This is to enable them to send useful bug reports. * (minor) When a zettel is stored as a file, its identifier is additionally stored within the metadata. This helps for better robustness in case the file names were corrupted. In addition, there could be a tool that compares the identifier with the file name. <h3>WebUI</h3> * (minor) Remove list of tags in “List Zettel” 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 “clone” to “copy”. * (major) All predefined HTML templates have now a visibility value “expert”. If you want to see them as an non-expert owner, you must temporary enable <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 “Folge” (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 “Zettelstore”. * (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> * (major) Rename identifier of <i>Zettelstore Runtime Configuration</i> to <tt>00000000000100</tt> (previously <tt>00000000000001</tt>). This is done to gain some free identifier with smaller number to be used internally. <b>If you customized this zettel, please make sure to rename it to the new identifier.</b> * (major) Rename the two essential metadata keys of a user zettel to <tt>credential</tt> and <tt>user-id</tt>. The previous values were <tt>cred</tt> and <tt>ident</tt>. <b>If you enabled user authentication and added some user zettel, make sure to change them accordingly. Otherwise these users will not authenticated any more.</b> * (minor) Rename the scheme of the box URL where predefined zettel are stored to “const”. The previous value was “globals”. <h3>Zettelmarkup</h3> * (bug) Allow to specify a <i>fragment</i> in a reference to a zettel. Used to link to an internal position within a zettel. This applies to CommonMark too. <h3>API</h3> * (bug) Encoding binary content in format “json” now results in valid JSON content. * (bug) All query parameters of selecting zettel must be true, regardless if a specific key occurs more than one or not. * (minor) Encode all inherited meta values in all formats except “raw”. A meta value is called <i>inherited</i> if there is a key starting with <tt>default-</tt> in the <i>Zettelstore Runtime Configuration</i>. Applies to WebUI also. * (minor) Automatic calculated identifier for headings (only for “html”, “djson”, “native” format and for the Web user interface). You can use this to provide a zettel reference that links to the heading, without specifying an explicit mark (<code>[!mark]</code>). * (major) Allow to retrieve all references of a given zettel. |
︙ | ︙ | |||
1338 1339 1340 1341 1342 1343 1344 | nor host name, are considered “local references” (in contrast to “zettel references” and “external references”). When a local reference is displayed as an URL on the WebUI, it will not opened in a new window/tab. They will receive a <i>local</i> marker, when encoded as “djson” or “native”. Local references are listed on the <i>Info page</i> of each zettel. | | | | | < | | | | | | | | | | | < | | | | | | | < | | 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 | nor host name, are considered “local references” (in contrast to “zettel references” and “external references”). When a local reference is displayed as an URL on the WebUI, it will not opened in a new window/tab. They will receive a <i>local</i> marker, when encoded as “djson” or “native”. Local references are listed on the <i>Info page</i> of each zettel. * (minor) Change the default value for some visual sugar putd after an external URL to <tt>&\#8599;&\#xfe0e;</tt> (“↗︎”). This affects the former key <tt>icon-material</tt> of the <i>Zettelstore Runtime Configuration</i>, which is renamed to <tt>marker-external</tt>. * (major) Allow multiple zettel to act as templates for creating new zettel. All zettel with a role value “new-template” act as a template to create a new zettel. The WebUI menu item “New” changed to a drop-down list with all those zettel, ordered by their identifier. All metadata keys with the prefix <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 “New Zettel” 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 “New Menu” zettel. * (minor) When a page should be opened in a new windows (e.g. for external references), the web browser is instructed to decouple the new page from the previous one for privacy and security reasons. In detail, the web browser is instructed to omit referrer information and to omit a JS object linking to the page that contained the external link. * (minor) If the value of the <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. <a name="0_0_5"></a> <h2>Changes for Version 0.0.5 (2020-10-22)</h2> * Application Programming Interface (API) to allow external software to retrieve zettel data from the Zettelstore. * Specify boxes, where zettel are stored, via an URL. * Add support for a custom footer. <a name="0_0_4"></a> <h2>Changes for Version 0.0.4 (2020-09-11)</h2> * Optional user authentication/authorization. * New sub-commands <tt>file</tt> (use Zettelstore as a command line filter), <tt>password</tt> (for authentication), and <tt>config</tt>. <a name="0_0_3"></a> <h2>Changes for Version 0.0.3 (2020-08-31)</h2> * Starting Zettelstore has been changed by introducing sub-commands. This change is also reflected on the server installation procedures. * Limitations on renaming zettel has been relaxed. <a name="0_0_2"></a> <h2>Changes for Version 0.0.2 (2020-08-28)</h2> * Configuration zettel now has ID <tt>00000000000001</tt> (previously: <tt>00000000000000</tt>). * The zettel with ID <tt>00000000000000</tt> is no longer shown in any zettel list. If you changed the configuration zettel, you should rename it manually in its file directory. * Creating a new zettel is now done by cloning an existing zettel. To mimic the previous behaviour, a zettel with ID <tt>00000000040001</tt> is introduced. You can change it if you need a different template zettel. <a name="0_0_1"></a> <h2>Changes for Version 0.0.1 (2020-08-21)</h2> * Initial public release. |
Changes to www/download.wiki.
1 2 3 4 5 6 7 8 9 10 11 | <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> | | | | | | | | | 1 2 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>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.8.0</code> (2022-10-20). * [/uv/zettelstore-0.8.0-linux-amd64.zip|Linux] (amd64) * [/uv/zettelstore-0.8.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore-0.8.0-windows-amd64.zip|Windows] (amd64) * [/uv/zettelstore-0.8.0-darwin-amd64.zip|macOS] (amd64) * [/uv/zettelstore-0.8.0-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.8.0.zip|here]. Just unzip the contained files and put them into your zettel folder or configure a file box to read the zettel directly from the ZIP file. |
Changes to www/index.wiki.
︙ | ︙ | |||
12 13 14 15 16 17 18 | 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)]. | | | | | | < < | | | | | | | | | 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 | 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://zettelstore.de/client|Zettelstore Client] provides client software to access Zettelstore via its API more easily, [https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed software, which often connects to Zettelstore via its API. Some of the software packages may be experimental. [https://twitter.com/zettelstore|Stay tuned] … <hr> <h3>Latest Release: 0.8.0 (2022-10-20)</h3> * [./download.wiki|Download] * [./changes.wiki#0_8|Change summary] * [/timeline?p=v0.8.0&bt=v0.7.0&y=ci|Check-ins for version 0.8.0], [/vdiff?to=v0.8.0&from=v0.7.0|content diff] * [/timeline?df=v0.8.0&y=ci|Check-ins derived from the 0.8.0 release], [/vdiff?from=v0.8.0&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://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 "anonymous"). |
Changes to www/plan.wiki.
1 2 3 4 5 | <title>Limitations and planned improvements</title> Here is a list of some shortcomings of Zettelstore. They are planned to be solved. | > > > | > > > | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <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 put your file into the directory where your zettel are stored. Make sure that the file name starts with unique 14 digits that make up the zettel identifier. * … <h3>Smaller limitations</h3> * Quoted attribute values are not yet supported in Zettelmarkup: <code>{key="value with space"}</code>. * The horizontal tab character (<tt>U+0009</tt>) is not supported. * Missing support for citation keys. * Changing the content syntax is not reflected in file extension. * File names with additional text besides the zettel identifier are not always preserved. |
︙ | ︙ |
Deleted zettel/content.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/content_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/digraph.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/digraph_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/edge.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/id.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/id_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/set.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/set_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/slice.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/slice_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/collection.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/meta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/meta_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/parse.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/parse_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/type.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/type_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/values.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/write.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/write_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/zettel.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |