/********************************************************************** * * * **********************************************************************/ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ var object = require('ig-object') var types = require('ig-types') var pwpath = require('./path') var parser = require('./parser') var filters = require('./filters/base') var markdown = require('./filters/markdown') //--------------------------------------------------------------------- // Page... var relProxy = function(name){ var func = function(path='.:$ARGS', ...args){ path = this.resolvePathVars(path) return this.store[name]( pwpath.relative(this.path, path), ...args) } Object.defineProperty(func, 'name', {value: name}) return func } var relMatchProxy = function(name){ var func = function(path='.:$ARGS', strict=this.strict){ if(path === true || path === false){ strict = path path = '.:$ARGS' } path = this.resolvePathVars(path) return this.store[name]( pwpath.relative(this.path, path), strict) } Object.defineProperty(func, 'name', {value: name}) return func } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var BasePage = module.BasePage = object.Constructor('BasePage', { // root page used to clone new instances via the .clone(..) method... //root: undefined, // a base page to be used as a base for cloning if root is of a // different "class"... //__clone_proto__: undefined, // // Format: // { // : true, // : , // } // actions: { location: true, referrer: true, path: true, name: true, dir: true, // alias... args: 'argstr', title: true, resolved: true, rootpath: true, length: true, type: true, ctime: true, mtime: true, // XXX //tags: true, }, // These actions will be default get :$ARGS appended if no args are // explicitly given... // XXX INHERIT_ARGS actions_inherit_args: new Set([ 'location', 'args', ]), // NOTE: this can be inherited... //store: undefined, //__store: undefined, get store(){ return this.__store ?? (this.root ?? {}).__store }, set store(value){ this.__store = value }, // Path variables... // // XXX PATH_VARS should these be here??? // other places path variables can be resolved: // - navigation (below) // - macro expansion... path_vars: { NOW: function(){ return Date.timeStamp() }, PATH: function(){ return this.path }, NAME: function(){ return this.name }, DIR: function(){ return this.dir }, ARGS: function(){ return pwpath.obj2args(this.args) }, TITLE: function(){ return this.title }, /*/ XXX this needs: // - macro context... // - sort order... INDEX: function(context){ return context.index }, //*/ }, resolvePathVars: function(path='', context={}){ var that = this return path == '.' ? path : pwpath.normalize( Object.entries(this.path_vars) .reduce(function(res, [key, func]){ return res .replace( new RegExp('(\\${'+key+'}|\\$'+key+')', 'g'), func.call(that, context)) }, path)) }, // page location... // // NOTE: path variables are resolved relative to the page BEFORE // navigation... // NOTE: the actual work is done by the .navigate(..) method... __location: undefined, get location(){ return this.__location ?? '/' }, set location(path){ // trigger the event... this.navigate(path) }, // referrer -- a previous page location... referrer: undefined, // events... // //__beforenavigate__: function(location){ .. }, // //__navigate__: function(){ .. }, // // XXX revise naming... // XXX should this be able to prevent navigation??? onBeforeNavigate: types.event.PureEvent('beforeNavigate', function(_, location){ '__beforenavigate__' in this && this.__beforenavigate__(location) }), navigate: types.event.Event('navigate', function(handle, location){ var {path, args} = pwpath.splitArgs(location) this.trigger("onBeforeNavigate", location) this.referrer = this.location var cur = this.__location = this.resolvePathVars( // NOTE: this is done instead of simply assigning // location as-is to normalize the paths and // arguments... pwpath.joinArgs( pwpath.relative( this.path, path) // keep root path predictable... || '/', pwpath.obj2args(args))) // trigger handlers... '__navigate__' in this && this.__navigate__() handle() }), get path(){ return pwpath.splitArgs(this.location).path }, set path(value){ this.location = value }, get args(){ return pwpath.splitArgs(this.location).args }, set args(args){ args = pwpath.obj2args(args) ?? '' this.location = args == '' ? '.' : '.:'+ args }, // helper... get argstr(){ return pwpath.obj2args(this.args) }, set argstr(value){ this.args = value }, // NOTE: these are mostly here as helpers to be accessed via page // actions... // XXX should these be here or in Page??? // XXX should this call .match(..) or .resolve(..)??? get resolved(){ return this.resolve() }, get rootpath(){ return this.root ? this.root.path : this.path }, // XXX should this encode/decode??? get name(){ return pwpath.basename(this.path) }, set name(value){ if(pwpath.normalize(value) == ''){ return } this.move( /^[\\\/]/.test(value) ? value : '../'+value) }, get dir(){ return pwpath.dirname(this.path) }, set dir(value){ var to = pwpath.join(value, this.name) this.move( /^[\\\/]/.test(to) ? to : '../'+to) }, // XXX TITLE / EXPERIMENTAL... get title(){ return pwpath.decodeElem(this.name) }, set title(value){ this.name = pwpath.encodeElem(value) }, get isPattern(){ return this.path.includes('*') }, get ctime(){ return async function(){ var t = ((await this.data) ?? {}).ctime return t ? new Date(t).getTimeStamp() : t }.call(this) }, get mtime(){ return async function(){ var t = ((await this.data) ?? {}).mtime return t ? new Date(t).getTimeStamp() : t }.call(this) }, // store interface... // // XXX we are only doing modifiers here... // ...these ar mainly used to disable writing in .ro(..) __update__: function(data){ return this.store.update(this.path, data) }, __delete__: function(path='.'){ return this.store.delete(pwpath.relative(this.path, path)) }, __energetic: undefined, get energetic(){ return async function(){ return this.__energetic === true || ((this.actions && this.actions[this.name] && !!this[ this.actions[this.name] === true ? this.name : this.actions[this.name] ].energetic) || !!await this.store.isEnergetic(this.path)) }.call(this) }, set energetic(value){ this.__energetic = value }, // page data... // strict: undefined, get data(){ return (async function(){ // direct actions... if(this.actions && this.actions[this.name]){ var name = this.actions[this.name] === true ? this.name : this.actions[this.name] var args = this.args var page = this.get('..', {args}) var res = (this.isPattern && !this.__energetic && !page[name].energetic) ? await page .map(function(page){ var res = page[name] return typeof(res) == 'function' ? res.bind(page.get(name, {args})) : function(){ return res } }) : await page[name] return typeof(res) == 'function' ? res.bind(this) : res instanceof Array ? res : function(){ return res } } var that = this // NOTE: we need to make sure each page gets the chance to handle // its context (i.e. bind action to page).... if(this.isPattern // XXX ENERGETIC... && !await this.energetic){ return this .map(function(page){ return page.data }) } // single page... // XXX ENERGETIC... var res = await this.store.get(this.path, !!this.strict, !!await this.energetic) return typeof(res) == 'function' ? res.bind(this) : res }).call(this) }, set data(value){ if(this.actions && this.actions[this.name]){ var name = this.actions[this.name] === true ? this.name : this.actions[this.name] var page = this.get('..') // NOTE: this can return a promise, as we'll need to assign // we do not care about it as long as it's not a function... // XXX not sure if this is a good idea... var res = page[name] // set... typeof(res) == 'function' ? page[name](value.text ?? value) : (page[name] = value.text ?? value) // normal update... } else { this.__update__(value) } }, // tags... // /* get tags(){ return async function(){ return (await this.data).tags ?? [] }.call(this) }, /*/ get tags(){ var tags = this.store.tags var path = pwpath.sanitize(this.path) return tags instanceof Promise ? tags.then(function(tags){ return tags.paths[path] ?? [] }) : this.store.tags.paths[path] ?? [] }, //*/ set tags(value){ return async function(){ this.data = { ...(await this.data), tags: [...value], } }.call(this) }, tag: async function(...tags){ this.tags = [...new Set([ ...(await this.tags), ...tags, ])] return this }, untag: async function(...tags){ this.tags = (await this.tags) .filter(function(tag){ return !tags.includes(tag) }) return this }, toggleTags: async function(...tags){ var t = new Set(await this.tags) for(var tag of tags){ t.has(tag) ? t.delete(tag) : t.add(tag) } this.tags = t return this }, // metadata... // // NOTE: in the general case this is the same as .data but in also allows // storing of data (metadata) for pattern paths... get metadata(){ return this.store.metadata(this.path) }, set metadata(value){ this.__update__(value) }, get type(){ return async function(){ return this.store.isStore(this.path) ? 'store' : typeof(await this.data) == 'function' ? 'action' : 'page' }.bind(this)() }, // number of matching pages... // NOTE: this can be both sync and async... get length(){ var p = this.resolve(this.location) return p instanceof Array ? p.length : p instanceof Promise ? p.then(function(res){ return res instanceof Array ? res.length : 1 }) : 1 }, // relative proxies to store... exists: relProxy('exists'), //* XXX MATCH match: relMatchProxy('match'), /*/ match: async function(path='.', strict=false){ if(path === true || path === false){ strict = path path = '.' } path = pwpath.relative(this.path, path) var res = await this.store.match(path, strict) return res.length == 0 ? // XXX are we going outside of match semantics here??? this.store.find(path) : res }, //*/ resolve: relMatchProxy('resolve'), delete: types.event.Event('delete', async function(handle, path='.', base=true){ handle(false) if(path === true || path === false){ base = path path = '.' } var page = this.get(path) if(page.isPattern){ base && this.__delete__(this.path.split('*')[0]) for(var p of await this.get('path').raw){ this.__delete__(p) } } else { this.__delete__(path) } handle() return this }), // XXX should these be implemented here or proxy to .store??? // XXX do we sanity check to no not contain '*'??? copy: async function(to, base=true){ if(this.get(to).path == this.path){ return this } // copy children... if(this.isPattern){ var base = this.path.split('*')[0] // copy the base... base && (this.get(to).data = await this.get(base).data) for(var from of await this.get('path').raw){ this.get(pwpath.join(to, from.slice(base.length))).data = await this.get(from).data } // copy self... } else { this.get(to).data = await this.data } // change location... this.path = to return this }, move: async function(to, base=true){ var from = this.path if(this.get(to).path == this.path){ return this } await this.copy(to, base) this.delete(from, base) return this }, // // Find current path (non-strict) // .find() // .find(false) // .find('.') // .find('.', false) // -> path // -> undefined // // Find current path in strict/non-strict mode... // .find(true) // .find(false) // -> path // -> undefined // // Find path relative to current page (strict/non-strict) // .find([, ]) // -> path // -> undefined // // XXX ARGS preserve args... find: function(path='.', strict=false){ if(path === true || path === false){ strict = path path = '.' } return this.store.find( pwpath.relative(this.path, path), strict) }, // // .get([, ]) // .get(, [, ]) // -> // get: function(path, strict, data={}){ if(strict instanceof Object){ data = strict strict = undefined } return this.clone({ location: path, ...data, referrer: data.referrer //?? this.path, ?? this.referrer, strict, }) }, // XXX should this be an iterator??? each: async function*(path){ // NOTE: we are trying to avoid resolving non-pattern paths unless // we really have to... path = path ? pwpath.relative(this.path, path) : this.location var paths = path.includes('*') // XXX ENERGETIC... && !(await this.energetic // XXX test if energetic action... || await this.store.isEnergetic(path)) ? this.resolve(path) : path paths = paths instanceof Array ? paths : paths instanceof Promise ? await paths : [paths] /*/ XXX MATCH paths = paths.length == 0 ? [await this.find(path)] : paths //*/ for(var path of paths){ yield this.get('/'+ path) } }, [Symbol.asyncIterator]: async function*(){ yield* this.each() }, map: async function(func){ return this.each().map(func) }, filter: async function(func){ return this.each().filter(func) }, reduce: async function(func, dfl){ return this.each().reduce(func, dfl) }, // sorting... // sort: async function(...cmp){ // normalize to path... this.metadata = { order: await this.store.sort(this.path, ...cmp) } return this }, reverse: async function(){ // not sorting single pages... if(this.length <= 1){ return this } this.sort('reverse') return this }, // // Clone a page optionally asigning data into it... // .clone() // .clone({ .. }[, ]) // -> // // Fully clone a page optionally asigning data into it... // .clone(true[, ]) // .clone(true, { .. }[, ]) // -> // // // Normal cloning will inherit all the "clones" from the original // page overloading .location and .referrer // // NOTE: by default is false unless fully cloning // clone: function(data={}, history=false){ var [data, ...args] = [...arguments] var full = data === true history = typeof(args[args.length-1]) == 'boolean' ? args.pop() : full data = full ? args[0] ?? {} : data var src = this.__clone_proto__ ?? (this.root || {}).__clone_proto__ ?? this.root ?? this return Object.assign( full ? // full copy... // XXX src or this??? //this.constructor(this.path, this.referrer, this.store) src.constructor(this.path, this.referrer, this.store) // NOTE: this will restrict all the clones to the first // generation maintaining the original (.root) page as // the common root... // this will make all the non-shadowed attrs set on the // root visible to all sub-pages. : Object.create(src), // XXX //{...this}, { root: this.root ?? this, location: this.location, referrer: this.referrer, }, data) }, // Create a read-only page... // // NOTE: all pages that are created via a read-only page are also // read-only. // XXX EXPERIMENTAL... ro: function(data={}){ return Object.assign({ __proto__: this, __update__: function(){ return this }, __delete__: function(){ return this }, }, data) }, // Create a virtual page at current path... // // Virtual pages do not affect store data in any way but behave like // normal pages. // // NOTE: .get(..) / .clone(..) will return normal non-virtual pages // unless the target path is the same as the virtual page .path... // NOTE: changing .path/.location is not supported. // XXX EXPERIMENTAL... virtual: function(data={}){ var that = this return { __proto__: this, // make the location read-only... get location(){ // NOTE: since we are not providing this as a basis for // inheritance we do not need to properly access // the parent prop... // ...otherwise use: // object.parentProperty(..) return this.__proto__.location }, __update__: function(data){ Object.assign(this.data, data) return this }, __delete__: function(){ return this }, // NOTE: we need to proxy .clone(..) back to parent so as to // avoid overloading .data in the children too... // NOTE: we are also keeping all first level queries resolving // to current path also virtual... clone: function(...args){ var res = that.clone(...args) return res.path == this.path ? that.virtual(this.data) : res }, data: Object.assign( { ctime: Date.now(), mtime: Date.now(), }, data), } }, // XXX should this be update or assign??? // XXX how should this work on multiple pages... // ...right now this will write what-ever is given, even if it // will never be explicitly be accessible... // XXX sync/async??? update: types.event.Event('update', function(_, ...data){ return Object.assign(this, ...data) }), // XXX should this take an options/dict argument???? __init__: function(path, referrer, store){ if(referrer && typeof(referrer) != 'string'){ store = referrer referrer = undefined } // NOTE: this will allow inheriting .store from the prototype if(store){ this.store = store } this.location = path this.referrer = referrer }, }) // pepper in event functionality... types.event.EventMixin(BasePage.prototype) //--------------------------------------------------------------------- // XXX do we need anything else like .doc, attrs??? var Macro = module.Macro = function(spec, func){ var args = [...arguments] // function... func = args.pop() // arg sepc... ;(args.length > 0 && args[args.length-1] instanceof Array) && (func.arg_spec = args.pop()) // XXX do we need anything else like .doc, attrs??? return func } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var Page = module.Page = object.Constructor('Page', BasePage, { __parser__: parser.parser, NESTING_DEPTH_LIMIT: 20, NESTING_RECURSION_TEST_THRESHOLD: 50, // Filter that will isolate the page/include/.. from parent filters... ISOLATED_FILTERS: 'isolated', // list of macros that will get raw text of their content... QUOTING_MACROS: ['quote'], // templates used to render a page via .text PAGE_TEMPLATE: '_view', // NOTE: comment this out to make the system fail when nothing is // resolved, not even the System/NotFound page... // NOTE: we can't use any of the page actions here (like @source(./path)) // as if we reach this it's likely all the bootstrap is either also // not present or broken. // NOTE: to force the system to fail set this to undefined. NOT_FOUND_ERROR: 'NotFoundError', RECURSION_ERROR: 'RecursionError', NOT_FOUND_TEMPLATE_ERROR: 'NotFoundTemplateError', QUOTE_ACTION_PAGE: 'QuoteActionPage', // Format: // { // : Set([, ...]), // } // // NOTE: this is stored in .root... //__dependencies: undefined, get dependencies(){ return (this.root ?? this).__dependencies ?? {} }, set dependencies(value){ ((this.root ?? this).__dependencies) = value }, // NOTE: for this to populate .text must be done at least once... get depends(){ return (this.dependencies ?? {})[this.path] }, set depends(value){ if(value == null){ delete (this.dependencies ?? {})[this.path] } else { ;(this.dependencies = this.dependencies ?? {})[this.path] = value } }, // The page that started the current render... // // This is set by .text and maintained by .clone(..). // // NOTE: for manual rendering (.parse(..), ... etc.) this has to be // setup manually. //renderer: undefined, get renderer(){ return this.__render_root ?? this }, set renderer(value){ this.__render_root = value }, // // () // -> // -> undefined // // XXX might be a good idea to fix filter order... filters: { // placeholders... nofilters: function(){}, isolated: function(){}, // XXX TESTING... dummy: function(){}, test: function(source){ return source .replace(/test/g, 'TEST') }, 'quote-tags': function(source){ return source .replace(/&/g, '&') .replace(//g, '>') }, // XXX one way to do this in a stable manner is to wrap the source // in something like .. and only // process those removing the wrapper in dom... // ...not sure how to handle -wikiword filter calls -- now // this is entirely handled by the parser without calling this... wikiword: function(){}, 'quote-wikiword': function(){}, markdown: markdown.markdown, 'quote-markdown': markdown.quoteMarkdown, text: function(source){ return `
${source}
` }, }, // // (, , ){ .. } // -> undefined // -> // -> // -> // -> () // -> ... // // XXX do we need to make .macro.__proto__ module level object??? // XXX ASYNC make these support async page getters... macros: { __proto__: { // // @([ ][ local]) // @(name=[ default=][ local]) // // @arg([ ][ local]) // @arg(name=[ default=][ local]) // // [ ][ local]/> // [ default=][ local]/> // // Resolution order: // - local // - .renderer // - .root // // NOTE: default value is parsed when accessed... arg: Macro( ['name', 'default', ['local']], function(args){ var v = this.args[args.name] || (!args.local && (this.renderer && this.renderer.args[args.name]) || (this.root && this.root.args[args.name])) v = v === true ? args.name : v return v || (args.default && this.parse(args.default)) }), '': Macro( ['name', 'default', ['local']], function(args){ return this.macros.arg.call(this, args) }), args: function(){ return pwpath.obj2args(this.args) }, // // @filter() // /> // // > // ... // // // ::= // // | - // // XXX REVISE... filter: function(args, body, state, expand=true){ var that = this var outer = state.filters = state.filters ?? [] var local = Object.keys(args) // trigger quote-filter... var quote = local .map(function(filter){ return (that.filters[filter] ?? {})['quote'] ?? [] }) .flat() quote.length > 0 && this.macros['quote-filter'] .call(this, Object.fromEntries(Object.entries(quote)), null, state) // local filters... if(body != null){ // expand the body... var ast = expand ? this.__parser__.expand(this, body, state) : body instanceof Array ? body // NOTE: wrapping the body in an array effectively // escapes it from parsing... : [body] return async function(state){ // XXX can we lose stuff from state this way??? // ...at this stage it should more or less be static -- check! var res = await this.__parser__.parse(this, ast, { ...state, filters: local.includes(this.ISOLATED_FILTERS) ? local : [...outer, ...local], }) return {data: res} } // global filters... } else { state.filters = [...outer, ...local] } }, // // @include() // // @include( isolated recursive=) // @include(src= isolated recursive=) // // .. > // // // // NOTE: there can be two ways of recursion in pWiki: // - flat recursion // /A -> /A -> /A -> .. // - nested recursion // /A -> /A/A -> /A/A/A -> .. // Both can be either direct (type I) or indirect (type II). // The former is trivial to check for while the later is // not quite so, as we can have different contexts at // different paths that would lead to different resulting // renders. // At the moment nested recursion is checked in a fast but // not 100% correct manner focusing on path depth and ignoring // the context, this potentially can lead to false positives. // XXX need a way to make encode option transparent... include: Macro( ['src', 'recursive', 'join', ['s', 'strict', 'isolated']], async function*(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 // this renders the src in the context of current page while // include is rendered in the context of its page but with // the same state... // i.e. for @include(PATH) the paths within the included page // are resolved relative to PATH while for @source(PATH) // relative to the page containing the @source(..) statement... source: Macro( // XXX should this have the same args as include??? ['src', 'recursive', 'join', ['s', 'strict']], //['src'], async function*(args, body, state){ var that = this yield* this.macros.include.call(this, 'source', args, body, state, 'sources', 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... // // NOTE: this is essentially the same as @source(..) but returns ''. // XXX revise name... load: Macro( ['src', ['strict']], async function*(args, body, state){ var that = this yield* this.macros.include.call(this, 'load', args, body, state, 'sources', async function(src, state){ await that.parse(this.raw, state) return '' }) }), // // @quote() // // [ filter=" ..."]/> // // // // // .. // // // // 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 need a way to escape macros -- i.e. include in a quoted text... quote: Macro( ['src', 'filter', 'text', 'join', ['expandactions']], async function*(args, body, state){ var src = args.src //|| args[0] var base = this.get(this.path.split(/\*/).shift()) var text = args.text ?? body ?? [] // 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 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) : this.get(src).asPages()) : text instanceof Array ? [text.join('')] : typeof(text) == 'string' ? [text] : text // empty... if(!pages){ 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 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... 'quote-filter': function(args, body, state){ var filters = state.quote_filters = state.quote_filters ?? [] filters.splice(filters.length, 0, ...Object.keys(args)) }, // // /> // // text=/> // // > // ... // // // Force show a slot... // // // Force hide a slot... //