added tests + cleaning up @slot(..)

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2026-06-28 15:28:42 +03:00
parent f2a47de99f
commit b9b11ecbda
3 changed files with 256 additions and 16 deletions

View File

@ -11,7 +11,9 @@
"ig-actions": "*",
"ig-features": "*",
"ig-object": "*",
"ig-types": "*",
"ig-types": "*"
},
"devDependencies": {
"ig-test": "*"
},
"disabled-dependencies": {

View File

@ -229,7 +229,9 @@ module.BaseParser = {
macro.arg_spec
?? [],
args),
body,
// XXX
body
?? args.body,
state,
...rest) },
@ -256,7 +258,12 @@ module.BaseParser = {
// args: {
// <index>: <value>,
// <key>: <value>,
//
// ...
//
// // special case: .body argument's value is treated in
// // the same way as block body -- it is parsed.
// body: <ast>,
// }
// match: <string>,
// }
@ -335,6 +342,10 @@ module.BaseParser = {
?? groups.unnamedArg)
.replace(/\\(["'])/g, '$1') }
// special case: .body arg -> lex...
if(args.body){
args.body = [...this.lex(page, args.body)] }
// macro-spec...
yield {
name: (cur.nameInline
@ -384,7 +395,7 @@ 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){
group: function*(page, lex, to=false, parent, context){
// XXX we can't get .raw from the page without going async...
//lex = lex
// ?? this.lex(page)
@ -421,14 +432,28 @@ module.BaseParser = {
: value.match ) }
continue }
// special case: .body argument -> group...
if((value.args ?? {}).body){
value.args.body =
[...this.group(
page,
value.args.body.iter(),
false,
parent,
value.name)] }
// assert nesting rules...
// NOTE: we only check for direct nesting...
// XXX might be a good idea to link nested block to the parent...
if(this.macros[value.name] instanceof Array
// stray nesting...
&& (context
&& !this.macros[value.name].includes(context))
// stray nesting/closing...
&& !this.macros[value.name].includes(to)
// do not complain about closing nestable tags...
&& !(value.name == to
&& value.type == 'closing')){
&& !(value.name == to
&& value.type == 'closing') ){
throw new Error(
'Unexpected <'+ value.name +'> macro'
+(to ?
@ -556,6 +581,12 @@ module.BaseParser = {
&& typeof(that.macros[name]) != 'string'){
continue }
// expand down...
body
&& (body = this.expand(page, body, state))
;(args ?? {}).body
&& (args.body = this.expand(page, args.body, state))
// cleanup...
elem = {...elem}
delete elem.error
@ -646,11 +677,13 @@ module.BaseParser = {
for(var elem of ast){
// nesting...
while(elem.value){
while(elem && elem.value){
elem = elem.value
// exec stage II macros...
if(typeof(elem) == 'function'){
elem = elem(state) } }
elem = elem(state) } }
if(elem == null){
continue }
// atomic values...
if(typeof(elem) != 'object'){
merge(elem)
@ -662,8 +695,12 @@ module.BaseParser = {
continue }
// expand ast...
if(elem instanceof Array){
//merge(...this.resolve(page, elem, state))
merge(...this.resolve(page, elem, state))
continue }
// expand .body attribute...
if((elem.attrs ?? {}).body instanceof Array){
elem.attrs.body = this.resolve(page, elem.attrs.body, state) }
// nested macro with no value set -- skip...
if(this.macros[elem.name] instanceof Array){
continue }
@ -697,7 +734,8 @@ module.BaseParser = {
if(ast.length > 1){
throw new Error('!!!!')
}
return ast[0] },
return ast[0]
?? '' },
// XXX this can't be used from within macros -- will deadlock the results...
// XXX how should this play with filters???
@ -714,7 +752,8 @@ module.BaseParser = {
if(ast.length > 1){
throw new Error('!!!!')
}
return ast[0] },
return ast[0]
?? '' },
@ -1247,6 +1286,10 @@ module.parser = {
// This also works for cases where slots override slots they
// are contained in, this will not lead to recursion.
//
// XXX do we actually need <content/>??
// <slot slot>
// <slot slot.content/>
// </slot>
// XXX do we show a slot with unfilled content???
// ...what's the point in having <content>, can't is just
// be replaced by a slot?
@ -1260,10 +1303,7 @@ module.parser = {
function(parser, args, body, state){
var name = args.name
var text = args.text
?? body
// NOTE: this can't be undefined for .expand(..) to work
// correctly...
?? []
return Promise.awaitOrRun(
parser.parseNested(this, name, state),
function(name){
@ -1281,19 +1321,28 @@ module.parser = {
: name in slots)
// set slot value...
// XXX simplify this...
var stack = []
slots[name]
&& stack.push(slots[name])
delete slots[name]
var slot = parser.expand(this, text, state)
text = text ?
parser.expand(this, text ?? [], state)
: text
if(body && text){
stack.push(body)
var slot = text
} else {
var slot = body ?? text }
var original = slot
slots[name]
&& stack.unshift(slot)
slot = slots[name] ??= slot
// handle <content/>...
for(prev of stack){
for(prev of stack.reverse()){
// get the first <content/>
// XXX this is a flat search, should be deep...
// NOTE: this is a flat search because we can't
// have indirect nesting (see: .group(..))
for(var i in prev){
if(typeof(prev[i]) != 'string'
&& prev[i].name == 'content'){

189
v3/pwiki/test/parser.js Executable file
View File

@ -0,0 +1,189 @@
#!/usr/bin/node
//---------------------------------------------------------------------
var test = require('ig-test')
var parser = require('../parser').parser
//---------------------------------------------------------------------
test.Setups({
empty: function(assert){
return [ '', '' ] },
slot_empty: function(assert){
return [
'@slot(slot)',
'@slot("slot")',
'@slot(\'slot\')',
'@slot(name=slot)',
'@slot(name="slot")',
'@slot(name=\'slot\')',
'<slot slot/>',
'<slot "slot"/>',
'<slot \'slot\'/>',
'<slot name=slot/>',
'<slot name="slot"/>',
'<slot name=\'slot\'/>',
'' ] },
slot_value: function(assert){
return [
'<slot slot value/>',
'<slot slot text=value/>',
'@slot(slot value)',
'@slot(slot text=value)',
'value' ] },
slot_fill: function(assert){
var ins = this.slot_value(assert).slice(0, -1)
return [
...ins.map(function(e){
return e + '@slot(slot other)' }),
...ins.map(function(e){
return e + '<slot slot other/>' }),
...ins.map(function(e){
return e + '<slot slot>other</slot>' }),
'other' ] },
slot_fill_fill: function(assert){
var ins = this.slot_fill(assert).slice(0, -1)
return [
...ins.map(function(e){
return e + '@slot(slot third)' }),
...ins.map(function(e){
return e + '<slot slot third/>' }),
...ins.map(function(e){
return e + '<slot slot>third</slot>' }),
'third' ] },
slot_content_empty: function(assert){
return [
'@slot(slot body="[[ <content/> ]]")',
'<slot slot>[[ <content/> ]]</slot>',
'[[ ]]' ] },
slot_content_default: function(assert){
return [
'@slot(slot default body="[[ <content/> ]]")',
'<slot slot default>[[ <content/> ]]</slot>',
'[[ default ]]' ] },
slot_content_fill: function(assert){
var ins = this.slot_content_default(assert)
var expect = ins.pop()
return [
...ins.map(function(i){
return i +'@slot(slot value)' }),
expect.replace('default', 'value') ] },
slot_nested: function(assert){
return [
'<slot slot>[[ <slot slot.content/> ]]</slot>@slot(slot.content value)',
'[[ value ]]' ] },
slot_nested_overwrite: function(assert){
return [
'<slot slot>[[ <slot slot.content/> ]]</slot>@slot(slot value)',
'value' ] },
// XXX the question with the next tow is:
// should body override or nest?
// ...both are logical but in either case the result should be
// consistent.
//
// XXX should the new body/content override or expand the original???
slot_content_content: function(assert){
var ins = this.slot_content_default(assert)
var expect = ins.pop()
return [
...ins.map(function(i){
return i +'@slot(slot body="(( @content() ))")' }),
// XXX should this override the default above???
//expect.replace('default', '(( default ))') ] },
expect.replace('default', '(( ))') ] },
slot_content_content_content: function(assert){
var ins = this.slot_content_content(assert)
var expect = ins.pop()
return [
...ins.map(function(i){
return i +'@slot(slot body="<< @content() >>")' }),
// XXX should this override the default above???
//expect.replace('default', '(( default ))') ] },
expect.replace('(( ))', '(( << >> ))') ] },
// XXX if we are expanding (see above) why are we overriding here???
slot_content_content_fill: function(assert){
var ins = this.slot_content_content(assert)
var expect = ins.pop()
return [
...ins.map(function(i){
return i +'@slot(slot value)' }),
expect.replace('(( ))', '(( value ))') ] },
//expect.replace('(( ))', 'value') ] },
//
// XXX these need to be revised...
// ...do we actually need hidden/shown???
//
slot_shown: function(assert){
var ins = this.slot_value(assert)
var expect = ins.pop()
return [
...ins.map(function(i){
return i +' @slot(slot that shown)' }),
'that that' ] },
slot_hidden: function(assert){
return [
'<slot slot value hidden/>',
'@slot(slot value hidden)',
'' ] },
slot_hidden_value: function(assert){
var ins = this.slot_hidden(assert)
var expect = ins.pop()
return [
...ins.map(function(i){
return i +'@slot(slot other)' }),
'' ] },
slot_hidden_shown: function(assert){
var ins = this.slot_hidden(assert)
var expect = ins.pop()
return [
...ins.map(function(i){
return i +'@slot(slot shown)' }),
'' ] },
})
test.Modifiers({
})
test.Tests({
parse: function(assert, state){
var res
var inputs = state.slice(0, -1)
var expect = state.at(-1)
var i = 0
for(var input of inputs){
assert(
(res = parser.parse(
{},
input,
{}))
=== expect,
'Parsing:',
'\n\t in: "'+ input +'"',
'\n\t out: "'+ res +'"',
'\n\texpected: "'+ expect +'"') } },
})
test.Cases({
})
//---------------------------------------------------------------------
// make the test runnable as a standalone script...
__filename == (require.main || {}).filename
&& test.run()
//---------------------------------------------------------------------
// vim:set ts=4 sw=4 :