mirror of
				https://github.com/flynx/pWiki.git
				synced 2025-10-31 02:50:08 +00:00 
			
		
		
		
	moved macros to parser (mostly working as before)...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
		
							parent
							
								
									9275d01899
								
							
						
					
					
						commit
						8dcff32823
					
				
							
								
								
									
										883
									
								
								v2/pwiki/page.js
									
									
									
									
									
								
							
							
						
						
									
										883
									
								
								v2/pwiki/page.js
									
									
									
									
									
								
							| @ -911,884 +911,6 @@ object.Constructor('Page', BasePage, { | |||||||
| 			return `<pre>${source}</pre>` }, | 			return `<pre>${source}</pre>` }, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	//
 |  | ||||||
| 	// 	<macro>(<args>, <body>, <state>){ .. }
 |  | ||||||
| 	// 		-> undefined
 |  | ||||||
| 	// 		-> <text>
 |  | ||||||
| 	// 		-> <array>
 |  | ||||||
| 	// 		-> <iterator>
 |  | ||||||
| 	// 		-> <func>(<state>)
 |  | ||||||
| 	// 			-> ...
 |  | ||||||
| 	//
 |  | ||||||
| 	// XXX do we need to make .macro.__proto__ module level object???
 |  | ||||||
| 	// XXX ASYNC make these support async page getters...
 |  | ||||||
