Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.15.0 +0.16.1 Index: ast/inline.go ================================================================== --- ast/inline.go +++ ast/inline.go @@ -193,18 +193,19 @@ type FormatKind int // Constants for FormatCode const ( _ FormatKind = iota - FormatEmph // Emphasized text. - FormatStrong // Strongly emphasized text. - FormatInsert // Inserted text. - FormatDelete // Deleted text. - FormatSuper // Superscripted text. - FormatSub // SubscriptedText. - FormatQuote // Quoted text. - FormatSpan // Generic inline container. + FormatEmph // Emphasized text + FormatStrong // Strongly emphasized text + FormatInsert // Inserted text + FormatDelete // Deleted text + FormatSuper // Superscripted text + FormatSub // SubscriptedText + FormatQuote // Quoted text + FormatMark // Marked text + FormatSpan // Generic inline container ) func (*FormatNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the formatted text. Index: box/compbox/log.go ================================================================== --- box/compbox/log.go +++ box/compbox/log.go @@ -22,11 +22,11 @@ func genLogM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Log") m.Set(api.KeySyntax, meta.SyntaxText) m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) - m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.ZidLayout)) + m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.TimestampLayout)) return m } func genLogC(*meta.Meta) []byte { const tsFormat = "2006-01-02 15:04:05.999999" Index: box/constbox/base.css ================================================================== --- box/constbox/base.css +++ box/constbox/base.css @@ -81,11 +81,11 @@ h3 { font-size:1.15rem; margin:.75rem 0 } h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold } h5 { font-size:1.05rem; margin:.8rem 0 } h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter } p { margin: .5rem 0 0 0 } - p.zs-tag-zettel { margin-top: .5rem; margin-left: 0.5rem } + p.zs-meta-zettel { margin-top: .5rem; margin-left: 0.5rem } li,figure,figcaption,dl { margin: 0 } dt { margin: .5rem 0 0 0 } dt+dd { margin-top: 0 } dd { margin: .5rem 0 0 2rem } dd > p:first-child { margin: 0 0 0 0 } Index: box/constbox/constbox.go ================================================================== --- box/constbox/constbox.go +++ box/constbox/constbox.go @@ -4,10 +4,13 @@ // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package constbox puts zettel inside the executable. package constbox @@ -165,11 +168,11 @@ api.KeyTitle: "Zettelstore Dependencies", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityLogin, + api.KeyVisibility: api.ValueVisibilityPublic, api.KeyCreated: "20210504135842", api.KeyModified: "20230601163100", }, zettel.NewContent(contentDependencies)}, id.BaseTemplateZid: { @@ -196,11 +199,11 @@ constHeader{ api.KeyTitle: "Zettelstore Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230510155300", - api.KeyModified: "20230907203300", + api.KeyModified: "20231126180500", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentZettelSxn)}, id.InfoTemplateZid: { constHeader{ @@ -246,11 +249,11 @@ constHeader{ api.KeyTitle: "Zettelstore List Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230704122100", - api.KeyModified: "20231002120600", + api.KeyModified: "20231129112800", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentListZettelSxn)}, id.ErrorTemplateZid: { constHeader{ @@ -276,11 +279,11 @@ constHeader{ api.KeyTitle: "Zettelstore Sxn Base Code", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230619132800", - api.KeyModified: "20231012154500", + api.KeyModified: "20231228172200", api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityExpert, api.KeyPrecursor: string(api.ZidSxnPrelude), }, zettel.NewContent(contentBaseCodeSxn)}, @@ -288,11 +291,11 @@ constHeader{ api.KeyTitle: "Zettelstore Sxn Prelude", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20231006181700", - api.KeyModified: "20231019140400", + api.KeyModified: "20231228171900", api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentPreludeSxn)}, id.MustParse(api.ZidBaseCSS): { @@ -299,10 +302,11 @@ constHeader{ api.KeyTitle: "Zettelstore Base CSS", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxCSS, api.KeyCreated: "20200804111624", + api.KeyModified: "20231129112800", api.KeyVisibility: api.ValueVisibilityPublic, }, zettel.NewContent(contentBaseCSS)}, id.MustParse(api.ZidUserCSS): { constHeader{ @@ -328,10 +332,11 @@ api.KeyTitle: "New Menu", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyCreated: "20210217161829", + api.KeyModified: "20231129111800", api.KeyVisibility: api.ValueVisibilityCreator, }, zettel.NewContent(contentNewTOCZettel)}, id.MustParse(api.ZidTemplateNewZettel): { constHeader{ @@ -341,10 +346,21 @@ api.KeyCreated: "20201028185209", api.KeyModified: "20230929132900", meta.NewPrefix + api.KeyRole: api.ValueRoleZettel, api.KeyVisibility: api.ValueVisibilityCreator, }, + zettel.NewContent(nil)}, + id.MustParse(api.ZidTemplateNewRole): { + constHeader{ + api.KeyTitle: "New Role", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20231129110800", + meta.NewPrefix + api.KeyRole: api.ValueRoleRole, + meta.NewPrefix + api.KeyTitle: "", + api.KeyVisibility: api.ValueVisibilityCreator, + }, zettel.NewContent(nil)}, id.MustParse(api.ZidTemplateNewTag): { constHeader{ api.KeyTitle: "New Tag", api.KeyRole: api.ValueRoleConfiguration, @@ -365,10 +381,50 @@ meta.NewPrefix + api.KeyUserID: "", meta.NewPrefix + api.KeyUserRole: api.ValueUserRoleReader, api.KeyVisibility: api.ValueVisibilityOwner, }, zettel.NewContent(nil)}, + id.MustParse(api.ZidRoleZettelZettel): { + constHeader{ + api.KeyTitle: api.ValueRoleZettel, + api.KeyRole: api.ValueRoleRole, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20231129161400", + api.KeyLang: api.ValueLangEN, + api.KeyVisibility: api.ValueVisibilityLogin, + }, + zettel.NewContent(contentRoleZettel)}, + id.MustParse(api.ZidRoleConfigurationZettel): { + constHeader{ + api.KeyTitle: api.ValueRoleConfiguration, + api.KeyRole: api.ValueRoleRole, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20231129162800", + api.KeyLang: api.ValueLangEN, + api.KeyVisibility: api.ValueVisibilityLogin, + }, + zettel.NewContent(contentRoleConfiguration)}, + id.MustParse(api.ZidRoleRoleZettel): { + constHeader{ + api.KeyTitle: api.ValueRoleRole, + api.KeyRole: api.ValueRoleRole, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20231129162900", + api.KeyLang: api.ValueLangEN, + api.KeyVisibility: api.ValueVisibilityLogin, + }, + zettel.NewContent(contentRoleRole)}, + id.MustParse(api.ZidRoleTagZettel): { + constHeader{ + api.KeyTitle: api.ValueRoleTag, + api.KeyRole: api.ValueRoleRole, + api.KeySyntax: meta.SyntaxZmk, + api.KeyCreated: "20231129162000", + api.KeyLang: api.ValueLangEN, + api.KeyVisibility: api.ValueVisibilityLogin, + }, + zettel.NewContent(contentRoleTag)}, id.DefaultHomeZid: { constHeader{ api.KeyTitle: "Home", api.KeyRole: api.ValueRoleZettel, api.KeySyntax: meta.SyntaxZmk, @@ -429,8 +485,20 @@ //go:embed emoji_spin.gif var contentEmoji []byte //go:embed newtoc.zettel var contentNewTOCZettel []byte + +//go:embed rolezettel.zettel +var contentRoleZettel []byte + +//go:embed roleconfiguration.zettel +var contentRoleConfiguration []byte + +//go:embed rolerole.zettel +var contentRoleRole []byte + +//go:embed roletag.zettel +var contentRoleTag []byte //go:embed home.zettel var contentHomeZettel []byte Index: box/constbox/listzettel.sxn ================================================================== --- box/constbox/listzettel.sxn +++ box/constbox/listzettel.sxn @@ -1,14 +1,20 @@ `(article (header (h1 ,heading)) (form (@ (action ,search-url)) (input (@ (class "zs-input") (type "text") (placeholder "Search..") (name ,query-key-query) (value ,query-value) (dir "auto")))) ,@(if (bound? 'tag-zettel) - `((p (@ (class "zs-tag-zettel")) "Tag zettel: " ,@tag-zettel)) + `((p (@ (class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel)) ) ,@(if (bound? 'create-tag-zettel) - `((p (@ (class "zs-tag-zettel")) "Create tag zettel: " ,@create-tag-zettel)) + `((p (@ (class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel)) + ) + ,@(if (bound? 'role-zettel) + `((p (@ (class "zs-meta-zettel")) "Role zettel: " ,@role-zettel)) + ) + ,@(if (bound? 'create-role-zettel) + `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel)) ) ,@content ,@endnotes (form (@ (action ,(if (bound? 'create-url) create-url))) "Other encodings: " Index: box/constbox/newtoc.zettel ================================================================== --- box/constbox/newtoc.zettel +++ box/constbox/newtoc.zettel @@ -1,5 +1,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 Template|00000000090003]] +* [[New Role|00000000090004]] +* [[New Tag|00000000090003]] * [[New User|00000000090002]] Index: box/constbox/prelude.sxn ================================================================== --- box/constbox/prelude.sxn +++ box/constbox/prelude.sxn @@ -4,23 +4,31 @@ ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- -;;; This zettel contains all sxn definitions that are independent of specific +;;; This zettel contains sxn definitions that are independent of specific ;;; subsystems, such as WebUI, API, or other. It just contains generic code to -;;; be used elsewhere. +;;; be used in all places. ;; Constants NIL and T (defconst NIL ()) (defconst T 'T) +;; defunconst macro to define functions that are bound as a constant. +;; +;; (defunconst NAME ARGS EXPR ...) +(defmacro defunconst (name args . body) + `(begin (defun ,name ,args ,@body) (defconst ,name ,name))) + ;; Function not -(defun not (x) (if x NIL T)) -(defconst not not) +(defunconst not (x) (if x NIL T)) ;; let macro ;; ;; (let (BINDING ...) EXPR ...), where BINDING is a list of two elements ;; (SYMBOL EXPR) ADDED box/constbox/roleconfiguration.zettel Index: box/constbox/roleconfiguration.zettel ================================================================== --- /dev/null +++ box/constbox/roleconfiguration.zettel @@ -0,0 +1,20 @@ +Zettel with role ""configuration"" are used within Zettelstore to manage and to show the current configuration of the software. + +Typically, there are some public zettel that show the license of this software, its dependencies, some CSS code to make the default web user interface a litte bit nicer, and the defult image to singal a broken image. + +Other zettel are only visible if an user has authenticated itself, or if there is no authentication enabled. +In this case, one additional configuration zettel is the zettel containing the version number of this software. +Other zettel are showing the supported metadata keys and supported syntax values. +Zettel that allow to configure the menu of template to create new zettel are also using the role ""configuration"". + +Most important is the zettel that contains the runtime configuration. +You may change its metadata value to change the behaviour of the software. + +One configuration is the ""expert mode"". +If enabled, and if you are authorized so see them, you will discover some more zettel. +For example, HTML templates to customize the default web user interface, to show the application log, to see statistics about zettel boxes, to show the host name and it operating system, and many more. + +You are allowed to add your own configuration zettel, for example if you want to customize the look and feel of zettel by placing relevant data into your own zettel. + +By default, user zettel (for authentification) use also the role ""configuration"". +However, you are allowed to change this. ADDED box/constbox/rolerole.zettel Index: box/constbox/rolerole.zettel ================================================================== --- /dev/null +++ box/constbox/rolerole.zettel @@ -0,0 +1,10 @@ +A zettel with the role ""role"" describes a specific role. +The described role must be the title of such a zettel. + +This zettel is such a zettel, as it describes the meaning of the role ""role"". +Therefore it has the title ""role"" too. +If you like, this zettel is a meta-role. + +You are free to create your own role-describing zettel. +For example, you want to document the intended meaning of the role. +You might also be interested to describe needed metadata so that some software is enabled to analyse or to process your zettel. ADDED box/constbox/roletag.zettel Index: box/constbox/roletag.zettel ================================================================== --- /dev/null +++ box/constbox/roletag.zettel @@ -0,0 +1,6 @@ +A zettel with role ""tag"" is a zettel that describes specific tag. +The tag name must be the title of such a zettel. + +Such zettel are similar to this specific zettel: this zettel describes zettel with a role ""tag"". +These zettel with the role ""tag"" describe specific tags. +These might form a hierarchy of meta-tags (and meta-roles). ADDED box/constbox/rolezettel.zettel Index: box/constbox/rolezettel.zettel ================================================================== --- /dev/null +++ box/constbox/rolezettel.zettel @@ -0,0 +1,7 @@ +A zettel with the role ""zettel"" is typically used to document your own thoughts. +Such zettel are the main reason to use the software Zettelstore. + +The only predefined zettel with the role ""zettel"" is the [[default home zettel|00010000000000]], which contains some welcome information. + +You are free to change this. +In this case you should modify this zettel too, so that it reflects your own use of zettel with the role ""zettel"". Index: box/constbox/wuicode.sxn ================================================================== --- box/constbox/wuicode.sxn +++ box/constbox/wuicode.sxn @@ -4,66 +4,69 @@ ;;; This file is part of Zettelstore. ;;; ;;; Zettelstore is licensed under the latest version of the EUPL (European ;;; Union Public License). Please see file LICENSE.txt for your rights and ;;; obligations under this license. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- ;; Contains WebUI specific code, but not related to a specific template. ;; wui-list-item returns the argument as a HTML list item. -(defun wui-item (s) `(li ,s)) +(defunconst wui-item (s) `(li ,s)) ;; wui-table-row takes a pair and translates it into a HTML table row with ;; two columns. -(defun wui-table-row (p) +(defunconst wui-table-row (p) `(tr (td ,(car p)) (td ,(cdr p)))) ;; wui-valid-link translates a local link into a HTML link. A link is a pair ;; (valid . url). If valid is not truish, only the invalid url is returned. -(defun wui-valid-link (l) +(defunconst wui-valid-link (l) (if (car l) `(li (a (@ (href ,(cdr l))) ,(cdr l))) `(li ,(cdr l)))) ;; wui-link takes a link (title . url) and returns a HTML reference. -(defun wui-link (q) +(defunconst wui-link (q) `(a (@ (href ,(cdr q))) ,(car q))) ;; wui-item-link taks a pair (text . url) and returns a HTML link inside ;; a list item. -(defun wui-item-link (q) `(li ,(wui-link q))) +(defunconst wui-item-link (q) `(li ,(wui-link q))) ;; wui-tdata-link taks a pair (text . url) and returns a HTML link inside ;; a table data item. -(defun wui-tdata-link (q) `(td ,(wui-link q))) +(defunconst wui-tdata-link (q) `(td ,(wui-link q))) ;; wui-item-popup-link is like 'wui-item-link, but the HTML link will open ;; a new tab / window. -(defun wui-item-popup-link (e) +(defunconst wui-item-popup-link (e) `(li (a (@ (href ,e) (target "_blank") (rel "noopener noreferrer")) ,e))) ;; wui-option-value returns a value for an HTML option element. -(defun wui-option-value (v) `(option (@ (value ,v)))) +(defunconst wui-option-value (v) `(option (@ (value ,v)))) ;; wui-datalist returns a HTML datalist with the given HTML identifier and a ;; list of values. -(defun wui-datalist (id lst) +(defunconst wui-datalist (id lst) (if lst `((datalist (@ (id ,id)) ,@(map wui-option-value lst))))) ;; wui-pair-desc-item takes a pair '(term . text) and returns a list with ;; a HTML description term and a HTML description data. -(defun wui-pair-desc-item (p) `((dt ,(car p)) (dd ,(cdr p)))) +(defunconst wui-pair-desc-item (p) `((dt ,(car p)) (dd ,(cdr p)))) ;; wui-meta-desc returns a HTML description list made from the list of pairs ;; given. -(defun wui-meta-desc (l) +(defunconst wui-meta-desc (l) `(dl ,@(apply append (map wui-pair-desc-item l)))) ;; wui-enc-matrix returns the HTML table of all encodings and parts. -(defun wui-enc-matrix (matrix) +(defunconst wui-enc-matrix (matrix) `(table ,@(map (lambda (row) `(tr (th ,(car row)) ,@(map wui-tdata-link (cdr row)))) matrix))) @@ -83,14 +86,14 @@ ) ) ) ) -;;; ACTION-SEPARATOR defines a HTML value that separates actions links. +;; ACTION-SEPARATOR defines a HTML value that separates actions links. (defvar ACTION-SEPARATOR '(@H " · ")) -;;; ROLE-DEFAULT-actions returns the default text for actions. +;; ROLE-DEFAULT-actions returns the default text for actions. (defun ROLE-DEFAULT-actions (env) `(,@(let ((copy-url (environment-lookup 'copy-url env))) (if (defined? copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy")))) ,@(let ((version-url (environment-lookup 'version-url env))) (if (defined? version-url) `((@H " · ") (a (@ (href ,version-url)) "Version")))) @@ -99,11 +102,11 @@ ,@(let ((folge-url (environment-lookup 'folge-url env))) (if (defined? folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge")))) ) ) -;;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag". +;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag". (defun ROLE-tag-actions (env) `(,@(ROLE-DEFAULT-actions env) ,@(let ((title (environment-lookup 'title env))) (if (and (defined? title) title) `(,ACTION-SEPARATOR (a (@ (href ,(query->url (string-append "tags:" title)))) "Zettel")) @@ -110,15 +113,15 @@ ) ) ) ) -;;; ROLE-DEFAULT-heading returns the default text for headings, below the -;;; references of a zettel. In most cases it should be called from an -;;; overwriting function. +;; ROLE-DEFAULT-heading returns the default text for headings, below the +;; references of a zettel. In most cases it should be called from an +;; overwriting function. (defun ROLE-DEFAULT-heading (env) `(,@(let ((meta-url (environment-lookup 'meta-url env))) (if (defined? meta-url) `((br) "URL: " ,(url-to-html meta-url)))) ,@(let ((meta-author (environment-lookup 'meta-author env))) (if (and (defined? meta-author) meta-author) `((br) "By " ,meta-author))) ) ) Index: box/constbox/zettel.sxn ================================================================== --- box/constbox/zettel.sxn +++ box/constbox/zettel.sxn @@ -19,12 +19,12 @@ ) ,@content ,endnotes ,@(if (or folge-links subordinate-links back-links successor-links) `((nav - ,@(if folge-links `((details (@ (open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links))))) - ,@(if subordinate-links `((details (@ (open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links))))) - ,@(if back-links `((details (@ (open)) (summary "Incoming") (ul ,@(map wui-item-link back-links))))) - ,@(if successor-links `((details (@ (open)) (summary "Successors") (ul ,@(map wui-item-link successor-links))))) + ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-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))))) + ,@(if successor-links `((details (@ (,successor-open)) (summary "Successors") (ul ,@(map wui-item-link successor-links))))) )) ) ) Index: box/manager/anteroom.go ================================================================== --- box/manager/anteroom.go +++ box/manager/anteroom.go @@ -23,20 +23,18 @@ arReload arZettel ) type anteroom struct { - num uint64 next *anteroom waiting id.Set curLoad int reload bool } type anteroomQueue struct { mx sync.Mutex - nextNum uint64 first *anteroom last *anteroom maxLoad int } @@ -71,46 +69,38 @@ ar.last.next = room ar.last = room } func (ar *anteroomQueue) makeAnteroom(zid id.Zid) *anteroom { - ar.nextNum++ - if zid == id.Invalid { - return &anteroom{num: ar.nextNum, next: nil, waiting: nil, curLoad: 0, reload: true} - } - c := ar.maxLoad - if c == 0 { - c = 100 - } - waiting := id.NewSetCap(ar.maxLoad, zid) - return &anteroom{num: ar.nextNum, next: nil, waiting: waiting, curLoad: 1, reload: false} + if zid == id.Invalid { + panic(zid) + } + waiting := id.NewSetCap(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 = ar.makeAnteroom(id.Invalid) + ar.first = &anteroom{next: nil, waiting: nil, curLoad: 0, reload: true} ar.last = ar.first } -func (ar *anteroomQueue) Reload(allZids id.Set) uint64 { +func (ar *anteroomQueue) Reload(allZids id.Set) { ar.mx.Lock() defer ar.mx.Unlock() ar.deleteReloadedRooms() if ns := len(allZids); ns > 0 { - ar.nextNum++ - ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: allZids, curLoad: ns, reload: true} + ar.first = &anteroom{next: ar.first, waiting: allZids, curLoad: ns, reload: true} if ar.first.next == nil { ar.last = ar.first } - return ar.nextNum + } else { + ar.first = nil + ar.last = nil } - - ar.first = nil - ar.last = nil - return 0 } func (ar *anteroomQueue) deleteReloadedRooms() { room := ar.first for room != nil && room.reload { @@ -120,34 +110,32 @@ if room == nil { ar.last = nil } } -func (ar *anteroomQueue) Dequeue() (arAction, id.Zid, uint64) { +func (ar *anteroomQueue) Dequeue() (arAction, id.Zid, bool) { ar.mx.Lock() defer ar.mx.Unlock() first := ar.first - if first == nil { - return arNothing, id.Invalid, 0 - } - roomNo := first.num - if first.waiting == nil && first.reload { - ar.removeFirst() - return arReload, id.Invalid, roomNo - } - for zid := range first.waiting { - delete(first.waiting, zid) - if len(first.waiting) == 0 { - ar.removeFirst() - } - return arZettel, zid, roomNo - } - ar.removeFirst() - return arNothing, id.Invalid, 0 + if first != nil { + if first.waiting == nil && first.reload { + ar.removeFirst() + return arReload, id.Invalid, false + } + for zid := range first.waiting { + delete(first.waiting, zid) + if len(first.waiting) == 0 { + ar.removeFirst() + } + return arZettel, zid, first.reload + } + ar.removeFirst() + } + return arNothing, id.Invalid, false } func (ar *anteroomQueue) removeFirst() { ar.first = ar.first.next if ar.first == nil { ar.last = nil } } Index: box/manager/anteroom_test.go ================================================================== --- box/manager/anteroom_test.go +++ box/manager/anteroom_test.go @@ -18,13 +18,13 @@ func TestSimple(t *testing.T) { t.Parallel() ar := newAnteroomQueue(2) ar.EnqueueZettel(id.Zid(1)) - action, zid, rno := ar.Dequeue() - if zid != id.Zid(1) || action != arZettel || rno != 1 { - t.Errorf("Expected arZettel/1/1, but got %v/%v/%v", action, zid, rno) + action, zid, lastReload := ar.Dequeue() + if zid != id.Zid(1) || action != arZettel || lastReload { + t.Errorf("Expected arZettel/1/false, but got %v/%v/%v", action, zid, lastReload) } _, zid, _ = ar.Dequeue() if zid != id.Invalid { t.Errorf("Expected invalid Zid, but got %v", zid) } Index: box/manager/indexer.go ================================================================== --- box/manager/indexer.go +++ box/manager/indexer.go @@ -91,25 +91,21 @@ } } } func (mgr *Manager) idxWorkService(ctx context.Context) { - var roomNum uint64 var start time.Time for { - switch action, zid, arRoomNum := mgr.idxAr.Dequeue(); action { + switch action, zid, lastReload := mgr.idxAr.Dequeue(); action { case arNothing: return case arReload: mgr.idxLog.Debug().Msg("reload") - roomNum = 0 zids, err := mgr.FetchZids(ctx) if err == nil { start = time.Now() - if rno := mgr.idxAr.Reload(zids); rno > 0 { - roomNum = rno - } + mgr.idxAr.Reload(zids) mgr.idxMx.Lock() mgr.idxLastReload = time.Now().Local() mgr.idxSinceReload = 0 mgr.idxMx.Unlock() } @@ -117,24 +113,21 @@ mgr.idxLog.Debug().Zid(zid).Msg("zettel") zettel, err := mgr.GetZettel(ctx, zid) if err != nil { // Zettel was deleted or is not accessible b/c of other reasons mgr.idxLog.Trace().Zid(zid).Msg("delete") - mgr.idxMx.Lock() - mgr.idxSinceReload++ - mgr.idxMx.Unlock() mgr.idxDeleteZettel(ctx, zid) continue } mgr.idxLog.Trace().Zid(zid).Msg("update") + mgr.idxUpdateZettel(ctx, zettel) mgr.idxMx.Lock() - if arRoomNum == roomNum { + if lastReload { mgr.idxDurReload = time.Since(start) } mgr.idxSinceReload++ mgr.idxMx.Unlock() - mgr.idxUpdateZettel(ctx, zettel) } } } func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool { @@ -189,16 +182,30 @@ case meta.TypeURL: if _, err := url.Parse(pair.Value); err == nil { cData.urls.Add(pair.Value) } default: - for _, word := range strfun.NormalizeWords(pair.Value) { - cData.words.Add(word) + if descr.Type.IsSet { + for _, val := range meta.ListFromValue(pair.Value) { + idxCollectMetaValue(cData.words, val) + } + } else { + idxCollectMetaValue(cData.words, pair.Value) } } } } + +func idxCollectMetaValue(stWords store.WordSet, value string) { + if words := strfun.NormalizeWords(value); len(words) > 0 { + for _, word := range words { + stWords.Add(word) + } + } else { + stWords.Add(value) + } +} func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) { for ref := range cData.refs { if mgr.HasZettel(ctx, ref) { zi.AddBackRef(ref) Index: box/manager/memstore/memstore.go ================================================================== --- box/manager/memstore/memstore.go +++ box/manager/memstore/memstore.go @@ -268,13 +268,13 @@ } return back } func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set { - m := ms.makeMeta(zidx) 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 } Index: box/notify/fsdir.go ================================================================== --- box/notify/fsdir.go +++ box/notify/fsdir.go @@ -165,11 +165,11 @@ } if ev.Has(fsnotify.Create) { err := fsdn.base.Add(fsdn.path) if err != nil { - fsdn.log.IfErr(err).Str("name", fsdn.path).Msg("Unable to add directory") + fsdn.log.Error().Err(err).Str("name", fsdn.path).Msg("Unable to add directory") select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: fsdn.log.Trace().Int("i", 2).Msg("done dir event processing") return false Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -41,11 +41,11 @@ Content: zettel.NewContent(inp.Src[inp.Pos:]), }, m.GetDefault(api.KeySyntax, meta.SyntaxZmk), nil, ) - encdr := encoder.Create(api.Encoder(enc)) + encdr := encoder.Create(api.Encoder(enc), &encoder.CreateParameter{Lang: m.GetDefault(api.KeyLang, api.ValueLangEN)}) if encdr == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc) return 2, nil } _, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata) Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -65,10 +65,11 @@ ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) ucQuery := usecase.NewQuery(protectedBoxManager) ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery) ucQuery.SetEvaluate(&ucEvaluate) ucTagZettel := usecase.NewTagZettel(protectedBoxManager, &ucQuery) + ucRoleZettel := usecase.NewRoleZettel(protectedBoxManager, &ucQuery) ucListSyntax := usecase.NewListSyntax(protectedBoxManager) ucListRoles := usecase.NewListRoles(protectedBoxManager) ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager) ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager) ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager) @@ -103,11 +104,11 @@ webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete)) webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax)) webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate)) } webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh)) - webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(&ucQuery, &ucTagZettel, &ucReIndex)) + webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex)) webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetZettel)) webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler()) webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler( ucParseZettel, &ucEvaluate, ucGetZettel, ucGetAllZettel, &ucQuery)) @@ -115,11 +116,11 @@ // API webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler()) webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion)) webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh)) - webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery, &ucTagZettel, &ucReIndex)) + webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex)) webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate)) if !authManager.IsReadonly() { webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel)) webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate)) webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) Index: config/config.go ================================================================== --- config/config.go +++ config/config.go @@ -17,12 +17,16 @@ "zettelstore.de/z/zettel/meta" ) // Key values that are supported by Config.Get const ( - KeyFooterZettel = "footer-zettel" - KeyHomeZettel = "home-zettel" + KeyFooterZettel = "footer-zettel" + KeyHomeZettel = "home-zettel" + KeyShowBackLinks = "show-back-links" + KeyShowFolgeLinks = "show-folge-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 { Index: docs/manual/00001000000000.zettel ================================================================== --- docs/manual/00001000000000.zettel +++ docs/manual/00001000000000.zettel @@ -1,12 +1,13 @@ id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk -created: 00010101000000 -modified: 20231002143058 +created: 20210301190630 +modified: 20231125185455 +show-back-links: false * [[Introduction|00001001000000]] * [[Design goals|00001002000000]] * [[Installation|00001003000000]] * [[Configuration|00001004000000]] Index: docs/manual/00001004020000.zettel ================================================================== --- docs/manual/00001004020000.zettel +++ docs/manual/00001004020000.zettel @@ -2,11 +2,12 @@ title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20230807171016 +modified: 20231126180829 +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. @@ -18,17 +19,20 @@ Default: (the empty string). ; [!default-license|''default-license''] : License value to be used when rendering content. Can be overwritten in a zettel with [[meta key|00001006020000]] ''license''. + Default: (the empty string). ; [!default-visibility|''default-visibility''] : Visibility to be used, if zettel does not specify a value for the [[''visibility''|00001006020000#visibility]] metadata key. + Default: ""login"". ; [!expert-mode|''expert-mode''] : If set to a [[boolean true value|00001006030500]], all zettel with [[visibility ""expert""|00001010070200]] will be shown (to the owner, if [[authentication is enabled|00001010040100]]; to all, otherwise). This affects most computed zettel. + Default: ""False"". ; [!footer-zettel|''footer-zettel''] : Identifier of a zettel that is rendered as HTML and will be placed as the footer of every zettel in the [[web user interface|00001014000000]]. 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. @@ -52,17 +56,34 @@ Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]]. ; [!max-transclusions|''max-transclusions''] : Maximum number of indirect transclusion. This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]]. + Default: ""1024"". +; [!show-back-links|''show-back-links''], [!show-folge-links|''show-folge-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]], [[''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: """". + + May be [[overwritten|00001004020200]] in a user zettel, so that setting will only affect the given user. + Alternatively, it may be overwritten in a zettel, so that that the setting will affect only the given zettel. + + This zettel is an example of a zettel that sets ''show-back-links'' to ""false"". ; [!site-name|''site-name''] : Name of the Zettelstore instance. Will be used when displaying some lists. + Default: ""Zettelstore"". ; [!yaml-header|''yaml-header''] : If [[true|00001006030500]], metadata and content will be separated by ''---\\n'' instead of an empty line (''\\n\\n''). + Default: ""False"". You will probably use this key, if you are working with another software processing [[Markdown|https://daringfireball.net/projects/markdown/]] that uses a subset of [[YAML|https://yaml.org/]] to specify metadata. ; [!zettel-file-syntax|''zettel-file-syntax''] : If you create a new zettel with a syntax different to ""zmk"", Zettelstore will store the zettel as two files: Index: docs/manual/00001004020200.zettel ================================================================== --- docs/manual/00001004020200.zettel +++ docs/manual/00001004020200.zettel @@ -2,11 +2,11 @@ title: Runtime configuration data that may be user specific or zettel specific role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20221205155521 -modified: 20230317183403 +modified: 20231126180752 Some metadata of the [[runtime configuration|00001004020000]] may be overwritten in an [[user zettel|00001010040200]]. A subset of those may be overwritten in zettel that is currently used. This allows to specify user specific or zettel specific behavior. @@ -14,5 +14,9 @@ |=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 +|[[''show-back-links''|00001004020000#show-back-links]]|Y|Y| +|[[''show-folge-links''|00001004020000#show-folge-links]]|Y|Y| +|[[''show-subordinate-links''|00001004020000#show-subordinate-links]]|Y|Y| +|[[''show-successor-links''|00001004020000#show-successor-links]]|Y|Y| Index: docs/manual/00001005090000.zettel ================================================================== --- docs/manual/00001005090000.zettel +++ docs/manual/00001005090000.zettel @@ -2,11 +2,11 @@ title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 -modified: 20231002104819 +modified: 20231129173425 The following table lists all predefined zettel with their purpose. |= Identifier :|= Title | Purpose | [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore @@ -33,14 +33,19 @@ | [[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 -| [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu +| [[00000000060010]] | zettel | [[Role zettel|00001012051800]] for the role ""[[zettel|00001006020100#zettel]]"" +| [[00000000060020]] | confguration | [[Role zettel|00001012051800]] for the role ""[[confguration|00001006020100#confguration]]"" +| [[00000000060030]] | role | [[Role zettel|00001012051800]] for the role ""[[role|00001006020100#role]]"" +| [[00000000060040]] | tag | [[Role zettel|00001012051800]] for the role ""[[tag|00001006020100#tag]]"" +| [[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]] | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. **Important:** All identifier may change until a stable version of the software is released. Index: docs/manual/00001006020100.zettel ================================================================== --- docs/manual/00001006020100.zettel +++ docs/manual/00001006020100.zettel @@ -1,28 +1,34 @@ id: 00001006020100 title: Supported Zettel Roles role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk -created: 00010101000000 -modified: 20230829233016 +created: 20210126175322 +modified: 20231129173620 The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing. You are free to define your own roles. It is allowed to set an empty value or to omit the role. Some roles are defined for technical reasons: ; [!configuration|''configuration''] -: A zettel that contains some configuration data for the Zettelstore. +: 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 hierarchiy 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 hierarchiy 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''] : A small note, to remember something. @@ -29,9 +35,9 @@ Notes are not real zettel, they just help to create a real zettel. Think of them as Post-it notes. ; [!literature|''literature''] : Contains some remarks about a book, a paper, a web page, etc. You should add a citation key for citing it. -; [!zettel|''zettel''] -: A real zettel that contains your own thoughts. +; ''zettel'' +: (as described above) However, you are free to define additional roles, e.g. ''material'' for literature that is web-based only, ''slide'' for presentation slides, ''paper'' for the text of a scientific paper, ''project'' to define a project, ... Index: docs/manual/00001006034500.zettel ================================================================== --- docs/manual/00001006034500.zettel +++ docs/manual/00001006034500.zettel @@ -2,34 +2,36 @@ title: Timestamp Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230612183509 +modified: 20231030182858 Values of this type denote a point in time. === Allowed values -Must be a sequence of 14 digits (""0""--""9"") (same as an [[Identifier|00001006032000]]), with the restriction that is conforms to the pattern ""YYYYMMDDhhmmss"". +Must be a sequence of 4, 6, 8, 10, 12, or 14 digits (""0""--""9"") (similar to an [[Identifier|00001006032000]]), with the restriction that it must conform to the pattern ""YYYYMMDDhhmmss"". * YYYY is the year, * MM is the month, * DD is the day, * hh is the hour, * mm is the minute, * ss is the second. +If the sequence is less than 14 digits, they are expanded with the following rule: + +* YYYY is expanded to YYYY0101000000 +* YYYYMM is expanded to YYYYMM01000000 +* YYYYMMDD is expanded to YYYYMMDD000000 +* YYYYMMDDhh is expanded to YYYYMMDDhh0000 +* YYYYMMDDhhmm is expanded to YYYYMMDDhhmm00 + === Query comparison [[Search values|00001007706000]] with more than 14 characters are truncated to contain exactly 14 characters. - -When the [[search operators|00001007705000]] ""less"", ""not less"", ""greater"", and ""not greater"" are given, the length of the search value is checked. -If it contains less than 14 digits, zero digits (""0"") are appended, until it contains exactly 14 digits. - -All other comparisons assume that up to 14 characters are given. +Then, they are treated as timestamp data, as describe above, if they contain 4, 6, 8, 10, or 12 digits. Comparison is done through the string representation. In case of the search operators ""less"", ""not less"", ""greater"", and ""not greater"", this is the same as a numerical comparison. === Sorting -Sorting is done by comparing the [[String|00001006033500]] values. - -If both values are timestamp values, this works well because both have the same length. +Sorting is done by comparing the possibly expanded values. Index: docs/manual/00001007040100.zettel ================================================================== --- docs/manual/00001007040100.zettel +++ docs/manual/00001007040100.zettel @@ -1,11 +1,12 @@ id: 00001007040100 title: Zettelmarkup: Text Formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -modified: 20220218131003 +created: 20210126175322 +modified: 20231113191353 Text formatting is the way to make your text visually different. Every text formatting element begins with two same characters. It ends when these two same characters occur the second time. It is possible that some [[attributes|00001007050000]] follow immediately, without any separating character. @@ -27,7 +28,10 @@ * 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) mark some text that should belong together. It fills a similar role as [[region blocks|00001007030800]], but just for inline elements. ** Example: ``abc ::def::{=example} ghi`` is rendered in HTML as: abc ::def::{=example} ghi. Index: docs/manual/00001007800000.zettel ================================================================== --- docs/manual/00001007800000.zettel +++ docs/manual/00001007800000.zettel @@ -1,18 +1,19 @@ id: 00001007800000 title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk -modified: 20220810095559 +created: 20220805150154 +modified: 20231113191330 The following table gives an overview about the use of all characters that begin a markup element. |= Character :|= [[Blocks|00001007030000]] <|= [[Inlines|00001007040000]] < | ''!'' | (free) | (free) | ''"'' | [[Verse block|00001007030700]] | [[Short inline quote|00001007040100]] -| ''#'' | [[Ordered list|00001007030200]] | [[Tag|00001007040000]] +| ''#'' | [[Ordered list|00001007030200]] | [[marked / highlighted text|00001007040100]] | ''$'' | (reserved) | (reserved) | ''%'' | [[Comment block|00001007030900]] | [[Comment|00001007040000]] | ''&'' | (free) | [[Entity|00001007040000]] | ''\''' | (free) | [[Computer input|00001007040200]] | ''('' | (free) | (free) Index: docs/manual/00001012000000.zettel ================================================================== --- docs/manual/00001012000000.zettel +++ docs/manual/00001012000000.zettel @@ -2,11 +2,11 @@ title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20230928183244 +modified: 20231128183617 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. @@ -23,10 +23,11 @@ === Zettel lists * [[List all zettel|00001012051200]] * [[Query the list of all zettel|00001012051400]] * [[Determine a tag zettel|00001012051600]] +* [[Determine a role zettel|00001012051800]] === Working with zettel * [[Create a new zettel|00001012053200]] * [[Retrieve metadata and content of an existing zettel|00001012053300]] * [[Retrieve metadata of an existing zettel|00001012053400]] ADDED docs/manual/00001012051800.zettel Index: docs/manual/00001012051800.zettel ================================================================== --- /dev/null +++ docs/manual/00001012051800.zettel @@ -0,0 +1,71 @@ +id: 00001012051800 +title: API: Determine a role zettel +role: manual +tags: #api #manual #zettelstore +syntax: zmk +created: 20231128183917 +modified: 20231128184701 + +The [[endpoint|00001012920000]] ''/z'' also allows you to determine a ""role zettel"", i.e. a zettel that documents a given role. + +The query parameter ""''role''"" allows you to specify a value that is interpreted as the name of a role. +Zettelstore tries to determine the corresponding role zettel. + +A role zettel is a zettel with the [[''role''|00001006020100]] value ""role"" and a title that names the role. +If there is more than one zettel that qualifies, the zettel with the highest zettel identifier is used. + +For example, if you want to determine the role zettel for the role ""manual"", your request will be: +```sh +# curl -i 'http://127.0.0.1:23123/z?role=manual' +HTTP/1.1 302 Found +Content-Type: text/plain; charset=utf-8 +Location: /z/20231128184200 +Content-Length: 14 + +20231128184200 +``` + +If there is a corresponding role zettel, the response will use the HTTP status code 302 (""Found""), the HTTP response header ''Location'' will contain the URL of the role zettel. +Its zettel identifier will be returned in the HTTP response body. + +If you specified some more query parameter, these will be part of the URL in the response header ''Location'': + +```sh +# curl -i 'http://127.0.0.1:23123/z?role=manual&part=zettel' +HTTP/1.1 302 Found +Content-Type: text/plain; charset=utf-8 +Location: /z/20231128184200?part=zettel +Content-Length: 14 + +20231128184200 +``` + +Otherwise, if no role zettel was found, the response will use the HTTP status code 404 (""Not found""). + +```sh +# curl -i 'http://127.0.0.1:23123/z?role=norole' +HTTP/1.1 404 Not Found +Content-Type: text/plain; charset=utf-8 +Content-Length: 30 + +Role zettel not found: norole +``` + +To fulfill this service, Zettelstore will evaluate internally the query ''role:role title=ROLE'', there ''ROLE'' is the actual role. + +Of course, if you are only interested in the URL of the role zettel, you can make use of the HTTP ''HEAD'' method: + +```sh +# curl -I 'http://127.0.0.1:23123/z?role=manual' +HTTP/1.1 302 Found +Content-Type: text/plain; charset=utf-8 +Location: /z/20231128184200 +Content-Length: 14 +``` + +=== HTTP Status codes +; ''302'' +: Role zettel was found. + The HTTP header ''Location'' contains its URL, the body of the response contains its zettel identifier. +; ''404'' +: No zettel for the given role was found. Index: docs/manual/00001012054200.zettel ================================================================== --- docs/manual/00001012054200.zettel +++ docs/manual/00001012054200.zettel @@ -2,11 +2,11 @@ title: API: Update a zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 -modified: 20230807165948 +modified: 20231116110417 Updating metadata and content of a zettel is technically quite similar to [[creating a new zettel|00001012053200]]. In both cases you must provide the data for the new or updated zettel in the body of the HTTP request. One difference is the endpoint. @@ -15,11 +15,11 @@ The zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line. This is the same format as used by storing zettel within a [[directory box|00001006010000]]. ``` -# curl -X POST --data 'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200 +# curl -X PUT --data $'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200 ``` === Data input Alternatively, you may encode the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data''. The encoding is the same as the data output encoding when you [[retrieve a zettel|00001012053300#data-output]]. Index: docs/manual/00001012931600.zettel ================================================================== --- docs/manual/00001012931600.zettel +++ docs/manual/00001012931600.zettel @@ -2,11 +2,11 @@ title: Encoding of Sz Inline Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161845 -modified: 20230405123222 +modified: 20231113191203 === ''TEXT'' :::syntax __Text__ **=** ''(TEXT'' String '')''. ::: @@ -145,10 +145,15 @@ :::syntax __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 fto the original author). + :::syntax __QuoteFormat__ **=** ''(FORMAT-QUOTE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. ::: The inline text should be treated as quoted text. ADDED docs/manual/20231128184200.zettel Index: docs/manual/20231128184200.zettel ================================================================== --- /dev/null +++ docs/manual/20231128184200.zettel @@ -0,0 +1,7 @@ +id: 20231128184200 +title: manual +role: role +syntax: zmk +created: 20231128184200 + +Zettel with the role ""manual"" contain the manual of the zettelstore. Index: encoder/encoder.go ================================================================== --- encoder/encoder.go +++ encoder/encoder.go @@ -43,19 +43,24 @@ ErrNoWriteBlocks = errors.New("method WriteBlocks is not implemented") ErrNoWriteInlines = errors.New("method WriteInlines is not implemented") ) // Create builds a new encoder with the given options. -func Create(enc api.EncodingEnum) Encoder { +func Create(enc api.EncodingEnum, params *CreateParameter) Encoder { if create, ok := registry[enc]; ok { - return create() + return create(params) } return nil } // CreateFunc produces a new encoder. -type CreateFunc func() Encoder +type CreateFunc func(*CreateParameter) Encoder + +// CreateParameter contains values that are needed to create an encoder. +type CreateParameter struct { + Lang string // default language +} var registry = map[api.EncodingEnum]CreateFunc{} // Register the encoder for later retrieval. func Register(enc api.EncodingEnum, create CreateFunc) { Index: encoder/encoder_inline_test.go ================================================================== --- encoder/encoder_inline_test.go +++ encoder/encoder_inline_test.go @@ -145,30 +145,90 @@ }, { descr: "Quotes formatting", zmk: `""quotes""`, expect: expectMap{ - encoderHTML: "quotes", + encoderHTML: "“quotes”", encoderMD: "quotes", encoderSz: `(INLINE (FORMAT-QUOTE () (TEXT "quotes")))`, - encoderSHTML: `((q "quotes"))`, + encoderSHTML: `((@L (@H "“") "quotes" (@H "”")))`, encoderText: `quotes`, encoderZmk: useZmk, }, }, { descr: "Quotes formatting (german)", zmk: `""quotes""{lang=de}`, expect: expectMap{ - encoderHTML: `quotes`, + encoderHTML: `„quotes“`, encoderMD: "quotes", encoderSz: `(INLINE (FORMAT-QUOTE (quote (("lang" . "de"))) (TEXT "quotes")))`, - encoderSHTML: `((span (@ (lang . "de")) (q "quotes")))`, + encoderSHTML: `((span (@ (lang . "de")) (@H "„") "quotes" (@H "“")))`, encoderText: `quotes`, encoderZmk: `""quotes""{lang="de"}`, }, }, + { + descr: "Empty quotes (default)", + zmk: `""""`, + expect: expectMap{ + encoderHTML: `“”`, + encoderMD: "", + encoderSz: `(INLINE (FORMAT-QUOTE ()))`, + encoderSHTML: `((@L (@H "“" "”")))`, + encoderText: ``, + encoderZmk: useZmk, + }, + }, + { + descr: "Empty quotes (unknown)", + zmk: `""""{lang=unknown}`, + expect: expectMap{ + encoderHTML: `""`, + encoderMD: "", + encoderSz: `(INLINE (FORMAT-QUOTE (quote (("lang" . "unknown")))))`, + encoderSHTML: `((span (@ (lang . "unknown")) (@H """ """)))`, + encoderText: ``, + encoderZmk: `""""{lang="unknown"}`, + }, + }, + { + descr: "Nested quotes (default)", + zmk: `""say: ::""yes, ::""or?""::""::""`, + expect: expectMap{ + encoderHTML: `“say: ‘yes, “or?””`, + encoderMD: "say: yes, or?", + encoderSz: `(INLINE (FORMAT-QUOTE () (TEXT "say:") (SPACE) (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "yes,") (SPACE) (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "or?")))))))`, + encoderSHTML: `((@L (@H "“") "say:" " " (span (@L (@H "‘") "yes," " " (span (@L (@H "“") "or?" (@H "”"))) (@H "’"))) (@H "”")))`, + encoderText: `say: yes, or?`, + encoderZmk: useZmk, + }, + }, + { + descr: "Two quotes", + zmk: `""yes"" or ""no""`, + expect: expectMap{ + encoderHTML: `“yes” or “no”`, + encoderMD: "yes or no", + encoderSz: `(INLINE (FORMAT-QUOTE () (TEXT "yes")) (SPACE) (TEXT "or") (SPACE) (FORMAT-QUOTE () (TEXT "no")))`, + encoderSHTML: `((@L (@H "“") "yes" (@H "”")) " " "or" " " (@L (@H "“") "no" (@H "”")))`, + encoderText: `yes or no`, + encoderZmk: useZmk, + }, + }, + { + descr: "Mark formatting", + zmk: `##marked##`, + expect: expectMap{ + encoderHTML: `marked`, + encoderMD: "marked", + encoderSz: `(INLINE (FORMAT-MARK () (TEXT "marked")))`, + encoderSHTML: `((mark "marked"))`, + encoderText: `marked`, + encoderZmk: useZmk, + }, + }, { descr: "Span formatting", zmk: `::span::`, expect: expectMap{ encoderHTML: `span`, @@ -253,14 +313,14 @@ }, { descr: "Nested Span Quote formatting", zmk: `::""abc""::{lang=fr}`, expect: expectMap{ - encoderHTML: `abc`, + encoderHTML: `« abc »`, encoderMD: "abc", encoderSz: `(INLINE (FORMAT-SPAN (quote (("lang" . "fr"))) (FORMAT-QUOTE () (TEXT "abc"))))`, - encoderSHTML: `((span (@ (lang . "fr")) (q "abc")))`, + encoderSHTML: `((span (@ (lang . "fr")) (@L (@H "«" " ") "abc" (@H " " "»"))))`, encoderText: `abc`, encoderZmk: `::""abc""::{lang="fr"}`, }, }, { Index: encoder/encoder_test.go ================================================================== --- encoder/encoder_test.go +++ encoder/encoder_test.go @@ -76,11 +76,11 @@ } } func checkEncodings(t *testing.T, testNum int, pe parserEncoder, descr string, expected expectMap, zmkDefault string) { for enc, exp := range expected { - encdr := encoder.Create(enc) + encdr := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}) got, err := pe.encode(encdr) if err != nil { prefix := fmt.Sprintf("Test #%d", testNum) if d := descr; d != "" { prefix += "\nReason: " + d @@ -103,11 +103,11 @@ } } func checkSz(t *testing.T, testNum int, pe parserEncoder, descr string) { t.Helper() - encdr := encoder.Create(encoderSz) + encdr := encoder.Create(encoderSz, nil) exp, err := pe.encode(encdr) if err != nil { t.Error(err) return } Index: encoder/htmlenc/htmlenc.go ================================================================== --- encoder/htmlenc/htmlenc.go +++ encoder/htmlenc/htmlenc.go @@ -26,33 +26,36 @@ "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) func init() { - encoder.Register(api.EncoderHTML, func() encoder.Encoder { return Create() }) + encoder.Register(api.EncoderHTML, func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) }) } // Create an encoder. -func Create() *Encoder { +func Create(params *encoder.CreateParameter) *Encoder { // We need a new transformer every time, because tx.inVerse must be unique. // If we can refactor it out, the transformer can be created only once. return &Encoder{ tx: szenc.NewTransformer(), - th: shtml.NewTransformer(1, nil), + th: shtml.NewEvaluator(1, nil), + lang: params.Lang, textEnc: textenc.Create(), } } type Encoder struct { tx *szenc.Transformer - th *shtml.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, evalMeta encoder.EvalMetaFunc) (int, error) { - hm, err := he.th.Transform(he.tx.GetMeta(zn.InhMeta, evalMeta)) + env := shtml.MakeEnvironment(he.lang) + hm, err := he.th.Evaluate(he.tx.GetMeta(zn.InhMeta, evalMeta), &env) if err != nil { return 0, err } var isTitle ast.InlineSlice @@ -59,22 +62,22 @@ var htitle *sx.Pair plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle) if hasTitle { isTitle = parser.ParseSpacedText(plainTitle) xtitle := he.tx.GetSz(&isTitle) - htitle, err = he.th.Transform(xtitle) + htitle, err = he.th.Evaluate(xtitle, &env) if err != nil { return 0, err } } xast := he.tx.GetSz(&zn.Ast) - hast, err := he.th.Transform(xast) + hast, err := he.th.Evaluate(xast, &env) if err != nil { return 0, err } - hen := he.th.Endnotes() + hen := he.th.Endnotes(&env) sf := he.th.SymbolFactory() symAttr := sf.MustMake(sxhtml.NameSymAttr) head := sx.MakeList(sf.MustMake("head")) @@ -113,11 +116,12 @@ return gen.WriteHTML(w, doc) } // WriteMeta encodes meta data as HTML5. func (he *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { - hm, err := he.th.Transform(he.tx.GetMeta(m, evalMeta)) + env := shtml.MakeEnvironment(he.lang) + hm, err := he.th.Evaluate(he.tx.GetMeta(m, evalMeta), &env) if err != nil { return 0, err } gen := sxhtml.NewGenerator(he.th.SymbolFactory(), sxhtml.WithNewline) return gen.WriteListHTML(w, hm) @@ -127,28 +131,30 @@ return he.WriteBlocks(w, &zn.Ast) } // WriteBlocks encodes a block slice. func (he *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { - hobj, err := he.th.Transform(he.tx.GetSz(bs)) + env := shtml.MakeEnvironment(he.lang) + hobj, err := he.th.Evaluate(he.tx.GetSz(bs), &env) if err == nil { gen := sxhtml.NewGenerator(he.th.SymbolFactory()) length, err2 := gen.WriteListHTML(w, hobj) if err2 != nil { return length, err2 } - l, err2 := gen.WriteHTML(w, he.th.Endnotes()) + l, err2 := gen.WriteHTML(w, he.th.Endnotes(&env)) length += l return length, err2 } return 0, err } // WriteInlines writes an inline slice to the writer func (he *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { - hobj, err := he.th.Transform(he.tx.GetSz(is)) + env := shtml.MakeEnvironment(he.lang) + hobj, err := he.th.Evaluate(he.tx.GetSz(is), &env) if err == nil { gen := sxhtml.NewGenerator(sx.FindSymbolFactory(hobj)) length, err2 := gen.WriteListHTML(w, hobj) if err2 != nil { return length, err2 Index: encoder/mdenc/mdenc.go ================================================================== --- encoder/mdenc/mdenc.go +++ encoder/mdenc/mdenc.go @@ -19,11 +19,11 @@ "zettelstore.de/z/encoder" "zettelstore.de/z/zettel/meta" ) func init() { - encoder.Register(api.EncoderMD, func() encoder.Encoder { return Create() }) + encoder.Register(api.EncoderMD, func(*encoder.CreateParameter) encoder.Encoder { return Create() }) } // Create an encoder. func Create() *Encoder { return &myME } @@ -329,10 +329,14 @@ v.b.WriteString("__") case ast.FormatQuote: v.b.WriteString("") ast.Walk(v, &fn.Inlines) v.b.WriteString("") + case ast.FormatMark: + v.b.WriteString("") + ast.Walk(v, &fn.Inlines) + v.b.WriteString("") default: ast.Walk(v, &fn.Inlines) } } Index: encoder/shtmlenc/shtmlenc.go ================================================================== --- encoder/shtmlenc/shtmlenc.go +++ encoder/shtmlenc/shtmlenc.go @@ -22,67 +22,73 @@ "zettelstore.de/z/encoder/szenc" "zettelstore.de/z/zettel/meta" ) func init() { - encoder.Register(api.EncoderSHTML, func() encoder.Encoder { return Create() }) + encoder.Register(api.EncoderSHTML, func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) }) } // Create a SHTML encoder -func Create() *Encoder { +func Create(params *encoder.CreateParameter) *Encoder { // We need a new transformer every time, because tx.inVerse must be unique. // If we can refactor it out, the transformer can be created only once. return &Encoder{ - tx: szenc.NewTransformer(), - th: shtml.NewTransformer(1, nil), + tx: szenc.NewTransformer(), + th: shtml.NewEvaluator(1, nil), + lang: params.Lang, } } type Encoder struct { - tx *szenc.Transformer - th *shtml.Transformer + 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, evalMeta encoder.EvalMetaFunc) (int, error) { - metaSHTML, err := enc.th.Transform(enc.tx.GetMeta(zn.InhMeta, evalMeta)) + env := shtml.MakeEnvironment(enc.lang) + metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(zn.InhMeta, evalMeta), &env) if err != nil { return 0, err } - contentSHTML, err := enc.th.Transform(enc.tx.GetSz(&zn.Ast)) + contentSHTML, err := enc.th.Evaluate(enc.tx.GetSz(&zn.Ast), &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, evalMeta encoder.EvalMetaFunc) (int, error) { - metaSHTML, err := enc.th.Transform(enc.tx.GetMeta(m, evalMeta)) + env := shtml.MakeEnvironment(enc.lang) + metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(m, evalMeta), &env) if err != nil { return 0, err } - return metaSHTML.Print(w) + return sx.Print(w, metaSHTML) } func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return enc.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes a block slice to the writer func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { - hval, err := enc.th.Transform(enc.tx.GetSz(bs)) + env := shtml.MakeEnvironment(enc.lang) + hval, err := enc.th.Evaluate(enc.tx.GetSz(bs), &env) if err != nil { return 0, err } - return hval.Print(w) + return sx.Print(w, hval) } // WriteInlines writes an inline slice to the writer func (enc *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { - hval, err := enc.th.Transform(enc.tx.GetSz(is)) + env := shtml.MakeEnvironment(enc.lang) + hval, err := enc.th.Evaluate(enc.tx.GetSz(is), &env) if err != nil { return 0, err } - return hval.Print(w) + return sx.Print(w, hval) } Index: encoder/szenc/szenc.go ================================================================== --- encoder/szenc/szenc.go +++ encoder/szenc/szenc.go @@ -20,11 +20,11 @@ "zettelstore.de/z/encoder" "zettelstore.de/z/zettel/meta" ) func init() { - encoder.Register(api.EncoderSz, func() encoder.Encoder { return Create() }) + 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. Index: encoder/szenc/transform.go ================================================================== --- encoder/szenc/transform.go +++ encoder/szenc/transform.go @@ -71,10 +71,11 @@ ast.FormatDelete: t.zetSyms.SymFormatDelete, ast.FormatInsert: t.zetSyms.SymFormatInsert, ast.FormatSuper: t.zetSyms.SymFormatSuper, ast.FormatSub: t.zetSyms.SymFormatSub, ast.FormatQuote: t.zetSyms.SymFormatQuote, + ast.FormatMark: t.zetSyms.SymFormatMark, ast.FormatSpan: t.zetSyms.SymFormatSpan, } t.mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{ ast.LiteralZettel: t.zetSyms.SymLiteralZettel, ast.LiteralProg: t.zetSyms.SymLiteralProg, Index: encoder/textenc/textenc.go ================================================================== --- encoder/textenc/textenc.go +++ encoder/textenc/textenc.go @@ -19,11 +19,11 @@ "zettelstore.de/z/encoder" "zettelstore.de/z/zettel/meta" ) func init() { - encoder.Register(api.EncoderText, func() encoder.Encoder { return Create() }) + encoder.Register(api.EncoderText, func(*encoder.CreateParameter) encoder.Encoder { return Create() }) } // Create an encoder. func Create() *Encoder { return &myTE } Index: encoder/zmkenc/zmkenc.go ================================================================== --- encoder/zmkenc/zmkenc.go +++ encoder/zmkenc/zmkenc.go @@ -24,11 +24,11 @@ "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/meta" ) func init() { - encoder.Register(api.EncoderZmk, func() encoder.Encoder { return Create() }) + encoder.Register(api.EncoderZmk, func(*encoder.CreateParameter) encoder.Encoder { return Create() }) } // Create an encoder. func Create() *Encoder { return &myZE } @@ -354,11 +354,11 @@ v.textEnc.WriteInlines(&sb, &bn.Description) v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.") } var escapeSeqs = strfun.NewSet( - "\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", + "\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", "##", ) func (v *visitor) visitText(tn *ast.TextNode) { last := 0 for i := 0; i < len(tn.Text); i++ { @@ -450,10 +450,11 @@ ast.FormatInsert: []byte(">>"), ast.FormatDelete: []byte("~~"), ast.FormatSuper: []byte("^^"), ast.FormatSub: []byte(",,"), ast.FormatQuote: []byte(`""`), + ast.FormatMark: []byte("##"), ast.FormatSpan: []byte("::"), } func (v *visitor) visitFormat(fn *ast.FormatNode) { kind, ok := mapFormatKind[fn.Kind] Index: encoding/atom/atom.go ================================================================== --- encoding/atom/atom.go +++ encoding/atom/atom.go @@ -74,11 +74,11 @@ } func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) { entryUpdated := "" if val, found := m.Get(api.KeyPublished); found { - if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil { + if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil { entryUpdated = published.UTC().Format(time.RFC3339) } } link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String() Index: encoding/encoding.go ================================================================== --- encoding/encoding.go +++ encoding/encoding.go @@ -23,11 +23,11 @@ // LastUpdated returns the formated time of the zettel which was updated at the latest time. func LastUpdated(ml []*meta.Meta, timeFormat string) string { maxPublished := time.Date(1, time.January, 1, 0, 0, 0, 0, time.Local) for _, m := range ml { if val, found := m.Get(api.KeyPublished); found { - if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil { + if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil { if maxPublished.Before(published) { maxPublished = published } } } Index: encoding/rss/rss.go ================================================================== --- encoding/rss/rss.go +++ encoding/rss/rss.go @@ -84,11 +84,11 @@ } func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) { itemPublished := "" if val, found := m.Get(api.KeyPublished); found { - if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil { + if published, err := time.ParseInLocation(id.TimestampLayout, val, time.Local); err == nil { itemPublished = published.UTC().Format(time.RFC1123Z) } } link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String() Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -2,14 +2,14 @@ go 1.21 require ( github.com/fsnotify/fsnotify v1.7.0 - github.com/yuin/goldmark v1.5.6 - golang.org/x/crypto v0.14.0 - golang.org/x/term v0.13.0 - golang.org/x/text v0.13.0 - zettelstore.de/client.fossil v0.0.0-20231026155719-8c6fa07a0d0f - zettelstore.de/sx.fossil v0.0.0-20231026154942-e6a183740a4f + github.com/yuin/goldmark v1.6.0 + golang.org/x/crypto v0.16.0 + golang.org/x/term v0.15.0 + golang.org/x/text v0.14.0 + zettelstore.de/client.fossil v0.0.0-20231130151508-751754d40c73 + zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207 ) -require golang.org/x/sys v0.13.0 // indirect +require golang.org/x/sys v0.15.0 // indirect Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,16 +1,16 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/yuin/goldmark v1.5.6 h1:COmQAWTCcGetChm3Ig7G/t8AFAN00t+o8Mt4cf7JpwA= -github.com/yuin/goldmark v1.5.6/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -zettelstore.de/client.fossil v0.0.0-20231026155719-8c6fa07a0d0f h1:eW8wEMcqR+LvIwWxE1rl+6LZbXRM9wgBGQ9pPw/k1j8= -zettelstore.de/client.fossil v0.0.0-20231026155719-8c6fa07a0d0f/go.mod h1:uYFsUH4hQ/TLEjDFxzOLJkT/sltvcQ5aIM29XNkjR+c= -zettelstore.de/sx.fossil v0.0.0-20231026154942-e6a183740a4f h1:NFblKWyhNnXDDcF7C6zx7cDyIJ/7GGAusIwR6uZrfkM= -zettelstore.de/sx.fossil v0.0.0-20231026154942-e6a183740a4f/go.mod h1:Uw3OLM1ufOM4Xe0G51mvkTDUv2okd+HyDBMx+0ZG7ME= +github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= +github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +zettelstore.de/client.fossil v0.0.0-20231130151508-751754d40c73 h1:LPF8QWip3GRLBzgPdnsJnrvk1sGiMOSpqOwURiax9gg= +zettelstore.de/client.fossil v0.0.0-20231130151508-751754d40c73/go.mod h1:fN+1WxRorSbHduS0T0B4GI8o82EgFuUWBv6fwsAZF6o= +zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207 h1:8ch54z0w53bps6a00NDofEqo3AJ1l7ITXyC3XyLmlY4= +zettelstore.de/sx.fossil v0.0.0-20231130150648-05ef116ba207/go.mod h1:Uw3OLM1ufOM4Xe0G51mvkTDUv2okd+HyDBMx+0ZG7ME= Index: kernel/impl/cfg.go ================================================================== --- kernel/impl/cfg.go +++ kernel/impl/cfg.go @@ -91,26 +91,34 @@ keyZettelFileSyntax: { "Zettel file syntax", func(val string) (any, error) { return strings.Fields(val), nil }, true, }, - kernel.ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true}, + kernel.ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true}, + config.KeyShowBackLinks: {"Show back links", parseString, true}, + config.KeyShowFolgeLinks: {"Show folge 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.DefaultHomeZid, - kernel.ConfigInsecureHTML: config.NoHTML, - api.KeyLang: api.ValueLangEN, - keyMaxTransclusions: int64(1024), - keySiteName: "Zettelstore", - keyYAMLHeader: false, - keyZettelFileSyntax: nil, - kernel.ConfigSimpleMode: false, + keyDefaultCopyright: "", + keyDefaultLicense: "", + keyDefaultVisibility: meta.VisibilityLogin, + keyExpertMode: false, + config.KeyFooterZettel: id.Invalid, + config.KeyHomeZettel: id.DefaultHomeZid, + kernel.ConfigInsecureHTML: config.NoHTML, + api.KeyLang: api.ValueLangEN, + keyMaxTransclusions: int64(1024), + keySiteName: "Zettelstore", + keyYAMLHeader: false, + keyZettelFileSyntax: nil, + kernel.ConfigSimpleMode: false, + config.KeyShowBackLinks: "", + config.KeyShowFolgeLinks: "", + config.KeyShowSubordinateLinks: "", + config.KeyShowSuccessorLinks: "", } } func (cs *configService) GetLogger() *logger.Logger { return cs.logger } func (cs *configService) Start(*myKernel) error { Index: kernel/impl/core.go ================================================================== --- kernel/impl/core.go +++ kernel/impl/core.go @@ -79,11 +79,11 @@ kernel.CoreGoArch: runtime.GOARCH, kernel.CoreGoOS: runtime.GOOS, kernel.CoreGoVersion: runtime.Version(), kernel.CoreHostname: "*unknown host*", kernel.CorePort: 0, - kernel.CoreStarted: time.Now().Local().Format(id.ZidLayout), + kernel.CoreStarted: time.Now().Local().Format(id.TimestampLayout), kernel.CoreVerbose: false, } if hn, err := os.Hostname(); err == nil { cs.next[kernel.CoreHostname] = hn } Index: kernel/impl/impl.go ================================================================== --- kernel/impl/impl.go +++ kernel/impl/impl.go @@ -124,11 +124,11 @@ } func (kern *myKernel) Setup(progname, version string, versionTime time.Time) { kern.SetConfig(kernel.CoreService, kernel.CoreProgname, progname) kern.SetConfig(kernel.CoreService, kernel.CoreVersion, version) - kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.ZidLayout)) + kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.TimestampLayout)) } func (kern *myKernel) Start(headline, lineServer bool, configFilename string) { for _, srvD := range kern.srvs { srvD.srv.Freeze() Index: kernel/impl/server.go ================================================================== --- kernel/impl/server.go +++ kernel/impl/server.go @@ -37,11 +37,11 @@ for { conn, err := ln.Accept() if err != nil { // handle error - kern.logger.IfErr(err).Msg("Unable to accept connection") + kern.logger.Error().Err(err).Msg("Unable to accept connection") break } go handleLineConnection(conn, kern) } ln.Close() Index: logger/logger.go ================================================================== --- logger/logger.go +++ logger/logger.go @@ -181,18 +181,10 @@ func (l *Logger) Warn() *Message { return newMessage(l, WarnLevel) } // Error creates a message suitable for errors. func (l *Logger) Error() *Message { return newMessage(l, ErrorLevel) } -// IfErr creates an error message and sets the go error, if there is an error. -func (l *Logger) IfErr(err error) *Message { - if err != nil { - return newMessage(l, ErrorLevel).Err(err) - } - return nil -} - // Fatal creates a message suitable for fatal errors. func (l *Logger) Fatal() *Message { return newMessage(l, FatalLevel) } // Panic creates a message suitable for panicing. func (l *Logger) Panic() *Message { return newMessage(l, PanicLevel) } Index: parser/parser.go ================================================================== --- parser/parser.go +++ parser/parser.go @@ -133,10 +133,11 @@ // NormalizedSpacedText returns the given string, but normalize multiple spaces to one space. func NormalizedSpacedText(s string) string { return strings.Join(meta.ListFromValue(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 descr, found := m.Get(api.KeySummary); found { @@ -145,11 +146,11 @@ return in } if title, found := m.Get(api.KeyTitle); found { return ParseSpacedText(title) } - return nil + return ast.CreateInlineSliceFromWords("Zettel", "without", "title:", 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 Index: parser/zettelmark/inline.go ================================================================== --- parser/zettelmark/inline.go +++ parser/zettelmark/inline.go @@ -67,11 +67,11 @@ if inp.Ch == '{' { in, success = cp.parseEmbed() } case '%': in, success = cp.parseComment() - case '_', '*', '>', '~', '^', ',', '"', ':': + case '_', '*', '>', '~', '^', ',', '"', '#', ':': in, success = cp.parseFormat() case '@', '\'', '`', '=', runeModGrave: in, success = cp.parseLiteral() case '$': in, success = cp.parseLiteralMath() @@ -100,11 +100,11 @@ for { inp.Next() switch inp.Ch { // The following case must contain all runes that occur in parseInline! // Plus the closing brackets ] and } and ) and the middle | - case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&': + case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', '#', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&': return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])} } } } @@ -416,10 +416,11 @@ '>': ast.FormatInsert, '~': ast.FormatDelete, '^': ast.FormatSuper, ',': ast.FormatSub, '"': ast.FormatQuote, + '#': ast.FormatMark, ':': ast.FormatSpan, } func (cp *zmkP) parseFormat() (res ast.InlineNode, success bool) { inp := cp.inp Index: parser/zettelmark/zettelmark_test.go ================================================================== --- parser/zettelmark/zettelmark_test.go +++ parser/zettelmark/zettelmark_test.go @@ -291,19 +291,19 @@ } func TestFormat(t *testing.T) { t.Parallel() // Not for Insert / '>', because collision with quoted list - for _, ch := range []string{"_", "*", "~", "^", ",", "\"", ":"} { + for _, ch := range []string{"_", "*", "~", "^", ",", "\"", "#", ":"} { checkTcs(t, replace(ch, TestCases{ {"$", "(PARA $)"}, {"$$", "(PARA $$)"}, {"$$$", "(PARA $$$)"}, {"$$$$", "(PARA {$})"}, })) } - for _, ch := range []string{"_", "*", ">", "~", "^", ",", "\"", ":"} { + for _, ch := range []string{"_", "*", ">", "~", "^", ",", "\"", "#", ":"} { checkTcs(t, replace(ch, TestCases{ {"$$a$$", "(PARA {$ a})"}, {"$$a$$$", "(PARA {$ a} $)"}, {"$$$a$$", "(PARA {$ $a})"}, {"$$$a$$$", "(PARA {$ $a} $)"}, @@ -988,10 +988,11 @@ ast.FormatInsert: '>', ast.FormatDelete: '~', ast.FormatSuper: '^', ast.FormatSub: ',', ast.FormatQuote: '"', + ast.FormatMark: '#', ast.FormatSpan: ':', } var mapLiteralKind = map[ast.LiteralKind]rune{ ast.LiteralZettel: '@', Index: query/select.go ================================================================== --- query/select.go +++ query/select.go @@ -107,14 +107,16 @@ return nil } switch meta.Type(key) { case meta.TypeCredential: return matchValueNever - case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout + case meta.TypeID: return createMatchIDFunc(values, addSearch) case meta.TypeIDSet: return createMatchIDSetFunc(values, addSearch) + case meta.TypeTimestamp: + return createMatchTimestampFunc(values, addSearch) case meta.TypeNumber: return createMatchNumberFunc(values, addSearch) case meta.TypeTagSet: return createMatchTagSetFunc(values, addSearch) case meta.TypeWord: @@ -151,10 +153,22 @@ } } return true } } +func createMatchTimestampFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { + preds := valuesToTimestampPredicates(values, addSearch) + return func(value string) bool { + 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 string) bool { for _, pred := range preds { @@ -167,15 +181,11 @@ } func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { predList := valuesToWordSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch) return func(value string) bool { - tags := meta.ListFromValue(value) - // Remove leading '#' from each tag - for i, tag := range tags { - tags[i] = meta.CleanTag(tag) - } + tags := meta.TagsFromValue(value) for _, preds := range predList { for _, pred := range preds { if !pred(tags) { return false } @@ -387,10 +397,39 @@ func disambiguatedIDOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) } func createIDCompareFunc(cmpVal string, 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(value, op) + continue + } + fallthrough + default: + // Otherwise compare as a word. + if !op.isNegated() { + addSearch(v) // addSearch only for positive selections + } + result[i] = createWordCompareFunc(value, op) + } + } + return result +} + +func disambiguatedTimestampOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) } + +func createTimestampCompareFunc(cmpVal string, 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 { Index: query/sorter.go ================================================================== --- query/sorter.go +++ query/sorter.go @@ -55,15 +55,33 @@ if descending { return func(i, j int) bool { return ml[i].Zid > ml[j].Zid } } return func(i, j int) bool { return ml[i].Zid < ml[j].Zid } } + if keyType == meta.TypeTimestamp { + return createSortTimestampFunc(ml, key, descending) + } if keyType == meta.TypeNumber { return createSortNumberFunc(ml, key, descending) } return createSortStringFunc(ml, key, descending) } + +func createSortTimestampFunc(ml []*meta.Meta, key string, descending bool) sortFunc { + if descending { + return func(i, j int) bool { + iVal, iOk := ml[i].Get(key) + jVal, jOk := ml[j].Get(key) + return (iOk && (!jOk || meta.ExpandTimestamp(iVal) > meta.ExpandTimestamp(jVal))) || !jOk + } + } + return func(i, j int) bool { + iVal, iOk := ml[i].Get(key) + jVal, jOk := ml[j].Get(key) + return (iOk && (!jOk || meta.ExpandTimestamp(iVal) < meta.ExpandTimestamp(jVal))) || !jOk + } +} func createSortNumberFunc(ml []*meta.Meta, key string, descending bool) sortFunc { if descending { return func(i, j int) bool { iVal, iOk := getNum(ml[i], key) @@ -75,10 +93,19 @@ iVal, iOk := getNum(ml[i], key) jVal, jOk := getNum(ml[j], key) return (iOk && (!jOk || iVal < jVal)) || !jOk } } + +func getNum(m *meta.Meta, key string) (int64, bool) { + if s, ok := m.Get(key); ok { + if i, err := strconv.ParseInt(s, 10, 64); err == nil { + return i, true + } + } + return 0, false +} func createSortStringFunc(ml []*meta.Meta, key string, descending bool) sortFunc { if descending { return func(i, j int) bool { iVal, iOk := ml[i].Get(key) @@ -90,14 +117,5 @@ iVal, iOk := ml[i].Get(key) jVal, jOk := ml[j].Get(key) return (iOk && (!jOk || iVal < jVal)) || !jOk } } - -func getNum(m *meta.Meta, key string) (int64, bool) { - if s, ok := m.Get(key); ok { - if i, err := strconv.ParseInt(s, 10, 64); err == nil { - return i, true - } - } - return 0, false -} Index: tests/client/client_test.go ================================================================== --- tests/client/client_test.go +++ tests/client/client_test.go @@ -47,16 +47,16 @@ } } func TestListZettel(t *testing.T) { const ( - ownerZettel = 50 - configRoleZettel = 32 + ownerZettel = 55 + configRoleZettel = 33 writerZettel = ownerZettel - 24 readerZettel = ownerZettel - 24 - creatorZettel = 8 - publicZettel = 4 + creatorZettel = 10 + publicZettel = 5 ) testdata := []struct { user string exp int @@ -213,17 +213,18 @@ _, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidTOCNewTemplate)+" "+api.ItemsDirective) if err != nil { t.Error(err) return } - if got := len(metaSeq); got != 3 { - t.Errorf("Expected list of length 3, got %d", got) + if got := len(metaSeq); got != 4 { + t.Errorf("Expected list of length 4, got %d", got) return } checkListZid(t, metaSeq, 0, api.ZidTemplateNewZettel) - checkListZid(t, metaSeq, 1, api.ZidTemplateNewTag) - checkListZid(t, metaSeq, 2, api.ZidTemplateNewUser) + checkListZid(t, metaSeq, 1, api.ZidTemplateNewRole) + checkListZid(t, metaSeq, 2, api.ZidTemplateNewTag) + checkListZid(t, metaSeq, 3, api.ZidTemplateNewUser) } // func TestGetZettelContext(t *testing.T) { // const ( // allUserZid = api.ZettelID("20211019200500") @@ -279,11 +280,11 @@ if err != nil { t.Error(err) return } if got := len(metaSeq); got != 1 { - t.Errorf("Expected list of length 1, got %d", got) + t.Errorf("Expected list of length 1, got %d:\n%v", got, metaSeq) return } } func failNoErrorOrNoCode(t *testing.T, err error, goodCode int) bool { @@ -384,20 +385,41 @@ agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyRole) if err != nil { t.Error(err) return } - exp := []string{"configuration", "user", "tag", "zettel"} + exp := []string{"configuration", "role", "user", "tag", "zettel"} if len(agg) != len(exp) { t.Errorf("Expected %d different roles, but got %d (%v)", len(exp), len(agg), agg) } for _, id := range exp { if _, found := agg[id]; !found { t.Errorf("Role map expected key %q", id) } } } + +func TestRoleZettel(t *testing.T) { + t.Parallel() + c := getClient() + c.AllowRedirect(true) + c.SetAuth("owner", "owner") + ctx := context.Background() + zid, err := c.RoleZettel(ctx, "nosuchrole") + if err != nil { + t.Error("AAA", err) + } else if zid != "" { + t.Errorf("no zid expected, but got %q", zid) + } + zid, err = c.RoleZettel(ctx, "zettel") + exp := api.ZettelID("00000000060010") + if err != nil { + t.Error(err) + } else if zid != exp { + t.Errorf("role zettel for zettel should be %q, but got %q", exp, zid) + } +} func TestVersion(t *testing.T) { t.Parallel() c := getClient() ver, err := c.GetVersionInfo(context.Background()) Index: tests/markdown_test.go ================================================================== --- tests/markdown_test.go +++ tests/markdown_test.go @@ -46,11 +46,11 @@ func TestEncoderAvailability(t *testing.T) { t.Parallel() encoderMissing := false for _, enc := range encodings { - enc := encoder.Create(enc) + enc := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}) if enc == nil { t.Errorf("No encoder for %q found", enc) encoderMissing = true } } @@ -84,18 +84,18 @@ 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(st *testing.T) { - encoder.Create(enc).WriteBlocks(&sb, ast) + encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}).WriteBlocks(&sb, ast) sb.Reset() }) } } func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { - zmkEncoder := encoder.Create(api.EncoderZmk) + zmkEncoder := encoder.Create(api.EncoderZmk, nil) var buf bytes.Buffer testID := tc.Example*100 + 1 t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) { buf.Reset() zmkEncoder.WriteBlocks(&buf, ast) @@ -128,11 +128,11 @@ md string exp string }{ {`abc
def`, `abc@@
@@{="html"}def`}, } - zmkEncoder := encoder.Create(api.EncoderZmk) + 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) Index: tests/naughtystrings_test.go ================================================================== --- tests/naughtystrings_test.go +++ tests/naughtystrings_test.go @@ -15,10 +15,11 @@ "io" "os" "path/filepath" "testing" + "zettelstore.de/client.fossil/api" _ "zettelstore.de/z/cmd" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" @@ -53,11 +54,11 @@ return result } func getAllEncoder() (result []encoder.Encoder) { for _, enc := range encoder.GetEncodings() { - e := encoder.Create(enc) + e := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}) result = append(result, e) } return result } Index: tests/regression_test.go ================================================================== --- tests/regression_test.go +++ tests/regression_test.go @@ -119,11 +119,11 @@ } func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) { t.Helper() - if enc := encoder.Create(enc); enc != nil { + if enc := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}); enc != nil { var sf strings.Builder enc.WriteMeta(&sf, zn.Meta, parser.ParseMetadata) checkFileContent(t, resultName, sf.String()) return } Index: usecase/create_zettel.go ================================================================== --- usecase/create_zettel.go +++ usecase/create_zettel.go @@ -142,11 +142,11 @@ m := zettel.Meta if m.Zid.IsValid() { return m.Zid, nil // TODO: new error: already exists } - m.Set(api.KeyCreated, time.Now().Local().Format(id.ZidLayout)) + m.Set(api.KeyCreated, time.Now().Local().Format(id.TimestampLayout)) m.Delete(api.KeyModified) m.YamlSep = uc.rtConfig.GetYAMLHeader() zettel.Content.TrimSpace() zid, err := uc.port.CreateZettel(ctx, zettel) Index: usecase/get_special_zettel.go ================================================================== --- usecase/get_special_zettel.go +++ usecase/get_special_zettel.go @@ -19,11 +19,11 @@ "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // TagZettel is the usecase of retrieving a "tag zettel", i.e. a zettel that -// describes a given tag. A tag zettel must habe the tag's name in its title +// describes a given tag. A tag zettel must have the tag's name in its title // and must have a role=tag. // TagZettelPort is the interface used by this use case. type TagZettelPort interface { // GetZettel retrieves a specific zettel. @@ -62,5 +62,49 @@ // ErrTagZettelNotFound is returned if a tag zettel was not found. type ErrTagZettelNotFound struct{ Tag string } func (etznf ErrTagZettelNotFound) Error() string { return "tag zettel not found: " + etznf.Tag } + +// RoleZettel is the usecase of retrieving a "role zettel", i.e. a zettel that +// describes a given role. A role zettel must have the role's name in its title +// and must have a role=role. + +// RoleZettelPort is the interface used by this use case. +type RoleZettelPort interface { + // GetZettel retrieves a specific zettel. + GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) +} + +// RoleZettel is the data for this use case. +type RoleZettel struct { + port GetZettelPort + query *Query +} + +// NewRoleZettel creates a new use case. +func NewRoleZettel(port GetZettelPort, query *Query) RoleZettel { + return RoleZettel{port: port, query: query} +} + +// Run executes the use case. +func (uc RoleZettel) Run(ctx context.Context, role string) (zettel.Zettel, error) { + q := query.Parse( + api.KeyTitle + api.SearchOperatorEqual + role + " " + + api.KeyRole + api.SearchOperatorHas + api.ValueRoleRole) + ml, err := uc.query.Run(ctx, q) + if err != nil { + return zettel.Zettel{}, err + } + for _, m := range ml { + z, errZ := uc.port.GetZettel(ctx, m.Zid) + if errZ == nil { + return z, nil + } + } + return zettel.Zettel{}, ErrRoleZettelNotFound{Role: role} +} + +// ErrRoleZettelNotFound is returned if a role zettel was not found. +type ErrRoleZettelNotFound struct{ Role string } + +func (etznf ErrRoleZettelNotFound) Error() string { return "role zettel not found: " + etznf.Role } Index: web/adapter/api/api.go ================================================================== --- web/adapter/api/api.go +++ web/adapter/api/api.go @@ -70,11 +70,11 @@ } func (a *API) reportUsecaseError(w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { - a.log.IfErr(err).Msg(text) + a.log.Error().Err(err).Msg(text) http.Error(w, http.StatusText(code), code) return } // TODO: must call PrepareHeader somehow http.Error(w, text, code) Index: web/adapter/api/create_zettel.go ================================================================== --- web/adapter/api/create_zettel.go +++ web/adapter/api/create_zettel.go @@ -66,9 +66,10 @@ } h := adapter.PrepareHeader(w, contentType) h.Set(api.HeaderLocation, location.String()) w.WriteHeader(http.StatusCreated) - _, err = w.Write(result) - a.log.IfErr(err).Zid(newZid).Msg("Create Zettel") + if _, err = w.Write(result); err != nil { + a.log.Error().Err(err).Zid(newZid).Msg("Create Zettel") + } } } Index: web/adapter/api/get_data.go ================================================================== --- web/adapter/api/get_data.go +++ web/adapter/api/get_data.go @@ -27,8 +27,10 @@ sx.Int64(version.Minor), sx.Int64(version.Patch), sx.String(version.Info), sx.String(version.Hash), )) - a.log.IfErr(err).Msg("Write Version Info") + if err != nil { + a.log.Error().Err(err).Msg("Write Version Info") + } } } Index: web/adapter/api/get_zettel.go ================================================================== --- web/adapter/api/get_zettel.go +++ web/adapter/api/get_zettel.go @@ -62,11 +62,11 @@ } if err != nil { a.reportUsecaseError(w, err) return } - a.writeEncodedZettelPart(w, zn, em, enc, encStr, part) + a.writeEncodedZettelPart(ctx, w, zn, em, enc, encStr, part) } } } func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) { @@ -102,12 +102,13 @@ if err != nil { a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - err = writeBuffer(w, &buf, contentType) - a.log.IfErr(err).Zid(zid).Msg("Write Plain data") + if err = writeBuffer(w, &buf, contentType); err != nil { + a.log.Error().Err(err).Zid(zid).Msg("Write Plain data") + } } func (a *API) writeSzData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) { z, err := getZettel.Run(ctx, zid) if err != nil { @@ -129,20 +130,26 @@ obj = sexp.EncodeMetaRights(api.MetaRights{ Meta: z.Meta.Map(), Rights: a.getRights(ctx, z.Meta), }) } - err = a.writeObject(w, zid, obj) - a.log.IfErr(err).Zid(zid).Msg("write sx data") + if err = a.writeObject(w, zid, obj); err != nil { + a.log.Error().Err(err).Zid(zid).Msg("write sx data") + } } func (a *API) writeEncodedZettelPart( + ctx context.Context, w http.ResponseWriter, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc, enc api.EncodingEnum, encStr string, part partType, ) { - encdr := encoder.Create(enc) + encdr := encoder.Create( + enc, + &encoder.CreateParameter{ + Lang: a.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), + }) if encdr == nil { adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid, encStr)) return } var err error @@ -163,8 +170,9 @@ if buf.Len() == 0 { w.WriteHeader(http.StatusNoContent) return } - err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc)) - a.log.IfErr(err).Zid(zn.Zid).Msg("Write Encoded Zettel") + if err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc)); err != nil { + a.log.Error().Err(err).Zid(zn.Zid).Msg("Write Encoded Zettel") + } } Index: web/adapter/api/login.go ================================================================== --- web/adapter/api/login.go +++ web/adapter/api/login.go @@ -23,12 +23,13 @@ // MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !a.withAuth() { - err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour) - a.log.IfErr(err).Msg("Login/free") + if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil { + a.log.Error().Err(err).Msg("Login/free") + } return } var token []byte if ident, cred := retrieveIdentCred(r); ident != "" { var err error @@ -42,12 +43,13 @@ w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`) http.Error(w, "Authentication failed", http.StatusUnauthorized) return } - err := a.writeToken(w, string(token), a.tokenLifetime) - a.log.IfErr(err).Msg("Login") + if err := a.writeToken(w, string(token), a.tokenLifetime); err != nil { + a.log.Error().Err(err).Msg("Login") + } } } func retrieveIdentCred(r *http.Request) (string, string) { if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok { @@ -62,12 +64,13 @@ // MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user. func (a *API) MakeRenewAuthHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if !a.withAuth() { - err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour) - a.log.IfErr(err).Msg("Refresh/free") + if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil { + a.log.Error().Err(err).Msg("Refresh/free") + } return } authData := a.getAuthData(ctx) if authData == nil || len(authData.Token) == 0 || authData.User == nil { adapter.BadRequest(w, "Not authenticated") @@ -75,23 +78,25 @@ } totalLifetime := authData.Expires.Sub(authData.Issued) currentLifetime := authData.Now.Sub(authData.Issued) // If we are in the first quarter of the tokens lifetime, return the token if currentLifetime*4 < totalLifetime { - err := a.writeToken(w, string(authData.Token), totalLifetime-currentLifetime) - a.log.IfErr(err).Msg("Write old token") + if err := a.writeToken(w, string(authData.Token), totalLifetime-currentLifetime); err != nil { + a.log.Error().Err(err).Msg("Write old token") + } return } // Token is a little bit aged. Create a new one token, err := a.getToken(authData.User) if err != nil { a.reportUsecaseError(w, err) return } - err = a.writeToken(w, string(token), a.tokenLifetime) - a.log.IfErr(err).Msg("Write renewed token") + if err = a.writeToken(w, string(token), a.tokenLifetime); err != nil { + a.log.Error().Err(err).Msg("Write renewed token") + } } } func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error { return a.writeObject(w, id.Invalid, sx.MakeList( Index: web/adapter/api/query.go ================================================================== --- web/adapter/api/query.go +++ web/adapter/api/query.go @@ -29,16 +29,19 @@ "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // MakeQueryHandler creates a new HTTP handler to perform a query. -func (a *API) MakeQueryHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, reIndex *usecase.ReIndex) http.HandlerFunc { +func (a *API) MakeQueryHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() urlQuery := r.URL.Query() if a.handleTagZettel(w, r, tagZettel, urlQuery) { return + } + if a.handleRoleZettel(w, r, roleZettel, urlQuery) { + return } sq := adapter.GetQuery(urlQuery) metaSeq, err := queryMeta.Run(ctx, sq) @@ -248,10 +251,39 @@ w.Header().Set(api.HeaderContentType, content.PlainText) newURL := a.NewURLBuilder('z').SetZid(api.ZettelID(zid)) for key, slVals := range vals { if key == api.QueryKeyTag { continue + } + for _, val := range slVals { + newURL.AppendKVQuery(key, val) + } + } + http.Redirect(w, r, newURL.String(), http.StatusFound) + if _, err = io.WriteString(w, zid); err != nil { + a.log.Error().Err(err).Msg("redirect body") + } + return true +} + +func (a *API) handleRoleZettel(w http.ResponseWriter, r *http.Request, roleZettel *usecase.RoleZettel, vals url.Values) bool { + role := vals.Get(api.QueryKeyRole) + if role == "" { + return false + } + ctx := r.Context() + z, err := roleZettel.Run(ctx, role) + if err != nil { + a.reportUsecaseError(w, err) + return true + } + zid := z.Meta.Zid.String() + w.Header().Set(api.HeaderContentType, content.PlainText) + newURL := a.NewURLBuilder('z').SetZid(api.ZettelID(zid)) + for key, slVals := range vals { + if key == api.QueryKeyRole { + continue } for _, val := range slVals { newURL.AppendKVQuery(key, val) } } Index: web/adapter/response.go ================================================================== --- web/adapter/response.go +++ web/adapter/response.go @@ -73,10 +73,14 @@ } var etznf usecase.ErrTagZettelNotFound if errors.As(err, &etznf) { return http.StatusNotFound, "Tag zettel not found: " + etznf.Tag } + var erznf usecase.ErrRoleZettelNotFound + if errors.As(err, &erznf) { + return http.StatusNotFound, "Role zettel not found: " + erznf.Role + } var ebr ErrBadRequest if errors.As(err, &ebr) { return http.StatusBadRequest, ebr.Text } if errors.Is(err, box.ErrStopped) { Index: web/adapter/webui/favicon.go ================================================================== --- web/adapter/webui/favicon.go +++ web/adapter/webui/favicon.go @@ -35,9 +35,10 @@ wui.log.Info().Err(err).Msg("Unable to read favicon data") http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } - err = adapter.WriteData(w, data, "") - wui.log.IfErr(err).Msg("Write favicon") + if err = adapter.WriteData(w, data, ""); err != nil { + wui.log.Error().Err(err).Msg("Write favicon") + } } } Index: web/adapter/webui/get_info.go ================================================================== --- web/adapter/webui/get_info.go +++ web/adapter/webui/get_info.go @@ -54,11 +54,11 @@ if err != nil { wui.reportError(ctx, w, err) return } - enc := wui.getSimpleHTMLEncoder() + enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang)) getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel) evalMeta := func(val string) ast.InlineSlice { return ucEvaluate.RunMetadata(ctx, val) } pairs := zn.Meta.ComputedPairs() Index: web/adapter/webui/get_zettel.go ================================================================== --- web/adapter/webui/get_zettel.go +++ web/adapter/webui/get_zettel.go @@ -9,15 +9,17 @@ //----------------------------------------------------------------------------- package webui import ( + "context" "net/http" "zettelstore.de/client.fossil/api" "zettelstore.de/sx.fossil" "zettelstore.de/z/box" + "zettelstore.de/z/config" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" @@ -39,11 +41,11 @@ if err != nil { wui.reportError(ctx, w, err) return } - enc := wui.getSimpleHTMLEncoder() + enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang)) metaObj := enc.MetaSxn(zn.InhMeta, createEvalMetadataFunc(ctx, evaluate)) content, endnotes, err := enc.BlocksSxn(&zn.Ast) if err != nil { wui.reportError(ctx, w, err) return @@ -66,14 +68,14 @@ rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPredecessor, getTextTitle)) rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrecursor, getTextTitle)) rb.bindString("superior-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeySuperior, getTextTitle)) rb.bindString("content", content) rb.bindString("endnotes", endnotes) - rb.bindString("folge-links", wui.zettelLinksSxn(zn.InhMeta, api.KeyFolge, getTextTitle)) - rb.bindString("subordinate-links", wui.zettelLinksSxn(zn.InhMeta, api.KeySubordinates, getTextTitle)) - rb.bindString("back-links", wui.zettelLinksSxn(zn.InhMeta, api.KeyBack, getTextTitle)) - rb.bindString("successor-links", wui.zettelLinksSxn(zn.InhMeta, api.KeySuccessors, getTextTitle)) + wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, api.KeyFolge, config.KeyShowFolgeLinks, getTextTitle) + wui.bindLinks(ctx, &rb, "subordinate", zn.InhMeta, api.KeySubordinates, config.KeyShowSubordinateLinks, getTextTitle) + wui.bindLinks(ctx, &rb, "back", zn.InhMeta, api.KeyBack, config.KeyShowBackLinks, getTextTitle) + wui.bindLinks(ctx, &rb, "successor", zn.InhMeta, api.KeySuccessors, config.KeyShowSuccessorLinks, getTextTitle) if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" { for _, part := range []string{"meta", "actions", "heading"} { rb.rebindResolved("ROLE-"+role+"-"+part, "ROLE-DEFAULT-"+part) } } @@ -93,10 +95,29 @@ if values, ok := m.GetList(key); ok { return wui.transformIdentifierSet(values, getTextTitle) } return nil } + +func (wui *WebUI) bindLinks(ctx context.Context, rb *renderBinder, varPrefix string, m *meta.Meta, key, configKey string, getTextTitle getTextTitleFunc) { + varLinks := varPrefix + "-links" + var symOpen *sx.Symbol + switch wui.rtConfig.Get(ctx, m, configKey) { + case "false": + rb.bindString(varLinks, sx.Nil()) + return + case "close": + default: + symOpen = wui.symAttrOpen + } + lstLinks := wui.zettelLinksSxn(m, key, getTextTitle) + rb.bindString(varLinks, lstLinks) + if sx.IsNil(lstLinks) { + return + } + rb.bindString(varPrefix+"-open", symOpen) +} func (wui *WebUI) zettelLinksSxn(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair { values, ok := m.GetList(key) if !ok || len(values) == 0 { return nil Index: web/adapter/webui/htmlgen.go ================================================================== --- web/adapter/webui/htmlgen.go +++ web/adapter/webui/htmlgen.go @@ -18,11 +18,10 @@ "zettelstore.de/client.fossil/attrs" "zettelstore.de/client.fossil/maps" "zettelstore.de/client.fossil/shtml" "zettelstore.de/client.fossil/sz" "zettelstore.de/sx.fossil" - "zettelstore.de/sx.fossil/sxeval" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/szenc" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/meta" @@ -34,16 +33,17 @@ NewURLBuilder(key byte) *api.URLBuilder } type htmlGenerator struct { tx *szenc.Transformer - th *shtml.Transformer + th *shtml.Evaluator + lang string symAt *sx.Symbol } -func (wui *WebUI) createGenerator(builder urlBuilder) *htmlGenerator { - th := shtml.NewTransformer(1, wui.sf) +func (wui *WebUI) createGenerator(builder urlBuilder, lang string) *htmlGenerator { + th := shtml.NewEvaluator(1, wui.sf) symA := wui.symA symImg := th.Make("img") symAttr := wui.symAttr symHref := wui.symHref @@ -65,15 +65,11 @@ if !isPair || !symAttr.IsEqual(attr.Car()) { return nil, nil, nil } return attr, attr.Tail(), rest.Tail() } - linkZettel := func(args []sx.Object, prevFn sxeval.Callable) sx.Object { - obj, err := prevFn.Call(nil, args) - if err != nil { - return sx.Nil() - } + linkZettel := func(obj sx.Object) sx.Object { attr, assoc, rest := findA(obj) if attr == nil { return obj } @@ -92,115 +88,109 @@ } assoc = assoc.Cons(sx.Cons(symHref, sx.String(u.String()))) return rest.Cons(assoc.Cons(symAttr)).Cons(symA) } - th.SetRebinder(func(te *shtml.TransformEnv) { - te.Rebind(sz.NameSymLinkZettel, linkZettel) - te.Rebind(sz.NameSymLinkFound, linkZettel) - te.Rebind(sz.NameSymLinkBased, func(args []sx.Object, prevFn sxeval.Callable) sx.Object { - obj, err := prevFn.Call(nil, args) - if err != nil { - return sx.Nil() - } - attr, assoc, rest := findA(obj) - if attr == nil { - return obj - } - hrefP := assoc.Assoc(symHref) - if hrefP == nil { - return obj - } - href, ok := sx.GetString(hrefP.Cdr()) - if !ok { - return obj - } - u := builder.NewURLBuilder('/').SetRawLocal(href.String()) - assoc = assoc.Cons(sx.Cons(symHref, sx.String(u.String()))) - return rest.Cons(assoc.Cons(symAttr)).Cons(symA) - }) - te.Rebind(sz.NameSymLinkQuery, func(args []sx.Object, prevFn sxeval.Callable) sx.Object { - obj, err := prevFn.Call(nil, args) - if err != nil { - return sx.Nil() - } - attr, assoc, rest := findA(obj) - if attr == nil { - return obj - } - hrefP := assoc.Assoc(symHref) - if hrefP == nil { - return obj - } - href, ok := sx.GetString(hrefP.Cdr()) - if !ok { - return obj - } - ur, err := url.Parse(href.String()) - if err != nil { - return obj - } - q := ur.Query().Get(api.QueryKeyQuery) - if q == "" { - return obj - } - u := builder.NewURLBuilder('h').AppendQuery(q) - assoc = assoc.Cons(sx.Cons(symHref, sx.String(u.String()))) - return rest.Cons(assoc.Cons(symAttr)).Cons(symA) - }) - te.Rebind(sz.NameSymLinkExternal, func(args []sx.Object, prevFn sxeval.Callable) sx.Object { - obj, err := prevFn.Call(nil, args) - if err != nil { - return sx.Nil() - } - attr, assoc, rest := findA(obj) - if attr == nil { - return obj - } - assoc = assoc.Cons(sx.Cons(symClass, sx.String("external"))). - Cons(sx.Cons(symTarget, sx.String("_blank"))). - Cons(sx.Cons(symRel, sx.String("noopener noreferrer"))) - return rest.Cons(assoc.Cons(symAttr)).Cons(symA) - }) - te.Rebind(sz.NameSymEmbed, func(args []sx.Object, prevFn sxeval.Callable) sx.Object { - obj, err := prevFn.Call(nil, args) - if err != nil { - return sx.Nil() - } - pair, isPair := sx.GetPair(obj) - if !isPair || !symImg.IsEqual(pair.Car()) { - return obj - } - attr, isPair := sx.GetPair(pair.Tail().Car()) - if !isPair || !symAttr.IsEqual(attr.Car()) { - return obj - } - symSrc := th.Make("src") - srcP := attr.Tail().Assoc(symSrc) - if srcP == nil { - return obj - } - src, isString := sx.GetString(srcP.Cdr()) - if !isString { - return obj - } - zid := api.ZettelID(src) - if !zid.IsValid() { - return obj - } - u := builder.NewURLBuilder('z').SetZid(zid) - imgAttr := attr.Tail().Cons(sx.Cons(symSrc, sx.String(u.String()))).Cons(symAttr) - return pair.Tail().Tail().Cons(imgAttr).Cons(symImg) - }) + rebind(th, sz.NameSymLinkZettel, linkZettel) + rebind(th, sz.NameSymLinkFound, linkZettel) + rebind(th, sz.NameSymLinkBased, func(obj sx.Object) sx.Object { + attr, assoc, rest := findA(obj) + if attr == nil { + return obj + } + hrefP := assoc.Assoc(symHref) + if hrefP == nil { + return obj + } + href, ok := sx.GetString(hrefP.Cdr()) + if !ok { + return obj + } + u := builder.NewURLBuilder('/').SetRawLocal(href.String()) + assoc = assoc.Cons(sx.Cons(symHref, sx.String(u.String()))) + return rest.Cons(assoc.Cons(symAttr)).Cons(symA) + }) + rebind(th, sz.NameSymLinkQuery, func(obj sx.Object) sx.Object { + attr, assoc, rest := findA(obj) + if attr == nil { + return obj + } + hrefP := assoc.Assoc(symHref) + if hrefP == nil { + return obj + } + href, ok := sx.GetString(hrefP.Cdr()) + if !ok { + return obj + } + ur, err := url.Parse(href.String()) + if err != nil { + return obj + } + q := ur.Query().Get(api.QueryKeyQuery) + if q == "" { + return obj + } + u := builder.NewURLBuilder('h').AppendQuery(q) + assoc = assoc.Cons(sx.Cons(symHref, sx.String(u.String()))) + return rest.Cons(assoc.Cons(symAttr)).Cons(symA) + }) + rebind(th, sz.NameSymLinkExternal, func(obj sx.Object) sx.Object { + attr, assoc, rest := findA(obj) + if attr == nil { + return obj + } + assoc = assoc.Cons(sx.Cons(symClass, sx.String("external"))). + Cons(sx.Cons(symTarget, sx.String("_blank"))). + Cons(sx.Cons(symRel, sx.String("noopener noreferrer"))) + return rest.Cons(assoc.Cons(symAttr)).Cons(symA) + }) + rebind(th, sz.NameSymEmbed, func(obj sx.Object) sx.Object { + pair, isPair := sx.GetPair(obj) + if !isPair || !symImg.IsEqual(pair.Car()) { + return obj + } + attr, isPair := sx.GetPair(pair.Tail().Car()) + if !isPair || !symAttr.IsEqual(attr.Car()) { + return obj + } + symSrc := th.Make("src") + srcP := attr.Tail().Assoc(symSrc) + if srcP == nil { + return obj + } + src, isString := sx.GetString(srcP.Cdr()) + if !isString { + return obj + } + zid := api.ZettelID(src) + if !zid.IsValid() { + return obj + } + u := builder.NewURLBuilder('z').SetZid(zid) + imgAttr := attr.Tail().Cons(sx.Cons(symSrc, sx.String(u.String()))).Cons(symAttr) + return pair.Tail().Tail().Cons(imgAttr).Cons(symImg) }) return &htmlGenerator{ tx: szenc.NewTransformer(), th: th, + lang: lang, symAt: symAttr, } } + +func rebind(ev *shtml.Evaluator, name string, fn func(sx.Object) sx.Object) { + prevFn := ev.ResolveBinding(name) + ev.Rebind(name, func(args []sx.Object, env *shtml.Environment) sx.Object { + obj := prevFn(args, env) + if env.GetError() == nil { + return fn(obj) + } + return sx.Nil() + }) +} // SetUnique sets a prefix to make several HTML ids unique. func (g *htmlGenerator) SetUnique(s string) *htmlGenerator { g.th.SetUnique(s); return g } var mapMetaKey = map[string]string{ @@ -208,11 +198,12 @@ api.KeyLicense: "license", } func (g *htmlGenerator) MetaSxn(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair { tm := g.tx.GetMeta(m, evalMeta) - hm, err := g.th.Transform(tm) + env := shtml.MakeEnvironment(g.lang) + hm, err := g.th.Evaluate(tm, &env) if err != nil { return nil } ignore := strfun.NewSet(api.KeyTitle, api.KeyLang) @@ -253,11 +244,11 @@ newName, found := mapMetaKey[name] if !found { continue } a = a.Set("name", newName) - metaMap[newName] = g.th.TransformMeta(a) + metaMap[newName] = g.th.EvaluateMeta(a) } result := sx.Nil() keys := maps.Keys(metaMap) for i := len(keys) - 1; i >= 0; i-- { result = result.Cons(metaMap[keys[i]]) @@ -275,32 +266,34 @@ } metaTags := sb.String() if len(metaTags) == 0 { return nil } - return g.th.TransformMeta(attrs.Attributes{"name": "keywords", "content": metaTags}) + return g.th.EvaluateMeta(attrs.Attributes{"name": "keywords", "content": metaTags}) } func (g *htmlGenerator) BlocksSxn(bs *ast.BlockSlice) (content, endnotes *sx.Pair, _ error) { if bs == nil || len(*bs) == 0 { return nil, nil, nil } sx := g.tx.GetSz(bs) - sh, err := g.th.Transform(sx) + env := shtml.MakeEnvironment(g.lang) + sh, err := g.th.Evaluate(sx, &env) if err != nil { return nil, nil, err } - return sh, g.th.Endnotes(), nil + return sh, g.th.Endnotes(&env), nil } // InlinesSxHTML returns an inline slice, encoded as a SxHTML object. func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair { if is == nil || len(*is) == 0 { return nil } sx := g.tx.GetSz(is) - sh, err := g.th.Transform(sx) + env := shtml.MakeEnvironment(g.lang) + sh, err := g.th.Evaluate(sx, &env) if err != nil { return nil } return sh } Index: web/adapter/webui/lists.go ================================================================== --- web/adapter/webui/lists.go +++ web/adapter/webui/lists.go @@ -33,15 +33,18 @@ "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML. -func (wui *WebUI) MakeListHTMLMetaHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, reIndex *usecase.ReIndex) http.HandlerFunc { +func (wui *WebUI) MakeListHTMLMetaHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { urlQuery := r.URL.Query() if wui.handleTagZettel(w, r, tagZettel, urlQuery) { return + } + if wui.handleRoleZettel(w, r, roleZettel, urlQuery) { + return } q := adapter.GetQuery(urlQuery) q = q.SetDeterministic() ctx := r.Context() metaSeq, err := queryMeta.Run(ctx, q) @@ -75,11 +78,11 @@ } } } var content, endnotes *sx.Pair if bn := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig); bn != nil { - enc := wui.getSimpleHTMLEncoder() + enc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, nil, api.KeyLang)) content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn}) if err != nil { wui.reportError(ctx, w, err) return } @@ -101,13 +104,22 @@ if tzl := q.GetMetaValues(api.KeyTags); len(tzl) > 0 { sxTzl, sxNoTzl := wui.transformTagZettelList(ctx, tagZettel, tzl) if !sx.IsNil(sxTzl) { rb.bindString("tag-zettel", sxTzl) } - if !sx.IsNil(sxNoTzl) { + if !sx.IsNil(sxNoTzl) && wui.canCreate(ctx, user) { rb.bindString("create-tag-zettel", sxNoTzl) } + } + if rzl := q.GetMetaValues(api.KeyRole); len(rzl) > 0 { + sxRzl, sxNoRzl := wui.transformRoleZettelList(ctx, roleZettel, rzl) + if !sx.IsNil(sxRzl) { + rb.bindString("role-zettel", sxRzl) + } + if !sx.IsNil(sxNoRzl) && wui.canCreate(ctx, user) { + rb.bindString("create-role-zettel", sxNoRzl) + } } rb.bindString("content", content) rb.bindString("endnotes", endnotes) apiURL := wui.NewURLBuilder('z').AppendQuery(q.String()) seed, found := q.GetSeed() @@ -134,29 +146,44 @@ } func (wui *WebUI) transformTagZettelList(ctx context.Context, tagZettel *usecase.TagZettel, tags []string) (withZettel, withoutZettel *sx.Pair) { slices.Reverse(tags) for _, tag := range tags { + tag = meta.NormalizeTag(tag) if _, err := tagZettel.Run(ctx, tag); err == nil { u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyTag, tag) - withZettel = wui.prependTagZettel(withZettel, tag, u) + withZettel = wui.prependZettelLink(withZettel, tag, u) } else { u := wui.NewURLBuilder('c').SetZid(api.ZidTemplateNewTag).AppendKVQuery(queryKeyAction, valueActionNew).AppendKVQuery(api.KeyTitle, tag) - withoutZettel = wui.prependTagZettel(withoutZettel, tag, u) + withoutZettel = wui.prependZettelLink(withoutZettel, tag, u) + } + } + return withZettel, withoutZettel +} + +func (wui *WebUI) transformRoleZettelList(ctx context.Context, roleZettel *usecase.RoleZettel, roles []string) (withZettel, withoutZettel *sx.Pair) { + slices.Reverse(roles) + for _, role := range roles { + if _, err := roleZettel.Run(ctx, role); err == nil { + u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyRole, role) + withZettel = wui.prependZettelLink(withZettel, role, u) + } else { + u := wui.NewURLBuilder('c').SetZid(api.ZidTemplateNewRole).AppendKVQuery(queryKeyAction, valueActionNew).AppendKVQuery(api.KeyTitle, role) + withoutZettel = wui.prependZettelLink(withoutZettel, role, u) } } return withZettel, withoutZettel } -func (wui *WebUI) prependTagZettel(sxZtl *sx.Pair, tag string, u *api.URLBuilder) *sx.Pair { +func (wui *WebUI) prependZettelLink(sxZtl *sx.Pair, name string, u *api.URLBuilder) *sx.Pair { link := sx.MakeList( wui.symA, sx.MakeList( wui.symAttr, sx.Cons(wui.symHref, sx.String(u.String())), ), - sx.String(tag), + sx.String(name), ) if sxZtl != nil { sxZtl = sxZtl.Cons(sx.String(", ")) } return sxZtl.Cons(link) @@ -175,11 +202,11 @@ var err error if _, err = io.WriteString(w, xml.Header); err == nil { _, err = w.Write(data) } if err != nil { - wui.log.IfErr(err).Msg("unable to write RSS data") + wui.log.Error().Err(err).Msg("unable to write RSS data") } } func (wui *WebUI) renderAtom(w http.ResponseWriter, q *query.Query, ml []*meta.Meta) { var atomConfig atom.Configuration @@ -194,11 +221,11 @@ var err error if _, err = io.WriteString(w, xml.Header); err == nil { _, err = w.Write(data) } if err != nil { - wui.log.IfErr(err).Msg("unable to write Atom data") + wui.log.Error().Err(err).Msg("unable to write Atom data") } } func (wui *WebUI) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool { tag := vals.Get(api.QueryKeyTag) @@ -208,9 +235,24 @@ ctx := r.Context() z, err := tagZettel.Run(ctx, tag) if err != nil { wui.reportError(ctx, w, err) return true + } + wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(z.Meta.Zid.String()))) + return true +} + +func (wui *WebUI) handleRoleZettel(w http.ResponseWriter, r *http.Request, roleZettel *usecase.RoleZettel, vals url.Values) bool { + role := vals.Get(api.QueryKeyRole) + if role == "" { + return false + } + ctx := r.Context() + z, err := roleZettel.Run(ctx, role) + if err != nil { + wui.reportError(ctx, w, err) + return true } wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(z.Meta.Zid.String()))) return true } Index: web/adapter/webui/sxn_code.go ================================================================== --- web/adapter/webui/sxn_code.go +++ web/adapter/webui/sxn_code.go @@ -98,10 +98,10 @@ } return err2 } wui.log.Debug().Zid(zid).Str("form", form.Repr()).Msg("Loaded sxn code") - if _, err2 = wui.engine.Eval(env, form); err2 != nil { + if _, err2 = wui.engine.Eval(form, env, nil); err2 != nil { return err2 } } } Index: web/adapter/webui/template.go ================================================================== --- web/adapter/webui/template.go +++ web/adapter/webui/template.go @@ -33,23 +33,23 @@ "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) func (wui *WebUI) createRenderEngine() *sxeval.Engine { - root := sxeval.MakeRootEnvironment(len(syntaxes) + len(builtins) + 3) + root := sxeval.MakeRootEnvironment(len(specials) + len(builtins) + 3) engine := sxeval.MakeEngine(wui.sf, root) - for _, syntax := range syntaxes { - engine.BindSyntax(syntax) + for _, syntax := range specials { + engine.BindSpecial(syntax) } for _, b := range builtins { engine.BindBuiltin(b) } engine.BindBuiltin(&sxeval.Builtin{ Name: "url-to-html", MinArity: 1, MaxArity: 1, - IsPure: true, + TestPure: sxeval.AssertPure, Fn: func(_ *sxeval.Frame, args []sx.Object) (sx.Object, error) { text, err := sxbuiltins.GetString(args, 0) if err != nil { return nil, err } @@ -58,11 +58,11 @@ }) engine.BindBuiltin(&sxeval.Builtin{ Name: "zid-content-path", MinArity: 1, MaxArity: 1, - IsPure: true, + TestPure: sxeval.AssertPure, Fn: func(_ *sxeval.Frame, args []sx.Object) (sx.Object, error) { s, err := sxbuiltins.GetString(args, 0) if err != nil { return nil, err } @@ -76,11 +76,11 @@ }) engine.BindBuiltin(&sxeval.Builtin{ Name: "query->url", MinArity: 1, MaxArity: 1, - IsPure: true, + TestPure: sxeval.AssertPure, Fn: func(_ *sxeval.Frame, args []sx.Object) (sx.Object, error) { qs, err := sxbuiltins.GetString(args, 0) if err != nil { return nil, err } @@ -91,19 +91,19 @@ root.Freeze() return engine } var ( - syntaxes = []*sxeval.Syntax{ + specials = []*sxeval.Special{ &sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote &sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing &sxbuiltins.DefVarS, &sxbuiltins.DefConstS, // defvar, defconst - &sxbuiltins.SetXS, // set! - &sxbuiltins.DefineS, // define (DEPRECATED) &sxbuiltins.DefunS, &sxbuiltins.LambdaS, // defun, lambda + &sxbuiltins.SetXS, // set! &sxbuiltins.CondS, // cond &sxbuiltins.IfS, // if + &sxbuiltins.BeginS, // begin &sxbuiltins.DefMacroS, // defmacro } builtins = []*sxeval.Builtin{ &sxbuiltins.Identical, // == &sxbuiltins.NullP, // null? @@ -323,11 +323,11 @@ return lst } func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair { if footerZid, err := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyFooterZettel)); err == nil { if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil { - htmlEnc := wui.getSimpleHTMLEncoder().SetUnique("footer-") + htmlEnc := wui.getSimpleHTMLEncoder(wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang)).SetUnique("footer-") if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.Ast); err3 == nil { if content != nil && endnotes != nil { content.LastPair().SetCdr(sx.Cons(endnotes, nil)) } return content @@ -347,22 +347,22 @@ return nil, err } objs, err := reader.ReadAll() if err != nil { - wui.log.IfErr(err).Zid(zid).Msg("reading sxn template") + wui.log.Error().Err(err).Zid(zid).Msg("reading sxn template") return nil, err } if len(objs) != 1 { return nil, fmt.Errorf("expected 1 expression in template, but got %d", len(objs)) } - t, err := wui.engine.Parse(env, objs[0]) + t, err := wui.engine.Parse(objs[0], env) if err != nil { return nil, err } - wui.setSxnCache(zid, wui.engine.Rework(env, t)) + wui.setSxnCache(zid, wui.engine.Rework(t, env)) return t, nil } func (wui *WebUI) makeZettelReader(ctx context.Context, zid id.Zid) (*sxreader.Reader, error) { ztl, err := wui.box.GetZettel(ctx, zid) if err != nil { @@ -376,11 +376,11 @@ func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, env sxeval.Environment) (sx.Object, error) { templateExpr, err := wui.getSxnTemplate(ctx, zid, env) if err != nil { return nil, err } - return wui.engine.Execute(env, templateExpr) + return wui.engine.Execute(templateExpr, env, nil) } func (wui *WebUI) renderSxnTemplate(ctx context.Context, w http.ResponseWriter, templateID id.Zid, env sxeval.Environment) error { return wui.renderSxnTemplateStatus(ctx, w, http.StatusOK, templateID, env) } @@ -402,12 +402,13 @@ _, err = gen.WriteHTML(&sb, pageObj) if err != nil { return err } wui.prepareAndWriteHeader(w, code) - _, err = w.Write(sb.Bytes()) - wui.log.IfErr(err).Msg("Unable to write HTML via template") + if _, err = w.Write(sb.Bytes()); err != nil { + wui.log.Error().Err(err).Msg("Unable to write HTML via template") + } return nil // No error reporting, since we do not know what happended during write to client. } func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) Index: web/adapter/webui/webui.go ================================================================== --- web/adapter/webui/webui.go +++ web/adapter/webui/webui.go @@ -74,10 +74,11 @@ symMetaHeader *sx.Symbol symDetail *sx.Symbol symA, symHref *sx.Symbol symSpan *sx.Symbol symAttr *sx.Symbol + symAttrOpen *sx.Symbol } // webuiBox contains all box methods that are needed for WebUI operation. // // Note: these function must not do auth checking. @@ -131,10 +132,11 @@ symMetaHeader: sf.MustMake("META-HEADER"), symA: sf.MustMake("a"), symHref: sf.MustMake("href"), symSpan: sf.MustMake("span"), symAttr: sf.MustMake(sxhtml.NameSymAttr), + symAttrOpen: sf.MustMake("open"), } wui.engine = wui.createRenderEngine() wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(wui.observe) return wui @@ -193,11 +195,13 @@ func (wui *WebUI) canRefresh(user *meta.Meta) bool { return wui.policy.CanRefresh(user) } -func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator { return wui.createGenerator(wui) } +func (wui *WebUI) getSimpleHTMLEncoder(lang string) *htmlGenerator { + return wui.createGenerator(wui, lang) +} // GetURLPrefix returns the configured URL prefix of the web server. func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() } // NewURLBuilder creates a new URL builder object with the given key. Index: www/build.md ================================================================== --- www/build.md +++ www/build.md @@ -11,11 +11,12 @@ * [Git](https://git-scm.org) (so that Go can download some dependencies). See folder `docs/development` (a zettel box) for details. ## Clone the repository -Most of this is covered by the excellent Fossil [documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki). +Most of this is covered by the excellent Fossil +[documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki). 1. Create a directory to store your Fossil repositories. Let's assume, you have created `$HOME/fossils`. 1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossils/zettelstore.fossil`. 1. Create a working directory. @@ -63,15 +64,24 @@ ## A note on the use of Fossil Zettelstore is managed by the Fossil version control system. Fossil is an alternative to the ubiquitous Git version control system. However, Go seems to prefer Git and popular platforms that just support Git. -Some dependencies of Zettelstore, namely [Zettelstore client](https://zettelstore.de/client) and [sx](https://zettelstore.de/sx), are also managed by Fossil. +Some dependencies of Zettelstore, namely [Zettelstore +client](https://zettelstore.de/client) and [sx](https://zettelstore.de/sx), are +also managed by Fossil. Depending on your development setup, some error messages might occur. -If the error message mentions an environment variable called `GOVCS` you should set it to the value `GOVCS=zezzelstore.de:fossil` (alternatively more generous to `GOVCS=*:all`). -Since the Go build system is coupled with Git and some special platforms, you allow ot to download a Fossil repository from the host `zettelstore.de`. -The build tool set `GOVCS` to the right value, but you may use other `go` commands that try to download a Fossil repository. +If the error message mentions an environment variable called `GOVCS` you should +set it to the value `GOVCS=zettelstore.de:fossil` (alternatively more generous +to `GOVCS=*:all`). +Since the Go build system is coupled with Git and some special platforms, you +allow ot to download a Fossil repository from the host `zettelstore.de`. +The build tool set `GOVCS` to the right value, but you may use other `go` +commands that try to download a Fossil repository. -On some operating systems, namely Termux on Android, an error message might state that an user cannot be determined (`cannot determine user`). -In this case, Fossil is allowed to download the repository, but cannot associate it with an user name. -Set the environment variable `USER` to any user name, like: `USER=nobody go run tools/build.go build`. +On some operating systems, namely Termux on Android, an error message might +state that an user cannot be determined (`cannot determine user`). +In this case, Fossil is allowed to download the repository, but cannot +associate it with an user name. +Set the environment variable `USER` to any user name, like: +`USER=nobody go run tools/build.go build`. Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -1,9 +1,53 @@ Change Log + +

Changes for Version 0.17.0 (pending)

+ -

Changes for Version 0.16.0 (pending)

+

Changes for Version 0.16.1 (2023-12-28)

+ * Fix some Sxn definitions to allow role-based UI customizations. + (minor: webui) + +

Changes for Version 0.16.0 (2023-11-30)

+ * Sx function define is removed, as announced for version + 0.15.0. Use defvar (to define variables) or + defun (to define functions) instead. In addition + defunconst defines a constant function, which ensures a fixed + binding of its name to its function body (performance optimization). + (breaking: webui) + * Allow to determine a role zettel for a given role. + (major: api, webui) + * Present user the option to create a (missing) role zettel (in list view). + Results in a new predefined zettel with identifier 00000000090004, which + is a template for new role zettel. + (minor: webui) + * Timestamp values can be abbrevated by omitting most of its components. + Previously, such values that are not in the format YYYYMMDDhhmmss were + ignored. Now the following formats are also allowed: YYYY, YYYYMM, + YYYYMMDD, YYYYMMDDhh, YYYYMMDDhhmm. Querying and sorting work accordingly. + Previously, only a sequences of zeroes were appended, resulting in illegal + timestamps, e.g. for YYYY or YYYYMM. + (minor) + * SHTML encoder fixed w.r.t inline quoting. Previously, an <q> tag was + used, which is inappropriate. Restored smart quotes from version 0.3, but + with new SxHTML infrastructure. This affect the html encoder and the WebUI + too. Now, an empty quote should not result in a warning by HTML linters. + (minor: api, webui) + * Add new zettelmarkup inline formatting: ##Text## will mark / + highlight the given Text. It is typically used to highlight some text, + which is important for you, but not for the original author. When rendered + as HTML, the <mark> tag is used. + (minor: zettelmarkup) + * Add configuration keys to show, not to show, or show the closed list of + referencing zettel in the web user interface. You can set these + configurations system-wide, per user, or per zettel. Often it is used to + ensure a “clean” home zettel. Affects the list of incoming + / back links, folge zettel, subordinate zettel, and successor zettel. + (minor: webui) + * Some smaller bug fixes and improvements, to the software and to the + documentation.

Changes for Version 0.15.0 (2023-10-26)

* Sx function define is now deprecated. It will be removed in version 0.16. Use defvar or defun instead. Otherwise Index: www/download.wiki ================================================================== --- www/download.wiki +++ www/download.wiki @@ -7,20 +7,20 @@ * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it.

ZIP-ped Executables

-Build: v0.15.0 (2023-10-26). +Build: v0.16.1 (2023-12-28). - * [/uv/zettelstore-0.15.0-linux-amd64.zip|Linux] (amd64) - * [/uv/zettelstore-0.15.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) - * [/uv/zettelstore-0.15.0-darwin-arm64.zip|macOS] (arm64) - * [/uv/zettelstore-0.15.0-darwin-amd64.zip|macOS] (amd64) - * [/uv/zettelstore-0.15.0-windows-amd64.zip|Windows] (amd64) + * [/uv/zettelstore-0.16.1-linux-amd64.zip|Linux] (amd64) + * [/uv/zettelstore-0.16.1-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) + * [/uv/zettelstore-0.16.1-darwin-arm64.zip|macOS] (arm64) + * [/uv/zettelstore-0.16.1-darwin-amd64.zip|macOS] (amd64) + * [/uv/zettelstore-0.16.1-windows-amd64.zip|Windows] (amd64) Unzip the appropriate file, install and execute Zettelstore according to the manual.

Zettel for the manual

As a starter, you can download the zettel for the manual -[/uv/manual-0.15.0.zip|here]. +[/uv/manual-0.16.1.zip|here]. Just unzip the contained files and put them into your zettel folder or configure a file box to read the zettel directly from the ZIP file. Index: www/index.wiki ================================================================== --- www/index.wiki +++ www/index.wiki @@ -24,17 +24,17 @@ * [https://zettelstore.de/sx|Sx] provides an evaluator for symbolic expressions, which is unsed for HTML templates and more. [https://mastodon.social/tags/Zettelstore|Stay tuned] …
-

Latest Release: 0.15.0 (2023-10-26)

+

Latest Release: 0.16.1 (2023-12-28)

* [./download.wiki|Download] - * [./changes.wiki#0_15|Change summary] - * [/timeline?p=v0.15.0&bt=v0.14.0&y=ci|Check-ins for version 0.15.0], - [/vdiff?to=v0.15.0&from=v0.14.0|content diff] - * [/timeline?df=v0.15.0&y=ci|Check-ins derived from the 0.15.0 release], - [/vdiff?from=v0.15.0&to=trunk|content diff] + * [./changes.wiki#0_16|Change summary] + * [/timeline?p=v0.16.1&bt=v0.15.0&y=ci|Check-ins for version 0.16.1], + [/vdiff?to=v0.16.1&from=v0.15.0|content diff] + * [/timeline?df=v0.16.0&y=ci|Check-ins derived from the 0.16.0 release], + [/vdiff?from=v0.16.0&to=trunk|content diff] * [./plan.wiki|Limitations and planned improvements] * [/timeline?t=release|Timeline of all past releases]

Build instructions

Index: zettel/id/id.go ================================================================== --- zettel/id/id.go +++ zettel/id/id.go @@ -113,20 +113,17 @@ // Based on idea by Daniel Lemire: "Converting integers to fix-digit representations quickly" // https://lemire.me/blog/2021/11/18/converting-integers-to-fix-digit-representations-quickly/ func (zid Zid) toByteArray(result *[14]byte) { date := uint64(zid) / 1000000 fullyear := date / 10000 - century := fullyear / 100 - year := fullyear % 100 + century, year := fullyear/100, fullyear%100 monthday := date % 10000 - month := monthday / 100 - day := monthday % 100 + month, day := monthday/100, monthday%100 time := uint64(zid) % 1000000 - hmtime := time / 100 - second := time % 100 - hour := hmtime / 100 - minute := hmtime % 100 + hmtime, second := time/100, time%100 + hour, minute := hmtime/100, hmtime%100 + result[0] = byte(century/10) + '0' result[1] = byte(century%10) + '0' result[2] = byte(year/10) + '0' result[3] = byte(year%10) + '0' result[4] = byte(month/10) + '0' @@ -142,23 +139,23 @@ } // IsValid determines if zettel id is a valid one, e.g. consists of max. 14 digits. func (zid Zid) IsValid() bool { return 0 < zid && zid <= maxZid } -// ZidLayout to transform a date into a Zid and into other internal dates. -const ZidLayout = "20060102150405" +// TimestampLayout to transform a date into a Zid and into other internal dates. +const TimestampLayout = "20060102150405" // New returns a new zettel id based on the current time. func New(withSeconds bool) Zid { now := time.Now().Local() var s string if withSeconds { - s = now.Format(ZidLayout) + s = now.Format(TimestampLayout) } else { s = now.Format("20060102150400") } res, err := Parse(s) if err != nil { panic(err) } return res } Index: zettel/meta/meta_test.go ================================================================== --- zettel/meta/meta_test.go +++ zettel/meta/meta_test.go @@ -64,13 +64,13 @@ if got, ok := m.Get(api.KeyTitle); !ok || got != at { t.Errorf("Title is not %q, but %q", at, got) } } -func checkSet(t *testing.T, exp []string, m *Meta, key string) { +func checkTags(t *testing.T, exp []string, m *Meta) { t.Helper() - got, _ := m.GetList(key) + got, _ := m.GetList(api.KeyTags) for i, tag := range exp { if i < len(got) { if tag != got[i] { t.Errorf("Pos=%d, expected %q, got %q", i, exp[i], got[i]) } @@ -84,23 +84,23 @@ } func TestTagsHeader(t *testing.T) { t.Parallel() m := New(testID) - checkSet(t, []string{}, m, api.KeyTags) + checkTags(t, []string{}, m) addToMeta(m, api.KeyTags, "") - checkSet(t, []string{}, m, api.KeyTags) + checkTags(t, []string{}, m) addToMeta(m, api.KeyTags, " #t1 #t2 #t3 #t4 ") - checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, api.KeyTags) + checkTags(t, []string{"#t1", "#t2", "#t3", "#t4"}, m) addToMeta(m, api.KeyTags, "#t5") - checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags) + checkTags(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m) addToMeta(m, api.KeyTags, "t6") - checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags) + checkTags(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m) } func TestSyntax(t *testing.T) { t.Parallel() m := New(testID) Index: zettel/meta/parse_test.go ================================================================== --- zettel/meta/parse_test.go +++ zettel/meta/parse_test.go @@ -74,17 +74,18 @@ {api.KeyTags + ": #c # #", "c"}, {api.KeyTags + ": #c # #b", "b c"}, } for i, tc := range testcases { m := parseMetaStr(tc.src) - tags, found := m.GetTags(api.KeyTags) + tagsString, found := m.Get(api.KeyTags) if !found { if tc.exp != "" { t.Errorf("%d / %q: no %s found", i, tc.src, api.KeyTags) } continue } + tags := meta.TagsFromValue(tagsString) if tc.exp == "" && len(tags) > 0 { t.Errorf("%d / %q: expected no %s, but got %v", i, tc.src, api.KeyTags, tags) continue } got := strings.Join(tags, " ") Index: zettel/meta/type.go ================================================================== --- zettel/meta/type.go +++ zettel/meta/type.go @@ -119,11 +119,11 @@ } } // SetNow stores the current timestamp under the given key. func (m *Meta) SetNow(key string) { - m.Set(key, time.Now().Local().Format(id.ZidLayout)) + m.Set(key, time.Now().Local().Format(id.TimestampLayout)) } // BoolValue returns the value interpreted as a bool. func BoolValue(value string) bool { if len(value) > 0 { @@ -143,22 +143,33 @@ return false } // TimeValue returns the time value of the given value. func TimeValue(value string) (time.Time, bool) { - if t, err := time.Parse(id.ZidLayout, value); err == nil { + if t, err := time.Parse(id.TimestampLayout, ExpandTimestamp(value)); err == nil { return t, true } return time.Time{}, false } -// GetTime returns the time value of the given key. -func (m *Meta) GetTime(key string) (time.Time, bool) { - if value, ok := m.Get(key); ok { - return TimeValue(value) +// ExpandTimestamp makes a short-form timestamp larger. +func ExpandTimestamp(value string) string { + switch l := len(value); l { + case 4: // YYYY + return value + "0101000000" + case 6: // YYYYMM + return value + "01000000" + case 8, 10, 12: // YYYYMMDD, YYYYMMDDhh, YYYYMMDDhhmm + return value + "000000"[:14-l] + case 14: // YYYYMMDDhhmmss + return value + default: + if l > 14 { + return value[:14] + } + return value } - return time.Time{}, false } // ListFromValue transforms a string value into a list value. func ListFromValue(value string) []string { return strings.Fields(value) @@ -172,31 +183,36 @@ return nil, false } return ListFromValue(value), true } -// GetTags returns the list of tags as a string list. Each tag does not begin -// with the '#' character, in contrast to `GetList`. -func (m *Meta) GetTags(key string) ([]string, bool) { - tagsValue, ok := m.Get(key) - if !ok { - return nil, false - } - tags := ListFromValue(strings.ToLower(tagsValue)) - for i, tag := range tags { - tags[i] = CleanTag(tag) - } - return tags, len(tags) > 0 +// TagsFromValue returns the value as a sequence of normalized tags. +func TagsFromValue(value string) []string { + tags := ListFromValue(strings.ToLower(value)) + for i, tag := range tags { + if len(tag) > 1 && tag[0] == '#' { + tags[i] = tag[1:] + } + } + return tags } // CleanTag removes the number character ('#') from a tag value and lowercases it. func CleanTag(tag string) string { if len(tag) > 1 && tag[0] == '#' { return tag[1:] } return tag } + +// NormalizeTag adds a missing prefix "#" to the tag +func NormalizeTag(tag string) string { + if len(tag) > 0 && tag[0] == '#' { + return tag + } + return "#" + tag +} // GetNumber retrieves the numeric value of a given key. func (m *Meta) GetNumber(key string, def int64) int64 { if value, ok := m.Get(key); ok { if num, err := strconv.ParseInt(value, 10, 64); err == nil { Index: zettel/meta/type_test.go ================================================================== --- zettel/meta/type_test.go +++ zettel/meta/type_test.go @@ -31,16 +31,16 @@ t.Errorf("Value is not 14 digits long: %q", val) } if _, err := strconv.ParseInt(val, 10, 64); err != nil { t.Errorf("Unable to parse %q as an int64: %v", val, err) } - if _, ok = m.GetTime("key"); !ok { + if _, ok = meta.TimeValue(val); !ok { t.Errorf("Unable to get time from value %q", val) } } -func TestGetTime(t *testing.T) { +func TestTimeValue(t *testing.T) { t.Parallel() testCases := []struct { value string valid bool exp time.Time @@ -48,10 +48,22 @@ {"", false, time.Time{}}, {"1", false, time.Time{}}, {"00000000000000", false, time.Time{}}, {"98765432109876", false, time.Time{}}, {"20201221111905", true, time.Date(2020, time.December, 21, 11, 19, 5, 0, time.UTC)}, + {"2023", true, time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC)}, + {"20231", false, time.Time{}}, + {"202310", true, time.Date(2023, time.October, 1, 0, 0, 0, 0, time.UTC)}, + {"2023103", false, time.Time{}}, + {"20231030", true, time.Date(2023, time.October, 30, 0, 0, 0, 0, time.UTC)}, + {"202310301", false, time.Time{}}, + {"2023103016", true, time.Date(2023, time.October, 30, 16, 0, 0, 0, time.UTC)}, + {"20231030165", false, time.Time{}}, + {"202310301654", true, time.Date(2023, time.October, 30, 16, 54, 0, 0, time.UTC)}, + {"2023103016541", false, time.Time{}}, + {"20231030165417", true, time.Date(2023, time.October, 30, 16, 54, 17, 0, time.UTC)}, + {"2023103916541700", false, time.Time{}}, } for i, tc := range testCases { got, ok := meta.TimeValue(tc.value) if ok != tc.valid { t.Errorf("%d: parsing of %q should be %v, but got %v", i, tc.value, tc.valid, ok) Index: zettel/meta/values.go ================================================================== --- zettel/meta/values.go +++ zettel/meta/values.go @@ -10,11 +10,10 @@ package meta import ( "fmt" - "strings" "zettelstore.de/client.fossil/api" ) // Supported syntax values. @@ -107,13 +106,5 @@ if ur, ok := urMap[val]; ok { return ur } return UserRoleUnknown } - -// NormalizeTag adds a missing prefix "#" to the tag -func NormalizeTag(tag string) string { - if strings.HasPrefix(tag, "#") { - return tag - } - return "#" + tag -}