split out v3...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2026-06-26 20:49:24 +03:00
parent ac7c6072ba
commit 988bc45929
48 changed files with 6203 additions and 0 deletions

52
v3/Makefile Executable file
View File

@ -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

6
v3/bootstrap.js vendored Normal file

File diff suppressed because one or more lines are too long

1
v3/bootstrap/Doc.html Executable file
View File

@ -0,0 +1 @@
@include(./About)

1
v3/bootstrap/Doc/About.md Executable file
View File

@ -0,0 +1 @@
Placeholder file, this will be replaced with /README.md in bootstrap data...

252
v3/bootstrap/Doc/Macros.md Executable file
View File

@ -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:
```
<macro-name arg="value"/>
<macro-name arg="value">
...text...
</macro-name>
```
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)
<pwiki-comment>
_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._
</pwiki-comment>
- html-like macros -- escaped _the HTML way_
```
&lt;include src="\SomePage"\&gt;
```
Displayed in page as:
&lt;include src="\SomePage"\\&gt;
### 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:
```
<!--\[pWiki[ ... ]]-->
```
Show something in HTML but hide in pWiki:
<pre>
&lt;pwiki-comment&gt; ... &lt;/pwiki-comment&gt;
</pre>
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()
```
<pwiki-comment>Will produce: `1471389217848`</pwiki-comment>
<!--[pWiki[ Will produce: `@now()` ]]-->
### 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:**
<!--[pWiki[
- `[Templates/_edit/_edit]` &ndash; _see the macro at the end of the page._
]]-->
<pwiki-comment>
- [bootstrap \_edit](/bootstrap/Templates/_edit.html) &ndash; _see the
macro at the end of the page._
</pwiki-comment>
### 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:**
<pwiki-comment>
- [bootstrap css](/bootstrap/Templates/_css.html)
</pwiki-comment>
<!--[pWiki[
[Templates/\_css] / [bootstrap css](bootstrap/Templates/_css.html):
```
@source(Templates/_css)
```
]]-->
### 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:**
<pwiki-comment>
- [bootstrap view](/bootstrap/Templates/_view.html)
- [bootstrap edit](/bootstrap/Templates/_edit.html)
</pwiki-comment>
<!--[pWiki[
[Templates/\_view] / [bootstrap view](bootstrap/Templates/_view.html):
```
@source(Templates/_view)
```
[Templates/\_edit] / [bootstrap edit](bootstrap/Templates/_edit.html):
```
@source(Templates/_edit)
```
]]-->
### 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:**
<pwiki-comment>
- [bootstrap pages](/bootstrap/Templates/pages.html)
</pwiki-comment>
<!--[pWiki[
[Templates/pages] / [bootstrap pages](bootstrap/Templates/pages.html):
```
@source(Templates/pages)
```
]]-->
<!-- @filter(markdown) -->
<!-- vim:set ts=4 sw=4 ft=markdown : -->

267
v3/bootstrap/Doc/Path.md Executable file
View File

@ -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 <pwiki-comment>(seen: when viewing through pWiki)</pwiki-comment>
- 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 "&gt;&gt;"
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 |
| "&gt;&gt;" | \SourcePage | &gt;&gt;\/Target/Page | \SourcePage/Page |
| "/" - root dir | \SourcePage | \/Target/Page | \/Target/Page |
_Note that neither a leading ".." at root level, nor a trailing "&gt;&gt;"
in any path, will have any effect, and will simply be ignored (e.g.
"\/../Page" is same as "/Page" and "\Path/&gt;&gt;" 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.
<!-- @filter(markdown) -->
<!-- vim:set ts=4 sw=4 ft=markdown spell : -->

1
v3/bootstrap/Doc/Templates.md Executable file
View File

@ -0,0 +1 @@
XXX Document the template structure here XXX

View File

@ -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
}
/*********************************************************************/

99
v3/bootstrap/System/style.css Executable file
View File

@ -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 : */

20
v3/bootstrap/Templates.html Executable file
View File

@ -0,0 +1,20 @@
<p>
XXX Genereal template description...
</p>
<macro name="show-source" src="./**" sort="path">
<hr>
<h2>
<a href="#@source(./path)/_edit">@source(./path)</a>
</h2>
<p>
<pre><code>@quote(./raw)</code></pre>
</p>
</macro>
<macro name="show-source" src="Templates"/>
<macro name="show-source" src="System/style"/>
<!-- @filter(-wikiword) -->
<!-- vim:set ts=4 sw=4 : -->

View File

@ -0,0 +1 @@
@include(./outline)

View File

@ -0,0 +1,8 @@
<!-- place filters here so as not to takup page space: ... -->
Page @include(./path) is empty.<br><br>
Links to this page:<br>
@include(./links)<br><br>
<!-- vim:set ts=4 sw=4 : -->

View File

@ -0,0 +1 @@
@include(./todo)

View File

@ -0,0 +1,3 @@
<style>
@source(..)
</style>

View File

@ -0,0 +1,12 @@
<include src="../_view"/>
<slot name="toggle-edit-link">(<a href="#..">view</a>)</slot>
<slot name="title" class="title" contenteditable saveto="..">@source(../title)</slot>
<slot name="page-content">
<code><pre><quote src="../raw" class="raw" saveto=".." contenteditable/></pre></code>
</slot>
<!-- @filter(-wikiword) -->
<!-- vim:set ts=4 sw=4 : -->

View File

@ -0,0 +1,9 @@
<include src="../_view"/>
<slot name="title" class="title" contenteditable saveto="..">@source(../title)</slot>
<slot name="page-content">
@include(../outline)
</slot>
<!-- vim:set ts=4 sw=4 : -->

View File

@ -0,0 +1 @@
@source(..)

View File

@ -0,0 +1,9 @@
<include src="../_view"/>
<slot name="title" class="title" contenteditable saveto="..">@source(../title)</slot>
<slot name="page-content">
@include(../todo)
</slot>
<!-- vim:set ts=4 sw=4 : -->

View File

@ -0,0 +1,31 @@
@include(./style/_css)
<div>
<a href="#pages" class="pages-list-button button">&#x2630;</a>
[@source(../path)]
<slot name="toggle-edit-link"> (<a href="#./_edit">edit</a>) </slot>
<span class="separator"/>
<a href="#NewPage/_edit" class="new-page-button button">+</a>
</div>
<hr>
<h1 saveto="..">
<slot name="title" class="title">@source(../title)</slot>
</h1>
<br>
<div>
<slot name="page-content">
<include src=".." class="text" saveto=".." tabindex="0"/>
</slot>
</div>
<hr>
<a href="#/">home</a>
<!-- vim:set ts=4 sw=4 : -->

View File

@ -0,0 +1,12 @@
<macro src="../**">
<div class="item">
[@source(./path)]
<span class="separator"/>
<a class="button" href="#@source(./path)/delete">&times;</a>
</div>
<else>
No pages...
</else>
</macro>
<!-- vim:set ts=4 sw=4 : -->

View File

@ -0,0 +1,6 @@
<macro name="remove-button">
<span page="./path" class="remove-button button">&times;</span>
</macro>
<!-- vim:set ts=4 sw=4 : -->

View File

@ -0,0 +1,47 @@
<macro name="item-pre-controls"/>
<macro name="item-content" class="item-content">
<include
class="raw"
contenteditable
tabindex="0"
style="display:inline-block"
saveto="@source(./path)"
src="." />
</macro>
<macro name="item-post-controls">
<a class="button" href="#@source(./path)/delete">&times;</a>
</macro>
<div>
<span
class="raw"
contenteditable
tabindex="0"
saveto="@source(../path)/@now()"
style="display:inline-block" >
+
</span>
</div>
<div class="sortable">
<macro src="../*" sort="checked order -title">
<div class="item">
<div>
<span class="sort-handle">&#x2630;</span>
<macro name="item-pre-controls" src="."/>
<macro name="item-content" src="." />
<span class="separator"/>
<macro name="item-post-controls" src="."/>
</div>
<div style="padding-left: 30px">
<include
style="display:block"
src="@source(./path)/outline" />
</div>
</div>
</macro>
</div>
<!-- vim:set ts=4 sw=4 : -->

View File

@ -0,0 +1,12 @@
<macro src="../*">
<div class="item">
[@source(./path)]
<span class="separator"/>
<a class="button" href="#@source(./path)/delete">&times;</a>
</div>
<else>
No pages...
</else>
</macro>
<!-- vim:set ts=4 sw=4 : -->

View File

@ -0,0 +1,7 @@
<macro name="item-pre-controls">
<input type="checkbox" class="state" saveto="@source(./path)"/>
</macro>
<include src="../outline">
<!-- vim:set ts=4 sw=4 : -->

View File

@ -0,0 +1,15 @@
<div class="sortable">
<macro src="../*">
<div class="item">
<span class="sort-handle">&#x2630;</span>
<a href="#@source(./path)">@source(./title)</a>
<span class="separator"/>
<a class="button" href="#@source(./path)/delete">&times;</a>
</div>
<div style="padding-left: 30px">
<include style="display:block" src="@source(./path)/tree" />
</div>
</macro>
</div>
<!-- vim:set ts=4 sw=4 : -->

3
v3/bootstrap/Test/slot.html Executable file
View File

@ -0,0 +1,3 @@
<slot name=first text="first slot default"/>

3
v3/bootstrap/TestTodo.md Executable file
View File

@ -0,0 +1,3 @@
[Templates/outline/_edit]
@include(./todo)

1
v3/bootstrap/TestTodo/01.html Executable file
View File

@ -0,0 +1 @@
item 1

1
v3/bootstrap/TestTodo/02.html Executable file
View File

@ -0,0 +1 @@
item 2

1
v3/bootstrap/TestTodo/03.html Executable file
View File

@ -0,0 +1 @@
item 3

1
v3/bootstrap/TestTodo/04.html Executable file
View File

@ -0,0 +1 @@
item 4

View File

@ -0,0 +1,2 @@
@source(../name)<macro src="../*">
<include src="@source(./path)/tree" recursive="..."/></macro>

BIN
v3/img/pWiki-192px.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
v3/img/pWiki-48px.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
v3/img/pWiki-96px.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
v3/img/pWiki-i.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
v3/img/pWiki-s.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
v3/img/pWiki.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
v3/img/pWiki.kra Executable file

Binary file not shown.

BIN
v3/img/pWiki.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

347
v3/package-lock.json generated Normal file
View File

@ -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"
}
}
}

38
v3/package.json Executable file
View File

@ -0,0 +1,38 @@
{
"name": "pWiki",
"version": "0.0.1",
"author": "Alex A. Naanou <alex.nanou@gmail.com>",
"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"
}
}

14
v3/pwiki/_module.js Normal file
View File

@ -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 })

452
v3/pwiki/index.js Executable file
View File

@ -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(<name>[, <options>])
// makeIndex(<name>, <generate>[, <options>])
// -> <index-handler>
//
// Call/get
// <index-handler>()
// -> <data>
// -> <promise>
//
// Call the index handler method...
// <index-handler>('__call__', ..)
// -> ...
// -> <promise>
//
// Get merged data (cached)
// <index-handler>('get')
// -> <data>
// -> <promise>
// 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...
// <index-handler>('lazy')
// -> <data>
// -> <promise>
// NOTE: if <index-handler>(..) 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...
// <index-handler>('cached')
// -> <data>
// -> <promise>
// -> undefined
// NOTE: this is like 'lazy' but will not wait for <index-handler>)(..)
// to return, making it even faster but as a trade off it will
// return the cached and possibly outdated result even if
// <index-handler>(..) is synchronous.
//
// Get local data (uncached)...
// <index-handler>('local')
// -> <data>
// -> <promise>
//
// Clear cache...
// <index-handler>('clear')
//
// Reset cache (clear then get)...
// <index-handler>('reset')
// -> <data>
// -> <promise>
//
// Get index status...
// <index-handler>('status')
// -> 'empty'
// -> 'pending'
// -> 'cached'
// -> 'outdated'
//
// Run custom action...
// <index-handler>(<action-name>), ...)
// -> <data>
// -> <promise>
//
// 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 <data>...
// .__<name>__()
// -> <data>
//
// Merge local data with other sources...
// .__<name>_merge__(<data>)
// -> <data>
//
// Test if cache is valid...
// .__<name>_isvalid__(<timestamp>)
// -> <bool>
//
// Handle custom action...
// .__<name>_<action-name>__(<data>. ...)
// -> <data>
//
//
//
// Special attributes:
//
// Cached data...
// .__<name>_cache / .<name>
//
// Modification time...
// .__<name>_modified
//
// Pending generator promise...
// .__<name>_promise
//
//
// Options format:
// {
// // XXX
// attr: false
// | true
// | <name>,
//
// // 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: [
// <index-name>,
// ...
// ],
//
// // custom action...
// // NOTE: this is the same as defining .__<name>_<action-name>__(..)
// // method...
// <action-name>: <func>,
// }
//
//
// XXX do we separate internal methods and actions???
// i.e. __<name>_merge__(..) / __<name>_isvalid__(..) and the rest...
var makeIndex =
module.makeIndex =
function(name, generate, options={}){
// makeIndex(<name>, <options>)
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')
// -> <indexi>
//
// ...
//
// .index(obj, <action>, ...)
// -> <indexi>
//
//
// .index('obj, new', <name>, <generate>[, <options>])
// -> <index-handler>
//
// 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 })

2066
v3/pwiki/page.js Executable file

File diff suppressed because it is too large Load Diff

1838
v3/pwiki/parser.js Normal file

File diff suppressed because it is too large Load Diff

464
v3/pwiki/path.js Executable file
View File

@ -0,0 +1,464 @@
/**********************************************************************
*
*
*
**********************************************************************/
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var types = require('ig-types')
//---------------------------------------------------------------------
var makeEncoder = function(name, chars){
var pattern_attr = '__'+name+'_pattern'
return function(str){
var pattern = this[pattern_attr] =
this[pattern_attr]
?? RegExp(`[${
this[chars]
.replace(/\\/g, '\\\\') }]`, 'g')
return str
.replace(pattern,
function(s){
return ('%'+s.charCodeAt().toString(16)).toUpperCase() }) } }
var makeDecoder = function(name, encode, chars){
var pattern_attr = '__'+name+'_pattern'
return function(str){
var pattern = this[pattern_attr] =
this[pattern_attr]
?? RegExp(`%(${
this[encode](this[chars])
.slice(1)
.replace(/%/g, '|') })`, 'gi')
return str
.replace(pattern, function(_, c){
return String.fromCharCode('0x'+c) }) } }
//---------------------------------------------------------------------
// Path...
module = {
// Page returned when listing a path ending with '/'...
//
// If set to false treat dirs the same as pages (default)
//INDEX_PAGE: 'index',
INDEX_PAGE: false,
// The page returned when getting the '/' path...
//
// NOTE: this is the same as .INDEX_PAGE but only for '/'
ROOT_PAGE: 'WikiHome',
ALTERNATIVE_PAGES: [
'EmptyPage',
'NotFoundError',
],
// Default alternate search locations...
//
// NOTE: if a path here is relative it is also searched relative to
// the target path.
SEARCH_PATHS: [
'.templates',
],
// System path...
//
// This acts the same as elements in .SEARCH_PATHS but should contain
// all the default and fallback templates.
//
// NOTE: we can't use .pwiki here as it will be in conflict with the
// fs store's directory structure.
// XXX or can we?
SYSTEM_PATH: '/.system',
// XXX EXPERIMENTAL
ENCODED_PATH: '#:*%',
encode: makeEncoder('encode', 'ENCODED_PATH'),
decode: makeDecoder('decode', 'encode', 'ENCODED_PATH'),
ENCODED_ELEM: '#:*%\\/',
encodeElem: makeEncoder('encode_elem', 'ENCODED_ELEM'),
decodeElem: makeDecoder('decode_elem', 'encodeElem', 'ENCODED_ELEM'),
// chars we keep in path as-is -- decode on normalize...
//
UNENCODED_PATH: '\'"',
quote: makeEncoder('quote', 'UNENCODED_PATH'),
unquote: makeDecoder('unquote', 'quote', 'UNENCODED_PATH'),
quoteHTML: function(str){
return str
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;') },
unquoteHTML: function(str){
return str
.replace(/&amp;/g, '&')
.replace(/&gt;/g, '>')
.replace(/&lt;/g, '<') },
// Path utils...
//
// Path can be in one of two formats:
// string
// array
//
// NOTE: trailing/leading '/' are represented by '' at end/start of
// path list...
normalize: function(path='.', format='auto'){
format = format == 'auto' ?
(path instanceof Array ?
'array'
: 'string')
: format
var root = path[0] == ''
|| path[0] == '/'
path = this.unquote(
path instanceof Array ?
path.join('/')
: path)
// NOTE: this will also trim the path elements...
.split(/\s*[\\\/]+\s*/)
.reduce(function(res, e, i, L){
// special case: leading '..' / '.'
if(res.length == 0
&& e == '..'){
return [e] }
// multiple leading '..'...
;(e == '..'
&& res.at(-1) == '..' ?
res.push(e)
: e == '.'
// keep explicit '/' only at start/end of path...
|| (e == ''
&& i != 0
&& i != L.length-1)) ?
undefined
: e == '..'
|| res.at(-1) == '>>' ?
((res.length > 1
|| res[0] != '')
&& res.pop())
// NOTE: the last '>>' will be retained...
: res.push(e)
return res }, [])
// clear the trailing '/'...
path.at(-1) == ''
&& path.pop()
// trim trailing ':'...
path.at(-1)
&& path.at(-1).endsWith(':')
&& (path.push(
path.pop()
.replace(/:*$/, '')))
return format == 'string' ?
// special case: root -> keep '/'
((root
&& path.length == 1
&& path[0] == '') ?
('/'+ path.join('/'))
: path.join('/'))
: path },
sanitize: function(path, format='auto'){
format = format == 'auto' ?
(path instanceof Array ?
'array'
: 'string')
: format
path = this.split(path)
// leading: '/', '.' and '..'...
while(path[0] == ''
|| path[0] == '.'
|| path[0] == '..'){
path.shift() }
//trailing '/'
path.at(-1) == ''
&& path.pop()
return format == 'string' ?
path.join('/')
: path },
split: function(path){
return this.normalize(path, 'array') },
join: function(...parts){
return this.normalize(
(parts[0] instanceof Array ?
parts[0]
: parts),
'string') },
basename: function(path){
path = this.split(path)
return path.length == 0 ?
''
: path.length == 1 ?
path[0]
: (path.at(-1) == '' ?
path.at(-2)
: path.at(-1)) },
dirname: function(path){
path = this.split(path)
path = path.length == 1 ?
'/'
: path.length == 2 ?
path[0]
: (path.at(-1) == '' ?
path.slice(0, -2)
: path.slice(0, -1))
.join('/')
return path == '' ?
'/'
: path },
// XXX BUG? which is more correct??
// .relative('a/b/c', 'x')
// -> 'a/b/c/x' (current)
// or:
// .relative('a/b/c', 'x')
// -> 'a/b/x'
// ...not sure about this yet...
// XXX REVISE...
relative: function(parent, path, format='auto'){
format = format == 'auto' ?
(path instanceof Array ?
'array'
: 'string')
: format
// root path...
if(path[0] == '' || path[0] == '/'){
return this.normalize(path, format) }
// unify parent/path types...
parent = parent instanceof Array ?
parent
: parent.split(/\s*[\\\/]+\s*/)
path = path instanceof Array ?
path
: path.split(/\s*[\\\/]+\s*/)
// NOTE: relative paths are siblings and not children unless the
// parent is explicitly a directory (i.e. ends in '/')...
/* XXX RELATIVE -- leading @ in path is the same as a trailing / in parent...
path[0] == '@' ?
path.shift()
: parent.pop()
//*/
return this.normalize([...parent, ...path], format) },
// Build alternative paths for page acquisition...
//
// NOTE: if seen is given (when called recursively) this will not
// search for .ALTERNATIVE_PAGES...
// XXX should we keep the trailing '/'???
// XXX EXPERIMENTAL...
paths: function*(path='/', strict=false, seen=new Set()){
if(path === true || path === false){
strict = path
path = '/' }
if(strict instanceof Set){
seen = strict
strict = false }
var alt_pages = !strict
path = this.normalize(path, 'string')
// special case: root...
if(path == '/' || path == ''){
// normalize...
path = '/'
// as-is...
seen.add(path)
yield path
// special case: root page...
if(this.ROOT_PAGE){
yield* this.paths(this.normalize('/'+ this.ROOT_PAGE, 'string'), seen) }}
// NOTE: since path is already normalized we can trust the delimiter...
path = path.split(/\//g)
// normalize relative paths to root...
path[0] != ''
&& path.unshift('')
// paths ending in '/'...
if(path[path.length-1] == ''){
path.pop()
this.INDEX_PAGE
&& path.push(this.INDEX_PAGE) }
var searchPath = function*(path, search_paths){
// full path...
var p = this.normalize(['', ...path], 'string')
if(!seen.has(p)){
seen.add(p)
yield p }
path = path.slice()
search_paths = search_paths instanceof Array ?
search_paths
: [search_paths]
// search up the levels...
var cur, sub, sub_cur
do{
cur = [path.pop(), ...(cur ?? [])]
sub = cur.slice()
sub_cur = []
// template paths...
for(var tpl of search_paths){
// search the sub paths in the template dir...
var parent = ['', ...path]
do{
sub_cur = [sub.pop(), ...(sub_cur ?? [])]
p = this.relative(parent, tpl +'/'+ sub_cur.join('/'), 'string')
if(!seen.has(p)){
seen.add(p)
yield p }
}while(sub.length > 0)
}
// search the sub paths in the parent dir...
parent = parent.length > 1 ?
parent.slice(0, -1)
: parent
sub = cur.slice()
sub_cur = []
do{
sub_cur = [sub.pop(), ...(sub_cur ?? [])]
p = this.relative(parent, sub_cur.join('/'), 'string')
if(!seen.has(p)){
seen.add(p)
yield p }
}while(sub.length > 0)
}while(path.length > 0) }.bind(this)
yield* searchPath(path, this.SEARCH_PATHS)
yield* searchPath(path, this.SYSTEM_PATH)
if(alt_pages){
for(var page of [...this.ALTERNATIVE_PAGES]){
yield* this.paths(path.slice(0, -1).concat(page), true, seen) } } },
/*/
paths: function*(path='/', strict=false){
if(path === true || path === false){
strict = path
path = '/' }
var alt_pages = !strict
var seen = strict instanceof Set ?
strict
: new Set()
path = this.normalize(path, 'string')
// special case: root...
if(path == '/' || path == ''){
// normalize...
path = '/'
// as-is...
seen.add(path)
yield path
// special case: root page...
if(this.ROOT_PAGE){
yield* this.paths(this.normalize('/'+ this.ROOT_PAGE, 'string'), seen) }}
// NOTE: since path is already normalized we can trust the delimiter...
path = path.split(/\//g)
// normalize relative paths to root...
path[0] != ''
&& path.unshift('')
// paths ending in '/'...
if(path[path.length-1] == ''){
path.pop()
this.INDEX_PAGE
&& path.push(this.INDEX_PAGE) }
// search for page...
var page = path.pop()
for(var tpl of ['.', ...this.SEARCH_PATHS, this.SYSTEM_PATH]){
// search for page up the path...
var pg = page
var base = path.slice()
while(base.length > 0){
var p = base.slice()
while(p.length > 0){
// NOTE: we are adding '' to path here to get things
// relative to it and not relative to basedir...
var cur = this.relative([...p, ''], tpl +'/'+ pg, 'string')
if(!seen.has(cur)){
seen.add(cur)
yield cur }
// special case: non-relative template/page path...
if(tpl[0] == '/'){
break }
p.pop() }
// next search for tail sub-path...
// for a/b/c
// c in a/b -> b/c in a
pg = base.pop() +'/'+ pg } }
// alternative pages...
if(alt_pages){
for(var page of [...this.ALTERNATIVE_PAGES]){
yield* this.paths(path.concat(page), seen) }} },
//*/
names: function(path='/'){
path = path == '' ?
'/'
: path
path = this.normalize(path, 'string')
var name = path == '/' ?
['', this.ROOT_PAGE]
: [this.basename(path)]
return name == '' ?
this.ALTERNATIVE_PAGES.slice()
: [...name, ...this.ALTERNATIVE_PAGES] },
//
// .splitArgs(<path>)
// -> <spec>
//
// Format:
// {
// path: <path>
// args: {
// <name>:<value>,
// ...
// }
// }
//
// Syntax:
// <path> ::= <path>:<args>
// <args> ::=
// <arg> | <arg>:<args>
// <arg> ::=
// <value>
// | <name>=<value>
//
// XXX the problem here is that we could legitimately create path
// items containing ":" -- it either needs to be a reserved char
// or this scheme will not work...
splitArgs: function(path){
path = this.normalize(path, 'string')
var [path, ...args] = path.split(/(?<!\\):/g)
return {
path,
args: args.reduce(function(res, arg){
var [name, value] = arg.split(/=(.*)/)
res[name.trim()] = value ?? true
return res }, {}),
} },
obj2args: function(args){
return args instanceof Object ?
Object.entries(args)
.map(function([key, value]){
return value === true ?
key
: key +'='+ ((value + '').replace(/:/g, '\\:')) })
.join(':')
: args },
joinArgs: function(path, args={}){
path = this.join(path)
args = this.obj2args(args)
return args == '' ?
path
: path +':'+ args },
}
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })

65
v3/scripts/bootstrap.js vendored Executable file
View File

@ -0,0 +1,65 @@
/**********************************************************************
*
*
*
**********************************************************************/
var fs = require('fs')
var glob = require('glob')
/*********************************************************************/
var bootstrap = {}
var BOOTSTRAP_TEMPLATE =
`// This file is generated automatically, all changes made here will be lost.
var Bootstrap = $BOOTSTRAP
typeof(module) != "undefined"
&& (module.exports = Bootstrap)`
// XXX add support for json...
new glob.Glob('bootstrap/**/*.@(tpl|md|css|html|txt)', {})
.stream()
.on('data', function(path){
var p = path
.replace('bootstrap/', '')
.replace(/\.(json|txt|md|css|html|txt)/, '')
console.log('Found:', p)
bootstrap[p] = {
text: fs.readFileSync(path).toString(),
} })
.on('end', function(){
// extra root stuff...
if(fs.existsSync('README.md')){
console.log('Setting:', 'About')
bootstrap['Doc/About'] = {
text: fs.readFileSync('README.md').toString(),
} }
if(!bootstrap.WikiHome){
console.log('Setting:', 'WikiHome')
bootstrap.WikiHome = {
text: '@include(Doc/About)'
} }
if(fs.existsSync('LICENSE')){
console.log('Setting:', 'LICENSE')
bootstrap['LICENSE'] = {
text: `${
fs.readFileSync('LICENSE').toString()
}<!-- @filter(text) -->`,
} }
var txt = BOOTSTRAP_TEMPLATE
.replace(/\$BOOTSTRAP/g, JSON.stringify(bootstrap))
console.log('Writing:', 'bootstrap.js')
fs.writeFileSync('bootstrap.js', txt) })
/**********************************************************************
* vim:set ts=4 sw=4 : */