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'],
},
}