/********************************************************************** * * * **********************************************************************/ /*********************************************************************/ // Hepers... // var quoteRegExp = RegExp.quoteRegExp = RegExp.quoteRegExp || function(str){ return str .replace(/([\.\\\/\(\)\[\]\$\*\+\-\{\}\@\^\&\?\<\>])/g, '\\$1') } var path2lst = 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){ return (i > 0 && (p.trim() == '..' || p.trim() == '.') || (l[i+1] || '').trim() == '..') ? null : p.trim() }) // cleanup and clear '.'... .filter(function(p){ return p != null && p != '' })} var normalizePath = function(path){ return path2lst(path).join('/') } var clearWikiWords = function(elem){ // clear existing... elem.find('.wikiword').each(function(){ $(this).attr('bracketed') == 'yes' ? $(this).replaceWith(['['].concat(this.childNodes, [']'])) : $(this).replaceWith(this.childNodes) }) return elem } var setWikiWords = function(text, show_brackets, skip){ skip = skip || [] skip = skip instanceof Array ? skip : [skip] return text // set new... .replace( Wiki.__wiki_link__, function(l){ // check if wikiword is escaped... if(l[0] == '\\'){ return l.slice(1) } var path = l[0] == '[' ? l.slice(1, -1) : l var i = [].slice.call(arguments).slice(-2)[0] // XXX HACK check if we are inside a tag... var rest = text.slice(i+1) if(rest.indexOf('>') < rest.indexOf('<')){ return l } return skip.indexOf(l) < 0 ? ('' + (!!show_brackets ? path : l) +'') : l })} /*********************************************************************/ function Macro(doc, args, func){ func.doc = doc func.macro_args = args return func } // XXX should inline macros support named args??? var macro = { __include_marker__: '{{{INCLUDE-MARKER}}}', // Abstract macro syntax: // Inline macro: // @macro(arg ..) // // HTML-like: // // // HTML-like with body: // // ..text.. // // // XXX should inline macros support named args??? __macro__pattern__: [[ // @macro(arg ..) '\\\\?@([a-zA-Z-_]+)\\(([^)]*)\\)' ].join('|'), 'mg'], // default filters... // // NOTE: these are added AFTER the user defined filters... __filters__: [ 'wikiword', 'noscript', ], __post_filters__: [ //'noscript', 'title', 'editor', ], // Macros... // // XXX add support for sort and reverse attrs in all relavant macros // (see: macro for details) macro: { "pwiki-comment": Macro('hide in pWiki', [], function(context, elem, state){ return '' }), now: Macro('Create a now id', [], function(context, elem, state){ return ''+Date.now() }), // select filter to post-process text... filter: Macro('Filter to post-process text', ['name'], function(context, elem, state){ var filter = $(elem).attr('name') filter[0] == '-' ? // disabled -- keep at head of list... state.filters.unshift(filter) // normal -- tail... : state.filters.push(filter) return '' }), // include page/slot... // // NOTE: this will render the page in the caller's context. // NOTE: included pages are rendered completely independently // from the including page. include: Macro('Include page', ['src', 'isolated', 'text'], function(context, elem, state){ var path = $(elem).attr('src') // get and prepare the included page... state.include .push([elem, context.get(path)]) // return the marker... return this.__include_marker__ }), // NOTE: this is similar to include, the difference is that this // includes the page source to the current context while // include works in an isolated context source: Macro('Include page source (without parsing)', ['src'], function(context, elem, state){ var path = $(elem).attr('src') return context.get(path) .map(function(page){ return page.raw }) .join('\n') }), quote: Macro('Include quoted page source (without parsing)', ['src'], function(context, elem, state){ elem = $(elem) var path = elem.attr('src') return $(context.get(path) .map(function(page){ return elem .clone() .attr('src', page.path) .text(page.raw)[0] })) }), /* // fill/define slot (stage 1)... // // XXX which should have priority the arg text or the content??? _slot: Macro('Define/fill slot', ['name', 'text'], function(context, elem, state, parse){ var name = $(elem).attr('name') // XXX text = $(elem).html() text = text == '' ? $(elem).attr('text') : text text = this.parse(context, text, state, true) //text = parse(elem) if(state.slots[name] == null){ state.slots[name] = text // return a slot macro parsable by stage 2... //return '<_slot name="'+name+'">'+ text +'' return elem } else if(name in state.slots){ state.slots[name] = text return '' } }), //*/ // convert @ macro to html-like + parse content... slot: Macro('Define/fill slot', ['name', 'text'], function(context, elem, state, parse){ elem = $(elem) var name = elem.attr('name') // XXX text = elem.html() text = text.trim() == '' ? elem.html(elem.attr('text') || '').html() : text text = parse(elem) elem.attr('text', null) //elem.html(text) return elem }), // XXX revise macro definition rules -- see inside... // XXX do we need macro namespaces or context isolation (for inculdes)??? macro: Macro('Define/fill macro', ['name', 'src', 'sort'], function(context, elem, state, parse){ elem = $(elem) var name = elem.attr('name') var path = elem.attr('src') var sort = elem.attr('sort') state.templates = state.templates || {} // get named macro... if(name){ // XXX not sure which definition rules to use for macros... // - first define -- implemented now // - last define -- as in slots // - first contenr -- original //if(elem.html().trim() != ''){ if(elem.html().trim() != '' // do not redefine... && state.templates[name] == null){ state.templates[name] = elem.clone() } else if(name in state.templates) { elem = state.templates[name] } } // fill macro... if(path){ var pages = context.get(path) // no matching pages -- show the else block or nothing... if(pages.length == 0){ var e = elem .find('else').first().clone() .attr('src', path) parse(e, context) return e } // see if we need to overload attrs... sort = sort == null ? (elem.attr('sort') || '') : sort sort = sort .split(/\s+/g) .filter(function(e){ return e && e != '' }) // do the sorting... pages = sort.length > 0 ? pages.sort(sort) : pages // fill with pages... elem = elem.clone() .find('else') .remove() .end() return $(pages .map(function(page){ var e = elem.clone() .attr('src', page.path) parse(e, page) return e[0] })) } return '' }), }, // Post macros... // // XXX this is disabled for now, see end of .parse(..) post_macro: { '*': Macro('cleanup...', [], function(context, elem, state, parse, match){ if(match != null){ return match[0] == '\\' ? match.slice(1) : match } return elem }), /* _slot: Macro('', ['name'], function(context, elem, state){ var name = $(elem).attr('name') if(state.slots[name] == null){ return $(elem).html() } else if(name in state.slots){ return state.slots[name] } }), //*/ /* // XXX rename to post-include and post-quote 'page-text': Macro('', ['src'], function(context, elem, state){ return $(elem) .html(context.get(elem.attr('src')).text) }), 'page-raw': Macro('', ['src'], function(context, elem, state){ return $(elem) .text(context.get(elem.attr('src')).text) }), //*/ }, // Filters... // // Signature: // filter(text) -> html // filter: { default: 'html', html: function(context, elem){ return $(elem) }, text: function(context, elem){ return $('') .append($('
')
					.html($(elem).html())) },
		// XXX expperimental...
		json: function(context, elem){ 
			return $('')
				.html($(elem).text()
					// remove JS comments...
					.replace(/\s*\/\/.*$|\s*\/\*(.|[\n\r])*?\*\/\s*/mg, '')) },

		// XXX
		nl2br: function(context, elem){ 
			return $('
') .html($(elem) .html() .replace(/\n/g, '
\n')) }, wikiword: function(context, elem){ return $('') .html(setWikiWords( $(elem).html(), true, this.__include_marker__)) }, // XXX need to remove all on* event handlers... noscript: function(context, elem){ return $(elem) // remove script tags... .find('script') .remove() .end() // remove js links... .find('[href]') .filter(function(i, e){ return /javascript:/i.test($(e).attr('href')) }) .attr('href', '#') .end() .end() // remove event handlers... // XXX .off() will not work here as we need to remove on* handlers... }, // XXX move this to a plugin... markdown: function(context, elem){ var converter = new showdown.Converter({ strikethrough: true, tables: true, tasklists: true, }) return $('') .html(converter.makeHtml($(elem).html())) // XXX add click handling to checkboxes... .find('[checked]') .parent() .addClass('checked') .end() .end() }, }, // Post-filters... // // These are run on the final page. // // The main goal is to setup editors and other active stuff that the // user should not have direct access to, but that should be // configurable per instance... // // for tech and other details see .filter // post_filter: { noscript: function(context, elem){ // XXX return elem }, // Setup the page title and .title element... // // Use the text from: // 1) set it H1 if it is the first tag in .text // 2) set it to .location // title: function(context, elem){ elem = $(elem) var title = elem.find('.text h1').first() // show first H1 as title... if(elem.find('.text').text().trim().indexOf(title.text().trim()) == 0){ title.detach() elem.find('.title').html(title.html()) $('title').html(title.text()) // show location... } else { $('title').text(context.location) } return elem }, // XXX this needs save/reload... editor: function(context, elem){ // XXX title // - on focus set it to .title // XXX text // XXX raw // XXX checkbox return elem }, }, // // Parsing: // 1) expand macros // 2) apply filters // 3) merge and parse included pages: // 1) expand macros // 2) apply filters // 4) fill slots // 5) expand post-macros // // NOTE: stage 4 parsing is executed on the final merged page only // once. i.e. it is not performed on the included pages. // NOTE: included pages are parsed in their own context. // NOTE: slots are parsed in the context of their containing page // and not in the location they are being placed. // // XXX support quoted text... // XXX need to quote regexp chars of .__include_marker__... // XXX include recursion is detected but path recursion is not at // this point... // e.g. the folowing paths resolve to the same page: // /SomePage // /SomePage/SomePage // or any path matching: // /\/(SomePage\/)+/ parse: function(context, text, state, skip_post, pattern){ var that = this state = state || {} state.filters = state.filters || [] //state.slots = state.slots || {} state.include = state.include || [] state.seen = state.seen || [] //pattern = pattern || RegExp('@([a-zA-Z-_]+)\\(([^)]*)\\)', 'mg') pattern = pattern || RegExp.apply(null, this.__macro__pattern__) // XXX need to quote regexp chars... var include_marker = RegExp(this.__include_marker__, 'g') var parsed = typeof(text) == typeof('str') ? $('').html(text) : text var _parseText = function(context, text, macro){ return text.replace(pattern, function(match){ // quoted macro... if(match[0] == '\\' && macro['*'] == null){ return match.slice(1) } //return match } // XXX parse match... var d = match.match(/@([a-zA-Z-_:]*)\(([^)]*)\)/) var name = d[1] if(name in macro || '*' in macro){ var elem = $('<'+name+'/>') name = name in macro ? name : '*' // format positional args.... var a = d[2] .split(/((['"]).*?\2)|\s+/g) // cleanup... .filter(function(e){ return e && e != '' && !/^['"]$/.test(e)}) // remove quotes... .map(function(e){ return /^(['"]).*\1$/.test(e) ? e.slice(1, -1) : e }) // add the attrs to the element... name != '*' && a.forEach(function(e, i){ var k = ((macro[name] || {}).macro_args || [])[i] k && elem.attr(k, e) }) // call macro... var res = macro[name] .call(that, context, elem, state, function(elem, c){ return _parse(c || context, elem, macro) }, match) return res instanceof jQuery ? // merge html of the returned set of elements... res .map(function(i, e){ return e.outerHTML }) .toArray() .join('\n') : typeof(res) != typeof('str') ? res.outerHTML : res } return match }) } // NOTE: this modifies parsed in-place... var _parse = function(context, parsed, macro){ $(parsed).contents().each(function(_, e){ // #text / comment node -> parse the @... macros... if(e.nodeType == e.TEXT_NODE || e.nodeType == e.COMMENT_NODE){ // get actual element content... var text = $('
').append($(e).clone()).html() // conditional comment... if(e.nodeType == e.COMMENT_NODE && /^$/.test(text)){ text = text .replace(/^$/, '') } $(e).replaceWith(_parseText(context, text, macro)) // node -> html-style + attrs... } else { var name = e.nodeName.toLowerCase() // parse attr values... for(var i=0; i < e.attributes.length; i++){ var attr = e.attributes[i] attr.value = _parseText(context, attr.value, macro) } // macro match -> call macro... if(name in macro){ $(e).replaceWith(macro[name] .call(that, context, e, state, function(elem, c){ return _parse(c || context, elem, macro) })) // normal tag -> sub-tree... } else { _parse(context, e, macro) } } }) return parsed } var _filter = function(lst, filters){ lst // unique -- leave last occurance.. .filter(function(k, i, lst){ return k[0] != '-' // filter dupplicates... && lst.slice(i+1).indexOf(k) == -1 // filter disabled... && lst.slice(0, i).indexOf('-' + k) == -1 }) // unique -- leave first occurance.. //.filter(function(k, i, lst){ return lst.slice(0, i).indexOf(k) == -1 }) // apply the filters... .forEach(function(f){ var k = f // get filter aliases... var seen = [] while(typeof(k) == typeof('str') && seen.indexOf(k) == -1){ seen.push(k) k = filters[k] } // could not find the filter... if(!k){ //console.warn('Unknown filter:', f) return } // use the filter... parsed = k.call(that, context, parsed) }) } // macro stage... _parse(context, parsed, this.macro) // filter stage... _filter(state.filters.concat(this.__filters__), this.filter) // merge includes... parsed .html(parsed.html() .replace(include_marker, function(){ var page = state.include.shift() var elem = $(page.shift()) page = page.pop() var isolated = elem.attr('isolated') == 'true' var seen = state.seen.slice() if(seen.indexOf(page.path) >= 0){ return elem.html() } seen.push(page.path) return page.map(function(page){ return $('
') .append(elem .clone() .attr('src', page.path) .append(that .parse(page, page.raw, { //slots: !isolated ? state.slots : {}, templates: state.templates, seen: seen, }, !isolated))) //true))) .html() }) .join('\n') })) // post processing... if(!skip_post){ // fill slots... // XXX need to prevent this from processing slots in editable // elements... slots = {} // get slots... parsed.find('slot') .each(function(i, e){ e = $(e) // XXX not sure about this... // ...check if it prevents correct slot parsing // within an isolated include... if(e.parents('[isolated="true"]').length > 0){ return } var n = e.attr('name') n in slots && e.detach() slots[n] = e }) // place slots... parsed.find('slot') .each(function(i, e){ e = $(e) // XXX not sure about this... // ...check if it prevents correct slot parsing // within an isolated include... if(e.parents('[isolated="true"]').length > 0){ return } var n = e.attr('name') e.replaceWith(slots[n]) }) // post-macro... // XXX for some odd reason this clears the backslash from // quoted macros in raw fields... //this.post_macro // && _parse(context, parsed, this.post_macro) } // post-filter stage... // XXX get list from context.config... _filter(this.__post_filters__, this.post_filter) // XXX shuld we get rid of the root span??? return parsed.contents() }, } /*********************************************************************/ // XXX not sure about these... // XXX add docs... // XXX need to handle case: // .data is function + function returns a page // -> "redirect" to that page // ...is changing .path a good idea for redirecting??? var BaseData = { // Macro acces to standard page attributes (paths)... 'System/title': function(){ return this.get('..').title }, 'System/path': function(){ return this.dir }, 'System/dir': function(){ return this.get('..').dir }, 'System/location': function(){ return this.dir }, 'System/resolved': function(){ return 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 this.get('..').raw }, 'System/text': function(){ return this.get('..').text }, // XXX move this to Wiki.children + rename... 'System/list': function(){ var p = this.dir return Object.keys(this.__wiki_data) .map(function(k){ return k.indexOf(p) == 0 ? path2lst(k.slice(p.length)).shift() : null }) .filter(function(e){ return e != null }) .sort() .map(function(e){ return '['+ e +']' }) .join('
') }, // list links to this page... 'System/links': function(){ var that = this var p = this.dir var res = [] var wiki = this.__wiki_data Object.keys(wiki).forEach(function(k){ ;(wiki[k].links || []) .forEach(function(l){ ;(l == p || that .get(path2lst(l).slice(0, -1)) .acquire('./'+path2lst(l).pop()) == p) && res.push([l, k]) }) }) 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... //'System/sort': function(){ return this.get('..').sort() }, //'System/reverse': function(){ return this.get('..').reverse() }, /* 'System/delete': function(){ var p = this.dir delete this.__wiki_data[p] return this.get('..') }, //*/ } // data store... // Format: // { // : { // text: , // // links: [ // : , // ], // } // } // // XXX add .json support... /* var data = { // XXX might be a good idea to use this for outline... 'Templates/tree': { //text: ' [@source(./path)]
\n' text: '' +'
\n' +'\n' +'
\n' +' \n' +'@source(./title)\n' +'\n' +'×\n' +'
\n' +'
\n' +'\n' +'
\n' +'
\n' +'
\n' }, // XXX this is essentially identical to pages, except for the path... 'Templates/all_pages': { //text: ' [@source(./path)]
\n' text: '' +'\n' +'
\n' +'[@source(./path)]\n' +'\n' +'×\n' +'
\n' +'
\n' }, // XXX experimental... // XXX need sorting... 'Templates/outline': { text: '' +'\n' +'\n' +'\n' +'\n' +'\n' +'\n' +'\n' +'×\n' +'\n' +'\n' +'\n' +'
\n' // XXX select all on focus... +'\n' +'+\n' +'\n' +'
\n' //+'
\n' +'
\n' +'\n' +'
\n' +'
\n' +'\n' +'\n' +'\n' +'\n' +'\n' +'
\n' +'
\n' +'\n' +'
\n' +'
\n' // XXX do we need this or should we just use CSS??? //+'\n' // +'No items yet...\n' //+'\n' +'
\n' +'
\n' +'\n', }, // XXX see inside... 'Templates/todo': { text: '' // XXX this feels wrong... // ...and this will not wirk well with macro override rules... +'\n' +' \n' +'\n' +'\n' +'\n' }, // Views... // XXX experimental... 'Templates/_outline': { text: '' +'\n' +'\n' // XXX temporary until I figure out how to deal with the saveto=".." // in implicit vs. explicit _view +'' +'@source(../title)' +'\n' +'\n' +'\n' +'@include(../outline)' +'' +'\n', }, 'Templates/_todo': { text: '' +'\n' +'\n' // XXX temporary until I figure out how to deal with the saveto=".." // in implicit vs. explicit _view +'' +'@source(../title)' +'\n' +'\n' +'\n' +'@include(../todo)' +'' +'\n' }, } //*/ data = {} data.__proto__ = BaseData // XXX experimental... // ...for some reason these are called twice... var PathActions = { // XXX test: function(){ var p = path2lst(this.location) console.log('!!! TEST !!!') this.location = p.slice(0, -1) }, delete: function(){ var p = normalizePath(path2lst(this.location).slice(0, -1)) console.log('!!! DELETE: %s !!!', p) delete this.__wiki_data[p] this.location = p }, } /*********************************************************************/ // XXX add .json support... var Wiki = { __wiki_data: data, __config_page__: 'System/settings', __home_page__: 'WikiHome', __default_page__: 'EmptyPage', // Special sub-paths to look in on each level... __acquesition_order__: [ 'Templates', ], __post_acquesition_order__: [ ], // XXX should this be read only??? __system__: 'System', //__redirect_template__: 'RedirectTemplate', __wiki_link__: RegExp('('+[ '\\\\?(\\./|\\.\\./|[A-Z][a-z0-9]+[A-Z/])[a-zA-Z0-9/]*', '\\\\?\\[[^\\]]+\\]', ].join('|') +')', 'g'), __macro_parser__: macro, // Resolve path variables... // // Supported vars: // $NOW - resolves to 'P'+Date.now() // resolvePathVars: function(path){ return path .replace(/\$NOW|\$\{NOW\}/g, ''+Date.now()) }, resolvePathActions: function(){ // XXX this can happen when we are getting '.../*' of an empty item... if(this.path == null){ return this } var p = path2lst(this.path).pop() if(p in PathActions){ return PathActions[p].call(this) } return this }, // Resolve '.' and '..' relative to current page... // // NOTE: '.' is relative to .path and not to .dir // NOTE: this is a method as it needs the context to resolve... resolveDotPath: function(path){ path = normalizePath(path) // '.' or './*' return path == '.' || /^\.\//.test(path) ? //path.replace(/^\./, this.dir) path.replace(/^\./, this.path) // '..' or '../*' : path == '..' || /^\.\.\//.test(path) ? //path.replace(/^\.\./, // normalizePath(path2lst(this.dir).slice(0, -1))) path.replace(/^\.\./, this.dir) : path }, // Get list of paths resolving '*' and '**' // // XXX should we list parent pages??? // XXX should this acquire stuff??? // XXX should this support sorting and reversing??? resolveStarPath: function(path){ // no pattern in path -> return as-is... if(path.indexOf('*') < 0){ return [ path ] } // get the tail... var tail = path.split(/\*/g).pop() tail = tail == path ? '' : tail var pattern = RegExp('^' +normalizePath(path) // quote regexp chars... .replace(/([\.\\\/\(\)\[\]\$\+\-\{\}\@\^\&\?\<\>])/g, '\\$1') // convert '*' and '**' to regexp... .replace(/\*\*/g, '.*') .replace(/^\*|([^.])\*/g, '$1[^\\/]*') +'$') var data = this.__wiki_data 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) })) .map(function(p){ return tail != '' ? normalizePath(p +'/'+ tail) : p }) .filter(function(p){ return pattern.test(p) }) }, // current location... get location(){ return this.__location || this.__home_page__ }, set location(value){ delete this.__order delete this.__order_by this.__location = this.resolvePathVars(this.resolveDotPath(value)) this.resolvePathActions() }, get data(){ return this.__wiki_data[this.acquire()] }, attr: function(name, value){ // no args... if(arguments.length == 0){ return this // name... } else if(arguments.length == 1 && typeof(name) == typeof('str')){ return this.data[name] // object... } else if(arguments.length == 1){ var that = this Object.keys(name).forEach(function(k){ that.data[k] = name[k] }) // name value pair... } else { this.data[name] = value } return this }, // XXX experimental... get config(){ try{ return JSON.parse(this.get(this.__config_page__).code) || {} } catch(err){ console.error('CONFIG:', err) return {} } }, clone: function(){ var o = Object.create(Wiki) o.location = this.location //o.__location_at = this.__location_at // XXX o.__parent = this if(this.__order){ o.__order = this.__order.slice() } return o }, end: function(){ return this.__parent || this }, // page path... // // Format: // / // // NOTE: changing this will move the page to the new path and change // .location acordingly... // NOTE: same applies to path parts below... // NOTE: changing path will update all the links to the moving page. // NOTE: if a link can't be updated without a conflit then it is left // unchanged, and a redirect page will be created. // // XXX this can be null if we are getting '.../*' of an empty item... get path(){ return (this.__order || this.resolveStarPath(this.location))[this.at()] }, // XXX should link updating be part of this??? // XXX use a template for the redirect page... // XXX need to skip explicit '.' and '..' paths... set path(value){ value = this.resolvePathVars(this.resolveDotPath(value)) var l = this.location if(value == l || value == ''){ return } // old... var otitle = this.title var odir = this.dir if(this.exists(l)){ this.__wiki_data[value] = this.__wiki_data[l] } this.location = value // new... var ntitle = this.title var ndir = this.dir var redirect = false // update links to this page... this.pages(function(page){ //this.get('**').map(function(page){ // skip the old page... if(page.location == l){ return } page.raw = page.raw.replace(page.__wiki_link__, function(lnk){ var from = lnk[0] == '[' ? lnk.slice(1, -1) : lnk // get path/title... var p = path2lst(from) var t = p.pop() p = normalizePath(p) var target = page.get(p).acquire('./'+t) // page target changed... // NOTE: this can happen either when a link was an orphan // or if the new page path shadowed the original // target... // XXX should we report the exact condition here??? if(target == value){ console.log('Link target changed:', lnk, '->', value) return lnk // skip links that do not resolve to target... } else if(page.get(p).acquire('./'+t) != l){ return lnk } // format the new link... var to = p == '' ? ntitle : p +'/'+ ntitle to = lnk[0] == '[' ? '['+to+']' : to // explicit link change -- replace... if(from == l){ //console.log(lnk, '->', to) return to // path did not change -- change the title... } else if(ndir == odir){ // conflict: the new link will not resolve to the // target page... if(page.get(p).acquire('./'+ntitle) != value){ console.log('ERR:', lnk, '->', to, 'is shadowed by:', page.get(p).acquire('./'+ntitle)) // XXX should we add a note to the link??? redirect = true // replace title... } else { //console.log(lnk, '->', to) return to } // path changed -- keep link + add redirect page... } else { redirect = true } // no change... return lnk }) }) // redirect... // // XXX should we use a template here??? // ...might be a good idea to set a .redirect attr and either // do an internal/transparent redirect or show a redirect // template // ...might also be good to add an option to fix the link from // the redirect page... if(redirect){ console.log('CREATING REDIRECT PAGE:', l, '->', value, '') this.__wiki_data[l].raw = 'REDIRECT TO: ' + value +'<br>' +'<br><i>NOTE: This page was created when renaming the target ' +'page that resulted new link being broken (i.e. resolved ' +'to a different page from the target)</i>' this.__wiki_data[l].redirect = value // cleaup... } else { delete this.__wiki_data[l] } }, // path parts: directory... // // NOTE: see .path for details... get dir(){ return path2lst(this.path).slice(0, -1).join('/') }, set dir(value){ this.path = value +'/'+ this.title }, // path parts: title... // // NOTE: see .path for details... get title(){ return path2lst(this.path).pop() }, set title(value){ if(value == '' || value == null){ return } this.path = this.dir +'/'+ value }, // page content... // get raw(){ var data = this.data data = data instanceof Function ? data.call(this, this) : data return typeof(data) == typeof('str') ? data : data != null ? ('raw' in data ? data.raw : data.text) : '' }, set raw(value){ var l = this.location // prevent overwriting actions... if(this.data instanceof Function){ return } this.__wiki_data[l] = this.__wiki_data[l] || {} this.__wiki_data[l].text = value // cache links... delete this.__wiki_data[l].links this.__wiki_data[l].links = this.links }, get text(){ //return this.parse() // special case: if we are getting ./raw then do not parse text... return this.title == 'raw' ? this.raw : this.__macro_parser__.parse(this, this.raw) }, get code(){ return this.text.text() }, get checked(){ return this.data.checked }, set checked(value){ this.data.checked = value }, // NOTE: this is set by setting .text get links(){ var data = this.data || {} var links = data.links = data.links || (this.raw.match(this.__wiki_link__) || []) // unwrap explicit links... .map(function(e){ return e[0] == '[' ? e.slice(1, -1) : e }) // unique... .filter(function(e, i, l){ return l.slice(0, i).indexOf(e) == -1 }) return links }, // navigation... get parent(){ return this.get('..') }, get children(){ return this .get('./*') }, get siblings(){ return this .get('../*') }, // NOTE: .get() is not the same as .clone() in that .get() will resolve // the path to a specific location while .clone() will keep // everything as-is... // // XXX add prpper insyantiation ( .clone() )... get: function(path){ //var o = Object.create(this) var o = this.clone() // NOTE: this is here to resolve path patterns... o.location = this.path o.location = path || this.path return o }, exists: function(path){ return normalizePath(path || this.path) in this.__wiki_data }, // get title from dir and then go up the tree... // // XXX should we also acquire each path part??? acquire: function(path, no_default){ var that = this // handle paths and relative paths... var p = this.get(path) var title = p.title path = path2lst(p.dir) var acquire_from = this.__acquesition_order__ || [] var post_acquire_from = this.__post_acquesition_order__ || [] var data = this.__wiki_data var _get = function(path, title, lst){ lst = (lst == null || lst.length == 0) ? [''] : lst for(var i=0; i < lst.length; i++){ var p = path.concat([lst[i], title]) if(that.exists(p)){ p = normalizePath(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.__system__ && _get([this.__system__], title) // NOTE: this may be null... return p || ((!no_default && title != this.__default_page__) ? this.acquire('./'+this.__default_page__) : null) }, // iteration... get length(){ return (this.__order || this.resolveStarPath(this.location)) .length }, // get/set postion in list of pages... // XXX do we need to min/max normalize n?? at: function(n){ // get position... if(n == null){ return this.__location_at || 0 } var l = this.length // end of list... 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_at = n return res }, prev: function(){ var i = this.at() - 1 // NOTE: need to guard against overflows... return i >= 0 ? this.at(i) : null }, next: function(){ return this.at(this.at() + 1) }, map: 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 }, filter: function(func){ 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 }, forEach: function(func){ this.map(func) return this }, // sorting... // XXX make these not affect the general order unless they have to... // XXX add a reverse method... __default_sort_methods__: ['path'], __sort_methods__: { title: function(a, b){ return a.page.title < b.page.title ? -1 : a.page.title > b.page.title ? 1 : 0 }, path: function(a, b){ return a.page.path < b.page.path ? -1 : a.page.path > b.page.path ? 1 : 0 }, // XXX checked: function(a, b){ // XXX chech if with similar states the order is kept.... return a.page.checked == b.page.checked ? 0 : a.page.checked ? 1 : -1 }, // XXX date, ... // XXX use manual order and palce new items (not in order) at // top/bottom (option)... // XXX store the order in .__wiki_data manual: function(a, b){ // XXX return 0 }, }, // 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, ...) // -> page // NOTE: the next method is used iff the previous returns 0, // i.e. the items are 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. // This is different from the "reverse" method which will simply // reverse the result. // // 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. sort: function(){ var that = this var res = this.clone() var path = res.path var methods = arguments[0] instanceof Array ? arguments[0] : [].slice.call(arguments) res.__order_by = methods = methods.length == 0 ? this.__default_sort_methods__ : methods res.update() return res }, reverse: function(){ var res = this.clone() res.__order_by = (this.__order_by || []).slice() var i = res.__order_by.indexOf('reverse') i >= 0 ? res.__order_by.splice(i, 1) : res.__order_by.push('reverse') res.update() return res }, // XXX not sure if this is the way to go... update: function(){ var that = this if(this.__order || this.__order_by){ var path = this.path var reverse = false var methods = (this.__order_by || this.__default_sort_methods__) .map(function(m){ var reversed = m[0] == '-' m = reversed ? m.slice(1) : m if(m == 'reverse'){ reverse = !reverse return null } m = typeof(m) == typeof('str') ? that.__sort_methods__[m] : m instanceof Function ? m : null return m != null ? (reversed ? function(){ return -m.apply(this, arguments) } : m) : m }) .filter(function(m){ return !!m }) this.__order = this.resolveStarPath(this.location) if(methods.length > 0){ var method = function(a, b){ for(var i=0; i < methods.length; i++){ var res = methods[i].call(that, a, b) if(res != 0){ return res } } // keep order if nothing else works... return a.i - b.i } this.__order = this.__order .map(function(t, i){ return { i: i, page: that.get(t), } }) .sort(method) .map(function(t){ return t.page.path }) } reverse && this.__order.reverse() this.__location_at = this.__order.indexOf(path) } return this }, // serialization... // XXX need to account for '*' and '**' in path... // XXX json: function(path){ return path == null ? JSON.parse(JSON.stringify(this.__wiki_data)) : path == '.' ? { path: this.location, text: this.raw, } : { path: path, text: (this.__wiki_data[path] || {}).raw, } }, // XXX should we inherit from the default??? load: function(json){ this.__wiki_data = json }, // iteration... // XXX this is not page specific, might need refactoring... pages: function(callback){ var that = this Object.keys(this.__wiki_data) .forEach(function(location){ // XXX not sure if this is the right way to go... //var o = Object.create(that) var o = that.clone() o.location = location callback.call(o, o) }) return this }, } /********************************************************************** * vim:set ts=4 sw=4 : */