| 	macros: { __proto__: { |  | ||||||
| 		//
 |  | ||||||
| 		//	@(<name>[ <else>][ local])
 |  | ||||||
| 		//	@(name=<name>[ else=<value>][ local])
 |  | ||||||
| 		//
 |  | ||||||
| 		//	@arg(<name>[ <else>][ local])
 |  | ||||||
| 		//	@arg(name=<name>[ else=<value>][ local])
 |  | ||||||
| 		//
 |  | ||||||
| 		//	<arg <name>[ <else>][ local]/>
 |  | ||||||
| 		//	<arg name=<name>[ else=<value>][ local]/>
 |  | ||||||
| 		//
 |  | ||||||
| 		// Resolution order:
 |  | ||||||
| 		// 		- local
 |  | ||||||
| 		// 		- .renderer
 |  | ||||||
| 		// 		- .root
 |  | ||||||
| 		//
 |  | ||||||
| 		// NOTE: else (default) value is parsed when accessed...
 |  | ||||||
| 		arg: Macro( |  | ||||||
| 			['name', 'else', ['local']], |  | ||||||
| 			function(args){ |  | ||||||
| 				var v = this.args[args.name]  |  | ||||||
| 					|| (!args.local  |  | ||||||
| 						&& (this.renderer |  | ||||||
| 							&& this.renderer.args[args.name]) |  | ||||||
| 						|| (this.root |  | ||||||
| 							&& this.root.args[args.name])) |  | ||||||
| 				v = v === true ? |  | ||||||
| 					args.name |  | ||||||
| 					: v |  | ||||||
| 				return v |  | ||||||
| 					|| (args['else'] |  | ||||||
| 						&& this.parse(args['else'])) }), |  | ||||||
| 		'': Macro( |  | ||||||
| 			['name', 'else', ['local']], |  | ||||||
| 			function(args){ |  | ||||||
| 				return this.macros.arg.call(this, args) }), |  | ||||||
| 		args: function(){ |  | ||||||
| 			return pwpath.obj2args(this.args) }, |  | ||||||
| 		//
 |  | ||||||
| 		// 	@filter(<filter-spec>)
 |  | ||||||
| 		// 	<filter <filter-spec>/>
 |  | ||||||
| 		//
 |  | ||||||
| 		// 	<filter <filter-spec>>
 |  | ||||||
| 		// 		...
 |  | ||||||
| 		// 	</filter>
 |  | ||||||
| 		//
 |  | ||||||
| 		// 	<filter-spec> ::=
 |  | ||||||
| 		// 		<filter> <filter-spec>
 |  | ||||||
| 		// 		| -<filter> <filter-spec>
 |  | ||||||
| 		//
 |  | ||||||
| 		// XXX BUG: this does not show any results:
 |  | ||||||
| 		//			pwiki.parse('<filter test>moo test</filter>')
 |  | ||||||
| 		//				-> ''
 |  | ||||||
| 		//		while these do:
 |  | ||||||
| 		//    		pwiki.parse('<filter test/>moo test')
 |  | ||||||
| 		//				-> 'moo TEST'
 |  | ||||||
| 		//			await pwiki.parse('<filter test>moo test</filter>@var()')
 |  | ||||||
| 		//				-> 'moo TEST'
 |  | ||||||
| 		//		for more info see:
 |  | ||||||
| 		//			file:///L:/work/pWiki/pwiki2.html#/Editors/Results
 |  | ||||||
| 		//		XXX do we fix this or revise how/when filters work???
 |  | ||||||
| 		//			...including accounting for variables/expansions and the like...
 |  | ||||||
| 		// XXX REVISE...
 |  | ||||||
| 		filter: function(args, body, state, expand=true){ |  | ||||||
| 			var that = this |  | ||||||
| 
 |  | ||||||
| 			var outer = state.filters =  |  | ||||||
| 				state.filters ?? [] |  | ||||||
| 			var local = Object.keys(args) |  | ||||||
| 
 |  | ||||||
| 			// trigger quote-filter...
 |  | ||||||
| 			var quote = local |  | ||||||
| 				.map(function(filter){ |  | ||||||
| 					return (that.filters[filter] ?? {})['quote'] ?? [] }) |  | ||||||
| 				.flat() |  | ||||||
| 			quote.length > 0 |  | ||||||
| 				&& this.macros['quote-filter'] |  | ||||||
| 					.call(this, Object.fromEntries(Object.entries(quote)), null, state) |  | ||||||
| 
 |  | ||||||
| 			// local filters...
 |  | ||||||
| 			if(body != null){ |  | ||||||
| 				// expand the body...
 |  | ||||||
| 				var ast = expand ? |  | ||||||
| 						this.__parser__.expand(this, body, state) |  | ||||||
| 					: body instanceof Array ? |  | ||||||
| 						body |  | ||||||
| 					// NOTE: wrapping the body in an array effectively 
 |  | ||||||
| 					// 		escapes it from parsing...
 |  | ||||||
| 					: [body] |  | ||||||
| 
 |  | ||||||
| 				return function(state){ |  | ||||||
| 					// XXX can we loose stuff from state this way???
 |  | ||||||
| 					// 		...at this stage it should more or less be static -- check!
 |  | ||||||
| 					return Promise.awaitOrRun( |  | ||||||
| 						this.__parser__.parse(this, ast, { |  | ||||||
| 							...state, |  | ||||||
| 							filters: local.includes(this.ISOLATED_FILTERS) ? |  | ||||||
| 								local |  | ||||||
| 								: [...outer, ...local], |  | ||||||
| 						}), |  | ||||||
| 						function(res){ |  | ||||||
| 							return {data: res} }) } |  | ||||||
| 				/*/ // XXX ASYNC...
 |  | ||||||
| 				return async function(state){ |  | ||||||
| 					// XXX can we loose stuff from state this way???
 |  | ||||||
| 					// 		...at this stage it should more or less be static -- check!
 |  | ||||||
| 					var res =  |  | ||||||
| 						await this.__parser__.parse(this, ast, { |  | ||||||
| 							...state, |  | ||||||
| 							filters: local.includes(this.ISOLATED_FILTERS) ? |  | ||||||
| 								local |  | ||||||
| 								: [...outer, ...local], |  | ||||||
| 						}) |  | ||||||
| 					return {data: res} } |  | ||||||
| 				//*/
 |  | ||||||
| 
 |  | ||||||
| 			// global filters...
 |  | ||||||
| 			} else { |  | ||||||
| 				state.filters = [...outer, ...local] } }, |  | ||||||
| 		//
 |  | ||||||
| 		// 	@include(<path>)
 |  | ||||||
| 		//
 |  | ||||||
| 		// 	@include(<path> isolated recursive=<text>)
 |  | ||||||
| 		// 	@include(src=<path> isolated recursive=<text>)
 |  | ||||||
| 		//
 |  | ||||||
| 		// 	<include src=<path> .. >
 |  | ||||||
| 		// 		<text>
 |  | ||||||
| 		// 	</include>
 |  | ||||||
| 		//
 |  | ||||||
| 		// NOTE: there can be two ways of recursion in pWiki:
 |  | ||||||
| 		// 			- flat recursion
 |  | ||||||
| 		// 				/A -> /A -> /A -> ..
 |  | ||||||
| 		// 			- nested recursion 
 |  | ||||||
| 		// 				/A -> /A/A -> /A/A/A -> ..
 |  | ||||||
| 		// 		Both can be either direct (type I) or indirect (type II).
 |  | ||||||
| 		// 		The former is trivial to check for while the later is 
 |  | ||||||
| 		// 		not quite so, as we can have different contexts at 
 |  | ||||||
| 		// 		different paths that would lead to different resulting 
 |  | ||||||
| 		// 		renders.
 |  | ||||||
| 		// 		At the moment nested recursion is checked in a fast but 
 |  | ||||||
| 		// 		not 100% correct manner focusing on path depth and ignoring
 |  | ||||||
| 		// 		the context, this potentially can lead to false positives.
 |  | ||||||
| 		// XXX need a way to make encode option transparent...
 |  | ||||||
| 		// XXX store a page cache in state...
 |  | ||||||
| 		include: Macro( |  | ||||||
| 			['src', 'recursive', 'join',  |  | ||||||
| 				['s', 'strict', 'isolated']], |  | ||||||
| 			async function*(args, body, state, key='included', handler){ |  | ||||||
| 				var macro = 'include' |  | ||||||
| 				if(typeof(args) == 'string'){ |  | ||||||
| 					var [macro, args, body, state, key, handler] = arguments  |  | ||||||
| 					key = key ?? 'included' } |  | ||||||
| 				var base = this.get(this.path.split(/\*/).shift()) |  | ||||||
| 				var src = args.src |  | ||||||
| 					&& this.resolvePathVars( |  | ||||||
| 						await base.parse(args.src, state)) |  | ||||||
| 				if(!src){ |  | ||||||
| 					return } |  | ||||||
| 				// XXX INHERIT_ARGS special-case: inherit args by default...
 |  | ||||||
| 				// XXX should this be done when isolated???
 |  | ||||||
| 				if(this.actions_inherit_args  |  | ||||||
| 						&& this.actions_inherit_args.has(pwpath.basename(src)) |  | ||||||
| 						&& this.get(pwpath.dirname(src)).path == this.path){ |  | ||||||
| 					src += ':$ARGS' } |  | ||||||
| 				var recursive = args.recursive ?? body |  | ||||||
| 				var isolated = args.isolated  |  | ||||||
| 				var strict = args.strict |  | ||||||
| 				var strquotes = args.s |  | ||||||
| 				var join = args.join  |  | ||||||
| 					&& await base.parse(args.join, state) |  | ||||||
| 
 |  | ||||||
| 				var depends = state.depends =  |  | ||||||
| 					state.depends  |  | ||||||
| 						?? new Set() |  | ||||||
| 				// XXX DEPENDS_PATTERN
 |  | ||||||
| 				depends.add(src) |  | ||||||
| 
 |  | ||||||
| 				handler = handler  |  | ||||||
| 					?? async function(src, state){ |  | ||||||
| 						return isolated ? |  | ||||||
| 							//{data: await this.get(src)
 |  | ||||||
| 							{data: await this |  | ||||||
| 								.parse({ |  | ||||||
| 									seen: state.seen,  |  | ||||||
| 									depends, |  | ||||||
| 									renderer: state.renderer, |  | ||||||
| 								})} |  | ||||||
| 							//: this.get(src)
 |  | ||||||
| 							: this |  | ||||||
| 								.parse(state) } |  | ||||||
| 
 |  | ||||||
| 				var first = true |  | ||||||
| 				for await (var page of this.get(src).asPages(strict)){ |  | ||||||
| 					if(join && !first){ |  | ||||||
| 						yield join } |  | ||||||
| 					first = false |  | ||||||
| 
 |  | ||||||
| 					//var full = page.path
 |  | ||||||
| 					var full = page.location |  | ||||||
| 
 |  | ||||||
| 					// handle recursion...
 |  | ||||||
| 					var parent_seen = 'seen' in state |  | ||||||
| 					var seen = state.seen =  |  | ||||||
| 						new Set(state.seen ?? []) |  | ||||||
| 					if(seen.has(full) |  | ||||||
| 							// nesting path recursion...
 |  | ||||||
| 							|| (full.length % (this.NESTING_RECURSION_TEST_THRESHOLD || 50) == 0 |  | ||||||
| 								&& (pwpath.split(full).length > 3 |  | ||||||
| 									&& new Set([ |  | ||||||
| 											await page.find(), |  | ||||||
| 											await page.get('..').find(), |  | ||||||
| 											await page.get('../..').find(), |  | ||||||
| 										]).size == 1 |  | ||||||
| 									// XXX HACK???
 |  | ||||||
| 									|| pwpath.split(full).length > (this.NESTING_DEPTH_LIMIT || 20)))){ |  | ||||||
| 						if(recursive == null){ |  | ||||||
| 							console.warn( |  | ||||||
| 								`@${key}(..): ${ |  | ||||||
| 									seen.has(full) ? |  | ||||||
| 										'direct' |  | ||||||
| 										: 'depth-limit' |  | ||||||
| 								} recursion detected:`, full, seen)
 |  | ||||||
| 							yield page.get(page.RECURSION_ERROR).parse()  |  | ||||||
| 							continue } |  | ||||||
| 						// have the 'recursive' arg...
 |  | ||||||
| 						yield base.parse(recursive, state)  |  | ||||||
| 						continue } |  | ||||||
| 					seen.add(full) |  | ||||||
| 
 |  | ||||||
| 					// load the included page...
 |  | ||||||
| 					var res = await handler.call(page, full, state) |  | ||||||
| 					depends.add(full) |  | ||||||
| 					res = strquotes ? |  | ||||||
| 						res |  | ||||||
| 							.replace(/["']/g, function(c){ |  | ||||||
| 								return '%'+ c.charCodeAt().toString(16) }) |  | ||||||
| 						: res |  | ||||||
| 
 |  | ||||||
| 					// NOTE: we only track recursion down and not sideways...
 |  | ||||||
| 					seen.delete(full) |  | ||||||
| 					if(!parent_seen){ |  | ||||||
| 						delete state.seen } |  | ||||||
| 
 |  | ||||||
| 					yield res } }), |  | ||||||
| 		// NOTE: the main difference between this and @include is that 
 |  | ||||||
| 		// 		this renders the src in the context of current page while 
 |  | ||||||
| 		// 		include is rendered in the context of its page but with
 |  | ||||||
| 		// 		the same state...
 |  | ||||||
| 		// 		i.e. for @include(PATH) the paths within the included page 
 |  | ||||||
| 		// 		are resolved relative to PATH while for @source(PATH) 
 |  | ||||||
| 		// 		relative to the page containing the @source(..) statement...
 |  | ||||||
| 		source: Macro( |  | ||||||
| 			// XXX should this have the same args as include???
 |  | ||||||
| 			['src', 'recursive', 'join',  |  | ||||||
| 				['s', 'strict']], |  | ||||||
| 			//['src'],
 |  | ||||||
| 			async function*(args, body, state){ |  | ||||||
| 				var that = this |  | ||||||
| 				yield* this.macros.include.call(this,  |  | ||||||
| 					'source', |  | ||||||
| 					args, body, state, 'sources',  |  | ||||||
| 					async function(src, state){ |  | ||||||
| 						//return that.parse(that.get(src).raw, state) }) }),
 |  | ||||||
| 						return that.parse(this.raw, state) }) }), |  | ||||||
| 
 |  | ||||||
| 		// Load macro and slot definitions but ignore the page text...
 |  | ||||||
| 		//
 |  | ||||||
| 		// NOTE: this is essentially the same as @source(..) but returns ''.
 |  | ||||||
| 		// XXX revise name...
 |  | ||||||
| 		load: Macro( |  | ||||||
| 			['src', ['strict']], |  | ||||||
| 			async function*(args, body, state){ |  | ||||||
| 				var that = this |  | ||||||
| 				yield* this.macros.include.call(this,  |  | ||||||
| 					'load', |  | ||||||
| 					args, body, state, 'sources',  |  | ||||||
| 					async function(src, state){ |  | ||||||
| 						await that.parse(this.raw, state)  |  | ||||||
| 						return '' }) }), |  | ||||||
| 		//
 |  | ||||||
| 		// 	@quote(<src>)
 |  | ||||||
| 		//
 |  | ||||||
| 		// 	<quote src=<src>[ filter="<filter> ..."]/>
 |  | ||||||
| 		//
 |  | ||||||
| 		// 	<quote text=" .. "[ filter="<filter> ..."]/>
 |  | ||||||
| 		//
 |  | ||||||
| 		// 	<quote[ filter="<filter> ..."]>
 |  | ||||||
| 		// 		..
 |  | ||||||
| 		// 	</quote>
 |  | ||||||
| 		//
 |  | ||||||
| 		//
 |  | ||||||
| 		// NOTE: src ant text arguments are mutually exclusive, src takes 
 |  | ||||||
| 		// 		priority.
 |  | ||||||
| 		// NOTE: the filter argument has the same semantics as the filter 
 |  | ||||||
| 		// 		macro with one exception, when used in quote, the body is 
 |  | ||||||
| 		// 		not expanded...
 |  | ||||||
| 		// NOTE: the filter argument uses the same filters as @filter(..)
 |  | ||||||
| 		// NOTE: else argument implies strict mode...
 |  | ||||||
| 		// XXX need a way to escape macros -- i.e. include </quote> in a quoted text...
 |  | ||||||
| 		// XXX should join/else be sub-tags???
 |  | ||||||
| 		quote: Macro( |  | ||||||
| 			['src', 'filter', 'text', 'join', 'else', |  | ||||||
| 				['s', 'expandactions', 'strict']], |  | ||||||
| 			async function*(args, body, state){ |  | ||||||
| 				var src = args.src //|| args[0]
 |  | ||||||
| 				var base = this.get(this.path.split(/\*/).shift()) |  | ||||||
| 				var text = args.text  |  | ||||||
| 					?? body  |  | ||||||
| 					?? [] |  | ||||||
| 				var strict = !!(args.strict  |  | ||||||
| 					?? args['else']  |  | ||||||
| 					?? false) |  | ||||||
| 				// parse arg values...
 |  | ||||||
| 				src = src ?  |  | ||||||
| 					await base.parse(src, state) |  | ||||||
| 					: src |  | ||||||
| 				// XXX INHERIT_ARGS special-case: inherit args by default...
 |  | ||||||
| 				if(this.actions_inherit_args  |  | ||||||
| 						&& this.actions_inherit_args.has(pwpath.basename(src)) |  | ||||||
| 						&& this.get(pwpath.dirname(src)).path == this.path){ |  | ||||||
| 					src += ':$ARGS' } |  | ||||||
| 				var expandactions =  |  | ||||||
| 					args.expandactions |  | ||||||
| 						?? true |  | ||||||
| 				// XXX EXPERIMENTAL
 |  | ||||||
| 				var strquotes = args.s |  | ||||||
| 
 |  | ||||||
| 				var depends = state.depends =  |  | ||||||
| 					state.depends  |  | ||||||
| 						?? new Set() |  | ||||||
| 				// XXX DEPENDS_PATTERN
 |  | ||||||
| 				depends.add(src) |  | ||||||
| 
 |  | ||||||
| 				var pages = src ? |  | ||||||
| 						(!expandactions  |  | ||||||
| 								&& await this.get(src).type == 'action' ? |  | ||||||
| 							base.get(this.QUOTE_ACTION_PAGE) |  | ||||||
| 							: await this.get(src).asPages(strict)) |  | ||||||
| 					: text instanceof Array ? |  | ||||||
| 						[text.join('')] |  | ||||||
| 					: typeof(text) == 'string' ? |  | ||||||
| 						[text] |  | ||||||
| 					: text |  | ||||||
| 				// else...
 |  | ||||||
| 				pages = ((!pages |  | ||||||
| 				   			|| pages.length == 0)	 |  | ||||||
| 						&& args['else']) ? |  | ||||||
| 					[await base.parse(args['else'], state)] |  | ||||||
| 					: pages |  | ||||||
| 				// empty...
 |  | ||||||
| 				if(!pages || pages.length == 0){ |  | ||||||
| 					return } |  | ||||||
| 
 |  | ||||||
| 				var join = args.join  |  | ||||||
| 					&& await base.parse(args.join, state) |  | ||||||
| 				var first = true |  | ||||||
| 				for await (var page of pages){ |  | ||||||
| 					if(join && !first){ |  | ||||||
| 						yield join } |  | ||||||
| 					first = false |  | ||||||
| 
 |  | ||||||
| 					text = typeof(page) == 'string' ? |  | ||||||
| 							page |  | ||||||
| 						: (!expandactions  |  | ||||||
| 								&& await page.type == 'action') ? |  | ||||||
| 							base.get(this.QUOTE_ACTION_PAGE).raw |  | ||||||
| 						: await page.raw |  | ||||||
| 					text = strquotes ? |  | ||||||
| 						text |  | ||||||
| 							.replace(/["']/g, function(c){ |  | ||||||
| 								return '%'+ c.charCodeAt().toString(16) }) |  | ||||||
| 						: text |  | ||||||
| 
 |  | ||||||
| 					page.path |  | ||||||
| 						&& depends.add(page.path) |  | ||||||
| 
 |  | ||||||
| 					var filters =  |  | ||||||
| 						args.filter  |  | ||||||
| 							&& args.filter |  | ||||||
| 								.trim() |  | ||||||
| 								.split(/\s+/g) |  | ||||||
| 
 |  | ||||||
| 					// NOTE: we are delaying .quote_filters handling here to 
 |  | ||||||
| 					// 		make their semantics the same as general filters...
 |  | ||||||
| 					// 		...and since we are internally calling .filter(..)
 |  | ||||||
| 					// 		macro we need to dance around it's architecture too...
 |  | ||||||
| 					// NOTE: since the body of quote(..) only has filters applied 
 |  | ||||||
| 					// 		to it doing the first stage of .filter(..) as late 
 |  | ||||||
| 					// 		as the second stage here will have no ill effect...
 |  | ||||||
| 					// NOTE: this uses the same filters as @filter(..)
 |  | ||||||
| 					// NOTE: the function wrapper here isolates text in 
 |  | ||||||
| 					// 		a closure per function...
 |  | ||||||
| 					yield (function(text){ |  | ||||||
| 						return async function(state){ |  | ||||||
| 							// add global quote-filters...
 |  | ||||||
| 							filters = |  | ||||||
| 								(state.quote_filters  |  | ||||||
| 										&& !(filters ?? []).includes(this.ISOLATED_FILTERS)) ? |  | ||||||
| 									[...state.quote_filters, ...(filters ?? [])] |  | ||||||
| 									: filters |  | ||||||
| 							return filters ? |  | ||||||
| 								await this.__parser__.callMacro( |  | ||||||
| 										this, 'filter', filters, text, state, false) |  | ||||||
| 									.call(this, state) |  | ||||||
| 								: text } })(text) } }), |  | ||||||
| 		// very similar to @filter(..) but will affect @quote(..) filters...
 |  | ||||||
| 		'quote-filter': function(args, body, state){ |  | ||||||
| 			var filters = state.quote_filters =  |  | ||||||
| 				state.quote_filters ?? [] |  | ||||||
| 			filters.splice(filters.length, 0, ...Object.keys(args)) }, |  | ||||||
| 		//
 |  | ||||||
| 		//	<slot name=<name>/>
 |  | ||||||
| 		//
 |  | ||||||
| 		//	<slot name=<name> text=<text>/>
 |  | ||||||
| 		//
 |  | ||||||
| 		//	<slot name=<name>>
 |  | ||||||
| 		//		...
 |  | ||||||
| 		//	</slot>
 |  | ||||||
| 		//
 |  | ||||||
| 		//	Force show a slot...
 |  | ||||||
| 		//	<slot shown ... />
 |  | ||||||
| 		//
 |  | ||||||
| 		//	Force hide a slot...
 |  | ||||||
| 		//	<slot hidden ... />
 |  | ||||||
| 		//
 |  | ||||||
| 		//	Insert previous slot content...
 |  | ||||||
| 		//	<content/>
 |  | ||||||
| 		//
 |  | ||||||
| 		//
 |  | ||||||
| 		// NOTE: by default only the first slot with <name> is visible, 
 |  | ||||||
| 		// 		all other slots with <name> will replace its content, unless
 |  | ||||||
| 		// 		explicit shown/hidden arguments are given.
 |  | ||||||
| 		// NOTE: hidden has precedence over shown if both are given.
 |  | ||||||
| 		// NOTE: slots are handled in order of occurrence of opening tags 
 |  | ||||||
| 		// 		in text and not by hierarchy, i.e. the later slot overrides
 |  | ||||||
| 		// 		the former and the most nested overrides the parent.
 |  | ||||||
| 		// 		This also works for cases where slots override slots they 
 |  | ||||||
| 		// 		are contained in, this will not lead to recursion.
 |  | ||||||
| 		//
 |  | ||||||
| 		// XXX revise the use of hidden/shown use mechanic and if it's 
 |  | ||||||
| 		// 		needed...
 |  | ||||||
| 		slot: Macro( |  | ||||||
| 			['name', 'text', ['shown', 'hidden']], |  | ||||||
| 			async function(args, body, state){ |  | ||||||
| 				var name = args.name |  | ||||||
| 				var text = args.text  |  | ||||||
| 					?? body  |  | ||||||
| 					// NOTE: this can't be undefined for .expand(..) to work 
 |  | ||||||
| 					// 		correctly...
 |  | ||||||
| 					?? [] |  | ||||||
| 
 |  | ||||||
| 				var slots = state.slots =  |  | ||||||
| 					state.slots  |  | ||||||
| 						?? {} |  | ||||||
| 
 |  | ||||||
| 				// parse arg values...
 |  | ||||||
| 				name = name ? |  | ||||||
| 					await this.parse(name, state) |  | ||||||
| 					: name |  | ||||||
| 
 |  | ||||||
| 				//var hidden = name in slots
 |  | ||||||
| 				// XXX EXPERIMENTAL
 |  | ||||||
| 				var hidden =  |  | ||||||
| 					// 'hidden' has priority... 
 |  | ||||||
| 					args.hidden |  | ||||||
| 						// explicitly show... ()
 |  | ||||||
| 						|| (args.shown ? |  | ||||||
| 							false |  | ||||||
| 							// show first instance...
 |  | ||||||
| 							: name in slots) |  | ||||||
| 
 |  | ||||||
| 				// set slot value...
 |  | ||||||
| 				var stack = [] |  | ||||||
| 				slots[name] |  | ||||||
| 					&& stack.push(slots[name]) |  | ||||||
| 				delete slots[name] |  | ||||||
| 				var slot = await this.__parser__.expand(this, text, state) |  | ||||||
| 				var original = slot |  | ||||||
| 				slots[name] |  | ||||||
| 					&& stack.unshift(slot) |  | ||||||
| 				slot = slots[name] =  |  | ||||||
| 					slots[name]  |  | ||||||
| 						?? slot |  | ||||||
| 				// handle <content/>...
 |  | ||||||
| 				for(prev of stack){ |  | ||||||
| 					// get the first <content/>
 |  | ||||||
| 					for(var i in slot){ |  | ||||||
| 						if(typeof(slot[i]) != 'string' |  | ||||||
| 								&& slot[i].name == 'content'){ |  | ||||||
| 							break }  |  | ||||||
| 						i = null } |  | ||||||
| 					i != null |  | ||||||
| 						&& slot.splice(i, 1,  |  | ||||||
| 							...prev |  | ||||||
| 								// remove nested slot handlers...
 |  | ||||||
| 								.filter(function(e){ |  | ||||||
| 									return typeof(e) != 'function' |  | ||||||
| 											|| e.slot != name }) ) } |  | ||||||
| 				return hidden ? |  | ||||||
| 					'' |  | ||||||
| 					: Object.assign( |  | ||||||
| 						function(state){ |  | ||||||
| 							return (state.slots || {})[name] ?? original }, |  | ||||||
| 						{slot: name}) }),  |  | ||||||
| 		'content': ['slot'], |  | ||||||
| 
 |  | ||||||
| 		// XXX EXPERIMENTAL...
 |  | ||||||
| 		//
 |  | ||||||
| 		// NOTE: var value is parsed only on assignment and not on dereferencing...
 |  | ||||||
| 		//
 |  | ||||||
| 		// XXX should alpha/Alpha be 0 (current) or 1 based???
 |  | ||||||
| 		// XXX do we need a default attr???
 |  | ||||||
| 		// 		...i.e. if not defined set to ..
 |  | ||||||
| 		// XXX INC_DEC do we need inc/dec and parent???
 |  | ||||||
| 		'var': Macro( |  | ||||||
| 			['name', 'text',  |  | ||||||
| 				// XXX INC_DEC
 |  | ||||||
| 				['shown', 'hidden',  |  | ||||||
| 					'parent',  |  | ||||||
| 					'inc', 'dec',  |  | ||||||
| 					'alpha', 'Alpha', 'roman', 'Roman']], |  | ||||||
| 				/*/ |  | ||||||
| 				['shown', 'hidden']], |  | ||||||
| 				//*/
 |  | ||||||
| 			async function(args, body, state){ |  | ||||||
| 				var name = args.name |  | ||||||
| 				if(!name){ |  | ||||||
| 					return '' } |  | ||||||
| 				name = await this.parse(name, state) |  | ||||||
| 				// XXX INC_DEC
 |  | ||||||
| 				var inc = args.inc |  | ||||||
| 				var dec = args.dec |  | ||||||
| 				//*/
 |  | ||||||
| 				var text = args.text  |  | ||||||
| 					?? body  |  | ||||||
| 				// NOTE: .hidden has priority...
 |  | ||||||
| 				var show =  |  | ||||||
| 						('hidden' in args ? |  | ||||||
| 							!args.hidden |  | ||||||
| 							: undefined) |  | ||||||
| 						?? args.shown  |  | ||||||
| 
 |  | ||||||
| 				var vars = state.vars =  |  | ||||||
| 					state.vars  |  | ||||||
| 						?? {} |  | ||||||
| 				// XXX INC_DEC
 |  | ||||||
| 				if(args.parent && name in vars){ |  | ||||||
| 					while(!vars.hasOwnProperty(name) |  | ||||||
| 							&& vars.__proto__ !== Object.prototype){ |  | ||||||
| 						vars = vars.__proto__ } } |  | ||||||
| 
 |  | ||||||
| 				var handleFormat = function(value){ |  | ||||||
| 					// roman number...
 |  | ||||||
| 					if(args.roman || args.Roman){ |  | ||||||
| 						var n = parseInt(value) |  | ||||||
| 						return isNaN(n) ? |  | ||||||
| 								'' |  | ||||||
| 							: args.Roman ? |  | ||||||
| 								n.toRoman() |  | ||||||
| 							: n.toRoman().toLowerCase() } |  | ||||||
| 					// alpha number...
 |  | ||||||
| 					if(args.alpha || args.Alpha){ |  | ||||||
| 						var n = parseInt(value) |  | ||||||
| 						return isNaN(n) ? |  | ||||||
| 								'' |  | ||||||
| 							: args.Alpha ? |  | ||||||
| 								n.toAlpha().toUpperCase() |  | ||||||
| 							: n.toAlpha() }  |  | ||||||
| 					return value } |  | ||||||
| 
 |  | ||||||
| 				// inc/dec...
 |  | ||||||
| 				if(inc || dec){ |  | ||||||
| 					if(!(name in vars)  |  | ||||||
| 							|| isNaN(parseInt(vars[name]))){ |  | ||||||
| 						return '' } |  | ||||||
| 					var cur = parseInt(vars[name]) |  | ||||||
| 					cur +=  |  | ||||||
| 						inc === true ?  |  | ||||||
| 							1  |  | ||||||
| 						: !inc ? |  | ||||||
| 							0 |  | ||||||
| 						: parseInt(inc) |  | ||||||
| 					cur -=  |  | ||||||
| 						dec === true ?  |  | ||||||
| 							1  |  | ||||||
| 						: !dec ? |  | ||||||
| 							0 |  | ||||||
| 						: parseInt(dec) |  | ||||||
| 					vars[name] = cur + '' |  | ||||||
| 
 |  | ||||||
| 					// as-is...
 |  | ||||||
| 					return show ?? true ? |  | ||||||
| 						handleFormat(vars[name]) |  | ||||||
| 						: '' } |  | ||||||
| 				//*/
 |  | ||||||
| 
 |  | ||||||
| 				// set...
 |  | ||||||
| 				if(text){ |  | ||||||
| 					text = vars[name] =  |  | ||||||
| 						await this.parse(text, state) |  | ||||||
| 					return show ?? false ? |  | ||||||
| 						text |  | ||||||
| 						: '' |  | ||||||
| 				// get...
 |  | ||||||
| 				} else { |  | ||||||
| 					return handleFormat(vars[name] ?? '') } }), |  | ||||||
| 		vars: async function(args, body, state){ |  | ||||||
| 			var vars = state.vars =  |  | ||||||
| 				state.vars  |  | ||||||
| 					?? {} |  | ||||||
| 			for(var [name, value] of Object.entries(args)){ |  | ||||||
| 				vars[await this.parse(name, state)] = |  | ||||||
| 					await this.parse(value, state) } |  | ||||||
| 			return '' }, |  | ||||||
| 
 |  | ||||||
| 		// 	
 |  | ||||||
| 		// 	<macro src=<url>> .. </macro>
 |  | ||||||
| 		//
 |  | ||||||
| 		// 	<macro name=<name> src=<url> sort=<sort-spec>> .. </macro>
 |  | ||||||
| 		//
 |  | ||||||
| 		// 	<macro ...> ... </macro>
 |  | ||||||
| 		// 	<macro ... text=<text>/>
 |  | ||||||
| 		//
 |  | ||||||
| 		// 	<macro ... else=<text>> ... </macro>
 |  | ||||||
| 		// 	<macro ...>
 |  | ||||||
| 		// 		...
 |  | ||||||
| 		//
 |  | ||||||
| 		//
 |  | ||||||
| 		// 		<join>
 |  | ||||||
| 		// 			...
 |  | ||||||
| 		// 		</join>
 |  | ||||||
| 		//
 |  | ||||||
| 		// 		<else>
 |  | ||||||
| 		// 			...
 |  | ||||||
| 		// 		</else>
 |  | ||||||
| 		// 	</macro>
 |  | ||||||
| 		//
 |  | ||||||
| 		// Macro variables:
 |  | ||||||
| 		// 	macro:count
 |  | ||||||
| 		// 	macro:index
 |  | ||||||
| 		//
 |  | ||||||
| 		// NOTE: this handles src count argument internally partially 
 |  | ||||||
| 		// 		overriding <store>.match(..)'s implementation, this is done
 |  | ||||||
| 		// 		because @macro(..) needs to account for arbitrary nesting 
 |  | ||||||
| 		// 		that <store>.match(..) can not know about...
 |  | ||||||
| 		// 		XXX should we do the same for offset???
 |  | ||||||
| 		//
 |  | ||||||
| 		// XXX BUG: strict does not seem to work:
 |  | ||||||
| 		// 				@macro(src="./resolved-page" else="no" text="yes" strict)
 |  | ||||||
| 		// 					-> yes
 |  | ||||||
| 		// 			should be "no"
 |  | ||||||
| 		// 			...this seems to effect non-pattern pages...
 |  | ||||||
| 		// XXX should macro:index be 0 or 1 (current) based???
 |  | ||||||
| 		// XXX SORT sorting not implemented yet...
 |  | ||||||
| 		macro: Macro( |  | ||||||
| 			['name', 'src', 'sort', 'text', 'join', 'else', |  | ||||||
| 				['strict', 'isolated', 'inheritmacros', 'inheritvars']], |  | ||||||
| 			async function*(args, body, state){ |  | ||||||
| 				var that = this |  | ||||||
| 
 |  | ||||||
| 				// helpers...
 |  | ||||||
| 				var _getBlock = function(name){ |  | ||||||
| 					var block = args[name] ? |  | ||||||
| 						[{ |  | ||||||
| 							args: {}, |  | ||||||
| 							body: args[name], |  | ||||||
| 						}] |  | ||||||
| 						: (text ?? []) |  | ||||||
| 							.filter(function(e){  |  | ||||||
| 								return typeof(e) != 'string'  |  | ||||||
| 									&& e.name == name }) |  | ||||||
| 					if(block.length == 0){ |  | ||||||
| 						return } |  | ||||||
| 					// NOTE: when multiple blocks are present the 
 |  | ||||||
| 					// 		last one is used...
 |  | ||||||
| 					block = block.pop() |  | ||||||
| 					block =  |  | ||||||
| 						block.args.text  |  | ||||||
| 							?? block.body |  | ||||||
| 					return block } |  | ||||||
| 
 |  | ||||||
| 				var base = this.get(this.path.split(/\*/).shift()) |  | ||||||
| 				var macros = state.macros =  |  | ||||||
| 					state.macros  |  | ||||||
| 						?? {} |  | ||||||
| 				var vars = state.vars =  |  | ||||||
| 					state.vars  |  | ||||||
| 						?? {} |  | ||||||
| 				var depends = state.depends =  |  | ||||||
| 					state.depends  |  | ||||||
| 						?? new Set() |  | ||||||
| 
 |  | ||||||
| 				// uninheritable args...
 |  | ||||||
| 				// NOTE: arg handling is split in two, to make things simpler 
 |  | ||||||
| 				// 		to process for retrieved named macros...
 |  | ||||||
| 				var src = args.src |  | ||||||
| 				var text = args.text  |  | ||||||
| 					?? body  |  | ||||||
| 					?? [] |  | ||||||
| 				text = typeof(text) == 'string' ? |  | ||||||
| 					[...this.__parser__.group(this, text+'</macro>', 'macro')] |  | ||||||
| 					: text |  | ||||||
| 				var join, itext |  | ||||||
| 				var iargs = {} |  | ||||||
| 
 |  | ||||||
| 				// stored macros...
 |  | ||||||
| 				if(args.name){ |  | ||||||
| 					var name = await base.parse(args.name, state) |  | ||||||
| 					// define new named macro...
 |  | ||||||
| 					if(text.length != 0){ |  | ||||||
| 						// NOTE: we do not need to worry about saving 
 |  | ||||||
| 						// 		stateful text here because it is only 
 |  | ||||||
| 						// 		grouped and not expanded...
 |  | ||||||
| 						macros[name] =  |  | ||||||
| 							[ text,  |  | ||||||
| 								_getBlock('join'),  |  | ||||||
| 								JSON.parse(JSON.stringify(args)), ] |  | ||||||
| 					// use existing macro...
 |  | ||||||
| 					} else if(macros  |  | ||||||
| 							&& name in macros){ |  | ||||||
| 						;[itext, join, iargs] = macros[name] } } |  | ||||||
| 
 |  | ||||||
| 				// inheritable args...
 |  | ||||||
| 				// XXX is there a point in overloading text???
 |  | ||||||
| 				text = text.length > 0 ?  |  | ||||||
| 					text  |  | ||||||
| 					: itext ?? text |  | ||||||
| 				var sort = (args.sort  |  | ||||||
| 						?? iargs.sort  |  | ||||||
| 						?? '') |  | ||||||
| 					.split(/\s+/g) |  | ||||||
| 					.filter(function(e){  |  | ||||||
| 						return e != '' }) |  | ||||||
| 				var strict =  |  | ||||||
| 					('strict' in args ? |  | ||||||
| 							args.strict  |  | ||||||
| 							: iargs.strict) |  | ||||||
| 						//?? true
 |  | ||||||
| 						?? false |  | ||||||
| 				var isolated =  |  | ||||||
| 					('isolated' in args ? |  | ||||||
| 							args.isolated |  | ||||||
| 							: iargs.isolated) |  | ||||||
| 						?? true |  | ||||||
| 				var inheritmacros =  |  | ||||||
| 					('inheritmacros' in args ? |  | ||||||
| 							args.inheritmacros |  | ||||||
| 							: iargs.inheritmacros) |  | ||||||
| 						?? true |  | ||||||
| 				var inheritvars =  |  | ||||||
| 					('inheritvars' in args ? |  | ||||||
| 							args.inheritvars |  | ||||||
| 							: iargs.inheritvars) |  | ||||||
| 						?? true |  | ||||||
| 
 |  | ||||||
| 				if(src){ |  | ||||||
| 					src = await base.parse(src, state) |  | ||||||
| 					// XXX INHERIT_ARGS special-case: inherit args by default...
 |  | ||||||
| 					if(this.actions_inherit_args  |  | ||||||
| 							&& this.actions_inherit_args.has(pwpath.basename(src)) |  | ||||||
| 							&& this.get(pwpath.dirname(src)).path == this.path){ |  | ||||||
| 						src += ':$ARGS' } |  | ||||||
| 					// XXX DEPENDS_PATTERN
 |  | ||||||
| 					depends.add(src) |  | ||||||
| 
 |  | ||||||
| 					join = _getBlock('join')  |  | ||||||
| 						?? join  |  | ||||||
| 					join = join |  | ||||||
| 						&& await base.parse(join, state) |  | ||||||
| 
 |  | ||||||
| 					//var match = this.get(await base.parse(src, state))
 |  | ||||||
| 					//var match = this.get(src, strict)
 |  | ||||||
| 					var match = this.get(src) |  | ||||||
| 
 |  | ||||||
| 					// NOTE: thie does not introduce a dependency on each 
 |  | ||||||
| 					// 		of the iterated pages, that is handled by the 
 |  | ||||||
| 					// 		respective include/source/.. macros, this however
 |  | ||||||
| 					// 		only depends on page count...
 |  | ||||||
| 					depends.add(match.path) |  | ||||||
| 
 |  | ||||||
| 					// populate macrovars...
 |  | ||||||
| 					var macrovars = {} |  | ||||||
| 					for(var [key, value]  |  | ||||||
| 							of Object.entries( |  | ||||||
| 								Object.assign( |  | ||||||
| 									args, |  | ||||||
| 									iargs, |  | ||||||
| 									{ |  | ||||||
| 										strict, |  | ||||||
| 										isolated, |  | ||||||
| 										inheritmacros, |  | ||||||
| 										inheritvars, |  | ||||||
| 									}))){ |  | ||||||
| 						macrovars['macro:'+ key] =  |  | ||||||
| 							value === true ? |  | ||||||
| 								'yes' |  | ||||||
| 							: value === false ? |  | ||||||
| 								'no' |  | ||||||
| 							: value } |  | ||||||
| 
 |  | ||||||
| 					// handle count...
 |  | ||||||
| 					// NOTE: this duplicates <store>.match(..)'s functionality
 |  | ||||||
| 					// 		because we need to account for arbitrary macro 
 |  | ||||||
| 					// 		nesting that .match(..) does not know about...
 |  | ||||||
| 					// XXX revise var naming...
 |  | ||||||
| 					// XXX these can be overriden in nested macros...
 |  | ||||||
| 					var count = match.args.count |  | ||||||
| 					if(count){ |  | ||||||
| 						var c = |  | ||||||
| 							count == 'inherit' ? |  | ||||||
| 								(!('macro:count' in vars) ? |  | ||||||
| 									this.args.count |  | ||||||
| 									: undefined) |  | ||||||
| 								: count  |  | ||||||
| 						if(c !== undefined){ |  | ||||||
| 							vars['macro:count'] =  |  | ||||||
| 								isNaN(parseInt(c)) ? |  | ||||||
| 									c |  | ||||||
| 									: parseInt(c)  |  | ||||||
| 							vars['macro:index'] = 0 } } |  | ||||||
| 						 |  | ||||||
| 					// expand matches...
 |  | ||||||
| 					var first = true |  | ||||||
| 					for await(var page of match.asPages(strict)){ |  | ||||||
| 						// handle count...
 |  | ||||||
| 						if('macro:count' in vars){ |  | ||||||
| 							if(vars['macro:count'] <= vars['macro:index']){ |  | ||||||
| 								break } |  | ||||||
| 							object.sources(vars, 'macro:index') |  | ||||||
| 								.shift()['macro:index']++ } |  | ||||||
| 						// output join between elements....
 |  | ||||||
| 						if(join && !first){ |  | ||||||
| 							yield join } |  | ||||||
| 						first = false  |  | ||||||
| 						if(isolated){ |  | ||||||
| 							var _state = { |  | ||||||
| 								seen: state.seen,  |  | ||||||
| 								depends, |  | ||||||
| 								renderer: state.renderer, |  | ||||||
| 								macros: inheritmacros ? |  | ||||||
| 									{__proto__: macros} |  | ||||||
| 									: {}, |  | ||||||
| 								vars: inheritvars ? |  | ||||||
| 									{__proto__: vars,  |  | ||||||
| 										...macrovars} |  | ||||||
| 									: {...macrovars}, |  | ||||||
| 							} |  | ||||||
| 							yield this.__parser__.parse(page,  |  | ||||||
| 								this.__parser__.expand(page,  |  | ||||||
| 									text, _state), _state) |  | ||||||
| 						} else { |  | ||||||
| 							yield this.__parser__.expand(page, text, state) } } |  | ||||||
| 					// cleanup...
 |  | ||||||
| 					delete vars['macro:count'] |  | ||||||
| 					delete vars['macro:index'] |  | ||||||
| 					// else...
 |  | ||||||
| 					if(first |  | ||||||
| 							&& (text || args['else'])){ |  | ||||||
| 						var else_block = _getBlock('else') |  | ||||||
| 						if(else_block){ |  | ||||||
| 							yield this.__parser__.expand(this, else_block, state) } } } }), |  | ||||||
| 
 |  | ||||||
| 		// nesting rules...
 |  | ||||||
| 		'else': ['macro'], |  | ||||||
| 		'join': ['macro'], |  | ||||||
| 	} }, |  | ||||||
| 
 |  | ||||||
