diff --git a/v3/pwiki/parser.js b/v3/pwiki/parser.js index 9287e31..0e025ad 100644 --- a/v3/pwiki/parser.js +++ b/v3/pwiki/parser.js @@ -253,6 +253,14 @@ module.BaseParser = { if(typeof(blocks) == 'string' || join == null){ return blocks } + // we do not need to rebuild the ast for each use... + // XXX this can break things -- need to store the parse stage + // in the structure so as to determine where we left off... + // ...the problem is that we can't tell the difference + // between a parsed and expanded stages -- one way, re-running + // a stage is not an issue but the other way, skipping can + // break... + //join = this.ast(page, join, state) return blocks .map(function(block, i, l){ return [ @@ -305,7 +313,7 @@ module.BaseParser = { // // NOTE: this internally uses .macros' keys to generate the // lexing pattern. - lex: function*(page, str){ + lex: function*(str){ str = typeof(str) != 'string' ? str+'' : str @@ -434,9 +442,9 @@ module.BaseParser = { // // NOTE: this internaly uses .macros to check for propper nesting //group: function*(page, lex, to=false){ - group: function*(page, lex, to=false, parent, context){ + group: function*(lex, to=false, parent, context){ lex = typeof(lex) != 'object' ? - this.lex(page, lex) + this.lex(lex) : lex var quoting = to @@ -488,8 +496,8 @@ module.BaseParser = { // open block... if(value.type == 'opening'){ - //value.body = [...this.group(page, lex, value.name)] - value.body = [...this.group(page, lex, value.name, value)] + //value.body = [...this.group(lex, value.name)] + value.body = [...this.group(lex, value.name, value)] value.type = 'block' // unify .body, .args.body and .args.text into .body... @@ -499,7 +507,6 @@ module.BaseParser = { ?? value.args.text)){ value.body = [...this.group( - page, value.args.body ?? value.args.text, false, @@ -593,7 +600,7 @@ module.BaseParser = { expand: function(page, ast, state={}){ var that = this ast = typeof(ast) != 'object' ? - this.group(page, ast) + this.group(ast) : ast instanceof types.Generator ? ast : ast.iter() @@ -629,6 +636,7 @@ module.BaseParser = { continue } // expand down... + // XXX cache this as an AST body && !that.macros[name].lazy && (body = this.expand(page, body, state)) @@ -702,7 +710,8 @@ module.BaseParser = { // resolve stage II macros and merge results... // - /* XXX + // XXX DNF + // // XXX these yeild different results: // r = parser.resolve( // tests.P, @@ -722,84 +731,6 @@ module.BaseParser = { // XXX it looks like the first slot is resolved before the last // slot has a chance to set the value as it is waiting for // the first slot to finish... - resolve: function(page, ast, state={}){ - var that = this - ast = typeof(ast) != 'object' ? - this.expand(page, ast, state) - : ast instanceof types.Generator ? - ast - : ast.iter() - - // merge resolved elements into the last item of elems... - var elems = [] - var merge = function(...args){ - var prev = '' - while(args.length > 0){ - while(args.length > 0 - && typeof(args[0]) != 'object' - && typeof(args[0]) != 'function'){ - prev += args.shift() } - // merged section... - if(prev.length > 0){ - if(elems.length > 0 - && typeof(elems.at(-1)) == 'string'){ - elems[elems.length-1] += prev - } else { - elems.push(prev) } - prev = '' } - if(args.length > 0){ - elems.push(args.shift()) } } } - - // XXX can we make this more granular and wait only where we need - // to wait? - // ....the wait is logical as we need to return a string here - // eventually. - // should this be split into .resolve(..) nad .merge(..)??? - return Promise.awaitOrRun( - state.wait, - function(){ - // XXX can ast be a promise??? - // XXX elems can be unresolved -- need a merge strategy for them... - //var waiting = [] - for(var elem of ast){ - // nesting... - while(elem && elem.value){ - elem = elem.value - // exec stage II macros... - // XXX this should be called when all the promises before - // are resolved... - if(typeof(elem) == 'function'){ - elem = elem(state) } } - if(elem == null){ - continue } - // atomic values... - if(typeof(elem) != 'object'){ - merge(elem) - continue } - // value is resolved but "empty" -> skip... - if('value' in elem - && (elem.value == null - || elem.value == '')){ - continue } - // expand ast... - if(elem instanceof Array){ - // XXX this can be or containe promises... - merge(...that.resolve(page, elem, state)) - continue } - // expand .body attribute... - // XXX is this needed here??? - if(elem.body instanceof Array){ - console.warn('!!! RESOLVE_BODY') - // XXX this can be or containe promises... - elem.body = that.resolve(page, elem.body, state) } - // nested macro with no value set -- skip... - if(that.macros[elem.name] instanceof Array){ - continue } - // unresolved... - merge(elem) } - - return elems }) }, - //*/ resolve: function(page, ast, state={}){ var that = this ast = typeof(ast) != 'object' ? @@ -1534,6 +1465,8 @@ module.parser = { // not 100% correct manner focusing on path depth and ignoring // the context, this potentially can lead to false positives. // + // XXX might be a good idea to add a tag to place the + // loaded text... // XXX add path recursion test to data -- fail if two paths resolve // to the same context... // XXX need a way to make encode option transparent... @@ -1559,10 +1492,16 @@ module.parser = { function(page, args, body, state, handler){ var that = this + /* XXX see .joinBlocks(..) join caching for more info... + // cache body ast... + body = body ? + this.ast(body) + : body + //*/ + var recursive = state.recursive = args.recursive - ?? body ?? state.recursive var base = page.basepath @@ -1572,6 +1511,20 @@ module.parser = { return Promise.awaitOrRun( this.parseNested(page, src, state), function(src){ + // check for recursion... + // XXX do we do this for pattern paths??? + // XXX this does not catch the /A/A/A/... recursion... + var stack = state.include_stack ??= [] + // XXX is this the right separator??? + // ...need something that can't be in a path... + var base_src = base +'|'+ src + if(stack.includes(base_src)){ + if(recursive){ + return that.expand(page, recursive, state) } + throw new Error('Recursion:\n\t'+ + [...stack, base_src].join('\n\t\t-> ')) } + stack.push(base_src) + var cache = state.cache ??= {} if(cache[src]){ return cache[src] } @@ -1586,20 +1539,6 @@ module.parser = { // out of context... var depends = ((state.depends ??= {})[src] ??= {}) - // check for recursion... - // XXX do we do this for pattern paths??? - // XXX this does not catch the /A/A/A/... recursion... - var stack = state.include_stack ??= [] - // XXX is this the right separator??? - // ...need something that can't be in a path... - var base_src = base +'|'+ src - if(stack.includes(base_src)){ - if(recursive){ - return recursive } - throw new Error('Recursion:\n\t'+ - [...stack, base_src].join('\n\t\t-> ')) } - stack.push(base_src) - // content handler... handler ??= function(page, text, state){ @@ -1616,12 +1555,15 @@ module.parser = { : {}) : this.expand(page, text, state)} + var pageHandler = + function(text){ + // XXX handle body / ... + // XXX + return handler.call(that, page, text, state) } var resultHandler = function(pages){ - // XXX not sure if this can happen or why... - if(state.include_stack.at(-1) != base_src){ - throw new Error('Include stack error') } - state.include_stack.pop() + state.include_stack.at(-1) == base_src + && state.include_stack.pop() // cleanup... if(state.include_stack.length == 0){ delete state.include_stack @@ -1643,8 +1585,7 @@ module.parser = { .iter( that.joinBlocks( page, - pages.map(function(text){ - return handler.call(that, page, text, state) }), + pages.map(pageHandler), args.join, state)) .flat() diff --git a/v3/pwiki/test/parser.js b/v3/pwiki/test/parser.js index 224fc7c..fdbf931 100755 --- a/v3/pwiki/test/parser.js +++ b/v3/pwiki/test/parser.js @@ -18,8 +18,14 @@ module.exports.PAGES = { '/async/page': Promise.resolve('Page'), '/includePage': '@include(/page)', '/isolated': '@slot(slot original)', - '/includeSelf': '@include(/includeSelf)', - '/async/includeSelf': Promise.resolve('@include(/includeSelf)'), + + '/recursive/Self': '<< @include(/recursive/Self) >>', + '/recursive/OtherSelf': '<< @include(/recursive/SelfOther) >>', + '/recursive/SelfOther': '<< @include(/recursive/OtherSelf) >>', + '/async/recursive/Self': Promise.resolve('<< @include(/async/recursive/Self) >>'), + '/async/recursive/OtherSelf': Promise.resolve('<< @include(/async/recursive/SelfOther) >>'), + '/async/recursive/SelfOther': Promise.resolve('<< @include(/async/recursive/OtherSelf) >>'), + '/multi/page': [ 'A', 'B', 'C' ], } @@ -219,10 +225,26 @@ test.Setups({ 'original overloaded', ], } }, - // XXX recursion... - // XXX + // recursion... + // XXX test path recursion: /A -> /A/A -> /A/A/A -> ... + include_recursive_a: function(assert){ + return { + page: P, + code:[ + '@include(/recursive/Self recursive="recursion found")', + '@include(/async/recursive/Self recursive="recursion found")', + '<< recursion found >>', ], } }, + include_recursive_b: function(assert){ + return { + page: P, + code:[ + '@include(/recursive/SelfOther recursive="recursion found")', + '@include(/async/recursive/SelfOther recursive="recursion found")', + '<< << recursion found >> >>', ], } }, // quote... + // for inline quoting see: test.Modifiers.quote + // XXX }) @@ -251,32 +273,49 @@ test.Modifiers({ `[[ ${state.code.at(-1)} ]]`, ] return state }, + quote: function(assert, state){ + return state.code + .slice(0, -1) + .map(function(code){ + code = code.replace(/<\/quote>/, '</quote>') + return { + page: state.P, + code: [ + `${ code }`, + code, + ], + } }) }, }) test.Tests({ parse: async function(assert, state){ - var {page, code, st} = state - page ??= {} - st ??= {} + var states = + state instanceof Array ? + state + : [state] + for(state of states){ + var {page, code, st} = state + page ??= {} + st ??= {} - var res - var inputs = code.slice(0, -1) - var expect = code.at(-1) - var i = 0 - for(var input of inputs){ - var p = serialize.partialDeepCopy(page) - var s = serialize.partialDeepCopy(st) - assert( - (res = await parser.parse( - p, - input, - s)) - === expect, - 'Parsing:', - '\n\t in: "'+ input +'"', - '\n\t out: "'+ res +'"', - '\n\texpected: "'+ expect +'"') } }, + var res + var inputs = code.slice(0, -1) + var expect = code.at(-1) + var i = 0 + for(var input of inputs){ + var p = serialize.partialDeepCopy(page) + var s = serialize.partialDeepCopy(st) + assert( + (res = await parser.parse( + p, + input, + s)) + === expect, + 'Parsing:', + '\n\t in: "'+ input +'"', + '\n\t out: "'+ res +'"', + '\n\texpected: "'+ expect +'"') } } }, //asyncParse: async function(assert, state){ // return await this.parse(assert, state) }, })