diff --git a/v3/Makefile b/v3/Makefile new file mode 100755 index 0000000..1d8f71c --- /dev/null +++ b/v3/Makefile @@ -0,0 +1,52 @@ + + + +BOOTSTRAP_FILES := \ + $(wildcard bootstrap/*) \ + $(wildcard bootstrap/*/*) \ + README.md + +LOCAL_MODULES := \ + node_modules/ig-doc/doc.js \ + node_modules/ig-stoppable/stoppable.js \ + node_modules/ig-object/object.js \ + node_modules/ig-actions/actions.js \ + node_modules/ig-features/features.js + +EXT_MODULES := \ + $(wildcard node_modules/pouchdb/dist/*) \ + $(wildcard node_modules/jszip/dist/*) \ + $(wildcard node_modules/idb-keyval/dist/*.js) \ + $(wildcard node_modules/showdown/dist/*) + +POUCH_DB := \ + $(wildcard node_modules/pouchdb/dist/*) + + + +lib/types: node_modules + mkdir -p $@ + cp node_modules/ig-types/*js $@ + + +bootstrap.js: scripts/bootstrap.js $(BOOTSTRAP_FILES) + node $< + + +.PHONY: bootstrap +bootstrap: bootstrap.js + + +node_modules: + npm install + + +dev: node_modules lib/types $(EXT_MODULES) $(LOCAL_MODULES) bootstrap + cp $(LOCAL_MODULES) lib/ + cp $(EXT_MODULES) ext-lib/ + + +clean: + rm -f bootstrap.js + + diff --git a/v3/bootstrap.js b/v3/bootstrap.js new file mode 100644 index 0000000..3dcedad --- /dev/null +++ b/v3/bootstrap.js @@ -0,0 +1,6 @@ +// This file is generated automatically, all changes made here will be lost. + +var Bootstrap = {"TestTodo":{"text":"[Templates/outline/_edit]\r\n\r\n@include(./todo)\r\n"},"Templates":{"text":"

\r\nXXX Genereal template description...\r\n

\r\n\r\n\r\n\t
\r\n\t

\r\n\t\t@source(./path)\r\n\t

\r\n\t

\r\n\t\t

@quote(./raw)
\r\n\t

\r\n
\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"},"Doc":{"text":"@include(./About)\r\n"},"TestTodo/04":{"text":"item 4\n"},"TestTodo/03":{"text":"item 3\n"},"TestTodo/02":{"text":"item 2\n"},"TestTodo/01":{"text":"item 1\n"},"Test/slot":{"text":"\r\n\r\n\r\n"},"Templates/tree":{"text":"
\r\n\t\r\n\t\t
\r\n\t\t\t \r\n\t\t\t@source(./title)\r\n\t\t\t\r\n\t\t\t×\r\n\t\t
\r\n\t\t
\r\n\t\t\t\r\n\t\t
\r\n\t
\r\n
\r\n\r\n\r\n"},"Templates/todo":{"text":"\r\n\t\r\n\r\n\r\n\r\n\r\n\r\n"},"Templates/pages":{"text":"\r\n\t
\r\n\t\t[@source(./path)]\r\n\t\t\r\n\t\t×\r\n\t
\r\n\t\r\n\t\tNo pages...\r\n\t\r\n
\r\n\r\n\r\n"},"Templates/outline":{"text":"\r\n\r\n\r\n\t\r\n\r\n\r\n\r\n\t×\r\n\r\n\r\n\r\n
\r\n\t\r\n\t\t+\r\n\t\r\n
\r\n
\r\n\t\r\n\t\t
\r\n\t\t\t
\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t
\r\n\t\t\t
\r\n\t\t\t\t\r\n\t\t\t
\r\n\t\t
\r\n\t
\r\n
\r\n\r\n\r\n"},"Templates/macros":{"text":"\r\n\t×\r\n\r\n\r\n\r\n\r\n"},"Templates/all_pages":{"text":"\r\n\t
\r\n\t\t[@source(./path)]\r\n\t\t\r\n\t\t×\r\n\t
\r\n\t\r\n\t\tNo pages...\r\n\t\r\n
\r\n\r\n\r\n"},"Templates/_view":{"text":"\r\n@include(./style/_css)\r\n\r\n
\r\n\t \r\n\r\n\t[@source(../path)]\r\n\r\n\t (edit) \r\n\r\n\t\r\n\r\n\t+\r\n
\r\n\r\n
\r\n\t

\r\n\t\t@source(../title)\r\n\t

\r\n
\r\n\r\n
\r\n\t\r\n\t\t\r\n\t\r\n
\r\n\r\n
\r\nhome\r\n\r\n\r\n"},"Templates/_todo":{"text":"\r\n\r\n@source(../title)\r\n\r\n\r\n\t@include(../todo)\r\n\r\n\r\n\r\n"},"Templates/_raw":{"text":"@source(..)\r\n"},"Templates/_outline":{"text":"\r\n\r\n@source(../title)\r\n\r\n\r\n\t@include(../outline)\r\n\r\n\r\n\r\n"},"Templates/_edit":{"text":"\r\n\r\n(view)\r\n\r\n@source(../title)\r\n\r\n\r\n\t
\r\n
\r\n\r\n\r\n\r\n"},"Templates/_css":{"text":"\r\n"},"Templates/EmptyToDo":{"text":"@include(./todo)\r\n"},"Templates/EmptyPage":{"text":"\r\n\r\nPage @include(./path) is empty.

\r\n\r\nLinks to this page:
\r\n@include(./links)

\r\n\r\n\r\n"},"Templates/EmptyOutline":{"text":"@include(./outline)\r\n"},"System/style":{"text":"body {\r\n\tfont-family: /*worksans,*/ opensans, sans-serif;\r\n\r\n}\r\n\r\n.title img {\r\n\tvertical-align: middle;\r\n}\r\n\r\nh1, h2, h3 {\r\n\tborder-bottom: solid 1px rgba(0, 0, 0, 0.1);\r\n\tpadding-bottom: 5px;\r\n}\r\nh2, h3 {\r\n\tborder-bottom: solid 1px rgba(0, 0, 0, 0.05);\r\n}\r\n\r\n\r\n/* tables */\r\ntable {\r\n\twidth: 100%;\r\n}\r\ntable, td, th {\r\n\tborder-bottom: solid 1px gray;\r\n\tborder-collapse: collapse;\r\n}\r\ntd, th {\r\n\ttext-align: left;\r\n\tpadding: 5px;\r\n}\r\ntr:hover {\r\n\tbackground-color: rgba(0, 0, 0, 0.05);\r\n}\r\n\r\n\r\n.raw,\r\n.text {\r\n\tdisplay: block;\r\n}\r\n\r\n.item.checked {\r\n\topacity: 0.3;\r\n}\r\n.item.checked:hover {\r\n\topacity: 0.8;\r\n}\r\n.item.checked .item-content * {\r\n\ttext-decoration: line-through;\r\n}\r\n\r\n.button {\r\n\ttext-decoration: none;\r\n}\r\n.button:last-child {\r\n\tmargin-right: 5px;\r\n}\r\n\r\n.separator~* {\r\n\tfloat: right;\r\n}\r\n\r\npre {\r\n\tdisplay: block;\r\n\tbackground-color: rgba(0, 0, 0, 0.05);\r\n\tpadding: 10px;\r\n\tpadding-bottom: 15px;\r\n\r\n\t-moz-tab-size: 4;\r\n\t-o-tab-size: 4;\r\n\ttab-size: 4;\r\n}\r\n\r\n.item:hover {\r\n\tbackground-color: rgba(0, 0, 0, 0.05);\r\n}\r\n.item .button {\r\n\tdisplay: none;\r\n}\r\n.item:hover .button {\r\n\tdisplay: inline-block;\r\n}\r\n\r\n.sort-handle {\r\n\topacity: 0.1;\r\n\tpadding-left: 5px;\r\n\tpadding-right: 5px;\r\n\tcursor: pointer;\r\n\ttext-decoration: none;\r\n}\r\n.item:hover .sort-handle {\r\n\topacity: 0.3;\r\n}\r\n.sort-placeholder {\r\n\tdisplay: block;\r\n}\r\n\r\n/* @filter(-wikiword) */\r\n/* @filter(text) */\r\n/* vim:set ts=4 sw=4 ft=css : */\r\n"},"Doc/Templates":{"text":"XXX Document the template structure here XXX\r\n"},"Doc/Path":{"text":"# ![pWiki](img/pWiki-i.jpg) pWiki Path\r\n\r\nA Wiki is a set of _pages_, each uniquely defined by it's _title_. Titles \r\nare traditionally formatted as WikiWords. pWiki closely follows this \r\nculture, while not restricting either page title formatting nor page \r\nnesting (nested paths), though in the general case following the Wiki \r\nstyle is recommended.\r\n\r\n\r\n\r\n## Basic terminology\r\n\r\n**Path** \r\n_One or more strings (or parts) separated by \"/\" that identifies a view._\r\n\r\nWe call the last _part_ in a path sequence a _title_.\r\n\r\nWe call the sub-path without the _title_ a _basedir_ or simply _dir_.\r\n\r\nIn pWiki, there is no distinction between a page and a _directory_, thus\r\nwe do not use the later term, instead, we may use the term _sub-page_.\r\n\r\nPaths are case sensitive.\r\n\r\n\r\n**Page** \r\n_A set of data associated with a path._\r\n\r\nA page is identified by it's path, but this does not require every\r\nsub-path of that path to exist -- the full path is the identifier.\r\n\r\nNot every path _identifies_ a page, but every path _resolves_ to a page.\r\n\r\nSome pages are _bootstrapped_, i.e. are predefined in pWiki, these pages\r\ncan be overridden but can not be removed. This is done to make it simple \r\nto revert to the default state if something goes wrong.\r\n\r\n\r\n**View** \r\n_A path that resolves to a page that may or may not be at (identified by) \r\nthat specific path._\r\n\r\nA _view's_ path may match that of a specific page or may not match any\r\npage directly, but any view will resolve to a page via the _acquisition \r\nprocess_\r\n\r\nAny page is a view, every view resolves to a page, but not every view \r\nis a page.\r\n\r\n(see: _Page acquisiton_ below)\r\n\r\n\r\n**\\WikiWord** \r\n_A WikiWork is a specially formated string that is treated as a link in\r\npage text_\r\n\r\nIn pWiki a simple WikiWord is any string starting with a capital letter,\r\nmust contain at least and one more capital letter and can consist of \r\nletters, numbers, underscores (WikiWord itself is a good example)\r\n\r\nA WikiWord path (_WikiPath_) is a set of path parts separated by '/' the\r\nfirst part must start with a capital letter and the rest can contain \r\nletters, numbers and/or underscores (example: Path/to/somepage).\r\n\r\n_Note that this is not actually a part of the path specification but it \r\nis part of the Wiki culture and a convenient way to automatically link \r\nto pages (handled by pWiki macros when rendering pages)._\r\n\r\n\r\n\r\n**Special path characters** \r\nTitles of _user pages_ must not contain the leading underscore `_` \r\ncharacter, such paths are used internally.\r\n\r\n\r\n\r\n## Page acquisition\r\n\r\npWiki path system differs from how traditional file system paths are \r\nhandled. In pWiki if a path does not reference a page directly (i.e. \r\nit's a _view_), a search is conducted to find an alternative page. This \r\nsearch is called _page acquisition_.\r\n\r\n**Acquisition process:** \r\n_A set of rules defining how a page is retrieved via a path._\r\n\r\n\r\nThis is used as a simple and uniform mechanism to:\r\n- Get default pages for specific situations \r\n Like [Templates/EmptyPage] to handle the _page not found_ condition.\r\n- Define generic templates/pages accessible by multiple pages in path \r\n A good example would be the viewer used to show this page [Templates/\\_view]\r\n and all of it's _chrome_ like the path in the header and links in the \r\n footer (seen: when viewing through pWiki)\r\n- Overload default templates/pages\r\n\r\n\r\n### The acquisition order/rules:\r\n\r\n1. if _path_ matches a specific page, target _page_ is found \r\n1. if _path_ does not match a page:\r\n 1. if _title_ matches a page in the parent _path_, _page_ is found\r\n 1. repeat until we either have a match or reach root (empty _basedir_)\r\n1. if no match is found, check if title exists in [Templates] in _basedir_\r\n1. if no match is found, check if title exists in [/System]\r\n1. if no match is found, repeat process for `EmptyPage` instead of _title_\r\n\r\n\r\n**Example:** \r\n\r\nFor path `Path/To/Page` the following paths are checked in order \r\nand the first matching page is returned:\r\n\r\n- _Check path as-is then go up:_\r\n - `Path/To/Page` \r\n - `Path/Page`\r\n - `Page`\r\n- _Check in `Templates`, in path and up:_\r\n - `Path/To/Templates/Page`\r\n - `Path/Templates/Page`\r\n - `Templates/Page`\r\n- _Check root `System`:_\r\n - `System/Page`\r\n- _Check `EmptyPage` in path, then in templates:_\r\n - `Path/To/EmptyPage`\r\n - `Path/EmptyPage`\r\n - `EmptyPage`\r\n - `Path/To/Templates/EmptyPage`\r\n - `Path/Templates/EmptyPage`\r\n - `Templates/EmptyPage` _(This is guaranteed to exist)_\r\n\r\n\r\n**Exceptions:**\r\n\r\n- `System/settings` is global and _can not be overloaded_ for use as \r\nsystem configuration. This is done for security reasons.\r\n\r\n\r\n\r\n## Default pages\r\n\r\n**EmptyPage** \r\nA page resolved when a page does not exist. Used as a template for \r\nnew/empty pages.\r\n\r\nThis page is guaranteed to exist by the system.\r\n\r\nLocated at: Templates/EmptyPage\r\n\r\n\r\n**EmptyToDo** \r\nUsed as a template for new/empty ToDo pages.\r\n\r\nLocated at: Templates/EmptyToDo\r\n\r\n\r\n**EmptyOutline** \r\nUsed as a template for new/empty outline pages.\r\n\r\nLocated at: Templates/EmptyOutline\r\n\r\n\r\n**NoMatch** \r\nReturned when pattern matches no pages.\r\n\r\n\r\n\r\n## Relative and absolute paths\r\n\r\npWiki follows the traditional path semantics with one addition, the \">>\"\r\nthat is similar to \"..\" but works in the opposite direction, consuming \r\nthe next, i.e. child, path element instead of parent.\r\n\r\nTo illustrate the relative and absolute mechanics:\r\n\r\n| Title\t\t\t\t| Source Page | Path\t\t\t\t | Resolves to\t\t\t\t|\r\n|-------------------|-------------|-----------------------|-------------------------|\r\n| \".\" - current\t\t| \\SourcePage | \\\\./Target/Page\t\t | \\SourcePage/Target/Page |\r\n| \"..\" - parent\t\t| \\SourcePage | \\\\../Target/Page\t | \\Target/Page\t\t\t|\r\n| \">>\"\t\t| \\SourcePage | >>\\/Target/Page | \\SourcePage/Page\t\t|\r\n| \"/\" - root dir\t| \\SourcePage | \\/Target/Page\t\t | \\/Target/Page\t\t\t|\r\n\r\n\r\n_Note that neither a leading \"..\" at root level, nor a trailing \">>\" \r\nin any path, will have any effect, and will simply be ignored (e.g. \r\n\"\\/../Page\" is same as \"/Page\" and \"\\Path/>>\" is the same as \"Path\")_\r\n\r\n\r\n\r\n## Path patterns\r\n\r\nPath patterns are used to match/iterate multiple pages. The syntax is \r\nsimilar to path glob patterns.\r\n\r\n- \"\\*\" - matches any page in a sub-path on one level\r\n- \"\\*\\*\" - matches any page in a sub-path recursively\r\n\r\nNote that neither will match parts of paths that do not explicitly \r\nidentify pages.\r\n\r\n\r\n**Example:**\r\n\r\nXXX revise...\r\n\r\nFor the following paths:\r\n\r\n```\r\nWikiHome\r\nSomePage\r\nPath/to/OtherPage\r\nPath/Page\r\n```\r\n\r\nPatterns and their matches:\r\n- `*` will match:\r\n\t- WikiHome\r\n\t- SomePage\r\n- `**` will match:\r\n\t- WikiHome\r\n\t- SomePage\r\n\t- Path/to/OtherPage\r\n\t- Path/Page\r\n- `\\Path/*` will match:\r\n\t- Path/Page\r\n\r\nNote that neither `\\Path` nor `\\Path/to` does not refer to any real pages,\r\nthus neither is matched by any of the patterns explicitly.\r\n\r\n\r\n\r\n## Path actions\r\n\r\nXXX path elements that perform actions on pages but do not actually \r\ncorrespond to actual pages.\r\n\r\n\r\n\r\n## Path variables\r\n\r\nPath variables are resolved when path is resolved or accessed.\r\n\r\n**`$NOW`** \r\nReplaced with the current time.\r\n\r\n_Also see the `\\@now()` macro: [Doc/Macros]._\r\n\r\n\r\n**`$PATH`** \r\nReplaced with current page path.\r\n\r\n\r\n**`$BASE`** \r\nReplaced with current page basedir.\r\n\r\n\r\n**`$TITLE`** \r\nReplaced with current page title.\r\n\r\n\r\n**`$INDEX`** \r\nReplaced with current page index in pattern matching.\r\n\r\n\r\n\r\n\r\n\r\n"},"Doc/Macros":{"text":"# ![pWiki](img/pWiki-i.jpg) pWiki Macros\r\n\r\n## Syntax\r\n\r\nAny macro can be used in any of the two forms, either _inline_ or _HTML-like_.\r\n\r\nInline:\r\n```\r\n@macro-name(value)\r\n```\r\n\r\nHTML-style:\r\n```\r\n\r\n\r\n\r\n ...text...\r\n\r\n```\r\n\r\nThe two forms are almost identical, with the only difference being that the \r\ninline form does not support body text (note that some macros may provide\r\nthis functionality as an argument, namely `slot`).\r\n\r\nThe two forms exist to fill two distinct functions:\r\n- inline: compatible with attribute values and short\r\n- html-like: element-like, simpler when dealing with html\r\n\r\n\r\n\r\n### Escaping macros\r\n\r\nMacros can be escaped for inclusion in the page, the two types of macros \r\nare escaped a bit differently:\r\n\r\n- inline macros -- escaped by preceding with a \"\\\"\r\n\r\n ```\r\n \\\\@include(\\SomePage)\r\n ```\r\n\r\n Displayed in page as:\r\n\r\n \\@include(\\SomePage)\r\n\r\n \r\n _NOTE: if displayed on github, this will show an extra \"\\\" in both \r\n cases, this should be ignored as pWiki will consume the escaping \"\\\" \r\n in both the code example and the preview._\r\n \r\n\r\n\r\n- html-like macros -- escaped _the HTML way_\r\n\r\n ```\r\n <include src=\"\\SomePage\"\\>\r\n ```\r\n\r\n Displayed in page as:\r\n\r\n <include src=\"\\SomePage\"\\\\>\r\n\r\n\r\n\r\n### Conditional comments\r\n\r\nIn addition to HTML and filter-specific comments pWiki provides two types\r\nof conditional comments that serve two specific functions:\r\n\r\nShow something in pWiki but hide it in HTML:\r\n```\r\n\r\n```\r\n\r\nShow something in HTML but hide in pWiki:\r\n
\r\n<pwiki-comment> ... </pwiki-comment>\r\n
\r\n\r\n\r\nThis will enable writing documents (mainly in _markdown_) that are usable \r\nbot from within pWiki as well as outside.\r\n\r\n\r\n## Macros\r\n\r\n### now ()\r\n\r\nGet current date in seconds since epoch, this is equivalet Javascript's\r\n`Date.now()`.\r\n\r\nThis is mostly used for automatically creating paths (see: todo / outline)\r\n\r\nThis is different from `$NOW` in path (see: Doc/Path) in that this gets \r\nthe date once per page load, i.e. the date changes on page load, while \r\n`$NOW` is set every time the path is used, i.e. on every click or script\r\nuse.\r\n\r\n**Example:**\r\n```\r\n\\@now()\r\n```\r\n\r\nWill produce: `1471389217848`\r\n\r\n\r\n\r\n\r\n\r\n### filter (name)\r\n\r\nEnable or disable a page filter.\r\n\r\nA filter is a way to transform the page source.\r\n\r\nArguments:\r\n- `name` -- filter name. If name is preceded with a '-' then it \r\nwill be forced off. This is useful for disabling _default_ filters, or \r\nfilters added previously in templates.\r\n\r\nFilters:\r\n- wikiword (default)\r\n- markdown\r\n\r\n**Example:**\r\n\r\n\r\n\r\n\r\n\r\n- [bootstrap \\_edit](/bootstrap/Templates/_edit.html) – _see the \r\nmacro at the end of the page._\r\n\r\n\r\n\r\n\r\n### include (src isolated text)\r\n\r\nInclude a page. The included page is rendered independently from current\r\npage and is inserted as-is in macro body.\r\n\r\nNote that this will produce a `include` tag in the code that contains \r\nthe included page, this makes this tag not suitable for use anywhere \r\nbut an html element body.\r\n\r\nArguments:\r\n- `src` -- path to source page.\r\n- `isolated` -- prevent slots from included page from affecting the including page. \r\n- `text` -- is used when recursive include is detected and ignored otherwise.\r\n\r\n_For examples see `slot` macro exaples below._\r\n\r\n\r\n\r\n### source (src) / quote (src)\r\n\r\nInsert a page without rendering. This is similar to include but will not\r\nrender the page. \r\n\r\nThe difference between `source` and `quote` is:\r\n- _source_ includes the page as-is\r\n- _quotes_ escapes the page (i.e. _quotes_ it's source) for its code to \r\n display in the rendered HTML correctly.\r\n\r\nArguments:\r\n- `src` -- path to source page.\r\n\r\n**Example:**\r\n\r\n\r\n- [bootstrap css](/bootstrap/Templates/_css.html)\r\n\r\n\r\n\r\n\r\n\r\n### slot (name text)\r\n\r\nDefine or fill a slot.\r\n\r\nFirst occurrence of a `name` will _define_ a slot and fill it with `text`.\r\nEach new occurrence of a name will change slot content.\r\n\r\n**Example:**\r\n\r\n\r\n- [bootstrap view](/bootstrap/Templates/_view.html)\r\n- [bootstrap edit](/bootstrap/Templates/_edit.html)\r\n\r\n\r\n\r\n\r\n\r\n### macro (name src sort) / else ()\r\n\r\nApply macro to source page and include the result.\r\n\r\nThis is similar to include but does not require a separate page.\r\n\r\nBoth `name` and `src` are optional.\r\n\r\nIf `name` is given a _named macro_ is defined. This macro can be later \r\nreferenced (used) by name. A named macro can be redefined/overridden.\r\n\r\nIf `src` is given a macro is applied to a specific page or range of pages\r\n(see: WikiPath).\r\n\r\nFor a macro to be useful it must have a body (`text`), either defined as\r\na named macro or in the current macro.\r\n\r\nArguments:\r\n- `name` -- macro name (optional).\r\n- `src` -- path to source page (optional).\r\n- `sort` -- space separated list of methods to use for item sorting\r\n\r\n\r\n`else` macro is applicable inside `macro`. it is used when the `src` path\r\nof `macro` matches no pages.\r\n\r\n**Example:**\r\n\r\n\r\n- [bootstrap pages](/bootstrap/Templates/pages.html)\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n"},"Doc/About":{"text":""},"Theme/CLI/tree":{"text":"@source(../name)\n \n"},"WikiHome":{"text":"@include(Doc/About)"}} + +typeof(module) != "undefined" + && (module.exports = Bootstrap) \ No newline at end of file diff --git a/v3/bootstrap/Doc.html b/v3/bootstrap/Doc.html new file mode 100755 index 0000000..db03b84 --- /dev/null +++ b/v3/bootstrap/Doc.html @@ -0,0 +1 @@ +@include(./About) diff --git a/v3/bootstrap/Doc/About.md b/v3/bootstrap/Doc/About.md new file mode 100755 index 0000000..7d3ec9a --- /dev/null +++ b/v3/bootstrap/Doc/About.md @@ -0,0 +1 @@ +Placeholder file, this will be replaced with /README.md in bootstrap data... diff --git a/v3/bootstrap/Doc/Macros.md b/v3/bootstrap/Doc/Macros.md new file mode 100755 index 0000000..1270d61 --- /dev/null +++ b/v3/bootstrap/Doc/Macros.md @@ -0,0 +1,252 @@ +# ![pWiki](img/pWiki-i.jpg) pWiki Macros + +## Syntax + +Any macro can be used in any of the two forms, either _inline_ or _HTML-like_. + +Inline: +``` +@macro-name(value) +``` + +HTML-style: +``` + + + + ...text... + +``` + +The two forms are almost identical, with the only difference being that the +inline form does not support body text (note that some macros may provide +this functionality as an argument, namely `slot`). + +The two forms exist to fill two distinct functions: +- inline: compatible with attribute values and short +- html-like: element-like, simpler when dealing with html + + + +### Escaping macros + +Macros can be escaped for inclusion in the page, the two types of macros +are escaped a bit differently: + +- inline macros -- escaped by preceding with a "\" + + ``` + \\@include(\SomePage) + ``` + + Displayed in page as: + + \@include(\SomePage) + + + _NOTE: if displayed on github, this will show an extra "\" in both + cases, this should be ignored as pWiki will consume the escaping "\" + in both the code example and the preview._ + + + +- html-like macros -- escaped _the HTML way_ + + ``` + <include src="\SomePage"\> + ``` + + Displayed in page as: + + <include src="\SomePage"\\> + + + +### Conditional comments + +In addition to HTML and filter-specific comments pWiki provides two types +of conditional comments that serve two specific functions: + +Show something in pWiki but hide it in HTML: +``` + +``` + +Show something in HTML but hide in pWiki: +
+<pwiki-comment> ... </pwiki-comment>
+
+ + +This will enable writing documents (mainly in _markdown_) that are usable +bot from within pWiki as well as outside. + + +## Macros + +### now () + +Get current date in seconds since epoch, this is equivalet Javascript's +`Date.now()`. + +This is mostly used for automatically creating paths (see: todo / outline) + +This is different from `$NOW` in path (see: Doc/Path) in that this gets +the date once per page load, i.e. the date changes on page load, while +`$NOW` is set every time the path is used, i.e. on every click or script +use. + +**Example:** +``` +\@now() +``` + +Will produce: `1471389217848` + + + + + +### filter (name) + +Enable or disable a page filter. + +A filter is a way to transform the page source. + +Arguments: +- `name` -- filter name. If name is preceded with a '-' then it +will be forced off. This is useful for disabling _default_ filters, or +filters added previously in templates. + +Filters: +- wikiword (default) +- markdown + +**Example:** + + + + + +- [bootstrap \_edit](/bootstrap/Templates/_edit.html) – _see the +macro at the end of the page._ + + + + +### include (src isolated text) + +Include a page. The included page is rendered independently from current +page and is inserted as-is in macro body. + +Note that this will produce a `include` tag in the code that contains +the included page, this makes this tag not suitable for use anywhere +but an html element body. + +Arguments: +- `src` -- path to source page. +- `isolated` -- prevent slots from included page from affecting the including page. +- `text` -- is used when recursive include is detected and ignored otherwise. + +_For examples see `slot` macro exaples below._ + + + +### source (src) / quote (src) + +Insert a page without rendering. This is similar to include but will not +render the page. + +The difference between `source` and `quote` is: +- _source_ includes the page as-is +- _quotes_ escapes the page (i.e. _quotes_ it's source) for its code to + display in the rendered HTML correctly. + +Arguments: +- `src` -- path to source page. + +**Example:** + + +- [bootstrap css](/bootstrap/Templates/_css.html) + + + + + +### slot (name text) + +Define or fill a slot. + +First occurrence of a `name` will _define_ a slot and fill it with `text`. +Each new occurrence of a name will change slot content. + +**Example:** + + +- [bootstrap view](/bootstrap/Templates/_view.html) +- [bootstrap edit](/bootstrap/Templates/_edit.html) + + + + + +### macro (name src sort) / else () + +Apply macro to source page and include the result. + +This is similar to include but does not require a separate page. + +Both `name` and `src` are optional. + +If `name` is given a _named macro_ is defined. This macro can be later +referenced (used) by name. A named macro can be redefined/overridden. + +If `src` is given a macro is applied to a specific page or range of pages +(see: WikiPath). + +For a macro to be useful it must have a body (`text`), either defined as +a named macro or in the current macro. + +Arguments: +- `name` -- macro name (optional). +- `src` -- path to source page (optional). +- `sort` -- space separated list of methods to use for item sorting + + +`else` macro is applicable inside `macro`. it is used when the `src` path +of `macro` matches no pages. + +**Example:** + + +- [bootstrap pages](/bootstrap/Templates/pages.html) + + + + + + + diff --git a/v3/bootstrap/Doc/Path.md b/v3/bootstrap/Doc/Path.md new file mode 100755 index 0000000..071eb1b --- /dev/null +++ b/v3/bootstrap/Doc/Path.md @@ -0,0 +1,267 @@ +# ![pWiki](img/pWiki-i.jpg) pWiki Path + +A Wiki is a set of _pages_, each uniquely defined by it's _title_. Titles +are traditionally formatted as WikiWords. pWiki closely follows this +culture, while not restricting either page title formatting nor page +nesting (nested paths), though in the general case following the Wiki +style is recommended. + + + +## Basic terminology + +**Path** +_One or more strings (or parts) separated by "/" that identifies a view._ + +We call the last _part_ in a path sequence a _title_. + +We call the sub-path without the _title_ a _basedir_ or simply _dir_. + +In pWiki, there is no distinction between a page and a _directory_, thus +we do not use the later term, instead, we may use the term _sub-page_. + +Paths are case sensitive. + + +**Page** +_A set of data associated with a path._ + +A page is identified by it's path, but this does not require every +sub-path of that path to exist -- the full path is the identifier. + +Not every path _identifies_ a page, but every path _resolves_ to a page. + +Some pages are _bootstrapped_, i.e. are predefined in pWiki, these pages +can be overridden but can not be removed. This is done to make it simple +to revert to the default state if something goes wrong. + + +**View** +_A path that resolves to a page that may or may not be at (identified by) +that specific path._ + +A _view's_ path may match that of a specific page or may not match any +page directly, but any view will resolve to a page via the _acquisition +process_ + +Any page is a view, every view resolves to a page, but not every view +is a page. + +(see: _Page acquisiton_ below) + + +**\WikiWord** +_A WikiWork is a specially formated string that is treated as a link in +page text_ + +In pWiki a simple WikiWord is any string starting with a capital letter, +must contain at least and one more capital letter and can consist of +letters, numbers, underscores (WikiWord itself is a good example) + +A WikiWord path (_WikiPath_) is a set of path parts separated by '/' the +first part must start with a capital letter and the rest can contain +letters, numbers and/or underscores (example: Path/to/somepage). + +_Note that this is not actually a part of the path specification but it +is part of the Wiki culture and a convenient way to automatically link +to pages (handled by pWiki macros when rendering pages)._ + + + +**Special path characters** +Titles of _user pages_ must not contain the leading underscore `_` +character, such paths are used internally. + + + +## Page acquisition + +pWiki path system differs from how traditional file system paths are +handled. In pWiki if a path does not reference a page directly (i.e. +it's a _view_), a search is conducted to find an alternative page. This +search is called _page acquisition_. + +**Acquisition process:** +_A set of rules defining how a page is retrieved via a path._ + + +This is used as a simple and uniform mechanism to: +- Get default pages for specific situations + Like [Templates/EmptyPage] to handle the _page not found_ condition. +- Define generic templates/pages accessible by multiple pages in path + A good example would be the viewer used to show this page [Templates/\_view] + and all of it's _chrome_ like the path in the header and links in the + footer (seen: when viewing through pWiki) +- Overload default templates/pages + + +### The acquisition order/rules: + +1. if _path_ matches a specific page, target _page_ is found +1. if _path_ does not match a page: + 1. if _title_ matches a page in the parent _path_, _page_ is found + 1. repeat until we either have a match or reach root (empty _basedir_) +1. if no match is found, check if title exists in [Templates] in _basedir_ +1. if no match is found, check if title exists in [/System] +1. if no match is found, repeat process for `EmptyPage` instead of _title_ + + +**Example:** + +For path `Path/To/Page` the following paths are checked in order +and the first matching page is returned: + +- _Check path as-is then go up:_ + - `Path/To/Page` + - `Path/Page` + - `Page` +- _Check in `Templates`, in path and up:_ + - `Path/To/Templates/Page` + - `Path/Templates/Page` + - `Templates/Page` +- _Check root `System`:_ + - `System/Page` +- _Check `EmptyPage` in path, then in templates:_ + - `Path/To/EmptyPage` + - `Path/EmptyPage` + - `EmptyPage` + - `Path/To/Templates/EmptyPage` + - `Path/Templates/EmptyPage` + - `Templates/EmptyPage` _(This is guaranteed to exist)_ + + +**Exceptions:** + +- `System/settings` is global and _can not be overloaded_ for use as +system configuration. This is done for security reasons. + + + +## Default pages + +**EmptyPage** +A page resolved when a page does not exist. Used as a template for +new/empty pages. + +This page is guaranteed to exist by the system. + +Located at: Templates/EmptyPage + + +**EmptyToDo** +Used as a template for new/empty ToDo pages. + +Located at: Templates/EmptyToDo + + +**EmptyOutline** +Used as a template for new/empty outline pages. + +Located at: Templates/EmptyOutline + + +**NoMatch** +Returned when pattern matches no pages. + + + +## Relative and absolute paths + +pWiki follows the traditional path semantics with one addition, the ">>" +that is similar to ".." but works in the opposite direction, consuming +the next, i.e. child, path element instead of parent. + +To illustrate the relative and absolute mechanics: + +| Title | Source Page | Path | Resolves to | +|-------------------|-------------|-----------------------|-------------------------| +| "." - current | \SourcePage | \\./Target/Page | \SourcePage/Target/Page | +| ".." - parent | \SourcePage | \\../Target/Page | \Target/Page | +| ">>" | \SourcePage | >>\/Target/Page | \SourcePage/Page | +| "/" - root dir | \SourcePage | \/Target/Page | \/Target/Page | + + +_Note that neither a leading ".." at root level, nor a trailing ">>" +in any path, will have any effect, and will simply be ignored (e.g. +"\/../Page" is same as "/Page" and "\Path/>>" is the same as "Path")_ + + + +## Path patterns + +Path patterns are used to match/iterate multiple pages. The syntax is +similar to path glob patterns. + +- "\*" - matches any page in a sub-path on one level +- "\*\*" - matches any page in a sub-path recursively + +Note that neither will match parts of paths that do not explicitly +identify pages. + + +**Example:** + +XXX revise... + +For the following paths: + +``` +WikiHome +SomePage +Path/to/OtherPage +Path/Page +``` + +Patterns and their matches: +- `*` will match: + - WikiHome + - SomePage +- `**` will match: + - WikiHome + - SomePage + - Path/to/OtherPage + - Path/Page +- `\Path/*` will match: + - Path/Page + +Note that neither `\Path` nor `\Path/to` does not refer to any real pages, +thus neither is matched by any of the patterns explicitly. + + + +## Path actions + +XXX path elements that perform actions on pages but do not actually +correspond to actual pages. + + + +## Path variables + +Path variables are resolved when path is resolved or accessed. + +**`$NOW`** +Replaced with the current time. + +_Also see the `\@now()` macro: [Doc/Macros]._ + + +**`$PATH`** +Replaced with current page path. + + +**`$BASE`** +Replaced with current page basedir. + + +**`$TITLE`** +Replaced with current page title. + + +**`$INDEX`** +Replaced with current page index in pattern matching. + + + + + diff --git a/v3/bootstrap/Doc/Templates.md b/v3/bootstrap/Doc/Templates.md new file mode 100755 index 0000000..5f3dc8f --- /dev/null +++ b/v3/bootstrap/Doc/Templates.md @@ -0,0 +1 @@ +XXX Document the template structure here XXX diff --git a/v3/bootstrap/System/settings.json b/v3/bootstrap/System/settings.json new file mode 100755 index 0000000..a008ade --- /dev/null +++ b/v3/bootstrap/System/settings.json @@ -0,0 +1,34 @@ +/********************************************************************** +* !EXPERIMENTAL! +* +* These filters are required for .code to be JSON compatible: +* @filter(json) @filter(-wikiword) +* +* NOTE: currently inline editing may mess this up. +* NOTE: all the comments will be removed before parsing. +* +**********************************************************************/ + +// The actual root data to be parsed... +{ + // Wiki config... + "HomePage": "WikiHome", + "ShowSystemPages": false, + "ShowBasePages": true, + // NOTE: setting this to true will effectively allow pages to control + // your wiki. This is a potential threat if you allow untrusted + // content on your wiki... + // You are doing this at your own risk! + "AllowScripts": false, + + // PeerJS API key... + "PeerJS-API-key": "XXX", + // PeerJS server URL (leave blank for default)... + "PeerJS-Server": null, + + // XXX + "CouchDB-Server": null +} + + +/*********************************************************************/ diff --git a/v3/bootstrap/System/style.css b/v3/bootstrap/System/style.css new file mode 100755 index 0000000..393f6e5 --- /dev/null +++ b/v3/bootstrap/System/style.css @@ -0,0 +1,99 @@ +body { + font-family: /*worksans,*/ opensans, sans-serif; + +} + +.title img { + vertical-align: middle; +} + +h1, h2, h3 { + border-bottom: solid 1px rgba(0, 0, 0, 0.1); + padding-bottom: 5px; +} +h2, h3 { + border-bottom: solid 1px rgba(0, 0, 0, 0.05); +} + + +/* tables */ +table { + width: 100%; +} +table, td, th { + border-bottom: solid 1px gray; + border-collapse: collapse; +} +td, th { + text-align: left; + padding: 5px; +} +tr:hover { + background-color: rgba(0, 0, 0, 0.05); +} + + +.raw, +.text { + display: block; +} + +.item.checked { + opacity: 0.3; +} +.item.checked:hover { + opacity: 0.8; +} +.item.checked .item-content * { + text-decoration: line-through; +} + +.button { + text-decoration: none; +} +.button:last-child { + margin-right: 5px; +} + +.separator~* { + float: right; +} + +pre { + display: block; + background-color: rgba(0, 0, 0, 0.05); + padding: 10px; + padding-bottom: 15px; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; +} + +.item:hover { + background-color: rgba(0, 0, 0, 0.05); +} +.item .button { + display: none; +} +.item:hover .button { + display: inline-block; +} + +.sort-handle { + opacity: 0.1; + padding-left: 5px; + padding-right: 5px; + cursor: pointer; + text-decoration: none; +} +.item:hover .sort-handle { + opacity: 0.3; +} +.sort-placeholder { + display: block; +} + +/* @filter(-wikiword) */ +/* @filter(text) */ +/* vim:set ts=4 sw=4 ft=css : */ diff --git a/v3/bootstrap/Templates.html b/v3/bootstrap/Templates.html new file mode 100755 index 0000000..69cf1c9 --- /dev/null +++ b/v3/bootstrap/Templates.html @@ -0,0 +1,20 @@ +

