tweaks and fixes...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2026-07-04 01:07:53 +03:00
parent 97fb6ee856
commit 7b0aab07da
3 changed files with 146 additions and 170 deletions

View File

@ -261,25 +261,80 @@ macro at the end of the page._
### include (src isolated text) ### include (src isolated recursive) / content
Include a page. The included page is rendered independently from current ```
page and is inserted as-is in macro body. @include(<src>)
@include(<src> [recursive=".."] [join=".."] [isolated])
@include(<src> [recursive=".."] [join=".."] [isolated="partial"])
Note that this will produce a `include` tag in the code that contains <include <src>>
the included page, this makes this tag not suitable for use anywhere ...
but an html element body. <content/>
...
</include>
```
Arguments: Include a page. The included page is rendered relative to the spurce page
- `src` -- path to source page. resolved from `<src>`, independently from current page, but in the current
- `isolated` -- prevent slots from included page from affecting the including page. namespace, and is inserted as-is.
- `text` -- is used when recursive include is detected and ignored otherwise.
_For examples see `slot` macro exaples below._ If body is not empty, it will render instead of included page content.
If body contains `<content/>` macro, it will be replaced with included
page content.
If more than one page matches `<src>`, the body will be copied once per
page and the page's content will be inserted into each respective `<content/>`
macro.
`join` is inserted between pages if more than one page is matched by `<src>`.
If `isolated` is given, prevent slots from included page from affecting
the including page.
**Example:**
`/some/page`:
```
<slot X value/>
```
Normal case: render included page but in view of current namespace:
```
<slot X original/><include src=/some/page> [ <content/> ] </include>
```
Will render to: `value [ ]`
Completely isolated:
```
<slot X original/><include src=/some/page isolated> [ <content/> ] </include>
```
Will render to: `original [ value ]`
Isolated up, i.e. the included page sees local state but can not affect it:
```
<slot X original/><include src=/some/page isolated=partial> [ <content/> ] </include>
```
Will render to: `original [ ]`
### source (src)
```
@source(<src>)
<source <src>>
...
<content/>
...
</source>
```
Load page source into the current page.
### source (src) / quote (src) ### quote (src)
Insert a page without rendering. This is similar to include but will not Insert a page without rendering. This is similar to include but will not
render the page. render the page.

View File