| 	// XXX EXPERIMENTAL...
 | 	// XXX EXPERIMENTAL...
 | ||||||
| 	//
 | 	//
 | ||||||
| 	// 	Define a global macro...
 | 	// 	Define a global macro...
 | ||||||
| @ -1796,13 +918,14 @@ object.Constructor('Page', BasePage, { | |||||||
| 	// 	.defmacro(<name>, <args>, <func>)
 | 	// 	.defmacro(<name>, <args>, <func>)
 | ||||||
| 	// 		-> this
 | 	// 		-> this
 | ||||||
| 	//
 | 	//
 | ||||||
| 	// XXX do we need this???
 | 	/* XXX do we need this??? | ||||||
| 	defmacro: function(name, args, func){ | 	defmacro: function(name, args, func){ | ||||||
| 		this.macros[name] =  | 		this.__parser__.macros[name] =  | ||||||
| 			arguments.length == 2 ? | 			arguments.length == 2 ? | ||||||
| 				arguments[1] | 				arguments[1] | ||||||
| 				: Macro(args, func) | 				: Macro(args, func) | ||||||
| 		return this }, | 		return this }, | ||||||
|  | 	//*/
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	// direct actions...
 | 	// direct actions...
 | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ | |||||||
| (function(require){ var module={} // make module AMD/node compatible...
 | (function(require){ var module={} // make module AMD/node compatible...
 | ||||||
| /*********************************************************************/ | /*********************************************************************/ | ||||||
| 
 | 
 | ||||||
|  | var object = require('ig-object') | ||||||
| var types = require('ig-types') | var types = require('ig-types') | ||||||
| 
 | 
 | ||||||
| var pwpath = require('./path') | var pwpath = require('./path') | ||||||
| @ -209,7 +210,7 @@ module.BaseParser = { | |||||||
| 		return res }, | 		return res }, | ||||||
| 	// XXX should this be here or on page???
 | 	// XXX should this be here or on page???
 | ||||||
| 	callMacro: function(page, name, args, body, state, ...rest){ | 	callMacro: function(page, name, args, body, state, ...rest){ | ||||||
| 		var macro = page.macros[name]  | 		var macro = this.macros[name]  | ||||||
| 		return macro.call(page,  | 		return macro.call(page,  | ||||||
| 				this.parseArgs( | 				this.parseArgs( | ||||||
| 					macro.arg_spec  | 					macro.arg_spec  | ||||||
| @ -248,7 +249,7 @@ module.BaseParser = { | |||||||
| 	// 		}
 | 	// 		}
 | ||||||
| 	//
 | 	//
 | ||||||
| 	//
 | 	//
 | ||||||
| 	// NOTE: this internally uses page.macros' keys to generate the 
 | 	// NOTE: this internally uses .macros' keys to generate the 
 | ||||||
| 	// 		lexing pattern.
 | 	// 		lexing pattern.
 | ||||||
| 	lex: function*(page, str){ | 	lex: function*(page, str){ | ||||||
| 		str = typeof(str) != 'string' ? | 		str = typeof(str) != 'string' ? | ||||||
| @ -264,7 +265,7 @@ module.BaseParser = { | |||||||
| 
 | 
 | ||||||
| 		// XXX should this be cached???
 | 		// XXX should this be cached???
 | ||||||
| 		var macro_pattern = this.MACRO_PATTERN  | 		var macro_pattern = this.MACRO_PATTERN  | ||||||
| 			?? this.buildMacroPattern(Object.deepKeys(page.macros)) | 			?? this.buildMacroPattern(Object.deepKeys(this.macros)) | ||||||
| 		var macro_pattern_groups = this.MACRO_PATTERN_GROUPS  | 		var macro_pattern_groups = this.MACRO_PATTERN_GROUPS  | ||||||
| 			?? this.countMacroPatternGroups() | 			?? this.countMacroPatternGroups() | ||||||
| 		var macro_args_pattern = this.MACRO_ARGS_PATTERN  | 		var macro_args_pattern = this.MACRO_ARGS_PATTERN  | ||||||
| @ -368,7 +369,7 @@ module.BaseParser = { | |||||||
| 	// 			...
 | 	// 			...
 | ||||||
| 	// 		}
 | 	// 		}
 | ||||||
| 	//
 | 	//
 | ||||||
| 	// NOTE: this internaly uses page.macros to check for propper nesting
 | 	// NOTE: this internaly uses .macros to check for propper nesting
 | ||||||
| 	//group: function*(page, lex, to=false){
 | 	//group: function*(page, lex, to=false){
 | ||||||
| 	group: function*(page, lex, to=false, parent){ | 	group: function*(page, lex, to=false, parent){ | ||||||
| 		// XXX we can't get .raw from the page without going async...
 | 		// XXX we can't get .raw from the page without going async...
 | ||||||
| @ -410,8 +411,8 @@ module.BaseParser = { | |||||||
| 			// assert nesting rules...
 | 			// assert nesting rules...
 | ||||||
| 			// NOTE: we only check for direct nesting...
 | 			// NOTE: we only check for direct nesting...
 | ||||||
| 			// XXX might be a good idea to link nested block to the parent...
 | 			// XXX might be a good idea to link nested block to the parent...
 | ||||||
| 			if(page.macros[value.name] instanceof Array | 			if(this.macros[value.name] instanceof Array | ||||||
| 					&& !page.macros[value.name].includes(to) | 					&& !this.macros[value.name].includes(to) | ||||||
| 					// do not complain about closing nestable tags...
 | 					// do not complain about closing nestable tags...
 | ||||||
| 					&& !(value.name == to  | 					&& !(value.name == to  | ||||||
| 						&& value.type == 'closing')){ | 						&& value.type == 'closing')){ | ||||||
| @ -496,7 +497,7 @@ module.BaseParser = { | |||||||
| 					// macro...
 | 					// macro...
 | ||||||
| 					var {name, args, body} = value | 					var {name, args, body} = value | ||||||
| 					// nested macro -- skip...
 | 					// nested macro -- skip...
 | ||||||
| 					if(typeof(page.macros[name]) != 'function'){ | 					if(typeof(that.macros[name]) != 'function'){ | ||||||
| 						return {...value, skip: true} } | 						return {...value, skip: true} } | ||||||
| 					// macro call...
 | 					// macro call...
 | ||||||
| 					return Promise.awaitOrRun( | 					return Promise.awaitOrRun( | ||||||
| @ -505,7 +506,7 @@ module.BaseParser = { | |||||||
| 							res = res ?? '' | 							res = res ?? '' | ||||||
| 							// result...
 | 							// result...
 | ||||||
| 							if(res instanceof Array  | 							if(res instanceof Array  | ||||||
| 									|| page.macros[name] instanceof types.Generator){ | 									|| that.macros[name] instanceof types.Generator){ | ||||||
| 								return res | 								return res | ||||||
| 							} else { | 							} else { | ||||||
| 								return [res] } }) }, | 								return [res] } }) }, | ||||||
| @ -679,7 +680,17 @@ module.parser = { | |||||||
| 	// list of macros that will get raw text of their content...
 | 	// list of macros that will get raw text of their content...
 | ||||||
| 	QUOTING_MACROS: ['quote'], | 	QUOTING_MACROS: ['quote'], | ||||||
| 
 | 
 | ||||||
| 	// XXX move macros here from page.js...
 | 	//
 | ||||||
|  | 	// 	<macro>(<args>, <body>, <state>){ .. }
 | ||||||
|  | 	// 		-> undefined
 | ||||||
|  | 	// 		-> <text>
 | ||||||
|  | 	// 		-> <array>
 | ||||||
|  | 	// 		-> <iterator>
 | ||||||
|  | 	// 		-> <func>(<state>)
 | ||||||
|  | 	// 			-> ...
 | ||||||
|  | 	//
 | ||||||
|  | 	// XXX do we need to make .macro.__proto__ module level object???
 | ||||||
|  | 	// XXX ASYNC make these support async page getters...
 | ||||||
| 	macros: { | 	macros: { | ||||||
| 		//
 | 		//
 | ||||||
| 		//	@(<name>[ <else>][ local])
 | 		//	@(<name>[ <else>][ local])
 | ||||||
| @ -715,11 +726,836 @@ module.parser = { | |||||||
| 		'': Macro( | 		'': Macro( | ||||||
| 			['name', 'else', ['local']], | 			['name', 'else', ['local']], | ||||||
| 			function(args){ | 			function(args){ | ||||||
| 				return this.macros.arg.call(this, args) }), | 				return this.__parser__.macros.arg.call(this, args) }), | ||||||
| 		args: function(){ | 		args: function(){ | ||||||
| 			return pwpath.obj2args(this.args) }, | 			return pwpath.obj2args(this.args) }, | ||||||
|  | 		//
 | ||||||
|  | 		// 	@filter(<filter-spec>)
 | ||||||
|  | 		// 	<filter <filter-spec>/>
 | ||||||
|  | 		//
 | ||||||
|  | 		// 	<filter <filter-spec>>
 | ||||||
|  | 		// 		...
 | ||||||
|  | 		// 	</filter>
 | ||||||
|  | 		//
 | ||||||
|  | 		// 	<filter-spec> ::=
 | ||||||
|  | 		// 		<filter> <filter-spec>
 | ||||||
|  | 		// 		| -<filter> <filter-spec>
 | ||||||
|  | 		//
 | ||||||
|  | 		// XXX BUG: this does not show any results:
 | ||||||
|  | 		//			pwiki.parse('<filter test>moo test</filter>')
 | ||||||
|  | 		//				-> ''
 | ||||||
|  | 		//		while these do:
 | ||||||
|  | 		//    		pwiki.parse('<filter test/>moo test')
 | ||||||
|  | 		//				-> 'moo TEST'
 | ||||||
|  | 		//			await pwiki.parse('<filter test>moo test</filter>@var()')
 | ||||||
|  | 		//				-> 'moo TEST'
 | ||||||
|  | 		//		for more info see:
 | ||||||
|  | 		//			file:///L:/work/pWiki/pwiki2.html#/Editors/Results
 | ||||||
|  | 		//		XXX do we fix this or revise how/when filters work???
 | ||||||
|  | 		//			...including accounting for variables/expansions and the like...
 | ||||||
|  | 		// XXX REVISE...
 | ||||||
|  | 		filter: function(args, body, state, expand=true){ | ||||||
|  | 			var that = this | ||||||
| 
 | 
 | ||||||
| 		// XXX
 | 			var outer = state.filters =  | ||||||
|  | 				state.filters ?? [] | ||||||
|  | 			var local = Object.keys(args) | ||||||
|  | 
 | ||||||
|  | 			// trigger quote-filter...
 | ||||||
|  | 			var quote = local | ||||||
|  | 				.map(function(filter){ | ||||||
|  | 					return (that.filters[filter] ?? {})['quote'] ?? [] }) | ||||||
|  | 				.flat() | ||||||
|  | 			quote.length > 0 | ||||||
|  | 				&& this.__parser__.macros['quote-filter'] | ||||||
|  | 					.call(this, Object.fromEntries(Object.entries(quote)), null, state) | ||||||
|  | 
 | ||||||
|  | 			// local filters...
 | ||||||
|  | 			if(body != null){ | ||||||
|  | 				// expand the body...
 | ||||||
|  | 				var ast = expand ? | ||||||
|  | 						this.__parser__.expand(this, body, state) | ||||||
|  | 					: body instanceof Array ? | ||||||
|  | 						body | ||||||
|  | 					// NOTE: wrapping the body in an array effectively 
 | ||||||
|  | 					// 		escapes it from parsing...
 | ||||||
|  | 					: [body] | ||||||
|  | 
 | ||||||
|  | 				return function(state){ | ||||||
|  | 					// XXX can we loose stuff from state this way???
 | ||||||
|  | 					// 		...at this stage it should more or less be static -- check!
 | ||||||
|  | 					return Promise.awaitOrRun( | ||||||
|  | 						this.__parser__.parse(this, ast, { | ||||||
|  | 							...state, | ||||||
|  | 							filters: local.includes(this.ISOLATED_FILTERS) ? | ||||||
|  | 								local | ||||||
|  | 								: [...outer, ...local], | ||||||
|  | 						}), | ||||||
|  | 						function(res){ | ||||||
|  | 							return {data: res} }) } | ||||||
|  | 				/*/ // XXX ASYNC...
 | ||||||
|  | 				return async function(state){ | ||||||
|  | 					// XXX can we loose stuff from state this way???
 | ||||||
|  | 					// 		...at this stage it should more or less be static -- check!
 | ||||||
|  | 					var res =  | ||||||
|  | 						await this.__parser__.parse(this, ast, { | ||||||
|  | 							...state, | ||||||
|  | 							filters: local.includes(this.ISOLATED_FILTERS) ? | ||||||
|  | 								local | ||||||
|  | 								: [...outer, ...local], | ||||||
|  | 						}) | ||||||
|  | 					return {data: res} } | ||||||
|  | 				//*/
 | ||||||
|  | 
 | ||||||
|  | 			// global filters...
 | ||||||
|  | 			} else { | ||||||
|  | 				state.filters = [...outer, ...local] } }, | ||||||
|  | 		//
 | ||||||
|  | 		// 	@include(<path>)
 | ||||||
|  | 		//
 | ||||||
|  | 		// 	@include(<path> isolated recursive=<text>)
 | ||||||
|  | 		// 	@include(src=<path> isolated recursive=<text>)
 | ||||||
|  | 		//
 | ||||||
|  | 		// 	<include src=<path> .. >
 | ||||||
|  | 		// 		<text>
 | ||||||
|  | 		// 	</include>
 | ||||||
|  | 		//
 | ||||||
|  | 		// NOTE: there can be two ways of recursion in pWiki:
 | ||||||
|  | 		// 			- flat recursion
 | ||||||
|  | 		// 				/A -> /A -> /A -> ..
 | ||||||
|  | 		// 			- nested recursion 
 | ||||||
|  | 		// 				/A -> /A/A -> /A/A/A -> ..
 | ||||||
|  | 		// 		Both can be either direct (type I) or indirect (type II).
 | ||||||
|  | 		// 		The former is trivial to check for while the later is 
 | ||||||
|  | 		// 		not quite so, as we can have different contexts at 
 | ||||||
|  | 		// 		different paths that would lead to different resulting 
 | ||||||
|  | 		// 		renders.
 | ||||||
|  | 		// 		At the moment nested recursion is checked in a fast but 
 | ||||||
|  | 		// 		not 100% correct manner focusing on path depth and ignoring
 | ||||||
|  | 		// 		the context, this potentially can lead to false positives.
 | ||||||
|  | 		// XXX need a way to make encode option transparent...
 | ||||||
|  | 		// XXX store a page cache in state...
 | ||||||
|  | 		include: Macro( | ||||||
|  | 			['src', 'recursive', 'join',  | ||||||
|  | 				['s', 'strict', 'isolated']], | ||||||
|  | 			async function*(args, body, state, key='included', handler){ | ||||||
|  | 				var macro = 'include' | ||||||
|  | 				if(typeof(args) == 'string'){ | ||||||
|  | 					var [macro, args, body, state, key, handler] = arguments  | ||||||
|  | 					key = key ?? 'included' } | ||||||
|  | 				var base = this.get(this.path.split(/\*/).shift()) | ||||||
|  | 				var src = args.src | ||||||
|  | 					&& this.resolvePathVars( | ||||||
|  | 						await base.parse(args.src, state)) | ||||||
|  | 				if(!src){ | ||||||
|  | 					return } | ||||||
|  | 				// XXX INHERIT_ARGS special-case: inherit args by default...
 | ||||||
|  | 				// XXX should this be done when isolated???
 | ||||||
|  | 				if(this.actions_inherit_args  | ||||||
|  | 						&& this.actions_inherit_args.has(pwpath.basename(src)) | ||||||
|  | 						&& this.get(pwpath.dirname(src)).path == this.path){ | ||||||
|  | 					src += ':$ARGS' } | ||||||
|  | 				var recursive = args.recursive ?? body | ||||||
|  | 				var isolated = args.isolated  | ||||||
|  | 				var strict = args.strict | ||||||
|  | 				var strquotes = args.s | ||||||
|  | 				var join = args.join  | ||||||
|  | 					&& await base.parse(args.join, state) | ||||||
|  | 
 | ||||||
|  | 				var depends = state.depends =  | ||||||
|  | 					state.depends  | ||||||
|  | 						?? new Set() | ||||||
|  | 				// XXX DEPENDS_PATTERN
 | ||||||
|  | 				depends.add(src) | ||||||
|  | 
 | ||||||
|  | 				handler = handler  | ||||||
|  | 					?? async function(src, state){ | ||||||
|  | 						return isolated ? | ||||||
|  | 							//{data: await this.get(src)
 | ||||||
|  | 							{data: await this | ||||||
|  | 								.parse({ | ||||||
|  | 									seen: state.seen,  | ||||||
|  | 									depends, | ||||||
|  | 									renderer: state.renderer, | ||||||
|  | 								})} | ||||||
|  | 							//: this.get(src)
 | ||||||
|  | 							: this | ||||||
|  | 								.parse(state) } | ||||||
|  | 
 | ||||||
|  | 				var first = true | ||||||
|  | 				for await (var page of this.get(src).asPages(strict)){ | ||||||
|  | 					if(join && !first){ | ||||||
|  | 						yield join } | ||||||
|  | 					first = false | ||||||
|  | 
 | ||||||
|  | 					//var full = page.path
 | ||||||
|  | 					var full = page.location | ||||||
|  | 
 | ||||||
|  | 					// handle recursion...
 | ||||||
|  | 					var parent_seen = 'seen' in state | ||||||
|  | 					var seen = state.seen =  | ||||||
|  | 						new Set(state.seen ?? []) | ||||||
|  | 					if(seen.has(full) | ||||||
|  | 							// nesting path recursion...
 | ||||||
|  | 							|| (full.length % (this.NESTING_RECURSION_TEST_THRESHOLD || 50) == 0 | ||||||
|  | 								&& (pwpath.split(full).length > 3 | ||||||
|  | 									&& new Set([ | ||||||
|  | 											await page.find(), | ||||||
|  | 											await page.get('..').find(), | ||||||
|  | 											await page.get('../..').find(), | ||||||
|  | 										]).size == 1 | ||||||
|  | 									// XXX HACK???
 | ||||||
|  | 									|| pwpath.split(full).length > (this.NESTING_DEPTH_LIMIT || 20)))){ | ||||||
|  | 						if(recursive == null){ | ||||||
|  | 							console.warn( | ||||||
|  | 								`@${key}(..): ${ | ||||||
|  | 									seen.has(full) ? | ||||||
|  | 										'direct' | ||||||
|  | 										: 'depth-limit' | ||||||
|  | 								} recursion detected:`, full, seen)
 | ||||||
|  | 							yield page.get(page.RECURSION_ERROR).parse()  | ||||||
|  | 							continue } | ||||||
|  | 						// have the 'recursive' arg...
 | ||||||
|  | 						yield base.parse(recursive, state)  | ||||||
|  | 						continue } | ||||||
|  | 					seen.add(full) | ||||||
|  | 
 | ||||||
|  | 					// load the included page...
 | ||||||
|  | 					var res = await handler.call(page, full, state) | ||||||
|  | 					depends.add(full) | ||||||
|  | 					res = strquotes ? | ||||||
|  | 						res | ||||||
|  | 							.replace(/["']/g, function(c){ | ||||||
|  | 								return '%'+ c.charCodeAt().toString(16) }) | ||||||
|  | 						: res | ||||||
|  | 
 | ||||||
|  | 					// NOTE: we only track recursion down and not sideways...
 | ||||||
|  | 					seen.delete(full) | ||||||
|  | 					if(!parent_seen){ | ||||||
|  | 						delete state.seen } | ||||||
|  | 
 | ||||||
|  | 					yield res } }), | ||||||
|  | 		// NOTE: the main difference between this and @include is that 
 | ||||||
|  | 		// 		this renders the src in the context of current page while 
 | ||||||
|  | 		// 		include is rendered in the context of its page but with
 | ||||||
|  | 		// 		the same state...
 | ||||||
|  | 		// 		i.e. for @include(PATH) the paths within the included page 
 | ||||||
|  | 		// 		are resolved relative to PATH while for @source(PATH) 
 | ||||||
|  | 		// 		relative to the page containing the @source(..) statement...
 | ||||||
|  | 		source: Macro( | ||||||
|  | 			// XXX should this have the same args as include???
 | ||||||
|  | 			['src', 'recursive', 'join',  | ||||||
|  | 				['s', 'strict']], | ||||||
|  | 			//['src'],
 | ||||||
|  | 			async function*(args, body, state){ | ||||||
|  | 				var that = this | ||||||
|  | 				yield* this.__parser__.macros.include.call(this,  | ||||||
|  | 					'source', | ||||||
|  | 					args, body, state, 'sources',  | ||||||
|  | 					async function(src, state){ | ||||||
|  | 						//return that.parse(that.get(src).raw, state) }) }),
 | ||||||
|  | 						return that.parse(this.raw, state) }) }), | ||||||
|  | 
 | ||||||
|  | 		// Load macro and slot definitions but ignore the page text...
 | ||||||
|  | 		//
 | ||||||
|  | 		// NOTE: this is essentially the same as @source(..) but returns ''.
 | ||||||
|  | 		// XXX revise name...
 | ||||||
|  | 		load: Macro( | ||||||
|  | 			['src', ['strict']], | ||||||
|  | 			async function*(args, body, state){ | ||||||
|  | 				var that = this | ||||||
|  | 				yield* this.__parser__.macros.include.call(this,  | ||||||
|  | 					'load', | ||||||
|  | 					args, body, state, 'sources',  | ||||||
|  | 					async function(src, state){ | ||||||
|  | 						await that.parse(this.raw, state)  | ||||||
|  | 						return '' }) }), | ||||||
|  | 		//
 | ||||||
|  | 		// 	@quote(<src>)
 | ||||||
|  | 		//
 | ||||||
|  | 		// 	<quote src=<src>[ filter="<filter> ..."]/>
 | ||||||
|  | 		//
 | ||||||
|  | 		// 	<quote text=" .. "[ filter="<filter> ..."]/>
 | ||||||
|  | 		//
 | ||||||
|  | 		// 	<quote[ filter="<filter> ..."]>
 | ||||||
|  | 		// 		..
 | ||||||
|  | 		// 	</quote>
 | ||||||
|  | 		//
 | ||||||
|  | 		//
 | ||||||
|  | 		// NOTE: src ant text arguments are mutually exclusive, src takes 
 | ||||||
|  | 		// 		priority.
 | ||||||
|  | 		// NOTE: the filter argument has the same semantics as the filter 
 | ||||||
|  | 		// 		macro with one exception, when used in quote, the body is 
 | ||||||
|  | 		// 		not expanded...
 | ||||||
|  | 		// NOTE: the filter argument uses the same filters as @filter(..)
 | ||||||
|  | 		// NOTE: else argument implies strict mode...
 | ||||||
|  | 		// XXX need a way to escape macros -- i.e. include </quote> in a quoted text...
 | ||||||
|  | 		// XXX should join/else be sub-tags???
 | ||||||
|  | 		quote: Macro( | ||||||
|  | 			['src', 'filter', 'text', 'join', 'else', | ||||||
|  | 				['s', 'expandactions', 'strict']], | ||||||
|  | 			async function*(args, body, state){ | ||||||
|  | 				var src = args.src //|| args[0]
 | ||||||
|  | 				var base = this.get(this.path.split(/\*/).shift()) | ||||||
|  | 				var text = args.text  | ||||||
|  | 					?? body  | ||||||
|  | 					?? [] | ||||||
|  | 				var strict = !!(args.strict  | ||||||
|  | 					?? args['else']  | ||||||
|  | 					?? false) | ||||||
|  | 				// parse arg values...
 | ||||||
|  | 				src = src ?  | ||||||
|  | 					await base.parse(src, state) | ||||||
|  | 					: src | ||||||
|  | 				// XXX INHERIT_ARGS special-case: inherit args by default...
 | ||||||
|  | 				if(this.actions_inherit_args  | ||||||
|  | 						&& this.actions_inherit_args.has(pwpath.basename(src)) | ||||||
|  | 						&& this.get(pwpath.dirname(src)).path == this.path){ | ||||||
|  | 					src += ':$ARGS' } | ||||||
|  | 				var expandactions =  | ||||||
|  | 					args.expandactions | ||||||
|  | 						?? true | ||||||
|  | 				// XXX EXPERIMENTAL
 | ||||||
|  | 				var strquotes = args.s | ||||||
|  | 
 | ||||||
|  | 				var depends = state.depends =  | ||||||
|  | 					state.depends  | ||||||
|  | 						?? new Set() | ||||||
|  | 				// XXX DEPENDS_PATTERN
 | ||||||
|  | 				depends.add(src) | ||||||
|  | 
 | ||||||
|  | 				var pages = src ? | ||||||
|  | 						(!expandactions  | ||||||
|  | 								&& await this.get(src).type == 'action' ? | ||||||
|  | 							base.get(this.QUOTE_ACTION_PAGE) | ||||||
|  | 							: await this.get(src).asPages(strict)) | ||||||
|  | 					: text instanceof Array ? | ||||||
|  | 						[text.join('')] | ||||||
|  | 					: typeof(text) == 'string' ? | ||||||
|  | 						[text] | ||||||
|  | 					: text | ||||||
|  | 				// else...
 | ||||||
|  | 				pages = ((!pages | ||||||
|  | 				   			|| pages.length == 0)	 | ||||||
|  | 						&& args['else']) ? | ||||||
|  | 					[await base.parse(args['else'], state)] | ||||||
|  | 					: pages | ||||||
|  | 				// empty...
 | ||||||
|  | 				if(!pages || pages.length == 0){ | ||||||
|  | 					return } | ||||||
|  | 
 | ||||||
|  | 				var join = args.join  | ||||||
|  | 					&& await base.parse(args.join, state) | ||||||
|  | 				var first = true | ||||||
|  | 				for await (var page of pages){ | ||||||
|  | 					if(join && !first){ | ||||||
|  | 						yield join } | ||||||
|  | 					first = false | ||||||
|  | 
 | ||||||
|  | 					text = typeof(page) == 'string' ? | ||||||
|  | 							page | ||||||
|  | 						: (!expandactions  | ||||||
|  | 								&& await page.type == 'action') ? | ||||||
|  | 							base.get(this.QUOTE_ACTION_PAGE).raw | ||||||
|  | 						: await page.raw | ||||||
|  | 					text = strquotes ? | ||||||
|  | 						text | ||||||
|  | 							.replace(/["']/g, function(c){ | ||||||
|  | 								return '%'+ c.charCodeAt().toString(16) }) | ||||||
|  | 						: text | ||||||
|  | 
 | ||||||
|  | 					page.path | ||||||
|  | 						&& depends.add(page.path) | ||||||
|  | 
 | ||||||
|  | 					var filters =  | ||||||
|  | 						args.filter  | ||||||
|  | 							&& args.filter | ||||||
|  | 								.trim() | ||||||
|  | 								.split(/\s+/g) | ||||||
|  | 
 | ||||||
|  | 					// NOTE: we are delaying .quote_filters handling here to 
 | ||||||
|  | 					// 		make their semantics the same as general filters...
 | ||||||
|  | 					// 		...and since we are internally calling .filter(..)
 | ||||||
|  | 					// 		macro we need to dance around it's architecture too...
 | ||||||
|  | 					// NOTE: since the body of quote(..) only has filters applied 
 | ||||||
|  | 					// 		to it doing the first stage of .filter(..) as late 
 | ||||||
|  | 					// 		as the second stage here will have no ill effect...
 | ||||||
|  | 					// NOTE: this uses the same filters as @filter(..)
 | ||||||
|  | 					// NOTE: the function wrapper here isolates text in 
 | ||||||
|  | 					// 		a closure per function...
 | ||||||
|  | 					yield (function(text){ | ||||||
|  | 						return async function(state){ | ||||||
|  | 							// add global quote-filters...
 | ||||||
|  | 							filters = | ||||||
|  | 								(state.quote_filters  | ||||||
|  | 										&& !(filters ?? []).includes(this.ISOLATED_FILTERS)) ? | ||||||
|  | 									[...state.quote_filters, ...(filters ?? [])] | ||||||
|  | 									: filters | ||||||
|  | 							return filters ? | ||||||
|  | 								await this.__parser__.callMacro( | ||||||
|  | 										this, 'filter', filters, text, state, false) | ||||||
|  | 									.call(this, state) | ||||||
|  | 								: text } })(text) } }), | ||||||
|  | 		// very similar to @filter(..) but will affect @quote(..) filters...
 | ||||||
|  | 		'quote-filter': function(args, body, state){ | ||||||
|  | 			var filters = state.quote_filters =  | ||||||
|  | 				state.quote_filters ?? [] | ||||||
|  | 			filters.splice(filters.length, 0, ...Object.keys(args)) }, | ||||||
|  | 		//
 | ||||||
|  | 		//	<slot name=<name>/>
 | ||||||
|  | 		//
 | ||||||
|  | 		//	<slot name=<name> text=<text>/>
 | ||||||
|  | 		//
 | ||||||
|  | 		//	<slot name=<name>>
 | ||||||
|  | 		//		...
 | ||||||
|  | 		//	</slot>
 | ||||||
|  | 		//
 | ||||||
|  | 		//	Force show a slot...
 | ||||||
|  | 		//	<slot shown ... />
 | ||||||
|  | 		//
 | ||||||
|  | 		//	Force hide a slot...
 | ||||||
|  | 		//	<slot hidden ... />
 | ||||||
|  | 		//
 | ||||||
|  | 		//	Insert previous slot content...
 | ||||||
|  | 		//	<content/>
 | ||||||
|  | 		//
 | ||||||
|  | 		//
 | ||||||
|  | 		// NOTE: by default only the first slot with <name> is visible, 
 | ||||||
|  | 		// 		all other slots with <name> will replace its content, unless
 | ||||||
|  | 		// 		explicit shown/hidden arguments are given.
 | ||||||
|  | 		// NOTE: hidden has precedence over shown if both are given.
 | ||||||
|  | 		// NOTE: slots are handled in order of occurrence of opening tags 
 | ||||||
|  | 		// 		in text and not by hierarchy, i.e. the later slot overrides
 | ||||||
|  | 		// 		the former and the most nested overrides the parent.
 | ||||||
|  | 		// 		This also works for cases where slots override slots they 
 | ||||||
|  | 		// 		are contained in, this will not lead to recursion.
 | ||||||
|  | 		//
 | ||||||
|  | 		// XXX revise the use of hidden/shown use mechanic and if it's 
 | ||||||
|  | 		// 		needed...
 | ||||||
|  | 		slot: Macro( | ||||||
|  | 			['name', 'text', ['shown', 'hidden']], | ||||||
|  | 			async function(args, body, state){ | ||||||
|  | 				var name = args.name | ||||||
|  | 				var text = args.text  | ||||||
|  | 					?? body  | ||||||
|  | 					// NOTE: this can't be undefined for .expand(..) to work 
 | ||||||
|  | 					// 		correctly...
 | ||||||
|  | 					?? [] | ||||||
|  | 
 | ||||||
|  | 				var slots = state.slots =  | ||||||
|  | 					state.slots  | ||||||
|  | 						?? {} | ||||||
|  | 
 | ||||||
|  | 				// parse arg values...
 | ||||||
|  | 				name = name ? | ||||||
|  | 					await this.parse(name, state) | ||||||
|  | 					: name | ||||||
|  | 
 | ||||||
|  | 				//var hidden = name in slots
 | ||||||
|  | 				// XXX EXPERIMENTAL
 | ||||||
|  | 				var hidden =  | ||||||
|  | 					// 'hidden' has priority... 
 | ||||||
|  | 					args.hidden | ||||||
|  | 						// explicitly show... ()
 | ||||||
|  | 						|| (args.shown ? | ||||||
|  | 							false | ||||||
|  | 							// show first instance...
 | ||||||
|  | 							: name in slots) | ||||||
|  | 
 | ||||||
|  | 				// set slot value...
 | ||||||
|  | 				var stack = [] | ||||||
|  | 				slots[name] | ||||||
|  | 					&& stack.push(slots[name]) | ||||||
|  | 				delete slots[name] | ||||||
|  | 				var slot = await this.__parser__.expand(this, text, state) | ||||||
|  | 				var original = slot | ||||||
|  | 				slots[name] | ||||||
|  | 					&& stack.unshift(slot) | ||||||
|  | 				slot = slots[name] =  | ||||||
|  | 					slots[name]  | ||||||
|  | 						?? slot | ||||||
|  | 				// handle <content/>...
 | ||||||
|  | 				for(prev of stack){ | ||||||
|  | 					// get the first <content/>
 | ||||||
|  | 					for(var i in slot){ | ||||||
|  | 						if(typeof(slot[i]) != 'string' | ||||||
|  | 								&& slot[i].name == 'content'){ | ||||||
|  | 							break }  | ||||||
|  | 						i = null } | ||||||
|  | 					i != null | ||||||
|  | 						&& slot.splice(i, 1,  | ||||||
|  | 							...prev | ||||||
|  | 								// remove nested slot handlers...
 | ||||||
|  | 								.filter(function(e){ | ||||||
|  | 									return typeof(e) != 'function' | ||||||
|  | 											|| e.slot != name }) ) } | ||||||
|  | 				return hidden ? | ||||||
|  | 					'' | ||||||
|  | 					: Object.assign( | ||||||
|  | 						function(state){ | ||||||
|  | 							return (state.slots || {})[name] ?? original }, | ||||||
|  | 						{slot: name}) }),  | ||||||
|  | 		'content': ['slot'], | ||||||
|  | 
 | ||||||
|  | 		// XXX EXPERIMENTAL...
 | ||||||
|  | 		//
 | ||||||
|  | 		// NOTE: var value is parsed only on assignment and not on dereferencing...
 | ||||||
|  | 		//
 | ||||||
|  | 		// XXX should alpha/Alpha be 0 (current) or 1 based???
 | ||||||
|  | 		// XXX do we need a default attr???
 | ||||||
|  | 		// 		...i.e. if not defined set to ..
 | ||||||
|  | 		// XXX INC_DEC do we need inc/dec and parent???
 | ||||||
|  | 		'var': Macro( | ||||||
|  | 			['name', 'text',  | ||||||
|  | 				// XXX INC_DEC
 | ||||||
|  | 				['shown', 'hidden',  | ||||||
|  | 					'parent',  | ||||||
|  | 					'inc', 'dec',  | ||||||
|  | 					'alpha', 'Alpha', 'roman', 'Roman']], | ||||||
|  | 				/*/ | ||||||
|  | 				['shown', 'hidden']], | ||||||
|  | 				//*/
 | ||||||
|  | 			async function(args, body, state){ | ||||||
|  | 				var name = args.name | ||||||
|  | 				if(!name){ | ||||||
|  | 					return '' } | ||||||
|  | 				name = await this.parse(name, state) | ||||||
|  | 				// XXX INC_DEC
 | ||||||
|  | 				var inc = args.inc | ||||||
|  | 				var dec = args.dec | ||||||
|  | 				//*/
 | ||||||
|  | 				var text = args.text  | ||||||
|  | 					?? body  | ||||||
|  | 				// NOTE: .hidden has priority...
 | ||||||
|  | 				var show =  | ||||||
|  | 						('hidden' in args ? | ||||||
|  | 							!args.hidden | ||||||
|  | 							: undefined) | ||||||
|  | 						?? args.shown  | ||||||
|  | 
 | ||||||
|  | 				var vars = state.vars =  | ||||||
|  | 					state.vars  | ||||||
|  | 						?? {} | ||||||
|  | 				// XXX INC_DEC
 | ||||||
|  | 				if(args.parent && name in vars){ | ||||||
|  | 					while(!vars.hasOwnProperty(name) | ||||||
|  | 							&& vars.__proto__ !== Object.prototype){ | ||||||
|  | 						vars = vars.__proto__ } } | ||||||
|  | 
 | ||||||
|  | 				var handleFormat = function(value){ | ||||||
|  | 					// roman number...
 | ||||||
|  | 					if(args.roman || args.Roman){ | ||||||
|  | 						var n = parseInt(value) | ||||||
|  | 						return isNaN(n) ? | ||||||
|  | 								'' | ||||||
|  | 							: args.Roman ? | ||||||
|  | 								n.toRoman() | ||||||
|  | 							: n.toRoman().toLowerCase() } | ||||||
|  | 					// alpha number...
 | ||||||
|  | 					if(args.alpha || args.Alpha){ | ||||||
|  | 						var n = parseInt(value) | ||||||
|  | 						return isNaN(n) ? | ||||||
|  | 								'' | ||||||
|  | 							: args.Alpha ? | ||||||
|  | 								n.toAlpha().toUpperCase() | ||||||
|  | 							: n.toAlpha() }  | ||||||
|  | 					return value } | ||||||
|  | 
 | ||||||
|  | 				// inc/dec...
 | ||||||
|  | 				if(inc || dec){ | ||||||
|  | 					if(!(name in vars)  | ||||||
|  | 							|| isNaN(parseInt(vars[name]))){ | ||||||
|  | 						return '' } | ||||||
|  | 					var cur = parseInt(vars[name]) | ||||||
|  | 					cur +=  | ||||||
|  | 						inc === true ?  | ||||||
|  | 							1  | ||||||
|  | 						: !inc ? | ||||||
|  | 							0 | ||||||
|  | 						: parseInt(inc) | ||||||
|  | 					cur -=  | ||||||
|  | 						dec === true ?  | ||||||
|  | 							1  | ||||||
|  | 						: !dec ? | ||||||
|  | 							0 | ||||||
|  | 						: parseInt(dec) | ||||||
|  | 					vars[name] = cur + '' | ||||||
|  | 
 | ||||||
|  | 					// as-is...
 | ||||||
|  | 					return show ?? true ? | ||||||
|  | 						handleFormat(vars[name]) | ||||||
|  | 						: '' } | ||||||
|  | 				//*/
 | ||||||
|  | 
 | ||||||
|  | 				// set...
 | ||||||
|  | 				if(text){ | ||||||
|  | 					text = vars[name] =  | ||||||
|  | 						await this.parse(text, state) | ||||||
|  | 					return show ?? false ? | ||||||
|  | 						text | ||||||
|  | 						: '' | ||||||
|  | 				// get...
 | ||||||
|  | 				} else { | ||||||
|  | 					return handleFormat(vars[name] ?? '') } }), | ||||||
|  | 		vars: async function(args, body, state){ | ||||||
|  | 			var vars = state.vars =  | ||||||
|  | 				state.vars  | ||||||
|  | 					?? {} | ||||||
|  | 			for(var [name, value] of Object.entries(args)){ | ||||||
|  | 				vars[await this.parse(name, state)] = | ||||||
|  | 					await this.parse(value, state) } | ||||||
|  | 			return '' }, | ||||||
|  | 
 | ||||||
|  | 		// 	
 | ||||||
|  | 		// 	<macro src=<url>> .. </macro>
 | ||||||
|  | 		//
 | ||||||
|  | 		// 	<macro name=<name> src=<url> sort=<sort-spec>> .. </macro>
 | ||||||
|  | 		//
 | ||||||
|  | 		// 	<macro ...> ... </macro>
 | ||||||
|  | 		// 	<macro ... text=<text>/>
 | ||||||
|  | 		//
 | ||||||
|  | 		// 	<macro ... else=<text>> ... </macro>
 | ||||||
|  | 		// 	<macro ...>
 | ||||||
|  | 		// 		...
 | ||||||
|  | 		//
 | ||||||
|  | 		//
 | ||||||
|  | 		// 		<join>
 | ||||||
|  | 		// 			...
 | ||||||
|  | 		// 		</join>
 | ||||||
|  | 		//
 | ||||||
|  | 		// 		<else>
 | ||||||
|  | 		// 			...
 | ||||||
|  | 		// 		</else>
 | ||||||
|  | 		// 	</macro>
 | ||||||
|  | 		//
 | ||||||
|  | 		// Macro variables:
 | ||||||
|  | 		// 	macro:count
 | ||||||
|  | 		// 	macro:index
 | ||||||
|  | 		//
 | ||||||
|  | 		// NOTE: this handles src count argument internally partially 
 | ||||||
|  | 		// 		overriding <store>.match(..)'s implementation, this is done
 | ||||||
|  | 		// 		because @macro(..) needs to account for arbitrary nesting 
 | ||||||
|  | 		// 		that <store>.match(..) can not know about...
 | ||||||
|  | 		// 		XXX should we do the same for offset???
 | ||||||
|  | 		//
 | ||||||
|  | 		// XXX BUG: strict does not seem to work:
 | ||||||
|  | 		// 				@macro(src="./resolved-page" else="no" text="yes" strict)
 | ||||||
|  | 		// 					-> yes
 | ||||||
|  | 		// 			should be "no"
 | ||||||
|  | 		// 			...this seems to effect non-pattern pages...
 | ||||||
|  | 		// XXX should macro:index be 0 or 1 (current) based???
 | ||||||
|  | 		// XXX SORT sorting not implemented yet...
 | ||||||
|  | 		macro: Macro( | ||||||
|  | 			['name', 'src', 'sort', 'text', 'join', 'else', | ||||||
|  | 				['strict', 'isolated', 'inheritmacros', 'inheritvars']], | ||||||
|  | 			async function*(args, body, state){ | ||||||
|  | 				var that = this | ||||||
|  | 
 | ||||||
|  | 				// helpers...
 | ||||||
|  | 				var _getBlock = function(name){ | ||||||
|  | 					var block = args[name] ? | ||||||
|  | 						[{ | ||||||
|  | 							args: {}, | ||||||
|  | 							body: args[name], | ||||||
|  | 						}] | ||||||
|  | 						: (text ?? []) | ||||||
|  | 							.filter(function(e){  | ||||||
|  | 								return typeof(e) != 'string'  | ||||||
|  | 									&& e.name == name }) | ||||||
|  | 					if(block.length == 0){ | ||||||
|  | 						return } | ||||||
|  | 					// NOTE: when multiple blocks are present the 
 | ||||||
|  | 					// 		last one is used...
 | ||||||
|  | 					block = block.pop() | ||||||
|  | 					block =  | ||||||
|  | 						block.args.text  | ||||||
|  | 							?? block.body | ||||||
|  | 					return block } | ||||||
|  | 
 | ||||||
|  | 				var base = this.get(this.path.split(/\*/).shift()) | ||||||
|  | 				var macros = state.macros =  | ||||||
|  | 					state.macros  | ||||||
|  | 						?? {} | ||||||
|  | 				var vars = state.vars =  | ||||||
|  | 					state.vars  | ||||||
|  | 						?? {} | ||||||
|  | 				var depends = state.depends =  | ||||||
|  | 					state.depends  | ||||||
|  | 						?? new Set() | ||||||
|  | 
 | ||||||
|  | 				// uninheritable args...
 | ||||||
|  | 				// NOTE: arg handling is split in two, to make things simpler 
 | ||||||
|  | 				// 		to process for retrieved named macros...
 | ||||||
|  | 				var src = args.src | ||||||
|  | 				var text = args.text  | ||||||
|  | 					?? body  | ||||||
|  | 					?? [] | ||||||
|  | 				text = typeof(text) == 'string' ? | ||||||
|  | 					[...this.__parser__.group(this, text+'</macro>', 'macro')] | ||||||
|  | 					: text | ||||||
|  | 				var join, itext | ||||||
|  | 				var iargs = {} | ||||||
|  | 
 | ||||||
|  | 				// stored macros...
 | ||||||
|  | 				if(args.name){ | ||||||
|  | 					var name = await base.parse(args.name, state) | ||||||
|  | 					// define new named macro...
 | ||||||
|  | 					if(text.length != 0){ | ||||||
|  | 						// NOTE: we do not need to worry about saving 
 | ||||||
|  | 						// 		stateful text here because it is only 
 | ||||||
|  | 						// 		grouped and not expanded...
 | ||||||
|  | 						macros[name] =  | ||||||
|  | 							[ text,  | ||||||
|  | 								_getBlock('join'),  | ||||||
|  | 								JSON.parse(JSON.stringify(args)), ] | ||||||
|  | 					// use existing macro...
 | ||||||
|  | 					} else if(macros  | ||||||
|  | 							&& name in macros){ | ||||||
|  | 						;[itext, join, iargs] = macros[name] } } | ||||||
|  | 
 | ||||||
|  | 				// inheritable args...
 | ||||||
|  | 				// XXX is there a point in overloading text???
 | ||||||
|  | 				text = text.length > 0 ?  | ||||||
|  | 					text  | ||||||
|  | 					: itext ?? text | ||||||
|  | 				var sort = (args.sort  | ||||||
|  | 						?? iargs.sort  | ||||||
|  | 						?? '') | ||||||
|  | 					.split(/\s+/g) | ||||||
|  | 					.filter(function(e){  | ||||||
|  | 						return e != '' }) | ||||||
|  | 				var strict =  | ||||||
|  | 					('strict' in args ? | ||||||
|  | 							args.strict  | ||||||
|  | 							: iargs.strict) | ||||||
|  | 						//?? true
 | ||||||
|  | 						?? false | ||||||
|  | 				var isolated =  | ||||||
|  | 					('isolated' in args ? | ||||||
|  | 							args.isolated | ||||||
|  | 							: iargs.isolated) | ||||||
|  | 						?? true | ||||||
|  | 				var inheritmacros =  | ||||||
|  | 					('inheritmacros' in args ? | ||||||
|  | 							args.inheritmacros | ||||||
|  | 							: iargs.inheritmacros) | ||||||
|  | 						?? true | ||||||
|  | 				var inheritvars =  | ||||||
|  | 					('inheritvars' in args ? | ||||||
|  | 							args.inheritvars | ||||||
|  | 							: iargs.inheritvars) | ||||||
|  | 						?? true | ||||||
|  | 
 | ||||||
|  | 				if(src){ | ||||||
|  | 					src = await base.parse(src, state) | ||||||
|  | 					// XXX INHERIT_ARGS special-case: inherit args by default...
 | ||||||
|  | 					if(this.actions_inherit_args  | ||||||
|  | 							&& this.actions_inherit_args.has(pwpath.basename(src)) | ||||||
|  | 							&& this.get(pwpath.dirname(src)).path == this.path){ | ||||||
|  | 						src += ':$ARGS' } | ||||||
|  | 					// XXX DEPENDS_PATTERN
 | ||||||
|  | 					depends.add(src) | ||||||
|  | 
 | ||||||
|  | 					join = _getBlock('join')  | ||||||
|  | 						?? join  | ||||||
|  | 					join = join | ||||||
|  | 						&& await base.parse(join, state) | ||||||
|  | 
 | ||||||
|  | 					//var match = this.get(await base.parse(src, state))
 | ||||||
|  | 					//var match = this.get(src, strict)
 | ||||||
|  | 					var match = this.get(src) | ||||||
|  | 
 | ||||||
|  | 					// NOTE: thie does not introduce a dependency on each 
 | ||||||
|  | 					// 		of the iterated pages, that is handled by the 
 | ||||||
|  | 					// 		respective include/source/.. macros, this however
 | ||||||
|  | 					// 		only depends on page count...
 | ||||||
|  | 					depends.add(match.path) | ||||||
|  | 
 | ||||||
|  | 					// populate macrovars...
 | ||||||
|  | 					var macrovars = {} | ||||||
|  | 					for(var [key, value]  | ||||||
|  | 							of Object.entries( | ||||||
|  | 								Object.assign( | ||||||
|  | 									args, | ||||||
|  | 									iargs, | ||||||
|  | 									{ | ||||||
|  | 										strict, | ||||||
|  | 										isolated, | ||||||
|  | 										inheritmacros, | ||||||
|  | 										inheritvars, | ||||||
|  | 									}))){ | ||||||
|  | 						macrovars['macro:'+ key] =  | ||||||
|  | 							value === true ? | ||||||
|  | 								'yes' | ||||||
|  | 							: value === false ? | ||||||
|  | 								'no' | ||||||
|  | 							: value } | ||||||
|  | 
 | ||||||
|  | 					// handle count...
 | ||||||
|  | 					// NOTE: this duplicates <store>.match(..)'s functionality
 | ||||||
|  | 					// 		because we need to account for arbitrary macro 
 | ||||||
|  | 					// 		nesting that .match(..) does not know about...
 | ||||||
|  | 					// XXX revise var naming...
 | ||||||
|  | 					// XXX these can be overriden in nested macros...
 | ||||||
|  | 					var count = match.args.count | ||||||
|  | 					if(count){ | ||||||
|  | 						var c = | ||||||
|  | 							count == 'inherit' ? | ||||||
|  | 								(!('macro:count' in vars) ? | ||||||
|  | 									this.args.count | ||||||
|  | 									: undefined) | ||||||
|  | 								: count  | ||||||
|  | 						if(c !== undefined){ | ||||||
|  | 							vars['macro:count'] =  | ||||||
|  | 								isNaN(parseInt(c)) ? | ||||||
|  | 									c | ||||||
|  | 									: parseInt(c)  | ||||||
|  | 							vars['macro:index'] = 0 } } | ||||||
|  | 						 | ||||||
|  | 					// expand matches...
 | ||||||
|  | 					var first = true | ||||||
|  | 					for await(var page of match.asPages(strict)){ | ||||||
|  | 						// handle count...
 | ||||||
|  | 						if('macro:count' in vars){ | ||||||
|  | 							if(vars['macro:count'] <= vars['macro:index']){ | ||||||
|  | 								break } | ||||||
|  | 							object.sources(vars, 'macro:index') | ||||||
|  | 								.shift()['macro:index']++ } | ||||||
|  | 						// output join between elements....
 | ||||||
|  | 						if(join && !first){ | ||||||
|  | 							yield join } | ||||||
|  | 						first = false  | ||||||
|  | 						if(isolated){ | ||||||
|  | 							var _state = { | ||||||
|  | 								seen: state.seen,  | ||||||
|  | 								depends, | ||||||
|  | 								renderer: state.renderer, | ||||||
|  | 								macros: inheritmacros ? | ||||||
|  | 									{__proto__: macros} | ||||||
|  | 									: {}, | ||||||
|  | 								vars: inheritvars ? | ||||||
|  | 									{__proto__: vars,  | ||||||
|  | 										...macrovars} | ||||||
|  | 									: {...macrovars}, | ||||||
|  | 							} | ||||||
|  | 							yield this.__parser__.parse(page,  | ||||||
|  | 								this.__parser__.expand(page,  | ||||||
|  | 									text, _state), _state) | ||||||
|  | 						} else { | ||||||
|  | 							yield this.__parser__.expand(page, text, state) } } | ||||||
|  | 					// cleanup...
 | ||||||
|  | 					delete vars['macro:count'] | ||||||
|  | 					delete vars['macro:index'] | ||||||
|  | 					// else...
 | ||||||
|  | 					if(first | ||||||
|  | 							&& (text || args['else'])){ | ||||||
|  | 						var else_block = _getBlock('else') | ||||||
|  | 						if(else_block){ | ||||||
|  | 							yield this.__parser__.expand(this, else_block, state) } } } }), | ||||||
|  | 
 | ||||||
|  | 		// nesting rules...
 | ||||||
|  | 		'else': ['macro'], | ||||||
|  | 		'join': ['macro'], | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user