+XXX Genereal template description... +

+ + +
+

+ @source(./path) +

+

+

@quote(./raw)
+

+
+ + + + + + + diff --git a/v3/bootstrap/Templates/EmptyOutline.html b/v3/bootstrap/Templates/EmptyOutline.html new file mode 100755 index 0000000..c4c465a --- /dev/null +++ b/v3/bootstrap/Templates/EmptyOutline.html @@ -0,0 +1 @@ +@include(./outline) diff --git a/v3/bootstrap/Templates/EmptyPage.html b/v3/bootstrap/Templates/EmptyPage.html new file mode 100755 index 0000000..001e183 --- /dev/null +++ b/v3/bootstrap/Templates/EmptyPage.html @@ -0,0 +1,8 @@ + + +Page @include(./path) is empty.

+ +Links to this page:
+@include(./links)

+ + diff --git a/v3/bootstrap/Templates/EmptyToDo.html b/v3/bootstrap/Templates/EmptyToDo.html new file mode 100755 index 0000000..7046988 --- /dev/null +++ b/v3/bootstrap/Templates/EmptyToDo.html @@ -0,0 +1 @@ +@include(./todo) diff --git a/v3/bootstrap/Templates/_css.html b/v3/bootstrap/Templates/_css.html new file mode 100755 index 0000000..ca60dc4 --- /dev/null +++ b/v3/bootstrap/Templates/_css.html @@ -0,0 +1,3 @@ + diff --git a/v3/bootstrap/Templates/_edit.html b/v3/bootstrap/Templates/_edit.html new file mode 100755 index 0000000..6efb50f --- /dev/null +++ b/v3/bootstrap/Templates/_edit.html @@ -0,0 +1,12 @@ + + +(view) + +@source(../title) + + +
+
+ + + diff --git a/v3/bootstrap/Templates/_outline.html b/v3/bootstrap/Templates/_outline.html new file mode 100755 index 0000000..e02635d --- /dev/null +++ b/v3/bootstrap/Templates/_outline.html @@ -0,0 +1,9 @@ + + +@source(../title) + + + @include(../outline) + + + diff --git a/v3/bootstrap/Templates/_raw.txt b/v3/bootstrap/Templates/_raw.txt new file mode 100755 index 0000000..a62cf6a --- /dev/null +++ b/v3/bootstrap/Templates/_raw.txt @@ -0,0 +1 @@ +@source(..) diff --git a/v3/bootstrap/Templates/_todo.html b/v3/bootstrap/Templates/_todo.html new file mode 100755 index 0000000..627476d --- /dev/null +++ b/v3/bootstrap/Templates/_todo.html @@ -0,0 +1,9 @@ + + +@source(../title) + + + @include(../todo) + + + diff --git a/v3/bootstrap/Templates/_view.html b/v3/bootstrap/Templates/_view.html new file mode 100755 index 0000000..b1eaed5 --- /dev/null +++ b/v3/bootstrap/Templates/_view.html @@ -0,0 +1,31 @@ + +@include(./style/_css) + +
+ + + [@source(../path)] + + (edit) + + + + + +
+ +
+

