From 782d2e7acdacfa75d78519b255e57d5f9a8d048a Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Sat, 4 Jul 2026 13:55:10 +0300 Subject: [PATCH] @include(..)'s body is now resolved in the context of the loaded page (non-isolated)... now I'm not sure what's the difference between @include(..) and @macro(..) other than the name... Am I designing a language with a single operator with the logic pushed out into paths??? =) Signed-off-by: Alex A. Naanou --- v3/bootstrap/Doc/Macros.md | 1 + v3/pwiki/parser.js | 274 +++++-------------------------------- v3/pwiki/test/parser.js | 75 ++++++++-- 3 files changed, 98 insertions(+), 252 deletions(-) diff --git a/v3/bootstrap/Doc/Macros.md b/v3/bootstrap/Doc/Macros.md index be3aa44..2b10594 100755 --- a/v3/bootstrap/Doc/Macros.md +++ b/v3/bootstrap/Doc/Macros.md @@ -260,6 +260,7 @@ If `isolated` is given, prevent slots from included page from affecting the including page. **Example:** + `/some/page`: ``` diff --git a/v3/pwiki/parser.js b/v3/pwiki/parser.js index 0057f46..fc9db21 100644 --- a/v3/pwiki/parser.js +++ b/v3/pwiki/parser.js @@ -442,7 +442,6 @@ module.BaseParser = { // .body -> .args.body -> .args.text // // NOTE: this internaly uses .macros to check for propper nesting - //group: function*(page, lex, to=false){ group: function*(lex, to=false, context){ lex = typeof(lex) != 'object' ? this.lex(lex) @@ -526,6 +525,9 @@ module.BaseParser = { // normal value... yield value } }, + // NOTE: the output of this can be safely cached, it does not depend + // on anything external and as long as the code stays the same + // this will not change. // XXX do we need a pre-parse stage??? // - expand local macros // - collect links @@ -558,11 +560,20 @@ module.BaseParser = { // waitAll: | null, // // // wait for all... + // // NOTE: this is set just before .expand(..) returns and is + // // not available during the expansion process. // wait: | null, // // ... // } // + // NOTE: .waitNested and .waitAll are "live", i.e. while expanding, + // at any given time they contain the promise of the last async + // element upto the point of read. .wait however is set at the + // end of .expand(..), i.e. it has no meaning until .expand(..) + // finishes, and represents the last .waitAll but will return + // the whole expanded ast. + // // // XXX the parser is always sync // - macros always return sync but can be resolved or not resolved @@ -819,236 +830,6 @@ module.BaseParser = { // ...should filters be client-side only?? parseNested: function(page, ast, state={}, nested_handlers={}){ return this.parse(page, ast, state, nested_handlers, 'waitNestedi') }, - - - - // Expand macros... - // - // ::= [ , .. ] - // ::= - // - // | - // | - // | { skip: true, ... } - // | { data: } - // | - // - // XXX macros: we are mixing up ast state and parse state... - // one should only be used for parsing and be forgotten after - // the ast is constructed the other should be part of the ast... - // XXX DOC inconsistent await behavior... - // in var macro, as an example, there are two paths: the assign and - // the get, the assign calls .parse(..) and awaits for the result - // while the get path is "sync", this results in the first exec path - // getting pushed to the next execution frame while the second is run - // in sync with the caller, here is a simplified demo: - // console.log(1) - // // note that we are NOTE await'ing for the function here... - // (async function f(){ - // console.log(2)})() - // console.log(3) - // -> prints 1, 2, 3 - // and: - // console.log(1) - // (async function f(){ - // // note the await -- this is the only difference... - // console.log(await 2)})() - // console.log(3) - // -> prints 1, 3, 2 - // this could both be a bug or a feature depending on how you look - // at it, but it makes promise sequencing very unpredictable... - // ...another problem here is that in the var macro, simply adding - // an await of something does not fix the issue, we need to await - // for something significant -- await this.parse('') works, while - // await vars[name] does not -- to the contrary of the above example... - /* XXX - expand: function(page, ast, state={}){ - var that = this - ast = ast == null ? - Promise.awaitOrRun( - page.raw, - function(raw){ - return that.group(page, raw ?? '') }) - : typeof(ast) != 'object' ? - this.group(page, ast) - : ast instanceof types.Generator ? - ast - : ast.iter() - - // NOTE this must execute sequentially for things that depend on - // lexical scope not to get lost in the mess... - return Promise.seqiter(ast, - function(value){ - // text block... - if(typeof(value) == 'string'){ - return value } - // macro... - var {name, args, body} = value - // nested macro -- skip... - if(typeof(that.macros[name]) != 'function'){ - return {...value, skip: true} } - // macro call... - return Promise.awaitOrRun( - that.callMacro(page, name, args, body, state), - function(res){ - res = res ?? '' - // result... - if(res instanceof Array - || that.macros[name] instanceof types.Generator){ - return res - } else { - return [res] } }) }, - function(err){ - console.error(err) - return [page.parse( - // XXX add line number and page path... - '@include("./ParseError' - +':path=' - // XXX use pwpath.encodeElem(..) ??? - + page.path - +':msg=' - + err.message - // quote html stuff... - .replace(/&/g, '&') - .replace(//g, '>') - // quote argument syntax... - .replace(/["']/g, function(c){ - return '%'+ c.charCodeAt().toString(16) }) - .replace(/:/g, ':') - .replace(/=/g, '=') - +'")')] }) - .sync() }, - //*/ - - - // recursively resolve and enumerate the ast... - // - // ::= [ , .. ] - // ::= - // - // | { data: } - // | - // - // (state) - // -> - // - // - // NOTE: (..) is called in the context of page... - // - // XXX should this also resolve e.data??? - /* XXX - resolve: function(page, ast, state={}){ - var that = this - ast = ast - ?? this.expand(page, null, state) - ast = typeof(ast) != 'object' ? - this.expand(page, ast, state) - : ast - // XXX .awaitOrRun(..) will check inside the input array for promises, do - // we need to do this??? - ast = Promise.awaitOrRun( - ast, - function(ast){ - var async_content = false - return ast - .map(function(e){ - // expand delayed sections... - e = typeof(e) == 'function' ? - e.call(page, state) - : e - // promise... - if(e instanceof Promise){ - async_content = true - return e - // expand arrays... - } else if(e instanceof Array - || e instanceof types.Generator){ - return that.resolve(page, e, state) - // data -- unwrap content... - } else if(e instanceof Object && 'data' in e){ - var res = Promise.awaitOrRun( - that.resolve(page, e.data, state), - function(e){ - return { data: e } }) - res instanceof Promise - && (async_content = true) - return res - // skipped items... - } else if(e instanceof Object && e.skip){ - return [] - } else { - return [e] } }) - // NOTE: if we still have promises in the ast, wrap the - // whole thing in a promise... - .run(function(){ - return async_content ? - Promise.iter(this) - : this }) - .flat() }) - return ast instanceof Promise ? - // keep the API consistently array-like... - ast.iter() - : ast }, - //*/ - - // Fully parse a page... - // - // This runs in two stages: - // - resolve the page - // - lex the page -- .lex(..) - // - group block elements -- .group(..) - // - expand macros -- .expand(..) - // - resolve ast -- .resolve(..) - // - apply filters - // - // NOTE: this has to synchronize everything between stage 1 (up to - // and including expand) and stage 2 (post-handlers, filters, ...) - // because the former need a fully loaded and expanded page if - // we want to do this in 2 stages and not 3... - // XXX might be fun to try a load-and-tweak approach the first - // version used -- i.e. setting placeholders and replacing - // them on demand rather than on encounter (as is now), e.g. - // a slot when loaded will replace the prior occurrences... - // - // XXX add a special filter to clear pending filters... (???) - /* XXX - parse: function(page, ast, state={}){ - var that = this - return this.resolve(page, ast, state) - // filters... - .map(function(section){ - // normalize types... - section = - typeof(section) == 'number' ? - section + '' - : section == null ? - '' - : section - return ( - // expand section... - typeof(section) != 'string' ? - section.data - // global filters... - : state.filters ? - that.normalizeFilters(state.filters) - .reduce(function(res, filter){ - // unknown filter... - // NOTE: we try not to break on user errors - // if we can help it... - if(page.filters[filter] == null){ - console.warn( - '.parse(..): unsupported filter: '+ filter) - return res } - // NOTE: if a filter returns falsy then it - // will have no effect on the result... - return page.filters[filter].call(page, res) - ?? res }, section) - // no global filters... - : section ) }) - .flat() - .join('') }, - //*/ } @@ -1088,9 +869,6 @@ function(macro){ - - - // XXX RENAME... // ...this is more of an expander/executer... // ...might be a good idea to also do a check without executing... @@ -1511,6 +1289,8 @@ module.parser = { state.recursive = args.recursive ?? state.recursive + // XXX get default... + recursive ??= '' var base = page.basepath // XXX we do this before or after we parse??? @@ -1523,10 +1303,7 @@ module.parser = { // XXX will this work for @source(..)??? var stack = state.include_stack ??= [] if(stack.includes(src)){ - if(recursive){ - return that.expand(page, recursive, state) } - throw new Error('Recursion:\n\t'+ - [...stack, src].join('\n\t\t-> ')) } + return that.expand(page, recursive, state) } stack.push(src) var cache = state.cache ??= {} @@ -1559,12 +1336,12 @@ module.parser = { : this.expand(page, text, state) } var pageHandler = - function(text){ + function([path, text]){ var content_handled = false return body ? // handle body / ... Promise.awaitOrRun( - that.expand(page, body, state, + that.expand(page.get(path), body, state, { content: function(){ content_handled = true return text = handler.call(that, page, src, text, state) } }), @@ -1590,8 +1367,12 @@ module.parser = { // get and run things... return Promise.awaitOrRun( + page.get(src).matched, page.get(src).raw, - function(pages){ + function(paths, pages){ + paths = paths instanceof Array ? + paths + : [paths] pages = pages instanceof Array ? pages : [pages] @@ -1601,7 +1382,9 @@ module.parser = { .iter( that.joinBlocks( page, - pages.map(pageHandler), + Array + .zip(paths, pages) + .map(pageHandler), args.join, state)) .flat() @@ -1718,6 +1501,7 @@ module.parser = { filters.splice(filters.length, 0, ...Object.keys(args)) }, //*/ + // expand the body in the context of a page... // // > .. // @@ -1759,6 +1543,12 @@ module.parser = { // XXX SORT sorting not implemented yet... // XXX UPDATE... macro: Macro( + ['name', 'src', 'sort', 'text', 'join', 'else', + ['strict', 'isolated', 'inheritmacros', 'inheritvars']], + lazy( + function(page, args, body, state){ + })), + _macro: Macro( ['name', 'src', 'sort', 'text', 'join', 'else', ['strict', 'isolated', 'inheritmacros', 'inheritvars']], async function*(args, body, state){ diff --git a/v3/pwiki/test/parser.js b/v3/pwiki/test/parser.js index 864ac54..7ad83ab 100755 --- a/v3/pwiki/test/parser.js +++ b/v3/pwiki/test/parser.js @@ -53,8 +53,43 @@ module.exports.P = { get basepath(){ return path.dirname(this.path ?? '') }, + get matched(){ + var path_action_pattern = /\/path\/?/ + var path = this.path.replace(path_action_pattern, '') + var path_action = path != this.path + + if(path.includes('*')){ + var that = this + var re = new RegExp(path + .replace(/\//, '\\/') + .replace(/\*/g, '[^\\/]+')) + return Object.keys(this.__pages__) + .map(function(p){ + return re.test(p) ? + [path_action ? + p +'/path' + :p] + : [] }) + .flat() } + return [this.path] }, + + // * -> a promise or a list of promises??? get raw(){ - return this.__pages__[this.path] ?? '' }, + var res = [] + var path_action_pattern = /\/path\/?/ + var path_action = this.path.match(path_action_pattern) + + for(var path of this.matched){ + res.push( + path_action ? + path.replace(path_action_pattern, '') + : this.__pages__[path]) } + + return res.length == 1 ? + res[0] + : res }, + + // XXX should this return an arrya for a multi-match??? get text(){ return this.__parser__.parse(this, this.raw, {}) }, @@ -194,12 +229,6 @@ test.Setups({ return i +' | @slot(slot shown)' }), ' | value' ]} }, - // XXX var... - // XXX - - // XXX arg... - // XXX - // include... // XXX see inside... include: function(assert, path='/blank', expected){ @@ -239,13 +268,23 @@ test.Setups({ 'original overloaded', ], } }, + /* XXX + include_partial_isolated: function(assert){ + return { + page: P, + code:[ + '@include(isolated="partial" /isolated) @slot(slot overloaded)', + 'original overloaded', + ], + } }, + //*/ // content... - include_content: function(assert){ + include_content: function(assert, path='/page'){ return { page: P, code: [ - '[[ ]]', - '[[ '+ P.get('/page').raw +' ]]', ], } }, + '[[ ]]', + '[[ '+ P.get(path).raw +' ]]', ], } }, // XXX // recursion... @@ -268,6 +307,22 @@ test.Setups({ // quote... // for inline quoting see: test.Modifiers.quote // XXX + quote_content: function(assert, path='/isolated'){ + return { + page: P, + code: [ + '[[ ]]', + '[[ '+ P.get(path).raw +' ]]', ], } }, + + // XXX macro... + // XXX + + // XXX var... + // XXX + + // XXX arg... + // XXX + })