diff --git a/v2/pwiki/page.js b/v2/pwiki/page.js
index 3e187c9..cd0bc07 100755
--- a/v2/pwiki/page.js
+++ b/v2/pwiki/page.js
@@ -911,884 +911,6 @@ object.Constructor('Page', BasePage, {
 			return `
${source}` },
 	},
 
-	//
-	// 	(, , ){ .. }
-	// 		-> undefined
-	// 		-> 
-	// 		-> 
-	// 		-> 
-	// 		-> ()
-	// 			-> ...
-	//
-	// XXX do we need to make .macro.__proto__ module level object???
-	// XXX ASYNC make these support async page getters...
-	macros: { __proto__: {
-		//
-		//	@([ ][ local])
-		//	@(name=[ else=][ local])
-		//
-		//	@arg([ ][ local])
-		//	@arg(name=[ else=][ local])
-		//
-		//	[ ][ local]/>
-		//	[ else=][ 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()
-		// 	/>
-		//
-		// 	>
-		// 		...
-		// 	
-		//
-		// 	 ::=
-		// 		 
-		// 		| - 
-		//
-		// XXX BUG: this does not show any results:
-		//			pwiki.parse('moo test')
-		//				-> ''
-		//		while these do:
-		//    		pwiki.parse('moo test')
-		//				-> 'moo TEST'
-		//			await pwiki.parse('moo test@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()
-		//
-		// 	@include( isolated recursive=)
-		// 	@include(src= isolated recursive=)
-		//
-		// 	 .. >
-		// 		
-		// 	
-		//
-		// 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()
-		//
-		// 	[ filter=" ..."]/>
-		//
-		// 	
-		//
-		// 	
-		// 		..
-		// 	
-		//
-		//
-		// 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 
 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)) },
-		//
-		//	/>
-		//
-		//	 text=/>
-		//
-		//	>
-		//		...
-		//	
-		//
-		//	Force show a slot...
-		//	
-		//
-		//	Force hide a slot...
-		//	
-		//
-		//	Insert previous slot content...
-		//	
-		//
-		//
-		// NOTE: by default only the first slot with  is visible, 
-		// 		all other slots with  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 ...
-				for(prev of stack){
-					// get the first 
-					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 '' },
-
-		// 	
-		// 	> .. 
-		//
-		// 	 src= sort=> .. 
-		//
-		// 	 ... 
-		// 	/>
-		//
-		// 	> ... 
-		// 	
-		// 		...
-		//
-		//
-		// 		
-		// 			...
-		// 		
-		//
-		// 		
-		// 			...
-		// 		
-		// 	
-		//
-		// Macro variables:
-		// 	macro:count
-		// 	macro:index
-		//
-		// NOTE: this handles src count argument internally partially 
-		// 		overriding .match(..)'s implementation, this is done
-		// 		because @macro(..) needs to account for arbitrary nesting 
-		// 		that .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')]
-					: 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 .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...
 	//
 	// 	Define a global macro...
