@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 <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2026-07-04 13:55:10 +03:00
parent 79ffe58b46
commit 782d2e7acd
3 changed files with 98 additions and 252 deletions

View File

@ -260,6 +260,7 @@ If `isolated` is given, prevent slots from included page from affecting
the including page.
**Example:**
`/some/page`:
```
<slot X value/>

View File

@ -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: <promise> | null,
//
// // wait for all...
// // NOTE: this is set just before .expand(..) returns and is
// // not available during the expansion process.
// wait: <promise> | 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...
//
// <ast> ::= [ <item>, .. ]
// <item> ::=
// <func>
// | <promise>
// | <string>
// | { skip: true, ... }
// | { data: <ast> }
// | <ast>
//
// 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// quote argument syntax...
.replace(/["']/g, function(c){
return '%'+ c.charCodeAt().toString(16) })
.replace(/:/g, '&colon;')
.replace(/=/g, '&equals;')
+'")')] })
.sync() },
//*/
// recursively resolve and enumerate the ast...
//
// <ast> ::= [ <item>, .. ]
// <item> ::=
// <string>
// | { data: <ast> }
// | <func>
//
// <func>(state)
// -> <ast>
//
//
// NOTE: <func>(..) 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 / <content/>...
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...
//
// <macro src=<url>> .. </macro>
//
@ -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){

View File

@ -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: [
'<include "/page">[[ <content/> ]]</include>',
'[[ '+ P.get('/page').raw +' ]]', ], } },
'<include "'+ path +'">[[ <content/> ]]</include>',
'[[ '+ P.get(path).raw +' ]]', ], } },
// XXX
// recursion...
@ -268,6 +307,22 @@ test.Setups({
// quote...
// for inline quoting see: test.Modifiers.quote
// XXX <quote src=.. />
quote_content: function(assert, path='/isolated'){
return {
page: P,
code: [
'<quote "'+ path +'">[[ <content/> ]]</quote>',
'[[ '+ P.get(path).raw +' ]]', ], } },
// XXX macro...
// XXX
// XXX var...
// XXX
// XXX arg...
// XXX
})