Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.18.0 +0.19.0-dev Index: auth/auth.go ================================================================== --- auth/auth.go +++ auth/auth.go @@ -93,13 +93,10 @@ 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. CanRefresh(user *meta.Meta) bool Index: auth/policy/anon.go ================================================================== --- auth/policy/anon.go +++ auth/policy/anon.go @@ -34,14 +34,10 @@ 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) } func (ap *anonPolicy) CanRefresh(user *meta.Meta) bool { Index: auth/policy/box.go ================================================================== --- auth/policy/box.go +++ auth/policy/box.go @@ -120,26 +120,10 @@ 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) } func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error { Index: auth/policy/default.go ================================================================== --- auth/policy/default.go +++ auth/policy/default.go @@ -26,11 +26,10 @@ 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 @@ -113,20 +113,10 @@ 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 } if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok { Index: auth/policy/policy.go ================================================================== --- auth/policy/policy.go +++ auth/policy/policy.go @@ -58,14 +58,10 @@ 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) } func (p *prePolicy) CanRefresh(user *meta.Meta) bool { Index: auth/policy/policy_test.go ================================================================== --- auth/policy/policy_test.go +++ auth/policy/policy_test.go @@ -57,11 +57,10 @@ 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) }) } } @@ -393,98 +392,10 @@ } 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,8 +18,7 @@ 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 @@ -35,16 +35,10 @@ 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. DeleteZettel(ctx context.Context, zid id.Zid) error @@ -200,10 +194,18 @@ // ReadStats populates st with box statistics ReadStats(st *Stats) // Dump internal data to a Writer. Dump(w io.Writer) + + // Return zid mapper + Mapper() Mapper +} + +// Mapper is used for mapping old-style to and from new-style zettel identifier +type Mapper interface { + LookupZidO(id.ZidN) (id.Zid, bool) } // UpdateReason gives an indication, why the ObserverFunc was called. type UpdateReason uint8 @@ -210,11 +212,12 @@ // Values for Reason const ( _ UpdateReason = iota OnReady // Box is started and fully operational OnReload // Box was reloaded - OnZettel // Something with a zettel happened + OnZettel // Something with an existing zettel happened + OnDelete // A zettel was deleted ) // UpdateInfo contains all the data about a changed zettel. type UpdateInfo struct { Box BaseBox @@ -222,10 +225,13 @@ Zid id.Zid } // 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, bool) // 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. Index: box/compbox/compbox.go ================================================================== --- box/compbox/compbox.go +++ box/compbox/compbox.go @@ -30,11 +30,11 @@ ) func init() { manager.Register( " comp", - func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { + func(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { return getCompBox(cdata.Number, cdata.Enricher, cdata.Mapper), nil }) } type compBox struct { @@ -64,11 +64,11 @@ // 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}, + 9999999996: {genMappingM, genMappingC}, // TEMP for v0.19-dev } // Get returns the one program box. func getCompBox(boxNumber int, mf box.Enricher, mapper manager.Mapper) *compBox { return &compBox{ @@ -140,25 +140,10 @@ } } 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 { err = box.ErrReadOnly Index: box/compbox/mapping.go ================================================================== --- box/compbox/mapping.go +++ box/compbox/mapping.go @@ -15,10 +15,11 @@ import ( "bytes" "context" + "t73f.de/r/zsc/api" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // Zettelstore Identifier Mapping. @@ -26,40 +27,22 @@ // 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") + m := getTitledMeta(zid, "Zettelstore Identifier Mapping View (TEMP for v0.19-dev)") + m.Set(api.KeySyntax, meta.SyntaxText) + m.Set(api.KeyVisibility, api.ValueVisibilityLogin) + return m } func genMappingC(ctx context.Context, cb *compBox) []byte { - var buf bytes.Buffer - toNew, err := cb.mapper.OldToNewMapping(ctx) + src, err := cb.mapper.FetchAsBytes(ctx) if err != nil { + var buf bytes.Buffer 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() + return src } Index: box/compbox/memory.go ================================================================== --- box/compbox/memory.go +++ box/compbox/memory.go @@ -40,10 +40,12 @@ 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) Index: box/compbox/warnings.go ================================================================== --- box/compbox/warnings.go +++ box/compbox/warnings.go @@ -15,16 +15,19 @@ import ( "bytes" "context" + "t73f.de/r/zsc/api" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) func genWarningsM(zid id.Zid) *meta.Meta { - return getTitledMeta(zid, "Zettelstore Warnings") + m := getTitledMeta(zid, "Zettelstore Warnings") + m.Set(api.KeyVisibility, api.ValueVisibilityLogin) + return m } func genWarningsC(ctx context.Context, cb *compBox) []byte { var buf bytes.Buffer buf.WriteString("* [[Zettel without stored creation date|query:created-missing:true]]\n") Index: box/constbox/base.css ================================================================== --- box/constbox/base.css +++ box/constbox/base.css @@ -14,11 +14,10 @@ *,*::before,*::after { box-sizing: border-box; } html { - font-size: 1rem; font-family: serif; scroll-behavior: smooth; height: 100%; } body { @@ -87,45 +86,40 @@ article > * + * { margin-top: .5rem } article header { padding: 0; margin: 0; } - 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 } + 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 } li,figure,figcaption,dl { margin: 0 } - dt { margin: .5rem 0 0 0 } + dt { margin: .5em 0 0 0 } dt+dd { margin-top: 0 } - dd { margin: .5rem 0 0 2rem } + dd { margin: .5em 0 0 2em } dd > p:first-child { margin: 0 0 0 0 } blockquote { - border-left: 0.5rem solid lightgray; - padding-left: 1rem; - margin-left: 1rem; - margin-right: 2rem; - font-style: italic; - } - blockquote p { margin-bottom: .5rem } - blockquote cite { font-style: normal } + border-left: .5em solid lightgray; + padding-left: 1em; + margin-left: 1em; + margin-right: 2em; + } + blockquote p { margin-bottom: .5em } table { border-collapse: collapse; border-spacing: 0; max-width: 100%; } - 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%) - } + 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%) } main form { padding: 0 .5em; margin: .5em 0 0 0; } main form:after { @@ -157,100 +151,100 @@ padding-left: 1em; padding-right: 1em; } a:not([class]) { text-decoration-skip-ink: auto } a.broken { text-decoration: line-through } - a.external::after { content: "➚"; display: inline-block } + a[rel~="external"]::after { content: "➚"; display: inline-block } img { max-width: 100% } img.right { float: right } ol.zs-endnotes { - padding-top: .5rem; + padding-top: .5em; border-top: 1px solid; } kbd { font-family:monospace } code,pre { font-family: monospace; font-size: 85%; } code { - padding: .1rem .2rem; + padding: .1em .2em; background: #f0f0f0; border: 1px solid #ccc; - border-radius: .25rem; + border-radius: .25em; } pre { - padding: .5rem .7rem; + padding: .5em .7em; max-width: 100%; overflow: auto; border: 1px solid #ccc; - border-radius: .5rem; + border-radius: .5em; background: #f0f0f0; } pre code { font-size: 95%; position: relative; padding: 0; border: none; } div.zs-indication { - padding: .5rem .7rem; + padding: .5em .7em; max-width: 100%; - border-radius: .5rem; + border-radius: .5em; border: 1px solid black; } div.zs-indication p:first-child { margin-top: 0 } span.zs-indication { border: 1px solid black; - border-radius: .25rem; - padding: .1rem .2rem; + border-radius: .25em; + padding: .1rem .2em; font-size: 95%; } .zs-info { background-color: lightblue; - padding: .5rem 1rem; + padding: .5em 1em; } .zs-warning { background-color: lightyellow; - padding: .5rem 1rem; + padding: .5em 1em; } .zs-error { background-color: lightpink; border-style: none !important; font-weight: bold; } - td.left { text-align:left } - td.center { text-align:center } - td.right { text-align:right } + td.left, th.left { text-align:left } + td.center, th.center { text-align:center } + td.right, th.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: .2rem } + .zs-deprecated { border-style: dashed; padding: .2em } .zs-meta { font-size:.75rem; color:#444; - margin-bottom:1rem; + margin-bottom:1em; } .zs-meta a { color:#444 } - h1+.zs-meta { margin-top:-1rem } - nav > details { margin-top:1rem } + h1+.zs-meta { margin-top:-1em } + nav > details { margin-top:1em } details > summary { width: 100%; background-color: #eee; font-family:sans-serif; } details > ul { margin-top:0; - padding-left:2rem; + padding-left:2em; background-color: #eee; } - footer { padding: 0 1rem } + footer { padding: 0 1em } @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(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { + func(_ *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, @@ -95,25 +95,10 @@ } } 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 { err = box.ErrReadOnly @@ -199,21 +184,21 @@ constHeader{ api.KeyTitle: "Zettelstore Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230510155300", - api.KeyModified: "20240219145100", + api.KeyModified: "20240826110000", 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: "20240618170000", + api.KeyModified: "20240826110800", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentInfoSxn)}, id.FormTemplateZid: { constHeader{ @@ -223,27 +208,17 @@ 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: "20240219145200", + api.KeyModified: "20240826110800", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentDeleteSxn)}, id.ListTemplateZid: { constHeader{ @@ -280,11 +255,11 @@ constHeader{ api.KeyTitle: "Zettelstore Sxn Base Code", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20230619132800", - api.KeyModified: "20240618170100", + api.KeyModified: "20240826142000", api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityExpert, api.KeyPrecursor: string(api.ZidSxnPrelude), }, zettel.NewContent(contentBaseCodeSxn)}, @@ -303,11 +278,11 @@ constHeader{ api.KeyTitle: "Zettelstore Base CSS", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxCSS, api.KeyCreated: "20200804111624", - api.KeyModified: "20231129112800", + api.KeyModified: "20240827143500", api.KeyVisibility: api.ValueVisibilityPublic, }, zettel.NewContent(contentBaseCSS)}, id.MustParse(api.ZidUserCSS): { constHeader{ @@ -431,10 +406,21 @@ api.KeySyntax: meta.SyntaxNone, api.KeyLang: api.ValueLangEN, api.KeyCreated: "20240703235900", api.KeyVisibility: api.ValueVisibilityLogin, }, + zettel.NewContent(nil)}, + id.MustParse(api.ZidMapping): { + constHeader{ + api.KeyTitle: "Zettelstore Identifier Mapping", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxText, + api.KeyLang: api.ValueLangEN, + api.KeyCreated: "20240807114600", + api.KeyReadOnly: api.ValueTrue, + api.KeyVisibility: api.ValueVisibilityLogin, + }, zettel.NewContent(nil)}, id.DefaultHomeZid: { constHeader{ api.KeyTitle: "Home", api.KeyRole: api.ValueRoleZettel, @@ -467,13 +453,10 @@ 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 Index: box/constbox/delete.sxn ================================================================== --- box/constbox/delete.sxn +++ box/constbox/delete.sxn @@ -10,11 +10,11 @@ ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article - (header (h1 "Delete Zettel " ,zid)) + (header (h1 "Delete Zettel " ,zid " / " ,zid-n)) (p "Do you really want to delete this zettel?") ,@(if shadowed-box `((div (@ (class "zs-info")) (h2 "Information") (p "If you delete this zettel, the previously shadowed zettel from overlayed box " ,shadowed-box " becomes available.") Index: box/constbox/info.sxn ================================================================== --- box/constbox/info.sxn +++ box/constbox/info.sxn @@ -10,19 +10,18 @@ ;;; SPDX-License-Identifier: EUPL-1.2 ;;; SPDX-FileCopyrightText: 2023-present Detlef Stern ;;;---------------------------------------------------------------------------- `(article - (header (h1 "Information for Zettel " ,zid) + (header (h1 "Information for Zettel " ,zid " / " ,zid-n) (p (a (@ (href ,web-url)) "Web") (@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/rename.sxn Index: box/constbox/rename.sxn ================================================================== --- box/constbox/rename.sxn +++ /dev/null @@ -1,42 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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/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 "noopener noreferrer")) ,e))) + `(li (a (@ (href ,e) (target "_blank") (rel "external 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 Index: box/constbox/zettel.sxn ================================================================== --- box/constbox/zettel.sxn +++ box/constbox/zettel.sxn @@ -14,11 +14,11 @@ `(article (header (h1 ,heading) (div (@ (class "zs-meta")) ,@(if (bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " · "))) - ,zid (@H " · ") + ,zid " / " ,zid-n (@H " · ") (a (@ (href ,info-url)) "Info") (@H " · ") "(" ,@(if (bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role))) ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role)) `((@H " → ") (a (@ (href ,folge-role-url)) ,meta-folge-role))) ")" 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) { - 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) 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, false) } } 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) + dp.notifyChanged(meta.Zid, box.OnZettel) 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,66 +314,20 @@ } dp.updateEntryFromMetaContent(entry, meta, zettel.Content) dp.dirSrv.UpdateDirEntry(entry) err := dp.srvSetZettel(ctx, entry, zettel) if err == nil { - dp.notifyChanged(zid) + dp.notifyChanged(zid, box.OnZettel) } 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 } entry := dp.dirSrv.GetDirEntry(zid) @@ -393,11 +347,11 @@ if err != nil { return nil } err = dp.srvDeleteZettel(ctx, entry, zid) if err == nil { - dp.notifyChanged(zid) + dp.notifyChanged(zid, box.OnDelete) } 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 chan<- box.UpdateInfo + notify box.UpdateNotifier dirSrv *notify.DirService } func (zb *zipBox) Location() string { if strings.HasPrefix(zb.name, "/") { @@ -170,28 +170,10 @@ 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 entry := zb.dirSrv.GetDirEntry(zid) 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, min, def, max int) int { +func GetQueryInt(u *url.URL, key string, minVal, defVal, maxVal int) int { sVal := u.Query().Get(key) if sVal == "" { - return def + return defVal } iVal, err := strconv.Atoi(sVal) if err != nil { - return def + return defVal } - if iVal < min { - return min + if iVal < minVal { + return minVal } - if iVal > max { - return max + if iVal > maxVal { + return maxVal } return iVal } Index: box/manager/box.go ================================================================== --- box/manager/box.go +++ box/manager/box.go @@ -54,26 +54,46 @@ } return false } // CreateZettel creates a new zettel. -func (mgr *Manager) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { +func (mgr *Manager) CreateZettel(ctx context.Context, ztl 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 { - zettel.Meta = mgr.cleanMetaProperties(zettel.Meta) - zid, err := box.CreateZettel(ctx, zettel) + ztl.Meta = mgr.cleanMetaProperties(ztl.Meta) + zidO, err := box.CreateZettel(ctx, ztl) if err == nil { - mgr.idxUpdateZettel(ctx, zettel) + mgr.idxUpdateZettel(ctx, ztl) + + err = mgr.createMapping(ctx, zidO) } - return zid, err + return zidO, err } return id.Invalid, box.ErrReadOnly +} +func (mgr *Manager) createMapping(ctx context.Context, zidO id.Zid) error { + mgr.mappingMx.Lock() + defer mgr.mappingMx.Unlock() + mappingZettel, err := mgr.getZettel(ctx, id.MappingZid) + if err != nil { + mgr.mgrLog.Error().Err(err).Msg("Unable to get mapping zettel") + return err + } + + zidN := mgr.zidMapper.GetZidN(zidO) + mappingZettel.Content = zettel.NewContent(mgr.zidMapper.AsBytes()) + if err = mgr.UpdateZettel(ctx, mappingZettel); err != nil { + mgr.mgrLog.Error().Err(err).Zid(zidO).Uint("zidN", uint64(zidN)).Msg("Unable to update mapping zettel") + return err + } + mgr.mgrLog.Debug().Zid(zidO).Msg("add to mapping zettel") + return nil } // GetZettel retrieves a specific zettel. func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetZettel") @@ -80,10 +100,13 @@ 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) @@ -137,11 +160,17 @@ } } return result, nil } -func (mgr *Manager) HasZettel(ctx context.Context, zid id.Zid) bool { +// FetchZidsO returns the set of all old-style zettel identifer managed by the box. +func (mgr *Manager) FetchZidsO(ctx context.Context) (*id.Set, error) { + mgr.mgrLog.Debug().Msg("FetchZidsO") + return mgr.fetchZids(ctx) +} + +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() @@ -152,18 +181,20 @@ } } 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 } @@ -239,10 +270,13 @@ 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 } @@ -250,47 +284,10 @@ 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 } @@ -303,29 +300,48 @@ } return false } // DeleteZettel removes the zettel from the box. -func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { - mgr.mgrLog.Debug().Zid(zid).Msg("DeleteZettel") +func (mgr *Manager) DeleteZettel(ctx context.Context, zidO id.Zid) error { + mgr.mgrLog.Debug().Zid(zidO).Msg("DeleteZettel") if err := mgr.checkContinue(ctx); err != nil { return err } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { - err := p.DeleteZettel(ctx, zid) + err := p.DeleteZettel(ctx, zidO) if err == nil { - mgr.idxDeleteZettel(ctx, zid) - return nil + mgr.idxDeleteZettel(ctx, zidO) + + err = mgr.deleteMapping(ctx, zidO) + return err } var errZNF box.ErrZettelNotFound if !errors.As(err, &errZNF) && !errors.Is(err, box.ErrReadOnly) { return err } } - return box.ErrZettelNotFound{Zid: zid} + return box.ErrZettelNotFound{Zid: zidO} +} +func (mgr *Manager) deleteMapping(ctx context.Context, zidO id.Zid) error { + mgr.mappingMx.Lock() + defer mgr.mappingMx.Unlock() + mappingZettel, err := mgr.getZettel(ctx, id.MappingZid) + if err != nil { + mgr.mgrLog.Error().Err(err).Msg("Unable to get mapping zettel") + return err + } + mgr.zidMapper.DeleteO(zidO) + mappingZettel.Content = zettel.NewContent(mgr.zidMapper.AsBytes()) + if err = mgr.updateZettel(ctx, mappingZettel); err != nil { + mgr.mgrLog.Error().Err(err).Zid(zidO).Msg("Unable to update mapping zettel") + return err + } + mgr.mgrLog.Debug().Zid(zidO).Msg("remove from mapping zettel") + return nil } // Remove all (computed) properties from metadata before storing the zettel. func (mgr *Manager) cleanMetaProperties(m *meta.Meta) *meta.Meta { result := m.Clone() Index: box/manager/enrich.go ================================================================== --- box/manager/enrich.go +++ box/manager/enrich.go @@ -23,10 +23,24 @@ "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 new zid + if m.ZidN.IsValid() { + if zidN, found := mgr.zidMapper.LookupZidN(m.Zid); found && m.ZidN != zidN { + mgr.mgrLog.Error().Zid(m.Zid). + Uint("stored", uint64(m.ZidN)).Uint("mapped", uint64(zidN)). + Msg("mapped != stored") + } + } else { + if zidN, found := mgr.zidMapper.LookupZidN(m.Zid); found { + m.ZidN = zidN + } else { + mgr.mgrLog.Error().Zid(m.Zid).Msg("no mapping found") + } + } // 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,19 +155,25 @@ } func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) { var cData collectData cData.initialize() - collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) + if mustIndexZettel(zettel.Meta) { + 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.DefaultHomeZid +} 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() { @@ -209,11 +215,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) } }) @@ -224,11 +230,11 @@ 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) @@ -235,15 +241,10 @@ 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 @@ -13,11 +13,13 @@ // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager import ( + "bytes" "context" + "fmt" "io" "net/url" "sync" "time" @@ -27,28 +29,30 @@ "zettelstore.de/z/box/manager/store" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" + "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/id/mapper" "zettelstore.de/z/zettel/meta" ) // 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 chan<- box.UpdateInfo + Notify box.UpdateNotifier 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) + FetchAsBytes(context.Context) ([]byte, 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() { @@ -100,11 +104,12 @@ observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} infos chan box.UpdateInfo propertyKeys strfun.Set // Set of property key names - zidMapper *zidMapper + zidMapper *mapper.Mapper + mappingMx sync.Mutex // protects updates to mapping zettel // Indexer data idxLog *logger.Logger idxStore store.Store idxAr *anteroomQueue @@ -121,10 +126,11 @@ 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 @@ -149,13 +155,13 @@ idxLog: boxLog.Clone().Str("box", "index").Child(), idxStore: createIdxStore(rtConfig), idxAr: newAnteroomQueue(1000), idxReady: make(chan struct{}, 1), } - mgr.zidMapper = NewZidMapper(mgr) + mgr.zidMapper = mapper.Make(mgr) - cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos, Mapper: mgr.zidMapper} + cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.notifyChanged, 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 @@ -222,15 +228,16 @@ if ignoreUpdate(cache, now, reason, zid) { mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored") continue } - mgr.idxEnqueue(reason, zid) + isStarted := mgr.State() == box.StartStateStarted + mgr.idxEnqueue(reason, zid, isStarted) if ci.Box == nil { ci.Box = mgr } - if mgr.State() == box.StartStateStarted { + if isStarted { mgr.notifyObserver(&ci) } } case <-mgr.done: return @@ -255,20 +262,38 @@ reason: reason, } return false } -func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { +func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zidO id.Zid, isStarted bool) { switch reason { case box.OnReady: return case box.OnReload: mgr.idxAr.Reset() case box.OnZettel: - mgr.idxAr.EnqueueZettel(zid) + if isStarted { + if zidO > id.MappingZid { + if _, found := mgr.zidMapper.LookupZidN(zidO); !found { + mgr.createMapping(context.Background(), zidO) + } + } else if zidO == id.MappingZid { + if _, err := mgr.getAndUpdateMapping(context.Background()); err != nil { + mgr.mgrLog.Error().Err(err).Msg("ID mapping update problem") + } else { + mgr.mgrLog.Info().Msg("ID mapping updated") + } + } + } + mgr.idxAr.EnqueueZettel(zidO) + case box.OnDelete: + if isStarted && zidO > id.MappingZid { + mgr.deleteMapping(context.Background(), zidO) + } + mgr.idxAr.EnqueueZettel(zidO) default: - mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") + mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zidO).Msg("Unknown notification reason") return } select { case mgr.idxReady <- struct{}{}: default: @@ -314,11 +339,13 @@ mgr.idxAr.Reset() // Ensure an initial index run mgr.done = make(chan struct{}) go mgr.notifier() mgr.waitBoxesAreStarted() + mgr.setupIdentifierMapping() mgr.setState(box.StartStateStarted) + mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady}) go mgr.idxIndexer() return nil } @@ -344,10 +371,55 @@ return false } } return true } + +func (mgr *Manager) setupIdentifierMapping() { + ctx := context.Background() + z, err := mgr.getAndUpdateMapping(ctx) + if err != nil { + mgr.mgrLog.Error().Err(err).Msg("error while reading and updating id mapping") + } + + mapping, err := mgr.zidMapper.FetchAsBytes(ctx) + if err != nil { + mgr.mgrLog.Error().Err(err).Msg("Unable to get current identifier mapping") + return + } + + content := z.Content.AsBytes() + if !bytes.Equal(content, mapping) { + z.Content = zettel.NewContent(mapping) + if err = mgr.updateZettel(ctx, z); err != nil { + mgr.mgrLog.Error().Err(err).Msg("Unable to write identifier mapping zettel") + } else { + mgr.mgrLog.Info().Msg("Mapping was updated") + } + } else { + mgr.mgrLog.Info().Msg("No mapping update") + } +} + +// Mapper returns the mapper used in this manager box. +func (mgr *Manager) Mapper() box.Mapper { return mgr.zidMapper } + +func (mgr *Manager) getAndUpdateMapping(ctx context.Context) (zettel.Zettel, error) { + z, err := mgr.getZettel(ctx, id.MappingZid) + if err != nil { + return z, fmt.Errorf("get id mapping zettel: %w", err) + } + if z.Content.IsBinary() { + return z, fmt.Errorf("id mapping zettel is binary") + } + z.Content.TrimSpace() + content := z.Content.AsBytes() + if err = mgr.zidMapper.ParseAndUpdate(content); err != nil { + err = fmt.Errorf("id mapping zettel parsing: %w", err) + } + return z, err +} // Stop the started box. Now only the Start() function is allowed. func (mgr *Manager) Stop(ctx context.Context) { mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() @@ -385,11 +457,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{Reason: box.OnZettel, Zid: zid} + mgr.infos <- box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: zid} return nil } // ReadStats populates st with box statistics. func (mgr *Manager) ReadStats(st *box.Stats) { @@ -435,5 +507,11 @@ if mgr.State() != box.StartStateStarted { return box.ErrStopped } return ctx.Err() } + +func (mgr *Manager) notifyChanged(bbox box.BaseBox, zid id.Zid, reason box.UpdateReason, force bool) { + if infos := mgr.infos; infos != nil && (zid != id.MappingZid || force) { + 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 @@ -455,72 +455,10 @@ 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 @@ -51,14 +51,10 @@ // 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 // Optimize removes unneeded space. DELETED box/manager/zidmapper.go Index: box/manager/zidmapper.go ================================================================== --- box/manager/zidmapper.go +++ /dev/null @@ -1,199 +0,0 @@ -//----------------------------------------------------------------------------- -// 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) { - if chci := mb.cdata.Notify; chci != nil { - chci <- box.UpdateInfo{Box: mb, Reason: box.OnZettel, Zid: zid} +func (mb *memBox) notifyChanged(zid id.Zid, reason box.UpdateReason) { + if notify := mb.cdata.Notify; notify != nil { + notify(mb, zid, reason, false) } } 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) + mb.notifyChanged(zid, box.OnZettel) mb.log.Trace().Zid(zid).Msg("CreateZettel") return zid, nil } func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { @@ -199,43 +199,15 @@ zettel.Meta = m mb.zettel[m.Zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(m.Zid) + mb.notifyChanged(m.Zid, box.OnZettel) 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() return ok @@ -249,11 +221,11 @@ return box.ErrZettelNotFound{Zid: zid} } delete(mb.zettel, zid) mb.curBytes -= oldZettel.Length() mb.mx.Unlock() - mb.notifyChanged(zid) + mb.notifyChanged(zid, box.OnDelete) 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,11 +16,10 @@ import ( "errors" "fmt" "path/filepath" "regexp" - "strings" "sync" "zettelstore.de/z/box" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" @@ -41,10 +40,11 @@ // 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 chan<- box.UpdateInfo + infos box.UpdateNotifier 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, chci chan<- box.UpdateInfo) *DirService { +func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, notify box.UpdateNotifier) *DirService { return &DirService{ box: box, log: log, notifier: notifier, - infos: chci, + infos: notify, state: DsCreated, } } // State the current service state. @@ -184,40 +184,10 @@ } 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() if ds.entries == nil { @@ -289,18 +259,18 @@ case Update: ds.mx.Lock() zid := ds.onUpdateFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { - ds.notifyChange(zid) + ds.notifyChange(zid, box.OnZettel) } case Delete: ds.mx.Lock() zid := ds.onDeleteFileEvent(ds.entries, ev.Name) ds.mx.Unlock() if zid != id.Invalid { - ds.notifyChange(zid) + ds.notifyChange(zid, box.OnDelete) } default: ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") } return newEntries, true @@ -314,18 +284,18 @@ return zids } func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) { for _, zid := range zids { - ds.notifyChange(zid) + ds.notifyChange(zid, box.OnZettel) delete(prevEntries, zid) } // These were previously stored, by are not found now. // Notify system that these were deleted, e.g. for updating the index. for zid := range prevEntries { - ds.notifyChange(zid) + ds.notifyChange(zid, box.OnDelete) } } func (ds *DirService) onDestroyDirectory() { ds.mx.Lock() @@ -332,11 +302,11 @@ entries := ds.entries ds.entries = nil ds.state = DsMissing ds.mx.Unlock() for zid := range entries { - ds.notifyChange(zid) + ds.notifyChange(zid, box.OnDelete) } } var validFileName = regexp.MustCompile(`^(\d{14})`) @@ -603,11 +573,11 @@ return newLen < oldLen } return newExt < oldExt } -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} +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, true) } } Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -73,11 +73,10 @@ 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( @@ -94,12 +93,10 @@ 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)) @@ -125,11 +122,10 @@ 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,21 +160,22 @@ 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" ) @@ -207,13 +208,15 @@ 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) @@ -225,10 +228,11 @@ } 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: docs/development/20210916193200.zettel ================================================================== --- docs/development/20210916193200.zettel +++ docs/development/20210916193200.zettel @@ -23,6 +23,7 @@ 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/20231218181900.zettel ================================================================== --- docs/development/20231218181900.zettel +++ docs/development/20231218181900.zettel @@ -69,11 +69,10 @@ * 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/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: 20240710183532 +modified: 20240926144803 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,13 +48,14 @@ 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 the developers). +: If set to [[true|00001006030500]], allows to debug the Zettelstore software (mostly used by Zettelstore 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''] @@ -118,10 +119,15 @@ ; [!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. ADDED docs/manual/00001004010200.zettel Index: docs/manual/00001004010200.zettel ================================================================== --- /dev/null +++ docs/manual/00001004010200.zettel @@ -0,0 +1,29 @@ +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/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: 20240710173506 +modified: 20240711183257 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. @@ -31,11 +31,11 @@ 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.[^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.[^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 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. +You can create these special zettel 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: @@ -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 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. +If you want to read the original zettel, you have to delete the zettel (which removes it from the file directory). Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software. * [[List of predefined zettel|00001005090000]] === Boxes: alternative ways to store zettel Index: docs/manual/00001005090000.zettel ================================================================== --- docs/manual/00001005090000.zettel +++ docs/manual/00001005090000.zettel @@ -2,11 +2,11 @@ title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 -modified: 20240709180005 +modified: 20240711183318 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 @@ -28,11 +28,10 @@ | [[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]] 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: 20240708154737 +modified: 20240711183409 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]]. @@ -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 renamed[^Renaming a zettel is deprecated. This feature will be removed in version 0.19 or later.] or deleted, these files will be deleted. + If a zettel is deleted, these files will also 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/00001006050200.zettel ================================================================== --- docs/manual/00001006050200.zettel +++ docs/manual/00001006050200.zettel @@ -2,11 +2,11 @@ title: Alphanumeric Zettel Identifier role: manual tags: #design #manual #zettelstore syntax: zmk created: 20240705200557 -modified: 20240710173133 +modified: 20240807173414 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. @@ -18,32 +18,32 @@ === 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 +; Version 0.18 (current) : 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]].] + Zettel [[Zettelstore Warnings|00000000000102]] (''00000000000102'') lists these problematic zettel identifier. 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]].] + The zettel [[Zettelstore Identifier Mapping|00009999999999]] (''00009999999999'') will show this mapping. ; Version 0.19 : The new identifier format will be used initially internal. + Operation to rename a zettel, i.e. assigning a new identifier to a zettel, is removed permanently. + 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,15 +2,14 @@ title: Reserved zettel identifier role: manual tags: #design #manual #zettelstore syntax: zmk created: 20210721105704 -modified: 20240708154858 +modified: 20240711183638 [[Zettel identifier|00001006050000]] are typically created by examine the current date and time. -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. +By renaming the name of the underlying zettel file, you are able to provide any sequence of 14 digits[^Zettel identifier format will be migrated to a new format after version 0.19.]. 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''. 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: 20240708154954 +modified: 20240711183714 Whether an operation of the Zettelstore is allowed or rejected, depends on various factors. The following rules are checked first, in this order: @@ -41,12 +41,9 @@ *** 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: 20240708154140 +modified: 20240711183736 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,13 +32,12 @@ * [[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]] DELETED docs/manual/00001012054400.zettel Index: docs/manual/00001012054400.zettel ================================================================== --- docs/manual/00001012054400.zettel +++ /dev/null @@ -1,57 +0,0 @@ -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/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: 20240708155042 +modified: 20240711183819 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,11 +22,10 @@ | ''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: 20240708155122 +modified: 20240711183931 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 | User is allowed to rename the zettel[^Renaming a zettel is deprecated and will be removed in version 0.19 or later.] +| 4 | 16 | (not in use; was assigned to an operation) | 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/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: 20240221134749 +modified: 20240830155745 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. @@ -52,5 +52,11 @@ Therefore, Zettelstore disallows any HTML content as a default. If you know what you are doing, e.g. because you will never copy HTML code you do not understand, you can relax this default. ** **Solution 1:** If you want zettel with syntax ""html"" not to be ignored, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""html"". ** **Solution 2:** If you want zettel with syntax ""html"" not to be ignored, **and** want to allow HTML in Markdown, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""markdown"". ** **Solution 3:** If you want zettel with syntax ""html"" not to be ignored, **and** want to allow HTML in Markdown, **and** want to use HTML code within Zettelmarkup, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""zettelmarkup"". + +=== Search for specific content +* **Problem:** If you are searching for zettel with zettel content ""EUPL"", the zettel with Zettelstore's [[License|00000000000004]] is not shown, but it does contain the character sequence ""EUPL"". +** **Solution:** The content of zettel with a zettel identifier less or equal ''00009999999999'' is not searched. + These zettel are predefined zettel, sometimes computed zettel, with some content not related to your research. + For these zettel, only the metadata can be searched. Index: encoder/encoder_block_test.go ================================================================== --- encoder/encoder_block_test.go +++ encoder/encoder_block_test.go @@ -331,14 +331,14 @@ zmk: `|h1>|=h2|h3:| |%--+---+---+ |h1h2h3c1c2c3f1f2=f3`, + encoderHTML: `
h1h2h3
c1c2c3
f1f2=f3
`, encoderMD: "", encoderSz: `(BLOCK (TABLE ((CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) ((CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) ((CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`, - encoderSHTML: `((table (thead (tr (td (@ (class . "right")) "h1") (td "h2") (td (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`, + encoderSHTML: `((table (thead (tr (th (@ (class . "right")) "h1") (th "h2") (th (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`, encoderText: "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3", encoderZmk: `|=h1>|=h2|=h3: |quotes", + encoderMD: "“quotes”", encoderSz: `(INLINE (FORMAT-QUOTE () (TEXT "quotes")))`, encoderSHTML: `((@L (@H "“") "quotes" (@H "”")))`, encoderText: `quotes`, encoderZmk: useZmk, }, @@ -161,11 +161,11 @@ { descr: "Quotes formatting (german)", zmk: `""quotes""{lang=de}`, expect: expectMap{ encoderHTML: `„quotes“`, - encoderMD: "quotes", + encoderMD: "„quotes“", encoderSz: `(INLINE (FORMAT-QUOTE (("lang" . "de")) (TEXT "quotes")))`, encoderSHTML: `((span (@ (lang . "de")) (@H "„") "quotes" (@H "“")))`, encoderText: `quotes`, encoderZmk: `""quotes""{lang="de"}`, }, @@ -173,11 +173,11 @@ { descr: "Empty quotes (default)", zmk: `""""`, expect: expectMap{ encoderHTML: `“”`, - encoderMD: "", + encoderMD: "“”", encoderSz: `(INLINE (FORMAT-QUOTE ()))`, encoderSHTML: `((@L (@H "“" "”")))`, encoderText: ``, encoderZmk: useZmk, }, @@ -185,11 +185,11 @@ { descr: "Empty quotes (unknown)", zmk: `""""{lang=unknown}`, expect: expectMap{ encoderHTML: `""`, - encoderMD: "", + encoderMD: """", encoderSz: `(INLINE (FORMAT-QUOTE (("lang" . "unknown"))))`, encoderSHTML: `((span (@ (lang . "unknown")) (@H """ """)))`, encoderText: ``, encoderZmk: `""""{lang="unknown"}`, }, @@ -197,11 +197,11 @@ { descr: "Nested quotes (default)", zmk: `""say: ::""yes, ::""or?""::""::""`, expect: expectMap{ encoderHTML: `“say: ‘yes, “or?””`, - encoderMD: "say: yes, or?", + encoderMD: `“say: ‘yes, “or?”’”`, encoderSz: `(INLINE (FORMAT-QUOTE () (TEXT "say: ") (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "yes, ") (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "or?")))))))`, encoderSHTML: `((@L (@H "“") "say: " (span (@L (@H "‘") "yes, " (span (@L (@H "“") "or?" (@H "”"))) (@H "’"))) (@H "”")))`, encoderText: `say: yes, or?`, encoderZmk: useZmk, }, @@ -209,11 +209,11 @@ { descr: "Two quotes", zmk: `""yes"" or ""no""`, expect: expectMap{ encoderHTML: `“yes” or “no”`, - encoderMD: "yes or no", + encoderMD: `“yes” or “no”`, encoderSz: `(INLINE (FORMAT-QUOTE () (TEXT "yes")) (TEXT " or ") (FORMAT-QUOTE () (TEXT "no")))`, encoderSHTML: `((@L (@H "“") "yes" (@H "”")) " or " (@L (@H "“") "no" (@H "”")))`, encoderText: `yes or no`, encoderZmk: useZmk, }, @@ -281,11 +281,11 @@ { descr: "Input formatting", zmk: `''input''`, expect: expectMap{ encoderHTML: `input`, - encoderMD: "input", + encoderMD: "`input`", encoderSz: `(INLINE (LITERAL-INPUT () "input"))`, encoderSHTML: `((kbd "input"))`, encoderText: `input`, encoderZmk: useZmk, }, @@ -293,11 +293,11 @@ { descr: "Output formatting", zmk: `==output==`, expect: expectMap{ encoderHTML: `output`, - encoderMD: "output", + encoderMD: "`output`", encoderSz: `(INLINE (LITERAL-OUTPUT () "output"))`, encoderSHTML: `((samp "output"))`, encoderText: `output`, encoderZmk: useZmk, }, @@ -317,11 +317,11 @@ { descr: "Nested Span Quote formatting", zmk: `::""abc""::{lang=fr}`, expect: expectMap{ encoderHTML: `« abc »`, - encoderMD: "abc", + encoderMD: "« abc »", encoderSz: `(INLINE (FORMAT-SPAN (("lang" . "fr")) (FORMAT-QUOTE () (TEXT "abc"))))`, encoderSHTML: `((span (@ (lang . "fr")) (@L (@H "«" " ") "abc" (@H " " "»"))))`, encoderText: `abc`, encoderZmk: `::""abc""::{lang="fr"}`, }, @@ -471,38 +471,38 @@ }, { descr: "Dummy Link", zmk: `[[abc]]`, expect: expectMap{ - encoderHTML: `abc`, + encoderHTML: `abc`, encoderMD: "[abc](abc)", encoderSz: `(INLINE (LINK-EXTERNAL () "abc"))`, - encoderSHTML: `((a (@ (class . "external") (href . "abc")) "abc"))`, + encoderSHTML: `((a (@ (href . "abc") (rel . "external")) "abc"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Simple URL", zmk: `[[https://zettelstore.de]]`, expect: expectMap{ - encoderHTML: `https://zettelstore.de`, + encoderHTML: `https://zettelstore.de`, encoderMD: "", encoderSz: `(INLINE (LINK-EXTERNAL () "https://zettelstore.de"))`, - encoderSHTML: `((a (@ (class . "external") (href . "https://zettelstore.de")) "https://zettelstore.de"))`, + encoderSHTML: `((a (@ (href . "https://zettelstore.de") (rel . "external")) "https://zettelstore.de"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "URL with Text", zmk: `[[Home|https://zettelstore.de]]`, expect: expectMap{ - encoderHTML: `Home`, + encoderHTML: `Home`, encoderMD: "[Home](https://zettelstore.de)", encoderSz: `(INLINE (LINK-EXTERNAL () "https://zettelstore.de" (TEXT "Home")))`, - encoderSHTML: `((a (@ (class . "external") (href . "https://zettelstore.de")) "Home"))`, + encoderSHTML: `((a (@ (href . "https://zettelstore.de") (rel . "external")) "Home"))`, encoderText: `Home`, encoderZmk: useZmk, }, }, { Index: encoder/htmlenc/htmlenc.go ================================================================== --- encoder/htmlenc/htmlenc.go +++ encoder/htmlenc/htmlenc.go @@ -29,11 +29,14 @@ "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) func init() { - encoder.Register(api.EncoderHTML, func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) }) + encoder.Register( + api.EncoderHTML, + func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) }, + ) } // Create an encoder. func Create(params *encoder.CreateParameter) *Encoder { // We need a new transformer every time, because tx.inVerse must be unique. @@ -44,10 +47,11 @@ lang: params.Lang, textEnc: textenc.Create(), } } +// Encoder contains all data needed for encoding. type Encoder struct { tx *szenc.Transformer th *shtml.Evaluator lang string textEnc *textenc.Encoder @@ -76,11 +80,11 @@ xast := he.tx.GetSz(&zn.Ast) hast, err := he.th.Evaluate(xast, &env) if err != nil { return 0, err } - hen := he.th.Endnotes(&env) + hen := shtml.Endnotes(&env) var head sx.ListBuilder head.Add(shtml.SymHead) head.Add(sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.MakeString("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta)) head.ExtendBang(hm) @@ -121,10 +125,11 @@ } gen := sxhtml.NewGenerator().SetNewline() return gen.WriteListHTML(w, hm) } +// WriteContent encodes the zettel content. func (he *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return he.WriteBlocks(w, &zn.Ast) } // WriteBlocks encodes a block slice. @@ -136,11 +141,11 @@ length, err2 := gen.WriteListHTML(w, hobj) if err2 != nil { return length, err2 } - l, err2 := gen.WriteHTML(w, he.th.Endnotes(&env)) + l, err2 := gen.WriteHTML(w, shtml.Endnotes(&env)) length += l return length, err2 } return 0, err } Index: encoder/mdenc/mdenc.go ================================================================== --- encoder/mdenc/mdenc.go +++ encoder/mdenc/mdenc.go @@ -16,29 +16,37 @@ import ( "io" "t73f.de/r/zsc/api" + "t73f.de/r/zsc/attrs" + "t73f.de/r/zsc/shtml" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/zettel/meta" ) func init() { - encoder.Register(api.EncoderMD, func(*encoder.CreateParameter) encoder.Encoder { return Create() }) + encoder.Register( + api.EncoderMD, + func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) }, + ) } // Create an encoder. -func Create() *Encoder { return &myME } - -type Encoder struct{} +func Create(params *encoder.CreateParameter) *Encoder { + return &Encoder{lang: params.Lang} +} -var myME Encoder +// Encoder contains all data needed for encoding. +type Encoder struct { + lang string +} // WriteZettel writes the encoded zettel to the writer. -func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { - v := newVisitor(w) +func (me *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { + v := newVisitor(w, me.lang) v.acceptMeta(zn.InhMeta, evalMeta) if zn.InhMeta.YamlSep { v.b.WriteString("---\n") } else { v.b.WriteByte('\n') @@ -47,12 +55,12 @@ length, err := v.b.Flush() return length, err } // WriteMeta encodes meta data as markdown. -func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { - v := newVisitor(w) +func (me *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { + v := newVisitor(w, me.lang) v.acceptMeta(m, evalMeta) length, err := v.b.Flush() return length, err } @@ -68,39 +76,63 @@ } v.b.WriteByte('\n') } } -func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { - return ze.WriteBlocks(w, &zn.Ast) +// WriteContent encodes the zettel content. +func (me *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { + return me.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes the content of a block slice to the writer. -func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { - v := newVisitor(w) +func (me *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) { + v := newVisitor(w, me.lang) ast.Walk(v, bs) length, err := v.b.Flush() return length, err } // WriteInlines writes an inline slice to the writer -func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { - v := newVisitor(w) +func (me *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { + v := newVisitor(w, me.lang) ast.Walk(v, is) length, err := v.b.Flush() return length, err } // visitor writes the abstract syntax tree to an EncWriter. type visitor struct { - b encoder.EncWriter - listInfo []int - listPrefix string + b encoder.EncWriter + listInfo []int + listPrefix string + langStack shtml.LangStack + quoteNesting uint +} + +func newVisitor(w io.Writer, lang string) *visitor { + return &visitor{b: encoder.NewEncWriter(w), langStack: shtml.NewLangStack(lang)} +} + +// pushAttribute adds the current attributes to the visitor. +func (v *visitor) pushAttributes(a attrs.Attributes) { + if value, ok := a.Get("lang"); ok { + v.langStack.Push(value) + } else { + v.langStack.Dup() + } } -func newVisitor(w io.Writer) *visitor { - return &visitor{b: encoder.NewEncWriter(w)} +// popAttributes removes the current attributes from the visitor. +func (v *visitor) popAttributes() { v.langStack.Pop() } + +// getLanguage returns the current language, +func (v *visitor) getLanguage() string { return v.langStack.Top() } + +func (v *visitor) getQuotes() (string, string, bool) { + qi := shtml.GetQuoteInfo(v.getLanguage()) + leftQ, rightQ := qi.GetQuotes(v.quoteNesting) + return leftQ, rightQ, qi.GetNBSp() } func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.BlockSlice: @@ -179,10 +211,13 @@ func (v *visitor) visitRegion(rn *ast.RegionNode) { if rn.Kind != ast.RegionQuote { return } + v.pushAttributes(rn.Attrs) + defer v.popAttributes() + first := true for _, bn := range rn.Blocks { pn, ok := bn.(*ast.ParaNode) if !ok { continue @@ -195,10 +230,13 @@ ast.Walk(v, &pn.Inlines) } } func (v *visitor) visitHeading(hn *ast.HeadingNode) { + v.pushAttributes(hn.Attrs) + defer v.popAttributes() + const headingSigns = "###### " v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-1:]) ast.Walk(v, &hn.Inlines) } @@ -279,14 +317,20 @@ } } } func (v *visitor) visitLink(ln *ast.LinkNode) { + v.pushAttributes(ln.Attrs) + defer v.popAttributes() + v.writeReference(ln.Ref, ln.Inlines) } func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) { + v.pushAttributes(en.Attrs) + defer v.popAttributes() + v.b.WriteByte('!') v.writeReference(en.Ref, en.Inlines) } func (v *visitor) writeReference(ref *ast.Reference, is ast.InlineSlice) { @@ -313,10 +357,13 @@ } return ref.URL.Scheme != "" } func (v *visitor) visitFormat(fn *ast.FormatNode) { + v.pushAttributes(fn.Attrs) + defer v.popAttributes() + switch fn.Kind { case ast.FormatEmph: v.b.WriteByte('*') ast.Walk(v, &fn.Inlines) v.b.WriteByte('*') @@ -323,25 +370,38 @@ case ast.FormatStrong: v.b.WriteString("__") ast.Walk(v, &fn.Inlines) v.b.WriteString("__") case ast.FormatQuote: - v.b.WriteString("") - ast.Walk(v, &fn.Inlines) - v.b.WriteString("") + v.writeQuote(fn) case ast.FormatMark: v.b.WriteString("") ast.Walk(v, &fn.Inlines) v.b.WriteString("") default: ast.Walk(v, &fn.Inlines) } } + +func (v *visitor) writeQuote(fn *ast.FormatNode) { + leftQ, rightQ, withNbsp := v.getQuotes() + v.b.WriteString(leftQ) + if withNbsp { + v.b.WriteString(" ") + } + v.quoteNesting++ + ast.Walk(v, &fn.Inlines) + v.quoteNesting-- + if withNbsp { + v.b.WriteString(" ") + } + v.b.WriteString(rightQ) +} func (v *visitor) visitLiteral(ln *ast.LiteralNode) { switch ln.Kind { - case ast.LiteralProg: + case ast.LiteralProg, ast.LiteralInput, ast.LiteralOutput: v.b.WriteByte('`') v.b.Write(ln.Content) v.b.WriteByte('`') case ast.LiteralComment, ast.LiteralHTML: // ignore everything default: Index: encoder/shtmlenc/shtmlenc.go ================================================================== --- encoder/shtmlenc/shtmlenc.go +++ encoder/shtmlenc/shtmlenc.go @@ -39,10 +39,11 @@ th: shtml.NewEvaluator(1), lang: params.Lang, } } +// Encoder contains all data needed for encoding. type Encoder struct { tx *szenc.Transformer th *shtml.Evaluator lang string } @@ -70,10 +71,11 @@ return 0, err } return sx.Print(w, metaSHTML) } +// WriteContent encodes the zettel content. func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return enc.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes a block slice to the writer Index: encoder/szenc/szenc.go ================================================================== --- encoder/szenc/szenc.go +++ encoder/szenc/szenc.go @@ -33,10 +33,11 @@ // We need a new transformer every time, because trans.inVerse must be unique. // If we can refactor it out, the transformer can be created only once. return &Encoder{trans: NewTransformer()} } +// Encoder contains all data needed for encoding. type Encoder struct { trans *Transformer } // WriteZettel writes the encoded zettel to the writer. @@ -49,10 +50,11 @@ // WriteMeta encodes meta data as s-expression. func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { return enc.trans.GetMeta(m, evalMeta).Print(w) } +// WriteContent encodes the zettel content. func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return enc.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes a block slice to the writer Index: encoder/szenc/transform.go ================================================================== --- encoder/szenc/transform.go +++ encoder/szenc/transform.go @@ -30,14 +30,16 @@ func NewTransformer() *Transformer { t := Transformer{} return &t } +// Transformer contains all data needed to transform into a s-expression. type Transformer struct { inVerse bool } +// GetSz transforms the given node into a sx list. func (t *Transformer) GetSz(node ast.Node) *sx.Pair { switch n := node.(type) { case *ast.BlockSlice: return t.getBlockList(n).Cons(sz.SymBlock) case *ast.InlineSlice: @@ -359,10 +361,11 @@ meta.TypeURL: sz.SymTypeURL, meta.TypeWord: sz.SymTypeWord, meta.TypeZettelmarkup: sz.SymTypeZettelmarkup, } +// GetMeta transforms the given metadata into a sx list. func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair { pairs := m.ComputedPairs() objs := make(sx.Vector, 0, len(pairs)) for _, p := range pairs { key := p.Key Index: encoder/textenc/textenc.go ================================================================== --- encoder/textenc/textenc.go +++ encoder/textenc/textenc.go @@ -29,10 +29,11 @@ } // Create an encoder. func Create() *Encoder { return &myTE } +// Encoder contains all data needed for encoding. type Encoder struct{} var myTE Encoder // Only a singleton is required. // WriteZettel writes metadata and content. @@ -71,10 +72,11 @@ buf.WriteString(meta.CleanTag(tag)) } } +// WriteContent encodes the zettel content. func (te *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return te.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes the content of a block slice to the writer. Index: encoder/zmkenc/zmkenc.go ================================================================== --- encoder/zmkenc/zmkenc.go +++ encoder/zmkenc/zmkenc.go @@ -33,10 +33,11 @@ } // Create an encoder. func Create() *Encoder { return &myZE } +// Encoder contains all data needed for encoding. type Encoder struct{} var myZE Encoder // WriteZettel writes the encoded zettel to the writer. @@ -73,10 +74,11 @@ } v.b.WriteByte('\n') } } +// WriteContent encodes the zettel content. func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) { return ze.WriteBlocks(w, &zn.Ast) } // WriteBlocks writes the content of a block slice to the writer. Index: encoding/atom/atom.go ================================================================== --- encoding/atom/atom.go +++ encoding/atom/atom.go @@ -27,18 +27,21 @@ "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) +// ContentType specifies the HTTP content type for Atom. const ContentType = "application/atom+xml" +// Configuration contains data to configure the Atom encoding. type Configuration struct { Title string Generator string NewURLBuilderAbs func() *api.URLBuilder } +// Setup initializes the Configuration. func (c *Configuration) Setup(cfg config.Config) { baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string) c.Title = cfg.GetSiteName() c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) + @@ -45,10 +48,11 @@ " " + kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') } } +// Marshal encodes the result of a query as Atom. func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) []byte { atomUpdated := encoding.LastUpdated(ml, time.RFC3339) feedLink := c.NewURLBuilderAbs().String() var buf bytes.Buffer Index: encoding/rss/rss.go ================================================================== --- encoding/rss/rss.go +++ encoding/rss/rss.go @@ -28,20 +28,23 @@ "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) +// ContentType specifies the HTTP content type for RSS. const ContentType = "application/rss+xml" +// Configuration contains data to configure the RSS encoding. type Configuration struct { Title string Language string Copyright string Generator string NewURLBuilderAbs func() *api.URLBuilder } +// Setup initializes the Configuration. func (c *Configuration) Setup(ctx context.Context, cfg config.Config) { baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string) defVals := cfg.AddDefaultValues(ctx, &meta.Meta{}) c.Title = cfg.GetSiteName() @@ -51,10 +54,11 @@ " " + kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') } } +// Marshal encodes the result of a query as Atom. func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) []byte { rssPublished := encoding.LastUpdated(ml, time.RFC1123Z) atomLink := "" if s := q.String(); s != "" { Index: evaluator/list.go ================================================================== --- evaluator/list.go +++ evaluator/list.go @@ -33,17 +33,17 @@ ) // QueryAction transforms a list of metadata according to query actions into a AST nested list. func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta, rtConfig config.Config) (ast.BlockNode, int) { ap := actionPara{ - ctx: ctx, - q: q, - ml: ml, - kind: ast.NestedListUnordered, - min: -1, - max: -1, - title: rtConfig.GetSiteName(), + ctx: ctx, + q: q, + ml: ml, + kind: ast.NestedListUnordered, + minVal: -1, + maxVal: -1, + title: rtConfig.GetSiteName(), } actions := q.Actions() if len(actions) == 0 { return ap.createBlockNodeMeta("") } @@ -54,17 +54,17 @@ ap.kind = ast.NestedListOrdered continue } if strings.HasPrefix(act, api.MinAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { - ap.min = num + ap.minVal = num continue } } if strings.HasPrefix(act, api.MaxAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { - ap.max = num + ap.maxVal = num continue } } if act == api.TitleAction && i+1 < len(actions) { ap.title = strings.Join(actions[i+1:], " ") @@ -102,17 +102,17 @@ } return bn, numItems } type actionPara struct { - ctx context.Context - q *query.Query - ml []*meta.Meta - kind ast.NestedListKind - min int - max int - title string + ctx context.Context + q *query.Query + ml []*meta.Meta + kind ast.NestedListKind + minVal int + maxVal int + title string } func (ap *actionPara) createBlockNodeWord(key string) (ast.BlockNode, int) { var buf bytes.Buffer ccs, bufLen := ap.prepareCatAction(key, &buf) @@ -172,21 +172,21 @@ } return &ast.ParaNode{Inlines: para}, len(ccs) } func (ap *actionPara) limitTags(ccs meta.CountedCategories) meta.CountedCategories { - if min, max := ap.min, ap.max; min > 0 || max > 0 { - if min < 0 { - min = ccs[len(ccs)-1].Count - } - if max < 0 { - max = ccs[0].Count - } - if ccs[len(ccs)-1].Count < min || max < ccs[0].Count { + if minVal, maxVal := ap.minVal, ap.maxVal; minVal > 0 || maxVal > 0 { + if minVal < 0 { + minVal = ccs[len(ccs)-1].Count + } + if maxVal < 0 { + maxVal = ccs[0].Count + } + if ccs[len(ccs)-1].Count < minVal || maxVal < ccs[0].Count { temp := make(meta.CountedCategories, 0, len(ccs)) for _, cat := range ccs { - if min <= cat.Count && cat.Count <= max { + if minVal <= cat.Count && cat.Count <= maxVal { temp = append(temp, cat) } } return temp } Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -1,19 +1,19 @@ module zettelstore.de/z -go 1.22 +go 1.23 require ( github.com/fsnotify/fsnotify v1.7.0 - github.com/yuin/goldmark v1.7.4 - golang.org/x/crypto v0.25.0 - golang.org/x/term v0.22.0 - golang.org/x/text v0.16.0 - t73f.de/r/sx v0.0.0-20240513163553-ec4fcc6539ca - t73f.de/r/sxwebs v0.0.0-20240613142113-66fc5a284245 - t73f.de/r/zsc v0.0.0-20240711144034-b141c81ad9b7 + github.com/yuin/goldmark v1.7.8 + golang.org/x/crypto v0.28.0 + golang.org/x/term v0.25.0 + golang.org/x/text v0.19.0 + t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5 + t73f.de/r/sxwebs v0.0.0-20240814085618-5b4b5c496c94 + t73f.de/r/zsc v0.0.0-20240826124629-97640fce4430 ) require ( - golang.org/x/sys v0.22.0 // indirect - t73f.de/r/webs v0.0.0-20240617100047-8730e9917915 // indirect + golang.org/x/sys v0.26.0 // indirect + t73f.de/r/webs v0.0.0-20240814085020-19dac746d568 // indirect ) Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,20 +1,20 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= -github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -t73f.de/r/sx v0.0.0-20240513163553-ec4fcc6539ca h1:vvDqiuUfBLf+t/gpiSyqIFAdvZ7FLigOH38bqMY+v8k= -t73f.de/r/sx v0.0.0-20240513163553-ec4fcc6539ca/go.mod h1:G9pD1j2R6y9ZkPBb81mSnmwaAvTOg7r6jKp/OF7WeFA= -t73f.de/r/sxwebs v0.0.0-20240613142113-66fc5a284245 h1:raE7KUgoGsp2DzXOko9dDXEsSJ/VvoXCDYeICx7i6uo= -t73f.de/r/sxwebs v0.0.0-20240613142113-66fc5a284245/go.mod h1:ErPBVUyE2fOktL/8M7lp/PR93wP/o9RawMajB1uSqj8= -t73f.de/r/webs v0.0.0-20240617100047-8730e9917915 h1:rwUaPBIH3shrUIkmw51f4RyCplsCU+ISZHailsLiHTE= -t73f.de/r/webs v0.0.0-20240617100047-8730e9917915/go.mod h1:UGAAtul0TK5ACeZ6zTS3SX6GqwMFXxlUpHiV8oqNq5w= -t73f.de/r/zsc v0.0.0-20240711144034-b141c81ad9b7 h1:Ysb9nud8uhB4N1hUMW3GmFvWabo1r6UlcG/DhhubyCQ= -t73f.de/r/zsc v0.0.0-20240711144034-b141c81ad9b7/go.mod h1:FH9nouOzCHoR0Nbk6gBK31gGJqQI8dGVXoyGI45yHkM= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5 h1:ug4hohM6pK28M8Uo0o3+XvjBure2wfEtuCnHVIdqBZY= +t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5/go.mod h1:VRvsWoBErPKvMieDMMk1hsh1tb9sA4ijEQWGw/TbtQ0= +t73f.de/r/sxwebs v0.0.0-20240814085618-5b4b5c496c94 h1:gLneaEyYotvcY/dDznzdcSXK1RqsJVi2AfeYDc1iVwM= +t73f.de/r/sxwebs v0.0.0-20240814085618-5b4b5c496c94/go.mod h1:83W3QFkmrniIKv6R+Xq+imvbSolhoutTnNhW0ErJoco= +t73f.de/r/webs v0.0.0-20240814085020-19dac746d568 h1:Pa+vO2r++qhcShv0p7t/gIrJ1DHPMn4gopEXLxDmoRg= +t73f.de/r/webs v0.0.0-20240814085020-19dac746d568/go.mod h1:NSoOON8be62MfQZzlCApK27Jt2zhIa6Vrmo9RJ4tOnQ= +t73f.de/r/zsc v0.0.0-20240826124629-97640fce4430 h1:35PQJZlo05a1rJHTreTQn6bBfTcII9UN2lMxr/7YUFk= +t73f.de/r/zsc v0.0.0-20240826124629-97640fce4430/go.mod h1:PWnU0AvNxVumQiQBMBr9GeGTaAv8ZD78voHaPIs0omI= Index: kernel/impl/cfg.go ================================================================== --- kernel/impl/cfg.go +++ kernel/impl/cfg.go @@ -182,11 +182,11 @@ cs.SwitchNextToCur() // Poor man's restart return nil } func (cs *configService) observe(ci box.UpdateInfo) { - if ci.Reason != box.OnZettel || ci.Zid == id.ConfigurationZid { + if (ci.Reason != box.OnZettel && ci.Reason != box.OnDelete) || ci.Zid == id.ConfigurationZid { cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe") go func() { cs.mxService.RLock() mgr := cs.manager cs.mxService.RUnlock() Index: kernel/impl/cmd.go ================================================================== --- kernel/impl/cmd.go +++ kernel/impl/cmd.go @@ -142,11 +142,11 @@ func(*cmdSession, string, []string) bool { return false }, }, "config": {"show configuration keys", cmdConfig}, "crlf": { "toggle crlf mode", - func(sess *cmdSession, cmd string, args []string) bool { + func(sess *cmdSession, _ string, _ []string) bool { if len(sess.eol) == 1 { sess.eol = []byte{'\r', '\n'} sess.println("crlf is on") } else { sess.eol = []byte{'\n'} @@ -157,11 +157,11 @@ }, "dump-index": {"writes the content of the index", cmdDumpIndex}, "dump-recover": {"show data of last recovery", cmdDumpRecover}, "echo": { "toggle echo mode", - func(sess *cmdSession, cmd string, args []string) bool { + func(sess *cmdSession, _ string, _ []string) bool { sess.echo = !sess.echo if sess.echo { sess.println("echo is on") } else { sess.println("echo is off") @@ -172,11 +172,11 @@ "end-profile": {"stop profiling", cmdEndProfile}, "env": {"show environment values", cmdEnvironment}, "get-config": {"show current configuration data", cmdGetConfig}, "header": { "toggle table header", - func(sess *cmdSession, cmd string, args []string) bool { + func(sess *cmdSession, _ string, _ []string) bool { sess.header = !sess.header if sess.header { sess.println("header are on") } else { sess.println("header are off") @@ -192,11 +192,11 @@ "restart": {"restart service", cmdRestart}, "services": {"show available services", cmdServices}, "set-config": {"set next configuration data", cmdSetConfig}, "shutdown": { "shutdown Zettelstore", - func(sess *cmdSession, cmd string, args []string) bool { sess.kern.Shutdown(false); return false }, + func(sess *cmdSession, _ string, _ []string) bool { sess.kern.Shutdown(false); return false }, }, "start": {"start service", cmdStart}, "stat": {"show service statistics", cmdStat}, "stop": {"stop service", cmdStop}, } @@ -339,17 +339,12 @@ } return true } func cmdStop(sess *cmdSession, cmd string, args []string) bool { - srvnum, ok := lookupService(sess, cmd, args) - if !ok { - return true - } - err := sess.kern.doStopService(srvnum) - if err != nil { - sess.println(err.Error()) + if srvnum, ok := lookupService(sess, cmd, args); ok { + sess.kern.doStopService(srvnum) } return true } func cmdStat(sess *cmdSession, cmd string, args []string) bool { Index: kernel/impl/config.go ================================================================== --- kernel/impl/config.go +++ kernel/impl/config.go @@ -231,23 +231,23 @@ } return true, nil } func parseInt64(val string) (any, error) { - if u64, err := strconv.ParseInt(val, 10, 64); err == nil { + u64, err := strconv.ParseInt(val, 10, 64) + if err == nil { return u64, nil - } else { - return nil, err } + return nil, err } func parseZid(val string) (any, error) { - if zid, err := id.Parse(val); err == nil { + zid, err := id.Parse(val) + if err == nil { return zid, nil - } else { - return id.Invalid, err } + return id.Invalid, err } func parseInvalidZid(val string) (any, error) { zid, _ := id.Parse(val) return zid, nil Index: kernel/impl/impl.go ================================================================== --- kernel/impl/impl.go +++ kernel/impl/impl.go @@ -444,21 +444,20 @@ srv.SwitchNextToCur() } return nil } -func (kern *myKernel) StopService(srvnum kernel.Service) error { +func (kern *myKernel) StopService(srvnum kernel.Service) { kern.mx.Lock() defer kern.mx.Unlock() - return kern.doStopService(srvnum) + kern.doStopService(srvnum) } -func (kern *myKernel) doStopService(srvnum kernel.Service) error { +func (kern *myKernel) doStopService(srvnum kernel.Service) { for _, srv := range kern.sortDependency(srvnum, kern.depStop, false) { srv.Stop(kern) } - return nil } func (kern *myKernel) sortDependency( srvnum kernel.Service, srvdeps serviceDependency, @@ -550,19 +549,19 @@ type kernelService struct { kernel *myKernel } -func (*kernelService) Initialize(*logger.Logger) {} -func (ks *kernelService) GetLogger() *logger.Logger { return ks.kernel.logger } -func (*kernelService) ConfigDescriptions() []serviceConfigDescription { return nil } -func (*kernelService) SetConfig(key, value string) error { return errAlreadyFrozen } -func (*kernelService) GetCurConfig(key string) interface{} { return nil } -func (*kernelService) GetNextConfig(key string) interface{} { return nil } -func (*kernelService) GetCurConfigList(all bool) []kernel.KeyDescrValue { return nil } -func (*kernelService) GetNextConfigList() []kernel.KeyDescrValue { return nil } -func (*kernelService) GetStatistics() []kernel.KeyValue { return nil } -func (*kernelService) Freeze() {} -func (*kernelService) Start(*myKernel) error { return nil } -func (*kernelService) SwitchNextToCur() {} -func (*kernelService) IsStarted() bool { return true } -func (*kernelService) Stop(*myKernel) {} +func (*kernelService) Initialize(*logger.Logger) {} +func (ks *kernelService) GetLogger() *logger.Logger { return ks.kernel.logger } +func (*kernelService) ConfigDescriptions() []serviceConfigDescription { return nil } +func (*kernelService) SetConfig(string, string) error { return errAlreadyFrozen } +func (*kernelService) GetCurConfig(string) interface{} { return nil } +func (*kernelService) GetNextConfig(string) interface{} { return nil } +func (*kernelService) GetCurConfigList(bool) []kernel.KeyDescrValue { return nil } +func (*kernelService) GetNextConfigList() []kernel.KeyDescrValue { return nil } +func (*kernelService) GetStatistics() []kernel.KeyValue { return nil } +func (*kernelService) Freeze() {} +func (*kernelService) Start(*myKernel) error { return nil } +func (*kernelService) SwitchNextToCur() {} +func (*kernelService) IsStarted() bool { return true } +func (*kernelService) Stop(*myKernel) {} Index: kernel/impl/web.go ================================================================== --- kernel/impl/web.go +++ kernel/impl/web.go @@ -45,15 +45,15 @@ ws.descr = descriptionMap{ kernel.WebAssetDir: { "Asset file directory", func(val string) (any, error) { val = filepath.Clean(val) - if finfo, err := os.Stat(val); err == nil && finfo.IsDir() { + finfo, err := os.Stat(val) + if err == nil && finfo.IsDir() { return val, nil - } else { - return nil, err } + return nil, err }, true, }, kernel.WebBaseURL: { "Base URL", @@ -80,10 +80,11 @@ return ap.String(), nil }, true}, kernel.WebMaxRequestSize: {"Max Request Size", parseInt64, true}, kernel.WebPersistentCookie: {"Persistent cookie", parseBool, true}, + kernel.WebProfiling: {"Runtime profiling", parseBool, true}, kernel.WebSecureCookie: {"Secure cookie", parseBool, true}, kernel.WebTokenLifetimeAPI: { "Token lifetime API", makeDurationParser(10*time.Minute, 0, 1*time.Hour), true, @@ -109,10 +110,11 @@ kernel.WebBaseURL: "http://127.0.0.1:23123/", kernel.WebListenAddress: "127.0.0.1:23123", kernel.WebMaxRequestSize: int64(16 * 1024 * 1024), kernel.WebPersistentCookie: false, kernel.WebSecureCookie: true, + kernel.WebProfiling: false, kernel.WebTokenLifetimeAPI: 1 * time.Hour, kernel.WebTokenLifetimeHTML: 10 * time.Minute, kernel.WebURLPrefix: "/", } } @@ -141,10 +143,11 @@ baseURL := ws.GetNextConfig(kernel.WebBaseURL).(string) listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string) urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string) persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool) secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool) + profile := ws.GetNextConfig(kernel.WebProfiling).(bool) maxRequestSize := ws.GetNextConfig(kernel.WebMaxRequestSize).(int64) if maxRequestSize < 1024 { maxRequestSize = 1024 } @@ -156,11 +159,23 @@ if lap := netip.MustParseAddrPort(listenAddr); !kern.auth.manager.WithAuth() && !lap.Addr().IsLoopback() { ws.logger.Info().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled") } - srvw := impl.New(ws.logger, listenAddr, baseURL, urlPrefix, persistentCookie, secureCookie, maxRequestSize, kern.auth.manager) + sd := impl.ServerData{ + Log: ws.logger, + ListenAddr: listenAddr, + BaseURL: baseURL, + URLPrefix: urlPrefix, + MaxRequestSize: maxRequestSize, + Auth: kern.auth.manager, + PersistentCookie: persistentCookie, + SecureCookie: secureCookie, + Profiling: profile, + ZidMapper: kern.box.manager.Mapper(), + } + srvw := impl.New(sd) err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg) if err != nil { ws.logger.Error().Err(err).Msg("Unable to create") return err } Index: kernel/kernel.go ================================================================== --- kernel/kernel.go +++ kernel/kernel.go @@ -92,11 +92,11 @@ // RestartService stops and restarts the given service, while maintaining service dependencies. RestartService(Service) error // StopService stop the given service. - StopService(Service) error + StopService(Service) // GetServiceStatistics returns a key/value list with statistical data. GetServiceStatistics(Service) []KeyValue // DumpIndex writes some data about the internal index into a writer. @@ -191,10 +191,11 @@ const ( WebAssetDir = "asset-dir" WebBaseURL = "base-url" WebListenAddress = "listen" WebPersistentCookie = "persistent" + WebProfiling = "profiling" WebMaxRequestSize = "max-request-size" WebSecureCookie = "secure" WebTokenLifetimeAPI = "api-lifetime" WebTokenLifetimeHTML = "html-lifetime" WebURLPrefix = "prefix" Index: parser/markdown/markdown.go ================================================================== --- parser/markdown/markdown.go +++ parser/markdown/markdown.go @@ -328,38 +328,45 @@ } return sb.String() } func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice { + var segBuf bytes.Buffer + for c := node.FirstChild(); c != nil; c = c.NextSibling() { + segment := c.(*gmAst.Text).Segment + segBuf.Write(segment.Value(p.source)) + } + content := segBuf.Bytes() + + // Clean code span + if len(content) == 0 { + content = nil + } else { + lastPos := 0 + var buf bytes.Buffer + for pos, ch := range content { + if ch == '\n' { + buf.Write(content[lastPos:pos]) + if pos < len(content)-1 { + buf.WriteByte(' ') + } + lastPos = pos + 1 + } + } + buf.Write(content[lastPos:]) + content = buf.Bytes() + } + return ast.InlineSlice{ &ast.LiteralNode{ Kind: ast.LiteralProg, Attrs: nil, //TODO - Content: cleanCodeSpan(node.Text(p.source)), + Content: content, }, } } -func cleanCodeSpan(text []byte) []byte { - if len(text) == 0 { - return nil - } - lastPos := 0 - var buf bytes.Buffer - for pos, ch := range text { - if ch == '\n' { - buf.Write(text[lastPos:pos]) - if pos < len(text)-1 { - buf.WriteByte(' ') - } - lastPos = pos + 1 - } - } - buf.Write(text[lastPos:]) - return buf.Bytes() -} - func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice { kind := ast.FormatEmph if node.Level == 2 { kind = ast.FormatStrong } Index: parser/plain/plain.go ================================================================== --- parser/plain/plain.go +++ parser/plain/plain.go @@ -14,11 +14,10 @@ // Package plain provides a parser for plain text data. package plain import ( "bytes" - "strings" "t73f.de/r/sx/sxreader" "t73f.de/r/zsc/attrs" "t73f.de/r/zsc/input" "zettelstore.de/z/ast" @@ -123,19 +122,21 @@ Syntax: syntax, }} } func scanSVG(inp *input.Input) string { - for input.IsSpace(inp.Ch) { - inp.Next() - } - svgSrc := string(inp.Src[inp.Pos:]) - if !strings.HasPrefix(svgSrc, " - return svgSrc + ch := inp.Ch + if input.IsSpace(ch) || input.IsEOLEOS(ch) || ch == '>' { + // TODO: check proper end + return string(inp.Src[pos:]) + } + return "" } func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { rd := sxreader.MakeReader(bytes.NewReader(inp.Src)) _, err := rd.ReadAll() ADDED parser/plain/plain_test.go Index: parser/plain/plain_test.go ================================================================== --- /dev/null +++ parser/plain/plain_test.go @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------------- +// 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 plain_test + +import ( + "testing" + + "t73f.de/r/zsc/input" + "zettelstore.de/z/encoder/szenc" + "zettelstore.de/z/parser" + "zettelstore.de/z/zettel/meta" +) + +func TestParseSVG(t *testing.T) { + testCases := []struct { + name string + src string + exp string + }{ + {"common", " ", "(INLINE (EMBED-BLOB () \"svg\" \"\"))"}, + {"error", " 7 { delims = 7 } hn = &ast.HeadingNode{Level: delims - 2, Inlines: nil} for { @@ -359,11 +360,11 @@ inp := cp.inp kinds := cp.parseNestedListKinds() if kinds == nil { return nil, false } - cp.skipSpace() + inp.SkipSpace() if kinds[len(kinds)-1] != ast.NestedListQuote && input.IsEOLEOS(inp.Ch) { return nil, false } if len(kinds) < len(cp.lists) { @@ -444,11 +445,11 @@ inp.Next() if inp.Ch != ' ' { return nil, false } inp.Next() - cp.skipSpace() + inp.SkipSpace() descrl := cp.descrl if descrl == nil { descrl = &ast.DescriptionListNode{} cp.descrl = descrl } @@ -478,11 +479,11 @@ inp.Next() if inp.Ch != ' ' { return nil, false } inp.Next() - cp.skipSpace() + inp.SkipSpace() descrl := cp.descrl if descrl == nil || len(descrl.Descriptions) == 0 { return nil, false } defPos := len(descrl.Descriptions) - 1 Index: parser/zettelmark/inline.go ================================================================== --- parser/zettelmark/inline.go +++ parser/zettelmark/inline.go @@ -159,11 +159,11 @@ } func (cp *zmkP) parseReference(openCh, closeCh rune) (ref string, is ast.InlineSlice, _ bool) { inp := cp.inp inp.Next() - cp.skipSpace() + inp.SkipSpace() if inp.Ch == openCh { // Additional opening chars result in a fail return "", nil, false } pos := inp.Pos @@ -192,11 +192,11 @@ } inp.SetPos(pos) } } - cp.skipSpace() + inp.SkipSpace() pos = inp.Pos if !cp.readReferenceToClose(closeCh) { return "", nil, false } ref = strings.TrimSpace(string(inp.Src[pos:inp.Pos])) @@ -311,13 +311,13 @@ attrs := cp.parseInlineAttributes() return &ast.FootnoteNode{Inlines: ins, Attrs: attrs}, true } func (cp *zmkP) parseLinkLikeRest() (ast.InlineSlice, bool) { - cp.skipSpace() - ins := ast.InlineSlice{} inp := cp.inp + inp.SkipSpace() + ins := ast.InlineSlice{} for inp.Ch != ']' { in := cp.parseInline() if in == nil { return nil, false } @@ -382,11 +382,11 @@ } for inp.Ch == '%' { inp.Next() } attrs := cp.parseInlineAttributes() - cp.skipSpace() + inp.SkipSpace() pos := inp.Pos for { if input.IsEOLEOS(inp.Ch) { return &ast.LiteralNode{ Kind: ast.LiteralComment, Index: parser/zettelmark/zettelmark.go ================================================================== --- parser/zettelmark/zettelmark.go +++ parser/zettelmark/zettelmark.go @@ -154,11 +154,11 @@ if pos < inp.Pos { return attrs.Attributes{"": string(inp.Src[pos:inp.Pos])} } // No immediate name: skip spaces - cp.skipSpace() + inp.SkipSpace() return cp.parseInlineAttributes() } func (cp *zmkP) parseInlineAttributes() attrs.Attributes { inp := cp.inp @@ -238,14 +238,8 @@ return } } } -func (cp *zmkP) skipSpace() { - for inp := cp.inp; inp.Ch == ' '; { - inp.Next() - } -} - func isNameRune(ch rune) bool { return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '-' || ch == '_' } Index: query/context.go ================================================================== --- query/context.go +++ query/context.go @@ -32,10 +32,11 @@ } // ContextDirection specifies the direction a context should be calculated. type ContextDirection uint8 +// Constants for ContextDirection. const ( ContextDirBoth ContextDirection = iota ContextDirForward ContextDirBackward ) @@ -44,10 +45,11 @@ type ContextPort interface { GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *Query) ([]*meta.Meta, error) } +// Print the spec on the given print environment. func (spec *ContextSpec) Print(pe *PrintEnv) { pe.printSpace() pe.writeString(api.ContextDirective) if spec.Full { pe.printSpace() @@ -63,10 +65,11 @@ } pe.printPosInt(api.CostDirective, spec.MaxCost) pe.printPosInt(api.MaxDirective, spec.MaxCount) } +// Execute the specification. func (spec *ContextSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ContextPort) []*meta.Meta { maxCost := float64(spec.MaxCost) if maxCost <= 0 { maxCost = 17 } Index: query/parser.go ================================================================== --- query/parser.go +++ query/parser.go @@ -43,18 +43,20 @@ inp *input.Input } func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS } func (ps *parserState) acceptSingleKw(s string) bool { - if ps.inp.Accept(s) && (ps.isSpace() || ps.isActionSep() || ps.mustStop()) { + inp := ps.inp + if inp.Accept(s) && (inp.IsSpace() || ps.isActionSep() || ps.mustStop()) { return true } return false } func (ps *parserState) acceptKwArgs(s string) bool { - if ps.inp.Accept(s) && ps.isSpace() { - ps.skipSpace() + inp := ps.inp + if inp.Accept(s) && inp.IsSpace() { + inp.SkipSpace() return true } return false } @@ -70,15 +72,15 @@ searchOperatorLessChar = '<' searchOperatorGreaterChar = '>' ) func (ps *parserState) parse(q *Query) *Query { - ps.skipSpace() + inp := ps.inp + inp.SkipSpace() if ps.mustStop() { return q } - inp := ps.inp firstPos := inp.Pos zidSet := id.NewSet() for { pos := inp.Pos zid, found := ps.scanZid() @@ -89,20 +91,20 @@ if !zidSet.Contains(zid) { zidSet.Add(zid) q = createIfNeeded(q) q.zids = append(q.zids, zid) } - ps.skipSpace() + inp.SkipSpace() if ps.mustStop() { q.zids = nil break } } hasContext := false for { - ps.skipSpace() + inp.SkipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.acceptSingleKw(api.ContextDirective) { @@ -139,11 +141,11 @@ inp.SetPos(firstPos) // No directive -> restart at beginning q.zids = nil } for { - ps.skipSpace() + inp.SkipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.acceptSingleKw(api.OrDirective) { @@ -201,11 +203,11 @@ func (ps *parserState) parseContext(q *Query) *Query { inp := ps.inp spec := &ContextSpec{} for { - ps.skipSpace() + inp.SkipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.acceptSingleKw(api.FullDirective) { @@ -266,11 +268,11 @@ func (ps *parserState) parseUnlinked(q *Query) *Query { inp := ps.inp spec := &UnlinkedSpec{} for { - ps.skipSpace() + inp.SkipSpace() if ps.mustStop() { break } pos := inp.Pos if ps.acceptKwArgs(api.PhraseDirective) { @@ -342,14 +344,15 @@ } return q, true } func (ps *parserState) parseActions(q *Query) *Query { - ps.inp.Next() + inp := ps.inp + inp.Next() var words []string for { - ps.skipSpace() + inp.SkipSpace() word := ps.scanWord() if len(word) == 0 { break } words = append(words, string(word)) @@ -373,11 +376,11 @@ if len(key) > 0 { // Assert: hasOp == false op, hasOp = ps.scanSearchOp() // Assert hasOp == true if op == cmpExist || op == cmpNotExist { - if ps.isSpace() || ps.isActionSep() || ps.mustStop() { + if inp.IsSpace() || ps.isActionSep() || ps.mustStop() { return q.addKey(string(key), op) } ps.inp.SetPos(pos) hasOp = false text = ps.scanWord() @@ -412,11 +415,11 @@ func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) { inp := ps.inp pos := inp.Pos allowKey := !hasOp - for !ps.isSpace() && !ps.isActionSep() && !ps.mustStop() { + for !inp.IsSpace() && !ps.isActionSep() && !ps.mustStop() { if allowKey { switch inp.Ch { case searchOperatorNotChar, existOperatorChar, searchOperatorEqualChar, searchOperatorHasChar, searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar, @@ -433,11 +436,11 @@ } func (ps *parserState) scanWord() []byte { inp := ps.inp pos := inp.Pos - for !ps.isSpace() && !ps.isActionSep() && !ps.mustStop() { + for !inp.IsSpace() && !ps.isActionSep() && !ps.mustStop() { inp.Next() } return inp.Src[pos:inp.Pos] } @@ -510,25 +513,8 @@ return op.negate(), true } return op, true } -func (ps *parserState) skipSpace() { - for ps.isSpace() { - ps.inp.Next() - } -} - -func (ps *parserState) isSpace() bool { - switch ch := ps.inp.Ch; ch { - case input.EOS: - return false - case ' ', '\t', '\n', '\r': - return true - default: - return input.IsSpace(ch) - } -} - func (ps *parserState) isActionSep() bool { return ps.inp.Ch == actionSeparatorChar } Index: query/print.go ================================================================== --- query/print.go +++ query/print.go @@ -147,10 +147,11 @@ pe.writeString(s) } } } +// Human returns the query as a human readable string. func (q *Query) Human() string { var sb strings.Builder q.PrintHuman(&sb) return sb.String() } Index: query/specs.go ================================================================== --- query/specs.go +++ query/specs.go @@ -16,17 +16,19 @@ import "t73f.de/r/zsc/api" // IdentSpec contains all specification values to calculate the ident directive. type IdentSpec struct{} +// Print the spec on the given print environment. func (spec *IdentSpec) Print(pe *PrintEnv) { pe.printSpace() pe.writeString(api.IdentDirective) } // ItemsSpec contains all specification values to calculate items. type ItemsSpec struct{} +// Print the spec on the given print environment. func (spec *ItemsSpec) Print(pe *PrintEnv) { pe.printSpace() pe.writeString(api.ItemsDirective) } Index: query/unlinked.go ================================================================== --- query/unlinked.go +++ query/unlinked.go @@ -22,18 +22,20 @@ // UnlinkedSpec contains all specification values to calculate unlinked references. type UnlinkedSpec struct { words []string } +// Print the spec on the given print environment. func (spec *UnlinkedSpec) Print(pe *PrintEnv) { pe.printSpace() pe.writeString(api.UnlinkedDirective) for _, word := range spec.words { pe.writeStrings(" ", api.PhraseDirective, " ", word) } } +// GetWords returns all title words of a given query result. func (spec *UnlinkedSpec) GetWords(metaSeq []*meta.Meta) []string { if words := spec.words; len(words) > 0 { result := make([]string, len(words)) copy(result, words) return result Index: tests/client/client_test.go ================================================================== --- tests/client/client_test.go +++ tests/client/client_test.go @@ -54,12 +54,12 @@ func TestListZettel(t *testing.T) { const ( ownerZettel = 60 configRoleZettel = 38 - writerZettel = ownerZettel - 28 - readerZettel = ownerZettel - 28 + writerZettel = ownerZettel - 25 + readerZettel = ownerZettel - 25 creatorZettel = 10 publicZettel = 5 ) testdata := []struct { Index: tests/client/crud_test.go ================================================================== --- tests/client/crud_test.go +++ tests/client/crud_test.go @@ -23,11 +23,11 @@ ) // --------------------------------------------------------------------------- // Tests that change the Zettelstore must nor run parallel to other tests. -func TestCreateGetRenameDeleteZettel(t *testing.T) { +func TestCreateGetDeleteZettel(t *testing.T) { // Is not to be allowed to run in parallel with other tests. zettel := `title: A Test Example content.` c := getClient() @@ -50,21 +50,15 @@ Example content.` if string(data) != exp { t.Errorf("Expected zettel data: %q, but got %q", exp, data) } - newZid := nextZid(zid) - err = c.RenameZettel(context.Background(), zid, newZid) - if err != nil { - t.Error("Cannot rename", zid, ":", err) - newZid = zid - } - - doDelete(t, c, newZid) + + doDelete(t, c, zid) } -func TestCreateGetRenameDeleteZettelData(t *testing.T) { +func TestCreateGetDeleteZettelDataCreator(t *testing.T) { // Is not to be allowed to run in parallel with other tests. c := getClient() c.SetAuth("creator", "creator") zid, err := c.CreateZettelData(context.Background(), api.ZettelData{ Meta: nil, @@ -77,20 +71,13 @@ } if !zid.IsValid() { t.Error("Invalid zettel ID", zid) return } - newZid := nextZid(zid) - c.SetAuth("owner", "owner") - err = c.RenameZettel(context.Background(), zid, newZid) - if err != nil { - t.Error("Cannot rename", zid, ":", err) - newZid = zid - } c.SetAuth("owner", "owner") - doDelete(t, c, newZid) + doDelete(t, c, zid) } func TestCreateGetDeleteZettelData(t *testing.T) { // Is not to be allowed to run in parallel with other tests. c := getClient() Index: tests/markdown_test.go ================================================================== --- tests/markdown_test.go +++ tests/markdown_test.go @@ -86,11 +86,11 @@ func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) { var sb strings.Builder testID := tc.Example*100 + 1 for _, enc := range encodings { - t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) { + t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(*testing.T) { encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}).WriteBlocks(&sb, ast) sb.Reset() }) } } Index: tools/build/build.go ================================================================== --- tools/build/build.go +++ tools/build/build.go @@ -247,13 +247,14 @@ env []string name string }{ {"amd64", "linux", nil, "zettelstore"}, {"arm", "linux", []string{"GOARM=6"}, "zettelstore"}, - {"amd64", "darwin", nil, "zettelstore"}, {"arm64", "darwin", nil, "zettelstore"}, + {"amd64", "darwin", nil, "zettelstore"}, {"amd64", "windows", nil, "zettelstore.exe"}, + {"arm64", "android", nil, "zettelstore"}, } for _, rel := range releases { env := append([]string{}, rel.env...) env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os) env = append(env, tools.EnvDirectProxy...) Index: tools/devtools/devtools.go ================================================================== --- tools/devtools/devtools.go +++ tools/devtools/devtools.go @@ -37,10 +37,11 @@ {"unparam", "mvdan.cc/unparam@latest"}, {"staticcheck", "honnef.co/go/tools/cmd/staticcheck@latest"}, {"govulncheck", "golang.org/x/vuln/cmd/govulncheck@latest"}, {"deadcode", "golang.org/x/tools/cmd/deadcode@latest"}, {"errcheck", "github.com/kisielk/errcheck@latest"}, + {"revive", "github.com/mgechev/revive@latest"}, } for _, tool := range tools { err := doGoInstall(tool.pack) if err != nil { return err Index: tools/htmllint/htmllint.go ================================================================== --- tools/htmllint/htmllint.go +++ tools/htmllint/htmllint.go @@ -9,10 +9,11 @@ // // SPDX-License-Identifier: EUPL-1.2 // SPDX-FileCopyrightText: 2023-present Detlef Stern //----------------------------------------------------------------------------- +// Package main provides a tool to check the validity of HTML zettel documents. package main import ( "context" "flag" @@ -59,11 +60,11 @@ for _, zid := range zidsToUse(zids, perm, kd.sampleSize) { var nmsgs int nmsgs, err = validateHTML(client, kd.uc, api.ZettelID(zid)) if err != nil { fmt.Fprintf(os.Stderr, "* error while validating zettel %v with: %v\n", zid, err) - msgCount += 1 + msgCount++ } else { msgCount += nmsgs } } if msgCount == 1 { @@ -107,11 +108,10 @@ {getHTMLZettel, "zettel HTML encoding", -1}, {createJustKey('h'), "zettel web view", -1}, {createJustKey('i'), "zettel info view", -1}, {createJustKey('e'), "zettel edit form", 100}, {createJustKey('c'), "zettel create form", 10}, - {createJustKey('b'), "zettel rename form", 100}, {createJustKey('d'), "zettel delete dialog", 200}, } type urlCreator func(*client.Client, api.ZettelID) *api.URLBuilder Index: tools/tools.go ================================================================== --- tools/tools.go +++ tools/tools.go @@ -24,30 +24,38 @@ "strings" "zettelstore.de/z/strfun" ) -var EnvDirectProxy = []string{"GOPROXY=direct"} -var EnvGoVCS = []string{"GOVCS=zettelstore.de:fossil,t73f.de:fossil"} +// Some constants to make Go work with fossil. +var ( + EnvDirectProxy = []string{"GOPROXY=direct"} + EnvGoVCS = []string{"GOVCS=zettelstore.de:fossil,t73f.de:fossil"} +) + +// Verbose signals a verbose tool execution. var Verbose bool +// ExecuteCommand executes a specific command. func ExecuteCommand(env []string, name string, arg ...string) (string, error) { LogCommand("EXEC", env, name, arg) var out strings.Builder cmd := PrepareCommand(env, name, arg, nil, &out, os.Stderr) err := cmd.Run() return out.String(), err } +// ExecuteFilter executes an external program to be used as a filter. func ExecuteFilter(data []byte, env []string, name string, arg ...string) (string, string, error) { LogCommand("EXEC", env, name, arg) var stdout, stderr strings.Builder cmd := PrepareCommand(env, name, arg, bytes.NewReader(data), &stdout, &stderr) err := cmd.Run() return stdout.String(), stderr.String(), err } +// PrepareCommand creates a commands to be executed. func PrepareCommand(env []string, name string, arg []string, in io.Reader, stdout, stderr io.Writer) *exec.Cmd { if len(env) > 0 { env = append(env, os.Environ()...) } cmd := exec.Command(name, arg...) @@ -55,10 +63,12 @@ cmd.Stdin = in cmd.Stdout = stdout cmd.Stderr = stderr return cmd } + +// LogCommand logs the execution of a command. func LogCommand(exec string, env []string, name string, arg []string) { if Verbose { if len(env) > 0 { for i, e := range env { fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e) @@ -66,10 +76,11 @@ } fmt.Fprintln(os.Stderr, exec, name, arg) } } +// Check the source with some linters. func Check(forRelease bool) error { if err := CheckGoTest("./..."); err != nil { return err } if err := checkGoVet(); err != nil { @@ -81,19 +92,23 @@ if err := checkStaticcheck(); err != nil { return err } if err := checkUnparam(forRelease); err != nil { return err + } + if err := checkRevive(); err != nil { + return err } if forRelease { if err := checkGoVulncheck(); err != nil { return err } } return checkFossilExtra() } +// CheckGoTest runs all internal unti tests. func CheckGoTest(pkg string, testParams ...string) error { var env []string env = append(env, EnvDirectProxy...) env = append(env, EnvGoVCS...) args := []string{"test", pkg} @@ -140,10 +155,21 @@ if err != nil { fmt.Fprintln(os.Stderr, "Some staticcheck problems found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } + } + return err +} + +func checkRevive() error { + out, err := ExecuteCommand(EnvGoVCS, "revive", "./...") + if err != nil || out != "" { + fmt.Fprintln(os.Stderr, "Some revive problems found") + if len(out) > 0 { + fmt.Fprintln(os.Stderr, out) + } } return err } func checkUnparam(forRelease bool) error { DELETED usecase/rename_zettel.go Index: usecase/rename_zettel.go ================================================================== --- usecase/rename_zettel.go +++ /dev/null @@ -1,65 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-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: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package usecase - -import ( - "context" - - "zettelstore.de/z/box" - "zettelstore.de/z/logger" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" -) - -// RenameZettelPort is the interface used by this use case. -type RenameZettelPort interface { - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) - RenameZettel(ctx context.Context, curZid, newZid id.Zid) error -} - -// RenameZettel is the data for this use case. -type RenameZettel struct { - log *logger.Logger - port RenameZettelPort -} - -// ErrZidInUse is returned if the zettel id is not appropriate for the box operation. -type ErrZidInUse struct{ Zid id.Zid } - -func (err ErrZidInUse) Error() string { - return "Zettel id already in use: " + err.Zid.String() -} - -// NewRenameZettel creates a new use case. -func NewRenameZettel(log *logger.Logger, port RenameZettelPort) RenameZettel { - return RenameZettel{log: log, port: port} -} - -// Run executes the use case. -func (uc *RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error { - noEnrichCtx := box.NoEnrichContext(ctx) - if _, err := uc.port.GetZettel(noEnrichCtx, curZid); err != nil { - return err - } - if newZid == curZid { - // Nothing to do - return nil - } - if _, err := uc.port.GetZettel(noEnrichCtx, newZid); err == nil { - return ErrZidInUse{Zid: newZid} - } - err := uc.port.RenameZettel(ctx, curZid, newZid) - uc.log.Info().User(ctx).Zid(curZid).Err(err).Zid(newZid).Msg("Rename zettel") - return err -} Index: web/adapter/api/api.go ================================================================== --- web/adapter/api/api.go +++ web/adapter/api/api.go @@ -94,13 +94,10 @@ result |= api.ZettelCanRead } if pol.CanWrite(user, m, m) { result |= api.ZettelCanWrite } - if pol.CanRename(user, m) { - result |= api.ZettelCanRename - } if pol.CanDelete(user, m) { result |= api.ZettelCanDelete } if result == 0 { return api.ZettelCanNone Index: web/adapter/api/command.go ================================================================== --- web/adapter/api/command.go +++ web/adapter/api/command.go @@ -23,12 +23,12 @@ // MakePostCommandHandler creates a new HTTP handler to execute certain commands. func (a *API) MakePostCommandHandler( ucIsAuth *usecase.IsAuthenticated, ucRefresh *usecase.Refresh, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() switch api.Command(r.URL.Query().Get(api.QueryKeyCommand)) { case api.CommandAuthenticated: handleIsAuthenticated(ctx, w, ucIsAuth) return @@ -40,11 +40,11 @@ } w.WriteHeader(http.StatusNoContent) return } http.Error(w, "Unknown command", http.StatusBadRequest) - } + }) } func handleIsAuthenticated(ctx context.Context, w http.ResponseWriter, ucIsAuth *usecase.IsAuthenticated) { switch ucIsAuth.Run(ctx) { case usecase.IsAuthenticatedDisabled: Index: web/adapter/api/create_zettel.go ================================================================== --- web/adapter/api/create_zettel.go +++ web/adapter/api/create_zettel.go @@ -25,12 +25,12 @@ "zettelstore.de/z/zettel/id" ) // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. -func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() enc, encStr := getEncoding(r, q) var zettel zettel.Zettel var err error switch enc { @@ -72,7 +72,7 @@ h.Set(api.HeaderLocation, location.String()) w.WriteHeader(http.StatusCreated) if _, err = w.Write(result); err != nil { a.log.Error().Err(err).Zid(newZid).Msg("Create Zettel") } - } + }) } Index: web/adapter/api/delete_zettel.go ================================================================== --- web/adapter/api/delete_zettel.go +++ web/adapter/api/delete_zettel.go @@ -19,12 +19,12 @@ "zettelstore.de/z/usecase" "zettelstore.de/z/zettel/id" ) // MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel. -func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } @@ -32,7 +32,7 @@ if err = deleteZettel.Run(r.Context(), zid); err != nil { a.reportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) - } + }) } Index: web/adapter/api/get_data.go ================================================================== --- web/adapter/api/get_data.go +++ web/adapter/api/get_data.go @@ -20,12 +20,12 @@ "zettelstore.de/z/usecase" "zettelstore.de/z/zettel/id" ) // MakeGetDataHandler creates a new HTTP handler to return zettelstore data. -func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { version := ucVersion.Run() err := a.writeObject(w, id.Invalid, sx.MakeList( sx.Int64(version.Major), sx.Int64(version.Minor), sx.Int64(version.Patch), @@ -33,7 +33,7 @@ sx.MakeString(version.Hash), )) if err != nil { a.log.Error().Err(err).Msg("Write Version Info") } - } + }) } Index: web/adapter/api/get_zettel.go ================================================================== --- web/adapter/api/get_zettel.go +++ web/adapter/api/get_zettel.go @@ -32,12 +32,16 @@ "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // MakeGetZettelHandler creates a new HTTP handler to return a zettel in various encodings. -func (a *API) MakeGetZettelHandler(getZettel usecase.GetZettel, parseZettel usecase.ParseZettel, evaluate usecase.Evaluate) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (a *API) MakeGetZettelHandler( + getZettel usecase.GetZettel, + parseZettel usecase.ParseZettel, + evaluate usecase.Evaluate, +) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } @@ -45,14 +49,14 @@ q := r.URL.Query() part := getPart(q, partContent) ctx := r.Context() switch enc, encStr := getEncoding(r, q); enc { case api.EncoderPlain: - a.writePlainData(w, ctx, zid, part, getZettel) + a.writePlainData(ctx, w, zid, part, getZettel) case api.EncoderData: - a.writeSzData(w, ctx, zid, part, getZettel) + a.writeSzData(ctx, w, zid, part, getZettel) default: var zn *ast.ZettelNode var em func(value string) ast.InlineSlice if q.Has(api.QueryKeyParseOnly) { @@ -68,14 +72,14 @@ a.reportUsecaseError(w, err) return } a.writeEncodedZettelPart(ctx, w, zn, em, enc, encStr, part) } - } + }) } -func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) { +func (a *API) writePlainData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) { var buf bytes.Buffer var contentType string var err error z, err := getZettel.Run(box.NoEnrichContext(ctx), zid) @@ -111,11 +115,11 @@ if err = writeBuffer(w, &buf, contentType); err != nil { a.log.Error().Err(err).Zid(zid).Msg("Write Plain data") } } -func (a *API) writeSzData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) { +func (a *API) writeSzData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) { z, err := getZettel.Run(ctx, zid) if err != nil { a.reportUsecaseError(w, err) return } Index: web/adapter/api/login.go ================================================================== --- web/adapter/api/login.go +++ web/adapter/api/login.go @@ -23,12 +23,12 @@ "zettelstore.de/z/web/adapter" "zettelstore.de/z/zettel/id" ) // MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. -func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !a.withAuth() { if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil { a.log.Error().Err(err).Msg("Login/free") } return @@ -49,11 +49,11 @@ } if err := a.writeToken(w, string(token), a.tokenLifetime); err != nil { a.log.Error().Err(err).Msg("Login") } - } + }) } func retrieveIdentCred(r *http.Request) (string, string) { if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok { return ident, cred @@ -63,12 +63,12 @@ } return "", "" } // MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user. -func (a *API) MakeRenewAuthHandler() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (a *API) MakeRenewAuthHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if !a.withAuth() { if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil { a.log.Error().Err(err).Msg("Refresh/free") } @@ -96,15 +96,15 @@ return } if err = a.writeToken(w, string(token), a.tokenLifetime); err != nil { a.log.Error().Err(err).Msg("Write renewed token") } - } + }) } func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error { return a.writeObject(w, id.Invalid, sx.MakeList( sx.MakeString("Bearer"), sx.MakeString(token), sx.Int64(int64(lifetime/time.Second)), )) } Index: web/adapter/api/query.go ================================================================== --- web/adapter/api/query.go +++ web/adapter/api/query.go @@ -32,12 +32,17 @@ "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // MakeQueryHandler creates a new HTTP handler to perform a query. -func (a *API) MakeQueryHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (a *API) MakeQueryHandler( + queryMeta *usecase.Query, + tagZettel *usecase.TagZettel, + roleZettel *usecase.RoleZettel, + reIndex *usecase.ReIndex, +) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() urlQuery := r.URL.Query() if a.handleTagZettel(w, r, tagZettel, urlQuery) || a.handleRoleZettel(w, r, roleZettel, urlQuery) { return } @@ -94,26 +99,26 @@ } if err = writeBuffer(w, &buf, contentType); err != nil { a.log.Error().Err(err).Msg("write result buffer") } - } + }) } func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, actions []string) error { - min, max := -1, -1 + minVal, maxVal := -1, -1 if len(actions) > 0 { acts := make([]string, 0, len(actions)) for _, act := range actions { if strings.HasPrefix(act, api.MinAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { - min = num + minVal = num continue } } if strings.HasPrefix(act, api.MaxAction) { if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 { - max = num + maxVal = num continue } } acts = append(acts, act) } @@ -121,11 +126,11 @@ if act == api.KeysAction { return encodeKeysArrangement(w, enc, ml, act) } switch key := strings.ToLower(act); meta.Type(key) { case meta.TypeWord, meta.TypeTagSet: - return encodeMetaKeyArrangement(w, enc, ml, key, min, max) + return encodeMetaKeyArrangement(w, enc, ml, key, minVal, maxVal) } } } return enc.writeMetaList(w, ml) } @@ -138,15 +143,15 @@ } } return enc.writeArrangement(w, act, arr) } -func encodeMetaKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, min, max int) error { +func encodeMetaKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, minVal, maxVal int) error { arr0 := meta.CreateArrangement(ml, key) arr := make(meta.Arrangement, len(arr0)) for k0, ml0 := range arr0 { - if len(ml0) < min || (max > 0 && len(ml0) > max) { + if len(ml0) < minVal || (maxVal > 0 && len(ml0) > maxVal) { continue } arr[k0] = ml0 } return enc.writeArrangement(w, key, arr) DELETED web/adapter/api/rename_zettel.go Index: web/adapter/api/rename_zettel.go ================================================================== --- web/adapter/api/rename_zettel.go +++ /dev/null @@ -1,70 +0,0 @@ -//----------------------------------------------------------------------------- -// 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 api - -import ( - "net/http" - "net/url" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/usecase" - "zettelstore.de/z/zettel/id" -) - -// MakeRenameZettelHandler creates a new HTTP handler to update a zettel. -func (a *API) MakeRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - zid, err := id.Parse(r.URL.Path[1:]) - if err != nil { - http.NotFound(w, r) - return - } - newZid, found := getDestinationZid(r) - if !found { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - if err = renameZettel.Run(r.Context(), zid, newZid); err != nil { - a.reportUsecaseError(w, err) - return - } - w.WriteHeader(http.StatusNoContent) - } -} - -func getDestinationZid(r *http.Request) (id.Zid, bool) { - if values, ok := r.Header[api.HeaderDestination]; ok { - for _, value := range values { - if zid, ok2 := getZidFromURL(value); ok2 { - return zid, true - } - } - } - return id.Invalid, false -} - -func getZidFromURL(val string) (id.Zid, bool) { - u, err := url.Parse(val) - if err != nil { - return id.Invalid, false - } - if len(u.Path) < len(api.ZidVersion) { - return id.Invalid, false - } - zid, err := id.Parse(u.Path[len(u.Path)-len(api.ZidVersion):]) - if err != nil { - return id.Invalid, false - } - return zid, true -} Index: web/adapter/api/update_zettel.go ================================================================== --- web/adapter/api/update_zettel.go +++ web/adapter/api/update_zettel.go @@ -22,12 +22,12 @@ "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" ) // MakeUpdateZettelHandler creates a new HTTP handler to update a zettel. -func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return } @@ -51,7 +51,7 @@ if err = updateZettel.Run(r.Context(), zettel, true); err != nil { a.reportUsecaseError(w, err) return } w.WriteHeader(http.StatusNoContent) - } + }) } Index: web/adapter/response.go ================================================================== --- web/adapter/response.go +++ web/adapter/response.go @@ -68,14 +68,10 @@ } var eiz box.ErrInvalidZid if errors.As(err, &eiz) { return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", eiz.Zid) } - var ezin usecase.ErrZidInUse - if errors.As(err, &ezin) { - return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", ezin.Zid) - } var etznf usecase.ErrTagZettelNotFound if errors.As(err, &etznf) { return http.StatusNotFound, "Tag zettel not found: " + etznf.Tag } var erznf usecase.ErrRoleZettelNotFound Index: web/adapter/webui/create_zettel.go ================================================================== --- web/adapter/webui/create_zettel.go +++ web/adapter/webui/create_zettel.go @@ -34,13 +34,16 @@ ) // MakeGetCreateZettelHandler creates a new HTTP handler to display the // HTML edit view for the various zettel creation methods. func (wui *WebUI) MakeGetCreateZettelHandler( - getZettel usecase.GetZettel, createZettel *usecase.CreateZettel, - ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { + getZettel usecase.GetZettel, + createZettel *usecase.CreateZettel, + ucListRoles usecase.ListRoles, + ucListSyntax usecase.ListSyntax, +) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() op := getCreateAction(q.Get(queryKeyAction)) path := r.URL.Path[1:] zid, err := id.Parse(path) @@ -67,11 +70,11 @@ newTitle := parser.NormalizedSpacedText(q.Get(api.KeyTitle)) wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData) case actionVersion: wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData) } - } + }) } func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) { roleData := dataListFromArrangement(ucListRoles.Run(ctx)) syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx)) @@ -124,12 +127,12 @@ } } // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. -func (wui *WebUI) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (wui *WebUI) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() reEdit, zettel, err := parseZettelForm(r, id.Invalid) if err == errMissingContent { wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing")) return @@ -149,20 +152,22 @@ if reEdit { wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(newZid.ZettelID())) } else { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID())) } - } + }) } // MakeGetZettelFromListHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakeGetZettelFromListHandler( - queryMeta *usecase.Query, evaluate *usecase.Evaluate, - ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc { - - return func(w http.ResponseWriter, r *http.Request) { + queryMeta *usecase.Query, + evaluate *usecase.Evaluate, + ucListRoles usecase.ListRoles, + ucListSyntax usecase.ListSyntax, +) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { q := adapter.GetQuery(r.URL.Query()) ctx := r.Context() metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q) if err != nil { wui.reportError(ctx, w, err) @@ -185,7 +190,7 @@ m.Set(api.KeyQuery, qval) } zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(zmkContent.Bytes())} roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) wui.renderZettelForm(ctx, w, zettel, "Zettel from list", wui.createNewURL, roleData, syntaxData) - } + }) } Index: web/adapter/webui/delete_zettel.go ================================================================== --- web/adapter/webui/delete_zettel.go +++ web/adapter/webui/delete_zettel.go @@ -27,12 +27,15 @@ "zettelstore.de/z/zettel/meta" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. -func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel, getAllZettel usecase.GetAllZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (wui *WebUI) MakeGetDeleteZettelHandler( + getZettel usecase.GetZettel, + getAllZettel usecase.GetAllZettel, +) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) @@ -65,11 +68,11 @@ err = rb.err } if err != nil { wui.reportError(ctx, w, err) } - } + }) } func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) *sx.Pair { zidMap := make(strfun.Set) addListValues(zidMap, m, api.KeyBackward) @@ -98,12 +101,12 @@ } } } // MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel. -func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) @@ -113,7 +116,7 @@ if err = deleteZettel.Run(r.Context(), zid); err != nil { wui.reportError(ctx, w, err) return } wui.redirectFound(w, r, wui.NewURLBuilder('/')) - } + }) } Index: web/adapter/webui/edit_zettel.go ================================================================== --- web/adapter/webui/edit_zettel.go +++ web/adapter/webui/edit_zettel.go @@ -22,12 +22,16 @@ "zettelstore.de/z/zettel/id" ) // MakeEditGetZettelHandler creates a new HTTP handler to display the // HTML edit view of a zettel. -func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (wui *WebUI) MakeEditGetZettelHandler( + getZettel usecase.GetZettel, + ucListRoles usecase.ListRoles, + ucListSyntax usecase.ListSyntax, +) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) @@ -40,17 +44,17 @@ return } roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "", roleData, syntaxData) - } + }) } // MakeEditSetZettelHandler creates a new HTTP handler to store content of // an existing zettel. -func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) @@ -76,7 +80,7 @@ if reEdit { wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(zid.ZettelID())) } else { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid.ZettelID())) } - } + }) } Index: web/adapter/webui/favicon.go ================================================================== --- web/adapter/webui/favicon.go +++ web/adapter/webui/favicon.go @@ -20,12 +20,13 @@ "path/filepath" "zettelstore.de/z/web/adapter" ) -func (wui *WebUI) MakeFaviconHandler(baseDir string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +// MakeFaviconHandler creates a HTTP handler to retrieve the favicon. +func (wui *WebUI) MakeFaviconHandler(baseDir string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { filename := filepath.Join(baseDir, "favicon.ico") f, err := os.Open(filename) if err != nil { wui.log.Debug().Err(err).Msg("Favicon not found") http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) @@ -41,7 +42,7 @@ } if err = adapter.WriteData(w, data, ""); err != nil { wui.log.Error().Err(err).Msg("Write favicon") } - } + }) } Index: web/adapter/webui/get_info.go ================================================================== --- web/adapter/webui/get_info.go +++ web/adapter/webui/get_info.go @@ -39,12 +39,12 @@ ucParseZettel usecase.ParseZettel, ucEvaluate *usecase.Evaluate, ucGetZettel usecase.GetZettel, ucGetAllZettel usecase.GetAllZettel, ucQuery *usecase.Query, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() path := r.URL.Path[1:] zid, err := id.Parse(path) @@ -115,11 +115,11 @@ err = rb.err } if err != nil { wui.reportError(ctx, w, err) } - } + }) } func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sx.Pair) { for i := len(links) - 1; i >= 0; i-- { ref := links[i] Index: web/adapter/webui/get_zettel.go ================================================================== --- web/adapter/webui/get_zettel.go +++ web/adapter/webui/get_zettel.go @@ -29,12 +29,15 @@ "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". -func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getZettel usecase.GetZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (wui *WebUI) MakeGetHTMLZettelHandler( + evaluate *usecase.Evaluate, + getZettel usecase.GetZettel, +) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() path := r.URL.Path[1:] zid, err := id.Parse(path) if err != nil { wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) @@ -92,11 +95,11 @@ err = rb.err } if err != nil { wui.reportError(ctx, w, err) } - } + }) } func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair { if values, ok := m.GetList(key); ok { return wui.transformIdentifierSet(values, getTextTitle) Index: web/adapter/webui/goaction.go ================================================================== --- web/adapter/webui/goaction.go +++ web/adapter/webui/goaction.go @@ -18,18 +18,18 @@ "zettelstore.de/z/usecase" ) // MakeGetGoActionHandler creates a new HTTP handler to execute certain commands. -func (wui *WebUI) MakeGetGoActionHandler(ucRefresh *usecase.Refresh) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (wui *WebUI) MakeGetGoActionHandler(ucRefresh *usecase.Refresh) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Currently, command "refresh" is the only command to be executed. err := ucRefresh.Run(ctx) if err != nil { wui.reportError(ctx, w, err) return } wui.redirectFound(w, r, wui.NewURLBuilder('/')) - } + }) } Index: web/adapter/webui/home.go ================================================================== --- web/adapter/webui/home.go +++ web/adapter/webui/home.go @@ -24,17 +24,17 @@ "zettelstore.de/z/web/server" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" ) -type getRootStore interface { +type getRootPort interface { GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) } // MakeGetRootHandler creates a new HTTP handler to show the root URL. -func (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (wui *WebUI) MakeGetRootHandler(s getRootPort) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if p := r.URL.Path; p != "/" { wui.reportError(ctx, w, adapter.ErrResourceNotFound{Path: p}) return } @@ -55,7 +55,7 @@ if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil { wui.redirectFound(w, r, wui.NewURLBuilder('i')) return } wui.redirectFound(w, r, wui.NewURLBuilder('h')) - } + }) } Index: web/adapter/webui/htmlgen.go ================================================================== --- web/adapter/webui/htmlgen.go +++ web/adapter/webui/htmlgen.go @@ -129,18 +129,18 @@ u := builder.NewURLBuilder('h').AppendQuery(q) assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String()))) return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) }) rebind(th, sz.SymLinkExternal, func(obj sx.Object) sx.Object { - attr, assoc, rest := findA(obj) + attr, _, rest := findA(obj) if attr == nil { return obj } - assoc = assoc.Cons(sx.Cons(shtml.SymAttrClass, sx.MakeString("external"))). - Cons(sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank"))). - Cons(sx.Cons(shtml.SymAttrRel, sx.MakeString("noopener noreferrer"))) - return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA) + a := sz.GetAttributes(attr) + a = a.Set("target", "_blank") + a = a.Add("rel", "external").Add("rel", "noreferrer") + return rest.Cons(shtml.EvaluateAttrbute(a)).Cons(shtml.SymA) }) rebind(th, sz.SymEmbed, func(obj sx.Object) sx.Object { pair, isPair := sx.GetPair(obj) if !isPair || !shtml.SymIMG.IsEqual(pair.Car()) { return obj @@ -273,11 +273,11 @@ env := shtml.MakeEnvironment(g.lang) sh, err := g.th.Evaluate(sx, &env) if err != nil { return nil, nil, err } - return sh, g.th.Endnotes(&env), nil + return sh, shtml.Endnotes(&env), nil } // InlinesSxHTML returns an inline slice, encoded as a SxHTML object. func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair { if is == nil || len(*is) == 0 { Index: web/adapter/webui/lists.go ================================================================== --- web/adapter/webui/lists.go +++ web/adapter/webui/lists.go @@ -38,12 +38,16 @@ "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML. -func (wui *WebUI) MakeListHTMLMetaHandler(queryMeta *usecase.Query, tagZettel *usecase.TagZettel, roleZettel *usecase.RoleZettel, reIndex *usecase.ReIndex) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (wui *WebUI) MakeListHTMLMetaHandler( + queryMeta *usecase.Query, + tagZettel *usecase.TagZettel, + roleZettel *usecase.RoleZettel, + reIndex *usecase.ReIndex) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { urlQuery := r.URL.Query() if wui.handleTagZettel(w, r, tagZettel, urlQuery) || wui.handleRoleZettel(w, r, roleZettel, urlQuery) { return } @@ -148,11 +152,11 @@ err = rb.err } if err != nil { wui.reportError(ctx, w, err) } - } + }) } func (wui *WebUI) transformTagZettelList(ctx context.Context, tagZettel *usecase.TagZettel, tags []string) (withZettel, withoutZettel *sx.Pair) { slices.Reverse(tags) for _, tag := range tags { Index: web/adapter/webui/login.go ================================================================== --- web/adapter/webui/login.go +++ web/adapter/webui/login.go @@ -25,20 +25,20 @@ "zettelstore.de/z/zettel/id" ) // MakeGetLoginOutHandler creates a new HTTP handler to display the HTML login view, // or to execute a logout. -func (wui *WebUI) MakeGetLoginOutHandler() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (wui *WebUI) MakeGetLoginOutHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() if query.Has("logout") { wui.clearToken(r.Context(), w) wui.redirectFound(w, r, wui.NewURLBuilder('/')) return } wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false) - } + }) } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { env, rb := wui.createRenderEnv(ctx, "login", wui.rtConfig.Get(ctx, nil, api.KeyLang), "Login", nil) rb.bindString("retry", sx.MakeBoolean(retry)) @@ -49,12 +49,12 @@ wui.reportError(ctx, w, err) } } // MakePostLoginHandler creates a new HTTP handler to authenticate the given user. -func (wui *WebUI) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (wui *WebUI) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !wui.authz.WithAuth() { wui.redirectFound(w, r, wui.NewURLBuilder('/')) return } ctx := r.Context() @@ -73,7 +73,7 @@ return } wui.setToken(w, token) wui.redirectFound(w, r, wui.NewURLBuilder('/')) - } + }) } DELETED web/adapter/webui/rename_zettel.go Index: web/adapter/webui/rename_zettel.go ================================================================== --- web/adapter/webui/rename_zettel.go +++ /dev/null @@ -1,105 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-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: 2020-present Detlef Stern -//----------------------------------------------------------------------------- - -package webui - -import ( - "fmt" - "net/http" - "strings" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/box" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" - "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel/id" -) - -// MakeGetRenameZettelHandler creates a new HTTP handler to display the -// HTML rename view of a zettel. -func (wui *WebUI) MakeGetRenameZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - path := r.URL.Path[1:] - zid, err := id.Parse(path) - if err != nil { - wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) - return - } - - z, err := getZettel.Run(ctx, zid) - if err != nil { - wui.reportError(ctx, w, err) - return - } - m := z.Meta - - user := server.GetUser(ctx) - env, rb := wui.createRenderEnv( - ctx, "rename", - wui.rtConfig.Get(ctx, nil, api.KeyLang), "Rename Zettel "+m.Zid.String(), user) - rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel))) - wui.bindCommonZettelData(ctx, &rb, user, m, nil) - if rb.err == nil { - err = wui.renderSxnTemplate(ctx, w, id.RenameTemplateZid, env) - } else { - err = rb.err - } - if err != nil { - wui.reportError(ctx, w, err) - } - } -} - -// MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel. -func (wui *WebUI) MakePostRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - path := r.URL.Path[1:] - curZid, err := id.Parse(path) - if err != nil { - wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path}) - return - } - - if err = r.ParseForm(); err != nil { - wui.log.Trace().Err(err).Msg("unable to read rename zettel form") - wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form")) - return - } - formCurZidStr := r.PostFormValue("curzid") - if formCurZid, err1 := id.Parse(formCurZidStr); err1 != nil || formCurZid != curZid { - if err1 != nil { - wui.log.Trace().Str("formCurzid", formCurZidStr).Err(err1).Msg("unable to parse as zid") - } else if formCurZid != curZid { - wui.log.Trace().Zid(formCurZid).Zid(curZid).Msg("zid differ (form/url)") - } - wui.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form")) - return - } - formNewZid := strings.TrimSpace(r.PostFormValue("newzid")) - newZid, err := id.Parse(formNewZid) - if err != nil { - wui.reportError( - ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", formNewZid))) - return - } - - if err = renameZettel.Run(r.Context(), curZid, newZid); err != nil { - wui.reportError(ctx, w, err) - return - } - wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID())) - } -} Index: web/adapter/webui/template.go ================================================================== --- web/adapter/webui/template.go +++ web/adapter/webui/template.go @@ -138,11 +138,11 @@ shtml.SymA, sx.MakeList( sxhtml.SymAttr, sx.Cons(shtml.SymAttrHref, sx.MakeString(us)), sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank")), - sx.Cons(shtml.SymAttrRel, sx.MakeString("noopener noreferrer")), + sx.Cons(shtml.SymAttrRel, sx.MakeString("external noreferrer")), ), text) } } return text @@ -241,10 +241,11 @@ strZid := m.Zid.String() apiZid := api.ZettelID(strZid) newURLBuilder := wui.NewURLBuilder rb.bindString("zid", sx.MakeString(strZid)) + rb.bindString("zid-n", sx.MakeString(m.ZidN.String())) rb.bindString("web-url", sx.MakeString(newURLBuilder('h').SetZid(apiZid).String())) if content != nil && wui.canWrite(ctx, user, m, *content) { rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(apiZid).String())) } rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(apiZid).String())) @@ -254,13 +255,10 @@ } rb.bindString("version-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String())) rb.bindString("child-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionChild).String())) rb.bindString("folge-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String())) } - if wui.canRename(ctx, user, m) { - rb.bindString("rename-url", sx.MakeString(newURLBuilder('b').SetZid(apiZid).String())) - } if wui.canDelete(ctx, user, m) { rb.bindString("delete-url", sx.MakeString(newURLBuilder('d').SetZid(apiZid).String())) } if val, found := m.Get(api.KeyUselessFiles); found { rb.bindString("useless", sx.Cons(sx.MakeString(val), nil)) Index: web/adapter/webui/webui.go ================================================================== --- web/adapter/webui/webui.go +++ web/adapter/webui/webui.go @@ -80,11 +80,10 @@ type webuiBox interface { CanCreateZettel(context.Context) bool GetZettel(context.Context, id.Zid) (zettel.Zettel, error) GetMeta(context.Context, id.Zid) (*meta.Meta, error) CanUpdateZettel(context.Context, zettel.Zettel) bool - AllowRenameZettel(context.Context, id.Zid) bool CanDeleteZettel(context.Context, id.Zid) bool } // New creates a new WebUI struct. func New(log *logger.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager, @@ -174,14 +173,10 @@ ctx context.Context, user, meta *meta.Meta, content zettel.Content) bool { return wui.policy.CanWrite(user, meta, meta) && wui.box.CanUpdateZettel(ctx, zettel.Zettel{Meta: meta, Content: content}) } -func (wui *WebUI) canRename(ctx context.Context, user, m *meta.Meta) bool { - return wui.policy.CanRename(user, m) && wui.box.AllowRenameZettel(ctx, m.Zid) -} - func (wui *WebUI) canDelete(ctx context.Context, user, m *meta.Meta) bool { return wui.policy.CanDelete(user, m) && wui.box.CanDeleteZettel(ctx, m.Zid) } func (wui *WebUI) canRefresh(user *meta.Meta) bool { Index: web/content/content.go ================================================================== --- web/content/content.go +++ web/content/content.go @@ -22,10 +22,11 @@ "t73f.de/r/zsc/api" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/meta" ) +// Some MIME encoding values. const ( UnknownMIME = "application/octet-stream" mimeGIF = "image/gif" mimeHTML = "text/html; charset=utf-8" mimeJPEG = "image/jpeg" @@ -98,10 +99,12 @@ // Additional syntaxes "application/pdf": "pdf", "text/javascript": "js", } +// SyntaxFromMIME returns the syntax for a zettel based on MIME encoding value +// and the actual data. func SyntaxFromMIME(m string, data []byte) string { mt, _, _ := mime.ParseMediaType(m) if syntax, found := mime2syntax[mt]; found { return syntax } Index: web/server/impl/impl.go ================================================================== --- web/server/impl/impl.go +++ web/server/impl/impl.go @@ -19,10 +19,11 @@ "net/http" "time" "t73f.de/r/zsc/api" "zettelstore.de/z/auth" + "zettelstore.de/z/box" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel/meta" ) @@ -32,21 +33,44 @@ server httpServer router httpRouter persistentCookie bool secureCookie bool } + +// ServerData contains the data needed to configure a server. +type ServerData struct { + Log *logger.Logger + ListenAddr string + BaseURL string + URLPrefix string + MaxRequestSize int64 + Auth auth.TokenManager + PersistentCookie bool + SecureCookie bool + Profiling bool + ZidMapper box.Mapper +} // New creates a new web server. -func New(log *logger.Logger, listenAddr, baseURL, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server { +func New(sd ServerData) server.Server { srv := myServer{ - log: log, - baseURL: baseURL, - persistentCookie: persistentCookie, - secureCookie: secureCookie, + log: sd.Log, + baseURL: sd.BaseURL, + persistentCookie: sd.PersistentCookie, + secureCookie: sd.SecureCookie, + } + + rd := routerData{ + log: sd.Log, + urlPrefix: sd.URLPrefix, + maxRequestSize: sd.MaxRequestSize, + auth: sd.Auth, + profiling: sd.Profiling, + zidmapper: sd.ZidMapper, } - srv.router.initializeRouter(log, urlPrefix, maxRequestSize, auth) - srv.server.initializeHTTPServer(listenAddr, &srv.router) + srv.router.initializeRouter(rd) + srv.server.initializeHTTPServer(sd.ListenAddr, &srv.router) return &srv } func (srv *myServer) Handle(pattern string, handler http.Handler) { srv.router.Handle(pattern, handler) Index: web/server/impl/router.go ================================================================== --- web/server/impl/router.go +++ web/server/impl/router.go @@ -14,18 +14,21 @@ package impl import ( "io" "net/http" + "net/http/pprof" "regexp" + rtprf "runtime/pprof" "strings" - "t73f.de/r/zsc/api" "zettelstore.de/z/auth" + "zettelstore.de/z/box" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" + "zettelstore.de/z/zettel/id" ) type ( methodHandler [server.MethodLAST]http.Handler routingTable [256]*methodHandler @@ -35,11 +38,10 @@ http.MethodHead: server.MethodHead, http.MethodGet: server.MethodGet, http.MethodPost: server.MethodPost, http.MethodPut: server.MethodPut, http.MethodDelete: server.MethodDelete, - api.MethodMove: server.MethodMove, } // httpRouter handles all routing for zettelstore. type httpRouter struct { log *logger.Logger @@ -51,22 +53,49 @@ listTable routingTable zettelTable routingTable ur server.UserRetriever mux *http.ServeMux maxReqSize int64 + zidmapper box.Mapper +} + +type routerData struct { + log *logger.Logger + urlPrefix string + maxRequestSize int64 + auth auth.TokenManager + profiling bool + zidmapper box.Mapper } // initializeRouter creates a new, empty router with the given root handler. -func (rt *httpRouter) initializeRouter(log *logger.Logger, urlPrefix string, maxRequestSize int64, auth auth.TokenManager) { - rt.log = log - rt.urlPrefix = urlPrefix - rt.auth = auth +func (rt *httpRouter) initializeRouter(rd routerData) { + rt.log = rd.log + rt.urlPrefix = rd.urlPrefix + rt.auth = rd.auth rt.minKey = 255 rt.maxKey = 0 rt.reURL = regexp.MustCompile("^$") rt.mux = http.NewServeMux() - rt.maxReqSize = maxRequestSize + rt.maxReqSize = rd.maxRequestSize + rt.zidmapper = rd.zidmapper + + if rd.profiling { + rt.setRuntimeProfiling() + } +} + +func (rt *httpRouter) setRuntimeProfiling() { + rt.mux.HandleFunc("GET /rtp/", pprof.Index) + for _, profile := range rtprf.Profiles() { + name := profile.Name() + rt.mux.Handle("GET /rtp/"+name, pprof.Handler(name)) + } + rt.mux.HandleFunc("GET /rtp/cmdline", pprof.Cmdline) + rt.mux.HandleFunc("GET /rtp/profile", pprof.Profile) + rt.mux.HandleFunc("GET /rtp/symbol", pprof.Symbol) + rt.mux.HandleFunc("GET /rtp/trace", pprof.Trace) } func (rt *httpRouter) addRoute(key byte, method server.Method, handler http.Handler, table *routingTable) { // Set minKey and maxKey; re-calculate regexp. if key < rt.minKey || rt.maxKey < key { @@ -75,11 +104,11 @@ } if rt.maxKey < key { rt.maxKey = key } rt.reURL = regexp.MustCompile( - "^/(?:([" + string(rt.minKey) + "-" + string(rt.maxKey) + "])(?:/(?:([0-9]{14})/?)?)?)$") + "^/(?:([" + string(rt.minKey) + "-" + string(rt.maxKey) + "])(?:/(?:((?:[0-9]{14})|(?:[0-9a-zA-Z]{4}))/?)?)?)$") } mh := table[key] if mh == nil { mh = new(methodHandler) @@ -147,14 +176,21 @@ rt.log.Debug().Str("key", match[1]).Str("zid", match[2]).Msg("path match") } key := match[1][0] var mh *methodHandler - if match[2] == "" { + if sZid := match[2]; sZid == "" { mh = rt.listTable[key] } else { mh = rt.zettelTable[key] + if len(sZid) == 4 { + if zidN, err := id.ParseN(sZid); err == nil { + if zidO, found := rt.zidmapper.LookupZidO(zidN); found { + match[2] = zidO.String() + } + } + } } method, ok := mapMethod[r.Method] if ok && mh != nil { if handler := mh[method]; handler != nil { r.URL.Path = "/" + match[2] Index: web/server/server.go ================================================================== --- web/server/server.go +++ web/server/server.go @@ -36,11 +36,10 @@ const ( MethodGet Method = iota MethodHead MethodPost MethodPut - MethodMove MethodDelete MethodLAST // must always be the last one ) // Router allows to state routes for various URL paths. Index: www/build.md ================================================================== --- www/build.md +++ www/build.md @@ -1,14 +1,17 @@ # How to build Zettelstore + ## Prerequisites + You must install the following software: * A current, supported [release of Go](https://go.dev/doc/devel/release), * [staticcheck](https://staticcheck.io/), * [shadow](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow), * [unparam](https://mvdan.cc/unparam), * [govulncheck](https://golang.org/x/vuln/cmd/govulncheck), +* [revive](https://revive.run/), * [Fossil](https://fossil-scm.org/), * [Git](https://git-scm.org) (so that Go can download some dependencies). See folder `docs/development` (a zettel box) for details. @@ -28,13 +31,11 @@ In the directory `tools` there are some Go files to automate most aspects of building and testing, (hopefully) platform-independent. The build script is called as: -``` -go run tools/build/build.go [-v] COMMAND -``` + go run tools/build/build.go [-v] COMMAND The flag `-v` enables the verbose mode. It outputs all commands called by the tool. Some important `COMMAND`s are: @@ -46,23 +47,19 @@ * `version`: prints the current version information. Therefore, the easiest way to build your own version of the Zettelstore software is to execute the command -``` -go run tools/build/build.go build -``` + go run tools/build/build.go build In case of errors, please send the output of the verbose execution: -``` -go run tools/build/build.go -v build -``` + go run tools/build/build.go -v build Other tools are: -* `go run tools/clean/clean.go` cleans your Go development worspace. +* `go run tools/clean/clean.go` cleans your Go development workspace. * `go run tools/check/check.go` executes all linters and unit tests. If you add the option `-r` linters are more strict, to be used for a release version. * `go run tools/devtools/devtools.go` install all needed software (see above). * `go run tools/htmllint/htmllint.go [URL]` checks all generated HTML of a @@ -69,10 +66,11 @@ Zettelstore accessible at the given URL (default: http://localhost:23123). * `go run tools/testapi/testapi.go` tests the API against a running Zettelstore, which is started automatically. ## A note on the use of Fossil + Zettelstore is managed by the Fossil version control system. Fossil is an alternative to the ubiquitous Git version control system. However, Go seems to prefer Git and popular platforms that just support Git. Some dependencies of Zettelstore, namely [Zettelstore @@ -82,14 +80,14 @@ might occur. If the error message mentions an environment variable called `GOVCS` you should set it to the value `GOVCS=zettelstore.de:fossil` (alternatively more generous to `GOVCS=*:all`). Since the Go build system is coupled with Git and some -special platforms, you allow ot to download a Fossil repository from the host -`zettelstore.de`. The build tool set `GOVCS` to the right value, but you may -use other `go` commands that try to download a Fossil repository. +special platforms, you must allow Go to download a Fossil repository from the +host `zettelstore.de`. The build tool sets `GOVCS` to the right value, but you +may use other `go` commands that try to download a Fossil repository. On some operating systems, namely Termux on Android, an error message might state that an user cannot be determined (`cannot determine user`). In this case, Fossil is allowed to download the repository, but cannot associate it with an user name. Set the environment variable `USER` to any user name, like: `USER=nobody go run tools/build.go build`. Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -1,9 +1,34 @@ Change Log

Changes for Version 0.19.0 (pending)

+ * Remove support for renaming zettel, i.e. changing zettel identifier. Was + announced as deprecated in version 0.18. + (breaking: api, webui) + * Zettel content for zettel with ID starting with 0000 is not indexed any + more. If you search / query for zettel content, these zettel will not be + returned. + (breaking: api, webui) + * Allow to use new-style zettel identifier in WebUI and API. Please remember + the new-style identifier are currently not stable. Therefore you should + not store them for future use, e.g. as a link to another zettel or as a + bookmark in your browser or in your database. + (major: api, webui) + * Fix wrong quote translation for markdown encoder. + (minor) + * Generate <th> in table header (was: <td>). + Also applies to SHTML encoder. (minor: webui, api) + * External links are now generated in shtml and html with attribute + rel="external" (previously: class="external"). + (minor: webui, api) + * Show new format zettel identifier in zettel view, info view and delete + view. + (minor: webui) + * Allow to enable runtime profiling of the software, to be used by + developers. + (minor)

Changes for Version 0.18.0 (2024-07-11)

* Remove Sx macro defunconst. Use defun instead. (breaking: webui) Index: www/impri.wiki ================================================================== --- www/impri.wiki +++ www/impri.wiki @@ -12,7 +12,7 @@ be used to send the data of the website you requested to you and to mitigate possible attacks against this website. This website is hosted by [https://ionos.de|1&1 IONOS SE]. According to -[https://www.ionos.de/hilfe/datenschutz/datenverarbeitung-von-webseitenbesuchern-ihres-11-ionos-produktes/andere-11-ionos-produkte/|their information], +[https://www.ionos.de/hilfe/datenschutz/datenverarbeitung-von-webseitenbesuchern-ihres-ionos-produktes/andere-ionos-produkte/|their information], no processing of personal data is done by them. Index: www/index.wiki ================================================================== --- www/index.wiki +++ www/index.wiki @@ -36,10 +36,11 @@ * [./plan.wiki|Limitations and planned improvements] * [/timeline?t=release|Timeline of all past releases]

Build instructions

+ Just install [https://go.dev/dl/|Go] and some Go-based tools. Please read the [./build.md|instructions] for details. * [/dir?ci=trunk|Source code] * [/download|Download the source code] as a tarball or a ZIP file Index: zettel/content.go ================================================================== --- zettel/content.go +++ zettel/content.go @@ -75,11 +75,11 @@ if input.IsEOLEOS(inp.Ch) { inp.Next() pos = inp.Pos continue } - if !input.IsSpace(inp.Ch) { + if !inp.IsSpace() { break } inp.Next() } zc.data = bytes.TrimRightFunc(inp.Src[pos:], unicode.IsSpace) Index: zettel/id/id.go ================================================================== --- zettel/id/id.go +++ zettel/id/id.go @@ -44,18 +44,18 @@ LoginTemplateZid = MustParse(api.ZidLoginTemplate) ListTemplateZid = MustParse(api.ZidListTemplate) ZettelTemplateZid = MustParse(api.ZidZettelTemplate) InfoTemplateZid = MustParse(api.ZidInfoTemplate) FormTemplateZid = MustParse(api.ZidFormTemplate) - RenameTemplateZid = MustParse(api.ZidRenameTemplate) DeleteTemplateZid = MustParse(api.ZidDeleteTemplate) ErrorTemplateZid = MustParse(api.ZidErrorTemplate) StartSxnZid = MustParse(api.ZidSxnStart) BaseSxnZid = MustParse(api.ZidSxnBase) PreludeSxnZid = MustParse(api.ZidSxnPrelude) EmojiZid = MustParse(api.ZidEmoji) TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate) + MappingZid = MustParse(api.ZidMapping) DefaultHomeZid = MustParse(api.ZidDefaultHome) ) const maxZid = 99999999999999 @@ -245,14 +245,14 @@ d2 := d12 % 36 d34 := uint32(zid) % (36 * 36) d3 := d34 / 36 d4 := d34 % 36 - const digits = "0123456789abcdefghijklmnopqrstuvwxyz" + const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" result[0] = digits[d1] result[1] = digits[d2] result[2] = digits[d3] result[3] = digits[d4] } // IsValid determines if zettel id is a valid one, e.g. consists of max. 14 digits. func (zid ZidN) IsValid() bool { return 0 < zid && zid <= maxZidN } ADDED zettel/id/mapper/mapper.go Index: zettel/id/mapper/mapper.go ================================================================== --- /dev/null +++ zettel/id/mapper/mapper.go @@ -0,0 +1,340 @@ +//----------------------------------------------------------------------------- +// 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: 2020-present Detlef Stern +//----------------------------------------------------------------------------- + +// Package mapper provides a mechanism to map zettel identifier with 14 digits +// to their successing identifier with four characters. +package mapper + +import ( + "bytes" + "context" + "fmt" + "maps" + "sync" + "time" + + "t73f.de/r/zsc/input" + "zettelstore.de/z/zettel/id" +) + +// Mapper 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 Mapper struct { + fetcher Fetcher + 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 +} + +// Fetcher is an object that will fetch all identifier currently in user. +type Fetcher interface { + // FetchZidsO fetch all old-style zettel identifier. + FetchZidsO(context.Context) (*id.Set, error) +} + +// Make creates a new Mapper. +func Make(fetcher Fetcher) *Mapper { + 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 + 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 + 9999999996: id.MustParseN("00zw"), // Current ZidMapping, TEMP for v0.19-dev + 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 &Mapper{ + 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 *Mapper) 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 *Mapper) Warnings(ctx context.Context) (*id.Set, error) { + allZidsO, err := zm.fetcher.FetchZidsO(ctx) + if err != nil { + return nil, err + } + warnings := id.NewSet() + allZidsO.ForEach(func(zid id.Zid) { + if !zm.isWellDefined(zid) { + warnings = warnings.Add(zid) + } + }) + return warnings, nil +} + +// LookupZidN returns the new-style identifier for a given old-style identifier. +func (zm *Mapper) LookupZidN(zidO id.Zid) (id.ZidN, bool) { + if !zidO.IsValid() { + panic(zidO) + } + zm.mx.RLock() + zidN, found := zm.toNew[zidO] + zm.mx.RUnlock() + return zidN, found +} + +// GetZidN returns a new-style identifier for a given old-style identifier. +// If the old-style identifier is currently not mapped, the mapping will be +// established. +func (zm *Mapper) GetZidN(zidO id.Zid) id.ZidN { + if zidN, found := zm.LookupZidN(zidO); found { + return zidN + } + + 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 +} + +// LookupZidO returns the old-style identifier for a new-style identifier. +func (zm *Mapper) LookupZidO(zidN id.ZidN) (id.Zid, bool) { + if zm != nil { + zm.mx.RLock() + zidO, found := zm.toOld[zidN] + zm.mx.RUnlock() + return zidO, found + } + return id.Invalid, false +} + +// DeleteO removes a mapping with the given old-style identifier. +func (zm *Mapper) DeleteO(zidO id.Zid) { + if _, found := zm.defined[zidO]; found { + return + } + zm.mx.Lock() + if zidN, found := zm.toNew[zidO]; found { + delete(zm.toNew, zidO) + delete(zm.toOld, zidN) + if lastZidN := zm.nextZidN - 1; zidN == lastZidN { + zm.nextZidN = lastZidN + } + } + zm.mx.Unlock() +} + +// AsBytes returns the current mapping as lines, where each line contains the +// old and the new zettel identifier. +func (zm *Mapper) AsBytes() []byte { + zm.mx.RLock() + defer zm.mx.RUnlock() + return zm.asBytes() +} +func (zm *Mapper) asBytes() []byte { + allZidsO := id.NewSetCap(len(zm.toNew)) + for zidO := range zm.toNew { + allZidsO = allZidsO.Add(zidO) + } + var buf bytes.Buffer + first := true + allZidsO.ForEach(func(zidO id.Zid) { + if !first { + buf.WriteByte('\n') + } + first = false + zidN := zm.toNew[zidO] + buf.WriteString(zidO.String()) + buf.WriteByte(' ') + buf.WriteString(zidN.String()) + }) + return buf.Bytes() +} + +// FetchAsBytes fetches all zettel identifier and returns the mapping as lines, +// where each line contains the old zid and the new zid. +func (zm *Mapper) FetchAsBytes(ctx context.Context) ([]byte, error) { + allZidsO, err := zm.fetcher.FetchZidsO(ctx) + if err != nil { + return nil, err + } + allZidsO.ForEach(func(zidO id.Zid) { + _ = zm.GetZidN(zidO) + }) + zm.mx.Lock() + defer zm.mx.Unlock() + if len(zm.toNew) != allZidsO.Length() { + for zidO, zidN := range zm.toNew { + if allZidsO.Contains(zidO) { + continue + } + delete(zm.toNew, zidO) + delete(zm.toOld, zidN) + } + } + return zm.asBytes(), nil +} + +// ParseAndUpdate parses the given content and updates the Mapping. +func (zm *Mapper) ParseAndUpdate(content []byte) (err error) { + zm.mx.Lock() + defer zm.mx.Unlock() + inp := input.NewInput(content) + for inp.Ch != input.EOS { + inp.SkipSpace() + pos := inp.Pos + zidO := readZidO(inp) + if !zidO.IsValid() { + inp.SkipToEOL() + inp.EatEOL() + if err == nil { + err = fmt.Errorf("unable to parse old zid: %q", string(inp.Src[pos:inp.Pos])) + } + continue + } + inp.SkipSpace() + zidN := readZidN(inp) + if !zidN.IsValid() { + inp.SkipToEOL() + inp.EatEOL() + if err == nil { + err = fmt.Errorf("unable to parse new zid: %q", string(inp.Src[pos:inp.Pos])) + } + continue + } + inp.SkipToEOL() + inp.EatEOL() + + if oldZidN, found := zm.toNew[zidO]; found { + if oldZidN != zidN { + err = fmt.Errorf("old zid %v already mapped to %v, overwrite: %v", zidO, oldZidN, zidN) + } + continue + } + zm.toNew[zidO] = zidN + zm.toOld[zidN] = zidO + zm.nextZidN = max(zm.nextZidN, zidN+1) + } + return err +} + +func readZidO(inp *input.Input) id.Zid { + pos := inp.Pos + for '0' <= inp.Ch && inp.Ch <= '9' { + inp.Next() + } + zidO, _ := id.Parse(string(inp.Src[pos:inp.Pos])) + return zidO +} +func readZidN(inp *input.Input) id.ZidN { + pos := inp.Pos + for ('0' <= inp.Ch && inp.Ch <= '9') || ('a' <= inp.Ch && inp.Ch <= 'z') || ('A' <= inp.Ch && inp.Ch <= 'Z') { + inp.Next() + } + zidN, _ := id.ParseN(string(inp.Src[pos:inp.Pos])) + return zidN +} Index: zettel/id/set.go ================================================================== --- zettel/id/set.go +++ zettel/id/set.go @@ -285,13 +285,12 @@ // ----- unchecked base operations func newFromSlice(seq Slice) *Set { if l := len(seq); l == 0 { return nil - } else { - return &Set{seq: seq} } + return &Set{seq: seq} } func (s *Set) add(zid Zid) { if pos, found := s.find(zid); !found { s.seq = slices.Insert(s.seq, pos, zid) Index: zettel/meta/meta.go ================================================================== --- zettel/meta/meta.go +++ zettel/meta/meta.go @@ -168,10 +168,11 @@ const NewPrefix = "new-" // Meta contains all meta-data of a zettel. type Meta struct { Zid id.Zid + ZidN id.ZidN pairs map[string]string YamlSep bool } // New creates a new chunk for storing metadata. @@ -202,10 +203,11 @@ // Clone returns a new copy of the metadata. func (m *Meta) Clone() *Meta { return &Meta{ Zid: m.Zid, + ZidN: m.ZidN, pairs: m.Map(), YamlSep: m.YamlSep, } } Index: zettel/meta/parse.go ================================================================== --- zettel/meta/parse.go +++ zettel/meta/parse.go @@ -29,11 +29,11 @@ skipToEOL(inp) inp.EatEOL() } meta := New(zid) for { - skipSpace(inp) + inp.SkipSpace() switch inp.Ch { case '\r': if inp.Peek() == '\n' { inp.Next() } @@ -62,35 +62,29 @@ pos := inp.Pos for isHeader(inp.Ch) { inp.Next() } key := inp.Src[pos:inp.Pos] - skipSpace(inp) + inp.SkipSpace() if inp.Ch == ':' { inp.Next() } var val []byte for { - skipSpace(inp) + inp.SkipSpace() pos = inp.Pos skipToEOL(inp) val = append(val, inp.Src[pos:inp.Pos]...) inp.EatEOL() - if !input.IsSpace(inp.Ch) { + if !inp.IsSpace() { break } val = append(val, ' ') } addToMeta(m, string(key), string(val)) } -func skipSpace(inp *input.Input) { - for input.IsSpace(inp.Ch) { - inp.Next() - } -} - func skipToEOL(inp *input.Input) { for { switch inp.Ch { case '\n', '\r', input.EOS: return