mirror of
https://github.com/flynx/pWiki.git
synced 2025-10-28 17:40:07 +00:00
1680 lines
43 KiB
JavaScript
Executable File
1680 lines
43 KiB
JavaScript
Executable File
/**********************************************************************
|
|
*
|
|
*
|
|
*
|
|
**********************************************************************/
|
|
|
|
|
|
/*********************************************************************/
|
|
// 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 ?
|
|
('<a '
|
|
+'class="wikiword" '
|
|
+'href="#'+ path +'" '
|
|
+'bracketed="'+ (show_brackets && l[0] == '[' ? 'yes' : 'no') +'" '
|
|
//+'onclick="event.preventDefault(); go($(this).attr(\'href\').slice(1))" '
|
|
+'>'
|
|
+ (!!show_brackets ? path : l)
|
|
+'</a>')
|
|
: 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:
|
|
// <macro arg=value ../>
|
|
//
|
|
// HTML-like with body:
|
|
// <macro arg=value ..>
|
|
// ..text..
|
|
// </macro>
|
|
//
|
|
// 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 +'</slot>'
|
|
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 $('<span>')
|
|
.append($('<pre>')
|
|
.html($(elem).html())) },
|
|
// XXX expperimental...
|
|
json: function(context, elem){
|
|
return $('<span>')
|
|
.html($(elem).text()
|
|
// remove JS comments...
|
|
.replace(/\s*\/\/.*$|\s*\/\*(.|[\n\r])*?\*\/\s*/mg, '')) },
|
|
|
|
// XXX
|
|
nl2br: function(context, elem){
|
|
return $('<div>')
|
|
.html($(elem)
|
|
.html()
|
|
.replace(/\n/g, '<br>\n')) },
|
|
|
|
wikiword: function(context, elem){
|
|
return $('<span>')
|
|
.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 $('<span>')
|
|
.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') ?
|
|
$('<span>').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 = $('<div>').append($(e).clone()).html()
|
|
|
|
// conditional comment...
|
|
if(e.nodeType == e.COMMENT_NODE
|
|
&& /^<!--\s*\[pWiki\[(.|\n)*\]\]\s*-->$/.test(text)){
|
|
text = text
|
|
.replace(/^<!--\s*\[pWiki\[/, '')
|
|
.replace(/\]\]\s*-->$/, '') }
|
|
|
|
$(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 $('<div>')
|
|
.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('<br>') },
|
|
// 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] +'] <i>from page: ['+ e[1] +']</i>' })
|
|
.map(function(e){
|
|
return '['+ e[1] +'] <i>-> ['+ e[0] +']</i>' })
|
|
.sort()
|
|
.join('<br>') },
|
|
|
|
// 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:
|
|
// {
|
|
// <path>: {
|
|
// text: <text>,
|
|
//
|
|
// links: [
|
|
// <offset>: <link>,
|
|
// ],
|
|
// }
|
|
// }
|
|
//
|
|
// XXX add .json support...
|
|
/*
|
|
var data = {
|
|
// XXX might be a good idea to use this for outline...
|
|
'Templates/tree': {
|
|
//text: '<macro src="../**"> [@source(./path)]<br> </macro>\n'
|
|
text: ''
|
|
+'<div class="sortable">\n'
|
|
+'<macro src="../*">\n'
|
|
+'<div class="item">\n'
|
|
+'<span class="sort-handle">☰</span> \n'
|
|
+'<a href="#@source(./path)">@source(./title)</a>\n'
|
|
+'<span class="separator"/>\n'
|
|
+'<a class="button" href="#@source(./path)/delete">×</a>\n'
|
|
+'</div>\n'
|
|
+'<div style="padding-left: 30px">\n'
|
|
+'<include '
|
|
+'style="display:block" '
|
|
+'src="@source(./path)/tree" '
|
|
+'/>\n'
|
|
+'</div>\n'
|
|
+'</macro>\n'
|
|
+'</div>\n'
|
|
},
|
|
// XXX this is essentially identical to pages, except for the path...
|
|
'Templates/all_pages': {
|
|
//text: '<macro src="../**"> [@source(./path)]<br> </macro>\n'
|
|
text: ''
|
|
+'<macro src="../**">\n'
|
|
+'<div class="item">\n'
|
|
+'[@source(./path)]\n'
|
|
+'<span class="separator"/>\n'
|
|
+'<a class="button" href="#@source(./path)/delete">×</a>\n'
|
|
+'</div>\n'
|
|
+'</macro>\n'
|
|
},
|
|
// XXX experimental...
|
|
// XXX need sorting...
|
|
'Templates/outline': {
|
|
text: ''
|
|
+'<macro name="item-pre-controls"/>\n'
|
|
+'\n'
|
|
+'<macro name="item-content">\n'
|
|
+'<include '
|
|
+'class="raw" '
|
|
+'contenteditable tabindex="0" '
|
|
+'style="display:inline-block" '
|
|
+'saveto="@source(./path)" '
|
|
+'src="."'
|
|
+'/>\n'
|
|
+'</macro>\n'
|
|
+'\n'
|
|
+'<macro name="item-post-controls">\n'
|
|
+'<a class="button" href="#@source(./path)/delete">×</a>\n'
|
|
+'</macro>\n'
|
|
+'\n'
|
|
+'\n'
|
|
+'<div>\n'
|
|
// XXX select all on focus...
|
|
+'<span class="raw" contenteditable tabindex="0" '
|
|
+'saveto="@source(../path)/@now()" style="display:inline-block">\n'
|
|
+'+\n'
|
|
+'</span>\n'
|
|
+'</div>\n'
|
|
//+'<br>\n'
|
|
+'<div class="sortable">\n'
|
|
+'<macro src="../*">\n'
|
|
+'<div class="item">\n'
|
|
+'<div>\n'
|
|
+'<span class="sort-handle">☰</span>\n'
|
|
+'<macro name="item-pre-controls" src="."/>\n'
|
|
+'<macro name="item-content" src="."/>\n'
|
|
+'<span class="separator"/>\n'
|
|
+'<macro name="item-post-controls" src="."/>\n'
|
|
+'</div>\n'
|
|
+'<div style="padding-left: 30px">\n'
|
|
+'<include '
|
|
+'style="display:block" '
|
|
+'src="@source(./path)/outline" '
|
|
+'/>\n'
|
|
+'</div>\n'
|
|
+'</div>\n'
|
|
// XXX do we need this or should we just use CSS???
|
|
//+'<else>\n'
|
|
// +'<i>No items yet...</i>\n'
|
|
//+'</else>\n'
|
|
+'</macro>\n'
|
|
+'</div>\n'
|
|
+'\n',
|
|
},
|
|
// XXX see inside...
|
|
'Templates/todo': {
|
|
text: ''
|
|
// XXX this feels wrong...
|
|
// ...and this will not wirk well with macro override rules...
|
|
+'<macro name="item-pre-controls">\n'
|
|
+' <input type="checkbox"/>\n'
|
|
+'</macro>\n'
|
|
+'\n'
|
|
+'<include src="../outline">\n'
|
|
},
|
|
|
|
// Views...
|
|
// XXX experimental...
|
|
'Templates/_outline': {
|
|
text: ''
|
|
+'<include src="../_view"/>\n'
|
|
+'\n'
|
|
// XXX temporary until I figure out how to deal with the saveto=".."
|
|
// in implicit vs. explicit _view
|
|
+'<slot name="title" class="title" contenteditable saveto="..">'
|
|
+'@source(../title)'
|
|
+'</slot>\n'
|
|
+'\n'
|
|
+'<slot name="page-content">\n'
|
|
+'@include(../outline)'
|
|
+'</slot>'
|
|
+'\n',
|
|
},
|
|
'Templates/_todo': {
|
|
text: ''
|
|
+'<include src="../_view"/>\n'
|
|
+'\n'
|
|
// XXX temporary until I figure out how to deal with the saveto=".."
|
|
// in implicit vs. explicit _view
|
|
+'<slot name="title" class="title" contenteditable saveto="..">'
|
|
+'@source(../title)'
|
|
+'</slot>\n'
|
|
+'\n'
|
|
+'<slot name="page-content">\n'
|
|
+'@include(../todo)'
|
|
+'</slot>'
|
|
+'\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:
|
|
// <dir>/<title>
|
|
//
|
|
// 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 : */
|