/********************************************************************** * * * XXX this seems a bit overcomplicated... * **********************************************************************/ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ var object = require('lib/object') var actions = require('lib/actions') var features = require('lib/features') var macro = require('macro') /*********************************************************************/ // Split path into a list and handle special path elements... // // The path is split via '/' or '\', no difference is made between the // two styles, e.g. 'a/b' is the same as 'a\b'. // // Consecutive '/' or '\' are treated as one, e.g. 'a///b' and 'a/b' // are the same. // // Special path items supported: // "." - Current position indicator // this is simply removed from positions other than 0 // a/./b/c -> a/b/c // ./b/c -> ./b/c // ".." - Consumes path item above (pop) / up one level // a/../b/c -> b/c // ../b/c -> ../b/c // ">>" - Consumes path item below (shift) // a/>>/b/c -> a/c // a/b/c/>> -> a/b/c (XXX ???) // // NOTE: the path is handled out of context, this leading '.' and '..' // are left as-is. // NOTE: '>>' has no effect when at last position (XXX ???) var path2list = module.path2list = function(path){ return (path instanceof Array ? path : path.split(/[\\\/]+/g)) // handle '..' (lookahead) and trim path elements... // NOTE: this will not touch the leading '.' or '..' .map(function(p, i, l){ // remove '..' and '.' out at positions > 0... return (i > 0 && (p.trim() == '..' || p.trim() == '.') // remove items followed by '..'... || (l[i+1] || '').trim() == '..' // remove items preceded by '>>'... || (l[i-1] || '').trim() == '>>') ? null : p.trim() }) // cleanup and clear '>>'... .filter(function(p){ return p != null && p != '' && p != '>>' })} // Normalize path... // // This is the same as path2list(..) but also joins the path with '/' var normalizePath = module.normalizePath = function(path){ return path2list(path).join('/') } // XXX add pattern to match all subpaths above... // Ex: // 'a/b/c/^' shold mathc 'a/b/c', 'a/b', 'a' var path2re = module.path2re = function(path){ return RegExp('^' +normalizePath(path) /*/ quote regexp chars... .replace(/([\.\\\/\(\)\[\]\$\+\-\{\}\@\^\&\?\<\>])/g, '\\$1') /*/// XXX experimental... .replace(/([\.\\\/\(\)\[\]\$\+\-\{\}\@\^\&\?\>])/g, '\\$1') // '<' -> handle subpath matching... // XXX this needs * up the stack... .replace(/.* 0 ? res[res.length-1] +'\/' : '') + e]) }, []) .join('|') })` }) //*/ // convert '*' and '**' to regexp... .replace(/\*\*/g, '.*') .replace(/^\*|([^.])\*/g, '$1[^\\/]*') +'$')} /*********************************************************************/ // pWiki featureset... var pWikiFeatures = module.pWikiFeatures = new features.FeatureSet() /* // base pWiki object... var pWiki = module.pWiki = object.Constructor('pWiki', actions.MetaActions) // base instance constructor... pWikiFeatures.__actions__ = function(){ return actions.Actions(pWiki()) } //*/ /*********************************************************************/ var BaseData = module.BaseData = { // Macro acces to standard page attributes (paths)... 'System/title': function(){ return { text: this.get('..').title() } }, 'System/path': function(){ return { text: this.base() } }, 'System/dir': function(){ return { text: this.get('..').base() } }, 'System/location': function(){ return { text: this.base() } }, 'System/resolved': function(){ return { text: this.get('..').acquire() } }, // page data... // // NOTE: special case: ./raw is treated a differently when getting .text // i.e: // .get('./raw').text // is the same as: // .get('.').raw 'System/raw': function(){ return { text: this.get('..').raw() } }, 'System/html': function(){ return { text: this.get('..').html() } }, // list all path elements on a level, including empty path sections... // XXX update these to the new format -- must return an object... // XXX move this to Wiki.children + rename... // XXX 'System/list': function(){ return 'NoImplemented' var p = this.dir return Object.keys(this.__wiki_data) .map(function(k){ if(k.indexOf(p) == 0){ return path2lst(k.slice(p.length)).shift() } return null }) .filter(function(e){ return e != null }) .sort() .map(function(e){ return '['+ e +']' }) .join('
') }, // list links to this page... // XXX this is done, though we cant use this until we solve .html(..) // macro recursion issues... // XXX cache the result + need a strategy to drop parts of cache when // irrelevant -- when path/text changes... // XXX might be a good idea to move this to the store, at least the // management, part... 'System/links': function(){ return 'NoImplemented' var that = this var p = this.path() var res = [] this.wiki.match('**') .forEach(function(p){ var pa = that.acquire(p) that.get(p) // XXX this will render the page which might not be // the best idea in some cases... .links() .forEach(function(l){ var la = that.acquire(l) if(l == p || la == p || la == pa){ res.push([l, p]) } }) }) // cache the result... // XXX this.attr('rev-links', res) return res //.map(function(e){ return '['+ e[0] +'] from page: ['+ e[1] +']' }) .map(function(e){ return '['+ e[1] +'] -> ['+ e[0] +']' }) .sort() .join('
') }, // Page modifiers/actions... // XXX these needs redirecting... // ...not sure if using history here is the right way to go... 'System/_sort': function(){ this.get('..').sort() }, 'System/sort': function(){ // XXX does not work for some reason... //this.get('../_sort') this.get('..').sort() history && history.back() }, 'System/_reverse': function(){ this.get('..').reverse() }, 'System/reverse': function(){ // XXX does not work for some reason... //this.get('../_reverse') this.get('..').reverse() history && history.back() }, 'System/_delete': function(){ this.get('..').clear() }, 'System/delete': function(){ // XXX does not work for some reason... //this.get('../_delete') this.get('..').clear() history && history.back() }, 'System/back': function(){ history.go(-2) }, // XXX not sure how to deal with this... //'System/foreward': function(){ // history.go(1) }, // XXX need to support simple functions... // ...return a list to simulate a list of pages... 'System/test': function(){ return ['list', 'of', 'links'] }, } /*********************************************************************/ // XXX should this be art of the main API or a separate entity??? // XXX should we combine page and wiki api??? // - pWikiData is wiki api // - pWiki is page api var pWikiData = module.pWikiData = { __data: null, // XXX search: function(query, sort){ }, // Get a list of matching paths... // // XXX sort path API... // ...should we be able to spec sort in path??? // XXX should we account for order here??? match: function(path, sort, count, from){ var data = this.__data || {} from = from || 0 // XXX normalize this to account for '*' //var order = (data[path] || {}).order || [] if(path == null){ return [] } // strict path... if(path.indexOf('*') < 0){ return path in data ? [ path ] : [] } sort = sort || (data[path] || {}).sort || ['order'] sort = sort instanceof Array ? sort : [sort] var order = (data[path] || {}).order || [] var pattern = path2re(path) return Object.keys(data) // XXX is this correct??? .concat(Object.keys(data.__proto__) // do not repeat overloaded stuff... .filter(function(e){ return !data.hasOwnProperty(e) })) .filter(function(p){ return pattern.test(p) }) // page... .slice(from, count ? from + count : undefined) // prepare to sort... .map(function(p, i){ return sort .map(function(method){ // explicit order... if(method instanceof Array){ i = method.indexOf(p) i = i < 0 ? method.indexOf('*') : i i = i < 0 ? method.length : i return i } // drop the reversal marker... method = method[0] == '-' ? method.slice(1) : method // stored order... if(method == 'order'){ i = order.indexOf(p) i = i < 0 ? order.indexOf('*') : i i = i < 0 ? order.length : i return i } return method == 'path' ? p.toLowerCase() : method == 'Path' ? p : method == 'title' ? path2list(p).pop().toLowerCase() : method == 'Title' ? path2list(p).pop() // special case... : method == 'checked' ? (data[p][method] ? 1 : 0) // attr... : data[p][method] }) .concat([i, p]) }) // sort... .sort(function(a, b){ for(var i=0; i < sort.length+1; i++){ var reverse = (sort[i] || '')[0] == '-' ? -1 : 1 if(a[i] == b[i]){ continue } return (a[i] > b[i] ? 1 : -1) * reverse } return 0 }) // cleanup... .map(function(e){ return e.pop() }) }, // Get/set data at path... // // XXX should this overwrite or expand??? // XXX should from be pattern compatible??? data: function(path, value){ // get the data... if(value == null){ if(this.__data == null){ return null } var data = this.__data[path] return data == null ? null : data instanceof Function ? data : JSON.parse(JSON.stringify(data)) // set the data... } else { this.__data = this.__data || {} this.__data[path] = JSON.parse(JSON.stringify(value)) return this } }, // Move data from path to path... // // XXX should from be pattern compatible??? move: function(from, to){ if(this.__data == null){ return } var d = this.__data[from] this.clear(from) this.__data[to] = d return this }, // Clear a path... // clear: function(path){ if(this.__data == null){ return this } this.remove(this.match(path)) return this }, // explicitly remove path... // // NOTE: this is similar to .clear(..) but will not expand patterns, // thus only one page is is removed per path. remove: function(path){ path = arguments.length > 1 ? [].slice.call(arguments) : path instanceof Array ? path : [path] var data = this.__data path.forEach(function(p){ delete data[p] }) return this }, // XXX json: function(data){ if(arguments.length == 0){ return JSON.parse(JSON.stringify(this.__data)) } else { this.__data = data } }, } /*********************************************************************/ // Base pWiki page API... // // Page data format: // { // 'order': [ , .. ] | undefined, // 'order-unsorted-first': <bool>, // // 'text': <string>, // // // XXX not yet used... // 'links': [ .. ], // } // var pWikiBase = module.pWikiBase = actions.Actions({ config: { 'home-page': 'WikiHome', 'default-page': 'EmptyPage', 'no-match-page': 'NoMatch', 'system-path': 'System', 'acquesition-order': [ 'Templates', ], 'post-acquesition-order': [], 'order-unsorted-first': false, // sorting... 'default-sort-methods': [ 'path', ], }, // pWikiData... wiki: null, // XXX should this be local/dump??? json: ['', function(){ }], // Location and path API... refresh: ['', function(force){ // get/set location and base fields... var location = this.__location = this.__location || {} var path = location.path = location.path || this.config['home-path'] || 'WikiHome' var at = location.at || 0 // get location cache... var match = location.match // refresh the cache... if(match == null || force){ this.order(force) } }], location: ['Page/Get or set location', function(value){ if(value === null){ return } var location = this.__location || this.refresh().location() // get location... if(arguments.length == 0){ return location } // set location index... if(typeof(value) == typeof(123)){ location.at = value // set location path... } else if(typeof(value) == typeof('str')){ this.__location = { path: this.resolve(value), at: 0, } // object... } else { this.__location = value // NOTE: we are returning here without a refresh to avoid // recursion... // NOTE: a refresh will get called when the location value // is accessed for the first time... // XXX should we clear .match here??? return } this.refresh(true) }], exists: ['Page/Check if path explicitly exists.', function(path){ var at = path ? 0 : this.at() path = path || this.path() return this.wiki.match(this.get(path).location().path)[at] !== undefined }], // Resolve path statically... // // This will: // - expand variables // - resolve relative paths ('.', '..', and '>>') // // Supported variables: // $NOW - resolves to current date (same as Date.now()) // // $PATH - resolves to page path (same as .path()) // $BASE - resolves to page base path (same as .base()) // $TITLE - resolves to page title (same as .title()) // // $INDEX - resolves to page index (same as .at()) // // NOTE: all variables are resolved relative to the page from which // .resolve(..) was called, e.g. the following two are equivalent: // <page>.resolve('$PATH') // <page>.path() // NOTE: this will not resolve path patterns ('*' and '**') resolve: ['Path/Resolve relative path and expand path variables', function(path){ path = path || this.path() // path variables... // XXX make this more modular... path = path // NOTE: these are equivalent to '..' and '.' but not // identical -- the variables are useful for things // like moving a page to: // "Trash/$PATH" // ...to move the above page out of trash move it to: // ">>/$PATH" .replace(/\$PATH|\$\{PATH\}/g, this.path()) .replace(/\$BASE|\$\{BASE\}/g, this.base()) .replace(/\$TITLE|\$\{TITLE\}/g, this.title()) .replace(/\$INDEX|\$\{INDEX\}/g, this.at()) .replace(/\$NOW|\$\{NOW\}/g, Date.now()) path = normalizePath(path) // relative paths -- "." and ".." if(path.indexOf('.') >= 0){ path = // '.' or './*' path == '.' || /^\.\//.test(path) ? normalizePath(path.replace(/^\./, this.path())) // '..' or '../*' : path == '..' || /^\.\.\//.test(path) ? normalizePath(path.replace(/^\.\./, this.base())) : path } return path }], // XXX should this get a page??? acquire: ['Path/Acquire the page path that the given path resolves to', function(path, no_default){ var that = this // handle paths and relative paths... var p = this.get(path || this.path()) var title = p.title() path = path2list(p.base()) var acquire_from = this.config['acquesition-order'] || [] var post_acquire_from = this.config['post-acquesition-order'] || [] var _get = function(path, title, lst){ lst = (lst == null || lst.length == 0) ? [''] : lst for(var i=0; i < lst.length; i++){ var p = normalizePath(path.concat([lst[i], title])) if(that.exists(p)){ return that.wiki.data(p) && p } } } while(true){ // get title from path... var p = _get(path, title) // get title from special paths in path... || _get(path, title, acquire_from) if(p != null){ return p } if(path.length == 0){ break } path.pop() } // default paths... var p = _get(path, title, post_acquire_from) // system path... || this.config['system-path'] && _get([this.config['system-path']], title) // NOTE: this may be null... return p || ((!no_default && title != this.config['default-page']) ? this.acquire('./'+this.config['default-page']) : null) }], // XXX pattern does not match anything needs to be handled correctly... // XXX do we need to normalize 'at'??? path: ['Page/Get or set path', function(value){ // get explcit path from location (acounting for 'at')... if(arguments.length == 0){ var location = this.location() return location.match[location.at] || this.config['no-match-page'] || '' // move page to path... } else if(value != null) { this.wiki.move(this.path(), this.resolve(value)) this.location(value) } }], title: ['Page/Get or set title', function(value){ if(arguments.length == 0){ return path2list(this.path()).pop() || '' } else if(value != null){ this.path(this.base() +'/'+ value) } }], base: ['Page/Get or set directory', function(base){ if(arguments.length == 0){ return path2list(this.path()).slice(0, -1).join('/') } else if(base != null){ this.path(base +'/'+ this.title()) } }], // Object API... // NOTE: a clone references the same data and .config, no copying // is done. clone: ['Page/Get page clone (new reference)', function(){ var o = Object.create(this) .location(JSON.parse(JSON.stringify(this.location()))) o.__parent_context = this return o }], end: ['Page/Get parent context of clone', function(){ return this.__parent_context || this }], // XXX should this return false on empty path??? copy: ['Page/Copy page to path', function(path){ return path != null && this .get(path) // NOTE: this is here mainly to maintain the context stack... .clone() .data(this.data()) }], get: ['Page/Get page by path', function(path){ return this .clone() .location(path) }], // Order and iteration API... get length(){ // special case -- non-pattern path... if(this.location().path.indexOf('*') < 0){ return 1 } this.refresh() return this.location().match.length }, at: ['Page/Get index or page at given index', function(n){ // get current index... if(n == null){ return this.location().at || 0 } // get page at index... var l = this.length // self... if(n == this.at()){ return this // out of bounds... } else if(n >= l || n < -l){ return null } var res = this.clone() n = n < 0 ? l - n : n // XXX do we min/max n??? n = Math.max(n, 0) n = Math.min(l-1, n) res.location(n) return res }], prev: ['Page/Get previous page', function(){ var i = this.at() - 1 // NOTE: need to guard against overflows... return i >= 0 ? this.at(i) : null }], next: ['Page/Get next page', function(){ return this.at(this.at() + 1) }], map: ['Page/', function(func){ var res = [] for(var i=0; i < this.length; i++){ var page = this.at(i) res.push(func.call(page, page, i)) } return res }], // NOTE: a filter can take a function or a string path pattern... // NOTE: only absolute path patterns are supported... filter: ['Page/', function(func){ // we got a sting pattern... if(typeof(func) == typeof('str')){ var pattern = path2re(func) func = function(page){ return pattern.test(page.path()) } } var res = [] for(var i=0; i < this.length; i++){ var page = this.at(i) func.call(page, page, i) && res.push(page) } return res }], each: ['Page/', function(func){ this.map(func) }], // XXX reduce??? // Get/set sibling order... // // Get order... // .order() // -> order // // Force get order... // .order(true) // .order('force') // -> order // NOTE: this will overwrite cache. // // Get saved order... // .order('saved') // -> order // // Save list of paths as order explicitly... // .order([<title>, .. ]) // -> page // // Save order persistently... // .order('save') // -> page // // Remove set order, local if available else persistent... // .order('clear') // -> page // // Remove all ordering... // .order('clear-all') // -> page // // // List of paths passed to .order(..) can contain a '*' to indicate // the pages not specified by the list. // By default all unspecified pages will get appended to the resulting // list, same as appending a '*' to the tail of the list passed to // .order(..) // // // NOTE: saving order to data is supported ONLY for paths that contain // one and only one pattern and in the last path segment... // NOTE: clearing persistent ordering will remove a page (parent) from // data if it contains nothing but the order... // NOTE: this will also maintain page position within order (.at()) // // NOTE: the actual sorting/ordering is done in .wiki.match(..) // // XXX should we also cache the saved sort and order??? // XXX (LEAK?) not sure if the current location where order is stored // is the right way to go -- would be really hard to clean out... // ...might be a good idea to clear pattern paths that match no // pages from data... order: ['Page/Get or set sibling pages order', function(order){ var location = this.location() var path = location.path || '' var page = (location.match || [])[location.at || 0] // get order... if(order == null || order == 'force' || order === true){ // no patterns in path -> no ordering... if(path.indexOf('*') < 0){ if(!location.match){ location.match = [ path ] this.location(location) } return [ path ] } // get cached order if not forced... if(location.match != null && order == null){ return location.match.slice() } // XXX should we check if this returns a function??? var parent = this.wiki.data(path) || {} var sort = (location.sort || parent.sort || ['order']).slice() var i = sort.indexOf('order') location.order && i >= 0 && sort.splice(i, 1, location.order) var order = this.wiki.match(path, sort) // filter out paths containing '*' .filter(function(p){ return p.indexOf('*') < 0 }) // save cache... location.match = order location.at = page ? order.indexOf(page) : 0 this.location(location) return order.slice() // get saved order... } else if(order == 'saved'){ return location.order // XXX should we check if this returns a function??? || (this.wiki.data(path) || {}).order || [] // clear order... // XXX should this: // - clear all always // - explicitly clear only local or persistent // - progressively clear local then persistent (current) } else if(order == 'clear' || order == 'clear-all'){ var local = !!location.order // local order... delete location.order // clear persistent order... if(!local || order == 'clear-all'){ // XXX should we check if this returns a function??? var parent = this.wiki.data(path) // persistent order... if(parent && parent.order){ delete parent.order // remove if empty... if(Object.keys(parent).length == 0){ this.wiki.remove(path) // save... } else { this.wiki.data(path, parent) } } } // save order... } else if(order == 'save'){ // XXX should we check if this returns a function??? var parent = this.wiki.data(path) || {} var order = parent.order = location.order || this.order() this.wiki.data(path, parent) delete location.order // set order... } else { location.order = order } // save cache... this.location(location) this.order(true) }], // Sort siblings... // // Sort pages via default method // .sort() // -> page // // Sort pages via method // .sort(method) // -> page // // Sort pages via method1, then method2, ... // .sort(method1, method2, ...) // .sort([method1, method2, ...]) // -> page // NOTE: the next method is used iff the previous concludes the // values equal... // // To reverse a specific method, prepend it's name with "-", e.g. // "title" will do the default ascending sort while "-title" will do // a descending sort. // // Supported methods: // path - compare paths (case-insensitive) // Path - compare paths (case-sensitive) // title - compare titles (case-insensitive) // Title - compare titles (case-sensitive) // checked - checked state // order - the set manual order (see .order(..)) // <attribute> - compare data attributes // // // NOTE: the sort is local to the returned object. // NOTE: the sorted object may loose sync form the actual wiki as the // list of siblings is cached. // ...the resulting object is not to be stored for long. // NOTE: the actual sorting is done by the store... // // XXX add 'save' and 'saved' actions... sort: ['Page/', function(methods){ var that = this var res = this.clone() var location = this.location() methods = methods instanceof Array ? methods : [].slice.call(arguments) location.sort = methods.length == 0 ? (this.config['default-sort-methods'] || ['path']) : methods res.location(location) res.order(true) return res }], // XXX should this be persistent??? // ...e.g. survive .order('force') or .order('clear') reverse: ['Page/', function(){ var location = this.location() // reverse the match... location.match && location.match.reverse() // reverse order... location.order = this.order().reverse() // reverse sort... if(location.sort){ location.sort = location.sort .map(function(m){ return m[0] == '-' ? m.slice(1) : '-'+m }) } this.location(location) }], // Data API... data: ['Page/Get or set data', function(value){ // get -> acquire page and get it's data... if(arguments.length == 0){ var d = this.wiki.data(this.acquire()) || {} return d instanceof Function ? d.call(this) : d // set -> get explicit path and set data to it... } else if(value != null) { this.wiki.data(this.path(), value || {}) } }], clear: ['Page/Clear page', function(){ this.wiki.clear(this.path()) }], attr: ['Page/Get or set attribute', function(name, value){ var d = this.data() // get... if(arguments.length == 1){ return d[name] === undefined ? // force returning undefined... actions.UNDEFINED : d[name] // clear... } else if(value === undefined){ delete d[name] // set... } else { d[name] = value } // write the data... // XXX is it good to write the whole thing??? this.data(d) }], // shorthands... raw: ['Page/', function(value){ return arguments.length == 0 ? (this.attr('text') || '') : this.attr('text', value) }], checked: ['Page/', function(value){ return arguments.length == 0 ? !!this.attr('checked') : this.attr('checked', value || undefined) }], // Init... // // Special config attrs: // wiki - wiki object // // NOTE: the input object may get modified... (XXX) __init__: [function(config){ config = config || {} if('wiki' in config){ this.wiki = config.wiki // XXX don't like modifying the input... delete config.wiki } var cfg = this.config = Object.create(this.config) return function(){ // copy the given config... Object.keys(config).forEach(function(k){ cfg[k] = JSON.parse(JSON.stringify(config[k])) }) } }], }) // Data processing and macros... // var pWikiMacros = module.pWikiMacros = actions.Actions(pWikiBase, { __macro_parser__: macro, config: { }, html: ['Page/', function(value){ // get... return arguments.length == 0 ? (this.title() == 'raw' ? // special case -- if title is 'raw' then return text as-is... (this.raw() || '') // parse macros... : (this.__macro_parser__ || pWikiMacros.__macro_parser__) .parse(this, this.raw())) // set... : this // clear cached stuff related to text... .attr('links', undefined) // set the value... .raw(value) }], code: ['Page/', function(value){ return arguments.length == 0 ? this.html().text() // XXX should we un-encode here??? : this.html(value) }], links: ['Page/List links from page', function(force){ // get and cache links... if(force || this.attr('links') == null){ var text = this.html() var links = typeof(text) == typeof('str') ? [] : text.find('[href]') .map(function(){ var url = $(this).attr('href') return url[0] == '#' ? url.slice(1) : null }) .toArray() this.attr('links', links) return links } // get cached links... return this.attr('links') }], // Init... // // Special config attrs: // macro - macro processor (optional) // __init__: [function(config){ if('macro' in config){ this.__macro_parser__ = config.macro // XXX don't like modifying the input... delete config.macro } }], }) // pWiki Page... // // NOTE: looks like multiple inheritance, feels like multiple inheritance // but sadly is not multiple inheritance... // ...though, functionally, this is 90% there, about as far as we // can get using native JS lookup mechanisms, or at least the // farthest I've pushed it so far... var pWikiPage = module.pWikiPage = object.Constructor('pWikiPage', actions.mix( // XXX not sure if we need this here... //actions.MetaActions, pWikiBase, pWikiMacros)) /*********************************************************************/ // Experiment with hidden promises... var hiddenPromise = module.hiddenPromise = { __promise: null, then: function(func){ var that = this // trigger lazy functions if present... if(this.__lazy != null){ var lazy = this.__lazy delete this.__lazy var res = this .then(lazy) .then(func) // clear any lazy stuff queued by the above to avoid any // side-effects... // // XXX should this be done here (sunc) or in a .then(..)??? delete this.__lazy return res } // no promise... if(this.__promise == null){ this.__promise = new Promise(function(resolve, reject){ resolve(func.call(that)) }) // existing promise... } else { this.__promise = this.__promise.then(function(){ return func.apply(that, [].slice.call(arguments)) }) } return this }, // NOTE: this ignores the function if there is no promise... // XXX not sure if this is correct... catch: function(func){ if(this.__promise != null){ this.__promise = this.__promise.catch(func) } return this }, // Like then, but the function will get called only if a .then(..) is // called right after... // // NOTE: only the last lazy function is stored, the rest are discarded. lazy: function(func){ this.__lazy = func return this }, clearLazy: function(){ delete this.__lazy return this }, // example method (sync)... // // Protocol: // .data() - "get" data value... // .data('new value') // - set data value... // // In both cases the method will return the object (this) // // In both cases the internal promise when resolved will get passed // the value, in both cases the old value... // // A more full example: // hiddenPromise // // get and print the value (undefined)... // .data() // .then(function(value){ console.log(value) }) // // set a new value... // .data('new value') // // get and print the new value... // .data() // .then(function(value){ console.log(value) }) // sdata: function(d){ this.clearLazy() // get... if(arguments.length == 0){ this.lazy(function(){ return this.__data }) // set... } else { this.then(function(){ var res = this.__data this.__data = d return res }) } return this }, // async data... // // NOTE: this is the same as above but will do it's work async (after // a second)... data: function(d){ var that = this this.clearLazy() // get... if(arguments.length == 0){ //this.then(function(){ this.lazy(function(){ return new Promise(function(r){ setTimeout( function(){ r(that.__data) }, 1000) }) }) // set... } else { this.then(function(){ return new Promise(function(r){ setTimeout( function(){ var res = that.__data that.__data = d r(res) }, 1000) }) }) } return this }, } /*********************************************************************/ var pWikiLocalStorage = pWikiFeatures.Feature({ title: '', tag: 'localstorage-store', config: { 'localstorage-key': 'pwiki-gen2-data', }, actions: actions.Actions({ // XXX do not use .__data save: ['', function(){ localstorage[this.config['localstorage-key']] = JSON.stringify(this.wiki.__data) }], }), handlers: [ // XXX add lifecicle load handler... // XXX [[ 'update', 'clear', ], function(){ this.save() }], [[ 'path', 'data', ], function(){ arguments.length > 1 && this.save() }], ], }) var pWikiPouchDBStore = pWikiFeatures.Feature({ title: '', tag: 'pouchdb-store', }) var pWikiPeerJSSync = pWikiFeatures.Feature({ title: '', tag: 'peerjs-sync', }) //--------------------------------------------------------------------- // XXX should this extend pWiki or encapsulate (current)??? var pWikiUIActions = actions.Actions({ config: { 'special-paths': { //'History/back': 'historyBack', //'History/forward': 'historyForward', }, }, dom: null, page: null, // XXX might be a good idea to add actions to setup/clear a filter... __dom_filters__: { // sortable elements... // TODO: make elements movable from/to nested lists... '.sortable': function(elems){ var wiki = this.page elems .sortable({ handle: '.sort-handle', placeholder: 'sort-placeholder', forcePlaceholderSize: true, axis: 'y', // event handlers... update: function(evt, ui){ // get item list... var order = ui.item .parent().children('macro[src]') .map(function(){ return $(this).attr('src') }) .toArray() // save the order... wiki .get(order[0] + '/../*') .order(['*'].concat(order)) .order('save') }, }) // NOTE: we are only adding touch to the active elements // to avoid the side-effect of it canceling the default // behaviour (i.e. scrolling)... .find('.sort-handle') .addTouch() }, // title editor... '.title': function(elems){ var client = this var wiki = this.page elems .focus(function(){ var to = $(this).attr('saveto') || '.' $(this).text(wiki.get(to).title()) }) .blur(function(){ var to = $(this).attr('saveto') || '.' var text = $(this).text().trim() var page = wiki.get(to) if(text[0] == '/'){ page.path(text) } else { page.title(text) } // XXX need to account for changed path sufixes... wiki.path(page.path) client.reload() }) $('title').text(elems.first().text()) }, // raw text editor... '.raw': function(elems){ var client = this var wiki = this.page elems .focus(function(){ var to = $(this).attr('saveto') || '.' console.log('EDITING:', wiki.get(to).path()) }) .on('keyup', function(){ var to = wiki.get($(this).attr('saveto') || '.').path() console.log('SAVING:', to) //Wiki.get(to).raw($(this).text()) wiki.get(to).raw($(this)[0].innerText) }) // XXX do this live, but on a timeout after user input... // XXX need to place the cursor in the same position... .blur(function(){ client.reload() }) }, // checkbox handlers... 'input[type="checkbox"].state': function(elems){ var client = this var wiki = this.page elems // initial state... .each(function(){ var path = $(this).attr('saveto') var value = !!wiki.get(path).checked() $(this) .prop('checked', value) .parents('.item').first() [value ? 'addClass' : 'removeClass']('checked') }) // handle clicks... .click(function(){ var path = $(this).attr('saveto') var value = $(this).prop('checked') wiki.get(path).checked(value) $(this) .parents('.item').first() [value ? 'addClass' : 'removeClass']('checked') // XXX //client.save() }) }, }, // XXX add support for anchors -- #Wiki/Path#anchor... // ...not working yet... location: ['', function(path){ var page = this.page if(arguments.length == 0){ // XXX is this correct??? return page.path() } path = path.trim().split('#') var hash = path[1] path = path[0] // special paths... if(path in this.config['special-paths']){ this[this.config['special-paths'][path]]() } var orig = this.location() page.location(path) this.reload() // reset scroll location... orig != this.location() && this.dom .scrollParent() .scrollLeft(0) .scrollTop(0) // focus hash.. // XXX not working yet... hash != null && hash != '' && this.dom .scrollParent() .scrollLeft(0) .scrollTop( (this.dom .find('#'+hash+', a[name="'+hash+'"]').first() .offset() || {}).top || 0) && console.log('HASH:', hash) }], reload: ['', function(){ var that = this var page = this.page this.dom .attr('wiki-active', 'no') .empty() // update path and render page... // XXX revise the default view approach... .append(page.title()[0] == '_' ? page.html() : page.get('./_view').html()) // activate page controls... .ready(function(){ that.updateDom() }) }], // XXX might be a good idea to add actions to setup/clear a filter... updateDom: ['', function(dom){ var that = this dom = dom || this.dom if(dom.attr('wiki-active') == 'yes'){ return } dom.attr('wiki-active', 'yes') var filters = this.__dom_filters__ || pWikiUIActions.__dom_filters__ // apply dom filters... Object.keys(filters) .forEach(function(pattern){ // XXX for some reason this works but has no effect... filters[pattern].call(that, dom.find(pattern)) }) }], // shorthand... get: ['', function(){ return this.page.get.apply(this.page, arguments) }] /* // XXX url? // - couch url // - 'local' load: ['', function(){ }], // XXX navigation... // ...these in addition to default scrolling should focus elements up: ['', function(){}], down: ['', function(){}], left: ['', function(){}], right: ['', function(){}], togglePages: ['', function(){}], toggleWikis: ['', function(){}], // should this be in the editor feature??? toggleEdit: ['', function(){}], //*/ }) var pWikiUI = pWikiFeatures.Feature({ title: '', tag: 'ui', }) // XXX STUB: not sure if this is the right way... var pWikiClient = module.pWikiClient = object.Constructor('pWikiClient', actions.mix( actions.MetaActions, pWikiUIActions)) /*********************************************************************/ module._test_data = { 'System/EmptyPage': { text: '[@source(./path)] is empty...' }, 'WikiMain': {}, 'folder/page1': {}, 'folder/page2': {}, 'folder/page3': {}, } // XXX not sure if this is a good way to do this -- needs to be reusable // for different stores... module._test_data.__proto__ = BaseData module._test = function(){ var wiki = Object.create(pWikiData) wiki.__data = Object.create(module._test_data) var page = new pWikiPage({ wiki: wiki, }) // XXX do some testing... // XXX return page } /********************************************************************** * vim:set ts=4 sw=4 : */ return module })