Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From v0.4 To v0.5.1
2022-08-02
| ||
10:06 | Merge changes from release 0.5.1 ... (check-in: ad7c201f7e user: stern tags: trunk) | |
09:27 | Version 0.5.1 ... (Leaf check-in: e10f2a270d user: stern tags: release, v0.5.1, release-0.5) | |
09:09 | Add IP address when logging about some auth problems ... (check-in: b6687c0706 user: stern tags: release-0.5) | |
2022-03-10
| ||
13:20 | Increase version to 0.5-dev to begin next development cycle ... (check-in: 73f6c7eb13 user: stern tags: trunk) | |
2022-03-09
| ||
14:06 | Version 0.4 ... (check-in: 54ed47f372 user: stern tags: trunk, release, v0.4) | |
13:54 | Update release checklist ... (check-in: 711a670d7a user: stern tags: trunk) | |
Changes to VERSION.
|
| | | 1 | 0.5.1 |
Changes to ast/ast.go.
︙ | ︙ | |||
61 62 63 64 65 66 67 | // InlineNode is the interface that all inline nodes must implement. type InlineNode interface { Node inlineNode() } | < < < < < < < | 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | // InlineNode is the interface that all inline nodes must implement. type InlineNode interface { Node inlineNode() } // Reference is a reference to external or internal material. type Reference struct { URL *url.URL Value string State RefState } |
︙ | ︙ |
Changes to ast/block.go.
1 2 3 4 5 6 7 8 9 10 11 12 | //----------------------------------------------------------------------------- // 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 | | | 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 */ } |
︙ | ︙ | |||
51 52 53 54 55 56 57 | Inlines InlineSlice } func (*ParaNode) blockNode() { /* Just a marker */ } func (*ParaNode) itemNode() { /* Just a marker */ } func (*ParaNode) descriptionNode() { /* Just a marker */ } | < < < | < | < | < < | | > > > > > > > | | | 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 | Inlines InlineSlice } func (*ParaNode) blockNode() { /* Just a marker */ } func (*ParaNode) itemNode() { /* Just a marker */ } func (*ParaNode) descriptionNode() { /* Just a marker */ } // CreateParaNode creates a parameter block from inline nodes. func CreateParaNode(nodes ...InlineNode) *ParaNode { return &ParaNode{Inlines: nodes} } // WalkChildren walks down the inline elements. func (pn *ParaNode) WalkChildren(v Visitor) { Walk(v, &pn.Inlines) } //-------------------------------------------------------------------------- // VerbatimNode contains uninterpreted text type VerbatimNode struct { Kind VerbatimKind Attrs attrs.Attributes Content []byte } // VerbatimKind specifies the format that is applied to code inline nodes. type VerbatimKind int // Constants for VerbatimCode const ( _ VerbatimKind = iota VerbatimZettel // Zettel content VerbatimProg // Program code VerbatimEval // Code to be externally interpreted. Syntax is stored in default attribute. VerbatimComment // Block comment VerbatimHTML // Block HTML, e.g. for Markdown VerbatimMath // Block math mode ) func (*VerbatimNode) blockNode() { /* Just a marker */ } func (*VerbatimNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ } // Supported syntax values for VerbatimEval. const ( VerbatimEvalSyntaxDraw = "draw" ) //-------------------------------------------------------------------------- // RegionNode encapsulates a region of block nodes. type RegionNode struct { Kind RegionKind Attrs attrs.Attributes Blocks BlockSlice Inlines InlineSlice // Optional text at the end of the region } // RegionKind specifies the actual region type. type RegionKind int // Values for RegionCode const ( _ RegionKind = iota RegionSpan // Just a span of blocks RegionQuote // A longer quotation RegionVerse // Line breaks matter |
︙ | ︙ | |||
126 127 128 129 130 131 132 | } //-------------------------------------------------------------------------- // HeadingNode stores the heading text and level. type HeadingNode struct { Level int | | | | < < | | | 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 | } //-------------------------------------------------------------------------- // HeadingNode stores the heading text and level. type HeadingNode struct { Level int Attrs attrs.Attributes Slug string // Heading text, normalized Fragment string // Heading text, suitable to be used as an unique URL fragment Inlines InlineSlice // Heading text, possibly formatted } func (*HeadingNode) blockNode() { /* Just a marker */ } func (*HeadingNode) itemNode() { /* Just a marker */ } // WalkChildren walks the heading text. func (hn *HeadingNode) WalkChildren(v Visitor) { Walk(v, &hn.Inlines) } //-------------------------------------------------------------------------- // HRuleNode specifies a horizontal rule. type HRuleNode struct { Attrs attrs.Attributes } func (*HRuleNode) blockNode() { /* Just a marker */ } func (*HRuleNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (*HRuleNode) WalkChildren(Visitor) { /* No children*/ } //-------------------------------------------------------------------------- // NestedListNode specifies a nestable list, either ordered or unordered. type NestedListNode struct { Kind NestedListKind Items []ItemSlice Attrs attrs.Attributes } // NestedListKind specifies the actual list type. type NestedListKind uint8 // Values for ListCode const ( |
︙ | ︙ |
Changes to ast/inline.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package ast import ( "unicode/utf8" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- package ast import ( "unicode/utf8" "zettelstore.de/c/attrs" ) // Definitions of inline nodes. // InlineSlice is a list of BlockNodes. type InlineSlice []InlineNode |
︙ | ︙ | |||
96 97 98 99 100 101 102 103 | // WalkChildren does nothing. func (*BreakNode) WalkChildren(Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // LinkNode contains the specified link. type LinkNode struct { Ref *Reference | > | < > < < > | < | < < | > < | < | < < > < | < < | 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 | // WalkChildren does nothing. func (*BreakNode) WalkChildren(Visitor) { /* No children*/ } // -------------------------------------------------------------------------- // LinkNode contains the specified link. type LinkNode struct { Attrs attrs.Attributes // Optional attributes Ref *Reference Inlines InlineSlice // The text associated with the link. } func (*LinkNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the link text. func (ln *LinkNode) WalkChildren(v Visitor) { if len(ln.Inlines) > 0 { Walk(v, &ln.Inlines) } } // -------------------------------------------------------------------------- // EmbedRefNode contains the specified embedded reference material. type EmbedRefNode struct { Attrs attrs.Attributes // Optional attributes Ref *Reference // The reference to be embedded. Syntax string // Syntax of referenced material, if known Inlines InlineSlice // Optional text associated with the image. } func (*EmbedRefNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the text that describes the embedded material. func (en *EmbedRefNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) } // -------------------------------------------------------------------------- // EmbedBLOBNode contains the specified embedded BLOB material. type EmbedBLOBNode struct { Attrs attrs.Attributes // Optional attributes Syntax string // Syntax of Blob Blob []byte // BLOB data itself. Inlines InlineSlice // Optional text associated with the image. } func (*EmbedBLOBNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the text that describes the embedded material. func (en *EmbedBLOBNode) WalkChildren(v Visitor) { Walk(v, &en.Inlines) } // -------------------------------------------------------------------------- // CiteNode contains the specified citation. type CiteNode struct { Attrs attrs.Attributes // Optional attributes Key string // The citation key Inlines InlineSlice // Optional text associated with the citation. } func (*CiteNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the cite text. func (cn *CiteNode) WalkChildren(v Visitor) { Walk(v, &cn.Inlines) } // -------------------------------------------------------------------------- // MarkNode contains the specified merked position. // It is a BlockNode too, because although it is typically parsed during inline // mode, it is moved into block mode afterwards. type MarkNode struct { |
︙ | ︙ | |||
187 188 189 190 191 192 193 194 | } } // -------------------------------------------------------------------------- // FootnoteNode contains the specified footnote. type FootnoteNode struct { Inlines InlineSlice // The footnote text. | > < | < < | | | < < | | > | 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 | } } // -------------------------------------------------------------------------- // FootnoteNode contains the specified footnote. type FootnoteNode struct { Attrs attrs.Attributes // Optional attributes Inlines InlineSlice // The footnote text. } func (*FootnoteNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the footnote text. func (fn *FootnoteNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) } // -------------------------------------------------------------------------- // FormatNode specifies some inline formatting. type FormatNode struct { Kind FormatKind Attrs attrs.Attributes // Optional attributes. Inlines InlineSlice } // FormatKind specifies the format that is applied to the inline nodes. type FormatKind int // Constants for FormatCode const ( _ FormatKind = iota FormatEmph // Emphasized text. FormatStrong // Strongly emphasized text. FormatInsert // Inserted text. FormatDelete // Deleted text. FormatSuper // Superscripted text. FormatSub // SubscriptedText. FormatQuote // Quoted text. FormatSpan // Generic inline container. ) func (*FormatNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the formatted text. func (fn *FormatNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) } // -------------------------------------------------------------------------- // LiteralNode specifies some uninterpreted text. type LiteralNode struct { Kind LiteralKind Attrs attrs.Attributes // Optional attributes. Content []byte } // LiteralKind specifies the format that is applied to code inline nodes. type LiteralKind int // Constants for LiteralCode const ( _ LiteralKind = iota LiteralZettel // Zettel content LiteralProg // Inline program code LiteralInput // Computer input, e.g. Keyboard strokes LiteralOutput // Computer output LiteralComment // Inline comment LiteralHTML // Inline HTML, e.g. for Markdown LiteralMath // Inline math mode ) func (*LiteralNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (*LiteralNode) WalkChildren(Visitor) { /* No children*/ } |
Changes to ast/ref.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ |
Changes to ast/ref_test.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ |
Changes to ast/walk.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ |
Changes to ast/walk_test.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package ast_test import ( "testing" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- 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"), |
︙ | ︙ | |||
42 43 44 45 46 47 48 | }, &ast.ParaNode{ Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "intermediate", "text."), }, ast.CreateParaNode( &ast.FormatNode{ Kind: ast.FormatEmph, | | | 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | }, &ast.ParaNode{ Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "intermediate", "text."), }, ast.CreateParaNode( &ast.FormatNode{ Kind: ast.FormatEmph, Attrs: attrs.Attributes(map[string]string{ "": "class", "color": "green", }), Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "emphasized", "text."), }, &ast.SpaceNode{Lexeme: " "}, &ast.LinkNode{ |
︙ | ︙ |
Changes to auth/impl/impl.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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. |
︙ | ︙ |
Changes to box/constbox/base.css.
︙ | ︙ | |||
121 122 123 124 125 126 127 | clear: both; visibility: hidden; } main form div { margin: .5em 0 0 0 } input { font-family: monospace } input[type="submit"],button,select { font: inherit } label { font-family: sans-serif; font-size:.9rem } | < < | < < > | | 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 | clear: both; visibility: hidden; } main form div { margin: .5em 0 0 0 } input { font-family: monospace } input[type="submit"],button,select { font: inherit } label { font-family: sans-serif; font-size:.9rem } textarea { font-family: monospace; resize: vertical; width: 100%; } .zs-input { padding: .5em; display:block; border:none; border-bottom:1px solid #ccc; width:100%; } input.zs-primary { float:right } input.zs-secondary { float:left } 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 } code,pre { font-family: monospace; font-size: 85%; |
︙ | ︙ |
Changes to box/constbox/base.mustache.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <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}}}"> <title>{{Title}}</title> </head> <body> <nav class="zs-menu"> <a href="{{{HomeURL}}}">Home</a> {{#WithUser}} <div class="zs-dropdown"> | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <!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"> |
︙ | ︙ | |||
52 53 54 55 56 57 58 | <form action="{{{SearchURL}}}"> <input type="text" placeholder="Search.." name="{{QueryKeySearch}}"> </form> </nav> <main class="content"> {{{Content}}} </main> | | > | < | < | 56 57 58 59 60 61 62 63 64 65 66 | <form action="{{{SearchURL}}}"> <input type="text" placeholder="Search.." name="{{QueryKeySearch}}"> </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> |
Changes to box/constbox/constbox.go.
︙ | ︙ | |||
291 292 293 294 295 296 297 298 299 | constHeader{ api.KeyTitle: "Zettelstore User CSS", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: "css", api.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent([]byte("/* User-defined CSS */"))}, id.EmojiZid: { constHeader{ | > > > > > > > > | | 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 | constHeader{ api.KeyTitle: "Zettelstore User CSS", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: "css", 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.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.KeyVisibility: api.ValueVisibilityPublic, }, domain.NewContent(contentEmoji)}, id.TOCNewTemplateZid: { |
︙ | ︙ |
Changes to box/constbox/delete.mustache.
︙ | ︙ | |||
33 34 35 36 37 38 39 | {{/HasUselessFiles}} <dl> {{#MetaPairs}} <dt>{{Key}}:</dt><dd>{{Value}}</dd> {{/MetaPairs}} </dl> <form method="POST"> | | | 33 34 35 36 37 38 39 40 41 42 43 | {{/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}} |
Changes to box/constbox/form.mustache.
1 2 3 4 5 6 | <article> <header> <h1>{{Heading}}</h1> </header> <form method="POST"> <div> | | | | | > > > > > > > | | | | | | > > > > > > | | | > | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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> |
Changes to box/constbox/login.mustache.
1 2 3 4 5 6 7 8 9 | <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> | | | | | 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> |
Changes to box/constbox/rename.mustache.
︙ | ︙ | |||
27 28 29 30 31 32 33 | {{/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}}"> | | | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | {{/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> |
Changes to box/dirbox/dirbox.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "context" "errors" "net/url" "os" "path/filepath" "sync" | < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | "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" |
︙ | ︙ | |||
233 234 235 236 237 238 239 | if !entry.IsValid() { return domain.Zettel{}, box.ErrNotFound } m, c, err := dp.srvGetMetaContent(ctx, entry, zid) if err != nil { return domain.Zettel{}, err } | < < | 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 | 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 search.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") for _, entry := range entries { |
︙ | ︙ | |||
277 278 279 280 281 282 283 | // The following loop could be parallelized if needed for performance. for _, entry := range entries { m, err := dp.srvGetMeta(ctx, entry, entry.Zid) if err != nil { dp.log.Trace().Err(err).Msg("ApplyMeta/getMeta") return err } | < | 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | // The following loop could be parallelized if needed for performance. for _, entry := range entries { m, err := dp.srvGetMeta(ctx, entry, entry.Zid) if err != nil { dp.log.Trace().Err(err).Msg("ApplyMeta/getMeta") return err } dp.cdata.Enricher.Enrich(ctx, m, dp.number) handle(m) } return nil } func (dp *dirBox) CanUpdateZettel(context.Context, domain.Zettel) bool { |
︙ | ︙ | |||
397 398 399 400 401 402 403 | } 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") } | < < < < < < < < < | 393 394 395 396 397 398 399 | } 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/service.go.
1 2 3 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ |
Changes to box/filebox/filebox.go.
1 2 3 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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. |
︙ | ︙ | |||
67 68 69 70 71 72 73 | } return ext } // CalcDefaultMeta returns metadata with default values for the given entry. func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta { m := meta.New(zid) | < < < < < | 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | } return ext } // CalcDefaultMeta returns metadata with default values for the given entry. func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta { m := meta.New(zid) m.Set(api.KeySyntax, calculateSyntax(ext)) return m } // CleanupMeta enhances the given metadata. func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta bool, uselessFiles []string) { if inMeta { if syntax, ok := m.Get(api.KeySyntax); !ok || syntax == "" { dm := CalcDefaultMeta(zid, ext) syntax, ok = dm.Get(api.KeySyntax) if !ok { panic("Default meta must contain syntax") } |
︙ | ︙ |
Changes to box/filebox/zipbox.go.
1 2 3 | //----------------------------------------------------------------------------- // Copyright (c) 2021-2022 Detlef Stern // | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ |
Changes to box/manager/manager.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( "context" "io" "net/url" | < > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // 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" |
︙ | ︙ | |||
76 77 78 79 80 81 82 | if _, ok := registry[scheme]; ok { panic(scheme) } registry[scheme] = create } // GetSchemes returns all registered scheme, ordered by scheme string. | | < < < < < < < | 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | 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 |
︙ | ︙ |
Changes to box/manager/memstore/memstore.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 28 29 | "fmt" "io" "sort" "strings" "sync" "zettelstore.de/c/api" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" ) type metaRefs struct { forward id.Slice | > | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | "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 |
︙ | ︙ | |||
586 587 588 589 590 591 592 | } func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { if len(srefs) == 0 { return } fmt.Fprintln(w, "====", title) | < < < < < | | 587 588 589 590 591 592 593 594 595 596 597 598 | } 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]) } } |
Changes to box/notify/directory_test.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | import ( "testing" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. | < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) func TestSeekZid(t *testing.T) { |
︙ | ︙ | |||
45 46 47 48 49 50 51 | } } } func TestNewExtIsBetter(t *testing.T) { extVals := []string{ // Main Formats | | | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | } } } func TestNewExtIsBetter(t *testing.T) { extVals := []string{ // Main Formats api.ValueSyntaxZmk, "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", } |
︙ | ︙ |
Changes to box/notify/entry.go.
1 2 3 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ | |||
95 96 97 98 99 100 101 | } func calcContentExt(syntax string, yamlSep bool, getZettelFileSyntax func() []string) string { if yamlSep { return extZettel } switch syntax { | | | 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | } 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 cmd/cmd_file.go.
︙ | ︙ | |||
37 38 39 40 41 42 43 | domain.Zettel{ Meta: m, Content: domain.NewContent(inp.Src[inp.Pos:]), }, m.GetDefault(api.KeySyntax, api.ValueSyntaxZmk), nil, ) | | < < | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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_run.go.
︙ | ︙ | |||
22 23 24 25 26 27 28 | "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/server" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { | | | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | "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)") fs.Uint("p", 23123, "port number web service") fs.String("d", "", "zettel directory") fs.Bool("r", false, "system-wide read-only mode") fs.Bool("v", false, "verbose mode") fs.Bool("debug", false, "debug mode") } |
︙ | ︙ | |||
63 64 65 66 67 68 69 | ucCreateZettel := usecase.NewCreateZettel(ucLog, rtConfig, protectedBoxManager) ucGetMeta := usecase.NewGetMeta(protectedBoxManager) ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) ucGetZettel := usecase.NewGetZettel(protectedBoxManager) ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta) ucListMeta := usecase.NewListMeta(protectedBoxManager) | > | | | > < < | 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 | ucCreateZettel := usecase.NewCreateZettel(ucLog, rtConfig, protectedBoxManager) ucGetMeta := usecase.NewGetMeta(protectedBoxManager) ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) ucGetZettel := usecase.NewGetZettel(protectedBoxManager) ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta) ucListMeta := usecase.NewListMeta(protectedBoxManager) ucListSyntax := usecase.NewListSyntax(protectedBoxManager) ucListRoles := usecase.NewListRoles(protectedBoxManager) ucListTags := usecase.NewListTags(protectedBoxManager) ucZettelContext := usecase.NewZettelContext(protectedBoxManager, rtConfig) ucDelete := usecase.NewDeleteZettel(ucLog, protectedBoxManager) ucUpdate := usecase.NewUpdateZettel(ucLog, protectedBoxManager) ucRename := usecase.NewRenameZettel(ucLog, protectedBoxManager) ucUnlinkedRefs := usecase.NewUnlinkedReferences(protectedBoxManager, rtConfig) ucRefresh := usecase.NewRefresh(ucLog, protectedBoxManager) ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager)) // 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, ucListRoles, ucListTags, &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.MakeListMetaHandler(ucListMeta)) webSrv.AddZettelRoute('j', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel)) webSrv.AddListRoute('m', server.MethodGet, a.MakeListMapMetaHandler(ucListRoles, ucListTags)) 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.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)) |
︙ | ︙ |
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 |
︙ | ︙ | |||
59 60 61 62 63 64 65 | // Get returns the command identified by the given name and a bool to signal success. func Get(name string) (Command, bool) { cmd, ok := commands[name] return cmd, ok } // List returns a sorted list of all registered command names. | | < < < < < < < | 59 60 61 62 63 64 65 66 | // Get returns the command identified by the given name and a bool to signal success. func Get(name string) (Command, bool) { cmd, ok := commands[name] return cmd, ok } // List returns a sorted list of all registered command names. func List() []string { return maps.Keys(commands) } |
Changes to cmd/main.go.
1 2 3 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // | | > | < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 cmd import ( "errors" "flag" "fmt" "net" "net/url" "os" "runtime/debug" "strconv" "strings" "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", Func: func(*flag.FlagSet) (int, error) { fmt.Println("Available commands:") for _, name := range List() { |
︙ | ︙ | |||
60 61 62 63 64 65 66 | Func: runFunc, Boxes: true, Header: true, LineServer: true, SetFlags: flgRun, }) RegisterCommand(Command{ | | | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | Func: runFunc, Boxes: true, Header: true, LineServer: true, SetFlags: flgRun, }) RegisterCommand(Command{ Name: strRunSimple, Func: runFunc, Simple: true, Boxes: true, Header: true, // LineServer: true, SetFlags: func(fs *flag.FlagSet) { // fs.Uint("a", 0, "port number kernel service (0=disable)") |
︙ | ︙ | |||
84 85 86 87 88 89 90 | }) RegisterCommand(Command{ Name: "password", Func: cmdPassword, }) } | | < | > | > > > | | | > > > > > > > > > > > > | | 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 | }) 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": if portStr, err := parsePort(flg.Value.String()); err == nil { cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", portStr)) } case "a": |
︙ | ︙ | |||
156 157 158 159 160 161 162 163 164 165 166 167 168 169 | const ( keyAdminPort = "admin-port" keyDebug = "debug-mode" keyDefaultDirBoxType = "default-dir-box-type" keyInsecureCookie = "insecure-cookie" keyListenAddr = "listen-addr" keyLogLevel = "log-level" keyOwner = "owner" keyPersistentCookie = "persistent-cookie" keyBoxOneURI = kernel.BoxURIs + "1" keyReadOnly = "read-only-mode" keyTokenLifetimeHTML = "token-lifetime-html" keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" | > | 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | const ( keyAdminPort = "admin-port" keyDebug = "debug-mode" keyDefaultDirBoxType = "default-dir-box-type" keyInsecureCookie = "insecure-cookie" keyListenAddr = "listen-addr" keyLogLevel = "log-level" keyMaxRequestSize = "max-request-size" keyOwner = "owner" keyPersistentCookie = "persistent-cookie" keyBoxOneURI = kernel.BoxURIs + "1" keyReadOnly = "read-only-mode" keyTokenLifetimeHTML = "token-lifetime-html" keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" |
︙ | ︙ | |||
205 206 207 208 209 210 211 212 213 214 215 216 217 218 | ok = setConfigValue( ok, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, cfg.GetDefault(keyURLPrefix, "/")) ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) ok = setConfigValue(ok, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) ok = setConfigValue( ok, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) ok = setConfigValue( ok, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) if !ok { return errors.New("unable to set configuration") | > > > | 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | ok = setConfigValue( ok, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, cfg.GetDefault(keyURLPrefix, "/")) ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie)) ok = setConfigValue(ok, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie)) if val, found := cfg.Get(keyMaxRequestSize); found { ok = setConfigValue(ok, kernel.WebService, kernel.WebMaxRequestSize, val) } ok = setConfigValue( ok, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) ok = setConfigValue( ok, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) if !ok { return errors.New("unable to set configuration") |
︙ | ︙ | |||
258 259 260 261 262 263 264 265 266 267 | createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) { compbox.Setup(cfg) return manager.New(boxURIs, authManager, rtConfig) } } else { createManager = func([]*url.URL, auth.Manager, config.Config) (box.Manager, error) { return nil, nil } } kern.SetCreators( func(readonly bool, owner id.Zid) (auth.Manager, error) { | > > > > > > | | 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) { compbox.Setup(cfg) return manager.New(boxURIs, authManager, rtConfig) } } else { createManager = func([]*url.URL, auth.Manager, config.Config) (box.Manager, error) { return nil, nil } } secret := cfg.GetDefault("secret", "") if len(secret) < 16 && cfg.GetDefault(keyOwner, "") != "" { fmt.Fprintf(os.Stderr, "secret must have at least length 16 when authentication is enabled, but is %q\n", secret) return 2 } kern.SetCreators( func(readonly bool, owner id.Zid) (auth.Manager, error) { return impl.New(readonly, owner, secret), nil }, createManager, func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error { setupRouting(srv, plMgr, authMgr, rtConfig) return nil }, ) |
︙ | ︙ | |||
285 286 287 288 289 290 291 292 293 294 295 296 | 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 { 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 } | > > > | > | > > > > > > > > > > > > > > > > > > > > > > | 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 | 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 } return executeCommand(strRunSimple, "-d", dir) } var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") var memprofile = flag.String("memprofile", "", "write memory profile to `file`") // Main is the real entrypoint of the zettelstore. func Main(progName, buildVersion string) int { fullVersion := retrieveFullVersion(buildVersion) kernel.Main.SetConfig(kernel.CoreService, kernel.CoreProgname, progName) kernel.Main.SetConfig(kernel.CoreService, kernel.CoreVersion, fullVersion) flag.Parse() if *cpuprofile != "" || *memprofile != "" { if *cpuprofile != "" { kernel.Main.StartProfiling(kernel.ProfileCPU, *cpuprofile) } else { kernel.Main.StartProfiling(kernel.ProfileHead, *memprofile) } defer kernel.Main.StopProfiling() } args := flag.Args() if len(args) == 0 { return runSimple() } return executeCommand(args[0], args[1:]...) } func retrieveFullVersion(version string) string { info, ok := debug.ReadBuildInfo() if !ok { return version } var revision, dirty string for _, kv := range info.Settings { switch kv.Key { case "vcs.revision": revision = "+" + kv.Value if len(revision) > 11 { revision = revision[:11] } case "vcs.modified": if kv.Value == "true" { dirty = "-dirty" } } } return version + revision + dirty } |
Changes to cmd/register.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | 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. | | < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) |
Changes to config/config.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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. |
︙ | ︙ | |||
20 21 22 23 24 25 26 | // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig // AddDefaultValues enriches the given meta data with its default values. AddDefaultValues(m *meta.Meta) *meta.Meta | < < < < < < < < < < < < | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig // AddDefaultValues enriches the given meta data with its default values. AddDefaultValues(m *meta.Meta) *meta.Meta // GetDefaultLang returns the current value of the "default-lang" key. GetDefaultLang() string // GetSiteName returns the current value of the "site-name" key. GetSiteName() string // GetHomeZettel returns the value of the "home-zettel" key. GetHomeZettel() id.Zid // GetMaxTransclusions return the maximum number of indirect transclusions. GetMaxTransclusions() int // GetYAMLHeader returns the current value of the "yaml-header" key. GetYAMLHeader() bool // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. |
︙ | ︙ | |||
70 71 72 73 74 75 76 | // GetExpertMode returns the current value of the "expert-mode" key. GetExpertMode() bool // GetVisibility returns the visibility value of the metadata. GetVisibility(m *meta.Meta) meta.Visibility } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | // GetExpertMode returns the current value of the "expert-mode" key. GetExpertMode() bool // GetVisibility returns the visibility value of the metadata. GetVisibility(m *meta.Meta) meta.Visibility } // GetLang returns the value of the "lang" key of the given meta. If there is // no such value, GetDefaultLang is returned. func GetLang(m *meta.Meta, cfg Config) string { if val, ok := m.Get(api.KeyLang); ok { return val } return cfg.GetDefaultLang() } |
Changes to docs/manual/00001003305000.zettel.
︙ | ︙ | |||
38 39 40 41 42 43 44 | All you have to do is to open your web browser, enter the appropriate URL, and there you go. On the negative side, you will not be notified when you enter the wrong data in the Task scheduler and Zettelstore fails to start. This can be mitigated by first using the command line prompt to start Zettelstore with the appropriate options. Once everything works, you can register Zettelstore to be automatically started by the task scheduler. There you should make sure that you have followed the first steps as described on the [[parent page|00001003300000]]. | | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | All you have to do is to open your web browser, enter the appropriate URL, and there you go. On the negative side, you will not be notified when you enter the wrong data in the Task scheduler and Zettelstore fails to start. This can be mitigated by first using the command line prompt to start Zettelstore with the appropriate options. Once everything works, you can register Zettelstore to be automatically started by the task scheduler. There you should make sure that you have followed the first steps as described on the [[parent page|00001003300000]]. To start the Task scheduler management console, press the Windows logo key and the key ''R'', type ''taskschd.msc''. Select the OK button. {{00001003305102}} This will start the ""Task Scheduler"". Now, create a new task with ""Create Task ..."" |
︙ | ︙ |
Changes to docs/manual/00001003310000.zettel.
︙ | ︙ | |||
38 39 40 41 42 43 44 | === Launch Agent If you want to execute Zettelstore automatically and less visible, and if you know a little bit about working in the terminal application, then you could try to run Zettelstore under the control of the [[Launchd system|https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Introduction.html]]. First, you have to create a description for ""Launchd"". This is a text file named ''zettelstore.plist'' with the following content. | | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | === Launch Agent If you want to execute Zettelstore automatically and less visible, and if you know a little bit about working in the terminal application, then you could try to run Zettelstore under the control of the [[Launchd system|https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Introduction.html]]. First, you have to create a description for ""Launchd"". This is a text file named ''zettelstore.plist'' with the following content. It assumes that you have copied the Zettelstore executable in a local folder called ''~/bin'' and have created a file for [[startup configuration|00001004010000]] called ''zettelstore.cfg'', which is placed in the same folder[^If you are not using a configuration file, just remove the lines ``<string>-c</string>`` and ``<string>/Users/USERNAME/bin/zettelstore.cfg</string>``.]: ``` <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> |
︙ | ︙ |
Changes to docs/manual/00001004010000.zettel.
1 2 3 4 5 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20220419193611 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. |
︙ | ︙ | |||
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 | ; [!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. ; [!owner|''owner''] : [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore. The owner has full authorization for the Zettelstore. Only if owner is set to some value, user [[authentication|00001010000000]] is enabled. ; [!persistent-cookie|''persistent-cookie''] : A [[boolean value|00001006030500]] to make the access cookie persistent. This is helpful if you access the Zettelstore via a mobile device. On these devices, the operating system is free to stop the web browser and to remove temporary cookies. Therefore, an authenticated user will be logged off. If ""true"", a persistent cookie is used. Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds. Default: ""false"" ; [!read-only-mode|''read-only-mode''] : Puts the Zettelstore service into a read-only mode, if set to a [[true value|00001006030500]]. No changes are possible. Default: ""false"". ; [!token-lifetime-api|''token-lifetime-api''], [!token-lifetime-html|''token-lifetime-html''] : Define lifetime of access tokens in minutes. Values are only valid if authentication is enabled, i.e. key ''owner'' is set. ''token-lifetime-api'' is for accessing Zettelstore via its [[API|00001012000000]]. Default: ""10"". | > > > > > > > > > > > > > | 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 | ; [!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. The owner has full authorization for the Zettelstore. Only if owner is set to some value, user [[authentication|00001010000000]] is enabled. Ensure that key [[''secret''|#secret]] is set to a value of at least 16 bytes. Otherwise the Zettelstore will not start for security reasons. ; [!persistent-cookie|''persistent-cookie''] : A [[boolean value|00001006030500]] to make the access cookie persistent. This is helpful if you access the Zettelstore via a mobile device. On these devices, the operating system is free to stop the web browser and to remove temporary cookies. Therefore, an authenticated user will be logged off. If ""true"", a persistent cookie is used. Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds. Default: ""false"" ; [!read-only-mode|''read-only-mode''] : Puts the Zettelstore service into a read-only mode, if set to a [[true value|00001006030500]]. No changes are possible. Default: ""false"". ; [!secret|''secret''] : A string value to make the communication with external clients strong enough so that sessions of the [[web user interface|00001014000000]] or [[API access token|00001010040700]] cannot be modified by some external unfriendly party. The string must have a length of at least 16 bytes. It is only needed to set this value, if [[authentication is enabled|00001010040100]] by setting key [[''owner''|#owner]] to some user identification. ; [!token-lifetime-api|''token-lifetime-api''], [!token-lifetime-html|''token-lifetime-html''] : Define lifetime of access tokens in minutes. Values are only valid if authentication is enabled, i.e. key ''owner'' is set. ''token-lifetime-api'' is for accessing Zettelstore via its [[API|00001012000000]]. Default: ""10"". |
︙ | ︙ |
Changes to docs/manual/00001004011400.zettel.
1 2 3 4 5 | id: 00001004011400 title: Configure file directory boxes 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 28 | id: 00001004011400 title: Configure file directory boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20220724200512 Under certain circumstances, it is preferable to further configure a file directory box. This is done by appending query parameters after the base box URI ''dir:\//DIR''. The following parameters are supported: |= Parameter:|Description|Default value:| |type|(Sub-) Type of the directory service|(value of ""[[default-dir-box-type|00001004010000#default-dir-box-type]]"") |worker|Number of worker that can access the directory in parallel|7 |readonly|Allow only operations that do not create or change zettel|n/a === Type On some operating systems, Zettelstore tries to detect changes to zettel files outside of Zettelstore's control[^This includes Linux, Windows, and macOS.]. On other operating systems, this may be not possible, due to technical limitations. Automatic detection of external changes is also not possible, if zettel files are put on an external service, such as a file server accessed via SMB/CIFS or NFS. To cope with this uncertainty, Zettelstore provides various internal implementations of a directory box. The default values should match the needs of different users, as explained in the [[installation part|00001003000000]] of this manual. The following values are supported: ; simple : Is not able to detect external changes. |
︙ | ︙ |
Changes to docs/manual/00001004020000.zettel.
1 2 3 4 5 | id: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001004020000 title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk modified: 20220628110920 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. |
︙ | ︙ | |||
21 22 23 24 25 26 27 | This value 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]]. ; [!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). | < < < < < < < < < < < | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | This value 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]]. ; [!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"". |
︙ | ︙ |
Changes to docs/manual/00001004051000.zettel.
1 2 3 4 5 | id: 00001004051000 title: The ''run'' 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 28 29 | id: 00001004051000 title: The ''run'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk modified: 20220724162050 === ``zettelstore run`` This starts the web service. ``` zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v] ``` ; [!a|''-a PORT''] : Specifies the TCP port through which you can reach the [[administrator console|00001004100000]]. See the explanation of [[''admin-port''|00001004010000#admin-port]] for more details. ; [!c|''-c CONFIGFILE''] : Specifies ''CONFIGFILE'' as a file, where [[startup configuration data|00001004010000]] is read. It is ignored, when the given file is not available, nor readable. Default: tries to read the following files in the ""current directory"": ''zettelstore.cfg'', ''zsconfig.txt'', ''zscfg.txt'', ''_zscfg'', and ''.zscfg''. ; [!d|''-d DIR''] : Specifies ''DIR'' as the directory that contains all zettel. Default is ''./zettel'' (''.\\zettel'' on Windows), where ''.'' denotes the ""current directory"". ; [!debug|''-debug''] : Allows better debugging of the internal web server by disabling any timeout values. You should specify this only as a developer. |
︙ | ︙ |
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 14 15 16 17 18 19 20 21 22 23 | 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.] If no startup configuration was found, the sub-command allows only to specify a zettel directory. The directory will be created automatically, if it does not exist. This is a difference to the ''run'' sub-command, where the directory must exists. In contrast to the ''run'' sub-command, other command line parameter are not allowed. ``` zettelstore run-simple [-d DIR] ``` |
︙ | ︙ |
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 | 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'' |
︙ | ︙ |
Changes to docs/manual/00001004100000.zettel.
︙ | ︙ | |||
14 15 16 17 18 19 20 | In fact, the administrator console is __not__ a full telnet service. It is merely a simple line-oriented service where each input line is interpreted separately. Therefore, you can also use tools like [[netcat|https://nc110.sourceforge.io/]], [[socat|http://www.dest-unreach.org/socat/]], etc. After connecting to the administrator console, there is no further authentication. It is not needed because you must be logged in on the same computer where Zettelstore is running. You cannot connect to the administrator console if you are on a different computer. | | | 14 15 16 17 18 19 20 21 22 23 24 25 | In fact, the administrator console is __not__ a full telnet service. It is merely a simple line-oriented service where each input line is interpreted separately. Therefore, you can also use tools like [[netcat|https://nc110.sourceforge.io/]], [[socat|http://www.dest-unreach.org/socat/]], etc. After connecting to the administrator console, there is no further authentication. It is not needed because you must be logged in on the same computer where Zettelstore is running. You cannot connect to the administrator console if you are on a different computer. Of course, on multi-user systems with encrusted users, you should not enable the administrator console. * Enable via [[command line|00001004051000#a]] * Enable via [[configuration file|00001004010000#admin-port]] * [[List of supported commands|00001004101000]] |
Changes to docs/manual/00001005000000.zettel.
︙ | ︙ | |||
42 43 44 45 46 47 48 | # the empty file extension is used, when the content must be stored in its own file, e.g. image data; in this case, the filename just the 14 digits of the zettel identifier, and optional characters except the period ''"."''. Other filename extensions are used to determine the ""syntax"" of a zettel. This allows to use other content within the Zettelstore, e.g. images or HTML templates. For example, you want to store an important figure in the Zettelstore that is encoded as a ''.png'' file. Since each zettel contains some metadata, e.g. the title of the figure, the question arises where these data should be stores. | | | | 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 | # the empty file extension is used, when the content must be stored in its own file, e.g. image data; in this case, the filename just the 14 digits of the zettel identifier, and optional characters except the period ''"."''. Other filename extensions are used to determine the ""syntax"" of a zettel. This allows to use other content within the Zettelstore, e.g. images or HTML templates. For example, you want to store an important figure in the Zettelstore that is encoded as a ''.png'' file. Since each zettel contains some metadata, e.g. the title of the figure, the question arises where these data should be stores. The solution is a meta-file with the same zettel identifier, but without a filename extension. Zettelstore recognizes this situation and reads in both files for the one zettel containing the figure. It maintains this relationship as long as theses files exists. In case of some textual zettel content you do not want to store the metadata and the zettel content in two different files. Here the ''.zettel'' extension will signal that the metadata and the zettel content will be put in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator""). === Predefined zettel Zettelstore contains some [[predefined zettel|00001005090000]] to work properly. The [[configuration zettel|00001004020000]] is one example. To render the builtin [[web user interface|00001014000000]], some templates are used, as well as a [[layout specification in CSS|00000000020001]]. The icon that visualizes a broken image is a [[predefined GIF image|00000000040001]]. All of these are visible to the Zettelstore as zettel. One reason for this is to allow you to modify these zettel to adapt Zettelstore to your needs and visual preferences. Where are these zettel stored? They are stored within the Zettelstore software itself, because one [[design goal|00001002000000]] was to have just one executable file to use Zettelstore. But data stored within an executable program cannot be changed later[^Well, it can, but it is a very bad idea to allow this. Mostly for security reasons.]. To allow changing predefined zettel, both the file store and the internal zettel store are internally chained together. If you change a zettel, it will be always stored as a file. If a zettel is requested, Zettelstore will first try to read that zettel from a file. If such a file was not found, the internal zettel store is searched secondly. Therefore, the file store ""shadows"" the internal zettel store. |
︙ | ︙ |
Changes to docs/manual/00001005090000.zettel.
1 2 3 4 5 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #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: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk modified: 20220321192401 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 Meta HTML Template | Used when displaying a list of zettel | [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel | [[00000000010402]] | Zettelstore Info HTML 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 | [[00000000010500]] | Zettelstore List Roles HTML Template | Layout for listing all roles | [[00000000010600]] | Zettelstore List Tags HTML Template | Layout of tags lists | [[00000000020001]] | Zettelstore Base CSS | 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 [[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. |
︙ | ︙ | |||
21 22 23 24 25 26 27 | The zettel content is your valuable content. Zettelstore contains some predefined parsers that interpret the zettel content to the syntax of the zettel. This includes markup languages, like [[Zettelmarkup|00001007000000]] and [[CommonMark|00001008010500]]. 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.]. | | > > > > > > > > > > > > > > > > > > > | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | The zettel content is your valuable content. Zettelstore contains some predefined parsers that interpret the zettel content to the syntax of the zettel. This includes markup languages, like [[Zettelmarkup|00001007000000]] and [[CommonMark|00001008010500]]. 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/00001006020000.zettel.
1 2 3 4 5 | id: 00001006020000 title: Supported Metadata Keys 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 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk modified: 20220628111132 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]]. ; [!all-tags|''all-tags''] : A property (a computed values that is not stored) that contains both the value of [[''tags''|#tags]] together with all [[tags|00001007040000#tag]] that are specified within the content. ; [!back|''back''] : Is a property that contains the identifier of all zettel that reference the zettel of this metadata, that are not referenced by this zettel. Basically, it is the value of [[''backward''|#backward]], but without any zettel identifier that is contained in [[''forward''|#forward]]. ; [!backward|''backward''] : Is a property that contains the identifier of all zettel that reference the zettel of this metadata. References within invertible values are not included here, e.g. [[''precursor''|#precursor]]. ; [!box-number|''box-number''] : Is a computed value and contains the number of the box where the zettel was found. For all but the [[predefined zettel|00001005090000]], this number is equal to the number __X__ specified in startup configuration key [[''box-uri-__X__''|00001004010000#box-uri-x]]. ; [!copyright|''copyright''] |
︙ | ︙ | |||
68 69 70 71 72 73 74 | ; [!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]]. | | | | | 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 | ; [!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. ; [!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''] |
︙ | ︙ |
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 | 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''] |
︙ | ︙ |
Changes to docs/manual/00001006036000.zettel.
1 2 3 4 5 | id: 00001006036000 title: WordSet Key Type 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 | id: 00001006036000 title: WordSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk modified: 20220724201056 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. === Match operator A value matches a WordSet value, if the first value is equal to one of the word values in the word set. === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Changes to docs/manual/00001006055000.zettel.
1 2 3 4 5 | id: 00001006055000 title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001006055000 title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk modified: 20220311111751 [[Zettel identifier|00001006050000]] are typically created by examine the current date and time. By renaming a zettel, you are able to provide any sequence of 14 digits. If no other zettel has the same identifier, you are allowed to rename a zettel. To make things easier, you normally should not use zettel identifier that begin with four zeroes (''0000''). |
︙ | ︙ | |||
37 38 39 40 41 42 43 | | 00009000000000 | 0000999999999 | Reserved for applications This list may change in the future. ==== External Applications |= From | To | Description | 00009000001000 | 00009000001999 | [[Zettel Presenter|https://zettelstore.de/contrib]], an application to display zettel as a HTML-based slideshow | > | 37 38 39 40 41 42 43 44 | | 00009000000000 | 0000999999999 | Reserved for applications This list may change in the future. ==== External Applications |= From | To | Description | 00009000001000 | 00009000001999 | [[Zettel Presenter|https://zettelstore.de/contrib]], an application to display zettel as a HTML-based slideshow | 00009000002000 | 00009000002999 | [[Zettel Blog|https://zettelstore.de/contrib]], an application to collect and transform zettel into a blog |
Changes to docs/manual/00001007000000.zettel.
1 2 3 4 5 6 7 8 9 10 | id: 00001007000000 title: Zettelmarkup role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220113185501 Zettelmarkup is a rich plain-text based markup language for writing zettel content. Besides the zettel content, Zettelmarkup is also used for specifying the title of a zettel, regardless of the syntax of a zettel. | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | id: 00001007000000 title: Zettelmarkup role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220113185501 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. [[CommonMark|00001008010500]] is an attempt to make it simpler by providing a comprehensive specification, combined with an extra chapter to give hints for the implementation. Zettelmarkup follows some simple principles that anybody who knows to ho write software should be able understand to create an implementation. Zettelmarkup is a markup language on its own. This is in contrast to Markdown, which is basically a super-set of HTML. While HTML is a markup language that will probably last for a long time, it cannot be easily translated to other formats, such as PDF, JSON, or LaTeX. Additionally, it is allowed to embed other languages into HTML, such as CSS or even JavaScript. This could create problems with longevity as well as security problems. Zettelmarkup is a rich markup language, but it focuses on relatively short zettel content. It allows embedding other content, simple tables, quotations, description lists, and images. It provides a broad range of inline formatting, including __emphasized__, **strong**, ~~deleted~~{-} and >>inserted>> text. Footnotes[^like this] are supported, links to other zettel and to external material, as well as citation keys. Zettelmarkup might be seen as a proprietary markup language. But if you want to use [[Markdown/CommonMark|00001008010000]] and you need support for footnotes or tables, you'll end up with proprietary extensions. However, the Zettelstore supports CommonMark as a zettel syntax, so you can mix both Zettelmarkup zettel and CommonMark zettel in one store to get the best of both worlds. |
︙ | ︙ |
Changes to docs/manual/00001007010000.zettel.
︙ | ︙ | |||
29 30 31 32 33 34 35 | Some inline elements do not follow the rule of two identical character, especially to specify [[footnotes|00001007040330]], [[citation keys|00001007040340]], and local marks. These elements begin with one opening square bracket (""``[``""), use a character for specifying the kind of the inline, typically allow to specify some content, and end with one closing square bracket (""``]``""). One inline element that does not begin with two characters is the ""entity"". It allows to specify any Unicode character. The specification of that character is put between an ampersand character and a semicolon: ``&...;``{=zmk}. | | | 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | Some inline elements do not follow the rule of two identical character, especially to specify [[footnotes|00001007040330]], [[citation keys|00001007040340]], and local marks. These elements begin with one opening square bracket (""``[``""), use a character for specifying the kind of the inline, typically allow to specify some content, and end with one closing square bracket (""``]``""). One inline element that does not begin with two characters is the ""entity"". It allows to specify any Unicode character. The specification of that character is put between an ampersand character and a semicolon: ``&...;``{=zmk}. For example, an ""n-dash"" could also be specified as ``–``{==zmk}. The backslash character (""``\\``"") possibly gives the next character a special meaning. This allows to resolve some left ambiguities. For example, a list of depth 2 will begin a line with ``** Item 2.2``{=zmk}. An inline element to strongly emphasize some text begin with a space will be specified as ``** Text**``{=zmk}. To force the inline element formatting at the beginning of a line, ``**\\ Text**``{=zmk} should better be specified. |
︙ | ︙ |
Changes to docs/manual/00001007030000.zettel.
1 2 3 4 5 | id: 00001007030000 title: Zettelmarkup: Block-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001007030000 title: Zettelmarkup: Block-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220311181036 Every markup for blocks-structured elements (""blocks"") begins at the very first position of a line. There are five kinds of blocks: lists, one-line blocks, line-range blocks, tables, and paragraphs. === Lists |
︙ | ︙ | |||
35 36 37 38 39 40 41 42 43 44 45 46 47 48 | Additionally, all other blocks elements are allowed in line-range blocks. * [[Verbatim blocks|00001007030500]] do not interpret their content, * [[Quotation blocks|00001007030600]] specify a block-length quotations, * [[Verse blocks|00001007030700]] allow to enter poetry, lyrics and similar text, where line endings are important, * [[Region blocks|00001007030800]] just mark regions, e.g. for common formatting, * [[Comment blocks|00001007030900]] allow to enter text that will be ignored when rendered. * [[Inline-Zettel blocks|00001007031200]] provide a mechanism to specify zettel content with a new syntax without creating a new zettel. === Tables Similar to lists are tables not specified explicitly. A sequence of table rows is considered a [[table|00001007031000]]. A table row itself is a sequence of table cells. | > > | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | Additionally, all other blocks elements are allowed in line-range blocks. * [[Verbatim blocks|00001007030500]] do not interpret their content, * [[Quotation blocks|00001007030600]] specify a block-length quotations, * [[Verse blocks|00001007030700]] allow to enter poetry, lyrics and similar text, where line endings are important, * [[Region blocks|00001007030800]] just mark regions, e.g. for common formatting, * [[Comment blocks|00001007030900]] allow to enter text that will be ignored when rendered. * [[Evaluation blocks|00001007031300]] specify some content to be evaluated by either Zettelstore or external software. * [[Math-mode blocks|00001007031400]] can be used to enter mathematical formulas / equations. * [[Inline-Zettel blocks|00001007031200]] provide a mechanism to specify zettel content with a new syntax without creating a new zettel. === Tables Similar to lists are tables not specified explicitly. A sequence of table rows is considered a [[table|00001007031000]]. A table row itself is a sequence of table cells. |
︙ | ︙ |
Changes to docs/manual/00001007030200.zettel.
︙ | ︙ | |||
87 88 89 90 91 92 93 | *# A.3 * B * C ::: Please note that two lists cannot be separated by an empty line. | | | 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | *# A.3 * B * C ::: Please note that two lists cannot be separated by an empty line. Instead you should put a horizontal rule (""thematic break"") between them. You could also use a [[mark element|00001007040350]] or a hard line break to separate the two lists: ```zmk # One # Two [!sep] # Uno # Due |
︙ | ︙ |
Changes to docs/manual/00001007030600.zettel.
1 2 3 4 5 6 7 8 9 | id: 00001007030600 title: Zettelmarkup: Quotation Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218131806 A simple way to enter a quotation is to use the [[quotation list|00001007030200]]. A quotation list loosely follows the convention of quoting text within emails. | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 00001007030600 title: Zettelmarkup: Quotation Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218131806 A simple way to enter a quotation is to use the [[quotation list|00001007030200]]. A quotation list loosely follows the convention of quoting text within emails. However, if you want to attribute the quotation to someone, a quotation block is more appropriately. This kind of line-range block begins with at least three less-than characters (""''<''"", U+003C) at the first position of a line. You can add some [[attributes|00001007050000]] on the beginning line of a quotation block, following the initiating characters. The quotation does not support the default attribute, nor the generic attribute. Attributes are interpreted on HTML rendering. Any other character in this line will be ignored |
︙ | ︙ |
Changes to docs/manual/00001007030800.zettel.
1 2 3 4 5 | id: 00001007030800 title: Zettelmarkup: Region 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 | id: 00001007030800 title: Zettelmarkup: Region Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220323190829 Region blocks does not directly have a visual representation. They just group a range of lines. You can use region blocks to enter [[attributes|00001007050000]] that apply only to this range of lines. One example is to enter a multi-line warning that should be visible. This kind of line-range block begins with at least three colon characters (""'':''"", U+003A) at the first position of a line[^Since a [[description text|00001007030100]] only use exactly one colon character at the first position of a line, there is no possible ambiguity between these elements.]. You can add some [[attributes|00001007050000]] on the beginning line of a region block, following the initiating characters. The region block does not support the default attribute, but it supports the generic attribute. Some generic attributes, like ``=note``, ``=warning`` will be rendered special. All other generic attributes are used as a CSS class definition. Attributes are interpreted on HTML rendering. Any other character in this line will be ignored. Text following the beginning line will be interpreted, until a line begins with at least the same number of the same characters given at the beginning line. This allows to enter a region block within a region block. At the ending line, you can enter some [[inline elements|00001007040000]] after the colon characters. These will interpreted as some attribution text. |
︙ | ︙ | |||
64 65 66 67 68 69 70 | Generic attributes that are result in a special HTML rendering are: * example * note * tip * important * caution * warning | > > > > > > > > > > > > > | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | Generic attributes that are result in a special HTML rendering are: * example * note * tip * important * caution * warning All other generic attribute values are rendered as a CSS class: ```zmk :::abc def ::: ``` is rendered as ::::example :::abc def ::: :::: |
Changes to docs/manual/00001007030900.zettel.
︙ | ︙ | |||
9 10 11 12 13 14 15 | 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. | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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. For example: |
︙ | ︙ |
Changes to docs/manual/00001007031000.zettel.
1 2 3 4 5 6 7 | id: 00001007031000 title: Zettelmarkup: Tables role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218131107 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | id: 00001007031000 title: Zettelmarkup: Tables role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218131107 Tables are used to show some data in a two-dimensional fashion. In zettelmarkup, table are not specified explicitly, but by entering __table rows__. Therefore, a table can be seen as a sequence of table rows. A table row is nothing as a sequence of __table cells__. The length of a table is the number of table rows, the width of a table is the maximum length of its rows. The first cell of a row must begin with the vertical bar character (""''|''"", U+007C) at the first position of a line. The other cells of a row begin with the same vertical bar character at later positions in that line. |
︙ | ︙ |
Changes to docs/manual/00001007031200.zettel.
1 2 3 4 5 | id: 00001007031200 title: Zettelmarkup: Inline-Zettel Block 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 26 27 28 29 30 31 32 33 | id: 00001007031200 title: Zettelmarkup: Inline-Zettel Block role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220311112247 An inline-zettel block allows to specify some content with another syntax without creating a new zettel. This is useful, for example, if you want to embed some [[Markdown|00001008010500]] content, because you are too lazy to translate Markdown into Zettelmarkup. Another example is to specify HTML code to use it for some kind of web front-end framework. As all other [[line-range blocks|00001007030000#line-range-blocks]], an inline-zettel block begins with at least three identical characters, starting at the first position of a line. For inline-zettel blocks, the at-sign character (""''@''"", U+0040) is used. You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters. The inline-zettel block uses the attribute key ""syntax"" to specify the [[syntax|00001008000000]] of the inline-zettel. Alternatively, you can use the generic attribute to specify the syntax value. If no value is provided, ""[[text|00001008000000#text]]"" is assumed. Any other character in this first 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 at-sign characters given at the beginning line. This allows to enter some at-sign characters in the text that should not be interpreted at this level. Some examples: ```zmk @@@markdown A link to [this](00001007031200) zettel. @@@ ``` will be rendered as: :::example @@@markdown |
︙ | ︙ |
Added docs/manual/00001007031300.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 | 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: `````zmk ~~~~ ~~~ ~~~~ ````` will be rendered in HTML as: :::example ~~~~ ~~~ ~~~~ ::: `````zmk ~~~{-} This is some text with no real sense. ~~~~ ````` will be rendered as: :::example ~~~{-} This is some text with no real sense. ~~~~ ::: `````zmk ~~~draw +---+ +---+ | A | --> | B | +---+ +---+ ~~~ ````` will be rendered as: :::example ~~~draw +---+ +---+ | A | --> | B | +---+ +---+ ~~~ ::: |
Added docs/manual/00001007031400.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 | 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: `````zmk $$$$ $$$ $$$$ ````` will be rendered in HTML as: :::example $$$$ $$$ $$$$ ::: `````zmk $$${-} This is some text with no real sense. $$$$ ````` will be rendered as: :::example $$${-} This is some text with no real sense. $$$$ ::: In the future, Zettelstore might somehow support mathematical formulae with a $$\TeX$$-like syntax. Until then, `````zmk $$$ \begin{align*} f(x) &= x^2\\ g(x) &= \frac{1}{x}\\ F(x) &= \int^a_b \frac{1}{3}x^3 \end{align*} $$$ ````` is rendered as: :::example $$$ \begin{align*} f(x) &= x^2\\ g(x) &= \frac{1}{x}\\ F(x) &= \int^a_b \frac{1}{3}x^3 \end{align*} $$$ ::: |
Changes to docs/manual/00001007040100.zettel.
︙ | ︙ | |||
18 19 20 21 22 23 24 | ** Example: ``abc __def__ ghi`` is rendered in HTML as: ::abc __def__ ghi::{=example}. * The asterisk character (""''*''"", U+002A) strongly emphasized its enclosed text. ** Example: ``abc **def** ghi`` is rendered in HTML as: ::abc **def** ghi::{=example}. * The greater-than sign character (""''>''"", U+003E) marks text as inserted. ** Example: ``abc >>def>> ghi`` is rendered in HTML as: ::abc >>def>> ghi::{=example}. * Similar, the tilde character (""''~''"", U+007E) marks deleted text. ** Example: ``abc ~~def~~ ghi`` is rendered in HTML as: ::abc ~~def~~ ghi::{=example}. | | | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | ** Example: ``abc __def__ ghi`` is rendered in HTML as: ::abc __def__ ghi::{=example}. * The asterisk character (""''*''"", U+002A) strongly emphasized its enclosed text. ** Example: ``abc **def** ghi`` is rendered in HTML as: ::abc **def** ghi::{=example}. * The greater-than sign character (""''>''"", U+003E) marks text as inserted. ** Example: ``abc >>def>> ghi`` is rendered in HTML as: ::abc >>def>> ghi::{=example}. * Similar, the tilde character (""''~''"", U+007E) marks deleted text. ** Example: ``abc ~~def~~ ghi`` is rendered in HTML as: ::abc ~~def~~ ghi::{=example}. * 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/00001007040200.zettel.
1 2 3 4 5 | id: 00001007040200 title: Zettelmarkup: Literal-like formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001007040200 title: Zettelmarkup: Literal-like formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220311185110 There are some reasons to mark text that should be rendered as uninterpreted: * Mark text as literal, sometimes as part of a program. * Mark text as input you give into a computer via a keyboard. * Mark text as output from some computer, e.g. shown at the command line. === Literal text |
︙ | ︙ | |||
47 48 49 50 51 52 53 | Attributes can be specified, the default attribute has the same semantic as for literal text. === Inline-zettel snippet To specify an inline snippet in a different [[syntax|00001008000000]], delimit your text with two at-sign characters (""''@''"", U+0040) on each side. You can add some [[attributes|00001007050000]] immediate after the two closing at-sign characters to specify the syntax to use. Either use the attribute key ""syntax"" or use the generic attribute to specify the syntax value. | | > > > > > > > > > > > > > > > > > > > | 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 | Attributes can be specified, the default attribute has the same semantic as for literal text. === Inline-zettel snippet To specify an inline snippet in a different [[syntax|00001008000000]], delimit your text with two at-sign characters (""''@''"", U+0040) on each side. You can add some [[attributes|00001007050000]] immediate after the two closing at-sign characters to specify the syntax to use. Either use the attribute key ""syntax"" or use the generic attribute to specify the syntax value. If no value is provided, ""[[text|00001008000000#text]]"" is assumed. Examples: * ``A @@-->@@ B`` renders in HTML as ::A @@-->@@ B::{=example}. * ``@@<small>@@{=html}Small@@</small>@@{=html}`` renders in HTML as ::@@<small>@@{=html}Small@@</small>@@{=html}::{=example}. To some degree, an inline-zettel snippet is the @@<small>@@{=html}smaller@@</small>@@{=html} sibling of the [[inline-zettel block|00001007031200]]. For HTML syntax, the same rules apply. === Math mode / $$\TeX$$ input This allows to enter text, that is typically interpreted by $$\TeX$$ or similar software. The main difference to all other literal-like formatting above is that the backslash character (""''\\''"", U+005C) has no special meaning. Therefore it is well suited the enter text with a lot of backslash characters. Math mode text is delimited with two dollar signs (""''$''"", U+0024) on each side. You can add some [[attributes|00001007050000]] immediate after the two closing at-sign characters to specify the syntax to use. Either use the attribute key ""syntax"" or use the generic attribute to specify the syntax value. If no syntax value is provided, math mode text roughly corresponds to literal text. Currently, Zettelstore does not support any syntax. This will probably change. However, external software might support specific syntax value, like ""tex"", ""latex"", ""mathjax"", ""itex"", or ""webtex"". Even an empty syntax value might be supported. Example: * ``Happy $$\\TeX$$!``is rendered as ::Happy $$\TeX$$!::{=example} |
Changes to docs/manual/00001007040324.zettel.
1 2 3 4 5 | id: 00001007040324 title: Zettelmarkup: Inline-mode 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 | id: 00001007040324 title: Zettelmarkup: Inline-mode Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220311110814 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. If it contains other transclusions, these will be expanded, recursively. When an endless recursion is detected, expansion does not take place. Instead an error message replaces the transclude specification. |
︙ | ︙ |
Changes to docs/manual/00001007040340.zettel.
1 2 3 4 5 6 7 | id: 00001007040340 title: Zettelmarkup: Citation Key role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218133447 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | id: 00001007040340 title: Zettelmarkup: Citation Key role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220218133447 A citation key references some external material that is part of a bibliographical collection. Currently, Zettelstore implements this only partially, it is ""work in progress"". However, the syntax is: beginning with a left square bracket and followed by an at sign character (""''@''"", U+0040), a the citation key is given. The key is typically a sequence of letters and digits. If a comma character (""'',''"", U+002C) or a vertical bar character is given, the following is interpreted as [[inline elements|00001007040000]]. A right square bracket ends the text and the citation key element. |
Changes to docs/manual/00001007050000.zettel.
1 2 3 4 5 | id: 00001007050000 title: Zettelmarkup: Attributes role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001007050000 title: Zettelmarkup: Attributes role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk modified: 20220630194106 Attributes allows to modify the way how material is presented. Alternatively, they provide additional information to markup elements. To some degree, attributes are similar to [[HTML attributes|https://html.spec.whatwg.org/multipage/dom.html#global-attributes]]. Typical use cases for attributes are to specify the (natural) [[language|00001007050100]] for a text region, to specify the [[programming language|00001007050200]] for highlighting program code, or to make white space visible in plain text. |
︙ | ︙ | |||
50 51 52 53 54 55 56 | The key ""''-''"" (just hyphen-minus) is special. It is called __default attribute__ and has a markup specific meaning. For example, when used for plain text, it replaces the non-visible space with a visible representation: * ''``Hello, world``{-}'' produces ==Hello, world=={-}. * ''``Hello, world``'' produces ==Hello, world==. | < < | < < < < < < < < < < < | < < < | | < < < | < < < < < < < < | < < < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | The key ""''-''"" (just hyphen-minus) is special. It is called __default attribute__ and has a markup specific meaning. For example, when used for plain text, it replaces the non-visible space with a visible representation: * ''``Hello, world``{-}'' produces ==Hello, world=={-}. * ''``Hello, world``'' produces ==Hello, world==. Attributes may be continued on the next line when a space or line ending character is possible. In case of a quoted attribute value, the line ending character will be part of the attribute value. For example: ``` {key="quoted value"} ``` will produce a value ''quoted\\nvalue'' (where \\n denotes a line ending character). ``` ::GREEN::{class=example background=grey} ``` is allowed, but not ``` ::GREEN::{background=color: green} ```. However, ``` ::GREEN::{background=color:" green"} ``` is allowed, because line endings are allowed within quotes. For [[block-structured elements|00001007030000]], there is a syntax variant if you only want to specify a generic attribute. For all line-range blocks you can specify the generic attributes directly in the first line, after the three (or more) characters starting the block. ``` :::attr ... ::: ``` is equivalent to ``` :::{=attr} ... ::: ```. For block-structured elements, spaces are allowed between the blocks characters and the attributes. ``` === Heading {example} ``` is allowed and equivalent to ``` === Heading{example} ```. For [[inline-structured elements|00001007040000]], the attributes must immediately follow the inline markup. ``::GREEN::{example}`` is allowed, but not ``::GREEN:: {example}``. === Reference material * [[Supported attribute values for natural languages|00001007050100]] * [[Supported attribute values for programming languages|00001007050200]] |
Changes to docs/manual/00001007060000.zettel.
1 2 3 4 5 | id: 00001007060000 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 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | id: 00001007060000 title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk modified: 20220311120759 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]] | ''+'' | (free) | (free) | '','' | (free) | [[Sub-scripted text|00001007040100]] | ''-'' | [[Horizontal rule|00001007030400]] | ""[[en-dash|00001007040000]]"" | ''.'' | (free) | [[Horizontal ellipsis|00001007040000]] | ''/'' | (free) | (free) | '':'' | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]] | '';'' | [[Description term|00001007030100]] | [[Small text|00001007040100]] | ''<'' | [[Quotation block|00001007030600]] | (free) | ''='' | [[Headings|00001007030300]] | [[Computer output|00001007040200]] | ''>'' | [[Quotation lists|00001007030200]] | [[Inserted text|00001007040100]] | ''?'' | (free) | (free) | ''@'' | [[Inline-Zettel block|00001007031200]] | [[Inline-zettel snippet|00001007040200#inline-zettel-snippet]] | ''['' | (reserved) | [[Linked material|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]] | ''\\'' | (blocked by inline meaning) | [[Escape character|00001007040000]] | '']'' | (reserved) | End of [[link|00001007040300]], [[citation key|00001007040300]], [[footnote|00001007040300]], [[mark|00001007040300]] | ''^'' | (free) | [[Super-scripted text|00001007040100]] | ''_'' | (free) | [[Emphasized text|00001007040100]] | ''`'' | [[Verbatim block|00001007030500]] | [[Literal text|00001007040200]] | ''{'' | [[Transclusion|00001007031100]] | [[Embedded material|00001007040300]], [[Attribute|00001007050000]] | ''|'' | [[Table row / table cell|00001007031000]] | Separator within link and [[embed|00001007040320]] formatting | ''}'' | End of [[Transclusion|00001007031100]] | End of embedded material, End of Attribute | ''~'' | [[Evaluation block|00001007031300]] | [[Deleted text|00001007040100]] |
Changes to docs/manual/00001008000000.zettel.
1 2 3 4 5 | id: 00001008000000 title: Other Markup Languages 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | id: 00001008000000 title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk modified: 20220627192329 [[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]]). 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]]. |
︙ | ︙ | |||
50 51 52 53 54 55 56 | : Just 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. | > > > > > > > | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | : Just 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/00001008010000.zettel.
1 2 3 4 5 | id: 00001008010000 title: Use Markdown within Zettelstore role: manual tags: #manual #markdown #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: 00001008010000 title: Use Markdown within Zettelstore role: manual tags: #manual #markdown #zettelstore syntax: zmk modified: 20220627192014 If you are customized to use Markdown as your markup language, you can configure Zettelstore to support your decision. Zettelstore supports the [[CommonMark|00001008010500]] dialect of Markdown. === Use Markdown as the default markup language of Zettelstore Update the [[New Zettel|00000000090001]] template (and other relevant template zettel) by setting the syntax value to ''md'' or ''markdown''. Whether to use ''md'' or ''markdown'' is not just a matter to taste. It also depends on the value of [[''zettel-file-syntax''|00001004020000#zettel-file-syntax]] and, to some degree, on the value of [[''yaml-header''|00001004020000#yaml-header]]. If you set ''yaml-header'' to true, then new content is always stored in a file with the extension ''.zettel''. Otherwise ''zettel-file-syntax'' lists all syntax values, where its content should be stored in a file with the extension ''.zettel''. If neither ''yaml-header'' nor ''zettel-file-syntax'' is set, new content is stored in a file where its file name extension is the same as the syntax value of that zettel. In this case it makes a difference, whether you specify ''md'' or ''markdown''. If you specify the syntax ''md'', your content will be stored in a file with the ''.md'' extension. Similar for the syntax ''markdown''. If you want to process the files that store the zettel content, e.g. with some other Markdown tools, this may be important. Not every Markdown tool allows both file extensions. BTW, metadata is stored in a file without a file extension, if neither ''yaml-header'' nor ''zettel-file-syntax'' is set. === Security aspects You should be aware that Markdown is a super-set of HTML. Any HTML code is valid Markdown code. If you write your own zettel, this is probably not a problem. However, if you receive zettel from others, you should be careful. An attacker might include malicious HTML code in your zettel. For example, HTML allows to embed JavaScript, a full-sized programming language that drives many web sites. When a zettel is displayed, JavaScript code might be executed, sometimes with harmful results. |
︙ | ︙ |
Changes to docs/manual/00001008010500.zettel.
︙ | ︙ | |||
15 16 17 18 19 20 21 | But they provide proprietary extensions, which makes it harder to change to another CommonMark implementation if needed. Plus, they sometimes build on an older specification of CommonMark. Zettelstore supports the latest CommonMark [[specification version 0.30 (2021-06-19)|https://spec.commonmark.org/0.30/]]. If possible, Zettelstore will adapt to newer versions when they are available. To provide CommonMark support, Zettelstore uses currently the [[Goldmark|https://github.com/yuin/goldmark]] implementation, which passes all validation tests of CommonMark. | | | | 15 16 17 18 19 20 21 22 23 24 25 26 | But they provide proprietary extensions, which makes it harder to change to another CommonMark implementation if needed. Plus, they sometimes build on an older specification of CommonMark. Zettelstore supports the latest CommonMark [[specification version 0.30 (2021-06-19)|https://spec.commonmark.org/0.30/]]. If possible, Zettelstore will adapt to newer versions when they are available. To provide CommonMark support, Zettelstore uses currently the [[Goldmark|https://github.com/yuin/goldmark]] implementation, which passes all validation tests of CommonMark. Internally, CommonMark is translated into some kind of super-set of [[Zettelmarkup|00001007000000]], which additionally allows to use HTML code.[^Effectively, Markdown and CommonMark are itself super-sets of HTML.] This Zettelmarkup super-set is later [[encoded|00001012920500]], often into [[HTML|00001012920510]]. Because Zettelstore HTML encoding philosophy differs a little bit to that of CommonMark, Zettelstore itself will not pass the CommonMark test suite fully. However, no CommonMark language element will fail to be encoded as HTML. In most cases, the differences are not visible for an user, but only by comparing the generated HTML code. |
Changes to docs/manual/00001008050000.zettel.
1 2 3 4 5 | id: 00001008050000 title: The ""draw"" language role: manual tags: #graphic #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 | 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 +-------+ .-------. | Box 1 | ----> | Box 2 | +-------+ '-------' ~~~ ``` Zettelstore translates this to: ~~~draw +-------+ .-------. | Box 1 | ----> | Box 2 | +-------+ '-------' ~~~ Technically spoken, the drawing is translated to a [[SVG|00001008000000#svg]] element. The following characters are interpreted to create a graphical representation. Some of them will start a path that results in a recognized object. |=Character:|Meaning|Path Start: |
︙ | ︙ |
Changes to docs/manual/00001010000000.zettel.
︙ | ︙ | |||
38 39 40 41 42 43 44 | Or you want to allow them to create new zettel, or to change them. It is up to you. If someone is authenticated as the owner of the Zettelstore (hopefully you), no restrictions apply. But as an owner, you can create ""user zettel"" to allow others to access your Zettelstore in various ways. Even if you do not want to share your Zettelstore with other persons, creating user zettel can be useful if you plan to access your Zettelstore via the [[API|00001012000000]]. | | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | Or you want to allow them to create new zettel, or to change them. It is up to you. If someone is authenticated as the owner of the Zettelstore (hopefully you), no restrictions apply. But as an owner, you can create ""user zettel"" to allow others to access your Zettelstore in various ways. Even if you do not want to share your Zettelstore with other persons, creating user zettel can be useful if you plan to access your Zettelstore via the [[API|00001012000000]]. Additionally, you can specify that a zettel is publicly visible. In this case no one has to authenticate itself to see the content of the zettel. Or you can specify that a zettel is visible only to the owner. In this case, no authenticated user will be able to read and change that protected zettel. * [[Visibility rules for zettel|00001010070200]] * [[User roles|00001010070300]] define basic rights of an user * [[Authorization and read-only mode|00001010070400]] |
︙ | ︙ |
Changes to docs/manual/00001010040100.zettel.
1 2 3 4 5 6 7 8 9 | id: 00001010040100 title: Enable authentication role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk To enable authentication, you must create a zettel that stores [[authentication data|00001010040200]] for the owner. Then you must reference this zettel within the [[startup configuration|00001004010000#owner]] under the key ''owner''. Once the startup configuration contains a valid [[zettel identifier|00001006050000]] under that key, authentication is enabled. | > > > | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001010040100 title: Enable authentication role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk modified: 20220419192817 To enable authentication, you must create a zettel that stores [[authentication data|00001010040200]] for the owner. Then you must reference this zettel within the [[startup configuration|00001004010000#owner]] under the key ''owner''. Once the startup configuration contains a valid [[zettel identifier|00001006050000]] under that key, authentication is enabled. Please note that you must also set key ''secret'' of the [[startup configuration|00001004010000#secret]] to some random string data (minimum length is 16 bytes) to secure the data exchanged with a client system. |
Changes to docs/manual/00001012000000.zettel.
1 2 3 4 5 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220627183444 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. |
︙ | ︙ | |||
23 24 25 26 27 28 29 | === Zettel lists * [[List metadata of all zettel|00001012051200]] * [[Shape the list of zettel metadata|00001012051800]] ** [[Selection of zettel|00001012051810]] ** [[Limit the list length|00001012051830]] ** [[Content search|00001012051840]] ** [[Sort the list of zettel metadata|00001012052000]] | | < | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | === Zettel lists * [[List metadata of all zettel|00001012051200]] * [[Shape the list of zettel metadata|00001012051800]] ** [[Selection of zettel|00001012051810]] ** [[Limit the list length|00001012051830]] ** [[Content search|00001012051840]] ** [[Sort the list of zettel metadata|00001012052000]] * [[Map metadata values to lists of zettel identifier|00001012052400]] === 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]] |
︙ | ︙ |
Changes to docs/manual/00001012052400.zettel.
1 | id: 00001012052400 | | > > | > > > > > > > > > > > > > > > > | | | | 1 2 3 4 5 6 7 8 9 10 11 12 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: 00001012052400 title: API: Map metadata values to list of zettel identifier role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220627183323 The [[endpoint|00001012920000]] ''/m'' allows to retrieve a map of metadata values (of a specific key) to the list of zettel identifier, which reference zettel containing this value under the given metadata key. Currently, two keys are supported: * [[''role''|00001006020100]] * [[''tags''|00001006020000#tags]] To list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/m?_key=role''. If successful, the output is a JSON object: ```sh # curl http://127.0.0.1:23123/m?_key=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 ''/m?_key=tags''. If successful, the output is a JSON object: ```sh # curl http://127.0.0.1:23123/m?_key=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. Please note that this structure will likely change in the future to be more compliant with other API calls. |
Deleted docs/manual/00001012052600.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 | 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"'' |
︙ | ︙ | |||
23 24 25 26 27 28 29 | 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. | | | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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"'' |
︙ | ︙ |
Changes to docs/manual/00001012053300.zettel.
1 2 3 4 5 | id: 00001012053300 title: API: Retrieve metadata and content of an existing 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 | id: 00001012053300 title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220724163741 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": { |
︙ | ︙ | |||
49 50 51 52 53 54 55 56 57 58 59 60 61 62 | 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]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|00001012920800]]''. Valid values are ""zettel"", ""[[meta|00001012053400]]"", and ""content"" (the default value). ````sh | > | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | 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|00001012920800]]''. Valid values are ""zettel"", ""[[meta|00001012053400]]"", and ""content"" (the default value). ````sh |
︙ | ︙ |
Changes to docs/manual/00001012053500.zettel.
1 2 3 4 5 | id: 00001012053500 title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001012053500 title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk modified: 20220410153546 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 |
︙ | ︙ | |||
52 53 54 55 56 57 58 | <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"> ``` | < < | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | <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'' : Zettel not found. You probably used a zettel identifier that is not used in the Zettelstore. |
Changes to docs/manual/00001012920000.zettel.
1 2 3 4 5 | id: 00001012920000 title: Endpoints used by the API 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 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: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20220627183408 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: [[list zettel AS JSON|00001012051200]] | 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: [[map metadata values|00001012052400]] | GET: [[retrieve metadata|00001012053400]] | **M**etadata | ''o'' | | GET: [[list zettel order|00001012054000]] | **O**rder | ''p'' | | GET: [[retrieve parsed zettel|00001012053600]]| **P**arsed | ''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 2 3 4 5 | id: 00001012920500 title: Encodings available via the [[API|00001012000000]] role: manual tags: #api #manual #reference #zettelstore syntax: zmk | | < | > | 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]] |
Changes to docs/manual/00001012920503.zettel.
1 2 3 4 5 | id: 00001012920503 title: ZJSON Encoding 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 17 18 | id: 00001012920503 title: ZJSON Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20220422191748 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''. |
︙ | ︙ |
Deleted docs/manual/00001012920513.zettel.
|
| < < < < < < < < < < < < < |
Added docs/manual/00001012920516.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 | id: 00001012920516 title: Sexpr Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20220724170637 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). |
Changes to docs/manual/00001012920519.zettel.
1 2 3 4 5 6 7 8 9 10 | id: 00001012920519 title: Text Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20210726193119 A zettel representation contains just all textual data of a zettel. Could be used for creating a search index. | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 | id: 00001012920519 title: Text Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk modified: 20210726193119 A zettel representation contains just all textual data of a zettel. Could be used for creating a search index. Every line may contain zero, one, or more words, separated by space character. If transferred via HTTP, the content type will be ''text/plain''. |
Changes to docs/manual/00001012921000.zettel.
1 2 3 4 5 6 | id: 00001012921000 title: API: JSON structure of an access token tags: #api #manual #reference #zettelstore syntax: zmk role: manual | | | 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 domain/id/id.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ | |||
44 45 46 47 48 49 50 51 52 53 54 55 56 57 | FormTemplateZid = MustParse(api.ZidFormTemplate) RenameTemplateZid = MustParse(api.ZidRenameTemplate) DeleteTemplateZid = MustParse(api.ZidDeleteTemplate) ContextTemplateZid = MustParse(api.ZidContextTemplate) RolesTemplateZid = MustParse(api.ZidRolesTemplate) TagsTemplateZid = MustParse(api.ZidTagsTemplate) ErrorTemplateZid = MustParse(api.ZidErrorTemplate) EmojiZid = MustParse(api.ZidEmoji) TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate) DefaultHomeZid = MustParse(api.ZidDefaultHome) ) const maxZid = 99999999999999 | > | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | FormTemplateZid = MustParse(api.ZidFormTemplate) RenameTemplateZid = MustParse(api.ZidRenameTemplate) DeleteTemplateZid = MustParse(api.ZidDeleteTemplate) ContextTemplateZid = MustParse(api.ZidContextTemplate) RolesTemplateZid = MustParse(api.ZidRolesTemplate) TagsTemplateZid = MustParse(api.ZidTagsTemplate) ErrorTemplateZid = MustParse(api.ZidErrorTemplate) RoleCSSMapZid = MustParse(api.ZidRoleCSSMap) EmojiZid = MustParse(api.ZidEmoji) TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate) DefaultHomeZid = MustParse(api.ZidDefaultHome) ) const maxZid = 99999999999999 |
︙ | ︙ |
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 } |
Changes to domain/meta/meta.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 28 29 | "regexp" "sort" "strings" "unicode" "unicode/utf8" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" "zettelstore.de/z/input" "zettelstore.de/z/strfun" ) type keyUsage int | > | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | "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 |
︙ | ︙ | |||
102 103 104 105 106 107 108 | return *d } return DescriptionKey{Type: Type(name)} } // GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name. func GetSortedKeyDescriptions() []*DescriptionKey { | < | < < < | | | 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | 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() { |
︙ | ︙ | |||
226 227 228 229 230 231 232 233 234 235 236 237 238 239 | // 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) } } 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. | > > > > > > > > > > | 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | // 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. |
︙ | ︙ | |||
249 250 251 252 253 254 255 256 257 258 259 260 261 262 | // stored, the given default value is returned. func (m *Meta) GetDefault(key, def string) string { if value, ok := m.Get(key); ok { return value } return def } // Pairs returns 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) } | > > > > > > > > > | 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | // 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) } |
︙ | ︙ |
Changes to domain/meta/meta_test.go.
1 2 3 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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'. |
︙ | ︙ | |||
36 37 38 39 40 41 42 | } } } func TestTitleHeader(t *testing.T) { t.Parallel() m := New(testID) | | | | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | } } } 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) } |
︙ | ︙ |
Changes to domain/meta/parse.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- // Package meta provides the domain specific type 'meta'. package meta import ( | < > | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // 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 { |
︙ | ︙ | |||
125 126 127 128 129 130 131 | 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) | < < < < < < | | 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | 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) |
︙ | ︙ | |||
155 156 157 158 159 160 161 | switch key { case "", api.KeyID: // Empty key and 'id' key will be ignored return } switch Type(key) { | < < < < | 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | 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] == '#' }) case TypeWord: m.Set(key, strings.ToLower(v)) case TypeWordSet: addSet(m, key, strings.ToLower(v), func(s string) bool { return true }) case TypeID: |
︙ | ︙ |
Changes to domain/meta/parse_test.go.
1 2 3 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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'. |
︙ | ︙ | |||
52 53 54 55 56 57 58 | 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) } } | < < < < < > | 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 | 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 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) } } |
︙ | ︙ |
Changes to domain/meta/type.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "strconv" "strings" "sync" "time" "zettelstore.de/c/api" | < | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "strconv" "strings" "sync" "time" "zettelstore.de/c/api" ) // DescriptionType is a description of a specific key type. type DescriptionType struct { Name string IsSet bool } |
︙ | ︙ | |||
39 40 41 42 43 44 45 | t := &DescriptionType{name, isSet} registeredTypes[name] = t return t } // Supported key types. var ( | | | | | | | | | | | | | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | 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) } |
︙ | ︙ |
Changes to encoder/encoder.go.
︙ | ︙ | |||
41 42 43 44 45 46 47 | 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.
︙ | ︙ | |||
35 36 37 38 39 40 41 | 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{ | | | | | | | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | 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") |
︙ | ︙ |
Changes to encoder/encoder_block_test.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | package encoder_test var tcsBlock = []zmkTestCase{ { descr: "Empty Zettelmarkup should produce near nothing", zmk: "", expect: expectMap{ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | < < < | | | | | | < < < < < < < < < < < < < < < < < < < < < < < < > | | | | < | < < < < | | | | | | | | | | | | | | < < | | | < | < | | | < | < < | | | | | | | | | < | | | | | > | > > > > > > > > > > > > > > > > > > > > > | | > > > > > > > > > > > | | < < < < < < < < | | < < | < < < < < < > | | < < < < < | < < < < < < < > | > > > > > > > > > > > | | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | 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: "", zmk: ``, expect: expectMap{ encoderZJSON: `[]`, encoderHTML: ``, encoderSexpr: `()`, encoderText: "", encoderZmk: useZmk, }, }, } // func TestEncoderBlock(t *testing.T) { // executeTestCases(t, tcsBlock) // } |
Changes to encoder/encoder_inline_test.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | package encoder_test var tcsInline = []zmkTestCase{ { descr: "Empty Zettelmarkup should produce near nothing (inline)", zmk: "", expect: expectMap{ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | > | > > > > > > > > > > | | > > > > > > > > > > > | | | | | | | > > | > > > > > > > > > | | | | | | | | | | | | | > > > > > > > > > > > | | | | | | | | | | | | | > | > > > > > > > > > > | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | 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: `<q lang="de">quotes</q>`, 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\u00a0</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: "Dummy Embed", zmk: `{{abc}}`, expect: expectMap{ encoderZJSON: `[{"":"Embed","s":"abc"}]`, encoderHTML: `<img src="abc">`, encoderSexpr: `((EMBED () (EXTERNAL "abc") ""))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "", zmk: ``, expect: expectMap{ encoderZJSON: `[]`, encoderHTML: ``, encoderSexpr: `()`, encoderText: ``, encoderZmk: useZmk, }, }, } |
Changes to encoder/encoder_test.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 | package encoder_test import ( "bytes" "fmt" "testing" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" | > > | | | | | < | | | | | < > < | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | 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/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)} } 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 { |
︙ | ︙ |
Deleted encoder/env.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/htmlenc/block.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
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/htmlenc/inline.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/htmlenc/visitor.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoder/nativeenc/nativeenc.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, getReference(n.Ref)) case *ast.BLOBNode: return getBLOB(n) case *ast.TextNode: return sxpf.NewPairFromValues(sexpr.SymText, sxpf.NewString(n.Text)) case *ast.TagNode: return sxpf.NewPairFromValues(sexpr.SymTag, sxpf.NewString(n.Tag)) 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.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.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 } |
Changes to encoder/textenc/textenc.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" ) func init() { | | < < > > > | > > | | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | "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{} var myTE Encoder // Only a singleton is required. // WriteZettel writes metadata and content. func (te *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { v := newVisitor(w) te.WriteMeta(&v.b, zn.InhMeta, evalMeta) v.visitBlockSlice(&zn.Ast) length, err := v.b.Flush() return length, err } // WriteMeta encodes metadata as text. func (te *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { buf := encoder.NewEncWriter(w) for _, pair := range m.ComputedPairs() { switch meta.Type(pair.Key) { case meta.TypeTagSet: writeTagSet(&buf, meta.ListFromValue(pair.Value)) case meta.TypeZettelmarkup: is := evalMeta(pair.Value) |
︙ | ︙ | |||
62 63 64 65 66 67 68 | buf.WriteByte(' ') } buf.WriteString(meta.CleanTag(tag)) } } | | | | > < | < < < | 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 | buf.WriteByte(' ') } buf.WriteString(meta.CleanTag(tag)) } } func (te *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return te.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes the content of a block slice to the writer. func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { v := newVisitor(w) v.visitBlockSlice(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 := 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 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) return nil case *ast.InlineSlice: v.visitInlineSlice(n) return nil case *ast.VerbatimNode: v.visitVerbatim(n) return nil case *ast.RegionNode: v.visitBlockSlice(&n.Blocks) if len(n.Inlines) > 0 { |
︙ | ︙ | |||
218 219 220 221 222 223 224 225 226 227 228 229 230 | func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) { for i, bn := range *bs { v.writePosChar(i, '\n') ast.Walk(v, bn) } } func (v *visitor) writePosChar(pos int, ch byte) { if pos > 0 { v.b.WriteByte(ch) } } | > > > > > > > > | 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) { for i, bn := range *bs { v.writePosChar(i, '\n') ast.Walk(v, bn) } } func (v *visitor) visitInlineSlice(is *ast.InlineSlice) { for i, in := range *is { v.inlinePos = i ast.Walk(v, in) } v.inlinePos = 0 } func (v *visitor) writePosChar(pos int, ch byte) { if pos > 0 { v.b.WriteByte(ch) } } |
Changes to encoder/zjsonenc/zjsonenc.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "fmt" "io" "strconv" "zettelstore.de/c/api" "zettelstore.de/c/zjson" "zettelstore.de/z/ast" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" "zettelstore.de/z/strfun" ) func init() { | > | < < > > | | | > | | | | | | | | | < < | < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | 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: |
︙ | ︙ | |||
191 192 193 194 195 196 197 198 199 200 201 202 203 204 | v.b.WriteByte('}') return nil } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: zjson.TypeVerbatimZettel, ast.VerbatimProg: zjson.TypeVerbatimCode, ast.VerbatimComment: zjson.TypeVerbatimComment, ast.VerbatimHTML: zjson.TypeVerbatimHTML, } func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { kind, ok := mapVerbatimKind[vn.Kind] if !ok { | > > | 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | 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 { |
︙ | ︙ | |||
435 436 437 438 439 440 441 442 443 444 445 446 447 448 | 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, } func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) { v.b.WriteByte('[') for i, bn := range *bs { v.writeComma(i) ast.Walk(v, bn) | > | 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 | 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) |
︙ | ︙ | |||
456 457 458 459 460 461 462 | v.writeComma(i) ast.Walk(v, in) } v.b.WriteByte(']') } // visitAttributes write JSON attributes | | | 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 | 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 { |
︙ | ︙ |
Changes to encoder/zmkenc/zmkenc.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | package zmkenc import ( "fmt" "io" "zettelstore.de/c/api" | | | < < > > | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | 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() { |
︙ | ︙ | |||
68 69 70 71 72 73 74 | } 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. | | | | | < | | < < < | 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 | } 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: |
︙ | ︙ | |||
182 183 184 185 186 187 188 189 190 191 192 193 194 195 | } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: "@@@", ast.VerbatimComment: "%%%", ast.VerbatimHTML: "@@@", // Attribute is set to {="html"} ast.VerbatimProg: "```", } func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { kind, ok := mapVerbatimKind[vn.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim kind %d", vn.Kind)) } | > > | 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: "@@@", ast.VerbatimComment: "%%%", ast.VerbatimHTML: "@@@", // Attribute is set to {="html"} ast.VerbatimProg: "```", ast.VerbatimEval: "~~~", ast.VerbatimMath: "$$$", } func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { kind, ok := mapVerbatimKind[vn.Kind] if !ok { panic(fmt.Sprintf("Unknown verbatim kind %d", vn.Kind)) } |
︙ | ︙ | |||
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 | func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralZettel: v.writeLiteral('@', ln.Attrs, ln.Content) case ast.LiteralProg: v.writeLiteral('`', ln.Attrs, ln.Content) case ast.LiteralInput: v.writeLiteral('\'', ln.Attrs, ln.Content) case ast.LiteralOutput: v.writeLiteral('=', ln.Attrs, ln.Content) case ast.LiteralComment: if v.inlinePos > 0 { v.b.WriteByte(' ') } | > > > | > > | | | | 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 | func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralZettel: v.writeLiteral('@', ln.Attrs, ln.Content) case ast.LiteralProg: v.writeLiteral('`', ln.Attrs, ln.Content) case ast.LiteralMath: v.b.WriteStrings("$$", string(ln.Content), "$$") v.visitAttributes(ln.Attrs) case ast.LiteralInput: v.writeLiteral('\'', ln.Attrs, ln.Content) case ast.LiteralOutput: v.writeLiteral('=', ln.Attrs, ln.Content) case ast.LiteralComment: if v.inlinePos > 0 { v.b.WriteByte(' ') } v.b.WriteString("%%") v.visitAttributes(ln.Attrs) v.b.WriteByte(' ') v.b.Write(ln.Content) case ast.LiteralHTML: v.writeLiteral('x', syntaxToHTML(ln.Attrs), ln.Content) default: panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind)) } } func (v *visitor) writeLiteral(code byte, a attrs.Attributes, content []byte) { v.b.WriteBytes(code, code) v.writeEscaped(string(content), code) v.b.WriteBytes(code, code) v.visitAttributes(a) } // visitAttributes write HTML attributes func (v *visitor) visitAttributes(a attrs.Attributes) { if a.IsEmpty() { return } v.b.WriteByte('{') for i, k := range a.Keys() { if i > 0 { v.b.WriteByte(' ') |
︙ | ︙ | |||
517 518 519 520 521 522 523 | v.b.WriteBytes('\\', b) last = i + 1 } } v.b.WriteString(s[last:]) } | | | 520 521 522 523 524 525 526 527 528 529 | 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 evaluator/evaluator.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "context" "errors" "fmt" "strconv" "strings" "zettelstore.de/c/api" | | > < < < < < < < < < < | | | | | < < < < < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | "context" "errors" "fmt" "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" ) // 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) } // 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 } evaluateNode(ctx, port, rtConfig, &zn.Ast) cleaner.CleanBlockSlice(&zn.Ast) } // EvaluateInline evaluates the given inline list in the given context, with // the given ports, and the given environment. func EvaluateInline(ctx context.Context, port Port, rtConfig config.Config, is *ast.InlineSlice) { evaluateNode(ctx, port, rtConfig, is) cleaner.CleanInlineSlice(is) } func evaluateNode(ctx context.Context, port Port, rtConfig config.Config, n ast.Node) { e := evaluator{ ctx: ctx, port: port, rtConfig: rtConfig, transcludeMax: rtConfig.GetMaxTransclusions(), transcludeCount: 0, costMap: map[id.Zid]transcludeCost{}, embedMap: map[string]ast.InlineSlice{}, marker: &ast.ZettelNode{}, } ast.Walk(&e, n) } type evaluator struct { ctx context.Context port Port rtConfig config.Config transcludeMax int transcludeCount int costMap map[id.Zid]transcludeCost marker *ast.ZettelNode embedMap map[string]ast.InlineSlice } |
︙ | ︙ | |||
150 151 152 153 154 155 156 | if i+1 < len(bns) { newIns = append(newIns, bns[i+1:]...) } return newIns } func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode { | > | > > > > > > | | > > | | | 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 | if i+1 < len(bns) { newIns = append(newIns, bns[i+1:]...) } return newIns } func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode { switch vn.Kind { case ast.VerbatimZettel: return e.evalVerbatimZettel(vn) case ast.VerbatimEval: if syntax, found := vn.Attrs.Get(""); found && syntax == 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 { if a != nil { if val, ok := a.Get(api.KeySyntax); ok { return val } if val, ok := a.Get(""); ok { return val } |
︙ | ︙ | |||
245 246 247 248 249 250 251 | func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) } func (e *evaluator) visitInlineSlice(is *ast.InlineSlice) { for i := 0; i < len(*is); i++ { in := (*is)[i] ast.Walk(e, in) switch n := in.(type) { | < < | 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) } func (e *evaluator) visitInlineSlice(is *ast.InlineSlice) { for i := 0; i < len(*is); i++ { in := (*is)[i] ast.Walk(e, in) switch n := in.(type) { case *ast.LinkNode: (*is)[i] = e.evalLinkNode(n) case *ast.EmbedRefNode: i += embedNode(is, i, e.evalEmbedRefNode(n)) case *ast.LiteralNode: i += embedNode(is, i, e.evalLiteralNode(n)) } |
︙ | ︙ | |||
284 285 286 287 288 289 290 | } if i+1 < len(ins) { newIns = append(newIns, ins[i+1:]...) } return newIns } | | | < | < < | < < < < < < < < < < < < < < | < < | < | 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 i+1 < len(ins) { newIns = append(newIns, ins[i+1:]...) } return newIns } func (e *evaluator) evalLinkNode(ln *ast.LinkNode) ast.InlineNode { if len(ln.Inlines) == 0 { ln.Inlines = ast.InlineSlice{&ast.TextNode{Text: ln.Ref.Value}} } ref := ln.Ref if ref == nil || ref.State != ast.RefStateZettel { return ln } zid := mustParseZid(ref) _, err := e.port.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 { ln.Ref.State = ast.RefStateBroken return ln } ln.Ref.State = ast.RefStateZettel return ln } func getLinkInline(ln *ast.LinkNode) ast.InlineSlice { if ln.Inlines != nil { return ln.Inlines } |
︙ | ︙ | |||
350 351 352 353 354 355 356 | } switch ref.State { case ast.RefStateZettel: // Only zettel references will be evaluated. case ast.RefStateInvalid, ast.RefStateBroken: e.transcludeCount++ | | | | > | | 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 | } switch ref.State { case ast.RefStateZettel: // Only zettel references will be evaluated. case ast.RefStateInvalid, ast.RefStateBroken: e.transcludeCount++ return createInlineErrorImage(en) case ast.RefStateSelf: e.transcludeCount++ return createInlineErrorText(ref, "Self", "embed", "reference:") case ast.RefStateFound, ast.RefStateHosted, ast.RefStateBased, ast.RefStateExternal: return en default: panic(fmt.Sprintf("Unknown state %v for reference %v", ref.State, ref)) } zid := mustParseZid(ref) zettel, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) if err != 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] |
︙ | ︙ | |||
399 400 401 402 403 404 405 406 407 408 409 410 411 412 | // Search for text to be embedded. result = findInlineSlice(&zn.Ast, ref.URL.Fragment) e.embedMap[ref.Value] = result } if len(result) == 0 { return &ast.LiteralNode{ Kind: ast.LiteralComment, Content: append([]byte("Nothing to transclude: "), ref.String()...), } } if ec := cost.ec; ec > 0 { e.transcludeCount += cost.ec } | > | 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 | // Search for text to be embedded. result = findInlineSlice(&zn.Ast, ref.URL.Fragment) e.embedMap[ref.Value] = result } if len(result) == 0 { return &ast.LiteralNode{ Kind: ast.LiteralComment, Attrs: map[string]string{"-": ""}, Content: append([]byte("Nothing to transclude: "), ref.String()...), } } if ec := cost.ec; ec > 0 { e.transcludeCount += cost.ec } |
︙ | ︙ | |||
422 423 424 425 426 427 428 | } func (e *evaluator) evalLiteralNode(ln *ast.LiteralNode) ast.InlineNode { if ln.Kind != ast.LiteralZettel { return ln } e.transcludeCount++ | | > < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 | } 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"), } } return &result } func createInlineErrorImage(en *ast.EmbedRefNode) *ast.EmbedRefNode { errorZid := id.EmojiZid en.Ref = ast.ParseReference(errorZid.String()) if len(en.Inlines) == 0 { en.Inlines = parser.ParseMetadata("Error placeholder") } return en } func createInlineErrorText(ref *ast.Reference, msgWords ...string) ast.InlineNode { text := strings.Join(msgWords, " ") if ref != nil { text += ": " + ref.String() + "." } ln := &ast.LiteralNode{ Kind: ast.LiteralInput, |
︙ | ︙ | |||
515 516 517 518 519 520 521 | 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 { | | | 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 | 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(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) |
︙ | ︙ | |||
561 562 563 564 565 566 567 | func (fs *fragmentSearcher) Visit(node ast.Node) ast.Visitor { if len(fs.result) > 0 { return nil } switch n := node.(type) { case *ast.BlockSlice: | > > > > > > > > > > | | | | | | | > | > | | | | | | | | | | | | | | < < | < < | 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 | func (fs *fragmentSearcher) Visit(node ast.Node) ast.Visitor { if len(fs.result) > 0 { return nil } switch n := node.(type) { case *ast.BlockSlice: fs.visitBlockSlice(n) case *ast.InlineSlice: fs.visitInlineSlice(n) default: return fs } return nil } func (fs *fragmentSearcher) visitBlockSlice(bs *ast.BlockSlice) { for i, bn := range *bs { if hn, ok := bn.(*ast.HeadingNode); ok && hn.Fragment == fs.fragment { fs.result = (*bs)[i+1:].FirstParagraphInlines() return } ast.Walk(fs, bn) } } func (fs *fragmentSearcher) visitInlineSlice(is *ast.InlineSlice) { for i, in := range *is { if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment { ris := skipSpaceNodes((*is)[i+1:]) if len(mn.Inlines) > 0 { fs.result = append(ast.InlineSlice{}, mn.Inlines...) fs.result = append(fs.result, &ast.SpaceNode{Lexeme: " "}) fs.result = append(fs.result, ris...) } else { fs.result = ris } return } ast.Walk(fs, in) } } func skipSpaceNodes(ins ast.InlineSlice) ast.InlineSlice { for i, in := range ins { switch in.(type) { case *ast.SpaceNode: case *ast.BreakNode: default: return ins[i:] } } return nil } |
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.18 require ( codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0 github.com/fsnotify/fsnotify v1.5.4 github.com/pascaldekloe/jwt v1.12.0 github.com/yuin/goldmark v1.4.13 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 golang.org/x/text v0.3.7 zettelstore.de/c v0.0.0-20220729135959-532261810eac ) require golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 // 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.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 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.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 h1:z8Hj/bl9cOV2grsOpEaQFUaly0JWN3i97mo3jXKJNp0= golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= zettelstore.de/c v0.0.0-20220729135959-532261810eac h1:T8GP4P8pHNfjQ2lXtpRXBmNF685xI0kraoou3tKqH2Y= zettelstore.de/c v0.0.0-20220729135959-532261810eac/go.mod h1:8wPlP6rYiXkfoCNE84K3bmhSAKHz15+xWSHMH26JAU0= |
Changes to input/input.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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. |
︙ | ︙ | |||
187 188 189 190 191 192 193 | } inp.Next() } } func (inp *Input) scanEntityNamed(pos int) (string, bool) { for { switch inp.Ch { | | > > > > > > > > > | > > > | > > > | > > > | 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 | } 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() } } } // 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]...) } } |
Changes to input/input_test.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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. |
︙ | ︙ | |||
63 64 65 66 67 68 69 | continue } if tc.exp != got { t.Errorf("ID=%d, text=%q: expected %q, but got %q", id, tc.text, tc.exp, got) } } } | > > > > > > > > > > > > > | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | 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 } } } |
Changes to input/runes.go.
1 | //----------------------------------------------------------------------------- | | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //----------------------------------------------------------------------------- // 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 // IsSpace returns true if rune is a whitespace. func IsSpace(ch rune) bool { switch ch { case ' ', '\t': return true |
︙ | ︙ |
Changes to kernel/impl/cfg.go.
︙ | ︙ | |||
32 33 34 35 36 37 38 | // Predefined Metadata keys for runtime configuration // See: https://zettelstore.de/manual/h/00001004020000 const ( keyDefaultCopyright = "default-copyright" keyDefaultLang = "default-lang" keyDefaultLicense = "default-license" | < < < < < < | < < < | | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | // Predefined Metadata keys for runtime configuration // See: https://zettelstore.de/manual/h/00001004020000 const ( keyDefaultCopyright = "default-copyright" keyDefaultLang = "default-lang" keyDefaultLicense = "default-license" keyDefaultVisibility = "default-visibility" keyExpertMode = "expert-mode" keyFooterHTML = "footer-html" keyHomeZettel = "home-zettel" keyMarkerExternal = "marker-external" keyMaxTransclusions = "max-transclusions" keySiteName = "site-name" keyYAMLHeader = "yaml-header" keyZettelFileSyntax = "zettel-file-syntax" ) func (cs *configService) Initialize(logger *logger.Logger) { cs.logger = logger cs.descr = descriptionMap{ keyDefaultCopyright: {"Default copyright", parseString, true}, keyDefaultLang: {"Default language", parseString, true}, keyDefaultLicense: {"Default license", parseString, true}, keyDefaultVisibility: { "Default zettel visibility", func(val string) interface{} { vis := meta.GetVisibility(val) if vis == meta.VisibilityUnknown { return nil } return vis }, true, }, keyExpertMode: {"Expert mode", parseBool, true}, keyFooterHTML: {"Footer HTML", parseString, true}, keyHomeZettel: {"Home zettel", parseZid, true}, 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) interface{} { return strings.Fields(val) }, true, }, kernel.ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true}, } cs.next = interfaceMap{ keyDefaultCopyright: "", keyDefaultLang: api.ValueLangEN, keyDefaultLicense: "", keyDefaultVisibility: meta.VisibilityLogin, keyExpertMode: false, keyFooterHTML: "", keyHomeZettel: id.DefaultHomeZid, keyMarkerExternal: "➚", keyMaxTransclusions: int64(1024), keySiteName: "Zettelstore", keyYAMLHeader: false, keyZettelFileSyntax: nil, kernel.ConfigSimpleMode: false, } } func (cs *configService) GetLogger() *logger.Logger { return cs.logger } |
︙ | ︙ | |||
185 186 187 188 189 190 191 | } } var defaultKeys = map[string]string{ api.KeyCopyright: keyDefaultCopyright, api.KeyLang: keyDefaultLang, api.KeyLicense: keyDefaultLicense, | < < < | 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | } } var defaultKeys = map[string]string{ api.KeyCopyright: keyDefaultCopyright, api.KeyLang: keyDefaultLang, api.KeyLicense: keyDefaultLicense, api.KeyVisibility: keyDefaultVisibility, } // AddDefaultValues enriches the given meta data with its default values. func (cfg *myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { if cfg == nil { return m |
︙ | ︙ | |||
225 226 227 228 229 230 231 | func (cfg *myConfig) getBool(key string) bool { cfg.mx.RLock() val := cfg.data.GetBool(key) cfg.mx.RUnlock() return val } | < < < < < < < < < < < < < < < < < < < < < < | 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 | func (cfg *myConfig) getBool(key string) bool { cfg.mx.RLock() val := cfg.data.GetBool(key) cfg.mx.RUnlock() return val } // GetDefaultLang returns the current value of the "default-lang" key. func (cfg *myConfig) GetDefaultLang() string { return cfg.getString(keyDefaultLang) } // GetSiteName returns the current value of the "site-name" key. func (cfg *myConfig) GetSiteName() string { return cfg.getString(keySiteName) } // GetHomeZettel returns the value of the "home-zettel" key. func (cfg *myConfig) GetHomeZettel() id.Zid { val := cfg.getString(keyHomeZettel) if homeZid, err := id.Parse(val); err == nil { return homeZid } cfg.mx.RLock() val, _ = cfg.orig.Get(keyHomeZettel) homeZid, _ := id.Parse(val) cfg.mx.RUnlock() return homeZid } // GetMaxTransclusions return the maximum number of indirect transclusions. func (cfg *myConfig) GetMaxTransclusions() int { const defaultValue = 1024 cfg.mx.RLock() val := cfg.data.GetNumber(keyMaxTransclusions, defaultValue) cfg.mx.RUnlock() if 0 < val && val < 100000000 { |
︙ | ︙ | |||
312 313 314 315 316 317 318 | // GetVisibility returns the visibility value, or "login" if none is given. func (cfg *myConfig) GetVisibility(m *meta.Meta) meta.Visibility { if val, ok := m.Get(api.KeyVisibility); ok { if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } } | > > > | | > > > > > > | 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | // GetVisibility returns the visibility value, or "login" if none is given. func (cfg *myConfig) GetVisibility(m *meta.Meta) meta.Visibility { if val, ok := m.Get(api.KeyVisibility); ok { if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } } val := cfg.getString(keyDefaultVisibility) if vis := meta.GetVisibility(val); vis != meta.VisibilityUnknown { return vis } cfg.mx.RLock() val, _ = cfg.orig.Get(keyDefaultVisibility) vis := meta.GetVisibility(val) cfg.mx.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 |
︙ | ︙ | |||
194 195 196 197 198 199 200 | }, "start": {"start service", cmdStart}, "stat": {"show service statistics", cmdStat}, "stop": {"stop service", cmdStop}, } func cmdHelp(sess *cmdSession, _ string, _ []string) bool { | | < < < < < < < | 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | }, "start": {"start service", cmdStart}, "stat": {"show service statistics", cmdStat}, "stop": {"stop service", cmdStop}, } func cmdHelp(sess *cmdSession, _ string, _ []string) bool { cmds := maps.Keys(commands) table := [][]string{{"Command", "Description"}} for _, cmd := range cmds { table = append(table, []string{cmd, commands[cmd].Text}) } sess.printTable(table) return true } |
︙ | ︙ | |||
560 561 562 563 564 565 566 | table = append(table, []string{env[:pos], env[pos+1:]}) } } sess.printTable(table) return true } | | < < < < < < < | 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 | table = append(table, []string{env[:pos], env[pos+1:]}) } } sess.printTable(table) return true } func sortedServiceNames(sess *cmdSession) []string { return maps.Keys(sess.kern.srvNames) } func getService(sess *cmdSession, name string) (serviceData, bool) { srvD, found := sess.kern.srvNames[name] if !found { sess.println("Unknown service", name) } return srvD, found } |
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 | //----------------------------------------------------------------------------- // 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" "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) interface{} type configDescription struct { |
︙ | ︙ | |||
50 51 52 53 54 55 56 | cur interfaceMap next interfaceMap } func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() | < | < < < | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | cur interfaceMap next interfaceMap } func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() keys := maps.Keys(cfg.descr) result := make([]serviceConfigDescription, 0, len(keys)) for _, k := range keys { text := cfg.descr[k].text if strings.HasSuffix(k, "-") { text = text + " (list)" } result = append(result, serviceConfigDescription{Key: k, Descr: text}) |
︙ | ︙ | |||
223 224 225 226 227 228 229 | switch val[0] { case '0', 'f', 'F', 'n', 'N': return false } return true } | | | < | | | 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | switch val[0] { case '0', 'f', 'F', 'n', 'N': return false } return true } func parseInt64(val string) any { if u64, err := strconv.ParseInt(val, 10, 64); err == nil { return u64 } return nil } func parseZid(val string) interface{} { if zid, err := id.Parse(val); err == nil { return zid } return id.Invalid } |
Changes to kernel/impl/core.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | package impl import ( "fmt" "net" "os" "runtime" | < > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package impl import ( "fmt" "net" "os" "runtime" "sync" "time" "zettelstore.de/c/maps" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" ) type coreService struct { srvConfig |
︙ | ︙ | |||
95 96 97 98 99 100 101 | func (cs *coreService) Stop(*myKernel) { cs.started = false } func (cs *coreService) GetStatistics() []kernel.KeyValue { cs.mxRecover.RLock() defer cs.mxRecover.RUnlock() | < | < < < | 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | func (cs *coreService) Stop(*myKernel) { cs.started = false } func (cs *coreService) GetStatistics() []kernel.KeyValue { cs.mxRecover.RLock() defer cs.mxRecover.RUnlock() names := maps.Keys(cs.mapRecover) result := make([]kernel.KeyValue, 0, 3*len(names)) for _, n := range names { ri := cs.mapRecover[n] result = append( result, kernel.KeyValue{ Key: fmt.Sprintf("Recover %q / Count", n), |
︙ | ︙ |
Changes to kernel/impl/web.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ | |||
27 28 29 30 31 32 33 | type webService struct { srvConfig mxService sync.RWMutex srvw server.Server setupServer kernel.SetupWebServerFunc } | < < < < < < < < < < > | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | type webService struct { srvConfig mxService sync.RWMutex srvw server.Server setupServer kernel.SetupWebServerFunc } func (ws *webService) Initialize(logger *logger.Logger) { ws.logger = logger ws.descr = descriptionMap{ kernel.WebListenAddress: { "Listen address", func(val string) interface{} { host, port, err := net.SplitHostPort(val) if err != nil { return nil } if _, err = net.LookupPort("tcp", port); err != nil { return nil } return net.JoinHostPort(host, port) }, true}, kernel.WebMaxRequestSize: {"Max Request Size", parseInt64, true}, kernel.WebPersistentCookie: {"Persistent cookie", parseBool, true}, kernel.WebSecureCookie: {"Secure cookie", parseBool, true}, kernel.WebTokenLifetimeAPI: { "Token lifetime API", makeDurationParser(10*time.Minute, 0, 1*time.Hour), true, }, |
︙ | ︙ | |||
78 79 80 81 82 83 84 85 86 87 88 89 90 91 | return nil }, true, }, } ws.next = interfaceMap{ kernel.WebListenAddress: "127.0.0.1:23123", kernel.WebPersistentCookie: false, kernel.WebSecureCookie: true, kernel.WebTokenLifetimeAPI: 1 * time.Hour, kernel.WebTokenLifetimeHTML: 10 * time.Minute, kernel.WebURLPrefix: "/", } } | > | 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | return nil }, true, }, } ws.next = interfaceMap{ kernel.WebListenAddress: "127.0.0.1:23123", kernel.WebMaxRequestSize: int64(16 * 1024 * 1024), kernel.WebPersistentCookie: false, kernel.WebSecureCookie: true, kernel.WebTokenLifetimeAPI: 1 * time.Hour, kernel.WebTokenLifetimeHTML: 10 * time.Minute, kernel.WebURLPrefix: "/", } } |
︙ | ︙ | |||
109 110 111 112 113 114 115 | func (ws *webService) GetLogger() *logger.Logger { return ws.logger } func (ws *webService) Start(kern *myKernel) error { listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string) urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string) persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool) secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool) | > > > | > | | 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | func (ws *webService) GetLogger() *logger.Logger { return ws.logger } func (ws *webService) Start(kern *myKernel) error { listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string) urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string) persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool) secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool) maxRequestSize := ws.GetNextConfig(kernel.WebMaxRequestSize).(int64) if maxRequestSize < 1024 { maxRequestSize = 1024 } srvw := impl.New(ws.logger, listenAddr, urlPrefix, persistentCookie, secureCookie, maxRequestSize, kern.auth.manager) err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, kern.cfg.rtConfig) if err != nil { ws.logger.Fatal().Err(err).Msg("Unable to create") return err } if kern.core.GetNextConfig(kernel.CoreDebug).(bool) { srvw.SetDebug() |
︙ | ︙ |
Changes to kernel/kernel.go.
︙ | ︙ | |||
163 164 165 166 167 168 169 170 171 172 173 174 175 176 | BoxDirTypeSimple = "simple" ) // Constants for web service keys. const ( WebListenAddress = "listen" WebPersistentCookie = "persistent" WebSecureCookie = "secure" WebTokenLifetimeAPI = "api-lifetime" WebTokenLifetimeHTML = "html-lifetime" WebURLPrefix = "prefix" ) // KeyDescrValue is a triple of config data. | > | 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | BoxDirTypeSimple = "simple" ) // Constants for web service keys. const ( WebListenAddress = "listen" WebPersistentCookie = "persistent" WebMaxRequestSize = "max-request-size" WebSecureCookie = "secure" WebTokenLifetimeAPI = "api-lifetime" WebTokenLifetimeHTML = "html-lifetime" WebURLPrefix = "prefix" ) // KeyDescrValue is a triple of config data. |
︙ | ︙ |
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 | //----------------------------------------------------------------------------- // 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" ) |
︙ | ︙ | |||
117 118 119 120 121 122 123 124 125 126 127 128 129 130 | m.buf = append(m.buf, user.Zid.Bytes()...) } } } } return m } // Zid adds a zettel identifier to the full message func (m *Message) Zid(zid id.Zid) *Message { return m.Bytes("zid", zid.Bytes()) } // Msg add the given text to the message and writes it to the log. | > > > > > > > > > > > | 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 | m.buf = append(m.buf, user.Zid.Bytes()...) } } } } return m } // HTTPIP adds the IP address of a HTTP request to the message. func (m *Message) HTTPIP(r *http.Request) *Message { if r == nil { return m } if from := r.Header.Get("X-Forwarded-For"); from != "" { return m.Str("ip", from) } return m.Str("IP", r.RemoteAddr) } // Zid adds a zettel identifier to the full message func (m *Message) Zid(zid id.Zid) *Message { return m.Bytes("zid", zid.Bytes()) } // Msg add the given text to the message and writes it to the log. |
︙ | ︙ |
Changes to parser/blob/blob.go.
︙ | ︙ | |||
46 47 48 49 50 51 52 | }) } func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice { if p := parser.Get(syntax); p != nil { syntax = p.Name } | | | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | }) } 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), }} } |
︙ | ︙ |
Changes to parser/cleaner/cleaner.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package cleaner provides funxtions to clean up the parsed AST. package cleaner import ( "bytes" "strconv" | < | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | // Package cleaner provides funxtions 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. func CleanBlockSlice(bs *ast.BlockSlice) { cleanNode(bs) } // CleanInlineSlice cleans the given inline list. func CleanInlineSlice(is *ast.InlineSlice) { cleanNode(is) } func cleanNode(n ast.Node) { cv := cleanVisitor{ textEnc: textenc.Create(), hasMark: false, doMark: false, } ast.Walk(&cv, n) if cv.hasMark { cv.doMark = true ast.Walk(&cv, n) } } type cleanVisitor struct { textEnc *textenc.Encoder ids map[string]ast.Node hasMark bool doMark bool } func (cv *cleanVisitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { |
︙ | ︙ |
Changes to parser/draw/canvas.go.
︙ | ︙ | |||
26 27 28 29 30 31 32 | "sort" "unicode/utf8" ) // newCanvas returns a new Canvas, initialized from the provided data. If tabWidth is set to a non-negative // value, that value will be used to convert tabs to spaces within the grid. Creation of the Canvas // can fail if the diagram contains invalid UTF-8 sequences. | | < < < < < < < < | | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | "sort" "unicode/utf8" ) // newCanvas returns a new Canvas, initialized from the provided data. If tabWidth is set to a non-negative // value, that value will be used to convert tabs to spaces within the grid. Creation of the Canvas // can fail if the diagram contains invalid UTF-8 sequences. func newCanvas(data []byte) (*canvas, error) { c := &canvas{} lines := bytes.Split(data, []byte("\n")) c.siz.Y = len(lines) // Diagrams will often not be padded to a uniform width. To overcome this, we scan over // each line and figure out which is the longest. This becomes the width of the canvas. for i, line := range lines { if ok := utf8.Valid(line); !ok { return nil, fmt.Errorf("invalid UTF-8 encoding on line %d", i) } if i1 := utf8.RuneCount(line); i1 > c.siz.X { c.siz.X = i1 } } c.grid = make([]char, c.siz.X*c.siz.Y) c.visited = make([]bool, c.siz.X*c.siz.Y) for y, line := range lines { |
︙ | ︙ | |||
71 72 73 74 75 76 77 | } } c.findObjects() return c, nil } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | } } c.findObjects() return c, nil } // canvas is the parsed source data. type canvas struct { // (0,0) is top left. grid []char visited []bool objs objects siz image.Point |
︙ | ︙ | |||
139 140 141 142 143 144 145 | func (c *canvas) objects() objects { return c.objs } // size returns the visual dimensions of the Canvas. func (c *canvas) size() image.Point { return c.siz } // findObjects finds all objects (lines, polygons, and text) within the underlying grid. func (c *canvas) findObjects() { | | > > | > | | > | > | > > > | | | | | | | | | | | | | | | | > > | | > | > | | > | | | | | | | | | < < < | 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 | func (c *canvas) objects() objects { return c.objs } // size returns the visual dimensions of the Canvas. func (c *canvas) size() image.Point { return c.siz } // findObjects finds all objects (lines, polygons, and text) within the underlying grid. func (c *canvas) findObjects() { c.findPaths() 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 } // Found the start of a one or multiple connected paths. Traverse all // connecting points. This will generate multiple objects if multiple // paths (either open or closed) are found. c.visit(p) objs := c.scanPath([]point{p}) for _, obj := range objs { // For all points in all objects found, mark the points as visited. for _, p := range obj.Points() { c.visit(p) } } 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 } // scanText will return nil if the text at this area is simply // setting options on a container object. obj := c.scanText(p) if obj == nil { continue } for _, p := range obj.Points() { c.visit(p) } c.objs = append(c.objs, obj) } } } // scanPath tries to complete a total path (for lines or polygons) starting with some partial path. // It recurses when it finds multiple unvisited outgoing paths. func (c *canvas) scanPath(points []point) objects { cur := points[len(points)-1] next := c.next(cur) |
︙ | ︙ | |||
253 254 255 256 257 258 259 260 261 262 | func (c *canvas) next(pos point) []point { // Our caller must have called c.visit prior to calling this function. if !c.isVisited(pos) { panic(fmt.Errorf("internal error; revisiting %s", pos)) } var out []point ch := c.at(pos) if ch.canHorizontal() { | > > > > > > > > > > > > > > > > < < < < < < < < < < < < < < < | 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 (c *canvas) next(pos point) []point { // Our caller must have called c.visit prior to calling this function. if !c.isVisited(pos) { panic(fmt.Errorf("internal error; revisiting %s", pos)) } var out []point nextHorizontal := func(p point) { if !c.isVisited(p) && c.at(p).canHorizontal() { out = append(out, p) } } nextVertical := func(p point) { if !c.isVisited(p) && c.at(p).canVertical() { out = append(out, p) } } nextDiagonal := func(from, to point) { if !c.isVisited(to) && c.at(to).canDiagonalFrom(c.at(from)) { out = append(out, to) } } ch := c.at(pos) if ch.canHorizontal() { if c.canLeft(pos) { n := pos n.x-- nextHorizontal(n) } if c.canRight(pos) { n := pos n.x++ nextHorizontal(n) } } if ch.canVertical() { if c.canUp(pos) { n := pos n.y-- nextVertical(n) } if c.canDown(pos) { n := pos n.y++ nextVertical(n) } } if c.canDiagonal(pos) { if c.canUp(pos) { if c.canLeft(pos) { n := pos n.x-- n.y-- nextDiagonal(pos, n) } |
︙ | ︙ |
Changes to parser/draw/canvas_test.go.
︙ | ︙ | |||
297 298 299 300 301 302 303 | false, }, // 9 Indented box { []string{ "", | | | | | | | 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 | false, }, // 9 Indented box { []string{ "", " +-+", " | |", " +-+", }, []string{"Path{[(1,1) (2,1) (3,1) (3,2) (3,3) (2,3) (1,3) (1,2)]}"}, []string{""}, [][]point{{{x: 1, y: 1}, {x: 3, y: 1}, {x: 3, y: 3}, {x: 1, y: 3}}}, false, }, // 10 Diagonal lines with arrows { []string{ "^ ^", |
︙ | ︙ | |||
442 443 444 445 446 447 448 | {x: 11, y: 2, hint: 0}, {x: 12, y: 2, hint: 0}, {x: 13, y: 2, hint: 0}, }, }, true, }, | | > > > > > > > > > > > > > > > > > > > > > > > > > | | 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 | {x: 11, y: 2, hint: 0}, {x: 12, y: 2, hint: 0}, {x: 13, y: 2, hint: 0}, }, }, true, }, // 14 Multiple closed path on one object { []string{ "+-+-+", "| | |", "+-+-+", }, []string{ "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (1,2) (0,2) (0,1)]}", "Path{[(0,0) (1,0) (2,0) (3,0) (4,0) (4,1) (4,2) (3,2) (2,2) (2,1)]}", // TODO (2,0) }, []string{"", ""}, [][]point{ { {0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {3, 0, 0}, {4, 0, 0}, {4, 1, 0}, {4, 2, 0}, {3, 2, 0}, {2, 2, 0}, {1, 2, 0}, {0, 2, 0}, {0, 1, 0}, }, { {0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {3, 0, 0}, {4, 0, 0}, {4, 1, 0}, {4, 2, 0}, {3, 2, 0}, {2, 2, 0}, {2, 1, 0}, // TODO: {2, 0, 0} }, }, true, }, } for i, line := range data { c, err := newCanvas([]byte(strings.Join(line.input, "\n"))) if err != nil { t.Fatalf("Test %d: error creating canvas: %s", i, err) } objs := c.objects() if line.strings != nil { if got := getStrings(objs); !reflect.DeepEqual(line.strings, got) { t.Errorf("%d: expected %q, but got %q", i, line.strings, got) |
︙ | ︙ | |||
531 532 533 534 535 536 537 | {{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 0, y: 2}}, {{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 2, y: 2}, {x: 2, y: 1}}, {{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 3, y: 2}, {x: 3, y: 1}}, }, }, } for i, line := range data { | | | 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 | {{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 0, y: 2}}, {{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 2, y: 2}, {x: 2, y: 1}}, {{x: 0, y: 0}, {x: 5, y: 0}, {x: 5, y: 2}, {x: 3, y: 2}, {x: 3, y: 1}}, }, }, } for i, line := range data { c, err := newCanvas([]byte(strings.Join(line.input, "\n"))) if err != nil { t.Fatalf("Test %d: error creating canvas: %s", i, err) } objs := c.objects() if line.strings != nil { if got := getStrings(objs); !reflect.DeepEqual(line.strings, got) { t.Errorf("%d: expected %q, but got %q", i, line.strings, got) |
︙ | ︙ | |||
634 635 636 637 638 639 640 | 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() | | | 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 | 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) } objs := c.objects() if len(objs) != expected { b.Fatalf("%d != %d", len(objs), expected) |
︙ | ︙ |
Changes to parser/draw/draw.go.
1 2 3 4 5 6 7 8 9 10 | //----------------------------------------------------------------------------- // 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. //----------------------------------------------------------------------------- | | > > > | | | | | < < < < < < < < < < < < | | | > | | > > > | | > | | | | < | | | > | | | | < < < | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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) 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 } } return defVal } func canvasErrMsg(err error) ast.InlineSlice { return ast.CreateInlineSliceFromWords("Error:", err.Error()) } func noSVGErrMsg() ast.InlineSlice { |
︙ | ︙ |
Changes to parser/draw/object.go.
︙ | ︙ | |||
57 58 59 60 61 62 63 | func (o *object) String() string { if o.isJustText() { return fmt.Sprintf("Text{%s %q}", o.points[0], string(o.text)) } return fmt.Sprintf("Path{%v}", o.points) } | < < < < < < < < < < < < < < < < < < < > | | < | | | 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 | func (o *object) String() string { if o.isJustText() { return fmt.Sprintf("Text{%s %q}", o.points[0], string(o.text)) } return fmt.Sprintf("Path{%v}", o.points) } // seal finalizes the object, setting its text, its corners, and its various rendering hints. func (o *object) seal(c *canvas) { if c.at(o.points[0]).isArrow() { o.points[0].hint = startMarker c.hasStartMarker = true } if c.at(o.points[len(o.points)-1]).isArrow() { o.points[len(o.points)-1].hint = endMarker c.hasEndMarker = true } o.corners, o.isClosed = pointsToCorners(o.points) o.text = make([]rune, len(o.points)) for i, p := range o.points { ch := c.at(p) if !o.isJustText() { if ch.isTick() { o.points[i].hint = tick } else if ch.isDot() { o.points[i].hint = dot } if ch.isDashed() { o.isDashed = true } for _, corner := range o.corners { if corner.x == p.x && corner.y == p.y && c.at(p).isRoundedCorner() { o.points[i].hint = roundedCorner } } } o.text[i] = rune(ch) } } // objects implements a sortable collection of Object interfaces. type objects []*object func (o objects) Len() int { return len(o) } |
︙ | ︙ | |||
175 176 177 178 179 180 181 182 183 184 185 | } else if isDiagonalNW(points[0], points[1]) { dir = dirNW } else if isDiagonalNE(points[0], points[1]) { dir = dirNE } else { panic(fmt.Errorf("discontiguous points: %+v", points)) } // Starting from the third point, check to see if the directionality between points P and // P-1 has changed. for i := 2; i < l; i++ { | > > > > > > > < < < < < < | 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 | } else if isDiagonalNW(points[0], points[1]) { dir = dirNW } else if isDiagonalNE(points[0], points[1]) { dir = dirNE } else { panic(fmt.Errorf("discontiguous points: %+v", points)) } cornerFunc := func(idx, newDir int) { if dir != newDir { out = append(out, points[idx-1]) dir = newDir } } // Starting from the third point, check to see if the directionality between points P and // P-1 has changed. for i := 2; i < l; i++ { if isHorizontal(points[i-1], points[i]) { cornerFunc(i, dirH) } else if isVertical(points[i-1], points[i]) { cornerFunc(i, dirV) } else if isDiagonalSE(points[i-1], points[i]) { cornerFunc(i, dirSE) } else if isDiagonalSW(points[i-1], points[i]) { |
︙ | ︙ |
Changes to parser/draw/svg_test.go.
︙ | ︙ | |||
55 56 57 58 59 60 61 | []string{ " foo", }, 265, }, } for i, line := range data { | | | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | []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) } actual := string(canvasToSVG(canvas, "", 9, 16)) // TODO(dhobsd): Use golden file? Worth postponing once output is actually // nice. if line.length != len(actual) { |
︙ | ︙ |
Changes to parser/markdown/markdown.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "bytes" "fmt" gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" | < | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | "bytes" "fmt" 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", |
︙ | ︙ | |||
49 50 51 52 53 54 55 | return bs.FirstParagraphInlines() } func parseMarkdown(inp *input.Input) *mdP { source := []byte(inp.Src[inp.Pos:]) parser := gm.DefaultParser() node := parser.Parse(gmText.NewReader(source)) | | | | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | return bs.FirstParagraphInlines() } func parseMarkdown(inp *input.Input) *mdP { source := []byte(inp.Src[inp.Pos:]) parser := gm.DefaultParser() node := parser.Parse(gmText.NewReader(source)) textEnc := textenc.Create() return &mdP{source: source, docNode: node, textEnc: textEnc} } type mdP struct { source []byte docNode gmAst.Node textEnc *textenc.Encoder } func (p *mdP) acceptBlockChildren(docNode gmAst.Node) ast.BlockSlice { if docNode.Type() != gmAst.TypeDocument { panic(fmt.Sprintf("Expected document, but got node type %v", docNode.Type())) } result := make(ast.BlockSlice, 0, docNode.ChildCount()) |
︙ | ︙ | |||
129 130 131 132 133 134 135 | Kind: ast.VerbatimProg, Attrs: nil, //TODO Content: p.acceptRawText(node), } } func (p *mdP) acceptFencedCodeBlock(node *gmAst.FencedCodeBlock) *ast.VerbatimNode { | | | | | 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | Kind: ast.VerbatimProg, Attrs: nil, //TODO Content: p.acceptRawText(node), } } func (p *mdP) acceptFencedCodeBlock(node *gmAst.FencedCodeBlock) *ast.VerbatimNode { var a attrs.Attributes if language := node.Language(p.source); len(language) > 0 { a = a.Set("class", "language-"+cleanText(language, true)) } return &ast.VerbatimNode{ Kind: ast.VerbatimProg, Attrs: a, Content: p.acceptRawText(node), } } func (p *mdP) acceptRawText(node gmAst.Node) []byte { lines := node.Lines() result := make([]byte, 0, 512) |
︙ | ︙ | |||
172 173 174 175 176 177 178 | p.acceptItemSlice(node), }, } } func (p *mdP) acceptList(node *gmAst.List) ast.ItemNode { kind := ast.NestedListUnordered | | | | | 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 | p.acceptItemSlice(node), }, } } func (p *mdP) acceptList(node *gmAst.List) ast.ItemNode { kind := ast.NestedListUnordered var a attrs.Attributes if node.IsOrdered() { kind = ast.NestedListOrdered if node.Start != 1 { a = a.Set("start", fmt.Sprintf("%d", node.Start)) } } items := make([]ast.ItemSlice, 0, node.ChildCount()) for child := node.FirstChild(); child != nil; child = child.NextSibling() { item, ok := child.(*gmAst.ListItem) if !ok { panic(fmt.Sprintf("Expected list item node, but got %v", child.Kind())) } items = append(items, p.acceptItemSlice(item)) } return &ast.NestedListNode{ Kind: kind, Items: items, Attrs: a, } } func (p *mdP) acceptItemSlice(node gmAst.Node) ast.ItemSlice { result := make(ast.ItemSlice, 0, node.ChildCount()) for elem := node.FirstChild(); elem != nil; elem = elem.NextSibling() { if item := p.acceptBlock(elem); item != nil { |
︙ | ︙ | |||
401 402 403 404 405 406 407 | Inlines: p.acceptInlineChildren(node), }, } } func (p *mdP) acceptLink(node *gmAst.Link) ast.InlineSlice { ref := ast.ParseReference(cleanText(node.Destination, true)) | | | | | | | | 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 | Inlines: p.acceptInlineChildren(node), }, } } func (p *mdP) acceptLink(node *gmAst.Link) ast.InlineSlice { ref := ast.ParseReference(cleanText(node.Destination, true)) var a attrs.Attributes if title := node.Title; len(title) > 0 { a = a.Set("title", cleanText(title, true)) } return ast.InlineSlice{ &ast.LinkNode{ Ref: ref, Inlines: p.acceptInlineChildren(node), Attrs: a, }, } } func (p *mdP) acceptImage(node *gmAst.Image) ast.InlineSlice { ref := ast.ParseReference(cleanText(node.Destination, true)) var a attrs.Attributes if title := node.Title; len(title) > 0 { a = a.Set("title", cleanText(title, true)) } return ast.InlineSlice{ &ast.EmbedRefNode{ Ref: ref, Inlines: p.flattenInlineSlice(node), Attrs: a, }, } } func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice { is := p.acceptInlineChildren(node) var buf bytes.Buffer |
︙ | ︙ |
Changes to parser/parser_test.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | "testing" "zettelstore.de/c/api" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. | < < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | "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}, |
︙ | ︙ |
Changes to parser/plain/plain.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package plain provides a parser for plain text data. package plain import ( "strings" "zettelstore.de/c/api" | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // 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() { |
︙ | ︙ | |||
71 72 73 74 75 76 77 | func parseBlocksHTML(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { return doParseBlocks(inp, syntax, ast.VerbatimHTML) } func doParseBlocks(inp *input.Input, syntax string, kind ast.VerbatimKind) ast.BlockSlice { return ast.BlockSlice{ &ast.VerbatimNode{ Kind: kind, | | | < < < < < < < < < < < < < < < < | | 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 | func parseBlocksHTML(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { return doParseBlocks(inp, syntax, ast.VerbatimHTML) } func doParseBlocks(inp *input.Input, syntax string, kind ast.VerbatimKind) ast.BlockSlice { return ast.BlockSlice{ &ast.VerbatimNode{ Kind: kind, Attrs: attrs.Attributes{"": syntax}, Content: inp.ScanLineContent(), }, } } func parseInlines(inp *input.Input, syntax string) ast.InlineSlice { return doParseInlines(inp, syntax, ast.LiteralProg) } func parseInlinesHTML(inp *input.Input, syntax string) ast.InlineSlice { return doParseInlines(inp, syntax, ast.LiteralHTML) } func doParseInlines(inp *input.Input, syntax string, kind ast.LiteralKind) ast.InlineSlice { inp.SkipToEOL() return ast.InlineSlice{&ast.LiteralNode{ Kind: kind, Attrs: attrs.Attributes{"": syntax}, Content: append([]byte(nil), inp.Src[0:inp.Pos]...), }} } func parseSVGBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { is := parseSVGInlines(inp, syntax) if len(is) == 0 { |
︙ | ︙ |
Changes to parser/zettelmark/block.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | "zettelstore.de/z/input" ) // parseBlockSlice parses a sequence of blocks. func (cp *zmkP) parseBlockSlice() ast.BlockSlice { inp := cp.inp var lastPara *ast.ParaNode | | > | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | "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{} for inp.Ch != input.EOS { bn, cont := cp.parseBlock(lastPara) if bn != nil { bs = append(bs, bn) } if !cont { lastPara, _ = bn.(*ast.ParaNode) |
︙ | ︙ | |||
53 54 55 56 57 58 59 | return nil, false case '\n', '\r': inp.EatEOL() cp.cleanupListsAfterEOL() return nil, false case ':': bn, success = cp.parseColon() | | | 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | return nil, false case '\n', '\r': inp.EatEOL() cp.cleanupListsAfterEOL() return nil, false case ':': bn, success = cp.parseColon() case '@', '`', runeModGrave, '%', '~', '$': cp.clearStacked() bn, success = cp.parseVerbatim() case '"', '<': cp.clearStacked() bn, success = cp.parseRegion() case '=': cp.clearStacked() |
︙ | ︙ | |||
92 93 94 95 96 97 98 | if success { return bn, false } } inp.SetPos(pos) cp.clearStacked() pn := cp.parsePara() | > > | > > > > > > > > > > | 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 | if success { return bn, false } } inp.SetPos(pos) cp.clearStacked() pn := cp.parsePara() if startsWithSpaceSoftBreak(pn) { pn.Inlines = pn.Inlines[2:] } else if lastPara != nil { lastPara.Inlines = append(lastPara.Inlines, pn.Inlines...) return nil, true } return pn, false } func startsWithSpaceSoftBreak(pn *ast.ParaNode) bool { ins := pn.Inlines if len(ins) < 2 { return false } _, isSpace := ins[0].(*ast.SpaceNode) _, isBreak := ins[1].(*ast.BreakNode) return isSpace && isBreak } func (cp *zmkP) cleanupListsAfterEOL() { for _, l := range cp.lists { if lits := len(l.Items); lits > 0 { l.Items[lits-1] = append(l.Items[lits-1], &nullItemNode{}) } } |
︙ | ︙ | |||
126 127 128 129 130 131 132 | return cp.parseRegion() } return cp.parseDefDescr() } // parsePara parses paragraphed inline material. func (cp *zmkP) parsePara() *ast.ParaNode { | | | | | | | 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 | return cp.parseRegion() } return cp.parseDefDescr() } // parsePara parses paragraphed inline material. func (cp *zmkP) parsePara() *ast.ParaNode { ins := ast.InlineSlice{} for { in := cp.parseInline() if in == nil { return &ast.ParaNode{Inlines: ins} } ins = append(ins, in) if _, ok := in.(*ast.BreakNode); ok { ch := cp.inp.Ch switch ch { // Must contain all cases from above switch in parseBlock. case input.EOS, '\n', '\r', '@', '`', runeModGrave, '%', '~', '$', '"', '<', '=', '-', '*', '#', '>', ';', ':', ' ', '|', '{': return &ast.ParaNode{Inlines: ins} } } } } // countDelim read from input until a non-delimiter is found and returns number of delimiter chars. func (cp *zmkP) countDelim(delim rune) int { |
︙ | ︙ | |||
162 163 164 165 166 167 168 | func (cp *zmkP) parseVerbatim() (rn *ast.VerbatimNode, success bool) { inp := cp.inp fch := inp.Ch cnt := cp.countDelim(fch) if cnt < 3 { return nil, false } | | > > > > | 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 | func (cp *zmkP) parseVerbatim() (rn *ast.VerbatimNode, success bool) { inp := cp.inp fch := inp.Ch cnt := cp.countDelim(fch) if cnt < 3 { return nil, false } attrs := cp.parseBlockAttributes() inp.SkipToEOL() if inp.Ch == input.EOS { return nil, false } var kind ast.VerbatimKind switch fch { case '@': kind = ast.VerbatimZettel case '`', runeModGrave: kind = ast.VerbatimProg case '%': kind = ast.VerbatimComment case '~': kind = ast.VerbatimEval case '$': kind = ast.VerbatimMath default: panic(fmt.Sprintf("%q is not a verbatim char", fch)) } rn = &ast.VerbatimNode{Kind: kind, Attrs: attrs, Content: make([]byte, 0, 512)} for { inp.EatEOL() posL := inp.Pos |
︙ | ︙ | |||
218 219 220 221 222 223 224 | if !ok { panic(fmt.Sprintf("%q is not a region char", fch)) } cnt := cp.countDelim(fch) if cnt < 3 { return nil, false } | | | 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | if !ok { panic(fmt.Sprintf("%q is not a region char", fch)) } cnt := cp.countDelim(fch) if cnt < 3 { return nil, false } attrs := cp.parseBlockAttributes() inp.SkipToEOL() if inp.Ch == input.EOS { return nil, false } rn = &ast.RegionNode{ Kind: kind, Attrs: attrs, |
︙ | ︙ | |||
295 296 297 298 299 300 301 | } in := cp.parseInline() if in == nil { return hn, true } hn.Inlines = append(hn.Inlines, in) if inp.Ch == '{' && inp.Peek() != '{' { | | | | 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 | } in := cp.parseInline() if in == nil { return hn, true } hn.Inlines = append(hn.Inlines, in) if inp.Ch == '{' && inp.Peek() != '{' { attrs := cp.parseBlockAttributes() hn.Attrs = attrs inp.SkipToEOL() return hn, true } } } // parseHRule parses a horizontal rule. func (cp *zmkP) parseHRule() (hn *ast.HRuleNode, success bool) { inp := cp.inp if cp.countDelim(inp.Ch) < 3 { return nil, false } attrs := cp.parseBlockAttributes() inp.SkipToEOL() return &ast.HRuleNode{Attrs: attrs}, true } var mapRuneNestedList = map[rune]ast.NestedListKind{ '*': ast.NestedListUnordered, '#': ast.NestedListOrdered, |
︙ | ︙ | |||
338 339 340 341 342 343 344 | if len(kinds) < len(cp.lists) { cp.lists = cp.lists[:len(kinds)] } ln, newLnCount := cp.buildNestedList(kinds) pn := cp.parseLinePara() if pn == nil { | | | 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 | if len(kinds) < len(cp.lists) { cp.lists = cp.lists[:len(kinds)] } ln, newLnCount := cp.buildNestedList(kinds) pn := cp.parseLinePara() if pn == nil { pn = &ast.ParaNode{} } ln.Items = append(ln.Items, ast.ItemSlice{pn}) return cp.cleanupParsedNestedList(newLnCount) } func (cp *zmkP) parseNestedListKinds() []ast.NestedListKind { inp := cp.inp |
︙ | ︙ | |||
496 497 498 499 500 501 502 | cp.lists = cp.lists[:cnt] if cnt == 0 { return false } ln := cp.lists[cnt-1] pn := cp.parseLinePara() if pn == nil { | | | 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 | cp.lists = cp.lists[:cnt] if cnt == 0 { return false } ln := cp.lists[cnt-1] pn := cp.parseLinePara() if pn == nil { pn = &ast.ParaNode{} } lbn := ln.Items[len(ln.Items)-1] if lpn, ok := lbn[len(lbn)-1].(*ast.ParaNode); ok { lpn.Inlines = append(lpn.Inlines, pn.Inlines...) } else { ln.Items[len(ln.Items)-1] = append(ln.Items[len(ln.Items)-1], pn) } |
︙ | ︙ | |||
544 545 546 547 548 549 550 | cp.descrl.Descriptions[defPos].Descriptions[descrPos] = append(cp.descrl.Descriptions[defPos].Descriptions[descrPos], pn) } return true } // parseLinePara parses one line of inline material. func (cp *zmkP) parseLinePara() *ast.ParaNode { | | | | | | | 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 | cp.descrl.Descriptions[defPos].Descriptions[descrPos] = append(cp.descrl.Descriptions[defPos].Descriptions[descrPos], pn) } return true } // parseLinePara parses one line of inline material. func (cp *zmkP) parseLinePara() *ast.ParaNode { ins := ast.InlineSlice{} for { in := cp.parseInline() if in == nil { if len(ins) == 0 { return nil } return &ast.ParaNode{Inlines: ins} } ins = append(ins, in) if _, ok := in.(*ast.BreakNode); ok { return &ast.ParaNode{Inlines: ins} } } } // parseRow parse one table row. func (cp *zmkP) parseRow() ast.BlockNode { inp := cp.inp |
︙ | ︙ |
Changes to parser/zettelmark/inline.go.
1 2 3 4 5 6 7 8 9 10 | //----------------------------------------------------------------------------- // 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. //----------------------------------------------------------------------------- | < | < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 zettelmark import ( "bytes" "fmt" "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() if in == nil { break } ins = append(ins, in) } return ins } func (cp *zmkP) parseInline() ast.InlineNode { inp := cp.inp pos := inp.Pos if cp.nestingLevel <= maxNestingLevel { cp.nestingLevel++ |
︙ | ︙ | |||
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 | return cp.parseTag() case '%': in, success = cp.parseComment() case '_', '*', '>', '~', '^', ',', '"', ':': in, success = cp.parseFormat() case '@', '\'', '`', '=', runeModGrave: in, success = cp.parseLiteral() case '\\': return cp.parseBackslash() case '-': in, success = cp.parseNdash() case '&': in, success = cp.parseEntity() } if success { return in } } inp.SetPos(pos) return cp.parseText() } func (cp *zmkP) parseText() *ast.TextNode { inp := cp.inp pos := inp.Pos if inp.Ch == '\\' { | > > > | | < < < < < | 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 | return cp.parseTag() 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() case '-': in, success = cp.parseNdash() case '&': in, success = cp.parseEntity() } if success { return in } } inp.SetPos(pos) return cp.parseText() } func (cp *zmkP) parseText() *ast.TextNode { inp := cp.inp pos := inp.Pos if inp.Ch == '\\' { cp.inp.Next() 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 inp.Next() switch inp.Ch { case '\n', '\r': inp.EatEOL() return &ast.BreakNode{Hard: true} |
︙ | ︙ | |||
153 154 155 156 157 158 159 | 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 { | | | 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | 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 } |
︙ | ︙ | |||
293 294 295 296 297 298 299 | case ' ', ',', '|': inp.Next() } ins, ok := cp.parseLinkLikeRest() if !ok { return nil, false } | | | | | | | | | | | | | | | | 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 | case ' ', ',', '|': inp.Next() } ins, ok := cp.parseLinkLikeRest() if !ok { return nil, false } attrs := cp.parseInlineAttributes() return &ast.CiteNode{Key: string(inp.Src[pos:posL]), Inlines: ins, Attrs: attrs}, true } func (cp *zmkP) parseFootnote() (*ast.FootnoteNode, bool) { cp.inp.Next() ins, ok := cp.parseLinkLikeRest() if !ok { return nil, false } attrs := cp.parseInlineAttributes() return &ast.FootnoteNode{Inlines: ins, Attrs: attrs}, true } func (cp *zmkP) parseLinkLikeRest() (ast.InlineSlice, bool) { cp.skipSpace() ins := ast.InlineSlice{} inp := cp.inp for inp.Ch != ']' { in := cp.parseInline() if in == nil { return nil, false } ins = append(ins, in) if _, ok := in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) { return nil, false } } inp.Next() 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, }, true } } return nil, false } func (cp *zmkP) parseMark() (*ast.MarkNode, bool) { inp := cp.inp inp.Next() pos := inp.Pos for inp.Ch != '|' && inp.Ch != ']' { if !isNameRune(inp.Ch) { return nil, false } inp.Next() } mark := inp.Src[pos:inp.Pos] ins := ast.InlineSlice{} if inp.Ch == '|' { inp.Next() var ok bool ins, ok = cp.parseLinkLikeRest() if !ok { return nil, false } } else { inp.Next() } mn := &ast.MarkNode{Mark: string(mark), Inlines: ins} return mn, true } func (cp *zmkP) parseTag() ast.InlineNode { inp := cp.inp posH := inp.Pos inp.Next() |
︙ | ︙ | |||
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 | inp.Next() if inp.Ch != '%' { return nil, false } for inp.Ch == '%' { inp.Next() } cp.skipSpace() pos := inp.Pos for { if input.IsEOLEOS(inp.Ch) { return &ast.LiteralNode{ Kind: ast.LiteralComment, Content: append([]byte(nil), inp.Src[pos:inp.Pos]...), }, true } inp.Next() } } | > > | 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 | inp.Next() if inp.Ch != '%' { return nil, false } for inp.Ch == '%' { inp.Next() } attrs := cp.parseInlineAttributes() cp.skipSpace() pos := inp.Pos for { if input.IsEOLEOS(inp.Ch) { return &ast.LiteralNode{ Kind: ast.LiteralComment, Attrs: attrs, Content: append([]byte(nil), inp.Src[pos:inp.Pos]...), }, true } inp.Next() } } |
︙ | ︙ | |||
437 438 439 440 441 442 443 | if inp.Ch == input.EOS { return nil, false } if inp.Ch == fch { inp.Next() if inp.Ch == fch { inp.Next() | | > | | | | > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | if inp.Ch == input.EOS { return nil, false } if inp.Ch == fch { inp.Next() if inp.Ch == fch { inp.Next() fn.Attrs = cp.parseInlineAttributes() return fn, true } fn.Inlines = append(fn.Inlines, &ast.TextNode{Text: string(fch)}) } else if in := cp.parseInline(); in != nil { if _, ok = in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) { return nil, false } fn.Inlines = append(fn.Inlines, in) } } } var mapRuneLiteral = map[rune]ast.LiteralKind{ '@': ast.LiteralZettel, '`': ast.LiteralProg, runeModGrave: ast.LiteralProg, '\'': ast.LiteralInput, '=': ast.LiteralOutput, // No '$': ast.LiteralMath, because paring literal math is a little different } func (cp *zmkP) parseLiteral() (res ast.InlineNode, success bool) { inp := cp.inp fch := inp.Ch kind, ok := mapRuneLiteral[fch] if !ok { panic(fmt.Sprintf("%q is not a formatting char", fch)) } inp.Next() // read 2nd formatting character if inp.Ch != fch { return nil, false } litn := &ast.LiteralNode{Kind: kind} inp.Next() var buf bytes.Buffer for { if inp.Ch == input.EOS { return nil, false } if inp.Ch == fch { if inp.Peek() == fch { inp.Next() inp.Next() litn.Attrs = cp.parseInlineAttributes() litn.Content = buf.Bytes() return litn, true } buf.WriteRune(fch) inp.Next() } else { tn := cp.parseText() buf.WriteString(tn.Text) } } } func (cp *zmkP) parseLiteralMath() (res ast.InlineNode, success bool) { inp := cp.inp inp.Next() // read 2nd formatting character if inp.Ch != '$' { return nil, false } inp.Next() pos := inp.Pos for { if inp.Ch == input.EOS { return nil, false } if inp.Ch == '$' && inp.Peek() == '$' { content := append([]byte{}, inp.Src[pos:inp.Pos]...) inp.Next() inp.Next() fn := &ast.LiteralNode{ Kind: ast.LiteralMath, Attrs: cp.parseInlineAttributes(), Content: content, } return fn, true } inp.Next() } } func (cp *zmkP) parseNdash() (res *ast.TextNode, success bool) { inp := cp.inp if inp.Peek() != inp.Ch { return nil, false } inp.Next() |
︙ | ︙ |
Changes to parser/zettelmark/node.go.
1 | //----------------------------------------------------------------------------- | | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //----------------------------------------------------------------------------- // 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 // post-processing. |
︙ | ︙ |
Changes to parser/zettelmark/post-processor.go.
︙ | ︙ | |||
79 80 81 82 83 84 85 86 87 88 89 90 91 92 | pp.inVerse = oldVerse } func (pp *postProcessor) visitNestedList(ln *ast.NestedListNode) { for i, item := range ln.Items { ln.Items[i] = pp.processItemSlice(item) } } func (pp *postProcessor) visitDescriptionList(dn *ast.DescriptionListNode) { for i, def := range dn.Descriptions { if len(def.Term) > 0 { ast.Walk(pp, &dn.Descriptions[i].Term) } | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | pp.inVerse = oldVerse } func (pp *postProcessor) visitNestedList(ln *ast.NestedListNode) { for i, item := range ln.Items { ln.Items[i] = pp.processItemSlice(item) } if ln.Kind != ast.NestedListQuote { return } items := []ast.ItemSlice{} collectedInlines := ast.InlineSlice{} addCollectedParagraph := func() { if len(collectedInlines) > 1 { items = append(items, []ast.ItemNode{&ast.ParaNode{Inlines: collectedInlines[1:]}}) collectedInlines = ast.InlineSlice{} } } for _, item := range ln.Items { if len(item) != 1 { // i.e. 0 or > 1 addCollectedParagraph() items = append(items, item) continue } // len(item) == 1 if pn, ok := item[0].(*ast.ParaNode); ok { collectedInlines = append(collectedInlines, &ast.BreakNode{}) collectedInlines = append(collectedInlines, pn.Inlines...) continue } addCollectedParagraph() items = append(items, item) } addCollectedParagraph() ln.Items = items } func (pp *postProcessor) visitDescriptionList(dn *ast.DescriptionListNode) { for i, def := range dn.Descriptions { if len(def.Term) > 0 { ast.Walk(pp, &dn.Descriptions[i].Term) } |
︙ | ︙ |
Changes to parser/zettelmark/zettelmark.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 15 16 17 | // under this license. //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( "unicode" "zettelstore.de/c/api" | > | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // 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() { |
︙ | ︙ | |||
66 67 68 69 70 71 72 | // clearStacked removes all multi-line nodes from parser. func (cp *zmkP) clearStacked() { cp.lists = nil cp.table = nil cp.descrl = nil } | | < < < | | < | | < < < < < | | | < < < < < < | < < < < < < | < | | | | | | | | | > | > > | < > | | | | | | | | | | < < < | < < < < | 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 | // clearStacked removes all multi-line nodes from parser. func (cp *zmkP) clearStacked() { cp.lists = nil cp.table = nil cp.descrl = nil } func (cp *zmkP) parseNormalAttribute(attrs map[string]string) bool { inp := cp.inp posK := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if posK == inp.Pos { return false } key := string(inp.Src[posK:inp.Pos]) if inp.Ch != '=' { attrs[key] = "" return true } return cp.parseAttributeValue(key, attrs) } func (cp *zmkP) parseAttributeValue(key string, attrs map[string]string) bool { inp := cp.inp inp.Next() if inp.Ch == '"' { return cp.parseQuotedAttributeValue(key, attrs) } posV := inp.Pos for { switch inp.Ch { case input.EOS: return false case '\n', '\r', ' ', '}': updateAttrs(attrs, key, string(inp.Src[posV:inp.Pos])) return true } inp.Next() } } func (cp *zmkP) parseQuotedAttributeValue(key string, attrs map[string]string) bool { inp := cp.inp inp.Next() var sb strings.Builder for { switch inp.Ch { case input.EOS: return false case '"': updateAttrs(attrs, key, sb.String()) inp.Next() return true case '\\': inp.Next() switch inp.Ch { case input.EOS, '\n', '\r': return false } fallthrough default: sb.WriteRune(inp.Ch) inp.Next() } } } func updateAttrs(attrs map[string]string, key, val string) { if prevVal := attrs[key]; len(prevVal) > 0 { attrs[key] = prevVal + " " + val } else { attrs[key] = val } } func (cp *zmkP) parseBlockAttributes() attrs.Attributes { inp := cp.inp pos := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if pos < inp.Pos { return attrs.Attributes{"": string(inp.Src[pos:inp.Pos])} } // No immediate name: skip spaces cp.skipSpace() return cp.parseInlineAttributes() } func (cp *zmkP) parseInlineAttributes() attrs.Attributes { inp := cp.inp pos := inp.Pos if attrs, success := cp.doParseAttributes(); success { return attrs } inp.SetPos(pos) return nil } // doParseAttributes reads attributes. func (cp *zmkP) doParseAttributes() (res attrs.Attributes, success bool) { inp := cp.inp if inp.Ch != '{' { return nil, false } inp.Next() a := attrs.Attributes{} if !cp.parseAttributeValues(a) { return nil, false } inp.Next() return a, true } func (cp *zmkP) parseAttributeValues(a attrs.Attributes) bool { inp := cp.inp for { cp.skipSpaceLine() switch inp.Ch { case input.EOS: return false case '}': return true case '.': inp.Next() posC := inp.Pos for isNameRune(inp.Ch) { inp.Next() } if posC == inp.Pos { return false } updateAttrs(a, "class", string(inp.Src[posC:inp.Pos])) case '=': delete(a, "") if !cp.parseAttributeValue("", a) { return false } default: if !cp.parseNormalAttribute(a) { return false } } switch inp.Ch { case '}': return true case '\n', '\r': case ' ', ',': inp.Next() default: return false } } } func (cp *zmkP) skipSpaceLine() { for inp := cp.inp; ; { switch inp.Ch { case ' ': inp.Next() case '\n', '\r': inp.EatEOL() default: |
︙ | ︙ |
Changes to parser/zettelmark/zettelmark_test.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package zettelmark_test provides some tests for the zettelmarkup parser. package zettelmark_test import ( "bytes" "fmt" | < | < < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // 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/input" "zettelstore.de/z/parser" ) type TestCase struct{ source, want string } type TestCases []TestCase func replace(s string, tcs TestCases) TestCases { var testCases TestCases |
︙ | ︙ | |||
369 370 371 372 373 374 375 376 377 378 379 380 381 382 | checkTcs(t, TestCases{ {"''````''", "(PARA {' ````})"}, {"''``a``''", "(PARA {' ``a``})"}, {"''``''``", "(PARA {' ``} ``)"}, {"''\\'''", "(PARA {' '})"}, }) } func TestMixFormatCode(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"__abc__\n**def**", "(PARA {_ abc} SB {* def})"}, {"''abc''\n==def==", "(PARA {' abc} SB {= def})"}, {"__abc__\n==def==", "(PARA {_ abc} SB {= def})"}, | > > > > > > > > > > > > > > > > > > > > > | 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 | checkTcs(t, TestCases{ {"''````''", "(PARA {' ````})"}, {"''``a``''", "(PARA {' ``a``})"}, {"''``''``", "(PARA {' ``} ``)"}, {"''\\'''", "(PARA {' '})"}, }) } func TestLiteralMath(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, {"$$a$$", "(PARA {$ a})"}, {"$$a$$$", "(PARA {$ a} $)"}, {"$$$a$$", "(PARA {$ $a})"}, {"$$$a$$$", "(PARA {$ $a} $)"}, {`$\$`, "(PARA $$)"}, {`$\$$`, "(PARA $$$)"}, {`$$\$`, "(PARA $$$)"}, {`$$a\$$`, `(PARA {$ a\})`}, {`$$a$\$`, "(PARA $$a$$)"}, {`$$a\$$$`, `(PARA {$ a\} $)`}, {"$$a$${go}", "(PARA {$ a}[ATTR go])"}, }) } func TestMixFormatCode(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"__abc__\n**def**", "(PARA {_ abc} SB {* def})"}, {"''abc''\n==def==", "(PARA {' abc} SB {= def})"}, {"__abc__\n==def==", "(PARA {_ abc} SB {= def})"}, |
︙ | ︙ | |||
414 415 416 417 418 419 420 | } func TestVerbatimZettel(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"@@@\n@@@", "(ZETTEL)"}, {"@@@\nabc\n@@@", "(ZETTEL\nabc)"}, | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | } func TestVerbatimZettel(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"@@@\n@@@", "(ZETTEL)"}, {"@@@\nabc\n@@@", "(ZETTEL\nabc)"}, {"@@@@def\nabc\n@@@@", "(ZETTEL\nabc)[ATTR =def]"}, }) } func TestVerbatimCode(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"```\n```", "(PROG)"}, {"```\nabc\n```", "(PROG\nabc)"}, {"```\nabc\n````", "(PROG\nabc)"}, {"````\nabc\n````", "(PROG\nabc)"}, {"````\nabc\n```\n````", "(PROG\nabc\n```)"}, {"````go\nabc\n````", "(PROG\nabc)[ATTR =go]"}, }) } func TestVerbatimEval(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"~~~\n~~~", "(EVAL)"}, {"~~~\nabc\n~~~", "(EVAL\nabc)"}, {"~~~\nabc\n~~~~", "(EVAL\nabc)"}, {"~~~~\nabc\n~~~~", "(EVAL\nabc)"}, {"~~~~\nabc\n~~~\n~~~~", "(EVAL\nabc\n~~~)"}, {"~~~~go\nabc\n~~~~", "(EVAL\nabc)[ATTR =go]"}, }) } func TestVerbatimMath(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"$$$\n$$$", "(MATH)"}, {"$$$\nabc\n$$$", "(MATH\nabc)"}, {"$$$\nabc\n$$$$", "(MATH\nabc)"}, {"$$$$\nabc\n$$$$", "(MATH\nabc)"}, {"$$$$\nabc\n$$$\n$$$$", "(MATH\nabc\n$$$)"}, {"$$$$go\nabc\n$$$$", "(MATH\nabc)[ATTR =go]"}, }) } func TestVerbatimComment(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"%%%\n%%%", "(COMMENT)"}, {"%%%\nabc\n%%%", "(COMMENT\nabc)"}, {"%%%%go\nabc\n%%%%", "(COMMENT\nabc)[ATTR =go]"}, }) } func TestPara(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"a\n\nb", "(PARA a)(PARA b)"}, {"a\n \nb", "(PARA a)(PARA b)"}, }) } func TestSpanRegion(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {":::\n:::", "(SPAN)"}, {":::\nabc\n:::", "(SPAN (PARA abc))"}, {":::\nabc\n::::", "(SPAN (PARA abc))"}, |
︙ | ︙ | |||
573 574 575 576 577 578 579 580 581 582 583 584 585 586 | // Changing list type adds a new list {"* abc\n# def", "(UL {(PARA abc)})(OL {(PARA def)})"}, // Quotation lists may have empty items {">", "(QL {})"}, }) } func TestEnumAfterPara(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"abc\n* def", "(PARA abc)(UL {(PARA def)})"}, {"abc\n*def", "(PARA abc SB *def)"}, }) | > > > > > > > > > | 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 | // Changing list type adds a new list {"* abc\n# def", "(UL {(PARA abc)})(OL {(PARA def)})"}, // Quotation lists may have empty items {">", "(QL {})"}, }) } func TestQuoteList(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"> w1 w2", "(QL {(PARA w1 SP w2)})"}, {"> w1\n> w2", "(QL {(PARA w1 SB w2)})"}, {"> w1\n>\n>w2", "(QL {(PARA w1)} {})(PARA >w2)"}, }) } func TestEnumAfterPara(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"abc\n* def", "(PARA abc)(UL {(PARA def)})"}, {"abc\n*def", "(PARA abc SB *def)"}, }) |
︙ | ︙ | |||
649 650 651 652 653 654 655 | {":::{=go}\n:::", "(SPAN)[ATTR =go]"}, {":::{go}\n:::", "(SPAN)[ATTR go]"}, {":::{go=py}\n:::", "(SPAN)[ATTR go=py]"}, {":::{.go=py}\n:::", "(SPAN)"}, {":::{go=}\n:::", "(SPAN)[ATTR go]"}, {":::{.go=}\n:::", "(SPAN)"}, {":::{go py}\n:::", "(SPAN)[ATTR go py]"}, | | | | 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 | {":::{=go}\n:::", "(SPAN)[ATTR =go]"}, {":::{go}\n:::", "(SPAN)[ATTR go]"}, {":::{go=py}\n:::", "(SPAN)[ATTR go=py]"}, {":::{.go=py}\n:::", "(SPAN)"}, {":::{go=}\n:::", "(SPAN)[ATTR go]"}, {":::{.go=}\n:::", "(SPAN)"}, {":::{go py}\n:::", "(SPAN)[ATTR go py]"}, {":::{go\npy}\n:::", "(SPAN)[ATTR go py]"}, {":::{.go py}\n:::", "(SPAN)[ATTR class=go py]"}, {":::{go .py}\n:::", "(SPAN)[ATTR class=py go]"}, {":::{.go py=3}\n:::", "(SPAN)[ATTR class=go py=3]"}, {"::: { go } \n:::", "(SPAN)[ATTR go]"}, {"::: { .go } \n:::", "(SPAN)[ATTR class=go]"}, }) checkTcs(t, replace("\"", TestCases{ {":::{py=3}\n:::", "(SPAN)[ATTR py=3]"}, {":::{py=$2 3$}\n:::", "(SPAN)[ATTR py=$2 3$]"}, {":::{py=$2\\$3$}\n:::", "(SPAN)[ATTR py=2$3]"}, {":::{py=2$3}\n:::", "(SPAN)[ATTR py=2$3]"}, {":::{py=$2\n3$}\n:::", "(SPAN)[ATTR py=$2\n3$]"}, {":::{py=$2 3}\n:::", "(SPAN)"}, {":::{py=2 py=3}\n:::", "(SPAN)[ATTR py=$2 3$]"}, {":::{.go .py}\n:::", "(SPAN)[ATTR class=$go py$]"}, {":::{go go}\n:::", "(SPAN)[ATTR go]"}, {":::{=py =go}\n:::", "(SPAN)[ATTR =go]"}, })) } |
︙ | ︙ | |||
695 696 697 698 699 700 701 | {"::a::{\ngo\n}", "(PARA {: a}[ATTR go])"}, }) checkTcs(t, replace("\"", TestCases{ {"::a::{py=3}", "(PARA {: a}[ATTR py=3])"}, {"::a::{py=$2 3$}", "(PARA {: a}[ATTR py=$2 3$])"}, {"::a::{py=$2\\$3$}", "(PARA {: a}[ATTR py=2$3])"}, {"::a::{py=2$3}", "(PARA {: a}[ATTR py=2$3])"}, | | | 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 | {"::a::{\ngo\n}", "(PARA {: a}[ATTR go])"}, }) checkTcs(t, replace("\"", TestCases{ {"::a::{py=3}", "(PARA {: a}[ATTR py=3])"}, {"::a::{py=$2 3$}", "(PARA {: a}[ATTR py=$2 3$])"}, {"::a::{py=$2\\$3$}", "(PARA {: a}[ATTR py=2$3])"}, {"::a::{py=2$3}", "(PARA {: a}[ATTR py=2$3])"}, {"::a::{py=$2\n3$}", "(PARA {: a}[ATTR py=$2\n3$])"}, {"::a::{py=$2 3}", "(PARA {: a} {py=$2 SP 3})"}, {"::a::{py=2 py=3}", "(PARA {: a}[ATTR py=$2 3$])"}, {"::a::{.go .py}", "(PARA {: a}[ATTR class=$go py$])"}, })) } |
︙ | ︙ | |||
910 911 912 913 914 915 916 917 918 919 920 921 922 923 | } return nil } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: "(ZETTEL", ast.VerbatimProg: "(PROG", ast.VerbatimComment: "(COMMENT", } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: "(SPAN", ast.RegionQuote: "(QUOTE", ast.RegionVerse: "(VERSE", | > > | 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 | } return nil } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: "(ZETTEL", ast.VerbatimProg: "(PROG", ast.VerbatimEval: "(EVAL", ast.VerbatimMath: "(MATH", ast.VerbatimComment: "(COMMENT", } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: "(SPAN", ast.RegionQuote: "(QUOTE", ast.RegionVerse: "(VERSE", |
︙ | ︙ | |||
949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 | var mapLiteralKind = map[ast.LiteralKind]rune{ ast.LiteralZettel: '@', ast.LiteralProg: '`', ast.LiteralInput: '\'', ast.LiteralOutput: '=', ast.LiteralComment: '%', } func (tv *TestVisitor) visitInlineSlice(is *ast.InlineSlice) { for _, in := range *is { tv.buf.WriteByte(' ') ast.Walk(tv, in) } } | > | < < < < < < | | > > > > > > > > > | 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 | 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 } |
Changes to search/print.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- // Package search provides a zettel search. package search import ( "io" | < > | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- // Package search provides a zettel search. package search import ( "io" "strconv" "strings" "zettelstore.de/c/api" "zettelstore.de/c/maps" ) func (s *Search) String() string { var sb strings.Builder s.Print(&sb) return sb.String() } |
︙ | ︙ | |||
36 37 38 39 40 41 42 | } space := false if len(s.search) > 0 { io.WriteString(w, "ANY") printSelectExprValues(w, s.search) space = true } | < < < < < | | | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | } space := false if len(s.search) > 0 { io.WriteString(w, "ANY") printSelectExprValues(w, s.search) space = true } for _, name := range maps.Keys(s.mvals) { if space { io.WriteString(w, " AND ") } io.WriteString(w, name) printSelectExprValues(w, s.mvals[name]) space = true } if s.negate { io.WriteString(w, ")") space = true } |
︙ | ︙ |
Changes to search/search.go.
︙ | ︙ | |||
52 53 54 55 56 57 58 | // Search specifies a mechanism for selecting zettel. type Search struct { mx sync.RWMutex // Protects other attributes // Fields to be used for selecting preMatch MetaMatchFunc // Match that must be true | | | | | | | 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 | // Search specifies a mechanism for selecting zettel. type Search struct { mx sync.RWMutex // Protects other attributes // Fields to be used for selecting preMatch MetaMatchFunc // Match that must be true mvals expMetaValues // Expected values for a meta datum search []expValue // Search string negate bool // Negate the result of the whole selecting process // Fields to be used for sorting order string // Name of meta key. None given: use "id" descending bool // Sort by order, but descending offset int // <= 0: no offset limit int // <= 0: no limit } type expMetaValues map[string][]expValue // Clone the search value. func (s *Search) Clone() *Search { if s == nil { return nil } c := new(Search) c.preMatch = s.preMatch c.mvals = make(expMetaValues, len(s.mvals)) for k, v := range s.mvals { c.mvals[k] = v } c.search = append([]expValue{}, s.search...) c.negate = s.negate c.order = s.order c.descending = s.descending c.offset = s.offset c.limit = s.limit |
︙ | ︙ | |||
138 139 140 141 142 143 144 | if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() if key == "" { s.addSearch(val) | | | | | 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | if s == nil { s = new(Search) } s.mx.Lock() defer s.mx.Unlock() if key == "" { s.addSearch(val) } else if s.mvals == nil { s.mvals = expMetaValues{key: {val}} } else { s.mvals[key] = append(s.mvals[key], val) } return s } func (s *Search) addSearch(val expValue) { if val.negate { val.op = val.op.negate() |
︙ | ︙ | |||
296 297 298 299 300 301 302 | // is calculated via metadata enrichments. func (s *Search) EnrichNeeded() bool { if s == nil { return false } s.mx.RLock() defer s.mx.RUnlock() | | | 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | // is calculated via metadata enrichments. func (s *Search) EnrichNeeded() bool { if s == nil { return false } s.mx.RLock() defer s.mx.RUnlock() for key := range s.mvals { if meta.IsComputed(key) { return true } } return meta.IsComputed(s.order) } |
︙ | ︙ |
Changes to search/select.go.
︙ | ︙ | |||
33 34 35 36 37 38 39 | if len(posSpecs) > 0 || len(negSpecs) > 0 || len(nomatch) > 0 { return makeSearchMetaMatchFunc(posSpecs, negSpecs, nomatch) } return nil } func (s *Search) createSelectSpecs() (posSpecs, negSpecs []matchSpec, nomatch []string) { | | | | | 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | if len(posSpecs) > 0 || len(negSpecs) > 0 || len(nomatch) > 0 { return makeSearchMetaMatchFunc(posSpecs, negSpecs, nomatch) } return nil } func (s *Search) createSelectSpecs() (posSpecs, negSpecs []matchSpec, nomatch []string) { posSpecs = make([]matchSpec, 0, len(s.mvals)) negSpecs = make([]matchSpec, 0, len(s.mvals)) for key, values := range s.mvals { if !meta.KeyIsValid(key) { continue } if always, never := countEmptyValues(values); always+never > 0 { if never == 0 { posSpecs = append(posSpecs, matchSpec{key, matchValueAlways}) continue |
︙ | ︙ | |||
389 390 391 392 393 394 395 | } } return true } func matchMetaNegSpecs(m *meta.Meta, negSpecs []matchSpec) bool { for _, s := range negSpecs { if s.match == nil { | | | 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 | } } return true } func matchMetaNegSpecs(m *meta.Meta, negSpecs []matchSpec) bool { for _, s := range negSpecs { if s.match == nil { if value, ok := m.Get(s.key); ok && matchValueAlways(value) { return false } } else if value, ok := m.Get(s.key); !ok || !s.match(value) { return false } } return true } |
Changes to strfun/escape.go.
︙ | ︙ | |||
61 62 63 64 65 66 67 | 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. | | > | 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | 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': |
︙ | ︙ | |||
87 88 89 90 91 92 93 | b[3] = '0' b[4] = jsHex[ch>>4] b[5] = jsHex[ch&0xF] } else { continue } } | | > > > | > > > > | > > | > > | 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | 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 testdata/testbox/19700101000000.zettel.
1 2 3 4 5 6 7 8 9 10 11 | id: 19700101000000 title: Startup Configuration role: configuration tags: #invisible syntax: none box-uri-1: mem: box-uri-2: dir:testdata/testbox?readonly modified: 20210629174022 owner: 20210629163300 token-lifetime-api: 1 visibility: owner | > | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 19700101000000 title: Startup Configuration role: configuration tags: #invisible syntax: none box-uri-1: mem: box-uri-2: dir:testdata/testbox?readonly modified: 20210629174022 owner: 20210629163300 secret: 1234567890123456 token-lifetime-api: 1 visibility: owner |
Changes to tests/client/client_test.go.
︙ | ︙ | |||
45 46 47 48 49 50 51 | } } } func TestListZettel(t *testing.T) { const ( | | | | | | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | } } } func TestListZettel(t *testing.T) { const ( ownerZettel = 49 configRoleZettel = 31 writerZettel = ownerZettel - 25 readerZettel = ownerZettel - 25 creatorZettel = 7 publicZettel = 4 ) testdata := []struct { user string exp int |
︙ | ︙ | |||
163 164 165 166 167 168 169 | func TestGetParsedEvaluatedZettel(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") encodings := []api.EncodingEnum{ api.EncoderZJSON, api.EncoderHTML, | | | | 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 | 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 } if len(content) == 0 { t.Errorf("Empty content for parsed encoding %v", enc) } content, err = c.GetEvaluatedZettel(context.Background(), api.ZidDefaultHome, enc) if err != nil { t.Error(err) continue } if len(content) == 0 { t.Errorf("Empty content for evaluated encoding %v", enc) } |
︙ | ︙ | |||
329 330 331 332 333 334 335 | } } func TestListTags(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") | | | 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 | } } func TestListTags(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") tm, err := c.ListMapMeta(context.Background(), api.KeyTags) if err != nil { t.Error(err) return } tags := []struct { key string size int |
︙ | ︙ | |||
363 364 365 366 367 368 369 | } } func TestListRoles(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") | | | | | | 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 | } } func TestListRoles(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.ListMapMeta(context.Background(), 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() |
︙ | ︙ |
Changes to tests/client/crud_test.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ | |||
40 41 42 43 44 45 46 | } data, err := c.GetZettel(context.Background(), zid, api.PartZettel) if err != nil { t.Error("Cannot read zettel", zid, err) return } exp := `title: A Test | < < | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | } data, err := c.GetZettel(context.Background(), zid, api.PartZettel) if err != nil { t.Error("Cannot read zettel", zid, err) return } exp := `title: A Test Example content.` if string(data) != exp { t.Errorf("Expected zettel data: %q, but got %q", exp, data) } newZid := nextZid(zid) err = c.RenameZettel(context.Background(), zid, newZid) |
︙ | ︙ |
Changes to tests/client/embed_test.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 client_test import ( "context" "strings" |
︙ | ︙ | |||
38 39 40 41 42 43 44 | content, err := c.GetZettel(context.Background(), abcZid, api.PartContent) if err != nil { t.Error(err) return } baseContent := string(content) for zid, siz := range contentMap { | | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | content, err := c.GetZettel(context.Background(), abcZid, api.PartContent) if err != nil { t.Error(err) return } baseContent := string(content) for zid, siz := range contentMap { content, err = c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML) if err != nil { t.Error(err) continue } sContent := string(content) prefix := "<p>" if !strings.HasPrefix(sContent, prefix) { |
︙ | ︙ | |||
60 61 62 63 64 65 66 | } got := sContent[len(prefix) : len(content)-len(suffix)] if expect := strings.Repeat(baseContent, siz); expect != got { t.Errorf("Unexpected content for zettel %q\nExpect: %q\nGot: %q", zid, expect, got) } } | | | | < | | 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 | } got := sContent[len(prefix) : len(content)-len(suffix)] if expect := strings.Repeat(baseContent, siz); expect != got { t.Errorf("Unexpected content for zettel %q\nExpect: %q\nGot: %q", zid, expect, got) } } content, err = c.GetEvaluatedZettel(context.Background(), abc10000Zid, api.EncoderHTML) if err != nil { t.Error(err) return } checkContentContains(t, abc10000Zid, string(content), "Too\u00a0many\u00a0transclusions") } 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 } checkContentContains(t, abc10Zid, string(content), "Error placeholder") } func stringHead(s string) string { const maxLen = 40 if len(s) <= maxLen { return s } |
︙ | ︙ | |||
124 125 126 127 128 129 130 | ) recursiveZettel := map[api.ZettelID]api.ZettelID{ selfRecursiveZid: selfRecursiveZid, indirectRecursive1Zid: indirectRecursive2Zid, indirectRecursive2Zid: indirectRecursive1Zid, } for zid, errZid := range recursiveZettel { | | | | | | | 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 | ) recursiveZettel := map[api.ZettelID]api.ZettelID{ selfRecursiveZid: selfRecursiveZid, indirectRecursive1Zid: indirectRecursive2Zid, indirectRecursive2Zid: indirectRecursive1Zid, } for zid, errZid := range recursiveZettel { content, err := c.GetEvaluatedZettel(context.Background(), zid, api.EncoderHTML) if err != nil { t.Error(err) continue } sContent := string(content) checkContentContains(t, zid, sContent, "Recursive\u00a0transclusion") checkContentContains(t, zid, sContent, string(errZid)) } } func TestNothingToTransclude(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") const ( transZid = api.ZettelID("20211020184342") emptyZid = api.ZettelID("20211020184300") ) content, err := c.GetEvaluatedZettel(context.Background(), transZid, api.EncoderHTML) if err != nil { t.Error(err) return } sContent := string(content) checkContentContains(t, transZid, sContent, "<!-- Nothing to transclude") checkContentContains(t, transZid, sContent, string(emptyZid)) } func TestSelfEmbedRef(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") const selfEmbedZid = api.ZettelID("20211020185400") content, err := c.GetEvaluatedZettel(context.Background(), selfEmbedZid, api.EncoderHTML) if err != nil { t.Error(err) return } checkContentContains(t, selfEmbedZid, string(content), "Self\u00a0embed\u00a0reference") } func checkContentContains(t *testing.T, zid api.ZettelID, content, expected string) { if !strings.Contains(content, expected) { t.Helper() t.Errorf("Zettel %q should contain %q, but does not: %q", zid, expected, content) } } |
Changes to tests/markdown_test.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 | "os" "testing" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" _ "zettelstore.de/z/encoder/htmlenc" | | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | "os" "testing" "zettelstore.de/c/api" "zettelstore.de/z/ast" "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" |
︙ | ︙ | |||
41 42 43 44 45 46 47 | Section string `json:"section"` } func TestEncoderAvailability(t *testing.T) { t.Parallel() encoderMissing := false for _, enc := range encodings { | | | 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 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") |
︙ | ︙ | |||
75 76 77 78 79 80 81 | } 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) { | | | | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | } 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() |
︙ | ︙ |
Changes to tests/regression_test.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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. |
︙ | ︙ | |||
32 33 34 35 36 37 38 | "zettelstore.de/z/parser" _ "zettelstore.de/z/box/dirbox" ) var encodings = []api.EncodingEnum{ api.EncoderHTML, | | | | | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | "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) |
︙ | ︙ | |||
117 118 119 120 121 122 123 | } return u.Path[len(root):] } func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { t.Helper() | | | 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | } 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)) } |
︙ | ︙ | |||
155 156 157 158 159 160 161 | } ss.Stop(context.Background()) } type myConfig struct{} func (*myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { return m } | < < < < | | | | | 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 | } ss.Stop(context.Background()) } type myConfig struct{} func (*myConfig) AddDefaultValues(m *meta.Meta) *meta.Meta { return m } func (*myConfig) GetDefaultLang() string { return "" } func (*myConfig) GetFooterHTML() string { return "" } func (*myConfig) GetHomeZettel() id.Zid { return id.Invalid } func (*myConfig) GetListPageSize() int { return 0 } func (*myConfig) GetMarkerExternal() string { return "" } func (*myConfig) GetSiteName() string { return "" } func (*myConfig) GetYAMLHeader() bool { return false } func (*myConfig) GetZettelFileSyntax() []string { return nil } func (*myConfig) GetSimpleMode() bool { return false } func (*myConfig) GetExpertMode() bool { return false } func (*myConfig) GetVisibility(*meta.Meta) meta.Visibility { return meta.VisibilityPublic } func (*myConfig) GetMaxTransclusions() int { return 1024 } var testConfig = &myConfig{} func TestMetaRegression(t *testing.T) { t.Parallel() wd, err := os.Getwd() if err != nil { |
︙ | ︙ |
Changes to tools/build.go.
︙ | ︙ | |||
19 20 21 22 23 24 25 | "fmt" "io" "io/fs" "net" "os" "os/exec" "path/filepath" | < | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | "fmt" "io" "io/fs" "net" "os" "os/exec" "path/filepath" "strings" "time" "zettelstore.de/z/strfun" ) var directProxy = []string{"GOPROXY=direct"} |
︙ | ︙ | |||
69 70 71 72 73 74 75 | return "", err } return strings.TrimFunc(string(content), func(r rune) bool { return r <= ' ' }), nil } | > > > > > > > | | < < < < < < < < < < < | | | < < | < | | < < | | | < < < | | < < < < < < < | 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 | 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 "" |
︙ | ︙ | |||
207 208 209 210 211 212 213 | } func checkUnparam(forRelease bool) error { path, err := findExecStrict("unparam", forRelease) if path == "" { return err } | | | | | | < < > > | | | | | < < < > > > | 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 | } 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 |
︙ | ︙ | |||
344 345 346 347 348 349 350 | if len(out) > 0 { fmt.Println(out) } return nil } func cmdManual() error { | | | 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 | 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 { |
︙ | ︙ | |||
395 396 397 398 399 400 401 | return err } defer manualFile.Close() _, err = io.Copy(w, manualFile) return err } | | > > > | | < < < | | | | 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 | return err } defer manualFile.Close() _, err = io.Copy(w, manualFile) return err } func getReleaseVersionData() string { if fossil := getFossilDirty(); fossil != "" { fmt.Fprintln(os.Stderr, "Warning: releasing a dirty version") } base := getVersion() if strings.HasSuffix(base, "dev") { return base[:len(base)-3] + "preview-" + time.Now().Format("20060102") } return base } func cmdRelease() error { if err := cmdCheck(true); err != nil { return err } base := getReleaseVersionData() releases := []struct { arch string os string env []string name string }{ {"amd64", "linux", nil, "zettelstore"}, {"arm", "linux", []string{"GOARM=6"}, "zettelstore"}, {"amd64", "darwin", nil, "zettelstore"}, {"arm64", "darwin", nil, "zettelstore"}, {"amd64", "windows", nil, "zettelstore.exe"}, } for _, rel := range releases { env := append([]string{}, rel.env...) env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os) env = append(env, directProxy...) zsName := filepath.Join("releases", rel.name) if err := doBuild(env, base, zsName); err != nil { return err } zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch) if err := createReleaseZip(zsName, zipName, rel.name); err != nil { return err } if err := os.Remove(zsName); err != nil { |
︙ | ︙ |
Changes to usecase/authenticate.go.
1 2 3 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- // Copyright (c) 2020-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" |
︙ | ︙ | |||
45 46 47 48 49 50 51 | token: token, port: port, ucGetUser: NewGetUser(authz, port), } } // Run executes the use case. | > > > | | | | | 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 | 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. func (uc *Authenticate) Run(ctx context.Context, r *http.Request, ident, credential string, d time.Duration, k auth.TokenKind) ([]byte, error) { identMeta, err := uc.ucGetUser.Run(ctx, ident) defer addDelay(time.Now(), 500*time.Millisecond, 100*time.Millisecond) if identMeta == nil || err != nil { uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("No user with given ident found") compensateCompare() return nil, err } if hashCred, ok := identMeta.Get(api.KeyCredential); ok { ok, err = cred.CompareHashAndCredential(hashCred, identMeta.Zid, ident, credential) if err != nil { uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("Error while comparing credentials") return nil, err } if ok { token, err2 := uc.token.GetToken(identMeta, d, k) if err2 != nil { uc.log.Info().Str("ident", ident).Err(err).Msg("Unable to produce authentication token") return nil, err2 } uc.log.Info().Str("user", ident).Msg("Successful") return token, nil } uc.log.Info().Str("ident", ident).HTTPIP(r).Msg("Credentials don't match") return nil, nil } uc.log.Info().Str("ident", ident).Msg("No credential stored") compensateCompare() return nil, nil } |
︙ | ︙ |
Changes to usecase/create_zettel.go.
︙ | ︙ | |||
42 43 44 45 46 47 48 | port: port, } } // PrepareCopy the zettel for further modification. func (*CreateZettel) PrepareCopy(origZettel domain.Zettel) domain.Zettel { m := origZettel.Meta.Clone() | | | | | | | | | | | | | 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 | 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} } // 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) } } |
︙ | ︙ | |||
109 110 111 112 113 114 115 | // 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 } | < < | < < < < < < | 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | // 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.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/evaluate.go.
︙ | ︙ | |||
35 36 37 38 39 40 41 | rtConfig: rtConfig, getZettel: getZettel, getMeta: getMeta, } } // Run executes the use case. | | | | | | 35 36 37 38 39 40 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 | rtConfig: rtConfig, getZettel: getZettel, getMeta: getMeta, } } // 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(zettel, syntax, uc.rtConfig), nil if err != nil { return nil, err } evaluator.EvaluateZettel(ctx, uc, uc.rtConfig, zn) return zn, nil } // 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 } // 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) } |
︙ | ︙ |
Deleted usecase/list_meta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/list_role.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted usecase/list_tags.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added usecase/lists.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 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 | //----------------------------------------------------------------------------- // 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/search" ) // ListMetaPort is the interface used by this use case. type ListMetaPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // ListMeta is the data for this use case. type ListMeta struct { port ListMetaPort } // NewListMeta creates a new use case. func NewListMeta(port ListMetaPort) ListMeta { return ListMeta{port: port} } // Run executes the use case. func (uc ListMeta) Run(ctx context.Context, s *search.Search) ([]*meta.Meta, error) { return uc.port.SelectMeta(ctx, s) } // -------- List roles ------------------------------------------------------- // ListSyntaxPort is the interface used by this use case. type ListSyntaxPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // 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) { var s *search.Search s = s.AddExpr(api.KeySyntax, "") // We look for all metadata with a syntax key metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), s) if err != nil { return nil, err } result := meta.CreateArrangement(metas, api.KeySyntax) for _, syn := range parser.GetSyntaxes() { if _, found := result[syn]; !found { result[syn] = nil } } return result, nil } // -------- List roles ------------------------------------------------------- // ListRolesPort is the interface used by this use case. type ListRolesPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // 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) { var s *search.Search s = s.AddExpr(api.KeyRole, "") // We look for all metadata with a role key metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), s) if err != nil { return nil, err } return meta.CreateArrangement(metas, api.KeyRole), nil } // -------- List tags -------------------------------------------------------- // ListTagsPort is the interface used by this use case. type ListTagsPort interface { // SelectMeta returns all zettel metadata that match the selection criteria. SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // ListTags is the data for this use case. type ListTags struct { port ListTagsPort } // NewListTags creates a new use case. func NewListTags(port ListTagsPort) ListTags { return ListTags{port: port} } // Run executes the use case. func (uc ListTags) Run(ctx context.Context, minCount int) (meta.Arrangement, error) { var s *search.Search s = s.AddExpr(api.KeyTags, "") // We look for all metadata with a tag metas, err := uc.port.SelectMeta(ctx, s) if err != nil { return nil, err } result := meta.CreateArrangement(metas, api.KeyAllTags) if minCount > 1 { for t, ms := range result { if len(ms) < minCount { delete(result, t) } } } return result, nil } |
Changes to usecase/order.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ | |||
35 36 37 38 39 40 41 | 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, ) { | | | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | 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/unlinked_refs.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | "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" | | | | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/domain" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/search" ) // UnlinkedReferencesPort is the interface used by this use case. type UnlinkedReferencesPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) SelectMeta(ctx context.Context, s *search.Search) ([]*meta.Meta, error) } // UnlinkedReferences is the data for this use case. type UnlinkedReferences struct { port UnlinkedReferencesPort rtConfig config.Config encText *textenc.Encoder } // NewUnlinkedReferences creates a new use case. func NewUnlinkedReferences(port UnlinkedReferencesPort, rtConfig config.Config) UnlinkedReferences { return UnlinkedReferences{ port: port, rtConfig: rtConfig, encText: textenc.Create(), } } // Run executes the usecase with already evaluated title value. func (uc *UnlinkedReferences) Run(ctx context.Context, title string, s *search.Search) ([]*meta.Meta, error) { words := makeWords(title) if len(words) == 0 { |
︙ | ︙ | |||
93 94 95 96 97 98 99 | v.text = v.joinWords(words) for _, pair := range zettel.Meta.Pairs() { if meta.Type(pair.Key) != meta.TypeZettelmarkup { continue } is := parser.ParseMetadata(pair.Value) | | | | 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 | v.text = v.joinWords(words) for _, pair := range zettel.Meta.Pairs() { if meta.Type(pair.Key) != meta.TypeZettelmarkup { continue } is := parser.ParseMetadata(pair.Value) evaluator.EvaluateInline(ctx, uc.port, uc.rtConfig, &is) ast.Walk(&v, &is) if v.found { result = append(result, cand) continue candLoop } } syntax := zettel.Meta.GetDefault(api.KeySyntax, "") if !parser.IsTextParser(syntax) { continue } zn, err := parser.ParseZettel(zettel, syntax, nil), nil if err != nil { continue } evaluator.EvaluateZettel(ctx, uc.port, uc.rtConfig, zn) ast.Walk(&v, &zn.Ast) if v.found { result = append(result, cand) } } return result } |
︙ | ︙ |
Changes to web/adapter/api/content_type.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | 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{ | | | > | < | < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 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", |
︙ | ︙ |
Changes to web/adapter/api/get_eval_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | package api import ( "net/http" "zettelstore.de/c/api" "zettelstore.de/z/ast" | < < < | < < < < < < < < < | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 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_lists.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- // Package api provides api handlers for web requests. package api import ( "bytes" "net/http" "strconv" "zettelstore.de/c/api" "zettelstore.de/z/domain/meta" "zettelstore.de/z/usecase" ) // MakeListMapMetaHandler creates a new HTTP handler to retrieve mappings of // metadata values of a specific key to the list of zettel IDs, which contain // this value. func (a *API) MakeListMapMetaHandler(listRole usecase.ListRoles, listTags usecase.ListTags) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var ar meta.Arrangement query := r.URL.Query() iMinCount, err := strconv.Atoi(query.Get(api.QueryKeyMin)) if err != nil || iMinCount < 0 { iMinCount = 0 } ctx := r.Context() key := query.Get(api.QueryKeyKey) switch key { case api.KeyRole: ar, err = listRole.Run(ctx) case api.KeyTags: ar, err = listTags.Run(ctx, iMinCount) default: a.log.Info().Str("key", key).Msg("illegal key for retrieving meta map") http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if err != nil { a.reportUsecaseError(w, err) return } mm := make(api.MapMeta, len(ar)) for tag, metaList := range ar { zidList := make([]api.ZettelID, 0, len(metaList)) for _, m := range metaList { zidList = append(zidList, api.ZettelID(m.Zid.String())) } mm[tag] = zidList } var buf bytes.Buffer err = encodeJSONData(&buf, api.MapListJSON{Map: mm}) if err != nil { a.log.Fatal().Err(err).Msg("Unable to store map list in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = writeBuffer(w, &buf, ctJSON) a.log.IfErr(err).Str("key", key).Msg("write meta map") } } |
Changes to web/adapter/api/get_parsed_zettel.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "bytes" "fmt" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/ast" | < < | < < < < < < < | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | 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.String(), encStr)) return } var err error var buf bytes.Buffer switch part { |
︙ | ︙ |
Deleted web/adapter/api/get_role_list.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/get_tags_list.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/api/get_unlinked_refs.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "bytes" "net/http" "strings" "zettelstore.de/c/api" "zettelstore.de/z/domain/id" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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, |
︙ | ︙ | |||
40 41 42 43 44 45 46 | a.reportUsecaseError(w, err) return } q := r.URL.Query() phrase := q.Get(api.QueryKeyPhrase) if phrase == "" { | | | | | | | | > | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | a.reportUsecaseError(w, err) return } q := r.URL.Query() phrase := q.Get(api.QueryKeyPhrase) if phrase == "" { if zmkTitle, found := zm.Get(api.KeyTitle); found { isTitle := evaluate.RunMetadata(ctx, zmkTitle) encdr := textenc.Create() var b strings.Builder _, err = encdr.WriteInlines(&b, &isTitle) if err == nil { phrase = b.String() } } } metaList, err := unlinkedRefs.Run( ctx, phrase, adapter.AddUnlinkedRefsToSearch(adapter.GetSearch(q), zm)) if err != nil { a.reportUsecaseError(w, err) |
︙ | ︙ |
Changes to web/adapter/api/get_zettel.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "bytes" "context" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/box" | < | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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 { |
︙ | ︙ | |||
73 74 75 76 77 78 79 | if err == nil { _, err = z.Content.Write(&buf) } case partMeta: contentType = ctPlainText _, err = z.Meta.Write(&buf) case partContent: | | | 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | 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) |
︙ | ︙ |
Changes to web/adapter/api/get_zettel_list.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "bytes" "fmt" "net/http" "zettelstore.de/c/api" | < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import ( "bytes" "fmt" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListMetaHandler creates a new HTTP handler for the use case "list some zettel". func (a *API) MakeListMetaHandler(listMeta usecase.ListMeta) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { |
︙ | ︙ | |||
69 70 71 72 73 74 75 | if err != nil { a.reportUsecaseError(w, err) return } var buf bytes.Buffer for _, m := range metaList { | | | 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | 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") } } |
Changes to web/adapter/api/login.go.
1 2 3 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ | |||
29 30 31 32 33 34 35 | 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 | | | 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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"`) |
︙ | ︙ |
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 61 62 63 64 65 66 67 68 | //----------------------------------------------------------------------------- // 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 ( "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 len(encoding) > 0 { 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 } type partType int const ( _ partType = iota partMeta partContent |
︙ | ︙ |
Changes to web/adapter/request.go.
︙ | ︙ | |||
46 47 48 49 50 51 52 | if val, err := strconv.Atoi(s); err == nil { return val, true } } return 0, false } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | if val, err := strconv.Atoi(s); err == nil { return val, true } } return 0, false } // GetSearch retrieves the specified search and sorting options from a query. func GetSearch(q url.Values) (s *search.Search) { for key, values := range q { switch key { case api.QueryKeySort, api.QueryKeyOrder: s = extractOrderFromQuery(values, s) case api.QueryKeyOffset: |
︙ | ︙ |
Changes to web/adapter/response.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "errors" "fmt" "net/http" "zettelstore.de/c/api" | < < < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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) |
︙ | ︙ | |||
67 68 69 70 71 72 73 | 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() } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 64 65 66 67 68 69 70 | 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/create_zettel.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // 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 ( | | | | > > < < < < < > | | | | < | | > > > > > > > > > > > > | < < < < > > < > > < | | > > > > | | > > > | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | //----------------------------------------------------------------------------- // 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/config" "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" ) // 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 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, false) 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)) return roleData, syntaxData } func dataListFromArrangement(ar meta.Arrangement, err error) []string { if err == nil { l := ar.Counted() l.SortByCount() return l.Categories() } return nil } func (wui *WebUI) renderZettelForm( ctx context.Context, w http.ResponseWriter, zettel domain.Zettel, title, heading string, roleData []string, syntaxData []string, ) { user := wui.getUser(ctx) m := zettel.Meta var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), title, "", user, &base) wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{ Heading: heading, MetaTitle: m.GetDefault(api.KeyTitle, ""), MetaTags: m.GetDefault(api.KeyTags, ""), MetaRole: m.GetDefault(api.KeyRole, ""), HasRoleData: len(roleData) > 0, RoleData: roleData, HasSyntaxData: len(syntaxData) > 0, SyntaxData: syntaxData, MetaSyntax: m.GetDefault(api.KeySyntax, ""), MetaPairsRest: m.PairsRest(), IsTextContent: !zettel.Content.IsBinary(), Content: zettel.Content.AsString(), }) } // 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 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // 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 ( | < < > < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | //----------------------------------------------------------------------------- // Copyright (c) 2020-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/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. func (wui *WebUI) MakeGetDeleteZettelHandler( getMeta usecase.GetMeta, getAllMeta usecase.GetAllMeta, evaluate *usecase.Evaluate, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } ms, err := getAllMeta.Run(ctx, zid) |
︙ | ︙ | |||
61 62 63 64 65 66 67 | getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) incomingLinks = wui.encodeIncoming(m, getTextTitle) } uselessFiles := retrieveUselessFiles(m) user := wui.getUser(ctx) var base baseData | | | 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) incomingLinks = wui.encodeIncoming(m, getTextTitle) } uselessFiles := retrieveUselessFiles(m) user := wui.getUser(ctx) var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Delete Zettel "+m.Zid.String(), "", user, &base) wui.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct { Zid string MetaPairs []meta.Pair HasShadows bool ShadowedBox string HasIncoming bool Incoming []simpleLink |
︙ | ︙ | |||
127 128 129 130 131 132 133 | if val, ok := m.Get(inverseKey); ok { zidMap.Set(val) } case meta.TypeIDSet: addListValues(zidMap, m, inverseKey) } } | < < < < < | | 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | if val, ok := m.Get(inverseKey); ok { zidMap.Set(val) } case meta.TypeIDSet: addListValues(zidMap, m, inverseKey) } } return 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 2 3 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // | | < < | < < < < < | < < < | < < < < < < < < < < | > > > > | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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()))) } } } |
Changes to web/adapter/webui/forms.go.
︙ | ︙ | |||
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | "zettelstore.de/z/input" ) type formZettelData struct { Heading string MetaTitle string MetaRole string MetaTags string MetaSyntax string MetaPairsRest []meta.Pair IsTextContent bool Content string } var ( bsCRLF = []byte{'\r', '\n'} bsLF = []byte{'\n'} ) | > > > > | | > | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | "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) |
︙ | ︙ | |||
63 64 65 66 67 68 69 | 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 { | | | | 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | 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 { |
︙ | ︙ |
Changes to web/adapter/webui/get_info.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package webui import ( "bytes" "context" | < < < | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- 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/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) type metaDataInfo struct { Key string Value string |
︙ | ︙ | |||
48 49 50 51 52 53 54 | 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() | < < < < < | < < < < < < < < < < < < < < < | | | | | 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 | 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 } enc := wui.getSimpleHTMLEncoder() pairs := zn.Meta.ComputedPairs() metaData := make([]metaDataInfo, len(pairs)) getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) for i, p := range pairs { var buf bytes.Buffer wui.writeHTMLMetaValue( &buf, p.Key, p.Value, getTextTitle, func(val string) ast.InlineSlice { return evaluate.RunMetadata(ctx, val) }, enc) metaData[i] = metaDataInfo{p.Key, buf.String()} } summary := collect.References(zn) locLinks, extLinks := splitLocExtLinks(append(summary.Links, summary.Embeds...)) textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate) phrase := q.Get(api.QueryKeyPhrase) if phrase == "" { phrase = textTitle } phrase = strings.TrimSpace(phrase) unlinkedMeta, err := unlinkedRefs.Run( ctx, phrase, adapter.AddUnlinkedRefsToSearch(nil, zn.InhMeta)) if err != nil { wui.reportError(ctx, w, err) return } unLinks := wui.buildHTMLMetaList(ctx, unlinkedMeta, evaluate) shadowLinks := getShadowLinks(ctx, zid, getAllMeta) endnotes, err := enc.BlocksString(&ast.BlockSlice{}) if err != nil { endnotes = "" } user := wui.getUser(ctx) canCreate := wui.canCreate(ctx, user) apiZid := api.ZettelID(zid.String()) var base baseData wui.makeBaseData(ctx, config.GetLang(zn.InhMeta, wui.rtConfig), textTitle, "", user, &base) wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct { Zid string WebURL string ContextURL string CanWrite bool EditURL string CanFolge bool |
︙ | ︙ |
Changes to web/adapter/webui/get_zettel.go.
︙ | ︙ | |||
8 9 10 11 12 13 14 | // under this license. //----------------------------------------------------------------------------- package webui import ( "bytes" | < < | < < < < < < < < < < < < < < < < < | > | | | | | < < < < < < | > | < < > > > | > | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 | // under this license. //----------------------------------------------------------------------------- package webui import ( "bytes" "net/http" "zettelstore.de/c/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/usecase" ) // 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() metaHeader := enc.MetaString(zn.InhMeta, func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) }) textTitle := wui.encodeTitleAsText(ctx, zn.InhMeta, evaluate) htmlTitle := encodeTitleAsHTML(ctx, zn.InhMeta, evaluate, enc, false) htmlContent, err := enc.BlocksString(&zn.Ast) if err != nil { wui.reportError(ctx, w, err) return } var roleCSSURL string cssZid, err := wui.retrieveCSSZidFromRole(ctx, *zn.InhMeta) if err != nil { wui.reportError(ctx, w, err) return } if cssZid != id.Invalid { roleCSSURL = wui.NewURLBuilder('z').SetZid(api.ZettelID(cssZid.String())).String() } user := wui.getUser(ctx) roleText := zn.Meta.GetDefault(api.KeyRole, "*") tags := wui.buildTagInfos(zn.Meta) canCreate := wui.canCreate(ctx, user) getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) extURL, hasExtURL := zn.Meta.Get(api.KeyURL) folgeLinks := wui.encodeZettelLinks(zn.InhMeta, api.KeyFolge, getTextTitle) backLinks := wui.encodeZettelLinks(zn.InhMeta, api.KeyBack, getTextTitle) apiZid := api.ZettelID(zid.String()) var base baseData wui.makeBaseData(ctx, config.GetLang(zn.InhMeta, wui.rtConfig), textTitle, roleCSSURL, user, &base) base.MetaHeader = metaHeader wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct { HTMLTitle string RoleCSS string CanWrite bool EditURL string Zid string InfoURL string RoleText string RoleURL string HasTags bool |
︙ | ︙ | |||
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | Content string HasFolgeLinks bool FolgeLinks []simpleLink HasBackLinks bool BackLinks []simpleLink }{ HTMLTitle: htmlTitle, CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: wui.NewURLBuilder('e').SetZid(apiZid).String(), Zid: zid.String(), InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(), RoleText: roleText, RoleURL: wui.NewURLBuilder('h').AppendQuery("role", roleText).String(), HasTags: len(tags) > 0, Tags: tags, CanCopy: canCreate && !zn.Content.IsBinary(), CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionCopy).String(), CanFolge: canCreate, FolgeURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionFolge).String(), PrecursorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPrecursor, getTextTitle), ExtURL: extURL, HasExtURL: hasExtURL, | > | < < | < < | < < < | < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 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 | Content string HasFolgeLinks bool FolgeLinks []simpleLink HasBackLinks bool BackLinks []simpleLink }{ HTMLTitle: htmlTitle, RoleCSS: roleCSSURL, CanWrite: wui.canWrite(ctx, user, zn.Meta, zn.Content), EditURL: wui.NewURLBuilder('e').SetZid(apiZid).String(), Zid: zid.String(), InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(), RoleText: roleText, RoleURL: wui.NewURLBuilder('h').AppendQuery("role", roleText).String(), HasTags: len(tags) > 0, Tags: tags, CanCopy: canCreate && !zn.Content.IsBinary(), CopyURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionCopy).String(), CanFolge: canCreate, FolgeURL: wui.NewURLBuilder('c').SetZid(apiZid).AppendQuery(queryKeyAction, valueActionFolge).String(), PrecursorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPrecursor, getTextTitle), ExtURL: extURL, HasExtURL: hasExtURL, ExtNewWindow: htmlAttrNewWindow(hasExtURL), Content: htmlContent, HasFolgeLinks: len(folgeLinks) > 0, FolgeLinks: folgeLinks, HasBackLinks: len(backLinks) > 0, BackLinks: backLinks, }) } } func encodeInlinesText(is *ast.InlineSlice, enc *textenc.Encoder) (string, error) { if is == nil || len(*is) == 0 { return "", nil } var buf bytes.Buffer _, err := enc.WriteInlines(&buf, is) if err != nil { return "", err } return buf.String(), nil } func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink { |
︙ | ︙ |
Added web/adapter/webui/htmlgen.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | //----------------------------------------------------------------------------- // 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/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 newWindow bool env *html.EncEnvironment } func createGenerator(builder urlBuilder, extMarker string, newWindow bool) *htmlGenerator { env := html.NewEncEnvironment(nil, 1) gen := &htmlGenerator{ builder: builder, textEnc: textenc.Create(), extMarker: extMarker, newWindow: newWindow, env: env, } env.Builtins.Set(sexpr.SymTag, sxpf.NewBuiltin("tag", true, 0, -1, gen.generateTag)) env.Builtins.Set(sexpr.SymLinkZettel, sxpf.NewBuiltin("linkZ", true, 2, -1, gen.generateLinkZettel)) env.Builtins.Set(sexpr.SymLinkFound, sxpf.NewBuiltin("linkZ", true, 2, -1, gen.generateLinkZettel)) env.Builtins.Set(sexpr.SymLinkBased, sxpf.NewBuiltin("linkB", true, 2, -1, gen.generateLinkBased)) env.Builtins.Set(sexpr.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.KeyAllTags); ok { writeMetaTags(&buf, tags) ignore.Set(api.KeyAllTags) ignore.Set(api.KeyTags) } else if tags, ok = m.Get(api.KeyTags); ok { writeMetaTags(&buf, tags) ignore.Set(api.KeyTags) } for _, p := range m.ComputedPairs() { key := p.Key if ignore.Has(key) { continue } if altKey, found := mapMetaKey[key]; found { buf.WriteString(`<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, noLink bool) (string, error) { if is == nil || len(*is) == 0 { return "", nil } return html.EvaluateInline(g.env, sexprenc.GetSexpr(is), !noLink, noLink), nil } func (g *htmlGenerator) generateTag(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) { if !sxpf.IsNil(args) { env := senv.(*html.EncEnvironment) s := env.GetString(args) if env.IgnoreLinks() { env.WriteEscaped(s) } else { u := g.builder.NewURLBuilder('h').AppendQuery(api.KeyAllTags, "#"+strings.ToLower(s)) env.WriteStrings(`<a href="`, u.String(), `">#`) env.WriteEscaped(s) env.WriteString("</a>") } } return nil, nil } func (g *htmlGenerator) generateLinkZettel(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) { env := senv.(*html.EncEnvironment) if a, refValue, ok := html.PrepareLink(env, args); ok { zid, fragment, hasFragment := strings.Cut(refValue, "#") u := g.builder.NewURLBuilder('h').SetZid(api.ZettelID(zid)) if hasFragment { u = u.SetFragment(fragment) } html.WriteLink(env, args, a.Set("href", u.String()), refValue, "") } return nil, nil } func (g *htmlGenerator) generateLinkBased(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) { env := senv.(*html.EncEnvironment) if a, refValue, ok := html.PrepareLink(env, args); ok { u := g.builder.NewURLBuilder('/').SetRawLocal(refValue) html.WriteLink(env, args, a.Set("href", u.String()), refValue, "") } return nil, nil } func (g *htmlGenerator) 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.
︙ | ︙ | |||
18 19 20 21 22 23 24 | "net/url" "time" "zettelstore.de/c/api" "zettelstore.de/c/html" "zettelstore.de/z/ast" "zettelstore.de/z/box" | < < < | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | "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{' '} type evalMetadataFunc = func(string) ast.InlineSlice 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: |
︙ | ︙ | |||
63 64 65 66 67 68 69 | 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: | | | 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | 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, false)) 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) } |
︙ | ︙ | |||
171 172 173 174 175 176 177 | } return "", 0 } return wui.encodeTitleAsText(ctx, m, evaluate), 1 } } | | | | | | < | < | < < < < | < | < > | > | > | | | < < < < < | | 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 | } return "", 0 } return wui.encodeTitleAsText(ctx, m, evaluate), 1 } } func encodeTitleAsHTML( ctx context.Context, m *meta.Meta, evaluate *usecase.Evaluate, gen *htmlGenerator, noLink bool, ) string { plainTitle := m.GetTitle() return encodeZmkMetadata( plainTitle, func(val string) ast.InlineSlice { return evaluate.RunMetadata(ctx, val) }, gen, noLink) } func (wui *WebUI) encodeTitleAsText(ctx context.Context, m *meta.Meta, evaluate *usecase.Evaluate) string { plainTitle := m.GetTitle() is := evaluate.RunMetadata(ctx, plainTitle) result, err := encodeInlinesText(&is, wui.gentext) if err != nil { return err.Error() } return result } func encodeZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator, noLink bool) string { is := evalMetadata(value) result, err := gen.InlinesString(&is, noLink) if err != nil { return err.Error() } return result } |
Changes to web/adapter/webui/lists.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 | "sort" "strconv" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" | < | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | "sort" "strconv" "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/domain/id" "zettelstore.de/z/domain/meta" "zettelstore.de/z/search" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of // zettel as HTML. func (wui *WebUI) MakeListHTMLMetaHandler( listMeta usecase.ListMeta, listRole usecase.ListRoles, listTags usecase.ListTags, evaluate *usecase.Evaluate, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() switch query.Get("_l") { case "r": |
︙ | ︙ | |||
65 66 67 68 69 70 71 | if err != nil { wui.reportError(ctx, w, err) return } user := wui.getUser(ctx) metas := wui.buildHTMLMetaList(ctx, metaList, evaluate) var base baseData | | | | > > | | | 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 | if err != nil { wui.reportError(ctx, w, err) return } user := wui.getUser(ctx) metas := wui.buildHTMLMetaList(ctx, metaList, evaluate) var base baseData wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base) wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct { Title string Metas []simpleLink }{ Title: title, Metas: metas, }) } type roleInfo struct { Text string URL string } func (wui *WebUI) renderRolesList(w http.ResponseWriter, r *http.Request, listRole usecase.ListRoles) { ctx := r.Context() roleArrangement, err := listRole.Run(ctx) if err != nil { wui.reportError(ctx, w, err) return } roleList := roleArrangement.Counted() roleList.SortByName() roleInfos := make([]roleInfo, len(roleList)) for i, role := range roleList { roleInfos[i] = roleInfo{role.Name, wui.NewURLBuilder('h').AppendQuery("role", role.Name).String()} } user := wui.getUser(ctx) var base baseData wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base) wui.renderTemplate(ctx, w, id.RolesTemplateZid, &base, struct { Roles []roleInfo }{ Roles: roleInfos, }) } |
︙ | ︙ | |||
159 160 161 162 163 164 165 | for i := 0; i < len(tagsList); i++ { count := tagsList[i].iCount tagsList[i].Count = strconv.Itoa(count) tagsList[i].Size = strconv.Itoa(countMap[count]) } var base baseData | | | 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | for i := 0; i < len(tagsList); i++ { count := tagsList[i].iCount tagsList[i].Count = strconv.Itoa(count) tagsList[i].Size = strconv.Itoa(countMap[count]) } var base baseData wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base) minCounts := make([]countInfo, 0, len(countList)) for _, c := range countList { sCount := strconv.Itoa(c) minCounts = append(minCounts, countInfo{sCount, base.ListTagsURL + "&min=" + sCount}) } wui.renderTemplate(ctx, w, id.TagsTemplateZid, &base, struct { |
︙ | ︙ | |||
214 215 216 217 218 219 220 | } depthURL.AppendQuery(api.QueryKeyDepth, depth) depthLinks[i].Text = depth depthLinks[i].URL = depthURL.String() } var base baseData user := wui.getUser(ctx) | | | 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | } depthURL.AppendQuery(api.QueryKeyDepth, depth) depthLinks[i].Text = depth depthLinks[i].URL = depthURL.String() } var base baseData user := wui.getUser(ctx) wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), wui.rtConfig.GetSiteName(), "", user, &base) wui.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct { Title string InfoURL string Depths []simpleLink Start simpleLink Metas []simpleLink }{ |
︙ | ︙ | |||
249 250 251 252 253 254 255 | } var buf bytes.Buffer s.Print(&buf) return buf.String() } // buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering. | < | < < > < < < < < < < | | 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | } var buf bytes.Buffer s.Print(&buf) return buf.String() } // buildHTMLMetaList builds a zettel list based on a meta list for HTML rendering. func (wui *WebUI) buildHTMLMetaList(ctx context.Context, metaList []*meta.Meta, evaluate *usecase.Evaluate) []simpleLink { metas := make([]simpleLink, 0, len(metaList)) encHTML := wui.getSimpleHTMLEncoder() for _, m := range metaList { metas = append(metas, simpleLink{ Text: encodeTitleAsHTML(ctx, m, evaluate, encHTML, true), URL: wui.NewURLBuilder('h').SetZid(api.ZettelID(m.Zid.String())).String(), }) } return metas } |
Changes to web/adapter/webui/login.go.
1 | //----------------------------------------------------------------------------- | | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ | |||
32 33 34 35 36 37 38 | } wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false) } } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { var base baseData | | | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | } wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false) } } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { var base baseData wui.makeBaseData(ctx, wui.rtConfig.GetDefaultLang(), "Login", "", nil, &base) wui.renderTemplate(ctx, w, id.LoginTemplateZid, &base, struct { Title string Retry bool }{ Title: base.Title, Retry: retry, }) |
︙ | ︙ | |||
55 56 57 58 59 60 61 | } ctx := r.Context() ident, cred, ok := adapter.GetCredentialsViaForm(r) if !ok { wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read login form")) return } | | | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | } 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('/')) } } |
Changes to web/adapter/webui/rename_zettel.go.
1 2 3 | //----------------------------------------------------------------------------- // Copyright (c) 2020-2022 Detlef Stern // | | | 1 2 3 4 5 6 7 8 9 10 11 | //----------------------------------------------------------------------------- // 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 |
︙ | ︙ | |||
37 38 39 40 41 42 43 | m, err := getMeta.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } | < < < < < < | | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | m, err := getMeta.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } getTextTitle := wui.makeGetTextTitle(ctx, getMeta, evaluate) incomingLinks := wui.encodeIncoming(m, getTextTitle) uselessFiles := retrieveUselessFiles(m) user := wui.getUser(ctx) var base baseData wui.makeBaseData(ctx, config.GetLang(m, wui.rtConfig), "Rename Zettel "+zid.String(), "", user, &base) wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct { Zid string MetaPairs []meta.Pair HasIncoming bool Incoming []simpleLink HasUselessFiles bool UselessFiles []string |
︙ | ︙ |
Changes to web/adapter/webui/response.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | package webui import ( "net/http" "zettelstore.de/c/api" | < < < < < < < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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) } |
Changes to web/adapter/webui/webui.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package webui provides web-UI handlers for web requests. package webui import ( "bytes" "context" | < > | > > > > | > | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | // 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 |
︙ | ︙ | |||
82 83 84 85 86 87 88 89 90 91 92 93 94 95 | debug: kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool), ab: ab, rtConfig: rtConfig, authz: authz, token: token, box: mgr, policy: pol, tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration), cssBaseURL: ab.NewURLBuilder('z').SetZid(api.ZidBaseCSS).String(), cssUserURL: ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(), homeURL: ab.NewURLBuilder('/').String(), listZettelURL: ab.NewURLBuilder('h').String(), listRolesURL: ab.NewURLBuilder('h').AppendQuery("_l", "r").String(), | > > | 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | 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("_l", "r").String(), |
︙ | ︙ | |||
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 | 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() } 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) 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( | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | 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( |
︙ | ︙ | |||
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 | } type baseData struct { Lang string MetaHeader string CSSBaseURL string CSSUserURL string Title string HomeURL string WithUser bool WithAuth bool UserIsValid bool UserZettelURL string UserIdent string LoginURL string LogoutURL string ListZettelURL string ListRolesURL string ListTagsURL string CanRefresh bool RefreshURL string HasNewZettelLinks bool NewZettelLinks []simpleLink SearchURL string QueryKeySearch string Content string FooterHTML string } | > > | > > > > > > > > > | 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 | } type baseData struct { Lang string MetaHeader string CSSBaseURL string CSSUserURL string CSSRoleURL string Title string HomeURL string WithUser bool WithAuth bool UserIsValid bool UserZettelURL string UserIdent string LoginURL string LogoutURL string ListZettelURL string ListRolesURL string ListTagsURL string CanRefresh bool RefreshURL string HasNewZettelLinks bool NewZettelLinks []simpleLink SearchURL string QueryKeySearch string Content string FooterHTML string DebugMode bool } func (wui *WebUI) makeBaseData(ctx context.Context, lang, title, roleCSSURL string, user *meta.Meta, data *baseData) { var userZettelURL string var userIdent string userIsValid := user != nil if userIsValid { userZettelURL = wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String() userIdent = user.GetDefault(api.KeyUserID, "") } newZettelLinks := wui.fetchNewTemplates(ctx, user) data.Lang = lang data.CSSBaseURL = wui.cssBaseURL data.CSSUserURL = wui.cssUserURL data.CSSRoleURL = roleCSSURL data.Title = title data.HomeURL = wui.homeURL data.WithAuth = wui.withAuth data.WithUser = data.WithAuth data.UserIsValid = userIsValid data.UserZettelURL = userZettelURL data.UserIdent = userIdent data.LoginURL = wui.loginURL data.LogoutURL = wui.logoutURL data.ListZettelURL = wui.listZettelURL data.ListRolesURL = wui.listRolesURL data.ListTagsURL = wui.listTagsURL data.CanRefresh = wui.canRefresh(user) data.RefreshURL = wui.refreshURL data.HasNewZettelLinks = len(newZettelLinks) > 0 data.NewZettelLinks = newZettelLinks data.SearchURL = wui.searchURL data.QueryKeySearch = api.QueryKeySearch data.FooterHTML = wui.rtConfig.GetFooterHTML() data.DebugMode = wui.debug } func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator { return createGenerator(wui, "", false) } func (wui *WebUI) createZettelEncoder() *htmlGenerator { return createGenerator(wui, wui.rtConfig.GetMarkerExternal(), true) } // htmlAttrNewWindow returns HTML attribute string for opening a link in a new window. // If hasURL is false an empty string is returned. func htmlAttrNewWindow(hasURL bool) string { if hasURL { return " target=\"_blank\" ref=\"noopener noreferrer\"" |
︙ | ︙ | |||
262 263 264 265 266 267 268 | m, err2 := wui.box.GetMeta(ctx, zid) if err2 != nil { continue } if !wui.policy.CanRead(user, m) { continue } | | < | | | 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 | m, err2 := wui.box.GetMeta(ctx, zid) if err2 != nil { continue } if !wui.policy.CanRead(user, m) { continue } title := m.GetTitle() astTitle := parser.ParseMetadata(title) menuTitle, err2 := wui.getSimpleHTMLEncoder().InlinesString(&astTitle, false) 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())). |
︙ | ︙ | |||
297 298 299 300 301 302 303 | func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { wui.log.Error().Msg(err.Error()) } user := wui.getUser(ctx) var base baseData | | | 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 | func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { wui.log.Error().Msg(err.Error()) } user := wui.getUser(ctx) var base baseData wui.makeBaseData(ctx, api.ValueLangEN, "Error", "", user, &base) wui.renderTemplateStatus(ctx, w, code, id.ErrorTemplateZid, &base, struct { ErrorTitle string ErrorText string }{ ErrorTitle: http.StatusText(code), ErrorText: text, }) |
︙ | ︙ | |||
336 337 338 339 340 341 342 | wui.setToken(w, tok) } } var content bytes.Buffer err = t.Render(&content, data) if err == nil { wui.prepareAndWriteHeader(w, code) | < < | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 | wui.setToken(w, tok) } } var content bytes.Buffer err = t.Render(&content, data) if err == nil { wui.prepareAndWriteHeader(w, code) base.Content = content.String() err = bt.Render(w, base) } if err != nil { wui.log.IfErr(err).Msg("Unable to write HTML via template") } } func (wui *WebUI) getUser(ctx context.Context) *meta.Meta { return wui.ab.GetUser(ctx) } // 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) } |
︙ | ︙ |
Changes to web/server/impl/impl.go.
︙ | ︙ | |||
28 29 30 31 32 33 34 | server httpServer router httpRouter persistentCookie bool secureCookie bool } // New creates a new web server. | | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | server httpServer router httpRouter persistentCookie bool secureCookie bool } // New creates a new web server. func New(log *logger.Logger, listenAddr, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server { srv := myServer{ log: log, persistentCookie: persistentCookie, secureCookie: secureCookie, } srv.router.initializeRouter(log, urlPrefix, maxRequestSize, auth) srv.server.initializeHTTPServer(listenAddr, &srv.router) return &srv } func (srv *myServer) Handle(pattern string, handler http.Handler) { srv.router.Handle(pattern, handler) } |
︙ | ︙ |
Changes to web/server/impl/router.go.
︙ | ︙ | |||
46 47 48 49 50 51 52 53 54 55 | minKey byte maxKey byte reURL *regexp.Regexp listTable routingTable zettelTable routingTable ur server.UserRetriever mux *http.ServeMux } // initializeRouter creates a new, empty router with the given root handler. | > | > | 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 | minKey byte maxKey byte reURL *regexp.Regexp listTable routingTable zettelTable routingTable ur server.UserRetriever mux *http.ServeMux maxReqSize int64 } // initializeRouter creates a new, empty router with the given root handler. func (rt *httpRouter) initializeRouter(log *logger.Logger, urlPrefix string, maxRequestSize int64, auth auth.TokenManager) { rt.log = log rt.urlPrefix = urlPrefix rt.auth = auth rt.minKey = 255 rt.maxKey = 0 rt.reURL = regexp.MustCompile("^$") rt.mux = http.NewServeMux() rt.maxReqSize = maxRequestSize } func (rt *httpRouter) addRoute(key byte, method server.Method, handler http.Handler, table *routingTable) { // Set minKey and maxKey; re-calculate regexp. if key < rt.minKey || rt.maxKey < key { if key < rt.minKey { rt.minKey = key |
︙ | ︙ | |||
104 105 106 107 108 109 110 | 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 { | | | > | 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 | 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} msg.Str("method", r.Method).Str("uri", r.RequestURI).HTTPIP(r).Msg("ServeHTTP") } if prefixLen := len(rt.urlPrefix); prefixLen > 1 { if len(r.URL.Path) < prefixLen || r.URL.Path[:prefixLen] != rt.urlPrefix { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) if withDebug { rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("/ServeHTTP/prefix") } return } r.URL.Path = r.URL.Path[prefixLen-1:] } r.Body = http.MaxBytesReader(w, r.Body, rt.maxReqSize) match := rt.reURL.FindStringSubmatch(r.URL.Path) if len(match) != 3 { rt.mux.ServeHTTP(w, rt.addUserContext(r)) if withDebug { rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("match other") } return |
︙ | ︙ | |||
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 | if withDebug { rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("no match") } } func (rt *httpRouter) addUserContext(r *http.Request) *http.Request { if rt.ur == nil { return r } k := auth.KindJSON t := getHeaderToken(r) if len(t) == 0 { k = auth.KindHTML t = getSessionToken(r) } if len(t) == 0 { return r } tokenData, err := rt.auth.CheckToken(t, k) if err != nil { return r } ctx := r.Context() user, err := rt.ur.GetUser(ctx, tokenData.Zid, tokenData.Ident) if err != nil { return r } return r.WithContext(updateContext(ctx, user, &tokenData)) } func getSessionToken(r *http.Request) []byte { cookie, err := r.Cookie(sessionName) | > > > > > | 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 | if withDebug { rt.log.Debug().Int("sc", int64(w.(*traceResponseWriter).statusCode)).Msg("no match") } } func (rt *httpRouter) addUserContext(r *http.Request) *http.Request { if rt.ur == nil { // No auth needed return r } k := auth.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 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 31 32 33 34 35 36 37 38 39 40 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 | <title>Change Log</title> <a name="0_6"></a> <h2>Changes for Version 0.6.0 (pending)</h2> <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) <h2>Changes for Version 0.5.0 (2022-07-29)</h2> * Removed zettel syntax “draw”. The new default syntax for inline zettel is now “text”. A drawing can now be made by using the “evaluation block” syntax (see below) by setting the generic attribute to “draw”. (breaking: zettelmarkup, api, webui) * 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. (minor: zettelmarkup) * Add initial support for a TeX-like math-mode to Zettelmarkup (both block- and inline-structured elements). Currently, support only the syntax, but WebUI does not render these elements in a special way. (minor: zettelmarkup) * For block-structured elements, attributes may now span more than one line. If a line ending occurs within a quoted attribute value, the line ending characters are part of the attributes value. (minor: zettelmarkup) * Zettel 00000000029000 acts as a map of zettel roles to identifier of zettel that are included as additional CSS. Allows to display zettel differently depending on their role. Use case: slides that are processed by Zettel Presenter will use the same CSS if they are rendered by Zettelstore WebUI. (minor: webui) * A zettel can be saved while creating / editing it. There is no need to manually re-edit it by using the 'e' endpoint. (minor: webui) * Zettel role and zettel syntax are backed by a HTML5 data list element which lists supported and used values to help to enter a valid value. (mirnor: webui) * Allow to use startup configuration, even if started in simple mode. (minor) * Log authentication issues in level "sense"; add caller IP address to some web server log messages. (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 inprovements, 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, |
︙ | ︙ |
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.5.1</code> (2022-08-02). * [/uv/zettelstore-0.5.1-linux-amd64.zip|Linux] (amd64) * [/uv/zettelstore-0.5.1-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore-0.5.1-windows-amd64.zip|Windows] (amd64) * [/uv/zettelstore-0.5.1-darwin-amd64.zip|macOS] (amd64) * [/uv/zettelstore-0.5.1-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.5.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.
︙ | ︙ | |||
20 21 22 23 24 25 26 | 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> | | | | | | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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.5.1 (2022-08-02)</h3> * [./download.wiki|Download] * [./changes.wiki#0_5_0|Change summary] * [/timeline?p=v0.5.1&bt=v0.4&y=ci|Check-ins for version 0.5.1], [/vdiff?to=v0.5.1&from=v0.4|content diff] * [/timeline?df=v0.5.0&y=ci|Check-ins derived from the 0.5.0 release], [/vdiff?from=v0.5.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. |
︙ | ︙ |
Changes to www/plan.wiki.
︙ | ︙ | |||
22 23 24 25 26 27 28 | always preserved. * Some file systems differentiate filenames with different cases (e.g. some on Linux, sometimes on macOS), others do not (default on macOS, most on Windows). Zettelstore is not able to detect these differences. Do not put files in your directory boxes and in files boxes that differ only by upper / lower case letters. * … | < < < < < | 22 23 24 25 26 27 28 | always preserved. * Some file systems differentiate filenames with different cases (e.g. some on Linux, sometimes on macOS), others do not (default on macOS, most on Windows). Zettelstore is not able to detect these differences. Do not put files in your directory boxes and in files boxes that differ only by upper / lower case letters. * … |