added basic tag support...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2022-10-26 17:06:06 +03:00
parent 0df1919259
commit f58b5fbd56
3 changed files with 146 additions and 41 deletions

View File

@ -76,6 +76,8 @@ object.Constructor('BasePage', {
type: true, type: true,
ctime: true, ctime: true,
mtime: 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 // These actions will be default get :$ARGS appended if no args are
// explicitly given... // explicitly given...
@ -337,6 +339,28 @@ object.Constructor('BasePage', {
} else { } else {
this.__update__(value) } }, 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... // metadata...
// //
// NOTE: in the general case this is the same as .data but in also allows // 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 that = this
var name = args.name //?? args[0] var name = args.name //?? args[0]
var src = args.src var src = args.src
var all = args.all
var base = this.get(this.path.split(/\*/).shift()) var base = this.get(this.path.split(/\*/).shift())
var sort = (args.sort ?? '') var sort = (args.sort ?? '')
.split(/\s+/g) .split(/\s+/g)
@ -1973,7 +1996,7 @@ module.System = {
<a href="#@source(s ../../path)/list">&#x21D1;</a> <a href="#@source(s ../../path)/list">&#x21D1;</a>
@source(../path) @source(../path)
</slot> </slot>
<macro src="../*:@(all)" join="@source(line-separator)"> <macro src="../*:$ARGS" join="@source(line-separator)">
<a href="#@source(s ./path)">@source(./name)</a> <a href="#@source(s ./path)">@source(./name)</a>
<sup> <sup>
<macro src="./isAction"> <macro src="./isAction">
@ -1991,7 +2014,7 @@ module.System = {
tree: { tree: {
text: object.doc` text: object.doc`
<slot title/> <slot title/>
<macro src="../*:@(all)"> <macro src="../*:$ARGS">
<div> <div>
<div class="item"> <div class="item">
<a class="tree-page-title" href="#@source(s ./path)">@source(./title)</a> <a class="tree-page-title" href="#@source(s ./path)">@source(./title)</a>
@ -1999,12 +2022,12 @@ module.System = {
<a class="show-on-hover" href="#@source(s ./path)/delete">&times;</a> <a class="show-on-hover" href="#@source(s ./path)/delete">&times;</a>
</div> </div>
<div style="padding-left: 30px"> <div style="padding-left: 30px">
@include("./tree:@(all)") @include("./tree:$ARGS")
</div> </div>
</div> </div>
</macro>` }, </macro>` },
all: { all: {
text: `@include("../**/path:@(all)" join="@source(line-separator)")`}, text: `@include("../**/path:$ARGS" join="@source(line-separator)")`},
info: { info: {
text: object.doc` text: object.doc`
<slot pre> <slot pre>
@ -2022,6 +2045,8 @@ module.System = {
type: @source(../type)<br> type: @source(../type)<br>
tags: @source(../tags)<br>
ctime: @source(../ctime)<br> ctime: @source(../ctime)<br>
mtime: @source(../mtime)<br> mtime: @source(../mtime)<br>
@ -2187,7 +2212,7 @@ module.Templates = {
text: object.doc` text: object.doc`
<slot title/> <slot title/>
<slot header><content/><a href="#./$NOW/edit">&#128462;</a></slot> <slot header><content/><a href="#./$NOW/edit">&#128462;</a></slot>
<macro src="*:@(all)" join="<br>"> <macro src="*:$ARGS" join="<br>">
<div class="item"> <div class="item">
<a href="#@source(s ./path)/edit">@source(./title)</a> <a href="#@source(s ./path)/edit">@source(./title)</a>
<a class="show-on-hover" href="#@source(s ./path)/info">&#128712;</a> <a class="show-on-hover" href="#@source(s ./path)/info">&#128712;</a>

View File

@ -124,16 +124,22 @@ module.BaseStore = {
index: async function(action='get', ...args){ index: async function(action='get', ...args){
return index.index(this, ...arguments) }, return index.index(this, ...arguments) },
// XXX INDEX... //
// Format:
// [
// <path>,
// ...
// ]
//
__paths__: async function(){ __paths__: async function(){
return Object.keys(this.data) }, return Object.keys(this.data) },
// XXX unique???
__paths_merge__: async function(data){ __paths_merge__: async function(data){
return (await data) return (await data)
.concat((this.next .concat((this.next
&& 'paths' in this.next) ? && 'paths' in this.next) ?
await this.next.paths await this.next.paths
: []) }, : [])
.unique() },
__paths_isvalid__: function(t){ __paths_isvalid__: function(t){
var changed = var changed =
!!this.__paths_next_exists != !!this.next !!this.__paths_next_exists != !!this.next
@ -159,6 +165,16 @@ module.BaseStore = {
get paths(){ get paths(){
return this.__paths() }, return this.__paths() },
//
// Format:
// {
// <name>: [
// <path>,
// ...
// ],
// ...
// }
//
__names_isvalid__: function(t){ __names_isvalid__: function(t){
return this.__paths_isvalid__(t) }, return this.__paths_isvalid__(t) },
// NOTE: this is built from .paths so there is no need to define a // NOTE: this is built from .paths so there is no need to define a
@ -197,8 +213,76 @@ module.BaseStore = {
return this.__names() }, return this.__names() },
// XXX tags // XXX tags
//
// Format:
// {
// tags: {
// <tag>: Set([
// <path>,
// ...
// ]),
// ...
// },
// paths: {
// <path>: Set([
// <tag>,
// ...
// ]),
// ...
// }
// }
//
// 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('*') if(path.includes('*')
|| path.includes('**')){ || path.includes('**')){
var order = (this.metadata(path) ?? {}).order || [] var order = (this.metadata(path) ?? {}).order || []
var {path, args} = pwpath.splitArgs(path) var {path, args} = pwpath.splitArgs(path)
var all = args.all var all = args.all
var tags = args.tags
tags = typeof(tags) == 'string' ?
this.parseTags(tags)
: false
tags && await this.tags
args = pwpath.joinArgs('', args) args = pwpath.joinArgs('', args)
// NOTE: we are matching full paths only here so leading and // NOTE: we are matching full paths only here so leading and
// trainling '/' are optional... // trainling '/' are optional...
var pattern = new RegExp(`^\\/?` var pattern = new RegExp(`^\\/?`
@ -317,6 +408,14 @@ module.BaseStore = {
// skip metadata paths... // skip metadata paths...
if(p.includes('*')){ if(p.includes('*')){
return res } 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)] var m = [...p.matchAll(pattern)]
m.length > 0 m.length > 0
&& (!all ? && (!all ?
@ -363,7 +462,6 @@ module.BaseStore = {
if(path.includes('*') if(path.includes('*')
|| path.includes('**')){ || path.includes('**')){
var p = pwpath.splitArgs(path) var p = pwpath.splitArgs(path)
var all = p.args.all
var args = pwpath.joinArgs('', p.args) var args = pwpath.joinArgs('', p.args)
p = pwpath.split(p.path) p = pwpath.split(p.path)
var tail = [] var tail = []
@ -372,12 +470,11 @@ module.BaseStore = {
tail = tail.join('/') tail = tail.join('/')
if(tail.length > 0){ if(tail.length > 0){
return (await this.match( return (await this.match(
p.join('/') + (all ? ':all' : ''), p.join('/') + args,
strict)) strict))
.map(function(p){ .map(function(p){
all && var {path, args} = pwpath.splitArgs(p)
(p = p.replace(/:all/, '')) return pwpath.joinArgs(pwpath.join(path, tail), args) }) } }
return pwpath.join(p, tail) + args }) } }
// direct... // direct...
return this.match(path, strict) }, return this.match(path, strict) },
// //

View File

@ -17,17 +17,18 @@
* - CLI * - CLI
* *
* *
* XXX TAGS should ./tags (i.e. .tagstr) return a list of tags???
* XXX TAGS
* - add tags to page -- macro/filter
* - <page>.text -> <page>.tags (cached on .update(..))
* - manual
* - a way to list tags -- folder like? - ???
* - tag cache <store>.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??? * XXX INDEX DOC can index validation be async???
* ...likely no * ...likely no
* XXX INDEX add option to set default action (get/lazy/cached) * 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 CachedStore seems to be broken (see: pwiki/store/base.js:837)
* XXX might be a good idea to create memory store (sandbox) from the * XXX might be a good idea to create memory store (sandbox) from the
* page API -- action?? * page API -- action??
@ -106,24 +107,6 @@
* - count + elem-offset * - count + elem-offset
* - from + to * - from + to
* XXX revise/update sort... * 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
* - <page>.text -> <page>.tags (cached on .update(..))
* - manual
* - a way to list tags -- folder like?
* - tag cache <store>.tags
* format:
* {
* <tag>: [<path>, ...],
* }
* - 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 FEATURE images...
* XXX async/live render... * XXX async/live render...
* might be fun to push the async parts of the render to the dom... * might be fun to push the async parts of the render to the dom...