/********************************************************************** * * * **********************************************************************/ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ var object = require('ig-object') var types = require('ig-types') // XXX this should be optional... // XXX is this a good idea??? //var dateparser = require('any-date-parser') var pwpath = require('./path') var parser = require('./parser') var filters = require('./filters/base') var markdown = require('./filters/markdown') //--------------------------------------------------------------------- // Page... var relProxy = function(name){ var func = function(path='.:$ARGS', ...args){ path = this.resolvePathVars(path) return this.store[name]( pwpath.relative(this.path, path), ...args) } Object.defineProperty(func, 'name', {value: name}) return func } var relMatchProxy = function(name){ var func = function(path='.:$ARGS', strict=this.strict){ if(path === true || path === false){ strict = path path = '.:$ARGS' } path = this.resolvePathVars(path) return this.store[name]( pwpath.relative(this.path, path), strict) } Object.defineProperty(func, 'name', {value: name}) return func } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var BasePage = module.BasePage = object.Constructor('BasePage', { // root page used to clone new instances via the .clone(..) method... //root: undefined, // a base page to be used as a base for cloning if root is of a // different "class"... //__clone_proto__: undefined, // // Format: // { // : true, // : , // } // actions: { location: true, referrer: true, path: true, name: true, dir: true, // alias... args: 'argstr', title: true, resolved: true, rootpath: true, length: true, type: true, ctime: true, mtime: true, // XXX //tags: true, }, // These actions will be default get :$ARGS appended if no args are // explicitly given... // XXX INHERIT_ARGS actions_inherit_args: new Set([ 'location', 'args', ]), // NOTE: this can be inherited... //store: undefined, //__store: undefined, get store(){ return this.__store ?? (this.root ?? {}).__store }, set store(value){ this.__store = value }, // Path variables... // // XXX PATH_VARS should these be here??? // other places path variables can be resolved: // - navigation (below) // - macro expansion... path_vars: { NOW: function(){ return Date.timeStamp() }, PATH: function(){ return this.path }, NAME: function(){ return this.name }, DIR: function(){ return this.dir }, ARGS: function(){ return pwpath.obj2args(this.args) }, TITLE: function(){ return this.title }, /*/ XXX this needs: // - macro context... // - sort order... INDEX: function(context){ return context.index }, //*/ }, resolvePathVars: function(path='', context={}){ var that = this return path == '.' ? path : pwpath.normalize( Object.entries(this.path_vars) .reduce(function(res, [key, func]){ return res .replace( new RegExp('(\\${'+key+'}|\\$'+key+')', 'g'), func.call(that, context)) }, path)) }, // page location... // // NOTE: path variables are resolved relative to the page BEFORE // navigation... // NOTE: the actual work is done by the .navigate(..) method... __location: undefined, get location(){ return this.__location ?? '/' }, set location(path){ // trigger the event... this.navigate(path) }, // referrer -- a previous page location... referrer: undefined, // events... // //__beforenavigate__: function(location){ .. }, // //__navigate__: function(){ .. }, // // XXX revise naming... // XXX should this be able to prevent navigation??? onBeforeNavigate: types.event.PureEvent('beforeNavigate', function(_, location){ '__beforenavigate__' in this && this.__beforenavigate__(location) }), navigate: types.event.Event('navigate', function(handle, location){ var {path, args} = pwpath.splitArgs(location) this.trigger("onBeforeNavigate", location) this.referrer = this.location var cur = this.__location = this.resolvePathVars( // NOTE: this is done instead of simply assigning // location as-is to normalize the paths and // arguments... pwpath.joinArgs( pwpath.relative( this.path, path) // keep root path predictable... || '/', pwpath.obj2args(args))) // trigger handlers... '__navigate__' in this && this.__navigate__() handle() }), get path(){ return pwpath.splitArgs(this.location).path }, set path(value){ this.location = value }, get args(){ return pwpath.splitArgs(this.location).args }, set args(args){ args = pwpath.obj2args(args) ?? '' this.location = args == '' ? '.' : '.:'+ args }, // helper... get argstr(){ return pwpath.obj2args(this.args) }, set argstr(value){ this.args = value }, // NOTE: these are mostly here as helpers to be accessed via page // actions... // XXX should these be here or in Page??? // XXX should this call .match(..) or .resolve(..)??? get resolved(){ return this.resolve() }, get rootpath(){ return this.root ? this.root.path : this.path }, // XXX should this encode/decode??? get name(){ return pwpath.basename(this.path) }, set name(value){ if(pwpath.normalize(value) == ''){ return } this.move( /^[\\\/]/.test(value) ? value : '../'+value) }, get dir(){ return pwpath.dirname(this.path) }, set dir(value){ var to = pwpath.join(value, this.name) this.move( /^[\\\/]/.test(to) ? to : '../'+to) }, get title(){ return pwpath.decodeElem(this.name) }, set title(value){ this.name = pwpath.encodeElem(value) }, get isPattern(){ return this.path.includes('*') }, // XXX EXPERIMENTAL... get ctime(){ var that = this return Promise.awaitOrRun( this.data, function(data){ var t = (data ?? {}).ctime return t ? new Date(t).getTimeStamp() : t }) }, get mtime(){ var that = this return Promise.awaitOrRun( this.data, function(data){ var t = (data ?? {}).mtime return t ? new Date(t).getTimeStamp() : t }) }, /*/ // XXX ASYNC... get ctime(){ return async function(){ var t = ((await this.data) ?? {}).ctime return t ? new Date(t).getTimeStamp() : t }.call(this) }, get mtime(){ return async function(){ var t = ((await this.data) ?? {}).mtime return t ? new Date(t).getTimeStamp() : t }.call(this) }, //*/ // store interface... // // XXX we are only doing modifiers here... // ...these ar mainly used to disable writing in .ro(..) __update__: function(data){ return this.store.update(this.path, data) }, __delete__: function(path='.'){ return this.store.delete(pwpath.relative(this.path, path)) }, __energetic: undefined, get energetic(){ return this.__energetic === true || ((this.actions && this.actions[this.name] && !!this[ this.actions[this.name] === true ? this.name : this.actions[this.name] ].energetic) || Promise.awaitOrRun( this.store.isEnergetic(this.path), function(res){ return !!res })) }, set energetic(value){ this.__energetic = value }, // page data... // strict: undefined, get data(){ var that = this // direct actions... if(this.actions && this.actions[this.name]){ var name = this.actions[this.name] === true ? this.name : this.actions[this.name] var args = this.args var page = this.get('..', {args}) return Promise.awaitOrRun( (this.isPattern && !this.__energetic && !page[name].energetic) ? page .map(function(page){ var res = page[name] return typeof(res) == 'function' ? res.bind(page.get(name, {args})) : function(){ return res } }) : page[name], function(res){ return typeof(res) == 'function' ? res.bind(that) : res instanceof Array ? res : function(){ return res } }, // NOTE: we are passing null into the error handler to // prevent the actual data (function) from being // consumed... null) } // store data... return Promise.awaitOrRun( this.energetic, function(energetic){ // pattern... // NOTE: we need to make sure each page gets the chance to handle // its context (i.e. bind action to page).... if(that.isPattern && !energetic){ return that .map(function(page){ return page.data }) } // single page... return Promise.awaitOrRun( that.store.get(that.path, !!that.strict, !!energetic), function(res){ return typeof(res) == 'function' ? res.bind(that) : res }) }) }, set data(value){ if(this.actions && this.actions[this.name]){ var name = this.actions[this.name] === true ? this.name : this.actions[this.name] var page = this.get('..') // NOTE: this can return a promise, as we'll need to assign // we do not care about it as long as it's not a function... // XXX not sure if this is a good idea... var res = page[name] // set... typeof(res) == 'function' ? page[name](value.text ?? value) : (page[name] = value.text ?? value) // normal update... } else { this.__update__(value) } }, // tags... // /* get tags(){ return async function(){ return (await this.data).tags ?? [] }.call(this) }, /*/ get tags(){ var tags = this.store.tags var path = pwpath.sanitize(this.path) return tags instanceof Promise ? tags.then(function(tags){ return tags.paths[path] ?? [] }) : this.store.tags.paths[path] ?? [] }, //*/ set tags(value){ return async function(){ this.data = { ...(await this.data), tags: [...value], } }.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 }, toggleTags: async function(...tags){ var t = new Set(await this.tags) for(var tag of tags){ t.has(tag) ? t.delete(tag) : t.add(tag) } this.tags = t return this }, // metadata... // // NOTE: in the general case this is the same as .data but in also allows // storing of data (metadata) for pattern paths... get metadata(){ return this.store.metadata(this.path) }, set metadata(value){ // clear... if(arguments.length > 0 && value == null){ return this.__delete__() } // set... this.__update__(value) }, // XXX ASYNC??? get type(){ return async function(){ return this.store.isStore(this.path) ? 'store' : typeof(await this.data) == 'function' ? 'action' : 'page' }.bind(this)() }, // number of matching pages... // NOTE: this can be both sync and async... get length(){ var p = this.resolve(this.location) return p instanceof Array ? p.length : p instanceof Promise ? p.then(function(res){ return res instanceof Array ? res.length : 1 }) : 1 }, // relative proxies to store... exists: relProxy('exists'), // XXX which match should we use??? //match: relMatchProxy('match'), match: function(path='.', strict=false){ var that = this if(path === true || path === false){ strict = path path = '.' } path = pwpath.relative(this.path, path) return Promise.awaitOrRun( this.store.match(path, strict), function(res){ return res.length == 0 ? // XXX are we going outside of match semantics here??? that.store.find(path) : res }) }, /*/ // XXX ASYNC... match: async function(path='.', strict=false){ if(path === true || path === false){ strict = path path = '.' } path = pwpath.relative(this.path, path) var res = await this.store.match(path, strict) return res.length == 0 ? // XXX are we going outside of match semantics here??? this.store.find(path) : res }, //*/ resolve: relMatchProxy('resolve'), delete: types.event.Event('delete', async function(handle, path='.', base=true){ handle(false) if(path === true || path === false){ base = path path = '.' } var page = this.get(path) if(page.isPattern){ base && this.__delete__(this.path.split('*')[0]) for(var p of await this.get('path').raw){ this.__delete__(p) } } else { this.__delete__(path) } handle() return this }), // XXX should these be implemented here or proxy to .store??? // XXX do we sanity check to no not contain '*'??? copy: async function(to, base=true){ if(this.get(to).path == this.path){ return this } // copy children... if(this.isPattern){ var base = this.path.split('*')[0] // copy the base... base && (this.get(to).data = await this.get(base).data) for(var from of await this.get('path').raw){ this.get(pwpath.join(to, from.slice(base.length))).data = await this.get(from).data } // copy self... } else { this.get(to).data = await this.data } // change location... this.path = to return this }, move: async function(to, base=true){ var from = this.path if(this.get(to).path == this.path){ return this } await this.copy(to, base) this.delete(from, base) return this }, // // Find current path (non-strict) // .find() // .find(false) // .find('.') // .find('.', false) // -> path // -> undefined // // Find current path in strict/non-strict mode... // .find(true) // .find(false) // -> path // -> undefined // // Find path relative to current page (strict/non-strict) // .find([, ]) // -> path // -> undefined // // XXX ARGS preserve args... find: function(path='.', strict=false){ if(path === true || path === false){ strict = path path = '.' } return this.store.find( pwpath.relative(this.path, path), strict) }, // // .get([, ]) // .get(, [, ]) // -> // get: function(path, strict, data={}){ if(strict instanceof Object){ data = strict strict = undefined } return this.clone({ location: path, ...data, referrer: data.referrer //?? this.path, ?? this.referrer, strict, }) }, // XXX should this be an iterator??? each: function(path, strict){ var that = this if(path === true || path === false){ strict = path path = null } strict = strict ?? this.strict // NOTE: we are trying to avoid resolving non-pattern paths unless // we really have to... path = path ? pwpath.relative(this.path, path) : this.location var paths = path.includes('*') ? Promise.awaitOrRun( this.energetic, this.store.isEnergetic(path), function(a, b){ return !(a || b) ? that.resolve(path) : path }) : path paths = Promise.awaitOrRun( paths, function(paths){ return (paths instanceof Array || paths instanceof Promise) ? paths : [paths] }) return Promise.iter( paths, function(path){ return strict ? Promise.awaitOrRun( that.exists('/'+path), function(exists){ return exists ? that.get('/'+ path) : [] }) : that.get('/'+ path) }) .sync() }, // XXX is this correct here??? [Symbol.asyncIterator]: async function*(){ yield* this.each() }, map: function(func){ return this.each().map(func) }, filter: function(func){ return this.each().filter(func) }, reduce: function(func, dfl){ return this.each().reduce(func, dfl) }, // sorting... // // XXX revise how we sore order... sort: async function(...cmp){ // normalize to path... this.metadata = { order: await this.store.sort(this.path, ...cmp) } return this }, // .sortAs() // .sortAs([, .. ]) sortAs: async function(order){ this.metadata = order instanceof Array ? { order: order .map(function(p){ return pwpath.sanitize(p) }) } : { order: (await this.metadata)['order_'+ order] } return this }, // .saveSortAs() // .saveSortAs(, [, .. ]) saveSortAs: async function(name, order=null){ order = order ?? (await this.metadata).order this.metadata = {['order_'+ name]: order} return this }, reverse: async function(){ // not sorting single pages... if(this.length <= 1){ return this } this.sort('reverse') return this }, // // Clone a page optionally asigning data into it... // .clone() // .clone({ .. }[, ]) // -> // // Fully clone a page optionally asigning data into it... // .clone(true[, ]) // .clone(true, { .. }[, ]) // -> // // // Normal cloning will inherit all the "clones" from the original // page overloading .location and .referrer // // NOTE: by default is false unless fully cloning // clone: function(data={}, history=false){ var [data, ...args] = [...arguments] var full = data === true history = typeof(args[args.length-1]) == 'boolean' ? args.pop() : full data = full ? args[0] ?? {} : data var src = this.__clone_proto__ ?? (this.root || {}).__clone_proto__ ?? this.root ?? this return Object.assign( full ? // full copy... // XXX src or this??? //this.constructor(this.path, this.referrer, this.store) src.constructor(this.path, this.referrer, this.store) // NOTE: this will restrict all the clones to the first // generation maintaining the original (.root) page as // the common root... // this will make all the non-shadowed attrs set on the // root visible to all sub-pages. : Object.create(src), // XXX //{...this}, { root: this.root ?? this, location: this.location, referrer: this.referrer, }, data) }, // Create a read-only page... // // NOTE: all pages that are created via a read-only page are also // read-only. // XXX EXPERIMENTAL... ro: function(data={}){ return Object.assign({ __proto__: this, __update__: function(){ return this }, __delete__: function(){ return this }, }, data) }, // Create a virtual page at current path... // // Virtual pages do not affect store data in any way but behave like // normal pages. // // NOTE: .get(..) / .clone(..) will return normal non-virtual pages // unless the target path is the same as the virtual page .path... // NOTE: changing .path/.location is not supported. // XXX EXPERIMENTAL... virtual: function(data={}){ var that = this return { __proto__: this, // make the location read-only... get location(){ // NOTE: since we are not providing this as a basis for // inheritance we do not need to properly access // the parent prop... // ...otherwise use: // object.parentProperty(..) return this.__proto__.location }, __update__: function(data){ Object.assign(this.data, data) return this }, __delete__: function(){ return this }, // NOTE: we need to proxy .clone(..) back to parent so as to // avoid overloading .data in the children too... // NOTE: we are also keeping all first level queries resolving // to current path also virtual... clone: function(...args){ var res = that.clone(...args) return res.path == this.path ? that.virtual(this.data) : res }, data: Object.assign( { ctime: Date.now(), mtime: Date.now(), }, data), } }, // XXX should this be update or assign??? // XXX how should this work on multiple pages... // ...right now this will write what-ever is given, even if it // will never be explicitly be accessible... // XXX sync/async??? update: types.event.Event('update', function(_, ...data){ return Object.assign(this, ...data) }), // XXX should this take an options/dict argument???? __init__: function(path, referrer, store){ if(referrer && typeof(referrer) != 'string'){ store = referrer referrer = undefined } // NOTE: this will allow inheriting .store from the prototype if(store){ this.store = store } this.location = path this.referrer = referrer }, }) // pepper in event functionality... types.event.EventMixin(BasePage.prototype) //--------------------------------------------------------------------- var Page = module.Page = object.Constructor('Page', BasePage, { __parser__: parser.parser, NESTING_DEPTH_LIMIT: 20, NESTING_RECURSION_TEST_THRESHOLD: 50, // Filter that will isolate the page/include/.. from parent filters... ISOLATED_FILTERS: 'isolated', // list of macros that will get raw text of their content... QUOTING_MACROS: ['quote'], // templates used to render a page via .text PAGE_TEMPLATE: '_view', // NOTE: comment this out to make the system fail when nothing is // resolved, not even the System/NotFound page... // NOTE: we can't use any of the page actions here (like @source(./path)) // as if we reach this it's likely all the bootstrap is either also // not present or broken. // NOTE: to force the system to fail set this to undefined. NOT_FOUND_ERROR: 'NotFoundError', RECURSION_ERROR: 'RecursionError', NOT_FOUND_TEMPLATE_ERROR: 'NotFoundTemplateError', QUOTE_ACTION_PAGE: 'QuoteActionPage', // Format: // { // : Set([, ...]), // } // // NOTE: this is stored in .root... //__dependencies: undefined, get dependencies(){ return (this.root ?? this).__dependencies ?? {} }, set dependencies(value){ ((this.root ?? this).__dependencies) = value }, // NOTE: for this to populate .text must be done at least once... get depends(){ return (this.dependencies ?? {})[this.path] }, set depends(value){ if(value == null){ delete (this.dependencies ?? {})[this.path] } else { ;(this.dependencies = this.dependencies ?? {})[this.path] = value } }, // The page that started the current render... // // This is set by .text and maintained by .clone(..). // // NOTE: for manual rendering (.parse(..), ... etc.) this has to be // setup manually. //renderer: undefined, get renderer(){ return this.__render_root ?? this }, set renderer(value){ this.__render_root = value }, // // () // -> // -> undefined // // XXX might be a good idea to fix filter order... filters: { // placeholders... nofilters: function(){}, isolated: function(){}, // XXX TESTING... dummy: function(){}, test: function(source){ return source .replace(/test/g, 'TEST') }, 'quote-tags': function(source){ return source .replace(/&/g, '&') .replace(//g, '>') }, // XXX one way to do this in a stable manner is to wrap the source // in something like .. and only // process those removing the wrapper in dom... // ...not sure how to handle -wikiword filter calls -- now // this is entirely handled by the parser without calling this... wikiword: function(){}, 'quote-wikiword': function(){}, markdown: markdown.markdown, 'quote-markdown': markdown.quoteMarkdown, text: function(source){ return `
${source}
` }, }, // XXX EXPERIMENTAL... // // Define a global macro... // .defmacro(, ) // .defmacro(, , ) // -> this // /* XXX do we need this??? defmacro: function(name, args, func){ this.__parser__.macros[name] = arguments.length == 2 ? arguments[1] : Macro(args, func) return this }, //*/ // direct actions... // // These are evaluated directly without the need to go through the // whole page acquisition process... // // NOTE: these can not be overloaded. // (XXX should this be so?) // XXX should this be an object??? actions: { ...module.BasePage.prototype.actions, '!': true, // XXX EXPERIMENTAL... quote: true, }, // XXX should this be .raw or .parse()??? '!': Object.assign( function(){ return this.get('..:$ARGS', {energetic: true}).raw }, {energetic: true}), // XXX EXPERIMENTAL... // XXX this is html/web specific, should it be here??? // ... // XXX should this be .raw or .parse()??? // XXX ASYNC??? quote: async function(energetic=false){ return Promise.awaitOrRun( this.get('..:$ARGS', {energetic: await this.energetic}).raw, function(res){ return res instanceof Array ? res.map(pwpath.quoteHTML) : pwpath.quoteHTML(res) }) }, // events... // // NOTE: textUpdate event will not get triggered if text is updated // directly via .data or .__update__(..) /*/ XXX EVENTS do we need this??? onTextUpdate: types.event.Event('textUpdate', function(handle, text){ this.__update__({text}) }), // XXX EVENTS not sure where to trigger this??? // ...on .parse(..) is a bit too granular, something like .text?? // XXX not triggered yet... //onParse: types.event.Event('parse'), //*/ // page parser... // // NOTE: .__debug_last_render_state is mainly exposed for introspection // and debugging, set comment it out to disable... //__debug_last_render_state: undefined, // XXX should this handle pattern paths??? // XXX this might be a good spot to cache .raw in state... parse: function(text, state){ var that = this // .parser() if(arguments.length == 1 && text instanceof Object && !(text instanceof Array)){ state = text text = null } return Promise.awaitOrRun( //text, text ?? this.raw, function(text){ state = state ?? {} state.renderer = state.renderer ?? that // this is here for debugging and introspection... '__debug_last_render_state' in that && (that.__debug_last_render_state = state) // parse... return that.__parser__.parse( that.get('.', { renderer: state.renderer, args: that.args, }), text, state) }) }, // raw page text... // // NOTE: writing to .raw is the same as writing to .text... // NOTE: when matching multiple pages this will return a list... // // XXX revise how we handle .strict mode... get raw(){ var that = this return Promise.awaitOrRun( this.data, function(data){ // no data... // NOTE: if we hit this it means that nothing was resolved, // not even the System/NotFound page, i.e. something // went really wrong... // NOTE: in .strict mode this will explicitly fail and not try // to recover... if(data == null){ if(!this.strict && this.NOT_FOUND_ERROR){ var msg = this.get(this.NOT_FOUND_ERROR) return Promise.awaitOrRun( msg.match(), function(msg){ return msg.raw }) } // last resort... throw new Error('NOT FOUND ERROR: '+ this.path) } // get the data... return ( // action... typeof(data) == 'function' ? data() // multiple matches... : data instanceof Array ? // XXX Promise.all(data .map(function(d){ return typeof(d) == 'function'? d() : d.text }) .flat()) : data.text ) }, null) }, set raw(value){ this.data = {text: value} }, //this.onTextUpdate(value) }, // iterate matches or content list as pages... // // .asPages() // .asPages([, ]) // .asPages([, ]) // .asPages(, [, ]) // .asPages() // -> // // NOTE: this will get .raw for non-pattern pages this it can trigger // actions... // // XXX revise name... // XXX do we need both this and .each(..) // ...what is the difference??? // - handle path slightly differently... // - .asPages(..) handles list/generator pages... // XXX BUG: this does not respect strict of single pages if they do // not exist... // ...see: @macro(..) bug + .each(..) // FIXED: revise... asPages: async function*(path='.:$ARGS', strict=false){ // options... var args = [...arguments] var opts = typeof(args.at(-1)) == 'object' ? args.pop() : {} var {path, strict} = { ...opts, path: typeof(args[0]) == 'string' ? args.shift() : '.:$ARGS', strict: args.shift() ?? false, } var page = this.get(path, strict) // each... if(page.isPattern){ yield* page // handle lists in pages (actions, ... etc.)... } else { // strict + page does not exist... if(strict && !(await page.exists())){ return } var data = await page.data data = data instanceof types.Generator ? await data() : typeof(data) == 'function' ? data : data && 'text' in data ? data.text : null if(data instanceof Array || data instanceof types.Generator){ yield* data .map(function(p){ return page.virtual({text: p}) }) return } // do not iterate pages/actions that are undefined... if(data == null){ return } yield page } }, // expanded page text... // // NOTE: this uses .PAGE_TEMPLATE to render the page. // NOTE: writing to .raw is the same as writing to .text... // // XXX should render templates (_view and the like) be a special case // or render as any other page??? // ...currently they are rendered in the context of the page and // not in their own context... // XXX revise how we handle strict mode... // XXX would be nice to be able to chain .awaitOrRun(..) calls instead // of nesting them like here... get text(){ var that = this return Promise.awaitOrRun( !that.strict || that.resolve(true), function(exists){ // strict mode -- break on non-existing pages... if(!exists){ throw new Error('NOT FOUND ERROR: '+ that.location) } var path = pwpath.split(that.path) ;(path.at(-1) ?? '')[0] == '_' || path.push(that.PAGE_TEMPLATE) var tpl = pwpath.join(path) var tpl_name = path.pop() //var tpl_name = path.at(-1) // get the template relative to the top most pattern... return Promise.awaitOrRun( that.get(tpl).find(true), function(tpl){ if(!tpl){ console.warn('UNKNOWN RENDER TEMPLATE: '+ tpl_name) return that.get(that.NOT_FOUND_TEMPLATE_ERROR).parse() } var depends = that.depends = new Set([tpl]) // do the parse... // NOTE: we render the template in context of page... return that // NOTE: that.path can both contain a template and not, this // normalizes it to the path up to the template path... .get(path, {args: that.args}) .parse( that.get( '/'+tpl, {args: that.args}).raw, { depends, renderer: that, }) }) }) }, set text(value){ this.data = {text: value} }, //this.onTextUpdate(value) }, // pass on .renderer to clones... clone: function(data={}, ...args){ this.renderer && (data = {renderer: this.renderer, ...data}) return object.parentCall(Page.prototype.clone, this, data, ...args) }, }) //--------------------------------------------------------------------- // Markdown renderer... // XXX EXPERIMENTAL... var showdown = require('showdown') var MarkdownPage = module.MarkdownPage = object.Constructor('MarkdownPage', Page, { actions: { ...module.Page.prototype.actions, html: true, }, markdown: new showdown.Converter(), get html(){ return async function(){ return this.markdown.makeHtml(await this.raw) }.call(this) }, set html(value){ this.raw = this.markdown.makeMarkdown(value) }, }) // XXX HACK... var Page = MarkdownPage //--------------------------------------------------------------------- // Cached .text page... var getCachedProp = function(obj, name){ var that = obj var value = obj.cache ? obj.cache[name] : object.parentProperty(CachedPage.prototype, name).get.call(obj) value instanceof Promise && value.then(function(value){ that.cache = {[name]: value} }) return value } var setCachedProp = function(obj, name, value){ return object.parentProperty(CachedPage.prototype, name).set.call(obj, value) } // XXX this is good enough on the front-side to think about making the // cache persistent with a very large timeout (if set at all), but // we are not tracking changes on the store-side... var CachedPage = module.CachedPage = object.Constructor('CachedPage', Page, { // Sets what to use for cache id... // // Can be: // 'location' (default) // 'path' cache_id: 'location', // NOTE: set this to null/undefined/0 to disable... cache_timeout: '20m', // keep all the cache in one place -- .root //__cachestore: undefined, get cachestore(){ return (this.root ?? this).__cachestore }, set cachestore(value){ ;(this.root ?? this).__cachestore = value }, get cache(){ this.checkCache(this[this.cache_id]) return ((this.cachestore ?? {})[this[this.cache_id]] ?? {}).value }, // XXX check * paths for matches... set cache(value){ if(this.cachestore === false || this.cache == value){ return } var id = this[this.cache_id ?? 'location'] // clear... if(value == null){ delete (this.cachestore ?? {})[id] // set... } else { var prev = ((this.cachestore = this.cachestore ?? {})[id] ?? {}).value ?? {} ;(this.cachestore = this.cachestore ?? {})[id] = { created: Date.now(), // XXX valid: undefined, value: { ...prev, ...value, }, } } // clear depended pages from cache... for(var [key, deps] of Object.entries(this.dependencies)){ // XXX also check pattern paths... // ...the problem here is that it's getting probabilistic, // i.e. if we match * as a single path item then we might // miss creating a subtree (ex: /tree), while matching // /* to anything will give us lots of false positives... if(key != id && deps.has(id)){ delete this.cachestore[key] } } }, checkCache: function(...paths){ if(!this.cache_timeout || !this.cachestore){ return this } paths = paths.length == 0 ? Object.keys(this.cachestore) : paths for(var path of paths){ var {created, valid, value} = this.cachestore[path] ?? {} if(value){ var now = Date.now() valid = valid ?? Date.str2ms(this.cache_timeout) // drop cache... if(now > created + valid){ //console.log('CACHE: DROP:', this.path) delete this.cachestore[path] } } } return this }, clearCache: function(...paths){ if(this.cachestore){ if(arguments.length == 0){ this.cachestore = null } else { for(var path of paths){ delete this.cachestore[path] } } } return this }, __update__: function(){ this.cache = null return object.parentCall(CachedPage.prototype.__update__, this, ...arguments) }, __delete__: function(){ this.cache = null return object.parentCall(CachedPage.prototype.__delete__, this, ...arguments) }, /* XXX do we need to cache .raw??? // ...yes this makes things marginally faster but essentially // copies the db into memory... get raw(){ return getCachedProp(this, 'raw') }, set raw(value){ return setCachedProp(this, 'raw', value) }, //*/ get text(){ return getCachedProp(this, 'text') }, set text(value){ return setCachedProp(this, 'text', value) }, }) //--------------------------------------------------------------------- var toc = require('./dom/toc') var wikiword = require('./dom/wikiword') //var textarea = require('./dom/textarea') var pWikiPageElement = module.pWikiPageElement = // XXX CACHE... object.Constructor('pWikiPageElement', CachedPage, { /*/ object.Constructor('pWikiPageElement', Page, { //*/ dom: undefined, domFilters: { toc: toc.makeToc, // XXX see Page.filters.wikiword for notes... wikiword: wikiword.wikiWordText, //textarea: textarea.setupTextarea, }, // XXX CACHE __clone_constructor__: CachedPage, /*/ __clone_constructor__: Page, //*/ __clone_proto: undefined, get __clone_proto__(){ return (this.__clone_proto = this.__clone_proto ?? this.__clone_constructor__('/', '/', this.store)) }, set __clone_proto__(value){ this.__clone_proto = value }, actions: { ...CachedPage.prototype.actions, hash: true, }, // NOTE: setting location will reset .hash set it directly via either // one of: // .location = [path, hash] // .location = 'path#hash' hash: undefined, // NOTE: getting .location will not return the hash, so as not to force // the user to parse it out each time. get location(){ return object.parentProperty(pWikiPageElement.prototype, 'location') .get.call(this) }, set location(value){ var [value, hash] = // .location = [path, hash] value instanceof Array ? value // .location = '#' : value.includes('#') ? value.split('#') // no hash is given... : [value, undefined] this.hash = hash object.parentProperty(pWikiPageElement.prototype, 'location') .set.call(this, value) }, // events... // __pWikiLoadedDOMEvent: new Event('pwikiloaded'), onLoad: types.event.PureEvent('onLoad', function(){ this.dom.dispatchEvent(this.__pWikiLoadedDOMEvent) }), // XXX CACHE... __last_refresh_location: undefined, refresh: types.event.Event('refresh', async function(full=false){ // drop cache if re-refreshing or when full refresh requested... // XXX CACHE... ;(full || this.__last_refresh_location == this.location) && this.cache && (this.cache = null) this.__last_refresh_location = this.location var dom = this.dom dom.innerHTML = await this.text for(var filter of Object.values(this.domFilters)){ filter && filter.call(this, dom) } this.trigger('onLoad') return this }), // handle dom as first argument... __init__: function(dom, ...args){ if(typeof(Element) != 'undefined'){ if(dom instanceof Element){ this.dom = dom } else { args.unshift(dom) } } return object.parentCall(pWikiPageElement.prototype.__init__, this, ...args) }, }) //--------------------------------------------------------------------- // System pages/actions... // XXX move this to a more appropriate module... var System = module.System = { // base templates... // // These are used to control how a page is rendered. // // pWiki has to have a template appended to any path, if one is not // given then "_view" is used internally. // // A template is rendered in the context of the parent page, e.g. // for /path/to/page, the actual rendered template is /path/to/page/_view // and it is rendered from /path/to/page. // // A template is any page named starting with an underscore ("_") // thus it is not recommended to use underscores to start page names. // // The actual default template is controlled via .PAGE_TEMPLATE // // Example: // _list: { // text: '- @source(.)' }, // // XXX might be a good idea to add a history stack to the API (macros?) // XXX all of these should support pattern pages... _text: { text: '@include(.:$ARGS isolated join="@source(file-separator)")' }, _view: { text: object.doc` 🡠 🡢 🡡    [@source(./location/quote/!)]   