@@ -1796,13 +918,14 @@ object.Constructor('Page', BasePage, {
 	// 	.defmacro(, , )
 	// 		-> this
 	//
-	// XXX do we need this???
+	/* XXX do we need this???
 	defmacro: function(name, args, func){
-		this.macros[name] = 
+		this.__parser__.macros[name] = 
 			arguments.length == 2 ?
 				arguments[1]
 				: Macro(args, func)
 		return this },
+	//*/
 
 
 	// direct actions...
diff --git a/v2/pwiki/parser.js b/v2/pwiki/parser.js
index 9576c70..0d08dab 100755
--- a/v2/pwiki/parser.js
+++ b/v2/pwiki/parser.js
@@ -7,6 +7,7 @@
 (function(require){ var module={} // make module AMD/node compatible...
 /*********************************************************************/
 
+var object = require('ig-object')
 var types = require('ig-types')
 
 var pwpath = require('./path')
@@ -209,7 +210,7 @@ module.BaseParser = {
 		return res },
 	// XXX should this be here or on page???
 	callMacro: function(page, name, args, body, state, ...rest){
-		var macro = page.macros[name] 
+		var macro = this.macros[name] 
 		return macro.call(page, 
 				this.parseArgs(
 					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.
 	lex: function*(page, str){
 		str = typeof(str) != 'string' ?
@@ -264,7 +265,7 @@ module.BaseParser = {
 
 		// XXX should this be cached???
 		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 
 			?? this.countMacroPatternGroups()
 		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, parent){
 		// XXX we can't get .raw from the page without going async...
@@ -410,8 +411,8 @@ module.BaseParser = {
 			// assert nesting rules...
 			// NOTE: we only check for direct nesting...
 			// XXX might be a good idea to link nested block to the parent...
-			if(page.macros[value.name] instanceof Array
-					&& !page.macros[value.name].includes(to)
+			if(this.macros[value.name] instanceof Array
+					&& !this.macros[value.name].includes(to)
 					// do not complain about closing nestable tags...
 					&& !(value.name == to 
 						&& value.type == 'closing')){
@@ -496,7 +497,7 @@ module.BaseParser = {
 					// macro...
 					var {name, args, body} = value
 					// nested macro -- skip...
-					if(typeof(page.macros[name]) != 'function'){
+					if(typeof(that.macros[name]) != 'function'){
 						return {...value, skip: true} }
 					// macro call...
 					return Promise.awaitOrRun(
@@ -505,7 +506,7 @@ module.BaseParser = {
 							res = res ?? ''
 							// result...
 							if(res instanceof Array 
-									|| page.macros[name] instanceof types.Generator){
+									|| that.macros[name] instanceof types.Generator){
 								return res
 							} else {
 								return [res] } }) },
@@ -679,7 +680,17 @@ module.parser = {
 	// list of macros that will get raw text of their content...
 	QUOTING_MACROS: ['quote'],
 
-	// XXX move macros here from page.js...
+	//
+	// 	(, , ){ .. }
+	// 		-> undefined
+	// 		-> 
+	// 		-> 
+	// 		-> 
+	// 		-> ()
+	// 			-> ...
+	//
+	// XXX do we need to make .macro.__proto__ module level object???
+	// XXX ASYNC make these support async page getters...
 	macros: {
 		//
 		//	@([ ][ local])
@@ -715,11 +726,836 @@ module.parser = {
 		'': Macro(
 			['name', 'else', ['local']],
 			function(args){
-				return this.macros.arg.call(this, args) }),
+				return this.__parser__.macros.arg.call(this, args) }),
 		args: function(){
 			return pwpath.obj2args(this.args) },
+		//
+		// 	@filter()
+		// 	/>
+		//
+		// 	>
+		// 		...
+		// 	
+		//
+		// 	 ::=
+		// 		 
+		// 		| - 
+		//
+		// XXX BUG: this does not show any results:
+		//			pwiki.parse('moo test')
+		//				-> ''
+		//		while these do:
+		//    		pwiki.parse('moo test')
+		//				-> 'moo TEST'
+		//			await pwiki.parse('moo test@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()
+		//
+		// 	@include( isolated recursive=)
+		// 	@include(src= isolated recursive=)
+		//
+		// 	 .. >
+		// 		
+		// 	
+		//
+		// 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()
+		//
+		// 	[ filter=" ..."]/>
+		//
+		// 	
+		//
+		// 	
+		// 		..
+		// 	
+		//
+		//
+		// 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 
 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)) },
+		//
+		//	/>
+		//
+		//	 text=/>
+		//
+		//	>
+		//		...
+		//	
+		//
+		//	Force show a slot...
+		//	
+		//
+		//	Force hide a slot...
+		//	
+		//
+		//	Insert previous slot content...
+		//	
+		//
+		//
+		// NOTE: by default only the first slot with  is visible, 
+		// 		all other slots with  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 ...
+				for(prev of stack){
+					// get the first 
+					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 '' },
+
+		// 	
+		// 	> .. 
+		//
+		// 	 src= sort=> .. 
+		//
+		// 	 ... 
+		// 	/>
+		//
+		// 	> ... 
+		// 	
+		// 		...
+		//
+		//
+		// 		
+		// 			...
+		// 		
+		//
+		// 		
+		// 			...
+		// 		
+		// 	
+		//
+		// Macro variables:
+		// 	macro:count
+		// 	macro:index
+		//
+		// NOTE: this handles src count argument internally partially 
+		// 		overriding .match(..)'s implementation, this is done
+		// 		because @macro(..) needs to account for arbitrary nesting 
+		// 		that .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')]
+					: 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 .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'],
 	},
 
 }