pWiki/pwiki/page.js

2107 lines
58 KiB
JavaScript
Raw Normal View History

/**********************************************************************
*
*
*
**********************************************************************/
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
// XXX
//var object = require('lib/object')
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){
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='.', strict=this.strict){
if(path === true || path === false){
strict = path
path = '.' }
return this.store[name](
pwpath.relative(this.path, path),
strict) }
Object.defineProperty(func, 'name', {value: name})
return func }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX FUNC need to handle functions in store...
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,
actions: new Set([
'location',
'referrer',
'path',
'name',
'dir',
'title',
'resolved',
'rootpath',
'length',
'type',
'ctime',
'mtime',
]),
// 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 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 .onNavigate(..) method...
__location: undefined,
get location(){
return this.__location ?? '/' },
set location(path){
// trigger the event...
this.onNavigate(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.Event('beforeNavigate',
function(_, location){
'__beforenavigate__' in this
&& this.__beforenavigate__(location) }),
onNavigate: types.event.Event('navigate',
function(handle, location){
this.onBeforeNavigate(location)
this.referrer = this.location
var {path, args} = pwpath.splitArgs(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),
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 },
// 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 these be writable???
get name(){
return pwpath.basename(this.path) },
//set name(value){ },
get dir(){
return pwpath.dirname(this.path) },
//set dir(value){ },
/*/ XXX TITLE...
// NOTE: .__title is intentionally not persistent...
__title: undefined,
get title(){
return this.__title
?? this.path },
set title(value){
this.__title = value
this.__update__({title: 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.has(this.name)
&& !!this[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.has(this.name)){
var name = this.name
var page = this.get('..', {args: this.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)
: function(){
return res } })
: await page[name]
return typeof(res) == 'function' ?
res.bind(page)
: 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)
/*/ XXX TITLE...
// load the title if set...
res.title
?? (this.__title = res.title)
//*/
return typeof(res) == 'function' ?
//res.bind(this)
res.bind(this.get('..', {args: this.args}))
: res }).call(this) },
set data(value){
this.__update__(value) },
// 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.path)
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: function(path='.'){
this.__delete__(path)
return this },
// XXX should these be implemented here or proxy to .store???
copy: async function(to){
this.get(to).data = await this.data
this.path = to
return this },
move: async function(to){
var from = this.path
await this.copy(to)
this.delete(from)
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>[, <strict>])
// -> path
// -> undefined
//
find: function(path='.', strict=false){
if(path === true || path === false){
strict = path
path = '.' }
return this.store.find(
pwpath.relative(this.path, path), strict) },
//
// .get(<path>[, <data>])
// .get(<path>, <strict>[, <data>])
// -> <page>
//
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.path
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...
//
// XXX should this be page-level (current) store level???
// XXX when this is async, should this return a promise????
sort: async function(cmp){
// not sorting single pages...
if(this.length <= 1){
return this }
// sort...
this.metadata =
{ order: await this.each()
.sort(...arguments)
.map(function(p){
return p.path }) }
return this },
reverse: async function(){
// not sorting single pages...
if(this.length <= 1){
return this }
this.metadata = { order: (await this.match()).reverse() }
return this },
//
// Clone a page optionally asigning data into it...
// .clone()
// .clone({ .. }[, <clone-history>])
// -> <page>
//
// Fully clone a page optionally asigning data into it...
// .clone(true[, <clone-history>])
// .clone(true, { .. }[, <clone-history>])
// -> <page>
//
//
// Normal cloning will inherit all the "clones" from the original
// page overloading .location and .referrer
//
// NOTE: <clone-history> 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: 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 should these be something more generic like Object.assign(..) ???
// 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 }
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// XXX filters (and macros?) should be features for simpler plugin handlng (???)
// XXX STUB filters...
// XXX rename to pWikiPage???
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:
// {
// <path>: Set([<path>, ...]),
// }
//
// 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 },
//
// <filter>(<source>)
// -> <result>
// -> 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;') },
// XXX one way to do this in a stable manner is to wrap the source
// in something like <span wikiwords=yes> .. </span> 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 `<pre>${source}</pre>` },
},
//
// <macro>(<args>, <body>, <state>){ .. }
// -> undefined
// -> <text>
// -> <array>
// -> <iterator>
// -> <func>(<state>)
// -> ...
//
// XXX ASYNC make these support async page getters...
macros: {
//
// @(<name>[ <default>][ local])
// @(name=<name>[ default=<default>][ local])
//
// @arg(<name>[ <default>][ local])
// @arg(name=<name>[ default=<value>][ local])
//
// <arg <name>[ <default>][ local]/>
// <arg name=<name>[ default=<value>][ local]/>
//
// Resolution order:
// - local
// - .renderer
// - .root
//
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 }),
'': Macro(
['name', 'default', ['local']],
function(args){
return this.macros.arg.call(this, args) }),
// XXX do we need this???
args: function(){
console.log('!!!!')
return pwpath.obj2args(this.args) },
//
// @filter(<filter-spec>)
// <filter <filter-spec>/>
//
// <filter <filter-spec>>
// ...
// </filter>
//
// <filter-spec> ::=
// <filter> <filter-spec>
// | -<filter> <filter-spec>
//
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){
// 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(<path>)
//
// @include(<path> isolated recursive=<text>)
// @include(src=<path> isolated recursive=<text>)
//
// <include src=<path> .. >
// <text>
// </include>
//
// 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.
include: Macro(
['src', 'recursive', 'join', ['strict', 'nonstrict', '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
&& await base.parse(args.src, state)
if(!src){
return }
var recursive = args.recursive ?? body
var isolated = args.isolated
var strict = args.strict
&& !args.nonstrict
var join = args.join
&& await base.parse(args.join, state)
var depends = state.depends =
state.depends
?? new Set()
handler = handler
?? async function(src, state){
return isolated ?
{data: await this.get(src)
.parse({
seen: state.seen,
depends,
})}
: this.get(src)
.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
// handle recursion...
var parent_seen = 'seen' in state
var seen = state.seen =
new Set(state.seen ?? [])
// recursion detected...
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)
// 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...
source: Macro(
['src'],
async function*(args, body, state){
yield* this.macros.include.call(this,
'source',
args, body, state, 'sources',
async function(src, state){
return this.parse(this.get(src).raw, state) }) }),
//
// @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(..)
//
// XXX need a way to escape macros -- i.e. include </quote> 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
var expandactions = args.expandactions
var depends = state.depends =
state.depends
?? new Set()
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)) },
//
// <slot name=<name>/>
//
// <slot name=<name> text=<text>/>
//
// <slot name=<name>>
// ...
// </slot>
//
// Force show a slot...
// <slot shown ... />
//
// Force hide a slot...
// <slot hidden ... />
//
// Insert previous slot content...
// <content/>
//
//
// NOTE: by default only the first slot with <name> is visible,
// all other slots with <name> will replace its content, unless
// explicit shown/hidden arguments are given.
// NOTE: hidden has precedence over shown if both are given.
// NOTE: slots are handled in order of occurrence of opening tags
// in text and not by hierarchy, i.e. the later slot overrides
// the former and the most nested overrides the parent.
// This also works for cases where slots override slots they
// are contained in, this will not lead to recursion.
//
// XXX revise the use of hidden/shown use mechanic and if it's
// needed...
slot: Macro(
['name', 'text', ['shown', 'hidden']],
async function(args, body, state){
var name = args.name
var text = args.text
?? body
// NOTE: this can't be undefined for .expand(..) to work
// correctly...
?? []
var slots = state.slots =
state.slots
?? {}
// parse arg values...
name = name ?
await this.parse(name, state)
: name
//var hidden = name in slots
// XXX EXPERIMENTAL
var hidden =
// 'hidden' has priority...
args.hidden
// explicitly show... ()
|| (args.shown ?
false
// show first instance...
: name in slots)
// set slot value...
var stack = []
slots[name]
&& stack.push(slots[name])
delete slots[name]
var slot = await this.__parser__.expand(this, text, state)
slots[name]
&& stack.unshift(slot)
slot = slots[name] =
slots[name]
?? slot
// handle <content/>...
for(prev of stack){
// get the first <content/>
for(var i in slot){
if(typeof(slot[i]) != 'string'
&& slot[i].name == 'content'){
break }
i = null }
i != null
&& slot.splice(i, 1,
...prev
// remove nested slot handlers...
.filter(function(e){
return typeof(e) != 'function'
|| e.slot != name }) ) }
return hidden ?
''
: Object.assign(
function(state){
return state.slots[name] },
{slot: name}) }),
'content': ['slot'],
//
// <macro src=<url>> .. </macro>
//
// <macro name=<name> src=<url> sort=<sort-spec>> .. </macro>
//
// <macro ...> ... </macro>
// <macro ... text=<text>/>
//
// <macro ... else=<text>> ... </macro>
// <macro ...>
// ...
//
//
// <join>
// ...
// </join>
//
// <else>
// ...
// </else>
// </macro>
//
// NOTE: if both strict and nonstrict are given the later takes
// precedence.
//
// XXX SORT sorting not implemented yet....
macro: Macro(
['name', 'src', 'sort', 'text', 'join', 'else', ['strict', 'nonstrict']],
async function*(args, body, state){
var that = this
var name = args.name //?? args[0]
var src = args.src
var base = this.get(this.path.split(/\*/).shift())
var sort = (args.sort ?? '')
.split(/\s+/g)
.filter(function(e){
return e != '' })
// NOTE: args.text will need parsing...
var text = args.text
?? body
?? []
text = typeof(text) == 'string' ?
[...this.__parser__.group(this, text+'</macro>', 'macro')]
: text
var strict = args.strict
&& !args.nonstrict
var join
var depends = state.depends =
state.depends
?? new Set()
var _getBlock = function(name){
var block = args[name] ?
[{
args: {},
body: args[name],
}]
: (text ?? [])
.filter(function(e){
return typeof(e) != 'string'
&& e.name == name })
if(block.length == 0){
return }
// NOTE: when multiple blocks are present the
// last one is used...
block = block.pop()
block =
block.args.text
?? block.body
return block }
if(name){
name = await base.parse(name, state)
// define new named macro...
if(text.length != 0){
// NOTE: we do not need to worry about saving
// stateful text here because it is only
// grouped and not expanded...
;(state.macros = state.macros ?? {})[name] = [text, _getBlock('join')]
// use existing macro...
} else if(state.macros
&& name in state.macros){
[text, join] = state.macros[name] } }
if(src){
join = _getBlock('join')
?? join
join = join
&& await base.parse(join, state)
var match = this.get(await base.parse(src, state))
// NOTE: thie does not introduce a dependency on each
// of the iterated pages, that is handled by the
// respective include/source/.. macros, this however
// only depends on page count...
depends.add(match.path)
// expand matches...
var first = true
for await(var page of match.asPages(strict)){
if(join && !first){
yield join }
first = false
yield this.__parser__.expand(page, text, state) }
// else...
if(first
&& (text || args['else'])){
var else_block = _getBlock('else')
if(else_block){
yield this.__parser__.expand(this, else_block, state) } } } }),
// nesting rules...
'else': ['macro'],
'join': ['macro'],
},
// direct actions...
//
// These are evaluated directly without the need to go through the
// whole page acquisition process...
//
// NOTE: these can not be overloaded.
// (XXX should this be so?)
// XXX should this be an object???
actions: new Set([
...module.BasePage.prototype.actions,
'!',
// XXX DEBUG -- remove these...
'testDirect',
'testDirect!',
]),
'!': Object.assign(
function(){
return this.get('.', {energetic: true}).raw },
{energetic: true}),
// XXX DEBUG -- remove these...
testDirect: function(){
console.log('testDirect:', this.location,
this.args,
(this.renderer ?? {}).args,
(this.root ?? {}).args)
//console.log(' :', this, this.renderer)
return this.location },
'testDirect!': Object.assign(
function(){
console.log('testDirect!:', this.location,
this.args,
(this.renderer ?? {}).args,
(this.root ?? {}).args)
//console.log(' :', this, this.renderer)
return this.location },
{energetic: true}),
// events...
//
// NOTE: textUpdate event will not get triggered if text is updated
// directly via .data or .__update__(..)
/*/ XXX EVENTS do we need this???
onTextUpdate: types.event.Event('textUpdate',
function(handle, text){
this.__update__({text}) }),
// XXX EVENTS not sure where to trigger this???
// ...on .parse(..) is a bit too granular, something like .text??
// XXX not triggered yet...
//onParse: types.event.Event('parse'),
//*/
// page parser...
//
parse: async function(text, state){
var that = this
text = await text
// .parser(<state>)
if(arguments.length == 1
&& text instanceof Object
&& !(text instanceof Array)){
state = text
text = null }
state = state ?? {}
return this.__parser__.parse(this, text, state) },
// true if page has an array value but is not a pattern page...
//
// XXX the split into pattern and array pages feels a bit overcomplicated...
// ...can we merge the two and simplify things???
// XXX EXPERIMENTAL
get isArray(){ return (async function(){
return !this.isPattern
// NOTE: we can't only use .data here as it can be a function
// that will return an array...
&& await this.raw instanceof Array }).call(this) },
// raw page text...
//
// NOTE: writing to .raw is the same as writing to .text...
// NOTE: when matching multiple pages this will return a list...
//
// XXX revise how we handle .strict mode...
get raw(){ return (async function(){
var data = await this.data
// no data...
// NOTE: if we hit this it means that nothing was resolved,
// not even the System/NotFound page, i.e. something
// went really wrong...
// NOTE: in .strict mode this will explicitly fail and not try
// to recover...
if(data == null){
if(!this.strict
&& this.NOT_FOUND_ERROR){
var msg = this.get(this.NOT_FOUND_ERROR)
if(await msg.match()){
return msg.raw } }
// last resort...
throw new Error('NOT FOUND ERROR: '+ this.path) }
// get the data...
return (
// action...
typeof(data) == 'function' ?
data()
// multiple matches...
: data instanceof Array ?
Promise.all(data
.map(function(d){
return typeof(d) == 'function'?
d()
: d.text })
.flat())
: data.text )}).call(this) },
set raw(value){
this.__update__({text: value}) },
//this.onTextUpdate(value) },
// iterate matches or content list as pages...
//
// .asPages()
// .asPages(<path>[, <options>])
// .asPages(<strict>[, <options>])
// .asPages(<path>, <strict>[, <options>])
// .asPages(<options>)
// -> <iter>
//
// NOTE: this will get .raw for non-pattern pages this it can trigger
// actions...
//
// XXX revise name...
asPages: async function*(path='.', strict=false, noexpandactions=false){
// options...
var args = [...arguments]
var opts = typeof(args.at(-1)) == 'object' ?
args.pop()
: {}
var {path, strict, noexpandactions} = {
...opts,
path: typeof(args[0]) == 'string' ?
args.shift()
: '.',
strict: args.shift()
?? false,
}
var page = this.get(path, strict)
// handle lists in pages (actions, ... etc.)...
if(!page.isPattern){
if(noexpandactions
&& await page.type == 'action'){
//yield this.get(this.QUOTE_ACTION_PAGE)
yield page
return }
var raw = await page.raw
if(raw == null){
return }
yield* raw instanceof Array ?
raw
.map(function(p){
return page.virtual({text: p}) })
: [page]
// each...
} else {
yield* page } },
// expanded page text...
//
// NOTE: this uses .PAGE_TEMPLATE to render the page.
// NOTE: writing to .raw is the same as writing to .text...
//
// XXX revise how we handle strict mode...
//
// NOTE: .__debug_last_render_state is mainly exposed for introspection
// and debugging, set comment it out to disable...
//__debug_last_render_state: undefined,
get text(){ return (async function(){
// strict mode -- break on non-existing pages...
if(this.strict
&& !await this.resolve(true)){
throw new Error('NOT FOUND ERROR: '+ this.location) }
var path = pwpath.split(this.path)
path.at(-1)[0] == '_'
|| path.push(this.PAGE_TEMPLATE)
var tpl = pwpath.join(path)
var tpl_name = path.pop()
//path = pwpath.joinArgs(path, this.args)
// get the template relative to the top most pattern...
tpl = await this.get(tpl).find(true)
if(!tpl){
console.warn('UNKNOWN RENDER TEMPLATE: '+ tpl_name)
return this.get(this.NOT_FOUND_TEMPLATE_ERROR).parse() }
// render template in context of page...
var depends = this.depends = new Set([tpl])
var state = {depends}
// this is here for debugging and introspection...
'__debug_last_render_state' in this
&& (this.__debug_last_render_state = state)
//var data = { renderer: this }
var data = {
renderer: this,
args: this.args,
}
return this.get(path, data)
.parse(
this.get('/'+tpl, data).raw,
state) }).call(this) },
set text(value){
this.__update__({text: value}) },
//this.onTextUpdate(value) },
// pass on .renderer to clones...
clone: function(data={}, ...args){
this.renderer
&& (data = {renderer: this.renderer, ...data})
return object.parentCall(Page.prototype.clone, this, data, ...args) },
})
//---------------------------------------------------------------------
var getCachedProp =
function(obj, name){
var that = obj
var value = obj.cache ?
obj.cache[name]
: object.parentProperty(CachedPage.prototype, name).get.call(obj)
value instanceof Promise
&& value.then(function(value){
that.cache = {[name]: value} })
return value }
var setCachedProp =
function(obj, name, value){
return object.parentProperty(CachedPage.prototype, name).set.call(obj, value) }
// XXX this is good enough on the front-side to think about making the
// cache persistent with a very large timeout (if set at all), but
// we are not tracking changes on the store-side...
var CachedPage =
module.CachedPage =
object.Constructor('CachedPage', Page, {
// Sets what to use for cache id...
//
// Can be:
// 'location' (default)
// 'path'
cache_id: 'location',
// NOTE: set this to null/undefined/0 to disable...
cache_timeout: '20m',
// keep all the cache in one place -- .root
//__cachestore: undefined,
get cachestore(){
return (this.root ?? this).__cachestore },
set cachestore(value){
;(this.root ?? this).__cachestore = value },
get cache(){
this.checkCache(this[this.cache_id])
return ((this.cachestore ?? {})[this[this.cache_id]] ?? {}).value },
// XXX check * paths for matches...
set cache(value){
if(this.cachestore === false
|| this.cache == value){
return }
var id = this[this.cache_id ?? 'location']
// clear...
if(value == null){
delete (this.cachestore ?? {})[id]
// set...
} else {
var prev = ((this.cachestore = this.cachestore ?? {})[id] ?? {}).value ?? {}
;(this.cachestore = this.cachestore ?? {})[id] = {
created: Date.now(),
// XXX
valid: undefined,
value: {
...prev,
...value,
},
} }
// clear depended pages from cache...
for(var [key, deps] of Object.entries(this.dependencies)){
// XXX also check pattern paths...
// ...the problem here is that it's getting probabilistic,
// i.e. if we match * as a single path item then we might
// miss creating a subtree (ex: /tree), while matching
// /* to anything will give us lots of false positives...
if(key != id && deps.has(id)){
delete this.cachestore[key] } } },
checkCache: function(...paths){
if(!this.cache_timeout || !this.cachestore){
return this }
paths = paths.length == 0 ?
Object.keys(this.cachestore)
: paths
for(var path of paths){
var {created, valid, value} = this.cachestore[path] ?? {}
if(value){
var now = Date.now()
valid = valid
?? Date.str2ms(this.cache_timeout)
// drop cache...
if(now > created + valid){
//console.log('CACHE: DROP:', this.path)
delete this.cachestore[path] } } }
return this },
clearCache: function(...paths){
if(this.cachestore){
if(arguments.length == 0){
this.cachestore = null
} else {
for(var path of paths){
delete this.cachestore[path] } } }
return this },
__update__: function(){
this.cache = null
return object.parentCall(CachedPage.prototype.__update__, this, ...arguments) },
__delete__: function(){
this.cache = null
return object.parentCall(CachedPage.prototype.__delete__, this, ...arguments) },
/* XXX do we need to cache .raw???
// ...yes this makes things marginally faster but essentially
// copies the db into memory...
get raw(){
return getCachedProp(this, 'raw') },
set raw(value){
return setCachedProp(this, 'raw', value) },
//*/
get text(){
return getCachedProp(this, 'text') },
set text(value){
return setCachedProp(this, 'text', value) },
})
//---------------------------------------------------------------------
var wikiword = require('./dom/wikiword')
var pWikiPageElement =
module.pWikiPageElement =
// XXX CACHE...
object.Constructor('pWikiPageElement', CachedPage, {
/*/
object.Constructor('pWikiPageElement', Page, {
//*/
dom: undefined,
domFilters: {
// XXX see Page.filters.wikiword for notes...
wikiword: wikiword.wikiWordText,
},
// XXX CACHE
__clone_constructor__: CachedPage,
/*/
__clone_constructor__: Page,
//*/
__clone_proto: undefined,
get __clone_proto__(){
return (this.__clone_proto = this.__clone_proto
?? this.__clone_constructor__('/', '/', this.store)) },
set __clone_proto__(value){
this.__clone_proto = value },
actions: new Set([
...CachedPage.prototype.actions,
'hash'
]),
// NOTE: setting location will reset .hash set it directly via either
// one of:
// .location = [path, hash]
// .location = 'path#hash'
hash: undefined,
// NOTE: getting .location will not return the hash, so as not to force
// the user to parse it out each time.
get location(){
return object.parentProperty(pWikiPageElement.prototype, 'location')
.get.call(this) },
set location(value){
var [value, hash] =
// .location = [path, hash]
value instanceof Array ?
value
// .location = '<path>#<hash>'
: value.includes('#') ?
value.split('#')
// no hash is given...
: [value, undefined]
this.hash = hash
object.parentProperty(pWikiPageElement.prototype, 'location')
.set.call(this, value) },
/*/ XXX this is not persistent, is this what we want???
// XXX should this default to .path or to .name???
get title(){
return this.dom.getAttribute('title')
|| (this.dom.querySelector('h1') || {}).innerText
|| this.path
|| object.parentProperty(pWikiPageElement.prototype, 'title')
.get.call(this) },
set title(value){
this.dom.setAttribute('title', value)
object.parentProperty(pWikiPageElement.prototype, 'title')
.set.call(this, value) },
//*/
// events...
//
__pWikiLoadedDOMEvent: new Event('pwikiloaded'),
onLoad: types.event.Event('onLoad', function(){
this.dom.dispatchEvent(this.__pWikiLoadedDOMEvent) }),
// XXX CACHE...
__last_refresh_location: undefined,
refresh: async function(full=false){
// drop cache if re-refreshing or when full refresh requested...
// XXX CACHE...
;(full
|| this.__last_refresh_location == this.location)
&& this.cache
&& (this.cache = null)
this.__last_refresh_location = this.location
var dom = this.dom
dom.innerHTML = await this.text
for(var filter of Object.values(this.domFilters)){
filter
&& filter.call(this, dom) }
this.onLoad()
return this },
// handle dom as first argument...
__init__: function(dom, ...args){
if(typeof(Element) != 'undefined'){
if(dom instanceof Element){
this.dom = dom
} else {
args.unshift(dom) } }
return object.parentCall(pWikiPageElement.prototype.__init__, this, ...args) },
})
//---------------------------------------------------------------------
// System pages/actions...
// XXX move this to a more appropriate module...
var System =
module.System = {
// base templates...
//
// These are used to control how a page is rendered.
//
// pWiki has to have a template appended to any path, if one is not
// given then "_view" is used internally.
//
// A template is rendered in the context of the parent page, e.g.
// for /path/to/page, the actual rendered template is /path/to/page/_view
// and it is rendered from /path/to/page.
//
// A template is any page named starting with an underscore ("_")
// thus it is not recommended to use underscores to start page names.
//
// The actual default template is controlled via <page>.PAGE_TEMPLATE
//
// Example:
// _list: {
// text: '<macro src="." join="\n">- @source(.)</macro>' },
//
// XXX all of these should support pattern pages...
_text: {
text: '@include(.:$ARGS isolated join="@source(file-separator)")' },
// XXX /rootpath here is not relative -- makes reuse harder...
_view: {
text: object.doc`
<slot name="header">
<a href="#/list">&#9776</a>
@source(./path/!)
<a href="#@source(./path/!)/_edit">&#9998;</a>
</slot>
<hr>
<slot name="content"></slot>
<hr>
<slot name="footer"></slot>
<!-- fill slots defaults -->
<slot name="content" hidden>
@include(.:$ARGS join="@source(file-separator)" recursive="")
</slot>` },
// XXX add join...
_raw: {
text: '@quote(.)' },
// XXX not sure if this is the right way to go...
_code: {
text:
'<macro src="." join="@source(file-separator)">'
+'<pre wikiwords="no"><quote filter="quote-tags" src="."/></pre>'
+'</macro>'},
/* XXX can we reuse _view here???
_edit: {
text:
'@include(PageTemplate)'
+'<slot name="header">@source(./path)</slot>'
+'<slot name="content">'
+'<macro src="." join="@source(file-separator)">'
+'<pre class="editor" '
+'wikiwords="no" '
+'contenteditable '
+'oninput="saveContent(\'@source(./path)\', this.innerText)">'
+'<quote filter="quote-tags" src="."/>'
+'</pre>'
+'</macro>'
+'</slot>'},
/*/
_edit: {
text:
'@source(./path)'
+'<hr>'
+'<macro src="." join="@source(file-separator)">'
+'<h1 '
+'contenteditable '
// XXX need to make this savable...
+'oninput="saveContent(\'@source(./path)/name\')">'
+'@source(./name)'
+'</h1>'
+'<pre class="editor" '
+'wikiwords="no" '
+'contenteditable '
+'oninput="saveContent(\'@source(./path)\', this.innerText)">'
+'<quote filter="quote-tags" src="."/>'
+'</pre>'
+'</macro>'},
//*/
edit: {
text: object.doc`
@include(_view)
<slot name="header">@source(../path)</slot>
<slot name="content">
<macro src=".." join="@source(file-separator)">
<h1 class="title-editor"
contenteditable
oninput="saveContent(\'@source(./path)/name\')">
@source(./name)
</h1>
<pre class="editor"
wikiwords="no"
contenteditable
oninput="saveContent(\'@source(./path)\', this.innerText)"
><quote filter="quote-tags" src="."/></pre>
</macro>
</slot>`},
// XXX debug...
_path: {text: '@source(./path join=" ")'},
list: {
text: object.doc`
<slot name="header">
<a href="#/list">&#9776</a>
<a href="#@source(../../path)/list">&#x21D1;</a>
@source(../path)
</slot>
<macro src="../*" join="@source(line-separator)">
<a href="#@source(./path)">@source(./name)</a>
<sup>
<macro src="./isAction">
a
<else>
<macro src="./isStore">s</macro>
</else>
</macro>
</sup>
(<a href="#@source(./path)/list">@include(./*/length/!)</a>)
&nbsp;
<a href="#@source(./path)/delete">&times;</a>
</macro>` },
// XXX this is really slow...
tree: {
text: object.doc`
<macro src="../*">
<div>
<div class="item">
<a href="#@source(./path)">@source(./name)</a>
<a class="show-on-hover" href="#@source(./path)/info">&#128712;</a>
<a class="show-on-hover" href="#@source(./path)/delete">&times;</a>
</div>
<div style="padding-left: 30px">
@source(./tree)
</div>
</div>
</macro>` },
all: {
text: `@include(../**/path join="@source(line-separator)")`},
info: {
text: object.doc`
Path: @source(../path)
(<a href="#@source(../path)/edit">edit</a>)<br>
Resolved path: @source(../resolved)
(<a href="#@source(../resolved)/edit">edit</a>)<br>
Referrer: @source(../referrer)
(<a href="#@source(../referrer)/edit">edit</a>)<br>
type: @source(../type)<br>
ctime: @source(../ctime)<br>
mtime: @source(../mtime)<br>
<hr>
<pre wikiwords="no"><quote filter="quote-tags" src=".."/></pre> ` },
// XXX need to also be able to list things about each store...
stores: function(){
return Object.keys(this.store.substores ?? {}) },
// page parts...
//
'line-separator': { text: '<br>' },
'file-separator': { text: '<hr>' },
// base system pages...
//
// NOTE: these are last resort pages, preferably overloaded in /Templates.
//
RecursionError: {
text: 'RECURSION ERROR: @source(../path)' },
NotFoundError: {
text: 'NOT FOUND ERROR: @source(./path)' },
NotFoundTemplateError: {
text: 'NOT FOUND TEMPLATE ERROR: @source(../path)' },
DeletingPage: {
text: 'Deleting: @source(../path)' },
PageTemplate: {
text: object.doc`
<slot name="header">@source(./path)/_edit</slot>
<hr>
<slot name="content"></slot>
<hr>
<slot name="footer"></slot> ` },
QuoteActionPage: {
text: '[ native code ]' },
// page actions...
//
/* XXX broken...
// XXX this does not work as energetic...
// XXX for some reason this is called twice...
time: async function(){
var t = Date.now()
var text = await this.get('../_text').text
var time = Date.now() - t
console.log('RENDER TIME:', time)
return object.doc`
Time to render: ${time}ms <br>
<ht>
${text}`},
//*/
// XXX EXPERIMENTAL -- page types...
isAction: async function(){
return await this.get('..').type == 'action' ?
'action'
: undefined },
isStore: async function(){
return await this.get('..').type == 'store' ?
'store'
: undefined },
// XXX DEBUG -- remove these...
testPage: {
text: object.doc`<pre>
location: @source(./location)
path: @source(./path)
args: <args/>
</pre>`},
testAction: function(){
console.log('testAction:', this.location,
this.args,
(this.renderer ?? {}).args,
(this.root ?? {}).args)
//console.log(' :', this, this.renderer)
return this.location },
'testAction!': Object.assign(
function(){
console.log('testAction!:', this.location,
this.args,
(this.renderer ?? {}).args,
(this.root ?? {}).args)
//console.log(' :', this, this.renderer)
return this.location },
{energetic: true}),
// actions...
//
delete: function(){
var target = this.get('..')
console.log('DELETE:', target.path)
target.delete()
// redirect...
this.renderer
&& (this.renderer.location = this.referrer)
// show info about the delete operation...
return target.get('DeletingPage/_text').text },
// XXX copy/move/...
// XXX do we need this as a page action???
move: function(){
var from = this.get('..')
// XXX this is ugly...
// ...need to standardize how we get arguments when rendering....
var to = this.args.to
|| (this.renderer || {args:{}}).args.to
console.log('MOVE:', from.path, to)
// XXX
if(to){
// XXX move...
}
// redirect...
this.renderer
&& (this.renderer.location = this.referrer)
// XXX if we return undefined here this will not fully redirect,
// keeping the move page open but setting the url to the
// redirected page...
return '' },
// XXX System/back
// XXX System/forward
// XXX System/sort
// XXX System/reverse
}
var Test =
module.Test = {
'list/action': function(){
return [...'abcdef'] },
'list/statuc': {
text: [...'abcdef'] },
slots: {
text: object.doc`
Sequential:
<slot name="sequential">unfilled</slot>
<slot name="sequential">filled</slot>
<slot name="sequential">refilled</slot>
<br><br>
Nested:
<slot name="nested">
unfilled
<slot name="nested">
filled
<slot name="nested">
refilled
</slot>
</slot>
</slot>
<br><br>
Content: A B C:
<slot name="slot-content">A</slot>
<slot name="slot-content"><content/> B</slot>
<slot name="slot-conten"><content/> C</slot>
<br><br>
Nested content: A B C:
<slot name="nested-slot-content">
A
<slot name="nested-slot-content">
<content/> B
<slot name="nested-slot-content">
<content/> C
</slot>
</slot>
</slot>
<br><br>
Mixed content: X A B C Z:
<slot name="mixed-slot-content">
X
</slot>
<slot name="mixed-slot-content">
<content/> A
<slot name="mixed-slot-content">
<content/> B
</slot>
<slot name="mixed-slot-content">
<content/> C
</slot>
</slot>
<slot name="mixed-slot-content">
<content/> Z
</slot> ` },
macros: {
text: object.doc`
<macro name="list" join="<br>">
- @include(./path)
</macro>
<macro name="list" src="/Test/*"/>
<br><br>
<macro name="list" src="/Test/*" join=",<br>"/>
`},
Subtree: {
text: object.doc`
This is here to test the performance of macros:<br>
./list <br>
./tree <br>
./**/path <br> ` },
}
// Generate pages...
PAGES=100
for(var i=0; i<PAGES; i++){
Test['Subtree/Page'+i] = {text: 'page: '+i} }
var Settings =
module.Settings = {
Export: {
text: '<button onclick="exportData()">Export</button>' },
// XXX
Config: {
text: '{}' },
}
/**********************************************************************
* vim:set ts=4 sw=4 nowrap : */ return module })