Compare commits

..

No commits in common. "2ad186888e75c903c61a9c3375accfe7c656c2b6" and "e7dc0194ad129b613219773396f56c8668b81fb6" have entirely different histories.

2 changed files with 228 additions and 235 deletions

View File

@ -418,7 +418,7 @@ module.BaseParser = {
: lex : lex
var quoting = to var quoting = to
&& !!this.macros[to].quoting && (page.QUOTING_MACROS ?? []).includes(to)
&& [] && []
// NOTE: we are not using for .. of .. here as it depletes the // NOTE: we are not using for .. of .. here as it depletes the
@ -530,8 +530,6 @@ module.BaseParser = {
// //
// // wait for all... // // wait for all...
// wait: <promise> | null, // wait: <promise> | null,
//
// ...
// } // }
// //
// //
@ -576,6 +574,14 @@ module.BaseParser = {
ast ast
: ast.iter() : ast.iter()
var wait = new Set()
state.wait instanceof Promise
&& wait.add(state.wait)
var resolve, reject
state.wait = new Promise(
function(){
;[resolve, reject] = arguments })
var elems = [] var elems = []
for(let elem of ast){ for(let elem of ast){
// text block... // text block...
@ -615,10 +621,9 @@ module.BaseParser = {
var res = that.callMacro(page, name, args, body, state) var res = that.callMacro(page, name, args, body, state)
// async... // async...
if(res instanceof Promise){ if(res instanceof Promise){
let all, nested
all = state.waitAll =
Promise.all([state.waitAll, res])
elem.resolving = res elem.resolving = res
wait.add(res)
state.waitAll = res
// XXX do we need to wait till the last .waitNested is // XXX do we need to wait till the last .waitNested is
// resolved? // resolved?
// ...should it's handlers complete?? // ...should it's handlers complete??
@ -627,13 +632,13 @@ module.BaseParser = {
// func to the macro... // func to the macro...
// ...do we need this? // ...do we need this?
if(!res.isolated){ if(!res.isolated){
nested = state.waitNested = state.waitNested = res }
Promise.all([state.waitNested, res]) }
res.then( res.then(
function(value){ function(value){
if(state.waitAll === all){ wait.delete(res)
if(state.waitAll === res){
delete state.waitAll } delete state.waitAll }
if(state.waitNested === nested){ if(state.waitNested === res){
delete state.waitNested } delete state.waitNested }
delete elem.resolving delete elem.resolving
elem.value = value elem.value = value
@ -654,52 +659,25 @@ module.BaseParser = {
elem = res } elem = res }
elems.push(elem) } elems.push(elem) }
// cleanup... // handle wait for isolated macros to resolve...
var wait var done = function(){
var waitAll = state.waitAll resolve(state)
state.waitAll delete state.wait }
&& state.waitAll if(wait.size > 0){
.then(function(){ Promise.all(wait)
// only cleanup our own mess =) .then(
waitAll === state.waitAll done,
&& (delete state.waitAll) }) function(err){
&& (wait = state.wait = waitAll reject(err) })
.then(function(){ // done...
wait === state.wait } else {
&& (delete state.wait) done() }
return elems }))
var waitNested = state.waitNested
state.waitNested
&& state.waitNested
.then(function(){
// only cleanup our own mess =)
waitNested === state.waitNested
&& (delete state.waitNested) })
return elems }, return elems },
// resolve stage II macros and merge results... // resolve stage II macros and merge results...
// //
/* XXX // XXX BUG: sync/async paths seem to diverge...
// XXX these yeild different results:
// r = parser.resolve(
// tests.P,
// '<slot X>@include(/async/page)</slot> <slot X value>',
// s = {})
// s.wait.then(function(){
// console.log(r) }) // -> ['Page '] (err)
// and:
// r = parser.expand(
// tests.P,
// '<slot X>@include(/async/page)</slot> <slot X slot/>',
// s = {})
// s.wait.then(function(){
// r = parser.resolve(test.P, r, s)
// console.log(r) }) // -> ['slot '] (correct)
// -> promises seem to not be sequenced correctly here...
// 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={}){ resolve: function(page, ast, state={}){
var that = this var that = this
ast = typeof(ast) != 'object' ? ast = typeof(ast) != 'object' ?
@ -727,89 +705,29 @@ module.BaseParser = {
prev = '' } prev = '' }
if(args.length > 0){ if(args.length > 0){
elems.push(args.shift()) } } } elems.push(args.shift()) } } }
var unpack = function(elem){
// XXX
}
// 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' ?
this.expand(page, ast, state)
: ast instanceof types.Generator ?
ast
: ast.iter()
// merge resolved elements into the last item of elems...
// XXX can ast be a promise??? // XXX can ast be a promise???
// XXX elems can be unresolved -- need a merge strategy for them... // XXX elems can be unresolved -- need a merge strategy for them...
var elems = []
for(var elem of ast){ for(var elem of ast){
// nesting... // nesting...
while(elem && elem.value){ while(elem && elem.value){
elem = elem.value
// exec stage II macros... // exec stage II macros...
if(typeof(elem.value) == 'function'){ if(typeof(elem) == 'function'){
let e = elem elem = elem(state) } }
// NOTE: if not everything is resolved, delay the stage II
// callbacks till .wait is done...
Promise.awaitOrRun(
state.wait,
function(){
return elem = e.value =
e.value(state) })
break }
elem = elem.value }
if(elem == null){ if(elem == null){
continue } continue }
// XXX do a delayed merge...
// ...but for this we need to also apply the rest of this iteration to the result...
if(elem.resolving){
console.log('!!!!!!!!!!!!!!!!!!!')
}
// atomic values... // atomic values...
if(typeof(elem) != 'object'){ if(typeof(elem) != 'object'){
elems.push(elem) merge(elem)
continue } continue }
// value is resolved but "empty" -> skip... // value is resolved but "empty" -> skip...
if('value' in elem if('value' in elem
@ -818,48 +736,61 @@ module.BaseParser = {
continue } continue }
// expand ast... // expand ast...
if(elem instanceof Array){ if(elem instanceof Array){
// XXX this can be or containe promises... // XXX this can containe promises...
elems.push(...that.resolve(page, elem, state)) merge(...this.resolve(page, elem, state))
continue } continue }
// expand .body attribute... // expand .body attribute...
/* XXX is this needed here??? if((elem.attrs ?? {}).body instanceof Array){
if(elem.body instanceof Array){ // XXX this can containe promises...
console.warn('!!! RESOLVE_BODY') elem.attrs.body = this.resolve(page, elem.attrs.body, state) }
// XXX this can be or containe promises...
elem.body = that.resolve(page, elem.body, state) }
//*/
// nested macro with no value set -- skip... // nested macro with no value set -- skip...
if(that.macros[elem.name] instanceof Array){ if(this.macros[elem.name] instanceof Array){
continue } continue }
// unresolved... // unresolved...
elems.push(elem) } merge(elem) }
return elems }, return elems },
// XXX render api... // XXX render api...
// XXX how should this play with filters??? // XXX how should this play with filters???
// ...should filters be client-side only?? // ...should filters be client-side only??
render: function*(page, ast, callback, state={}){ render: function*(page, ast, callback, state={}){
// XXX ast = this.resolve(page, ast, state)
}, for(var elem of ast){
yield Promise.awaitOrRun(
elem,
callback) } },
// XXX // XXX
parse: function(page, ast, state={}, wait='wait'){ _parse: function(page, ast, state={}){
var that = this
var reresolve = !!state.wait
return Promise.awaitOrRun( return Promise.awaitOrRun(
this.resolve(page, ast, state), this.resolve(page, ast, state),
state[wait], function(ast){
function(ast, reresolve){ // XXX
ast = reresolve ? if(ast.length > 1){
that.resolve(page, ast, state) throw new Error('!!!!')
: ast }
return (ast ?? '').join('') }) }, return ast[0]
?? '' }) },
// XXX
// XXX how should this play with filters??? // XXX how should this play with filters???
// ...should filters be client-side only?? // ...should filters be client-side only??
parseNested: function(page, ast, state={}){ parseNested: function(page, ast, state={}){
return this.parse(page, ast, state, 'waitNestedi') }, return Promise.awaitOrRun(
state.waitNested,
this._parse(page, ast, state),
function(_, ast){
return ast }) },
// XXX this can't be used from within macros -- will deadlock the results...
// XXX how should this play with filters???
// ...should filters be client-side only??
parse: function(page, ast, state={}){
return Promise.awaitOrRun(
state.wait,
this._parse(page, ast, state),
function(_, ast){
return ast }) },
@ -1121,12 +1052,6 @@ function(macro){
macro.lazy = true macro.lazy = true
return macro } return macro }
var quoting =
module.quoting =
function(macro){
macro.quoting = true
return macro }
// XXX RENAME... // XXX RENAME...
// ...this is more of an expander/executer... // ...this is more of an expander/executer...
@ -1135,6 +1060,9 @@ var parser =
module.parser = { module.parser = {
__proto__: BaseParser, __proto__: BaseParser,
// list of macros that will get raw text of their content...
QUOTING_MACROS: ['quote'],
// //
// <macro>(<args>, <body>, <state>){ .. } // <macro>(<args>, <body>, <state>){ .. }
// -> undefined // -> undefined
@ -1530,7 +1458,7 @@ module.parser = {
// is this a promise/value, iterable promise a generator // is this a promise/value, iterable promise a generator
// an async generator, ... or a combination/stack of the above??? // an async generator, ... or a combination/stack of the above???
lazy( lazy(
function(page, args, body, state, handler){ function(page, args, body, state, key='included', handler){
var that = this var that = this
var recursive = var recursive =
@ -1577,28 +1505,28 @@ module.parser = {
// content handler... // content handler...
handler ??= handler ??=
function(page, text, state){ function(page, text, state){
// XXX should this be the difference between return this._parse(page, text, state) }
// @include(..) and @source(..)???
// i.e.
// .include(..) -> .resolve(..)
// .source(..) -> .expand(..)
return args.isolated ?
this.resolve(page, text,
args.isolated == 'partial' ?
// XXX
serialize.partialDeepCopy(state)
: {})
: this.expand(page, text, state)}
var pageHandler = var pageHandler =
function(text, i, l){ function(text, i, l){
return [ return [
handler.call(that, page, text, state), handler.call(that,
page,
text,
// isolated up -- will see all
// the state but can have no
// side-effects...
args.isolated == 'partial' ?
serialize.partialDeepCopy(state)
// fully isolated...
: args.isolated ?
{}
: state),
// join... // join...
(args.join (args.join
&& i < l.length - 1) ? && i < l.length - 1) ?
that.expand(page, args.join, state) that._parse(page, args.join, state)
: [], : []
].flat() } ].flat() }
var resultHandler = var resultHandler =
function(pages){ function(pages){
@ -1630,6 +1558,106 @@ module.parser = {
.sync(), .sync(),
resultHandler ) }) }) })), resultHandler ) }) }) })),
_include: Macro(
['src', 'recursive', 'join',
['s', 'strict', 'isolated']],
async function*(parser, 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 // NOTE: the main difference between this and @include is that
// this renders the src in the context of current page while // this renders the src in the context of current page while
// include is rendered in the context of its page but with // include is rendered in the context of its page but with
@ -1643,48 +1671,30 @@ module.parser = {
['src', 'recursive', 'join', ['src', 'recursive', 'join',
['s', 'strict']], ['s', 'strict']],
//['src'], //['src'],
function(page, args, body, state){ async function*(args, body, state){
var that = this var that = this
return this.macros['include'].call(this, yield* this.__parser__.macros.include.call(this,
page, args, body, state, 'source',
function(page, text, state){ args, body, state, 'sources',
return that.expand(page, text, state) }) }), 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... // Load macro and slot definitions but ignore the page text...
// //
// NOTE: this is essentially the same as @source(..) but returns ''. // NOTE: this is essentially the same as @source(..) but returns ''.
// XXX revise name... // XXX revise name...
// XXX UPDATE...
load: Macro( load: Macro(
['src'], ['src', ['strict']],
function(args, body, state){ async function*(args, body, state){
var that = this var that = this
return Promise.awaitOrRun( yield* this.__parser__.macros.include.call(this,
this.macros['include'].call(this, page, args, body, state), 'load',
function(){ args, body, state, 'sources',
async function(src, state){
await that.parse(this.raw, state)
return '' }) }), return '' }) }),
// XXX a naive version, see ._quote(..) for reference...
quote: Macro(
['src', 'join', 'filter'],
quoting(
function(page, args, body, state){
var res =
(args.src ?
page.get(args.src).raw
: body ?
body
: [])
return Promise.awaitOrRun(
res,
function(res){
// filter...
if(args.filter){
// XXX
}
// XXX join...
// XXX
return res }) })),
// //
// @quote(<src>) // @quote(<src>)
// //
@ -1707,7 +1717,7 @@ module.parser = {
// XXX need a way to escape macros -- i.e. include </quote> in a quoted text... // XXX need a way to escape macros -- i.e. include </quote> in a quoted text...
// XXX should join/else be sub-tags??? // XXX should join/else be sub-tags???
// XXX UPDATE... // XXX UPDATE...
_quote: Macro( quote: Macro(
['src', 'filter', 'text', 'join', 'else', ['src', 'filter', 'text', 'join', 'else',
['s', 'expandactions', 'strict']], ['s', 'expandactions', 'strict']],
async function*(args, body, state){ async function*(args, body, state){
@ -1817,7 +1827,6 @@ module.parser = {
var filters = state.quote_filters = var filters = state.quote_filters =
state.quote_filters ?? [] state.quote_filters ?? []
filters.splice(filters.length, 0, ...Object.keys(args)) }, filters.splice(filters.length, 0, ...Object.keys(args)) },
//*/
// //
// <macro src=<url>> .. </macro> // <macro src=<url>> .. </macro>

View File

@ -17,7 +17,6 @@ module.exports.PAGES = {
'/page': 'Page', '/page': 'Page',
'/async/page': Promise.resolve('Page'), '/async/page': Promise.resolve('Page'),
'/includePage': '@include(/page)', '/includePage': '@include(/page)',
'/isolated': '@slot(slot original)'
} }
var P = var P =
@ -51,7 +50,6 @@ module.exports.P = {
//--------------------------------------------------------------------- //---------------------------------------------------------------------
test.Setups({ test.Setups({
// general parser...
empty: function(assert){ empty: function(assert){
return { code: [ '', '' ] }}, return { code: [ '', '' ] }},
@ -143,7 +141,11 @@ test.Setups({
return {code: [ return {code: [
'<slot slot>moo<slot slot>[[ <content/> ]]</slot></slot>', '<slot slot>moo<slot slot>[[ <content/> ]]</slot></slot>',
'[[ moo ]]' ]} }, '[[ moo ]]' ]} },
// shown/hidden..
/* XXX SHOWN_HIDDEN
// XXX these need to be revised...
// ...do we actually need hidden/shown???
//
slot_shown: function(assert){ slot_shown: function(assert){
var ins = this.slot_value(assert).code var ins = this.slot_value(assert).code
var expect = ins.pop() var expect = ins.pop()
@ -168,8 +170,9 @@ test.Setups({
var expect = ins.pop() var expect = ins.pop()
return {code: [ return {code: [
...ins.map(function(i){ ...ins.map(function(i){
return i +' | @slot(slot shown)' }), return i +'@slot(slot shown)' }),
' | value' ]} }, '' ]} },
//*/
// XXX var... // XXX var...
// XXX // XXX
@ -178,7 +181,7 @@ test.Setups({
// XXX // XXX
// include... // include...
// XXX see inside... // XXX these do not play well with modifiers...
include: function(assert, path='/blank', expected){ include: function(assert, path='/blank', expected){
return { return {
page: P, page: P,
@ -187,37 +190,18 @@ test.Setups({
// XXX for some reason this does not parse int a macro.... // XXX for some reason this does not parse int a macro....
//'<include /blank />', //'<include /blank />',
`<include "${path}" />`, `<include "${path}" />`,
`<include src="${path}" />`, expected
?? P.get(path).raw,
expected
?? P.get(path).raw,
], ],
}}, }},
include_page: function(assert){ include_page: function(assert){
return this.include(assert, '/page') }, return this.include(assert, '/page') },
include_include_page: function(assert){ include_include_page: function(assert){
return this.include(assert, '/includePage', 'Page') }, return this.include(assert, '/includePage', 'Page') },
/* XXX parser.resolve(..) does not handle promises correctly yet...
include_async: function(assert){ include_async: function(assert){
return this.include(assert, '/async/page', 'Page') }, return this.include(assert, '/async/page') },
// islotaed... //*/
include_nonisolated: function(assert){
return {
page: P,
code:[
'@include("/isolated") @slot(slot overloaded)',
'overloaded ',
],
} },
include_isolated: function(assert){
return {
page: P,
code:[
'@include(isolated /isolated) @slot(slot overloaded)',
'original overloaded',
],
} },
// XXX recursion...
// XXX
}) })
@ -250,7 +234,7 @@ test.Modifiers({
test.Tests({ test.Tests({
parse: async function(assert, state){ parse: function(assert, state){
var {page, code, st} = state var {page, code, st} = state
page ??= {} page ??= {}
st ??= {} st ??= {}
@ -263,7 +247,7 @@ test.Tests({
var p = serialize.partialDeepCopy(page) var p = serialize.partialDeepCopy(page)
var s = serialize.partialDeepCopy(st) var s = serialize.partialDeepCopy(st)
assert( assert(
(res = await parser.parse( (res = parser.parse(
p, p,
input, input,
s)) s))