From f58b5fbd56c941d23e7704ea23abb481bcd513de Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Wed, 26 Oct 2022 17:06:06 +0300 Subject: [PATCH] added basic tag support... Signed-off-by: Alex A. Naanou --- pwiki/page.js | 37 +++++++++++--- pwiki/store/base.js | 115 ++++++++++++++++++++++++++++++++++++++++---- pwiki2.js | 35 ++++---------- 3 files changed, 146 insertions(+), 41 deletions(-) diff --git a/pwiki/page.js b/pwiki/page.js index 73aa440..d6a733b 100755 --- a/pwiki/page.js +++ b/pwiki/page.js @@ -76,6 +76,8 @@ object.Constructor('BasePage', { type: true, ctime: true, mtime: true, + // XXX TAGS HACK -- should this be a list??? + tags: 'tagstr', }, // These actions will be default get :$ARGS appended if no args are // explicitly given... @@ -337,6 +339,28 @@ object.Constructor('BasePage', { } else { this.__update__(value) } }, + get tags(){ return async function(){ + return (await this.data).tags ?? [] }.call(this) }, + set tags(value){ return async function(){ + this.data = { + ...(await this.data), + tags: [...value], + } }.call(this) }, + // XXX TAGS HACK -- should this be a list??? + get tagstr(){ return async function(){ + return JSON.stringify(await this.tags ?? []) }.call(this) }, + tag: async function(...tags){ + this.tags = [...new Set([ + ...(await this.tags), + ...tags, + ])] + return this }, + untag: async function(...tags){ + this.tags = (await this.tags) + .filter(function(tag){ + return !tags.includes(tag) }) + return this }, + // metadata... // // NOTE: in the general case this is the same as .data but in also allows @@ -1263,7 +1287,6 @@ object.Constructor('Page', BasePage, { var that = this var name = args.name //?? args[0] var src = args.src - var all = args.all var base = this.get(this.path.split(/\*/).shift()) var sort = (args.sort ?? '') .split(/\s+/g) @@ -1973,7 +1996,7 @@ module.System = { @source(../path) - + @source(./name) @@ -1991,7 +2014,7 @@ module.System = { tree: { text: object.doc` - +
@source(./title) @@ -1999,12 +2022,12 @@ module.System = { ×
- @include("./tree:@(all)") + @include("./tree:$ARGS")
` }, all: { - text: `@include("../**/path:@(all)" join="@source(line-separator)")`}, + text: `@include("../**/path:$ARGS" join="@source(line-separator)")`}, info: { text: object.doc` @@ -2022,6 +2045,8 @@ module.System = { type: @source(../type)
+ tags: @source(../tags)
+ ctime: @source(../ctime)
mtime: @source(../mtime)
@@ -2187,7 +2212,7 @@ module.Templates = { text: object.doc` 🗎 - +
@source(./title) 🛈 diff --git a/pwiki/store/base.js b/pwiki/store/base.js index 2d66000..4b3fefe 100755 --- a/pwiki/store/base.js +++ b/pwiki/store/base.js @@ -124,16 +124,22 @@ module.BaseStore = { index: async function(action='get', ...args){ return index.index(this, ...arguments) }, - // XXX INDEX... + // + // Format: + // [ + // , + // ... + // ] + // __paths__: async function(){ return Object.keys(this.data) }, - // XXX unique??? __paths_merge__: async function(data){ return (await data) .concat((this.next && 'paths' in this.next) ? await this.next.paths - : []) }, + : []) + .unique() }, __paths_isvalid__: function(t){ var changed = !!this.__paths_next_exists != !!this.next @@ -159,6 +165,16 @@ module.BaseStore = { get paths(){ return this.__paths() }, + // + // Format: + // { + // : [ + // , + // ... + // ], + // ... + // } + // __names_isvalid__: function(t){ return this.__paths_isvalid__(t) }, // NOTE: this is built from .paths so there is no need to define a @@ -197,8 +213,76 @@ module.BaseStore = { return this.__names() }, // XXX tags + // + // Format: + // { + // tags: { + // : Set([ + // , + // ... + // ]), + // ... + // }, + // paths: { + // : Set([ + // , + // ... + // ]), + // ... + // } + // } + // + // XXX should this be here??? + parseTags: function(str){ + return str + .split(/\s*(?:([a-zA-Z1-9_-]+)|"(.+)"|'(.+)')\s*/g) + .filter(function(t){ + return t + && t != '' + && t != ',' }) }, + // XXX do we need these??? + // ...the question is if we have .__tags__(..) how do we + // partially .__tags_merge__(..) things??? + //__tags__: function(){ }, + //__tags_merge__: function(data){ }, + __tags_isvalid__: function(t){ + return this.__paths_isvalid__(t) }, + __tags: index.makeIndex('tags', + async function(){ + var tags = {} + var paths = {} + for(var path of (await this.paths)){ + var t = (await this.get(path)).tags + if(!t){ + continue } + paths[path] = new Set(t) + for(var tag of t){ + ;(tags[tag] = + tags[tag] ?? new Set([])) + .add(path) } } + return {tags, paths} }, { + update: async function(data, path, update){ + if(!('tags' in update)){ + return data } + var {tags, paths} = await data + // remove obsolete tags... + this.__tags.options.remove.call(this, data, path) + // add... + paths[path] = new Set(update.tags) + for(var tag of update.tags ?? []){ + ;(tags[tag] = + tags[tag] ?? new Set([])) + .add(path) } + return data }, + remove: async function(data, path){ + var {tags, paths} = await data + for(var tag of paths[path]){ + tags[tag].delete(path) } + return data }, }), + get tags(){ + return this.__tags() }, - // XXX text search index + // XXX text search index (???) // @@ -293,9 +377,16 @@ module.BaseStore = { if(path.includes('*') || path.includes('**')){ var order = (this.metadata(path) ?? {}).order || [] + var {path, args} = pwpath.splitArgs(path) var all = args.all + var tags = args.tags + tags = typeof(tags) == 'string' ? + this.parseTags(tags) + : false + tags && await this.tags args = pwpath.joinArgs('', args) + // NOTE: we are matching full paths only here so leading and // trainling '/' are optional... var pattern = new RegExp(`^\\/?` @@ -317,6 +408,14 @@ module.BaseStore = { // skip metadata paths... if(p.includes('*')){ return res } + // skip untagged pages... + if(tags){ + var t = that.tags.paths[p] + if(!t){ + return res } + for(var tag of tags){ + if(!t || !t.has(tag)){ + return res } } } var m = [...p.matchAll(pattern)] m.length > 0 && (!all ? @@ -363,7 +462,6 @@ module.BaseStore = { if(path.includes('*') || path.includes('**')){ var p = pwpath.splitArgs(path) - var all = p.args.all var args = pwpath.joinArgs('', p.args) p = pwpath.split(p.path) var tail = [] @@ -372,12 +470,11 @@ module.BaseStore = { tail = tail.join('/') if(tail.length > 0){ return (await this.match( - p.join('/') + (all ? ':all' : ''), + p.join('/') + args, strict)) .map(function(p){ - all && - (p = p.replace(/:all/, '')) - return pwpath.join(p, tail) + args }) } } + var {path, args} = pwpath.splitArgs(p) + return pwpath.joinArgs(pwpath.join(path, tail), args) }) } } // direct... return this.match(path, strict) }, // diff --git a/pwiki2.js b/pwiki2.js index dc64bbd..292d770 100755 --- a/pwiki2.js +++ b/pwiki2.js @@ -17,17 +17,18 @@ * - CLI * * +* XXX TAGS should ./tags (i.e. .tagstr) return a list of tags??? +* XXX TAGS +* - add tags to page -- macro/filter +* - .text -> .tags (cached on .update(..)) +* - manual +* - a way to list tags -- folder like? - ??? +* - tag cache .tags - DONE +* - tag-path filtering... - DONE +* XXX TAGS add a more advanced query -- e.g. "/**:tagged=y,z:untagged=x" ??? * XXX INDEX DOC can index validation be async??? * ...likely no * XXX INDEX add option to set default action (get/lazy/cached) -* XXX BUG: when editing the root page of a substore the page's .cache is -* not reset for some reason... -* ...the problem is in that .names() cache is not reset when a new -* page is created... -* ...this does not appear to affect normal pages... -* ...the root issue is that .__cache_add(..)/.__cache_remove(..) -* are called relative to the nested store and not the root... -* ...feels like we need to rethink the cache/index strategy globally... * XXX CachedStore seems to be broken (see: pwiki/store/base.js:837) * XXX might be a good idea to create memory store (sandbox) from the * page API -- action?? @@ -106,24 +107,6 @@ * - count + elem-offset * - from + to * XXX revise/update sort... -* XXX FEATURE tags: might be a good idea to add a .__match__(..) hook -* to enable store-level matching optimization... -* ...not trivial to route to alk the stores... -* XXX FEATURE tags and accompanying API... -* - add tags to page -- macro/filter -* - .text -> .tags (cached on .update(..)) -* - manual -* - a way to list tags -- folder like? -* - tag cache .tags -* format: -* { -* : [, ...], -* } -* - tag-path filtering... -* i.e. only show tags within a specific path/pattern... -* - path integration... -* i.e. a way to pass tags through path... -* /some/path:tags=a,b,c * XXX FEATURE images... * XXX async/live render... * might be fun to push the async parts of the render to the dom...