mirror of
				https://github.com/flynx/pWiki.git
				synced 2025-10-29 01:50: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 :                                                */
 |