moved macros to parser (mostly working as before)...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2023-09-24 14:30:32 +03:00
parent 9275d01899
commit 8dcff32823
2 changed files with 850 additions and 891 deletions

View File

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

View File

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