Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From v0.19.0 To v0.20.0
2025-03-07
| ||
16:48 | Fix release date for 0.20.0 in changelog ... (check-in: f0f37f254c user: stern tags: trunk) | |
16:06 | Version 0.20.0 ... (check-in: 674f1fe89d user: stern tags: trunk, release, v0.20.0) | |
10:41 | Update golang dependencies ... (check-in: b138cefd04 user: stern tags: trunk) | |
2024-12-13
| ||
14:01 | Increase version to 0.20.0-dev to begin next development cycle ... (check-in: 8d481c1e92 user: stern tags: trunk) | |
13:26 | Version 0.19.0 ... (check-in: bb751329ab user: stern tags: trunk, release, v0.19.0) | |
12:36 | Update to latest zettelstore client ... (check-in: 5ed6d8e8e3 user: stern tags: trunk) | |
Changes to VERSION.
|
| | | 1 | 0.20.0 |
Changes to ast/ast.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | // Package ast provides the abstract syntax tree for parsed zettel content. package ast import ( "net/url" | | | | | | | | | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // Package ast provides the abstract syntax tree for parsed zettel content. package ast import ( "net/url" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/zettel" ) // ZettelNode is the root node of the abstract syntax tree. // It is *not* part of the visitor pattern. type ZettelNode struct { Meta *meta.Meta // Original metadata Content zettel.Content // Original content Zid id.Zid // Zettel identification. InhMeta *meta.Meta // Metadata of the zettel, with inherited values. BlocksAST BlockSlice // Zettel abstract syntax tree is a sequence of block nodes. Syntax string // Syntax / parser that produced the Ast } // Node is the interface, all nodes must implement. type Node interface { WalkChildren(v Visitor) } |
︙ | ︙ |
Changes to ast/block.go.
︙ | ︙ | |||
76 77 78 79 80 81 82 | // VerbatimKind specifies the format that is applied to code inline nodes. type VerbatimKind int // Constants for VerbatimCode const ( _ VerbatimKind = iota VerbatimZettel // Zettel content | | | 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | // VerbatimKind specifies the format that is applied to code inline nodes. type VerbatimKind int // Constants for VerbatimCode const ( _ VerbatimKind = iota VerbatimZettel // Zettel content VerbatimCode // Program code VerbatimEval // Code to be externally interpreted. Syntax is stored in default attribute. VerbatimComment // Block comment VerbatimHTML // Block HTML, e.g. for Markdown VerbatimMath // Block math mode ) func (*VerbatimNode) blockNode() { /* Just a marker */ } |
︙ | ︙ | |||
249 250 251 252 253 254 255 | func (*TableNode) blockNode() { /* Just a marker */ } // WalkChildren walks down to the cells. func (tn *TableNode) WalkChildren(v Visitor) { if header := tn.Header; header != nil { for i := range header { | | > > > > > | | > | | | 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 | func (*TableNode) blockNode() { /* Just a marker */ } // WalkChildren walks down to the cells. func (tn *TableNode) WalkChildren(v Visitor) { if header := tn.Header; header != nil { for i := range header { Walk(v, header[i]) // Otherwise changes will not go back } } if rows := tn.Rows; rows != nil { for _, row := range rows { for i := range row { Walk(v, &row[i].Inlines) // Otherwise changes will not go back } } } } // WalkChildren walks the list of inline elements. func (cell *TableCell) WalkChildren(v Visitor) { Walk(v, &cell.Inlines) // Otherwise changes will not go back } //-------------------------------------------------------------------------- // TranscludeNode specifies block content from other zettel to embedded in // current zettel type TranscludeNode struct { Attrs attrs.Attributes Ref *Reference Inlines InlineSlice // Optional text. } func (*TranscludeNode) blockNode() { /* Just a marker */ } // WalkChildren walks the associated text. func (tn *TranscludeNode) WalkChildren(v Visitor) { Walk(v, &tn.Inlines) } //-------------------------------------------------------------------------- // BLOBNode contains just binary data that must be interpreted according to // a syntax. type BLOBNode struct { Description InlineSlice |
︙ | ︙ |
Changes to ast/inline.go.
︙ | ︙ | |||
22 23 24 25 26 27 28 | // InlineSlice is a list of BlockNodes. type InlineSlice []InlineNode func (*InlineSlice) inlineNode() { /* Just a marker */ } // WalkChildren walks down to the list. func (is *InlineSlice) WalkChildren(v Visitor) { | > | | > | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // InlineSlice is a list of BlockNodes. type InlineSlice []InlineNode func (*InlineSlice) inlineNode() { /* Just a marker */ } // WalkChildren walks down to the list. func (is *InlineSlice) WalkChildren(v Visitor) { if is != nil { for _, in := range *is { Walk(v, in) } } } // -------------------------------------------------------------------------- // TextNode just contains some text. type TextNode struct { |
︙ | ︙ | |||
193 194 195 196 197 198 199 | // LiteralKind specifies the format that is applied to code inline nodes. type LiteralKind int // Constants for LiteralCode const ( _ LiteralKind = iota | < | < | 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | // LiteralKind specifies the format that is applied to code inline nodes. type LiteralKind int // Constants for LiteralCode const ( _ LiteralKind = iota LiteralCode // Inline program code LiteralInput // Computer input, e.g. Keyboard strokes LiteralOutput // Computer output LiteralComment // Inline comment LiteralMath // Inline math mode ) func (*LiteralNode) inlineNode() { /* Just a marker */ } // WalkChildren does nothing. func (*LiteralNode) WalkChildren(Visitor) { /* No children*/ } |
Changes to ast/ref.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | package ast import ( "net/url" "strings" "t73f.de/r/zsc/api" | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package ast import ( "net/url" "strings" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" ) // QueryPrefix is the prefix that denotes a query expression. const QueryPrefix = api.QueryPrefix // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { |
︙ | ︙ | |||
75 76 77 78 79 80 81 82 83 84 | return RefStateHosted, path[1] == '/' } return RefStateInvalid, false } // String returns the string representation of a reference. func (r Reference) String() string { if r.URL != nil { return r.URL.String() } | > > > < < < | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | return RefStateHosted, path[1] == '/' } return RefStateInvalid, false } // String returns the string representation of a reference. func (r Reference) String() string { if r.State == RefStateQuery { return QueryPrefix + r.Value } if r.URL != nil { return r.URL.String() } return r.Value } // IsValid returns true if reference is valid func (r *Reference) IsValid() bool { return r.State != RefStateInvalid } // IsZettel returns true if it is a referencen to a local zettel. |
︙ | ︙ |
Added ast/sztrans/sztrans.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 | //----------------------------------------------------------------------------- // Copyright (c) 2025-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2025-present Detlef Stern //----------------------------------------------------------------------------- // Package sztrans allows to transform a sz representation of text into an // abstract syntax tree. package sztrans import ( "fmt" "log" "t73f.de/r/sx" "t73f.de/r/zsc/sz" "zettelstore.de/z/ast" ) type transformer struct{} // GetBlockSlice returns the sz representations as a AST BlockSlice func GetBlockSlice(pair *sx.Pair) (ast.BlockSlice, error) { if pair == nil { return nil, nil } var t transformer if obj := sz.Walk(&t, pair, nil); !obj.IsNil() { if sxn, isNode := obj.(sxNode); isNode { if bs, ok := sxn.node.(*ast.BlockSlice); ok { return *bs, nil } return nil, fmt.Errorf("no BlockSlice AST: %T/%v for %v", sxn.node, sxn.node, pair) } return nil, fmt.Errorf("no AST for %v: %v", pair, obj) } return nil, fmt.Errorf("error walking %v", pair) } func (t *transformer) VisitBefore(pair *sx.Pair, _ *sx.Pair) (sx.Object, bool) { if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol { switch sym { case sz.SymText: if p := pair.Tail(); p != nil { if s, isString := sx.GetString(p.Car()); isString { return sxNode{&ast.TextNode{Text: s.GetValue()}}, true } } case sz.SymSoft: return sxNode{&ast.BreakNode{Hard: false}}, true case sz.SymHard: return sxNode{&ast.BreakNode{Hard: true}}, true case sz.SymLiteralCode: return handleLiteral(ast.LiteralCode, pair.Tail()) case sz.SymLiteralComment: return handleLiteral(ast.LiteralComment, pair.Tail()) case sz.SymLiteralInput: return handleLiteral(ast.LiteralInput, pair.Tail()) case sz.SymLiteralMath: return handleLiteral(ast.LiteralMath, pair.Tail()) case sz.SymLiteralOutput: return handleLiteral(ast.LiteralOutput, pair.Tail()) case sz.SymThematic: return sxNode{&ast.HRuleNode{Attrs: sz.GetAttributes(pair.Tail().Head())}}, true case sz.SymVerbatimComment: return handleVerbatim(ast.VerbatimComment, pair.Tail()) case sz.SymVerbatimEval: return handleVerbatim(ast.VerbatimEval, pair.Tail()) case sz.SymVerbatimHTML: return handleVerbatim(ast.VerbatimHTML, pair.Tail()) case sz.SymVerbatimMath: return handleVerbatim(ast.VerbatimMath, pair.Tail()) case sz.SymVerbatimCode: return handleVerbatim(ast.VerbatimCode, pair.Tail()) case sz.SymVerbatimZettel: return handleVerbatim(ast.VerbatimZettel, pair.Tail()) } } return sx.Nil(), false } func handleLiteral(kind ast.LiteralKind, rest *sx.Pair) (sx.Object, bool) { if rest != nil { attrs := sz.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if s, isString := sx.GetString(curr.Car()); isString { return sxNode{&ast.LiteralNode{ Kind: kind, Attrs: attrs, Content: []byte(s.GetValue())}}, true } } } return nil, false } func handleVerbatim(kind ast.VerbatimKind, rest *sx.Pair) (sx.Object, bool) { if rest != nil { attrs := sz.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if s, isString := sx.GetString(curr.Car()); isString { return sxNode{&ast.VerbatimNode{ Kind: kind, Attrs: attrs, Content: []byte(s.GetValue()), }}, true } } } return nil, false } func (t *transformer) VisitAfter(pair *sx.Pair, _ *sx.Pair) sx.Object { if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol { switch sym { case sz.SymBlock: bns := collectBlocks(pair.Tail()) return sxNode{&bns} case sz.SymPara: return sxNode{&ast.ParaNode{Inlines: collectInlines(pair.Tail())}} case sz.SymHeading: return handleHeading(pair.Tail()) case sz.SymListOrdered: return sxNode{&ast.NestedListNode{ Kind: ast.NestedListOrdered, Items: collectItemSlices(pair.Tail()), Attrs: nil}} case sz.SymListUnordered: return sxNode{&ast.NestedListNode{ Kind: ast.NestedListUnordered, Items: collectItemSlices(pair.Tail()), Attrs: nil}} case sz.SymListQuote: return sxNode{&ast.NestedListNode{ Kind: ast.NestedListQuote, Items: collectItemSlices(pair.Tail()), Attrs: nil}} case sz.SymDescription: return handleDescription(pair.Tail()) case sz.SymTable: return handleTable(pair.Tail()) case sz.SymCell: return handleCell(ast.AlignDefault, pair.Tail()) case sz.SymCellCenter: return handleCell(ast.AlignCenter, pair.Tail()) case sz.SymCellLeft: return handleCell(ast.AlignLeft, pair.Tail()) case sz.SymCellRight: return handleCell(ast.AlignRight, pair.Tail()) case sz.SymRegionBlock: return handleRegion(ast.RegionSpan, pair.Tail()) case sz.SymRegionQuote: return handleRegion(ast.RegionQuote, pair.Tail()) case sz.SymRegionVerse: return handleRegion(ast.RegionVerse, pair.Tail()) case sz.SymTransclude: return handleTransclude(pair.Tail()) case sz.SymLinkHosted: return handleLink(ast.RefStateHosted, pair.Tail()) case sz.SymLinkInvalid: return handleLink(ast.RefStateInvalid, pair.Tail()) case sz.SymLinkZettel: return handleLink(ast.RefStateZettel, pair.Tail()) case sz.SymLinkSelf: return handleLink(ast.RefStateSelf, pair.Tail()) case sz.SymLinkFound: return handleLink(ast.RefStateFound, pair.Tail()) case sz.SymLinkBroken: return handleLink(ast.RefStateBroken, pair.Tail()) case sz.SymLinkHosted: return handleLink(ast.RefStateHosted, pair.Tail()) case sz.SymLinkBased: return handleLink(ast.RefStateBased, pair.Tail()) case sz.SymLinkQuery: return handleLink(ast.RefStateQuery, pair.Tail()) case sz.SymLinkExternal: return handleLink(ast.RefStateExternal, pair.Tail()) case sz.SymEmbed: return handleEmbed(pair.Tail()) case sz.SymCite: return handleCite(pair.Tail()) case sz.SymMark: return handleMark(pair.Tail()) case sz.SymEndnote: return handleEndnote(pair.Tail()) case sz.SymFormatDelete: return handleFormat(ast.FormatDelete, pair.Tail()) case sz.SymFormatEmph: return handleFormat(ast.FormatEmph, pair.Tail()) case sz.SymFormatInsert: return handleFormat(ast.FormatInsert, pair.Tail()) case sz.SymFormatMark: return handleFormat(ast.FormatMark, pair.Tail()) case sz.SymFormatQuote: return handleFormat(ast.FormatQuote, pair.Tail()) case sz.SymFormatSpan: return handleFormat(ast.FormatSpan, pair.Tail()) case sz.SymFormatSub: return handleFormat(ast.FormatSub, pair.Tail()) case sz.SymFormatSuper: return handleFormat(ast.FormatSuper, pair.Tail()) case sz.SymFormatStrong: return handleFormat(ast.FormatStrong, pair.Tail()) } log.Println("MISS", pair) } return pair } func collectBlocks(lst *sx.Pair) (result ast.BlockSlice) { for val := range lst.Values() { if sxn, isNode := val.(sxNode); isNode { if bn, isInline := sxn.node.(ast.BlockNode); isInline { result = append(result, bn) } } } return result } func collectInlines(lst *sx.Pair) (result ast.InlineSlice) { for val := range lst.Values() { if sxn, isNode := val.(sxNode); isNode { if in, isInline := sxn.node.(ast.InlineNode); isInline { result = append(result, in) } } } return result } func handleHeading(rest *sx.Pair) sx.Object { if rest != nil { if num, isNumber := rest.Car().(sx.Int64); isNumber && num > 0 && num < 6 { if curr := rest.Tail(); curr != nil { attrs := sz.GetAttributes(curr.Head()) if curr = curr.Tail(); curr != nil { if sSlug, isSlug := sx.GetString(curr.Car()); isSlug { if curr = curr.Tail(); curr != nil { if sUniq, isUniq := sx.GetString(curr.Car()); isUniq { return sxNode{&ast.HeadingNode{ Level: int(num), Attrs: attrs, Slug: sSlug.GetValue(), Fragment: sUniq.GetValue(), Inlines: collectInlines(curr.Tail()), }} } } } } } } } log.Println("HEAD", rest) return rest } func collectItemSlices(lst *sx.Pair) (result []ast.ItemSlice) { for val := range lst.Values() { if sxn, isNode := val.(sxNode); isNode { if bns, isBlockSlice := sxn.node.(*ast.BlockSlice); isBlockSlice { items := make(ast.ItemSlice, len(*bns)) for i, bn := range *bns { if it, ok := bn.(ast.ItemNode); ok { items[i] = it } } result = append(result, items) } if ins, isInline := sxn.node.(*ast.InlineSlice); isInline { items := make(ast.ItemSlice, len(*ins)) for i, bn := range *ins { if it, ok := bn.(ast.ItemNode); ok { items[i] = it } } result = append(result, items) } } } return result } func handleDescription(rest *sx.Pair) sx.Object { var descs []ast.Description for curr := rest; curr != nil; { term := collectInlines(curr.Head()) curr = curr.Tail() if curr == nil { descr := ast.Description{Term: term, Descriptions: nil} descs = append(descs, descr) break } car := curr.Car() if sx.IsNil(car) { descs = append(descs, ast.Description{Term: term, Descriptions: nil}) curr = curr.Tail() continue } sxn, isNode := car.(sxNode) if !isNode { descs = nil break } blocks, isBlocks := sxn.node.(*ast.BlockSlice) if !isBlocks { descs = nil break } descSlice := make([]ast.DescriptionSlice, 0, len(*blocks)) for _, bn := range *blocks { bns, isBns := bn.(*ast.BlockSlice) if !isBns { continue } ds := make(ast.DescriptionSlice, 0, len(*bns)) for _, b := range *bns { if defNode, isDef := b.(ast.DescriptionNode); isDef { ds = append(ds, defNode) } } descSlice = append(descSlice, ds) } descr := ast.Description{Term: term, Descriptions: descSlice} descs = append(descs, descr) curr = curr.Tail() } if len(descs) > 0 { return sxNode{&ast.DescriptionListNode{Descriptions: descs}} } log.Println("DESC", rest) return rest } func handleTable(rest *sx.Pair) sx.Object { if rest != nil { header := collectRow(rest.Head()) cols := len(header) var rows []ast.TableRow for curr := range rest.Tail().Pairs() { row := collectRow(curr.Head()) rows = append(rows, row) cols = max(cols, len(row)) } align := make([]ast.Alignment, cols) for i := range cols { align[i] = ast.AlignDefault } return sxNode{&ast.TableNode{ Header: header, Align: align, Rows: rows, }} } log.Println("TABL", rest) return rest } func collectRow(lst *sx.Pair) (row ast.TableRow) { for curr := range lst.Values() { if sxn, isNode := curr.(sxNode); isNode { if cell, isCell := sxn.node.(*ast.TableCell); isCell { row = append(row, cell) } } } return row } func handleCell(align ast.Alignment, rest *sx.Pair) sx.Object { return sxNode{&ast.TableCell{ Align: align, Inlines: collectInlines(rest), }} } func handleRegion(kind ast.RegionKind, rest *sx.Pair) sx.Object { if rest != nil { attrs := sz.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if blockList := curr.Head(); blockList != nil { return sxNode{&ast.RegionNode{ Kind: kind, Attrs: attrs, Blocks: collectBlocks(blockList), Inlines: collectInlines(curr.Tail()), }} } } } log.Println("REGI", rest) return rest } func handleTransclude(rest *sx.Pair) sx.Object { if rest != nil { attrs := sz.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { ref := collectReference(curr.Head()) return sxNode{&ast.TranscludeNode{ Attrs: attrs, Ref: ref, Inlines: collectInlines(curr.Tail()), }} } } log.Println("TRAN", rest) return rest } func handleLink(state ast.RefState, rest *sx.Pair) sx.Object { if rest != nil { attrs := sz.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if sref, isString := sx.GetString(curr.Car()); isString { ref := ast.ParseReference(sref.GetValue()) ref.State = state ins := collectInlines(curr.Tail()) return sxNode{&ast.LinkNode{ Attrs: attrs, Ref: ref, Inlines: ins, }} } } } log.Println("LINK", state, rest) return rest } func handleEmbed(rest *sx.Pair) sx.Object { if rest != nil { attrs := sz.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if ref := collectReference(curr.Head()); ref != nil { if curr = curr.Tail(); curr != nil { if syntax, isString := sx.GetString(curr.Car()); isString { return sxNode{&ast.EmbedRefNode{ Attrs: attrs, Ref: ref, Syntax: syntax.GetValue(), Inlines: collectInlines(curr.Tail()), }} } } } } } log.Println("EMBE", rest) return rest } func collectReference(pair *sx.Pair) *ast.Reference { if pair != nil { if sym, isSymbol := sx.GetSymbol(pair.Car()); isSymbol { if next := pair.Tail(); next != nil { if sRef, isString := sx.GetString(next.Car()); isString { ref := ast.ParseReference(sRef.GetValue()) switch sym { case sz.SymRefStateInvalid: ref.State = ast.RefStateInvalid case sz.SymRefStateZettel: ref.State = ast.RefStateZettel case sz.SymRefStateSelf: ref.State = ast.RefStateSelf case sz.SymRefStateFound: ref.State = ast.RefStateFound case sz.SymRefStateBroken: ref.State = ast.RefStateBroken case sz.SymRefStateHosted: ref.State = ast.RefStateHosted case sz.SymRefStateBased: ref.State = ast.RefStateBased case sz.SymRefStateQuery: ref.State = ast.RefStateQuery case sz.SymRefStateExternal: ref.State = ast.RefStateExternal } return ref } } } } return nil } func handleCite(rest *sx.Pair) sx.Object { if rest != nil { attrs := sz.GetAttributes(rest.Head()) if curr := rest.Tail(); curr != nil { if sKey, isString := sx.GetString(curr.Car()); isString { return sxNode{&ast.CiteNode{ Attrs: attrs, Key: sKey.GetValue(), Inlines: collectInlines(curr.Tail()), }} } } } log.Println("CITE", rest) return rest } func handleMark(rest *sx.Pair) sx.Object { if rest != nil { if sMark, isMarkS := sx.GetString(rest.Car()); isMarkS { if curr := rest.Tail(); curr != nil { if sSlug, isSlug := sx.GetString(curr.Car()); isSlug { if curr = curr.Tail(); curr != nil { if sUniq, isUniq := sx.GetString(curr.Car()); isUniq { return sxNode{&ast.MarkNode{ Mark: sMark.GetValue(), Slug: sSlug.GetValue(), Fragment: sUniq.GetValue(), Inlines: collectInlines(curr.Tail()), }} } } } } } } log.Println("MARK", rest) return rest } func handleEndnote(rest *sx.Pair) sx.Object { if rest != nil { attrs := sz.GetAttributes(rest.Head()) return sxNode{&ast.FootnoteNode{ Attrs: attrs, Inlines: collectInlines(rest.Tail()), }} } log.Println("ENDN", rest) return rest } func handleFormat(kind ast.FormatKind, rest *sx.Pair) sx.Object { if rest != nil { attrs := sz.GetAttributes(rest.Head()) return sxNode{&ast.FormatNode{ Kind: kind, Attrs: attrs, Inlines: collectInlines(rest.Tail()), }} } log.Println("FORM", kind, rest) return rest } type sxNode struct { node ast.Node } func (sxNode) IsNil() bool { return false } func (sxNode) IsAtom() bool { return true } func (n sxNode) String() string { return fmt.Sprintf("%T/%v", n.node, n.node) } func (n sxNode) GoString() string { return n.String() } func (n sxNode) IsEqual(other sx.Object) bool { return n.String() == other.String() } |
Changes to ast/walk_test.go.
︙ | ︙ | |||
59 60 61 62 63 64 65 | &ast.LinkNode{ Ref: &ast.Reference{Value: "http://zettelstore.de"}, Inlines: ast.InlineSlice{&ast.TextNode{Text: "URL text."}}, }, ), } v := benchVisitor{} | | | | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | &ast.LinkNode{ Ref: &ast.Reference{Value: "http://zettelstore.de"}, Inlines: ast.InlineSlice{&ast.TextNode{Text: "URL text."}}, }, ), } v := benchVisitor{} for b.Loop() { ast.Walk(&v, &root) } } type benchVisitor struct{} func (bv *benchVisitor) Visit(ast.Node) ast.Visitor { return bv } |
Changes to auth/auth.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | // Package auth provides services for authentification / authorization. package auth import ( "time" | | | | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // Package auth provides services for authentification / authorization. package auth import ( "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/config" ) // BaseManager allows to check some base auth modes. type BaseManager interface { // IsReadonly returns true, if the systems is configured to run in read-only-mode. IsReadonly() bool } |
︙ | ︙ |
Changes to auth/cred/cred.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | // Package cred provides some function for handling credentials. package cred import ( "bytes" "golang.org/x/crypto/bcrypt" | | > | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // Package cred provides some function for handling credentials. package cred import ( "bytes" "golang.org/x/crypto/bcrypt" "t73f.de/r/zsc/domain/id" ) // HashCredential returns a hashed vesion of the given credential func HashCredential(zid id.Zid, ident, credential string) (string, error) { fullCredential := createFullCredential(zid, ident, credential) res, err := bcrypt.GenerateFromPassword(fullCredential, bcrypt.DefaultCost) if err != nil { |
︙ | ︙ |
Changes to auth/impl/impl.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | import ( "errors" "hash/fnv" "io" "time" "t73f.de/r/sx" | | > < < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import ( "errors" "hash/fnv" "io" "time" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sexp" "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" ) type myAuth struct { readonly bool owner id.Zid secret []byte } |
︙ | ︙ | |||
80 81 82 83 84 85 86 | var ErrOtherKind = errors.New("auth: wrong token kind") // ErrNoZid signals that the 'zid' key is missing. var ErrNoZid = errors.New("auth: missing zettel id") // GetToken returns a token to be used for authentification. func (a *myAuth) GetToken(ident *meta.Meta, d time.Duration, kind auth.TokenKind) ([]byte, error) { | | | | 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | var ErrOtherKind = errors.New("auth: wrong token kind") // ErrNoZid signals that the 'zid' key is missing. var ErrNoZid = errors.New("auth: missing zettel id") // GetToken returns a token to be used for authentification. func (a *myAuth) GetToken(ident *meta.Meta, d time.Duration, kind auth.TokenKind) ([]byte, error) { subject, ok := ident.Get(meta.KeyUserID) if !ok || subject == "" { return nil, ErrNoIdent } now := time.Now().Round(time.Second) sClaim := sx.MakeList( sx.Int64(kind), sx.MakeString(string(subject)), sx.Int64(now.Unix()), sx.Int64(now.Add(d).Unix()), sx.Int64(ident.Zid), ) return sign(sClaim, a.secret) } |
︙ | ︙ | |||
163 164 165 166 167 168 169 | return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } | | | | 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } if val, ok := user.Get(meta.KeyUserRole); ok { if ur := val.AsUserRole(); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } func (a *myAuth) BoxWithPolicy(unprotectedBox box.Box, rtConfig config.Config) (box.Box, auth.Policy) { return policy.BoxWithPolicy(a, unprotectedBox, rtConfig) } |
Changes to auth/policy/anon.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" | > < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" "zettelstore.de/z/config" ) type anonPolicy struct { authConfig config.AuthConfig pre auth.Policy } |
︙ | ︙ |
Changes to auth/policy/box.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 | //----------------------------------------------------------------------------- package policy import ( "context" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/query" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel" | > > > < < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //----------------------------------------------------------------------------- package policy import ( "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/query" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel" ) // BoxWithPolicy wraps the given box inside a policy box. func BoxWithPolicy(manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig) (box.Box, auth.Policy) { pol := newPolicy(manager, authConfig) return newBox(box, pol), pol } |
︙ | ︙ | |||
74 75 76 77 78 79 80 | return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) } func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { return pp.box.GetAllZettel(ctx, zid) } | | | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) } func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { return pp.box.GetAllZettel(ctx, zid) } func (pp *polBox) FetchZids(ctx context.Context) (*idset.Set, error) { return nil, box.NewErrNotAllowed("fetch-zids", server.GetUser(ctx), id.Invalid) } func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.box.GetMeta(ctx, zid) if err != nil { return nil, err |
︙ | ︙ |
Changes to auth/policy/default.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( | | < | | | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" ) type defaultPolicy struct { manager auth.AuthzManager } func (*defaultPolicy) CanCreate(_, _ *meta.Meta) bool { return true } func (*defaultPolicy) CanRead(_, _ *meta.Meta) bool { return true } func (d *defaultPolicy) CanWrite(user, oldMeta, _ *meta.Meta) bool { return d.canChange(user, oldMeta) } func (d *defaultPolicy) CanDelete(user, m *meta.Meta) bool { return d.canChange(user, m) } func (*defaultPolicy) CanRefresh(user *meta.Meta) bool { return user != nil } func (d *defaultPolicy) canChange(user, m *meta.Meta) bool { metaRo, ok := m.Get(meta.KeyReadOnly) if !ok { return true } if user == nil { // If we are here, there is no authentication. // See owner.go:CanWrite. // No authentication: check for owner-like restriction, because the user // acts as an owner return metaRo != meta.ValueUserRoleOwner && !metaRo.AsBool() } userRole := d.manager.GetUserRole(user) switch metaRo { case meta.ValueUserRoleReader: return userRole > meta.UserRoleReader case meta.ValueUserRoleWriter: return userRole > meta.UserRoleWriter case meta.ValueUserRoleOwner: return userRole > meta.UserRoleOwner } return !metaRo.AsBool() } |
Changes to auth/policy/owner.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( | | < | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" "zettelstore.de/z/config" ) type ownerPolicy struct { manager auth.AuthzManager authConfig config.AuthConfig pre auth.Policy } func (o *ownerPolicy) CanCreate(user, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanCreate(user, newMeta) { return false } return o.userIsOwner(user) || o.userCanCreate(user, newMeta) } func (o *ownerPolicy) userCanCreate(user, newMeta *meta.Meta) bool { if o.manager.GetUserRole(user) == meta.UserRoleReader { return false } if _, ok := newMeta.Get(meta.KeyUserID); ok { return false } return true } func (o *ownerPolicy) CanRead(user, m *meta.Meta) bool { // No need to call o.pre.CanRead(user, meta), because it will always return true. |
︙ | ︙ | |||
59 60 61 62 63 64 65 | return false case meta.VisibilityPublic: return true } if user == nil { return false } | | | | | | | | 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 | return false case meta.VisibilityPublic: return true } if user == nil { return false } if _, ok := m.Get(meta.KeyUserID); ok { // Only the user can read its own zettel return user.Zid == m.Zid } switch o.manager.GetUserRole(user) { case meta.UserRoleReader, meta.UserRoleWriter, meta.UserRoleOwner: return true case meta.UserRoleCreator: return vis == meta.VisibilityCreator default: return false } } var noChangeUser = []string{ meta.KeyID, meta.KeyRole, meta.KeyUserID, meta.KeyUserRole, } func (o *ownerPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { if user == nil || !o.pre.CanWrite(user, oldMeta, newMeta) { return false } vis := o.authConfig.GetVisibility(oldMeta) if res, ok := o.checkVisibility(user, vis); ok { return res } if o.userIsOwner(user) { return true } if !o.userCanRead(user, oldMeta, vis) { return false } if _, ok := oldMeta.Get(meta.KeyUserID); ok { // Here we know, that user.Zid == newMeta.Zid (because of userCanRead) and // user.Zid == newMeta.Zid (because oldMeta.Zid == newMeta.Zid) for _, key := range noChangeUser { if oldMeta.GetDefault(key, "") != newMeta.GetDefault(key, "") { return false } } |
︙ | ︙ | |||
145 146 147 148 149 150 151 | func (o *ownerPolicy) userIsOwner(user *meta.Meta) bool { if user == nil { return false } if o.manager.IsOwner(user.Zid) { return true } | | | 144 145 146 147 148 149 150 151 152 153 154 155 | func (o *ownerPolicy) userIsOwner(user *meta.Meta) bool { if user == nil { return false } if o.manager.IsOwner(user.Zid) { return true } if val, ok := user.Get(meta.KeyUserRole); ok && val == meta.ValueUserRoleOwner { return true } return false } |
Changes to auth/policy/policy.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" | > < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" "zettelstore.de/z/config" ) // newPolicy creates a policy based on given constraints. func newPolicy(manager auth.AuthzManager, authConfig config.AuthConfig) auth.Policy { var pol auth.Policy if manager.IsReadonly() { pol = &roPolicy{} |
︙ | ︙ |
Changes to auth/policy/policy_test.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | package policy import ( "fmt" "testing" | | | | < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | package policy import ( "fmt" "testing" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" ) func TestPolicies(t *testing.T) { t.Parallel() testScene := []struct { readonly bool withAuth bool |
︙ | ︙ | |||
82 83 84 85 86 87 88 | return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } | | | | | | 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 | return meta.UserRoleUnknown } return meta.UserRoleOwner } if a.IsOwner(user.Zid) { return meta.UserRoleOwner } if val, ok := user.Get(meta.KeyUserRole); ok { if ur := val.AsUserRole(); ur != meta.UserRoleUnknown { return ur } } return meta.UserRoleReader } type authConfig struct{ simple, expert bool } func (ac *authConfig) GetSimpleMode() bool { return ac.simple } func (ac *authConfig) GetExpertMode() bool { return ac.expert } func (*authConfig) GetVisibility(m *meta.Meta) meta.Visibility { if val, ok := m.Get(meta.KeyVisibility); ok { return val.AsVisibility() } return meta.VisibilityLogin } func testCreate(t *testing.T, pol auth.Policy, withAuth, readonly bool) { t.Helper() anonUser := newAnon() |
︙ | ︙ | |||
259 260 261 262 263 264 265 | zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() userZettel := newUserZettel() writerNew := writer.Clone() | | | 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | zettel := newZettel() publicZettel := newPublicZettel() loginZettel := newLoginZettel() ownerZettel := newOwnerZettel() expertZettel := newExpertZettel() userZettel := newUserZettel() writerNew := writer.Clone() writerNew.Set(meta.KeyUserRole, owner.GetDefault(meta.KeyUserRole, "")) roFalse := newRoFalseZettel() roTrue := newRoTrueZettel() roReader := newRoReaderZettel() roWriter := newRoWriterZettel() roOwner := newRoOwnerZettel() notAuthNotReadonly := !withAuth && !readonly testCases := []struct { |
︙ | ︙ | |||
521 522 523 524 525 526 527 | visZid = id.Zid(1023) userZid = id.Zid(1025) ) func newAnon() *meta.Meta { return nil } func newCreator() *meta.Meta { user := meta.New(creatorZid) | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 | visZid = id.Zid(1023) userZid = id.Zid(1025) ) func newAnon() *meta.Meta { return nil } func newCreator() *meta.Meta { user := meta.New(creatorZid) user.Set(meta.KeyTitle, "Creator") user.Set(meta.KeyUserID, "ceator") user.Set(meta.KeyUserRole, meta.ValueUserRoleCreator) return user } func newReader() *meta.Meta { user := meta.New(readerZid) user.Set(meta.KeyTitle, "Reader") user.Set(meta.KeyUserID, "reader") user.Set(meta.KeyUserRole, meta.ValueUserRoleReader) return user } func newWriter() *meta.Meta { user := meta.New(writerZid) user.Set(meta.KeyTitle, "Writer") user.Set(meta.KeyUserID, "writer") user.Set(meta.KeyUserRole, meta.ValueUserRoleWriter) return user } func newOwner() *meta.Meta { user := meta.New(ownerZid) user.Set(meta.KeyTitle, "Owner") user.Set(meta.KeyUserID, "owner") user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner) return user } func newOwner2() *meta.Meta { user := meta.New(owner2Zid) user.Set(meta.KeyTitle, "Owner 2") user.Set(meta.KeyUserID, "owner-2") user.Set(meta.KeyUserRole, meta.ValueUserRoleOwner) return user } func newZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "Any Zettel") return m } func newPublicZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Public Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityPublic) return m } func newCreatorZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Creator Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityCreator) return m } func newLoginZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Login Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func newOwnerZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Owner Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityOwner) return m } func newExpertZettel() *meta.Meta { m := meta.New(visZid) m.Set(meta.KeyTitle, "Expert Zettel") m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) return m } func newRoFalseZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "No r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueFalse) return m } func newRoTrueZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "A r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueTrue) return m } func newRoReaderZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "Reader r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueUserRoleReader) return m } func newRoWriterZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "Writer r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueUserRoleWriter) return m } func newRoOwnerZettel() *meta.Meta { m := meta.New(zettelZid) m.Set(meta.KeyTitle, "Owner r/o Zettel") m.Set(meta.KeyReadOnly, meta.ValueUserRoleOwner) return m } func newUserZettel() *meta.Meta { m := meta.New(userZid) m.Set(meta.KeyTitle, "Any User") m.Set(meta.KeyUserID, "any") return m } |
Changes to auth/policy/readonly.go.
︙ | ︙ | |||
9 10 11 12 13 14 15 | // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import "t73f.de/r/zsc/domain/meta" type roPolicy struct{} func (*roPolicy) CanCreate(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanRead(_, _ *meta.Meta) bool { return true } func (*roPolicy) CanWrite(_, _, _ *meta.Meta) bool { return false } func (*roPolicy) CanDelete(_, _ *meta.Meta) bool { return false } |
︙ | ︙ |
Changes to box/box.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | import ( "context" "errors" "fmt" "io" "time" | | | | | | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import ( "context" "errors" "fmt" "io" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/query" "zettelstore.de/z/zettel" ) // BaseBox is implemented by all Zettel boxes. type BaseBox interface { // Location returns some information where the box is located. // Format is dependent of the box. Location() string |
︙ | ︙ | |||
128 129 130 131 132 133 134 | // Box is to be used outside the box package and its descendants. type Box interface { BaseBox WriteBox // FetchZids returns the set of all zettel identifer managed by the box. | | | 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | // Box is to be used outside the box package and its descendants. type Box interface { BaseBox WriteBox // FetchZids returns the set of all zettel identifer managed by the box. FetchZids(ctx context.Context) (*idset.Set, error) // GetMeta returns the metadata of the zettel with the given identifier. GetMeta(context.Context, id.Zid) (*meta.Meta, error) // SelectMeta returns a list of metadata that comply to the given selection criteria. // If `metaSeq` is `nil`, the box assumes metadata of all available zettel. SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) |
︙ | ︙ | |||
286 287 288 289 290 291 292 | err.Op, err.Zid) } return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op) } if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for user %v/%v", | | | | 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 | err.Op, err.Zid) } return fmt.Sprintf("operation %q not allowed for not authorized user", err.Op) } if err.Zid.IsValid() { return fmt.Sprintf( "operation %q on zettel %v not allowed for user %v/%v", err.Op, err.Zid, err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid) } return fmt.Sprintf( "operation %q not allowed for user %v/%v", err.Op, err.User.GetDefault(meta.KeyUserID, "?"), err.User.Zid) } // Is return true, if the error is of type ErrNotAllowed. func (*ErrNotAllowed) Is(error) bool { return true } // ErrStarted is returned when trying to start an already started box. var ErrStarted = errors.New("box is already started") |
︙ | ︙ |
Changes to box/compbox/compbox.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" | | > < < | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" ) func init() { manager.Register( " comp", func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return getCompBox(cdata.Number, cdata.Enricher), nil |
︙ | ︙ | |||
44 45 46 47 48 49 50 | } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta content func(context.Context, *compBox) []byte }{ | | | | | | | | | | | | | | | | | | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta content func(context.Context, *compBox) []byte }{ id.ZidVersion: {genVersionBuildM, genVersionBuildC}, id.ZidHost: {genVersionHostM, genVersionHostC}, id.ZidOperatingSystem: {genVersionOSM, genVersionOSC}, id.ZidLog: {genLogM, genLogC}, id.ZidMemory: {genMemoryM, genMemoryC}, id.ZidSx: {genSxM, genSxC}, // id.ZidHTTP: {genHttpM, genHttpC}, // id.ZidAPI: {genApiM, genApiC}, // id.ZidWebUI: {genWebUiM, genWebUiC}, // id.ZidConsole: {genConsoleM, genConsoleC}, id.ZidBoxManager: {genManagerM, genManagerC}, // id.ZidIndex: {genIndexM, genIndexC}, // id.ZidQuery: {genQueryM, genQueryC}, id.ZidMetadataKey: {genKeysM, genKeysC}, id.ZidParser: {genParserM, genParserC}, id.ZidStartupConfiguration: {genConfigZettelM, genConfigZettelC}, } // Get returns the one program box. func getCompBox(boxNumber int, mf box.Enricher) *compBox { return &compBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "comp").Int("boxnum", int64(boxNumber)).Child(), |
︙ | ︙ | |||
154 155 156 157 158 159 160 | st.ReadOnly = true st.Zettel = len(myZettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } func getTitledMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) | | | | | | | | | | | | 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 | st.ReadOnly = true st.Zettel = len(myZettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } func getTitledMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) m.Set(meta.KeyTitle, meta.Value(title)) return m } func updateMeta(m *meta.Meta) { if _, ok := m.Get(meta.KeySyntax); !ok { m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) } m.Set(meta.KeyRole, meta.ValueRoleConfiguration) if _, ok := m.Get(meta.KeyCreated); !ok { m.Set(meta.KeyCreated, meta.Value(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))) } m.Set(meta.KeyLang, meta.ValueLangEN) m.Set(meta.KeyReadOnly, meta.ValueTrue) if _, ok := m.Get(meta.KeyVisibility); !ok { m.Set(meta.KeyVisibility, meta.ValueVisibilityExpert) } } |
Changes to box/compbox/config.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | package compbox import ( "bytes" "context" | | | > | | > | | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | package compbox import ( "bytes" "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myConfig == nil { return nil } return getTitledMeta(zid, "Zettelstore Startup Configuration") } func genConfigZettelC(context.Context, *compBox) []byte { var buf bytes.Buffer second := false for key, val := range myConfig.All() { if second { buf.WriteByte('\n') } second = true buf.WriteString("; ''") buf.WriteString(key) buf.WriteString("''") if val != "" { buf.WriteString("\n: ``") for _, r := range val { if r == '`' { buf.WriteByte('\\') } buf.WriteRune(r) } buf.WriteString("``") } |
︙ | ︙ |
Changes to box/compbox/keys.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | package compbox import ( "bytes" "context" "fmt" | | | | < | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | package compbox import ( "bytes" "context" "fmt" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/kernel" ) func genKeysM(zid id.Zid) *meta.Meta { m := getTitledMeta(zid, "Zettelstore Supported Metadata Keys") m.Set(meta.KeyCreated, meta.Value(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))) m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func genKeysC(context.Context, *compBox) []byte { keys := meta.GetSortedKeyDescriptions() var buf bytes.Buffer buf.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") |
︙ | ︙ |
Changes to box/compbox/log.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | package compbox import ( "bytes" "context" | | | | < | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | package compbox import ( "bytes" "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/kernel" ) func genLogM(zid id.Zid) *meta.Meta { m := getTitledMeta(zid, "Zettelstore Log") m.Set(meta.KeySyntax, meta.ValueSyntaxText) m.Set(meta.KeyModified, meta.Value(kernel.Main.GetLastLogTime().Local().Format(id.TimestampLayout))) return m } func genLogC(context.Context, *compBox) []byte { const tsFormat = "2006-01-02 15:04:05.999999" entries := kernel.Main.RetrieveLogEntries() var buf bytes.Buffer |
︙ | ︙ |
Changes to box/compbox/manager.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | package compbox import ( "bytes" "context" "fmt" | | | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package compbox import ( "bytes" "context" "fmt" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/kernel" ) func genManagerM(zid id.Zid) *meta.Meta { return getTitledMeta(zid, "Zettelstore Box Manager") } func genManagerC(context.Context, *compBox) []byte { |
︙ | ︙ |
Changes to box/compbox/memory.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | import ( "bytes" "context" "fmt" "os" "runtime" | | | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import ( "bytes" "context" "fmt" "os" "runtime" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/kernel" ) func genMemoryM(zid id.Zid) *meta.Meta { return getTitledMeta(zid, "Zettelstore Memory") } func genMemoryC(context.Context, *compBox) []byte { |
︙ | ︙ |
Changes to box/compbox/parser.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | import ( "bytes" "context" "fmt" "slices" "strings" | | > < < | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | import ( "bytes" "context" "fmt" "slices" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" ) func genParserM(zid id.Zid) *meta.Meta { m := getTitledMeta(zid, "Zettelstore Supported Parser") m.Set(meta.KeyCreated, meta.Value(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))) m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func genParserC(context.Context, *compBox) []byte { var buf bytes.Buffer buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Text Format?:|=Image Format?:\n") syntaxes := parser.GetSyntaxes() |
︙ | ︙ |
Changes to box/compbox/sx.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | import ( "bytes" "context" "fmt" "t73f.de/r/sx" | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import ( "bytes" "context" "fmt" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" ) func genSxM(zid id.Zid) *meta.Meta { return getTitledMeta(zid, "Zettelstore Sx Engine") } func genSxC(context.Context, *compBox) []byte { |
︙ | ︙ |
Changes to box/compbox/version.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package compbox import ( "context" | | | | < | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //----------------------------------------------------------------------------- package compbox import ( "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/kernel" ) func genVersionBuildM(zid id.Zid) *meta.Meta { m := getTitledMeta(zid, "Zettelstore Version") m.Set(meta.KeyCreated, meta.Value(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))) m.Set(meta.KeyVisibility, meta.ValueVisibilityLogin) return m } func genVersionBuildC(context.Context, *compBox) []byte { return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) } func genVersionHostM(zid id.Zid) *meta.Meta { |
︙ | ︙ |
Changes to box/constbox/base.sxn.
︙ | ︙ | |||
36 37 38 39 40 41 42 | `((a (@ (href ,login-url)) "Login")) ) ))) ) (div (@ (class "zs-dropdown")) (button "Lists") (nav (@ (class "zs-dropdown-content")) | < < | | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | `((a (@ (href ,login-url)) "Login")) ) ))) ) (div (@ (class "zs-dropdown")) (button "Lists") (nav (@ (class "zs-dropdown-content")) ,@list-urls ,@(if (bound? 'refresh-url) `((a (@ (href ,refresh-url)) "Refresh"))) )) ,@(if new-zettel-links `((div (@ (class "zs-dropdown")) (button "New") (nav (@ (class "zs-dropdown-content")) ,@(map wui-link new-zettel-links) |
︙ | ︙ |
Changes to box/constbox/constbox.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | package constbox import ( "context" _ "embed" // Allow to embed file content "net/url" | | > < < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | package constbox import ( "context" _ "embed" // Allow to embed file content "net/url" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" ) func init() { manager.Register( " const", func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &constBox{ |
︙ | ︙ | |||
112 113 114 115 116 117 118 | func (cb *constBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(cb.zettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } var constZettelMap = map[id.Zid]constZettel{ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | > > > > > > > > > > | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | > | 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 | func (cb *constBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(cb.zettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } var constZettelMap = map[id.Zid]constZettel{ id.ZidConfiguration: { constHeader{ meta.KeyTitle: "Zettelstore Runtime Configuration", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxNone, meta.KeyCreated: "20200804111624", meta.KeyVisibility: meta.ValueVisibilityOwner, }, zettel.NewContent(nil)}, id.ZidLicense: { constHeader{ meta.KeyTitle: "Zettelstore License", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxText, meta.KeyCreated: "20210504135842", meta.KeyLang: meta.ValueLangEN, meta.KeyModified: "20220131153422", meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityPublic, }, zettel.NewContent(contentLicense)}, id.ZidAuthors: { constHeader{ meta.KeyTitle: "Zettelstore Contributors", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20210504135842", meta.KeyLang: meta.ValueLangEN, meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityLogin, }, zettel.NewContent(contentContributors)}, id.ZidDependencies: { constHeader{ meta.KeyTitle: "Zettelstore Dependencies", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityPublic, meta.KeyCreated: "20210504135842", meta.KeyModified: "20250212202400", }, zettel.NewContent(contentDependencies)}, id.ZidBaseTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Base HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230510155100", meta.KeyModified: "20241227212000", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentBaseSxn)}, id.ZidLoginTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Login Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", meta.KeyModified: "20240219145200", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentLoginSxn)}, id.ZidZettelTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Zettel HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230510155300", meta.KeyModified: "20250214153100", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentZettelSxn)}, id.ZidInfoTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Info HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", meta.KeyModified: "20241127170500", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentInfoSxn)}, id.ZidFormTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Form HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", meta.KeyModified: "20240219145200", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentFormSxn)}, id.ZidDeleteTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Delete HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20200804111624", meta.KeyModified: "20241127170530", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentDeleteSxn)}, id.ZidListTemplate: { constHeader{ meta.KeyTitle: "Zettelstore List Zettel HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230704122100", meta.KeyModified: "20240219145200", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentListZettelSxn)}, id.ZidErrorTemplate: { constHeader{ meta.KeyTitle: "Zettelstore Error HTML Template", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20210305133215", meta.KeyModified: "20240219145200", meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentErrorSxn)}, id.ZidSxnStart: { constHeader{ meta.KeyTitle: "Zettelstore Sxn Start Code", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230824160700", meta.KeyModified: "20240219145200", meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeyPrecursor: id.ZidSxnBase.String(), }, zettel.NewContent(contentStartCodeSxn)}, id.ZidSxnBase: { constHeader{ meta.KeyTitle: "Zettelstore Sxn Base Code", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20230619132800", meta.KeyModified: "20241118173500", meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, meta.KeyPrecursor: id.ZidSxnPrelude.String(), }, zettel.NewContent(contentBaseCodeSxn)}, id.ZidSxnPrelude: { constHeader{ meta.KeyTitle: "Zettelstore Sxn Prelude", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxSxn, meta.KeyCreated: "20231006181700", meta.KeyModified: "20240222121200", meta.KeyReadOnly: meta.ValueTrue, meta.KeyVisibility: meta.ValueVisibilityExpert, }, zettel.NewContent(contentPreludeSxn)}, id.ZidBaseCSS: { constHeader{ meta.KeyTitle: "Zettelstore Base CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxCSS, meta.KeyCreated: "20200804111624", meta.KeyModified: "20240827143500", meta.KeyVisibility: meta.ValueVisibilityPublic, }, zettel.NewContent(contentBaseCSS)}, id.ZidUserCSS: { constHeader{ meta.KeyTitle: "Zettelstore User CSS", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxCSS, meta.KeyCreated: "20210622110143", meta.KeyVisibility: meta.ValueVisibilityPublic, }, zettel.NewContent([]byte("/* User-defined CSS */"))}, id.ZidEmoji: { constHeader{ meta.KeyTitle: "Zettelstore Generic Emoji", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxGif, meta.KeyReadOnly: meta.ValueTrue, meta.KeyCreated: "20210504175807", meta.KeyVisibility: meta.ValueVisibilityPublic, }, zettel.NewContent(contentEmoji)}, id.ZidTOCListsMenu: { constHeader{ meta.KeyTitle: "Lists Menu", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyCreated: "20241223205400", meta.KeyVisibility: meta.ValueVisibilityPublic, }, zettel.NewContent(contentMenuListsZettel)}, id.ZidTOCNewTemplate: { constHeader{ meta.KeyTitle: "New Menu", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyCreated: "20210217161829", meta.KeyModified: "20231129111800", meta.KeyVisibility: meta.ValueVisibilityCreator, }, zettel.NewContent(contentMenuNewZettel)}, id.ZidTemplateNewZettel: { constHeader{ meta.KeyTitle: "New Zettel", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20201028185209", meta.KeyModified: "20230929132900", meta.NewPrefix + meta.KeyRole: meta.ValueRoleZettel, meta.KeyVisibility: meta.ValueVisibilityCreator, }, zettel.NewContent(nil)}, id.ZidTemplateNewRole: { constHeader{ meta.KeyTitle: "New Role", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20231129110800", meta.NewPrefix + meta.KeyRole: meta.ValueRoleRole, meta.NewPrefix + meta.KeyTitle: "", meta.KeyVisibility: meta.ValueVisibilityCreator, }, zettel.NewContent(nil)}, id.ZidTemplateNewTag: { constHeader{ meta.KeyTitle: "New Tag", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20230929132400", meta.NewPrefix + meta.KeyRole: meta.ValueRoleTag, meta.NewPrefix + meta.KeyTitle: "#", meta.KeyVisibility: meta.ValueVisibilityCreator, }, zettel.NewContent(nil)}, id.ZidTemplateNewUser: { constHeader{ meta.KeyTitle: "New User", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxNone, meta.KeyCreated: "20201028185209", meta.NewPrefix + meta.KeyCredential: "", meta.NewPrefix + meta.KeyUserID: "", meta.NewPrefix + meta.KeyUserRole: meta.ValueUserRoleReader, meta.KeyVisibility: meta.ValueVisibilityOwner, }, zettel.NewContent(nil)}, id.ZidRoleZettelZettel: { constHeader{ meta.KeyTitle: meta.ValueRoleZettel, meta.KeyRole: meta.ValueRoleRole, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20231129161400", meta.KeyLang: meta.ValueLangEN, meta.KeyVisibility: meta.ValueVisibilityLogin, }, zettel.NewContent(contentRoleZettel)}, id.ZidRoleConfigurationZettel: { constHeader{ meta.KeyTitle: meta.ValueRoleConfiguration, meta.KeyRole: meta.ValueRoleRole, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20241213103100", meta.KeyLang: meta.ValueLangEN, meta.KeyVisibility: meta.ValueVisibilityLogin, }, zettel.NewContent(contentRoleConfiguration)}, id.ZidRoleRoleZettel: { constHeader{ meta.KeyTitle: meta.ValueRoleRole, meta.KeyRole: meta.ValueRoleRole, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20231129162900", meta.KeyLang: meta.ValueLangEN, meta.KeyVisibility: meta.ValueVisibilityLogin, }, zettel.NewContent(contentRoleRole)}, id.ZidRoleTagZettel: { constHeader{ meta.KeyTitle: meta.ValueRoleTag, meta.KeyRole: meta.ValueRoleRole, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyCreated: "20231129162000", meta.KeyLang: meta.ValueLangEN, meta.KeyVisibility: meta.ValueVisibilityLogin, }, zettel.NewContent(contentRoleTag)}, id.ZidAppDirectory: { constHeader{ meta.KeyTitle: "Zettelstore Application Directory", meta.KeyRole: meta.ValueRoleConfiguration, meta.KeySyntax: meta.ValueSyntaxNone, meta.KeyLang: meta.ValueLangEN, meta.KeyCreated: "20240703235900", meta.KeyVisibility: meta.ValueVisibilityLogin, }, zettel.NewContent(nil)}, id.ZidDefaultHome: { constHeader{ meta.KeyTitle: "Home", meta.KeyRole: meta.ValueRoleZettel, meta.KeySyntax: meta.ValueSyntaxZmk, meta.KeyLang: meta.ValueLangEN, meta.KeyCreated: "20210210190757", meta.KeyModified: "20241216105800", }, zettel.NewContent(contentHomeZettel)}, } //go:embed license.txt var contentLicense []byte |
︙ | ︙ | |||
464 465 466 467 468 469 470 | //go:embed base.css var contentBaseCSS []byte //go:embed emoji_spin.gif var contentEmoji []byte | > > > | | | 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 | //go:embed base.css var contentBaseCSS []byte //go:embed emoji_spin.gif var contentEmoji []byte //go:embed menu_lists.zettel var contentMenuListsZettel []byte //go:embed menu_new.zettel var contentMenuNewZettel []byte //go:embed rolezettel.zettel var contentRoleZettel []byte //go:embed roleconfiguration.zettel var contentRoleConfiguration []byte |
︙ | ︙ |
Changes to box/constbox/dependencies.zettel.
︙ | ︙ | |||
126 127 128 129 130 131 132 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` | | > > | 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` === Sx, SxWebs, Webs, Zero, Zettelstore-Client These are companion projects, written by the main developer of Zettelstore. They are published under the same license, [[EUPL v1.2, or later|00000000000004]]. ; URL & Source Sx : [[https://t73f.de/r/sx]] ; URL & Source SxWebs : [[https://t73f.de/r/sxwebs]] ; URL & Source Webs : [[https://t73f.de/r/webs]] ; URL & Source Zero : [[https://t73f.de/r/zero]] ; URL & Source Zettelstore-Client : [[https://t73f.de/r/zsc]] ; License: : European Union Public License, version 1.2 (EUPL v1.2), or later. |
Changes to box/constbox/home.zettel.
1 2 | === Thank you for using Zettelstore! | | | | | | < | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | === Thank you for using Zettelstore! You will find the latest information about Zettelstore at [[https://zettelstore.de]]. Check this website regularly for [[updates|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version. You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before updating. Sometimes, you have to edit some of your Zettelstore-related zettel before updating. Since Zettelstore is currently in a development state, every update might fix some of your problems. If you have problems concerning Zettelstore, do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. === Reporting errors If you have encountered an error, please include the content of the following zettel in your mail (if possible): * [[Zettelstore Version|00000000000001]]: {{00000000000001}} * [[Zettelstore Operating System|00000000000003]] * [[Zettelstore Startup Configuration|00000000000096]] * [[Zettelstore Runtime Configuration|00000000000100]] Additionally, you have to describe, what you did before that error occurs and what you expected instead. Please do not forget to include the error message, if there is one. Some of above Zettelstore zettel can only be retrieved if you enabled ""expert mode"". Otherwise, only some zettel are linked. To enable expert mode, edit the zettel [[Zettelstore Runtime Configuration|00000000000100]]: please set the metadata value of the key ''expert-mode'' to true. To do so, enter the string ''expert-mode:true'' inside the edit view of the metadata. === Information about this zettel This zettel is your home zettel. It is part of the Zettelstore software itself. Every time you click on the [[Home|//]] link in the menu bar, you will be redirected to this zettel. You can change the content of this zettel by clicking on ""Edit"" above. |
︙ | ︙ |
Added box/constbox/menu_lists.zettel.
> > > > > > > | 1 2 3 4 5 6 7 | This zettel lists all entries of the ""Lists"" menu. * [[List Zettel|query:]] * [[List Roles|query:|role]] * [[List Tags|query:|tags]] An additional ""Refresh"" menu item is automatically added if appropriate. |
Added box/constbox/menu_new.zettel.
> > > > > > | 1 2 3 4 5 6 | This zettel lists all zettel that should act as a template for new zettel. These zettel will be included in the ""New"" menu of the WebUI. * [[New Zettel|00000000090001]] * [[New Role|00000000090004]] * [[New Tag|00000000090003]] * [[New User|00000000090002]] |
Deleted box/constbox/newtoc.zettel.
|
| < < < < < < |
Changes to box/constbox/zettel.sxn.
︙ | ︙ | |||
20 21 22 23 24 25 26 27 | (a (@ (href ,info-url)) "Info") (@H " · ") "(" ,@(if (bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role))) ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role)) `((@H " → ") (a (@ (href ,folge-role-url)) ,meta-folge-role))) ")" ,@(if tag-refs `((@H " · ") ,@tag-refs)) ,@(ROLE-DEFAULT-actions (current-binding)) ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs)) | > < > | < > > | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | (a (@ (href ,info-url)) "Info") (@H " · ") "(" ,@(if (bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role))) ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role)) `((@H " → ") (a (@ (href ,folge-role-url)) ,meta-folge-role))) ")" ,@(if tag-refs `((@H " · ") ,@tag-refs)) ,@(ROLE-DEFAULT-actions (current-binding)) ,@(if superior-refs `((br) "Superior: " ,superior-refs)) ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs)) ,@(if prequel-refs `((br) "Prequel: " ,prequel-refs)) ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs)) ,@(ROLE-DEFAULT-heading (current-binding)) ) ) ,@content ,endnotes ,@(if (or folge-links sequel-links back-links successor-links subordinate-links) `((nav ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links))))) ,@(if sequel-links `((details (@ (,sequel-open)) (summary "Sequel") (ul ,@(map wui-item-link sequel-links))))) ,@(if successor-links `((details (@ (,successor-open)) (summary "Successors") (ul ,@(map wui-item-link successor-links))))) ,@(if subordinate-links `((details (@ (,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links))))) ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links))))) )) ) ) |
Changes to box/dirbox/dirbox.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 25 26 27 28 29 30 31 | "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/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" | > > < < | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | "context" "errors" "net/url" "os" "path/filepath" "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/box/notify" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" ) func init() { manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { var log *logger.Logger if krnl := kernel.Main; krnl != nil { log = krnl.GetLogger(kernel.BoxService).Clone().Str("box", "dir").Int("boxnum", int64(cdata.Number)).Child() |
︙ | ︙ |
Changes to box/dirbox/service.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 | "context" "fmt" "io" "os" "path/filepath" "time" "t73f.de/r/zsc/input" "zettelstore.de/z/box/filebox" "zettelstore.de/z/box/notify" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/zettel" | > > < < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | "context" "fmt" "io" "os" "path/filepath" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/box/filebox" "zettelstore.de/z/box/notify" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/zettel" ) func fileService(i uint32, log *logger.Logger, dirPath string, cmds <-chan fileCmd) { // Something may panic. Ensure a running service. defer func() { if ri := recover(); ri != nil { kernel.Main.LogRecover("FileService", ri) |
︙ | ︙ |
Changes to box/filebox/filebox.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | import ( "errors" "net/url" "path/filepath" "strings" | | > < < | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import ( "errors" "net/url" "path/filepath" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" ) func init() { manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { path := getFilepathFromURL(u) ext := strings.ToLower(filepath.Ext(path)) if ext != ".zip" { |
︙ | ︙ | |||
55 56 57 58 59 60 61 | fileName := filepath.Join(components...) if len(components) > 0 && components[0] == "" { return "/" + fileName } return fileName } | | | | | | | | | | 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 | fileName := filepath.Join(components...) if len(components) > 0 && components[0] == "" { return "/" + fileName } return fileName } var alternativeSyntax = map[string]meta.Value{ "htm": "html", } func calculateSyntax(ext string) meta.Value { ext = strings.ToLower(ext) if syntax, ok := alternativeSyntax[ext]; ok { return syntax } return meta.Value(ext) } // CalcDefaultMeta returns metadata with default values for the given entry. func CalcDefaultMeta(zid id.Zid, ext string) *meta.Meta { m := meta.New(zid) m.Set(meta.KeySyntax, calculateSyntax(ext)) return m } // CleanupMeta enhances the given metadata. func CleanupMeta(m *meta.Meta, zid id.Zid, ext string, inMeta bool, uselessFiles []string) { if inMeta { if syntax, ok := m.Get(meta.KeySyntax); !ok || syntax == "" { dm := CalcDefaultMeta(zid, ext) syntax, ok = dm.Get(meta.KeySyntax) if !ok { panic("Default meta must contain syntax") } m.Set(meta.KeySyntax, syntax) } } if len(uselessFiles) > 0 { m.Set(meta.KeyUselessFiles, meta.Value(strings.Join(uselessFiles, " "))) } } |
Changes to box/filebox/zipbox.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "archive/zip" "context" "fmt" "io" "strings" "t73f.de/r/zsc/input" "zettelstore.de/z/box" "zettelstore.de/z/box/notify" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" | > > < < | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import ( "archive/zip" "context" "fmt" "io" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/box" "zettelstore.de/z/box/notify" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" ) type zipBox struct { log *logger.Logger number int name string enricher box.Enricher |
︙ | ︙ |
Changes to box/helper.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | package box import ( "net/url" "strconv" "time" | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package box import ( "net/url" "strconv" "time" "t73f.de/r/zsc/domain/id" ) // GetNewZid calculates a new and unused zettel identifier, based on the current date and time. func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) { withSeconds := false for range 90 { // Must be completed within 9 seconds (less than web/server.writeTimeout) zid := id.New(withSeconds) |
︙ | ︙ |
Changes to box/manager/anteroom.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package manager import ( "sync" | | > | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //----------------------------------------------------------------------------- package manager import ( "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" ) type arAction int const ( arNothing arAction = iota arReload arZettel ) type anteroom struct { next *anteroom waiting *idset.Set curLoad int reload bool } type anteroomQueue struct { mx sync.Mutex first *anteroom |
︙ | ︙ | |||
73 74 75 76 77 78 79 | ar.last = room } func (ar *anteroomQueue) makeAnteroom(zid id.Zid) *anteroom { if zid == id.Invalid { panic(zid) } | | | | 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 | ar.last = room } func (ar *anteroomQueue) makeAnteroom(zid id.Zid) *anteroom { if zid == id.Invalid { panic(zid) } waiting := idset.NewCap(max(ar.maxLoad, 100), zid) return &anteroom{next: nil, waiting: waiting, curLoad: 1, reload: false} } func (ar *anteroomQueue) Reset() { ar.mx.Lock() defer ar.mx.Unlock() ar.first = &anteroom{next: nil, waiting: nil, curLoad: 0, reload: true} ar.last = ar.first } func (ar *anteroomQueue) Reload(allZids *idset.Set) { ar.mx.Lock() defer ar.mx.Unlock() ar.deleteReloadedRooms() if !allZids.IsEmpty() { ar.first = &anteroom{next: ar.first, waiting: allZids, curLoad: allZids.Length(), reload: true} if ar.first.next == nil { |
︙ | ︙ |
Changes to box/manager/anteroom_test.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package manager import ( "testing" | | > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- package manager import ( "testing" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" ) func TestSimple(t *testing.T) { t.Parallel() ar := newAnteroomQueue(2) ar.EnqueueZettel(id.Zid(1)) action, zid, lastReload := ar.Dequeue() |
︙ | ︙ | |||
58 59 60 61 62 63 64 | ar := newAnteroomQueue(1) ar.EnqueueZettel(id.Zid(1)) ar.Reset() action, zid, _ := ar.Dequeue() if action != arReload || zid != id.Invalid { t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) } | | | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | ar := newAnteroomQueue(1) ar.EnqueueZettel(id.Zid(1)) ar.Reset() action, zid, _ := ar.Dequeue() if action != arReload || zid != id.Invalid { t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) } ar.Reload(idset.New(3, 4)) ar.EnqueueZettel(id.Zid(5)) ar.EnqueueZettel(id.Zid(5)) if ar.first == ar.last || ar.first.next != ar.last /*|| ar.first.next.next != ar.last*/ { t.Errorf("Expected 2 rooms") } action, zid1, _ := ar.Dequeue() if action != arZettel { |
︙ | ︙ | |||
85 86 87 88 89 90 91 | } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnteroomQueue(1) | | | 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } ar = newAnteroomQueue(1) ar.Reload(idset.New(id.Zid(6))) action, zid, _ = ar.Dequeue() if zid != id.Zid(6) || action != arZettel { t.Errorf("Expected 6/arZettel, but got %v/%v", zid, action) } action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) |
︙ | ︙ |
Changes to box/manager/box.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 | package manager import ( "context" "errors" "strings" "zettelstore.de/z/box" "zettelstore.de/z/query" "zettelstore.de/z/zettel" | > > > < < | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package manager import ( "context" "errors" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/query" "zettelstore.de/z/zettel" ) // Conatains all box.Box related functions // Location returns some information where the box is located. func (mgr *Manager) Location() string { if len(mgr.boxes) <= 2 { |
︙ | ︙ | |||
61 62 63 64 65 66 67 | if err := mgr.checkContinue(ctx); err != nil { return id.Invalid, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { ztl.Meta = mgr.cleanMetaProperties(ztl.Meta) | | | | 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | if err := mgr.checkContinue(ctx); err != nil { return id.Invalid, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { ztl.Meta = mgr.cleanMetaProperties(ztl.Meta) zid, err := box.CreateZettel(ctx, ztl) if err == nil { mgr.idxUpdateZettel(ctx, ztl) } return zid, err } return id.Invalid, box.ErrReadOnly } // GetZettel retrieves a specific zettel. func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetZettel") |
︙ | ︙ | |||
112 113 114 115 116 117 118 | result = append(result, z) } } return result, nil } // FetchZids returns the set of all zettel identifer managed by the box. | | | | < < < < < < | 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 | result = append(result, z) } } return result, nil } // FetchZids returns the set of all zettel identifer managed by the box. func (mgr *Manager) FetchZids(ctx context.Context) (*idset.Set, error) { mgr.mgrLog.Debug().Msg("FetchZids") if err := mgr.checkContinue(ctx); err != nil { return nil, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() return mgr.fetchZids(ctx) } func (mgr *Manager) fetchZids(ctx context.Context) (*idset.Set, error) { numZettel := 0 for _, p := range mgr.boxes { var mbstats box.ManagedBoxStats p.ReadStats(&mbstats) numZettel += mbstats.Zettel } result := idset.NewCap(numZettel) for _, p := range mgr.boxes { err := p.ApplyZid(ctx, func(zid id.Zid) { result.Add(zid) }, query.AlwaysIncluded) if err != nil { return nil, err } } return result, nil } func (mgr *Manager) hasZettel(ctx context.Context, zid id.Zid) bool { mgr.mgrLog.Debug().Zid(zid).Msg("HasZettel") if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() |
︙ | ︙ | |||
194 195 196 197 198 199 200 | compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq) if result := compSearch.Result(); result != nil { mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found without ApplyMeta") return result, nil } selected := map[id.Zid]*meta.Meta{} for _, term := range compSearch.Terms { | | | 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq) if result := compSearch.Result(); result != nil { mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found without ApplyMeta") return result, nil } selected := map[id.Zid]*meta.Meta{} for _, term := range compSearch.Terms { rejected := idset.New() handleMeta := func(m *meta.Meta) { zid := m.Zid if rejected.Contains(zid) { mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected") return } if _, ok := selected[zid]; ok { |
︙ | ︙ | |||
278 279 280 281 282 283 284 | return true } } return false } // DeleteZettel removes the zettel from the box. | | | | | | | | | | 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 | return true } } return false } // DeleteZettel removes the zettel from the box. func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { mgr.mgrLog.Debug().Zid(zid).Msg("DeleteZettel") if err := mgr.checkContinue(ctx); err != nil { return err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { err := p.DeleteZettel(ctx, zid) if err == nil { mgr.idxDeleteZettel(ctx, zid) return err } var errZNF box.ErrZettelNotFound if !errors.As(err, &errZNF) && !errors.Is(err, box.ErrReadOnly) { return err } } return box.ErrZettelNotFound{Zid: zid} } // Remove all (computed) properties from metadata before storing the zettel. func (mgr *Manager) cleanMetaProperties(m *meta.Meta) *meta.Meta { result := m.Clone() for key := range result.ComputedRest() { if mgr.propertyKeys.Contains(key) { result.Delete(key) } } return result } |
Changes to box/manager/collect.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 | //----------------------------------------------------------------------------- package manager import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/strfun" | > > < | | | < < < < | 12 13 14 15 16 17 18 19 20 21 22 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 manager import ( "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "zettelstore.de/z/ast" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/strfun" ) type collectData struct { refs *idset.Set words store.WordSet urls store.WordSet } func (data *collectData) initialize() { data.refs = idset.New() data.words = store.NewWordSet() data.urls = store.NewWordSet() } func collectZettelIndexData(zn *ast.ZettelNode, data *collectData) { ast.Walk(data, &zn.BlocksAST) } func (data *collectData) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.VerbatimNode: data.addText(string(n.Content)) case *ast.TranscludeNode: |
︙ | ︙ |
Changes to box/manager/enrich.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | package manager import ( "context" "strconv" | | | | < | < | | | < < | < < | | < < < | < < < < < | < | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | package manager import ( "context" "strconv" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" ) // Enrich computes additional properties and updates the given metadata. func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) { // Calculate computed, but stored values. if _, hasCreated := m.Get(meta.KeyCreated); !hasCreated { m.Set(meta.KeyCreated, computeCreated(m.Zid)) } if box.DoEnrich(ctx) { computePublished(m) if boxNumber > 0 { m.Set(meta.KeyBoxNumber, meta.Value(strconv.Itoa(boxNumber))) } mgr.idxStore.Enrich(ctx, m) } } func computeCreated(zid id.Zid) meta.Value { if zid <= 10101000000 { // A year 0000 is not allowed and therefore an artificial Zid. // In the year 0001, the month must be > 0. // In the month 000101, the day must be > 0. return "00010101000000" } seconds := min(zid%100, 59) zid /= 100 minutes := min(zid%100, 59) zid /= 100 hours := min(zid%100, 23) zid /= 100 day := zid % 100 zid /= 100 month := zid % 100 year := zid / 100 month, day = sanitizeMonthDay(year, month, day) created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds return meta.Value(created.String()) } func sanitizeMonthDay(year, month, day id.Zid) (id.Zid, id.Zid) { if day < 1 { day = 1 } if month < 1 { |
︙ | ︙ | |||
106 107 108 109 110 111 112 | } } } return month, day } func computePublished(m *meta.Meta) { | | | | | | | | | | | | 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 | } } } return month, day } func computePublished(m *meta.Meta) { if _, ok := m.Get(meta.KeyPublished); ok { return } if modified, ok := m.Get(meta.KeyModified); ok { if _, ok = modified.AsTime(); ok { m.Set(meta.KeyPublished, modified) return } } if created, ok := m.Get(meta.KeyCreated); ok { if _, ok = created.AsTime(); ok { m.Set(meta.KeyPublished, created) return } } zidValue := meta.Value(m.Zid.String()) if _, ok := zidValue.AsTime(); ok { m.Set(meta.KeyPublished, zidValue) return } // Neither the zettel was modified nor the zettel identifer contains a valid // timestamp. In this case do not set the "published" property. } |
Changes to box/manager/indexer.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "context" "fmt" "net/url" "time" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel" | > > > < < | | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | import ( "context" "fmt" "net/url" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel" ) // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchEqual(word string) *idset.Set { found := mgr.idxStore.SearchEqual(word) mgr.idxLog.Debug().Str("word", word).Int("found", int64(found.Length())).Msg("SearchEqual") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // SearchPrefix returns all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchPrefix(prefix string) *idset.Set { found := mgr.idxStore.SearchPrefix(prefix) mgr.idxLog.Debug().Str("prefix", prefix).Int("found", int64(found.Length())).Msg("SearchPrefix") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // SearchSuffix returns all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchSuffix(suffix string) *idset.Set { found := mgr.idxStore.SearchSuffix(suffix) mgr.idxLog.Debug().Str("suffix", suffix).Int("found", int64(found.Length())).Msg("SearchSuffix") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchContains(s string) *idset.Set { found := mgr.idxStore.SearchContains(s) mgr.idxLog.Debug().Str("s", s).Int("found", int64(found.Length())).Msg("SearchContains") if msg := mgr.idxLog.Trace(); msg.Enabled() { msg.Str("ids", fmt.Sprint(found)).Msg("IDs") } return found } |
︙ | ︙ | |||
170 171 172 173 174 175 176 | } func mustIndexZettel(m *meta.Meta) bool { return m.Zid >= id.Zid(999999900) } func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { | | | | | < < < | | | | | 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 | } func mustIndexZettel(m *meta.Meta) bool { return m.Zid >= id.Zid(999999900) } func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { for key, val := range m.Computed() { descr := meta.GetDescription(key) if descr.IsProperty() { continue } switch descr.Type { case meta.TypeID: mgr.idxUpdateValue(ctx, descr.Inverse, string(val), zi) case meta.TypeIDSet: for val := range val.Fields() { mgr.idxUpdateValue(ctx, descr.Inverse, val, zi) } case meta.TypeURL: if _, err := url.Parse(string(val)); err == nil { cData.urls.Add(string(val)) } default: if descr.Type.IsSet { for val := range val.Fields() { idxCollectMetaValue(cData.words, val) } } else { idxCollectMetaValue(cData.words, string(val)) } } } } func idxCollectMetaValue(stWords store.WordSet, value string) { if words := strfun.NormalizeWords(value); len(words) > 0 { |
︙ | ︙ | |||
244 245 246 247 248 249 250 | } func (mgr *Manager) idxDeleteZettel(ctx context.Context, zid id.Zid) { toCheck := mgr.idxStore.DeleteZettel(ctx, zid) mgr.idxCheckZettel(toCheck) } | | | 242 243 244 245 246 247 248 249 250 251 252 253 | } func (mgr *Manager) idxDeleteZettel(ctx context.Context, zid id.Zid) { toCheck := mgr.idxStore.DeleteZettel(ctx, zid) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCheckZettel(s *idset.Set) { s.ForEach(func(zid id.Zid) { mgr.idxAr.EnqueueZettel(zid) }) } |
Changes to box/manager/manager.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import ( "context" "io" "net/url" "sync" "time" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/mapstore" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" | > > > < < < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | import ( "context" "io" "net/url" "sync" "time" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/mapstore" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) // ConnectData contains all administration related values. type ConnectData struct { Number int // number of the box, starting with 1. Config config.Config Enricher box.Enricher |
︙ | ︙ | |||
89 90 91 92 93 94 95 | mgrMx sync.RWMutex rtConfig config.Config boxes []box.ManagedBox observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} infos chan box.UpdateInfo | | | 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | mgrMx sync.RWMutex rtConfig config.Config boxes []box.ManagedBox observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} infos chan box.UpdateInfo propertyKeys *set.Set[string] // Set of property key names // Indexer data idxLog *logger.Logger idxStore store.Store idxAr *anteroomQueue idxReady chan struct{} // Signal a non-empty anteroom to background task |
︙ | ︙ | |||
121 122 123 124 125 126 127 | mgr.stateMx.RUnlock() return state } // New creates a new managing box. func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) { descrs := meta.GetSortedKeyDescriptions() | | | | 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | mgr.stateMx.RUnlock() return state } // New creates a new managing box. func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) { descrs := meta.GetSortedKeyDescriptions() propertyKeys := set.New[string]() for _, kd := range descrs { if kd.IsProperty() { propertyKeys.Add(kd.Name) } } boxLog := kernel.Main.GetLogger(kernel.BoxService) mgr := &Manager{ mgrLog: boxLog.Clone().Str("box", "manager").Child(), rtConfig: rtConfig, infos: make(chan box.UpdateInfo, len(boxURIs)*10), |
︙ | ︙ | |||
245 246 247 248 249 250 251 | cache[zid] = destutterData{ deadAt: now.Add(500 * time.Millisecond), reason: reason, } return false } | | | | | | 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 | cache[zid] = destutterData{ deadAt: now.Add(500 * time.Millisecond), reason: reason, } return false } func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { switch reason { case box.OnReady: return case box.OnReload: mgr.idxAr.Reset() case box.OnZettel: mgr.idxAr.EnqueueZettel(zid) case box.OnDelete: mgr.idxAr.EnqueueZettel(zid) default: mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") return } select { case mgr.idxReady <- struct{}{}: default: } } |
︙ | ︙ |
Changes to box/manager/mapstore/mapstore.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 | // Package mapstore stored the index in main memory via a Go map. package mapstore import ( "context" "fmt" "io" "slices" "strings" "sync" | > | > | < < | | | | | | | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | // Package mapstore stored the index in main memory via a Go map. package mapstore import ( "context" "fmt" "io" "maps" "slices" "strings" "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" ) type zettelData struct { meta *meta.Meta // a local copy of the metadata, without computed keys dead *idset.Set // set of dead references in this zettel forward *idset.Set // set of forward references in this zettel backward *idset.Set // set of zettel that reference with zettel otherRefs map[string]bidiRefs words []string // list of words of this zettel urls []string // list of urls of this zettel } type bidiRefs struct { forward *idset.Set backward *idset.Set } func (zd *zettelData) optimize() { zd.dead.Optimize() zd.forward.Optimize() zd.backward.Optimize() for _, bidi := range zd.otherRefs { bidi.forward.Optimize() bidi.backward.Optimize() } } type mapStore struct { mx sync.RWMutex intern map[string]string // map to intern strings idx map[id.Zid]*zettelData dead map[id.Zid]*idset.Set // map dead refs where they occur words stringRefs urls stringRefs // Stats mxStats sync.Mutex updates uint64 } type stringRefs map[string]*idset.Set // New returns a new memory-based index store. func New() store.Store { return &mapStore{ intern: make(map[string]string, 1024), idx: make(map[id.Zid]*zettelData), dead: make(map[id.Zid]*idset.Set), words: make(stringRefs), urls: make(stringRefs), } } func (ms *mapStore) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { ms.mx.RLock() |
︙ | ︙ | |||
103 104 105 106 107 108 109 | defer ms.mx.RUnlock() zi, ok := ms.idx[m.Zid] if !ok { return false } var updated bool if !zi.dead.IsEmpty() { | | | | | | | | | | 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 | defer ms.mx.RUnlock() zi, ok := ms.idx[m.Zid] if !ok { return false } var updated bool if !zi.dead.IsEmpty() { m.Set(meta.KeyDead, zi.dead.MetaValue()) updated = true } back := removeOtherMetaRefs(m, zi.backward.Clone()) if !zi.backward.IsEmpty() { m.Set(meta.KeyBackward, zi.backward.MetaValue()) updated = true } if !zi.forward.IsEmpty() { m.Set(meta.KeyForward, zi.forward.MetaValue()) back.ISubstract(zi.forward) updated = true } for k, refs := range zi.otherRefs { if !refs.backward.IsEmpty() { m.Set(k, refs.backward.MetaValue()) back.ISubstract(refs.backward) updated = true } } if !back.IsEmpty() { m.Set(meta.KeyBack, back.MetaValue()) updated = true } return updated } // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (ms *mapStore) SearchEqual(word string) *idset.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := idset.New() if refs, ok := ms.words[word]; ok { result = result.IUnion(refs) } if refs, ok := ms.urls[word]; ok { result = result.IUnion(refs) } zid, err := id.Parse(word) if err != nil { return result } zi, ok := ms.idx[zid] if !ok { return result } return addBackwardZids(result, zid, zi) } // SearchPrefix returns all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. func (ms *mapStore) SearchPrefix(prefix string) *idset.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(prefix, strings.HasPrefix) l := len(prefix) if l > 14 { return result } |
︙ | ︙ | |||
187 188 189 190 191 192 193 | } } return result } // SearchSuffix returns all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. | | | 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | } } return result } // SearchSuffix returns all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. func (ms *mapStore) SearchSuffix(suffix string) *idset.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(suffix, strings.HasSuffix) l := len(suffix) if l > 14 { return result } |
︙ | ︙ | |||
213 214 215 216 217 218 219 | } } return result } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. | | | | | | | | | | | | | 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 | } } return result } // SearchContains returns all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. func (ms *mapStore) SearchContains(s string) *idset.Set { ms.mx.RLock() defer ms.mx.RUnlock() result := ms.selectWithPred(s, strings.Contains) if len(s) > 14 { return result } if _, err := id.ParseUint(s); err != nil { return result } for zid, zi := range ms.idx { if strings.Contains(zid.String(), s) { result = addBackwardZids(result, zid, zi) } } return result } func (ms *mapStore) selectWithPred(s string, pred func(string, string) bool) *idset.Set { // Must only be called if ms.mx is read-locked! result := idset.New() for word, refs := range ms.words { if !pred(word, s) { continue } result.IUnion(refs) } for u, refs := range ms.urls { if !pred(u, s) { continue } result.IUnion(refs) } return result } func addBackwardZids(result *idset.Set, zid id.Zid, zi *zettelData) *idset.Set { // Must only be called if ms.mx is read-locked! result = result.Add(zid) result = result.IUnion(zi.backward) for _, mref := range zi.otherRefs { result = result.IUnion(mref.backward) } return result } func removeOtherMetaRefs(m *meta.Meta, back *idset.Set) *idset.Set { for key, val := range m.Rest() { switch meta.Type(key) { case meta.TypeID: if zid, err := id.Parse(string(val)); err == nil { back = back.Remove(zid) } case meta.TypeIDSet: for val := range val.Fields() { if zid, err := id.Parse(val); err == nil { back = back.Remove(zid) } } } } return back } func (ms *mapStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) *idset.Set { ms.mx.Lock() defer ms.mx.Unlock() m := ms.makeMeta(zidx) zi, ziExist := ms.idx[zidx.Zid] if !ziExist || zi == nil { zi = &zettelData{} ziExist = false } // Is this zettel an old dead reference mentioned in other zettel? var toCheck *idset.Set if refs, ok := ms.dead[zidx.Zid]; ok { // These must be checked later again toCheck = refs delete(ms.dead, zidx.Zid) } zi.meta = m |
︙ | ︙ | |||
313 314 315 316 317 318 319 | ms.idx[zidx.Zid] = zi } zi.optimize() return toCheck } var internableKeys = map[string]bool{ | | | | | | | | | | | | | | | | 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 | ms.idx[zidx.Zid] = zi } zi.optimize() return toCheck } var internableKeys = map[string]bool{ meta.KeyRole: true, meta.KeySyntax: true, meta.KeyFolgeRole: true, meta.KeyLang: true, meta.KeyReadOnly: true, } func isInternableValue(key string) bool { if internableKeys[key] { return true } return strings.HasSuffix(key, meta.SuffixKeyRole) } func (ms *mapStore) internString(s string) string { if is, found := ms.intern[s]; found { return is } ms.intern[s] = s return s } func (ms *mapStore) makeMeta(zidx *store.ZettelIndex) *meta.Meta { origM := zidx.GetMeta() copyM := meta.New(origM.Zid) for key, val := range origM.All() { key = ms.internString(key) if isInternableValue(key) { copyM.Set(key, meta.Value(ms.internString(string(val)))) } else if key == meta.KeyBoxNumber || !meta.IsComputed(key) { copyM.Set(key, val) } } return copyM } func (ms *mapStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelData) { // Must only be called if ms.mx is write-locked! drefs := zidx.GetDeadRefs() newRefs, remRefs := zi.dead.Diff(drefs) zi.dead = drefs remRefs.ForEach(func(ref id.Zid) { ms.dead[ref] = ms.dead[ref].Remove(zidx.Zid) }) newRefs.ForEach(func(ref id.Zid) { ms.dead[ref] = ms.dead[ref].Add(zidx.Zid) }) } func (ms *mapStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelData) *idset.Set { // Must only be called if ms.mx is write-locked! brefs := zidx.GetBackRefs() newRefs, remRefs := zi.forward.Diff(brefs) zi.forward = brefs var toCheck *idset.Set remRefs.ForEach(func(ref id.Zid) { bzi := ms.getOrCreateEntry(ref) bzi.backward = bzi.backward.Remove(zidx.Zid) if bzi.meta == nil { toCheck = toCheck.Add(ref) } }) newRefs.ForEach(func(ref id.Zid) { bzi := ms.getOrCreateEntry(ref) bzi.backward = bzi.backward.Add(zidx.Zid) if bzi.meta == nil { toCheck = toCheck.Add(ref) } }) return toCheck } func (ms *mapStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelData) *idset.Set { // Must only be called if ms.mx is write-locked! inverseRefs := zidx.GetInverseRefs() for key, mr := range zi.otherRefs { if _, ok := inverseRefs[key]; ok { continue } ms.removeInverseMeta(zidx.Zid, key, mr.forward) } if zi.otherRefs == nil { zi.otherRefs = make(map[string]bidiRefs) } var toCheck *idset.Set for key, mrefs := range inverseRefs { mr := zi.otherRefs[key] newRefs, remRefs := mr.forward.Diff(mrefs) mr.forward = mrefs zi.otherRefs[key] = mr newRefs.ForEach(func(ref id.Zid) { |
︙ | ︙ | |||
453 454 455 456 457 458 459 | return zi } zi := &zettelData{} ms.idx[zid] = zi return zi } | | | | 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 | return zi } zi := &zettelData{} ms.idx[zid] = zi return zi } func (ms *mapStore) DeleteZettel(_ context.Context, zid id.Zid) *idset.Set { ms.mx.Lock() defer ms.mx.Unlock() return ms.doDeleteZettel(zid) } func (ms *mapStore) doDeleteZettel(zid id.Zid) *idset.Set { // Must only be called if ms.mx is write-locked! zi, ok := ms.idx[zid] if !ok { return nil } ms.deleteDeadSources(zid, zi) |
︙ | ︙ | |||
490 491 492 493 494 495 496 | } else { ms.dead[ref] = drefs } } }) } | | | | | 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 | } else { ms.dead[ref] = drefs } } }) } func (ms *mapStore) deleteForwardBackward(zid id.Zid, zi *zettelData) *idset.Set { // Must only be called if ms.mx is write-locked! zi.forward.ForEach(func(ref id.Zid) { if fzi, ok := ms.idx[ref]; ok { fzi.backward = fzi.backward.Remove(zid) } }) var toCheck *idset.Set zi.backward.ForEach(func(ref id.Zid) { if bzi, ok := ms.idx[ref]; ok { bzi.forward = bzi.forward.Remove(zid) toCheck = toCheck.Add(ref) } }) return toCheck } func (ms *mapStore) removeInverseMeta(zid id.Zid, key string, forward *idset.Set) { // Must only be called if ms.mx is write-locked! forward.ForEach(func(ref id.Zid) { bzi, ok := ms.idx[ref] if !ok || bzi.otherRefs == nil { return } bmr, ok := bzi.otherRefs[key] |
︙ | ︙ | |||
590 591 592 593 594 595 596 | } func (ms *mapStore) dumpIndex(w io.Writer) { if len(ms.idx) == 0 { return } io.WriteString(w, "==== Zettel Index\n") | | | | 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 | } func (ms *mapStore) dumpIndex(w io.Writer) { if len(ms.idx) == 0 { return } io.WriteString(w, "==== Zettel Index\n") zids := make([]id.Zid, 0, len(ms.idx)) for id := range ms.idx { zids = append(zids, id) } slices.Sort(zids) for _, id := range zids { fmt.Fprintln(w, "=====", id) zi := ms.idx[id] if !zi.dead.IsEmpty() { fmt.Fprintln(w, "* Dead:", zi.dead) } dumpSet(w, "* Forward:", zi.forward) |
︙ | ︙ | |||
624 625 626 627 628 629 630 | } func (ms *mapStore) dumpDead(w io.Writer) { if len(ms.dead) == 0 { return } fmt.Fprintf(w, "==== Dead References\n") | | | | | 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 | } func (ms *mapStore) dumpDead(w io.Writer) { if len(ms.dead) == 0 { return } fmt.Fprintf(w, "==== Dead References\n") zids := make([]id.Zid, 0, len(ms.dead)) for id := range ms.dead { zids = append(zids, id) } slices.Sort(zids) for _, id := range zids { fmt.Fprintln(w, ";", id) fmt.Fprintln(w, ":", ms.dead[id]) } } func dumpSet(w io.Writer, prefix string, s *idset.Set) { if !s.IsEmpty() { io.WriteString(w, prefix) s.ForEach(func(zid id.Zid) { io.WriteString(w, " ") w.Write(zid.Bytes()) }) fmt.Fprintln(w) |
︙ | ︙ | |||
662 663 664 665 666 667 668 | } func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { if len(srefs) == 0 { return } fmt.Fprintln(w, "====", title) | | | 662 663 664 665 666 667 668 669 670 671 672 673 | } func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { if len(srefs) == 0 { return } fmt.Fprintln(w, "====", title) for _, s := range slices.Sorted(maps.Keys(srefs)) { fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) fmt.Fprintln(w, ":", srefs[s]) } } |
Changes to box/manager/store/store.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | // Package store contains general index data for storing a zettel index. package store import ( "context" "io" | | > | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // Package store contains general index data for storing a zettel index. package store import ( "context" "io" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/query" ) // Stats records statistics about the store. type Stats struct { // Zettel is the number of zettel managed by the indexer. Zettel int |
︙ | ︙ | |||
47 48 49 50 51 52 53 | GetMeta(context.Context, id.Zid) (*meta.Meta, error) // Entrich metadata with data from store. Enrich(ctx context.Context, m *meta.Meta) // UpdateReferences for a specific zettel. // Returns set of zettel identifier that must also be checked for changes. | | | | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | GetMeta(context.Context, id.Zid) (*meta.Meta, error) // Entrich metadata with data from store. Enrich(ctx context.Context, m *meta.Meta) // UpdateReferences for a specific zettel. // Returns set of zettel identifier that must also be checked for changes. UpdateReferences(context.Context, *ZettelIndex) *idset.Set // DeleteZettel removes index data for given zettel. // Returns set of zettel identifier that must also be checked for changes. DeleteZettel(context.Context, id.Zid) *idset.Set // Optimize removes unneeded space. Optimize() // ReadStats populates st with store statistics. ReadStats(st *Stats) // Dump the content to a Writer. Dump(io.Writer) } |
Changes to box/manager/store/zettel.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store import ( | > | | > > | | | | | | | | | | | | | < < < < < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store import ( "maps" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" ) // ZettelIndex contains all index data of a zettel. type ZettelIndex struct { Zid id.Zid // zid of the indexed zettel meta *meta.Meta // full metadata backrefs *idset.Set // set of back references inverseRefs map[string]*idset.Set // references of inverse keys deadrefs *idset.Set // set of dead references words WordSet urls WordSet } // NewZettelIndex creates a new zettel index. func NewZettelIndex(m *meta.Meta) *ZettelIndex { return &ZettelIndex{ Zid: m.Zid, meta: m, backrefs: idset.New(), inverseRefs: make(map[string]*idset.Set), deadrefs: idset.New(), } } // AddBackRef adds a reference to a zettel where the current zettel links to // without any more information. func (zi *ZettelIndex) AddBackRef(zid id.Zid) { zi.backrefs.Add(zid) } // AddInverseRef adds a named reference to a zettel. On that zettel, the given // metadata key should point back to the current zettel. func (zi *ZettelIndex) AddInverseRef(key string, zid id.Zid) { if zids, ok := zi.inverseRefs[key]; ok { zids.Add(zid) return } zi.inverseRefs[key] = idset.New(zid) } // AddDeadRef adds a dead reference to a zettel. func (zi *ZettelIndex) AddDeadRef(zid id.Zid) { zi.deadrefs.Add(zid) } // SetWords sets the words to the given value. func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words } // SetUrls sets the words to the given value. func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls } // GetDeadRefs returns all dead references as a sorted list. func (zi *ZettelIndex) GetDeadRefs() *idset.Set { return zi.deadrefs } // GetMeta return just the raw metadata. func (zi *ZettelIndex) GetMeta() *meta.Meta { return zi.meta } // GetBackRefs returns all back references as a sorted list. func (zi *ZettelIndex) GetBackRefs() *idset.Set { return zi.backrefs } // GetInverseRefs returns all inverse meta references as a map of strings to a sorted list of references func (zi *ZettelIndex) GetInverseRefs() map[string]*idset.Set { return maps.Clone(zi.inverseRefs) } // GetWords returns a reference to the set of words. It must not be modified. func (zi *ZettelIndex) GetWords() WordSet { return zi.words } // GetUrls returns a reference to the set of URLs. It must not be modified. func (zi *ZettelIndex) GetUrls() WordSet { return zi.urls } |
Changes to box/membox/membox.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 27 | package membox import ( "context" "net/url" "sync" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" | > < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | package membox import ( "context" "net/url" "sync" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" ) func init() { manager.Register( "mem", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &memBox{ |
︙ | ︙ | |||
92 93 94 95 96 97 98 | mb.mx.RLock() defer mb.mx.RUnlock() return len(mb.zettel) < mb.maxZettel } func (mb *memBox) CreateZettel(_ context.Context, zettel zettel.Zettel) (id.Zid, error) { mb.mx.Lock() | | | 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | mb.mx.RLock() defer mb.mx.RUnlock() return len(mb.zettel) < mb.maxZettel } func (mb *memBox) CreateZettel(_ context.Context, zettel zettel.Zettel) (id.Zid, error) { mb.mx.Lock() newBytes := mb.curBytes + zettel.ByteSize() if mb.maxZettel < len(mb.zettel) || mb.maxBytes < newBytes { mb.mx.Unlock() return id.Invalid, box.ErrCapacity } zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) { _, ok := mb.zettel[zid] return !ok, nil |
︙ | ︙ | |||
170 171 172 173 174 175 176 | mb.mx.RLock() defer mb.mx.RUnlock() zid := zettel.Meta.Zid if !zid.IsValid() { return false } | | | | | | 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 | mb.mx.RLock() defer mb.mx.RUnlock() zid := zettel.Meta.Zid if !zid.IsValid() { return false } newBytes := mb.curBytes + zettel.ByteSize() if prevZettel, found := mb.zettel[zid]; found { newBytes -= prevZettel.ByteSize() } return newBytes < mb.maxBytes } func (mb *memBox) UpdateZettel(_ context.Context, zettel zettel.Zettel) error { m := zettel.Meta.Clone() if !m.Zid.IsValid() { return box.ErrInvalidZid{Zid: m.Zid.String()} } mb.mx.Lock() newBytes := mb.curBytes + zettel.ByteSize() if prevZettel, found := mb.zettel[m.Zid]; found { newBytes -= prevZettel.ByteSize() } if mb.maxBytes < newBytes { mb.mx.Unlock() return box.ErrCapacity } zettel.Meta = m |
︙ | ︙ | |||
217 218 219 220 221 222 223 | mb.mx.Lock() oldZettel, found := mb.zettel[zid] if !found { mb.mx.Unlock() return box.ErrZettelNotFound{Zid: zid} } delete(mb.zettel, zid) | | | 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | mb.mx.Lock() oldZettel, found := mb.zettel[zid] if !found { mb.mx.Unlock() return box.ErrZettelNotFound{Zid: zid} } delete(mb.zettel, zid) mb.curBytes -= oldZettel.ByteSize() mb.mx.Unlock() mb.notifyChanged(zid, box.OnDelete) mb.log.Trace().Msg("DeleteZettel") return nil } func (mb *memBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = false mb.mx.RLock() st.Zettel = len(mb.zettel) mb.mx.RUnlock() mb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } |
Changes to box/notify/directory.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 | import ( "errors" "fmt" "path/filepath" "regexp" "sync" "zettelstore.de/z/box" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/parser" "zettelstore.de/z/query" | > > > > < < | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import ( "errors" "fmt" "path/filepath" "regexp" "sync" "slices" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/box" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/parser" "zettelstore.de/z/query" ) type entrySet map[id.Zid]*DirEntry // DirServiceState signal the internal state of the service. // // The following state transitions are possible: |
︙ | ︙ | |||
272 273 274 275 276 277 278 | } default: ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") } return newEntries, true } | | | | | 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | } default: ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") } return newEntries, true } func getNewZids(entries entrySet) []id.Zid { zids := make([]id.Zid, 0, len(entries)) for zid := range entries { zids = append(zids, zid) } return zids } func (ds *DirService) onCreateDirectory(zids []id.Zid, prevEntries entrySet) { for _, zid := range zids { ds.notifyChange(zid, box.OnZettel) delete(prevEntries, zid) } // These were previously stored, by are not found now. // Notify system that these were deleted, e.g. for updating the index. |
︙ | ︙ | |||
499 500 501 502 503 504 505 | entry.ContentName = name entry.ContentExt = ext return addUselessFile(entry, contentName), "" } return addUselessFile(entry, name), "" } func addUselessFile(entry *DirEntry, name string) string { | | < | < | | | | | | | | 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 | entry.ContentName = name entry.ContentExt = ext return addUselessFile(entry, contentName), "" } return addUselessFile(entry, name), "" } func addUselessFile(entry *DirEntry, name string) string { if slices.Contains(entry.UselessFiles, name) { return "" } entry.UselessFiles = append(entry.UselessFiles, name) return name } func onlyExt(name string) string { ext := filepath.Ext(name) if ext == "" || ext[0] != '.' { return ext } return ext[1:] } func newNameIsBetter(oldName, newName string) bool { if len(oldName) < len(newName) { return false } return oldName > newName } var supportedSyntax, primarySyntax *set.Set[string] func init() { syntaxList := parser.GetSyntaxes() supportedSyntax = set.New(syntaxList...) primarySyntax = set.New[string]() for _, syntax := range syntaxList { if parser.Get(syntax).Name == syntax { primarySyntax.Add(syntax) } } } func newExtIsBetter(oldExt, newExt string) bool { oldSyntax := supportedSyntax.Contains(oldExt) if oldSyntax != supportedSyntax.Contains(newExt) { return !oldSyntax } if oldSyntax { if oldExt == "zmk" { return false } if newExt == "zmk" { return true } oldInfo := parser.Get(oldExt) newInfo := parser.Get(newExt) if oldASTParser := oldInfo.IsASTParser; oldASTParser != newInfo.IsASTParser { return !oldASTParser } if oldTextFormat := oldInfo.IsTextFormat; oldTextFormat != newInfo.IsTextFormat { return !oldTextFormat } if oldImageFormat := oldInfo.IsImageFormat; oldImageFormat != newInfo.IsImageFormat { return oldImageFormat } if oldPrimary := primarySyntax.Contains(oldExt); oldPrimary != primarySyntax.Contains(newExt) { return !oldPrimary } } oldLen := len(oldExt) newLen := len(newExt) if oldLen != newLen { |
︙ | ︙ |
Changes to box/notify/directory_test.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 | //----------------------------------------------------------------------------- package notify import ( "testing" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. | > > < < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- package notify import ( "testing" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) func TestSeekZid(t *testing.T) { testcases := []struct { name string zid id.Zid }{ |
︙ | ︙ | |||
48 49 50 51 52 53 54 | } } } func TestNewExtIsBetter(t *testing.T) { extVals := []string{ // Main Formats | | | | | | | | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | } } } func TestNewExtIsBetter(t *testing.T) { extVals := []string{ // Main Formats meta.ValueSyntaxZmk, meta.ValueSyntaxDraw, meta.ValueSyntaxMarkdown, meta.ValueSyntaxMD, // Other supported text formats meta.ValueSyntaxCSS, meta.ValueSyntaxSxn, meta.ValueSyntaxTxt, meta.ValueSyntaxHTML, meta.ValueSyntaxText, meta.ValueSyntaxPlain, // Supported text graphics formats meta.ValueSyntaxSVG, meta.ValueSyntaxNone, // Supported binary graphic formats meta.ValueSyntaxGif, meta.ValueSyntaxPNG, meta.ValueSyntaxJPEG, meta.ValueSyntaxWebp, meta.ValueSyntaxJPG, // Unsupported syntax values "gz", "cpp", "tar", "cppc", } for oldI, oldExt := range extVals { for newI, newExt := range extVals { if oldI <= newI { |
︙ | ︙ |
Changes to box/notify/entry.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package notify import ( "path/filepath" | > > | > < < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | //----------------------------------------------------------------------------- package notify import ( "path/filepath" "slices" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/parser" "zettelstore.de/z/zettel" ) const ( extZettel = "zettel" // file contains metadata and content extBin = "bin" // file contains binary content extTxt = "txt" // file contains non-binary content ) |
︙ | ︙ | |||
47 48 49 50 51 52 53 | // HasMetaInContent returns true, if metadata will be stored in the content file. func (e *DirEntry) HasMetaInContent() bool { return e.IsValid() && extIsMetaAndContent(e.ContentExt) } // SetupFromMetaContent fills entry data based on metadata and zettel content. | | | | | | | | | | < | | < | | 48 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 | // HasMetaInContent returns true, if metadata will be stored in the content file. func (e *DirEntry) HasMetaInContent() bool { return e.IsValid() && extIsMetaAndContent(e.ContentExt) } // SetupFromMetaContent fills entry data based on metadata and zettel content. func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content zettel.Content, getZettelFileSyntax func() []meta.Value) { if e.Zid != m.Zid { panic("Zid differ") } if contentName := e.ContentName; contentName != "" { if !extIsMetaAndContent(e.ContentExt) && e.MetaName == "" { e.MetaName = e.calcBaseName(contentName) } return } syntax := m.GetDefault(meta.KeySyntax, meta.DefaultSyntax) ext := calcContentExt(syntax, m.YamlSep, getZettelFileSyntax) metaName := e.MetaName eimc := extIsMetaAndContent(ext) if eimc { if metaName != "" { ext = contentExtWithMeta(syntax, content) } e.ContentName = e.calcBaseName(metaName) + "." + ext e.ContentExt = ext } else { if len(content.AsBytes()) > 0 { e.ContentName = e.calcBaseName(metaName) + "." + ext e.ContentExt = ext } if metaName == "" { e.MetaName = e.calcBaseName(e.ContentName) } } } func contentExtWithMeta(syntax meta.Value, content zettel.Content) string { p := parser.Get(string(syntax)) if content.IsBinary() { if p.IsImageFormat { return string(syntax) } return extBin } if p.IsImageFormat { return extTxt } return string(syntax) } func calcContentExt(syntax meta.Value, yamlSep bool, getZettelFileSyntax func() []meta.Value) string { if yamlSep { return extZettel } switch syntax { case meta.ValueSyntaxNone, meta.ValueSyntaxZmk: return extZettel } if slices.Contains(getZettelFileSyntax(), syntax) { return extZettel } return string(syntax) } func (e *DirEntry) calcBaseName(name string) string { if name == "" { return e.Zid.String() } return name[0 : len(name)-len(filepath.Ext(name))] } |
Changes to cmd/cmd_file.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 | "context" "flag" "fmt" "io" "os" "t73f.de/r/zsc/api" "t73f.de/r/zsc/input" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/zettel" | > > < < | | > > | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | "context" "flag" "fmt" "io" "os" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/zettel" ) // ---------- Subcommand: file ----------------------------------------------- func cmdFile(fs *flag.FlagSet) (int, error) { enc := fs.Lookup("t").Value.String() m, inp, err := getInput(fs.Args()) if m == nil { return 2, err } z := parser.ParseZettel( context.Background(), zettel.Zettel{ Meta: m, Content: zettel.NewContent(inp.Src[inp.Pos:]), }, string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)), nil, ) encdr := encoder.Create( api.Encoder(enc), &encoder.CreateParameter{Lang: string(m.GetDefault(meta.KeyLang, meta.ValueLangEN))}) if encdr == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc) return 2, nil } _, err = encdr.WriteZettel(os.Stdout, z) if err != nil { return 2, err } fmt.Println() return 0, nil } |
︙ | ︙ |
Changes to cmd/cmd_password.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | import ( "flag" "fmt" "os" "golang.org/x/term" | | | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import ( "flag" "fmt" "os" "golang.org/x/term" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth/cred" ) // ---------- Subcommand: password ------------------------------------------- func cmdPassword(fs *flag.FlagSet) (int, error) { if fs.NArg() == 0 { fmt.Fprintln(os.Stderr, "User name and user zettel identification missing") |
︙ | ︙ | |||
59 60 61 62 63 64 65 | ident := fs.Arg(0) hashedPassword, err := cred.HashCredential(zid, ident, password) if err != nil { return 2, err } fmt.Printf("%v: %s\n%v: %s\n", | | | | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | ident := fs.Arg(0) hashedPassword, err := cred.HashCredential(zid, ident, password) if err != nil { return 2, err } fmt.Printf("%v: %s\n%v: %s\n", meta.KeyCredential, hashedPassword, meta.KeyUserID, ident, ) return 0, nil } func getPassword(prompt string) (string, error) { fmt.Fprintf(os.Stderr, "%s: ", prompt) password, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Fprintln(os.Stderr) return string(password), err } |
Changes to cmd/cmd_run.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package cmd import ( "context" "flag" "net/http" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/server" | > < | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | package cmd import ( "context" "flag" "net/http" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/server" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { fs.String("c", "", "configuration file") fs.Uint("a", 0, "port number kernel service (0=disable)") |
︙ | ︙ |
Changes to cmd/command.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( "flag" | > > < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( "flag" "maps" "slices" "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 |
︙ | ︙ | |||
62 63 64 65 66 67 68 | // 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. | | | 63 64 65 66 67 68 69 70 | // 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 slices.Sorted(maps.Keys(commands)) } |
Changes to cmd/main.go.
︙ | ︙ | |||
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | "os" "runtime/debug" "strconv" "strings" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/input" "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/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" | > > < < | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | "os" "runtime/debug" "strconv" "strings" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "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/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) const strRunSimple = "run-simple" func init() { RegisterCommand(Command{ Name: "help", |
︙ | ︙ | |||
121 122 123 124 125 126 127 | } func getConfig(fs *flag.FlagSet) (string, *meta.Meta) { filename, cfg := fetchStartupConfiguration(fs) fs.Visit(func(flg *flag.Flag) { switch flg.Name { case "p": | | | | | | | | | | | 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 | } func getConfig(fs *flag.FlagSet) (string, *meta.Meta) { filename, cfg := fetchStartupConfiguration(fs) fs.Visit(func(flg *flag.Flag) { switch flg.Name { case "p": cfg.Set(keyListenAddr, meta.Value(net.JoinHostPort("127.0.0.1", flg.Value.String()))) case "a": cfg.Set(keyAdminPort, meta.Value(flg.Value.String())) case "d": val := flg.Value.String() if strings.HasPrefix(val, "/") { val = "dir://" + val } else { val = "dir:" + val } deleteConfiguredBoxes(cfg) cfg.Set(keyBoxOneURI, meta.Value(val)) case "l": cfg.Set(keyLogLevel, meta.Value(flg.Value.String())) case "debug": cfg.Set(keyDebug, meta.Value(flg.Value.String())) case "r": cfg.Set(keyReadOnly, meta.Value(flg.Value.String())) case "v": cfg.Set(keyVerbose, meta.Value(flg.Value.String())) } }) return filename, cfg } func deleteConfiguredBoxes(cfg *meta.Meta) { for key := range cfg.Rest() { if strings.HasPrefix(key, kernel.BoxURIs) { cfg.Delete(key) } } } const ( keyAdminPort = "admin-port" |
︙ | ︙ | |||
182 183 184 185 186 187 188 | func setServiceConfig(cfg *meta.Meta) bool { debugMode := cfg.GetBool(keyDebug) if debugMode && kernel.Main.GetKernelLogger().Level() > logger.DebugLevel { kernel.Main.SetLogLevel(logger.DebugLevel.String()) } if logLevel, found := cfg.Get(keyLogLevel); found { | | | 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | func setServiceConfig(cfg *meta.Meta) bool { debugMode := cfg.GetBool(keyDebug) if debugMode && kernel.Main.GetKernelLogger().Level() > logger.DebugLevel { kernel.Main.SetLogLevel(logger.DebugLevel.String()) } if logLevel, found := cfg.Get(keyLogLevel); found { kernel.Main.SetLogLevel(string(logLevel)) } err := setConfigValue(nil, kernel.CoreService, kernel.CoreDebug, debugMode) err = setConfigValue(err, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) if val, found := cfg.Get(keyAdminPort); found { err = setConfigValue(err, kernel.CoreService, kernel.CorePort, val) } |
︙ | ︙ | |||
277 278 279 280 281 282 283 | 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 } cfg.Delete("secret") | | | | 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | 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 } cfg.Delete("secret") secretHash := fmt.Sprintf("%x", sha256.Sum256([]byte(string(secret)))) kern.SetCreators( func(readonly bool, owner id.Zid) (auth.Manager, error) { return impl.New(readonly, owner, secretHash), nil }, createManager, func(srv server.Server, plMgr box.Manager, authMgr auth.Manager, rtConfig config.Config) error { setupRouting(srv, plMgr, authMgr, rtConfig) return nil }, ) |
︙ | ︙ |
Changes to collect/collect.go.
︙ | ︙ | |||
22 23 24 25 26 27 28 | Embeds []*ast.Reference // list of all embedded material Cites []*ast.CiteNode // list of all referenced citations } // References returns all references mentioned in the given zettel. This also // includes references to images. func References(zn *ast.ZettelNode) (s Summary) { | | | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | Embeds []*ast.Reference // list of all embedded material Cites []*ast.CiteNode // list of all referenced citations } // References returns all references mentioned in the given zettel. This also // includes references to images. func References(zn *ast.ZettelNode) (s Summary) { ast.Walk(&s, &zn.BlocksAST) return s } // Visit all node to collect data for the summary. func (s *Summary) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.TranscludeNode: |
︙ | ︙ |
Changes to collect/collect_test.go.
︙ | ︙ | |||
35 36 37 38 39 40 41 | summary := collect.References(zn) if summary.Links != nil || summary.Embeds != nil { t.Error("No links/images expected, but got:", summary.Links, "and", summary.Embeds) } intNode := &ast.LinkNode{Ref: parseRef("01234567890123")} para := ast.CreateParaNode(intNode, &ast.LinkNode{Ref: parseRef("https://zettelstore.de/z")}) | | | | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | summary := collect.References(zn) if summary.Links != nil || summary.Embeds != nil { t.Error("No links/images expected, but got:", summary.Links, "and", summary.Embeds) } intNode := &ast.LinkNode{Ref: parseRef("01234567890123")} para := ast.CreateParaNode(intNode, &ast.LinkNode{Ref: parseRef("https://zettelstore.de/z")}) zn.BlocksAST = ast.BlockSlice{para} summary = collect.References(zn) if summary.Links == nil || summary.Embeds != nil { t.Error("Links expected, and no images, but got:", summary.Links, "and", summary.Embeds) } para.Inlines = append(para.Inlines, intNode) summary = collect.References(zn) if cnt := len(summary.Links); cnt != 3 { t.Error("Link count does not work. Expected: 3, got", summary.Links) } } func TestEmbed(t *testing.T) { t.Parallel() zn := &ast.ZettelNode{ BlocksAST: ast.BlockSlice{ast.CreateParaNode(&ast.EmbedRefNode{Ref: parseRef("12345678901234")})}, } summary := collect.References(zn) if summary.Embeds == nil { t.Error("Only image expected, but got: ", summary.Embeds) } } |
Changes to collect/order.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/ast" | | | | | | | | | | < | < < | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/ast" // Order of internal links within the given zettel. func Order(zn *ast.ZettelNode) (result []*ast.LinkNode) { for _, bn := range zn.BlocksAST { ln, ok := bn.(*ast.NestedListNode) if !ok { continue } switch ln.Kind { case ast.NestedListOrdered, ast.NestedListUnordered: for _, is := range ln.Items { if ln := firstItemZettelLink(is); ln != nil { result = append(result, ln) } } } } return result } func firstItemZettelLink(is ast.ItemSlice) *ast.LinkNode { for _, in := range is { if pn, ok := in.(*ast.ParaNode); ok { if ln := firstInlineZettelLink(pn.Inlines); ln != nil { return ln } } } return nil } func firstInlineZettelLink(is ast.InlineSlice) (result *ast.LinkNode) { for _, inl := range is { switch in := inl.(type) { case *ast.LinkNode: return in case *ast.EmbedRefNode: result = firstInlineZettelLink(in.Inlines) case *ast.EmbedBLOBNode: result = firstInlineZettelLink(in.Inlines) case *ast.CiteNode: result = firstInlineZettelLink(in.Inlines) case *ast.FootnoteNode: // Ignore references in footnotes continue case *ast.FormatNode: result = firstInlineZettelLink(in.Inlines) default: continue } if result != nil { return result } } |
︙ | ︙ |
Changes to config/config.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | // Package config provides functions to retrieve runtime configuration data. package config import ( "context" | | | | > | | | > | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // Package config provides functions to retrieve runtime configuration data. package config import ( "context" "t73f.de/r/zsc/domain/meta" ) // Key values that are supported by Config.Get const ( KeyFooterZettel = "footer-zettel" KeyHomeZettel = "home-zettel" KeyListsMenuZettel = "lists-menu-zettel" KeyShowBackLinks = "show-back-links" KeyShowFolgeLinks = "show-folge-links" KeyShowSequelLinks = "show-sequel-links" KeyShowSubordinateLinks = "show-subordinate-links" KeyShowSuccessorLinks = "show-successor-links" // api.KeyLang ) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig |
︙ | ︙ | |||
51 52 53 54 55 56 57 | // GetMaxTransclusions returns the maximum number of indirect transclusions. GetMaxTransclusions() int // GetYAMLHeader returns the current value of the "yaml-header" key. GetYAMLHeader() bool // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. | | | 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | // GetMaxTransclusions returns the maximum number of indirect transclusions. GetMaxTransclusions() int // GetYAMLHeader returns the current value of the "yaml-header" key. GetYAMLHeader() bool // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. GetZettelFileSyntax() []meta.Value } // AuthConfig are relevant configuration values for authentication. type AuthConfig interface { // GetSimpleMode returns true if system tuns in simple-mode. GetSimpleMode() bool |
︙ | ︙ | |||
94 95 96 97 98 99 100 | return "secure" } // AllowHTML returns true, if the given HTML insecurity level matches the given syntax value. func (hi HTMLInsecurity) AllowHTML(syntax string) bool { switch hi { case SyntaxHTML: | | | > | | | 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | return "secure" } // AllowHTML returns true, if the given HTML insecurity level matches the given syntax value. func (hi HTMLInsecurity) AllowHTML(syntax string) bool { switch hi { case SyntaxHTML: return syntax == meta.ValueSyntaxHTML case MarkdownHTML: return syntax == meta.ValueSyntaxHTML || syntax == meta.ValueSyntaxMarkdown || syntax == meta.ValueSyntaxMD case ZettelmarkupHTML: return syntax == meta.ValueSyntaxZmk || syntax == meta.ValueSyntaxHTML || syntax == meta.ValueSyntaxMarkdown || syntax == meta.ValueSyntaxMD } return false } |
Changes to docs/manual/00001001000000.zettel.
1 2 3 4 5 6 | id: 00001001000000 title: Introduction to the Zettelstore role: manual tags: #introduction #manual #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 00001001000000 title: Introduction to the Zettelstore role: manual tags: #introduction #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102181246 [[Personal knowledge management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] involves collecting, classifying, storing, searching, retrieving, assessing, evaluating, and sharing knowledge as a daily activity. It's done by most individuals, not necessarily as part of their main business. It's essential for knowledge workers, such as students, researchers, lecturers, software developers, scientists, engineers, architects, etc. Many hobbyists build up a significant amount of knowledge, even if they do not need to think for a living. Personal knowledge management can be seen as a prerequisite for many kinds of collaboration. Zettelstore is software that collects and relates your notes (""zettel"") to represent and enhance your knowledge, supporting the ""[[Zettelkasten method|https://en.wikipedia.org/wiki/Zettelkasten]]"". The method is based on creating many individual notes, each containing one idea or piece of information, which are related to each other. Since knowledge is typically built up gradually, one major focus is a long-term store of these notes, hence the name ""Zettelstore"". |
Changes to docs/manual/00001002000000.zettel.
1 2 3 4 5 6 | id: 00001002000000 title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | id: 00001002000000 title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102191434 Zettelstore supports the following design goals: ; Longevity of stored notes / zettel : Every zettel you create should be readable without the help of any tool, even without Zettelstore. : It should not hard to write other software that works with your zettel. : Normal zettel should be stored in a single file. If this is not possible: at most in two files: one for the metadata, one for the content. The only exceptions are [[predefined zettel|00001005090000]] stored in the Zettelstore executable. : There is no additional database. ; Single user : All zettel belong to you, only to you. Zettelstore provides its services only to one person: you. If the computer running Zettelstore is securely configured, there should be no risk that others are able to read or update your zettel. : If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel. ; Ease of installation : If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate file directory and start working. : Upgrading the software is done just by replacing the executable with a newer one. ; Ease of operation : There is only one executable for Zettelstore and one directory, where your zettel are stored. : If you decide to use multiple directories, you are free to configure Zettelstore appropriately. ; Multiple modes of operation : You can use Zettelstore as a standalone software on your device, but you are not restricted to it. : You can install the software on a central server, or you can install it on all your devices with no restrictions on how to synchronize your zettel. ; Multiple user interfaces : Zettelstore provides a default [[web-based user interface|00001014000000]]. Anyone can provide alternative user interfaces, e.g. for special purposes. ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : External software can be written to deeply analyze your zettel and the structures they form. ; Security by default : Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks. : If you know what you are doing, Zettelstore allows you to relax some security-related preferences. However, even in this case, the more secure way is chosen. : The Zettelstore software uses a minimal design and uses other software dependencies only is essential needed. : There will be no plugin mechanism, which allows external software to control the inner workings of the Zettelstore software. |
Changes to docs/manual/00001003000000.zettel.
1 2 3 4 5 6 | id: 00001003000000 title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | id: 00001003000000 title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102185359 === The curious user You just want to check out the Zettelstore software * Grab the appropriate executable and copy it to any directory * Start the Zettelstore software, e.g. with a double click[^On Windows and macOS, the operating system tries to protect you from possible malicious software. If you encounter a problem, please refer to the [[Troubleshooting|00001018000000]]Â page.] * A sub-directory ""zettel"" will be created in the directory where you put the executable. It will contain your future zettel. * Open the URI [[http://localhost:23123]] with your web browser. A mostly empty Zettelstore is presented. There will be a zettel titled ""[[Home|00010000000000]]"" that contains some helpful information. * Please read the instructions for the [[web-based user interface|00001014000000]] and learn about the various ways to write zettel. * If you restart your device, please make sure to start your Zettelstore again. === The intermediate user You have already tried the Zettelstore software and now you want to use it permanently. Zettelstore should start automatically when you log into your computer. Please follow [[these instructions|00001003300000]]. === The server administrator You want to provide a shared Zettelstore that can be used from your various devices. Installing Zettelstore as a Linux service is not that hard. |
︙ | ︙ |
Changes to docs/manual/00001003300000.zettel.
1 2 3 4 5 6 | id: 00001003300000 title: Zettelstore installation for the intermediate user role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20211125191727 | | | | > > > > > > | 1 2 3 4 5 6 7 8 9 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: 00001003300000 title: Zettelstore installation for the intermediate user role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20211125191727 modified: 20250227220050 You have already tried the Zettelstore software and now you want to use it permanently. Zettelstore should start automatically when you log into your computer. * Grab the appropriate executable and copy it into the appropriate directory * If you want to place your zettel into another directory, or if you want more than one [[Zettelstore box|00001004011200]], or if you want to [[enable authentication|00001010040100]], or if you want to tweak your Zettelstore in some other way, create an appropriate [[startup configuration file|00001004010000]]. * If you created a startup configuration file, you need to test it: ** Start a command line prompt for your operating system. ** Navigate to the directory, where you placed the Zettelstore executable. In most cases, this is done by the command ``cd DIR``, where ''DIR'' denotes the directory, where you placed the executable. ** Start the Zettelstore: *** On Windows execute the command ``zettelstore.exe run -c CONFIG_FILE`` *** On macOS execute the command ``./zettelstore run -c CONFIG_FILE`` *** On Linux execute the command ``./zettelstore run -c CONFIG_FILE`` ** In all cases ''CONFIG_FILE'' must be replaced with the file name where you wrote the startup configuration. ** If you encounter some error messages, update the startup configuration, and try again. * Depending on your operating system, there are different ways to register Zettelstore to start automatically: ** [[Windows|00001003305000]] ** [[macOS|00001003310000]] ** [[Linux|00001003315000]] A word of caution: Never expose Zettelstore directly to the Internet. As a personal service, Zettelstore is not designed to handle all aspects of the open web. For instance, it lacks support for certificate handling, which is necessary for encrypted HTTP connections. To ensure security, [[install Zettelstore on a server|00001003600000]] and place it behind a proxy server designed for Internet exposure. For more details, see: [[External server to encrypt message transport|00001010090100]]. |
Changes to docs/manual/00001003315000.zettel.
1 2 3 4 5 6 | id: 00001003315000 title: Enable Zettelstore to start automatically on Linux role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20220114181521 | | | | 1 2 3 4 5 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: 00001003315000 title: Enable Zettelstore to start automatically on Linux role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20220114181521 modified: 20250102221716 Since there is no such thing as the one Linux, there are too many different ways to automatically start Zettelstore. * One way is to interpret your Linux desktop system as a server and use the [[recipe to install Zettelstore on a server|00001003600000]]. ** See below for a lighter alternative. * If you are using the [[Gnome Desktop|https://www.gnome.org/]], you could use the tool [[Tweak|https://wiki.gnome.org/action/show/Apps/Tweaks]] (formerly known as ""GNOME Tweak Tool"" or just ""Tweak Tool""). It allows to specify application that should run on startup / login. * [[KDE|https://kde.org/]] provides a system setting to [[autostart|https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/autostart/]] applications. * [[Xfce|https://xfce.org/]] allows to specify [[autostart applications|https://docs.xfce.org/xfce/xfce4-session/preferences#application_autostart]]. * [[LXDE|https://www.lxde.org/]] uses [[LXSession Edit|https://wiki.lxde.org/en/LXSession_Edit]] to allow users to specify autostart applications. If you use a different desktop environment, it often helps to to provide its name and the string ""autostart"" to google for it with the search engine of your choice. Yet another way is to make use of the middleware that is provided. Many Linux distributions make use of [[systemd|https://systemd.io/]], which allows to start processes on behalf of a user. On the command line, adapt the following script to your own needs and execute it: ``` # mkdir -p "$HOME/.config/systemd/user" # cd "$HOME/.config/systemd/user" # cat <<__EOF__ > zettelstore.service [Unit] Description=Zettelstore |
︙ | ︙ |
Changes to docs/manual/00001003600000.zettel.
1 2 3 4 5 6 | id: 00001003600000 title: Installation of Zettelstore on a server role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20211125191727 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001003600000 title: Installation of Zettelstore on a server role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20211125191727 modified: 20250227220033 You want to provide a shared Zettelstore that can be used from your various devices. Installing Zettelstore as a Linux service is not that hard. Grab the appropriate executable and copy it into the appropriate directory: ```sh # sudo mv zettelstore /usr/local/bin/zettelstore |
︙ | ︙ | |||
48 49 50 51 52 53 54 | # sudo systemctl start zettelstore ``` Use the commands ``systemctl``{=sh} and ``journalctl``{=sh} to manage the service, e.g.: ```sh # sudo systemctl status zettelstore # verify that it is running # sudo journalctl -u zettelstore # obtain the output of the running zettelstore ``` | > > > > > > | 48 49 50 51 52 53 54 55 56 57 58 59 60 | # sudo systemctl start zettelstore ``` Use the commands ``systemctl``{=sh} and ``journalctl``{=sh} to manage the service, e.g.: ```sh # sudo systemctl status zettelstore # verify that it is running # sudo journalctl -u zettelstore # obtain the output of the running zettelstore ``` A word of caution: Never expose Zettelstore directly to the Internet. As a personal service, Zettelstore is not designed to handle all aspects of the open web. For instance, it lacks support for certificate handling, which is necessary for encrypted HTTP connections. To ensure security, place Zettelstore behind a proxy server designed for Internet exposure. For more details, see: [[External server to encrypt message transport|00001010090100]]. |
Changes to docs/manual/00001004000000.zettel.
1 2 3 4 5 6 | id: 00001004000000 title: Configuration of Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001004000000 title: Configuration of Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102181034 There are several levels to change the behavior and/or the appearance of Zettelstore. # The first level is the way to start Zettelstore services and to manage it via command line (and, in part, via a graphical user interface). #* [[Command line parameters|00001004050000]] # As an intermediate user, you usually want to have more control over how Zettelstore is started. This may include the URI under which your Zettelstore is accessible, or the directories in which your Zettel are stored. You may want to permanently store the command line parameters so that you don't have to specify them every time you start Zettelstore. #* [[Zettelstore startup configuration|00001004010000]] |
︙ | ︙ |
Changes to docs/manual/00001004010000.zettel.
1 2 3 4 5 6 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001004010000 title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102180346 The configuration file, specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These cannot be stored in a [[configuration zettel|00001004020000]] because they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore needs to know in advance on which network address it 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. |
︙ | ︙ | |||
22 23 24 25 26 27 28 | A value of ""0"" (the default) disables it. The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]]. On most operating systems, the value must be greater than ""1024"" unless you start Zettelstore with the full privileges of a system administrator (which is not recommended). Default: ""0"" ; [!asset-dir|''asset-dir''] | | | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | A value of ""0"" (the default) disables it. The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]]. On most operating systems, the value must be greater than ""1024"" unless you start Zettelstore with the full privileges of a system administrator (which is not recommended). Default: ""0"" ; [!asset-dir|''asset-dir''] : Allows to specify a directory whose files are allowed to be transferred directly with the help of the web server. The URL prefix for these files is ''/assets/''. You can use this if you want to transfer files that are too large for a zettel, such as presentation, PDF, music or video files. Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the very special case that the directory is one of the configured [[boxes|#box-uri-x]].] If you specify only the URL prefix in your web client, the contents of the directory are listed. To avoid this, create an empty file in the directory named ""index.html"". |
︙ | ︙ |
Changes to docs/manual/00001004011200.zettel.
1 2 3 4 5 6 | id: 00001004011200 title: Zettelstore boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001004011200 title: Zettelstore boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102185551 A Zettelstore must store its zettel somehow and somewhere. In most cases you want to store your zettel as files in a directory. Under certain circumstances you may want to store your zettel elsewhere. An example is the [[predefined zettel|00001005090000]] that come with a Zettelstore. They are stored within the software itself. In another situation you may want to store your zettel volatile, e.g. if you want to provide a sandbox for experimenting. To cope with these (and more) situations, you configure Zettelstore to use one or more __boxes__. This is done via the ''box-uri-X'' keys of the [[startup configuration|00001004010000#box-uri-X]] (X is a number). Boxes are specified using special [[URIs|https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]], somehow similar to web addresses. |
︙ | ︙ |
Changes to docs/manual/00001004011400.zettel.
1 2 3 4 5 6 | id: 00001004011400 title: Configure file directory boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001004011400 title: Configure file directory boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102180416 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 workers 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. |
︙ | ︙ |
Changes to docs/manual/00001004011600.zettel.
1 2 3 4 5 6 | id: 00001004011600 title: Configure memory boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20220307112918 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | id: 00001004011600 title: Configure memory boxes role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20220307112918 modified: 20250102222236 Under most circumstances, it is preferable to further configure a memory box. This is done by appending query parameters after the base box URI ''mem:''. The following parameters are supported: |= Parameter:|Description|Default value:|Maximum value: |max-bytes|Maximum number of bytes the box will store|65535|1073741824 (1 GiB) |max-zettel|Maximum number of zettel|127|65535 The default values are somehow arbitrarily, but applicable for many use cases. While the number of zettel should be easily calculable by a user, the number of bytes might be a little more difficult. Metadata consumes 6 bytes for the zettel identifier and for each metadata value one byte for the separator, plus the length of key and data. Then size of the content is its size in bytes. For text content, its the number of bytes for its UTF-8 encoding. If one of the limits are exceeded, Zettelstore will give an error indication, based on the HTTP status code 507. |
Changes to docs/manual/00001004020000.zettel.
1 | id: 00001004020000 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001004020000 title: Configure a running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250131151530 show-back-links: false 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. Some of them can be overwritten in an [[user zettel|00001010040200]], a subset of those may be overwritten in zettel that is currently used. See the full list of [[metadata that may be overwritten|00001004020200]]. |
︙ | ︙ | |||
37 38 39 40 41 42 43 | Zettel content, delivered via the [[API|00001012000000]] as symbolic expressions, etc. is not affected. If the zettel identifier is invalid or references a zettel that could not be read (possibly because of a limited [[visibility setting|00001010070200]]), nothing is written as the footer. May be [[overwritten|00001004020200]] in a user zettel. Default: (an invalid zettel identifier) ; [!home-zettel|''home-zettel''] | | < | > > > > > > > > > | | | 37 38 39 40 41 42 43 44 45 46 47 48 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 | Zettel content, delivered via the [[API|00001012000000]] as symbolic expressions, etc. is not affected. If the zettel identifier is invalid or references a zettel that could not be read (possibly because of a limited [[visibility setting|00001010070200]]), nothing is written as the footer. May be [[overwritten|00001004020200]] in a user zettel. Default: (an invalid zettel identifier) ; [!home-zettel|''home-zettel''] : Specifies the identifier of the zettel that should be presented for the default view / home view. If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown. May be [[overwritten|00001004020200]] in a user zettel. ; [!lang|''lang''] : Language to be used when displaying content. Default: ""en"". This value is used as a default value, if it is not set in a user's zettel or in a zettel. It is also used to specify the language for all non-zettel content, e.g. lists or search results. Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]]. ; [!lists-menu-zettel|''lists-menu-zettel''] : Identifier of the zettel that specifies entries of the ""Lists"" menu (in the [[Web user interface|00001014000000]]). Every list item with a [[link|00001007040310]] is translated into a menu entry. If not given or if the identifier does not identify a zettel, or the zettel is not accessible for the current user, the zettel with the identifier ''00000000080001'' is used. May be [[overwritten|00001004020200]] in a user zettel. Default: ""00000000080001"". ; [!max-transclusions|''max-transclusions''] : Maximum number of indirect transclusion. This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]]. Default: ""1024"". ; [!show-back-links|''show-back-links''], [!show-folge-links|''show-folge-links''], [!show-sequel-links|''show-sequel-links''], [!show-subordinate-links|''show-subordinate-links''], [!show-successor-links|''show-successor-links''] : When displaying a zettel in the web user interface, references to other zettel are normally shown below the content of the zettel. This affects the metadata keys [[''back''|00001006020000#back]], [[''folge''|00001006020000#folge]], [[''sequel''|00001006020000#sequel]], [[''subordinates''|00001006020000#subordinates]], and [[''successors''|00001006020000#successors]]. These configuration keys may be used to show, not to show, or to close the list of referenced zettel. Allowed values are: ""false"" (will not show the list), ""close"" (will show the list closed), and ""open"" / """" (will show the list). Default: """". |
︙ | ︙ |
Changes to docs/manual/00001004020200.zettel.
1 2 3 4 5 6 | id: 00001004020200 title: Runtime configuration data that may be user specific or zettel specific role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20221205155521 | | | > > | 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: 00001004020200 title: Runtime configuration data that may be user specific or zettel specific role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20221205155521 modified: 20250131151259 Some metadata of the [[runtime configuration|00001004020000]] may be overwritten in an [[user zettel|00001010040200]]. A subset of those may be overwritten in the zettel that is currently used. This allows to specify user specific or zettel specific behavior. The following metadata keys are supported to provide a more specific behavior: |=Key|User:|Zettel:|Remarks |[[''footer-zettel''|00001004020000#footer-zettel]]|Y|N| |[[''home-zettel''|00001004020000#home-zettel]]|Y|N| |[[''lang''|00001004020000#lang]]|Y|Y|Making it user-specific could make zettel for other user less useful |[[''lists-menu-zettel''|00001004020000#lists-menu-zettel]]|Y|N| |[[''show-back-links''|00001004020000#show-back-links]]|Y|Y| |[[''show-folge-links''|00001004020000#show-folge-links]]|Y|Y| |[[''show-sequel-links''|00001004020000#show-sequel-links]]|Y|Y| |[[''show-subordinate-links''|00001004020000#show-subordinate-links]]|Y|Y| |[[''show-successor-links''|00001004020000#show-successor-links]]|Y|Y| |
Changes to docs/manual/00001004050000.zettel.
1 2 3 4 5 6 | id: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | id: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102174436 Zettelstore is not just a service that provides services of a zettelkasten. It allows some tasks to be executed at the command line. Typically, the task (""sub-command"") will be given at the command line as the first parameter. If no parameter is given, the Zettelstore is called as ``` zettelstore ``` This is equivalent to call it this way: ```sh mkdir -p ./zettel zettelstore run -d ./zettel -c ./.zscfg ``` Typically this is done by starting Zettelstore via a graphical user interface by double-clicking its file icon. === Sub-commands * [[``zettelstore help``|00001004050200]] lists all available sub-commands. * [[``zettelstore version``|00001004050400]] to display version information of Zettelstore. * [[``zettelstore run``|00001004051000]] to start the Zettelstore service. * [[``zettelstore run-simple``|00001004051100]] is typically called, when you start Zettelstore by double-clicking in your GUI. * [[``zettelstore file``|00001004051200]] to render files manually without activated/running Zettelstore services. * [[``zettelstore password``|00001004051400]] to calculate data for [[user authentication|00001010040200]]. Every sub-command allows the following command line options: ; [!h|''-h''] (or ''--help'') : Does not execute the sub-command, but shows allowed command line options (except ''-h'' / ''--help''). ; [!l|''-l LOGSPEC''] |
︙ | ︙ |
Changes to docs/manual/00001004051100.zettel.
1 2 3 4 5 6 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102221633 === ``zettelstore run-simple`` This sub-command is implicitly called, when a user starts Zettelstore by double-clicking on its GUI icon. It is a 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 exist. In contrast to the ''run'' sub-command, other command line parameter are not allowed. ``` zettelstore run-simple [-d DIR] ``` ; [!d|''-d DIR''] |
︙ | ︙ |
Changes to docs/manual/00001004051400.zettel.
1 2 3 4 5 6 | id: 00001004051400 title: The ''password'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | id: 00001004051400 title: The ''password'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102221851 This sub-command is used to create a hashed password for users to be authenticated. It reads a password from standard input (two times, both must be equal) and writes the hashed password to standard output. The general usage is: ``` zettelstore password IDENT ZETTEL-ID ``` ``IDENT`` is the identification for the user that should be authenticated. ``ZETTEL-ID`` is the [[identifier of the zettel|00001006050000]] that later acts as a user zettel. See [[Creating a user zettel|00001010040200]] for some background information. An example: ``` # zettelstore password bob 20200911115600 Password: Again: credential: $2a$10$1q92v1Ya8Too5HD/4rKpPuCP8fZTYPochsC6DcY1T4JKwhSx8uLu6 user-id: bob ``` This will produce a hashed password (""credential"") for the new user ""bob"" to be stored in zettel ""20200911115600"". You should copy the relevant output to the zettel of the user to be secured, especially by setting the meta keys ''credential'' and ''user-id'' to the copied values. Please note that the generated hashed password is tied to the given user identification (''user-id'') and to the identifier of its zettel. Changing one of these will prevent the user from being authenticated with the given password. In this case you have to re-run this sub-command. |
Changes to docs/manual/00001004100000.zettel.
1 2 3 4 5 6 | id: 00001004100000 title: Zettelstore Administrator Console role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210510141304 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | id: 00001004100000 title: Zettelstore Administrator Console role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210510141304 modified: 20250102212543 The administrator console is a service that is accessible only on the same computer on which Zettelstore is running. It allows an experienced user to monitor and control some of the inner workings of Zettelstore. You enable the administrator console by specifying a TCP port number greater than zero (better: greater than 1024) for it, either via the [[command-line parameter ''-a''|00001004051000#a]] or via the ''admin-port'' key of the [[startup configuration file|00001004010000#admin-port]]. After you enable the administrator console, you can use tools such as [[PuTTY|https://www.chiark.greenend.org.uk/~sgtatham/putty/]] or other telnet software to connect to the administrator console. 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 untrusted 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/00001004101000.zettel.
1 2 3 4 5 6 | id: 00001004101000 title: List of supported commands of the administrator console role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210510141304 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001004101000 title: List of supported commands of the administrator console role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210510141304 modified: 20250102190201 ; [!bye|''bye''] : Closes the connection to the administrator console. ; [!config|''config SERVICE''] : Displays all valid configuration keys for the given service. If a key ends with the hyphen-minus character (""''-''"", U+002D), the key denotes a list value. |
︙ | ︙ | |||
36 37 38 39 40 41 42 | ``get-config`` shows all current configuration data. ``get-config SERVICE`` shows only the current configuration data of the given service. ``get-config SERVICE KEY`` shows the current configuration data for the given service and key. ; [!header|''header''] | | | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | ``get-config`` shows all current configuration data. ``get-config SERVICE`` shows only the current configuration data of the given service. ``get-config SERVICE KEY`` shows the current configuration data for the given service and key. ; [!header|''header''] : Toggles the header mode, where each table is shown with a header nor not. ; [!log-level|''log-level''] : Displays or sets the [[logging level|00001004059700]] for the kernel or a service. ``log-level`` shows all known log level. ``log-level NAME`` shows log level for the given service or for the kernel. |
︙ | ︙ | |||
75 76 77 78 79 80 81 | It may be removed without any further notice at any time. In most cases, it is a tool for software developers to optimize Zettelstore's internal workings. ; [!refresh|''refresh''] : Refresh all internal data about zettel. ; [!restart|''restart SERVICE''] : Restart the given service and all other that depend on this. ; [!services|''services''] | | | 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | It may be removed without any further notice at any time. In most cases, it is a tool for software developers to optimize Zettelstore's internal workings. ; [!refresh|''refresh''] : Refresh all internal data about zettel. ; [!restart|''restart SERVICE''] : Restart the given service and all other that depend on this. ; [!services|''services''] : Displays a list of all available services and their current status. ; [!set-config|''set-config SERVICE KEY VALUE''] : Sets a single configuration value for the next configuration of a given service. It will become effective if the service is restarted. If the key specifies a list value, all other list values with a number greater than the given key are deleted. You can use the special number ""0"" to delete all values. E.g. ``set-config box box-uri-0 any_text`` will remove all values of the list __box-uri-__. |
︙ | ︙ |
Changes to docs/manual/00001005000000.zettel.
1 2 3 4 5 6 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001005000000 title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102191502 Zettelstore is a software that manages your zettel. Since every zettel must be readable without any special tool, most zettel have to be stored as ordinary files within specific directories. Typically, file names and file content must comply with specific rules so that Zettelstore can manage them. If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions. Zettelstore provides additional services to the user. Via the built-in [[web user interface|00001014000000]] you can work with zettel in various ways. For example, you are able to list zettel, to create new zettel, to edit them, or to delete them. You can view zettel details and relations between zettel. In addition, Zettelstore provides an ""application programming interface"" ([[API|00001012000000]]) that allows other software to communicate with the Zettelstore. Zettelstore becomes extensible by external software. For example, a more sophisticated user interface could be built, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel. |
︙ | ︙ | |||
37 38 39 40 41 42 43 | It is allowed that the file name contains other characters after the 14 digits. These are ignored by Zettelstore. Two filename extensions are used by Zettelstore: # ''.zettel'' is a format that stores metadata and content together in one file, # the empty file extension is used, when the content must be stored in its own file, e.g. image data; | | | | | | | | | 37 38 39 40 41 42 43 44 45 46 47 48 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 | It is allowed that the file name contains other characters after the 14 digits. These are ignored by Zettelstore. Two filename extensions are used by Zettelstore: # ''.zettel'' is a format that stores metadata and content together in one file, # 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 contains 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 stored. 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 these files exist. 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 stored 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 built-in [[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 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. If you want to read the original zettel, you have to delete the zettel (which removes it from the file directory). Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software. * [[List of predefined zettel|00001005090000]] === Boxes: alternative ways to store zettel As described above, a zettel may be stored either as a file inside a directory or within the Zettelstore software itself. Zettelstore allows other ways to store zettel by providing an abstraction called __box__.[^Formerly, zettel were stored physically in boxes, often made of wood.] A file directory which stores zettel is called a ""directory box"". But zettel may be also stored in a ZIP file, which is called ""file box"". For testing purposes, zettel may be stored in volatile memory (called __RAM__). This way is called ""memory box"". Other types of boxes could be added to Zettelstore. What about a ""remote Zettelstore box""? |
Changes to docs/manual/00001005090000.zettel.
1 2 3 4 5 6 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 | | | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 modified: 20250131151733 The following table lists all predefined zettel with their purpose. The content of most[^To be more exact: zettel with an identifier greater or equal ''00000999999900'' will have their content indexed.] of these zettel will not be indexed by Zettelstore. You will not find zettel when searched for some content, e.g. ""[[query:european]]"" will not find the [[Zettelstore License|00000000000004]]. However, metadata is always indexed, e.g. ""[[query:title:license]]"" will find the Zettelstore License zettel. |= 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 | [[00000000000008]] | Zettelstore Memory | Some statistics about main memory usage | [[00000000000009]] | Zettelstore Sx Engine | Statistics about the [[Sx|https://t73f.de/r/sx]] engine, which interprets symbolic expressions | [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the index process | [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more | [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore | [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]] | [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]] | [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view | [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]] | [[00000000010300]] | Zettelstore List Zettel HTML Template | Used when displaying a list of zettel | [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel | [[00000000010402]] | Zettelstore Info HTML Template | Layout for the information view of a specific zettel | [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text | [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel | [[00000000010700]] | Zettelstore Error HTML Template | View to show an error message | [[00000000019000]] | Zettelstore Sxn Start Code | Starting point of sxn functions to build the templates | [[00000000019990]] | Zettelstore Sxn Base Code | Base sxn functions to build the templates | [[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]] | [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid | [[00000000060010]] | zettel | [[Role zettel|00001012051800]] for the role ""[[zettel|00001006020100#zettel]]"" | [[00000000060020]] | configuration | [[Role zettel|00001012051800]] for the role ""[[confguration|00001006020100#configuration]]"" | [[00000000060030]] | role | [[Role zettel|00001012051800]] for the role ""[[role|00001006020100#role]]"" | [[00000000060040]] | tag | [[Role zettel|00001012051800]] for the role ""[[tag|00001006020100#tag]]"" | [[00000000080001]] | Lists Menu | Default items of the ""Lists"" menu; see [[lists-menu-zettel|00001004020000#lists-menu-zettel]] for customization options | [[00000000090000]] | New Menu | Contains items that should be in the zettel template menu | [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100#zettel]]"" | [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]] | [[00000000090003]] | New Tag | Template for a new [[tag zettel|00001006020100#tag]] | [[00000000090004]] | New Role | Template for a new [[role zettel|00001006020100#role]] | [[00000999999999]] | Zettelstore Application Directory | Maps application name to application specific zettel | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. In most cases, you must at least enable [[''expert-mode''|00001004020000#expert-mode]]. **Important:** All identifiers may change until a stable version of the software is released. |
Changes to docs/manual/00001006000000.zettel.
1 2 3 4 5 6 | id: 00001006000000 title: Layout of a Zettel role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001006000000 title: Layout of a Zettel role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102190828 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. |
︙ | ︙ | |||
38 39 40 41 42 43 44 | This is called ""[[parsed zettel|00001012053600]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'', but with the additional query parameter ''parseonly''. 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]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'' and specifying an encoding. 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. | | | | 38 39 40 41 42 43 44 45 46 47 48 49 | This is called ""[[parsed zettel|00001012053600]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'', but with the additional query parameter ''parseonly''. 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]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'' and specifying an encoding. 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, when 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 following the [[backward|00001006020000#backward]] metadata key of the transcluded zettel. |
Changes to docs/manual/00001006020000.zettel.
1 2 3 4 5 6 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 modified: 20250115163835 Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore. See the [[computed list of supported metadata keys|00000000000090]] for details. Most keys conform to a [[type|00001006030000]]. ; [!author|''author''] |
︙ | ︙ | |||
29 30 31 32 33 34 35 | ; [!created|''created''] : Date and time when a zettel was created through Zettelstore. If you create a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value. This is a computed value. There is no need to set it via Zettelstore. | | < < < < < < | | 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | ; [!created|''created''] : Date and time when a zettel was created through Zettelstore. If you create a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value. This is a computed value. There is no need to set it via Zettelstore. If it is not stored within a zettel, it will be computed based on the value of the [[Zettel Identifier|00001006050000]]: if it contains a value >= 19700101000000, it will be coerced to a date/time; otherwise the version time of the running software will be used. Please note that the value von ''created'' will be different (in most cases) to the value of [[''id''|#id]] / the zettel identifier, because it is exact up to the second. When calculating a zettel identifier, Zettelstore tries to set the second value to zero, if possible. ; [!credential|''credential''] : Contains the hashed password, as it was emitted by [[``zettelstore password``|00001004051400]]. It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key. It is only used for zettel with a ''role'' value of ""user"". ; [!dead|''dead''] : Property that contains all references that does __not__ identify a zettel. ; [!expire|''expire''] : A user-entered time stamp that document the point in time when the zettel should expire. When a zettel expires, Zettelstore does nothing. It is up to you to define required actions. ''expire'' is just a documentation. You could define a query and execute it regularly, for example [[query:expire? ORDER expire]]. Alternatively, a Zettelstore client software could define some actions when it detects expired zettel. ; [!folge|''folge''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''precursor''|#precursor]] value. ; [!folge-role|''folge-role''] |
︙ | ︙ | |||
87 88 89 90 91 92 93 | Basically the inverse of key [[''folge''|#folge]]. ; [!predecessor|''predecessor''] : References the zettel that contains a previous version of the content. In contrast to [[''precursor''|#precurso]] / [[''folge''|#folge]], this is a reference because of technical reasons, not because of content-related reasons. Basically the inverse of key [[''successors''|#successors]]. ; [!prequel|''prequel''] : Specifies a zettel that is conceptually a prequel zettel. | | | | 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | Basically the inverse of key [[''folge''|#folge]]. ; [!predecessor|''predecessor''] : References the zettel that contains a previous version of the content. In contrast to [[''precursor''|#precurso]] / [[''folge''|#folge]], this is a reference because of technical reasons, not because of content-related reasons. Basically the inverse of key [[''successors''|#successors]]. ; [!prequel|''prequel''] : Specifies a zettel that is conceptually a prequel zettel. This is a zettel that occurred somehow before the current zettel. ; [!published|''published''] : This property contains the timestamp of the last modification / creation of the zettel. If [[''modified''|#modified]] is set with a valid timestamp, it contains the its value. Otherwise, if [[''created''|#created]] is set with a valid timestamp, it contains the its value. Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used. In all other cases, this property is not set. It can be used for [[sorting|00001007700000]] zettel based on their publication date. |
︙ | ︙ | |||
112 113 114 115 116 117 118 119 120 121 122 123 124 125 | ; [!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. ; [!sequel|''sequel''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''prequel''|#prequel]] value. ; [!successors|''successors''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''predecessor''|#predecessor]] value. Therefore, it references all zettel that contain a new version of the content and/or metadata. In contrast to [[''folge''|#folge]], these are references because of technical reasons, not because of content-related reasons. In most cases, zettel referencing the current zettel should be updated to reference a successor zettel. The [[query reference|00001007040310]] [[query:backward? successors?]] lists all such zettel. ; [!summary|''summary''] | > > | | > > | | 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 | ; [!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. ; [!sequel|''sequel''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''prequel''|#prequel]] value. ; [!subordinates|''subordinates''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''superior''|#superior]] value. ; [!successors|''successors''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''predecessor''|#predecessor]] value. Therefore, it references all zettel that contain a new version of the content and/or metadata. In contrast to [[''folge''|#folge]], these are references because of technical reasons, not because of content-related reasons. In most cases, zettel referencing the current zettel should be updated to reference a successor zettel. The [[query reference|00001007040310]] [[query:backward? successors?]] lists all such zettel. ; [!summary|''summary''] : Summarizes the content of the zettel using plain text. ; [!superior|''superior''] : Specifies a zettel that is conceptually a superior zettel. This might be a more abstract zettel, or a zettel that should be higher in a hierarchy. ; [!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. ; [!url|''url''] : Defines a URL / URI for this zettel that possibly references external material. One use case is to specify the document that the current zettel comments on. The URL will be rendered special in the [[web user interface|00001014000000]] if you use the default template. ; [!useless-files|''useless-files''] : Contains the file names that are rejected to serve the content of a zettel. Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]]. If a zettel is deleted, these files will also be deleted. ; [!user-id|''user-id''] |
︙ | ︙ |
Changes to docs/manual/00001006020100.zettel.
1 2 3 4 5 6 | id: 00001006020100 title: Supported Zettel Roles role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | id: 00001006020100 title: Supported Zettel Roles role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 modified: 20250102175032 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 / information for the Zettelstore. Most prominent is [[00000000000100]], as described in [[00001004020000]]. ; [!manual|''manual''] : All zettel that document the inner workings of the Zettelstore software. This role is only used in this specific Zettelstore. ; [!role|''role''] : A zettel with the role ""role"" and a title, which names a [[role|00001006020000#role]], is treated as a __role zettel__. Basically, role zettel describe the role, and form a hierarchy of meta-roles. ; [!tag|''tag''] : A zettel with the role ""tag"" and a title, which names a [[tag|00001006020000#tags]], is treated as a __tag zettel__. Basically, tag zettel describe the tag, and form a hierarchy of meta-tags. ; [!zettel|''zettel''] : A zettel that contains your own thoughts. The real reason to use this software. If you adhere to the process outlined by Niklas Luhmann, a zettel could have one of the following three roles: ; [!note|''note''] |
︙ | ︙ |
Changes to docs/manual/00001006020400.zettel.
1 2 3 4 5 6 | id: 00001006020400 title: Supported values for metadata key ''read-only'' role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 | | | < | < | < | < | < | < | < | < | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | id: 00001006020400 title: Supported values for metadata key ''read-only'' role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 modified: 20250102205707 A zettel can be marked as read-only, if it contains a metadata value for key [[''read-only''|00001006020000#read-only]]. If user authentication is [[enabled|00001010040100]], it is possible to allow some users to change the zettel, depending on their [[user role|00001010070300]]. Otherwise, the read-only mark is just a binary value. === No authentication If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]] is interpreted as ""false"", anybody can modify the zettel. If the metadata value is something else (the value ""true"" is recommended), the user cannot modify the zettel through the [[web user interface|00001014000000]]. However, if the zettel is stored as a file in a [[directory box|00001004011400]], the zettel could be modified using an external editor. === Authentication enabled If there is no metadata value for key ''read-only'' or if its [[boolean value|00001006030500]] is interpreted as ""false"", anybody can modify the zettel. If the metadata value is the same as an explicit [[user role|00001010070300]], users with that role (or a role with lower rights) are not allowed to modify the zettel. ; ""reader"" : Neither an unauthenticated user nor a user with role ""reader"" is allowed to modify the zettel. Users with role ""writer"" or the owner itself still can modify the zettel. ; ""writer"" : Neither an unauthenticated user, nor users with roles ""reader"" or ""writer"" are allowed to modify the zettel. Only the owner of the Zettelstore can modify the zettel. If the metadata value is something else (one of the values ""true"" or ""owner"" is recommended), no user is allowed to modify the zettel through the [[web user interface|00001014000000]]. However, if the zettel is accessible as a file in a [[directory box|00001004011400]], the zettel could be modified using an external editor. Typically the owner of a Zettelstore has such access. |
Changes to docs/manual/00001006030000.zettel.
1 2 3 4 5 6 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 modified: 20250115172354 All [[supported metadata keys|00001006020000]] conform to a type. User-defined metadata keys conform also to a type, based on the suffix of the key. |=Suffix|Type | ''-date'' | [[Timestamp|00001006034500]] | ''-number'' | [[Number|00001006033000]] | ''-role'' | [[Word|00001006035500]] | ''-time'' | [[Timestamp|00001006034500]] | ''-url'' | [[URL|00001006035000]] | ''-zettel'' | [[Identifier|00001006032000]] | ''-zid'' | [[Identifier|00001006032000]] | ''-zids'' | [[IdentifierSet|00001006032500]] | any other suffix | [[EString|00001006031500]] The name of the metadata key is bound to the key type |
︙ | ︙ | |||
34 35 36 37 38 39 40 | * [[IdentifierSet|00001006032500]] * [[Number|00001006033000]] * [[String|00001006033500]] * [[TagSet|00001006034000]] * [[Timestamp|00001006034500]] * [[URL|00001006035000]] * [[Word|00001006035500]] | < | 33 34 35 36 37 38 39 | * [[IdentifierSet|00001006032500]] * [[Number|00001006033000]] * [[String|00001006033500]] * [[TagSet|00001006034000]] * [[Timestamp|00001006034500]] * [[URL|00001006035000]] * [[Word|00001006035500]] |
Changes to docs/manual/00001006031500.zettel.
1 2 3 4 5 6 | id: 00001006031500 title: EString Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001006031500 title: EString Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20250102164729 Values of this type are just a sequence of characters, possibly an empty sequence. An EString is the most general metadata key type, as it places no restrictions to the character sequence.[^Well, there are some minor restrictions that follow from the [[metadata syntax|00001006010000]].] === Allowed values All printable characters are allowed. === Query comparison |
︙ | ︙ |
Changes to docs/manual/00001006033000.zettel.
1 2 3 4 5 6 | id: 00001006033000 title: Number Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | id: 00001006033000 title: Number Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20250102220057 Values of this type denote a numeric integer value. === Allowed values Must be a sequence of digits (""0""--""9""), optionally prefixed with a ""-"" or a ""+"" character. === Query comparison [[Search operators|00001007705000]] for equality (""equal"" or ""not equal"", ""has"" or ""not has""), for lesser values (""less"" or ""not less""), or for greater values (""greater"" or ""not greater"") are executed by converting both the [[search value|00001007706000]] and the metadata value into integer values and then comparing them numerically. Integer values must be in the range -9223372036854775808 … 9223372036854775807. Comparisons with metadata values outside this range always return a negative match. Comparisons with search values outside this range will be executed as a comparison of the string representation values. All other comparisons (""match"", ""not match"", ""prefix"", ""not prefix"", ""suffix"", and ""not suffix"") are done on the given string representation of the number. In this case, the number ""+12"" will be treated as different to the number ""12"". === Sorting Sorting is done by comparing the numeric values. |
Changes to docs/manual/00001006034000.zettel.
1 2 3 4 5 6 | id: 00001006034000 title: TagSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001006034000 title: TagSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20250102205826 Values of this type denote a (sorted) set of tags. A set is different to a list, as no duplicate values are allowed. === Allowed values Every tag must begin with the number sign character (""''#''"", U+0023), followed by at least one printable character. Tags are separated by space characters. All characters are mapped to their lower case values. === Query comparison All comparisons are done case-sensitive, i.e. ""#hell"" will not be the prefix of ""#Hello"". |
︙ | ︙ |
Changes to docs/manual/00001006035000.zettel.
1 2 3 4 5 6 | id: 00001006035000 title: URL Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001006035000 title: URL Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 modified: 20250102205855 Values of this type denote a URL. === Allowed values All characters of a URL / URI are allowed. === Query comparison All comparisons are done case-insensitive. For example, ""hello"" is the suffix of ""http://example.com/Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. |
Deleted docs/manual/00001006036500.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001006050000.zettel.
1 2 3 4 5 6 | id: 00001006050000 title: Zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 | | | < | < | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | id: 00001006050000 title: Zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102165749 Each zettel is given a unique identifier. To some degree, the zettel identifier is part of the metadata. Basically, the identifier is given by the [[Zettelstore|00001005000000]] software. Every zettel identifier consists of 14 digits. They resemble a timestamp: the first four digits could represent the year, the next two represent the month, followed by day, hour, minute, and second. This allows to order zettel chronologically in a canonical way. In most cases the zettel identifier is the timestamp when the zettel was created. However, the Zettelstore software just checks for exactly 14 digits. Anybody is free to assign a ""non-timestamp"" identifier to a zettel, e.g. with a month part of ""35"" or with ""99"" as the last two digits. Some zettel identifier are [[reserved|00001006055000]] and should not be used otherwise. All identifiers of zettel initially provided by an empty Zettelstore begin with ""000000"", except the home zettel ''00010000000000''. Zettel identifier of this manual have been chosen to begin with ""000010"". A zettel can have any identifier that contains 14 digits and that is not in use by another zettel managed by the same Zettelstore. |
Changes to docs/manual/00001006055000.zettel.
1 2 3 4 5 6 | id: 00001006055000 title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210721105704 | | | | | | 1 2 3 4 5 6 7 8 9 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: 00001006055000 title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210721105704 modified: 20250102222416 [[Zettel identifier|00001006050000]] are typically created by examining the current date and time. By renaming the name of the underlying zettel file, you are able to provide any sequence of 14 digits. To make things easier, you must not use zettel identifier that begin with four zeroes (''0000''). All zettel provided by an empty zettelstore begin with six zeroes[^Exception: the predefined home zettel is ''00010000000000''. But you can [[configure|00001004020000#home-zettel]] another zettel with another identifier as the new home zettel.]. Zettel identifier of this manual have be chosen to begin with ''000010''. However, some external applications may need at least one defined zettel identifier to work properly. Zettel [[Zettelstore Application Directory|00000999999999]] (''00000999999999'') can be used to associate a name to a zettel identifier. For example, if your application is named ""app"", you create a metadata key ''app-zid''. Its value is the zettel identifier of the zettel that configures your application. === Reserved Zettel Identifier |= From | To | Description | 00000000000000 | 00000000000000 | This is an invalid zettel identifier | 00000000000001 | 00000999999999 | [[Predefined zettel|00001005090000]] | 00001000000000 | 00001099999999 | This [[Zettelstore manual|00001000000000]] | 00001100000000 | 00008999999999 | Reserved, do not use | 00009000000000 | 00009999999999 | Reserved for applications ==== External Applications |= From | To | Description | 00009000001000 | 00009000001999 | [[Zettel Presenter|https://zettelstore.de/contrib]], an application to display zettel as an HTML-based slideshow |
Changes to docs/manual/00001007010000.zettel.
1 2 3 4 5 6 | id: 00001007010000 title: Zettelmarkup: General Principles role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | id: 00001007010000 title: Zettelmarkup: General Principles role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250106174703 Any document can be thought of as a sequence of paragraphs and other [[block-structured elements|00001007030000]] (""blocks""), such as [[headings|00001007030300]], [[lists|00001007030200]], quotations, and code blocks. Some of these blocks can contain other blocks, for example lists may contain other lists or paragraphs. Other blocks contain [[inline-structured elements|00001007040000]] (""inlines""), such as text, [[links|00001007040310]], emphasized text, and images. With the exception of lists and tables, the markup for blocks always begins at the first position of a line with three or more identical characters. List blocks also begin at the first position of a line, but may need one or more identical character, plus a space character. [[Table blocks|00001007031000]] begin at the first position of a line with the character ""``|``"". Non-list blocks are either fully specified on that line or they span multiple lines and are delimited with the same three or more character. It depends on the block kind, whether blocks are specified on one line or on at least two lines. If a line does not begin with an explicit block element, the line is treated as a (implicit) paragraph block element that contains inline elements. This paragraph ends when a block element is detected at the beginning of a next line or when an empty line occurs. Some blocks may also contain inline elements, e.g. a heading. Inline elements mostly begin with two non-space, often identical characters. With some exceptions, two identical non-space characters begins a formatting range that is ended with the same two characters. Exceptions are: links, images, edits, comments, and both the ""en-dash"" and the ""horizontal ellipsis"". A link is given with ``[[...]]``{=zmk}, an image with ``{{...}}``{=zmk}, and an edit formatting with ``((...))``{=zmk}. An inline comment, beginning with the sequence ``%%``{=zmk}, always ends at the end of the line where it begins. The ""en-dash"" (""--"") is specified as ``--``{=zmk}, the ""horizontal ellipsis"" (""..."") as ``...``{=zmk}[^If put at the end of non-space text.]. 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 that begins with a space will be specified as ``** Text**``{=zmk}. To force the inline element formatting at the beginning of a line, ``**\\ Text**``{=zmk} should better be specified. Many block and inline elements can be refined by additional [[attributes|00001007050000]]. Attributes resemble roughly HTML attributes and are put near the corresponding elements by using the syntax ``{...}``{=zmk}. One example is to make space characters visible inside a inline literal element: ``1 + 2 = 3``{-} was specified by using the default attribute: ``\`\`1 + 2 = 3\`\`{-}``. To summarize: * With some exceptions, block-structural elements begins at the for position of a line with three identical characters. * The most important exception to this rule is the specification of lists. * If no block element is found, a paragraph with inline elements is assumed. * With some exceptions, inline-structural elements begin with two characters, quite often the same two characters. * The most important exceptions are links. * The backslash character can help to resolve possible ambiguities. * Attributes refine some block and inline elements. * Block elements have a higher priority than inline elements. These principles make automatic recognizing zettelmarkup an (relatively) easy task. By looking at the reference implementation, a moderately skilled software developer should be able to create an appropriate software in a different programming language. |
Changes to docs/manual/00001007030100.zettel.
1 2 3 4 5 6 | id: 00001007030100 title: Zettelmarkup: Description Lists role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 00001007030100 title: Zettelmarkup: Description Lists role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250102180137 A description list is a sequence of terms to be described together with the descriptions of each term. Every term can be described in multiple ways. A description term (short: __term__) is specified with one semicolon (""'';''"", U+003B) at the first position, followed by a space character and the described term, specified as a sequence of line elements. If the following lines should also be part of the term, exactly two spaces must be given at the beginning of each following line. The description of a term is given with one colon (""'':''"", U+003A) at the first position, followed by a space character and the description itself, specified as a sequence of [[inline elements|00001007040000]]. Similar to terms, following lines can also be part of the actual description, if they begin at each line with exactly two space characters. |
︙ | ︙ |
Changes to docs/manual/00001007030300.zettel.
1 2 3 4 5 6 | id: 00001007030300 title: Zettelmarkup: Headings role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007030300 title: Zettelmarkup: Headings role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250102210039 To specify a (sub-) section of a zettel, you should use the headings syntax: at the beginning of a new line type at least three equal signs (""''=''"", U+003D), plus at least one space and enter the text of the heading as [[inline elements|00001007040000]]. ```zmk === Level 1 Heading |
︙ | ︙ | |||
26 27 28 29 30 31 32 | ====== Level 4 Heading ======= Level 5 Heading ======== Level 5 Heading ::: === Notes | | | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | ====== Level 4 Heading ======= Level 5 Heading ======== Level 5 Heading ::: === Notes The heading level is translated to an HTML heading by adding 1 to the level, e.g. ``=== Level 1 Heading``{=zmk} translates to ==<h2>Level 1 Heading</h2>=={=html}. The ==<h1>=={=html} tag is rendered for the zettel title. This syntax is often used in a similar way in wiki implementation. However, trailing equal signs are __not__ removed, they are part of the heading text. If you use command line tools, you can easily create a draft table of contents with the command: ```sh grep -h '^====* ' ZETTEL_ID.zettel ``` |
Changes to docs/manual/00001007030400.zettel.
1 2 3 4 5 6 | id: 00001007030400 title: Zettelmarkup: Horizontal Rules / Thematic Break role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001007030400 title: Zettelmarkup: Horizontal Rules / Thematic Break role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250102220220 To signal a thematic break, you can specify a horizontal rule. This is done by entering at least three hyphen-minus characters (""''-''"", U+002D) at the first position of a line. You can add some [[attributes|00001007050000]], although the horizontal rule does not support the default attribute. Any other characters in this line will be ignored. If you do not enter the three hyphen-minus character at the very first position of a line, they are interpreted as [[inline elements|00001007040000]], typically as an ""en-dash"" followed by a hyphen-minus. Example: ```zmk --- ----{.zs-deprecated} ----- |
︙ | ︙ |
Changes to docs/manual/00001007030800.zettel.
1 2 3 4 5 6 | id: 00001007030800 title: Zettelmarkup: Region Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001007030800 title: Zettelmarkup: Region Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250102180106 Region blocks do 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. |
︙ | ︙ |
Changes to docs/manual/00001007030900.zettel.
1 2 3 4 5 6 | id: 00001007030900 title: Zettelmarkup: Comment Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007030900 title: Zettelmarkup: Comment Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250102222357 Comment blocks are quite similar to [[verbatim blocks|00001007030500]]: both are used to enter text that should not be interpreted. While the text entered inside a verbatim block will be processed somehow, text inside a comment block will be ignored[^Well, not completely ignored: text is read, but it will typically not rendered visible.]. Comment blocks are typically used to give some internal comments, e.g. the license of a text or some internal remarks. Comment blocks begin with at least three percent sign characters (""''%''"", U+0025) at the first position of a line. You can add some [[attributes|00001007050000]] on the beginning line of a comment block, following the initiating characters. |
︙ | ︙ | |||
30 31 32 33 34 35 36 | ``` will be completely ignored, while ```zmk %%%{-} Will be rendered %%% ``` | | | 30 31 32 33 34 35 36 37 | ``` will be completely ignored, while ```zmk %%%{-} Will be rendered %%% ``` will be rendered as some kind of comment[^This cannot be shown here, because an HTML comment will not be rendered visible; it will be in the HTML text.]. |
Changes to docs/manual/00001007031000.zettel.
1 2 3 4 5 6 | id: 00001007031000 title: Zettelmarkup: Tables role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007031000 title: Zettelmarkup: Tables role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250102210107 Tables are used to show some data in a two-dimensional fashion. In zettelmarkup, tables 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 but 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. |
︙ | ︙ | |||
29 30 31 32 33 34 35 | :::example | a1 | a2 | a3| | b1 | b2 | b3 | c1 | c2 ::: === Header row | | | 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | :::example | a1 | a2 | a3| | b1 | b2 | b3 | c1 | c2 ::: === Header row If any cell in the first row of a table contains an equal sign character (""''=''"", U+003D) as the very first character, then this first row will be interpreted as a __table header__ row. For example: ```zmk | a1 | a2 |= a3| | b1 | b2 | b3 | c1 | c2 ``` |
︙ | ︙ |
Changes to docs/manual/00001007031110.zettel.
1 2 3 4 5 6 | id: 00001007031110 title: Zettelmarkup: Zettel Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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: 00001007031110 title: Zettelmarkup: Zettel Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 modified: 20250102165258 A zettel transclusion is specified by the following sequence, starting at the first position in a line: ''{{{zettel-identifier}}}''. When evaluated, the referenced zettel is read. If it contains some transclusions itself, these will be expanded, recursively. When a recursion is detected, expansion does not take place. Instead an error message replaces the transclude specification. An error message is also given, if the zettel cannot be read or if too many transclusions are made. The maximum number of transclusion can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel. If everything went well, the referenced, expanded zettel will replace the transclusion element. For example, to include the text of the Zettel titled ""Zettel identifier"", just specify its identifier [[''00001006050000''|00001006050000]] in the transclusion element: ```zmk {{{00001006050000}}} ``` This will result in: :::example {{{00001006050000}}} ::: Please note: if the referenced zettel is changed, all transclusions will also change. This allows, for example, to create a bigger document just by transcluding smaller zettel. In addition, if a zettel __z__ transcludes a zettel __t__, but the current user is not allowed to view zettel __t__ (but zettel __z__), then the transclusion will not take place. To the current user, it seems that there was no transclusion in zettel __z__. This allows to create a zettel with content that seems to be changed, depending on the authorization of the current user. --- Any [[attributes|00001007050000]] added to the transclusion will set/overwrite the appropriate metadata of the included zettel. Of course, this applies only to those attributes, which have a valid name for a metadata key. This allows to control the evaluation of the included zettel, especially for zettel containing a diagram description. === See also [[Inline-mode transclusion|00001007040324]] does not work at the paragraph / block level, but is used for [[inline-structured elements|00001007040000]]. |
Changes to docs/manual/00001007031140.zettel.
1 2 3 4 5 6 | id: 00001007031140 title: Zettelmarkup: Query Transclusion role: manual tags: #manual #search #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007031140 title: Zettelmarkup: Query Transclusion role: manual tags: #manual #search #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 modified: 20241213153229 A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''. The line must literally start with the sequence ''{{{query:''. Everything after this prefix is interpreted as a [[query expression|00001007700000]]. When evaluated, the query expression is evaluated, often resulting in a list of [[links|00001007040310]] to zettel, matching the query expression. The result replaces the query transclusion element. |
︙ | ︙ | |||
34 35 36 37 38 39 40 | : The resulting list will be a numbered list. ; ''MINn'' (parameter) : Emit only those values with at least __n__ aggregated values. __n__ must be a positive integer, ''MIN'' must be given in upper-case letters. ; ''MAXn'' (parameter) : Emit only those values with at most __n__ aggregated values. __n__ must be a positive integer, ''MAX'' must be given in upper-case letters. | < < < < < < < < < | 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | : The resulting list will be a numbered list. ; ''MINn'' (parameter) : Emit only those values with at least __n__ aggregated values. __n__ must be a positive integer, ''MIN'' must be given in upper-case letters. ; ''MAXn'' (parameter) : Emit only those values with at most __n__ aggregated values. __n__ must be a positive integer, ''MAX'' must be given in upper-case letters. ; ''KEYS'' (aggregate) : Emit a list of all metadata keys, together with the number of zettel having the key. ; ''REDIRECT'', ''REINDEX'' (aggregate) : Will be ignored. These actions may have been copied from an existing [[API query call|00001012051400]] (or from a WebUI query), but are here superfluous (and possibly harmful). ; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or of type [[TagSet|00001006034000]] (aggregates) : Emit an aggregate of the given metadata key. |
︙ | ︙ |
Changes to docs/manual/00001007031200.zettel.
1 2 3 4 5 6 | id: 00001007031200 title: Zettelmarkup: Inline-Zettel Block role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20220201142439 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | id: 00001007031200 title: Zettelmarkup: Inline-Zettel Block role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20220201142439 modified: 20250102183744 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. Like 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]] to 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. |
︙ | ︙ |
Changes to docs/manual/00001007040000.zettel.
1 2 3 4 5 6 | id: 00001007040000 title: Zettelmarkup: Inline-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | id: 00001007040000 title: Zettelmarkup: Inline-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250102182659 Most characters you type are concerned with inline-structured elements. The content of a zettel contains in many cases just ordinary text, lightly formatted. Inline-structured elements allow to format your text and add some helpful links or images. Sometimes, you want to enter characters that have no representation on your keyboard. ; Text formatting : Every [[text formatting|00001007040100]] element begins with two same characters at the beginning. It lasts until the same two characters occurred the second time. Some of these elements explicitly support [[attributes|00001007050000]]. ; Literal-like formatting : Sometimes, you want to enter the text as it is. : This is the core motivation of [[literal-like formatting|00001007040200]]. ; Reference-like text : You can reference other zettel and (external) material within one zettel. This kind of reference may be a link, or an images that is display inline when the zettel is rendered. Footnotes sometimes factor out some useful text that hinders the flow of reading text. Internal marks allow to reference something within a zettel. An important aspect of all knowledge work is to reference others work, e.g. with citation keys. All these elements can be subsumed under [[reference-like text|00001007040300]]. === Other inline elements ==== Comment A comment begins with two consecutive percent sign characters (""''%''"", U+0025). It ends at the end of the line where it begins. ==== Backslash The backslash character (""''\\''"", U+005C) gives the next character another meaning. * If a space character follows, it is converted into a non-breaking space (U+00A0). * If a line ending follows the backslash character, the line break is converted from a __soft break__ into a __hard break__. * Every other character is taken as itself, but without the interpretation of a Zettelmarkup element. For example, if you want to enter a ""'']''"" into a [[footnote text|00001007040330]], you should escape it with a backslash. ==== Entities & more Sometimes it is not easy to enter special characters. If you know the Unicode code point of that character, or its name according to the [[HTML standard|https://html.spec.whatwg.org/multipage/named-characters.html]], you can enter it by number or by name. Regardless which method you use, an entity always begins with an ampersand character (""''&''"", U+0026) and ends with a semicolon character (""'';''"", U+003B). If you know the HTML name of the character you want to enter, put it between these two characters. Example: ``&`` is rendered as ::&::{=example}. If you want to enter its numeric code point, a number sign character must follow the ampersand character, followed by digits to base 10. Example: ``&`` is rendered in HTML as ::&::{=example}. You also can enter its numeric code point as a hex number, if you put the letter ""x"" after the numeric sign character. Example: ``&`` is rendered in HTML as ::&::{=example}. According to the [[HTML Standard|https://html.spec.whatwg.org/multipage/syntax.html#character-references]], some numeric code points are not allowed. These are all code point below the numeric value 32 (decimal) or 0x20 (hex) and all code points for [[noncharacter|https://infra.spec.whatwg.org/#noncharacter]] values. Since some Unicode characters are used quite often, a special notation is introduced for them: * Two consecutive hyphen-minus characters result in an __en-dash__ character. It is typically used in numeric ranges. ``pages 4--7`` will be rendered in HTML as: ::pages 4--7::{=example}. Alternative specifications are: ``–``, ``&x8211``, and ``–``. |
Changes to docs/manual/00001007040100.zettel.
1 2 3 4 5 6 | id: 00001007040100 title: Zettelmarkup: Text Formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | id: 00001007040100 title: Zettelmarkup: Text Formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250106174436 Text formatting is the way to make your text visually different. Every text formatting element begins with two identical characters. It ends when these two same characters occur the second time. It is possible that some [[attributes|00001007050000]] follow immediately, without any separating character. Text formatting can be nested, up to a reasonable limit. The following characters begin a text formatting: * The low line character (""''_''"", U+005F) emphasizes its text. ** 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}. * Similarly, 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 number sign (""''#''"", U+0023) marks the text visually, where the mark does not belong to the text itself. It is typically used to highlight some text that is important for you, but was not important for the original author. ** Example: ``abc ##def## ghi`` is rendered in HTML as: ::abc ##def## ghi::{=example}. * The colon character (""'':''"", U+003A) marks 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 6 | id: 00001007040200 title: Zettelmarkup: Literal-like formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007040200 title: Zettelmarkup: Literal-like formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250115180425 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 |
︙ | ︙ | |||
43 44 45 46 47 48 49 | Examples: * ``==The result is: 42==`` renders in HTML as ::==The result is: 42==::{=example}. * ``==The result is: 42=={-}`` renders in HTML as ::==The result is: 42=={-}::{=example}. Attributes can be specified, the default attribute has the same semantic as for literal text. | < < < < < < < < < < < < < < | | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | Examples: * ``==The result is: 42==`` renders in HTML as ::==The result is: 42==::{=example}. * ``==The result is: 42=={-}`` renders in HTML as ::==The result is: 42=={-}::{=example}. Attributes can be specified, the default attribute has the same semantic as for literal text. === 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 to 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. |
︙ | ︙ |
Changes to docs/manual/00001007040310.zettel.
1 2 3 4 5 6 | id: 00001007040310 title: Zettelmarkup: Links role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | id: 00001007040310 title: Zettelmarkup: Links role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 modified: 20250102183944 There are two kinds of links, regardless of links to (internal) other zettel or to (external) material. Both kinds begin with two consecutive left square bracket characters (""''[''"", U+005B) and end with two consecutive right square bracket characters (""'']''"", U+005D). If the content starts with more than two left square bracket characters, all but the last two will be treated as text. The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", U+007C): ``[[text|linkspecification]]``. The text is a sequence of [[inline elements|00001007040000]]. However, it should not contain links itself. The second form just provides a link specification between the square brackets. Its text is derived from the link specification, e.g. by interpreting the link specification as text: ``[[linkspecification]]``. === Link specifications The link specification for another zettel within the same Zettelstore is just the [[zettel identifier|00001006050000]]. To reference some content within a zettel, you can append a number sign character (""''#''"", U+0023) and the name of the mark to the zettel identifier. The resulting reference is called ""zettel reference"". If the link specification begins with the string ''query:'', the text following this string will be interpreted as a [[query expression|00001007700000]]. The resulting reference is called ""query reference"". When this type of reference is rendered, it will typically reference a list of all zettel that fulfills the query expression. A link specification starting with one slash character (""''/''"", U+002F), or one or two full stop characters (""''.''"", U+002E) followed by a slash character, will be interpreted as a local reference, called __hosted reference__. Such references will be interpreted relative to the web server hosting the Zettelstore. If a link specification begins with two slash characters (called __based reference__), it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]]. To specify some material outside the Zettelstore, just use a normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://tools.ietf.org/html/rfc3986]]. === Other topics If the link references another zettel, and this zettel is not readable for the current user, e.g. because of missing access rights, then only the associated text is presented. |
Changes to docs/manual/00001007040320.zettel.
1 2 3 4 5 6 | id: 00001007040320 title: Zettelmarkup: Inline Embedding / Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | id: 00001007040320 title: Zettelmarkup: Inline Embedding / Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 modified: 20250102210158 To some degree, a specification for embedded material is conceptually not too far away from a specification for [[linked material|00001007040310]]. Both contain a reference specification and optionally some text. In contrast to a link, the specification of embedded material must currently resolve to some kind of real content. This content replaces the embed specification. An embed specification begins with two consecutive left curly bracket characters (""''{''"", U+007B) and ends with two consecutive right curly bracket characters (""''}''"", U+007D). The curly brackets delimits either a reference specification or some text, a vertical bar character and the link specification, similar to a link. If the content starts with more than two left curly bracket characters, all but the last two will be treated as text. One difference to a link: if the text is not given, an empty string is assumed. The reference must point to some content, either zettel content or URL-referenced content. If the current user is not allowed to read the referenced zettel, the inline transclusion / embedding is ignored. If the referenced zettel does not exist, or is not readable because of other reasons, a [[spinning emoji|00000000040001]] is presented as a visual hint: Example: ``{{00000000000000}}`` will be rendered as ::{{00000000000000}}::{=example}. |
︙ | ︙ |
Changes to docs/manual/00001007040322.zettel.
1 2 3 4 5 6 | id: 00001007040322 title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001007040322 title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 modified: 20250102222115 Image content is assumed, if a URL is used or if the referenced zettel contains an image. Supported formats are: * Portable Network Graphics (""PNG""), as defined by [[RFC\ 2083|https://tools.ietf.org/html/rfc2083]]. * Graphics Interchange Format (""GIF"), as defined by [[https://www.w3.org/Graphics/GIF/spec-gif89a.txt]]. * JPEG / JPG, defined by the __Joint Photographic Experts Group__. * Scalable Vector Graphics (SVG), defined by [[https://www.w3.org/Graphics/SVG/]] |
︙ | ︙ |
Changes to docs/manual/00001007040324.zettel.
1 2 3 4 5 6 | id: 00001007040324 title: Zettelmarkup: Inline-mode Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | id: 00001007040324 title: Zettelmarkup: Inline-mode Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 modified: 20250102183508 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. The result of this (indirect) transclusion is searched for inline-structured elements. * If only a [[zettel identifier|00001006050000]] was specified, the first top-level [[paragraph|00001007030000#paragraphs]] is used. Since a paragraph is basically a sequence of inline-structured elements, these elements will replace the transclude specification. Example: ``{{00010000000000}}`` (see [[00010000000000]]) is rendered as ::{{00010000000000}}::{=example}. * If a fragment identifier was additionally specified, the element with the given fragment is searched: ** If it specifies a [[heading|00001007030300]], the next top-level paragraph is used. Example: ``{{00010000000000#reporting-errors}}`` is rendered as ::{{00010000000000#reporting-errors}}::{=example}. ** In case the fragment names a [[mark|00001007040350]], the inline-structured elements after the mark are used. Initial spaces and line breaks are ignored in this case. Example: ``{{00001007040322#spin}}`` is rendered as ::{{00001007040322#spin}}::{=example}. ** Just specifying the fragment identifier will reference something on the current page. This is not allowed, to prevent a possible endless recursion. * If the reference is a [[hosted or based|00001007040310#link-specifications]] link / URL to an image, that image will be rendered. Example: ``{{//z/00000000040001}}{alt=Emoji}`` is rendered as ::{{//z/00000000040001}}{alt=Emoji}::{=example} If no inline-structured elements are found, the transclude specification is replaced by an error message. To avoid an exploding ""transclusion bomb"", a form of [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]] (also known as ""XML bomb""), the total number of transclusions / expansions is limited. The limit can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel. === See also [[Full transclusion|00001007031100]] does not work inside some text, but is used for [[block-structured elements|00001007030000]]. |
Changes to docs/manual/00001007040340.zettel.
1 2 3 4 5 6 | id: 00001007040340 title: Zettelmarkup: Citation Key role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001007040340 title: Zettelmarkup: Citation Key role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 modified: 20250102210258 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), 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/00001007701000.zettel.
1 2 3 4 5 6 | id: 00001007701000 title: Query: Search Expression role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707205043 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001007701000 title: Query: Search Expression role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707205043 modified: 20250102210324 In its simplest form, a search expression just contains a string to be searched for with the help of a full-text search. For example, the string ''syntax'' will search for all zettel containing the word ""syntax"". If you want to search for all zettel with a title containing the word ""syntax"", you must specify ''title:syntax''. ""title"" denotes the [[metadata key|00001006010000]], in this case the [[supported metadata key ""title""|00001006020000#title]]. The colon character (""'':''"") is a [[search operator|00001007705000]], in this example to specify a match. ""syntax"" is the [[search value|00001007706000]] that must match to the value of the given metadata key, here ""title"". |
︙ | ︙ |
Changes to docs/manual/00001007702000.zettel.
1 2 3 4 5 6 | id: 00001007702000 title: Search term role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | id: 00001007702000 title: Search term role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 modified: 20250102210348 A search term allows you to specify one search restriction. The result [[search expression|00001007700000]], which contains more than one search term, will be the applications of all restrictions. A search term can be one of the following (the first three term are collectively called __search literals__): * A metadata-based search, by specifying the name of a [[metadata key|00001006010000]], followed by a [[search operator|00001007705000]], followed by an optional [[search value|00001007706000]]. All zettel containing the given metadata key with an allowed value (depending on the search operator) are selected. If no search value is given, then all zettel containing the given metadata key are selected (or ignored, for a negated search operator). * An optional [[search operator|00001007705000]], followed by a [[search value|00001007706000]]. This specifies a full-text search for the given search value. However, the operators ""less"" and ""greater"" are not supported, they are internally translated into the ""match"" operators. |
︙ | ︙ |
Changes to docs/manual/00001007705000.zettel.
1 2 3 4 5 6 | id: 00001007705000 title: Search operator role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 | | | | | 1 2 3 4 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: 00001007705000 title: Search operator role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 modified: 20250102210427 A search operator specifies how the comparison of a search value and a zettel should be executed. Every comparison is done case-insensitive, treating all uppercase letters the same as lowercase letters. The following are allowed search operator characters: * The exclamation mark character (""''!''"", U+0021) negates the meaning. * The equal sign character (""''=''"", U+003D) compares on equal content (""equals operator""). * The tilde character (""''~''"", U+007E) compares on matching (""match operator""). * The left square bracket character (""''[''"", U+005B) matches if there is some prefix (""prefix operator""). * The right square bracket character (""'']''"", U+005D) compares a suffix relationship (""suffix operator""). * The colon character (""'':''"", U+003A) compares depending on the on the actual [[key type|00001006030000]] (""has operator""). In most cases, it acts as a equals operator, but for some type it acts as the match operator. * The less-than sign character (""''<''"", U+003C) matches if the search value is somehow less than the metadata value (""less operator""). * The greater-than sign character (""''>''"", U+003E) matches if the search value is somehow greater than the metadata value (""greater operator""). * The question mark (""''?''"", U+003F) checks for an existing metadata key (""exist operator""). In this case no [[search value|00001007706000]] must be given. Since the exclamation mark character can be combined with the other, there are 18 possible combinations: # ""''!''"": is an abbreviation of the ""''!~''"" operator. # ""''~''"": is successful if the search value matched the value to be compared. # ""''!~''"": is successful if the search value does not match the value to be compared. |
︙ | ︙ |
Changes to docs/manual/00001007720300.zettel.
1 2 3 4 5 6 | id: 00001007720300 title: Query: Context Directive role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707204706 | | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | id: 00001007720300 title: Query: Context Directive role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707204706 modified: 20250202172633 A context directive calculates the __context__ of a list of zettel identifier. It starts with the keyword ''CONTEXT''. Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters. These are: * ''FULL'': additionally search for zettel with the same tags, * ''BACKWARD'': search for context only though backward links, * ''FORWARD'': search for context only through forward links, * ''COST'': one or more space characters, and a positive integer: set the maximum __cost__ (default: 17), * ''MAX'': one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200). * ''MIN'': one or more space characters, and a positive integer: set the minimum number of context zettel (default: 0). Takes precedence over ''COST'' and ''MAX''. If no ''BACKWARD'' and ''FORWARD'' is specified, a search for context zettel will be done though backward and forward links. The cost of a context zettel is calculated iteratively: * Each of the specified zettel hast a cost of one. * A zettel found as a single folge zettel or single precursor zettel has the cost of the originating zettel, plus 0.1. * A zettel found as a single sequel zettel or single prequel zettel has the cost of the originating zettel, plus 1.0. |
︙ | ︙ |
Changes to docs/manual/00001007780000.zettel.
1 2 3 4 5 6 | id: 00001007780000 title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | id: 00001007780000 title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 modified: 20250202164337 ``` QueryExpression := ZettelList? QueryDirective* SearchExpression ActionExpression? ZettelList := (ZID (SPACE+ ZID)*). ZID := '0'+ ('1' .. '9'') DIGIT* | ('1' .. '9') DIGIT*. QueryDirective := ContextDirective | IdentDirective | ItemsDirective | UnlinkedDirective. ContextDirective := "CONTEXT" (SPACE+ ContextDetail)*. ContextDetail := "FULL" | "BACKWARD" | "FORWARD" | "COST" SPACE+ PosInt | "MAX" SPACE+ PosInt | "MIN" SPACE+ PosInt. IdentDirective := IDENT. ItemsDirective := ITEMS. UnlinkedDirective := UNLINKED (SPACE+ PHRASE SPACE+ Word)*. SearchExpression := SearchTerm (SPACE+ SearchTerm)*. SearchTerm := SearchOperator? SearchValue | SearchKey SearchOperator SearchValue? | SearchKey ExistOperator |
︙ | ︙ | |||
40 41 42 43 44 45 46 | | ('!')? ('~' | ':' | '[' | '}'). ExistOperator := '?' | '!' '?'. PosInt := '0' | ('1' .. '9') DIGIT*. ActionExpression := '|' (Word (SPACE+ Word)*)? Action := Word | < | < < | 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | | ('!')? ('~' | ':' | '[' | '}'). ExistOperator := '?' | '!' '?'. PosInt := '0' | ('1' .. '9') DIGIT*. ActionExpression := '|' (Word (SPACE+ Word)*)? Action := Word | 'KEYS' | 'N' NO-SPACE* | 'MAX' PosInt | 'MIN' PosInt | 'REDIRECT' | 'REINDEX'. Word := NO-SPACE NO-SPACE* ``` |
Changes to docs/manual/00001007800000.zettel.
1 2 3 4 5 6 | id: 00001007800000 title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001007800000 title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 modified: 20250115180517 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]] | [[marked / highlighted text|00001007040100]] |
︙ | ︙ | |||
26 27 28 29 30 31 32 | | ''/'' | (free) | (free) | '':'' | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]] | '';'' | [[Description term|00001007030100]] | (free) | ''<'' | [[Quotation block|00001007030600]] | (free) | ''='' | [[Headings|00001007030300]] | [[Computer output|00001007040200]] | ''>'' | [[Quotation lists|00001007030200]] | [[Inserted text|00001007040100]] | ''?'' | (free) | (free) | | | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | | ''/'' | (free) | (free) | '':'' | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]] | '';'' | [[Description term|00001007030100]] | (free) | ''<'' | [[Quotation block|00001007030600]] | (free) | ''='' | [[Headings|00001007030300]] | [[Computer output|00001007040200]] | ''>'' | [[Quotation lists|00001007030200]] | [[Inserted text|00001007040100]] | ''?'' | (free) | (free) | ''@'' | [[Inline-Zettel block|00001007031200]] | (reserved) | ''['' | (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/00001007906000.zettel.
1 2 3 4 5 6 | id: 00001007906000 title: Zettelmarkup: Second Steps role: manual tags: #manual #tutorial #zettelmarkup #zettelstore syntax: zmk created: 20220811115501 | | | | 1 2 3 4 5 6 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: 00001007906000 title: Zettelmarkup: Second Steps role: manual tags: #manual #tutorial #zettelmarkup #zettelstore syntax: zmk created: 20220811115501 modified: 20250102221931 After you have [[learned|00001007903000]] the basic concepts and markup of Zettelmarkup (paragraphs, emphasized text, and lists), this zettel introduces you into the concepts of links, thematic breaks, and headings. === Links A Zettelstore is much more useful, if you connect related zettel. If you read a zettel later, this allows you to know about the context of a zettel. [[Zettelmarkup|00001007000000]] allows you to specify such a connection. A connection can be specified within a paragraph via [[Links|00001007040310]]. * A link always starts with two left square bracket characters and ends with two right square bracket characters: ''[[...]]''. * Within these character sequences you specify the [[zettel identifier|00001006050000]] of the zettel you want to reference: ''[[00001007903000]]'' will connect to zettel containing the first steps into Zettelmarkup. * In addition, you should give the link a more readable description. This is done by prepending the description before the reference and use the vertical bar character to separate both: ''[[First Steps|00001007903000]]''. You are not restricted to reference your zettel. Alternatively, you might specify a URL of an external website: ''[[Zettelstore|https://zettelstore.de]]''. Of course, if you just want to specify the URL, you are allowed to omit the description: ''[[https://zettelstore.de]]'' |= Zettelmarkup | Rendered output | Remark | ''[[00001007903000]]'' | [[00001007903000]] | If no description is given, the zettel identifier acts as a description | ''[[First Steps|00001007903000]]'' | [[First Steps|00001007903000]] | The description should be chosen so that you are not confused later | ''[[https://zettelstore.de]]'' | [[https://zettelstore.de]] | A link to an external URL is rendered differently | ''[[Zettelstore|https://zettelstore.de]]'' | [[Zettelstore|https://zettelstore.de]] | You can use any URL your browser is able to support |
︙ | ︙ |
Changes to docs/manual/00001008010500.zettel.
1 2 3 4 5 6 | id: 00001008010500 title: CommonMark role: manual tags: #manual #markdown #zettelstore syntax: zmk created: 20220113183435 | | | > | > > | | | | < < | 1 2 3 4 5 6 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: 00001008010500 title: CommonMark role: manual tags: #manual #markdown #zettelstore syntax: zmk created: 20220113183435 modified: 20250115200458 url: https://commonmark.org/ [[CommonMark|https://commonmark.org/]] is a Markdown dialect, an [[attempt|https://xkcd.com/927/]] to unify all the different, divergent dialects of Markdown by providing an unambiguous syntax specification for Markdown, together with a suite of comprehensive tests to validate implementation. Time will show, if this attempt is successful. However, CommonMark is a well specified Markdown dialect, in contrast to most (if not all) other dialects. Other software adopts CommonMark somehow, notably [[GitHub Flavored Markdown|https://github.github.com/gfm/]] (GFM). 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.31.2 (2024-01-28)|https://spec.commonmark.org/0.31.2/]]. 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. Effectively, Markdown and CommonMark are super-sets of HTML. Internally, CommonMark is translated into [[Zettelmarkup|00001007000000]]. Since Zettelmarkup supports HTML content only at the block level via [[Inline Zettel|00001007031200]], most uses of HTML within a CommonMark zettel are not translated as expected. Instead, inline level HTML is translated into [[literal text|00001007040200#literal-text]] with a [[generic attribute|00001007050000]] set to ``html``. Therefore, Zettelstore itself will not pass the CommonMark test suite fully. However, no CommonMark language element except inline HTML will fail to be encoded as HTML. In most cases, the differences are not visible for a user, but only by comparing the generated HTML code. |
Changes to docs/manual/00001010000000.zettel.
1 2 3 4 5 6 | id: 00001010000000 title: Security role: manual tags: #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001010000000 title: Security role: manual tags: #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 modified: 20250102212014 Your zettel may contain sensitive content. You probably want to ensure that only authorized persons can read and/or modify them. Zettelstore ensures this in various ways. === Local first The Zettelstore is designed to run on your local computer. |
︙ | ︙ | |||
40 41 42 43 44 45 46 | 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. | | | | | | 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | 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, nobody has to authenticate themselves to see the contents of the note. 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 a user * [[Authorization and read-only mode|00001010070400]] * [[Access rules|00001010070600]] define the policy which user is allowed to do what operation. === Encryption When Zettelstore is accessed remotely, the messages that are sent between Zettelstore and the client must be encrypted. Otherwise, an eavesdropper could fetch sensitive data, such as passwords or precious content that is not for the public. The Zettelstore itself does not encrypt messages. But you can put a server in front of it, which is able to handle encryption. Most generic web server software allow this. To enforce encryption, [[authenticated sessions|00001010040700]] are marked as secure by default. If you still want to access the Zettelstore remotely without encryption, you must change the startup configuration. Otherwise, authentication will not work. * [[Use a server for encryption|00001010090100]] |
Changes to docs/manual/00001010040200.zettel.
1 | id: 00001010040200 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001010040200 title: Creating a user zettel role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 modified: 20250102221859 All data used for authenticating a user is stored in a special zettel called ""user zettel"". A user zettel must have set the following two metadata fields: ; ''user-id'' (""user identification"") : The unique identification to be specified for authentication. ; ''credential'' : A hashed password as generated by the [[``zettelstore password``{=sh}|00001004051400]] command. |
︙ | ︙ | |||
24 25 26 27 28 29 30 | A user zettel may additionally contain metadata that [[overwrites corresponding values|00001004020200]] of the [[runtime configuration|00001004020000]]. A user zettel can only be created by the owner of the Zettelstore. The owner should execute the following steps to create a new user zettel: # Create a new zettel. | | | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | A user zettel may additionally contain metadata that [[overwrites corresponding values|00001004020200]] of the [[runtime configuration|00001004020000]]. A user zettel can only be created by the owner of the Zettelstore. The owner should execute the following steps to create a new user zettel: # Create a new zettel. # Save the zettel to get an [[identifier|00001006050000]] for this zettel. # Choose a unique identification for the user. #* If the identifier is not unique, authentication will not work for this user. # Execute the [[``zettelstore password``|00001004051400]] command. #* You have to specify the user identification and the zettel identifier #* If you should not know the password of the new user, send her/him the user identification and the user zettel identifier, so that the person can create the hashed password herself. # Edit the user zettel and add the hashed password under the meta key ''credential'' and the user identification under the key ''user-id''. |
Changes to docs/manual/00001010040400.zettel.
1 2 3 4 5 6 | id: 00001010040400 title: Authentication process role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001010040400 title: Authentication process role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 modified: 20250102222012 When someone tries to authenticate itself with a user identifier / ""user name"" and a password, the following process is executed: # If meta key ''owner'' of the configuration zettel does not have a valid [[zettel identifier|00001006050000]] as value, authentication fails. # Retrieve all zettel, where the meta key ''user-id'' has the same value as the given user identification. If the list is empty, authentication fails. # From above list, the zettel with the numerically smallest identifier is selected. Or in other words: the oldest zettel is selected[^This is done to prevent an attacker from creating a new note with the same user identification]. # If the zettel does not have a value for the meta key ''credential'', authentication fails. # The value of the meta key ''credential'' is compared with the given password. |
︙ | ︙ |
Changes to docs/manual/00001010040700.zettel.
1 2 3 4 5 6 | id: 00001010040700 title: Access token role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | id: 00001010040700 title: Access token role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 modified: 20250102215422 If a user is authenticated, an ""access token"" is created that must be sent with every request to prove the identity of the caller. Otherwise the user will not be recognized by Zettelstore. If the user was authenticated via the [[web user interface|00001014000000]], the access token is stored in a [[""session cookie""|https://en.wikipedia.org/wiki/HTTP_cookie#Session_cookie]]. When the web browser is closed, these cookies are not saved. If you want the web browser to store the cookie for the lifetime of that token, the owner must set ''persistent-cookie'' of the [[startup configuration|00001004010000]] to ''true''. If the web browser remains inactive for a period, the user will be automatically logged off, because each access token has a limited lifetime. The maximum length of this period is specified by the ''token-lifetime-html'' value of the startup configuration. Every time a web page is displayed, a fresh token is created and stored inside the cookie. If the user was authenticated via the API, the access token will be returned as the content of the response. Typically, the lifetime of this token is shorter, e.g. 10 minutes. It is specified by the ''token-lifetime-api'' value of the startup configuration. If you need more time, you can either [[re-authenticate|00001012050200]] the user or use an API call to [[renew the access token|00001012050400]]. If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), you must set the ''insecure-cookie'' value in the startup configuration to ''true''. In most cases, such a scenario is not recommended, because user name and password will be transferred as plain text. You could use such a scenario if you know all parties that access the local network where you access the Zettelstore. |
Changes to docs/manual/00001010070200.zettel.
1 2 3 4 5 6 | id: 00001010070200 title: Visibility rules for zettel role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001010070200 title: Visibility rules for zettel role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 modified: 20250102170611 For every zettel you can specify under which condition the zettel is visible to others. This is controlled with the metadata key [[''visibility''|00001006020000#visibility]]. The following values are supported: ; [!public|""public""] : The zettel is visible to everybody, even if the user is not authenticated. |
︙ | ︙ | |||
28 29 30 31 32 33 34 | This is for zettel with sensitive content that might irritate the owner. Computed zettel with internal runtime information are examples for such a zettel. When you install a Zettelstore, only [[some zettel|query:visibility:public]] have visibility ""public"". One is the zettel that contains [[CSS|00000000020001]] for displaying the [[web user interface|00001014000000]]. This is to ensure that the web interface looks nice even for not authenticated users. Another is the zettel containing the Zettelstore [[license|00000000000004]]. | | | 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | This is for zettel with sensitive content that might irritate the owner. Computed zettel with internal runtime information are examples for such a zettel. When you install a Zettelstore, only [[some zettel|query:visibility:public]] have visibility ""public"". One is the zettel that contains [[CSS|00000000020001]] for displaying the [[web user interface|00001014000000]]. This is to ensure that the web interface looks nice even for not authenticated users. Another is the zettel containing the Zettelstore [[license|00000000000004]]. The [[default image|00000000040001]], used if an image reference is invalid, is also publicly visible. Please note: if [[authentication is not enabled|00001010040100]], every user has the same rights as the owner of a Zettelstore. This is also true, if the Zettelstore runs additionally in [[read-only mode|00001004010000#read-only-mode]]. In this case, the [[runtime configuration zettel|00001004020000]] is shown (its visibility is ""owner""). The [[startup configuration|00001004010000]] is not shown, because the associated computed zettel with identifier ''00000000000096'' is stored with the visibility ""expert"". If you want to show such a zettel, you must set ''expert-mode'' to true. |
︙ | ︙ |
Changes to docs/manual/00001010070400.zettel.
1 2 3 4 5 6 | id: 00001010070400 title: Authorization and read-only mode role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001010070400 title: Authorization and read-only mode role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 modified: 20250102212150 It is possible to enable both the read-only mode of the Zettelstore __and__ authentication/authorization. Both modes are independent of each other. This gives four use cases: ; Not read-only, no authorization : Zettelstore runs on your local computer and you only work with it. ; Not read-only, with authorization : Zettelstore is accessed remotely. You need authentication to ensure that only valid users access your Zettelstore. ; With read-only, no authorization : Zettelstore presents its full content publicly to everyone. ; With read-only, with authorization : Nobody is allowed to change the content of the Zettelstore, and only specific zettel should be presented to the public. |
Changes to docs/manual/00001012000000.zettel.
1 2 3 4 5 6 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102222042 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 [[symbolic expressions|00001012930000]] as its main encoding formats for exchanging messages between a Zettelstore and its client software. There is an [[overview zettel|00001012920000]] that shows the structure of the endpoints used by the API and gives an indication about its use. === Authentication If [[authentication is enabled|00001010040100]], most API calls must include an [[access token|00001010040700]] that proves the identity of the caller. * [[Authenticate a user|00001012050200]] to obtain an access token * [[Renew an access token|00001012050400]] without costly re-authentication * [[Provide an access token|00001012050600]] when doing an API call === Zettel lists * [[List all zettel|00001012051200]] * [[Query the list of all zettel|00001012051400]] * [[Determine a tag zettel|00001012051600]] |
︙ | ︙ |
Changes to docs/manual/00001012050200.zettel.
1 2 3 4 5 6 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250102215926 Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]]. This token has to be used for other API calls. It is valid for a relatively short amount of time, as configured with the key ''token-lifetime-api'' of the [[startup configuration|00001004010000#token-lifetime-api]] (typically 10 minutes). The simplest way is to send user identification (''IDENT'') and password (''PASSWORD'') via [[HTTP Basic Authentication|https://tools.ietf.org/html/rfc7617]] and send them to the [[endpoint|00001012920000]] ''/a'' with a POST request: ```sh |
︙ | ︙ | |||
24 25 26 27 28 29 30 | If you do not want to use Basic Authentication, you can also send user identification and password as HTML form data: ```sh # curl -X POST -d 'username=IDENT&password=PASSWORD' http://127.0.0.1:23123/a ("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4OCwiaWF0IjoxNjgxMzA0MDI4LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.qIEyOMFXykCApWtBaqbSESwTL96stWl2LRICiRNAXUjcY-mwx_SSl9L5Fj2FvmrI1K1RBvWehjoq8KZUNjhJ9Q" 600) ``` | | | | | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | If you do not want to use Basic Authentication, you can also send user identification and password as HTML form data: ```sh # curl -X POST -d 'username=IDENT&password=PASSWORD' http://127.0.0.1:23123/a ("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4OCwiaWF0IjoxNjgxMzA0MDI4LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.qIEyOMFXykCApWtBaqbSESwTL96stWl2LRICiRNAXUjcY-mwx_SSl9L5Fj2FvmrI1K1RBvWehjoq8KZUNjhJ9Q" 600) ``` In all cases, you will receive a list containing three elements with all [[relevant data|00001012921000]] needed for further API calls. **Important:** obtaining a token is a time-intensive process. Zettelstore will delay every request to obtain a token for a certain amount of time. Please take into account that this request will take approximately 500 milliseconds, under certain circumstances more. However, if [[authentication is not enabled|00001010040100]] and you send an authentication request, no user identification/password checking is done and you receive an artificial token immediately, without any delay: ```sh # curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a ("Bearer" "freeaccess" 316224000) ``` In this case, it is even possible to omit the user identification/password. === HTTP Status codes In all cases of successful authentication, a list is returned, which contains the token as the second element. A successful authentication is signaled with the HTTP status code 200, as usual. Other status codes possibly sent by the Zettelstore: ; ''400'' : Unable to process the request. In most cases the form data was invalid. ; ''401'' : Authentication failed. Either the user identification is invalid or you provided the wrong password. ; ''403'' : Authentication is not active. |
Changes to docs/manual/00001012051200.zettel.
1 2 3 4 5 6 | id: 00001012051200 title: API: List all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001012051200 title: API: List all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20250224120441 To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header]. Always use the endpoint ''/z'' to work with a list of zettel. Without further specifications, a plain text document is returned, with one line per zettel. Each line contains in the first 14 characters the [[zettel identifier|00001006050000]]. Separated by a space character, the title of the zettel follows: ```sh |
︙ | ︙ | |||
29 30 31 32 33 34 35 | === Data output Alternatively, you may retrieve the zettel list as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data'': ```sh # curl 'http://127.0.0.1:23123/z?enc=data' | | | | | | | | | | | | | | | | > | | 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | === Data output Alternatively, you may retrieve the zettel list as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data'': ```sh # curl 'http://127.0.0.1:23123/z?enc=data' (meta-list (query "") (human "") (zettel (id "00001012921200") (meta (title "API: Encoding of Zettel Access Rights") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (backward "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (box-number "1") (created "00010101000000") (forward "00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300") (modified "20220201171959") (published "20220201171959")) (rights 62)) (zettel (id "00001007030100") ... ``` Pretty-printed, this results in: ``` (meta-list (query "") (human "") (zettel (id "00001012921200") (meta (title "API: Encoding of Zettel Access Rights") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (backward "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (box-number "1") (created "00010101000000") (forward "00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300") (modified "20220201171959") (published "20220201171959")) (rights 62)) (zettel (id "00001007030100") ... ``` * The result is a list, starting with the symbol ''meta-list''. * Then, some key/value pairs are following, also nested. * Keys ''query'' and ''human'' will be explained [[later in this manual|00001012051400]]. * Then comes the list of zettel. * ''zettel'' itself start, well, a zettel. * ''id'' denotes the zettel identifier, encoded as a string. * Nested in ''meta'' are the metadata, each as a key/value pair. * ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel. === Note This request (and similar others) will always return a list of metadata, provided the request was syntactically correct. |
︙ | ︙ |
Changes to docs/manual/00001012051400.zettel.
1 2 3 4 5 6 | id: 00001012051400 title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 | | | | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | id: 00001012051400 title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 modified: 20250224120055 precursor: 00001012051200 The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally specify some actions. A [[query|00001007700000]] consists of an optional [[search expression|00001007700000#search-expression]] and an optional [[list of actions|00001007700000#action-list]] (described below). If no search expression is provided, all zettel are selected. Similarly, if no valid action is specified, or the action list is empty, the list of all selected zettel metadata is returned. Search expression and action list are separated by a vertical bar character (""''|''"", U+007C), and must be given with the ''q'' query parameter. The query parameter ""''q''"" allows you to specify [[query expressions|00001007700000]] for a full-text search of all zettel content and/or restricting the search according to specific metadata. The query parameter can be provided multiple times. It loosely resembles the search form of the [[web user interface|00001014000000]] or those of [[Zettelmarkup's Query Transclusion|00001007031140]]. For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be: ```sh # curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1' 00001012921000 API: Structure of an access token 00001012920500 Formats available by the API 00001012920000 Endpoints used by the API ... ``` If you want to retrieve a data document, as a [[symbolic expression|00001012930500]]: ```sh # curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1&enc=data' (meta-list (query "title:API ORDER REVERSE id OFFSET 1") (human "title HAS API ORDER REVERSE id OFFSET 1") (zettel (id 1012921000) (meta (title "API: Structure of an access token") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012050600 00001012051200") (backward "00001012050200 00001012050400 00001012050600 00001012051200") (box-number "1") (created "20210126175322") (forward "00001012050200 00001012050400 00001012930000") (modified "20230412155303") (published "20230412155303")) (rights 62)) (zettel (id 1012920500) (meta (title "Encodings available via the API") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001006000000 00001008010000 00001008010500 00001012053500 00001012053600") (backward "00001006000000 00001008010000 00001008010500 00001012053500 00001012053600") (box-number "1") (created "20210126175322") (forward "00001012000000 00001012920510 00001012920513 00001012920516 00001012920519 00001012920522 00001012920525") (modified "20230403123653") (published "20230403123653")) (rights 62)) (zettel (id 1012920000) (meta (title "Endpoints used by the API") ... ``` The data object contains a key ''"meta-list"'' to signal that it contains a list of metadata values (and some more). It contains the keys ''"query"'' and ''"human"'' with a string value. Both will contain a textual description of the underlying query if you select only some zettel with a [[query expression|00001007700000]]. Without a selection, the values are the empty string. ''"query"'' returns the normalized query expression itself, while ''"human"'' is the normalized query expression to be read by humans. Then comes the list of zettel data. Data of a zettel is indicated by the symbol ''zettel'', followed by ''(id ID)'' that describes the zettel identifier as a numeric value. Leading zeroes are removed. Metadata starts with the symbol ''meta'', and each metadatum itself is a list of metadata key / metadata value. Metadata keys are encoded as a symbol, metadata values as a string. ''"rights"'' encodes the [[access rights|00001012921200]] for the given zettel. === Aggregates |
︙ | ︙ | |||
75 76 77 78 79 80 81 | Please note that the list is **not** sorted by the role name, so the same request might result in a different order. If you want a sorted list, you could sort it on the command line (``curl 'http://127.0.0.1:23123/z?q=|role' | sort``) or within the software that made the call to the Zettelstore. Of course, this list can also be returned as a data object: ```sh # curl 'http://127.0.0.1:23123/z?q=|role&enc=data' | | | | | 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 | Please note that the list is **not** sorted by the role name, so the same request might result in a different order. If you want a sorted list, you could sort it on the command line (``curl 'http://127.0.0.1:23123/z?q=|role' | sort``) or within the software that made the call to the Zettelstore. Of course, this list can also be returned as a data object: ```sh # curl 'http://127.0.0.1:23123/z?q=|role&enc=data' (aggregate "role" (query "| role") (human "| role") ("zettel" 10000000000 90001) ("configuration" 6 100 1000000100 20001 90 25001 92 4 40001 1 90000 5 90002) ("manual" 1008050000 1007031110 1008000000 1012920513 1005000000 1012931800 1010040700 1012931000 1012053600 1006050000 1012050200 1012000000 1012070500 1012920522 1006032500 1006020100 1007906000 1007030300 1012051400 1007040350 1007040324 1007706000 1012931900 1006030500 1004050200 1012054400 1007700000 1004050000 1006020000 1007030400 1012080100 1012920510 1007790000 1010070400 1005090000 1004011400 1006033000 1012930500 1001000000 1007010000 1006020400 1007040300 1010070300 1008010000 1003305000 1006030000 1006034000 1012054200 1012080200 1004010000 1003300000 1006032000 1003310000 1004059700 1007031000 1003600000 1004000000 1007030700 1007000000 1006055000 1007050200 1006036000 1012050600 1006000000 1012053900 1012920500 1004050400 1007031100 1007040340 1007020000 1017000000 1012053200 1007030600 1007040320 1003315000 1012054000 1014000000 1007030800 1010000000 1007903000 1010070200 1004051200 1007040330 1004051100 1004051000 1007050100 1012080500 1012053400 1006035500 1012054600 1004100000 1010040200 1012920000 1012920525 1004051400 1006031500 1012921200 1008010500 1012921000 1018000000 1012051200 1010040100 1012931200 1012920516 1007040310 1007780000 1007030200 1004101000 1012920800 1007030100 1007040200 1012053500 1007040000 1007040322 1007031300 1007031140 1012931600 1012931400 1004059900 1003000000 1006036500 1004020200 1010040400 1006033500 1000000000 1012053300 1007990000 1010090100 1007900000 1007030500 1004011600 1012930000 1007030900 1004020000 1007030000 1010070600 1007040100 1007800000 1012050400 1006010000 1007705000 1007702000 1007050000 1002000000 1007031200 1006035000 1006031000 1006034500 1004011200 1007031400 1012920519)) ``` The data object starts with the symbol ''aggregate'' to signal a different format compared to ''meta-list'' above. Then a string follows, which specifies the key on which the aggregate was performed. ''query'' and ''human'' have the same meaning as above. Then comes the result list of aggregates. Each aggregate starts with a string of the aggregate value, in this case the role value, followed by a list of zettel identifier, denoting zettel which have the given role value. Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/z?q=|tags''. If successful, the output is a data object: ```sh # curl 'http://127.0.0.1:23123/z?q=|tags&enc=data' (aggregate "tags" (query "| tags") (human "| tags") ("#zettel" 1006034500 1006034000 1006031000 1006020400 1006033500 1006036500 1006032500 1006020100 1006031500 1006030500 1006035500 1006033000 1006020000 1006036000 1006030000 1006032000 1006035000) ("#reference" 1006034500 1006034000 1007800000 1012920500 1006031000 1012931000 1006020400 1012930000 1006033500 1012920513 1007050100 1012920800 1007780000 1012921000 1012920510 1007990000 1006036500 1006032500 1006020100 1012931400 1012931800 1012920516 1012931600 1012920525 1012931200 1006031500 1012931900 1012920000 1005090000 1012920522 1006030500 1007050200 1012921200 1006035500 1012920519 1006033000 1006020000 1006036000 1006030000 1006032000 1012930500 1006035000) ("#graphic" 1008050000) ("#search" 1007700000 1007705000 1007790000 1007780000 1007702000 1007706000 1007031140) ("#installation" 1003315000 1003310000 1003000000 1003305000 1003300000 1003600000) ("#zettelmarkup" 1007900000 1007030700 1007031300 1007030600 1007800000 1007000000 1007031400 1007040100 1007030300 1007031200 1007040350 1007030400 1007030900 1007050100 1007040000 1007030500 1007903000 1007040200 1007040330 1007990000 1007040320 1007050000 1007040310 1007031100 1007040340 1007020000 1007031110 1007031140 1007040324 1007030800 1007031000 1007030000 1007010000 1007906000 1007050200 1007030100 1007030200 1007040300 1007040322) ("#design" 1005000000 1006000000 1002000000 1006050000 1006055000) ("#markdown" 1008010000 1008010500) ("#goal" 1002000000) ("#syntax" 1006010000) ... ``` If you want only those tags that occur at least 100 times, use the endpoint ''/z?q=|MIN100+tags''. You see from this that actions are separated by space characters. === Actions |
︙ | ︙ |
Changes to docs/manual/00001012053300.zettel.
1 2 3 4 5 6 | id: 00001012053300 title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211004093206 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | id: 00001012053300 title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211004093206 modified: 20241216104429 The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header]. ````sh # curl 'http://127.0.0.1:23123/z/00001012053300' The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header]. ```sh ... ```` Optionally, you may provide which parts of the zettel you are requesting. In this case, add an additional query parameter ''part=PART''. |
︙ | ︙ |
Changes to docs/manual/00001012053400.zettel.
1 2 3 4 5 6 | id: 00001012053400 title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001012053400 title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 modified: 20241216104120 The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header]. To retrieve the plain metadata of a zettel, use the query parameter ''part=meta'' ````sh # curl 'http://127.0.0.1:23123/z/00001012053400?part=meta' title: API: Retrieve metadata of an existing zettel role: manual |
︙ | ︙ |
Changes to docs/manual/00001012053500.zettel.
1 2 3 4 5 6 | id: 00001012053500 title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | id: 00001012053500 title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 modified: 20241216104145 The [[endpoint|00001012920000]] to work with evaluated metadata and content of a specific zettel is ''/z/{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 in [[Sz encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sz''. If successful, the output is a symbolic expression value: ```sh # curl 'http://127.0.0.1:23123/z/00001012053500?enc=sz' (BLOCK (PARA (TEXT "The ") (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (TEXT " to work with parsed metadata and content of a specific zettel is ") (LITERAL-INPUT () "/z/{ID}") (TEXT ", where ") (LITERAL-INPUT () "{ID}") (TEXT " is a placeholder for the ") ... ``` To select another encoding, you must provide the query parameter ''enc=ENCODING''. |
︙ | ︙ |
Changes to docs/manual/00001012053600.zettel.
1 2 3 4 5 6 | id: 00001012053600 title: API: Retrieve parsed metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | id: 00001012053600 title: API: Retrieve parsed metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20241216104306 The [[endpoint|00001012920000]] to work with parsed metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. A __parsed__ zettel is basically an [[unevaluated|00001012053500]] zettel: the zettel is read and analyzed, but its content is not __evaluated__. By using this endpoint, you are able to retrieve the structure of a zettel before it is evaluated. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter). For example: ```sh # curl 'http://127.0.0.1:23123/z/00001012053600?enc=sz&parseonly' (BLOCK (PARA (TEXT "The ") (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (TEXT " to work with parsed metadata and content of a specific zettel is ") (LITERAL-INPUT () "/z/{ID}") (TEXT ", where ") ... ``` Similar to [[retrieving an encoded zettel|00001012053500]], you can specify an [[encoding|00001012920500]] and state which [[part|00001012920800]] of a zettel you are interested in. |
︙ | ︙ |
Changes to docs/manual/00001012080200.zettel.
1 2 3 4 5 6 | id: 00001012080200 title: API: Check for authentication role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220103224858 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | id: 00001012080200 title: API: Check for authentication role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220103224858 modified: 20241216104549 API clients typically want to know, whether [[authentication is enabled|00001010040100]] or not. If authentication is enabled, they present some form of user interface to get user name and password for the actual authentication. Then they try to [[obtain an access token|00001012050200]]. If authentication is disabled, these steps are not needed. To check for enabled authentication, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=authenticated''. ```sh |
︙ | ︙ |
Changes to docs/manual/00001012080500.zettel.
1 2 3 4 5 6 | id: 00001012080500 title: API: Refresh internal data role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211230230441 | | | | | 1 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: 00001012080500 title: API: Refresh internal data role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211230230441 modified: 20250102212703 Zettelstore maintains some internal data to allow faster operations. One example is the [[content search|00001012051400]] for a term: Zettelstore does not need to scan all zettel to find all occurrences for the term. Instead, all words are stored internally, with a list of zettel where they occur. Another example is the way to determine which zettel are stored in a [[ZIP file|00001004011200]]. Scanning a ZIP file is a lengthy operation, therefore Zettelstore maintains a directory of zettel for each ZIP file. All these internal data may become stale. This should not happen, but when it comes e.g. to file handling, every operating system behaves differently in very subtle ways. To avoid stopping and re-starting Zettelstore, you can use the API to force Zettelstore to refresh its internal data if you think it is needed. To do this, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=refresh''. ```sh # curl -X POST 'http://127.0.0.1:23123/x?cmd=refresh' ``` |
︙ | ︙ |
Changes to docs/manual/00001012920516.zettel.
1 2 3 4 5 6 | id: 00001012920516 title: Sz Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20220422181104 | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001012920516 title: Sz Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20220422181104 modified: 20250102214350 A zettel representation that is a [[s-expression|00001012930000]] (also known as symbolic expression). It is (relatively) easy to parse and contains all relevant information of metadata, content or the whole zettel. For example, take a look at the Sz encoding of this page, which is available on the ""Info"" sub-page of this zettel: * [[//z/00001012920516?enc=sz&part=zettel]], * [[//z/00001012920516?enc=sz&part=meta]], * [[//z/00001012920516?enc=sz&part=content]]. Some zettel provide a more detailed description of the [[Sz encoding|00001012931000]]. If transferred via HTTP, the content type will be ''text/plain''. |
Changes to docs/manual/00001012920525.zettel.
1 2 3 4 5 6 | id: 00001012920525 title: SHTML Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230316181044 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012920525 title: SHTML Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230316181044 modified: 20250102180003 A zettel representation that is a [[s-expression|00001012930000]], syntactically similar to the [[Sz encoding|00001012920516]], but denotes [[HTML|00001012920510]] semantics. It is derived from a XML encoding in s-expressions, called [[SXML|https://en.wikipedia.org/wiki/SXML]]. It is (relatively) easy to parse and contains everything to transform it into real HTML. In contrast to HTML, SHTML is easier to parse and to manipulate. For example, take a look at the SHTML encoding of this page, which is available via the ""Info"" sub-page of this zettel: |
︙ | ︙ | |||
22 23 24 25 26 27 28 | Internally, if a zettel should be transformed into HTML, the zettel is translated into the [[Sz encoding|00001012920516]], which is transformed into this SHTML encoding to produce the [[HTML encoding|00001012920510]]. === Syntax of SHTML There are only two types of elements: atoms and lists, similar to the Sz encoding. 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. | | | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | Internally, if a zettel should be transformed into HTML, the zettel is translated into the [[Sz encoding|00001012920516]], which is transformed into this SHTML encoding to produce the [[HTML encoding|00001012920510]]. === Syntax of SHTML There are only two types of elements: atoms and lists, similar to the Sz encoding. 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. Before the last element of a list of at least two elements, a full stop character (""''.''"", U+002E) signal a pair as the last two elements. This allows a more space economic storage of data. An HTML tag like ``< a href="link">Text</a>`` is encoded in SHTML with a list, where the first element is a symbol named a the tag. The second element is an optional encoding of the tag's attributes. Further elements are either other tag encodings or a string. The above tag is encoded as ``(a (@ (href . "link")) "Text")``. Also possible is to encode the attribute without pairs: ``(a (@ (href "link")) "Text")`` (note the missing full stop character). |
Changes to docs/manual/00001012930500.zettel.
1 2 3 4 5 6 | id: 00001012930500 title: Syntax of Symbolic Expressions role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20230403151127 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012930500 title: Syntax of Symbolic Expressions role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20230403151127 modified: 20250102175559 === Syntax of 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. Internally, lists are composed of __cells__. A cell allows to store two values. |
︙ | ︙ | |||
53 54 55 56 57 58 59 | === Syntax of numbers (atom) A number is a non-empty sequence of digits (""0"" ... ""9""). The smallest number is ``0``, there are no negative numbers. === Syntax of symbols (atom) A symbol is a non-empty sequence of printable characters, except left or right parenthesis. | | | | | | | | | 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 | === Syntax of numbers (atom) A number is a non-empty sequence of digits (""0"" ... ""9""). The smallest number is ``0``, there are no negative numbers. === Syntax of symbols (atom) A symbol is a non-empty sequence of printable characters, except left or right parenthesis. Unicode characters of the following categories contain printable characters in the above sense: letter (L), number (N), punctuation (P), symbol (S). Symbols are case-sensitive, i.e. ""''ZETTEL''"" and ""''zettel''"" denote different symbols. === Syntax of strings (atom) 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 quotation 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 the sequence ""''\\xNM''"", where ''NM'' is the hex encoding of the character. Unicode characters with a code less than U+FFFF are encoded 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 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). === See also * Currently, Zettelstore uses [[Sx|https://t73f.de/r/sx]] (""Symbolic eXpression framework"") to implement symbolic expressions. The project page might contain additional information about the full syntax. Zettelstore only uses lists, numbers, strings, and symbols to represent zettel. |
Changes to docs/manual/00001012931000.zettel.
1 2 3 4 5 6 | id: 00001012931000 title: Encoding of Sz role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403153903 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | id: 00001012931000 title: Encoding of Sz role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403153903 modified: 20250102213403 Zettel in a [[Sz encoding|00001012920516]] are represented as a [[symbolic expression|00001012930000]]. To process these symbolic expressions, you need to know, how a specific part of a zettel is represented by a symbolic expression. Basically, each part of a zettel is represented as a list, often a nested list. The first element of that list is always a unique symbol, which denotes that part. The meaning / semantic of all other elements depend on that symbol. === Zettel A full zettel is represented by a list of two elements. The first elements represents the metadata, the second element represents the zettel content. :::syntax |
︙ | ︙ |
Changes to docs/manual/00001012931200.zettel.
1 2 3 4 5 6 | id: 00001012931200 title: Encoding of Sz Metadata role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161618 | | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | id: 00001012931200 title: Encoding of Sz Metadata role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161618 modified: 20250115172406 A single metadata (""metadatum"") is represented by a triple: a symbol representing the type, a symbol representing the key, and either a string or a list that represent the value. The symbol depends on the [[metadata key type|00001006030000]]. The value also depends somehow on the key type: a set of values is represented as a list, all other values are represented by a string, even if it is a number. The following table maps key types to symbols and to the type of the value representation. |=Key Type<| Symbol<| Value< | [[Credential|00001006031000]] | ''CREDENTIAL'' | string | [[EString|00001006031500]] | ''EMPTY-STRING'' | string | [[Identifier|00001006032000]] | ''ZID'' | string | [[IdentifierSet|00001006032500]] | ''ZID-SET'' | ListValue | [[Number|00001006033000]] | ''NUMBER'' | string | [[String|00001006033500]] | ''STRING'' | string | [[TagSet|00001006034000]] | ''TAG-SET'' | ListValue | [[Timestamp|00001006034500]] | ''TIMESTAMP'' | string | [[URL|00001006035000]] | ''URL'' | string | [[Word|00001006035500]] | ''WORD'' | string :::syntax __ListValue__ **=** ''('' String,,1,, String,,2,, … String,,n,, '')''. ::: Examples: * The title of this zettel is represented as: ''(EMPTY-STRING title "Encoding of Sz Metadata")'' * The tags of this zettel are represented as: ''(TAG-SET tags ("#api" "#manual" "#reference" "#zettelstore"))'' |
Changes to docs/manual/00001012931400.zettel.
1 2 3 4 5 6 | id: 00001012931400 title: Encoding of Sz Block Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161803 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012931400 title: Encoding of Sz Block Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161803 modified: 20250304220504 === ''PARA'' :::syntax __Paragraph__ **=** ''(PARA'' [[__InlineElement__|00001012931600]] … '')''. ::: A paragraph is just a list of inline elements. |
︙ | ︙ | |||
43 44 45 46 47 48 49 | ::: A list element is either a block or an inline. If it is a block, it may contain a nested list. === ''DESCRIPTION'' :::syntax __Description__ **=** ''(DESCRIPTION'' __DescriptionTerm__ __DescriptionValues__ __DescriptionTerm__ __DescriptionValues__ … '')''. ::: | | | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | ::: A list element is either a block or an inline. If it is a block, it may contain a nested list. === ''DESCRIPTION'' :::syntax __Description__ **=** ''(DESCRIPTION'' __DescriptionTerm__ __DescriptionValues__ __DescriptionTerm__ __DescriptionValues__ … '')''. ::: A description is a sequence of one or more terms and values. :::syntax __DescriptionTerm__ **=** ''('' [[__InlineElement__|00001012931600]] … '')''. ::: A description term is just an inline-structured value. :::syntax |
︙ | ︙ | |||
122 123 124 125 126 127 128 | Attributes may further specify the quotation. The inline typically describes author / source of the quotation. :::syntax __VerseRegion__ **=** ''(REGION-VERSE'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] … '')'' [[__InlineElement__|00001012931600]] … '')''. ::: A block region just treats the block to contain a verse. | | | 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | Attributes may further specify the quotation. The inline typically describes author / source of the quotation. :::syntax __VerseRegion__ **=** ''(REGION-VERSE'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] … '')'' [[__InlineElement__|00001012931600]] … '')''. ::: A block region just treats the block to contain a verse. Soft line breaks are transformed into hard line breaks to save the structure of the verse / poem. Attributes may further specify something. The inline typically describes author / source of the verse. === ''VERBATIM-*'' The following lists specifies some literal text of more than one line. The structure is always the same, the initial symbol denotes the actual usage. The content is encoded as a string, most likely to contain control characters that signals the end of a line. |
︙ | ︙ | |||
174 175 176 177 178 179 180 | The first string contains the syntax of the image. The second string contains the actual image. If the syntax is ""SVG"", then the second string contains the SVG code. Otherwise the (binary) image data is encoded with base64. === ''TRANSCLUDE'' :::syntax | | | 174 175 176 177 178 179 180 181 182 183 184 185 186 | The first string contains the syntax of the image. The second string contains the actual image. If the syntax is ""SVG"", then the second string contains the SVG code. Otherwise the (binary) image data is encoded with base64. === ''TRANSCLUDE'' :::syntax __Transclude__ **=** ''(TRANSCLUDE'' [[__Attributes__|00001012931000#attribute]] [[__Reference__|00001012931900]] [[__InlineElement__|00001012931600]] … '')''. ::: A transclude list only occurs for a parsed zettel, but not for a evaluated zettel. Evaluating a zettel also means that all transclusions are resolved. __Reference__ denotes the zettel to be transcluded. |
Changes to docs/manual/00001012931600.zettel.
1 2 3 4 5 6 | id: 00001012931600 title: Encoding of Sz Inline Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161845 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012931600 title: Encoding of Sz Inline Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161845 modified: 20250102221540 === ''TEXT'' :::syntax __Text__ **=** ''(TEXT'' String '')''. ::: Specifies the string as some text content, including white space characters. |
︙ | ︙ | |||
92 93 94 95 96 97 98 | === ''EMBED-BLOB'' :::syntax __EmbedBLOB__ **=** ''(EMBED-BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, '')''. ::: If used if some processed image has to be embedded inside some inline material. The first string specifies the syntax of the image content. The second string contains the image content. | | | 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | === ''EMBED-BLOB'' :::syntax __EmbedBLOB__ **=** ''(EMBED-BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, '')''. ::: If used if some processed image has to be embedded inside some inline material. The first string specifies the syntax of the image content. The second string contains the image content. If the syntax is ""SVG"", the image content is not encoded further. Otherwise a base64 encoding is used. === ''CITE'' :::syntax __CiteBLOB__ **=** ''(CITE'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. ::: The string contains the citation key. |
︙ | ︙ | |||
138 139 140 141 142 143 144 | __InsertFormat__ **=** ''(FORMAT-INSERT'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. ::: The inline text should be treated as inserted. :::syntax __MarkFormat__ **=** ''(FORMAT-MARK'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. ::: | | | 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | __InsertFormat__ **=** ''(FORMAT-INSERT'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. ::: The inline text should be treated as inserted. :::syntax __MarkFormat__ **=** ''(FORMAT-MARK'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. ::: The inline text should be treated as highlighted for the reader (but was not important to the original author). :::syntax __QuoteFormat__ **=** ''(FORMAT-QUOTE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. ::: The inline text should be treated as quoted text. :::syntax |
︙ | ︙ | |||
187 188 189 190 191 192 193 | __HTMLLiteral__ **=** ''(LITERAL-HTML'' [[__Attributes__|00001012931000#attribute]] String '')''. ::: The string contains text that should be treated as HTML code. :::syntax __InputLiteral__ **=** ''(LITERAL-INPUT'' [[__Attributes__|00001012931000#attribute]] String '')''. ::: | | | | 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | __HTMLLiteral__ **=** ''(LITERAL-HTML'' [[__Attributes__|00001012931000#attribute]] String '')''. ::: The string contains text that should be treated as HTML code. :::syntax __InputLiteral__ **=** ''(LITERAL-INPUT'' [[__Attributes__|00001012931000#attribute]] String '')''. ::: The string contains text that should be treated as input entered by a user. :::syntax __MathLiteral__ **=** ''(LITERAL-MATH'' [[__Attributes__|00001012931000#attribute]] String '')''. ::: The string contains text that should be treated as special code to be interpreted as mathematical formulas. :::syntax __OutputLiteral__ **=** ''(LITERAL-OUTPUT'' [[__Attributes__|00001012931000#attribute]] String '')''. ::: The string contains text that should be treated as computer output to be read by a user. :::syntax __ZettelLiteral__ **=** ''(LITERAL-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String '')''. ::: The string contains text that should be treated as (nested) zettel content. |
Changes to docs/manual/00001012931900.zettel.
1 2 3 4 5 6 | id: 00001012931900 title: Encoding of Sz Reference Values role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230405123046 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012931900 title: Encoding of Sz Reference Values role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230405123046 modified: 20250102172048 A reference is encoded as the actual reference value, and a symbol describing the state of that actual reference value. :::syntax __Reference__ **=** ''('' __ReferenceState__ String '')''. ::: The string contains the actual reference value. |
︙ | ︙ | |||
26 27 28 29 30 31 32 | This value is only possible before evaluating the zettel. ; ''SELF'' : The reference value is a reference to the same zettel, to a specific mark. ; ''FOUND'' : The reference value is a valid reference to an existing zettel. This value is only possible after evaluating the zettel. ; ''BROKEN'' | | | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | This value is only possible before evaluating the zettel. ; ''SELF'' : The reference value is a reference to the same zettel, to a specific mark. ; ''FOUND'' : The reference value is a valid reference to an existing zettel. This value is only possible after evaluating the zettel. ; ''BROKEN'' : The reference value is a valid reference to a missing zettel. This value is only possible after evaluating the zettel. ; ''HOSTED'' : The reference value starts with one slash character, denoting an absolute local reference. ; ''BASED'' : The reference value starts with two slash characters, denoting a local reference interpreted relative to the Zettelstore base URL. ; ''QUERY'' : The reference value contains a query expression. ; ''EXTERNAL'' : The reference value contains a full URL, referencing a resource outside of the Zettelstore server. |
Changes to docs/manual/00001017000000.zettel.
1 2 3 4 5 6 | id: 00001017000000 title: Tips and Tricks role: manual tags: #manual #zettelstore syntax: zmk created: 20220803170112 | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | id: 00001017000000 title: Tips and Tricks role: manual tags: #manual #zettelstore syntax: zmk created: 20220803170112 modified: 20250102190553 === Welcome Zettel * **Problem:** You want to put your Zettelstore into the public and need a starting zettel for your users. In addition, you still want a ""home zettel"", with all your references to internal, non-public zettel. Zettelstore only allows specifying one [[''home-zettel''|00001004020000#home-zettel]]. * **Solution 1:** *# Create a new zettel with all your references to internal, non-public zettel. Let's assume this zettel receives the zettel identifier ''20220803182600''. *# Create the zettel that should serve as the starting zettel for your users. It must have syntax [[Zettelmarkup|00001008000000#zmk]], i.e. the syntax metadata must be set to ''zmk''. If needed, set the runtime configuration [[''home-zettel''|00001004020000#home-zettel]] to the value of the identifier of this zettel. *# At the beginning of the start zettel, add the following [[Zettelmarkup|00001007000000]] text in a separate paragraph: ``{{{20220803182600}}}`` (you have to adapt to the actual value of the zettel identifier for your non-public home zettel). * **Discussion:** As stated in the description for a [[transclusion|00001007031100]], a transclusion will be ignored, if the transcluded zettel is not visible to the current user. In effect, the transclusion statement (above paragraph that contained ''{{{...}}}'') is ignored when rendering the zettel. * **Solution 2:** Set a user-specific value by adding metadata ''home-zettel'' to the [[user zettel|00001010040200]]. * **Discussion:** A value for ''home-zettel'' is first searched in the user zettel of the current authenticated user. Only if it is not found, the value is looked up in the runtime configuration zettel. If multiple user should use the same home zettel, its zettel identifier must be set in all relevant user zettel. |
︙ | ︙ | |||
44 45 46 47 48 49 50 | In general, the mapping must follow the pattern: ``(ROLE . ID)``, where ''ROLE'' is the placeholder for the role, and ''ID'' for the zettel identifier containing CSS code. For example, if you also want the role ""configuration"" to be rendered using that CSS, the code should be something like ``(set! CSS-ROLE-map '(("zettel" . "20220825200100") ("configuration" . "20220825200100")))``. * **Discussion:** you have to ensure that the CSS zettel is allowed to be read by the intended audience of the zettel with that given role. For example, if you made zettel with a specific role public visible, the CSS zettel must also have a [[''visibility: public''|00001010070200]] metadata. === Zettel synchronization with iCloud (Apple) | | | | | | | 44 45 46 47 48 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 | In general, the mapping must follow the pattern: ``(ROLE . ID)``, where ''ROLE'' is the placeholder for the role, and ''ID'' for the zettel identifier containing CSS code. For example, if you also want the role ""configuration"" to be rendered using that CSS, the code should be something like ``(set! CSS-ROLE-map '(("zettel" . "20220825200100") ("configuration" . "20220825200100")))``. * **Discussion:** you have to ensure that the CSS zettel is allowed to be read by the intended audience of the zettel with that given role. For example, if you made zettel with a specific role public visible, the CSS zettel must also have a [[''visibility: public''|00001010070200]] metadata. === Zettel synchronization with iCloud (Apple) * **Problem:** You use Zettelstore on various macOS computers and you want to use the same set of zettel across all computers. * **Solution:** Place your zettel in an iCloud folder. To configure Zettelstore to use the folder, you must specify its location within your directory structure as [[''box-uri-X''|00001004010000#box-uri-x]] (replace ''X'' with an appropriate number). Your iCloud folder is typically placed in the folder ''~/Library/Mobile Documents/com~apple~CloudDocs''. The ""''~''"" is a shortcut and specifies your home folder. Unfortunately, Zettelstore does not yet support this shortcut. Therefore you must replace it with the absolute name of your home folder. In addition, a space character is not allowed in a URI. You have to replace it with the sequence ""''%20''"". Let us assume, that you stored your zettel box inside the folder ""zettel"", which is located top-level in your iCloud folder. In this case, you must specify the following box URI within the startup configuration: ''box-uri-1: dir:///Users/USERNAME/Library/Mobile%20Documents/com~apple~CloudDocs/zettel'', replacing ''USERNAME'' with the username of that specific computer (and assuming you want to use it as the first box). * **Solution 2:** If you typically start your Zettelstore on the command line, you could use the ''-d DIR'' option for the [[''run''|00001004051000#d]] sub-command. In this case you are allowed to use the character ""''~''"". ''zettelstore run -d ~/Library/Mobile\\ Documents/com\\~apple\\~CloudDocs/zettel'' (The ""''\\''"" is needed by the command line processor to mask the following character to be processed in unintended ways.) * **Discussion:** Zettel files are synchronized between your computers via iCloud. It does not matter, if one of your computers is offline or switched off. iCloud will synchronize the zettel files if it later comes online. However, if you use more than one computer simultaneously, you must be aware that synchronization takes some time. It might take several seconds, maybe longer, that the new version of a zettel appears on the other computer. If you update the same zettel on multiple computers at nearly the same time, iCloud will not be able to synchronize the different versions in a safe manner. Zettelstore is intentionally not aware of any synchronization within its zettel boxes. If Zettelstore behaves strangely after a synchronization took place, the page about [[Troubleshooting|00001018000000#working-with-files]] might contain some useful information. |
Changes to docs/manual/00001018000000.zettel.
1 2 3 4 5 6 | id: 00001018000000 title: Troubleshooting role: manual tags: #manual #zettelstore syntax: zmk created: 20211027105921 | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | id: 00001018000000 title: Troubleshooting role: manual tags: #manual #zettelstore syntax: zmk created: 20211027105921 modified: 20250106180049 This page lists some problems and their solutions that may occur when using your Zettelstore. === Installation * **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer. Therefore, it will not start Zettelstore. ** **Solution:** Press the ''Ctrl'' key while opening the context menu of the Zettelstore executable with a right-click. A dialog is then opened where you can acknowledge that you understand the possible risks when you start Zettelstore. This dialog is only presented once for a given Zettelstore executable. * **Problem:** When you double-click on the Zettelstore executable icon, Windows complains that Zettelstore is an application from an unknown developer. ** **Solution:** Windows displays a dialog where you can acknowledge possible risks and allow to start Zettelstore. === Authentication * **Problem:** [[Authentication is enabled|00001010040100]] for a local running Zettelstore and there is a valid [[user zettel|00001010040200]] for the owner. But entering user name and password at the [[web user interface|00001014000000]] seems to be ignored, while entering a wrong password will result in an error message. ** **Explanation:** A local running Zettelstore typically means, that you are accessing the Zettelstore using an URL with schema ''http://'', and not ''https://'', for example ''http://localhost:23123''. The difference between these two is the missing encryption of user name / password and for the answer of the Zettelstore if you use the ''http://'' schema. To be secure by default, the Zettelstore will not work in an insecure environment. ** **Solution 1:** If you are sure that your communication medium is safe, even if you use the ''http:/\/'' schema (for example, you are running the Zettelstore on the same computer you are working on, or if the Zettelstore is running on a computer in your protected local network), then you could add the entry ''insecure-cookie: true'' in your [[startup configuration|00001004010000#insecure-cookie]] file. ** **Solution 2:** If you are not sure about the security of your communication medium (for example, if unknown persons might use your local network), then you should run an [[external server|00001010090100]] in front of your Zettelstore to enable the use of the ''https://'' schema. === Working with Zettel Files * **Problem:** When you delete a zettel file by removing it from the ""disk"", e.g. by dropping it into the trash folder, by dragging into another folder, or by removing it from the command line, Zettelstore sometimes does not detect the change. If you access the zettel via Zettelstore, an error is reported. ** **Explanation:** Sometimes, the operating system does not tell Zettelstore about the removed zettel. This occurs mostly under MacOS. ** **Solution 1:** If you are running Zettelstore in [[""simple-mode""|00001004051100]] or if you have enabled [[''expert-mode''|00001004020000#expert-mode]], you are allowed to refresh the internal data by selecting ""Refresh"" in the Web User Interface (you can find in the menu ""Lists""). ** **Solution 2:** There is an [[API|00001012080500]] call to make Zettelstore aware of this change. ** **Solution 3:** If you have an enabled [[Administrator Console|00001004100000]] you can use the command [[''refresh''|00001004101000#refresh]] to make your changes visible. ** **Solution 4:** You configure the zettel box as [[""simple""|00001004011400]]. === HTML content is not shown * **Problem:** You have entered some HTML code as content for your Zettelstore, but this content is not shown on the Web User Interface. |
︙ | ︙ |
Changes to encoder/encoder.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | import ( "errors" "fmt" "io" "t73f.de/r/zsc/api" | | | | | < < < < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import ( "errors" "fmt" "io" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/ast" ) // Encoder is an interface that allows to encode different parts of a zettel. type Encoder interface { WriteZettel(io.Writer, *ast.ZettelNode) (int, error) WriteMeta(io.Writer, *meta.Meta) (int, error) WriteContent(io.Writer, *ast.ZettelNode) (int, error) WriteBlocks(io.Writer, *ast.BlockSlice) (int, error) WriteInlines(io.Writer, *ast.InlineSlice) (int, error) } // Some errors to signal when encoder methods are not implemented. var ( ErrNoWriteZettel = errors.New("method WriteZettel is not implemented") ErrNoWriteMeta = errors.New("method WriteMeta is not implemented") ErrNoWriteContent = errors.New("method WriteContent is not implemented") ErrNoWriteBlocks = errors.New("method WriteBlocks is not implemented") ErrNoWriteInlines = errors.New("method WriteInlines is not implemented") |
︙ | ︙ |
Changes to encoder/encoder_blob_test.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package encoder_test import ( "testing" | | > < < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | //----------------------------------------------------------------------------- package encoder_test import ( "testing" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/config" "zettelstore.de/z/parser" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. ) type blobTestCase struct { descr string blob []byte |
︙ | ︙ | |||
50 51 52 53 54 55 56 | encoderZmk: `%% Unable to display BLOB with description 'PNG' and syntax 'png'.`, }, }, } func TestBlob(t *testing.T) { m := meta.New(id.Invalid) | | | | | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | encoderZmk: `%% Unable to display BLOB with description 'PNG' and syntax 'png'.`, }, }, } func TestBlob(t *testing.T) { m := meta.New(id.Invalid) m.Set(meta.KeyTitle, "PNG") for testNum, tc := range pngTestCases { inp := input.NewInput(tc.blob) bs := parser.ParseBlocks(inp, m, "png", config.NoHTML) checkEncodings(t, testNum, bs, false, tc.descr, tc.expect, "???") } } |
Changes to encoder/encoder_block_test.go.
︙ | ︙ | |||
334 335 336 337 338 339 340 | |f1|f2|=f3`, expect: expectMap{ encoderHTML: `<table><thead><tr><th class="right">h1</th><th>h2</th><th class="center">h3</th></tr></thead><tbody><tr><td class="left">c1</td><td>c2</td><td class="center">c3</td></tr><tr><td class="right">f1</td><td>f2</td><td class="center">=f3</td></tr></tbody></table>`, encoderMD: "", encoderSz: `(BLOCK (TABLE ((CELL-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")))))`, encoderSHTML: `((table (thead (tr (th (@ (class . "right")) "h1") (th "h2") (th (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`, encoderText: "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3", | | | | > > > | 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 | |f1|f2|=f3`, expect: expectMap{ encoderHTML: `<table><thead><tr><th class="right">h1</th><th>h2</th><th class="center">h3</th></tr></thead><tbody><tr><td class="left">c1</td><td>c2</td><td class="center">c3</td></tr><tr><td class="right">f1</td><td>f2</td><td class="center">=f3</td></tr></tbody></table>`, encoderMD: "", encoderSz: `(BLOCK (TABLE ((CELL-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")))))`, encoderSHTML: `((table (thead (tr (th (@ (class . "right")) "h1") (th "h2") (th (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`, encoderText: "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3", encoderZmk: /*`|=h1>|=h2|=h3: |<c1|c2|c3 |f1|f2|=f3`,*/ `|=>h1|=h2|=:h3 |<c1|c2|:c3 |>f1|f2|:=f3`, }, }, { descr: "Simple Endnote", zmk: `Text[^Endnote]`, expect: expectMap{ encoderHTML: "<p>Text<sup id=\"fnref:1\"><a class=\"zs-noteref\" href=\"#fn:1\" role=\"doc-noteref\">1</a></sup></p><ol class=\"zs-endnotes\"><li class=\"zs-endnote\" id=\"fn:1\" role=\"doc-endnote\" value=\"1\">Endnote <a class=\"zs-endnote-backref\" href=\"#fnref:1\" role=\"doc-backlink\">\u21a9\ufe0e</a></li></ol>", |
︙ | ︙ |
Changes to encoder/encoder_inline_test.go.
︙ | ︙ | |||
263 264 265 266 267 268 269 | encoderSz: `(INLINE (LITERAL-CODE (("-" . "")) "x y"))`, encoderSHTML: "((code \"x\u2423y\"))", encoderText: `x y`, encoderZmk: useZmk, }, }, { | | | 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | encoderSz: `(INLINE (LITERAL-CODE (("-" . "")) "x y"))`, encoderSHTML: "((code \"x\u2423y\"))", encoderText: `x y`, encoderZmk: useZmk, }, }, { descr: "HTML in code formatting", zmk: "``<script `` abc", expect: expectMap{ encoderHTML: "<code><script </code> abc", encoderMD: "`<script ` abc", encoderSz: `(INLINE (LITERAL-CODE () "<script ") (TEXT " abc"))`, encoderSHTML: `((code "<script ") " abc")`, encoderText: `<script abc`, |
︙ | ︙ | |||
383 384 385 386 387 388 389 | encoderSHTML: `((@@ "line comment"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Comment after text", | | | | 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 | encoderSHTML: `((@@ "line comment"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Comment after text", zmk: `Text%%{-} comment`, expect: expectMap{ encoderHTML: `Text<!-- comment -->`, encoderMD: "Text", encoderSz: `(INLINE (TEXT "Text") (LITERAL-COMMENT (("-" . "")) "comment"))`, encoderSHTML: `("Text" (@@ "comment"))`, encoderText: `Text`, encoderZmk: useZmk, }, }, { descr: "Comment after text and with -->", zmk: `Text%%{-} comment --> end`, expect: expectMap{ encoderHTML: `Text<!-- comment --> end -->`, encoderMD: "Text", encoderSz: `(INLINE (TEXT "Text") (LITERAL-COMMENT (("-" . "")) "comment --> end"))`, encoderSHTML: `("Text" (@@ "comment --> end"))`, encoderText: `Text`, encoderZmk: useZmk, |
︙ | ︙ | |||
630 631 632 633 634 635 636 | zmk: `{{abc}}`, expect: expectMap{ encoderHTML: `<img src="abc">`, encoderMD: "", encoderSz: `(INLINE (EMBED () (EXTERNAL "abc") ""))`, encoderSHTML: `((img (@ (src . "abc"))))`, encoderText: ``, | < < < < < < < < < < < < < < < < < < < < < < < < | 630 631 632 633 634 635 636 637 638 639 640 641 642 643 | zmk: `{{abc}}`, expect: expectMap{ encoderHTML: `<img src="abc">`, encoderMD: "", encoderSz: `(INLINE (EMBED () (EXTERNAL "abc") ""))`, encoderSHTML: `((img (@ (src . "abc"))))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "", zmk: ``, expect: expectMap{ |
︙ | ︙ |
Changes to encoder/encoder_test.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 | import ( "fmt" "strings" "testing" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/api" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" | > < | | | | | | < | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | import ( "fmt" "strings" "testing" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. _ "zettelstore.de/z/encoder/mdenc" // Allow to use markdown encoder. _ "zettelstore.de/z/encoder/shtmlenc" // Allow to use SHTML encoder. _ "zettelstore.de/z/encoder/szenc" // Allow to use sz encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) type zmkTestCase struct { descr string zmk string inline bool |
︙ | ︙ | |||
62 63 64 65 66 67 68 | } executeTestCases(t, append(tcsBlock, tcsInline...)) } func executeTestCases(t *testing.T, testCases []zmkTestCase) { for testNum, tc := range testCases { inp := input.NewInput([]byte(tc.zmk)) | < < < < < < | < | | | | | | | | | | < | | > > > | < < | > > > | | < < | < < | < | < < < | < < < | | | < < | 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 | } executeTestCases(t, append(tcsBlock, tcsInline...)) } func executeTestCases(t *testing.T, testCases []zmkTestCase) { for testNum, tc := range testCases { inp := input.NewInput([]byte(tc.zmk)) bs := parser.ParseBlocks(inp, nil, meta.ValueSyntaxZmk, config.NoHTML) checkEncodings(t, testNum, bs, tc.inline, tc.descr, tc.expect, tc.zmk) checkSz(t, testNum, bs, tc.inline, tc.descr) } } func checkEncodings(t *testing.T, testNum int, bs ast.BlockSlice, isInline bool, descr string, expected expectMap, zmkDefault string) { for enc, exp := range expected { encdr := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}) got, err := encode(encdr, bs, isInline) if err != nil { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } prefix += "\nMode: " + mode(isInline) t.Errorf("%s\nEncoder: %s\nError: %v", prefix, enc, err) continue } if enc == api.EncoderZmk && exp == useZmk { exp = zmkDefault } if got != exp { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } prefix += "\nMode: " + mode(isInline) t.Errorf("%s\nEncoder: %s\nExpected: %q\nGot: %q", prefix, enc, exp, got) } } } func checkSz(t *testing.T, testNum int, bs ast.BlockSlice, isInline bool, descr string) { t.Helper() encdr := encoder.Create(encoderSz, nil) exp, err := encode(encdr, bs, isInline) if err != nil { t.Error(err) return } val, err := sxreader.MakeReader(strings.NewReader(exp)).Read() if err != nil { t.Error(err) return } got := val.String() if exp != got { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d } prefix += "\nMode: " + mode(isInline) t.Errorf("%s\n\nExpected: %q\nGot: %q", prefix, exp, got) } } func encode(e encoder.Encoder, bs ast.BlockSlice, isInline bool) (string, error) { var sb strings.Builder if !isInline { _, err := e.WriteBlocks(&sb, &bs) return sb.String(), err } var is ast.InlineSlice if len(bs) > 0 { if pn, ok := bs[0].(*ast.ParaNode); ok { is = pn.Inlines } } _, err := e.WriteInlines(&sb, &is) return sb.String(), err } func mode(isInline bool) string { if isInline { return "inline" } return "block" } |
Changes to encoder/htmlenc/htmlenc.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 | import ( "io" "strings" "t73f.de/r/sx" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/shtml" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/szenc" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/parser" | > < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import ( "io" "strings" "t73f.de/r/sx" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/szenc" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/parser" ) func init() { encoder.Register( api.EncoderHTML, func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) }, ) |
︙ | ︙ | |||
54 55 56 57 58 59 60 | tx *szenc.Transformer th *shtml.Evaluator lang string textEnc *textenc.Encoder } // WriteZettel encodes a full zettel as HTML5. | | | | | | | > | > | < | | | | | 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 | tx *szenc.Transformer th *shtml.Evaluator lang string textEnc *textenc.Encoder } // WriteZettel encodes a full zettel as HTML5. func (he *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { env := shtml.MakeEnvironment(he.lang) hm, err := he.th.Evaluate(he.tx.GetMeta(zn.InhMeta), &env) if err != nil { return 0, err } var isTitle ast.InlineSlice var htitle *sx.Pair plainTitle, hasTitle := zn.InhMeta.Get(meta.KeyTitle) if hasTitle { isTitle = parser.ParseSpacedText(string(plainTitle)) xtitle := he.tx.GetSz(&isTitle) htitle, err = he.th.Evaluate(xtitle, &env) if err != nil { return 0, err } } xast := he.tx.GetSz(&zn.BlocksAST) hast, err := he.th.Evaluate(xast, &env) if err != nil { return 0, err } hen := shtml.Endnotes(&env) var head sx.ListBuilder head.AddN( shtml.SymHead, sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.MakeString("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta), ) head.ExtendBang(hm) var sb strings.Builder if hasTitle { he.textEnc.WriteInlines(&sb, &isTitle) } else { sb.Write(zn.Meta.Zid.Bytes()) } head.Add(sx.MakeList(shtml.SymAttrTitle, sx.MakeString(sb.String()))) var body sx.ListBuilder body.Add(shtml.SymBody) if hasTitle { body.Add(htitle.Cons(shtml.SymH1)) } body.ExtendBang(hast) if hen != nil { body.AddN(sx.Cons(shtml.SymHR, nil), hen) } doc := sx.MakeList( sxhtml.SymDoctype, sx.MakeList(shtml.SymHTML, head.List(), body.List()), ) gen := sxhtml.NewGenerator().SetNewline() return gen.WriteHTML(w, doc) } // WriteMeta encodes meta data as HTML5. func (he *Encoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { env := shtml.MakeEnvironment(he.lang) hm, err := he.th.Evaluate(he.tx.GetMeta(m), &env) if err != nil { return 0, err } gen := sxhtml.NewGenerator().SetNewline() return gen.WriteListHTML(w, hm) } // WriteContent encodes the zettel content. func (he *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return he.WriteBlocks(w, &zn.BlocksAST) } // WriteBlocks encodes a block slice. func (he *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { env := shtml.MakeEnvironment(he.lang) hobj, err := he.th.Evaluate(he.tx.GetSz(bs), &env) if err == nil { |
︙ | ︙ |
Changes to encoder/mdenc/mdenc.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 | package mdenc import ( "io" "t73f.de/r/zsc/api" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/shtml" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" | > < | | | | | | | < | < < < < < < < | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | package mdenc import ( "io" "t73f.de/r/zsc/api" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" ) func init() { encoder.Register( api.EncoderMD, func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) }, ) } // Create an encoder. func Create(params *encoder.CreateParameter) *Encoder { return &Encoder{lang: params.Lang} } // Encoder contains all data needed for encoding. type Encoder struct { lang string } // WriteZettel writes the encoded zettel to the writer. func (me *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { v := newVisitor(w, me.lang) v.acceptMeta(zn.InhMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { v.b.WriteByte('\n') } ast.Walk(v, &zn.BlocksAST) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as markdown. func (me *Encoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newVisitor(w, me.lang) v.acceptMeta(m) length, err := v.b.Flush() return length, err } func (v *visitor) acceptMeta(m *meta.Meta) { for key, val := range m.Computed() { v.b.WriteStrings(key, ": ", string(val), "\n") } } // WriteContent encodes the zettel content. func (me *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return me.WriteBlocks(w, &zn.BlocksAST) } // WriteBlocks writes the content of a block slice to the writer. func (me *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { v := newVisitor(w, me.lang) ast.Walk(v, bs) length, err := v.b.Flush() |
︙ | ︙ | |||
178 179 180 181 182 183 184 | } ast.Walk(v, bn) } } func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { lc := len(vn.Content) | | | 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | } ast.Walk(v, bn) } } func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { lc := len(vn.Content) if vn.Kind != ast.VerbatimCode || lc == 0 { return } v.writeSpaces(4) lcm1 := lc - 1 for i := 0; i < lc; i++ { b := vn.Content[i] if b != '\n' && b != '\r' { |
︙ | ︙ | |||
395 396 397 398 399 400 401 | v.b.WriteString(" ") } v.b.WriteString(rightQ) } func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { | | | | 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 | v.b.WriteString(" ") } v.b.WriteString(rightQ) } func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralCode, ast.LiteralInput, ast.LiteralOutput: v.b.WriteByte('`') v.b.Write(ln.Content) v.b.WriteByte('`') case ast.LiteralComment: // ignore everything default: v.b.Write(ln.Content) } } func (v *visitor) writeSpaces(n int) { for range n { v.b.WriteByte(' ') } } |
Changes to encoder/shtmlenc/shtmlenc.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 | package shtmlenc import ( "io" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/shtml" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/szenc" | > < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package shtmlenc import ( "io" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/szenc" ) func init() { encoder.Register(api.EncoderSHTML, func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) }) } // Create a SHTML encoder |
︙ | ︙ | |||
45 46 47 48 49 50 51 | type Encoder struct { tx *szenc.Transformer th *shtml.Evaluator lang string } // WriteZettel writes the encoded zettel to the writer. | | | | | | | | 45 46 47 48 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 | type Encoder struct { tx *szenc.Transformer th *shtml.Evaluator lang string } // WriteZettel writes the encoded zettel to the writer. func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { env := shtml.MakeEnvironment(enc.lang) metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(zn.InhMeta), &env) if err != nil { return 0, err } contentSHTML, err := enc.th.Evaluate(enc.tx.GetSz(&zn.BlocksAST), &env) if err != nil { return 0, err } result := sx.Cons(metaSHTML, contentSHTML) return result.Print(w) } // WriteMeta encodes meta data as s-expression. func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { env := shtml.MakeEnvironment(enc.lang) metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(m), &env) if err != nil { return 0, err } return sx.Print(w, metaSHTML) } // WriteContent encodes the zettel content. func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return enc.WriteBlocks(w, &zn.BlocksAST) } // WriteBlocks writes a block slice to the writer func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { env := shtml.MakeEnvironment(enc.lang) hval, err := enc.th.Evaluate(enc.tx.GetSz(bs), &env) if err != nil { |
︙ | ︙ |
Changes to encoder/szenc/szenc.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 | package szenc import ( "io" "t73f.de/r/sx" "t73f.de/r/zsc/api" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" | > < | | | | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | package szenc import ( "io" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderSz, func(*encoder.CreateParameter) encoder.Encoder { return Create() }) } // Create a S-expr encoder func Create() *Encoder { // We need a new transformer every time, because trans.inVerse must be unique. // If we can refactor it out, the transformer can be created only once. return &Encoder{trans: NewTransformer()} } // Encoder contains all data needed for encoding. type Encoder struct { trans *Transformer } // WriteZettel writes the encoded zettel to the writer. func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { content := enc.trans.GetSz(&zn.BlocksAST) meta := enc.trans.GetMeta(zn.InhMeta) return sx.MakeList(meta, content).Print(w) } // WriteMeta encodes meta data as s-expression. func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { return enc.trans.GetMeta(m).Print(w) } // WriteContent encodes the zettel content. func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return enc.WriteBlocks(w, &zn.BlocksAST) } // WriteBlocks writes a block slice to the writer func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { return enc.trans.GetSz(bs).Print(w) } // WriteInlines writes an inline slice to the writer func (enc *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { return enc.trans.GetSz(is).Print(w) } |
Changes to encoder/szenc/transform.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 | import ( "encoding/base64" "fmt" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/sz" "zettelstore.de/z/ast" | > < < | | | < | < < < | < < < < < | | | | | < < < | < < < | < < | | < < < < < < | < | < < < | < | < | < < | > > > | | | | | | | | | | < | | | | | | | | | | | | < | | | | | | | | | | | | | < < | < < < | | | | > > | | | | | | | | | | | | | | | | | | | | | | | | | | | < | < | > | < < | | | | < < < | | | | | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | import ( "encoding/base64" "fmt" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sz" "zettelstore.de/z/ast" ) // NewTransformer returns a new transformer to create s-expressions from AST nodes. func NewTransformer() *Transformer { t := Transformer{} return &t } // Transformer contains all data needed to transform into a s-expression. type Transformer struct { inVerse bool } // GetSz transforms the given node into a sx list. func (t *Transformer) GetSz(node ast.Node) *sx.Pair { switch n := node.(type) { case *ast.BlockSlice: return sz.MakeBlockList(t.getBlockList(n)) case *ast.InlineSlice: return sz.MakeInlineList(t.getInlineList(*n)) case *ast.ParaNode: return sz.MakePara(t.getInlineList(n.Inlines)) case *ast.VerbatimNode: return sz.MakeVerbatim(mapGetS(mapVerbatimKindS, n.Kind), getAttributes(n.Attrs), string(n.Content)) case *ast.RegionNode: return t.getRegion(n) case *ast.HeadingNode: return sz.MakeHeading(n.Level, getAttributes(n.Attrs), t.getInlineList(n.Inlines), n.Slug, n.Fragment) case *ast.HRuleNode: return sz.MakeThematic(getAttributes(n.Attrs)) case *ast.NestedListNode: return t.getNestedList(n) case *ast.DescriptionListNode: return t.getDescriptionList(n) case *ast.TableNode: return t.getTable(n) case *ast.TranscludeNode: return sz.MakeTransclusion(getAttributes(n.Attrs), getReference(n.Ref), t.getInlineList(n.Inlines)) case *ast.BLOBNode: return t.getBLOB(n) case *ast.TextNode: return sz.MakeText(n.Text) case *ast.BreakNode: if n.Hard { return sz.MakeHard() } return sz.MakeSoft() case *ast.LinkNode: return t.getLink(n) case *ast.EmbedRefNode: return sz.MakeEmbed(getAttributes(n.Attrs), getReference(n.Ref), n.Syntax, t.getInlineList(n.Inlines)) case *ast.EmbedBLOBNode: return t.getEmbedBLOB(n) case *ast.CiteNode: return sz.MakeCite(getAttributes(n.Attrs), n.Key, t.getInlineList(n.Inlines)) case *ast.FootnoteNode: return sz.MakeEndnote(getAttributes(n.Attrs), t.getInlineList(n.Inlines)) case *ast.MarkNode: return sz.MakeMark(n.Mark, n.Slug, n.Fragment, t.getInlineList(n.Inlines)) case *ast.FormatNode: return sz.MakeFormat(mapGetS(mapFormatKindS, n.Kind), getAttributes(n.Attrs), t.getInlineList(n.Inlines)) case *ast.LiteralNode: return sz.MakeLiteral(mapGetS(mapLiteralKindS, n.Kind), getAttributes(n.Attrs), string(n.Content)) } return sx.MakeList(sz.SymUnknown, sx.MakeString(fmt.Sprintf("%T %v", node, node))) } var mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{ ast.VerbatimZettel: sz.SymVerbatimZettel, ast.VerbatimCode: sz.SymVerbatimCode, ast.VerbatimEval: sz.SymVerbatimEval, ast.VerbatimMath: sz.SymVerbatimMath, ast.VerbatimComment: sz.SymVerbatimComment, ast.VerbatimHTML: sz.SymVerbatimHTML, } var mapFormatKindS = map[ast.FormatKind]*sx.Symbol{ ast.FormatEmph: sz.SymFormatEmph, ast.FormatStrong: sz.SymFormatStrong, ast.FormatDelete: sz.SymFormatDelete, ast.FormatInsert: sz.SymFormatInsert, ast.FormatSuper: sz.SymFormatSuper, ast.FormatSub: sz.SymFormatSub, ast.FormatQuote: sz.SymFormatQuote, ast.FormatMark: sz.SymFormatMark, ast.FormatSpan: sz.SymFormatSpan, } var mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{ ast.LiteralCode: sz.SymLiteralCode, ast.LiteralInput: sz.SymLiteralInput, ast.LiteralOutput: sz.SymLiteralOutput, ast.LiteralComment: sz.SymLiteralComment, ast.LiteralMath: sz.SymLiteralMath, } var mapRegionKindS = map[ast.RegionKind]*sx.Symbol{ ast.RegionSpan: sz.SymRegionBlock, ast.RegionQuote: sz.SymRegionQuote, ast.RegionVerse: sz.SymRegionVerse, } func (t *Transformer) getRegion(rn *ast.RegionNode) *sx.Pair { saveInVerse := t.inVerse if rn.Kind == ast.RegionVerse { t.inVerse = true } symBlocks := t.getBlockList(&rn.Blocks) t.inVerse = saveInVerse return sz.MakeRegion( mapGetS(mapRegionKindS, rn.Kind), getAttributes(rn.Attrs), symBlocks, t.getInlineList(rn.Inlines), ) } var mapNestedListKindS = map[ast.NestedListKind]*sx.Symbol{ ast.NestedListOrdered: sz.SymListOrdered, ast.NestedListUnordered: sz.SymListUnordered, ast.NestedListQuote: sz.SymListQuote, } func (t *Transformer) getNestedList(ln *ast.NestedListNode) *sx.Pair { var nlistObjs sx.ListBuilder nlistObjs.Add(mapGetS(mapNestedListKindS, ln.Kind)) isCompact := isCompactList(ln.Items) for _, item := range ln.Items { if isCompact && len(item) > 0 { paragraph := t.GetSz(item[0]) nlistObjs.Add(sz.MakeInlineList(paragraph.Tail())) continue } var itemObjs sx.ListBuilder for _, in := range item { itemObjs.Add(t.GetSz(in)) } if isCompact { nlistObjs.Add(sz.MakeInlineList(itemObjs.List())) } else { nlistObjs.Add(sz.MakeBlockList(itemObjs.List())) } } return nlistObjs.List() } func isCompactList(itemSlice []ast.ItemSlice) bool { for _, items := range itemSlice { if len(items) > 1 { return false } if len(items) == 1 { if _, ok := items[0].(*ast.ParaNode); !ok { return false } } } return true } func (t *Transformer) getDescriptionList(dn *ast.DescriptionListNode) *sx.Pair { var dlObjs sx.ListBuilder for _, def := range dn.Descriptions { dlObjs.Add(t.getInlineList(def.Term)) var descObjs sx.ListBuilder for _, b := range def.Descriptions { var dVal sx.ListBuilder for _, dn := range b { dVal.Add(t.GetSz(dn)) } descObjs.Add(sz.MakeBlockList(dVal.List())) } dlObjs.Add(sz.MakeBlockList(descObjs.List())) } return dlObjs.List().Cons(sz.SymDescription) } func (t *Transformer) getTable(tn *ast.TableNode) *sx.Pair { var lb sx.ListBuilder lb.AddN(sz.SymTable, t.getHeader(tn.Header)) for _, row := range tn.Rows { lb.Add(t.getRow(row)) } return lb.List() } func (t *Transformer) getHeader(header ast.TableRow) *sx.Pair { if len(header) == 0 { return nil } return t.getRow(header) } func (t *Transformer) getRow(row ast.TableRow) *sx.Pair { var lb sx.ListBuilder for _, cell := range row { lb.Add(t.getCell(cell)) } return lb.List() } var alignmentSymbolS = map[ast.Alignment]*sx.Symbol{ ast.AlignDefault: sz.SymCell, ast.AlignLeft: sz.SymCellLeft, ast.AlignCenter: sz.SymCellCenter, ast.AlignRight: sz.SymCellRight, } func (t *Transformer) getCell(cell *ast.TableCell) *sx.Pair { return sz.MakeCell(mapGetS(alignmentSymbolS, cell.Align), t.getInlineList(cell.Inlines)) } func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sx.Pair { var content string if bn.Syntax == meta.ValueSyntaxSVG { content = string(bn.Blob) } else { content = getBase64String(bn.Blob) } return sz.MakeBLOB(t.getInlineList(bn.Description), bn.Syntax, content) } var mapRefStateLink = map[ast.RefState]*sx.Symbol{ ast.RefStateInvalid: sz.SymLinkInvalid, ast.RefStateZettel: sz.SymLinkZettel, ast.RefStateSelf: sz.SymLinkSelf, ast.RefStateFound: sz.SymLinkFound, ast.RefStateBroken: sz.SymLinkBroken, ast.RefStateHosted: sz.SymLinkHosted, ast.RefStateBased: sz.SymLinkBased, ast.RefStateQuery: sz.SymLinkQuery, ast.RefStateExternal: sz.SymLinkExternal, } func (t *Transformer) getLink(ln *ast.LinkNode) *sx.Pair { return sz.MakeLink( mapGetS(mapRefStateLink, ln.Ref.State), getAttributes(ln.Attrs), ln.Ref.Value, t.getInlineList(ln.Inlines), ) } func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair { var content string if en.Syntax == meta.ValueSyntaxSVG { content = string(en.Blob) } else { content = getBase64String(en.Blob) } return sz.MakeEmbedBLOB(getAttributes(en.Attrs), en.Syntax, content, t.getInlineList(en.Inlines)) } func (t *Transformer) getBlockList(bs *ast.BlockSlice) *sx.Pair { var lb sx.ListBuilder for _, n := range *bs { lb.Add(t.GetSz(n)) } return lb.List() } func (t *Transformer) getInlineList(is ast.InlineSlice) *sx.Pair { var lb sx.ListBuilder for _, n := range is { lb.Add(t.GetSz(n)) } return lb.List() } func getAttributes(a attrs.Attributes) *sx.Pair { if a.IsEmpty() { return sx.Nil() } keys := a.Keys() var lb sx.ListBuilder for _, k := range keys { lb.Add(sx.Cons(sx.MakeString(k), sx.MakeString(a[k]))) } return lb.List() } var mapRefStateS = map[ast.RefState]*sx.Symbol{ ast.RefStateInvalid: sz.SymRefStateInvalid, ast.RefStateZettel: sz.SymRefStateZettel, ast.RefStateSelf: sz.SymRefStateSelf, ast.RefStateFound: sz.SymRefStateFound, ast.RefStateBroken: sz.SymRefStateBroken, ast.RefStateHosted: sz.SymRefStateHosted, ast.RefStateBased: sz.SymRefStateBased, ast.RefStateQuery: sz.SymRefStateQuery, ast.RefStateExternal: sz.SymRefStateExternal, } func getReference(ref *ast.Reference) *sx.Pair { return sx.MakeList(mapGetS(mapRefStateS, ref.State), sx.MakeString(ref.Value)) } var mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{ meta.TypeCredential: sz.SymTypeCredential, meta.TypeEmpty: sz.SymTypeEmpty, meta.TypeID: sz.SymTypeID, meta.TypeIDSet: sz.SymTypeIDSet, meta.TypeNumber: sz.SymTypeNumber, meta.TypeString: sz.SymTypeString, meta.TypeTagSet: sz.SymTypeTagSet, meta.TypeTimestamp: sz.SymTypeTimestamp, meta.TypeURL: sz.SymTypeURL, meta.TypeWord: sz.SymTypeWord, } // GetMeta transforms the given metadata into a sx list. func (t *Transformer) GetMeta(m *meta.Meta) *sx.Pair { var lb sx.ListBuilder lb.Add(sz.SymMeta) for key, val := range m.Computed() { ty := m.Type(key) symType := mapGetS(mapMetaTypeS, ty) var obj sx.Object if ty.IsSet { var setObjs sx.ListBuilder for _, val := range val.AsSlice() { setObjs.Add(sx.MakeString(val)) } obj = setObjs.List() } else { obj = sx.MakeString(string(val)) } lb.Add(sx.Nil().Cons(obj).Cons(sx.MakeSymbol(key)).Cons(symType)) } return lb.List() } func mapGetS[T comparable](m map[T]*sx.Symbol, k T) *sx.Symbol { if result, found := m[k]; found { return result } return sx.MakeSymbol(fmt.Sprintf("**%v:NOT-FOUND**", k)) } func getBase64String(data []byte) string { var sb strings.Builder encoder := base64.NewEncoder(base64.StdEncoding, &sb) _, err := encoder.Write(data) if err == nil { err = encoder.Close() } if err == nil { return sb.String() } return "" } |
Changes to encoder/textenc/textenc.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- // Package textenc encodes the abstract syntax tree into its text. package textenc import ( "io" "t73f.de/r/zsc/api" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" | > > < | | | | | < | | | < < < | | > | | > | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | //----------------------------------------------------------------------------- // Package textenc encodes the abstract syntax tree into its text. package textenc import ( "io" "iter" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" ) func init() { encoder.Register(api.EncoderText, func(*encoder.CreateParameter) encoder.Encoder { return Create() }) } // Create an encoder. func Create() *Encoder { return &myTE } // Encoder contains all data needed for encoding. 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) (int, error) { v := newVisitor(w) te.WriteMeta(&v.b, zn.InhMeta) v.visitBlockSlice(&zn.BlocksAST) length, err := v.b.Flush() return length, err } // WriteMeta encodes metadata as text. func (te *Encoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { buf := encoder.NewEncWriter(w) for key, val := range m.Computed() { if meta.Type(key) == meta.TypeTagSet { writeTagSet(&buf, val.Elems()) } else { buf.WriteString(string(val)) } buf.WriteByte('\n') } length, err := buf.Flush() return length, err } func writeTagSet(buf *encoder.EncWriter, tags iter.Seq[meta.Value]) { first := true for tag := range tags { if !first { buf.WriteByte(' ') } first = false buf.WriteString(string(tag.CleanTag())) } } // WriteContent encodes the zettel content. func (te *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return te.WriteBlocks(w, &zn.BlocksAST) } // 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() |
︙ | ︙ | |||
128 129 130 131 132 133 134 | return nil case *ast.DescriptionListNode: v.visitDescriptionList(n) return nil case *ast.TableNode: v.visitTable(n) return nil | | > > | 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | return nil case *ast.DescriptionListNode: v.visitDescriptionList(n) return nil case *ast.TableNode: v.visitTable(n) return nil case *ast.TranscludeNode: ast.Walk(v, &n.Inlines) case *ast.BLOBNode: return nil case *ast.TextNode: v.visitText(n.Text) return nil case *ast.BreakNode: if n.Hard { v.b.WriteByte('\n') |
︙ | ︙ |
Changes to encoder/zmkenc/zmkenc.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 | package zmkenc import ( "fmt" "io" "strings" "t73f.de/r/zsc/api" "t73f.de/r/zsc/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/textenc" | > > < < | | | | | | | < | < < < < < < < | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | package zmkenc import ( "fmt" "io" "strings" "t73f.de/r/zero/set" "t73f.de/r/zsc/api" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/textenc" ) func init() { encoder.Register(api.EncoderZmk, func(*encoder.CreateParameter) encoder.Encoder { return Create() }) } // Create an encoder. func Create() *Encoder { return &myZE } // Encoder contains all data needed for encoding. type Encoder struct{} var myZE Encoder // WriteZettel writes the encoded zettel to the writer. func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode) (int, error) { v := newVisitor(w) v.acceptMeta(zn.InhMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { v.b.WriteByte('\n') } ast.Walk(v, &zn.BlocksAST) length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as zmk. func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta) (int, error) { v := newVisitor(w) v.acceptMeta(m) length, err := v.b.Flush() return length, err } func (v *visitor) acceptMeta(m *meta.Meta) { for key, val := range m.Computed() { v.b.WriteStrings(key, ": ", string(val), "\n") } } // WriteContent encodes the zettel content. func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return ze.WriteBlocks(w, &zn.BlocksAST) } // 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) ast.Walk(v, bs) length, err := v.b.Flush() |
︙ | ︙ | |||
132 133 134 135 136 137 138 | case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.TranscludeNode: | | | 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | case *ast.NestedListNode: v.visitNestedList(n) case *ast.DescriptionListNode: v.visitDescriptionList(n) case *ast.TableNode: v.visitTable(n) case *ast.TranscludeNode: v.b.WriteStrings("{{{", n.Ref.String(), "}}}") // FIXME n.Inlines v.visitAttributes(n.Attrs) case *ast.BLOBNode: v.visitBLOB(n) case *ast.TextNode: v.visitText(n) case *ast.BreakNode: v.visitBreak(n) |
︙ | ︙ | |||
185 186 187 188 189 190 191 | } } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: "@@@", ast.VerbatimComment: "%%%", ast.VerbatimHTML: "@@@", // Attribute is set to {="html"} | | | 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | } } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: "@@@", ast.VerbatimComment: "%%%", ast.VerbatimHTML: "@@@", // Attribute is set to {="html"} ast.VerbatimCode: "```", ast.VerbatimEval: "~~~", ast.VerbatimMath: "$$$", } func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) { kind, ok := mapVerbatimKind[vn.Kind] if !ok { |
︙ | ︙ | |||
343 344 345 346 347 348 349 | v.b.WriteString(alignCode[cell.Align]) } ast.Walk(v, &cell.Inlines) } } func (v *visitor) visitBLOB(bn *ast.BLOBNode) { | | | | | 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 | v.b.WriteString(alignCode[cell.Align]) } ast.Walk(v, &cell.Inlines) } } func (v *visitor) visitBLOB(bn *ast.BLOBNode) { if bn.Syntax == meta.ValueSyntaxSVG { v.b.WriteStrings("@@@", bn.Syntax, "\n") v.b.Write(bn.Blob) v.b.WriteString("\n@@@\n") return } var sb strings.Builder v.textEnc.WriteInlines(&sb, &bn.Description) v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.") } var escapeSeqs = set.New( "\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", "##", ) func (v *visitor) visitText(tn *ast.TextNode) { last := 0 for i := 0; i < len(tn.Text); i++ { if b := tn.Text[i]; b == '\\' { v.b.WriteString(tn.Text[last:i]) v.b.WriteBytes('\\', b) last = i + 1 continue } if i < len(tn.Text)-1 { s := tn.Text[i : i+2] if escapeSeqs.Contains(s) { v.b.WriteString(tn.Text[last:i]) for j := range len(s) { v.b.WriteBytes('\\', s[j]) } i++ last = i + 1 continue |
︙ | ︙ | |||
414 415 416 417 418 419 420 | ast.Walk(v, &en.Inlines) v.b.WriteByte('|') } v.b.WriteStrings(en.Ref.String(), "}}") } func (v *visitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) { | | | 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 | ast.Walk(v, &en.Inlines) v.b.WriteByte('|') } v.b.WriteStrings(en.Ref.String(), "}}") } func (v *visitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) { if en.Syntax == meta.ValueSyntaxSVG { v.b.WriteString("@@") v.b.Write(en.Blob) v.b.WriteStrings("@@{=", en.Syntax, "}") return } v.b.WriteString("{{TODO: display inline BLOB}}") } |
︙ | ︙ | |||
468 469 470 471 472 473 474 | ast.Walk(v, &fn.Inlines) v.b.Write(kind) v.visitAttributes(fn.Attrs) } func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { | | < < < < < < < | 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 | ast.Walk(v, &fn.Inlines) v.b.Write(kind) v.visitAttributes(fn.Attrs) } func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { case ast.LiteralCode: v.writeLiteral('`', ln.Attrs, ln.Content) case ast.LiteralMath: v.b.WriteStrings("$$", string(ln.Content), "$$") v.visitAttributes(ln.Attrs) case ast.LiteralInput: v.writeLiteral('\'', ln.Attrs, ln.Content) case ast.LiteralOutput: v.writeLiteral('=', ln.Attrs, ln.Content) case ast.LiteralComment: v.b.WriteString("%%") v.visitAttributes(ln.Attrs) v.b.WriteByte(' ') v.b.Write(ln.Content) default: panic(fmt.Sprintf("Unknown literal kind %v", ln.Kind)) } } func (v *visitor) writeLiteral(code byte, a attrs.Attributes, content []byte) { v.b.WriteBytes(code, code) |
︙ | ︙ | |||
537 538 539 540 541 542 543 | last = i + 1 } } v.b.WriteString(s[last:]) } func syntaxToHTML(a attrs.Attributes) attrs.Attributes { | | | 522 523 524 525 526 527 528 529 530 | last = i + 1 } } v.b.WriteString(s[last:]) } func syntaxToHTML(a attrs.Attributes) attrs.Attributes { return a.Clone().Set("", meta.ValueSyntaxHTML).Remove(meta.KeySyntax) } |
Deleted encoding/atom/atom.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoding/encoding.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoding/rss/rss.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted encoding/xml/xml.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to evaluator/evaluator.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 | import ( "bytes" "context" "errors" "fmt" "path" "strconv" "strings" "t73f.de/r/sx/sxbuiltins" "t73f.de/r/sx/sxreader" | > | | | < < | | | | | | | | < < < < < < < < < < < < | > | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | import ( "bytes" "context" "errors" "fmt" "path" "slices" "strconv" "strings" "t73f.de/r/sx/sxbuiltins" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/parser" "zettelstore.de/z/parser/cleaner" "zettelstore.de/z/parser/draw" "zettelstore.de/z/query" "zettelstore.de/z/zettel" ) // Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel. type Port interface { GetZettel(context.Context, id.Zid) (zettel.Zettel, error) QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // EvaluateZettel evaluates the given zettel in the given context, with the // given ports, and the given environment. func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) { switch zn.Syntax { case meta.ValueSyntaxNone: // AST is empty, evaluate to a description list of metadata. zn.BlocksAST = evaluateMetadata(zn.Meta) case meta.ValueSyntaxSxn: zn.BlocksAST = evaluateSxn(zn.BlocksAST) default: EvaluateBlock(ctx, port, rtConfig, &zn.BlocksAST) } } func evaluateSxn(bs ast.BlockSlice) ast.BlockSlice { // Check for structure made in parser/plain/plain.go:parseSxnBlocks if len(bs) == 1 { // If len(bs) > 1 --> an error was found during parsing if vn, isVerbatim := bs[0].(*ast.VerbatimNode); isVerbatim && vn.Kind == ast.VerbatimCode { if classAttr, hasClass := vn.Attrs.Get(""); hasClass && classAttr == meta.ValueSyntaxSxn { rd := sxreader.MakeReader(bytes.NewReader(vn.Content)) if objs, err := rd.ReadAll(); err == nil { result := make(ast.BlockSlice, len(objs)) for i, obj := range objs { var buf bytes.Buffer sxbuiltins.Print(&buf, obj) result[i] = &ast.VerbatimNode{ Kind: ast.VerbatimCode, Attrs: attrs.Attributes{"": classAttr}, Content: buf.Bytes(), } } return result } } } } return bs } // EvaluateBlock evaluates the given block list in the given context, with // the given ports, and the given environment. func EvaluateBlock(ctx context.Context, port Port, rtConfig config.Config, bns *ast.BlockSlice) { e := evaluator{ ctx: ctx, port: port, rtConfig: rtConfig, transcludeMax: rtConfig.GetMaxTransclusions(), transcludeCount: 0, costMap: map[id.Zid]transcludeCost{}, embedMap: map[string]ast.InlineSlice{}, marker: &ast.ZettelNode{}, } ast.Walk(&e, bns) cleaner.CleanBlockSlice(bns, true) } type evaluator struct { ctx context.Context port Port rtConfig config.Config transcludeMax int |
︙ | ︙ | |||
187 188 189 190 191 192 193 | } func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode { switch vn.Kind { case ast.VerbatimZettel: return e.evalVerbatimZettel(vn) case ast.VerbatimEval: | | | | | | | | | 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 | } func (e *evaluator) evalVerbatimNode(vn *ast.VerbatimNode) ast.BlockNode { switch vn.Kind { case ast.VerbatimZettel: return e.evalVerbatimZettel(vn) case ast.VerbatimEval: if syntax, found := vn.Attrs.Get(""); found && syntax == meta.ValueSyntaxDraw { return draw.ParseDrawBlock(vn) } } return vn } func (e *evaluator) evalVerbatimZettel(vn *ast.VerbatimNode) ast.BlockNode { m := meta.New(id.Invalid) m.Set(meta.KeySyntax, getSyntax(vn.Attrs, meta.ValueSyntaxText)) zettel := zettel.Zettel{ Meta: m, Content: zettel.NewContent(vn.Content), } e.transcludeCount++ zn := e.evaluateEmbeddedZettel(zettel) return &zn.BlocksAST } func getSyntax(a attrs.Attributes, defSyntax meta.Value) meta.Value { if a != nil { if val, ok := a.Get(meta.KeySyntax); ok { return meta.Value(val) } if val, ok := a.Get(""); ok { return meta.Value(val) } } return defSyntax } func (e *evaluator) evalTransclusionNode(tn *ast.TranscludeNode) ast.BlockNode { ref := tn.Ref |
︙ | ︙ | |||
280 281 282 283 284 285 286 | e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec} e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first. } e.transcludeCount++ if ec := cost.ec; ec > 0 { e.transcludeCount += cost.ec } | | | | 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 | e.costMap[zid] = transcludeCost{zn: zn, ec: e.transcludeCount - ec} e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first. } e.transcludeCount++ if ec := cost.ec; ec > 0 { e.transcludeCount += cost.ec } return &zn.BlocksAST } func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode { q := query.Parse(expr) ml, err := e.port.QueryMeta(e.ctx, q) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return nil } return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel")) } result, _ := QueryAction(e.ctx, q, ml) if result != nil { ast.Walk(e, result) } return result } func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode { |
︙ | ︙ | |||
314 315 316 317 318 319 320 | } func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) } func setMetadataFromAttributes(m *meta.Meta, a attrs.Attributes) { for aKey, aVal := range a { if meta.KeyIsValid(aKey) { | | < < | 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 | } func makeBlockNode(in ast.InlineNode) ast.BlockNode { return ast.CreateParaNode(in) } func setMetadataFromAttributes(m *meta.Meta, a attrs.Attributes) { for aKey, aVal := range a { if meta.KeyIsValid(aKey) { m.Set(aKey, meta.Value(aVal)) } } } func (e *evaluator) visitInlineSlice(is *ast.InlineSlice) { for i := 0; i < len(*is); i++ { in := (*is)[i] ast.Walk(e, in) switch n := in.(type) { case *ast.LinkNode: (*is)[i] = e.evalLinkNode(n) case *ast.EmbedRefNode: i += embedNode(is, i, e.evalEmbedRefNode(n)) } } } func embedNode(is *ast.InlineSlice, i int, in ast.InlineNode) int { if ln, ok := in.(*ast.InlineSlice); ok { *is = replaceWithInlineNodes(*is, i, *ln) |
︙ | ︙ | |||
438 439 440 441 442 443 444 | if errors.Is(err, &box.ErrNotAllowed{}) { return nil } e.transcludeCount++ return createInlineErrorImage(en) } | | | 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 | if errors.Is(err, &box.ErrNotAllowed{}) { return nil } e.transcludeCount++ return createInlineErrorImage(en) } if syntax := string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)); parser.IsImageFormat(syntax) { e.updateImageRefNode(en, zettel.Meta, syntax) return en } else if !parser.IsASTParser(syntax) { // Not embeddable. e.transcludeCount++ return createInlineErrorText(ref, "Not", "embeddable (syntax="+syntax+")") } |
︙ | ︙ | |||
465 466 467 468 469 470 471 | e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first. } e.transcludeCount++ result, ok := e.embedMap[ref.Value] if !ok { // Search for text to be embedded. | | | 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 | e.transcludeCount = 0 // No stack needed, because embedding is done left-recursive, depth-first. } e.transcludeCount++ result, ok := e.embedMap[ref.Value] if !ok { // Search for text to be embedded. result = findInlineSlice(&zn.BlocksAST, ref.URL.Fragment) e.embedMap[ref.Value] = result } if len(result) == 0 { return &ast.LiteralNode{ Kind: ast.LiteralComment, Attrs: map[string]string{"-": ""}, Content: append([]byte("Nothing to transclude: "), ref.String()...), |
︙ | ︙ | |||
503 504 505 506 507 508 509 | if len(is) > 0 { en.Inlines = is } } } } | < < < < < < < < < < < < < < < < | | | 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 | if len(is) > 0 { en.Inlines = is } } } } func createInlineErrorImage(en *ast.EmbedRefNode) *ast.EmbedRefNode { errorZid := id.ZidEmoji en.Ref = ast.ParseReference(errorZid.String()) if len(en.Inlines) == 0 { en.Inlines = ast.InlineSlice{&ast.TextNode{Text: "Error placeholder"}} } return en } func createInlineErrorText(ref *ast.Reference, msgWords ...string) ast.InlineNode { text := strings.Join(msgWords, " ") if ref != nil { |
︙ | ︙ | |||
560 561 562 563 564 565 566 | } return &ast.EmbedRefNode{ Ref: ref, Syntax: ext, } } | < < < < < < | | | 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 | } return &ast.EmbedRefNode{ Ref: ref, Syntax: ext, } } func (e *evaluator) evaluateEmbeddedZettel(zettel zettel.Zettel) *ast.ZettelNode { zn := parser.ParseZettel(e.ctx, zettel, string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)), e.rtConfig) ast.Walk(e, &zn.BlocksAST) return zn } func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice { if fragment == "" { return firstInlinesToEmbed(*bs) } |
︙ | ︙ | |||
633 634 635 636 637 638 639 | } func (fs *fragmentSearcher) visitInlineSlice(is *ast.InlineSlice) { for i, in := range *is { if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment { ris := skipBreakeNodes((*is)[i+1:]) if len(mn.Inlines) > 0 { | | | 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 | } func (fs *fragmentSearcher) visitInlineSlice(is *ast.InlineSlice) { for i, in := range *is { if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment { ris := skipBreakeNodes((*is)[i+1:]) if len(mn.Inlines) > 0 { fs.result = slices.Clone(mn.Inlines) fs.result = append(fs.result, &ast.TextNode{Text: " "}) fs.result = append(fs.result, ris...) } else { fs.result = ris } return } |
︙ | ︙ |
Changes to evaluator/list.go.
︙ | ︙ | |||
19 20 21 22 23 24 25 | "math" "slices" "strconv" "strings" "t73f.de/r/zsc/api" "t73f.de/r/zsc/attrs" | | < | < < | < | < < < < < < < < | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | "math" "slices" "strconv" "strings" "t73f.de/r/zsc/api" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/ast" "zettelstore.de/z/parser" "zettelstore.de/z/query" ) // QueryAction transforms a list of metadata according to query actions into a AST nested list. func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta) (ast.BlockNode, int) { ap := actionPara{ ctx: ctx, q: q, ml: ml, kind: ast.NestedListUnordered, minVal: -1, maxVal: -1, } actions := q.Actions() if len(actions) == 0 { return ap.createBlockNodeMeta("") } acts := make([]string, 0, len(actions)) for _, act := range actions { if strings.HasPrefix(act, api.NumberedAction[0:1]) { ap.kind = ast.NestedListOrdered continue } if strings.HasPrefix(act, api.MinAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.minVal = num continue } } if strings.HasPrefix(act, api.MaxAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { ap.maxVal = num continue } } if act == api.ReIndexAction { continue } acts = append(acts, act) } var firstUnknowAct string for _, act := range acts { switch act { case api.KeysAction: return ap.createBlockNodeMetaKeys() } key := strings.ToLower(act) switch meta.Type(key) { case meta.TypeWord: return ap.createBlockNodeWord(key) |
︙ | ︙ | |||
106 107 108 109 110 111 112 | type actionPara struct { ctx context.Context q *query.Query ml []*meta.Meta kind ast.NestedListKind minVal int maxVal int | < | | | 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 | type actionPara struct { ctx context.Context q *query.Query ml []*meta.Meta kind ast.NestedListKind minVal int maxVal int } func (ap *actionPara) createBlockNodeWord(key string) (ast.BlockNode, int) { var buf bytes.Buffer ccs, bufLen := ap.prepareCatAction(key, &buf) if len(ccs) == 0 { return nil, 0 } items := make([]ast.ItemSlice, 0, len(ccs)) ccs.SortByName() for _, cat := range ccs { buf.WriteString(string(cat.Name)) items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(buf.String()), Inlines: ast.InlineSlice{&ast.TextNode{Text: string(cat.Name)}}, })}) buf.Truncate(bufLen) } return &ast.NestedListNode{ Kind: ap.kind, Items: items, Attrs: nil, |
︙ | ︙ | |||
149 150 151 152 153 154 155 | para := make(ast.InlineSlice, 0, len(ccs)) ccs.SortByName() for i, cat := range ccs { if i > 0 { para = append(para, &ast.TextNode{Text: " "}) } | | | | 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | para := make(ast.InlineSlice, 0, len(ccs)) ccs.SortByName() for i, cat := range ccs { if i > 0 { para = append(para, &ast.TextNode{Text: " "}) } buf.WriteString(string(cat.Name)) para = append(para, &ast.LinkNode{ Attrs: countMap[cat.Count], Ref: ast.ParseReference(buf.String()), Inlines: ast.InlineSlice{ &ast.TextNode{Text: string(cat.Name)}, }, }, &ast.FormatNode{ Kind: ast.FormatSuper, Attrs: nil, Inlines: ast.InlineSlice{&ast.TextNode{Text: strconv.Itoa(cat.Count)}}, }, |
︙ | ︙ | |||
207 208 209 210 211 212 213 | ccs := arr.Counted() ccs.SortByName() var buf bytes.Buffer bufLen := ap.prepareSimpleQuery(&buf) items := make([]ast.ItemSlice, 0, len(ccs)) for _, cat := range ccs { | | | | | 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 | ccs := arr.Counted() ccs.SortByName() var buf bytes.Buffer bufLen := ap.prepareSimpleQuery(&buf) items := make([]ast.ItemSlice, 0, len(ccs)) for _, cat := range ccs { buf.WriteString(string(cat.Name)) buf.WriteString(api.ExistOperator) q1 := buf.String() buf.Truncate(bufLen) buf.WriteString(api.ActionSeparator) buf.WriteString(string(cat.Name)) q2 := buf.String() buf.Truncate(bufLen) items = append(items, ast.ItemSlice{ast.CreateParaNode( &ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(q1), Inlines: ast.InlineSlice{&ast.TextNode{Text: string(cat.Name)}}, }, &ast.TextNode{Text: " "}, &ast.TextNode{Text: "(" + strconv.Itoa(cat.Count) + ", "}, &ast.LinkNode{ Attrs: nil, Ref: ast.ParseReference(q2), Inlines: ast.InlineSlice{&ast.TextNode{Text: "values"}}, |
︙ | ︙ | |||
346 347 348 349 350 351 352 | } } } return result } func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) } | < < < < < < < < < < < < < < < < < < < < < < < < < < | 333 334 335 336 337 338 339 | } } } return result } func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) } |
Changes to evaluator/metadata.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package evaluator import ( | | | | | | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package evaluator import ( "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/ast" ) func evaluateMetadata(m *meta.Meta) ast.BlockSlice { descrlist := &ast.DescriptionListNode{} for key, val := range m.All() { descrlist.Descriptions = append( descrlist.Descriptions, getMetadataDescription(key, val)) } return ast.BlockSlice{descrlist} } func getMetadataDescription(key string, value meta.Value) ast.Description { is := convertMetavalueToInlineSlice(value, meta.Type(key)) return ast.Description{ Term: ast.InlineSlice{&ast.TextNode{Text: key}}, Descriptions: []ast.DescriptionSlice{{&ast.ParaNode{Inlines: is}}}, } } func convertMetavalueToInlineSlice(value meta.Value, dt *meta.DescriptionType) ast.InlineSlice { var sliceData []string if dt.IsSet { sliceData = value.AsSlice() if len(sliceData) == 0 { return nil } } else { sliceData = []string{string(value)} } makeLink := dt == meta.TypeID || dt == meta.TypeIDSet result := make(ast.InlineSlice, 0, 2*len(sliceData)-1) for i, val := range sliceData { if i > 0 { result = append(result, &ast.TextNode{Text: " "}) |
︙ | ︙ |
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 17 18 19 20 | module zettelstore.de/z go 1.24 require ( github.com/fsnotify/fsnotify v1.8.0 github.com/yuin/goldmark v1.7.8 golang.org/x/crypto v0.36.0 golang.org/x/term v0.30.0 golang.org/x/text v0.23.0 t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3 t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b t73f.de/r/zero v0.0.0-20250226205915-c4194684acb7 t73f.de/r/zsc v0.0.0-20250307150109-16b4168715ab ) require ( golang.org/x/sys v0.31.0 // indirect t73f.de/r/webs v0.0.0-20250226210341-4a531b8bfb18 // indirect ) |
Changes to go.sum.
1 2 3 4 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= | | | | | | | | | | | | | | | > > | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3 h1:Jek4x1Qp59SWXI1enWVTeP1wxcVO96FuBpJBnnwOY98= t73f.de/r/sx v0.0.0-20250226205800-c12af029b6d3/go.mod h1:hzg05uSCMk3D/DWaL0pdlowfL2aWQeGIfD1S04vV+Xg= t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b h1:X+9mMDd3fKML5SPcQk4n28oDGFUwqjDiSmQrH2LHZwI= t73f.de/r/sxwebs v0.0.0-20250226210617-7bc3145c269b/go.mod h1:p+3JCSzNm9e+Yyub0ODRiLDeKaGVYWvBKYANZaAWYIA= t73f.de/r/webs v0.0.0-20250226210341-4a531b8bfb18 h1:p7rOFBzP6FE/aYN5MUfmGDrKP1H1IFs6v19T7hm7rXI= t73f.de/r/webs v0.0.0-20250226210341-4a531b8bfb18/go.mod h1:zk92hSKB4iWyT290+163seNzu350TA9XLATC9kOldqo= t73f.de/r/zero v0.0.0-20250226205915-c4194684acb7 h1:OuzHSfniY8UzLmo5zp1w23Kd9h7x9CSXP2jQ+kppeqU= t73f.de/r/zero v0.0.0-20250226205915-c4194684acb7/go.mod h1:T1vFcHoymUQcr7+vENBkS1yryZRZ3YB8uRtnMy8yRBA= t73f.de/r/zsc v0.0.0-20250307150109-16b4168715ab h1:km/Q2dFXd/4snkt5wLXYh1VoAGThi+cRrLghuz1FqVM= t73f.de/r/zsc v0.0.0-20250307150109-16b4168715ab/go.mod h1:vyDTRFUHrj3T0Gm3AGTI1n7s4HrpNWwISnFE5mlZ0eI= |
Changes to kernel/impl/auth.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 | package impl import ( "errors" "sync" "zettelstore.de/z/auth" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" | > < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package impl import ( "errors" "sync" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/auth" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) type authService struct { srvConfig mxService sync.RWMutex manager auth.Manager createManager kernel.CreateAuthManagerFunc |
︙ | ︙ |
Changes to kernel/impl/cfg.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | "context" "errors" "fmt" "strconv" "strings" "sync" | | > < < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | "context" "errors" "fmt" "strconv" "strings" "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) type configService struct { srvConfig mxService sync.RWMutex orig *meta.Meta manager box.Manager |
︙ | ︙ | |||
57 58 59 60 61 62 63 | cs.logger = logger cs.descr = descriptionMap{ keyDefaultCopyright: {"Default copyright", parseString, true}, keyDefaultLicense: {"Default license", parseString, true}, keyDefaultVisibility: { "Default zettel visibility", func(val string) (any, error) { | | | 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | cs.logger = logger cs.descr = descriptionMap{ keyDefaultCopyright: {"Default copyright", parseString, true}, keyDefaultLicense: {"Default license", parseString, true}, keyDefaultVisibility: { "Default zettel visibility", func(val string) (any, error) { vis := meta.Value(val).AsVisibility() if vis == meta.VisibilityUnknown { return nil, errUnknownVisibility } return vis, nil }, true, }, |
︙ | ︙ | |||
83 84 85 86 87 88 89 | case kernel.ConfigZmkHTML: return config.ZettelmarkupHTML, nil } return config.NoHTML, nil }), true, }, | | | > | | | > | | | | | | | | | | | | | | > | | | > | | | | 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 | case kernel.ConfigZmkHTML: return config.ZettelmarkupHTML, nil } return config.NoHTML, nil }), true, }, meta.KeyLang: {"Language", parseString, true}, keyMaxTransclusions: {"Maximum transclusions", parseInt64, true}, keySiteName: {"Site name", parseString, true}, keyYAMLHeader: {"YAML header", parseBool, true}, keyZettelFileSyntax: { "Zettel file syntax", func(val string) (any, error) { return strings.Fields(val), nil }, true, }, kernel.ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true}, config.KeyListsMenuZettel: {"Lists menu", parseZid, true}, config.KeyShowBackLinks: {"Show back links", parseString, true}, config.KeyShowFolgeLinks: {"Show folge links", parseString, true}, config.KeyShowSequelLinks: {"Show sequel links", parseString, true}, config.KeyShowSubordinateLinks: {"Show subordinate links", parseString, true}, config.KeyShowSuccessorLinks: {"Show successor links", parseString, true}, } cs.next = interfaceMap{ keyDefaultCopyright: "", keyDefaultLicense: "", keyDefaultVisibility: meta.VisibilityLogin, keyExpertMode: false, config.KeyFooterZettel: id.Invalid, config.KeyHomeZettel: id.ZidDefaultHome, kernel.ConfigInsecureHTML: config.NoHTML, meta.KeyLang: meta.ValueLangEN, keyMaxTransclusions: int64(1024), keySiteName: "Zettelstore", keyYAMLHeader: false, keyZettelFileSyntax: nil, kernel.ConfigSimpleMode: false, config.KeyListsMenuZettel: id.ZidTOCListsMenu, config.KeyShowBackLinks: "", config.KeyShowFolgeLinks: "", config.KeyShowSequelLinks: "", config.KeyShowSubordinateLinks: "", config.KeyShowSuccessorLinks: "", } } func (cs *configService) GetLogger() *logger.Logger { return cs.logger } func (cs *configService) Start(*myKernel) error { cs.logger.Info().Msg("Start Service") data := meta.New(id.ZidConfiguration) for _, kv := range cs.GetNextConfigList() { data.Set(kv.Key, meta.Value(kv.Value)) } cs.mxService.Lock() cs.orig = data cs.mxService.Unlock() return nil } |
︙ | ︙ | |||
155 156 157 158 159 160 161 | } func (cs *configService) setBox(mgr box.Manager) { cs.mxService.Lock() cs.manager = mgr cs.mxService.Unlock() mgr.RegisterObserver(cs.observe) | | | | < | | | | | | | | 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 | } func (cs *configService) setBox(mgr box.Manager) { cs.mxService.Lock() cs.manager = mgr cs.mxService.Unlock() mgr.RegisterObserver(cs.observe) cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ZidConfiguration}) } func (cs *configService) doUpdate(p box.BaseBox) error { z, err := p.GetZettel(context.Background(), id.ZidConfiguration) cs.logger.Trace().Err(err).Msg("got config meta") if err != nil { return err } m := z.Meta cs.mxService.Lock() for key := range cs.orig.All() { if val, ok := m.Get(key); ok { cs.SetConfig(key, string(val)) } else if defVal, defFound := cs.orig.Get(key); defFound { cs.SetConfig(key, string(defVal)) } } cs.mxService.Unlock() cs.SwitchNextToCur() // Poor man's restart return nil } func (cs *configService) observe(ci box.UpdateInfo) { if (ci.Reason != box.OnZettel && ci.Reason != box.OnDelete) || ci.Zid == id.ZidConfiguration { cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe") go func() { cs.mxService.RLock() mgr := cs.manager cs.mxService.RUnlock() if mgr != nil { cs.doUpdate(mgr) } else { cs.doUpdate(ci.Box) } }() } } // --- config.Config func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string { if m != nil { if val, found := m.Get(key); found { return string(val) } } if user := server.GetUser(ctx); user != nil { if val, found := user.Get(key); found { return string(val) } } result := cs.GetCurConfig(key) if result == nil { return "" } switch val := result.(type) { case string: return val case bool: if val { return meta.ValueTrue } return meta.ValueFalse case id.Zid: return val.String() case int: return strconv.Itoa(val) case []string: return strings.Join(val, " ") case meta.Visibility: |
︙ | ︙ | |||
243 244 245 246 247 248 249 | // AddDefaultValues enriches the given meta data with its default values. func (cs *configService) AddDefaultValues(ctx context.Context, m *meta.Meta) *meta.Meta { if cs == nil { return m } result := m cs.mxService.RLock() | | | | | | | | | | | | | > > > > > | | | | 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 | // AddDefaultValues enriches the given meta data with its default values. func (cs *configService) AddDefaultValues(ctx context.Context, m *meta.Meta) *meta.Meta { if cs == nil { return m } result := m cs.mxService.RLock() if _, found := m.Get(meta.KeyCopyright); !found { result = updateMeta(result, m, meta.KeyCopyright, cs.GetCurConfig(keyDefaultCopyright).(string)) } if _, found := m.Get(meta.KeyLang); !found { result = updateMeta(result, m, meta.KeyLang, cs.Get(ctx, nil, meta.KeyLang)) } if _, found := m.Get(meta.KeyLicense); !found { result = updateMeta(result, m, meta.KeyLicense, cs.GetCurConfig(keyDefaultLicense).(string)) } if _, found := m.Get(meta.KeyVisibility); !found { result = updateMeta(result, m, meta.KeyVisibility, cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility).String()) } cs.mxService.RUnlock() return result } func updateMeta(result, m *meta.Meta, key string, val string) *meta.Meta { if result == m { result = m.Clone() } result.Set(key, meta.Value(val)) return result } func (cs *configService) GetHTMLInsecurity() config.HTMLInsecurity { return cs.GetCurConfig(kernel.ConfigInsecureHTML).(config.HTMLInsecurity) } // GetSiteName returns the current value of the "site-name" key. func (cs *configService) GetSiteName() string { return cs.GetCurConfig(keySiteName).(string) } // GetMaxTransclusions return the maximum number of indirect transclusions. func (cs *configService) GetMaxTransclusions() int { return int(cs.GetCurConfig(keyMaxTransclusions).(int64)) } // GetYAMLHeader returns the current value of the "yaml-header" key. func (cs *configService) GetYAMLHeader() bool { return cs.GetCurConfig(keyYAMLHeader).(bool) } // GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key. func (cs *configService) GetZettelFileSyntax() []meta.Value { if zfs := cs.GetCurConfig(keyZettelFileSyntax); zfs != nil { zfsAS := zfs.([]string) result := make([]meta.Value, len(zfsAS)) for i, fs := range zfsAS { result[i] = meta.Value(fs) } return result } return nil } // --- config.AuthConfig // GetSimpleMode returns true if system tuns in simple-mode. func (cs *configService) GetSimpleMode() bool { return cs.GetCurConfig(kernel.ConfigSimpleMode).(bool) } // GetExpertMode returns the current value of the "expert-mode" key. func (cs *configService) GetExpertMode() bool { return cs.GetCurConfig(keyExpertMode).(bool) } // GetVisibility returns the visibility value, or "login" if none is given. func (cs *configService) GetVisibility(m *meta.Meta) meta.Visibility { if val, ok := m.Get(meta.KeyVisibility); ok { if vis := val.AsVisibility(); vis != meta.VisibilityUnknown { return vis } } vis := cs.GetCurConfig(keyDefaultVisibility).(meta.Visibility) if vis != meta.VisibilityUnknown { return vis } cs.mxService.RLock() val, _ := cs.orig.Get(keyDefaultVisibility) vis = val.AsVisibility() cs.mxService.RUnlock() return vis } |
Changes to kernel/impl/cmd.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 | //----------------------------------------------------------------------------- package impl import ( "fmt" "io" "os" "runtime/metrics" "slices" "strconv" "strings" | > < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | //----------------------------------------------------------------------------- package impl import ( "fmt" "io" "maps" "os" "runtime/metrics" "slices" "strconv" "strings" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" ) type cmdSession struct { w io.Writer |
︙ | ︙ | |||
99 100 101 102 103 104 105 | if colno >= len(maxLen) { maxLen = append(maxLen, 0) } colLen := strfun.Length(column) if colLen <= maxLen[colno] { continue } | < | < < < | 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | if colno >= len(maxLen) { maxLen = append(maxLen, 0) } colLen := strfun.Length(column) if colLen <= maxLen[colno] { continue } maxLen[colno] = min(colLen, sess.colwidth) } } return maxLen } func (sess *cmdSession) printRow(row []string, maxLen []int, prefix, delim string, pad rune) { for colno, column := range row { |
︙ | ︙ | |||
198 199 200 201 202 203 204 | }, "start": {"start service", cmdStart}, "stat": {"show service statistics", cmdStat}, "stop": {"stop service", cmdStop}, } func cmdHelp(sess *cmdSession, _ string, _ []string) bool { | < | | 194 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 { table := [][]string{{"Command", "Description"}} for _, cmd := range slices.Sorted(maps.Keys(commands)) { table = append(table, []string{cmd, commands[cmd].Text}) } sess.printTable(table) return true } func cmdConfig(sess *cmdSession, cmd string, args []string) bool { |
︙ | ︙ | |||
222 223 224 225 226 227 228 | table = append(table, []string{kd.Key, kd.Descr}) } sess.printTable(table) return true } func cmdGetConfig(sess *cmdSession, _ string, args []string) bool { showConfig(sess, args, | | | | | 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | table = append(table, []string{kd.Key, kd.Descr}) } sess.printTable(table) return true } func cmdGetConfig(sess *cmdSession, _ string, args []string) bool { showConfig(sess, args, listCurConfig, func(srv service, key string) any { return srv.GetCurConfig(key) }) return true } func cmdNextConfig(sess *cmdSession, _ string, args []string) bool { showConfig(sess, args, listNextConfig, func(srv service, key string) any { return srv.GetNextConfig(key) }) return true } func showConfig(sess *cmdSession, args []string, listConfig func(*cmdSession, service), getConfig func(service, string) any) { if len(args) == 0 { keys := make([]int, 0, len(sess.kern.srvs)) for k := range sess.kern.srvs { keys = append(keys, int(k)) } slices.Sort(keys) |
︙ | ︙ | |||
552 553 554 555 556 557 558 | table = append(table, []string{env[:pos], env[pos+1:]}) } } sess.printTable(table) return true } | | > > | 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 | table = append(table, []string{env[:pos], env[pos+1:]}) } } sess.printTable(table) return true } func sortedServiceNames(sess *cmdSession) []string { return slices.Sorted(maps.Keys(sess.kern.srvNames)) } func getService(sess *cmdSession, name string) (serviceData, bool) { srvD, found := sess.kern.srvNames[name] if !found { sess.println("Unknown service", name) } return srvD, found } |
Changes to kernel/impl/config.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- package impl import ( "errors" "fmt" "slices" "strconv" "strings" "sync" | > | < | | < < < < < < < < < | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | //----------------------------------------------------------------------------- package impl import ( "errors" "fmt" "maps" "slices" "strconv" "strings" "sync" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) type parseFunc func(string) (any, error) type configDescription struct { text string parse parseFunc canList bool } type descriptionMap map[string]configDescription type interfaceMap map[string]any func (m interfaceMap) Clone() interfaceMap { return maps.Clone(m) } type srvConfig struct { logger *logger.Logger mxConfig sync.RWMutex frozen bool descr descriptionMap cur interfaceMap next interfaceMap } func (cfg *srvConfig) ConfigDescriptions() []serviceConfigDescription { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() keys := slices.Sorted(maps.Keys(cfg.descr)) result := make([]serviceConfigDescription, 0, len(keys)) for _, k := range keys { text := cfg.descr[k].text if strings.HasSuffix(k, "-") { text = text + " (list)" } result = append(result, serviceConfigDescription{Key: k, Descr: text}) |
︙ | ︙ | |||
134 135 136 137 138 139 140 | continue } return d, k, num } return configDescription{}, "", -1 } | | | | | 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 | continue } return d, k, num } return configDescription{}, "", -1 } func (cfg *srvConfig) GetCurConfig(key string) any { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() if cfg.cur == nil { return cfg.next[key] } return cfg.cur[key] } func (cfg *srvConfig) GetNextConfig(key string) any { cfg.mxConfig.RLock() defer cfg.mxConfig.RUnlock() return cfg.next[key] } func (cfg *srvConfig) GetCurConfigList(all bool) []kernel.KeyDescrValue { return cfg.getOneConfigList(all, cfg.GetCurConfig) } func (cfg *srvConfig) GetNextConfigList() []kernel.KeyDescrValue { return cfg.getOneConfigList(true, cfg.GetNextConfig) } func (cfg *srvConfig) getOneConfigList(all bool, getConfig func(string) any) []kernel.KeyDescrValue { if len(cfg.descr) == 0 { return nil } keys := cfg.getSortedConfigKeys(all, getConfig) result := make([]kernel.KeyDescrValue, 0, len(keys)) for _, k := range keys { val := getConfig(k) |
︙ | ︙ | |||
179 180 181 182 183 184 185 | Descr: descr.text, Value: fmt.Sprintf("%v", val), }) } return result } | | | 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | Descr: descr.text, Value: fmt.Sprintf("%v", val), }) } return result } func (cfg *srvConfig) getSortedConfigKeys(all bool, getConfig func(string) any) []string { keys := make([]string, 0, len(cfg.descr)) for k, descr := range cfg.descr { if all || descr.canList { if !strings.HasSuffix(k, "-") { keys = append(keys, k) continue } |
︙ | ︙ |
Changes to kernel/impl/core.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( "fmt" "net" "os" "runtime" "sync" "time" | > > | < | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( "fmt" "maps" "net" "os" "runtime" "slices" "sync" "time" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" ) type coreService struct { srvConfig started bool mxRecover sync.RWMutex mapRecover map[string]recoverInfo } type recoverInfo struct { count uint64 ts time.Time info any stack []byte } func (cs *coreService) Initialize(logger *logger.Logger) { cs.logger = logger cs.mapRecover = make(map[string]recoverInfo) cs.descr = descriptionMap{ |
︙ | ︙ | |||
102 103 104 105 106 107 108 | func (cs *coreService) Stop(*myKernel) { cs.started = false } func (cs *coreService) GetStatistics() []kernel.KeyValue { cs.mxRecover.RLock() defer cs.mxRecover.RUnlock() | | | 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | func (cs *coreService) Stop(*myKernel) { cs.started = false } func (cs *coreService) GetStatistics() []kernel.KeyValue { cs.mxRecover.RLock() defer cs.mxRecover.RUnlock() names := slices.Sorted(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), |
︙ | ︙ | |||
142 143 144 145 146 147 148 | fmt.Sprintf("Time: %v", ri.ts), fmt.Sprintf("Reason: %v", ri.info), }, strfun.SplitLines(string(ri.stack))..., ) } | | | 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | fmt.Sprintf("Time: %v", ri.ts), fmt.Sprintf("Reason: %v", ri.info), }, strfun.SplitLines(string(ri.stack))..., ) } func (cs *coreService) updateRecoverInfo(name string, recoverInfo any, stack []byte) { cs.mxRecover.Lock() ri := cs.mapRecover[name] ri.count++ ri.ts = time.Now().Local() ri.info = recoverInfo ri.stack = stack cs.mapRecover[name] = ri cs.mxRecover.Unlock() } |
Changes to kernel/impl/impl.go.
︙ | ︙ | |||
26 27 28 29 30 31 32 33 34 | "runtime/pprof" "strconv" "strings" "sync" "syscall" "time" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" | > < | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | "runtime/pprof" "strconv" "strings" "sync" "syscall" "time" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) // myKernel is the main internal kernel. type myKernel struct { logWriter *kernelLogWriter logger *logger.Logger wg sync.WaitGroup |
︙ | ︙ | |||
232 233 234 235 236 237 238 | } } } func (kern *myKernel) parseLogLevel(logLevel string) (logger.Level, map[kernel.Service]logger.Level) { defaultLevel := logger.NoLevel srvLevel := map[kernel.Service]logger.Level{} | | | 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | } } } func (kern *myKernel) parseLogLevel(logLevel string) (logger.Level, map[kernel.Service]logger.Level) { defaultLevel := logger.NoLevel srvLevel := map[kernel.Service]logger.Level{} for spec := range strings.SplitSeq(logLevel, ";") { vals := cleanLogSpec(strings.Split(spec, ":")) switch len(vals) { case 0: case 1: if lvl := logger.ParseLevel(vals[0]); lvl.IsValid() { defaultLevel = lvl } |
︙ | ︙ | |||
272 273 274 275 276 277 278 | } func (kern *myKernel) GetLastLogTime() time.Time { return kern.logWriter.getLastLogTime() } // LogRecover outputs some information about the previous panic. | | | | 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 | } func (kern *myKernel) GetLastLogTime() time.Time { return kern.logWriter.getLastLogTime() } // LogRecover outputs some information about the previous panic. func (kern *myKernel) LogRecover(name string, recoverInfo any) bool { return kern.doLogRecover(name, recoverInfo) } func (kern *myKernel) doLogRecover(name string, recoverInfo any) bool { stack := debug.Stack() kern.logger.Error().Str("recovered_from", fmt.Sprint(recoverInfo)).Bytes("stack", stack).Msg(name) kern.core.updateRecoverInfo(name, recoverInfo, stack) return true } // --- Profiling --------------------------------------------------------- |
︙ | ︙ | |||
361 362 363 364 365 366 367 | defer kern.mx.Unlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.SetConfig(key, value) } return errUnknownService } | | | 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 | defer kern.mx.Unlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.SetConfig(key, value) } return errUnknownService } func (kern *myKernel) GetConfig(srvnum kernel.Service, key string) any { kern.mx.RLock() defer kern.mx.RUnlock() if srvD, ok := kern.srvs[srvnum]; ok { return srvD.srv.GetCurConfig(key) } return nil } |
︙ | ︙ | |||
499 500 501 502 503 504 505 | // ConfigDescriptions returns a sorted list of configuration descriptions. ConfigDescriptions() []serviceConfigDescription // SetConfig stores a configuration value. SetConfig(key, value string) error // GetCurConfig returns the current configuration value. | | | | 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 | // ConfigDescriptions returns a sorted list of configuration descriptions. ConfigDescriptions() []serviceConfigDescription // SetConfig stores a configuration value. SetConfig(key, value string) error // GetCurConfig returns the current configuration value. GetCurConfig(key string) any // GetNextConfig returns the next configuration value. GetNextConfig(key string) any // GetCurConfigList returns a sorted list of current configuration data. GetCurConfigList(all bool) []kernel.KeyDescrValue // GetNextConfigList returns a sorted list of next configuration data. GetNextConfigList() []kernel.KeyDescrValue |
︙ | ︙ | |||
551 552 553 554 555 556 557 | kernel *myKernel } func (*kernelService) Initialize(*logger.Logger) {} func (ks *kernelService) GetLogger() *logger.Logger { return ks.kernel.logger } func (*kernelService) ConfigDescriptions() []serviceConfigDescription { return nil } func (*kernelService) SetConfig(string, string) error { return errAlreadyFrozen } | | | | 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 | kernel *myKernel } func (*kernelService) Initialize(*logger.Logger) {} func (ks *kernelService) GetLogger() *logger.Logger { return ks.kernel.logger } func (*kernelService) ConfigDescriptions() []serviceConfigDescription { return nil } func (*kernelService) SetConfig(string, string) error { return errAlreadyFrozen } func (*kernelService) GetCurConfig(string) any { return nil } func (*kernelService) GetNextConfig(string) any { return nil } func (*kernelService) GetCurConfigList(bool) []kernel.KeyDescrValue { return nil } func (*kernelService) GetNextConfigList() []kernel.KeyDescrValue { return nil } func (*kernelService) GetStatistics() []kernel.KeyValue { return nil } func (*kernelService) Freeze() {} func (*kernelService) Start(*myKernel) error { return nil } func (*kernelService) SwitchNextToCur() {} func (*kernelService) IsStarted() bool { return true } func (*kernelService) Stop(*myKernel) {} |
Changes to kernel/impl/log.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( "os" "sync" "time" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) | > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package impl import ( "os" "slices" "sync" "time" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" ) |
︙ | ︙ | |||
50 51 52 53 54 55 56 | if level > logger.DebugLevel { klw.lastLog = ts klw.data[klw.writePos] = logEntry{ level: level, ts: ts, prefix: prefix, msg: msg, | | | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | if level > logger.DebugLevel { klw.lastLog = ts klw.data[klw.writePos] = logEntry{ level: level, ts: ts, prefix: prefix, msg: msg, details: slices.Clone(details), } klw.writePos++ if klw.writePos >= cap(klw.data) { klw.writePos = 0 klw.full = true } } |
︙ | ︙ |
Changes to kernel/impl/web.go.
︙ | ︙ | |||
142 143 144 145 146 147 148 | func (ws *webService) Start(kern *myKernel) error { baseURL := ws.GetNextConfig(kernel.WebBaseURL).(string) listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string) urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string) persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool) secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool) profile := ws.GetNextConfig(kernel.WebProfiling).(bool) | | < < < | 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | func (ws *webService) Start(kern *myKernel) error { baseURL := ws.GetNextConfig(kernel.WebBaseURL).(string) listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string) urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string) persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool) secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool) profile := ws.GetNextConfig(kernel.WebProfiling).(bool) maxRequestSize := max(ws.GetNextConfig(kernel.WebMaxRequestSize).(int64), 1024) if !strings.HasSuffix(baseURL, urlPrefix) { ws.logger.Error().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg( "url-prefix is not a suffix of base-url") return errWrongBasePrefix } |
︙ | ︙ | |||
174 175 176 177 178 179 180 | } srvw := impl.New(sd) err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg) if err != nil { ws.logger.Error().Err(err).Msg("Unable to create") return err } | < < < | 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | } srvw := impl.New(sd) err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg) if err != nil { ws.logger.Error().Err(err).Msg("Unable to create") return err } if err = srvw.Run(); err != nil { ws.logger.Error().Err(err).Msg("Unable to start") return err } ws.logger.Info().Str("listen", listenAddr).Str("base-url", baseURL).Msg("Start Service") ws.mxService.Lock() ws.srvw = srvw |
︙ | ︙ |
Changes to kernel/kernel.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 25 26 | package kernel import ( "io" "net/url" "time" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" | > < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package kernel import ( "io" "net/url" "time" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) // Kernel is the main internal service. type Kernel interface { // Setup sets the most basic data of a software: its name, its version, // and when the version was created. Setup(progname, version string, versionTime time.Time) |
︙ | ︙ | |||
47 48 49 50 51 52 53 | // SetLogLevel sets the logging level for logger maintained by the kernel. // // Its syntax is: (SERVICE ":")? LEVEL (";" (SERICE ":")? LEVEL)*. SetLogLevel(string) // LogRecover outputs some information about the previous panic. | | | | 47 48 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 | // SetLogLevel sets the logging level for logger maintained by the kernel. // // Its syntax is: (SERVICE ":")? LEVEL (";" (SERICE ":")? LEVEL)*. SetLogLevel(string) // LogRecover outputs some information about the previous panic. LogRecover(name string, recoverInfo any) bool // StartProfiling starts profiling the software according to a profile. // It is an error to start more than one profile. // // profileName is a valid profile (see runtime/pprof/Lookup()), or the // value "cpu" for profiling the CPI. // fileName is the name of the file where the results are written to. StartProfiling(profileName, fileName string) error // StopProfiling stops the current profiling and writes the result to // the file, which was named during StartProfiling(). // It will always be called before the software stops its operations. StopProfiling() error // SetConfig stores a configuration value. SetConfig(srv Service, key, value string) error // GetConfig returns a configuration value. GetConfig(srv Service, key string) any // GetConfigList returns a sorted list of configuration data. GetConfigList(Service) []KeyDescrValue // GetLogger returns a logger for the given service. GetLogger(Service) *logger.Logger |
︙ | ︙ |
Changes to logger/logger.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 | import ( "context" "strconv" "strings" "sync/atomic" "time" | | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import ( "context" "strconv" "strings" "sync/atomic" "time" "t73f.de/r/zsc/domain/meta" ) // Level defines the possible log levels type Level uint8 // Constants for Level const ( |
︙ | ︙ |
Changes to logger/logger_test.go.
︙ | ︙ | |||
41 42 43 44 45 46 47 | t.Errorf("%d: ParseLevel(%q) == %q, but got %q", i, tc.text, tc.exp, got) } } } func BenchmarkDisabled(b *testing.B) { log := logger.New(&stderrLogWriter{}, "").SetLevel(logger.NeverLevel) | | | | | | 41 42 43 44 45 46 47 48 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 | t.Errorf("%d: ParseLevel(%q) == %q, but got %q", i, tc.text, tc.exp, got) } } } func BenchmarkDisabled(b *testing.B) { log := logger.New(&stderrLogWriter{}, "").SetLevel(logger.NeverLevel) for b.Loop() { log.Info().Str("key", "val").Msg("Benchmark") } } type stderrLogWriter struct{} func (*stderrLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error { fmt.Fprintf(os.Stderr, "%v %v %v %v %v\n", level.Format(), ts, prefix, msg, string(details)) return nil } type testLogWriter struct{} func (*testLogWriter) WriteMessage(logger.Level, time.Time, string, string, []byte) error { return nil } func BenchmarkStrMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "") for b.Loop() { log.Info().Str("key", "val").Msg("Benchmark") } } func BenchmarkMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "") for b.Loop() { log.Info().Msg("Benchmark") } } func BenchmarkCloneStrMessage(b *testing.B) { log := logger.New(&testLogWriter{}, "").Clone().Str("sss", "ttt").Child() for b.Loop() { log.Info().Msg("123456789") } } |
Changes to logger/message.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | import ( "context" "net/http" "strconv" "sync" | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import ( "context" "net/http" "strconv" "sync" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" ) // Message presents a message to log. type Message struct { logger *Logger level Level buf []byte |
︙ | ︙ | |||
44 45 46 47 48 49 50 | } func recycleMessage(m *Message) { messagePool.Put(m) } var messagePool = &sync.Pool{ | | | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | } func recycleMessage(m *Message) { messagePool.Put(m) } var messagePool = &sync.Pool{ New: func() any { return &Message{ buf: make([]byte, 0, 500), } }, } // Enabled returns whether the message will log or not. |
︙ | ︙ | |||
112 113 114 115 116 117 118 | // User adds the user-id field of the given user to the message. func (m *Message) User(ctx context.Context) *Message { if m.Enabled() { if up := m.logger.uProvider; up != nil { if user := up.GetUser(ctx); user != nil { m.buf = append(m.buf, ", user="...) | | | 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | // User adds the user-id field of the given user to the message. func (m *Message) User(ctx context.Context) *Message { if m.Enabled() { if up := m.logger.uProvider; up != nil { if user := up.GetUser(ctx); user != nil { m.buf = append(m.buf, ", user="...) if userID, found := user.Get(meta.KeyUserID); found { m.buf = append(m.buf, userID...) } else { m.buf = append(m.buf, user.Zid.Bytes()...) } } } } |
︙ | ︙ |
Changes to parser/blob/blob.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package blob provides a parser of binary data. package blob import ( "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "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 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package blob provides a parser of binary data. package blob import ( "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.ValueSyntaxGif, AltNames: nil, IsASTParser: false, IsTextFormat: false, IsImageFormat: true, Parse: parseBlocks, }) parser.Register(&parser.Info{ Name: meta.ValueSyntaxJPEG, AltNames: []string{meta.ValueSyntaxJPG}, IsASTParser: false, IsTextFormat: false, IsImageFormat: true, Parse: parseBlocks, }) parser.Register(&parser.Info{ Name: meta.ValueSyntaxPNG, AltNames: nil, IsASTParser: false, IsTextFormat: false, IsImageFormat: true, Parse: parseBlocks, }) parser.Register(&parser.Info{ Name: meta.ValueSyntaxWebp, AltNames: nil, IsASTParser: false, IsTextFormat: false, IsImageFormat: true, Parse: parseBlocks, }) } func parseBlocks(inp *input.Input, m *meta.Meta, syntax string) ast.BlockSlice { if p := parser.Get(syntax); p != nil { syntax = p.Name } return ast.BlockSlice{&ast.BLOBNode{ Description: parser.ParseDescription(m), Syntax: syntax, Blob: []byte(inp.Src), }} } |
Changes to parser/cleaner/cleaner.go.
︙ | ︙ | |||
20 21 22 23 24 25 26 | "zettelstore.de/z/ast" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/strfun" ) // CleanBlockSlice cleans the given block list. | | < < < < < | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | "zettelstore.de/z/ast" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/strfun" ) // CleanBlockSlice cleans the given block list. func CleanBlockSlice(bs *ast.BlockSlice, allowHTML bool) { cv := cleanVisitor{ textEnc: textenc.Create(), allowHTML: allowHTML, hasMark: false, doMark: false, } ast.Walk(&cv, bs) if cv.hasMark { cv.doMark = true ast.Walk(&cv, bs) } } type cleanVisitor struct { textEnc *textenc.Encoder ids map[string]ast.Node allowHTML bool |
︙ | ︙ | |||
111 112 113 114 115 116 117 | if len(*is) == 0 { *is = nil return } for _, bn := range *is { ast.Walk(cv, bn) } | < < < < < < < < < < < < < < < < < < | 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | if len(*is) == 0 { *is = nil return } for _, bn := range *is { ast.Walk(cv, bn) } } func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) { if cv.doMark || hn == nil || len(hn.Inlines) == 0 { return } if hn.Slug == "" { |
︙ | ︙ |
Changes to parser/draw/draw.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 | // metadata "syntax" of a zettel). It will be used when a zettel is evaluated. package draw import ( "strconv" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/parser" | > < | | < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | // metadata "syntax" of a zettel). It will be used when a zettel is evaluated. package draw import ( "strconv" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.ValueSyntaxDraw, AltNames: []string{}, IsASTParser: true, IsTextFormat: true, IsImageFormat: false, Parse: parseBlocks, }) } const ( defaultFont = "" defaultScaleX = 10 defaultScaleY = 20 |
︙ | ︙ | |||
56 57 58 59 60 61 62 | scaleY = defaultScaleY } canvas, err := newCanvas(inp.Src[inp.Pos:]) if err != nil { return ast.BlockSlice{ast.CreateParaNode(canvasErrMsg(err)...)} } | | | < < < < < < < < < < < < < < < < < | 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 | scaleY = defaultScaleY } canvas, err := newCanvas(inp.Src[inp.Pos:]) if err != nil { return ast.BlockSlice{ast.CreateParaNode(canvasErrMsg(err)...)} } svg := canvasToSVG(canvas, string(font), int(scaleX), int(scaleY)) if len(svg) == 0 { return ast.BlockSlice{ast.CreateParaNode(noSVGErrMsg()...)} } return ast.BlockSlice{&ast.BLOBNode{ Description: parser.ParseDescription(m), Syntax: meta.ValueSyntaxSVG, Blob: svg, }} } // ParseDrawBlock parses the content of an eval verbatim node into an SVG image BLOB. func ParseDrawBlock(vn *ast.VerbatimNode) ast.BlockNode { font := defaultFont if val, found := vn.Attrs.Get("font"); found { font = val } scaleX := getScale(vn.Attrs, "x-scale", defaultScaleX) |
︙ | ︙ | |||
109 110 111 112 113 114 115 | } svg := canvasToSVG(canvas, font, scaleX, scaleY) if len(svg) == 0 { return ast.CreateParaNode(noSVGErrMsg()...) } return &ast.BLOBNode{ Description: nil, // TODO: look for attribute "summary" / "title" | | | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | } svg := canvasToSVG(canvas, font, scaleX, scaleY) if len(svg) == 0 { return ast.CreateParaNode(noSVGErrMsg()...) } return &ast.BLOBNode{ Description: nil, // TODO: look for attribute "summary" / "title" Syntax: meta.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 { |
︙ | ︙ |
Changes to parser/draw/draw_test.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 | //----------------------------------------------------------------------------- package draw_test import ( "testing" "t73f.de/r/zsc/input" "zettelstore.de/z/config" "zettelstore.de/z/parser" | > < | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | //----------------------------------------------------------------------------- package draw_test import ( "testing" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/config" "zettelstore.de/z/parser" ) func FuzzParseBlocks(f *testing.F) { f.Fuzz(func(t *testing.T, src []byte) { t.Parallel() inp := input.NewInput(src) parser.ParseBlocks(inp, nil, meta.ValueSyntaxDraw, config.NoHTML) }) } |
Changes to parser/markdown/markdown.go.
︙ | ︙ | |||
21 22 23 24 25 26 27 28 29 30 31 | "strings" gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/parser" | > < | | | < < < < < < | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | "strings" gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.ValueSyntaxMarkdown, AltNames: []string{meta.ValueSyntaxMD}, IsASTParser: true, IsTextFormat: true, IsImageFormat: false, Parse: parseBlocks, }) } func parseBlocks(inp *input.Input, _ *meta.Meta, _ string) ast.BlockSlice { p := parseMarkdown(inp) return p.acceptBlockChildren(p.docNode) } 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} } |
︙ | ︙ | |||
127 128 129 130 131 132 133 | return &ast.HRuleNode{ Attrs: nil, //TODO } } func (p *mdP) acceptCodeBlock(node *gmAst.CodeBlock) *ast.VerbatimNode { return &ast.VerbatimNode{ | | | | 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 | return &ast.HRuleNode{ Attrs: nil, //TODO } } func (p *mdP) acceptCodeBlock(node *gmAst.CodeBlock) *ast.VerbatimNode { return &ast.VerbatimNode{ Kind: ast.VerbatimCode, 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.VerbatimCode, Attrs: a, Content: p.acceptRawText(node), } } func (p *mdP) acceptRawText(node gmAst.Node) []byte { lines := node.Lines() |
︙ | ︙ | |||
354 355 356 357 358 359 360 | } buf.Write(content[lastPos:]) content = buf.Bytes() } return ast.InlineSlice{ &ast.LiteralNode{ | | | 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | } buf.Write(content[lastPos:]) content = buf.Bytes() } return ast.InlineSlice{ &ast.LiteralNode{ Kind: ast.LiteralCode, Attrs: nil, //TODO Content: content, }, } } func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice { |
︙ | ︙ | |||
441 442 443 444 445 446 447 | segs := make([][]byte, 0, node.Segments.Len()) for i := range node.Segments.Len() { segment := node.Segments.At(i) segs = append(segs, segment.Value(p.source)) } return ast.InlineSlice{ &ast.LiteralNode{ | | | | 435 436 437 438 439 440 441 442 443 444 445 446 447 | segs := make([][]byte, 0, node.Segments.Len()) for i := range node.Segments.Len() { segment := node.Segments.At(i) segs = append(segs, segment.Value(p.source)) } return ast.InlineSlice{ &ast.LiteralNode{ Kind: ast.LiteralCode, Attrs: attrs.Attributes{"": "html"}, Content: bytes.Join(segs, nil), }, } } |
Changes to parser/none/none.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package none provides a none-parser, e.g. for zettel with just metadata. package none import ( "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "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 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package none provides a none-parser, e.g. for zettel with just metadata. package none import ( "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.ValueSyntaxNone, AltNames: []string{}, IsASTParser: false, IsTextFormat: false, IsImageFormat: false, Parse: func(*input.Input, *meta.Meta, string) ast.BlockSlice { return nil }, }) } |
Changes to parser/parser.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | package parser import ( "context" "fmt" "strings" | | < | < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | package parser import ( "context" "fmt" "strings" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/parser/cleaner" "zettelstore.de/z/zettel" ) // Info describes a single parser. // // Before ParseBlocks() or ParseInlines() is called, ensure the input stream to // be valid. This can ce achieved on calling inp.Next() after the input stream // was created. type Info struct { Name string AltNames []string IsASTParser bool IsTextFormat bool IsImageFormat bool Parse func(*input.Input, *meta.Meta, string) ast.BlockSlice } var registry = map[string]*Info{} // Register the parser (info) for later retrieval. func Register(pi *Info) { if _, ok := registry[pi.Name]; ok { |
︙ | ︙ | |||
95 96 97 98 99 100 101 | return false } return pi.IsImageFormat } // ParseBlocks parses some input and returns a slice of block nodes. func ParseBlocks(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice { | | < < < < < < < < < < < < | > | | | < < | | | | | | > | | | | | | | 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 | return false } return pi.IsImageFormat } // ParseBlocks parses some input and returns a slice of block nodes. func ParseBlocks(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice { bs := Get(syntax).Parse(inp, m, syntax) cleaner.CleanBlockSlice(&bs, hi.AllowHTML(syntax)) return bs } // ParseSpacedText returns an inline slice that consists just of test and space node. // No Zettelmarkup parsing is done. It is typically used to transform the zettel // description into an inline slice. func ParseSpacedText(s string) ast.InlineSlice { return ast.InlineSlice{&ast.TextNode{Text: NormalizedSpacedText(s)}} } // NormalizedSpacedText returns the given string, but normalize multiple spaces to one space. func NormalizedSpacedText(s string) string { return strings.Join(strings.Fields(s), " ") } // ParseDescription returns a suitable description stored in the metadata as an inline slice. // This is done for an image in most cases. func ParseDescription(m *meta.Meta) ast.InlineSlice { if m == nil { return nil } if summary, found := m.Get(meta.KeySummary); found { return ParseSpacedText(string(summary)) } if title, found := m.Get(meta.KeyTitle); found { return ParseSpacedText(string(title)) } return ast.InlineSlice{&ast.TextNode{Text: "Zettel without title/summary: " + m.Zid.String()}} } // ParseZettel parses the zettel based on the syntax. func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode { m := zettel.Meta inhMeta := m if rtConfig != nil { inhMeta = rtConfig.AddDefaultValues(ctx, inhMeta) } if syntax == "" { syntax = string(inhMeta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)) } parseMeta := inhMeta if syntax == meta.ValueSyntaxNone { parseMeta = m } hi := config.NoHTML if rtConfig != nil { hi = rtConfig.GetHTMLInsecurity() } bs := ParseBlocks(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, hi) return &ast.ZettelNode{ Meta: m, Content: zettel.Content, Zid: m.Zid, InhMeta: inhMeta, BlocksAST: bs, Syntax: syntax, } } |
Changes to parser/parser_test.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package parser_test import ( "testing" | | | | | | | | | | | | | | | | | | | | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | //----------------------------------------------------------------------------- package parser_test import ( "testing" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/parser" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) func TestParserType(t *testing.T) { syntaxSet := set.New(parser.GetSyntaxes()...) testCases := []struct { syntax string ast bool image bool }{ {meta.ValueSyntaxHTML, false, false}, {meta.ValueSyntaxCSS, false, false}, {meta.ValueSyntaxDraw, true, false}, {meta.ValueSyntaxGif, false, true}, {meta.ValueSyntaxJPEG, false, true}, {meta.ValueSyntaxJPG, false, true}, {meta.ValueSyntaxMarkdown, true, false}, {meta.ValueSyntaxMD, true, false}, {meta.ValueSyntaxNone, false, false}, {meta.ValueSyntaxPlain, false, false}, {meta.ValueSyntaxPNG, false, true}, {meta.ValueSyntaxSVG, false, true}, {meta.ValueSyntaxSxn, false, false}, {meta.ValueSyntaxText, false, false}, {meta.ValueSyntaxTxt, false, false}, {meta.ValueSyntaxWebp, false, true}, {meta.ValueSyntaxZmk, true, false}, } for _, tc := range testCases { syntaxSet.Remove(tc.syntax) if got := parser.IsASTParser(tc.syntax); got != tc.ast { t.Errorf("Syntax %q is AST: %v, but got %v", tc.syntax, tc.ast, got) } if got := parser.IsImageFormat(tc.syntax); got != tc.image { t.Errorf("Syntax %q is image: %v, but got %v", tc.syntax, tc.image, got) } } for syntax := range syntaxSet.Values() { t.Errorf("Forgot to test syntax %q", syntax) } } |
Changes to parser/plain/plain.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 | package plain import ( "bytes" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/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 44 45 46 47 48 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 | package plain import ( "bytes" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.ValueSyntaxTxt, AltNames: []string{meta.ValueSyntaxPlain, meta.ValueSyntaxText}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, Parse: parseBlocks, }) parser.Register(&parser.Info{ Name: meta.ValueSyntaxHTML, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, Parse: parseBlocksHTML, }) parser.Register(&parser.Info{ Name: meta.ValueSyntaxCSS, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, Parse: parseBlocks, }) parser.Register(&parser.Info{ Name: meta.ValueSyntaxSVG, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: true, Parse: parseSVGBlocks, }) parser.Register(&parser.Info{ Name: meta.ValueSyntaxSxn, AltNames: []string{}, IsASTParser: false, IsTextFormat: true, IsImageFormat: false, Parse: parseSxnBlocks, }) } func parseBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { return doParseBlocks(inp, syntax, ast.VerbatimCode) } 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 parseSVGBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { is := parseSVGInlines(inp, syntax) if len(is) == 0 { return nil } return ast.BlockSlice{ast.CreateParaNode(is...)} } |
︙ | ︙ | |||
138 139 140 141 142 143 144 | } func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { rd := sxreader.MakeReader(bytes.NewReader(inp.Src)) _, err := rd.ReadAll() result := ast.BlockSlice{ &ast.VerbatimNode{ | | < < < < < < < < < | 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | } func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { rd := sxreader.MakeReader(bytes.NewReader(inp.Src)) _, err := rd.ReadAll() result := ast.BlockSlice{ &ast.VerbatimNode{ Kind: ast.VerbatimCode, Attrs: attrs.Attributes{"": syntax}, Content: inp.ScanLineContent(), }, } if err != nil { result = append(result, ast.CreateParaNode(&ast.TextNode{ Text: err.Error(), })) } return result } |
Changes to parser/plain/plain_test.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 | //----------------------------------------------------------------------------- package plain_test import ( "testing" "t73f.de/r/zsc/input" | > | | | | | | | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 plain_test import ( "testing" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/config" "zettelstore.de/z/encoder/szenc" "zettelstore.de/z/parser" ) func TestParseSVG(t *testing.T) { testCases := []struct { name string src string exp string }{ {"common", " <svg bla", "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg bla\")))"}, {"inkscape", "<svg\nbla", "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg\\nbla\")))"}, {"selfmade", "<svg>", "(BLOCK (PARA (EMBED-BLOB () \"svg\" \"<svg>\")))"}, {"error", "<svgbla", "(BLOCK)"}, {"error-", "<svg-bla", "(BLOCK)"}, {"error#", "<svg2bla", "(BLOCK)"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { inp := input.NewInput([]byte(tc.src)) bs := parser.ParseBlocks(inp, nil, meta.ValueSyntaxSVG, config.NoHTML) trans := szenc.NewTransformer() lst := trans.GetSz(&bs) if got := lst.String(); tc.exp != got { t.Errorf("\nexp: %q\ngot: %q", tc.exp, got) } }) } } |
Deleted parser/zettelmark/block.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/zettelmark/inline.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/zettelmark/node.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted parser/zettelmark/post-processor.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to parser/zettelmark/zettelmark.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( | | < | > | | | | < | < < < < < | < < < < < < | < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | < < < | < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( "log" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "t73f.de/r/zsc/sz/zmk" "zettelstore.de/z/ast" "zettelstore.de/z/ast/sztrans" "zettelstore.de/z/parser" ) func init() { parser.Register(&parser.Info{ Name: meta.ValueSyntaxZmk, AltNames: nil, IsASTParser: true, IsTextFormat: true, IsImageFormat: false, Parse: parseZmk, }) } func parseZmk(inp *input.Input, _ *meta.Meta, _ string) ast.BlockSlice { if obj := zmk.Parse(inp); obj != nil { bs, err := sztrans.GetBlockSlice(obj) if err == nil { return bs } log.Printf("sztrans error: %v, for %v\n", err, obj) } return nil } |
Changes to parser/zettelmark/zettelmark_fuzz_test.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 | //----------------------------------------------------------------------------- package zettelmark_test import ( "testing" "t73f.de/r/zsc/input" "zettelstore.de/z/config" "zettelstore.de/z/parser" | > < | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | //----------------------------------------------------------------------------- package zettelmark_test import ( "testing" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/config" "zettelstore.de/z/parser" ) func FuzzParseBlocks(f *testing.F) { f.Fuzz(func(t *testing.T, src []byte) { t.Parallel() inp := input.NewInput(src) parser.ParseBlocks(inp, nil, meta.ValueSyntaxZmk, config.NoHTML) }) } |
Changes to parser/zettelmark/zettelmark_test.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 | import ( "fmt" "strings" "testing" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/parser" | > < | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import ( "fmt" "strings" "testing" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/parser" ) type TestCase struct{ source, want string } type TestCases []TestCase func replace(s string, tcs TestCases) TestCases { var testCases TestCases |
︙ | ︙ | |||
44 45 46 47 48 49 50 | func checkTcs(t *testing.T, tcs TestCases) { t.Helper() for tcn, tc := range tcs { t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) { st.Helper() inp := input.NewInput([]byte(tc.source)) | | | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | func checkTcs(t *testing.T, tcs TestCases) { t.Helper() for tcn, tc := range tcs { t.Run(fmt.Sprintf("TC=%02d,src=%q", tcn, tc.source), func(st *testing.T) { st.Helper() inp := input.NewInput([]byte(tc.source)) bns := parser.ParseBlocks(inp, nil, meta.ValueSyntaxZmk, config.NoHTML) var tv TestVisitor ast.Walk(&tv, &bns) got := tv.String() if tc.want != got { st.Errorf("\nwant=%q\n got=%q", tc.want, got) } }) |
︙ | ︙ | |||
282 283 284 285 286 287 288 | {"%%a", "(PARA {% a})"}, {"%%%a", "(PARA {% a})"}, {"%% a", "(PARA {% a})"}, {"%%% a", "(PARA {% a})"}, {"%% % a", "(PARA {% % a})"}, {"%%a", "(PARA {% a})"}, {"a%%b", "(PARA a {% b})"}, | | | 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | {"%%a", "(PARA {% a})"}, {"%%%a", "(PARA {% a})"}, {"%% a", "(PARA {% a})"}, {"%%% a", "(PARA {% a})"}, {"%% % a", "(PARA {% % a})"}, {"%%a", "(PARA {% a})"}, {"a%%b", "(PARA a {% b})"}, {"a %%b", "(PARA a {% b})" /*"(PARA a {% b})"*/}, {" %%b", "(PARA {% b})"}, {"%%b ", "(PARA {% b })"}, {"100%", "(PARA 100%)"}, }) } func TestFormat(t *testing.T) { |
︙ | ︙ | |||
326 327 328 329 330 331 332 | {"__**a**__", "(PARA {_ {* a}})"}, {"__**__**", "(PARA __ {* __})"}, }) } func TestLiteral(t *testing.T) { t.Parallel() | | > | 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 | {"__**a**__", "(PARA {_ {* a}})"}, {"__**__**", "(PARA __ {* __})"}, }) } func TestLiteral(t *testing.T) { t.Parallel() for _, ch := range []string{"`", "'", "="} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, {"$$a$$", "(PARA {$ a})"}, {"$$a$$$", "(PARA {$ a} $)"}, {"$$$a$$", "(PARA {$ $a})"}, {"$$$a$$$", "(PARA {$ $a} $)"}, {"$\\$", "(PARA $$)"}, {"$\\$$", "(PARA $$$)"}, {"$$\\$", "(PARA $$$)"}, {"$$a\\$$", "(PARA $$a$$)"}, {"$$a$\\$", "(PARA $$a$$)"}, {"$$a\\$$$", "(PARA {$ a$})"}, {"$$a$${go}", "(PARA {$ a}[ATTR go])"}, })) } checkTcs(t, TestCases{ {"``<script `` abc", "(PARA {` <script } abc)"}, {"''````''", "(PARA {' ````})"}, {"''``a``''", "(PARA {' ``a``})"}, {"''``''``", "(PARA {' ``} ``)"}, {"''\\'''", "(PARA {' '})"}, }) } |
︙ | ︙ | |||
420 421 422 423 424 425 426 | {"E: &,?;c.", "(PARA E: &,?;c.)"}, }) } func TestVerbatimZettel(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ | | | | | | | | | | | | | 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 | {"E: &,?;c.", "(PARA E: &,?;c.)"}, }) } func TestVerbatimZettel(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"@@@\n@@@", "(ZETTEL)"}, {"@@@\nabc\n@@@", "(ZETTEL\nabc)"}, {"@@@@def\nabc\n@@@@", "(ZETTEL\nabc)[ATTR =def]"}, }) } func TestVerbatimCode(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"```\n```", "(PROG)"}, {"```\nabc\n```", "(PROG\nabc)"}, {"```\nabc\n````", "(PROG\nabc)"}, {"````\nabc\n````", "(PROG\nabc)"}, {"````\nabc\n```\n````", "(PROG\nabc\n```)"}, {"````go\nabc\n````", "(PROG\nabc)[ATTR =go]"}, }) } func TestVerbatimEval(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"~~~\n~~~", "(EVAL)"}, {"~~~\nabc\n~~~", "(EVAL\nabc)"}, {"~~~\nabc\n~~~~", "(EVAL\nabc)"}, {"~~~~\nabc\n~~~~", "(EVAL\nabc)"}, {"~~~~\nabc\n~~~\n~~~~", "(EVAL\nabc\n~~~)"}, {"~~~~go\nabc\n~~~~", "(EVAL\nabc)[ATTR =go]"}, }) } func TestVerbatimMath(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"$$$\n$$$", "(MATH)"}, {"$$$\nabc\n$$$", "(MATH\nabc)"}, {"$$$\nabc\n$$$$", "(MATH\nabc)"}, {"$$$$\nabc\n$$$$", "(MATH\nabc)"}, {"$$$$\nabc\n$$$\n$$$$", "(MATH\nabc\n$$$)"}, {"$$$$go\nabc\n$$$$", "(MATH\nabc)[ATTR =go]"}, }) } func TestVerbatimComment(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"%%%\n%%%", "(COMMENT)"}, {"%%%\nabc\n%%%", "(COMMENT\nabc)"}, {"%%%%go\nabc\n%%%%", "(COMMENT\nabc)[ATTR =go]"}, }) } func TestPara(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"a\n\nb", "(PARA a)(PARA b)"}, {"a\n \nb", "(PARA a)(PARA b)"}, }) } func TestSpanRegion(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {":::\n:::", "(SPAN)"}, {":::\nabc\n:::", "(SPAN (PARA abc))"}, {":::\nabc\n::::", "(SPAN (PARA abc))"}, {"::::\nabc\n::::", "(SPAN (PARA abc))"}, {"::::\nabc\n:::\ndef\n:::\n::::", "(SPAN (PARA abc)(SPAN (PARA def)))"}, {":::{go}\na\n:::", "(SPAN (PARA a))[ATTR go]"}, {":::\nabc\n::: def ", "(SPAN (PARA abc) (LINE def))"}, }) } func TestQuoteRegion(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"<<<\n<<<", "(QUOTE)"}, {"<<<\nabc\n<<<", "(QUOTE (PARA abc))"}, {"<<<\nabc\n<<<<", "(QUOTE (PARA abc))"}, {"<<<<\nabc\n<<<<", "(QUOTE (PARA abc))"}, {"<<<<\nabc\n<<<\ndef\n<<<\n<<<<", "(QUOTE (PARA abc)(QUOTE (PARA def)))"}, {"<<<go\na\n<<<", "(QUOTE (PARA a))[ATTR =go]"}, {"<<<\nabc\n<<< def ", "(QUOTE (PARA abc) (LINE def))"}, }) } func TestVerseRegion(t *testing.T) { t.Parallel() checkTcs(t, replace("\"", TestCases{ // {"$$$\n$$$", "(VERSE)"}, {"$$$\nabc\n$$$", "(VERSE (PARA abc))"}, {"$$$\nabc\n$$$$", "(VERSE (PARA abc))"}, {"$$$$\nabc\n$$$$", "(VERSE (PARA abc))"}, {"$$$\nabc\ndef\n$$$", "(VERSE (PARA abc HB def))"}, {"$$$$\nabc\n$$$\ndef\n$$$\n$$$$", "(VERSE (PARA abc)(VERSE (PARA def)))"}, {"$$$go\na\n$$$", "(VERSE (PARA a))[ATTR =go]"}, {"$$$\nabc\n$$$ def ", "(VERSE (PARA abc) (LINE def))"}, })) } func TestHeading(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ |
︙ | ︙ | |||
657 658 659 660 661 662 663 | {"; abc\n: def\n; ghi\n: jkl", "(DL (DT abc) (DD (PARA def)) (DT ghi) (DD (PARA jkl)))"}, }) } func TestTable(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ | | | 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 | {"; abc\n: def\n; ghi\n: jkl", "(DL (DT abc) (DD (PARA def)) (DT ghi) (DD (PARA jkl)))"}, }) } func TestTable(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ // {"|", "(TAB (TR))"}, {"||", "(TAB (TR (TD)))"}, {"| |", "(TAB (TR (TD)))"}, {"|a", "(TAB (TR (TD a)))"}, {"|a|", "(TAB (TR (TD a)))"}, {"|a| ", "(TAB (TR (TD a)(TD)))"}, {"|a|b", "(TAB (TR (TD a)(TD b)))"}, {"|a|b\n|c|d", "(TAB (TR (TD a)(TD b))(TR (TD c)(TD d)))"}, |
︙ | ︙ | |||
687 688 689 690 691 692 693 | {"{{{a}}}{go=b}", "(TRANSCLUDE a)[ATTR go=b]"}, }) } func TestBlockAttr(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 | {"{{{a}}}{go=b}", "(TRANSCLUDE a)[ATTR go=b]"}, }) } func TestBlockAttr(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {":::go\na\n:::", "(SPAN (PARA a))[ATTR =go]"}, {":::go=\na\n:::", "(SPAN (PARA a))[ATTR =go]"}, {":::{}\na\n:::", "(SPAN (PARA a))"}, {":::{ }\na\n:::", "(SPAN (PARA a))"}, {":::{.go}\na\n:::", "(SPAN (PARA a))[ATTR class=go]"}, {":::{=go}\na\n:::", "(SPAN (PARA a))[ATTR =go]"}, {":::{go}\na\n:::", "(SPAN (PARA a))[ATTR go]"}, {":::{go=py}\na\n:::", "(SPAN (PARA a))[ATTR go=py]"}, {":::{.go=py}\na\n:::", "(SPAN (PARA a))"}, {":::{go=}\na\n:::", "(SPAN (PARA a))[ATTR go]"}, {":::{.go=}\na\n:::", "(SPAN (PARA a))"}, {":::{go py}\na\n:::", "(SPAN (PARA a))[ATTR go py]"}, {":::{go\npy}\na\n:::", "(SPAN (PARA a))[ATTR go py]"}, {":::{.go py}\na\n:::", "(SPAN (PARA a))[ATTR class=go py]"}, {":::{go .py}\na\n:::", "(SPAN (PARA a))[ATTR class=py go]"}, {":::{.go py=3}\na\n:::", "(SPAN (PARA a))[ATTR class=go py=3]"}, {"::: { go } \na\n:::", "(SPAN (PARA a))[ATTR go]"}, {"::: { .go } \na\n:::", "(SPAN (PARA a))[ATTR class=go]"}, }) checkTcs(t, replace("\"", TestCases{ {":::{py=3}\na\n:::", "(SPAN (PARA a))[ATTR py=3]"}, {":::{py=$2 3$}\na\n:::", "(SPAN (PARA a))[ATTR py=$2 3$]"}, {":::{py=$2\\$3$}\na\n:::", "(SPAN (PARA a))[ATTR py=2$3]"}, {":::{py=2$3}\na\n:::", "(SPAN (PARA a))[ATTR py=2$3]"}, {":::{py=$2\n3$}\na\n:::", "(SPAN (PARA a))[ATTR py=$2\n3$]"}, {":::{py=$2 3}\na\n:::", "(SPAN (PARA a))"}, {":::{py=2 py=3}\na\n:::", "(SPAN (PARA a))[ATTR py=$2 3$]"}, {":::{.go .py}\na\n:::", "(SPAN (PARA a))[ATTR class=$go py$]"}, {":::{go go}\na\n:::", "(SPAN (PARA a))[ATTR go]"}, {":::{=py =go}\na\n:::", "(SPAN (PARA a))[ATTR =go]"}, })) } func TestInlineAttr(t *testing.T) { t.Parallel() checkTcs(t, TestCases{ {"::a::{}", "(PARA {: a})"}, |
︙ | ︙ | |||
869 870 871 872 873 874 875 | tv.sb.WriteString(")") } tv.sb.WriteString(")") } } tv.sb.WriteString(")") case *ast.TranscludeNode: | | | 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 | tv.sb.WriteString(")") } tv.sb.WriteString(")") } } tv.sb.WriteString(")") case *ast.TranscludeNode: fmt.Fprintf(&tv.sb, "(TRANSCLUDE %v)", n.Ref) // FIXME n.Inlines tv.visitAttributes(n.Attrs) case *ast.BLOBNode: tv.sb.WriteString("(BLOB ") tv.sb.WriteString(n.Syntax) tv.sb.WriteString(")") case *ast.TextNode: tv.sb.WriteString(n.Text) |
︙ | ︙ | |||
950 951 952 953 954 955 956 | return tv } return nil } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: "(ZETTEL", | | | 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 | return tv } return nil } var mapVerbatimKind = map[ast.VerbatimKind]string{ ast.VerbatimZettel: "(ZETTEL", ast.VerbatimCode: "(PROG", ast.VerbatimEval: "(EVAL", ast.VerbatimMath: "(MATH", ast.VerbatimComment: "(COMMENT", } var mapRegionKind = map[ast.RegionKind]string{ ast.RegionSpan: "(SPAN", |
︙ | ︙ | |||
988 989 990 991 992 993 994 | ast.FormatSub: ',', ast.FormatQuote: '"', ast.FormatMark: '#', ast.FormatSpan: ':', } var mapLiteralKind = map[ast.LiteralKind]rune{ | | < | 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 | ast.FormatSub: ',', ast.FormatQuote: '"', ast.FormatMark: '#', ast.FormatSpan: ':', } var mapLiteralKind = map[ast.LiteralKind]rune{ ast.LiteralCode: '`', ast.LiteralInput: '\'', ast.LiteralOutput: '=', ast.LiteralComment: '%', ast.LiteralMath: '$', } func (tv *TestVisitor) visitInlineSlice(is *ast.InlineSlice) { |
︙ | ︙ |
Changes to query/compiled.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | package query import ( "math/rand/v2" "slices" | | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package query import ( "math/rand/v2" "slices" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" ) // Compiled is a compiled query, to be used in a Box type Compiled struct { hasQuery bool seed int pick int |
︙ | ︙ |
Changes to query/context.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 | //----------------------------------------------------------------------------- package query import ( "container/heap" "context" "math" "t73f.de/r/zsc/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 | //----------------------------------------------------------------------------- package query import ( "container/heap" "context" "iter" "math" "slices" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" ) // ContextSpec contains all specification values for calculating a context. type ContextSpec struct { Direction ContextDirection MaxCost int MaxCount int MinCount int Full bool } // ContextDirection specifies the direction a context should be calculated. type ContextDirection uint8 // Constants for ContextDirection. |
︙ | ︙ | |||
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | pe.writeString(api.BackwardDirective) case ContextDirForward: pe.printSpace() pe.writeString(api.ForwardDirective) } pe.printPosInt(api.CostDirective, spec.MaxCost) pe.printPosInt(api.MaxDirective, spec.MaxCount) } // Execute the specification. func (spec *ContextSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ContextPort) []*meta.Meta { maxCost := float64(spec.MaxCost) if maxCost <= 0 { maxCost = 17 } maxCount := spec.MaxCount if maxCount <= 0 { maxCount = 200 } | > | | | | | < | < | | > | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | > | | | > | | | | | | | | | | | | | | | | | < < < < | | < | | | | | > | | | | | | | < < < > > > > | | | > > > > | > | | > > > > > > | 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 | pe.writeString(api.BackwardDirective) case ContextDirForward: pe.printSpace() pe.writeString(api.ForwardDirective) } pe.printPosInt(api.CostDirective, spec.MaxCost) pe.printPosInt(api.MaxDirective, spec.MaxCount) pe.printPosInt(api.MinDirective, spec.MinCount) } // Execute the specification. func (spec *ContextSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ContextPort) []*meta.Meta { maxCost := float64(spec.MaxCost) if maxCost <= 0 { maxCost = 17 } maxCount := spec.MaxCount if maxCount <= 0 { maxCount = 200 } tasks := newQueue(startSeq, maxCost, maxCount, spec.MinCount, port) isBackward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirBackward isForward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirForward result := make([]*meta.Meta, 0, max(spec.MinCount, 16)) for { m, cost, level := tasks.next() if m == nil { break } result = append(result, m) for key, val := range m.ComputedRest() { tasks.addPair(ctx, key, val, cost, level, isBackward, isForward) } if !spec.Full { continue } tasks.addTags(ctx, m.GetFields(meta.KeyTags), cost, level) } return result } type ztlCtxItem struct { cost float64 meta *meta.Meta level uint } type ztlCtxQueue []ztlCtxItem func (q ztlCtxQueue) Len() int { return len(q) } func (q ztlCtxQueue) Less(i, j int) bool { levelI, levelJ := q[i].level, q[j].level if levelI == 0 { if levelJ == 0 { return q[i].meta.Zid < q[j].meta.Zid } return true } if levelI == 1 { if levelJ == 0 { return false } if levelJ == 1 { costI, costJ := q[i].cost, q[j].cost if costI == costJ { return q[i].meta.Zid < q[j].meta.Zid } return costI < costJ } return true } if levelJ == 0 || levelJ == 1 { return false } costI, costJ := q[i].cost, q[j].cost if costI == costJ { return q[i].meta.Zid < q[j].meta.Zid } return costI < costJ } func (q ztlCtxQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i] } func (q *ztlCtxQueue) Push(x any) { *q = append(*q, x.(ztlCtxItem)) } func (q *ztlCtxQueue) Pop() any { old := *q n := len(old) item := old[n-1] old[n-1].meta = nil // avoid memory leak *q = old[0 : n-1] return item } type contextTask struct { port ContextPort seen *idset.Set queue ztlCtxQueue maxCost float64 maxCount int minCount int tagMetas map[string][]*meta.Meta tagZids map[string]*idset.Set // just the zids of tagMetas metaZid map[id.Zid]*meta.Meta // maps zid to meta for all meta retrieved with tags } func newQueue(startSeq []*meta.Meta, maxCost float64, maxCount, minCount int, port ContextPort) *contextTask { result := &contextTask{ port: port, seen: idset.New(), maxCost: maxCost, maxCount: max(maxCount, minCount), minCount: minCount, tagMetas: make(map[string][]*meta.Meta), tagZids: make(map[string]*idset.Set), metaZid: make(map[id.Zid]*meta.Meta), } queue := make(ztlCtxQueue, 0, len(startSeq)) for _, m := range startSeq { queue = append(queue, ztlCtxItem{cost: 1, meta: m}) } heap.Init(&queue) result.queue = queue return result } func (ct *contextTask) addPair(ctx context.Context, key string, value meta.Value, curCost float64, level uint, isBackward, isForward bool) { if key == meta.KeyBack { return } newCost := curCost + contextCost(key) if key == meta.KeyBackward { if isBackward { ct.addIDSet(ctx, newCost, level, value) } return } if key == meta.KeyForward { if isForward { ct.addIDSet(ctx, newCost, level, value) } return } hasInverse := meta.Inverse(key) != "" if (!hasInverse || !isBackward) && (hasInverse || !isForward) { return } if t := meta.Type(key); t == meta.TypeID { ct.addID(ctx, newCost, level, value) } else if t == meta.TypeIDSet { ct.addIDSet(ctx, newCost, level, value) } } func contextCost(key string) float64 { switch key { case meta.KeyFolge, meta.KeyPrecursor: return 0.1 case meta.KeySequel, meta.KeyPrequel: return 1.0 case meta.KeySuccessors, meta.KeyPredecessor: return 7 } return 2 } func (ct *contextTask) addID(ctx context.Context, newCost float64, level uint, value meta.Value) { if zid, errParse := id.Parse(string(value)); errParse == nil { if m, errGetMeta := ct.port.GetMeta(ctx, zid); errGetMeta == nil { ct.addMeta(m, newCost, level) } } } func (ct *contextTask) addMeta(m *meta.Meta, newCost float64, level uint) { if !ct.seen.Contains(m.Zid) { heap.Push(&ct.queue, ztlCtxItem{cost: newCost, meta: m, level: level + 1}) } } func (ct *contextTask) addIDSet(ctx context.Context, newCost float64, level uint, value meta.Value) { elems := value.AsSlice() refCost := referenceCost(newCost, len(elems)) for _, val := range elems { ct.addID(ctx, refCost, level, meta.Value(val)) } } func referenceCost(baseCost float64, numReferences int) float64 { nRefs := float64(numReferences) return nRefs*math.Log2(nRefs+1) + baseCost - 1 } func (ct *contextTask) addTags(ctx context.Context, tagiter iter.Seq[string], baseCost float64, level uint) { tags := slices.Collect(tagiter) var zidSet *idset.Set for _, tag := range tags { zs := ct.updateTagData(ctx, tag) zidSet = zidSet.IUnion(zs) } zidSet.ForEach(func(zid id.Zid) { minCost := math.MaxFloat64 costFactor := 1.1 for _, tag := range tags { tagZids := ct.tagZids[tag] if tagZids.Contains(zid) { cost := tagCost(baseCost, tagZids.Length()) if cost < minCost { minCost = cost } costFactor /= 1.1 } } ct.addMeta(ct.metaZid[zid], minCost*costFactor, level) }) } func (ct *contextTask) updateTagData(ctx context.Context, tag string) *idset.Set { if _, found := ct.tagMetas[tag]; found { return ct.tagZids[tag] } q := Parse(meta.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + meta.KeyID) ml, err := ct.port.SelectMeta(ctx, nil, q) if err != nil { ml = nil } ct.tagMetas[tag] = ml zids := idset.NewCap(len(ml)) for _, m := range ml { zid := m.Zid zids = zids.Add(zid) if _, found := ct.metaZid[zid]; !found { ct.metaZid[zid] = m } } ct.tagZids[tag] = zids return zids } func tagCost(baseCost float64, numTags int) float64 { nTags := float64(numTags) return nTags*math.Log2(nTags+1) + baseCost - 1 } func (ct *contextTask) next() (*meta.Meta, float64, uint) { for len(ct.queue) > 0 { item := heap.Pop(&ct.queue).(ztlCtxItem) m := item.meta zid := m.Zid if ct.seen.Contains(zid) { continue } cost, level := item.cost, item.level if ct.hasEnough(cost, level) { break } ct.seen.Add(zid) return m, cost, item.level } return nil, -1, 0 } func (ct *contextTask) hasEnough(cost float64, level uint) bool { if level <= 1 { // Always add direct descendants of the initial zettel return false } length := ct.seen.Length() if minCount := ct.minCount; 0 < minCount && minCount > length { return false } if maxCount := ct.maxCount; 0 < maxCount && maxCount <= length { return true } maxCost := ct.maxCost return maxCost == 0.0 || maxCost <= cost } |
Changes to query/parser.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | package query import ( "strconv" "t73f.de/r/zsc/api" | | > | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package query import ( "strconv" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" ) // Parse the query specification and return a Query object. func Parse(spec string) (q *Query) { return q.Parse(spec) } // Parse the query string and update the Query object. func (q *Query) Parse(spec string) *Query { |
︙ | ︙ | |||
76 77 78 79 80 81 82 | func (ps *parserState) parse(q *Query) *Query { inp := ps.inp inp.SkipSpace() if ps.mustStop() { return q } firstPos := inp.Pos | | | 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | func (ps *parserState) parse(q *Query) *Query { inp := ps.inp inp.SkipSpace() if ps.mustStop() { return q } firstPos := inp.Pos zidSet := idset.New() for { pos := inp.Pos zid, found := ps.scanZid() if !found { inp.SetPos(pos) break } |
︙ | ︙ | |||
228 229 230 231 232 233 234 | if ps.acceptKwArgs(api.CostDirective) { if ps.parseCost(spec) { continue } } inp.SetPos(pos) if ps.acceptKwArgs(api.MaxDirective) { | | > > > > > > | 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | if ps.acceptKwArgs(api.CostDirective) { if ps.parseCost(spec) { continue } } inp.SetPos(pos) if ps.acceptKwArgs(api.MaxDirective) { if ps.parseMaxCount(spec) { continue } } inp.SetPos(pos) if ps.acceptKwArgs(api.MinDirective) { if ps.parseMinCount(spec) { continue } } inp.SetPos(pos) break } |
︙ | ︙ | |||
250 251 252 253 254 255 256 | return false } if spec.MaxCost == 0 || spec.MaxCost >= num { spec.MaxCost = num } return true } | | > > > > > > > > > > | 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 | return false } if spec.MaxCost == 0 || spec.MaxCost >= num { spec.MaxCost = num } return true } func (ps *parserState) parseMaxCount(spec *ContextSpec) bool { num, ok := ps.scanPosInt() if !ok { return false } if spec.MaxCount == 0 || spec.MaxCount >= num { spec.MaxCount = num } return true } func (ps *parserState) parseMinCount(spec *ContextSpec) bool { num, ok := ps.scanPosInt() if !ok { return false } if spec.MinCount == 0 || spec.MinCount <= num { spec.MinCount = num } return true } func (ps *parserState) parseUnlinked(q *Query) *Query { inp := ps.inp spec := &UnlinkedSpec{} for { |
︙ | ︙ | |||
391 392 393 394 395 396 397 | } else if len(text) == 0 { // Only an empty search operation is found -> ignore it return q } q = createIfNeeded(q) if hasOp { if key == nil { | | | | | | 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 | } else if len(text) == 0 { // Only an empty search operation is found -> ignore it return q } q = createIfNeeded(q) if hasOp { if key == nil { q.addSearch(expValue{meta.Value(string(text)), op}) } else { last := len(q.terms) - 1 if q.terms[last].mvals == nil { q.terms[last].mvals = expMetaValues{string(key): {expValue{meta.Value(string(text)), op}}} } else { sKey := string(key) q.terms[last].mvals[sKey] = append(q.terms[last].mvals[sKey], expValue{meta.Value(string(text)), op}) } } } else { // Assert key == nil q.addSearch(expValue{meta.Value(string(text)), cmpMatch}) } return q } func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) { inp := ps.inp pos := inp.Pos |
︙ | ︙ |
Changes to query/parser_test.go.
︙ | ︙ | |||
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | {"1 IDENT|REINDEX", "00000000000001 IDENT | REINDEX"}, {"1 ITEMS", "00000000000001 ITEMS"}, {"ITEMS", "ITEMS"}, {"CONTEXT", "CONTEXT"}, {"CONTEXT a", "CONTEXT a"}, {"0 CONTEXT", "0 CONTEXT"}, {"1 CONTEXT", "00000000000001 CONTEXT"}, {"00000000000001 CONTEXT", "00000000000001 CONTEXT"}, {"100000000000001 CONTEXT", "100000000000001 CONTEXT"}, {"1 CONTEXT FULL", "00000000000001 CONTEXT FULL"}, {"1 CONTEXT BACKWARD", "00000000000001 CONTEXT BACKWARD"}, {"1 CONTEXT FORWARD", "00000000000001 CONTEXT FORWARD"}, {"1 CONTEXT COST ", "00000000000001 CONTEXT COST"}, {"1 CONTEXT COST 3", "00000000000001 CONTEXT COST 3"}, {"1 CONTEXT COST x", "00000000000001 CONTEXT COST x"}, {"1 CONTEXT MAX 5", "00000000000001 CONTEXT MAX 5"}, {"1 CONTEXT MAX y", "00000000000001 CONTEXT MAX y"}, {"1 CONTEXT MAX 5 COST 7", "00000000000001 CONTEXT COST 7 MAX 5"}, {"1 CONTEXT | N", "00000000000001 CONTEXT | N"}, {"1 1 CONTEXT", "00000000000001 CONTEXT"}, {"1 2 CONTEXT", "00000000000001 00000000000002 CONTEXT"}, {"2 1 CONTEXT", "00000000000002 00000000000001 CONTEXT"}, {"1 CONTEXT|N", "00000000000001 CONTEXT | N"}, | > > | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | {"1 IDENT|REINDEX", "00000000000001 IDENT | REINDEX"}, {"1 ITEMS", "00000000000001 ITEMS"}, {"ITEMS", "ITEMS"}, {"CONTEXT", "CONTEXT"}, {"CONTEXT a", "CONTEXT a"}, {"0 CONTEXT", "0 CONTEXT"}, {"1 CONTEXT", "00000000000001 CONTEXT"}, {"1 CONTEXT CONTEXT", "00000000000001 CONTEXT CONTEXT"}, {"00000000000001 CONTEXT", "00000000000001 CONTEXT"}, {"100000000000001 CONTEXT", "100000000000001 CONTEXT"}, {"1 CONTEXT FULL", "00000000000001 CONTEXT FULL"}, {"1 CONTEXT BACKWARD", "00000000000001 CONTEXT BACKWARD"}, {"1 CONTEXT FORWARD", "00000000000001 CONTEXT FORWARD"}, {"1 CONTEXT COST ", "00000000000001 CONTEXT COST"}, {"1 CONTEXT COST 3", "00000000000001 CONTEXT COST 3"}, {"1 CONTEXT COST x", "00000000000001 CONTEXT COST x"}, {"1 CONTEXT MAX 5", "00000000000001 CONTEXT MAX 5"}, {"1 CONTEXT MAX y", "00000000000001 CONTEXT MAX y"}, {"1 CONTEXT MIN 7", "00000000000001 CONTEXT MIN 7"}, {"1 CONTEXT MIN y", "00000000000001 CONTEXT MIN y"}, {"1 CONTEXT MAX 5 COST 7", "00000000000001 CONTEXT COST 7 MAX 5"}, {"1 CONTEXT | N", "00000000000001 CONTEXT | N"}, {"1 1 CONTEXT", "00000000000001 CONTEXT"}, {"1 2 CONTEXT", "00000000000001 00000000000002 CONTEXT"}, {"2 1 CONTEXT", "00000000000002 00000000000001 CONTEXT"}, {"1 CONTEXT|N", "00000000000001 CONTEXT | N"}, |
︙ | ︙ |
Changes to query/print.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 20 21 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "io" "strconv" "strings" "t73f.de/r/zsc/api" | > > | < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "io" "maps" "slices" "strconv" "strings" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" ) var op2string = map[compareOp]string{ cmpExist: api.ExistOperator, cmpNotExist: api.ExistNotOperator, cmpEqual: api.SearchOperatorEqual, cmpNotEqual: api.SearchOperatorNotEqual, |
︙ | ︙ | |||
58 59 60 61 62 63 64 | for _, d := range q.directives { d.Print(&env) } for i, term := range q.terms { if i > 0 { env.writeString(" OR") } | | | | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | for _, d := range q.directives { d.Print(&env) } for i, term := range q.terms { if i > 0 { env.writeString(" OR") } for _, name := range slices.Sorted(maps.Keys(term.keys)) { env.printSpace() env.writeString(name) if op := term.keys[name]; op == cmpExist || op == cmpNotExist { env.writeString(op2string[op]) } else { env.writeStrings(api.ExistOperator, " ", name, api.ExistNotOperator) } } for _, name := range slices.Sorted(maps.Keys(term.mvals)) { env.printExprValues(name, term.mvals[name]) } if len(term.search) > 0 { env.printExprValues("", term.search) } } env.printPosInt(api.PickDirective, q.pick) |
︙ | ︙ | |||
139 140 141 142 143 144 145 | default: if s, found := op2string[op]; found { pe.writeString(s) } else { pe.writeString("%" + strconv.Itoa(int(op))) } } | | | 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | default: if s, found := op2string[op]; found { pe.writeString(s) } else { pe.writeString("%" + strconv.Itoa(int(op))) } } if s := string(val.value); s != "" { pe.writeString(s) } } } // Human returns the query as a human readable string. func (q *Query) Human() string { |
︙ | ︙ | |||
167 168 169 170 171 172 173 | d.Print(&env) } for i, term := range q.terms { if i > 0 { env.writeString(" OR ") env.space = false } | | | | 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 | d.Print(&env) } for i, term := range q.terms { if i > 0 { env.writeString(" OR ") env.space = false } for _, name := range slices.Sorted(maps.Keys(term.keys)) { if env.space { env.writeString(" AND ") } env.writeString(name) switch term.keys[name] { case cmpExist: env.writeString(" EXIST") case cmpNotExist: env.writeString(" NOT EXIST") default: env.writeString(" IS SCHRÖDINGER'S CAT") } env.space = true } for _, name := range slices.Sorted(maps.Keys(term.mvals)) { if env.space { env.writeString(" AND ") } env.writeString(name) env.printHumanSelectExprValues(term.mvals[name]) env.space = true } |
︙ | ︙ | |||
252 253 254 255 256 257 258 | pe.writeString(" NOT GREATER ") default: pe.writeString(" MaTcH ") } if val.value == "" { pe.writeString("NOTHING") } else { | | | 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | pe.writeString(" NOT GREATER ") default: pe.writeString(" MaTcH ") } if val.value == "" { pe.writeString("NOTHING") } else { pe.writeString(string(val.value)) } } } func (pe *PrintEnv) printOrder(order []sortOrder) { for _, o := range order { if o.isRandom() { |
︙ | ︙ |
Changes to query/query.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 | //----------------------------------------------------------------------------- // Package query provides a query for zettel. package query import ( "context" "math/rand/v2" "slices" | > | > | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | //----------------------------------------------------------------------------- // Package query provides a query for zettel. package query import ( "context" "maps" "math/rand/v2" "slices" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" ) // Searcher is used to select zettel identifier based on search criteria. type Searcher interface { // Select all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. SearchEqual(word string) *idset.Set // Select all zettel that have a word with the given prefix. // The prefix must be normalized through Unicode NKFD, trimmed and not empty. SearchPrefix(prefix string) *idset.Set // Select all zettel that have a word with the given suffix. // The suffix must be normalized through Unicode NKFD, trimmed and not empty. SearchSuffix(suffix string) *idset.Set // Select all zettel that contains the given string. // The string must be normalized through Unicode NKFD, trimmed and not empty. SearchContains(s string) *idset.Set } // Query specifies a mechanism for querying zettel. type Query struct { // Präfixed zettel identifier. zids []id.Zid |
︙ | ︙ | |||
69 70 71 72 73 74 75 | } // GetZids returns a slide of all specified zettel identifier. func (q *Query) GetZids() []id.Zid { if q == nil || len(q.zids) == 0 { return nil } | < < | | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | } // GetZids returns a slide of all specified zettel identifier. func (q *Query) GetZids() []id.Zid { if q == nil || len(q.zids) == 0 { return nil } return slices.Clone(q.zids) } // Directive are executed to process the list of metadata. type Directive interface { Print(*PrintEnv) } |
︙ | ︙ | |||
138 139 140 141 142 143 144 | // Clone the query value. func (q *Query) Clone() *Query { if q == nil { return nil } c := new(Query) | < | < < | < < < < < < | < < < < < | < < < | < | < < | 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 | // Clone the query value. func (q *Query) Clone() *Query { if q == nil { return nil } c := new(Query) c.zids = q.GetZids() c.directives = q.GetDirectives() c.preMatch = q.preMatch c.terms = make([]conjTerms, len(q.terms)) for i, term := range q.terms { c.terms[i].keys = maps.Clone(term.keys) c.terms[i].mvals = maps.Clone(term.mvals) c.terms[i].search = slices.Clone(term.search) } c.seed = q.seed c.pick = q.pick c.order = slices.Clone(q.order) c.offset = q.offset c.limit = q.limit c.actions = q.actions return c } type compareOp uint8 |
︙ | ︙ | |||
234 235 236 237 238 239 240 | cmpNoLess: true, cmpNoGreater: true, } func (op compareOp) isNegated() bool { return negativeMap[op] } type expValue struct { | | | 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | cmpNoLess: true, cmpNoGreater: true, } func (op compareOp) isNegated() bool { return negativeMap[op] } type expValue struct { value meta.Value op compareOp } func (q *Query) addSearch(val expValue) { q.terms[len(q.terms)-1].addSearch(val) } func (q *Query) addKey(key string, op compareOp) *Query { q = createIfNeeded(q) |
︙ | ︙ | |||
256 257 258 259 260 261 262 | cmpHasNot: true, cmpNoMatch: true, } // GetMetaValues returns the slice of all values specified for a given metadata key. // If `withMissing` is true, all values are returned. Otherwise only those, // where the comparison operator will positively search for a value. | | | 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | cmpHasNot: true, cmpNoMatch: true, } // GetMetaValues returns the slice of all values specified for a given metadata key. // If `withMissing` is true, all values are returned. Otherwise only those, // where the comparison operator will positively search for a value. func (q *Query) GetMetaValues(key string, withMissing bool) (vals []meta.Value) { if q == nil { return nil } for _, term := range q.terms { if mvs, hasMv := term.mvals[key]; hasMv { for _, ev := range mvs { if withMissing || !missingMap[ev.op] { |
︙ | ︙ | |||
332 333 334 335 336 337 338 | if q == nil { return false } if len(q.zids) > 0 { return true } if len(q.actions) > 0 { | | | 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 | if q == nil { return false } if len(q.zids) > 0 { return true } if len(q.actions) > 0 { // Unknown, what an action may use. For examples: KEYS action uses all metadata. return true } for _, term := range q.terms { for key := range term.keys { if meta.IsProperty(key) { return true } |
︙ | ︙ | |||
407 408 409 410 411 412 413 | cTerm.Match = matchAlways } result.Terms = append(result.Terms, cTerm) } return result } | | | | | | 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 | cTerm.Match = matchAlways } result.Terms = append(result.Terms, cTerm) } return result } func metaList2idSet(ml []*meta.Meta) *idset.Set { if ml == nil { return nil } result := idset.NewCap(len(ml)) for _, m := range ml { result = result.Add(m.Zid) } return result } func (ct *conjTerms) retrieveAndCompileTerm(searcher Searcher, startSet *idset.Set) CompiledTerm { match := ct.compileMeta() // Match might add some searches var pred RetrievePredicate if searcher != nil { pred = ct.retrieveIndex(searcher) if startSet != nil { if pred == nil { pred = startSet.ContainsOrNil } else { predSet := idset.NewCap(startSet.Length()) startSet.ForEach(func(zid id.Zid) { if pred(zid) { predSet = predSet.Add(zid) } }) pred = predSet.ContainsOrNil } |
︙ | ︙ |
Changes to query/retrieve.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | // This file contains helper functions to search within the index. import ( "fmt" "strings" | | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | // This file contains helper functions to search within the index. import ( "fmt" "strings" "t73f.de/r/zsc/domain/id/idset" "zettelstore.de/z/strfun" ) type searchOp struct { s string op compareOp } type searchFunc func(string) *idset.Set type searchCallMap map[searchOp]searchFunc var cmpPred = map[compareOp]func(string, string) bool{ cmpEqual: stringEqual, cmpPrefix: strings.HasPrefix, cmpSuffix: strings.HasSuffix, cmpMatch: strings.Contains, |
︙ | ︙ | |||
65 66 67 68 69 70 71 | scm[searchOp{s: s, op: op}] = sf } func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) { normCalls = make(searchCallMap, len(search)) negCalls = make(searchCallMap, len(search)) for _, val := range search { | | | | | | | | | | < < < | < | | < | < | > | | | 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 | scm[searchOp{s: s, op: op}] = sf } func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) { normCalls = make(searchCallMap, len(search)) negCalls = make(searchCallMap, len(search)) for _, val := range search { for _, word := range strfun.NormalizeWords(string(val.value)) { if cmpOp := val.op; cmpOp.isNegated() { cmpOp = cmpOp.negate() negCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) } else { normCalls.addSearch(word, cmpOp, getSearchFunc(searcher, cmpOp)) } } } plainCalls = make(searchCallMap, len(search)) for _, val := range search { word := val.value.TrimSpace().ToLower() if cmpOp := val.op; cmpOp.isNegated() { cmpOp = cmpOp.negate() negCalls.addSearch(string(word), cmpOp, getSearchFunc(searcher, cmpOp)) } else { plainCalls.addSearch(string(word), cmpOp, getSearchFunc(searcher, cmpOp)) } } return normCalls, plainCalls, negCalls } func hasConflictingCalls(normCalls, plainCalls, negCalls searchCallMap) bool { for val := range negCalls { if _, found := normCalls[val]; found { return true } if _, found := plainCalls[val]; found { return true } } return false } func retrievePositives(normCalls, plainCalls searchCallMap) *idset.Set { if isSuperset(normCalls, plainCalls) { var normResult *idset.Set for c, sf := range normCalls { normResult = normResult.IntersectOrSet(sf(c.s)) } return normResult } cache := make(map[searchOp]*idset.Set) var plainResult *idset.Set for c, sf := range plainCalls { result := sf(c.s) if _, found := normCalls[c]; found { cache[c] = result } plainResult = plainResult.IntersectOrSet(result) } var normResult *idset.Set for c, sf := range normCalls { if result, found := cache[c]; found { normResult = normResult.IntersectOrSet(result) } else { normResult = normResult.IntersectOrSet(sf(c.s)) } } return normResult.IUnion(plainResult) } func isSuperset(normCalls, plainCalls searchCallMap) bool { for c := range plainCalls { if _, found := normCalls[c]; !found { return false } } return true } func retrieveNegatives(negCalls searchCallMap) *idset.Set { var negatives *idset.Set for val, sf := range negCalls { negatives = negatives.IUnion(sf(val.s)) } return negatives } func getSearchFunc(searcher Searcher, op compareOp) searchFunc { |
︙ | ︙ |
Changes to query/select.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | package query import ( "fmt" "strconv" "strings" | < < | < < | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package query import ( "fmt" "strconv" "strings" "t73f.de/r/zsc/domain/meta" ) type matchValueFunc func(value meta.Value) bool func matchValueNever(meta.Value) bool { return false } type matchSpec struct { key string match matchValueFunc } // compileMeta calculates a selection func based on the given select criteria. |
︙ | ︙ | |||
120 121 122 123 124 125 126 | return createMatchTimestampFunc(values, addSearch) case meta.TypeNumber: return createMatchNumberFunc(values, addSearch) case meta.TypeTagSet: return createMatchTagSetFunc(values, addSearch) case meta.TypeWord: return createMatchWordFunc(values, addSearch) | < < | | | | | | | | | | | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | | | 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 | return createMatchTimestampFunc(values, addSearch) case meta.TypeNumber: return createMatchNumberFunc(values, addSearch) case meta.TypeTagSet: return createMatchTagSetFunc(values, addSearch) case meta.TypeWord: return createMatchWordFunc(values, addSearch) } return createMatchStringFunc(values, addSearch) } func createMatchIDFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToIDPredicates(values, addSearch) return func(value meta.Value) bool { for _, pred := range preds { if !pred(value) { return false } } return true } } func createMatchIDSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { predList := valuesToSetPredicates(preprocessSet(values), addSearch) return func(value meta.Value) bool { ids := value.AsSlice() for _, preds := range predList { for _, pred := range preds { if !pred(ids) { return false } } } return true } } func createMatchTimestampFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToTimestampPredicates(values, addSearch) return func(value meta.Value) bool { value = meta.Value(meta.ExpandTimestamp(value)) for _, pred := range preds { if !pred(value) { return false } } return true } } func createMatchNumberFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToNumberPredicates(values, addSearch) return func(value meta.Value) bool { for _, pred := range preds { if !pred(value) { return false } } return true } } func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { predList := valuesToSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch) return func(value meta.Value) bool { tags := value.AsTags() for _, preds := range predList { for _, pred := range preds { if !pred(tags) { return false } } } return true } } func processTagSet(valueSet [][]expValue) [][]expValue { result := make([][]expValue, len(valueSet)) for i, values := range valueSet { tags := make([]expValue, len(values)) for j, val := range values { if tval := val.value; tval != "" && tval[0] == '#' { tval = tval.CleanTag() tags[j] = expValue{value: tval, op: val.op} } else { tags[j] = expValue{value: tval, op: val.op} } } result[i] = tags } return result } func createMatchWordFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToWordPredicates(sliceToLower(values), addSearch) return func(value meta.Value) bool { value = meta.Value(strings.ToLower(string(value))) for _, pred := range preds { if !pred(value) { return false } } return true } } func createMatchStringFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { preds := valuesToStringPredicates(sliceToLower(values), addSearch) return func(value meta.Value) bool { value = meta.Value(strings.ToLower(string(value))) for _, pred := range preds { if !pred(value) { return false } } return true } } func sliceToLower(sl []expValue) []expValue { result := make([]expValue, 0, len(sl)) for _, s := range sl { result = append(result, expValue{ value: meta.Value(strings.ToLower(string(s.value))), op: s.op, }) } return result } func preprocessSet(set []expValue) [][]expValue { result := make([][]expValue, 0, len(set)) for _, elem := range set { splitElems := strings.Split(string(elem.value), ",") valueElems := make([]expValue, 0, len(splitElems)) for _, se := range splitElems { e := strings.TrimSpace(se) if len(e) > 0 { valueElems = append(valueElems, expValue{value: meta.Value(e), op: elem.op}) } } if len(valueElems) > 0 { result = append(result, valueElems) } } return result } type stringPredicate func(meta.Value) bool func valuesToIDPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { result := make([]stringPredicate, len(values)) for i, v := range values { value := v.value if len(value) > 14 { value = value[:14] } switch op := disambiguatedIDOp(v.op); op { case cmpLess, cmpNoLess, cmpGreater, cmpNoGreater: if isDigits(string(value)) { // Never add the strValue to search. // Append enough zeroes to make it comparable as string. // (an ID and a timestamp always have 14 digits) strValue := string(value) + "00000000000000"[:14-len(value)] result[i] = createIDCompareFunc(meta.Value(strValue), op) continue } fallthrough default: // Otherwise compare as a word. if !op.isNegated() { addSearch(v) // addSearch only for positive selections |
︙ | ︙ | |||
378 379 380 381 382 383 384 | } } return true } func disambiguatedIDOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) } | | | | | | | 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 | } } return true } func disambiguatedIDOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) } func createIDCompareFunc(cmpVal meta.Value, cmpOp compareOp) stringPredicate { return createWordCompareFunc(cmpVal, cmpOp) } func valuesToTimestampPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { result := make([]stringPredicate, len(values)) for i, v := range values { value := meta.ExpandTimestamp(v.value) switch op := disambiguatedTimestampOp(v.op); op { case cmpLess, cmpNoLess, cmpGreater, cmpNoGreater: if isDigits(value) { // Never add the value to search. result[i] = createTimestampCompareFunc(meta.Value(value), op) continue } fallthrough default: // Otherwise compare as a word. if !op.isNegated() { addSearch(v) // addSearch only for positive selections } result[i] = createWordCompareFunc(meta.Value(value), op) } } return result } func disambiguatedTimestampOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) } func createTimestampCompareFunc(cmpVal meta.Value, cmpOp compareOp) stringPredicate { return createWordCompareFunc(cmpVal, cmpOp) } func valuesToNumberPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { result := make([]stringPredicate, len(values)) for i, v := range values { switch op := disambiguatedNumberOp(v.op); op { case cmpEqual, cmpNotEqual, cmpLess, cmpNoLess, cmpGreater, cmpNoGreater: iValue, err := strconv.ParseInt(string(v.value), 10, 64) if err == nil { // Never add the strValue to search. result[i] = createNumberCompareFunc(iValue, op) continue } fallthrough default: |
︙ | ︙ | |||
454 455 456 457 458 459 460 | case cmpGreater: cmpFunc = func(iMetaVal int64) bool { return iMetaVal > cmpVal } case cmpNoGreater: cmpFunc = func(iMetaVal int64) bool { return iMetaVal <= cmpVal } default: panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal)) } | | | | 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | case cmpGreater: cmpFunc = func(iMetaVal int64) bool { return iMetaVal > cmpVal } case cmpNoGreater: cmpFunc = func(iMetaVal int64) bool { return iMetaVal <= cmpVal } default: panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal)) } return func(metaVal meta.Value) bool { iMetaVal, err := strconv.ParseInt(string(metaVal), 10, 64) if err != nil { return false } return cmpFunc(iMetaVal) } } |
︙ | ︙ | |||
486 487 488 489 490 491 492 | case cmpHasNot: return cmpNoMatch default: return cmpOp } } | | | 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 | case cmpHasNot: return cmpNoMatch default: return cmpOp } } func createStringCompareFunc(cmpVal meta.Value, cmpOp compareOp) stringPredicate { return createWordCompareFunc(cmpVal, cmpOp) } func valuesToWordPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { result := make([]stringPredicate, len(values)) for i, v := range values { op := disambiguateWordOp(v.op) |
︙ | ︙ | |||
513 514 515 516 517 518 519 | case cmpHasNot: return cmpNotEqual default: return cmpOp } } | | | | | | | | | | | | | | | 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 | case cmpHasNot: return cmpNotEqual default: return cmpOp } } func createWordCompareFunc(cmpVal meta.Value, cmpOp compareOp) stringPredicate { switch cmpOp { case cmpEqual: return func(metaVal meta.Value) bool { return metaVal == cmpVal } case cmpNotEqual: return func(metaVal meta.Value) bool { return metaVal != cmpVal } case cmpPrefix: return func(metaVal meta.Value) bool { return strings.HasPrefix(string(metaVal), string(cmpVal)) } case cmpNoPrefix: return func(metaVal meta.Value) bool { return !strings.HasPrefix(string(metaVal), string(cmpVal)) } case cmpSuffix: return func(metaVal meta.Value) bool { return strings.HasSuffix(string(metaVal), string(cmpVal)) } case cmpNoSuffix: return func(metaVal meta.Value) bool { return !strings.HasSuffix(string(metaVal), string(cmpVal)) } case cmpMatch: return func(metaVal meta.Value) bool { return strings.Contains(string(metaVal), string(cmpVal)) } case cmpNoMatch: return func(metaVal meta.Value) bool { return !strings.Contains(string(metaVal), string(cmpVal)) } case cmpLess: return func(metaVal meta.Value) bool { return metaVal < cmpVal } case cmpNoLess: return func(metaVal meta.Value) bool { return metaVal >= cmpVal } case cmpGreater: return func(metaVal meta.Value) bool { return metaVal > cmpVal } case cmpNoGreater: return func(metaVal meta.Value) bool { return metaVal <= cmpVal } case cmpHas, cmpHasNot: panic(fmt.Sprintf("operator %d not disambiguated with value %q", cmpOp, cmpVal)) default: panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal)) } } |
︙ | ︙ | |||
596 597 598 599 600 601 602 | func stringEqual(val1, val2 string) bool { return val1 == val2 } func stringLess(val1, val2 string) bool { return val1 < val2 } func stringGreater(val1, val2 string) bool { return val1 > val2 } type compareStringFunc func(val1, val2 string) bool | | | | 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 | func stringEqual(val1, val2 string) bool { return val1 == val2 } func stringLess(val1, val2 string) bool { return val1 < val2 } func stringGreater(val1, val2 string) bool { return val1 > val2 } type compareStringFunc func(val1, val2 string) bool func makeStringSetPredicate(neededValue meta.Value, compare compareStringFunc, foundResult bool) stringSetPredicate { return func(metaVals []string) bool { for _, metaVal := range metaVals { if compare(metaVal, string(neededValue)) { return foundResult } } return !foundResult } } |
︙ | ︙ |
Changes to query/select_test.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | package query_test import ( "context" "testing" "t73f.de/r/zsc/api" | | | | > | | | | | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | package query_test import ( "context" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/query" ) func TestMatchZidNegate(t *testing.T) { q := query.Parse(meta.KeyID + api.SearchOperatorHasNot + id.ZidVersion.String() + " " + meta.KeyID + api.SearchOperatorHasNot + id.ZidLicense.String()) compiled := q.RetrieveAndCompile(context.Background(), nil, nil) testCases := []struct { zid id.Zid exp bool }{ {id.ZidVersion, false}, {id.ZidLicense, false}, {id.ZidAuthors, true}, } for i, tc := range testCases { m := meta.New(tc.zid) if compiled.Terms[0].Match(m) != tc.exp { if tc.exp { t.Errorf("%d: meta %v must match %q", i, m.Zid, q) } else { t.Errorf("%d: meta %v must not match %q", i, m.Zid, q) } } |
︙ | ︙ |
Changes to query/sorter.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | package query import ( "cmp" "strconv" | | < | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | package query import ( "cmp" "strconv" "t73f.de/r/zsc/domain/meta" ) type sortFunc func(i, j *meta.Meta) int func buildSortFunc(order []sortOrder) sortFunc { hasID := false sortFuncs := make([]sortFunc, 0, len(order)+1) for _, o := range order { sortFuncs = append(sortFuncs, o.buildSortfunc()) if o.key == meta.KeyID { hasID = true break } } if !hasID { sortFuncs = append(sortFuncs, defaultMetaSort) } |
︙ | ︙ | |||
48 49 50 51 52 53 54 | return 0 } } func (so *sortOrder) buildSortfunc() sortFunc { key := so.key keyType := meta.Type(key) | | | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | return 0 } } func (so *sortOrder) buildSortfunc() sortFunc { key := so.key keyType := meta.Type(key) if key == meta.KeyID || keyType == meta.TypeCredential { if so.descending { return defaultMetaSort } return func(i, j *meta.Meta) int { return cmp.Compare(i.Zid, j.Zid) } } if keyType == meta.TypeTimestamp { return createSortTimestampFunc(key, so.descending) |
︙ | ︙ | |||
109 110 111 112 113 114 115 | } return cmp.Compare(iVal, jVal) } } func getNum(m *meta.Meta, key string) (int64, bool) { if s, ok := m.Get(key); ok { | | | 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | } return cmp.Compare(iVal, jVal) } } func getNum(m *meta.Meta, key string) (int64, bool) { if s, ok := m.Get(key); ok { if i, err := strconv.ParseInt(string(s), 10, 64); err == nil { return i, true } } return 0, false } func createSortStringFunc(key string, descending bool) sortFunc { |
︙ | ︙ |
Changes to query/unlinked.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "t73f.de/r/zsc/api" | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- package query import ( "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/strfun" ) // UnlinkedSpec contains all specification values to calculate unlinked references. type UnlinkedSpec struct { words []string } |
︙ | ︙ | |||
38 39 40 41 42 43 44 | if words := spec.words; len(words) > 0 { result := make([]string, len(words)) copy(result, words) return result } result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title for _, m := range metaSeq { | | | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | if words := spec.words; len(words) > 0 { result := make([]string, len(words)) copy(result, words) return result } result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title for _, m := range metaSeq { title, hasTitle := m.Get(meta.KeyTitle) if !hasTitle { continue } result = append(result, strfun.MakeWords(string(title))...) } return result } |
Deleted strfun/set.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to tests/client/client_test.go.
︙ | ︙ | |||
23 24 25 26 27 28 29 30 31 32 | "net/url" "slices" "strconv" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/client" "zettelstore.de/z/kernel" ) | > > | < < < < < < | | | | | | | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | "net/url" "slices" "strconv" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/client" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/kernel" ) func nextZid(zid id.Zid) id.Zid { return zid + 1 } func TestNextZid(t *testing.T) { testCases := []struct { zid, exp id.Zid }{ {1, 2}, } for i, tc := range testCases { if got := nextZid(tc.zid); got != tc.exp { t.Errorf("%d: zid=%q, exp=%q, got=%q", i, tc.zid, tc.exp, got) } } } func TestListZettel(t *testing.T) { const ( ownerZettel = 58 configRoleZettel = 36 writerZettel = ownerZettel - 25 readerZettel = ownerZettel - 25 creatorZettel = 11 publicZettel = 6 ) testdata := []struct { user string exp int }{ {"", publicZettel}, |
︙ | ︙ | |||
91 92 93 94 95 96 97 | } got := len(l) if got != tc.exp { tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l) } }) } | | | 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | } got := len(l) if got != tc.exp { tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l) } }) } search := meta.KeyRole + api.SearchOperatorHas + meta.ValueRoleConfiguration + " ORDER id" q, h, l, err := c.QueryZettelData(context.Background(), search) if err != nil { t.Error(err) return } expQ := "role:configuration ORDER id" if q != expQ { |
︙ | ︙ | |||
124 125 126 127 128 129 130 | func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaRights) { t.Helper() if len(pl) != len(l) { t.Errorf("Different list lenght: Plain=%d, Data=%d", len(pl), len(l)) } else { for i, line := range pl { | > | | | | 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 | func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaRights) { t.Helper() if len(pl) != len(l) { t.Errorf("Different list lenght: Plain=%d, Data=%d", len(pl), len(l)) } else { for i, line := range pl { got, err := id.Parse(string(line[:14])) if err == nil && got != l[i].ID { t.Errorf("%d: Data=%q, got=%q", i, l[i].ID, got) } } } } func TestGetZettelData(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") z, err := c.GetZettelData(context.Background(), id.ZidDefaultHome) if err != nil { t.Error(err) return } if m := z.Meta; len(m) == 0 { t.Errorf("Exptected non-empty meta, but got %v", z.Meta) } if z.Content == "" || z.Encoding != "" { t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding) } mr, err := c.GetMetaData(context.Background(), id.ZidDefaultHome) if err != nil { t.Error(err) return } if mr.Rights == api.ZettelCanNone { t.Error("rights must be greater zero") } |
︙ | ︙ | |||
181 182 183 184 185 186 187 | c.SetAuth("owner", "owner") encodings := []api.EncodingEnum{ api.EncoderHTML, api.EncoderSz, api.EncoderText, } for _, enc := range encodings { | | | | | | | | | | | | | | | | | | | | > | | | 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 | c.SetAuth("owner", "owner") encodings := []api.EncodingEnum{ api.EncoderHTML, api.EncoderSz, api.EncoderText, } for _, enc := range encodings { content, err := c.GetParsedZettel(context.Background(), id.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(), id.ZidDefaultHome, enc) if err != nil { t.Error(err) continue } if len(content) == 0 { t.Errorf("Empty content for evaluated encoding %v", enc) } } } func checkListZid(t *testing.T, l []api.ZidMetaRights, pos int, expected id.Zid) { t.Helper() if got := l[pos].ID; got != expected { t.Errorf("Expected result[%d]=%v, but got %v", pos, expected, got) } } func TestGetZettelOrder(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") _, _, metaSeq, err := c.QueryZettelData(context.Background(), id.ZidTOCNewTemplate.String()+" "+api.ItemsDirective) if err != nil { t.Error(err) return } if got := len(metaSeq); got != 4 { t.Errorf("Expected list of length 4, got %d", got) return } checkListZid(t, metaSeq, 0, id.ZidTemplateNewZettel) checkListZid(t, metaSeq, 1, id.ZidTemplateNewRole) checkListZid(t, metaSeq, 2, id.ZidTemplateNewTag) checkListZid(t, metaSeq, 3, id.ZidTemplateNewUser) } func TestGetZettelContext(t *testing.T) { const ( allUserZid = id.Zid(20211019200500) ownerZid = id.Zid(20210629163300) writerZid = id.Zid(20210629165000) readerZid = id.Zid(20210629165024) creatorZid = id.Zid(20210629165050) limitAll = 3 ) t.Parallel() c := getClient() c.SetAuth("owner", "owner") rl, err := c.QueryZettel(context.Background(), ownerZid.String()+" CONTEXT LIMIT "+strconv.Itoa(limitAll)) if err != nil { t.Error(err) return } checkZidList(t, []id.Zid{ownerZid, allUserZid, writerZid}, rl) rl, err = c.QueryZettel(context.Background(), ownerZid.String()+" CONTEXT BACKWARD") if err != nil { t.Error(err) return } checkZidList(t, []id.Zid{ownerZid, allUserZid}, rl) } func checkZidList(t *testing.T, exp []id.Zid, got [][]byte) { t.Helper() if len(exp) != len(got) { t.Errorf("expected a list fo length %d, but got %d", len(exp), len(got)) return } for i, expZid := range exp { gotZid, err := id.Parse(string(got[i][:14])) if err != nil || expZid != gotZid { t.Errorf("lists differ at pos %d: expected id %v, but got %v", i, expZid, gotZid) } } } func TestGetUnlinkedReferences(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") _, _, metaSeq, err := c.QueryZettelData(context.Background(), id.ZidDefaultHome.String()+" "+api.UnlinkedDirective) if err != nil { t.Error(err) return } if got := len(metaSeq); got != 1 { t.Errorf("Expected list of length 1, got %d:\n%v", got, metaSeq) return |
︙ | ︙ | |||
320 321 322 323 324 325 326 | } } func TestListTags(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") | | | 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 | } } func TestListTags(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+meta.KeyTags) if err != nil { t.Error(err) return } tags := []struct { key string size int |
︙ | ︙ | |||
359 360 361 362 363 364 365 | c := getClient() c.AllowRedirect(true) c.SetAuth("owner", "owner") ctx := context.Background() zid, err := c.TagZettel(ctx, "nosuchtag") if err != nil { t.Error(err) | | | | | 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 | c := getClient() c.AllowRedirect(true) c.SetAuth("owner", "owner") ctx := context.Background() zid, err := c.TagZettel(ctx, "nosuchtag") if err != nil { t.Error(err) } else if zid != id.Invalid { t.Errorf("no zid expected, but got %q", zid) } zid, err = c.TagZettel(ctx, "#test") exp := id.Zid(20230929102100) if err != nil { t.Error(err) } else if zid != exp { t.Errorf("tag zettel for #test should be %q, but got %q", exp, zid) } } func TestListRoles(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+meta.KeyRole) if err != nil { t.Error(err) return } exp := []string{"configuration", "role", "user", "tag", "zettel"} if len(agg) != len(exp) { t.Errorf("Expected %d different roles, but got %d (%v)", len(exp), len(agg), agg) |
︙ | ︙ | |||
400 401 402 403 404 405 406 | c := getClient() c.AllowRedirect(true) c.SetAuth("owner", "owner") ctx := context.Background() zid, err := c.RoleZettel(ctx, "nosuchrole") if err != nil { t.Error("AAA", err) | | | | | | 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 | c := getClient() c.AllowRedirect(true) c.SetAuth("owner", "owner") ctx := context.Background() zid, err := c.RoleZettel(ctx, "nosuchrole") if err != nil { t.Error("AAA", err) } else if zid != id.Invalid { t.Errorf("no zid expected, but got %q", zid) } zid, err = c.RoleZettel(ctx, "zettel") exp := id.Zid(60010) if err != nil { t.Error(err) } else if zid != exp { t.Errorf("role zettel for zettel should be %q, but got %q", exp, zid) } } func TestRedirect(t *testing.T) { t.Parallel() c := getClient() search := "emoji" + api.ActionSeparator + api.RedirectAction ub := c.NewURLBuilder('z').AppendQuery(search) respRedirect, err := http.Get(ub.String()) if err != nil { t.Error(err) return } defer respRedirect.Body.Close() bodyRedirect, err := io.ReadAll(respRedirect.Body) if err != nil { t.Error(err) return } ub.ClearQuery().SetZid(id.ZidEmoji) respEmoji, err := http.Get(ub.String()) if err != nil { t.Error(err) return } defer respEmoji.Body.Close() bodyEmoji, err := io.ReadAll(respEmoji.Body) |
︙ | ︙ | |||
468 469 470 471 472 473 474 | c := getClient() c.SetAuth("reader", "reader") zid, err := c.GetApplicationZid(context.Background(), "app") if err != nil { t.Error(err) return } | | | | 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 | c := getClient() c.SetAuth("reader", "reader") zid, err := c.GetApplicationZid(context.Background(), "app") if err != nil { t.Error(err) return } if zid != id.ZidAppDirectory { t.Errorf("c.GetApplicationZid(\"app\") should result in %q, but got: %q", id.ZidAppDirectory, zid) } if zid, err = c.GetApplicationZid(context.Background(), "noappzid"); err == nil { t.Errorf(`c.GetApplicationZid("nozid") should result in error, but got: %v`, zid) } if zid, err = c.GetApplicationZid(context.Background(), "nozid"); err == nil { t.Errorf(`c.GetApplicationZid("nozid") should result in error, but got: %v`, zid) } |
︙ | ︙ |
Changes to tests/client/crud_test.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import ( "context" "strings" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/client" ) // --------------------------------------------------------------------------- // Tests that change the Zettelstore must nor run parallel to other tests. func TestCreateGetDeleteZettel(t *testing.T) { // Is not to be allowed to run in parallel with other tests. | > > | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import ( "context" "strings" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/client" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" ) // --------------------------------------------------------------------------- // Tests that change the Zettelstore must nor run parallel to other tests. func TestCreateGetDeleteZettel(t *testing.T) { // Is not to be allowed to run in parallel with other tests. |
︙ | ︙ | |||
81 82 83 84 85 86 87 | func TestCreateGetDeleteZettelData(t *testing.T) { // Is not to be allowed to run in parallel with other tests. c := getClient() c.SetAuth("owner", "owner") wrongModified := "19691231115959" zid, err := c.CreateZettelData(context.Background(), api.ZettelData{ Meta: api.ZettelMeta{ | | | | | | | | | | | | | | | | | | | | 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 | func TestCreateGetDeleteZettelData(t *testing.T) { // Is not to be allowed to run in parallel with other tests. c := getClient() c.SetAuth("owner", "owner") wrongModified := "19691231115959" zid, err := c.CreateZettelData(context.Background(), api.ZettelData{ Meta: api.ZettelMeta{ meta.KeyTitle: "A\nTitle", // \n must be converted into a space meta.KeyModified: wrongModified, }, }) if err != nil { t.Error("Cannot create zettel:", err) return } z, err := c.GetZettelData(context.Background(), zid) if err != nil { t.Error("Cannot get zettel:", zid, err) } else { exp := "A Title" if got := z.Meta[meta.KeyTitle]; got != exp { t.Errorf("Expected title %q, but got %q", exp, got) } if got := z.Meta[meta.KeyModified]; got != "" { t.Errorf("Create allowed to set the modified key: %q", got) } } doDelete(t, c, zid) } func TestUpdateZettel(t *testing.T) { c := getClient() c.SetAuth("owner", "owner") z, err := c.GetZettel(context.Background(), id.ZidDefaultHome, api.PartZettel) if err != nil { t.Error(err) return } if !strings.HasPrefix(string(z), "title: Home\n") { t.Error("Got unexpected zettel", z) return } newZettel := `title: Empty Home role: zettel syntax: zmk Empty` err = c.UpdateZettel(context.Background(), id.ZidDefaultHome, []byte(newZettel)) if err != nil { t.Error(err) return } zt, err := c.GetZettel(context.Background(), id.ZidDefaultHome, api.PartZettel) if err != nil { t.Error(err) return } if string(zt) != newZettel { t.Errorf("Expected zettel %q, got %q", newZettel, zt) } // Must delete to clean up for next tests doDelete(t, c, id.ZidDefaultHome) } func TestUpdateZettelData(t *testing.T) { c := getClient() c.SetAuth("writer", "writer") z, err := c.GetZettelData(context.Background(), id.ZidDefaultHome) if err != nil { t.Error(err) return } if got := z.Meta[meta.KeyTitle]; got != "Home" { t.Errorf("Title of zettel is not \"Home\", but %q", got) return } newTitle := "New Home" z.Meta[meta.KeyTitle] = newTitle wrongModified := "19691231235959" z.Meta[meta.KeyModified] = wrongModified err = c.UpdateZettelData(context.Background(), id.ZidDefaultHome, z) if err != nil { t.Error(err) return } zt, err := c.GetZettelData(context.Background(), id.ZidDefaultHome) if err != nil { t.Error(err) return } if got := zt.Meta[meta.KeyTitle]; got != newTitle { t.Errorf("Title of zettel is not %q, but %q", newTitle, got) } if got := zt.Meta[meta.KeyModified]; got == wrongModified { t.Errorf("Update did not change the modified key: %q", got) } // Must delete to clean up for next tests c.SetAuth("owner", "owner") doDelete(t, c, id.ZidDefaultHome) } func doDelete(t *testing.T, c *client.Client, zid id.Zid) { err := c.DeleteZettel(context.Background(), zid) if err != nil { t.Helper() t.Error("Cannot delete", zid, ":", err) } } |
Changes to tests/client/embed_test.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 22 23 24 | import ( "context" "strings" "testing" "t73f.de/r/zsc/api" ) const ( | > | | | | | | | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import ( "context" "strings" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" ) const ( abcZid = id.Zid(20211020121000) abc10Zid = id.Zid(20211020121100) ) func TestZettelTransclusion(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") const abc10000Zid = id.Zid(20211020121400) contentMap := map[id.Zid]int{ abcZid: 1, abc10Zid: 10, id.Zid(20211020121145): 100, id.Zid(20211020121300): 1000, } content, err := c.GetZettel(context.Background(), abcZid, api.PartContent) if err != nil { t.Error(err) return } baseContent := string(content) |
︙ | ︙ | |||
76 77 78 79 80 81 82 | } func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("reader", "reader") | | | 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | } func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("reader", "reader") zettelData, err := c.GetZettelData(context.Background(), id.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) |
︙ | ︙ | |||
118 119 120 121 122 123 124 | func TestRecursiveTransclusion(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") const ( | | | | | | | | | | | | 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 | func TestRecursiveTransclusion(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") const ( selfRecursiveZid = id.Zid(20211020182600) indirectRecursive1Zid = id.Zid(20211020183700) indirectRecursive2Zid = id.Zid(20211020183800) ) recursiveZettel := map[id.Zid]id.Zid{ 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 transclusion") checkContentContains(t, zid, sContent, errZid.String()) } } func TestNothingToTransclude(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") const ( transZid = id.Zid(20211020184342) emptyZid = id.Zid(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, emptyZid.String()) } func TestSelfEmbedRef(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") const selfEmbedZid = id.Zid(20211020185400) content, err := c.GetEvaluatedZettel(context.Background(), selfEmbedZid, api.EncoderHTML) if err != nil { t.Error(err) return } checkContentContains(t, selfEmbedZid, string(content), "Self embed reference") } func checkContentContains(t *testing.T, zid id.Zid, 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 25 26 27 28 29 30 31 32 33 34 35 36 37 | "encoding/json" "fmt" "os" "strings" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoder" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/mdenc" _ "zettelstore.de/z/encoder/shtmlenc" _ "zettelstore.de/z/encoder/szenc" _ "zettelstore.de/z/encoder/textenc" _ "zettelstore.de/z/encoder/zmkenc" "zettelstore.de/z/parser" _ "zettelstore.de/z/parser/markdown" _ "zettelstore.de/z/parser/zettelmark" | > < | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | "encoding/json" "fmt" "os" "strings" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoder" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/mdenc" _ "zettelstore.de/z/encoder/shtmlenc" _ "zettelstore.de/z/encoder/szenc" _ "zettelstore.de/z/encoder/textenc" _ "zettelstore.de/z/encoder/zmkenc" "zettelstore.de/z/parser" _ "zettelstore.de/z/parser/markdown" _ "zettelstore.de/z/parser/zettelmark" ) type markdownTestCase struct { Markdown string `json:"markdown"` HTML string `json:"html"` Example int `json:"example"` StartLine int `json:"start_line"` EndLine int `json:"end_line"` Section string `json:"section"` } func TestEncoderAvailability(t *testing.T) { t.Parallel() encoderMissing := false for _, enc := range encodings { enc := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}) if enc == nil { t.Errorf("No encoder for %q found", enc) encoderMissing = true } } if encoderMissing { panic("At least one encoder is missing. See test log") |
︙ | ︙ | |||
77 78 79 80 81 82 83 | ast := createMDBlockSlice(tc.Markdown, config.NoHTML) testAllEncodings(t, tc, &ast) testZmkEncoding(t, tc, &ast) } } func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice { | | | | | | | 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 | ast := createMDBlockSlice(tc.Markdown, config.NoHTML) testAllEncodings(t, tc, &ast) testZmkEncoding(t, tc, &ast) } } func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice { return parser.ParseBlocks(input.NewInput([]byte(markdown)), nil, meta.ValueSyntaxMarkdown, hi) } func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { var sb strings.Builder testID := tc.Example*100 + 1 for _, enc := range encodings { t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(*testing.T) { encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}).WriteBlocks(&sb, ast) sb.Reset() }) } } func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { zmkEncoder := encoder.Create(api.EncoderZmk, nil) var buf bytes.Buffer testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { buf.Reset() zmkEncoder.WriteBlocks(&buf, ast) // gotFirst := buf.String() testID = tc.Example*100 + 2 secondAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML) buf.Reset() zmkEncoder.WriteBlocks(&buf, &secondAst) gotSecond := buf.String() // if gotFirst != gotSecond { // st.Errorf("\nCMD: %q\n1st: %q\n2nd: %q", tc.Markdown, gotFirst, gotSecond) // } testID = tc.Example*100 + 3 thirdAst := parser.ParseBlocks(input.NewInput(buf.Bytes()), nil, meta.ValueSyntaxZmk, config.NoHTML) buf.Reset() zmkEncoder.WriteBlocks(&buf, &thirdAst) gotThird := buf.String() if gotSecond != gotThird { st.Errorf("\n1st: %q\n2nd: %q", gotSecond, gotThird) } }) } func TestAdditionalMarkdown(t *testing.T) { testcases := []struct { md string exp string }{ {`abc<br>def`, "abc``<br>``{=\"html\"}def"}, } zmkEncoder := encoder.Create(api.EncoderZmk, nil) var sb strings.Builder for i, tc := range testcases { ast := createMDBlockSlice(tc.md, config.MarkdownHTML) sb.Reset() zmkEncoder.WriteBlocks(&sb, &ast) got := sb.String() if got != tc.exp { t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got) } } } |
Changes to tests/naughtystrings_test.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | import ( "bufio" "io" "os" "path/filepath" "testing" | | < | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import ( "bufio" "io" "os" "path/filepath" "testing" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" _ "zettelstore.de/z/cmd" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" ) // Test all parser / encoder with a list of "naughty strings", i.e. unusual strings // that often crash software. func getNaughtyStrings() (result []string, err error) { fpath := filepath.Join("..", "testdata", "naughty", "blns.txt") |
︙ | ︙ | |||
55 56 57 58 59 60 61 | } } return result } func getAllEncoder() (result []encoder.Encoder) { for _, enc := range encoder.GetEncodings() { | | | 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | } } return result } func getAllEncoder() (result []encoder.Encoder) { for _, enc := range encoder.GetEncodings() { e := encoder.Create(enc, &encoder.CreateParameter{Lang: meta.ValueLangEN}) result = append(result, e) } return result } func TestNaughtyStringParser(t *testing.T) { blns, err := getNaughtyStrings() |
︙ | ︙ | |||
79 80 81 82 83 84 85 | } encs := getAllEncoder() if len(encs) == 0 { t.Fatal("no encoder found") } for _, s := range blns { for _, pinfo := range pinfos { | | < < < < < | 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | } encs := getAllEncoder() if len(encs) == 0 { t.Fatal("no encoder found") } for _, s := range blns { for _, pinfo := range pinfos { bs := pinfo.Parse(input.NewInput([]byte(s)), &meta.Meta{}, pinfo.Name) for _, enc := range encs { _, err = enc.WriteBlocks(io.Discard, &bs) if err != nil { t.Error(err) } } } } } |
Changes to tests/regression_test.go.
︙ | ︙ | |||
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | "net/url" "os" "path/filepath" "strings" "testing" "t73f.de/r/zsc/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" "zettelstore.de/z/encoder" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/query" | > < | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | "net/url" "os" "path/filepath" "strings" "testing" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" "zettelstore.de/z/encoder" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/query" _ "zettelstore.de/z/box/dirbox" ) var encodings = []api.EncodingEnum{ api.EncoderHTML, api.EncoderSz, |
︙ | ︙ | |||
120 121 122 123 124 125 126 | } return u.Path[len(root):] } func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { t.Helper() | | | | 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | } 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, &encoder.CreateParameter{Lang: meta.ValueLangEN}); enc != nil { var sf strings.Builder enc.WriteMeta(&sf, zn.Meta) checkFileContent(t, resultName, sf.String()) return } panic(fmt.Sprintf("Unknown writer encoding %q", enc)) } func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) { |
︙ | ︙ | |||
166 167 168 169 170 171 172 | func (*myConfig) AddDefaultValues(_ context.Context, m *meta.Meta) *meta.Meta { return m } func (*myConfig) GetHTMLInsecurity() config.HTMLInsecurity { return config.NoHTML } func (*myConfig) GetListPageSize() int { return 0 } func (*myConfig) GetSiteName() string { return "" } func (*myConfig) GetYAMLHeader() bool { return false } | | | 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | func (*myConfig) AddDefaultValues(_ context.Context, m *meta.Meta) *meta.Meta { return m } func (*myConfig) GetHTMLInsecurity() config.HTMLInsecurity { return config.NoHTML } func (*myConfig) GetListPageSize() int { return 0 } func (*myConfig) GetSiteName() string { return "" } func (*myConfig) GetYAMLHeader() bool { return false } func (*myConfig) GetZettelFileSyntax() []meta.Value { 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{} |
︙ | ︙ |
Changes to tools/build/build.go.
︙ | ︙ | |||
19 20 21 22 23 24 25 26 27 28 | "bytes" "flag" "fmt" "io" "io/fs" "os" "path/filepath" "strings" "time" | > | > < < | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | "bytes" "flag" "fmt" "io" "io/fs" "os" "path/filepath" "slices" "strings" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/strfun" "zettelstore.de/z/tools" ) func readVersionFile() (string, error) { content, err := os.ReadFile("VERSION") if err != nil { return "", err } |
︙ | ︙ | |||
213 214 215 216 217 218 219 | data, err := io.ReadAll(manualFile) if err != nil { return err } inp := input.NewInput(data) m := meta.NewFromInput(id.MustParse(versionZid), inp) | | | 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | data, err := io.ReadAll(manualFile) if err != nil { return err } inp := input.NewInput(data) m := meta.NewFromInput(id.MustParse(versionZid), inp) m.SetNow(meta.KeyModified) var buf bytes.Buffer if _, err = fmt.Fprintf(&buf, "id: %s\n", versionZid); err != nil { return err } if _, err = m.WriteComputed(&buf); err != nil { return err |
︙ | ︙ | |||
251 252 253 254 255 256 257 | {"arm", "linux", []string{"GOARM=6"}, "zettelstore"}, {"arm64", "darwin", nil, "zettelstore"}, {"amd64", "darwin", nil, "zettelstore"}, {"amd64", "windows", nil, "zettelstore.exe"}, {"arm64", "android", nil, "zettelstore"}, } for _, rel := range releases { | | | 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | {"arm", "linux", []string{"GOARM=6"}, "zettelstore"}, {"arm64", "darwin", nil, "zettelstore"}, {"amd64", "darwin", nil, "zettelstore"}, {"amd64", "windows", nil, "zettelstore.exe"}, {"arm64", "android", nil, "zettelstore"}, } for _, rel := range releases { env := slices.Clone(rel.env) env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os) env = append(env, tools.EnvDirectProxy...) env = append(env, tools.EnvGoVCS...) zsName := filepath.Join("releases", rel.name) if err := doBuild(env, base, zsName); err != nil { return err } |
︙ | ︙ |
Changes to tools/htmllint/htmllint.go.
︙ | ︙ | |||
24 25 26 27 28 29 30 31 32 33 34 35 36 37 | "os" "regexp" "slices" "strings" "t73f.de/r/zsc/api" "t73f.de/r/zsc/client" "zettelstore.de/z/tools" ) func main() { flag.BoolVar(&tools.Verbose, "v", false, "Verbose output") flag.Parse() | > | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | "os" "regexp" "slices" "strings" "t73f.de/r/zsc/api" "t73f.de/r/zsc/client" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/tools" ) func main() { flag.BoolVar(&tools.Verbose, "v", false, "Verbose output") flag.Parse() |
︙ | ︙ | |||
55 56 57 58 59 60 61 | } zids, perm := calculateZids(metaList) for _, kd := range keyDescr { msgCount := 0 fmt.Fprintf(os.Stderr, "Now checking: %s\n", kd.text) for _, zid := range zidsToUse(zids, perm, kd.sampleSize) { var nmsgs int | | | | | | | | | | | | 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 | } zids, perm := calculateZids(metaList) for _, kd := range keyDescr { msgCount := 0 fmt.Fprintf(os.Stderr, "Now checking: %s\n", kd.text) for _, zid := range zidsToUse(zids, perm, kd.sampleSize) { var nmsgs int nmsgs, err = validateHTML(client, kd.uc, zid) if err != nil { fmt.Fprintf(os.Stderr, "* error while validating zettel %v with: %v\n", zid, err) msgCount++ } else { msgCount += nmsgs } } if msgCount == 1 { fmt.Fprintln(os.Stderr, "==> found 1 possible issue") } else if msgCount > 1 { fmt.Fprintf(os.Stderr, "==> found %v possible issues\n", msgCount) } } return nil } func calculateZids(metaList []api.ZidMetaRights) ([]id.Zid, []int) { zids := make([]id.Zid, len(metaList)) for i, m := range metaList { zids[i] = m.ID } slices.Sort(zids) return zids, rand.Perm(len(metaList)) } func zidsToUse(zids []id.Zid, perm []int, sampleSize int) []id.Zid { if sampleSize < 0 || len(perm) <= sampleSize { return zids } if sampleSize == 0 { return nil } result := make([]id.Zid, sampleSize) for i := range sampleSize { result[i] = zids[perm[i]] } slices.Sort(result) return result } var keyDescr = []struct { uc urlCreator text string sampleSize int }{ {getHTMLZettel, "zettel HTML encoding", -1}, {createJustKey('h'), "zettel web view", -1}, {createJustKey('i'), "zettel info view", -1}, {createJustKey('e'), "zettel edit form", 100}, {createJustKey('c'), "zettel create form", 10}, {createJustKey('d'), "zettel delete dialog", 200}, } type urlCreator func(*client.Client, id.Zid) *api.URLBuilder func createJustKey(key byte) urlCreator { return func(c *client.Client, zid id.Zid) *api.URLBuilder { return c.NewURLBuilder(key).SetZid(zid) } } func getHTMLZettel(client *client.Client, zid id.Zid) *api.URLBuilder { return client.NewURLBuilder('z').SetZid(zid). AppendKVQuery(api.QueryKeyEncoding, api.EncodingHTML). AppendKVQuery(api.QueryKeyPart, api.PartZettel) } func validateHTML(client *client.Client, uc urlCreator, zid id.Zid) (int, error) { ub := uc(client, zid) if tools.Verbose { fmt.Fprintf(os.Stderr, "GET %v\n", ub) } data, err := client.Get(context.Background(), ub) if err != nil { return 0, err |
︙ | ︙ |
Changes to usecase/authenticate.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | import ( "context" "math/rand/v2" "net/http" "time" | | > < < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import ( "context" "math/rand/v2" "net/http" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" "zettelstore.de/z/auth/cred" "zettelstore.de/z/logger" ) // Authenticate is the data for this use case. type Authenticate struct { log *logger.Logger token auth.TokenManager ucGetUser *GetUser |
︙ | ︙ | |||
53 54 55 56 57 58 59 | 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 } | | | | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | if identMeta == nil || err != nil { uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("No user with given ident found") compensateCompare() return nil, err } if hashCred, ok := identMeta.Get(meta.KeyCredential); ok { ok, err = cred.CompareHashAndCredential(string(hashCred), identMeta.Zid, ident, credential) if err != nil { uc.log.Info().Str("ident", ident).Err(err).HTTPIP(r).Msg("Error while comparing credentials") return nil, err } if ok { token, err2 := uc.token.GetToken(identMeta, d, k) if err2 != nil { |
︙ | ︙ |
Changes to usecase/create_zettel.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | package usecase import ( "context" "time" | | > < < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | package usecase import ( "context" "time" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/config" "zettelstore.de/z/logger" "zettelstore.de/z/zettel" ) // CreateZettelPort is the interface used by this use case. type CreateZettelPort interface { // CreateZettel creates a new zettel. CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) } |
︙ | ︙ | |||
47 48 49 50 51 52 53 | } } // PrepareCopy the zettel for further modification. func (*CreateZettel) PrepareCopy(origZettel zettel.Zettel) zettel.Zettel { origMeta := origZettel.Meta m := origMeta.Clone() | | | | | | | | | | | | | | | | | | | | | | | | 46 47 48 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 | } } // PrepareCopy the zettel for further modification. func (*CreateZettel) PrepareCopy(origZettel zettel.Zettel) zettel.Zettel { origMeta := origZettel.Meta m := origMeta.Clone() if title, found := origMeta.Get(meta.KeyTitle); found { m.Set(meta.KeyTitle, prependTitle(title, "Copy", "Copy of ")) } setReadonly(m) content := origZettel.Content content.TrimSpace() return zettel.Zettel{Meta: m, Content: content} } // PrepareVersion the zettel for further modification. func (*CreateZettel) PrepareVersion(origZettel zettel.Zettel) zettel.Zettel { origMeta := origZettel.Meta m := origMeta.Clone() m.Set(meta.KeyPredecessor, meta.Value(origMeta.Zid.String())) setReadonly(m) content := origZettel.Content content.TrimSpace() return zettel.Zettel{Meta: m, Content: content} } // PrepareFolge the zettel for further modification. func (*CreateZettel) PrepareFolge(origZettel zettel.Zettel) zettel.Zettel { origMeta := origZettel.Meta m := meta.New(id.Invalid) if title, found := origMeta.Get(meta.KeyTitle); found { m.Set(meta.KeyTitle, prependTitle(title, "Folge", "Folge of ")) } updateMetaRoleTagsSyntax(m, origMeta) m.Set(meta.KeyPrecursor, meta.Value(origMeta.Zid.String())) return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)} } // PrepareSequel the zettel for further modification. func (*CreateZettel) PrepareSequel(origZettel zettel.Zettel) zettel.Zettel { origMeta := origZettel.Meta m := meta.New(id.Invalid) if title, found := origMeta.Get(meta.KeyTitle); found { m.Set(meta.KeyTitle, prependTitle(title, "Sequel", "Sequel of ")) } updateMetaRoleTagsSyntax(m, origMeta) m.Set(meta.KeyPrequel, meta.Value(origMeta.Zid.String())) return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)} } // PrepareNew the zettel for further modification. func (*CreateZettel) PrepareNew(origZettel zettel.Zettel, newTitle string) zettel.Zettel { m := meta.New(id.Invalid) om := origZettel.Meta m.SetNonEmpty(meta.KeyTitle, om.GetDefault(meta.KeyTitle, "")) updateMetaRoleTagsSyntax(m, om) const prefixLen = len(meta.NewPrefix) for key, val := range om.Rest() { if len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix { m.Set(key[prefixLen:], val) } } if newTitle != "" { m.Set(meta.KeyTitle, meta.Value(newTitle)) } content := origZettel.Content content.TrimSpace() return zettel.Zettel{Meta: m, Content: content} } func updateMetaRoleTagsSyntax(m, orig *meta.Meta) { m.SetNonEmpty(meta.KeyRole, orig.GetDefault(meta.KeyRole, "")) m.SetNonEmpty(meta.KeyTags, orig.GetDefault(meta.KeyTags, "")) m.SetNonEmpty(meta.KeySyntax, orig.GetDefault(meta.KeySyntax, meta.DefaultSyntax)) } func prependTitle(title, s0, s1 meta.Value) meta.Value { if len(title) > 0 { return s1 + title } return s0 } func setReadonly(m *meta.Meta) { if _, found := m.Get(meta.KeyReadOnly); found { // Currently, "false" is a safe value. // // If the current user and its role is known, a more elaborative calculation // could be done: set it to a value, so that the current user will be able // to modify it later. m.Set(meta.KeyReadOnly, meta.ValueFalse) } } // Run executes the use case. func (uc *CreateZettel) Run(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { m := zettel.Meta if m.Zid.IsValid() { return m.Zid, nil // TODO: new error: already exists } m.Set(meta.KeyCreated, meta.Value(time.Now().Local().Format(id.TimestampLayout))) m.Delete(meta.KeyModified) m.YamlSep = uc.rtConfig.GetYAMLHeader() zettel.Content.TrimSpace() zid, err := uc.port.CreateZettel(ctx, zettel) uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Create zettel") return zid, err } |
Changes to usecase/delete_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package usecase import ( "context" | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/logger" ) // DeleteZettelPort is the interface used by this use case. type DeleteZettelPort interface { // DeleteZettel removes the zettel from the box. DeleteZettel(ctx context.Context, zid id.Zid) error } |
︙ | ︙ |
Changes to usecase/evaluate.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 | //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/query" "zettelstore.de/z/zettel" | > > < < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/query" "zettelstore.de/z/zettel" ) // Evaluate is the data for this use case. type Evaluate struct { rtConfig config.Config ucGetZettel *GetZettel ucQuery *Query |
︙ | ︙ | |||
64 65 66 67 68 69 70 | return nil } bns := ast.BlockSlice{bn} evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns) return bns } | < < < < < < < | 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | return nil } bns := ast.BlockSlice{bn} evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns) return bns } // GetZettel retrieves the full zettel of a given zettel identifier. func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { return uc.ucGetZettel.Run(ctx, zid) } // QueryMeta returns a list of metadata that comply to the given selection criteria. func (uc *Evaluate) QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { return uc.ucQuery.Run(ctx, q) } |
Changes to usecase/get_all_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package usecase import ( "context" | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/zettel" ) // GetAllZettelPort is the interface used by this use case. type GetAllZettelPort interface { GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) } |
︙ | ︙ |
Changes to usecase/get_special_zettel.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | package usecase import ( "context" "t73f.de/r/zsc/api" | | | | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package usecase import ( "context" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/query" "zettelstore.de/z/zettel" ) // TagZettel is the usecase of retrieving a "tag zettel", i.e. a zettel that // describes a given tag. A tag zettel must have the tag's name in its title // and must have a role=tag. // TagZettelPort is the interface used by this use case. |
︙ | ︙ | |||
41 42 43 44 45 46 47 | // NewTagZettel creates a new use case. func NewTagZettel(port GetZettelPort, query *Query) TagZettel { return TagZettel{port: port, query: query} } // Run executes the use case. | | | | | | | | 41 42 43 44 45 46 47 48 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 | // NewTagZettel creates a new use case. func NewTagZettel(port GetZettelPort, query *Query) TagZettel { return TagZettel{port: port, query: query} } // Run executes the use case. func (uc TagZettel) Run(ctx context.Context, tag meta.Value) (zettel.Zettel, error) { tag = tag.NormalizeTag() q := query.Parse( meta.KeyTitle + api.SearchOperatorEqual + string(tag) + " " + meta.KeyRole + api.SearchOperatorHas + meta.ValueRoleTag) ml, err := uc.query.Run(ctx, q) if err != nil { return zettel.Zettel{}, err } for _, m := range ml { z, errZ := uc.port.GetZettel(ctx, m.Zid) if errZ == nil { return z, nil } } return zettel.Zettel{}, ErrTagZettelNotFound{Tag: tag} } // ErrTagZettelNotFound is returned if a tag zettel was not found. type ErrTagZettelNotFound struct{ Tag meta.Value } func (etznf ErrTagZettelNotFound) Error() string { return "tag zettel not found: " + string(etznf.Tag) } // RoleZettel is the usecase of retrieving a "role zettel", i.e. a zettel that // describes a given role. A role zettel must have the role's name in its title // and must have a role=role. // RoleZettelPort is the interface used by this use case. type RoleZettelPort interface { |
︙ | ︙ | |||
86 87 88 89 90 91 92 | // NewRoleZettel creates a new use case. func NewRoleZettel(port GetZettelPort, query *Query) RoleZettel { return RoleZettel{port: port, query: query} } // Run executes the use case. | | | | | | > > | 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 | // NewRoleZettel creates a new use case. func NewRoleZettel(port GetZettelPort, query *Query) RoleZettel { return RoleZettel{port: port, query: query} } // Run executes the use case. func (uc RoleZettel) Run(ctx context.Context, role meta.Value) (zettel.Zettel, error) { q := query.Parse( meta.KeyTitle + api.SearchOperatorEqual + string(role) + " " + meta.KeyRole + api.SearchOperatorHas + meta.ValueRoleRole) ml, err := uc.query.Run(ctx, q) if err != nil { return zettel.Zettel{}, err } for _, m := range ml { z, errZ := uc.port.GetZettel(ctx, m.Zid) if errZ == nil { return z, nil } } return zettel.Zettel{}, ErrRoleZettelNotFound{Role: role} } // ErrRoleZettelNotFound is returned if a role zettel was not found. type ErrRoleZettelNotFound struct{ Role meta.Value } func (etznf ErrRoleZettelNotFound) Error() string { return "role zettel not found: " + string(etznf.Role) } |
Changes to usecase/get_user.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 | package usecase import ( "context" "t73f.de/r/zsc/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/query" "zettelstore.de/z/zettel" | > > < < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package usecase import ( "context" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/query" "zettelstore.de/z/zettel" ) // Use case: return user identified by meta key ident. // --------------------------------------------------- // GetUserPort is the interface used by this use case. type GetUserPort interface { |
︙ | ︙ | |||
49 50 51 52 53 54 55 | func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) { ctx = box.NoEnrichContext(ctx) // It is important to try first with the owner. First, because another user // could give herself the same ''ident''. Second, in most cases the owner // will authenticate. identZettel, err := uc.port.GetZettel(ctx, uc.authz.Owner()) | | | | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) { ctx = box.NoEnrichContext(ctx) // It is important to try first with the owner. First, because another user // could give herself the same ''ident''. Second, in most cases the owner // will authenticate. identZettel, err := uc.port.GetZettel(ctx, uc.authz.Owner()) if err == nil && string(identZettel.Meta.GetDefault(meta.KeyUserID, "")) == ident { return identZettel.Meta, nil } // Owner was not found or has another ident. Try via list search. q := query.Parse(meta.KeyUserID + api.SearchOperatorHas + ident + " " + api.SearchOperatorHas + ident) metaList, err := uc.port.SelectMeta(ctx, nil, q) if err != nil { return nil, err } if len(metaList) < 1 { return nil, nil } |
︙ | ︙ | |||
90 91 92 93 94 95 96 | func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { userZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), zid) if err != nil { return nil, err } userMeta := userZettel.Meta | | | 90 91 92 93 94 95 96 97 98 99 100 101 | func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { userZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), zid) if err != nil { return nil, err } userMeta := userZettel.Meta if val, ok := userMeta.Get(meta.KeyUserID); !ok || string(val) != ident { return nil, nil } return userMeta, nil } |
Changes to usecase/get_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package usecase import ( "context" | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/zettel" ) // GetZettelPort is the interface used by this use case. type GetZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) } |
︙ | ︙ |
Changes to usecase/lists.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 | package usecase import ( "context" "t73f.de/r/zsc/api" "zettelstore.de/z/box" "zettelstore.de/z/parser" "zettelstore.de/z/query" | > < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package usecase import ( "context" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/parser" "zettelstore.de/z/query" ) // -------- List syntax ------------------------------------------------------ // ListSyntaxPort is the interface used by this use case. type ListSyntaxPort interface { SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) |
︙ | ︙ | |||
38 39 40 41 42 43 44 | // 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) { | | | | 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | // NewListSyntax creates a new use case. func NewListSyntax(port ListSyntaxPort) ListSyntax { return ListSyntax{port: port} } // Run executes the use case. func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) { q := query.Parse(meta.KeySyntax + api.ExistOperator) // We look for all metadata with a syntax key metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q) if err != nil { return nil, err } result := meta.CreateArrangement(metas, meta.KeySyntax) for _, syn := range parser.GetSyntaxes() { if _, found := result[syn]; !found { delete(result, syn) } } return result, nil } |
︙ | ︙ | |||
71 72 73 74 75 76 77 | // 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) { | | | | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | // NewListRoles creates a new use case. func NewListRoles(port ListRolesPort) ListRoles { return ListRoles{port: port} } // Run executes the use case. func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) { q := query.Parse(meta.KeyRole + api.ExistOperator) // We look for all metadata with an existing role key metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q) if err != nil { return nil, err } return meta.CreateArrangement(metas, meta.KeyRole), nil } |
Changes to usecase/parse_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 | //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/parser" | > < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/parser" ) // ParseZettel is the data for this use case. type ParseZettel struct { rtConfig config.Config getZettel GetZettel } |
︙ | ︙ |
Changes to usecase/query.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | import ( "context" "errors" "fmt" "strings" | | > > < < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import ( "context" "errors" "fmt" "strings" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/parser" "zettelstore.de/z/query" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel" ) // QueryPort is the interface used by this use case. type QueryPort interface { GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) |
︙ | ︙ | |||
116 117 118 119 120 121 122 | func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta { return spec.Execute(ctx, metaSeq, uc.port) } func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta { result := make([]*meta.Meta, 0, len(metaSeq)) for _, m := range metaSeq { | | | > > > > > | 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta { return spec.Execute(ctx, metaSeq, uc.port) } func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta { result := make([]*meta.Meta, 0, len(metaSeq)) for _, m := range metaSeq { zn, err := uc.ucEvaluate.Run(ctx, m.Zid, string(m.GetDefault(meta.KeySyntax, meta.DefaultSyntax))) if err != nil { continue } for _, ln := range collect.Order(zn) { ref := ln.Ref if !ref.IsZettel() { continue } if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil { if z, err3 := uc.port.GetZettel(ctx, collectedZid); err3 == nil { result = append(result, z.Meta) } } } } |
︙ | ︙ | |||
146 147 148 149 150 151 152 | sb.WriteString(word) } q := (*query.Query)(nil).Parse(sb.String()) candidates, err := uc.port.SelectMeta(ctx, nil, q) if err != nil { return nil } | | | | | | | | | < < < < < < < < < < < < < | | | 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 | sb.WriteString(word) } q := (*query.Query)(nil).Parse(sb.String()) candidates, err := uc.port.SelectMeta(ctx, nil, q) if err != nil { return nil } metaZids := idset.NewCap(len(metaSeq)) refZids := idset.NewCap(len(metaSeq) * 4) // Assumption: there are four zids per zettel for _, m := range metaSeq { metaZids.Add(m.Zid) refZids.Add(m.Zid) for key, val := range m.ComputedRest() { switch meta.Type(key) { case meta.TypeID: if zid, errParse := id.Parse(string(val)); errParse == nil { refZids.Add(zid) } case meta.TypeIDSet: for val := range val.Fields() { if zid, errParse := id.Parse(val); errParse == nil { refZids.Add(zid) } } } } } candidates = filterByZid(candidates, refZids) return uc.filterCandidates(ctx, candidates, words) } func filterByZid(candidates []*meta.Meta, ignoreSeq *idset.Set) []*meta.Meta { result := make([]*meta.Meta, 0, len(candidates)) for _, m := range candidates { if !ignoreSeq.Contains(m.Zid) { result = append(result, m) } } return result } func (uc *Query) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta { result := make([]*meta.Meta, 0, len(candidates)) for _, cand := range candidates { zettel, err := uc.port.GetZettel(ctx, cand.Zid) if err != nil { continue } v := unlinkedVisitor{ words: words, found: false, } v.text = v.joinWords(words) syntax := string(zettel.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax)) if !parser.IsASTParser(syntax) { continue } zn := uc.ucEvaluate.RunZettel(ctx, zettel, syntax) ast.Walk(&v, &zn.BlocksAST) if v.found { result = append(result, cand) } } return result } |
︙ | ︙ |
Changes to usecase/reindex.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package usecase import ( "context" | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/logger" ) // ReIndexPort is the interface used by this use case. type ReIndexPort interface { ReIndex(context.Context, id.Zid) error } |
︙ | ︙ |
Changes to usecase/update_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package usecase import ( "context" | | > < < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | //----------------------------------------------------------------------------- package usecase import ( "context" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/logger" "zettelstore.de/z/zettel" ) // UpdateZettelPort is the interface used by this use case. type UpdateZettelPort interface { // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) |
︙ | ︙ | |||
52 53 54 55 56 57 58 | return err } if zettel.Equal(oldZettel, false) { return nil } // Update relevant computed, but stored values. | | | | | | | | 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 | return err } if zettel.Equal(oldZettel, false) { return nil } // Update relevant computed, but stored values. if _, found := m.Get(meta.KeyCreated); !found { if val, crFound := oldZettel.Meta.Get(meta.KeyCreated); crFound { m.Set(meta.KeyCreated, val) } } m.SetNow(meta.KeyModified) m.YamlSep = oldZettel.Meta.YamlSep if m.Zid == id.ZidConfiguration { m.Set(meta.KeySyntax, meta.ValueSyntaxNone) } if !hasContent { zettel.Content = oldZettel.Content } zettel.Content.TrimSpace() err = uc.port.UpdateZettel(ctx, zettel) uc.log.Info().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel") return err } |
Changes to web/adapter/adapter.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | // Package adapter provides handlers for web requests, and some helper tools. package adapter import ( "context" "t73f.de/r/zsc/api" | | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // Package adapter provides handlers for web requests, and some helper tools. package adapter import ( "context" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/usecase" ) // TryReIndex executes a re-index if the appropriate query action is given. func TryReIndex(ctx context.Context, actions []string, metaSeq []*meta.Meta, reIndex *usecase.ReIndex) ([]string, error) { if lenActions := len(actions); lenActions > 0 { tempActions := make([]string, 0, lenActions) hasReIndex := false |
︙ | ︙ |
Changes to web/adapter/api/api.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 | import ( "bytes" "context" "net/http" "time" "t73f.de/r/zsc/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" | > < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import ( "bytes" "context" "net/http" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // API holds all data and methods for delivering API call results. type API struct { log *logger.Logger b server.Builder authz auth.AuthzManager |
︙ | ︙ |
Changes to web/adapter/api/create_zettel.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 | package api import ( "net/http" "t73f.de/r/sx" "t73f.de/r/zsc/api" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" "zettelstore.de/z/zettel" | > < | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package api import ( "net/http" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" "zettelstore.de/z/zettel" ) // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() |
︙ | ︙ | |||
52 53 54 55 56 57 58 | if err != nil { a.reportUsecaseError(w, err) return } var result []byte var contentType string | | | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | if err != nil { a.reportUsecaseError(w, err) return } var result []byte var contentType string location := a.NewURLBuilder('z').SetZid(newZid) switch enc { case api.EncoderPlain: result = newZid.Bytes() contentType = content.PlainText case api.EncoderData: result = []byte(sx.Int64(newZid).String()) contentType = content.SXPF |
︙ | ︙ |
Changes to web/adapter/api/delete_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 | //----------------------------------------------------------------------------- package api import ( "net/http" | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //----------------------------------------------------------------------------- package api import ( "net/http" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/usecase" ) // MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { |
︙ | ︙ |
Changes to web/adapter/api/get_data.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 | package api import ( "net/http" "t73f.de/r/sx" | | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package api import ( "net/http" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/usecase" ) // MakeGetDataHandler creates a new HTTP handler to return zettelstore data. func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { version := ucVersion.Run() err := a.writeObject(w, id.Invalid, sx.MakeList( |
︙ | ︙ |
Changes to web/adapter/api/get_zettel.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 | "bytes" "context" "fmt" "net/http" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/sexp" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/encoder" | > > < < < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | "bytes" "context" "fmt" "net/http" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sexp" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/encoder" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" ) // MakeGetZettelHandler creates a new HTTP handler to return a zettel in various encodings. func (a *API) MakeGetZettelHandler( getZettel usecase.GetZettel, parseZettel usecase.ParseZettel, evaluate usecase.Evaluate, |
︙ | ︙ | |||
54 55 56 57 58 59 60 | a.writePlainData(ctx, w, zid, part, getZettel) case api.EncoderData: a.writeSzData(ctx, w, zid, part, getZettel) default: var zn *ast.ZettelNode | < | < | < < < | | 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | a.writePlainData(ctx, w, zid, part, getZettel) case api.EncoderData: a.writeSzData(ctx, w, zid, part, getZettel) default: var zn *ast.ZettelNode if q.Has(api.QueryKeyParseOnly) { zn, err = parseZettel.Run(ctx, zid, q.Get(meta.KeySyntax)) } else { zn, err = evaluate.Run(ctx, zid, q.Get(meta.KeySyntax)) } if err != nil { a.reportUsecaseError(w, err) return } a.writeEncodedZettelPart(ctx, w, zn, enc, encStr, part) } }) } func (a *API) writePlainData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) { var buf bytes.Buffer var contentType string |
︙ | ︙ | |||
99 100 101 102 103 104 105 | } case partMeta: contentType = content.PlainText _, err = z.Meta.Write(&buf) case partContent: | | | 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | } case partMeta: contentType = content.PlainText _, err = z.Meta.Write(&buf) case partContent: contentType = content.MIMEFromSyntax(string(z.Meta.GetDefault(meta.KeySyntax, meta.DefaultSyntax))) _, err = z.Content.Write(&buf) } if err != nil { a.log.Error().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return |
︙ | ︙ | |||
144 145 146 147 148 149 150 | a.log.Error().Err(err).Zid(zid).Msg("write sx data") } } func (a *API) writeEncodedZettelPart( ctx context.Context, w http.ResponseWriter, zn *ast.ZettelNode, | < | | | | 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 | a.log.Error().Err(err).Zid(zid).Msg("write sx data") } } func (a *API) writeEncodedZettelPart( ctx context.Context, w http.ResponseWriter, zn *ast.ZettelNode, enc api.EncodingEnum, encStr string, part partType, ) { encdr := encoder.Create( enc, &encoder.CreateParameter{ Lang: a.rtConfig.Get(ctx, zn.InhMeta, meta.KeyLang), }) if encdr == nil { adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid, encStr)) return } var err error var buf bytes.Buffer switch part { case partZettel: _, err = encdr.WriteZettel(&buf, zn) case partMeta: _, err = encdr.WriteMeta(&buf, zn.InhMeta) case partContent: _, err = encdr.WriteContent(&buf, zn) } if err != nil { a.log.Error().Err(err).Zid(zn.Zid).Msg("Unable to store data in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return |
︙ | ︙ |
Changes to web/adapter/api/login.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 | package api import ( "net/http" "time" "t73f.de/r/sx" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" | > < | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | package api import ( "net/http" "time" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !a.withAuth() { if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil { |
︙ | ︙ |
Changes to web/adapter/api/query.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | "bytes" "fmt" "io" "net/http" "net/url" "strconv" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/sexp" "zettelstore.de/z/query" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" | > > > > < < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | "bytes" "fmt" "io" "net/http" "net/url" "strconv" "strings" "slices" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/sexp" "zettelstore.de/z/query" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" ) // MakeQueryHandler creates a new HTTP handler to perform a query. func (a *API) MakeQueryHandler( queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, |
︙ | ︙ | |||
57 58 59 60 61 62 63 | actions, err := adapter.TryReIndex(ctx, sq.Actions(), metaSeq, reIndex) if err != nil { a.reportUsecaseError(w, err) return } if len(actions) > 0 { if len(metaSeq) > 0 { | < | | | | | < | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | actions, err := adapter.TryReIndex(ctx, sq.Actions(), metaSeq, reIndex) if err != nil { a.reportUsecaseError(w, err) return } if len(actions) > 0 { if len(metaSeq) > 0 { if slices.Contains(actions, api.RedirectAction) { zid := metaSeq[0].Zid ub := a.NewURLBuilder('z').SetZid(zid) a.redirectFound(w, r, ub, zid) return } } } var encoder zettelEncoder var contentType string switch enc, _ := getEncoding(r, urlQuery); enc { |
︙ | ︙ | |||
171 172 173 174 175 176 177 | return err } } return nil } func (*plainZettelEncoder) writeArrangement(w io.Writer, _ string, arr meta.Arrangement) error { for key, ml := range arr { | | | 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | return err } } return nil } func (*plainZettelEncoder) writeArrangement(w io.Writer, _ string, arr meta.Arrangement) error { for key, ml := range arr { _, err := io.WriteString(w, string(key)) if err != nil { return err } for i, m := range ml { if i == 0 { _, err = io.WriteString(w, "\t") } else { |
︙ | ︙ | |||
203 204 205 206 207 208 209 | type dataZettelEncoder struct { sq *query.Query getRights func(*meta.Meta) api.ZettelRights } func (dze *dataZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error { | | > | > > > | | > > | > | > > < < < < < < > | > | | < | | < < < < < < | | | | | 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 | type dataZettelEncoder struct { sq *query.Query getRights func(*meta.Meta) api.ZettelRights } func (dze *dataZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error { var lb sx.ListBuilder lb.AddN( sx.MakeSymbol("meta-list"), sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())), sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())), ) symID, symZettel := sx.MakeSymbol("id"), sx.MakeSymbol("zettel") for _, m := range ml { msz := sexp.EncodeMetaRights(api.MetaRights{ Meta: m.Map(), Rights: dze.getRights(m), }) msz = sx.Cons(sx.MakeList(symID, sx.Int64(m.Zid)), msz.Cdr()).Cons(symZettel) lb.Add(msz) } _, err := sx.Print(w, lb.List()) return err } func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error { var lb sx.ListBuilder lb.AddN( sx.MakeSymbol("meta-list"), sx.MakeString(act), sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())), sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())), ) for aggKey, metaList := range arr { var lbMeta sx.ListBuilder lbMeta.Add(sx.MakeString(aggKey)) for _, m := range metaList { lbMeta.Add(sx.Int64(m.Zid)) } lb.Add(lbMeta.List()) } _, err := sx.Print(w, lb.List()) return err } func (a *API) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool { tag := vals.Get(api.QueryKeyTag) if tag == "" { return false } ctx := r.Context() z, err := tagZettel.Run(ctx, meta.Value(tag)) if err != nil { a.reportUsecaseError(w, err) return true } zid := z.Meta.Zid newURL := a.NewURLBuilder('z').SetZid(zid) for key, slVals := range vals { if key == api.QueryKeyTag { continue } for _, val := range slVals { newURL.AppendKVQuery(key, val) } } a.redirectFound(w, r, newURL, zid) return true } func (a *API) handleRoleZettel(w http.ResponseWriter, r *http.Request, roleZettel *usecase.RoleZettel, vals url.Values) bool { role := vals.Get(api.QueryKeyRole) if role == "" { return false } ctx := r.Context() z, err := roleZettel.Run(ctx, meta.Value(role)) if err != nil { a.reportUsecaseError(w, err) return true } zid := z.Meta.Zid newURL := a.NewURLBuilder('z').SetZid(zid) for key, slVals := range vals { if key == api.QueryKeyRole { continue } for _, val := range slVals { newURL.AppendKVQuery(key, val) } |
︙ | ︙ |
Changes to web/adapter/api/request.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 | import ( "io" "net/http" "net/url" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/api" "t73f.de/r/zsc/input" "t73f.de/r/zsc/sexp" "zettelstore.de/z/zettel" | > > < < | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import ( "io" "net/http" "net/url" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "t73f.de/r/zsc/sexp" "zettelstore.de/z/zettel" ) // getEncoding returns the data encoding selected by the caller. func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) { encoding := q.Get(api.QueryKeyEncoding) if encoding != "" { return api.Encoder(encoding), encoding |
︙ | ︙ | |||
129 130 131 132 133 134 135 | if err != nil { return zettel.Zettel{}, err } m := meta.New(zid) for k, v := range zd.Meta { if !meta.IsComputed(k) { | | | 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | if err != nil { return zettel.Zettel{}, err } m := meta.New(zid) for k, v := range zd.Meta { if !meta.IsComputed(k) { m.Set(meta.RemoveNonGraphic(k), meta.Value(meta.RemoveNonGraphic(v))) } } var content zettel.Content if err = content.SetDecoded(zd.Content, zd.Encoding); err != nil { return zettel.Zettel{}, err } return zettel.Zettel{ Meta: m, Content: content, }, nil } |
Changes to web/adapter/api/response.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | package api import ( "bytes" "net/http" "t73f.de/r/sx" | | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | package api import ( "bytes" "net/http" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/web/content" ) func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error { var buf bytes.Buffer if _, err := sx.Print(&buf, obj); err != nil { msg := a.log.Error().Err(err) if msg != nil { |
︙ | ︙ |
Changes to web/adapter/api/update_zettel.go.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 | package api import ( "net/http" "t73f.de/r/zsc/api" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/zettel" | > < | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package api import ( "net/http" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/zettel" ) // MakeUpdateZettelHandler creates a new HTTP handler to update a zettel. func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { |
︙ | ︙ |
Changes to web/adapter/response.go.
︙ | ︙ | |||
68 69 70 71 72 73 74 | } var eiz box.ErrInvalidZid if errors.As(err, &eiz) { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", eiz.Zid) } var etznf usecase.ErrTagZettelNotFound if errors.As(err, &etznf) { | | | | 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | } var eiz box.ErrInvalidZid if errors.As(err, &eiz) { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", eiz.Zid) } var etznf usecase.ErrTagZettelNotFound if errors.As(err, &etznf) { return http.StatusNotFound, "Tag zettel not found: " + string(etznf.Tag) } var erznf usecase.ErrRoleZettelNotFound if errors.As(err, &erznf) { return http.StatusNotFound, "Role zettel not found: " + string(erznf.Role) } var ebr ErrBadRequest if errors.As(err, &ebr) { return http.StatusBadRequest, ebr.Text } if errors.Is(err, box.ErrStopped) { return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err) |
︙ | ︙ |
Changes to web/adapter/webui/create_zettel.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | import ( "bytes" "context" "net/http" "strings" "t73f.de/r/sx" | | > < < | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | import ( "bytes" "context" "net/http" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/encoder/zmkenc" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel" ) // 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, |
︙ | ︙ | |||
61 62 63 64 65 66 67 | switch op { case actionCopy: wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData) case actionFolge: wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData) case actionNew: title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle()) | | | 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | switch op { case actionCopy: wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData) case actionFolge: wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData) case actionNew: title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle()) newTitle := parser.NormalizedSpacedText(q.Get(meta.KeyTitle)) wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData) case actionSequel: wui.renderZettelForm(ctx, w, createZettel.PrepareSequel(origZettel), "Sequel Zettel", "", roleData, syntaxData) case actionVersion: wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData) } }) |
︙ | ︙ | |||
99 100 101 102 103 104 105 | roleData []string, syntaxData []string, ) { user := server.GetUser(ctx) m := ztl.Meta var sb strings.Builder | | | | | | | 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 | roleData []string, syntaxData []string, ) { user := server.GetUser(ctx) m := ztl.Meta var sb strings.Builder for key, val := range m.Rest() { sb.WriteString(key) sb.WriteString(": ") sb.WriteString(string(val)) sb.WriteByte('\n') } env, rb := wui.createRenderEnv(ctx, "form", wui.getUserLang(ctx), title, user) rb.bindString("heading", sx.MakeString(title)) rb.bindString("form-action-url", sx.MakeString(formActionURL)) rb.bindString("role-data", makeStringList(roleData)) rb.bindString("syntax-data", makeStringList(syntaxData)) rb.bindString("meta", sx.MakeString(sb.String())) if !ztl.Content.IsBinary() { rb.bindString("content", sx.MakeString(ztl.Content.AsString())) } wui.bindCommonZettelData(ctx, &rb, user, m, &ztl.Content) if rb.err == nil { rb.err = wui.renderSxnTemplate(ctx, w, id.ZidFormTemplate, env) } if err := rb.err; err != nil { wui.reportError(ctx, w, err) } } // MakePostCreateZettelHandler creates a new HTTP handler to store content of |
︙ | ︙ | |||
146 147 148 149 150 151 152 | newZid, err := createZettel.Run(ctx, zettel) if err != nil { wui.reportError(ctx, w, err) return } if reEdit { | | | | | | | | 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 | newZid, err := createZettel.Run(ctx, zettel) if err != nil { wui.reportError(ctx, w, err) return } if reEdit { wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(newZid)) } else { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid)) } }) } // MakeGetZettelFromListHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakeGetZettelFromListHandler( queryMeta *usecase.Query, evaluate *usecase.Evaluate, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { q := adapter.GetQuery(r.URL.Query()) ctx := r.Context() metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q) if err != nil { wui.reportError(ctx, w, err) return } entries, _ := evaluator.QueryAction(ctx, q, metaSeq) bns := evaluate.RunBlockNode(ctx, entries) enc := zmkenc.Create() var zmkContent bytes.Buffer _, err = enc.WriteBlocks(&zmkContent, &bns) if err != nil { wui.reportError(ctx, w, err) return } m := meta.New(id.Invalid) m.Set(meta.KeyTitle, meta.Value(q.Human())) m.Set(meta.KeySyntax, meta.ValueSyntaxZmk) if qval := q.String(); qval != "" { m.Set(meta.KeyQuery, meta.Value(qval)) } zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(zmkContent.Bytes())} roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) wui.renderZettelForm(ctx, w, zettel, "Zettel from list", wui.createNewURL, roleData, syntaxData) }) } |
Changes to web/adapter/webui/delete_zettel.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 18 19 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "net/http" "t73f.de/r/sx" | > | | | | < < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "net/http" "slices" "t73f.de/r/sx" "t73f.de/r/zero/set" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/box" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. func (wui *WebUI) MakeGetDeleteZettelHandler( getZettel usecase.GetZettel, getAllZettel usecase.GetAllZettel, |
︙ | ︙ | |||
47 48 49 50 51 52 53 | wui.reportError(ctx, w, err) return } m := zs[0].Meta user := server.GetUser(ctx) env, rb := wui.createRenderEnv( | < | | | | | | | | < | | < | 46 47 48 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 | wui.reportError(ctx, w, err) return } m := zs[0].Meta user := server.GetUser(ctx) env, rb := wui.createRenderEnv( ctx, "delete", wui.getUserLang(ctx), "Delete Zettel "+m.Zid.String(), user) if len(zs) > 1 { rb.bindString("shadowed-box", sx.MakeString(string(zs[1].Meta.GetDefault(meta.KeyBoxNumber, "???")))) rb.bindString("incoming", nil) } else { rb.bindString("shadowed-box", nil) rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel))) } wui.bindCommonZettelData(ctx, &rb, user, m, nil) if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.ZidDeleteTemplate, env) } else { err = rb.err } if err != nil { wui.reportError(ctx, w, err) } }) } func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) *sx.Pair { zidMap := set.New[string]() addListValues(zidMap, m, meta.KeyBackward) for _, kd := range meta.GetSortedKeyDescriptions() { inverseKey := kd.Inverse if inverseKey == "" { continue } ikd := meta.GetDescription(inverseKey) switch ikd.Type { case meta.TypeID: if val, ok := m.Get(inverseKey); ok { zidMap.Add(string(val)) } case meta.TypeIDSet: addListValues(zidMap, m, inverseKey) } } return wui.zidLinksSxn(slices.Sorted(zidMap.Values()), getTextTitle) } func addListValues(zidMap *set.Set[string], m *meta.Meta, key string) { for val := range m.GetFields(key) { zidMap.Add(val) } } // MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel. func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() |
︙ | ︙ |
Changes to web/adapter/webui/edit_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 | //----------------------------------------------------------------------------- package webui import ( "net/http" "zettelstore.de/z/box" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" | > < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //----------------------------------------------------------------------------- package webui import ( "net/http" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/box" "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, |
︙ | ︙ | |||
74 75 76 77 78 79 80 | } if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil { wui.reportError(ctx, w, err) return } if reEdit { | | | | 74 75 76 77 78 79 80 81 82 83 84 85 86 | } if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil { wui.reportError(ctx, w, err) return } if reEdit { wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(zid)) } else { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid)) } }) } |
Changes to web/adapter/webui/forms.go.
︙ | ︙ | |||
18 19 20 21 22 23 24 | "errors" "io" "net/http" "regexp" "strings" "unicode" | | > < < | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | "errors" "io" "net/http" "regexp" "strings" "unicode" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/input" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/web/content" "zettelstore.de/z/zettel" ) var ( bsCRLF = []byte{'\r', '\n'} bsLF = []byte{'\n'} ) |
︙ | ︙ | |||
51 52 53 54 55 56 57 | if postMeta, ok := trimmedFormValue(r, "meta"); ok { m = meta.NewFromInput(zid, input.NewInput(removeEmptyLines([]byte(postMeta)))) m.Sanitize() } else { m = meta.New(zid) } if postTitle, ok := trimmedFormValue(r, "title"); ok { | | | | | | | | 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 | if postMeta, ok := trimmedFormValue(r, "meta"); ok { m = meta.NewFromInput(zid, input.NewInput(removeEmptyLines([]byte(postMeta)))) m.Sanitize() } else { m = meta.New(zid) } if postTitle, ok := trimmedFormValue(r, "title"); ok { m.Set(meta.KeyTitle, meta.Value(meta.RemoveNonGraphic(postTitle))) } if postTags, ok := trimmedFormValue(r, "tags"); ok { if tags := meta.Value(meta.RemoveNonGraphic(postTags)).AsSlice(); len(tags) > 0 { for i, tag := range tags { tags[i] = string(meta.Value(tag).NormalizeTag()) } m.SetList(meta.KeyTags, tags) } } if postRole, ok := trimmedFormValue(r, "role"); ok { m.SetWord(meta.KeyRole, meta.RemoveNonGraphic(postRole)) } if postSyntax, ok := trimmedFormValue(r, "syntax"); ok { m.SetWord(meta.KeySyntax, meta.RemoveNonGraphic(postSyntax)) } if data := textContent(r); data != nil { return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(data)}, nil } if data, m2 := uploadedContent(r, m); data != nil { return doSave, zettel.Zettel{Meta: m2, Content: zettel.NewContent(data)}, nil |
︙ | ︙ | |||
114 115 116 117 118 119 120 | if err2 != nil { return nil, m } if cts, found := fh.Header["Content-Type"]; found && len(cts) > 0 { ct := cts[0] if fileSyntax := content.SyntaxFromMIME(ct, data); fileSyntax != "" { m = m.Clone() | | | | | | 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 | if err2 != nil { return nil, m } if cts, found := fh.Header["Content-Type"]; found && len(cts) > 0 { ct := cts[0] if fileSyntax := content.SyntaxFromMIME(ct, data); fileSyntax != "" { m = m.Clone() m.Set(meta.KeySyntax, meta.Value(fileSyntax)) } } return data, m } } return nil, m } func allowEmptyContent(m *meta.Meta) bool { if syntax, found := m.Get(meta.KeySyntax); found { if syntax == meta.ValueSyntaxNone { return true } if pinfo := parser.Get(string(syntax)); pinfo != nil { return pinfo.IsTextFormat } } return true } var reEmptyLines = regexp.MustCompile(`(\n|\r)+\s*(\n|\r)+`) func removeEmptyLines(s []byte) []byte { b := bytes.TrimSpace(s) return reEmptyLines.ReplaceAllLiteral(b, []byte{'\n'}) } |
Changes to web/adapter/webui/get_info.go.
︙ | ︙ | |||
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | "context" "net/http" "slices" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/encoder" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/query" "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" | > > < | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | "context" "net/http" "slices" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/encoder" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/query" "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" ) // MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetInfoHandler( ucParseZettel usecase.ParseZettel, ucEvaluate *usecase.Evaluate, ucGetZettel usecase.GetZettel, |
︙ | ︙ | |||
49 50 51 52 53 54 55 | path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) return } | | < < | < | < < < | | > | | | | | | | | | | | | | | 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 | path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) return } zn, err := ucParseZettel.Run(ctx, zid, q.Get(meta.KeySyntax)) if err != nil { wui.reportError(ctx, w, err) return } getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel) var lbMetadata sx.ListBuilder for key, val := range zn.Meta.Computed() { sxval := wui.writeHTMLMetaValue(key, val, getTextTitle) lbMetadata.Add(sx.Cons(sx.MakeString(key), sxval)) } summary := collect.References(zn) locLinks, queryLinks, extLinks := wui.splitLocSeaExtLinks(append(summary.Links, summary.Embeds...)) title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle()) phrase := q.Get(api.QueryKeyPhrase) if phrase == "" { phrase = title } unlinkedMeta, err := ucQuery.Run(ctx, createUnlinkedQuery(zid, phrase)) if err != nil { wui.reportError(ctx, w, err) return } enc := wui.getSimpleHTMLEncoder(wui.getConfig(ctx, zn.InhMeta, meta.KeyLang)) entries, _ := evaluator.QueryAction(ctx, nil, unlinkedMeta) bns := ucEvaluate.RunBlockNode(ctx, entries) unlinkedContent, _, err := enc.BlocksSxn(&bns) if err != nil { wui.reportError(ctx, w, err) return } encTexts := encodingTexts() shadowLinks := getShadowLinks(ctx, zid, ucGetAllZettel) user := server.GetUser(ctx) env, rb := wui.createRenderEnv(ctx, "info", wui.getUserLang(ctx), title, user) rb.bindString("metadata", lbMetadata.List()) rb.bindString("local-links", locLinks) rb.bindString("query-links", queryLinks) rb.bindString("ext-links", extLinks) rb.bindString("unlinked-content", unlinkedContent) rb.bindString("phrase", sx.MakeString(phrase)) rb.bindString("query-key-phrase", sx.MakeString(api.QueryKeyPhrase)) rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts)) rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts)) rb.bindString("shadow-links", shadowLinks) wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content) if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.ZidInfoTemplate, env) } else { err = rb.err } if err != nil { wui.reportError(ctx, w, err) } }) } func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sx.Pair) { var lbLoc, lbQueries, lbExt sx.ListBuilder for _, ref := range links { switch ref.State { case ast.RefStateHosted, ast.RefStateBased: // Local lbLoc.Add(sx.MakeString(ref.String())) case ast.RefStateQuery: lbQueries.Add( sx.Cons( sx.MakeString(ref.Value), sx.MakeString(wui.NewURLBuilder('h').AppendQuery(ref.Value).String()))) case ast.RefStateExternal: lbExt.Add(sx.MakeString(ref.String())) } } return lbLoc.List(), lbQueries.List(), lbExt.List() } func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query { var sb strings.Builder sb.Write(zid.Bytes()) sb.WriteByte(' ') sb.WriteString(api.UnlinkedDirective) for _, word := range strfun.MakeWords(phrase) { sb.WriteByte(' ') sb.WriteString(api.PhraseDirective) sb.WriteByte(' ') sb.WriteString(word) } sb.WriteByte(' ') sb.WriteString(api.OrderDirective) sb.WriteByte(' ') sb.WriteString(meta.KeyID) return query.Parse(sb.String()) } func encodingTexts() []string { encodings := encoder.GetEncodings() encTexts := make([]string, 0, len(encodings)) for _, f := range encodings { encTexts = append(encTexts, f.String()) } slices.Sort(encTexts) return encTexts } var apiParts = []string{api.PartZettel, api.PartMeta, api.PartContent} func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool, encTexts []string) *sx.Pair { matrix := sx.Nil() u := wui.NewURLBuilder('z').SetZid(zid) for ip := len(apiParts) - 1; ip >= 0; ip-- { part := apiParts[ip] row := sx.Nil() for je := len(encTexts) - 1; je >= 0; je-- { enc := encTexts[je] if parseOnly { u.AppendKVQuery(api.QueryKeyParseOnly, "") } u.AppendKVQuery(api.QueryKeyPart, part) u.AppendKVQuery(api.QueryKeyEncoding, enc) row = row.Cons(sx.Cons(sx.MakeString(enc), sx.MakeString(u.String()))) u.ClearQuery() } matrix = matrix.Cons(sx.Cons(sx.MakeString(part), row)) } return matrix } func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sx.Pair { matrix := wui.infoAPIMatrix(zid, true, encTexts) u := wui.NewURLBuilder('z').SetZid(zid) for i, row := 0, matrix; i < len(apiParts) && row != nil; row = row.Tail() { line, isLine := sx.GetPair(row.Car()) if !isLine || line == nil { continue } last := line.LastPair() |
︙ | ︙ | |||
213 214 215 216 217 218 219 | } i++ } return matrix } func getShadowLinks(ctx context.Context, zid id.Zid, getAllZettel usecase.GetAllZettel) *sx.Pair { | | | | | | | 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | } i++ } return matrix } func getShadowLinks(ctx context.Context, zid id.Zid, getAllZettel usecase.GetAllZettel) *sx.Pair { var lb sx.ListBuilder if zl, err := getAllZettel.Run(ctx, zid); err == nil { for _, ztl := range zl { if boxNo, ok := ztl.Meta.Get(meta.KeyBoxNumber); ok { lb.Add(sx.MakeString(string(boxNo))) } } } return lb.List() } |
Changes to web/adapter/webui/get_zettel.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | //----------------------------------------------------------------------------- package webui import ( "context" "net/http" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/shtml" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" | > > > < < | > | | | | | > > | > | > > | > | | | | > | | > | | | | | < | < < | | | | | < | | | | < > | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | //----------------------------------------------------------------------------- package webui import ( "context" "net/http" "slices" "strings" "t73f.de/r/sx" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetHTMLZettelHandler( evaluate *usecase.Evaluate, getZettel usecase.GetZettel, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) return } q := r.URL.Query() zn, err := evaluate.Run(ctx, zid, q.Get(meta.KeySyntax)) if err != nil { wui.reportError(ctx, w, err) return } zettelLang := wui.getConfig(ctx, zn.InhMeta, meta.KeyLang) enc := wui.getSimpleHTMLEncoder(zettelLang) metaObj := enc.MetaSxn(zn.InhMeta) content, endnotes, err := enc.BlocksSxn(&zn.BlocksAST) if err != nil { wui.reportError(ctx, w, err) return } user := server.GetUser(ctx) getTextTitle := wui.makeGetTextTitle(ctx, getZettel) title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle()) env, rb := wui.createRenderEnv(ctx, "zettel", zettelLang, title, user) rb.bindSymbol(symMetaHeader, metaObj) rb.bindString("heading", sx.MakeString(title)) if role, found := zn.InhMeta.Get(meta.KeyRole); found && role != "" { rb.bindString( "role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery( meta.KeyRole+api.SearchOperatorHas+string(role)).String())) } if folgeRole, found := zn.InhMeta.Get(meta.KeyFolgeRole); found && folgeRole != "" { rb.bindString( "folge-role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery( meta.KeyRole+api.SearchOperatorHas+string(folgeRole)).String())) } rb.bindString("tag-refs", wui.transformTagSet(meta.KeyTags, zn.InhMeta.GetDefault(meta.KeyTags, "").AsSlice())) rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, meta.KeyPredecessor, getTextTitle)) rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, meta.KeyPrecursor, getTextTitle)) rb.bindString("prequel-refs", wui.identifierSetAsLinks(zn.InhMeta, meta.KeyPrequel, getTextTitle)) rb.bindString("superior-refs", wui.identifierSetAsLinks(zn.InhMeta, meta.KeySuperior, getTextTitle)) rb.bindString("urls", metaURLAssoc(zn.InhMeta)) rb.bindString("content", content) rb.bindString("endnotes", endnotes) wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, meta.KeyFolge, config.KeyShowFolgeLinks, getTextTitle) wui.bindLinks(ctx, &rb, "sequel", zn.InhMeta, meta.KeySequel, config.KeyShowSequelLinks, getTextTitle) wui.bindLinks(ctx, &rb, "subordinate", zn.InhMeta, meta.KeySubordinates, config.KeyShowSubordinateLinks, getTextTitle) wui.bindLinks(ctx, &rb, "back", zn.InhMeta, meta.KeyBack, config.KeyShowBackLinks, getTextTitle) wui.bindLinks(ctx, &rb, "successor", zn.InhMeta, meta.KeySuccessors, config.KeyShowSuccessorLinks, getTextTitle) if role, found := zn.InhMeta.Get(meta.KeyRole); found && role != "" { for _, part := range []string{"meta", "actions", "heading"} { rb.rebindResolved("ROLE-"+string(role)+"-"+part, "ROLE-DEFAULT-"+part) } } wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content) if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.ZidZettelTemplate, env) } else { err = rb.err } if err != nil { wui.reportError(ctx, w, err) } }) } func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair { return wui.transformIdentifierSet(m.GetFields(key), getTextTitle) } func metaURLAssoc(m *meta.Meta) *sx.Pair { var result sx.ListBuilder for key, val := range m.Rest() { if strings.HasSuffix(key, meta.SuffixKeyURL) { if val != "" { result.Add(sx.Cons(sx.MakeString(capitalizeMetaKey(key)), sx.MakeString(string(val)))) } } } return result.List() } func (wui *WebUI) bindLinks(ctx context.Context, rb *renderBinder, varPrefix string, m *meta.Meta, key, configKey string, getTextTitle getTextTitleFunc) { varLinks := varPrefix + "-links" var symOpen *sx.Symbol switch wui.getConfig(ctx, m, configKey) { case "false": rb.bindString(varLinks, sx.Nil()) return case "close": default: symOpen = shtml.SymAttrOpen } lstLinks := wui.zettelLinksSxn(m, key, getTextTitle) rb.bindString(varLinks, lstLinks) if sx.IsNil(lstLinks) { return } rb.bindString(varPrefix+"-open", symOpen) } func (wui *WebUI) zettelLinksSxn(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair { if values := slices.Collect(m.GetFields(key)); len(values) > 0 { return wui.zidLinksSxn(values, getTextTitle) } return nil } func (wui *WebUI) zidLinksSxn(values []string, getTextTitle getTextTitleFunc) *sx.Pair { var lb sx.ListBuilder for _, val := range values { zid, err := id.Parse(val) if err != nil { continue } if title, found := getTextTitle(zid); found > 0 { url := sx.MakeString(wui.NewURLBuilder('h').SetZid(zid).String()) if title == "" { lb.Add(sx.Cons(sx.MakeString(val), url)) } else { lb.Add(sx.Cons(sx.MakeString(title), url)) } } } return lb.List() } |
Changes to web/adapter/webui/home.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 21 22 23 24 25 | package webui import ( "context" "errors" "net/http" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel" | > < | < | | | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | package webui import ( "context" "errors" "net/http" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel" ) type getRootPort interface { GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) } // MakeGetRootHandler creates a new HTTP handler to show the root URL. func (wui *WebUI) MakeGetRootHandler(s getRootPort) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if p := r.URL.Path; p != "/" { wui.reportError(ctx, w, adapter.ErrResourceNotFound{Path: p}) return } homeZid, _ := id.Parse(wui.getConfig(ctx, nil, config.KeyHomeZettel)) if homeZid != id.ZidDefaultHome { if _, err := s.GetZettel(ctx, homeZid); err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } homeZid = id.ZidDefaultHome } _, err := s.GetZettel(ctx, homeZid) if err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(homeZid)) return } if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil { wui.redirectFound(w, r, wui.NewURLBuilder('i')) return } wui.redirectFound(w, r, wui.NewURLBuilder('h')) }) } |
Changes to web/adapter/webui/htmlgen.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "net/url" "strings" "t73f.de/r/sx" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/attrs" | > > > | > < < < | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "maps" "net/url" "slices" "strings" "t73f.de/r/sx" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zero/set" "t73f.de/r/zsc/api" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "t73f.de/r/zsc/sz" "zettelstore.de/z/ast" "zettelstore.de/z/encoder/szenc" ) // Builder allows to build new URLs for the web service. type urlBuilder interface { GetURLPrefix() string NewURLBuilder(key byte) *api.URLBuilder } |
︙ | ︙ | |||
73 74 75 76 77 78 79 | if hrefP == nil { return obj } href, ok := sx.GetString(hrefP.Cdr()) if !ok { return obj } | | > | > > > | 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | if hrefP == nil { return obj } href, ok := sx.GetString(hrefP.Cdr()) if !ok { return obj } strZid, fragment, hasFragment := strings.Cut(href.GetValue(), "#") zid, err := id.Parse(strZid) u := builder.NewURLBuilder('h') if err == nil { u = u.SetZid(zid) } if hasFragment { u = u.SetFragment(fragment) } assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String()))) return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) } |
︙ | ︙ | |||
118 119 120 121 122 123 124 | if !ok { return obj } ur, err := url.Parse(href.GetValue()) if err != nil { return obj } | | | | > > > | 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | if !ok { return obj } ur, err := url.Parse(href.GetValue()) if err != nil { return obj } urlQuery := ur.Query() if !urlQuery.Has(api.QueryKeyQuery) { return obj } u := builder.NewURLBuilder('h') if q := urlQuery.Get(api.QueryKeyQuery); q != "" { u = u.AppendQuery(q) } assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String()))) return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) }) rebind(th, sz.SymLinkExternal, func(obj sx.Object) sx.Object { attr, _, rest := findA(obj) if attr == nil { return obj |
︙ | ︙ | |||
153 154 155 156 157 158 159 | if srcP == nil { return obj } src, isString := sx.GetString(srcP.Cdr()) if !isString { return obj } | | | | 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | if srcP == nil { return obj } src, isString := sx.GetString(srcP.Cdr()) if !isString { return obj } zid, err := id.Parse(src.GetValue()) if err != nil { return obj } u := builder.NewURLBuilder('z').SetZid(zid) imgAttr := attr.Tail().Cons(sx.Cons(shtml.SymAttrSrc, sx.MakeString(u.String()))).Cons(sxhtml.SymAttr) return pair.Tail().Tail().Cons(imgAttr).Cons(shtml.SymIMG) }) |
︙ | ︙ | |||
184 185 186 187 188 189 190 | }) } // SetUnique sets a prefix to make several HTML ids unique. func (g *htmlGenerator) SetUnique(s string) *htmlGenerator { g.th.SetUnique(s); return g } var mapMetaKey = map[string]string{ | | | | | | | | | | | | | | | | | < | | | > | | > | | 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 | }) } // SetUnique sets a prefix to make several HTML ids unique. func (g *htmlGenerator) SetUnique(s string) *htmlGenerator { g.th.SetUnique(s); return g } var mapMetaKey = map[string]string{ meta.KeyCopyright: "copyright", meta.KeyLicense: "license", } func (g *htmlGenerator) MetaSxn(m *meta.Meta) *sx.Pair { tm := g.tx.GetMeta(m) env := shtml.MakeEnvironment(g.lang) hm, err := g.th.Evaluate(tm, &env) if err != nil { return nil } ignore := set.New(meta.KeyTitle, meta.KeyLang) metaMap := make(map[string]*sx.Pair, 32) if tags, ok := m.Get(meta.KeyTags); ok { metaMap[meta.KeyTags] = g.transformMetaTags(tags) ignore.Add(meta.KeyTags) } for elem := range hm.Values() { mlst, isPair := sx.GetPair(elem) if !isPair { continue } att, isPair := sx.GetPair(mlst.Tail().Car()) if !isPair { continue } if !att.Car().IsEqual(g.symAt) { continue } a := make(attrs.Attributes, 32) for aelem := range att.Tail().Values() { if p, ok := sx.GetPair(aelem); ok { key := p.Car() val := p.Cdr() if tail, isTail := sx.GetPair(val); isTail { val = tail.Car() } a = a.Set(sz.GoValue(key), sz.GoValue(val)) } } name, found := a.Get("name") if !found || ignore.Contains(name) { continue } newName, found := mapMetaKey[name] if !found { continue } a = a.Set("name", newName) metaMap[newName] = g.th.EvaluateMeta(a) } var lb sx.ListBuilder for _, key := range slices.Sorted(maps.Keys(metaMap)) { lb.Add(metaMap[key]) } return lb.List() } func (g *htmlGenerator) transformMetaTags(tags meta.Value) *sx.Pair { var sb strings.Builder first := true for val := range tags.Elems() { if !first { sb.WriteString(", ") } first = false sb.WriteString(string(val.CleanTag())) } metaTags := sb.String() if len(metaTags) == 0 { return nil } return g.th.EvaluateMeta(attrs.Attributes{"name": "keywords", "content": metaTags}) } |
︙ | ︙ | |||
279 280 281 282 283 284 285 | } // InlinesSxHTML returns an inline slice, encoded as a SxHTML object. func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair { if is == nil || len(*is) == 0 { return nil } | > > > > | | | 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 | } // InlinesSxHTML returns an inline slice, encoded as a SxHTML object. func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair { if is == nil || len(*is) == 0 { return nil } return g.nodeSxHTML(is) } func (g *htmlGenerator) nodeSxHTML(node ast.Node) *sx.Pair { sz := g.tx.GetSz(node) env := shtml.MakeEnvironment(g.lang) sh, err := g.th.Evaluate(sz, &env) if err != nil { return nil } return sh } |
Changes to web/adapter/webui/htmlmeta.go.
︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 | //----------------------------------------------------------------------------- package webui import ( "context" "errors" "t73f.de/r/sx" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/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 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 | //----------------------------------------------------------------------------- package webui import ( "context" "errors" "iter" "t73f.de/r/sx" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/box" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" ) func (wui *WebUI) writeHTMLMetaValue( key string, value meta.Value, getTextTitle getTextTitleFunc, ) sx.Object { switch kt := meta.Type(key); kt { case meta.TypeCredential: return sx.MakeString(string(value)) case meta.TypeEmpty: return sx.MakeString(string(value)) case meta.TypeID: return wui.transformIdentifier(value, getTextTitle) case meta.TypeIDSet: return wui.transformIdentifierSet(value.Fields(), getTextTitle) case meta.TypeNumber: return wui.transformKeyValueText(key, value, string(value)) case meta.TypeString: return sx.MakeString(string(value)) case meta.TypeTagSet: return wui.transformTagSet(key, value.AsSlice()) case meta.TypeTimestamp: if ts, ok := value.AsTime(); ok { return sx.MakeList( sx.MakeSymbol("time"), sx.MakeList( sxhtml.SymAttr, sx.Cons(sx.MakeSymbol("datetime"), sx.MakeString(ts.Format("2006-01-02T15:04:05"))), ), sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(ts.Format("2006-01-02 15:04:05"))), ) } return sx.Nil() case meta.TypeURL: return wui.url2html(sx.MakeString(string(value))) case meta.TypeWord: return wui.transformKeyValueText(key, value, string(value)) default: return sx.MakeList(shtml.SymSTRONG, sx.MakeString("Unhandled type: "), sx.MakeString(kt.Name)) } } func (wui *WebUI) transformIdentifier(val meta.Value, getTextTitle getTextTitleFunc) sx.Object { text := sx.MakeString(string(val)) zid, err := id.Parse(string(val)) if err != nil { return text } title, found := getTextTitle(zid) switch { case found > 0: ub := wui.NewURLBuilder('h').SetZid(zid) attrs := sx.Nil() if title != "" { attrs = attrs.Cons(sx.Cons(shtml.SymAttrTitle, sx.MakeString(title))) } attrs = attrs.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String()))).Cons(sxhtml.SymAttr) return sx.Nil().Cons(sx.MakeString(zid.String())).Cons(attrs).Cons(shtml.SymA) case found == 0: return sx.MakeList(sx.MakeSymbol("s"), text) default: // case found < 0: return text } } var space = sx.MakeString(" ") func (wui *WebUI) transformIdentifierSet(vals iter.Seq[string], getTextTitle getTextTitleFunc) *sx.Pair { var lb sx.ListBuilder lb.Add(shtml.SymSPAN) hadValue := false for val := range vals { if hadValue { lb.Add(space) } hadValue = true lb.Add(wui.transformIdentifier(meta.Value(val), getTextTitle)) } if hadValue { return lb.List() } return nil } func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair { var lb sx.ListBuilder lb.Add(shtml.SymSPAN) for i, tag := range tags { if i > 0 { lb.Add(space) } lb.Add(wui.transformKeyValueText(key, meta.Value(tag), tag)) } if len(tags) > 1 { lb.AddN(space, wui.transformKeyValuesText(key, tags, "(all)")) } return lb.List() } func (wui *WebUI) transformKeyValueText(key string, value meta.Value, text string) *sx.Pair { ub := wui.NewURLBuilder('h').AppendQuery(key + api.SearchOperatorHas + string(value)) return buildHref(ub, text) } func (wui *WebUI) transformKeyValuesText(key string, values []string, text string) *sx.Pair { ub := wui.NewURLBuilder('h') for _, val := range values { ub = ub.AppendQuery(key + api.SearchOperatorHas + val) |
︙ | ︙ | |||
143 144 145 146 147 148 149 | sxhtml.SymAttr, sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String())), ), sx.MakeString(text), ) } | < < < < < < < < < < < | 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | sxhtml.SymAttr, sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String())), ), sx.MakeString(text), ) } type getTextTitleFunc func(id.Zid) (string, int) func (wui *WebUI) makeGetTextTitle(ctx context.Context, getZettel usecase.GetZettel) getTextTitleFunc { return func(zid id.Zid) (string, int) { z, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return "", -1 } return "", 0 } return parser.NormalizedSpacedText(z.Meta.GetTitle()), 1 } } |
Changes to web/adapter/webui/lists.go.
︙ | ︙ | |||
11 12 13 14 15 16 17 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "context" | < | | | | < < < < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package webui import ( "context" "net/http" "net/url" "slices" "strconv" "strings" "t73f.de/r/sx" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/ast" "zettelstore.de/z/evaluator" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML. func (wui *WebUI) MakeListHTMLMetaHandler( queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, |
︙ | ︙ | |||
60 61 62 63 64 65 66 | return } actions, err := adapter.TryReIndex(ctx, q.Actions(), metaSeq, reIndex) if err != nil { wui.reportError(ctx, w, err) return } | < | < | | | < < < < < < < < < < > > | | > | < < < | | | | 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 | return } actions, err := adapter.TryReIndex(ctx, q.Actions(), metaSeq, reIndex) if err != nil { wui.reportError(ctx, w, err) return } if len(metaSeq) > 0 { if slices.Contains(actions, api.RedirectAction) { ub := wui.NewURLBuilder('h').SetZid(metaSeq[0].Zid) wui.redirectFound(w, r, ub) return } } userLang := wui.getUserLang(ctx) var content, endnotes *sx.Pair numEntries := 0 if bn, cnt := evaluator.QueryAction(ctx, q, metaSeq); bn != nil { enc := wui.getSimpleHTMLEncoder(userLang) content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn}) if err != nil { wui.reportError(ctx, w, err) return } numEntries = cnt } siteName := wui.rtConfig.GetSiteName() user := server.GetUser(ctx) env, rb := wui.createRenderEnv(ctx, "list", userLang, siteName, user) if q == nil { rb.bindString("heading", sx.MakeString(siteName)) } else { var sb strings.Builder q.PrintHuman(&sb) rb.bindString("heading", sx.MakeString(sb.String())) } rb.bindString("query-value", sx.MakeString(q.String())) if tzl := q.GetMetaValues(meta.KeyTags, false); len(tzl) > 0 { sxTzl, sxNoTzl := wui.transformTagZettelList(ctx, tagZettel, tzl) if !sx.IsNil(sxTzl) { rb.bindString("tag-zettel", sxTzl) } if !sx.IsNil(sxNoTzl) && wui.canCreate(ctx, user) { rb.bindString("create-tag-zettel", sxNoTzl) } } if rzl := q.GetMetaValues(meta.KeyRole, false); len(rzl) > 0 { sxRzl, sxNoRzl := wui.transformRoleZettelList(ctx, roleZettel, rzl) if !sx.IsNil(sxRzl) { rb.bindString("role-zettel", sxRzl) } if !sx.IsNil(sxNoRzl) && wui.canCreate(ctx, user) { rb.bindString("create-role-zettel", sxNoRzl) } |
︙ | ︙ | |||
143 144 145 146 147 148 149 | rb.bindString("data-url", sx.MakeString(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String())) if wui.canCreate(ctx, user) { rb.bindString("create-url", sx.MakeString(wui.createNewURL)) rb.bindString("seed", sx.Int64(seed)) } } if rb.err == nil { | | | | | | > | | | | | > | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | | | 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 | rb.bindString("data-url", sx.MakeString(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String())) if wui.canCreate(ctx, user) { rb.bindString("create-url", sx.MakeString(wui.createNewURL)) rb.bindString("seed", sx.Int64(seed)) } } if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.ZidListTemplate, env) } else { err = rb.err } if err != nil { wui.reportError(ctx, w, err) } }) } func (wui *WebUI) transformTagZettelList(ctx context.Context, tagZettel *usecase.TagZettel, tags []meta.Value) (withZettel, withoutZettel *sx.Pair) { slices.Reverse(tags) for _, tag := range tags { tag = tag.NormalizeTag() if _, err := tagZettel.Run(ctx, tag); err == nil { u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyTag, string(tag)) withZettel = wui.prependZettelLink(withZettel, string(tag), u) } else { u := wui.NewURLBuilder('c').SetZid(id.ZidTemplateNewTag).AppendKVQuery( queryKeyAction, valueActionNew).AppendKVQuery(meta.KeyTitle, string(tag)) withoutZettel = wui.prependZettelLink(withoutZettel, string(tag), u) } } return withZettel, withoutZettel } func (wui *WebUI) transformRoleZettelList(ctx context.Context, roleZettel *usecase.RoleZettel, roles []meta.Value) (withZettel, withoutZettel *sx.Pair) { slices.Reverse(roles) for _, role := range roles { if _, err := roleZettel.Run(ctx, role); err == nil { u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyRole, string(role)) withZettel = wui.prependZettelLink(withZettel, string(role), u) } else { u := wui.NewURLBuilder('c').SetZid(id.ZidTemplateNewRole).AppendKVQuery( queryKeyAction, valueActionNew).AppendKVQuery(meta.KeyTitle, string(role)) withoutZettel = wui.prependZettelLink(withoutZettel, string(role), u) } } return withZettel, withoutZettel } func (wui *WebUI) prependZettelLink(sxZtl *sx.Pair, name string, u *api.URLBuilder) *sx.Pair { link := sx.MakeList( shtml.SymA, sx.MakeList( sxhtml.SymAttr, sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())), ), sx.MakeString(name), ) if sxZtl != nil { sxZtl = sxZtl.Cons(sx.MakeString(", ")) } return sxZtl.Cons(link) } func (wui *WebUI) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool { tag := vals.Get(api.QueryKeyTag) if tag == "" { return false } ctx := r.Context() z, err := tagZettel.Run(ctx, meta.Value(tag)) if err != nil { wui.reportError(ctx, w, err) return true } wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(z.Meta.Zid)) return true } func (wui *WebUI) handleRoleZettel(w http.ResponseWriter, r *http.Request, roleZettel *usecase.RoleZettel, vals url.Values) bool { role := vals.Get(api.QueryKeyRole) if role == "" { return false } ctx := r.Context() z, err := roleZettel.Run(ctx, meta.Value(role)) if err != nil { wui.reportError(ctx, w, err) return true } wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(z.Meta.Zid)) return true } |
Changes to web/adapter/webui/login.go.
︙ | ︙ | |||
14 15 16 17 18 19 20 | package webui import ( "context" "net/http" "t73f.de/r/sx" | | < | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | package webui import ( "context" "net/http" "t73f.de/r/sx" "t73f.de/r/zsc/domain/id" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" ) // MakeGetLoginOutHandler creates a new HTTP handler to display the HTML login view, // or to execute a logout. func (wui *WebUI) MakeGetLoginOutHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() if query.Has("logout") { wui.clearToken(r.Context(), w) wui.redirectFound(w, r, wui.NewURLBuilder('/')) return } wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false) }) } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { env, rb := wui.createRenderEnv(ctx, "login", wui.getUserLang(ctx), "Login", nil) rb.bindString("retry", sx.MakeBoolean(retry)) if rb.err == nil { rb.err = wui.renderSxnTemplate(ctx, w, id.ZidLoginTemplate, env) } if err := rb.err; err != nil { wui.reportError(ctx, w, err) } } // MakePostLoginHandler creates a new HTTP handler to authenticate the given user. |
︙ | ︙ |
Changes to web/adapter/webui/sxn_code.go.
︙ | ︙ | |||
15 16 17 18 19 20 21 | import ( "context" "fmt" "io" "t73f.de/r/sx/sxeval" | | > | | | | | | | | | | < | | | | | | | | | < | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 | import ( "context" "fmt" "io" "t73f.de/r/sx/sxeval" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idgraph" "t73f.de/r/zsc/domain/id/idset" "t73f.de/r/zsc/domain/meta" ) func (wui *WebUI) loadAllSxnCodeZettel(ctx context.Context) (idgraph.Digraph, *sxeval.Binding, error) { // getMeta MUST currently use GetZettel, because GetMeta just uses the // Index, which might not be current. getMeta := func(ctx context.Context, zid id.Zid) (*meta.Meta, error) { z, err := wui.box.GetZettel(ctx, zid) if err != nil { return nil, err } return z.Meta, nil } dg := buildSxnCodeDigraph(ctx, id.ZidSxnStart, getMeta) if dg == nil { return nil, wui.rootBinding, nil } dg = dg.AddVertex(id.ZidSxnBase).AddEdge(id.ZidSxnStart, id.ZidSxnBase) dg = dg.AddVertex(id.ZidSxnPrelude).AddEdge(id.ZidSxnBase, id.ZidSxnPrelude) dg = dg.TransitiveClosure(id.ZidSxnStart) if zid, isDAG := dg.IsDAG(); !isDAG { return nil, nil, fmt.Errorf("zettel %v is part of a dependency cycle", zid) } bind := wui.rootBinding.MakeChildBinding("zettel", 128) for _, zid := range dg.SortReverse() { if err := wui.loadSxnCodeZettel(ctx, zid, bind); err != nil { return nil, nil, err } } return dg, bind, nil } type getMetaFunc func(context.Context, id.Zid) (*meta.Meta, error) func buildSxnCodeDigraph(ctx context.Context, startZid id.Zid, getMeta getMetaFunc) idgraph.Digraph { m, err := getMeta(ctx, startZid) if err != nil { return nil } var marked *idset.Set stack := []*meta.Meta{m} dg := idgraph.Digraph(nil).AddVertex(startZid) for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 { curr := stack[pos] stack = stack[:pos] if marked.Contains(curr.Zid) { continue } marked = marked.Add(curr.Zid) for pre := range curr.GetFields(meta.KeyPrecursor) { if preZid, errParse := id.Parse(pre); errParse == nil { m, err = getMeta(ctx, preZid) if err != nil { continue } stack = append(stack, m) dg.AddVertex(preZid) dg.AddEdge(curr.Zid, preZid) } } } return dg } func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid, bind *sxeval.Binding) error { |
︙ | ︙ |
Changes to web/adapter/webui/template.go.
︙ | ︙ | |||
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | "t73f.de/r/sx" "t73f.de/r/sx/sxbuiltins" "t73f.de/r/sx/sxeval" "t73f.de/r/sx/sxreader" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/shtml" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/parser" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel" | > > > < < | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | "t73f.de/r/sx" "t73f.de/r/sx/sxbuiltins" "t73f.de/r/sx/sxeval" "t73f.de/r/sx/sxreader" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" "t73f.de/r/zsc/shtml" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/parser" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel" ) func (wui *WebUI) createRenderBinding() *sxeval.Binding { root := sxeval.MakeRootBinding(len(specials) + len(builtins) + 3) for _, syntax := range specials { root.BindSpecial(syntax) } |
︙ | ︙ | |||
71 72 73 74 75 76 77 | if err != nil { return nil, err } zid, err := id.Parse(s.GetValue()) if err != nil { return nil, fmt.Errorf("parsing zettel identifier %q: %w", s.GetValue(), err) } | | | 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | if err != nil { return nil, err } zid, err := id.Parse(s.GetValue()) if err != nil { return nil, fmt.Errorf("parsing zettel identifier %q: %w", s.GetValue(), err) } ub := wui.NewURLBuilder('z').SetZid(zid) return sx.MakeString(ub.String()), nil }, }) root.BindBuiltin(&sxeval.Builtin{ Name: "query->url", MinArity: 1, MaxArity: 1, |
︙ | ︙ | |||
121 122 123 124 125 126 127 | &sxbuiltins.List, // list &sxbuiltins.Append, // append &sxbuiltins.Assoc, // assoc &sxbuiltins.Map, // map &sxbuiltins.Apply, // apply &sxbuiltins.Concat, // concat &sxbuiltins.BoundP, // bound? | | | 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | &sxbuiltins.List, // list &sxbuiltins.Append, // append &sxbuiltins.Assoc, // assoc &sxbuiltins.Map, // map &sxbuiltins.Apply, // apply &sxbuiltins.Concat, // concat &sxbuiltins.BoundP, // bound? &sxbuiltins.DefinedP, // defined? &sxbuiltins.CurrentBinding, // current-binding &sxbuiltins.BindingLookup, // binding-lookup } ) func (wui *WebUI) url2html(text sx.String) sx.Object { if u, errURL := url.Parse(text.GetValue()); errURL == nil { |
︙ | ︙ | |||
177 178 179 180 181 182 183 | rb.bindString("home-url", sx.MakeString(wui.homeURL)) rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth)) rb.bindString("user-is-valid", sx.MakeBoolean(userIsValid)) rb.bindString("user-zettel-url", sx.MakeString(userZettelURL)) rb.bindString("user-ident", sx.MakeString(userIdent)) rb.bindString("login-url", sx.MakeString(wui.loginURL)) rb.bindString("logout-url", sx.MakeString(wui.logoutURL)) | < < | | | | | | | | | | | | | | | | | | | | | | < | | > > > > > > > > > > > > > > > > > > > > > > > > | | > | | > > > | | | | > | | | | 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 | rb.bindString("home-url", sx.MakeString(wui.homeURL)) rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth)) rb.bindString("user-is-valid", sx.MakeBoolean(userIsValid)) rb.bindString("user-zettel-url", sx.MakeString(userZettelURL)) rb.bindString("user-ident", sx.MakeString(userIdent)) rb.bindString("login-url", sx.MakeString(wui.loginURL)) rb.bindString("logout-url", sx.MakeString(wui.logoutURL)) rb.bindString("list-urls", wui.buildListsMenuSxn(ctx, lang)) if wui.canRefresh(user) { rb.bindString("refresh-url", sx.MakeString(wui.refreshURL)) } rb.bindString("new-zettel-links", wui.fetchNewTemplatesSxn(ctx, user)) rb.bindString("search-url", sx.MakeString(wui.searchURL)) rb.bindString("query-key-query", sx.MakeString(api.QueryKeyQuery)) rb.bindString("query-key-seed", sx.MakeString(api.QueryKeySeed)) rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer rb.bindString("debug-mode", sx.MakeBoolean(wui.debug)) rb.bindSymbol(symMetaHeader, sx.Nil()) rb.bindSymbol(symDetail, sx.Nil()) return bind, rb } func (wui *WebUI) getUserRenderData(user *meta.Meta) (bool, string, string) { if user == nil { return false, "", "" } return true, wui.NewURLBuilder('h').SetZid(user.Zid).String(), string(user.GetDefault(meta.KeyUserID, "")) } type renderBinder struct { err error binding *sxeval.Binding } func makeRenderBinder(bind *sxeval.Binding, err error) renderBinder { return renderBinder{binding: bind, err: err} } func (rb *renderBinder) bindString(key string, obj sx.Object) { if rb.err == nil { rb.err = rb.binding.Bind(sx.MakeSymbol(key), obj) } } func (rb *renderBinder) bindSymbol(sym *sx.Symbol, obj sx.Object) { if rb.err == nil { rb.err = rb.binding.Bind(sym, obj) } } func (rb *renderBinder) bindKeyValue(key string, value meta.Value) { rb.bindString("meta-"+key, sx.MakeString(string(value))) if kt := meta.Type(key); kt.IsSet { rb.bindString("set-meta-"+key, makeStringList(value.AsSlice())) } } func (rb *renderBinder) rebindResolved(key, defKey string) { if rb.err == nil { if obj, found := rb.binding.Resolve(sx.MakeSymbol(key)); found { rb.bindString(defKey, obj) } } } func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) { zid := m.Zid strZid := zid.String() newURLBuilder := wui.NewURLBuilder rb.bindString("zid", sx.MakeString(strZid)) rb.bindString("web-url", sx.MakeString(newURLBuilder('h').SetZid(zid).String())) if content != nil && wui.canWrite(ctx, user, m, *content) { rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(zid).String())) } rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(zid).String())) if wui.canCreate(ctx, user) { if content != nil && !content.IsBinary() { rb.bindString("copy-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionCopy).String())) } rb.bindString("version-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionVersion).String())) rb.bindString("sequel-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionSequel).String())) rb.bindString("folge-url", sx.MakeString(newURLBuilder('c').SetZid(zid).AppendKVQuery(queryKeyAction, valueActionFolge).String())) } if wui.canDelete(ctx, user, m) { rb.bindString("delete-url", sx.MakeString(newURLBuilder('d').SetZid(zid).String())) } if val, found := m.Get(meta.KeyUselessFiles); found { rb.bindString("useless", sx.Cons(sx.MakeString(string(val)), nil)) } queryContext := strZid + " " + api.ContextDirective rb.bindString("context-url", sx.MakeString(newURLBuilder('h').AppendQuery(queryContext).String())) queryContext += " " + api.FullDirective rb.bindString("context-full-url", sx.MakeString(newURLBuilder('h').AppendQuery(queryContext).String())) if wui.canRefresh(user) { rb.bindString("reindex-url", sx.MakeString(newURLBuilder('h').AppendQuery( strZid+" "+api.IdentDirective+api.ActionSeparator+api.ReIndexAction).String())) } // Ensure to have title, role, tags, and syntax included as "meta-*" rb.bindKeyValue(meta.KeyTitle, m.GetDefault(meta.KeyTitle, "")) rb.bindKeyValue(meta.KeyRole, m.GetDefault(meta.KeyRole, "")) rb.bindKeyValue(meta.KeyTags, m.GetDefault(meta.KeyTags, "")) rb.bindKeyValue(meta.KeySyntax, m.GetDefault(meta.KeySyntax, meta.DefaultSyntax)) var metaPairs sx.ListBuilder for key, val := range m.Computed() { metaPairs.Add(sx.Cons(sx.MakeString(key), sx.MakeString(string(val)))) rb.bindKeyValue(key, val) } rb.bindString("metapairs", metaPairs.List()) } func (wui *WebUI) buildListsMenuSxn(ctx context.Context, lang string) *sx.Pair { var zn *ast.ZettelNode if menuZid, err := id.Parse(wui.getConfig(ctx, nil, config.KeyListsMenuZettel)); err == nil { if zn, err = wui.evalZettel.Run(ctx, menuZid, ""); err != nil { zn = nil } } if zn == nil { ctx = box.NoEnrichContext(ctx) ztl, err := wui.box.GetZettel(ctx, id.ZidTOCListsMenu) if err != nil { return nil } zn = wui.evalZettel.RunZettel(ctx, ztl, "") } htmlgen := wui.getSimpleHTMLEncoder(lang) var lb sx.ListBuilder for _, ln := range collect.Order(zn) { lb.Add(htmlgen.nodeSxHTML(ln)) } return lb.List() } func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) *sx.Pair { if !wui.canCreate(ctx, user) { return nil } ctx = box.NoEnrichContext(ctx) menu, err := wui.box.GetZettel(ctx, id.ZidTOCNewTemplate) if err != nil { return nil } var lb sx.ListBuilder for _, ln := range collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig)) { ref := ln.Ref if !ref.IsZettel() { continue } zid, err2 := id.Parse(ref.URL.Path) if err2 != nil { continue } z, err2 := wui.box.GetZettel(ctx, zid) if err2 != nil { continue } if !wui.policy.CanRead(user, z.Meta) { continue } text := sx.MakeString(parser.NormalizedSpacedText(z.Meta.GetTitle())) link := sx.MakeString(wui.NewURLBuilder('c').SetZid(zid). AppendKVQuery(queryKeyAction, valueActionNew).String()) lb.Add(sx.Cons(text, link)) } return lb.List() } func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair { if footerZid, err := id.Parse(wui.getConfig(ctx, nil, config.KeyFooterZettel)); err == nil { if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil { htmlEnc := wui.getSimpleHTMLEncoder(wui.getConfig(ctx, zn.InhMeta, meta.KeyLang)).SetUnique("footer-") if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.BlocksAST); err3 == nil { if content != nil && endnotes != nil { content.LastPair().SetCdr(sx.Cons(endnotes, nil)) } return content } } } |
︙ | ︙ | |||
382 383 384 385 386 387 388 | func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, bind *sxeval.Binding) error { detailObj, err := wui.evalSxnTemplate(ctx, templateID, bind) if err != nil { return err } bind.Bind(symDetail, detailObj) | | | 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 | func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, bind *sxeval.Binding) error { detailObj, err := wui.evalSxnTemplate(ctx, templateID, bind) if err != nil { return err } bind.Bind(symDetail, detailObj) pageObj, err := wui.evalSxnTemplate(ctx, id.ZidBaseTemplate, bind) if err != nil { return err } if msg := wui.log.Debug(); msg != nil { // pageObj.String() can be expensive to calculate. msg.Str("page", pageObj.String()).Msg("render") } |
︙ | ︙ | |||
413 414 415 416 417 418 419 | code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { wui.log.Error().Msg(err.Error()) } else { wui.log.Debug().Err(err).Msg("reportError") } user := server.GetUser(ctx) | | | | 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 | code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { wui.log.Error().Msg(err.Error()) } else { wui.log.Debug().Err(err).Msg("reportError") } user := server.GetUser(ctx) env, rb := wui.createRenderEnv(ctx, "error", meta.ValueLangEN, "Error", user) rb.bindString("heading", sx.MakeString(http.StatusText(code))) rb.bindString("message", sx.MakeString(text)) if rb.err == nil { rb.err = wui.renderSxnTemplateStatus(ctx, w, code, id.ZidErrorTemplate, env) } errSx := rb.err if errSx == nil { return } wui.log.Error().Err(errSx).Msg("while rendering error message") |
︙ | ︙ | |||
440 441 442 443 444 445 446 | <h1>Internal server error</h1> <p>When generating error code %d with message:</p><pre>%v</pre><p>an error occured:</p><pre>%v</pre> </body> </html>`, code, text, errSx) } func makeStringList(sl []string) *sx.Pair { | < < < | | | | | 467 468 469 470 471 472 473 474 475 476 477 478 479 | <h1>Internal server error</h1> <p>When generating error code %d with message:</p><pre>%v</pre><p>an error occured:</p><pre>%v</pre> </body> </html>`, code, text, errSx) } func makeStringList(sl []string) *sx.Pair { var lb sx.ListBuilder for _, s := range sl { lb.Add(sx.MakeString(s)) } return lb.List() } |
Changes to web/adapter/webui/webui.go.
︙ | ︙ | |||
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | "sync" "time" "t73f.de/r/sx" "t73f.de/r/sx/sxeval" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel" | > > > < < | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | "sync" "time" "t73f.de/r/sx" "t73f.de/r/sx/sxeval" "t73f.de/r/sxwebs/sxhtml" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/id/idgraph" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel" ) // WebUI holds all data for delivering the web ui. type WebUI struct { log *logger.Logger debug bool ab server.AuthBuilder |
︙ | ︙ | |||
53 54 55 56 57 58 59 | mxCache sync.RWMutex templateCache map[id.Zid]sxeval.Expr tokenLifetime time.Duration cssBaseURL string cssUserURL string homeURL string | < < < | | 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 | mxCache sync.RWMutex templateCache map[id.Zid]sxeval.Expr tokenLifetime time.Duration cssBaseURL string cssUserURL string homeURL string refreshURL string withAuth bool loginURL string logoutURL string searchURL string createNewURL string rootBinding *sxeval.Binding mxZettelBinding sync.Mutex zettelBinding *sxeval.Binding dag idgraph.Digraph genHTML *sxhtml.Generator } // webuiBox contains all box methods that are needed for WebUI operation. // // Note: these function must not do auth checking. type webuiBox interface { |
︙ | ︙ | |||
101 102 103 104 105 106 107 | policy: pol, evalZettel: evalZettel, templateCache: make(map[id.Zid]sxeval.Expr, 32), tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration), | | | < < < > > > > > > > | 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 | policy: pol, evalZettel: evalZettel, templateCache: make(map[id.Zid]sxeval.Expr, 32), tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration), cssBaseURL: ab.NewURLBuilder('z').SetZid(id.ZidBaseCSS).String(), cssUserURL: ab.NewURLBuilder('z').SetZid(id.ZidUserCSS).String(), homeURL: ab.NewURLBuilder('/').String(), refreshURL: ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(), withAuth: authz.WithAuth(), loginURL: loginoutBase.String(), logoutURL: loginoutBase.AppendKVQuery("logout", "").String(), searchURL: ab.NewURLBuilder('h').String(), createNewURL: ab.NewURLBuilder('c').String(), zettelBinding: nil, genHTML: sxhtml.NewGenerator().SetNewline(), } wui.rootBinding = wui.createRenderBinding() wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(wui.observe) return wui } func (wui *WebUI) getConfig(ctx context.Context, m *meta.Meta, key string) string { return wui.rtConfig.Get(ctx, m, key) } func (wui *WebUI) getUserLang(ctx context.Context) string { return wui.getConfig(ctx, nil, meta.KeyLang) } var ( symDetail = sx.MakeSymbol("DETAIL") symMetaHeader = sx.MakeSymbol("META-HEADER") ) func (wui *WebUI) observe(ci box.UpdateInfo) { |
︙ | ︙ |
Changes to web/content/content.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | package content import ( "mime" "net/http" "t73f.de/r/zsc/api" | | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | package content import ( "mime" "net/http" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/zettel" ) // Some MIME encoding values. const ( UnknownMIME = "application/octet-stream" mimeGIF = "image/gif" mimeHTML = "text/html; charset=utf-8" |
︙ | ︙ | |||
51 52 53 54 55 56 57 | if m, found := encoding2mime[enc]; found { return m } return UnknownMIME } var syntax2mime = map[string]string{ | | | | | | | | | | | | | | | | | | | | | | | | | | 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 | if m, found := encoding2mime[enc]; found { return m } return UnknownMIME } var syntax2mime = map[string]string{ meta.ValueSyntaxCSS: "text/css; charset=utf-8", meta.ValueSyntaxDraw: PlainText, meta.ValueSyntaxGif: mimeGIF, meta.ValueSyntaxHTML: mimeHTML, meta.ValueSyntaxJPEG: mimeJPEG, meta.ValueSyntaxJPG: mimeJPEG, meta.ValueSyntaxMarkdown: mimeMarkdown, meta.ValueSyntaxMD: mimeMarkdown, meta.ValueSyntaxNone: "", meta.ValueSyntaxPlain: PlainText, meta.ValueSyntaxPNG: mimePNG, meta.ValueSyntaxSVG: "image/svg+xml", meta.ValueSyntaxSxn: SXPF, meta.ValueSyntaxText: PlainText, meta.ValueSyntaxTxt: PlainText, meta.ValueSyntaxWebp: mimeWEBP, meta.ValueSyntaxZmk: "text/x-zmk; charset=utf-8", // Additional syntaxes that are parsed as plain text. "js": "text/javascript; charset=utf-8", "pdf": "application/pdf", "xml": "text/xml; charset=utf-8", } // MIMEFromSyntax returns a MIME encoding for a given syntax value. func MIMEFromSyntax(syntax string) string { if mt, found := syntax2mime[syntax]; found { return mt } return UnknownMIME } var mime2syntax = map[string]string{ mimeGIF: meta.ValueSyntaxGif, mimeJPEG: meta.ValueSyntaxJPEG, mimePNG: meta.ValueSyntaxPNG, mimeWEBP: meta.ValueSyntaxWebp, "text/html": meta.ValueSyntaxHTML, "text/markdown": meta.ValueSyntaxMarkdown, "text/plain": meta.ValueSyntaxText, // Additional syntaxes "application/pdf": "pdf", "text/javascript": "js", } // SyntaxFromMIME returns the syntax for a zettel based on MIME encoding value |
︙ | ︙ |
Changes to web/server/impl/http.go.
︙ | ︙ | |||
19 20 21 22 23 24 25 | "net/http" "time" ) // Server timeout values const ( shutdownTimeout = 5 * time.Second | < < < < < < < < < < < < | | < < < < < < < < < | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | "net/http" "time" ) // Server timeout values const ( shutdownTimeout = 5 * time.Second ) // httpServer is a HTTP server. type httpServer struct { http.Server } // initializeHTTPServer creates a new HTTP server object. func (srv *httpServer) initializeHTTPServer(addr string, handler http.Handler) { if addr == "" { addr = ":http" } srv.Server = http.Server{ Addr: addr, Handler: handler, } } // Run starts the web server, but does not wait for its completion. func (srv *httpServer) Run() error { ln, err := net.Listen("tcp", srv.Addr) if err != nil { return err |
︙ | ︙ |
Changes to web/server/impl/impl.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 23 24 25 | import ( "context" "net/http" "time" "t73f.de/r/zsc/api" "zettelstore.de/z/auth" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" | > < | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import ( "context" "net/http" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/meta" "zettelstore.de/z/auth" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) type myServer struct { log *logger.Logger baseURL string server httpServer router httpRouter |
︙ | ︙ | |||
138 139 140 141 142 143 144 | Token: data.Token, Now: data.Now, Issued: data.Issued, Expires: data.Expires, }) } | < | 138 139 140 141 142 143 144 145 146 | Token: data.Token, Now: data.Now, Issued: data.Issued, Expires: data.Expires, }) } func (srv *myServer) Run() error { return srv.server.Run() } func (srv *myServer) Stop() { srv.server.Stop() } |
Changes to web/server/server.go.
︙ | ︙ | |||
16 17 18 19 20 21 22 | import ( "context" "net/http" "time" "t73f.de/r/zsc/api" | | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import ( "context" "net/http" "time" "t73f.de/r/zsc/api" "t73f.de/r/zsc/domain/id" "t73f.de/r/zsc/domain/meta" ) // UserRetriever allows to retrieve user data based on a given zettel identifier. type UserRetriever interface { GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) } |
︙ | ︙ | |||
108 109 110 111 112 113 114 | // Server is the main web server for accessing Zettelstore via HTTP. type Server interface { Router Auth Builder | < | 108 109 110 111 112 113 114 115 116 117 | // Server is the main web server for accessing Zettelstore via HTTP. type Server interface { Router Auth Builder Run() error Stop() } |
Changes to www/build.md.
︙ | ︙ | |||
71 72 73 74 75 76 77 | Zettelstore is managed by the Fossil version control system. Fossil is an alternative to the ubiquitous Git version control system. However, Go seems to prefer Git and popular platforms that just support Git. Some dependencies of Zettelstore, namely [Zettelstore client](https://t73f.de/r/zsc), [webs](https://t73f.de/r/webs), | | | | | 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | Zettelstore is managed by the Fossil version control system. Fossil is an alternative to the ubiquitous Git version control system. However, Go seems to prefer Git and popular platforms that just support Git. Some dependencies of Zettelstore, namely [Zettelstore client](https://t73f.de/r/zsc), [webs](https://t73f.de/r/webs), [sx](https://t73f.de/r/sx), [sxwebs](https://t73f.de/r/sxwebs), and [zero](https://t73f.de/r/zero) are also managed by Fossil. Depending on your development setup, some error messages might occur. If the error message mentions an environment variable called `GOVCS` you should set it to the value `GOVCS=zettelstore.de:fossil` (alternatively more generous to `GOVCS=*:all`). Since the Go build system is coupled with Git and some special platforms, you must allow Go to download a Fossil repository from the host `zettelstore.de`. The build tool sets `GOVCS` to the right value, but you may use other `go` commands that try to download a Fossil repository. |
︙ | ︙ |
Changes to www/changes.wiki.
1 2 3 4 5 6 7 8 9 10 11 | <title>Change Log</title> <a id="0_20"></a> <h2>Changes for Version 0.20.0 (pending)</h2> <a id="0_19"></a> <h2>Changes for Version 0.19.0 (2024-12-13)</h2> * Remove support for renaming zettel, i.e. changing zettel identifier. Was announced as deprecated in version 0.18. (breaking: api, webui) * Format of zettel identifier will be not changed. The deprecation message | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | <title>Change Log</title> <a id="0_20"></a> <h2>Changes for Version 0.20.0 (pending)</h2> * Metadata with keys that have the suffix <code>-title</code> are no longer interpreted as Zettelmarkup. This was a leftover from [#0_11|v0.11], when type of metadata <code>title</code> was changed from Zettelmarkup to a possibly empty string. (breaking) * Type of metadata key <code>summary</code> changed from Zettelmarkup to plain text. You might need to update your zettel that contain this key. (breaking) * Remove support for metadata type <code>Zettelmarkup</code>. It made implementation too complex, and it was seldom used (see above). (breaking) * Remove metadata key <code>created-missing</code>, which was used to support the cancelled migration process into a four letter zettel identifier format. (breaking) * Remove support for inline zettel snippets. Mostyl used for some HTML trinks, which hinders portability and encoding in other formats. (breaking: zettelmarkup) * Remove support for inline HTML text within Markdown text. Such HTML code (did I ever say that Markdown is just a super-set of HTML?) is now translated into literal text. (breaking: markdown/CommonMark) * Query aggregates <code>ATOM</code> and <code>RSS</code> are removed, as well as the accompanying <code>TITLE</code> query action (parameter). Were announced as deprecated in version 0.19. (breaking) * Query data encoding and aggregate data encoding were changed to remove the superfluous <code>(list ...)</code>. Instead, the result list is now spliced into the returned s-expression list. If you use the Zettelstore Client, it will handle that for you. (breaking: api) * “Lists” menu is build by reading a zettel with menu items (Default: <code>00000000080001</code>) instead of being hard coded. Can be customized with runtime and user configuration <code>lists-menu-zettel</code>. (major: webui) * Show metadata values <code>superior</code> and <code>subordinate</code> on WebUI (again). Partially reverses the removal of support for these values in v0.19. However, creating child zettel is not supported. (major: webui) * Implement <code>CONTEXT</code> query more correctly, as stated in the manual; add <code>MIN</code> directive. (minor) * Remove timeouts for API and WebUI. If faced to the public internet, Zettelstore should run behind a full proxy, like Caddy server, Nginx, or similar. (minor: api, webui) * Some smaller bug fixes and improvements, to the software and to the documentation. <a id="0_19"></a> <h2>Changes for Version 0.19.0 (2024-12-13)</h2> * Remove support for renaming zettel, i.e. changing zettel identifier. Was announced as deprecated in version 0.18. (breaking: api, webui) * Format of zettel identifier will be not changed. The deprecation message |
︙ | ︙ | |||
74 75 76 77 78 79 80 | (major: dirbox) * Add expert-mode zettel “Zettelstore Warnings” to help identifying zettel to upgrade for future migration to planned new zettel identifier format. (minor: webui) * Add expert-mode zettel “Zettelstore Identifier Mapping” to show a possible mapping from the old identifier format to the new one. | | | 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | (major: dirbox) * Add expert-mode zettel “Zettelstore Warnings” to help identifying zettel to upgrade for future migration to planned new zettel identifier format. (minor: webui) * Add expert-mode zettel “Zettelstore Identifier Mapping” to show a possible mapping from the old identifier format to the new one. This should help users to possibly rename some zettel for a better mapping. (minor: webui) * Add metadata key <code>created-missing</code> to list zettel without stored metadata key <code>created</code>. Needed for migration to planned new zettelstore identifier format, which is not based on timestamp of zettel creation date. (minor) |
︙ | ︙ |
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.20.0</code> (2025-03-07). * [/uv/zettelstore-0.20.0-linux-amd64.zip|Linux] (amd64) * [/uv/zettelstore-0.20.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore-0.20.0-darwin-arm64.zip|macOS] (arm64) * [/uv/zettelstore-0.20.0-darwin-amd64.zip|macOS] (amd64) * [/uv/zettelstore-0.20.0-windows-amd64.zip|Windows] (amd64) Unzip the appropriate file, install and execute Zettelstore according to the manual. <h2>Zettel for the manual</h2> As a starter, you can download the zettel for the manual [/uv/manual-0.20.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.
︙ | ︙ | |||
22 23 24 25 26 27 28 | software, which often connects to Zettelstore via its API. Some of the software packages may be experimental. * [https://t73f.de/r/sx|Sx] provides an evaluator for symbolic expressions, which is used for HTML templates and more. [https://mastodon.social/tags/Zettelstore|Stay tuned] … <hr> | | | | | | | | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | software, which often connects to Zettelstore via its API. Some of the software packages may be experimental. * [https://t73f.de/r/sx|Sx] provides an evaluator for symbolic expressions, which is used for HTML templates and more. [https://mastodon.social/tags/Zettelstore|Stay tuned] … <hr> <h3>Latest Release: 0.20.0 (2025-03-07)</h3> * [./download.wiki|Download] * [./changes.wiki#0_20|Change summary] * [/timeline?p=v0.20.0&bt=v0.19.0&y=ci|Check-ins for version 0.20], [/vdiff?to=v0.20.0&from=v0.19.0|content diff] * [/timeline?df=v0.20.0&y=ci|Check-ins derived from the 0.20 release], [/vdiff?from=v0.20.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://go.dev/dl/|Go] and some Go-based tools. Please read |
︙ | ︙ |
Deleted zettel/id/digraph.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/digraph_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/edge.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/id.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/id_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/set.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/set_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/slice.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/id/slice_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/collection.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/meta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/meta_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/parse.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/parse_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/type.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/type_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/values.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/write.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted zettel/meta/write_test.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to zettel/zettel.go.
︙ | ︙ | |||
10 11 12 13 14 15 16 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package zettel provides specific types, constants, and functions for zettel. package zettel | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package zettel provides specific types, constants, and functions for zettel. package zettel import "t73f.de/r/zsc/domain/meta" // Zettel is the main data object of a zettelstore. type Zettel struct { Meta *meta.Meta // Some additional meta-data. Content Content // The content of the zettel itself. } // ByteSize returns the number of bytes to store the zettel (in a zettel view, // not in a technical view). func (z Zettel) ByteSize() int { return z.Meta.ByteSize() + z.Content.Length() } // Equal compares two zettel for equality. func (z Zettel) Equal(o Zettel, allowComputed bool) bool { return z.Meta.Equal(o.Meta, allowComputed) && z.Content.Equal(&o.Content) } |