@source(./title/quote/!)

@include(.:$ARGS join="@source(file-separator)" recursive="")
` }, // XXX add join... _raw: { text: '@quote(.)' }, // XXX not sure if this is the right way to go... _code: { text: '' +'
' +'
'}, /* XXX can we reuse _view here??? _edit: { text: '@include(PageTemplate)' +'@source(./path)' +'' +'' +'
'
						+''
					+'
' +'
' +'
'}, /*/ _edit: { text: '@source(./path/quote/!)' +'
' +'' +'

' +'@source(./title/quote)' +'

' +'
'
					+''
				+'
' +'
'}, edit: { // XXX not sure if we should use .title or .name here... text: object.doc`

@source(./title/quote) @macro(src="." strict else='new')


					/>
@macro(titleeditor .:$ARGS) @macro(texteditor .:$ARGS) @source(../title) (edit) ../.. @source(../location/quote/!) `}, // XXX EXPERIMENTAL... ed: { text: object.doc` @source(../ed-visual) `}, 'ed-visual': { text: object.doc` @load(./edit)
@quote(./html)
visual | text
`}, 'ed-text': { text: object.doc` @load(./edit)
visual | text
`}, // XXX debug... _path: {text: '@source(./path/! join=" ")'}, _location: {text: '@source(./location/! join=" ")'}, list: { text: object.doc` 🡠 🡢 🡡    @source(../path) @var(path "@source(s ./path)") @source(./name/quote) a s (@include(./*/length/!))   × ` }, // XXX this is really slow... // XXX need to handle count/offset arguments correctly... // ...for this we'll need to be able to either: // - count our own pages or // - keep a global count // ...with offset the issue is not solvable because we will not // see/count the children of skipped nodes -- the only way to // solve this is to completely handle offset in macro... tree: { text: object.doc` @var(path "@source(s ./path)")
@macro(tree "./*:$ARGS:count=inherit")
` }, /* XXX @var(..) vs. multiple @source(..) calls are not that different... tree2: { text: object.doc` This is a comparison with [../tree] -- \\@var(..) vs direct macro call...

@macro(tree "./*:$ARGS")
` }, //*/ all: { text: `@include("../**/path:$ARGS" join="@source(line-separator)")`}, info: { text: object.doc` @source(../title/quote) (info)

@source(../title/quote)

Path: [@source(../path/quote)] (edit)
Resolved path: [/@source(../resolved/quote)]
Referrer: [@source(../referrer/quote)]
Args:
type: @source(../type)
tags: @source(.)
related tags:
ctime: @source(../ctime)
mtime: @source(../mtime)

Resolved text:
` }, // XXX need to also be able to list things about each store... stores: function(){ return Object.keys(this.store.substores ?? {}) }, // tags... // // XXX should these be actions??? // ...actions do not yet support lists/generators... tags: async function*(){ yield* this.get('..').tags }, allTags: async function*(){ yield* Object.keys((await this.store.tags).tags) }, relatedTags: async function*(){ yield* this.store.relatedTags( ...((await this.args.tags) ?? this.get('..').tags ?? [])) }, // page parts... // 'line-separator': { text: '
' }, 'file-separator': { text: '
' }, // base system pages... // // NOTE: these are last resort pages, preferably overloaded in /Templates. // ParseError: { text: object.doc`
ParseError: @(msg "no message")
Page: [@(path "@source(./path/quote)")]
`,}, RecursionError: { text: 'RECURSION ERROR: @source(../path/quote)' }, NotFoundError: { //text: 'NOT FOUND ERROR: @source(./path)' }, text: object.doc`

NOT FOUND ERROR: @source(./path)

Nested pages:
` }, NotFoundTemplateError: { text: 'NOT FOUND TEMPLATE ERROR: @source(../path/quote)' }, DeletingPage: { text: 'Deleting: @source(../path/quote)' }, PageTemplate: { text: object.doc` @source(./path/quote)/edit

` }, QuoteActionPage: { text: '[ native code ]' }, Style: { text: object.doc` ` }, // page actions... // // XXX this does not work as energetic... time: async function(){ var t = Date.now() var text = await this.get('../_text:$ARGS').text var time = Date.now() - t console.log('RENDER TIME:', time) return object.doc` Time to render: ${time}ms

${text}`}, // XXX EXPERIMENTAL -- page types... isAction: async function(){ return await this.get('..').type == 'action' ? 'action' : undefined }, isStore: async function(){ return await this.get('..').type == 'store' ? 'store' : undefined }, /* XXX need a stable way to redirect after the action... // ...not sure if these are needed vs. pwiki.delete(..) and friends... // actions... // // XXX should ** be the default here... delete: function(){ var target = this.get('..') console.log('DELETE:', target.path) target.isPattern ? target.delete() : target.delete('**') // XXX if(this.referrer == this.path){ this.renderer.path = '..' return '' } // redirect... this.renderer && (this.renderer.location = this.referrer) // XXX this should not be needed... && this.renderer.refresh() // XXX returning undefined will stop the redirect... return '' }, // NOTE: this moves relative to the basedir and not relative to the // page... move: async function(){ var from = this.get('..') // XXX this is ugly... // ...need to standardize how we get arguments when rendering.... var to = this.args.to || (this.renderer || {args:{}}).args.to console.log('MOVE:', from.path, to) to && await (from.isPattern ? from : from.get('**')) .move( /^[\\\/]/.test(to[0]) ? to : pwpath.join('..', to)) // redirect... this.renderer && to //&& (this.renderer.location = this.referrer) && (this.renderer.location = from.path) // XXX this should not be needed... && this.renderer.refresh() // XXX if we return undefined here this will not fully redirect, // keeping the move page open but setting the url to the // redirected page... return '' }, // XXX copy/... //*/ // XXX System/sort // XXX System/reverse } var Templates = module.Templates = { // XXX should this be in templates??? // XXX for some reason this does not list files... FlatNotes: { text: object.doc` @var(editor_template FlatNotes/EmptyPage) 🗎 @var(path "@source(s ./path)") Empty ` }, // XXX this is not resolved... 'FlatNotes/EmptyPage': {text: ' '}, } var Test = module.Test = { // XXX do we support this??? //'list/action': function(){ // return [...'abcdef'] }, 'list/generator': function*(){ yield* [...'abcdef'] }, 'list/static': { text: [...'abcdef'] }, // this is shown by most listers by adding an :all argument to the url... '.hidden': { text: 'Hidden page...' }, '.hidden/subpage': { text: 'visible subpage...' }, '.hidden/.hidden': { text: 'hidden subpage...' }, slots: { text: object.doc` Sequential: unfilled filled refilled

Nested: unfilled filled refilled

Content: A B C: A B C

Nested content: A B C: A B C

Mixed content: X A B C Z: X A B C Z ` }, macros: { text: object.doc` - @include(./path)

`}, Subtree: { text: object.doc` This is here to test the performance of macros:
./list
./tree
./**/path
` }, } // Generate pages... PAGES=100 for(var i=0; i' }, // XXX need an import button... Export: { text: '' }, // XXX Config: { text: object.doc`{ }` }, Style: { text: object.doc` ` }, } /********************************************************************** * vim:set ts=4 sw=4 nowrap : */ return module })