mirror of
				https://github.com/flynx/pWiki.git
				synced 2025-11-04 04:50:09 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1680 lines
		
	
	
		
			43 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1680 lines
		
	
	
		
			43 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**********************************************************************
 | 
						|
* 
 | 
						|
*
 | 
						|
*
 | 
						|
**********************************************************************/
 | 
						|
 | 
						|
 | 
						|
/*********************************************************************/
 | 
						|
// 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 :                                                */
 |