+ @source(../title) +

+
+ +
+ + + +
+ +
+home + + diff --git a/v3/bootstrap/Templates/all_pages.html b/v3/bootstrap/Templates/all_pages.html new file mode 100755 index 0000000..efaa5d6 --- /dev/null +++ b/v3/bootstrap/Templates/all_pages.html @@ -0,0 +1,12 @@ + +
+ [@source(./path)] + + × +
+ + No pages... + +
+ + diff --git a/v3/bootstrap/Templates/macros.html b/v3/bootstrap/Templates/macros.html new file mode 100755 index 0000000..7e17ec8 --- /dev/null +++ b/v3/bootstrap/Templates/macros.html @@ -0,0 +1,6 @@ + + × + + + + diff --git a/v3/bootstrap/Templates/outline.html b/v3/bootstrap/Templates/outline.html new file mode 100755 index 0000000..4806000 --- /dev/null +++ b/v3/bootstrap/Templates/outline.html @@ -0,0 +1,47 @@ + + + + + + + + × + + + +
+ + + + +
+
+ +
+
+ + + + + +
+
+ +
+
+
+
+ + diff --git a/v3/bootstrap/Templates/pages.html b/v3/bootstrap/Templates/pages.html new file mode 100755 index 0000000..671cfd2 --- /dev/null +++ b/v3/bootstrap/Templates/pages.html @@ -0,0 +1,12 @@ + +
+ [@source(./path)] + + × +
+ + No pages... + +
+ + diff --git a/v3/bootstrap/Templates/todo.html b/v3/bootstrap/Templates/todo.html new file mode 100755 index 0000000..66871d3 --- /dev/null +++ b/v3/bootstrap/Templates/todo.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/v3/bootstrap/Templates/tree.html b/v3/bootstrap/Templates/tree.html new file mode 100755 index 0000000..096ad0b --- /dev/null +++ b/v3/bootstrap/Templates/tree.html @@ -0,0 +1,15 @@ +
+ +
+ + @source(./title) + + × +
+
+ +
+
+
+ + diff --git a/v3/bootstrap/Test/slot.html b/v3/bootstrap/Test/slot.html new file mode 100755 index 0000000..d2304cf --- /dev/null +++ b/v3/bootstrap/Test/slot.html @@ -0,0 +1,3 @@ + + + diff --git a/v3/bootstrap/TestTodo.md b/v3/bootstrap/TestTodo.md new file mode 100755 index 0000000..777e560 --- /dev/null +++ b/v3/bootstrap/TestTodo.md @@ -0,0 +1,3 @@ +[Templates/outline/_edit] + +@include(./todo) diff --git a/v3/bootstrap/TestTodo/01.html b/v3/bootstrap/TestTodo/01.html new file mode 100755 index 0000000..d08a3c1 --- /dev/null +++ b/v3/bootstrap/TestTodo/01.html @@ -0,0 +1 @@ +item 1 diff --git a/v3/bootstrap/TestTodo/02.html b/v3/bootstrap/TestTodo/02.html new file mode 100755 index 0000000..af33370 --- /dev/null +++ b/v3/bootstrap/TestTodo/02.html @@ -0,0 +1 @@ +item 2 diff --git a/v3/bootstrap/TestTodo/03.html b/v3/bootstrap/TestTodo/03.html new file mode 100755 index 0000000..aa1ac30 --- /dev/null +++ b/v3/bootstrap/TestTodo/03.html @@ -0,0 +1 @@ +item 3 diff --git a/v3/bootstrap/TestTodo/04.html b/v3/bootstrap/TestTodo/04.html new file mode 100755 index 0000000..bb06608 --- /dev/null +++ b/v3/bootstrap/TestTodo/04.html @@ -0,0 +1 @@ +item 4 diff --git a/v3/bootstrap/Theme/CLI/tree.txt b/v3/bootstrap/Theme/CLI/tree.txt new file mode 100755 index 0000000..91b5db9 --- /dev/null +++ b/v3/bootstrap/Theme/CLI/tree.txt @@ -0,0 +1,2 @@ +@source(../name) + diff --git a/v3/img/pWiki-192px.jpg b/v3/img/pWiki-192px.jpg new file mode 100755 index 0000000..f776b82 Binary files /dev/null and b/v3/img/pWiki-192px.jpg differ diff --git a/v3/img/pWiki-48px.jpg b/v3/img/pWiki-48px.jpg new file mode 100755 index 0000000..0efcf09 Binary files /dev/null and b/v3/img/pWiki-48px.jpg differ diff --git a/v3/img/pWiki-96px.jpg b/v3/img/pWiki-96px.jpg new file mode 100755 index 0000000..75accf5 Binary files /dev/null and b/v3/img/pWiki-96px.jpg differ diff --git a/v3/img/pWiki-i.jpg b/v3/img/pWiki-i.jpg new file mode 100755 index 0000000..02b5319 Binary files /dev/null and b/v3/img/pWiki-i.jpg differ diff --git a/v3/img/pWiki-s.jpg b/v3/img/pWiki-s.jpg new file mode 100755 index 0000000..fdbd25e Binary files /dev/null and b/v3/img/pWiki-s.jpg differ diff --git a/v3/img/pWiki.jpg b/v3/img/pWiki.jpg new file mode 100755 index 0000000..7dc5751 Binary files /dev/null and b/v3/img/pWiki.jpg differ diff --git a/v3/img/pWiki.kra b/v3/img/pWiki.kra new file mode 100755 index 0000000..f6a606d Binary files /dev/null and b/v3/img/pWiki.kra differ diff --git a/v3/img/pWiki.png b/v3/img/pWiki.png new file mode 100755 index 0000000..a16b6af Binary files /dev/null and b/v3/img/pWiki.png differ diff --git a/v3/package-lock.json b/v3/package-lock.json new file mode 100644 index 0000000..1d7a527 --- /dev/null +++ b/v3/package-lock.json @@ -0,0 +1,347 @@ +{ + "name": "pWiki", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pWiki", + "version": "0.0.1", + "license": "BSD", + "dependencies": { + "glob": "*", + "ig-actions": "*", + "ig-features": "*", + "ig-object": "*", + "ig-test": "*", + "ig-types": "*", + "requirejs": "*", + "uuid": "*" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ig-actions": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/ig-actions/-/ig-actions-3.26.0.tgz", + "integrity": "sha512-GYwip0mgj1KkwqLeXelxifRog04FQpKsvT0V8G09MyWdZHKVbRw+3oXQnrAwba/+bJhJpQhSBXi6ClNAb7ExHA==", + "license": "BSD-3-Clause", + "dependencies": { + "ig-object": "*" + } + }, + "node_modules/ig-argv": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/ig-argv/-/ig-argv-2.17.2.tgz", + "integrity": "sha512-EGm3Sx1vo9gnQKh2CJKdyNyY0IsKljz2zTUt1SV3BrErtRvFz3WuH5ltoD0ErSEUmyY5mtOklTrFSmXqA2joLQ==", + "license": "BSD-3-Clause", + "dependencies": { + "ig-object": "^5.4.16" + } + }, + "node_modules/ig-argv/node_modules/ig-object": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/ig-object/-/ig-object-5.6.0.tgz", + "integrity": "sha512-5MAUWSwfHKQNrgLroXxBHjlhrhVbhzlVqvUcfMDjUeK/ufWQ9THE0HDcvhfu+YrPfRjTR2QpD2Ygp+2H4O0C6g==", + "license": "BSD-3-Clause" + }, + "node_modules/ig-doc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ig-doc/-/ig-doc-1.0.0.tgz", + "integrity": "sha512-3+wAFWcvXv4czAk7pNa9+DEMDjlj+aJ7AQcB1ZnK49B+49nuJX7JkKJnBSF38C1YoEoxs12jvjuaqtSrvQa3KA==", + "license": "BSD-3-Clause" + }, + "node_modules/ig-features": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/ig-features/-/ig-features-3.4.7.tgz", + "integrity": "sha512-ejwYjnP1K5mXzbnLTrsuuB0lGSeUtO6UtBxYcGbXGdu9rgKjz3Q46qC9XYiM01zYw3MltivLCdc2kN6/RiOvZw==", + "license": "BSD-3-Clause", + "dependencies": { + "ig-actions": "^3.24.28", + "ig-object": "^5.4.14" + } + }, + "node_modules/ig-features/node_modules/ig-object": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/ig-object/-/ig-object-5.6.0.tgz", + "integrity": "sha512-5MAUWSwfHKQNrgLroXxBHjlhrhVbhzlVqvUcfMDjUeK/ufWQ9THE0HDcvhfu+YrPfRjTR2QpD2Ygp+2H4O0C6g==", + "license": "BSD-3-Clause" + }, + "node_modules/ig-object": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ig-object/-/ig-object-6.2.1.tgz", + "integrity": "sha512-mVGmqd+yiIf1hUo/hkqhc0JApXApuaFU4chmtO4nudL87bQ6jmJQ2qqSdUfe2hauNOHXNqG226qltCp39FHqXA==", + "license": "BSD-3-Clause", + "dependencies": { + "ig-doc": "*", + "ig-stoppable": "^2.0.0" + } + }, + "node_modules/ig-stoppable": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/ig-stoppable/-/ig-stoppable-2.0.4.tgz", + "integrity": "sha512-KxS8AGsjelRAmbbQuASj+XRuk99P4OOprd+lIUMU2nuRKPQItNQK/apls8IlR3kNp5ZdQqBdV+zVJmYGrxofnA==", + "license": "BSD-3-Clause" + }, + "node_modules/ig-test": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/ig-test/-/ig-test-1.6.7.tgz", + "integrity": "sha512-+gHKSemPMbMk+UjDaY74BwYmxWh1tRPa6vnoDf8aauXaFdoYkYLKM6lhEB5koerTXK69M9aPw609o7r91O5Z6Q==", + "license": "BSD-3-Clause", + "dependencies": { + "colors": "1.4.0", + "glob": "^7.1.6", + "ig-argv": "^2.16.3", + "ig-object": "^5.4.16" + }, + "bin": { + "runtests": "test.js" + } + }, + "node_modules/ig-test/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/ig-test/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/ig-test/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ig-test/node_modules/ig-object": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/ig-object/-/ig-object-5.6.0.tgz", + "integrity": "sha512-5MAUWSwfHKQNrgLroXxBHjlhrhVbhzlVqvUcfMDjUeK/ufWQ9THE0HDcvhfu+YrPfRjTR2QpD2Ygp+2H4O0C6g==", + "license": "BSD-3-Clause" + }, + "node_modules/ig-test/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ig-types": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/ig-types/-/ig-types-6.26.2.tgz", + "integrity": "sha512-VGg8MVmpblVCmZK52bJcDtPG3uRFiEyPnlDEIJv3MymTm+aUNWR/Th20NU2wfGV/Ux3KlsbGlhe+0c9ZKTYPDw==", + "license": "BSD-3-Clause", + "dependencies": { + "ig-object": "^6.0.0", + "ig-stoppable": "^2.0.0", + "object-run": "^1.0.1" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/object-run": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object-run/-/object-run-1.1.0.tgz", + "integrity": "sha512-evzNuK7N0AZBjZDNwWfX8xJoHYYWOuoXn0A0lHxddCsKQcmU8K68HQW7eloHwc6TGlNa3CCXHxdFnO+46qtJUA==", + "license": "BSD-3-Clause" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/requirejs": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.8.tgz", + "integrity": "sha512-7/cTSLOdYkNBNJcDMWf+luFvMriVm7eYxp4BcFCsAX0wF421Vyce5SXP17c+Jd5otXKGNehIonFlyQXSowL6Mw==", + "license": "MIT", + "bin": { + "r_js": "bin/r.js", + "r.js": "bin/r.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/v3/package.json b/v3/package.json new file mode 100755 index 0000000..692f043 --- /dev/null +++ b/v3/package.json @@ -0,0 +1,38 @@ +{ + "name": "pWiki", + "version": "0.0.1", + "author": "Alex A. Naanou ", + "license": "BSD", + "dependencies": { + "requirejs": "*", + "uuid": "*", + "glob": "*", + + "ig-actions": "*", + "ig-features": "*", + "ig-object": "*", + "ig-types": "*", + "ig-test": "*" + }, + "disabled-dependencies": { + "jszip": "*", + "any-date-parser": "*", + "file-saver": "*", + "flexsearch": "*", + "idb-keyval": "*", + "peer": "*", + "pouch-replicate-webrtc": "*", + "pouchdb": "*", + "showdown": "*", + "xss": "*", + "@toast-ui/editor": "*", + "medium-editor": "*", + "medium-editor-markdown": "*", + "pouchdb": "*", + "pouchdb-browser": "*", + "showdown": "*" + }, + "scripts": { + "bootstrap": "node scripts/bootstrap.js" + } +} diff --git a/v3/pwiki/_module.js b/v3/pwiki/_module.js new file mode 100644 index 0000000..6744ca5 --- /dev/null +++ b/v3/pwiki/_module.js @@ -0,0 +1,14 @@ +/********************************************************************** +* +* +* +**********************************************************************/ +((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) +(function(require){ var module={} // make module AMD/node compatible... +/*********************************************************************/ + + + + +/********************************************************************** +* vim:set ts=4 sw=4 nowrap : */ return module }) diff --git a/v3/pwiki/index.js b/v3/pwiki/index.js new file mode 100755 index 0000000..c49f097 --- /dev/null +++ b/v3/pwiki/index.js @@ -0,0 +1,452 @@ +/********************************************************************** +* +* +* +**********************************************************************/ +((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') + + +//--------------------------------------------------------------------- +// +// makeIndex([, ]) +// makeIndex(, [, ]) +// -> +// +// Call/get +// () +// -> +// -> +// +// Call the index handler method... +// ('__call__', ..) +// -> ... +// -> +// +// Get merged data (cached) +// ('get') +// -> +// -> +// NOTE: when a getter is pending (promise), all consecutive calls +// will resolve the original getter return value... +// +// Get sync or cached result and do "lazy" background update... +// ('lazy') +// -> +// -> +// NOTE: if (..) is synchronous, this will wait till +// it returns and will return the result. +// NOTE: 'lazy' mode is generally faster as it does all the checks and +// updating (if needed) in a background promise, but can return +// outdated cached results. +// NOTE: as a side-effect this avoids returning promises if a cached +// value is available. i.e. a promise is returned only when +// getting/generating a value for the first time. +// +// Get cached result and trigger a background update... +// ('cached') +// -> +// -> +// -> undefined +// NOTE: this is like 'lazy' but will not wait for )(..) +// to return, making it even faster but as a trade off it will +// return the cached and possibly outdated result even if +// (..) is synchronous. +// +// Get local data (uncached)... +// ('local') +// -> +// -> +// +// Clear cache... +// ('clear') +// +// Reset cache (clear then get)... +// ('reset') +// -> +// -> +// +// Get index status... +// ('status') +// -> 'empty' +// -> 'pending' +// -> 'cached' +// -> 'outdated' +// +// Run custom action... +// (), ...) +// -> +// -> +// +// NOTE: the main differences between the 'get', 'lazy' and 'cached' actions: +// 'get' +// generate/merge are all sync/async as defined +// when cached value available validate and return either the cached value or generate +// 'lazy' +// XXX +// 'cached' +// call get in background +// return cached value or undefined +// +// +// +// Special methods: +// +// Special method to generate local ... +// .____() +// -> +// +// Merge local data with other sources... +// .___merge__() +// -> +// +// Test if cache is valid... +// .___isvalid__() +// -> +// +// Handle custom action... +// ._____(. ...) +// -> +// +// +// +// Special attributes: +// +// Cached data... +// .___cache / . +// +// Modification time... +// .___modified +// +// Pending generator promise... +// .___promise +// +// +// Options format: +// { +// // XXX +// attr: false +// | true +// | , +// +// // list of dependencies that when changed will trigger a cache +// // drop on current index... +// // NOTE: dependency checking is done via .modified time, if value +// // is changed manually and not via an action then the system +// // will not catch the change. +// depends: [ +// , +// ... +// ], +// +// // custom action... +// // NOTE: this is the same as defining ._____(..) +// // method... +// : , +// } +// +// +// XXX do we separate internal methods and actions??? +// i.e. ___merge__(..) / ___isvalid__(..) and the rest... +var makeIndex = +module.makeIndex = +function(name, generate, options={}){ + // makeIndex(, ) + if(generate + && typeof(generate) != 'function'){ + options = generate + generate = options.generate } + + // attr names... + var cache = + typeof(options.attr) == 'string' ? + options.attr + // XXX revise default... + : !!options.attr ? + name + : `__${name}_cache` + var modified = `__${name}_modified` + var promise = `__${name}_promise` + var test = `__${name}_isvalid__` + var merge = `__${name}_merge__` + var special = `__${name}__` + + // set modified time... + var _stamp = function(that, res){ + res instanceof Promise ? + res.then(function(){ + that[modified] = Date.now() }) + : (that[modified] = Date.now()) + return res } + // make local cache... + var _make = function(that){ + return that[special] != null ? + that[special]() + : (generate + && generate.call(that)) } + var _smake = function(that){ + return _stamp(that, _make(that)) } + // unwrap a promised value into cache... + var _await = function(obj, val){ + if(val instanceof Promise){ + // NOTE: this avoids a race condition when a getter is called + // while a previous getter is still pending... + if(obj[promise] == null){ + obj[promise] = val + val.then( + function(value){ + delete obj[promise] + obj[cache] = value }, + function(err){ + // XXX should we report this??? + delete obj[promise] }) } + val = obj[promise] } + return val } + var _deferred = async function(obj, ...args){ + return meth.call(obj, ...args) } + + // build the method... + var meth + return (meth = Object.assign( + function(action, ...args){ + var that = this + + action = action === undefined ? + ('__call__' in options ? + '__call__' + : 'get') + : action + + // action: status... + if(action == 'status'){ + if(this[cache] instanceof Promise){ + return 'pending' } + if(cache in this){ + var cur = this[modified] + // user test... + if(test in this + && !this[test](cur)){ + return 'outdated' + // check dependencies... + } else if(meth.options.depends){ + for(var dep of meth.options.depends){ + if(this[`__${this[dep].index}_modified`] > cur){ + return 'outdated' } } } + return 'cached' } + return 'empty' } + + // action: lazy... + if(action == 'lazy'){ + if(this[cache] instanceof Promise){ + return this[cache] } + var res = meth.call(this, 'get') + return (this[cache] + && res instanceof Promise) ? + this[cache] + : res } + // action: cached... + if(action == 'cached'){ + _deferred(this, 'get') + return this[cache] } + // action: local... + // NOTE: this is intentionally not cached... + if(action == 'local'){ + return _make(this) } + + // action: clear/reset... + if(action == 'clear' + || action == 'reset'){ + delete this[cache] + 'reset' in options + && options['reset'].call(this, null, name) } + if(action == 'clear'){ + return } + + // validate cache... + if(cache in this + && meth.call(this, 'status') == 'outdated'){ + delete this[cache] } + + // action: other... + if(action != 'get' + && action != '__call__' + && action != 'reset'){ + var action_meth = `__${name}_${action}__` + // generate cache if not available... + var cur = cache in this ? + this[cache] + : meth.call(this, 'reset') + var res = _await(this, + this[cache] = + // NOTE: this[action_meth] will fully shadow options[action]... + action_meth in this ? + this[action_meth](cur, ...args) + : (action in options + && typeof(options[action]) == 'function') ? + //options[action].call(this, cur, ...args) + options[action].call(this, cur, name, ...args) + : cur) + res !== cur + && _stamp(this, res) + return res } + + // get/generate the data... + var res = _await(this, + this[cache] = + // cached... + this[cache] != null ? + this[cache] + // generate + merge... + : this[merge] != null ? + // NOTE: need to set the timestamp after the merge... + _stamp(this, + this[merge](_make(this))) + // generate... + : _smake(this)) + + // action: call... + // NOTE: this directly returns the result to user but will + // not automatically influence the stored value... + if(action == '__call__'){ + return options.__call__.call(this, res, name, ...args) } + + // action: get... + return res }, + { + index: name, + indexed: true, + options, + })) } + + + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// XXX +var iter = +module.iter = +function*(obj){ + for(var key of object.deepKeys(obj)){ + var d = object.values(obj, key, true).next().value.value + // XXX should makeIndex(..) be a constructor -- i.e. an instanceof test??? + if(typeof(d) == 'function' + && d.indexed){ + yield key } } } + + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// +// .index(obj) +// .index(obj, 'get') +// -> +// +// ... +// +// .index(obj, , ...) +// -> +// +// +// .index('obj, new', , [, ]) +// -> +// +// XXX +var index = +module.index = +async function(obj, action='get', ...args){ + // create a new index... + if(action == 'new'){ + var res = module.makeIndex(...args) + var [name, _, options={}] = args + var attr = name + if(options.attr){ + var attr = `__${name}` + Object.defineProperty(obj, name, { + get: function(){ + return obj[attr] }, }) } + return (obj[attr] = res) } + // propagate action... + return Object.fromEntries( + await Promise.all( + module.iter(obj) + .map(async function(name){ + return [ + obj[name].index, + await obj[name](action, ...args), + ] }))) } + + + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +var IndexManagerMixin = +module.IndexManagerMixin = +object.Mixin('IndexManagerMixin', { + // List of index handler attribute names... + // + // XXX rename??? + get index_attrs(){ + return [...module.iter(this)] }, + index: async function(action='get', ...args){ + return module.index(this, ...arguments) }, +}) + + + +//--------------------------------------------------------------------- + +var indexTest = +module.indexTest = +IndexManagerMixin({ + // tests... + // + moo: module.makeIndex('moo', () => 123), + + foo_index: module.makeIndex('foo', () => 123, { + attr: true, + add: function(cur, val){ + return cur + val }, + }), + + __boo_add__: function(cur, val){ + return cur + val }, + boo: module.makeIndex('boo', () => 123), + + __soo_add__: async function(cur, val){ + return await cur + val }, + __soo: module.makeIndex('soo', async () => 123), + get soo(){ + return this.__soo() }, + + __sum: module.makeIndex('sum', + async function(){ + return await this.moo() + + await this.foo_index() + + await this.boo() + + await this.soo }, + { depends: [ + 'moo', + 'foo_index', + 'boo', + '__soo', + ], }), + get sum(){ + return this.__sum() }, + + __merged__: function(){ + return 777 }, + __merged_merge__: async function(data){ + return (await data) + 777 }, + __merged: module.makeIndex('merged'), + get merged(){ + return this.__merged() }, +}) + + + + +/********************************************************************** +* vim:set ts=4 sw=4 nowrap : */ return module }) diff --git a/v3/pwiki/page.js b/v3/pwiki/page.js new file mode 100755 index 0000000..0e2fe68 --- /dev/null +++ b/v3/pwiki/page.js @@ -0,0 +1,2066 @@ +/********************************************************************** +* +* +* +**********************************************************************/ +((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') + +// XXX this should be optional... +// XXX is this a good idea??? +//var dateparser = require('any-date-parser') + +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) }, + + get title(){ + return pwpath.decodeElem(this.name) }, + set title(value){ + this.name = pwpath.encodeElem(value) }, + + get isPattern(){ + return this.path.includes('*') }, + + // XXX EXPERIMENTAL... + get ctime(){ + var that = this + return Promise.awaitOrRun( + this.data, + function(data){ + var t = (data ?? {}).ctime + return t ? + new Date(t).getTimeStamp() + : t }) }, + get mtime(){ + var that = this + return Promise.awaitOrRun( + this.data, + function(data){ + var t = (data ?? {}).mtime + return t ? + new Date(t).getTimeStamp() + : t }) }, + /*/ // XXX ASYNC... + 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 this.__energetic === true + || ((this.actions + && this.actions[this.name] + && !!this[ + this.actions[this.name] === true ? + this.name + : this.actions[this.name] ].energetic) + || Promise.awaitOrRun( + this.store.isEnergetic(this.path), + function(res){ + return !!res })) }, + set energetic(value){ + this.__energetic = value }, + + // page data... + // + strict: undefined, + get data(){ + var that = this + // 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}) + return Promise.awaitOrRun( + (this.isPattern + && !this.__energetic + && !page[name].energetic) ? + page + .map(function(page){ + var res = page[name] + return typeof(res) == 'function' ? + res.bind(page.get(name, {args})) + : function(){ + return res } }) + : page[name], + function(res){ + return typeof(res) == 'function' ? + res.bind(that) + : res instanceof Array ? + res + : function(){ + return res } }, + // NOTE: we are passing null into the error handler to + // prevent the actual data (function) from being + // consumed... + null) } + // store data... + return Promise.awaitOrRun( + this.energetic, + function(energetic){ + // pattern... + // NOTE: we need to make sure each page gets the chance to handle + // its context (i.e. bind action to page).... + if(that.isPattern + && !energetic){ + return that + .map(function(page){ + return page.data }) } + // single page... + return Promise.awaitOrRun( + that.store.get(that.path, !!that.strict, !!energetic), + function(res){ + return typeof(res) == 'function' ? + res.bind(that) + : res }) }) }, + 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){ + // clear... + if(arguments.length > 0 + && value == null){ + return this.__delete__() } + // set... + this.__update__(value) }, + + // XXX ASYNC??? + 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 which match should we use??? + //match: relMatchProxy('match'), + match: function(path='.', strict=false){ + var that = this + if(path === true || path === false){ + strict = path + path = '.' } + path = pwpath.relative(this.path, path) + return Promise.awaitOrRun( + this.store.match(path, strict), + function(res){ + return res.length == 0 ? + // XXX are we going outside of match semantics here??? + that.store.find(path) + : res }) }, + /*/ // XXX ASYNC... + 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: function(path, strict){ + var that = this + if(path === true || path === false){ + strict = path + path = null } + strict = strict + ?? this.strict + // 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('*') ? + Promise.awaitOrRun( + this.energetic, + this.store.isEnergetic(path), + function(a, b){ + return !(a || b) ? + that.resolve(path) + : path }) + : path + paths = Promise.awaitOrRun( + paths, + function(paths){ + return (paths instanceof Array + || paths instanceof Promise) ? + paths + : [paths] }) + return Promise.iter( + paths, + function(path){ + return strict ? + Promise.awaitOrRun( + that.exists('/'+path), + function(exists){ + return exists ? + that.get('/'+ path) + : [] }) + : that.get('/'+ path) }) + .sync() }, + // XXX is this correct here??? + [Symbol.asyncIterator]: async function*(){ + yield* this.each() }, + + map: function(func){ + return this.each().map(func) }, + filter: function(func){ + return this.each().filter(func) }, + reduce: function(func, dfl){ + return this.each().reduce(func, dfl) }, + + // sorting... + // + // XXX revise how we sore order... + sort: async function(...cmp){ + // normalize to path... + this.metadata = + { order: await this.store.sort(this.path, ...cmp) } + return this }, + // .sortAs() + // .sortAs([, .. ]) + sortAs: async function(order){ + this.metadata = + order instanceof Array ? + { order: order + .map(function(p){ + return pwpath.sanitize(p) }) } + : { order: (await this.metadata)['order_'+ order] } + return this }, + // .saveSortAs() + // .saveSortAs(, [, .. ]) + saveSortAs: async function(name, order=null){ + order = order + ?? (await this.metadata).order + this.metadata = {['order_'+ name]: order} + 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) + + + +//--------------------------------------------------------------------- + +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}
` }, + }, + + // XXX EXPERIMENTAL... + // + // Define a global macro... + // .defmacro(, ) + // .defmacro(, , ) + // -> this + // + /* XXX do we need this??? + defmacro: function(name, args, func){ + this.__parser__.macros[name] = + arguments.length == 2 ? + arguments[1] + : Macro(args, func) + return this }, + //*/ + + + // 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: { + ...module.BasePage.prototype.actions, + + '!': true, + // XXX EXPERIMENTAL... + quote: true, + }, + + // XXX should this be .raw or .parse()??? + '!': Object.assign( + function(){ + return this.get('..:$ARGS', {energetic: true}).raw }, + {energetic: true}), + // XXX EXPERIMENTAL... + // XXX this is html/web specific, should it be here??? + // ... + // XXX should this be .raw or .parse()??? + // XXX ASYNC??? + quote: async function(energetic=false){ + return Promise.awaitOrRun( + this.get('..:$ARGS', {energetic: await this.energetic}).raw, + function(res){ + return res instanceof Array ? + res.map(pwpath.quoteHTML) + : pwpath.quoteHTML(res) }) }, + + + // 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... + // + // NOTE: .__debug_last_render_state is mainly exposed for introspection + // and debugging, set comment it out to disable... + //__debug_last_render_state: undefined, + // XXX should this handle pattern paths??? + // XXX this might be a good spot to cache .raw in state... + parse: function(text, state){ + var that = this + // .parser() + if(arguments.length == 1 + && text instanceof Object + && !(text instanceof Array)){ + state = text + text = null } + return Promise.awaitOrRun( + //text, + text + ?? this.raw, + function(text){ + state = state ?? {} + state.renderer = state.renderer ?? that + // this is here for debugging and introspection... + '__debug_last_render_state' in that + && (that.__debug_last_render_state = state) + // parse... + return that.__parser__.parse( + that.get('.', { + renderer: state.renderer, + args: that.args, + }), + text, + state) }) }, + + // 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(){ + var that = this + return Promise.awaitOrRun( + this.data, + function(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) + return Promise.awaitOrRun( + msg.match(), + function(msg){ + 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 ? + // XXX + Promise.all(data + .map(function(d){ + return typeof(d) == 'function'? + d() + : d.text }) + .flat()) + : data.text ) }, + null) }, + set raw(value){ + this.data = {text: value} }, + //this.onTextUpdate(value) }, + + // iterate matches or content list as pages... + // + // .asPages() + // .asPages([, ]) + // .asPages([, ]) + // .asPages(, [, ]) + // .asPages() + // -> + // + // NOTE: this will get .raw for non-pattern pages this it can trigger + // actions... + // + // XXX revise name... + // XXX do we need both this and .each(..) + // ...what is the difference??? + // - handle path slightly differently... + // - .asPages(..) handles list/generator pages... + // XXX BUG: this does not respect strict of single pages if they do + // not exist... + // ...see: @macro(..) bug + .each(..) + // FIXED: revise... + asPages: async function*(path='.:$ARGS', strict=false){ + // options... + var args = [...arguments] + var opts = typeof(args.at(-1)) == 'object' ? + args.pop() + : {} + var {path, strict} = { + ...opts, + path: typeof(args[0]) == 'string' ? + args.shift() + : '.:$ARGS', + strict: args.shift() + ?? false, + } + + var page = this.get(path, strict) + // each... + if(page.isPattern){ + yield* page + // handle lists in pages (actions, ... etc.)... + } else { + // strict + page does not exist... + if(strict + && !(await page.exists())){ + return } + var data = await page.data + data = + data instanceof types.Generator ? + await data() + : typeof(data) == 'function' ? + data + : data && 'text' in data ? + data.text + : null + if(data instanceof Array + || data instanceof types.Generator){ + yield* data + .map(function(p){ + return page.virtual({text: p}) }) + return } + // do not iterate pages/actions that are undefined... + if(data == null){ + return } + + 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 should render templates (_view and the like) be a special case + // or render as any other page??? + // ...currently they are rendered in the context of the page and + // not in their own context... + // XXX revise how we handle strict mode... + // XXX would be nice to be able to chain .awaitOrRun(..) calls instead + // of nesting them like here... + get text(){ + var that = this + return Promise.awaitOrRun( + !that.strict + || that.resolve(true), + function(exists){ + // strict mode -- break on non-existing pages... + if(!exists){ + throw new Error('NOT FOUND ERROR: '+ that.location) } + + var path = pwpath.split(that.path) + ;(path.at(-1) ?? '')[0] == '_' + || path.push(that.PAGE_TEMPLATE) + var tpl = pwpath.join(path) + var tpl_name = path.pop() + //var tpl_name = path.at(-1) + + // get the template relative to the top most pattern... + return Promise.awaitOrRun( + that.get(tpl).find(true), + function(tpl){ + if(!tpl){ + console.warn('UNKNOWN RENDER TEMPLATE: '+ tpl_name) + return that.get(that.NOT_FOUND_TEMPLATE_ERROR).parse() } + + var depends = that.depends = new Set([tpl]) + // do the parse... + // NOTE: we render the template in context of page... + return that + // NOTE: that.path can both contain a template and not, this + // normalizes it to the path up to the template path... + .get(path, {args: that.args}) + .parse( + that.get( + '/'+tpl, + {args: that.args}).raw, + { + depends, + renderer: that, + }) }) }) }, + set text(value){ + this.data = {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) }, +}) + + + +//--------------------------------------------------------------------- +// Markdown renderer... +// XXX EXPERIMENTAL... + +var showdown = require('showdown') + +var MarkdownPage = +module.MarkdownPage = +object.Constructor('MarkdownPage', Page, { + actions: { + ...module.Page.prototype.actions, + + html: true, + }, + + markdown: new showdown.Converter(), + + get html(){ return async function(){ + return this.markdown.makeHtml(await this.raw) }.call(this) }, + set html(value){ + this.raw = this.markdown.makeMarkdown(value) }, +}) + +// XXX HACK... +var Page = MarkdownPage + + +//--------------------------------------------------------------------- +// Cached .text page... + +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 toc = require('./dom/toc') +var wikiword = require('./dom/wikiword') +//var textarea = require('./dom/textarea') + +var pWikiPageElement = +module.pWikiPageElement = +// XXX CACHE... +object.Constructor('pWikiPageElement', CachedPage, { +/*/ +object.Constructor('pWikiPageElement', Page, { +//*/ + dom: undefined, + + + domFilters: { + toc: toc.makeToc, + // XXX see Page.filters.wikiword for notes... + wikiword: wikiword.wikiWordText, + //textarea: textarea.setupTextarea, + }, + + // 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: { + ...CachedPage.prototype.actions, + + hash: true, + }, + + + // 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 = '#' + : value.includes('#') ? + value.split('#') + // no hash is given... + : [value, undefined] + this.hash = hash + object.parentProperty(pWikiPageElement.prototype, 'location') + .set.call(this, value) }, + + // events... + // + __pWikiLoadedDOMEvent: new Event('pwikiloaded'), + onLoad: types.event.PureEvent('onLoad', function(){ + this.dom.dispatchEvent(this.__pWikiLoadedDOMEvent) }), + + // XXX CACHE... + __last_refresh_location: undefined, + refresh: types.event.Event('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.trigger('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_TEMPLATE + // + // Example: + // _list: { + // text: '- @source(.)' }, + // + // XXX might be a good idea to add a history stack to the API (macros?) + // XXX all of these should support pattern pages... + _text: { + text: '@include(.:$ARGS isolated join="@source(file-separator)")' }, + _view: { + text: object.doc` + + + + + + + + 🡠 + 🡢 + 🡡 + +    + + [@source(./location/quote/!)] + +    + + + + + +
+ +
+ + + + +

@source(./title/quote/!)

+ @include(.:$ARGS join="@source(file-separator)" recursive="") +
` }, + // XXX add join... + _raw: { + text: '@quote(.)' }, + + // XXX not sure if this is the right way to go... + _code: { + text: + '' + +'
' + +'
'}, + /* XXX can we reuse _view here??? + _edit: { + text: + '@include(PageTemplate)' + +'@source(./path)' + +'' + +'' + +'
'
+						+''
+					+'
' + +'
' + +'
'}, + /*/ + _edit: { + text: + '@source(./path/quote/!)' + +'
' + +'' + +'

' + +'@source(./title/quote)' + +'

' + +'
'
+					+''
+				+'
' + +'
'}, + edit: { + // XXX not sure if we should use .title or .name here... + text: object.doc` + +

+ + @source(./title/quote) + + @macro(src="." + strict + else='new') +

+
+ +

+					/>
+
+ + @macro(titleeditor .:$ARGS) + @macro(texteditor .:$ARGS) + + + + @source(../title) (edit) + + ../.. + @source(../location/quote/!) + + + + `}, + // XXX EXPERIMENTAL... + ed: { + text: object.doc` @source(../ed-visual) `}, + 'ed-visual': { + text: object.doc` + @load(./edit) + + + +
+ @quote(./html) +
+
+ + + + + + +
+ visual + | text +
+
`}, + 'ed-text': { + text: object.doc` + @load(./edit) + + +
+
+ + + + + + +
+ visual + | text +
+
`}, + + // XXX debug... + _path: {text: '@source(./path/! join=" ")'}, + _location: {text: '@source(./location/! join=" ")'}, + + + list: { + text: object.doc` + + + 🡠 + 🡢 + 🡡 + +    + @source(../path) + + + @var(path "@source(s ./path)") + @source(./name/quote) + + + a + + s + + + + (@include(./*/length/!)) +   + × + ` }, + // XXX this is really slow... + // XXX need to handle count/offset arguments correctly... + // ...for this we'll need to be able to either: + // - count our own pages or + // - keep a global count + // ...with offset the issue is not solvable because we will not + // see/count the children of skipped nodes -- the only way to + // solve this is to completely handle offset in macro... + tree: { + text: object.doc` + + + + + @var(path "@source(s ./path)") +
+ +
+ @macro(tree "./*:$ARGS:count=inherit") +
+
+
` }, + /* XXX @var(..) vs. multiple @source(..) calls are not that different... + tree2: { + text: object.doc` + + + This is a comparison with [../tree] -- \\@var(..) vs direct macro call...

+ + +
+ +
+ @macro(tree "./*:$ARGS") +
+
+
` }, + //*/ + all: { + text: `@include("../**/path:$ARGS" join="@source(line-separator)")`}, + info: { + text: object.doc` + + @source(../title/quote) (info) + + +

@source(../title/quote)

+
+ + Path: [@source(../path/quote)] + (edit)
+ + Resolved path: [/@source(../resolved/quote)]
+
+ Referrer: [@source(../referrer/quote)]
+ Args:
+ + type: @source(../type)
+ + tags: + + @source(.) +
+ related tags: +
+ + ctime: @source(../ctime)
+ mtime: @source(../mtime)
+ +
+ Resolved text: +
+
` }, + + // XXX need to also be able to list things about each store... + stores: function(){ + return Object.keys(this.store.substores ?? {}) }, + + // tags... + // + // XXX should these be actions??? + // ...actions do not yet support lists/generators... + tags: async function*(){ + yield* this.get('..').tags }, + allTags: async function*(){ + yield* Object.keys((await this.store.tags).tags) }, + relatedTags: async function*(){ + yield* this.store.relatedTags( + ...((await this.args.tags) + ?? this.get('..').tags + ?? [])) }, + + // page parts... + // + 'line-separator': { text: '
' }, + 'file-separator': { text: '
' }, + + // base system pages... + // + // NOTE: these are last resort pages, preferably overloaded in /Templates. + // + ParseError: { + text: object.doc` + +
+
ParseError: @(msg "no message")
+ Page: [@(path "@source(./path/quote)")] +
`,}, + RecursionError: { + text: 'RECURSION ERROR: @source(../path/quote)' }, + NotFoundError: { + //text: 'NOT FOUND ERROR: @source(./path)' }, + text: object.doc` + + +

NOT FOUND ERROR: @source(./path)

+ + +
+ Nested pages:
+ +
+
` }, + NotFoundTemplateError: { + text: 'NOT FOUND TEMPLATE ERROR: @source(../path/quote)' }, + + DeletingPage: { + text: 'Deleting: @source(../path/quote)' }, + + PageTemplate: { + text: object.doc` + @source(./path/quote)/edit +
+ +
+ ` }, + QuoteActionPage: { + text: '[ native code ]' }, + + Style: { + text: object.doc` + ` }, + + // page actions... + // + + // XXX this does not work as energetic... + time: async function(){ + var t = Date.now() + var text = await this.get('../_text:$ARGS').text + var time = Date.now() - t + console.log('RENDER TIME:', time) + return object.doc` + + Time to render: ${time}ms
+
+ ${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 need a stable way to redirect after the action... + // ...not sure if these are needed vs. pwiki.delete(..) and friends... + // actions... + // + // XXX should ** be the default here... + delete: function(){ + var target = this.get('..') + + console.log('DELETE:', target.path) + + target.isPattern ? + target.delete() + : target.delete('**') + + // XXX + if(this.referrer == this.path){ + this.renderer.path = '..' + return '' } + + // redirect... + this.renderer + && (this.renderer.location = this.referrer) + // XXX this should not be needed... + && this.renderer.refresh() + // XXX returning undefined will stop the redirect... + return '' }, + // NOTE: this moves relative to the basedir and not relative to the + // page... + move: async 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) + + to + && await (from.isPattern ? + from + : from.get('**')) + .move( + /^[\\\/]/.test(to[0]) ? + to + : pwpath.join('..', to)) + + // redirect... + this.renderer + && to + //&& (this.renderer.location = this.referrer) + && (this.renderer.location = from.path) + // XXX this should not be needed... + && this.renderer.refresh() + // 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 copy/... + //*/ + + // XXX System/sort + // XXX System/reverse +} + +var Templates = +module.Templates = { + // XXX should this be in templates??? + // XXX for some reason this does not list files... + FlatNotes: { + text: object.doc` + @var(editor_template FlatNotes/EmptyPage) + + + 🗎 + + @var(path "@source(s ./path)") + + + Empty + + ` }, + // XXX this is not resolved... + 'FlatNotes/EmptyPage': {text: ' '}, +} + +var Test = +module.Test = { + // XXX do we support this??? + //'list/action': function(){ + // return [...'abcdef'] }, + 'list/generator': function*(){ + yield* [...'abcdef'] }, + 'list/static': { + text: [...'abcdef'] }, + + // this is shown by most listers by adding an :all argument to the url... + '.hidden': { + text: 'Hidden page...' }, + '.hidden/subpage': { + text: 'visible subpage...' }, + '.hidden/.hidden': { + text: 'hidden subpage...' }, + + slots: { + text: object.doc` + Sequential: + unfilled + filled + refilled +

+ Nested: + + unfilled + + filled + + refilled + + + +

+ Content: A B C: + A + B + C +

+ Nested content: A B C: + + A + + B + + C + + + +

+ Mixed content: X A B C Z: + + X + + + A + + B + + + C + + + + Z + ` }, + macros: { + text: object.doc` + + - @include(./path) + + + +

+ + `}, + + Subtree: { + text: object.doc` + This is here to test the performance of macros:
+ ./list
+ ./tree
+ ./**/path
` }, +} +// Generate pages... +PAGES=100 +for(var i=0; i' }, + // XXX need an import button... + Export: { + text: '' }, + // XXX + Config: { + text: object.doc`{ + }` }, + Style: { + text: object.doc` + ` }, +} + + + +/********************************************************************** +* vim:set ts=4 sw=4 nowrap : */ return module }) diff --git a/v3/pwiki/parser.js b/v3/pwiki/parser.js new file mode 100644 index 0000000..7f05e5d --- /dev/null +++ b/v3/pwiki/parser.js @@ -0,0 +1,1838 @@ +/********************************************************************** +* +* +* +**********************************************************************/ +((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') + + + + + +//--------------------------------------------------------------------- +// Parser... + +// XXX ASAP move the macros here... +// XXX should we warn about stuff like -- currently +// this will simply be ignored, i.e. passed trough the parser +// without change... +// XXX might be a good idea to both think of a good async parse and +// create tools for sync parsing (get links etc.)... +// XXX need to correctly handle nested and escaped quotes... +// i.e. +// "aaa \"bbb \\"ccc\\" bbb\" aaa" + +// XXX RENAME... +// ...this handles the syntax and lexing... +var BaseParser = +module.BaseParser = { + // patterns... + // + // The way the patterns are organized might seem a bit overcomplicated + // and it has to be to be able to reuse the same pattern in different + // contexts, e.g. the arguments pattern... + + // + // needs: + // STOP -- '\\>' or ')' + // PREFIX -- 'inline' or 'elem' + // + // XXX should we support unquoted macros as single arguments??? + // i.e. '@(aaa @(bbb ccc))' should collect '@(bbb ccc)' as an + // argument, currently this will be stplit at whitespace... + // ...this is logical but then we'll need to group all nested + // levels, not yet sure how to do this cleanly (one way is to + // write a dedicated parser) + MACRO_ARGS: ['(\\s*(',[ + // arg='val' | arg="val" | arg=val + '(?[a-z:-_]+)\\s*=\\s*(?'+([ + '"(?(\\"|[^"])*?)"', + "'(?(\\'|[^'])*?)'", + '(?[^\\sSTOP\'"]+)', + ].join('|'))+')', + // "arg" | 'arg' + '"(?(\\"|[^"])*?)"', + "'(?(\\'|[^'])*?)'", + // arg + // NOTE: this is last because it could eat up parts of + // the above alternatives... + //'|\\s+[^\\s\\/>\'"]+', + '(?[^\\sSTOP\'"]+)', + ].join('|'), + '))'].join(''), + MACRO_ARGS_PATTERN: undefined, + // + // .buildArgsPattern([, [, ]]) + // -> + // + // .buildArgsPattern([, [, false]]) + // -> + // + buildArgsPattern: function(prefix='elem', stop='', regexp='smig'){ + var pattern = this.MACRO_ARGS + .replace(/PREFIX/g, prefix) + .replace(/STOP/g, stop) + return regexp ? + new RegExp(pattern, regexp) + : pattern }, + + // + // needs: + // MACROS + // INLINE_ARGS + // UNNAMED_ARGS + // ARGS + // + // XXX BUG?: this does not consume closing ')'... + // `A @(aaa @(bbb)) B` + // -> ['A', '@(aaa', '@(bbb', ')) B'] (simplified) + // This works correctly: + // `A @(aaa "@(bbb)") B` + MACRO: '('+([ + // @macro(arg ..) + '\\\\?@(?MACROS)\\((?INLINE_ARGS)\\)', + // @(arg ..) + '\\\\?@\\((?UNNAMED_ARGS)\\)', + // | + '<\\s*(?MACROS)(?\\sARGS)?\\s*/?>', + // + 'MACROS)\\s*>', + ].join('|'))+')', + MACRO_PATTERN: undefined, + MACRO_PATTERN_GROUPS: undefined, + // + // .buildMacroPattern([, ]) + // -> + // + // .buildMacroPattern([, false]) + // -> + // + buildMacroPattern: function(macros=['MACROS'], regexp='smig'){ + var pattern = this.MACRO + .replace(/MACROS/g, + macros + .filter(function(m){ + return m.length > 0 }) + .join('|')) + .replace(/INLINE_ARGS/g, + this.buildArgsPattern('inline', ')', false) +'*') + .replace(/UNNAMED_ARGS/g, + this.buildArgsPattern('unnamed', ')', false) +'*') + .replace(/ARGS/g, + this.buildArgsPattern('elem', '\\/>', false) +'*') + return regexp ? + new RegExp(pattern, regexp) + : pattern }, + countMacroPatternGroups: function(){ + // NOTE: the -2 here is to compensate for the leading and trailing ""'s... + return ''.split(this.buildMacroPattern()).length - 2 }, + + // XXX should this be closer to .stripComments(..) + // XXX do we need basic inline and block commets a-la lisp??? + COMMENT_PATTERN: RegExp('('+[ + // + '', + + // .. + '<\\s*pwiki-comment[^>]*>.*?<\\/\\s*pwiki-comment\\s*>', + // + '<\\s*pwiki-comment[^\\/>]*\\/>', + + // html comments... + '', + ].join('|') +')', 'smig'), + + + // helpers... + // + normalizeFilters: function(filters){ + var skip = new Set() + return filters + .flat() + .tailUnique() + .filter(function(filter){ + filter[0] == '-' + && skip.add(filter.slice(1)) + return filter[0] != '-' }) + .filter(function(filter){ + return !skip.has(filter) })}, + // + // Spec format: + // [, ... [, ...]] + // + // Keyword arguments if given without a value are true by default, + // explicitly setting a keyword argument to 'true' or 'yes' will set + // it to true, explicitly setting to 'false' or 'no' will set it to + // false, any other value will be set as-is... + // + // NOTE: the input to this is formatted by .lex(..) + // NOTE: arg pre-parsing is dome by .lex(..) but at that stage we do not + // yet touch the actual macros (we need them to get the .arg_spec) + // so the actual parsing is done in .expand(..) + parseArgs: function(spec, args){ + // spec... + var order = spec.slice() + var bools = new Set( + order[order.length-1] instanceof Array ? + order.pop() + : []) + order = order + .filter(function(k){ + return !(k in args) }) + + var res = {} + var pos = Object.entries(args) + // stage 1: populate res with explicit data and place the rest in pos... + .reduce(function(pos, [key, value]){ + ;/^[0-9]+$/.test(key) ? + (bools.has(value) ? + // bool... + (res[value] = true) + // positional... + : (pos[key*1] = value)) + // keyword/bool default values... + : bools.has(key) ? + (res[key] = + // value escaping... + value[0] == '\\' ? + value.slice(1) + : (value == 'true' || value == 'yes') ? + true + : (value == 'false' || value == 'no') ? + false + : value) + // keyword... + : (res[key] = value) + return pos }, []) + // stage 2: populate implicit values from pos... + .forEach(function(e, i){ + order.length == 0 ? + (res[e] = true) + : (res[order.shift()] = e) }) + return res }, + callMacro: function(page, macro, args, body, state, ...rest){ + do { + macro = this.macros[macro] + } while(typeof(macro) == 'string') + return macro.call(page, + this, + this.parseArgs( + macro.arg_spec + ?? [], + args), + body, + state, + ...rest) }, + + + // Strip comments... + // + stripComments: function(str){ + return str + .replace(this.COMMENT_PATTERN, + function(...a){ + return a.pop().uncomment + || '' }) }, + + // Lexically split the string (generator)... + // + // ::= + // + // | { + // name: , + // type: 'inline' + // | 'element' + // | 'opening' + // | 'closing', + // args: { + // : , + // : , + // ... + // } + // match: , + // } + // + // + // NOTE: this internally uses .macros' keys to generate the + // lexing pattern. + lex: function*(page, str){ + str = typeof(str) != 'string' ? + str+'' + : str + // XXX we can't get .raw from the page without going async... + //str = str + // ?? page.raw + // NOTE: we are doing a separate pass for comments to completely + // decouple them from the base macro syntax, making them fully + // transparent... + str = this.stripComments(str) + + // XXX should this be cached??? + var macro_pattern = this.MACRO_PATTERN + ?? this.buildMacroPattern(Object.deepKeys(this.macros)) + var macro_pattern_groups = this.MACRO_PATTERN_GROUPS + ?? this.countMacroPatternGroups() + var macro_args_pattern = this.MACRO_ARGS_PATTERN + ?? this.buildArgsPattern() + + var lst = str.split(macro_pattern) + + var macro = false + while(lst.length > 0){ + if(macro){ + var match = lst.splice(0, macro_pattern_groups)[0] + // NOTE: we essentially are parsing the detected macro a + // second time here, this gives us access to named groups + // avoiding maintaining match indexes with the .split(..) + // output... + // XXX for some reason .match(..) here returns a list with a string... + var cur = [...match.matchAll(macro_pattern)][0].groups + // special case: escaped inline macro -> keep as text... + if(match.startsWith('\\@')){ + yield match + macro = false + continue } + // args... + var args = {} + var i = -1 + for(var {groups} + of (cur.argsInline + ?? cur.argsUnnamed + ?? cur.argsOpen + ?? '') + .matchAll(macro_args_pattern)){ + i++ + args[groups.elemArgName + ?? groups.inlineArgName + ?? groups.unnamedArgName + ?? i] = + (groups.elemSingleQuotedValue + ?? groups.inlineSingleQuotedValue + ?? groups.unnamedSingleQuotedValue + ?? groups.elemDoubleQuotedValue + ?? groups.inlineDoubleQuotedValue + ?? groups.unnamedDoubleQuotedValue + ?? groups.elemValue + ?? groups.inlineValue + ?? groups.unnamedValue + ?? groups.elemSingleQuotedArg + ?? groups.inlineSingleQuotedArg + ?? groups.unnamedSingleQuotedArg + ?? groups.elemDoubleQuotedArg + ?? groups.inlineDoubleQuotedArg + ?? groups.unnamedDoubleQuotedArg + ?? groups.elemArg + ?? groups.inlineArg + ?? groups.unnamedArg) + .replace(/\\(["'])/g, '$1') } + + // macro-spec... + yield { + name: (cur.nameInline + ?? cur.nameOpen + ?? cur.nameClose + ?? '') + .toLowerCase(), + type: match[0] == '@' ? + 'inline' + : match[1] == '/' ? + 'closing' + : match[match.length-2] == '/' ? + 'element' + : 'opening', + args, + match, + } + macro = false + // normal text... + } else { + var str = lst.shift() + // skip empty strings from output... + if(str != ''){ + yield str } + macro = true } } }, + + // NOTE: so as to avod cluterring the main parser flow the macros are + // defined separtly below... + macros: undefined, + + // Group block elements (generator)... + // + // ::= + // + // | { + // type: 'inline' + // | 'element' + // | 'block', + // body: [ + // , + // ... + // ], + // + // // rest of items are the same as for lex(..) + // ... + // } + // + // NOTE: this internaly uses .macros to check for propper nesting + //group: function*(page, lex, to=false){ + group: function*(page, lex, to=false, parent){ + // XXX we can't get .raw from the page without going async... + //lex = lex + // ?? this.lex(page) + lex = typeof(lex) != 'object' ? + this.lex(page, lex) + : lex + + var quoting = to + && (page.QUOTING_MACROS ?? []).includes(to) + && [] + + // NOTE: we are not using for .. of .. here as it depletes the + // generator even if the end is not reached... + while(true){ + var {value, done} = lex.next() + // check if unclosed blocks remaining... + if(done){ + if(to){ + throw new Error( + 'Premature end of input: Expected ') } + return } + + // special case: quoting -> collect text... + // NOTE: we do not care about nesting here... + if(quoting !== false){ + if(value.name == to + && value.type == 'closing'){ + yield quoting.join('') + return + } else { + quoting.push( + typeof(value) == 'string' ? + value + : value.match ) } + continue } + + // assert nesting rules... + // NOTE: we only check for direct nesting... + // XXX might be a good idea to link nested block to the parent... + if(this.macros[value.name] instanceof Array + && !this.macros[value.name].includes(to) + // do not complain about closing nestable tags... + && !(value.name == to + && value.type == 'closing')){ + throw new Error( + 'Unexpected <'+ value.name +'> macro' + +(to ? + ' in <'+to+'>' + : '')) } + // open block... + if(value.type == 'opening'){ + //value.body = [...this.group(page, lex, value.name)] + value.body = [...this.group(page, lex, value.name, value)] + value.type = 'block' + // close block... + } else if(value.type == 'closing'){ + if(value.name != to){ + throw new Error('Unexpected ') } + // NOTE: we are intentionally not yielding the value here... + return } + // normal value... + yield value } }, + + // XXX do we need a pre-parse stage??? + // - expand local macros + // - collect links + // - ... + ast: function(...args){ + return [...this.group(...args)] }, + + // Expand macros (stage I)... + // + // .expand(, [, ]) + // -> + // + // + // ::= [ , .. ] + // ::= + // { + // + // value: ..., + // } + // | + // + // NOTE: the returned structure is re-expandable. + // + // + // ::= { + // waitNested: | null, + // wait: | null, + // } + // + // + // XXX the parser is always sync + // - macros always return sync but can be resolved or not resolved + // - each level is always sequential + // - isolated macros can be resolved in any order + // - macros can wait on: + // - nested + // e.g. a @var needs all the previous @var's to resolve + // but does not care about isolated @include's + // - full + // a full include needs all nesteds to resolve, both + // isolated and not + // - first nested item in one macro waits for last relevant + // nested item in previous macro -> i.e. for previous + // macro to resolve in a relevant way... + // - everything returns an ast + // - callbacks/events/handlers to trigger on specific macro + // resolution + // XXX Remove async/await... (???) + // - for trivial stuff they are nice + // - infectios -- force all subsequent levels to use async/await + // - provide no alternatives + // ...i.e. promises can be extended to indlude partial + // states and the like, async/await can't... + // XXX Q: do we need generators? + // XXX Handle errors... + fullAST: true, + // XXX this needs a careful rewrite of the .macros.* for the new scheme: + // - expand + // - "merge" + // a rendering API: a set ov events/callbacks allowing both + // sync (text) and async (DOM) rendering + // in the simplest form: take the expanded AST and merge + // into a single string + expand: function(page, ast, state={}){ + var that = this + ast = typeof(ast) != 'object' ? + this.group(page, ast) + : ast instanceof types.Generator ? + ast + : ast.iter() + + // XXX revise names... + var wait = new Set() + state.wait instanceof Promise + && wait.add(state.wait) + var resolve, reject + state.wait = new Promise( + function(){ + ;[resolve, reject] = arguments }) + + var elems = [] + for(let elem of ast){ + // text block... + if(typeof(elem) == 'string'){ + elems.push(elem) + continue } + // do not re-expand expanded elements... + if('value' in elem + && !state.forceReExpand){ + elems.push({...elem}) + continue } + + var {name, args, body} = elem + + // XXX revise -- do we need this??? + // nested macro -- skip... + if(typeof(that.macros[name]) != 'function' + && typeof(that.macros[name]) != 'string'){ + continue } + + // cleanup... + elem = {...elem} + delete elem.error + delete elem.value + //delete elem.resolving + + // call macro... + var res = that.callMacro(page, name, args, body, state) + // async... + if(res instanceof Promise){ + elem.resolving = res + wait.add(res) + if(!res.isolated){ + state.waitNested = res } + res.then( + function(value){ + wait.delete(res) + if(state.waitNested === res){ + delete state.waitNested } + delete elem.resolving + elem.value = value + return value }, + function(err){ + state.errors ??= [] + state.errors.push(elem) + delete elem.resolving + elem.error = err }) + // sync... + } else if('fullAST' in state ? + state.fullAST + : 'fullAST' in page ? + page.fullAST + : this.fullAST){ + elem.value = res + } else { + elem = res } + elems.push(elem) } + + // handle wait for isolated macros to resolve... + var done = function(){ + resolve(state) + delete state.wait } + if(wait.size > 0){ + Promise.all(wait) + .then( + done, + function(err){ + reject(err) }) + // done... + } else { + done() } + + return elems }, + + // resolve stage II macros and merge results... + // + resolve: function(page, ast, state={}){ + var that = this + ast = typeof(ast) != 'object' ? + this.expand(page, ast, state) + : ast instanceof types.Generator ? + ast + : ast.iter() + + // merge resolved elements into the last item of elems... + var elems = [] + var merge = function(...args){ + var prev = '' + while(args.length > 0){ + while(args.length > 0 + && typeof(args[0]) != 'object' + && typeof(args[0]) != 'function'){ + prev += args.shift() } + // merged section... + if(prev.length > 0){ + if(elems.length > 0 + && typeof(elems.at(-1)) == 'string'){ + elems[elems.length-1] += prev + } else { + elems.push(prev) } + prev = '' } + if(args.length > 0){ + elems.push(args.shift()) } } } + + for(var elem of ast){ + // nesting... + while(elem.value){ + elem = elem.value + // exec stage II macros... + if(typeof(elem) == 'function'){ + elem = elem(state) } } + // atomic values... + if(typeof(elem) != 'object'){ + merge(elem) + continue } + // value is resolved but "empty" -> skip... + if('value' in elem + && (elem.value == null + || elem.value == '')){ + continue } + // expand ast... + if(elem instanceof Array){ + merge(...this.resolve(page, elem, state)) + continue } + // unresolved... + merge(elem) } + + return elems }, + + // XXX + parseNested: function(page, ast, state={}){ + var that = this + ast = this.resolve(page, ast, state) + // XXX + if(state.waitNested){ + // XXX for some odd reason returning this breaks everything... + return state.waitNested + .then(function(){ + return that.parse(page, ast, state) }) } + // XXX + if(ast.length > 1){ + throw new Error('!!!!') + } + return ast[0] }, + + parse: function(page, ast, state={}){ + var that = this + ast = this.resolve(page, ast, state) + // XXX + if(state.wait){ + // XXX for some odd reason returning this breaks everything... + return state.wait + .then(function(){ + return that.parse(page, ast, state) }) } + // XXX + if(ast.length > 1){ + throw new Error('!!!!') + } + return ast[0] }, + + + + // Expand macros... + // + // ::= [ , .. ] + // ::= + // + // | + // | + // | { skip: true, ... } + // | { data: } + // | + // + // XXX macros: we are mixing up ast state and parse state... + // one should only be used for parsing and be forgotten after + // the ast is constructed the other should be part of the ast... + // XXX DOC inconsistent await behavior... + // in var macro, as an example, there are two paths: the assign and + // the get, the assign calls .parse(..) and awaits for the result + // while the get path is "sync", this results in the first exec path + // getting pushed to the next execution frame while the second is run + // in sync with the caller, here is a simplified demo: + // console.log(1) + // // note that we are NOTE await'ing for the function here... + // (async function f(){ + // console.log(2)})() + // console.log(3) + // -> prints 1, 2, 3 + // and: + // console.log(1) + // (async function f(){ + // // note the await -- this is the only difference... + // console.log(await 2)})() + // console.log(3) + // -> prints 1, 3, 2 + // this could both be a bug or a feature depending on how you look + // at it, but it makes promise sequencing very unpredictable... + // ...another problem here is that in the var macro, simply adding + // an await of something does not fix the issue, we need to await + // for something significant -- await this.parse('') works, while + // await vars[name] does not -- to the contrary of the above example... + /* XXX + expand: function(page, ast, state={}){ + var that = this + ast = ast == null ? + Promise.awaitOrRun( + page.raw, + function(raw){ + return that.group(page, raw ?? '') }) + : typeof(ast) != 'object' ? + this.group(page, ast) + : ast instanceof types.Generator ? + ast + : ast.iter() + + // NOTE this must execute sequentially for things that depend on + // lexical scope not to get lost in the mess... + return Promise.seqiter(ast, + function(value){ + // text block... + if(typeof(value) == 'string'){ + return value } + // macro... + var {name, args, body} = value + // nested macro -- skip... + if(typeof(that.macros[name]) != 'function'){ + return {...value, skip: true} } + // macro call... + return Promise.awaitOrRun( + that.callMacro(page, name, args, body, state), + function(res){ + res = res ?? '' + // result... + if(res instanceof Array + || that.macros[name] instanceof types.Generator){ + return res + } else { + return [res] } }) }, + function(err){ + console.error(err) + return [page.parse( + // XXX add line number and page path... + '@include("./ParseError' + +':path=' + // XXX use pwpath.encodeElem(..) ??? + + page.path + +':msg=' + + err.message + // quote html stuff... + .replace(/&/g, '&') + .replace(//g, '>') + // quote argument syntax... + .replace(/["']/g, function(c){ + return '%'+ c.charCodeAt().toString(16) }) + .replace(/:/g, ':') + .replace(/=/g, '=') + +'")')] }) + .sync() }, + //*/ + + + // recursively resolve and enumerate the ast... + // + // ::= [ , .. ] + // ::= + // + // | { data: } + // | + // + // (state) + // -> + // + // + // NOTE: (..) is called in the context of page... + // + // XXX should this also resolve e.data??? + /* XXX + resolve: function(page, ast, state={}){ + var that = this + ast = ast + ?? this.expand(page, null, state) + ast = typeof(ast) != 'object' ? + this.expand(page, ast, state) + : ast + // XXX .awaitOrRun(..) will check inside the input array for promises, do + // we need to do this??? + ast = Promise.awaitOrRun( + ast, + function(ast){ + var async_content = false + return ast + .map(function(e){ + // expand delayed sections... + e = typeof(e) == 'function' ? + e.call(page, state) + : e + // promise... + if(e instanceof Promise){ + async_content = true + return e + // expand arrays... + } else if(e instanceof Array + || e instanceof types.Generator){ + return that.resolve(page, e, state) + // data -- unwrap content... + } else if(e instanceof Object && 'data' in e){ + var res = Promise.awaitOrRun( + that.resolve(page, e.data, state), + function(e){ + return { data: e } }) + res instanceof Promise + && (async_content = true) + return res + // skipped items... + } else if(e instanceof Object && e.skip){ + return [] + } else { + return [e] } }) + // NOTE: if we still have promises in the ast, wrap the + // whole thing in a promise... + .run(function(){ + return async_content ? + Promise.iter(this) + : this }) + .flat() }) + return ast instanceof Promise ? + // keep the API consistently array-like... + ast.iter() + : ast }, + //*/ + + // Fully parse a page... + // + // This runs in two stages: + // - resolve the page + // - lex the page -- .lex(..) + // - group block elements -- .group(..) + // - expand macros -- .expand(..) + // - resolve ast -- .resolve(..) + // - apply filters + // + // NOTE: this has to synchronize everything between stage 1 (up to + // and including expand) and stage 2 (post-handlers, filters, ...) + // because the former need a fully loaded and expanded page if + // we want to do this in 2 stages and not 3... + // XXX might be fun to try a load-and-tweak approach the first + // version used -- i.e. setting placeholders and replacing + // them on demand rather than on encounter (as is now), e.g. + // a slot when loaded will replace the prior occurrences... + // + // XXX add a special filter to clear pending filters... (???) + /* XXX + parse: function(page, ast, state={}){ + var that = this + return this.resolve(page, ast, state) + // filters... + .map(function(section){ + // normalize types... + section = + typeof(section) == 'number' ? + section + '' + : section == null ? + '' + : section + return ( + // expand section... + typeof(section) != 'string' ? + section.data + // global filters... + : state.filters ? + that.normalizeFilters(state.filters) + .reduce(function(res, filter){ + // unknown filter... + // NOTE: we try not to break on user errors + // if we can help it... + if(page.filters[filter] == null){ + console.warn( + '.parse(..): unsupported filter: '+ filter) + return res } + // NOTE: if a filter returns falsy then it + // will have no effect on the result... + return page.filters[filter].call(page, res) + ?? res }, section) + // no global filters... + : section ) }) + .flat() + .join('') }, + //*/ +} + + +// XXX do we need anything else like .doc, attrs??? +// XXX might be a good idea to offload arg value parsing to here... +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()) + return func } + + +// XXX RENAME... +// ...this is more of an expander/executer... +// ...might be a good idea to also do a check without executing... +var parser = +module.parser = { + __proto__: BaseParser, + + // list of macros that will get raw text of their content... + QUOTING_MACROS: ['quote'], + + // + // (, , ){ .. } + // -> undefined + // -> + // -> + // -> + // -> () + // -> ... + // + // XXX do we need to make .macro.__proto__ module level object??? + // XXX ASYNC make these support async page getters... + macros: { + + // Args... + // + // @([ ][ local]) + // @(name=[ else=][ local]) + // + // @arg([ ][ local]) + // @arg(name=[ else=][ local]) + // + // [ ][ local]/> + // [ else=][ local]/> + // + // Resolution order: + // - local + // - .renderer + // - .root + // + // NOTE: else (default) value is parsed when accessed... + '': 'arg', + arg: Macro( + ['name', 'else', ['local']], + function(parser, args, _, state){ + 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['else'] + && parser.expand(this, args['else'], state)) }), + args: function(){ + return pwpath.obj2args(this.args) }, + + // Slot... + // + // /> + // + // text=/> + // + // > + // ... + // + // + // Force show a slot... + // + // + // Force hide a slot... + //