From 246392f888bcf47500ce6d1468ae010e97591e7c Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Thu, 4 Aug 2022 11:00:21 +0300 Subject: [PATCH] split things up + playing with browser... Signed-off-by: Alex A. Naanou --- .gitignore | 1 + browser.js | 27 + page.js | 1086 +++++++++++++++++++++++++++++++ parser.js | 496 +++++++++++++++ pwiki2-test.js | 127 ++++ pwiki2.html | 62 ++ pwiki2.js | 1655 +----------------------------------------------- 7 files changed, 1824 insertions(+), 1630 deletions(-) create mode 100755 browser.js create mode 100755 page.js create mode 100755 parser.js create mode 100755 pwiki2-test.js create mode 100755 pwiki2.html diff --git a/.gitignore b/.gitignore index 5dca793..0deea7b 100755 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ Session.vim *.zip node_modules targets + diff --git a/browser.js b/browser.js new file mode 100755 index 0000000..0101def --- /dev/null +++ b/browser.js @@ -0,0 +1,27 @@ +/********************************************************************** +* +* +* +**********************************************************************/ +((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') + +var pwpath = require('./lib/path') + +var basestore = require('./store/base') +var localstoragestore = require('./store/localstorage') + +// XXX for some reason this does not run quietly in browser +//var pouchdbstore = require('./store/pouchdb') + +// XXX this fails silently in browser... +//var bootstrap = require('./bootstrap') + + + +/********************************************************************** +* vim:set ts=4 sw=4 : */ return module }) diff --git a/page.js b/page.js new file mode 100755 index 0000000..e2227b4 --- /dev/null +++ b/page.js @@ -0,0 +1,1086 @@ +/********************************************************************** +* +* +* +**********************************************************************/ +((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) +(function(require){ var module={} // make module AMD/node compatible... +/*********************************************************************/ + +// XXX +//var object = require('lib/object') +var object = require('ig-object') +var types = require('ig-types') + +var pwpath = require('./lib/path') +var parser = require('./parser') + +var basestore = require('./store/base') + +//var localstoragestore = require('./store/localstorage') +// XXX for some reason this does not run quietly in browser +//var pouchdbstore = require('./store/pouchdb') +//var filestore = require('./store/file') + + +//--------------------------------------------------------------------- +// Page... + +var relProxy = +function(name){ + var func = function(path='.', ...args){ + return this.store[name]( + pwpath.relative(this.location, path), + ...args) } + Object.defineProperty(func, 'name', {value: name}) + return func } +var relMatchProxy = +function(name){ + var func = function(path='.', strict=this.strict){ + if(path === true || path === false){ + strict = path + path = '.' } + return this.store[name]( + pwpath.relative(this.location, path), + strict) } + Object.defineProperty(func, 'name', {value: name}) + return func } + +var __HANDLE_NAVIGATE = +module.__HANDLE_NAVIGATE = + types.event.EventCommand('HANDLE_NAVIGATE') + +// XXX PATH_VARS +// XXX HISTORY do we need history management??? +// XXX FUNC need to handle functions in store... +var BasePage = +module.BasePage = +object.Constructor('BasePage', { + // NOTE: this can be inherited... + //store: undefined, + + // root page used to clone new instances via the .clone(..) method... + //root: undefined, + + // Path variables... + // + // XXX PATH_VARS should these be here??? + // other places path variables can be resolved: + // - navigation (below) + // - macro expansion... + // XXX EXPERIMENTAL... + path_vars: { + NOW: function(){ + return Date.now() }, + PATH: function(){ + return this.path }, + NAME: function(){ + return this.name }, + DIR: function(){ + return this.dir }, + //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 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 .onNavigate(..) method... + __location: undefined, + get location(){ + return this.__location ?? '/' }, + set location(path){ + // trigger the event... + this.onNavigate(path) }, + // referrer -- a previous page location... + referrer: undefined, + + // events... + // + // XXX revise naming... + // XXX should this be able to prevent navigation??? + onBeforeNavigate: types.event.Event('beforeNavigate'), + onNavigate: types.event.Event('navigate', + function(handle, path){ + // special case: we are triggering handlers only... + // NOTE: this usually means that we are setting .__location + // externally... + // XXX HISTORY this is only used for history at this point... + if(path === module.__HANDLE_NAVIGATE){ + handle() + return } + this.onBeforeNavigate(path) + this.referrer = this.location + var cur = this.__location = + this.resolvePathVars( + pwpath.relative( + this.location, + path)) + //* XXX HISTORY... + if(this.history !== false){ + this.history.includes(this.__location) + && this.history.splice( + this.history.indexOf(this.__location)+1, + this.history.length) + this.history.push(cur) } + // trigger handlers... + handle() }), + + // .path is a proxy to .location + // XXX do we need this??? + get path(){ + return this.location }, + set path(value){ + this.location = value }, + + // XXX do we need this... + get resolvedPath(){ + return this.match() }, + + // XXX should these be writable??? + get name(){ + return pwpath.split(this.path).pop() }, + //set name(value){ }, + get dir(){ + return pwpath.relative(this.location, '..') }, + //set dir(value){ }, + get isPattern(){ + return this.location.includes('*') }, + + // history... + // + //* XXX HISTORY... + // NOTE: set this to false to disable history... + __history: undefined, + get history(){ + if(this.__history === false){ + return false } + if(!this.hasOwnProperty('__history')){ + this.__history = [] } + //this.__history = (this.__history ?? []).slice() } + return this.__history }, + back: function(offset=1){ + var h = this.history + if(h === false + || h.length <= 1){ + return this } + // get position in history... + var p = h.indexOf(this.location) + // if outside of history go to last element... + p = p < 0 ? + h.length + : p + p = Math.max( + Math.min( + h.length-1 + - p + + offset, + h.length-1), + 0) + this.onBeforeNavigate(this.path) + this.referrer = this.location + var path = this.__location = h[h.length-1 - p] + this.onNavigate(module.__HANDLE_NAVIGATE, path) + return this }, + forward: function(offset=1){ + return this.back(-offset) }, + //*/ + + // 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.location, data) }, + __delete__: function(path='.'){ + return this.store.delete(pwpath.relative(this.location, path)) }, + + // page data... + // + strict: undefined, + get data(){ return (async function(){ + var that = this + // NOTE: we need to make sure each page gets the chance to handle + // its context.... + if(this.isPattern){ + return this + .map(function(page){ + return page.data }) } + // single page... + var res = await this.store.get(this.location, !!this.strict) + return typeof(res) == 'function' ? + res.bind(this) + : res }).call(this) }, + //return this.store.get(this.location, !!this.strict) }, + set data(value){ + this.__update__(value) }, + + // 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.location) }, + set metadata(value){ + this.__update__(value) }, + + // 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'), + find: relProxy('find'), + match: relMatchProxy('match'), + resolve: relMatchProxy('resolve'), + delete: function(path='.'){ + this.__delete__() + return this }, + + // + // .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.location, + strict, + }) }, + + // XXX should this be an iterator??? + each: function(path){ + var that = this + // NOTE: we are trying to avoid resolving non-pattern paths unless + // we really have to... + path = path ? + pwpath.relative(this.path, path) + : this.path + //var paths = this.match(path) + var paths = path.includes('*') ? + this.resolve(path) + : path + paths = paths instanceof Array ? + paths + : paths instanceof Promise ? + paths.iter() + : [paths] + return paths + .map(function(path){ + return that.get('/'+ path) }) }, + + 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 should this be page-level (current) store level??? + // XXX when this is async, should this return a promise???? + sort: async function(cmp){ + // not sorting single pages... + if(this.length <= 1){ + return this } + // sort... + this.metadata = { order: await this.each() + .sort(...arguments) + .map(function(p){ + return p.path }) } + return this }, + reverse: async function(){ + // not sorting single pages... + if(this.length <= 1){ + return this } + this.metadata = { order: (await this.match()).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 + // + // XXX HISTORY should we clear history by default... + 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 + return Object.assign( + full ? + // full copy... + this.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(this.root ?? this), + { + root: this.root ?? this, + location: this.location, + referrer: this.referrer, + }, + // XXX HISTORY... + this.__history !== false ? + { __history: + history ? + (this.__history ?? []).slice() + : [] } + :{}, + //*/ + 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: function(...data){ + return Object.assign(this, ...data) }, + + __init__: function(path, referrer, store){ + // 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) + + + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// XXX should these be something more generic like Object.assign(..) ??? + +// XXX revise... +var Filter = +module.Filter = +function(...args){ + var func = args.pop() + args.length > 0 + && Object.assign(func, args.pop()) + return func } + +// XXX do we need anything else like .doc, attrs??? +var Macro = +module.Macro = +function(spec, func){ + var args = [...arguments] + // function... + func = args.pop() + // arg sepc... + ;(args.length > 0 && args[args.length-1] instanceof Array) + && (func.arg_spec = args.pop()) + // XXX do we need anything else like .doc, attrs??? + return func } + + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +module.PAGE_NOT_FOUND = '404: PAGE NOT FOUND: $PATH' + +// XXX PATH_VARS need to handle path variables... +// XXX filters (and macros?) should be features for simpler plugin handlng (???) +// XXX STUB filters... +var Page = +module.Page = +object.Constructor('Page', BasePage, { + __parser__: parser.parser, + + // 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_TPL: '_text', + + // 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. + PAGE_NOT_FOUND: module.PAGE_NOT_FOUND, + + // + // () + // -> + // -> 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') }, + + wikiword: Filter( + {quote: 'quote-wikiword'}, + function(source){ + // XXX + return source }), + 'quote-wikiword': function(source){ + // XXX + return source }, + + markdown: Filter( + {quote: 'quote-markdown'}, + function(source){ + // XXX + return source }), + 'quote-markdown': function(source){ + // XXX + return source }, + }, + + // + // (, , ){ .. } + // -> undefined + // -> + // -> + // -> + // -> () + // -> ... + // + // XXX ASYNC make these support async page getters... + macros: { + // + // + // + now: function(){ + return ''+ Date.now() }, + // + // @filter() + // /> + // + // > + // ... + // + // + // ::= + // + // | - + // + filter: function(args, body, state, expand=true){ + var that = this + var filters = state.filters = + state.filters ?? [] + // separate local filters... + if(body){ + var outer_filters = filters + filters = state.filters = + [outer_filters] } + + // merge in new filters... + var local = Object.keys(args) + filters.splice(filters.length, 0, ...local) + + // trigger quote-filter... + var quote = local + .map(function(filter){ + return (that.filters[filter] ?? {})['quote'] ?? [] }) + .flat() + quote.length > 0 + && this.macros['quote-filter'] + .call(this, Object.fromEntries(Object.entries(quote)), null, state) + + // local filters... + if(body){ + // isolate from parent... + state.filters.includes(this.ISOLATED_FILTERS) + && state.filters[0] instanceof Array + && state.filters.shift() + + // expand the body... + var ast = expand ? + // XXX async... + //[...this.__parser__.expand(this, body, state)] + this.__parser__.expand(this, body, state) + : body instanceof Array ? + body + // NOTE: wrapping the body in an array effectively + // escapes it from parsing... + : [body] + filters = state.filters + + state.filters = outer_filters + + // parse the body after we are done expanding... + return async function(state){ + var outer_filters = state.filters + state.filters = this.__parser__.normalizeFilters(filters) + var res = + this.parse(ast, state) + .iter() + .flat() + .join('') + state.filters = outer_filters + return { data: await res } } } }, + // + // @include() + // + // @include( isolated recursive=) + // @include(src= isolated recursive=) + // + // .. > + // + // + // + // XXX RECURSION recursion detection is still a bit off... + // XXX 'text' argument is changed to 'recursive'... + // XXX revise recursion checks.... + // XXX should this be lazy??? + include: Macro( + ['src', 'recursive', ['isolated']], + async function(args, body, state, key='included', handler){ + var macro = 'include' + if(typeof(args) == 'string'){ + var [macro, args, body, state, key="included", handler] = arguments } + // positional args... + var src = args.src + var recursive = args.recursive || body + var isolated = args.isolated + + if(!src){ + return } + // parse arg values... + src = await this.parse(src, state) + var full = this.get(src).path + + handler = handler + ?? function(){ + return this.get(src) + .parse( + isolated ? + {[key]: state[key]} + : state) } + + // handle recursion... + var parent_seen = state[key] + var seen = state[key] = + state[key] + ?? [] + // recursion detected... + if(seen.includes(full)){ + if(!recursive){ + throw new Error( + macro +': recursion detected: ' + + seen.concat([full]).join(' -> ')) } + // have the 'recursive' arg... + return this.parse(recursive, state) } + seen.push(full) + + // load the included page... + var res = await handler.call(this) + + // restore previous include chain... + if(parent_seen){ + state[key] = parent_seen + } else { + delete state[key] } + + return res }), + source: Macro( + ['src'], + async function(args, body, state){ + var src = args.src + // parse arg values... + src = src ? + await this.parse(src, state) + : src + return this.macros.include.call(this, + 'source', + args, body, state, 'sources', + async function(){ + return await this.parse(await this.get(src).raw +'', state) }) }), + // + // @quote() + // + // [ filter=" ..."]/> + // + // + // + // + // .. + // + // + // + // NOTE: src ant text arguments are mutually exclusive, src takes + // priority. + // NOTE: the filter argument has the same semantics as the filter + // macro with one exception, when used in quote, the body is + // not expanded... + // + // XXX need a way to escape macros -- i.e. include in a quoted text... + quote: Macro( + ['src', 'filter', 'text'], + async function(args, body, state){ + var src = args.src //|| args[0] + var text = args.text + ?? body + ?? [] + // parse arg values... + src = src ? + await this.parse(src, state) + : src + text = src ? + // source page... + await this.get(src).raw + : text instanceof Array ? + text.join('') + : text + + // empty... + if(!text){ + return } + + var filters = + args.filter + && args.filter + .trim() + .split(/\s+/g) + + // NOTE: we are delaying .quote_filters handling here to + // make their semantics the same as general filters... + // ...and since we are internally calling .filter(..) + // macro we need to dance around it's architecture too... + // NOTE: since the body of quote(..) only has filters applied + // to it doing the first stage of .filter(..) as late + // as the second stage here will have no ill effect... + return function(state){ + // add global quote-filters... + filters = + (state.quote_filters + && !(filters ?? []).includes(this.ISOLATED_FILTERS)) ? + [...state.quote_filters, ...(filters ?? [])] + : filters + if(filters){ + filters = Object.fromEntries(Object.entries(filters)) + return this.macros.filter + .call(this, filters, text, state, false) + .call(this, state) } + return text } }), + // very similar to @filter(..) but will affect @quote(..) filters... + 'quote-filter': function(args, body, state){ + var filters = state.quote_filters = + state.quote_filters ?? [] + filters.splice(filters.length, 0, ...Object.keys(args)) }, + // + // /> + // + // text=/> + // + // > + // ... + // + // + // Force show a slot... + // + // + // Force hide a slot... + //