@ -443,7 +443,7 @@ module.BaseParser = {
// //
// NOTE: this internaly uses .macros to check for propper nesting // NOTE: this internaly uses .macros to check for propper nesting
//group: function*(page, lex, to=false){ //group: function*(page, lex, to=false){
group: function*(lex, to=false, parent, context){ group: function*(lex, to=false, context){
lex = typeof(lex) != 'object' ? lex = typeof(lex) != 'object' ?
this.lex(lex) this.lex(lex)
: lex : lex
@ -511,7 +511,6 @@ module.BaseParser = {
value.args.body value.args.body
?? value.args.text, ?? value.args.text,
false, false,
parent,
value.name)] } value.name)] }
delete value.args.body delete value.args.body
delete value.args.text delete value.args.text
@ -1446,7 +1445,7 @@ module.parser = {
return ((st ?? state).slots ?? {})[name] }, return ((st ?? state).slots ?? {})[name] },
{slot: name}) }) })), {slot: name}) }) })),
// XXX do not like this name... // XXX do not like this name...
content: ['slot', 'include'], content: ['slot', 'include', 'quote'],
// //
@ -1521,18 +1520,14 @@ module.parser = {
this.parseNested(page, src, state), this.parseNested(page, src, state),
function(src){ function(src){
// check for recursion... // check for recursion...
// XXX do we do this for pattern paths??? // XXX will this work for @source(..)???
// XXX this does not catch the /A/A/A/... recursion...
var stack = state.include_stack ??= [] var stack = state.include_stack ??= []
// XXX is this the right separator??? if(stack.includes(src)){
// ...need something that can't be in a path...
var base_src = base +'|'+ src
if(stack.includes(base_src)){
if(recursive){ if(recursive){
return that.expand(page, recursive, state) } return that.expand(page, recursive, state) }
throw new Error('Recursion:\n\t'+ throw new Error('Recursion:\n\t'+
[...stack, base_src].join('\n\t\t-> ')) } [...stack, src].join('\n\t\t-> ')) }
stack.push(base_src) stack.push(src)
var cache = state.cache ??= {} var cache = state.cache ??= {}
if(cache[src]){ if(cache[src]){
@ -1550,19 +1545,18 @@ module.parser = {
// content handler... // content handler...
handler ??= handler ??=
function(page, text, state){ function(page, src, text, state){
// XXX should this be the difference between page = page.get(src)
// @include(..) and @source(..)???
// i.e.
// .include(..) -> .resolve(..)
// .source(..) -> .expand(..)
return args.isolated ? return args.isolated ?
this.resolve(page, text, this.resolve(
args.isolated == 'partial' ? page,
// XXX text,
serialize.partialDeepCopy(state) Object.assign(
: {}) args.isolated == 'partial' ?
: this.expand(page, text, state)} serialize.partialDeepCopy(state)
: {},
{stack: state.stack ?? []}))
: this.expand(page, text, state) }
var pageHandler = var pageHandler =
function(text){ function(text){
@ -1573,18 +1567,18 @@ module.parser = {
that.expand(page, body, state, that.expand(page, body, state,
{ content: function(){ { content: function(){
content_handled = true content_handled = true
return text = handler.call(that, page, text, state) } }), return text = handler.call(that, page, src, text, state) } }),
function(text){ function(text){
// if no <content/> present we still // if no <content/> present we still
// need to handle the included page... // need to handle the included page...
content_handled content_handled
|| handler.call(that, page, text, state) || handler.call(that, page, src, text, state)
return text }) return text })
// place as-is... // place as-is...
: handler.call(that, page, text, state) } : handler.call(that, page, src, text, state) }
var resultHandler = var resultHandler =
function(pages){ function(pages){
state.include_stack.at(-1) == base_src state.include_stack.at(-1) == src
&& state.include_stack.pop() && state.include_stack.pop()
// cleanup... // cleanup...
if(state.include_stack.length == 0){ if(state.include_stack.length == 0){
@ -1629,25 +1623,51 @@ module.parser = {
var that = this var that = this
return this.macros['include'].call(this, return this.macros['include'].call(this,
page, args, body, state, page, args, body, state,
function(page, text, state){ function(page, src, text, state){
return that.expand(page, text, state) }) }), return that.expand(page, text, 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...
load: Macro( load: Macro(
['src'], ['src'],
function(args, body, state){ function(page, args, body, state){
var that = this var that = this
return Promise.awaitOrRun( return Promise.awaitOrRun(
this.macros['include'].call(this, page, args, body, state), this.macros['include'].call(this, page, args, body, state),
function(){ function(){
return '' }) }), return '' }) }),
// XXX a naive version, see ._quote(..) for reference... //
// @quote(<src>)
//
// <quote src=<src>[ filter="<filter> ..."]/>
//
// <quote text=" .. "[ filter="<filter> ..."]/>
//
// <quote[ filter="<filter> ..."]>
// ..
// </quote>
//
// This has two modes of operation, i.e. the body can be treated
// in two destinct ways:
// - if both src and body are given -- like @include(..) body is
// used as a wrapper for the quoted page
// - if only body is given -- body is inderted as is without
// any processing.
// XXX not sure I like this, this might change in the futore...
// one way to split the two is to split this into two macros,
// but I can't make the split logical/obvious...
//
// NOTE: src ant text arguments are mutually exclusive, src takes
// priority.
// NOTE: the filter argument has the same semantics as the filter
// macro with one exception, when used in quote, the body is
// not expanded...
// NOTE: the filter argument uses the same filters as @filter(..)
// XXX might be a good idea to do an auto-filter that would be // XXX might be a good idea to do an auto-filter that would be
// apropriately selected according to format -- md, html, ... // apropriately selected according to format -- md, html, ...
// XXX filter...
quote: Macro( quote: Macro(
['src', 'join', 'filter'], ['src', 'join', 'filter'],
quoting( quoting(
@ -1666,6 +1686,20 @@ module.parser = {
return Promise.awaitOrRun( return Promise.awaitOrRun(
text, text,
function(text){ function(text){
// XXX not sure I like that this has two "modes"...
text = src && body ?
// XXX do we need to account for generators???
(text instanceof Array ?
text
: [text])
.map(function(text){
return that.expand(
page,
that.ast(body.join(''), false, 'quote'),
state,
{ content: function(){
return text }, }) })
: text
return that.joinBlocks( return that.joinBlocks(
page, page,
that.filterBlocks( that.filterBlocks(
@ -1676,133 +1710,7 @@ module.parser = {
args.join, args.join,
state) }) }) })), state) }) }) })),
// /* XXX
// @quote(<src>)
//
// <quote src=<src>[ filter="<filter> ..."]/>
//
// <quote text=" .. "[ filter="<filter> ..."]/>
//
// <quote[ filter="<filter> ..."]>
// ..
// </quote>
//
//
// NOTE: src ant text arguments are mutually exclusive, src takes
// priority.
// NOTE: the filter argument has the same semantics as the filter
// macro with one exception, when used in quote, the body is
// not expanded...
// NOTE: the filter argument uses the same filters as @filter(..)
// NOTE: else argument implies strict mode...
// XXX need a way to escape macros -- i.e. include </quote> in a quoted text...
// XXX should join/else be sub-tags???
// XXX UPDATE...
_quote: Macro(
['src', 'filter', 'text', 'join', 'else',
['s', 'expandactions', 'strict']],
async function*(args, body, state){
var src = args.src //|| args[0]
var base = this.get(this.path.split(/\*/).shift())
var text = args.text
?? body
?? []
var strict = !!(args.strict
?? args['else']
?? false)
// parse arg values...
src = src ?
await base.parse(src, state)
: src
// XXX INHERIT_ARGS special-case: inherit args by default...
if(this.actions_inherit_args
&& this.actions_inherit_args.has(pwpath.basename(src))
&& this.get(pwpath.dirname(src)).path == this.path){
src += ':$ARGS' }
var expandactions =
args.expandactions
?? true
// XXX EXPERIMENTAL
var strquotes = args.s
var depends = state.depends =
state.depends
?? new Set()
// XXX DEPENDS_PATTERN
depends.add(src)
var pages = src ?
(!expandactions
&& await this.get(src).type == 'action' ?
base.get(this.QUOTE_ACTION_PAGE)
: await this.get(src).asPages(strict))
: text instanceof Array ?
[text.join('')]
: typeof(text) == 'string' ?
[text]
: text
// else...
pages = ((!pages
|| pages.length == 0)
&& args['else']) ?
[await base.parse(args['else'], state)]
: pages
// empty...
if(!pages || pages.length == 0){
return }
var join = args.join
&& await base.parse(args.join, state)
var first = true
for await (var page of pages){
if(join && !first){
yield join }
first = false
text = typeof(page) == 'string' ?
page
: (!expandactions
&& await page.type == 'action') ?
base.get(this.QUOTE_ACTION_PAGE).raw
: await page.raw
text = strquotes ?
text
.replace(/["']/g, function(c){
return '%'+ c.charCodeAt().toString(16) })
: text
page.path
&& depends.add(page.path)
var filters =
args.filter
&& args.filter
.trim()
.split(/\s+/g)
// NOTE: we are delaying .quote_filters handling here to
// make their semantics the same as general filters...
// ...and since we are internally calling .filter(..)
// macro we need to dance around it's architecture too...
// NOTE: since the body of quote(..) only has filters applied
// to it doing the first stage of .filter(..) as late
// as the second stage here will have no ill effect...
// NOTE: this uses the same filters as @filter(..)
// NOTE: the function wrapper here isolates text in
// a closure per function...
yield (function(text){
return async function(state){
// add global quote-filters...
filters =
(state.quote_filters
&& !(filters ?? []).includes(this.ISOLATED_FILTERS)) ?
[...state.quote_filters, ...(filters ?? [])]
: filters
return filters ?
await this.__parser__.callMacro(
this, 'filter', filters, text, state, false)
.call(this, state)
: text } })(text) } }),
// very similar to @filter(..) but will affect @quote(..) filters... // very similar to @filter(..) but will affect @quote(..) filters...
'quote-filter': function(args, body, state){ 'quote-filter': function(args, body, state){
var filters = state.quote_filters = var filters = state.quote_filters =

View File

@ -27,6 +27,19 @@ module.exports.PAGES = {
'/async/recursive/SelfOther': Promise.resolve('<< @include(/async/recursive/OtherSelf) >>'), '/async/recursive/SelfOther': Promise.resolve('<< @include(/async/recursive/OtherSelf) >>'),
'/multi/page': [ 'A', 'B', 'C' ], '/multi/page': [ 'A', 'B', 'C' ],
'/async/multi/page': Promise.resolve([ 'A', 'B', 'C' ]),
// XXX is this actually possible ???
'/multi/async/page': [
Promise.resolve('A'),
Promise.resolve('B'),
Promise.resolve('C'),
],
// XXX is this actually possible ???
'/multi/async/multi/page': Promise.resolve([
Promise.resolve('A'),
Promise.resolve('B'),
Promise.resolve('C'),
]),
} }
var P = var P =