Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.20.0-dev +0.18.0 Index: auth/auth.go ================================================================== --- auth/auth.go +++ auth/auth.go @@ -92,10 +92,13 @@ // User is allowed to read zettel CanRead(user, m *meta.Meta) bool // User is allowed to write zettel. CanWrite(user, oldMeta, newMeta *meta.Meta) bool + + // User is allowed to rename zettel + CanRename(user, m *meta.Meta) bool // User is allowed to delete zettel. CanDelete(user, m *meta.Meta) bool // User is allowed to refresh box data. Index: auth/policy/anon.go ================================================================== --- auth/policy/anon.go +++ auth/policy/anon.go @@ -33,10 +33,14 @@ } func (ap *anonPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return ap.pre.CanWrite(user, oldMeta, newMeta) && ap.checkVisibility(oldMeta) } + +func (ap *anonPolicy) CanRename(user, m *meta.Meta) bool { + return ap.pre.CanRename(user, m) && ap.checkVisibility(m) +} func (ap *anonPolicy) CanDelete(user, m *meta.Meta) bool { return ap.pre.CanDelete(user, m) && ap.checkVisibility(m) } Index: auth/policy/box.go ================================================================== --- auth/policy/box.go +++ auth/policy/box.go @@ -119,10 +119,26 @@ if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) { return pp.box.UpdateZettel(ctx, zettel) } return box.NewErrNotAllowed("Write", user, zid) } + +func (pp *polBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { + return pp.box.AllowRenameZettel(ctx, zid) +} + +func (pp *polBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { + z, err := pp.box.GetZettel(ctx, curZid) + if err != nil { + return err + } + user := server.GetUser(ctx) + if pp.policy.CanRename(user, z.Meta) { + return pp.box.RenameZettel(ctx, curZid, newZid) + } + return box.NewErrNotAllowed("Rename", user, curZid) +} func (pp *polBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return pp.box.CanDeleteZettel(ctx, zid) } Index: auth/policy/default.go ================================================================== --- auth/policy/default.go +++ auth/policy/default.go @@ -26,10 +26,11 @@ func (*defaultPolicy) CanCreate(_, _ *meta.Meta) bool { return true } func (*defaultPolicy) CanRead(_, _ *meta.Meta) bool { return true } func (d *defaultPolicy) CanWrite(user, oldMeta, _ *meta.Meta) bool { return d.canChange(user, oldMeta) } +func (d *defaultPolicy) CanRename(user, m *meta.Meta) bool { return d.canChange(user, m) } func (d *defaultPolicy) CanDelete(user, m *meta.Meta) bool { return d.canChange(user, m) } func (*defaultPolicy) CanRefresh(user *meta.Meta) bool { return user != nil } func (d *defaultPolicy) canChange(user, m *meta.Meta) bool { Index: auth/policy/owner.go ================================================================== --- auth/policy/owner.go +++ auth/policy/owner.go @@ -112,10 +112,20 @@ case meta.UserRoleReader, meta.UserRoleCreator: return false } return o.userCanCreate(user, newMeta) } + +func (o *ownerPolicy) CanRename(user, m *meta.Meta) bool { + if user == nil || !o.pre.CanRename(user, m) { + return false + } + if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok { + return res + } + return o.userIsOwner(user) +} func (o *ownerPolicy) CanDelete(user, m *meta.Meta) bool { if user == nil || !o.pre.CanDelete(user, m) { return false } Index: auth/policy/policy.go ================================================================== --- auth/policy/policy.go +++ auth/policy/policy.go @@ -57,10 +57,14 @@ func (p *prePolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool { return oldMeta != nil && newMeta != nil && oldMeta.Zid == newMeta.Zid && p.post.CanWrite(user, oldMeta, newMeta) } + +func (p *prePolicy) CanRename(user, m *meta.Meta) bool { + return m != nil && p.post.CanRename(user, m) +} func (p *prePolicy) CanDelete(user, m *meta.Meta) bool { return m != nil && p.post.CanDelete(user, m) } Index: auth/policy/policy_test.go ================================================================== --- auth/policy/policy_test.go +++ auth/policy/policy_test.go @@ -57,10 +57,11 @@ ts.readonly, ts.withAuth, ts.expert, ts.simple) t.Run(name, func(tt *testing.T) { testCreate(tt, pol, ts.withAuth, ts.readonly) testRead(tt, pol, ts.withAuth, ts.expert) testWrite(tt, pol, ts.withAuth, ts.readonly, ts.expert) + testRename(tt, pol, ts.withAuth, ts.readonly, ts.expert) testDelete(tt, pol, ts.withAuth, ts.readonly, ts.expert) testRefresh(tt, pol, ts.withAuth, ts.expert, ts.simple) }) } } @@ -391,10 +392,98 @@ {owner2, roTrue, roTrue, false}, } for _, tc := range testCases { t.Run("Write", func(tt *testing.T) { got := pol.CanWrite(tc.user, tc.old, tc.new) + if tc.exp != got { + tt.Errorf("exp=%v, but got=%v", tc.exp, got) + } + }) + } +} + +func testRename(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) { + t.Helper() + anonUser := newAnon() + creator := newCreator() + reader := newReader() + writer := newWriter() + owner := newOwner() + owner2 := newOwner2() + zettel := newZettel() + expertZettel := newExpertZettel() + roFalse := newRoFalseZettel() + roTrue := newRoTrueZettel() + roReader := newRoReaderZettel() + roWriter := newRoWriterZettel() + roOwner := newRoOwnerZettel() + notAuthNotReadonly := !withAuth && !readonly + testCases := []struct { + user *meta.Meta + meta *meta.Meta + exp bool + }{ + // No meta + {anonUser, nil, false}, + {creator, nil, false}, + {reader, nil, false}, + {writer, nil, false}, + {owner, nil, false}, + {owner2, nil, false}, + // Any zettel + {anonUser, zettel, notAuthNotReadonly}, + {creator, zettel, notAuthNotReadonly}, + {reader, zettel, notAuthNotReadonly}, + {writer, zettel, notAuthNotReadonly}, + {owner, zettel, !readonly}, + {owner2, zettel, !readonly}, + // Expert zettel + {anonUser, expertZettel, notAuthNotReadonly && expert}, + {creator, expertZettel, notAuthNotReadonly && expert}, + {reader, expertZettel, notAuthNotReadonly && expert}, + {writer, expertZettel, notAuthNotReadonly && expert}, + {owner, expertZettel, !readonly && expert}, + {owner2, expertZettel, !readonly && expert}, + // No r/o zettel + {anonUser, roFalse, notAuthNotReadonly}, + {creator, roFalse, notAuthNotReadonly}, + {reader, roFalse, notAuthNotReadonly}, + {writer, roFalse, notAuthNotReadonly}, + {owner, roFalse, !readonly}, + {owner2, roFalse, !readonly}, + // Reader r/o zettel + {anonUser, roReader, false}, + {creator, roReader, false}, + {reader, roReader, false}, + {writer, roReader, notAuthNotReadonly}, + {owner, roReader, !readonly}, + {owner2, roReader, !readonly}, + // Writer r/o zettel + {anonUser, roWriter, false}, + {creator, roWriter, false}, + {reader, roWriter, false}, + {writer, roWriter, false}, + {owner, roWriter, !readonly}, + {owner2, roWriter, !readonly}, + // Owner r/o zettel + {anonUser, roOwner, false}, + {creator, roOwner, false}, + {reader, roOwner, false}, + {writer, roOwner, false}, + {owner, roOwner, false}, + {owner2, roOwner, false}, + // r/o = true zettel + {anonUser, roTrue, false}, + {creator, roTrue, false}, + {reader, roTrue, false}, + {writer, roTrue, false}, + {owner, roTrue, false}, + {owner2, roTrue, false}, + } + for _, tc := range testCases { + t.Run("Rename", func(tt *testing.T) { + got := pol.CanRename(tc.user, tc.meta) if tc.exp != got { tt.Errorf("exp=%v, but got=%v", tc.exp, got) } }) } Index: auth/policy/readonly.go ================================================================== --- auth/policy/readonly.go +++ auth/policy/readonly.go @@ -18,7 +18,8 @@ type roPolicy struct{} func (*roPolicy) CanCreate(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanRead(_, _ *meta.Meta) bool { return true } func (*roPolicy) CanWrite(_, _, _ *meta.Meta) bool { return false } +func (*roPolicy) CanRename(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanDelete(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanRefresh(user *meta.Meta) bool { return user != nil } Index: box/box.go ================================================================== --- box/box.go +++ box/box.go @@ -34,10 +34,16 @@ // Format is dependent of the box. Location() string // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) + + // AllowRenameZettel returns true, if box will not disallow renaming the zettel. + AllowRenameZettel(ctx context.Context, zid id.Zid) bool + + // RenameZettel changes the current Zid to a new Zid. + RenameZettel(ctx context.Context, curZid, newZid id.Zid) error // CanDeleteZettel returns true, if box could possibly delete the given zettel. CanDeleteZettel(ctx context.Context, zid id.Zid) bool // DeleteZettel removes the zettel from the box. @@ -204,12 +210,11 @@ // Values for Reason const ( _ UpdateReason = iota OnReady // Box is started and fully operational OnReload // Box was reloaded - OnZettel // Something with an existing zettel happened - OnDelete // A zettel was deleted + OnZettel // Something with a zettel happened ) // UpdateInfo contains all the data about a changed zettel. type UpdateInfo struct { Box BaseBox @@ -218,13 +223,10 @@ } // UpdateFunc is a function to be called when a change is detected. type UpdateFunc func(UpdateInfo) -// UpdateNotifier is an UpdateFunc, but with separate values. -type UpdateNotifier func(BaseBox, id.Zid, UpdateReason) - // Subject is a box that notifies observers about changes. type Subject interface { // RegisterObserver registers an observer that will be notified // if one or all zettel are found to be changed. RegisterObserver(UpdateFunc) Index: box/compbox/compbox.go ================================================================== --- box/compbox/compbox.go +++ box/compbox/compbox.go @@ -30,19 +30,20 @@ ) func init() { manager.Register( " comp", - func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { - return getCompBox(cdata.Number, cdata.Enricher), nil + func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { + return getCompBox(cdata.Number, cdata.Enricher, cdata.Mapper), nil }) } type compBox struct { log *logger.Logger number int enricher box.Enricher + mapper manager.Mapper } var myConfig *meta.Meta var myZettel = map[id.Zid]struct { meta func(id.Zid) *meta.Meta @@ -62,19 +63,22 @@ // id.MustParse(api.ZidIndex): {genIndexM, genIndexC}, // id.MustParse(api.ZidQuery): {genQueryM, genQueryC}, id.MustParse(api.ZidMetadataKey): {genKeysM, genKeysC}, id.MustParse(api.ZidParser): {genParserM, genParserC}, id.MustParse(api.ZidStartupConfiguration): {genConfigZettelM, genConfigZettelC}, + id.MustParse(api.ZidWarnings): {genWarningsM, genWarningsC}, + id.MustParse(api.ZidMapping): {genMappingM, genMappingC}, } // Get returns the one program box. -func getCompBox(boxNumber int, mf box.Enricher) *compBox { +func getCompBox(boxNumber int, mf box.Enricher, mapper manager.Mapper) *compBox { return &compBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "comp").Int("boxnum", int64(boxNumber)).Child(), number: boxNumber, enricher: mf, + mapper: mapper, } } // Setup remembers important values. func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } @@ -135,10 +139,25 @@ } } } return nil } + +func (*compBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { + _, ok := myZettel[zid] + return !ok +} + +func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) (err error) { + if _, ok := myZettel[curZid]; ok { + err = box.ErrReadOnly + } else { + err = box.ErrZettelNotFound{Zid: curZid} + } + cb.log.Trace().Err(err).Msg("RenameZettel") + return err +} func (*compBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { if _, ok := myZettel[zid]; ok { ADDED box/compbox/mapping.go Index: box/compbox/mapping.go ================================================================== --- /dev/null +++ box/compbox/mapping.go @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2024-present Detlef Stern +// +// This file is part of Zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2024-present Detlef Stern +//----------------------------------------------------------------------------- + +package compbox + +import ( + "bytes" + "context" + + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) + +// Zettelstore Identifier Mapping. +// +// In the first stage of migration process, it is a computed zettel showing a +// hypothetical mapping. In later stages, it will be stored as a normal zettel +// that is updated when a new zettel is created or an old zettel is deleted. + +func genMappingM(zid id.Zid) *meta.Meta { + return getTitledMeta(zid, "Zettelstore Identifier Mapping") +} + +func genMappingC(ctx context.Context, cb *compBox) []byte { + var buf bytes.Buffer + toNew, err := cb.mapper.OldToNewMapping(ctx) + if err != nil { + buf.WriteString("**Error while fetching: ") + buf.WriteString(err.Error()) + buf.WriteString("**\n") + return buf.Bytes() + } + oldZids := id.NewSetCap(len(toNew)) + for zidO := range toNew { + oldZids.Add(zidO) + } + first := true + oldZids.ForEach(func(zidO id.Zid) { + if first { + buf.WriteString("**Note**: this mapping is preliminary.\n") + buf.WriteString("It only shows you how it could look if the migration is done.\n") + buf.WriteString("Use this page to update your zettel if something strange is shown.\n") + buf.WriteString("```\n") + first = false + } + buf.WriteString(zidO.String()) + buf.WriteByte(' ') + buf.WriteString(toNew[zidO].String()) + buf.WriteByte('\n') + }) + if !first { + buf.WriteString("```") + } + return buf.Bytes() +} Index: box/compbox/memory.go ================================================================== --- box/compbox/memory.go +++ box/compbox/memory.go @@ -40,12 +40,10 @@ fmt.Fprintf(&buf, "|Page Size|%d\n", pageSize) fmt.Fprintf(&buf, "|Pages|%d\n", m.HeapSys/uint64(pageSize)) fmt.Fprintf(&buf, "|Heap Objects|%d\n", m.HeapObjects) fmt.Fprintf(&buf, "|Heap Sys (KiB)|%d\n", m.HeapSys/1024) fmt.Fprintf(&buf, "|Heap Inuse (KiB)|%d\n", m.HeapInuse/1024) - fmt.Fprintf(&buf, "|CPUs|%d\n", runtime.NumCPU()) - fmt.Fprintf(&buf, "|Threads|%d\n", runtime.NumGoroutine()) debug := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool) if debug { for i, bysize := range m.BySize { fmt.Fprintf(&buf, "|Size %2d: %d|%d - %d → %d\n", i, bysize.Size, bysize.Mallocs, bysize.Frees, bysize.Mallocs-bysize.Frees) ADDED box/compbox/warnings.go Index: box/compbox/warnings.go ================================================================== --- /dev/null +++ box/compbox/warnings.go @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2024-present Detlef Stern +// +// This file is part of Zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2024-present Detlef Stern +//----------------------------------------------------------------------------- + +package compbox + +import ( + "bytes" + "context" + + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) + +func genWarningsM(zid id.Zid) *meta.Meta { + return getTitledMeta(zid, "Zettelstore Warnings") +} + +func genWarningsC(ctx context.Context, cb *compBox) []byte { + var buf bytes.Buffer + buf.WriteString("* [[Zettel without stored creation date|query:created-missing:true]]\n") + buf.WriteString("* [[Zettel with strange creation date|query:created<19700101000000]]\n") + + ws, err := cb.mapper.Warnings(ctx) + if err != nil { + buf.WriteString("**Error while fetching: ") + buf.WriteString(err.Error()) + buf.WriteString("**\n") + return buf.Bytes() + } + first := true + ws.ForEach(func(zid id.Zid) { + if first { + first = false + buf.WriteString("=== Mapper Warnings\n") + } + buf.WriteString("* [[") + buf.WriteString(zid.String()) + buf.WriteString("]]\n") + }) + + return buf.Bytes() +} Index: box/constbox/base.css ================================================================== --- box/constbox/base.css +++ box/constbox/base.css @@ -14,10 +14,11 @@ *,*::before,*::after { box-sizing: border-box; } html { + font-size: 1rem; font-family: serif; scroll-behavior: smooth; height: 100%; } body { @@ -86,40 +87,45 @@ article > * + * { margin-top: .5rem } article header { padding: 0; margin: 0; } - h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal; margin:.4em 0 } - h1 { font-size:1.5em } - h2 { font-size:1.25em } - h3 { font-size:1.15em } - h4 { font-size:1.05em; font-weight: bold } - h5 { font-size:1.05em } - h6 { font-size:1.05em; font-weight: lighter } - p { margin: .5em 0 0 0 } - p.zs-meta-zettel { margin-top: .5em; margin-left: .5em } + h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal } + h1 { font-size:1.5rem; margin:.65rem 0 } + h2 { font-size:1.25rem; margin:.70rem 0 } + h3 { font-size:1.15rem; margin:.75rem 0 } + h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold } + h5 { font-size:1.05rem; margin:.8rem 0 } + h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter } + p { margin: .5rem 0 0 0 } + p.zs-meta-zettel { margin-top: .5rem; margin-left: 0.5rem } li,figure,figcaption,dl { margin: 0 } - dt { margin: .5em 0 0 0 } + dt { margin: .5rem 0 0 0 } dt+dd { margin-top: 0 } - dd { margin: .5em 0 0 2em } + dd { margin: .5rem 0 0 2rem } dd > p:first-child { margin: 0 0 0 0 } blockquote { - border-left: .5em solid lightgray; - padding-left: 1em; - margin-left: 1em; - margin-right: 2em; + border-left: 0.5rem solid lightgray; + padding-left: 1rem; + margin-left: 1rem; + margin-right: 2rem; + font-style: italic; } - blockquote p { margin-bottom: .5em } + blockquote p { margin-bottom: .5rem } + blockquote cite { font-style: normal } table { border-collapse: collapse; border-spacing: 0; max-width: 100%; } - td, th {text-align: left; padding: .25em .5em;} - th { font-weight: bold } - thead th { border-bottom: 2px solid hsl(0, 0%, 70%) } - td { border-bottom: 1px solid hsl(0, 0%, 85%) } + thead>tr>td { border-bottom: 2px solid hsl(0, 0%, 70%); font-weight: bold } + tfoot>tr>td { border-top: 2px solid hsl(0, 0%, 70%); font-weight: bold } + td { + text-align: left; + padding: .25rem .5rem; + border-bottom: 1px solid hsl(0, 0%, 85%) + } main form { padding: 0 .5em; margin: .5em 0 0 0; } main form:after { @@ -151,100 +157,100 @@ padding-left: 1em; padding-right: 1em; } a:not([class]) { text-decoration-skip-ink: auto } a.broken { text-decoration: line-through } - a[rel~="external"]::after { content: "➚"; display: inline-block } + a.external::after { content: "➚"; display: inline-block } img { max-width: 100% } img.right { float: right } ol.zs-endnotes { - padding-top: .5em; + padding-top: .5rem; border-top: 1px solid; } kbd { font-family:monospace } code,pre { font-family: monospace; font-size: 85%; } code { - padding: .1em .2em; + padding: .1rem .2rem; background: #f0f0f0; border: 1px solid #ccc; - border-radius: .25em; + border-radius: .25rem; } pre { - padding: .5em .7em; + padding: .5rem .7rem; max-width: 100%; overflow: auto; border: 1px solid #ccc; - border-radius: .5em; + border-radius: .5rem; background: #f0f0f0; } pre code { font-size: 95%; position: relative; padding: 0; border: none; } div.zs-indication { - padding: .5em .7em; + padding: .5rem .7rem; max-width: 100%; - border-radius: .5em; + border-radius: .5rem; border: 1px solid black; } div.zs-indication p:first-child { margin-top: 0 } span.zs-indication { border: 1px solid black; - border-radius: .25em; - padding: .1rem .2em; + border-radius: .25rem; + padding: .1rem .2rem; font-size: 95%; } .zs-info { background-color: lightblue; - padding: .5em 1em; + padding: .5rem 1rem; } .zs-warning { background-color: lightyellow; - padding: .5em 1em; + padding: .5rem 1rem; } .zs-error { background-color: lightpink; border-style: none !important; font-weight: bold; } - td.left, th.left { text-align:left } - td.center, th.center { text-align:center } - td.right, th.right { text-align:right } + td.left { text-align:left } + td.center { text-align:center } + td.right { text-align:right } .zs-font-size-0 { font-size:75% } .zs-font-size-1 { font-size:83% } .zs-font-size-2 { font-size:100% } .zs-font-size-3 { font-size:117% } .zs-font-size-4 { font-size:150% } .zs-font-size-5 { font-size:200% } - .zs-deprecated { border-style: dashed; padding: .2em } + .zs-deprecated { border-style: dashed; padding: .2rem } .zs-meta { font-size:.75rem; color:#444; - margin-bottom:1em; + margin-bottom:1rem; } .zs-meta a { color:#444 } - h1+.zs-meta { margin-top:-1em } - nav > details { margin-top:1em } + h1+.zs-meta { margin-top:-1rem } + nav > details { margin-top:1rem } details > summary { width: 100%; background-color: #eee; font-family:sans-serif; } details > ul { margin-top:0; - padding-left:2em; + padding-left:2rem; background-color: #eee; } - footer { padding: 0 1em } + footer { padding: 0 1rem } @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } Index: box/constbox/constbox.go ================================================================== --- box/constbox/constbox.go +++ box/constbox/constbox.go @@ -31,11 +31,11 @@ ) func init() { manager.Register( " const", - func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { + func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return &constBox{ log: kernel.Main.GetLogger(kernel.BoxService).Clone(). Str("box", "const").Int("boxnum", int64(cdata.Number)).Child(), number: cdata.Number, zettel: constZettelMap, @@ -94,10 +94,25 @@ handle(m) } } return nil } + +func (cb *constBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { + _, ok := cb.zettel[zid] + return !ok +} + +func (cb *constBox) RenameZettel(_ context.Context, curZid, _ id.Zid) (err error) { + if _, ok := cb.zettel[curZid]; ok { + err = box.ErrReadOnly + } else { + err = box.ErrZettelNotFound{Zid: curZid} + } + cb.log.Trace().Err(err).Msg("RenameZettel") + return err +} func (*constBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { if _, ok := cb.zettel[zid]; ok { @@ -184,21 +199,21 @@ constHeader{ api.KeyTitle: "Zettelstore Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230510155300", - api.KeyModified: "20241127170400", + api.KeyModified: "20240219145100", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentZettelSxn)}, id.InfoTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Info HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20200804111624", - api.KeyModified: "20241127170500", + api.KeyModified: "20240618170000", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentInfoSxn)}, id.FormTemplateZid: { constHeader{ @@ -208,17 +223,27 @@ api.KeyCreated: "20200804111624", api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentFormSxn)}, + id.RenameTemplateZid: { + constHeader{ + api.KeyTitle: "Zettelstore Rename Form HTML Template", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20200804111624", + api.KeyModified: "20240219145200", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentRenameSxn)}, id.DeleteTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Delete HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20200804111624", - api.KeyModified: "20241127170530", + api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentDeleteSxn)}, id.ListTemplateZid: { constHeader{ @@ -255,11 +280,11 @@ constHeader{ api.KeyTitle: "Zettelstore Sxn Base Code", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230619132800", - api.KeyModified: "20241118173500", + api.KeyModified: "20240618170100", api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityExpert, api.KeyPrecursor: string(api.ZidSxnPrelude), }, zettel.NewContent(contentBaseCodeSxn)}, @@ -278,11 +303,11 @@ constHeader{ api.KeyTitle: "Zettelstore Base CSS", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxCSS, api.KeyCreated: "20200804111624", - api.KeyModified: "20240827143500", + api.KeyModified: "20231129112800", api.KeyVisibility: api.ValueVisibilityPublic, }, zettel.NewContent(contentBaseCSS)}, id.MustParse(api.ZidUserCSS): { constHeader{ @@ -301,20 +326,10 @@ api.KeyReadOnly: api.ValueTrue, api.KeyCreated: "20210504175807", api.KeyVisibility: api.ValueVisibilityPublic, }, zettel.NewContent(contentEmoji)}, - id.TOCListsMenuZid: { - constHeader{ - api.KeyTitle: "Lists Menu", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, - api.KeyLang: api.ValueLangEN, - api.KeyCreated: "20241223205400", - api.KeyVisibility: api.ValueVisibilityPublic, - }, - zettel.NewContent(contentMenuListsZettel)}, id.TOCNewTemplateZid: { constHeader{ api.KeyTitle: "New Menu", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxZmk, @@ -321,11 +336,11 @@ api.KeyLang: api.ValueLangEN, api.KeyCreated: "20210217161829", api.KeyModified: "20231129111800", api.KeyVisibility: api.ValueVisibilityCreator, }, - zettel.NewContent(contentMenuNewZettel)}, + zettel.NewContent(contentNewTOCZettel)}, id.MustParse(api.ZidTemplateNewZettel): { constHeader{ api.KeyTitle: "New Zettel", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxZmk, @@ -382,11 +397,11 @@ id.MustParse(api.ZidRoleConfigurationZettel): { constHeader{ api.KeyTitle: api.ValueRoleConfiguration, api.KeyRole: api.ValueRoleRole, api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20241213103100", + api.KeyCreated: "20231129162800", api.KeyLang: api.ValueLangEN, api.KeyVisibility: api.ValueVisibilityLogin, }, zettel.NewContent(contentRoleConfiguration)}, id.MustParse(api.ZidRoleRoleZettel): { @@ -419,16 +434,15 @@ api.KeyVisibility: api.ValueVisibilityLogin, }, zettel.NewContent(nil)}, id.DefaultHomeZid: { constHeader{ - api.KeyTitle: "Home", - api.KeyRole: api.ValueRoleZettel, - api.KeySyntax: meta.SyntaxZmk, - api.KeyLang: api.ValueLangEN, - api.KeyCreated: "20210210190757", - api.KeyModified: "20241216105800", + api.KeyTitle: "Home", + api.KeyRole: api.ValueRoleZettel, + api.KeySyntax: meta.SyntaxZmk, + api.KeyLang: api.ValueLangEN, + api.KeyCreated: "20210210190757", }, zettel.NewContent(contentHomeZettel)}, } //go:embed license.txt @@ -453,10 +467,13 @@ var contentInfoSxn []byte //go:embed form.sxn var contentFormSxn []byte +//go:embed rename.sxn +var contentRenameSxn []byte + //go:embed delete.sxn var contentDeleteSxn []byte //go:embed listzettel.sxn var contentListZettelSxn []byte @@ -477,15 +494,12 @@ var contentBaseCSS []byte //go:embed emoji_spin.gif var contentEmoji []byte -//go:embed menu_lists.zettel -var contentMenuListsZettel []byte - -//go:embed menu_new.zettel -var contentMenuNewZettel []byte +//go:embed newtoc.zettel +var contentNewTOCZettel []byte //go:embed rolezettel.zettel var contentRoleZettel []byte //go:embed roleconfiguration.zettel Index: box/constbox/home.zettel ================================================================== --- box/constbox/home.zettel +++ box/constbox/home.zettel @@ -1,14 +1,15 @@ === Thank you for using Zettelstore! You will find the lastest information about Zettelstore at [[https://zettelstore.de]]. -Check this website regularly for [[updates|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version. -You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before updating. -Sometimes, you have to edit some of your Zettelstore-related zettel before updating. -Since Zettelstore is currently in a development state, every update might fix some of your problems. +Check that website regulary for [[upgrades|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version. +You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before upgrading. +Sometimes, you have to edit some of your Zettelstore-related zettel before upgrading. +Since Zettelstore is currently in a development state, every upgrade might fix some of your problems. -If you have problems concerning Zettelstore, do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. +If you have problems concerning Zettelstore, +do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]]. === Reporting errors If you have encountered an error, please include the content of the following zettel in your mail (if possible): * [[Zettelstore Version|00000000000001]]: {{00000000000001}} * [[Zettelstore Operating System|00000000000003]] Index: box/constbox/info.sxn ================================================================== --- box/constbox/info.sxn +++ box/constbox/info.sxn @@ -18,10 +18,11 @@ (@H " · ") (a (@ (href ,context-url)) "Context") (@H " / ") (a (@ (href ,context-full-url)) "Full") ,@(if (bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit"))) ,@(ROLE-DEFAULT-actions (current-binding)) ,@(if (bound? 'reindex-url) `((@H " · ") (a (@ (href ,reindex-url)) "Reindex"))) + ,@(if (bound? 'rename-url) `((@H " · ") (a (@ (href ,rename-url)) "Rename"))) ,@(if (bound? 'delete-url) `((@H " · ") (a (@ (href ,delete-url)) "Delete"))) ) ) (h2 "Interpreted Metadata") (table ,@(map wui-info-meta-table-row metadata)) DELETED box/constbox/menu_lists.zettel Index: box/constbox/menu_lists.zettel ================================================================== --- box/constbox/menu_lists.zettel +++ /dev/null @@ -1,7 +0,0 @@ -This zettel lists all entries of the ""Lists"" menu. - -* [[List Zettel|query:]] -* [[List Roles|query:|role]] -* [[List Tags|query:|tags]] - -An additional ""Refresh"" menu item is automatically added if appropriate. DELETED box/constbox/menu_new.zettel Index: box/constbox/menu_new.zettel ================================================================== --- box/constbox/menu_new.zettel +++ /dev/null @@ -1,6 +0,0 @@ -This zettel lists all zettel that should act as a template for new zettel. -These zettel will be included in the ""New"" menu of the WebUI. -* [[New Zettel|00000000090001]] -* [[New Role|00000000090004]] -* [[New Tag|00000000090003]] -* [[New User|00000000090002]] ADDED box/constbox/newtoc.zettel Index: box/constbox/newtoc.zettel ================================================================== --- /dev/null +++ box/constbox/newtoc.zettel @@ -0,0 +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 Role|00000000090004]] +* [[New Tag|00000000090003]] +* [[New User|00000000090002]] ADDED box/constbox/rename.sxn Index: box/constbox/rename.sxn ================================================================== --- /dev/null +++ box/constbox/rename.sxn @@ -0,0 +1,42 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present Detlef Stern +;;; +;;; This file is part of Zettelstore. +;;; +;;; Zettelstore is licensed under the latest version of the EUPL (European +;;; Union Public License). Please see file LICENSE.txt for your rights and +;;; obligations under this license. +;;; +;;; SPDX-License-Identifier: EUPL-1.2 +;;; SPDX-FileCopyrightText: 2023-present Detlef Stern +;;;---------------------------------------------------------------------------- + +`(article + (header (h1 "Rename Zettel " ,zid)) + (p "Do you really want to rename this zettel?") + ,@(if incoming + `((div (@ (class "zs-warning")) + (h2 "Warning!") + (p "If you rename this zettel, incoming references from the following zettel will become invalid.") + (ul ,@(map wui-item-link incoming)) + )) + ) + ,@(if (and (bound? 'useless) useless) + `((div (@ (class "zs-warning")) + (h2 "Warning!") + (p "Renaming this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.") + (ul ,@(map wui-item useless)) + )) + ) + (form (@ (method "POST")) + (input (@ (type "hidden") (id "curzid") (name "curzid") (value ,zid))) + (div + (label (@ (for "newzid")) "New zettel id") + (input (@ (class "zs-input") (type "text") (inputmode "numeric") (id "newzid") (name "newzid") + (pattern "\\d{14}") + (title "New zettel identifier, must be unique") + (placeholder "ZID..") (value ,zid) (autofocus)))) + (div (input (@ (class "zs-primary") (type "submit") (value "Rename")))) + ) + ,(wui-meta-desc metapairs) +) Index: box/constbox/roleconfiguration.zettel ================================================================== --- box/constbox/roleconfiguration.zettel +++ box/constbox/roleconfiguration.zettel @@ -1,10 +1,8 @@ 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. -There is some CSS code to make the default web user interface a litte bit nicer. -The default image to signal a broken image can be configured too. +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"". @@ -11,12 +9,12 @@ 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 to see them, you will discover some more zettel. +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. Index: box/constbox/wuicode.sxn ================================================================== --- box/constbox/wuicode.sxn +++ box/constbox/wuicode.sxn @@ -36,11 +36,11 @@ (defun 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) - `(li (a (@ (href ,e) (target "_blank") (rel "external noreferrer")) ,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)))) ;; wui-datalist returns a HTML datalist with the given HTML identifier and a @@ -90,12 +90,12 @@ (defun ROLE-DEFAULT-actions (binding) `(,@(let ((copy-url (binding-lookup 'copy-url binding))) (if (defined? copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy")))) ,@(let ((version-url (binding-lookup 'version-url binding))) (if (defined? version-url) `((@H " · ") (a (@ (href ,version-url)) "Version")))) - ,@(let ((sequel-url (binding-lookup 'sequel-url binding))) - (if (defined? sequel-url) `((@H " · ") (a (@ (href ,sequel-url)) "Sequel")))) + ,@(let ((child-url (binding-lookup 'child-url binding))) + (if (defined? child-url) `((@H " · ") (a (@ (href ,child-url)) "Child")))) ,@(let ((folge-url (binding-lookup 'folge-url binding))) (if (defined? folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge")))) ) ) Index: box/constbox/zettel.sxn ================================================================== --- box/constbox/zettel.sxn +++ box/constbox/zettel.sxn @@ -24,20 +24,20 @@ ")" ,@(if tag-refs `((@H " · ") ,@tag-refs)) ,@(ROLE-DEFAULT-actions (current-binding)) ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs)) ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs)) - ,@(if prequel-refs `((br) "Prequel: " ,prequel-refs)) + ,@(if superior-refs `((br) "Superior: " ,superior-refs)) ,@(ROLE-DEFAULT-heading (current-binding)) ) ) ,@content ,endnotes - ,@(if (or folge-links sequel-links back-links successor-links) + ,@(if (or folge-links subordinate-links back-links successor-links) `((nav ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links))))) - ,@(if sequel-links `((details (@ (,sequel-open)) (summary "Sequel") (ul ,@(map wui-item-link sequel-links))))) + ,@(if 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/dirbox/dirbox.go ================================================================== --- box/dirbox/dirbox.go +++ box/dirbox/dirbox.go @@ -201,14 +201,14 @@ for _, c := range dp.fCmds { close(c) } } -func (dp *dirBox) notifyChanged(zid id.Zid, reason box.UpdateReason) { - if notify := dp.cdata.Notify; notify != nil { - dp.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChanged") - notify(dp, zid, reason) +func (dp *dirBox) notifyChanged(zid id.Zid) { + if chci := dp.cdata.Notify; chci != nil { + dp.log.Trace().Zid(zid).Msg("notifyChanged") + chci <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid} } } func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd { // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function @@ -242,11 +242,11 @@ err = dp.srvSetZettel(ctx, &entry, zettel) if err == nil { err = dp.dirSrv.UpdateDirEntry(&entry) } - dp.notifyChanged(meta.Zid, box.OnZettel) + dp.notifyChanged(meta.Zid) dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel") return meta.Zid, err } func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { @@ -314,19 +314,65 @@ } dp.updateEntryFromMetaContent(entry, meta, zettel.Content) dp.dirSrv.UpdateDirEntry(entry) err := dp.srvSetZettel(ctx, entry, zettel) if err == nil { - dp.notifyChanged(zid, box.OnZettel) + dp.notifyChanged(zid) } dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel") return err } func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content zettel.Content) { entry.SetupFromMetaContent(m, content, dp.cdata.Config.GetZettelFileSyntax) } + +func (dp *dirBox) AllowRenameZettel(context.Context, id.Zid) bool { + return !dp.readonly +} + +func (dp *dirBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { + if curZid == newZid { + return nil + } + curEntry := dp.dirSrv.GetDirEntry(curZid) + if !curEntry.IsValid() { + return box.ErrZettelNotFound{Zid: curZid} + } + if dp.readonly { + return box.ErrReadOnly + } + + // Check whether zettel with new ID already exists in this box. + if dp.HasZettel(ctx, newZid) { + return box.ErrInvalidZid{Zid: newZid.String()} + } + + oldMeta, oldContent, err := dp.srvGetMetaContent(ctx, curEntry, curZid) + if err != nil { + return err + } + + newEntry, err := dp.dirSrv.RenameDirEntry(curEntry, newZid) + if err != nil { + return err + } + oldMeta.Zid = newZid + newZettel := zettel.Zettel{Meta: oldMeta, Content: zettel.NewContent(oldContent)} + if err = dp.srvSetZettel(ctx, &newEntry, newZettel); err != nil { + // "Rollback" rename. No error checking... + dp.dirSrv.RenameDirEntry(&newEntry, curZid) + return err + } + err = dp.srvDeleteZettel(ctx, curEntry, curZid) + if err == nil { + dp.notifyChanged(curZid) + dp.notifyChanged(newZid) + } + dp.log.Trace().Zid(curZid).Zid(newZid).Err(err).Msg("RenameZettel") + return err +} func (dp *dirBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { if dp.readonly { return false } @@ -347,11 +393,11 @@ if err != nil { return nil } err = dp.srvDeleteZettel(ctx, entry, zid) if err == nil { - dp.notifyChanged(zid, box.OnDelete) + dp.notifyChanged(zid) } dp.log.Trace().Zid(zid).Err(err).Msg("DeleteZettel") return err } Index: box/filebox/zipbox.go ================================================================== --- box/filebox/zipbox.go +++ box/filebox/zipbox.go @@ -33,11 +33,11 @@ type zipBox struct { log *logger.Logger number int name string enricher box.Enricher - notify box.UpdateNotifier + notify chan<- box.UpdateInfo dirSrv *notify.DirService } func (zb *zipBox) Location() string { if strings.HasPrefix(zb.name, "/") { @@ -169,10 +169,28 @@ zb.enricher.Enrich(ctx, m, zb.number) handle(m) } return nil } + +func (zb *zipBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { + entry := zb.dirSrv.GetDirEntry(zid) + return !entry.IsValid() +} + +func (zb *zipBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error { + err := box.ErrReadOnly + if curZid == newZid { + err = nil + } + curEntry := zb.dirSrv.GetDirEntry(curZid) + if !curEntry.IsValid() { + err = box.ErrZettelNotFound{Zid: curZid} + } + zb.log.Trace().Err(err).Msg("RenameZettel") + return err +} func (*zipBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } func (zb *zipBox) DeleteZettel(_ context.Context, zid id.Zid) error { err := box.ErrReadOnly Index: box/helper.go ================================================================== --- box/helper.go +++ box/helper.go @@ -45,22 +45,22 @@ _, ok := u.Query()[key] return ok } // GetQueryInt is a helper function to extract int values of a specified range from a box URI. -func GetQueryInt(u *url.URL, key string, minVal, defVal, maxVal int) int { +func GetQueryInt(u *url.URL, key string, min, def, max int) int { sVal := u.Query().Get(key) if sVal == "" { - return defVal + return def } iVal, err := strconv.Atoi(sVal) if err != nil { - return defVal + return def } - if iVal < minVal { - return minVal + if iVal < min { + return min } - if iVal > maxVal { - return maxVal + if iVal > max { + return max } return iVal } Index: box/manager/box.go ================================================================== --- box/manager/box.go +++ box/manager/box.go @@ -54,22 +54,22 @@ } return false } // CreateZettel creates a new zettel. -func (mgr *Manager) CreateZettel(ctx context.Context, ztl zettel.Zettel) (id.Zid, error) { +func (mgr *Manager) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { mgr.mgrLog.Debug().Msg("CreateZettel") if err := mgr.checkContinue(ctx); err != nil { return id.Invalid, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { - ztl.Meta = mgr.cleanMetaProperties(ztl.Meta) - zid, err := box.CreateZettel(ctx, ztl) + zettel.Meta = mgr.cleanMetaProperties(zettel.Meta) + zid, err := box.CreateZettel(ctx, zettel) if err == nil { - mgr.idxUpdateZettel(ctx, ztl) + mgr.idxUpdateZettel(ctx, zettel) } return zid, err } return id.Invalid, box.ErrReadOnly } @@ -80,13 +80,10 @@ if err := mgr.checkContinue(ctx); err != nil { return zettel.Zettel{}, err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - return mgr.getZettel(ctx, zid) -} -func (mgr *Manager) getZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { for i, p := range mgr.boxes { var errZNF box.ErrZettelNotFound if z, err := p.GetZettel(ctx, zid); !errors.As(err, &errZNF) { if err == nil { mgr.Enrich(ctx, z.Meta, i+1) @@ -140,11 +137,11 @@ } } return result, nil } -func (mgr *Manager) hasZettel(ctx context.Context, zid id.Zid) bool { +func (mgr *Manager) HasZettel(ctx context.Context, zid id.Zid) bool { mgr.mgrLog.Debug().Zid(zid).Msg("HasZettel") if err := mgr.checkContinue(ctx); err != nil { return false } mgr.mgrMx.RLock() @@ -155,20 +152,18 @@ } } return false } -// GetMeta returns just the metadata of the zettel with the given identifier. func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta") if err := mgr.checkContinue(ctx); err != nil { return nil, err } m, err := mgr.idxStore.GetMeta(ctx, zid) if err != nil { - // TODO: Call GetZettel and return just metadata, in case the index is not complete. return nil, err } mgr.Enrich(ctx, m, 0) return m, nil } @@ -244,13 +239,10 @@ func (mgr *Manager) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel") if err := mgr.checkContinue(ctx); err != nil { return err } - return mgr.updateZettel(ctx, zettel) -} -func (mgr *Manager) updateZettel(ctx context.Context, zettel zettel.Zettel) error { if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { zettel.Meta = mgr.cleanMetaProperties(zettel.Meta) if err := box.UpdateZettel(ctx, zettel); err != nil { return err } @@ -257,10 +249,47 @@ mgr.idxUpdateZettel(ctx, zettel) return nil } return box.ErrReadOnly } + +// AllowRenameZettel returns true, if box will not disallow renaming the zettel. +func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { + if err := mgr.checkContinue(ctx); err != nil { + return false + } + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + for _, p := range mgr.boxes { + if !p.AllowRenameZettel(ctx, zid) { + return false + } + } + return true +} + +// RenameZettel changes the current zid to a new zid. +func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { + mgr.mgrLog.Debug().Zid(curZid).Zid(newZid).Msg("RenameZettel") + if err := mgr.checkContinue(ctx); err != nil { + return err + } + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + for i, p := range mgr.boxes { + err := p.RenameZettel(ctx, curZid, newZid) + var errZNF box.ErrZettelNotFound + if err != nil && !errors.As(err, &errZNF) { + for j := range i { + mgr.boxes[j].RenameZettel(ctx, newZid, curZid) + } + return err + } + } + mgr.idxRenameZettel(ctx, curZid, newZid) + return nil +} // CanDeleteZettel returns true, if box could possibly delete the given zettel. func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { if err := mgr.checkContinue(ctx); err != nil { return false @@ -285,11 +314,11 @@ defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { err := p.DeleteZettel(ctx, zid) if err == nil { mgr.idxDeleteZettel(ctx, zid) - return err + return nil } var errZNF box.ErrZettelNotFound if !errors.As(err, &errZNF) && !errors.Is(err, box.ErrReadOnly) { return err } Index: box/manager/enrich.go ================================================================== --- box/manager/enrich.go +++ box/manager/enrich.go @@ -23,10 +23,11 @@ "zettelstore.de/z/zettel/meta" ) // Enrich computes additional properties and updates the given metadata. func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) { + // Calculate computed, but stored values. _, hasCreated := m.Get(api.KeyCreated) if !hasCreated { m.Set(api.KeyCreated, computeCreated(m.Zid)) } Index: box/manager/indexer.go ================================================================== --- box/manager/indexer.go +++ box/manager/indexer.go @@ -155,26 +155,20 @@ } func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) { var cData collectData cData.initialize() - if mustIndexZettel(zettel.Meta) { - collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) - } + collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) m := zettel.Meta zi := store.NewZettelIndex(m) mgr.idxCollectFromMeta(ctx, m, zi, &cData) mgr.idxProcessData(ctx, zi, &cData) toCheck := mgr.idxStore.UpdateReferences(ctx, zi) mgr.idxCheckZettel(toCheck) } -func mustIndexZettel(m *meta.Meta) bool { - return m.Zid >= id.Zid(999999900) -} - func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) { for _, pair := range m.ComputedPairs() { descr := meta.GetDescription(pair.Key) if descr.IsProperty() { continue @@ -215,11 +209,11 @@ } } func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) { cData.refs.ForEach(func(ref id.Zid) { - if mgr.hasZettel(ctx, ref) { + if mgr.HasZettel(ctx, ref) { zi.AddBackRef(ref) } else { zi.AddDeadRef(ref) } }) @@ -230,20 +224,25 @@ func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } - if !mgr.hasZettel(ctx, zid) { + if !mgr.HasZettel(ctx, zid) { zi.AddDeadRef(zid) return } if inverseKey == "" { zi.AddBackRef(zid) return } zi.AddInverseRef(inverseKey, zid) } + +func (mgr *Manager) idxRenameZettel(ctx context.Context, curZid, newZid id.Zid) { + toCheck := mgr.idxStore.RenameZettel(ctx, curZid, newZid) + mgr.idxCheckZettel(toCheck) +} func (mgr *Manager) idxDeleteZettel(ctx context.Context, zid id.Zid) { toCheck := mgr.idxStore.DeleteZettel(ctx, zid) mgr.idxCheckZettel(toCheck) } Index: box/manager/manager.go ================================================================== --- box/manager/manager.go +++ box/manager/manager.go @@ -36,11 +36,19 @@ // ConnectData contains all administration related values. type ConnectData struct { Number int // number of the box, starting with 1. Config config.Config Enricher box.Enricher - Notify box.UpdateNotifier + Notify chan<- box.UpdateInfo + Mapper Mapper +} + +// Mapper allows to inspect the mapping between old-style and new-style zettel identifier. +type Mapper interface { + Warnings(context.Context) (*id.Set, error) // Fetch problematic zettel identifier + + OldToNewMapping(ctx context.Context) (map[id.Zid]id.ZidN, error) } // Connect returns a handle to the specified box. func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (box.ManagedBox, error) { if authManager.IsReadonly() { @@ -92,10 +100,11 @@ observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} infos chan box.UpdateInfo propertyKeys strfun.Set // Set of property key names + zidMapper *zidMapper // Indexer data idxLog *logger.Logger idxStore store.Store idxAr *anteroomQueue @@ -112,11 +121,10 @@ mgr.stateMx.Lock() mgr.state = newState mgr.stateMx.Unlock() } -// State returns the box.StartState of the manager. func (mgr *Manager) State() box.StartState { mgr.stateMx.RLock() state := mgr.state mgr.stateMx.RUnlock() return state @@ -141,12 +149,13 @@ idxLog: boxLog.Clone().Str("box", "index").Child(), idxStore: createIdxStore(rtConfig), idxAr: newAnteroomQueue(1000), idxReady: make(chan struct{}, 1), } + mgr.zidMapper = NewZidMapper(mgr) - cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.notifyChanged} + cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos, Mapper: mgr.zidMapper} boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) for _, uri := range boxURIs { p, err := Connect(uri, authManager, &cdata) if err != nil { return nil, err @@ -213,16 +222,15 @@ if ignoreUpdate(cache, now, reason, zid) { mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored") continue } - isStarted := mgr.State() == box.StartStateStarted mgr.idxEnqueue(reason, zid) if ci.Box == nil { ci.Box = mgr } - if isStarted { + if mgr.State() == box.StartStateStarted { mgr.notifyObserver(&ci) } } case <-mgr.done: return @@ -254,12 +262,10 @@ case box.OnReady: return case box.OnReload: mgr.idxAr.Reset() case box.OnZettel: - mgr.idxAr.EnqueueZettel(zid) - case box.OnDelete: mgr.idxAr.EnqueueZettel(zid) default: mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") return } @@ -309,11 +315,10 @@ mgr.done = make(chan struct{}) go mgr.notifier() mgr.waitBoxesAreStarted() mgr.setState(box.StartStateStarted) - mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady}) go mgr.idxIndexer() return nil } @@ -380,11 +385,11 @@ func (mgr *Manager) ReIndex(ctx context.Context, zid id.Zid) error { mgr.mgrLog.Debug().Msg("ReIndex") if err := mgr.checkContinue(ctx); err != nil { return err } - mgr.infos <- box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: zid} + mgr.infos <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid} return nil } // ReadStats populates st with box statistics. func (mgr *Manager) ReadStats(st *box.Stats) { @@ -430,11 +435,5 @@ if mgr.State() != box.StartStateStarted { return box.ErrStopped } return ctx.Err() } - -func (mgr *Manager) notifyChanged(bbox box.BaseBox, zid id.Zid, reason box.UpdateReason) { - if infos := mgr.infos; infos != nil { - mgr.infos <- box.UpdateInfo{Box: bbox, Reason: reason, Zid: zid} - } -} Index: box/manager/mapstore/mapstore.go ================================================================== --- box/manager/mapstore/mapstore.go +++ box/manager/mapstore/mapstore.go @@ -454,10 +454,72 @@ } zi := &zettelData{} ms.idx[zid] = zi return zi } + +func (ms *mapStore) RenameZettel(_ context.Context, curZid, newZid id.Zid) *id.Set { + ms.mx.Lock() + defer ms.mx.Unlock() + + curZi, curFound := ms.idx[curZid] + _, newFound := ms.idx[newZid] + if !curFound || newFound { + return nil + } + newZi := &zettelData{ + meta: copyMeta(curZi.meta, newZid), + dead: ms.copyDeadReferences(curZi.dead), + forward: ms.copyForward(curZi.forward, newZid), + backward: nil, // will be done through tocheck + otherRefs: nil, // TODO: check if this will be done through toCheck + words: copyStrings(ms.words, curZi.words, newZid), + urls: copyStrings(ms.urls, curZi.urls, newZid), + } + + ms.idx[newZid] = newZi + toCheck := ms.doDeleteZettel(curZid) + toCheck = toCheck.IUnion(ms.dead[newZid]) + delete(ms.dead, newZid) + toCheck = toCheck.Add(newZid) // should update otherRefs + return toCheck +} +func copyMeta(m *meta.Meta, newZid id.Zid) *meta.Meta { + result := m.Clone() + result.Zid = newZid + return result +} + +func (ms *mapStore) copyDeadReferences(curDead *id.Set) *id.Set { + // Must only be called if ms.mx is write-locked! + curDead.ForEach(func(ref id.Zid) { + ms.dead[ref] = ms.dead[ref].Add(ref) + }) + return curDead.Clone() +} +func (ms *mapStore) copyForward(curForward *id.Set, newZid id.Zid) *id.Set { + // Must only be called if ms.mx is write-locked! + curForward.ForEach(func(ref id.Zid) { + if fzi, found := ms.idx[ref]; found { + fzi.backward = fzi.backward.Add(newZid) + } + + }) + return curForward.Clone() +} +func copyStrings(msStringMap stringRefs, curStrings []string, newZid id.Zid) []string { + // Must only be called if ms.mx is write-locked! + if l := len(curStrings); l > 0 { + result := make([]string, l) + for i, s := range curStrings { + result[i] = s + msStringMap[s] = msStringMap[s].Add(newZid) + } + return result + } + return nil +} func (ms *mapStore) DeleteZettel(_ context.Context, zid id.Zid) *id.Set { ms.mx.Lock() defer ms.mx.Unlock() return ms.doDeleteZettel(zid) Index: box/manager/store/store.go ================================================================== --- box/manager/store/store.go +++ box/manager/store/store.go @@ -50,10 +50,14 @@ Enrich(ctx context.Context, m *meta.Meta) // UpdateReferences for a specific zettel. // Returns set of zettel identifier that must also be checked for changes. UpdateReferences(context.Context, *ZettelIndex) *id.Set + + // RenameZettel changes all references of current zettel identifier to new + // zettel identifier. + RenameZettel(_ context.Context, curZid, newZid id.Zid) *id.Set // DeleteZettel removes index data for given zettel. // Returns set of zettel identifier that must also be checked for changes. DeleteZettel(context.Context, id.Zid) *id.Set ADDED box/manager/zidmapper.go Index: box/manager/zidmapper.go ================================================================== --- /dev/null +++ box/manager/zidmapper.go @@ -0,0 +1,199 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-present Detlef Stern +// +// This file is part of Zettelstore. +// +// Zettelstore is licensed under the latest version of the EUPL (European Union +// Public License). Please see file LICENSE.txt for your rights and obligations +// under this license. +// +// SPDX-License-Identifier: EUPL-1.2 +// SPDX-FileCopyrightText: 2021-present Detlef Stern +//----------------------------------------------------------------------------- + +package manager + +import ( + "context" + "maps" + "sync" + "time" + + "zettelstore.de/z/zettel/id" +) + +// zidMapper transforms old-style zettel identifier (14 digits) into new one (4 alphanums). +// +// Since there are no new-style identifier defined, there is only support for old-style +// identifier by checking, whether they are suported as new-style or not. +// +// This will change in later versions. +type zidMapper struct { + fetcher zidfetcher + defined map[id.Zid]id.ZidN // predefined mapping, constant after creation + mx sync.RWMutex // protect toNew ... nextZidN + toNew map[id.Zid]id.ZidN // working mapping old->new + toOld map[id.ZidN]id.Zid // working mapping new->old + nextZidM id.ZidN // next zid for manual + hadManual bool + nextZidN id.ZidN // next zid for normal zettel +} + +type zidfetcher interface { + fetchZids(context.Context) (*id.Set, error) +} + +// NewZidMapper creates a new ZipMapper. +func NewZidMapper(fetcher zidfetcher) *zidMapper { + defined := map[id.Zid]id.ZidN{ + id.Invalid: id.InvalidN, + 1: id.MustParseN("0001"), // ZidVersion + 2: id.MustParseN("0002"), // ZidHost + 3: id.MustParseN("0003"), // ZidOperatingSystem + 4: id.MustParseN("0004"), // ZidLicense + 5: id.MustParseN("0005"), // ZidAuthors + 6: id.MustParseN("0006"), // ZidDependencies + 7: id.MustParseN("0007"), // ZidLog + 8: id.MustParseN("0008"), // ZidMemory + 9: id.MustParseN("0009"), // ZidSx + 10: id.MustParseN("000a"), // ZidHTTP + 11: id.MustParseN("000b"), // ZidAPI + 12: id.MustParseN("000c"), // ZidWebUI + 13: id.MustParseN("000d"), // ZidConsole + 20: id.MustParseN("000e"), // ZidBoxManager + 21: id.MustParseN("000f"), // ZidZettel + 22: id.MustParseN("000g"), // ZidIndex + 23: id.MustParseN("000h"), // ZidQuery + 90: id.MustParseN("000i"), // ZidMetadataKey + 92: id.MustParseN("000j"), // ZidParser + 96: id.MustParseN("000k"), // ZidStartupConfiguration + 100: id.MustParseN("000l"), // ZidRuntimeConfiguration + 101: id.MustParseN("000m"), // ZidDirectory + 102: id.MustParseN("000n"), // ZidWarnings + 10100: id.MustParseN("000s"), // Base HTML Template + 10200: id.MustParseN("000t"), // Login Form Template + 10300: id.MustParseN("000u"), // List Zettel Template + 10401: id.MustParseN("000v"), // Detail Template + 10402: id.MustParseN("000w"), // Info Template + 10403: id.MustParseN("000x"), // Form Template + 10404: id.MustParseN("001z"), // Rename Form Template (will be removed in the future) + 10405: id.MustParseN("000y"), // Delete Template + 10700: id.MustParseN("000z"), // Error Template + 19000: id.MustParseN("000q"), // Sxn Start Code + 19990: id.MustParseN("000r"), // Sxn Base Code + 20001: id.MustParseN("0010"), // Base CSS + 25001: id.MustParseN("0011"), // User CSS + 40001: id.MustParseN("000o"), // Generic Emoji + 59900: id.MustParseN("000p"), // Sxn Prelude + 60010: id.MustParseN("0012"), // zettel + 60020: id.MustParseN("0013"), // confguration + 60030: id.MustParseN("0014"), // role + 60040: id.MustParseN("0015"), // tag + 90000: id.MustParseN("0016"), // New Menu + 90001: id.MustParseN("0017"), // New Zettel + 90002: id.MustParseN("0018"), // New User + 90003: id.MustParseN("0019"), // New Tag + 90004: id.MustParseN("001a"), // New Role + // 100000000, // Manual -> 0020-00yz + 9999999997: id.MustParseN("00zx"), // ZidSession + 9999999998: id.MustParseN("00zy"), // ZidAppDirectory + 9999999999: id.MustParseN("00zz"), // ZidMapping + 10000000000: id.MustParseN("0100"), // ZidDefaultHome + } + toNew := maps.Clone(defined) + toOld := make(map[id.ZidN]id.Zid, len(toNew)) + for o, n := range toNew { + if _, found := toOld[n]; found { + panic("duplicate predefined zid") + } + toOld[n] = o + } + + return &zidMapper{ + fetcher: fetcher, + defined: defined, + toNew: toNew, + toOld: toOld, + nextZidM: id.MustParseN("0020"), + hadManual: false, + nextZidN: id.MustParseN("0101"), + } +} + +// isWellDefined returns true, if the given zettel identifier is predefined +// (as stated in the manual), or is part of the manual itself, or is greater than +// 19699999999999. +func (zm *zidMapper) isWellDefined(zid id.Zid) bool { + if _, found := zm.defined[zid]; found || (1000000000 <= zid && zid <= 1099999999) { + return true + } + if _, err := time.Parse("20060102150405", zid.String()); err != nil { + return false + } + return 19700000000000 <= zid +} + +// Warnings returns all zettel identifier with warnings. +func (zm *zidMapper) Warnings(ctx context.Context) (*id.Set, error) { + allZids, err := zm.fetcher.fetchZids(ctx) + if err != nil { + return nil, err + } + warnings := id.NewSet() + allZids.ForEach(func(zid id.Zid) { + if !zm.isWellDefined(zid) { + warnings = warnings.Add(zid) + } + }) + return warnings, nil +} + +func (zm *zidMapper) GetZidN(zidO id.Zid) id.ZidN { + zm.mx.RLock() + if zidN, found := zm.toNew[zidO]; found { + zm.mx.RUnlock() + return zidN + } + zm.mx.RUnlock() + + zm.mx.Lock() + defer zm.mx.Unlock() + // Double check to avoid races + if zidN, found := zm.toNew[zidO]; found { + return zidN + } + + if 1000000000 <= zidO && zidO <= 1099999999 { + if zidO == 1000000000 { + zm.hadManual = true + } + if zm.hadManual { + zidN := zm.nextZidM + zm.nextZidM++ + zm.toNew[zidO] = zidN + zm.toOld[zidN] = zidO + return zidN + } + } + + zidN := zm.nextZidN + zm.nextZidN++ + zm.toNew[zidO] = zidN + zm.toOld[zidN] = zidO + return zidN +} + +// OldToNewMapping returns the mapping of old format identifier to new format identifier. +func (zm *zidMapper) OldToNewMapping(ctx context.Context) (map[id.Zid]id.ZidN, error) { + allZids, err := zm.fetcher.fetchZids(ctx) + if err != nil { + return nil, err + } + + result := make(map[id.Zid]id.ZidN, allZids.Length()) + allZids.ForEach(func(zidO id.Zid) { + zidN := zm.GetZidN(zidO) + result[zidO] = zidN + }) + return result, nil +} Index: box/membox/membox.go ================================================================== --- box/membox/membox.go +++ box/membox/membox.go @@ -52,13 +52,13 @@ mx sync.RWMutex // Protects the following fields zettel map[id.Zid]zettel.Zettel curBytes int } -func (mb *memBox) notifyChanged(zid id.Zid, reason box.UpdateReason) { - if notify := mb.cdata.Notify; notify != nil { - notify(mb, zid, reason) +func (mb *memBox) notifyChanged(zid id.Zid) { + if chci := mb.cdata.Notify; chci != nil { + chci <- box.UpdateInfo{Box: mb, Reason: box.OnZettel, Zid: zid} } } func (mb *memBox) Location() string { return mb.u.String() @@ -114,11 +114,11 @@ zettel.Meta = meta mb.zettel[zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(zid, box.OnZettel) + mb.notifyChanged(zid) mb.log.Trace().Zid(zid).Msg("CreateZettel") return zid, nil } func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { @@ -199,14 +199,42 @@ zettel.Meta = m mb.zettel[m.Zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(m.Zid, box.OnZettel) + mb.notifyChanged(m.Zid) mb.log.Trace().Msg("UpdateZettel") return nil } + +func (*memBox) AllowRenameZettel(context.Context, id.Zid) bool { return true } + +func (mb *memBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error { + mb.mx.Lock() + zettel, ok := mb.zettel[curZid] + if !ok { + mb.mx.Unlock() + return box.ErrZettelNotFound{Zid: curZid} + } + + // Check that there is no zettel with newZid + if _, ok = mb.zettel[newZid]; ok { + mb.mx.Unlock() + return box.ErrInvalidZid{Zid: newZid.String()} + } + + meta := zettel.Meta.Clone() + meta.Zid = newZid + zettel.Meta = meta + mb.zettel[newZid] = zettel + delete(mb.zettel, curZid) + mb.mx.Unlock() + mb.notifyChanged(curZid) + mb.notifyChanged(newZid) + mb.log.Trace().Msg("RenameZettel") + return nil +} func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { mb.mx.RLock() _, ok := mb.zettel[zid] mb.mx.RUnlock() @@ -221,11 +249,11 @@ return box.ErrZettelNotFound{Zid: zid} } delete(mb.zettel, zid) mb.curBytes -= oldZettel.Length() mb.mx.Unlock() - mb.notifyChanged(zid, box.OnDelete) + mb.notifyChanged(zid) mb.log.Trace().Msg("DeleteZettel") return nil } func (mb *memBox) ReadStats(st *box.ManagedBoxStats) { Index: box/notify/directory.go ================================================================== --- box/notify/directory.go +++ box/notify/directory.go @@ -16,10 +16,11 @@ import ( "errors" "fmt" "path/filepath" "regexp" + "strings" "sync" "zettelstore.de/z/box" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" @@ -40,11 +41,10 @@ // dsWorking --directory missing--> dsMissing // dsMissing --last list notification--> dsWorking // --Stop--> dsStopping type DirServiceState uint8 -// Constants for DirServiceState const ( DsCreated DirServiceState = iota DsStarting // Reading inital scan DsWorking // Initial scan complete, fully operational DsMissing // Directory is missing @@ -55,26 +55,26 @@ type DirService struct { box box.ManagedBox log *logger.Logger dirPath string notifier Notifier - infos box.UpdateNotifier + infos chan<- box.UpdateInfo mx sync.RWMutex // protects status, entries state DirServiceState entries entrySet } // ErrNoDirectory signals missing directory data. var ErrNoDirectory = errors.New("unable to retrieve zettel directory information") // NewDirService creates a new directory service. -func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, notify box.UpdateNotifier) *DirService { +func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService { return &DirService{ box: box, log: log, notifier: notifier, - infos: notify, + infos: chci, state: DsCreated, } } // State the current service state. @@ -183,10 +183,40 @@ return ds.logMissingEntry("update") } ds.entries[entry.Zid] = &entry return nil } + +// RenameDirEntry replaces an existing directory entry with a new one. +func (ds *DirService) RenameDirEntry(oldEntry *DirEntry, newZid id.Zid) (DirEntry, error) { + ds.mx.Lock() + defer ds.mx.Unlock() + if ds.entries == nil { + return DirEntry{}, ds.logMissingEntry("rename") + } + if _, found := ds.entries[newZid]; found { + return DirEntry{}, box.ErrInvalidZid{Zid: newZid.String()} + } + oldZid := oldEntry.Zid + newEntry := DirEntry{ + Zid: newZid, + MetaName: renameFilename(oldEntry.MetaName, oldZid, newZid), + ContentName: renameFilename(oldEntry.ContentName, oldZid, newZid), + ContentExt: oldEntry.ContentExt, + // Duplicates must not be set, because duplicates will be deleted + } + delete(ds.entries, oldZid) + ds.entries[newZid] = &newEntry + return newEntry, nil +} + +func renameFilename(name string, curID, newID id.Zid) string { + if cur := curID.String(); strings.HasPrefix(name, cur) { + name = newID.String() + name[len(cur):] + } + return name +} // DeleteDirEntry removes a entry from the directory. func (ds *DirService) DeleteDirEntry(zid id.Zid) error { ds.mx.Lock() defer ds.mx.Unlock() @@ -259,18 +289,18 @@ case Update: ds.mx.Lock() zid := ds.onUpdateFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { - ds.notifyChange(zid, box.OnZettel) + ds.notifyChange(zid) } case Delete: ds.mx.Lock() zid := ds.onDeleteFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { - ds.notifyChange(zid, box.OnDelete) + ds.notifyChange(zid) } default: ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") } return newEntries, true @@ -284,18 +314,18 @@ return zids } func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) { for _, zid := range zids { - ds.notifyChange(zid, box.OnZettel) + ds.notifyChange(zid) delete(prevEntries, zid) } // These were previously stored, by are not found now. // Notify system that these were deleted, e.g. for updating the index. for zid := range prevEntries { - ds.notifyChange(zid, box.OnDelete) + ds.notifyChange(zid) } } func (ds *DirService) onDestroyDirectory() { ds.mx.Lock() @@ -302,11 +332,11 @@ entries := ds.entries ds.entries = nil ds.state = DsMissing ds.mx.Unlock() for zid := range entries { - ds.notifyChange(zid, box.OnDelete) + ds.notifyChange(zid) } } var validFileName = regexp.MustCompile(`^(\d{14})`) @@ -573,11 +603,11 @@ return newLen < oldLen } return newExt < oldExt } -func (ds *DirService) notifyChange(zid id.Zid, reason box.UpdateReason) { - if notify := ds.infos; notify != nil { - ds.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChange") - notify(ds.box, zid, reason) +func (ds *DirService) notifyChange(zid id.Zid) { + if chci := ds.infos; chci != nil { + ds.log.Trace().Zid(zid).Msg("notifyChange") + chci <- box.UpdateInfo{Box: ds.box, Reason: box.OnZettel, Zid: zid} } } Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -73,10 +73,11 @@ 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) ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager) ucReIndex := usecase.NewReIndex(logUc, protectedBoxManager) ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) a := api.New( @@ -93,10 +94,12 @@ webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir)) } // Web user interface if !authManager.IsReadonly() { + webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(ucGetZettel)) + webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename)) webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(&ucQuery, &ucEvaluate, ucListRoles, ucListSyntax)) webSrv.AddListRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel)) webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler( ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax)) webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel)) @@ -122,10 +125,11 @@ 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)) + webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename)) } if authManager.WithAuth() { webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager)) } Index: cmd/main.go ================================================================== --- cmd/main.go +++ cmd/main.go @@ -160,22 +160,21 @@ const ( keyAdminPort = "admin-port" keyAssetDir = "asset-dir" keyBaseURL = "base-url" - keyBoxOneURI = kernel.BoxURIs + "1" keyDebug = "debug-mode" keyDefaultDirBoxType = "default-dir-box-type" keyInsecureCookie = "insecure-cookie" keyInsecureHTML = "insecure-html" keyListenAddr = "listen-addr" keyLogLevel = "log-level" keyMaxRequestSize = "max-request-size" keyOwner = "owner" keyPersistentCookie = "persistent-cookie" + keyBoxOneURI = kernel.BoxURIs + "1" keyReadOnly = "read-only-mode" - keyRuntimeProfiling = "runtime-profiling" keyTokenLifetimeHTML = "token-lifetime-html" keyTokenLifetimeAPI = "token-lifetime-api" keyURLPrefix = "url-prefix" keyVerbose = "verbose-mode" ) @@ -208,15 +207,13 @@ break } err = setConfigValue(err, kernel.BoxService, key, val) } - err = setConfigValue( - err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML)) + err = setConfigValue(err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML)) - err = setConfigValue( - err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) + err = setConfigValue(err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123")) if val, found := cfg.Get(keyBaseURL); found { err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val) } if val, found := cfg.Get(keyURLPrefix); found { err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val) @@ -228,11 +225,10 @@ } err = setConfigValue( err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, "")) err = setConfigValue( err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, "")) - err = setConfigValue(err, kernel.WebService, kernel.WebProfiling, debugMode || cfg.GetBool(keyRuntimeProfiling)) if val, found := cfg.Get(keyAssetDir); found { err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val) } return err == nil } Index: cmd/zettelstore/main.go ================================================================== --- cmd/zettelstore/main.go +++ cmd/zettelstore/main.go @@ -19,11 +19,11 @@ "zettelstore.de/z/cmd" ) // Version variable. Will be filled by build process. -var version string +var version string = "" func main() { exitCode := cmd.Main("Zettelstore", version) os.Exit(exitCode) } Index: collect/order.go ================================================================== --- collect/order.go +++ collect/order.go @@ -14,56 +14,59 @@ // Package collect provides functions to collect items from a syntax tree. package collect import "zettelstore.de/z/ast" -// Order of internal links within the given zettel. -func Order(zn *ast.ZettelNode) (result []*ast.LinkNode) { +// Order of internal reference within the given zettel. +func Order(zn *ast.ZettelNode) (result []*ast.Reference) { for _, bn := range zn.Ast { ln, ok := bn.(*ast.NestedListNode) if !ok { continue } switch ln.Kind { case ast.NestedListOrdered, ast.NestedListUnordered: for _, is := range ln.Items { - if ln := firstItemZettelLink(is); ln != nil { - result = append(result, ln) + if ref := firstItemZettelReference(is); ref != nil { + result = append(result, ref) } } } } return result } -func firstItemZettelLink(is ast.ItemSlice) *ast.LinkNode { +func firstItemZettelReference(is ast.ItemSlice) *ast.Reference { for _, in := range is { if pn, ok := in.(*ast.ParaNode); ok { - if ln := firstInlineZettelLink(pn.Inlines); ln != nil { - return ln + if ref := firstInlineZettelReference(pn.Inlines); ref != nil { + return ref } } } return nil } -func firstInlineZettelLink(is ast.InlineSlice) (result *ast.LinkNode) { +func firstInlineZettelReference(is ast.InlineSlice) (result *ast.Reference) { for _, inl := range is { switch in := inl.(type) { case *ast.LinkNode: - return in + if ref := in.Ref; ref.IsZettel() { + return ref + } + result = firstInlineZettelReference(in.Inlines) case *ast.EmbedRefNode: - result = firstInlineZettelLink(in.Inlines) + result = firstInlineZettelReference(in.Inlines) case *ast.EmbedBLOBNode: - result = firstInlineZettelLink(in.Inlines) + result = firstInlineZettelReference(in.Inlines) case *ast.CiteNode: - result = firstInlineZettelLink(in.Inlines) + result = firstInlineZettelReference(in.Inlines) case *ast.FootnoteNode: // Ignore references in footnotes continue case *ast.FormatNode: - result = firstInlineZettelLink(in.Inlines) + result = firstInlineZettelReference(in.Inlines) default: continue } if result != nil { return result Index: config/config.go ================================================================== --- config/config.go +++ config/config.go @@ -20,16 +20,16 @@ "zettelstore.de/z/zettel/meta" ) // Key values that are supported by Config.Get const ( - KeyFooterZettel = "footer-zettel" - KeyHomeZettel = "home-zettel" - KeyShowBackLinks = "show-back-links" - KeyShowFolgeLinks = "show-folge-links" - KeyShowSequelLinks = "show-sequel-links" - KeyShowSuccessorLinks = "show-successor-links" + 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/development/20210916193200.zettel ================================================================== --- docs/development/20210916193200.zettel +++ docs/development/20210916193200.zettel @@ -1,11 +1,11 @@ id: 20210916193200 title: Required Software role: zettel syntax: zmk created: 20210916193200 -modified: 20241213124936 +modified: 20231213194509 The following software must be installed: * A current, supported [[release of Go|https://go.dev/doc/devel/release]], * [[Fossil|https://fossil-scm.org/]], @@ -15,15 +15,14 @@ ```sh export PATH=$PATH:/usr/local/go/bin export PATH=$PATH:$(go env GOPATH)/bin ``` -The internal build tool needs the following software tools. -They can be installed / updated via the build tool itself: ``go run tools/devtools/devtools.go``. +The internal build tool need the following software. +It can be installed / updated via the build tool itself: ``go run tools/devtools/devtools.go``. Otherwise you can install the software by hand: * [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``, * [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``, * [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``, -* [[revive|https://revive.run]] via ``go install github.com/mgechev/revive@vlatest``, * [[govulncheck|https://golang.org/x/vuln/cmd/govulncheck]] via ``go install golang.org/x/vuln/cmd/govulncheck@latest``, Index: docs/development/20210916194900.zettel ================================================================== --- docs/development/20210916194900.zettel +++ docs/development/20210916194900.zettel @@ -1,54 +1,54 @@ id: 20210916194900 title: Checklist for Release role: zettel syntax: zmk created: 20210916194900 -modified: 20241213125640 +modified: 20231213194631 -# Sync with the official repository: +# Sync with the official repository #* ``fossil sync -u`` -# Make sure that there is no workspace defined: +# Make sure that there is no workspace defined. #* ``ls ..`` must not have a file ''go.work'', in no parent folder. -# Make sure that all dependencies are up-to-date: +# Make sure that all dependencies are up-to-date. #* ``cat go.mod`` # Clean up your Go workspace: -#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``) +#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``). # All internal tests must succeed: -#* ``go run tools/check/check.go -r`` (alternatively: ``make relcheck``) +#* ``go run tools/check/check.go -r`` (alternatively: ``make relcheck``). # The API tests must succeed on every development platform: -#* ``go run tools/testapi/testapi.go`` (alternatively: ``make api``) +#* ``go run tools/testapi/testapi.go`` (alternatively: ``make api``). # Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual: #* ``go run -race cmd/zettelstore/main.go run -d docs/manual`` #* ``linkchecker http://127.0.0.1:23123 2>&1 | tee lc.txt`` #* Check all ""Error: 404 Not Found"" -#* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/z'' for those zettel that are accessible only in ''expert-mode'' +#* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/p'' with encoding ''html'' for those zettel that are accessible only in ''expert-mode''. #* Try to resolve other error messages and warnings #* Warnings about empty content can be ignored # On every development platform, the box with 10.000 zettel must run, with ''-race'' enabled: -#* ``go run -race cmd/zettelstore/main.go run -d DIR`` +#* ``go run -race cmd/zettelstore/main.go run -d DIR``. # Create a development release: -#* ``go run tools/build.go release`` (alternatively: ``make release``) +#* ``go run tools/build.go release`` (alternatively: ``make release``). # On every platform (esp. macOS), the box with 10.000 zettel must run properly: #* ``./zettelstore -d DIR`` -# Update files in directory ''www'': -#* ''index.wiki'' -#* ''download.wiki'' -#* ''changes.wiki'' -#* ''plan.wiki'' +# Update files in directory ''www'' +#* index.wiki +#* download.wiki +#* changes.wiki +#* plan.wiki # Set file ''VERSION'' to the new release version. - It **must** consists of three numbers: ''MAJOR.MINOR.PATCH'', even if ''PATCH'' is zero. + It _must_ consist of three digits: MAJOR.MINOR.PATCH, even if PATCH is zero # Disable Fossil autosync mode: #* ``fossil setting autosync off`` # Commit the new release version: #* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"`` #* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''. - Otherwise client software will not be able to import ''zettelstore.de/z''. + Otherwise client will not be able to import ''zettelkasten.de/z''. # Clean up your Go workspace: -#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``) +#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``). # Create the release: -#* ``go run tools/build/build.go release`` (alternatively: ``make release``) +#* ``go run tools/build/build.go release`` (alternatively: ``make release``). # Remove previous executables: #* ``fossil uv remove --glob '*-PREVVERSION*'`` # Add executables for release: #* ``cd releases`` #* ``fossil uv add *.zip`` Index: docs/development/20231218181900.zettel ================================================================== --- docs/development/20231218181900.zettel +++ docs/development/20231218181900.zettel @@ -69,10 +69,11 @@ * Check all zettel HTML encodings, via the path ''/z/ZID?enc=html&part=zettel'' * Check all zettel web views, via the path ''/h/ZID'' * The info page of all zettel is checked, via path ''/i/ZID'' * A subset of max. 100 zettel will be checked for the validity of their edit page, via ''/e/ZID'' * 10 random zettel are checked for a valid create form, via ''/c/ZID'' +* The zettel rename form will be checked for 100 zettel, via ''/b/ZID'' * A maximum of 200 random zettel are checked for a valid delete dialog, via ''/d/ZID'' Depending on the selected Zettelstore, the command might take a long time. You can shorten the time, if you disable any zettel query in the footer. Index: docs/manual/00001000000000.zettel ================================================================== --- docs/manual/00001000000000.zettel +++ docs/manual/00001000000000.zettel @@ -2,11 +2,11 @@ title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20241128141924 +modified: 20231125185455 show-back-links: false * [[Introduction|00001001000000]] * [[Design goals|00001002000000]] * [[Installation|00001003000000]] @@ -20,8 +20,8 @@ * [[Web user interface|00001014000000]] * [[Tips and Tricks|00001017000000]] * [[Troubleshooting|00001018000000]] * Frequently asked questions -Version: {{00001000000001}} +Version: {{00001000000001}}. Licensed under the EUPL-1.2-or-later. Index: docs/manual/00001003000000.zettel ================================================================== --- docs/manual/00001003000000.zettel +++ docs/manual/00001003000000.zettel @@ -2,18 +2,17 @@ title: Installation of the Zettelstore software role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20241213101917 +modified: 20220119145756 === The curious user You just want to check out the Zettelstore software * Grab the appropriate executable and copy it into any directory -* Start the Zettelstore software, e.g. with a double click[^On Windows and macOS, the operating system tries to protect you from possible malicious software. - If you encounter a problem, please take a look on the [[Troubleshooting|00001018000000]] page.] +* Start the Zettelstore software, e.g. with a double click[^On Windows and macOS, the operating system tries to protect you from possible malicious software. If you encounter problem, please take a look on the [[Troubleshooting|00001018000000]] page.] * A sub-directory ""zettel"" will be created in the directory where you put the executable. It will contain your future zettel. * Open the URI [[http://localhost:23123]] with your web browser. It will present you a mostly empty Zettelstore. There will be a zettel titled ""[[Home|00010000000000]]"" that contains some helpful information. Index: docs/manual/00001003305000.zettel ================================================================== --- docs/manual/00001003305000.zettel +++ docs/manual/00001003305000.zettel @@ -2,11 +2,11 @@ title: Enable Zettelstore to start automatically on Windows role: manual tags: #installation #manual #zettelstore syntax: zmk created: 20211125191727 -modified: 20241213103259 +modified: 20220218125541 Windows is a complicated beast. There are several ways to automatically start Zettelstore. === Startup folder @@ -33,11 +33,11 @@ The Windows Task scheduler allows you to start Zettelstore as an background task. This is both an advantage and a disadvantage. -On the plus side, Zettelstore runs in the background, and it does not disturb you. +On the plus side, Zettelstore runs in the background, and it does not disturbs you. All you have to do is to open your web browser, enter the appropriate URL, and there you go. On the negative side, you will not be notified when you enter the wrong data in the Task scheduler and Zettelstore fails to start. This can be mitigated by first using the command line prompt to start Zettelstore with the appropriate options. Once everything works, you can register Zettelstore to be automatically started by the task scheduler. @@ -70,11 +70,11 @@ {{00001003305112}} The next steps are the trickiest. -If you did not create a startup configuration file, then create an action that starts a program. +If you did not created a startup configuration file, then create an action that starts a program. Enter the file path where you placed the Zettelstore executable. The ""Browse ..."" button helps you with that.[^I store my Zettelstore executable in the sub-directory ''bin'' of my home directory.] It is essential that you also enter a directory, which serves as the environment for your zettelstore. The (sub-) directory ''zettel'', which will contain your zettel, will be placed in this directory. Index: docs/manual/00001004010000.zettel ================================================================== --- docs/manual/00001004010000.zettel +++ docs/manual/00001004010000.zettel @@ -2,11 +2,11 @@ title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20240926144803 +modified: 20240710183532 The configuration file, specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These cannot be stored in a [[configuration zettel|00001004020000]] because they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore needs to know in advance on which network address it must listen or where zettel are stored. An attacker that is able to change the owner can do anything. @@ -48,14 +48,13 @@ This allows to configuring than one box. If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ""dir://.zettel"". In this case, even a key ''box-uri-2'' will be ignored. ; [!debug-mode|''debug-mode''] -: If set to [[true|00001006030500]], allows to debug the Zettelstore software (mostly used by Zettelstore developers). +: If set to [[true|00001006030500]], allows to debug the Zettelstore software (mostly used by the developers). Disables any timeout values of the internal web server and does not send some security-related data. Sets [[''log-level''|#log-level]] to ""debug"". - Enables [[''runtime-profiling''|#runtime-profiling]]. Do not enable it for a production server. Default: ""false"" ; [!default-dir-box-type|''default-dir-box-type''] @@ -119,15 +118,10 @@ ; [!read-only-mode|''read-only-mode''] : If set to a [[true value|00001006030500]] the Zettelstore service puts into a read-only mode. No changes are possible. Default: ""false"". -; [!runtime-profiling|''runtime-profiling''] -: A boolean value that enables a web interface to obtain [[runtime profiling information|00001004010200]]. - - Default: ""false"", but it is set to ""true"" if [[''debug-mode''|#debug-mode]] is enabled. - In this case, it cannot be disabled. ; [!secret|''secret''] : A string value to make the communication with external clients strong enough so that sessions of the [[web user interface|00001014000000]] or [[API access token|00001010040700]] cannot be altered by some external unfriendly party. The string must have a length of at least 16 bytes. This value is only needed to be set if [[authentication is enabled|00001010040100]] by setting the key [[''owner''|#owner]] to some user identification value. DELETED docs/manual/00001004010200.zettel Index: docs/manual/00001004010200.zettel ================================================================== --- docs/manual/00001004010200.zettel +++ /dev/null @@ -1,29 +0,0 @@ -id: 00001004010200 -title: Zettelstore runtime profiling -role: manual -tags: #configuration #manual #zettelstore -syntax: zmk -created: 20240926144556 -modified: 20240926144951 - -For debugging purposes, you can enable runtime profiling by setting the startup configuration [[''runtime-profiling''|00001004010000#runtime-profiling]]. -Typically, a Zettelstore developer will do this. -In certain cases, a Zettelstore developer will ask you to enable runtime profiling, because you encountered a hard error. - -Runtime profiling will generate some data that can be retrieved through the builtin web server. -The following URL paths are valid: - -|=Path|Description -|''/rtp/''|Show an index page, where you can navigate to detailed information -|''/rtp/allocs''|Show a sampling of all past memory allocations -|''/rtp/block''|Show stack traces that led to internal blocking -|''/rtp/cmdline''|Show the running Zettelstore command line, with arguments separated by NUL bytes -|''/rtp/goroutine''|Show stack traces of all current internal activities -|''/rtp/heap''|Show a sampling of memory allocations of live objects -|''/rtp/mutex''|Show stack traces of holders of contended mutexes -|''/rtp/profile''|Execute a CPU profile -|''/rtp/symbol''|Shows function names for given program counter value -|''/rtp/trace''|Show trace of execution of the current program -|''/rtp/threadcreate''|Show stack traces that led to the creation of new OS threads - -See documentation for Go standard package [[''net/http/pprof''|https://pkg.go.dev/net/http/pprof]]. Index: docs/manual/00001004020000.zettel ================================================================== --- docs/manual/00001004020000.zettel +++ docs/manual/00001004020000.zettel @@ -2,11 +2,11 @@ title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20241118175216 +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. @@ -58,13 +58,13 @@ ; [!max-transclusions|''max-transclusions''] : Maximum number of indirect transclusion. This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]]. Default: ""1024"". -; [!show-back-links|''show-back-links''], [!show-folge-links|''show-folge-links''], [!show-sequel-links|''show-sequel-links''], [!show-successor-links|''show-successor-links''] +; [!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]], [[''sequel''|00001006020000#sequel]], and [[''prequel''|00001006020000#prequel]]. + 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). 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: 20241118175124 +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. @@ -16,7 +16,7 @@ |[[''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-sequel-links''|00001004020000#show-sequel-links]]|Y|Y| +|[[''show-subordinate-links''|00001004020000#show-subordinate-links]]|Y|Y| |[[''show-successor-links''|00001004020000#show-successor-links]]|Y|Y| Index: docs/manual/00001005000000.zettel ================================================================== --- docs/manual/00001005000000.zettel +++ docs/manual/00001005000000.zettel @@ -2,11 +2,11 @@ title: Structure of Zettelstore role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20241213101751 +modified: 20240710173506 Zettelstore is a software that manages your zettel. Since every zettel must be readable without any special tool, most zettel has to be stored as ordinary files within specific directories. Typically, file names and file content must comply to specific rules so that Zettelstore can manage them. If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions. @@ -16,11 +16,11 @@ For example, you are able to list zettel, to create new zettel, to edit them, or to delete them. You can view zettel details and relations between zettel. In addition, Zettelstore provides an ""application programming interface"" ([[API|00001012000000]]) that allows other software to communicate with the Zettelstore. Zettelstore becomes extensible by external software. -For example, a more sophisticated user interface could be built, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel. +For example, a more sophisticated user interface could be build, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel. === Where zettel are stored Your zettel are stored typically as files in a specific directory. If you have not explicitly specified the directory, a default directory will be used. @@ -27,15 +27,15 @@ The directory has to be specified at [[startup time|00001004010000]]. Nested directories are not supported (yet). Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]]. If you create a new zettel via the [[web user interface|00001014000000]] or via the [[API|00001012053200]], the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss''). -This allows zettel to be sorted naturally by creation time. +This allows zettel to be sorted naturally by creation time.[^Zettel identifier format will be migrated to a new format after version 0.19, without reference to the creation date. See [[Alphanumeric Zettel Identifier|00001006050200]] for some details.] -Since the only restriction on zettel identifiers are the 14 digits, you are free to use other digit sequences. +Since the only restriction on zettel identifiers are the 14 digits, you are free to use other digit sequences.[^Zettel identifier format will be migrated to a new format after version 0.19, without reference to the creation date.] The [[configuration zettel|00001004020000]] is one prominent example, as well as these manual zettel. -You can create these special zettel by manually renaming the underlying zettel files. +You can create these special zettel identifiers either with the __rename__[^Renaming is deprecated als will be removed in version 0.19 or after.] function of Zettelstore or by manually renaming the underlying zettel files. It is allowed that the file name contains other characters after the 14 digits. These are ignored by Zettelstore. Two filename extensions are used by Zettelstore: @@ -47,11 +47,11 @@ For example, you want to store an important figure in the Zettelstore that is encoded as a ''.png'' file. Since each zettel contains some metadata, e.g. the title of the figure, the question arises where these data should be stores. The solution is a meta-file with the same zettel identifier, but without a filename extension. Zettelstore recognizes this situation and reads in both files for the one zettel containing the figure. -It maintains this relationship as long as these files exists. +It maintains this relationship as long as theses files exists. In case of some textual zettel content you do not want to store the metadata and the zettel content in two different files. Here the ''.zettel'' extension will signal that the metadata and the zettel content will be put in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator""). === Predefined zettel @@ -72,11 +72,11 @@ If you change a zettel, it will be always stored as a file. If a zettel is requested, Zettelstore will first try to read that zettel from a file. If such a file was not found, the internal zettel store is searched secondly. Therefore, the file store ""shadows"" the internal zettel store. -If you want to read the original zettel, you have to delete the zettel (which removes it from the file directory). +If you want to read the original zettel, you either have to delete the zettel (which removes it from the file directory), or you have to rename[^Renaming is deprecated als will be removed in version 0.19 or after.] it to another zettel identifier. Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software. * [[List of predefined zettel|00001005090000]] === Boxes: alternative ways to store zettel Index: docs/manual/00001005090000.zettel ================================================================== --- docs/manual/00001005090000.zettel +++ docs/manual/00001005090000.zettel @@ -2,17 +2,13 @@ title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 -modified: 20241223214236 +modified: 20240709180005 -The following table lists all predefined zettel with their purpose. - -The content of most[^To be more exact: zettel with an identifier greater or equal ''00000999999900'' will have their content indexed.] of these zettel will not be indexed by Zettelstore. -You will not find zettel when searched for some content, e.g. ""[[query:european]]"" will not find the [[Zettelstore License|00000000000004]]. -However, metadata is always indexed, e.g. ""[[query:title:license]]"" will find the Zettelstore License zettel. +The following table lists all predefined zettel with their purpose.[^Zettel identifier format will be migrated to a new format after version 0.19.] |= Identifier :|= Title | Purpose | [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore | [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore | [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore @@ -25,16 +21,18 @@ | [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the the index process | [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more | [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore | [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]] | [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]] +| [[00000000000102]] | Zettelstore Warnings | Warnings about potential problematic zettel identifier | [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view | [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]] | [[00000000010300]] | Zettelstore List Zettel HTML Template | Used when displaying a list of zettel | [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel | [[00000000010402]] | Zettelstore Info HTML Template | Layout for the information view of a specific zettel | [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text +| [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]] | [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel | [[00000000010700]] | Zettelstore Error HTML Template | View to show an error message | [[00000000019000]] | Zettelstore Sxn Start Code | Starting point of sxn functions to build the templates | [[00000000019990]] | Zettelstore Sxn Base Code | Base sxn functions to build the templates | [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]] @@ -42,18 +40,17 @@ | [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid | [[00000000060010]] | zettel | [[Role zettel|00001012051800]] for the role ""[[zettel|00001006020100#zettel]]"" | [[00000000060020]] | 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]]"" -| [[00000000080001]] | Lists Menu | Contains the items of the ""Lists"" menu | [[00000000090000]] | New Menu | Contains items that should be in the zettel template menu | [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100#zettel]]"" | [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]] | [[00000000090003]] | New Tag | Template for a new [[tag zettel|00001006020100#tag]] | [[00000000090004]] | New Role | Template for a new [[role zettel|00001006020100#role]] -| [[00000999999999]] | Zettelstore Application Directory | Maps application name to application specific zettel +| [[00009999999998]] | Zettelstore Application Directory | Maps application name to application specific zettel | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. In most cases, you must at least enable [[''expert-mode''|00001004020000#expert-mode]]. **Important:** All identifier may change until a stable version of the software is released. Index: docs/manual/00001006020000.zettel ================================================================== --- docs/manual/00001006020000.zettel +++ docs/manual/00001006020000.zettel @@ -2,11 +2,11 @@ title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 -modified: 20241118175033 +modified: 20240708154737 Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore. See the [[computed list of supported metadata keys|00000000000090]] for details. Most keys conform to a [[type|00001006030000]]. @@ -87,13 +87,10 @@ Basically the inverse of key [[''folge''|#folge]]. ; [!predecessor|''predecessor''] : References the zettel that contains a previous version of the content. In contrast to [[''precursor''|#precurso]] / [[''folge''|#folge]], this is a reference because of technical reasons, not because of content-related reasons. Basically the inverse of key [[''successors''|#successors]]. -; [!prequel|''prequel''] -: Specifies a zettel that is conceptually a prequel zettel. - This is a zettel that occured somehow before the current zettel. ; [!published|''published''] : This property contains the timestamp of the mast modification / creation of the zettel. If [[''modified''|#modified]] is set with a valid timestamp, it contains the its value. Otherwise, if [[''created''|#created]] is set with a valid timestamp, it contains the its value. Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used. @@ -112,21 +109,24 @@ ; [!role|''role''] : Defines the role of the zettel. Can be used for selecting zettel. See [[supported zettel roles|00001006020100]]. If not given, it is ignored. -; [!sequel|''sequel''] -: Is a property that contains identifier of all zettel that reference this zettel through the [[''prequel''|#prequel]] value. +; [!subordinates|''subordinates''] +: Is a property that contains identifier of all zettel that reference this zettel through the [[''superior''|#superior]] value. ; [!successors|''successors''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''predecessor''|#predecessor]] value. Therefore, it references all zettel that contain a new version of the content and/or metadata. In contrast to [[''folge''|#folge]], these are references because of technical reasons, not because of content-related reasons. In most cases, zettel referencing the current zettel should be updated to reference a successor zettel. The [[query reference|00001007040310]] [[query:backward? successors?]] lists all such zettel. ; [!summary|''summary''] : Summarizes the content of the zettel. You may use all [[inline-structued elements|00001007040000]] of Zettelmarkup. +; [!superior|''superior''] +: Specifies a zettel that is conceptually a superior zettel. + This might be a more abstract zettel, or a zettel that should be higher in a hierarchy. ; [!syntax|''syntax''] : Specifies the syntax that should be used for interpreting the zettel. The zettel about [[other markup languages|00001008000000]] defines supported values. If it is not given, it defaults to ''plain''. ; [!tags|''tags''] @@ -140,11 +140,11 @@ One use case is to specify the document that the current zettel comments on. The URL will be rendered special in the [[web user interface|00001014000000]] if you use the default template. ; [!useless-files|''useless-files''] : Contains the file names that are rejected to serve the content of a zettel. Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]]. - If a zettel is deleted, these files will also be deleted. + If a zettel is renamed[^Renaming a zettel is deprecated. This feature will be removed in version 0.19 or later.] or deleted, these files will be deleted. ; [!user-id|''user-id''] : Provides some unique user identification for an [[user zettel|00001010040200]]. It is used as a user name for authentication. It is only used for zettel with a ''role'' value of ""user"". Index: docs/manual/00001006050000.zettel ================================================================== --- docs/manual/00001006050000.zettel +++ docs/manual/00001006050000.zettel @@ -2,19 +2,21 @@ title: Zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20241128141443 +modified: 20240708154551 Each zettel is given a unique identifier. To some degree, the zettel identifier is part of the metadata. Basically, the identifier is given by the [[Zettelstore|00001005000000]] software. + +=== Timestamp-based identifier Every zettel identifier consists of 14 digits. They resemble a timestamp: the first four digits could represent the year, the -next two represent the month, following by day, hour, minute, and second. +next two represent the month, following by day, hour, minute, and second.[^Zettel identifier format will be migrated to a new format after version 0.19, without reference to the creation date.] This allows to order zettel chronologically in a canonical way. In most cases the zettel identifier is the timestamp when the zettel was created. @@ -26,5 +28,16 @@ All identifiers of zettel initially provided by an empty Zettelstore begin with ""000000"", except the home zettel ''00010000000000''. Zettel identifier of this manual have be chosen to begin with ""000010"". A zettel can have any identifier that contains 14 digits and that is not in use by another zettel managed by the same Zettelstore. + +=== Identifiers with four alphanumeric characters +In the future, above identifier format will change. +The migration to the new format starts with Zettelstore version 0.18 and will last approximately until version 0.22. + +Above described format of 14 digits will be changed to four alphanumeric characters, i.e. the digits ''0'' to ''9'', and the letters ''a'' to ''z''. +You might note that using 14 digits you are allowed a little less than 10^^14^^ Zettel, i.e. more than 999 trillion zettel, while the new scheme only allows you to create 36^^4^^-1 zettel (1679615 zettel, to be exact). +Since Zettelstore is a single-user system, more than a million zettel should be enough. +However, there must be a way to replace an identifier with 14 digits by an identifier with four characters. + +As a first step, the list of [[reserved zettel identifier|00001006055000]] is updated, as well as ways of client software to use predefined identifier. ADDED docs/manual/00001006050200.zettel Index: docs/manual/00001006050200.zettel ================================================================== --- /dev/null +++ docs/manual/00001006050200.zettel @@ -0,0 +1,49 @@ +id: 00001006050200 +title: Alphanumeric Zettel Identifier +role: manual +tags: #design #manual #zettelstore +syntax: zmk +created: 20240705200557 +modified: 20240710173133 +precursor: 00001006050000 + +Timestamp-based zettel identifier (14 digits) will be migrated to a new format. +Instead of using the current date and time of zettel creation, the new format is based in incrementing zettel identifier. +When creating a new zettel, its identifier is calculated by adding one to the current maximum zettel identifier. +The external representation if the new format identifier is a sequence of four alphanumeric characters, i.e. the 36 +characters ''0'' … ''9'', and ''a'' … ''z''. +The external representation is basically a ""base-36"" encoding of the number. + +The characters ''A'' … ''Z'' are mapped to the lower-case ''a'' … ''z''. + +=== Migration process +Please note: the following is just a plan. +Plans tend to be revised if they get in contact with reality. + +; Version 0.18 +: Provides some tools to check your own zettelstore for problematic zettel identifier. + For example, zettel without metadata key ''created'' should be updated by the user, especially if the zettel identifier is below ''19700101000000''. + Most likely, this is the case for zettel created before version 0.7 (2022-08-17). + + Zettel [[Zettelstore Warnings|00000000000102]] (''00000000000102'') lists these problematic zettel identifier.[^Only visible in [[expert mode|00001004020000#expert-mode]].] + You should update your zettel to remove these warnings to ensure a smooth migration. + + If you have developed an application, that defines a specific zettel identifier to be used as application configuration, you should must the new zettel [[Zettelstore Application Directory|00009999999998]] (''00009999999998''). + + There is an explicit, but preliminary mapping of the old format to the new one, and vice versa. + This mapping will be calculated with the order of the identifier in the old format. + The zettel [[Zettelstore Identifier Mapping|00009999999999]] (''00009999999999'') will show this mapping.[^Only visible in [[expert mode|00001004020000#expert-mode]].] + +; Version 0.19 +: The new identifier format will be used initially internal. + The old format with 14 digits is still used to create URIs and to link zettel. + + You will have some time to update your zettel data if you detect some issues. + + Operation to rename a zettel, i.e. assigning a new identifier to a zettel, is remove permanently. +; Version 0.20 +: The internal search index is based on the new format identifier. +; Version 0.21 +: The new format is used to calculate URIs and to form links. +; Version 0.22 +: Old format identifier are full legacy. Index: docs/manual/00001006055000.zettel ================================================================== --- docs/manual/00001006055000.zettel +++ docs/manual/00001006055000.zettel @@ -2,32 +2,35 @@ title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210721105704 -modified: 20241202100917 +modified: 20240708154858 [[Zettel identifier|00001006050000]] are typically created by examine the current date and time. -By renaming the name of the underlying zettel file, you are able to provide any sequence of 14 digits. +By renaming[^The rename operation id deprecated and will be removed in version 0.19 or later.] a zettel, you are able to provide any sequence of 14 digits[^Zettel identifier format will be migrated to a new format after version 0.19.]. +If no other zettel has the same identifier, you are allowed to rename a zettel. To make things easier, you must not use zettel identifier that begin with four zeroes (''0000''). All zettel provided by an empty zettelstore begin with six zeroes[^Exception: the predefined home zettel ''00010000000000''. But you can [[configure|00001004020000#home-zettel]] another zettel with another identifier as the new home zettel.]. Zettel identifier of this manual have be chosen to begin with ''000010''. However, some external applications may need at least one defined zettel identifier to work properly. -Zettel [[Zettelstore Application Directory|00000999999999]] (''00000999999999'') can be used to associate a name to a zettel identifier. +Zettel [[Zettelstore Application Directory|00009999999998]] (''00009999999998'') can be used to associate a name to a zettel identifier. For example, if your application is named ""app"", you create a metadata key ''app-zid''. Its value is the zettel identifier of the zettel that configures your application. === Reserved Zettel Identifier |= From | To | Description -| 00000000000000 | 00000000000000 | This is an invalid zettel identifier -| 00000000000001 | 00000999999999 | [[Predefined zettel|00001005090000]] -| 00001000000000 | 00001099999999 | This [[Zettelstore manual|00001000000000]] -| 00001100000000 | 00008999999999 | Reserved, do not use -| 00009000000000 | 00009999999999 | Reserved for applications +| 00000000000000 | 0000000000000 | This is an invalid zettel identifier +| 00000000000001 | 0000099999999 | [[Predefined zettel|00001005090000]] +| 00001000000000 | 0000109999999 | This [[Zettelstore manual|00001000000000]] +| 00001100000000 | 0000899999999 | Reserved, do not use. +| 00009000000000 | 0000999999999 | Reserved for applications (legacy) + +Since the format of zettel identifier will change in the near future, no external application is allowed to use the range ''00000000000001'' … ''0000999999999''. -==== External Applications +==== External Applications (Legacy) |= From | To | Description | 00009000001000 | 00009000001999 | [[Zettel Presenter|https://zettelstore.de/contrib]], an application to display zettel as a HTML-based slideshow Index: docs/manual/00001007000000.zettel ================================================================== --- docs/manual/00001007000000.zettel +++ docs/manual/00001007000000.zettel @@ -2,20 +2,20 @@ title: Zettelmarkup role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 -modified: 20241212152823 +modified: 20221209192105 Zettelmarkup is a rich plain-text based markup language for writing zettel content. Besides the zettel content, Zettelmarkup is also used for specifying the title of a zettel, regardless of the syntax of a zettel. Zettelmarkup supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer. Zettelmarkup can be much easier parsed / consumed by a software compared to other markup languages. Writing a parser for [[Markdown|https://daringfireball.net/projects/markdown/syntax]] is quite challenging. [[CommonMark|00001008010500]] is an attempt to make it simpler by providing a comprehensive specification, combined with an extra chapter to give hints for the implementation. -Zettelmarkup follows some simple principles that anybody who knows how ho write software should be able understand to create an implementation. +Zettelmarkup follows some simple principles that anybody who knows to ho write software should be able understand to create an implementation. Zettelmarkup is a markup language on its own. This is in contrast to Markdown, which is basically a super-set of HTML: every HTML document is a valid Markdown document.[^To be precise: the content of the ```` of each HTML document is a valid Markdown document.] While HTML is a markup language that will probably last for a long time, it cannot be easily translated to other formats, such as PDF, JSON, or LaTeX. Additionally, it is allowed to embed other languages into HTML, such as CSS or even JavaScript. Index: docs/manual/00001007010000.zettel ================================================================== --- docs/manual/00001007010000.zettel +++ docs/manual/00001007010000.zettel @@ -2,11 +2,11 @@ title: Zettelmarkup: General Principles role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 -modified: 20241213101524 +modified: 20211124175047 Any document can be thought as a sequence of paragraphs and other [[block-structured elements|00001007030000]] (""blocks""), such as [[headings|00001007030300]], [[lists|00001007030200]], quotations, and code blocks. Some of these blocks can contain other blocks, for example lists may contain other lists or paragraphs. Other blocks contain [[inline-structured elements|00001007040000]] (""inlines""), such as text, [[links|00001007040310]], emphasized text, and images. @@ -46,11 +46,11 @@ Attributes resemble roughly HTML attributes and are put near the corresponding elements by using the syntax ``{...}``{=zmk}. One example is to make space characters visible inside a inline literal element: ``1 + 2 = 3``{-} was specified by using the default attribute: ``\`\`1 + 2 = 3\`\`{-}``. To summarize: -* With some exceptions, block-structural elements begins at the for position of a line with three identical characters. +* With some exceptions, blocks-structural elements begins at the for position of a line with three identical characters. * The most important exception to this rule is the specification of lists. * If no block element is found, a paragraph with inline elements is assumed. * With some exceptions, inline-structural elements begins with two characters, quite often the same two characters. * The most important exceptions are links. * The backslash character can help to resolve possible ambiguities. Index: docs/manual/00001007030000.zettel ================================================================== --- docs/manual/00001007030000.zettel +++ docs/manual/00001007030000.zettel @@ -2,11 +2,11 @@ title: Zettelmarkup: Block-Structured Elements role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 -modified: 20241212153023 +modified: 20220311181036 Every markup for blocks-structured elements (""blocks"") begins at the very first position of a line. There are five kinds of blocks: lists, one-line blocks, line-range blocks, tables, and paragraphs. @@ -64,12 +64,12 @@ Some text follows. ::: This is because headings need at least three equal sign character. A paragraph is essentially a sequence of [[inline-structured elements|00001007040000]]. -Inline-structured elements can span more than one line. +Inline-structured elements cam span more than one line. Paragraphs are separated by empty lines. If you want to specify a second paragraph inside a list item, or if you want to continue a paragraph on a second and more line within a list item, you must begin the paragraph with a certain number of space characters. The number of space characters depends on the kind of a list and the relevant nesting level. A line that begins with a space character and which is outside of a list or does not contain the right number of space characters is considered to be part of a paragraph. Index: docs/manual/00001007030200.zettel ================================================================== --- docs/manual/00001007030200.zettel +++ docs/manual/00001007030200.zettel @@ -2,25 +2,25 @@ title: Zettelmarkup: Nested Lists role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 -modified: 20241213121000 +modified: 20220218133902 -There are three kinds of lists that can be nested: ordered lists, unordered lists, and quotation lists. +There are thee kinds of lists that can be nested: ordered lists, unordered lists, and quotation lists. -Ordered lists are specified with the number sign (""''#''"", U+0023), unordered lists use the asterisk (""''*''"", U+002A), and quotation lists are specified with the greater-than sign (""''>''"", U+003E). +Ordered lists are specified with the number sign (""''#''"", U+0023), unordered lists use the asterisk (""''*''"", U+002A), and quotation lists are specified with the greater-than sing (""''>''"", U+003E). Let's call these three characters __list characters__. Any nested list item is specified by a non-empty sequence of list characters, followed by a space character and a sequence of [[inline elements|00001007040000]]. In case of a quotation list as the last list character, the space character followed by a sequence of inline elements is optional. The number / count of list characters gives the nesting of the lists. If the following lines should also be part of the list item, exactly the same number of spaces must be given at the beginning of each of the following lines as it is the lists are nested, plus one additional space character. In other words: the inline elements must begin at the same column as it was on the previous line. -The resulting sequence of inline elements is merged into a paragraph. -Appropriately indented paragraphs can be specified after the first one. +The resulting sequence on inline elements is merged into a paragraph. +Appropriately indented paragraphs can specified after the first one. Since each blocks-structured element has to be specified at the first position of a line, none of the nested list items may contain anything else than paragraphs. Some examples: ```zmk # One Index: docs/manual/00001007031000.zettel ================================================================== --- docs/manual/00001007031000.zettel +++ docs/manual/00001007031000.zettel @@ -2,16 +2,16 @@ title: Zettelmarkup: Tables role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 -modified: 20241212153641 +modified: 20220218131107 Tables are used to show some data in a two-dimensional fashion. -In zettelmarkup, tables are not specified explicitly, but by entering __table rows__. +In zettelmarkup, table are not specified explicitly, but by entering __table rows__. Therefore, a table can be seen as a sequence of table rows. -A table row is nothing but a sequence of __table cells__. +A table row is nothing as a sequence of __table cells__. The length of a table is the number of table rows, the width of a table is the maximum length of its rows. The first cell of a row must begin with the vertical bar character (""''|''"", U+007C) at the first position of a line. The other cells of a row begin with the same vertical bar character at later positions in that line. A cell is delimited by the vertical bar character of the next cell or by the end of the current line. Index: docs/manual/00001007031140.zettel ================================================================== --- docs/manual/00001007031140.zettel +++ docs/manual/00001007031140.zettel @@ -2,11 +2,11 @@ title: Zettelmarkup: Query Transclusion role: manual tags: #manual #search #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 -modified: 20241213153229 +modified: 20240219161800 A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''. The line must literally start with the sequence ''{{{query:''. Everything after this prefix is interpreted as a [[query expression|00001007700000]]. @@ -36,10 +36,19 @@ : Emit only those values with at least __n__ aggregated values. __n__ must be a positive integer, ''MIN'' must be given in upper-case letters. ; ''MAXn'' (parameter) : Emit only those values with at most __n__ aggregated values. __n__ must be a positive integer, ''MAX'' must be given in upper-case letters. +; ''TITLE'' (parameter) +: All words following ''TITLE'' are joined together to form a title. + It is used for the ''ATOM'' and ''RSS'' action. +; ''ATOM'' (aggregate) +: Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed. + The document is embedded into the referencing zettel. +; ''RSS'' (aggregate) +: Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed. + The document is embedded into the referencing zettel. ; ''KEYS'' (aggregate) : Emit a list of all metadata keys, together with the number of zettel having the key. ; ''REDIRECT'', ''REINDEX'' (aggregate) : Will be ignored. These actions may have been copied from an existing [[API query call|00001012051400]] (or from a WebUI query), but are here superfluous (and possibly harmful). Index: docs/manual/00001007040322.zettel ================================================================== --- docs/manual/00001007040322.zettel +++ docs/manual/00001007040322.zettel @@ -2,11 +2,11 @@ title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 -modified: 20241202101206 +modified: 20221112111054 Image content is assumed, if an URL is used or if the referenced zettel contains an image. Supported formats are: @@ -22,8 +22,7 @@ They must follow the last right curly bracket character immediately. One prominent example is to specify an explicit title attribute that is shown on certain web browsers when the zettel is rendered in HTML: Examples: * [!spin|``{{Spinning Emoji|00000000040001}}{title=Emoji width=30}``] is rendered as ::{{Spinning Emoji|00000000040001}}{title=Emoji width=30}::{=example}. -* The above image is also the placeholder for a non-existing invalid zettel or for using an invalid zettel identifier: -** ``{{99999999999999}}`` will be rendered as ::{{99999999999999}}::{=example}. -** ``{{00000000000000}}`` will be rendered as ::{{00000000000000}}::{=example}. +* The above image is also the placeholder for a non-existent zettel: +** ``{{00000000009999}}`` will be rendered as ::{{00000000009999}}::{=example}. Index: docs/manual/00001007720300.zettel ================================================================== --- docs/manual/00001007720300.zettel +++ docs/manual/00001007720300.zettel @@ -2,11 +2,11 @@ title: Query: Context Directive role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707204706 -modified: 20241118174741 +modified: 20240209191045 A context directive calculates the __context__ of a list of zettel identifier. It starts with the keyword ''CONTEXT''. Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters. @@ -19,12 +19,12 @@ If no ''BACKWARD'' and ''FORWARD'' is specified, a search for context zettel will be done though backward and forward links. The cost of a context zettel is calculated iteratively: * Each of the specified zettel hast a cost of one. -* A zettel found as a single folge zettel or single precursor zettel has the cost of the originating zettel, plus 0.1. -* A zettel found as a single sequel zettel or single prequel zettel has the cost of the originating zettel, plus 1.0. +* A zettel found as a single folge zettel or single precursor zettel has the cost of the originating zettel, plus one. +* A zettel found as a single subordinate zettel or single superior zettel has the cost of the originating zettel, plus 1.2. * A zettel found as a single successor zettel or single predecessor zettel has the cost of the originating zettel, plus seven. * A zettel found via another link without being part of a [[set of zettel identifier|00001006032500]], has the cost of the originating zettel, plus two. * A zettel which is part of a set of zettel identifier, has the cost of the originating zettel, plus one of the four choices above and multiplied with roughly a linear-logarithmic value based on the size of the set. * A zettel with the same tag, has the cost of the originating zettel, plus a linear-logarithmic number based on the number of zettel with this tag. If a zettel belongs to more than one tag compared with the current zettel, there is a discount of 90% per additional tag. Index: docs/manual/00001007780000.zettel ================================================================== --- docs/manual/00001007780000.zettel +++ docs/manual/00001007780000.zettel @@ -2,11 +2,11 @@ title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 -modified: 20241213153200 +modified: 20240219155949 ``` QueryExpression := ZettelList? QueryDirective* SearchExpression ActionExpression? ZettelList := (ZID (SPACE+ ZID)*). ZID := '0'+ ('1' .. '9'') DIGIT* @@ -42,13 +42,16 @@ | '!' '?'. PosInt := '0' | ('1' .. '9') DIGIT*. ActionExpression := '|' (Word (SPACE+ Word)*)? Action := Word + | 'ATOM' | 'KEYS' | 'N' NO-SPACE* | 'MAX' PosInt | 'MIN' PosInt | 'REDIRECT' - | 'REINDEX'. + | 'REINDEX' + | 'RSS' + | 'TITLE' (SPACE Word)* . Word := NO-SPACE NO-SPACE* ``` Index: docs/manual/00001007800000.zettel ================================================================== --- docs/manual/00001007800000.zettel +++ docs/manual/00001007800000.zettel @@ -2,11 +2,11 @@ title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 -modified: 20241125182149 +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) @@ -17,11 +17,11 @@ | ''&'' | (free) | [[Entity|00001007040000]] | ''\''' | (free) | [[Computer input|00001007040200]] | ''('' | (free) | (free) | '')'' | (free) | (free) | ''*'' | [[Unordered list|00001007030200]] | [[strongly emphasized text|00001007040100]] -| ''+'' | (reserved) | (reserved) +| ''+'' | (free) | (free) | '','' | (free) | [[Sub-scripted text|00001007040100]] | ''-'' | [[Horizontal rule|00001007030400]] | ""[[en-dash|00001007040000]]"" | ''.'' | (free) | (free) | ''/'' | (free) | (free) | '':'' | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]] Index: docs/manual/00001010000000.zettel ================================================================== --- docs/manual/00001010000000.zettel +++ docs/manual/00001010000000.zettel @@ -2,14 +2,14 @@ title: Security role: manual tags: #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 -modified: 20241213102811 +modified: 20221018123622 -Your zettel may contain sensitive content. -You probably want to ensure that only authorized persons can read and/or modify them. +Your zettel could contain sensitive content. +You probably want to ensure that only authorized person can read and/or modify them. Zettelstore ensures this in various ways. === Local first The Zettelstore is designed to run on your local computer. If you do not configure it in other ways, no person from another computer can connect to your Zettelstore. @@ -18,17 +18,17 @@ In the case that you own multiple computers, you do not have to access the Zettelstore remotely. You could install Zettelstore on each computer and set-up some software to synchronize your zettel. Since zettel are stored as ordinary files, this task could be done in various ways. === Read-only -You can start the Zettelstore in a read-only mode. +You can start the Zettelstore in an read-only mode. Nobody, not even you as the owner of the Zettelstore, can change something via its interfaces[^However, as an owner, you have access to the files that store the zettel. If you modify the files, these changes will be reflected via its interfaces.]. You enable read-only mode through the key ''readonly'' in the [[startup configuration zettel|00001004010000#readonly]] or with the ''-r'' option of the ``zettelstore run`` sub-command. === Authentication -The Zettelstore can be configured that users must authenticate themselves to gain access to the content. +The Zettelstore can be configured that a user must authenticate itself to gain access to the content. * [[How to enable authentication|00001010040100]] * [[How to add a new user|00001010040200]] * [[How users are authenticated|00001010040400]] (some technical background) * [[Authenticated sessions|00001010040700]] Index: docs/manual/00001010040700.zettel ================================================================== --- docs/manual/00001010040700.zettel +++ docs/manual/00001010040700.zettel @@ -2,17 +2,17 @@ title: Access token role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 -modified: 20241213101607 +modified: 20211202120950 If an user is authenticated, an ""access token"" is created that must be sent with every request to prove the identity of the caller. Otherwise the user will not be recognized by Zettelstore. If the user was authenticated via the [[web user interface|00001014000000]], the access token is stored in a [[""session cookie""|https://en.wikipedia.org/wiki/HTTP_cookie#Session_cookie]]. -When the web browser is closed, these cookies are not saved. +When the web browser is closed, theses cookies are not saved. If you want web browser to store the cookie as long as lifetime of that token, the owner must set ''persistent-cookie'' of the [[startup configuration|00001004010000]] to ''true''. If the web browser remains inactive for a period, the user will be automatically logged off, because each access token has a limited lifetime. The maximum length of this period is specified by the ''token-lifetime-html'' value of the startup configuration. Every time a web page is displayed, a fresh token is created and stored inside the cookie. @@ -20,8 +20,8 @@ If the user was authenticated via the API, the access token will be returned as the content of the response. Typically, the lifetime of this token is more short term, e.g. 10 minutes. It is specified by the ''token-lifetime-api'' value of the startup configuration. If you need more time, you can either [[re-authenticate|00001012050200]] the user or use an API call to [[renew the access token|00001012050400]]. -If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), you must set the ''insecure-cookie'' value of the startup configuration to ''true''. +If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), your must set the ''insecure-cookie'' value of the startup configuration to ''true''. In most cases, such a scenario is not recommended, because user name and password will be transferred as plain text. You could make use of such scenario if you know all parties that access the local network where you access the Zettelstore. Index: docs/manual/00001010070600.zettel ================================================================== --- docs/manual/00001010070600.zettel +++ docs/manual/00001010070600.zettel @@ -2,11 +2,11 @@ title: Access rules role: manual tags: #authorization #configuration #manual #security #zettelstore syntax: zmk created: 20210126175322 -modified: 20240711183714 +modified: 20240708154954 Whether an operation of the Zettelstore is allowed or rejected, depends on various factors. The following rules are checked first, in this order: @@ -41,9 +41,12 @@ *** Since the user just updates some uncritical values, grant the access In other words: a user is allowed to change its user zettel, even if s/he has no writer privilege and if only uncritical data is changed. ** If the ''user-role'' of the user is ""reader"", reject the access. ** If the user is not allowed to create a new zettel, reject the access. ** Otherwise grant the access. +* Rename a zettel[^Renaming is deprecated. This operation will be removed in version 0.19 or later.] +** Reject the access. + Only the owner of the Zettelstore is currently allowed to give a new identifier for a zettel. * Delete a zettel ** Reject the access. Only the owner of the Zettelstore is allowed to delete a zettel. This may change in the future. 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: 20240711183736 +modified: 20240708154140 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. @@ -32,12 +32,13 @@ * [[Retrieve metadata and content of an existing zettel|00001012053300]] * [[Retrieve metadata of an existing zettel|00001012053400]] * [[Retrieve evaluated metadata and content of an existing zettel in various encodings|00001012053500]] * [[Retrieve parsed metadata and content of an existing zettel in various encodings|00001012053600]] * [[Update metadata and content of a zettel|00001012054200]] +* [[Rename a zettel|00001012054400]] (deprecated) * [[Delete a zettel|00001012054600]] === Various helper methods * [[Retrieve administrative data|00001012070500]] * [[Execute some commands|00001012080100]] ** [[Check for authentication|00001012080200]] ** [[Refresh internal data|00001012080500]] Index: docs/manual/00001012051200.zettel ================================================================== --- docs/manual/00001012051200.zettel +++ docs/manual/00001012051200.zettel @@ -2,13 +2,13 @@ title: API: List all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20241216104355 +modified: 20230807170810 -To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header]. +To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. Always use the endpoint ''/z'' to work with a list of zettel. Without further specifications, a plain text document is returned, with one line per zettel. Each line contains in the first 14 characters the [[zettel identifier|00001006050000]]. Separated by a space character, the title of the zettel follows: Index: docs/manual/00001012051400.zettel ================================================================== --- docs/manual/00001012051400.zettel +++ docs/manual/00001012051400.zettel @@ -2,14 +2,14 @@ title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 -modified: 20241216104329 +modified: 20240711161320 precursor: 00001012051200 -The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions. +The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions. A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below). An empty search expression will select all zettel. An empty list of action, or no valid action, returns the list of all selected zettel metadata. Index: docs/manual/00001012053300.zettel ================================================================== --- docs/manual/00001012053300.zettel +++ docs/manual/00001012053300.zettel @@ -2,21 +2,21 @@ title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211004093206 -modified: 20241216104429 +modified: 20230807170259 The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. -For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header]. +For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. ````sh # curl 'http://127.0.0.1:23123/z/00001012053300' The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. -For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header]. +For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. ```sh ... ```` Index: docs/manual/00001012053400.zettel ================================================================== --- docs/manual/00001012053400.zettel +++ docs/manual/00001012053400.zettel @@ -2,13 +2,13 @@ title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 -modified: 20241216104120 +modified: 20230807170155 -The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header]. +The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. To retrieve the plain metadata of a zettel, use the query parameter ''part=meta'' ````sh # curl 'http://127.0.0.1:23123/z/00001012053400?part=meta' Index: docs/manual/00001012053500.zettel ================================================================== --- docs/manual/00001012053500.zettel +++ docs/manual/00001012053500.zettel @@ -2,15 +2,15 @@ title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 -modified: 20241216104145 +modified: 20240620171057 The [[endpoint|00001012920000]] to work with evaluated metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. -For example, to retrieve some evaluated data about this zettel you are currently viewing in [[Sz encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sz''. +For example, to retrieve some evaluated data about this zettel you are currently viewing in [[Sz encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sz''. If successful, the output is a symbolic expression value: ```sh # curl 'http://127.0.0.1:23123/z/00001012053500?enc=sz' (BLOCK (PARA (TEXT "The ") (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (TEXT " to work with parsed metadata and content of a specific zettel is ") (LITERAL-INPUT () "/z/{ID}") (TEXT ", where ") (LITERAL-INPUT () "{ID}") (TEXT " is a placeholder for the ") ... ``` Index: docs/manual/00001012053600.zettel ================================================================== --- docs/manual/00001012053600.zettel +++ docs/manual/00001012053600.zettel @@ -2,18 +2,18 @@ title: API: Retrieve parsed metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20241216104306 +modified: 20240620170909 The [[endpoint|00001012920000]] to work with parsed metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. A __parsed__ zettel is basically an [[unevaluated|00001012053500]] zettel: the zettel is read and analyzed, but its content is not __evaluated__. By using this endpoint, you are able to retrieve the structure of a zettel before it is evaluated. -For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter). +For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter). For example: ```sh # curl 'http://127.0.0.1:23123/z/00001012053600?enc=sz&parseonly' (BLOCK (PARA (TEXT "The ") (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (TEXT " to work with parsed metadata and content of a specific zettel is ") (LITERAL-INPUT () "/z/{ID}") (TEXT ", where ") ... ``` ADDED docs/manual/00001012054400.zettel Index: docs/manual/00001012054400.zettel ================================================================== --- /dev/null +++ docs/manual/00001012054400.zettel @@ -0,0 +1,57 @@ +id: 00001012054400 +title: API: Rename a zettel +role: manual +tags: #api #manual #zettelstore #deprecated +syntax: zmk +created: 20210713150005 +modified: 20240708154151 + +**Note:** this operation is deprecated and will be removed in version 0.19 (or later). +Do not use it anymore. + +If your client application depends on this operation, please get in contact with the [[author/maintainer|00000000000005]] of Zettelstore to find a solution. + +--- +**Deprecated** + +Renaming a zettel is effectively just specifying a new identifier for the zettel. +Since more than one [[box|00001004011200]] might contain a zettel with the old identifier, the rename operation must success in every relevant box to be overall successful. +If the rename operation fails in one box, Zettelstore tries to rollback previous successful operations. + +As a consequence, you cannot rename a zettel when its identifier is used in a read-only box. +This applies to all [[predefined zettel|00001005090000]], for example. + +The [[endpoint|00001012920000]] to rename a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. +You must send a HTTP MOVE request to this endpoint, and you must specify the new zettel identifier as an URL, placed under the HTTP request header key ''Destination''. +``` +# curl -X MOVE -H "Destination: 10000000000001" http://127.0.0.1:23123/z/00001000000000 +``` + +Only the last 14 characters of the value of ''Destination'' are taken into account and those must form an unused [[zettel identifier|00001006050000]]. +If the value contains less than 14 characters that do not form an unused zettel identifier, the response will contain a HTTP status code ''400''. +All other characters, besides those 14 digits, are effectively ignored. +However, the value should form a valid URL that could be used later to [[read the content|00001012053300]] of the freshly renamed zettel. + +=== HTTP Status codes +; ''204'' +: Rename was successful, there is no body in the response. +; ''400'' +: Request was not valid. + For example, the HTTP header did not contain a valid ''Destination'' key, or the new identifier is already in use. +; ''403'' +: You are not allowed to delete the given zettel. + In most cases you have either not enough [[access rights|00001010070600]] or at least one box containing the given identifier operates in read-only mode. +; ''404'' +: Zettel not found. + You probably used a zettel identifier that is not used in the Zettelstore. + +=== Rationale for the MOVE method +HTTP [[standardizes|https://www.rfc-editor.org/rfc/rfc7231.txt]] eight methods. +None of them is conceptually close to a rename operation. + +Everyone is free to ""invent"" some new method to be used in HTTP. +To avoid a divergency, there is a [[methods registry|https://www.iana.org/assignments/http-methods/]] that tracks those extensions. +The [[HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)|https://www.rfc-editor.org/rfc/rfc4918.txt]] defines the method MOVE that is quite close to the desired rename operation. +In fact, some command line tools use a ""move"" method for renaming files. + +Therefore, Zettelstore adopts somehow WebDAV's MOVE method and its use of the ''Destination'' HTTP header key. Index: docs/manual/00001012080200.zettel ================================================================== --- docs/manual/00001012080200.zettel +++ docs/manual/00001012080200.zettel @@ -2,13 +2,13 @@ title: API: Check for authentication role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220103224858 -modified: 20241216104549 +modified: 20220908163156 -API clients typically want to know, whether [[authentication is enabled|00001010040100]] or not. +API clients typically wants to know, whether [[authentication is enabled|00001010040100]] or not. If authentication is enabled, they present some form of user interface to get user name and password for the actual authentication. Then they try to [[obtain an access token|00001012050200]]. If authentication is disabled, these steps are not needed. To check for enabled authentication, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=authenticated''. Index: docs/manual/00001012080500.zettel ================================================================== --- docs/manual/00001012080500.zettel +++ docs/manual/00001012080500.zettel @@ -2,16 +2,16 @@ title: API: Refresh internal data role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211230230441 -modified: 20241216104624 +modified: 20220923104836 Zettelstore maintains some internal data to allow faster operations. One example is the [[content search|00001012051400]] for a term: Zettelstore does not need to scan all zettel to find all occurrences for the term. -Instead, all words are stored internally, with a list of zettel where they occur. +Instead, all word are stored internally, with a list of zettel where they occur. Another example is the way to determine which zettel are stored in a [[ZIP file|00001004011200]]. Scanning a ZIP file is a lengthy operation, therefore Zettelstore maintains a directory of zettel for each ZIP file. All these internal data may become stale. Index: docs/manual/00001012920000.zettel ================================================================== --- docs/manual/00001012920000.zettel +++ docs/manual/00001012920000.zettel @@ -2,11 +2,11 @@ title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 -modified: 20240711183819 +modified: 20240708155042 All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where: ; ''PREFIX'' : is the URL prefix (default: ""/""), configured via the ''url-prefix'' [[startup configuration|00001004010000]], ; ''LETTER'' @@ -22,10 +22,11 @@ | ''x'' | GET: [[retrieve administrative data|00001012070500]] | | E**x**ecute | | POST: [[execute command|00001012080100]] | ''z'' | GET: [[list zettel|00001012051200]]/[[query zettel|00001012051400]] | GET: [[retrieve zettel|00001012053300]] | **Z**ettel | | POST: [[create new zettel|00001012053200]] | PUT: [[update zettel|00001012054200]] | | | DELETE: [[delete zettel|00001012054600]] +| | | MOVE: [[rename zettel|00001012054400]][^Renaming a zettel is deprecated and will be removed in version 0.19 or later.] The full URL will contain either the ""http"" oder ""https"" scheme, a host name, and an optional port number. The API examples will assume the ""http"" schema, the local host ""127.0.0.1"", the default port ""23123"", and the default empty ''PREFIX'' ""/"". Therefore, all URLs in the API documentation will begin with ""http://127.0.0.1:23123/"". Index: docs/manual/00001012921200.zettel ================================================================== --- docs/manual/00001012921200.zettel +++ docs/manual/00001012921200.zettel @@ -2,11 +2,11 @@ title: API: Encoding of Zettel Access Rights role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20220201173115 -modified: 20240711183931 +modified: 20240708155122 Various API calls return a symbolic expression list ''(rights N)'', with ''N'' as a number, that encodes the access rights the user currently has. ''N'' is an integer number between 0 and 62.[^Not all values in this range are used.] The value ""0"" signals that something went wrong internally while determining the access rights. @@ -18,11 +18,11 @@ |=Bit number:|Bit value:|Meaning | 1 | 2 | User is allowed to create a new zettel | 2 | 4 | User is allowed to read the zettel | 3 | 8 | User is allowed to update the zettel -| 4 | 16 | (not in use; was assigned to an operation) +| 4 | 16 | User is allowed to rename the zettel[^Renaming a zettel is deprecated and will be removed in version 0.19 or later.] | 5 | 32 | User is allowed to delete the zettel The algorithm to calculate the actual access rights from the value is relatively simple: # Search for the biggest bit value that is less than the rights value. This is an access right for the current user. Index: docs/manual/00001012931400.zettel ================================================================== --- docs/manual/00001012931400.zettel +++ docs/manual/00001012931400.zettel @@ -2,11 +2,11 @@ title: Encoding of Sz Block Elements role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20230403161803 -modified: 20241216104739 +modified: 20240123120132 === ''PARA'' :::syntax __Paragraph__ **=** ''(PARA'' [[__InlineElement__|00001012931600]] … '')''. ::: @@ -45,11 +45,11 @@ If it is a block, it may contain a nested list. === ''DESCRIPTION'' :::syntax __Description__ **=** ''(DESCRIPTION'' __DescriptionTerm__ __DescriptionValues__ __DescriptionTerm__ __DescriptionValues__ … '')''. ::: -A description is a sequence of one or more terms and values. +A description is a sequence of one ore more terms and values. :::syntax __DescriptionTerm__ **=** ''('' [[__InlineElement__|00001012931600]] … '')''. ::: A description term is just an inline-structured value. @@ -124,11 +124,11 @@ :::syntax __VerseRegion__ **=** ''(REGION-VERSE'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] … '')'' [[__InlineElement__|00001012931600]] … '')''. ::: A block region just treats the block to contain a verse. -Soft line breaks are transformed into hard line breaks to save the structure of the verse / poem. +Soft line break are transformed into hard line breaks to save the structure of the verse / poem. Attributes may further specify something. The inline typically describes author / source of the verse. === ''VERBATIM-*'' The following lists specifies some literal text of more than one line. 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: 20241216104906 +modified: 20240620170546 === ''TEXT'' :::syntax __Text__ **=** ''(TEXT'' String '')''. ::: @@ -94,11 +94,11 @@ __EmbedBLOB__ **=** ''(EMBED-BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, '')''. ::: If used if some processed image has to be embedded inside some inline material. The first string specifies the syntax of the image content. The second string contains the image content. -If the syntax is ""SVG"", the image content is not encoded further. +If the syntax is ""SVG"", the image content is not further encoded. Otherwise a base64 encoding is used. === ''CITE'' :::syntax __CiteBLOB__ **=** ''(CITE'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. @@ -140,11 +140,11 @@ The inline text should be treated as inserted. :::syntax __MarkFormat__ **=** ''(FORMAT-MARK'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. ::: -The inline text should be treated as highlighted for the reader (but was not important to the original author). +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. Index: docs/manual/00001017000000.zettel ================================================================== --- docs/manual/00001017000000.zettel +++ docs/manual/00001017000000.zettel @@ -2,11 +2,11 @@ title: Tips and Tricks role: manual tags: #manual #zettelstore syntax: zmk created: 20220803170112 -modified: 20241216105111 +modified: 20231012154803 === Welcome Zettel * **Problem:** You want to put your Zettelstore into the public and need a starting zettel for your users. In addition, you still want a ""home zettel"", with all your references to internal, non-public zettel. Zettelstore only allows to specify one [[''home-zettel''|00001004020000#home-zettel]]. @@ -67,14 +67,14 @@ ''zettelstore run -d ~/Library/Mobile\\ Documents/com\\~apple\\~CloudDocs/zettel'' (The ""''\\''"" is needed by the command line processor to mask the following character to be processed in unintended ways.) * **Discussion:** Zettel files are synchronized between your computers via iCloud. - Is does not matter, if one of your computers is offline or switched off. + Is does not matter, if one of your computer is offline / switched off. iCloud will synchronize the zettel files if it later comes online. However, if you use more than one computer simultaneously, you must be aware that synchronization takes some time. - It might take several seconds, maybe longer, that the new version of a zettel appears on the other computer. + It might take several seconds, maybe longer, that new new version of a zettel appears on the other computer. If you update the same zettel on multiple computers at nearly the same time, iCloud will not be able to synchronize the different versions in a safe manner. Zettelstore is intentionally not aware of any synchronization within its zettel boxes. If Zettelstore behaves strangely after a synchronization took place, the page about [[Troubleshooting|00001018000000#working-with-files]] might contain some useful information. Index: docs/manual/00001018000000.zettel ================================================================== --- docs/manual/00001018000000.zettel +++ docs/manual/00001018000000.zettel @@ -2,11 +2,11 @@ title: Troubleshooting role: manual tags: #manual #zettelstore syntax: zmk created: 20211027105921 -modified: 20241212153148 +modified: 20240221134749 This page lists some problems and their solutions that may occur when using your Zettelstore. === Installation * **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer. @@ -13,11 +13,11 @@ Therefore, it will not start Zettelstore. ** **Solution:** Press the ''Ctrl'' key while opening the context menu of the Zettelstore executable with a right-click. A dialog is then opened where you can acknowledge that you understand the possible risks when you start Zettelstore. This dialog is only resented once for a given Zettelstore executable. * **Problem:** When you double-click on the Zettelstore executable icon, Windows complains that Zettelstore is an application from an unknown developer. -** **Solution:** Windows displays a dialog where you can acknowledge possible risks and allow to start Zettelstore. +** **Solution:** Windows displays a dialog where you can acknowledge possible risks and allows to start Zettelstore. === Authentication * **Problem:** [[Authentication is enabled|00001010040100]] for a local running Zettelstore and there is a valid [[user zettel|00001010040200]] for the owner. But entering user name and password at the [[web user interface|00001014000000]] seems to be ignored, while entering a wrong password will result in an error message. ** **Explanation:** A local running Zettelstore typically means, that you are accessing the Zettelstore using an URL with schema ''http://'', and not ''https://'', for example ''http://localhost:23123''. @@ -38,12 +38,12 @@ === HTML content is not shown * **Problem:** You have entered some HTML code as content for your Zettelstore, but this content is not shown on the Web User Interface. You may have entered a Zettel with syntax [[""html""|00001008000000#html]], or you have used an [[inline-zettel block|00001007031200]] with syntax ""html"", or you entered a Zettel with syntax [[""markdown""|00001008000000#markdown]] (or ""md"") and used some HTML code fragments. -** **Explanation:** Working with HTML code from unknown sources may lead to severe security problems. - The HTML code may force web browsers to load more content from external server, it may contain malicious JavaScript code, it may reference to CSS artifacts that itself load from external servers and may contain malicious software. +** **Explanation:** Working with HTML code from unknown sources may lead so severe security problems. + The HTML code may force web browsers to load more content from external server, it may contain malicious JavaScript code, it may reference to CSS artifacts that itself load from external servers and may contains malicious software. Zettelstore tries to do its best to ignore problematic HTML code, but it may fail. Either because of unknown bugs or because of yet unknown changes in the future. Zettelstore sets a restrictive [[Content Security Policy|https://www.w3.org/TR/CSP/]], but this depends on web browsers to implement them correctly and on users to not disable it. Zettelstore will not display any HTML code, which contains a ``