Compare commits
No commits in common. "2d07a5c5f66670368040e6581c3e2d325d9f7c97" and "c540073b6600a832bf4481a78973786a25146a69" have entirely different histories.
2d07a5c5f6
...
c540073b66
|
|
@ -96,7 +96,7 @@ API_URI=/_/v1/
|
|||
if db:
|
||||
self.db = db
|
||||
elif self.config.IN_MEMORY_DB:
|
||||
self.db = GrungDB.with_schema(schema, path=self.path.database, storage=MemoryStorage)
|
||||
self.db = GrungDB.with_schema(schema, path=None, storage=MemoryStorage)
|
||||
else:
|
||||
self.db = GrungDB.with_schema(
|
||||
schema, path=self.path.database, sort_keys=True, indent=4, separators=(",", ": ")
|
||||
|
|
@ -165,7 +165,6 @@ API_URI=/_/v1/
|
|||
else:
|
||||
search_uri = uri
|
||||
|
||||
self.log.debug(f"Looking for page with uri {search_uri}")
|
||||
page = table.get(where("uri") == search_uri, recurse=False)
|
||||
if not page:
|
||||
# load the parent to check for write permissions
|
||||
|
|
@ -183,7 +182,7 @@ API_URI=/_/v1/
|
|||
raise UnauthorizedError(f"User {user.doc_id} does not have permission to create under {parent_uri}.")
|
||||
|
||||
obj = getattr(schema, table.name)
|
||||
page = obj(name=page_name, body=obj.default.format(name=page_name), parent=parent, uri=search_uri)
|
||||
page = obj(name=page_name, body=obj.default.format(name=page_name), parent=parent)
|
||||
|
||||
# validate the page name before we try to create anything
|
||||
page._metadata.fields["name"].validate(page, db=self.db)
|
||||
|
|
|
|||
|
|
@ -1,240 +1,41 @@
|
|||
from ttfrog import app, schema
|
||||
|
||||
TEMPLATE = """
|
||||
# Wiki Syntax
|
||||
{{toc box depth="3"}}
|
||||
# Heading 1
|
||||
|
||||
## Basic Formatting
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
The wiki uses [GitHub-flavored Markdown](https://github.github.com/gfm/).
|
||||
#### Heading 4
|
||||
|
||||
{{style equal-widths striped
|
||||
##### Heading 5
|
||||
|
||||
| Type | To Get |
|
||||
|------|--------|
|
||||
| `*emphasis*` | *emphasis* |
|
||||
| `**bold**` | **bold** |
|
||||
| `[link label](/link/address)` | [link label](/link/address) |
|
||||
| `` `inline` `` | `inline` |
|
||||
| `***` | <hr> |
|
||||
| `# Heading 1` | {{{ <h1>Heading 1</h1> }}} |
|
||||
| `## Heading 2` | {{{ <h2>Heading 2</h2> }}} |
|
||||
| `### Heading 3` | {{{ <h3>Heading 3</h3> }}} |
|
||||
| `#### Heading 4` | {{{ <h4>Heading 4</h4> }}} |
|
||||
| `##### Heading 5` | {{{ <h5>Heading 5</h5> }}} |
|
||||
| `###### Heading 6` | {{{ <h6>Heading 6</h6> }}} |
|
||||
| {{{
|
||||
###### Heading 6
|
||||
|
||||
<pre>
|
||||
* unordered
|
||||
* list
|
||||
* items
|
||||
</pre>
|
||||
***
|
||||
|
||||
}}} | {{{
|
||||
Normal text.
|
||||
**Bold text.**
|
||||
*Italic Text.*
|
||||
[A link](/).
|
||||
|
||||
<ul><li>unordered</li><li>list</li><li>items</li></ul>
|
||||
1. a
|
||||
2. numbered
|
||||
3. list.
|
||||
|
||||
}}} |
|
||||
| {{{
|
||||
> a block quote
|
||||
|
||||
<pre>
|
||||
1. ordered
|
||||
1. list
|
||||
1. items
|
||||
</pre>
|
||||
|
||||
}}} | {{{ <ol><li>ordered</li><li>list</li><li>items</li></ol> }}} |
|
||||
| {{{
|
||||
|
||||
<pre>
|
||||
> blockquote
|
||||
</pre>
|
||||
|
||||
}}} | <blockquote>blockquote</blockquote> |
|
||||
| {{{
|
||||
<pre>
|
||||
```
|
||||
preformatted
|
||||
code
|
||||
block
|
||||
```
|
||||
</pre>
|
||||
}}} | {{{ <pre>preformatted code<br>block</pre> }}} |
|
||||
| {{{
|
||||
|
||||
<pre>
|
||||
\\{\\{\\{
|
||||
|
||||
multi-line content,
|
||||
whitespace preserved,
|
||||
\\*\\*markup\\*\\* and \\<i\\>HTML\\</i\\> included.
|
||||
|
||||
\\}\\}\\}
|
||||
</pre>
|
||||
|
||||
}}} | {{{ <p>multi-line content,<br>whitespace preserved,<br><b>markup</b> and <i>HTML</i> included.</p> }}} |
|
||||
|
||||
}}
|
||||
|
||||
## Macros
|
||||
|
||||
Macros extend authoring options with computed values evaluated at display time.
|
||||
There are two types: *inline*, which can be used in the middle of a line of
|
||||
markdown, and *block*, which enclose a series of lines.
|
||||
|
||||
### Inline Macros
|
||||
|
||||
The basic format of an inline macro is:
|
||||
|
||||
`{{NAME [KEYWORD ..] [PARAM="VALUE" ..]}}`
|
||||
|
||||
* `NAME` is the name of the macro. It is always lower case.
|
||||
* `KEYWORD` is a bare word
|
||||
* `PARAM` is a parameter name
|
||||
* `VALUE` is the value of the associated parameter
|
||||
|
||||
No macro requires keywords or parameters, but some support them; they are documented below.
|
||||
|
||||
### Block Macros
|
||||
|
||||
The format of a block macro uses the same format for name, keywords and parameters. The closing
|
||||
braces must appear on their own line, and both the opening and closing lines must be
|
||||
surrounded by blank lines. This ensures the block macro is not interpreted as markdown.
|
||||
|
||||
```
|
||||
Before the macro..
|
||||
|
||||
{{NAME [KEYWORD ..] [PARAM="VALUE" ..]
|
||||
|
||||
..inside the macro..
|
||||
|
||||
}}
|
||||
|
||||
..after the macro.
|
||||
```
|
||||
|
||||
### Supported Macros
|
||||
|
||||
This section documents all supported macros.
|
||||
|
||||
WIP (move to its own page)
|
||||
|
||||
{{style striped equal-widths
|
||||
|
||||
| Type | To Get | Description |
|
||||
|------|--------|-------------|
|
||||
|{{{ <pre>{{user}}</pre> }}} | {{user}} | A hyperlink to the current user's wiki page. |
|
||||
|{{{ <pre>{{style [STYLE ..]<br>pretty markup goes here!<br><br>}}</pre> }}}| pretty markup goes here! | Styled markup. See Block Styling and Positioning, below. |
|
||||
|{{{ <pre>{{toc [STYLE ..] depth="N"}}</pre>}}} | {{toc depth="1"}} | A table of contents. The default depth is 3, and styles may be assigned as with the `style` macro. |
|
||||
|{{{ <pre>{{widget NAME}}</pre> }}} | *varies* | Insert specified widget; see [Wiki Syntax: Widgets](/Widgets). |
|
||||
|
||||
|
||||
### Block Styling and Positioning
|
||||
|
||||
Several styles are supported by the editor, which may be freely combined. These include:
|
||||
|
||||
* **box**: block element with background and border
|
||||
* **full-width**: Expand block to fill all horizontal space
|
||||
* **right**: right-aligned floating block (maximum 30% of full width)
|
||||
* **left**: left-aligned floating block (maximum 30% of full width)
|
||||
* **center**: centered block
|
||||
* **inline**: contained within the current text line
|
||||
|
||||
#### Examples
|
||||
|
||||
{{style striped equal-widths
|
||||
|
||||
| Type | To Get |
|
||||
|------|--------|
|
||||
| {{{ <pre>{{style box<br><br>😶 box, full-width<br><br>}}<br><br>normal text</pre> }}} | {{{ <div class='box'>😶 box, full-width</div>normal text }}} |
|
||||
| {{{ <pre>{{style right<br><br>😶 right-aligned, inline<br><br>}}<br><br>normal text</pre> }}} | {{{ <div class='right'>😶 right-aligned, inline</div>normal text }}} |
|
||||
| {{{ <pre>{{style box right<br><br>😶 box right-aligned, inline<br><br>}}<br><br>normal text</pre> }}} | {{{ <div class='box right'>😶 box right-aligned, inline</div>normal text }}} |
|
||||
| {{{ <pre>{{style box center<br><br>😶 box center-aligned, block<br><br>}}<br><br>normal text</pre> }}} | {{{ <div class='box center'>😶 box center-aligned, block</div>normal text }}} |
|
||||
| {{{ <pre>{{style box left<br><br>😶 box left-aligned, inline<br><br>}}<br><br>normal text</pre> }}} | {{{ <div class='box left'>😶 box left-aligned, inline</div>normal text }}} |
|
||||
|
||||
}}
|
||||
|
||||
### Tables
|
||||
|
||||
Basic tables follow the Github-Flavoured Markdown. By default, table cells may not contain newlines, but you can add multi-line content to your tables using {{{ {{{ and }}} }}}:
|
||||
|
||||
{{style equal-widths striped
|
||||
|
||||
| Type | To Get |
|
||||
|------|--------|
|
||||
| {{{
|
||||
|
||||
<pre>
|
||||
\\| head 1 \\| head 2 \\|
|
||||
\\| ------ \\| ------ \\|
|
||||
\\| cell 1 \\| cell 2 \\|
|
||||
</pre>
|
||||
|
||||
}}} | {{{ <table><tr><th>head 1</th><th>head 2</th></tr><tr><td>cell 1</td><td>cell 2</td></tr></table> }}} |
|
||||
| {{{
|
||||
|
||||
<pre>
|
||||
\\| head 1 \\| head 2 \\|
|
||||
\\| ------ \\| ------ \\|
|
||||
\\| \\{\\{\\{
|
||||
|
||||
multi-line
|
||||
content
|
||||
|
||||
}}} \\| cell 3 \\|
|
||||
</pre>
|
||||
|
||||
}}} | {{{ <table><tr><th>head 1</th><th>head 2</th></tr><tr><td><p>multi-line<br>content</p></td><td>cell 3</td></tr></table> }}} |
|
||||
|
||||
}}
|
||||
|
||||
|
||||
#### Table Styling
|
||||
|
||||
{{style box right outer
|
||||
|
||||
**Striped, Equal-Widths Example:**
|
||||
|
||||
{{style striped equal-widths inner1
|
||||
|
||||
| A | B|
|
||||
|---|--|
|
||||
| This table has | and |
|
||||
| equal | alternating |
|
||||
| widths! | backgrounds! |
|
||||
|
||||
}}
|
||||
|
||||
|
||||
**Layout Example:**
|
||||
|
||||
{{style layout inner2
|
||||
|
||||
| A | B|
|
||||
|---|--|
|
||||
| row 1, col 1 | row 1, col 2|
|
||||
| row 2, col 1 | row 2, col 2|
|
||||
|
||||
}}
|
||||
|
||||
}}
|
||||
|
||||
|
||||
By wrapping a table in a `style` macro, you can use the following block-level styles on the tables themselves:
|
||||
|
||||
* `striped`: Apply zebra-striping to table rows
|
||||
* `equal-widths`: Enforce equal widths of all table columns
|
||||
* `layout`: Hide the table headers, borders, and background colors. This is useful in cases
|
||||
where you would like to control layout of elements using tables.
|
||||
| A | Table | Section |
|
||||
| --- | ----- | ------- |
|
||||
| foo | bar | baz |
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def bootstrap():
|
||||
"""
|
||||
Bootstrap the database entries by poplating the first Page, the Admin user and the Admins group.
|
||||
Bootstrap the database entries by populating the first Page, the Admin user and the Admins group.
|
||||
"""
|
||||
app.check_state()
|
||||
|
||||
|
|
@ -244,13 +45,10 @@ def bootstrap():
|
|||
# create the top-level pages
|
||||
root = app.db.save(schema.Page(name=app.config.VIEW_URI, body="This is the home page", uri=""))
|
||||
|
||||
root.add_member(schema.Page(name="Wiki", body=TEMPLATE))
|
||||
|
||||
users = root.add_member(schema.Page(name="User", body="# Users\nusers go here."))
|
||||
groups = root.add_member(schema.Page(name="Group", body="# Groups\ngroups go here."))
|
||||
npcs = root.add_member(schema.Page(name="NPC", body="# NPCS!"))
|
||||
widgets = root.add_member(schema.Page(name="Widget", body="Widgets go here."))
|
||||
widgets.add_member(schema.Widget(name="hello", body=schema.Widget.default))
|
||||
wiki = root.add_member(schema.Page(name="Wiki", body=TEMPLATE))
|
||||
|
||||
# create the NPCs
|
||||
npcs.add_member(schema.NPC(name="Sabetha", body=""))
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from grung.objects import (
|
|||
TextFilePointer,
|
||||
Timestamp,
|
||||
)
|
||||
from grung.validators import LengthValidator, PatternValidator
|
||||
from grung.validators import PatternValidator, LengthValidator
|
||||
from tinydb import where
|
||||
|
||||
from ttfrog.exceptions import MalformedRequestError
|
||||
|
|
@ -303,88 +303,3 @@ class NPC(Page):
|
|||
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class Widget(Page):
|
||||
"""
|
||||
Wiki UX widgets
|
||||
"""
|
||||
|
||||
default = dedent(
|
||||
"""
|
||||
This is a sample widget that you can customize to your liking. Widget pages must contain at minimum the
|
||||
**Template** section. It, along with the optional **CSS** and **Processor** sections, will be parsed by
|
||||
wiki at display time. All other content on this page is ignored, so you can include usage docs, exmaples,
|
||||
and so on, just like this text and the annnotations below in *italics*.
|
||||
|
||||
The name of your widget is the portion of the URI following `/Widget/`.
|
||||
|
||||
# Hello, World Example Widget
|
||||
*Provide a description of your widget.*
|
||||
|
||||
Insert the word "HELLO" and optionally a name.
|
||||
|
||||
## Usage
|
||||
*Display the usage of your widget. Ensure that the example is enclosed in a preformatted (`pre`) block.*
|
||||
*Be sure to include a description of any parameters and keyword arguments.*
|
||||
|
||||
<pre>
|
||||
{{widget hello [NAME] }}
|
||||
</pre>
|
||||
|
||||
|
||||
## Example
|
||||
*Include one or more example uses of the widget. Once you save this page, you can refer to your widget directly.*
|
||||
|
||||
Here is what it looks like to say hello to the whole world: {{widget hello world}}!
|
||||
|
||||
|
||||
## Template
|
||||
*The template is javascript string that will be evaluated when the widget is loaded. Any keyword parameters*
|
||||
*you specify in the Usage section above will be available as variables, so you can use variable subsetitution*
|
||||
*in your template definition. The template must be enclose in a code block.*
|
||||
|
||||
```
|
||||
HELLO ${{token.keywords.split(" ").slice(1).join(" ") || ""}}
|
||||
```
|
||||
|
||||
## CSS
|
||||
*If you want to customize the styling of your widget, include CSS in this section. This CSS must be enclosed*
|
||||
*in a code block.*
|
||||
|
||||
```
|
||||
display: inline;
|
||||
background: green;
|
||||
padding: 3px;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
```
|
||||
|
||||
## Processor
|
||||
*If you want full control over how your widget is processed, you can override the default processor*
|
||||
*here. The processor function below is the default.*
|
||||
|
||||
```
|
||||
function(token, widget) {{
|
||||
/*
|
||||
* token The token object created by the wiki parser. token.keywords and
|
||||
* token.params contain the keyword and paramater arguments from
|
||||
* the wiki page source where the widget was used.
|
||||
* widget: The widget instance. widget.css, widget.template, and
|
||||
* widget.processor contain the definitions parsed from the
|
||||
* corresponding sections on this page.
|
||||
*/
|
||||
var ret = '';
|
||||
eval("ret = `" + widget.template + "`");
|
||||
return ret;
|
||||
}}
|
||||
```
|
||||
"""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
inherited = [field for field in super().fields() if field.name not in ("members", "uid")]
|
||||
return inherited + [
|
||||
String("name", primary_key=True),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{% block title %}TTFROG{% endblock %}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='froghat.css' ) }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='site.css' ) }}">
|
||||
{% block styles %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
|
@ -22,9 +22,9 @@
|
|||
<div class='content'>
|
||||
<main>
|
||||
{% for message in g.messages %}
|
||||
<dialog class="alert">
|
||||
<div class="alert">
|
||||
{{ message }}
|
||||
</dialog>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
|
@ -36,6 +36,7 @@
|
|||
{% endblock %}
|
||||
</footer>
|
||||
|
||||
<script src="{{ url_for('static', filename='site.js') }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,26 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='editor/editor.css' ) }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article id='froghat'>{{ page.body }}</article>
|
||||
<div id='{% if user.can_write(page) %}editor{% else %}viewer{% endif %}' class='read-only'>
|
||||
{{ page.body }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
<!-- for converting markdown to html -->
|
||||
<script src="{{ url_for('static', filename='purify.min.js' ) }}"></script>
|
||||
<script src="{{ url_for('static', filename='marked.umd.min.js' ) }}"></script>
|
||||
<script src="{{ url_for('static', filename='froghat.js' ) }}"></script>
|
||||
{% if user.can_write(page) %}
|
||||
<script src="{{ url_for('static', filename='turndown.js' ) }}"></script>
|
||||
<script src="{{ url_for('static', filename='joplin-turndown-plugin-gfm.js' ) }}"></script>
|
||||
<script src="{{ url_for('static', filename='froghat-editor.js' ) }}"></script>
|
||||
{% endif %}
|
||||
<script>
|
||||
const wiki = new Froghat{% if user.can_write(page) %}Editor{% endif %}({plugins: [MacroPlugin]});
|
||||
wiki.run();
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='editor/commonmark.js' ) }}"></script>
|
||||
<script src="{{ url_for('static', filename='editor/turndown.js' ) }}"></script>
|
||||
<script src="{{ url_for('static', filename='editor/turndown-plugin-gfm.js' ) }}"></script>
|
||||
<script src="{{ url_for('static', filename='editor/editor.js' ) }}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
10207
src/ttfrog/themes/default/static/editor/commonmark.js
Normal file
10207
src/ttfrog/themes/default/static/editor/commonmark.js
Normal file
File diff suppressed because one or more lines are too long
22
src/ttfrog/themes/default/static/editor/editor.css
Normal file
22
src/ttfrog/themes/default/static/editor/editor.css
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#editor {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#editor.loaded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#editor.view {
|
||||
}
|
||||
|
||||
#editor.edit {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#editor.wysiwyg {
|
||||
}
|
||||
|
||||
#editor.wysiwyg .md {
|
||||
opacity: 0.5;
|
||||
}
|
||||
343
src/ttfrog/themes/default/static/editor/editor.js
Normal file
343
src/ttfrog/themes/default/static/editor/editor.js
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
class Editor {
|
||||
|
||||
#states = {
|
||||
VIEW: 'view',
|
||||
EDIT: 'edit',
|
||||
WYSIWYG: 'wysiwyg'
|
||||
}
|
||||
|
||||
commonmark = null;
|
||||
turndown = null;
|
||||
#cachedHTML = null;
|
||||
#cachedMarkdown = null;
|
||||
#state = null;
|
||||
#changed = false;
|
||||
|
||||
constructor(settings) {
|
||||
/*
|
||||
* Create a new Editor instance.
|
||||
*/
|
||||
this.element = document.getElementById(settings.editorId || 'editor');
|
||||
this.source = this.element.innerHTML;
|
||||
this.commonmark = {
|
||||
reader: new commonmark.Parser(),
|
||||
writer: new commonmark.HtmlRenderer(),
|
||||
};
|
||||
this.turndown = new TurndownService({
|
||||
headingStyle: 'atx',
|
||||
codeBlockStyle: 'fenced',
|
||||
});
|
||||
|
||||
this.plugins = {};
|
||||
settings.plugins.forEach(plugin => {
|
||||
this.plugins[plugin.name] = new plugin({name: plugin.name, editor: this});
|
||||
});
|
||||
this.#bindEvents();
|
||||
this.getHTML();
|
||||
this.element.classList.add("loaded");
|
||||
}
|
||||
|
||||
#bindEvents() {
|
||||
this.element.addEventListener('keydown', (evt) => {
|
||||
if (this.#state === this.#states.VIEW) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
if (this.#state === this.#states.EDIT) {
|
||||
evt.preventDefault();
|
||||
this.insertAtCursor(document.createTextNode("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#cachedMarkdown != this.element.innerHTML) {
|
||||
this.#changed = true;
|
||||
this.#cachedMarkdown = this.element.innerHTML;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
getState() {
|
||||
return this.#state;
|
||||
}
|
||||
|
||||
setState(newState) {
|
||||
this.#state = newState;
|
||||
Object.values(this.#states).forEach(state => {
|
||||
if (state == newState) {
|
||||
this.element.classList.add(state);
|
||||
} else {
|
||||
this.element.classList.remove(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getHTML() {
|
||||
/*
|
||||
* Convert the markdown source to HTML.
|
||||
*/
|
||||
if (this.#changed || !this.#cachedHTML) {
|
||||
var md = this.getMarkdown();
|
||||
var parsed = this.commonmark.reader.parse(md);
|
||||
var html = this.commonmark.writer.render(parsed);
|
||||
Object.values(this.plugins).forEach(plugin => {
|
||||
html = plugin.toHTML(html);
|
||||
});
|
||||
this.#cachedHTML = html;
|
||||
|
||||
}
|
||||
return this.#cachedHTML;
|
||||
}
|
||||
|
||||
getMarkdown() {
|
||||
/*
|
||||
* Return the current markdown.
|
||||
*/
|
||||
if (this.getState() === this.#states.EDIT) {
|
||||
this.#cachedMarkdown = this.element.innerHTML;
|
||||
} else if (this.getState() === this.#states.WYSIWYG) {
|
||||
var md = this.element.innerHTML;
|
||||
Object.values(this.plugins).forEach(plugin => {
|
||||
md = plugin.toMarkdown(md);
|
||||
});
|
||||
this.#cachedMarkdown = this.turndown.turndown(md);
|
||||
} else if (!this.#cachedMarkdown) {
|
||||
this.#cachedMarkdown = this.source;
|
||||
}
|
||||
this.#cachedMarkdown = this.#cachedMarkdown.replaceAll(/^>/mg, '>');
|
||||
return this.#cachedMarkdown;
|
||||
}
|
||||
|
||||
reset() {
|
||||
/*
|
||||
* Discard any unsaved edits and reset the editor to its initial state.
|
||||
*/
|
||||
this.#cachedHTML = null;
|
||||
this.#cachedMarkdown = null;
|
||||
this.view();
|
||||
}
|
||||
|
||||
view() {
|
||||
/*
|
||||
* Convert the editor read-only mode and display the current HTML.
|
||||
*/
|
||||
if (this.getState() === this.#states.VIEW) {
|
||||
return;
|
||||
}
|
||||
this.element.innerHTML = this.getHTML();
|
||||
this.setState(this.#states.VIEW);
|
||||
}
|
||||
|
||||
wysiwyg() {
|
||||
/*
|
||||
* Put the editor in WYSIWYG editing mode.
|
||||
*/
|
||||
if (this.getState() === this.#states.WYSIWYG) {
|
||||
return;
|
||||
}
|
||||
this.#changed = false;
|
||||
this.element.contentEditable = true;
|
||||
this.element.innerHTML = this.getHTML();
|
||||
this.setState(this.#states.WYSIWYG);
|
||||
}
|
||||
|
||||
edit() {
|
||||
/*
|
||||
* Put the editor into source editing mode.
|
||||
*/
|
||||
if (this.#state === this.#states.EDIT) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#changed = false;
|
||||
this.element.contentEditable = true;
|
||||
this.element.innerHTML = this.getMarkdown();
|
||||
this.setState(this.#states.EDIT);
|
||||
}
|
||||
|
||||
insertAtCursor(node) {
|
||||
var sel, range, html;
|
||||
sel = window.getSelection();
|
||||
range = sel.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
range.insertNode(node);
|
||||
range.setStartAfter(node);
|
||||
this.element.focus();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class EditorPlugin {
|
||||
constructor(settings) {
|
||||
this.name = settings.name;
|
||||
this.editor = settings.editor;
|
||||
};
|
||||
|
||||
cleanAttribute(attribute) {
|
||||
return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : ''
|
||||
}
|
||||
|
||||
toMarkdown(html) {
|
||||
return html;
|
||||
};
|
||||
|
||||
toHTML(md) {
|
||||
return md;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
class TablePlugin extends EditorPlugin {
|
||||
constructor(settings) {
|
||||
super(settings);
|
||||
this.editor.turndown.use(turndownPluginGfm.tables);
|
||||
this.pattern = /(?<contents>(?:\|[^|]+?)+\|[\s\n]*)(?:<|$)/gims;
|
||||
this.dividerPattern = /\|\s*\-+\s*\|/;
|
||||
}
|
||||
|
||||
toHTML(md) {
|
||||
md.matchAll(this.pattern).forEach(matched => {
|
||||
var html = '<table><thead>';
|
||||
var cellTag = 'th';
|
||||
matched.groups.contents.split("\n").forEach(line => {
|
||||
if (this.dividerPattern.test(line)) {
|
||||
html += "</thead><tbody>";
|
||||
cellTag = 'td';
|
||||
} else {
|
||||
html += "<tr>";
|
||||
var cells = line.split("|");
|
||||
html += "<" + cellTag + ">" + cells.slice(1, cells.length - 1).join("</" + cellTag + "><" + cellTag + ">") + "</" + cellTag + ">";
|
||||
html += "</tr>";
|
||||
}
|
||||
});
|
||||
html += "</tbody></table>";
|
||||
md = md.replaceAll(matched[1], html);
|
||||
});
|
||||
return md;
|
||||
}
|
||||
}
|
||||
|
||||
class MacroPlugin extends EditorPlugin {
|
||||
|
||||
macros = {
|
||||
// image: {}
|
||||
// toc {}
|
||||
// widget {}
|
||||
//
|
||||
html: {
|
||||
toHTML: (settings) => {
|
||||
var html = settings.block.replaceAll(">", ">").replaceAll("<", "<");
|
||||
return html;
|
||||
},
|
||||
toMarkdown: (node) => {
|
||||
const utf8encoder = new TextEncoder();
|
||||
return "{{html\n" + b64decode(node.dataset.block) + "\n}}";
|
||||
}
|
||||
},
|
||||
user: {
|
||||
toHTML: (settings) => {
|
||||
return document.querySelector("nav > ul > li.user > a:first-child").outerHTML;
|
||||
},
|
||||
},
|
||||
npc: {
|
||||
toHTML: (settings) => {
|
||||
var name = camelCase(settings.keywords).join(" ");
|
||||
var target = name.replaceAll(" ", "");
|
||||
return `<a href="/NPC/${target}" data-npc-name="${name}">👤 ${name}</a>`;
|
||||
},
|
||||
toMarkdown: (node) => {
|
||||
return `{{npc ${node.firstChild.dataset.npcName}}}`;
|
||||
},
|
||||
},
|
||||
spell: {
|
||||
toHTML: (settings) => {
|
||||
var name = camelCase(settings.keywords).join(" ");
|
||||
var target = name.replaceAll(" ", "");
|
||||
return `<a href="/Spell/${target}" data-spell-name="${name}">✨ ${name}</a>`;
|
||||
},
|
||||
toMarkdown: (node) => {
|
||||
return `{{spell ${node.firstChild.dataset.spellName}}}`;
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
constructor(settings) {
|
||||
super(settings);
|
||||
this.pattern = /(?<contents>{{(?<name>\w+)(?<keywords>(?:\s*\w+)*)(?<parameters>(?:\s+\w+="[^"]*")*?)(?<block>\b.*?)?}})/gms;
|
||||
this.paramPattern = /\b(?<name>[^=]+)="(?<value>[^"]*?)"\b/gm;
|
||||
|
||||
var plugin = this;
|
||||
this.editor.turndown.addRule('macros', {
|
||||
filter: ['span'],
|
||||
replacement: function (content, node, options) {
|
||||
const parser = plugin.macros[node.getAttribute('data-macro-name')].toMarkdown;
|
||||
if (parser) {
|
||||
return parser(node);
|
||||
}
|
||||
|
||||
var md = '{{' + node.getAttribute('data-macro-name');
|
||||
for (var param in node.getAttributeNames()) {
|
||||
var val = node.getAttribute(param);
|
||||
if (val) {
|
||||
md += val + (param == 'data-macro-name' ? " " : `="${node.getAttribute(param)}" `);
|
||||
}
|
||||
}
|
||||
md += '}}';
|
||||
return md;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
toHTML(md) {
|
||||
const utf8encoder = new TextEncoder();
|
||||
var output = md;
|
||||
output.matchAll(this.pattern).forEach(matched => {
|
||||
var macroName = matched.groups.name;
|
||||
var html = `<span class='macro' data-macro-name='${macroName}'`;
|
||||
var settings = {
|
||||
block: matched.groups.block,
|
||||
keywords: matched.groups.keywords,
|
||||
params: {},
|
||||
}
|
||||
matched.groups.parameters.matchAll(this.paramPattern).forEach(param => {
|
||||
settings.params[param.groups.name] = param.groups.value;
|
||||
html += ` data-param-${param.groups.name}="${param.groups.value}"`;
|
||||
});
|
||||
html += ` data-block="${b64encode(matched.groups.block)}"`;
|
||||
html += '>';
|
||||
html += this.macros[macroName].toHTML(settings);
|
||||
html += '</span>';
|
||||
output = output.replaceAll(matched[1], html);
|
||||
});
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
function camelCase(words) {
|
||||
var output = [];
|
||||
words.trim().split(/\s+/g).forEach(word => {
|
||||
var lcWord = word.toLowerCase();
|
||||
output.push(lcWord.charAt(0).toUpperCase() + lcWord.slice(1));
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
function b64encode(input) {
|
||||
const utf8encoder = new TextEncoder();
|
||||
return utf8encoder.encode(input).toBase64();
|
||||
}
|
||||
|
||||
function b64decode(input) {
|
||||
const utf8encoder = new TextEncoder();
|
||||
return utf8encoder.decode(input.fromBase64());
|
||||
}
|
||||
|
||||
|
||||
editor = new Editor({
|
||||
plugins: [TablePlugin, MacroPlugin]
|
||||
});
|
||||
editor.view();
|
||||
|
|
@ -43,7 +43,6 @@ var rules = {};
|
|||
rules.tableCell = {
|
||||
filter: ['th', 'td'],
|
||||
replacement: function (content, node) {
|
||||
if (tableShouldBeSkipped(nodeParentTable(node))) return content;
|
||||
return cell(content, node)
|
||||
}
|
||||
};
|
||||
|
|
@ -51,26 +50,19 @@ rules.tableCell = {
|
|||
rules.tableRow = {
|
||||
filter: 'tr',
|
||||
replacement: function (content, node) {
|
||||
const parentTable = nodeParentTable(node);
|
||||
if (tableShouldBeSkipped(parentTable)) return content;
|
||||
|
||||
var borderCells = '';
|
||||
var alignMap = { left: ':--', right: '--:', center: ':-:' };
|
||||
|
||||
if (isHeadingRow(node)) {
|
||||
const colCount = tableColCount(parentTable);
|
||||
for (var i = 0; i < colCount; i++) {
|
||||
const childNode = colCount >= node.childNodes.length ? null : node.childNodes[i];
|
||||
for (var i = 0; i < node.childNodes.length; i++) {
|
||||
var border = '---';
|
||||
var align = childNode ? (childNode.getAttribute('align') || '').toLowerCase() : '';
|
||||
var align = (
|
||||
node.childNodes[i].getAttribute('align') || ''
|
||||
).toLowerCase();
|
||||
|
||||
if (align) border = alignMap[align] || border;
|
||||
|
||||
if (childNode) {
|
||||
borderCells += cell(border, node.childNodes[i]);
|
||||
} else {
|
||||
borderCells += cell(border, null, i);
|
||||
}
|
||||
borderCells += cell(border, node.childNodes[i]);
|
||||
}
|
||||
}
|
||||
return '\n' + content + (borderCells ? '\n' + borderCells : '')
|
||||
|
|
@ -81,27 +73,13 @@ rules.table = {
|
|||
// Only convert tables with a heading row.
|
||||
// Tables with no heading row are kept using `keep` (see below).
|
||||
filter: function (node) {
|
||||
return node.nodeName === 'TABLE'
|
||||
return node.nodeName === 'TABLE' && isHeadingRow(node.rows[0])
|
||||
},
|
||||
|
||||
replacement: function (content, node) {
|
||||
if (tableShouldBeSkipped(node)) return content;
|
||||
|
||||
replacement: function (content) {
|
||||
// Ensure there are no blank lines
|
||||
content = content.replace(/\n+/g, '\n');
|
||||
|
||||
// If table has no heading, add an empty one so as to get a valid Markdown table
|
||||
var secondLine = content.trim().split('\n');
|
||||
if (secondLine.length >= 2) secondLine = secondLine[1];
|
||||
var secondLineIsDivider = secondLine.indexOf('| ---') === 0;
|
||||
|
||||
var columnCount = tableColCount(node);
|
||||
var emptyHeader = '';
|
||||
if (columnCount && !secondLineIsDivider) {
|
||||
emptyHeader = '|' + ' |'.repeat(columnCount) + '\n' + '|' + ' --- |'.repeat(columnCount);
|
||||
}
|
||||
|
||||
return '\n\n' + emptyHeader + content + '\n\n'
|
||||
content = content.replace('\n\n', '\n');
|
||||
return '\n\n' + content + '\n\n'
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -142,71 +120,16 @@ function isFirstTbody (element) {
|
|||
)
|
||||
}
|
||||
|
||||
function cell (content, node = null, index = null) {
|
||||
if (index === null) index = indexOf.call(node.parentNode.childNodes, node);
|
||||
function cell (content, node) {
|
||||
var index = indexOf.call(node.parentNode.childNodes, node);
|
||||
var prefix = ' ';
|
||||
if (index === 0) prefix = '| ';
|
||||
let filteredContent = content.trim().replace(/\n\r/g, '<br>').replace(/\n/g, "<br>");
|
||||
filteredContent = filteredContent.replace(/\|+/g, '\\|');
|
||||
while (filteredContent.length < 3) filteredContent += ' ';
|
||||
if (node) filteredContent = handleColSpan(filteredContent, node, ' ');
|
||||
return prefix + filteredContent + ' |'
|
||||
}
|
||||
|
||||
function nodeContainsTable(node) {
|
||||
if (!node.childNodes) return false;
|
||||
|
||||
for (let i = 0; i < node.childNodes.length; i++) {
|
||||
const child = node.childNodes[i];
|
||||
if (child.nodeName === 'TABLE') return true;
|
||||
if (nodeContainsTable(child)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Various conditions under which a table should be skipped - i.e. each cell
|
||||
// will be rendered one after the other as if they were paragraphs.
|
||||
function tableShouldBeSkipped(tableNode) {
|
||||
if (!tableNode) return true;
|
||||
if (!tableNode.rows) return true;
|
||||
if (tableNode.rows.length === 1 && tableNode.rows[0].childNodes.length <= 1) return true; // Table with only one cell
|
||||
|
||||
// Not sure why we're excluding this. possibly because it'll freak out the parser? --evilchili
|
||||
//if (nodeContainsTable(tableNode)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function nodeParentTable(node) {
|
||||
let parent = node.parentNode;
|
||||
while (parent.nodeName !== 'TABLE') {
|
||||
parent = parent.parentNode;
|
||||
if (!parent) return null;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
function handleColSpan(content, node, emptyChar) {
|
||||
const colspan = node.getAttribute('colspan') || 1;
|
||||
for (let i = 1; i < colspan; i++) {
|
||||
content += ' | ' + emptyChar.repeat(3);
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
function tableColCount(node) {
|
||||
let maxColCount = 0;
|
||||
for (let i = 0; i < node.rows.length; i++) {
|
||||
const row = node.rows[i];
|
||||
const colCount = row.childNodes.length;
|
||||
if (colCount > maxColCount) maxColCount = colCount;
|
||||
}
|
||||
return maxColCount
|
||||
return prefix + content + ' |'
|
||||
}
|
||||
|
||||
function tables (turndownService) {
|
||||
turndownService.keep(function (node) {
|
||||
return node.nodeName === 'TABLE'
|
||||
return node.nodeName === 'TABLE' && !isHeadingRow(node.rows[0])
|
||||
});
|
||||
for (var key in rules) turndownService.addRule(key, rules[key]);
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
class FroghatEditor extends Froghat {
|
||||
|
||||
run() {
|
||||
this.states = {
|
||||
VIEW: 'view',
|
||||
EDIT: 'edit',
|
||||
WYSIWYG: 'wysiwyg'
|
||||
}
|
||||
|
||||
this.turndown = new TurndownService({
|
||||
headingStyle: 'atx',
|
||||
codeBlockStyle: 'fenced',
|
||||
});
|
||||
this.turndown.use([turndownPluginGfm.gfm, turndownPluginGfm.tables]);
|
||||
this.turndown.keep(['pre']);
|
||||
this.#bindEvents();
|
||||
|
||||
this.plugins().forEach(plugin => { plugin.setEditable() });
|
||||
this.element.classList.add("loaded");
|
||||
this.view();
|
||||
}
|
||||
|
||||
#bindEvents() {
|
||||
this.element.addEventListener('keydown', (evt) => {
|
||||
if (this.state === this.states.VIEW) {
|
||||
return;
|
||||
}
|
||||
if (this.cachedMarkdown != this.element.textContent) {
|
||||
this.changed = true;
|
||||
this.cachedMarkdown = this.element.textContent;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
htmlToMarkdown(html) {
|
||||
return this.turndown.turndown(html || this.element.innerHTML);
|
||||
}
|
||||
|
||||
getMarkdown() {
|
||||
/*
|
||||
* Return the current markdown.
|
||||
*/
|
||||
if (this.getState() === this.states.EDIT) {
|
||||
var html = this.element.innerHTML;
|
||||
html = html.replaceAll(/<(?:div|br)>/ig, '');
|
||||
html = html.replaceAll(/<\/div>/ig, "\n");
|
||||
this.cachedMarkdown = decodeHtmlEntities(html);
|
||||
} else if (this.getState() === this.states.WYSIWYG) {
|
||||
this.cachedMarkdown = this.htmlToMarkdown(this.element.innerHTML);
|
||||
} if (!this.cachedMarkdown) {
|
||||
this.cachedMarkdown = this.element.textContent;
|
||||
}
|
||||
return this.cachedMarkdown;
|
||||
}
|
||||
|
||||
wysiwyg() {
|
||||
/*
|
||||
* Put the editor in WYSIWYG editing mode.
|
||||
*/
|
||||
if (this.getState() === this.states.WYSIWYG) {
|
||||
return;
|
||||
}
|
||||
this.changed = false;
|
||||
this.element.contentEditable = true;
|
||||
this.element.innerHTML = this.getHTML();
|
||||
Array.from(this.element.querySelectorAll('.macro')).forEach(el => {
|
||||
if (el.dataset.editable == "false") {
|
||||
el.contentEditable = false;
|
||||
el.style.opacity = 0.5;
|
||||
}
|
||||
});
|
||||
this.setState(this.states.WYSIWYG);
|
||||
}
|
||||
|
||||
edit() {
|
||||
/*
|
||||
* Put the editor into source editing mode.
|
||||
*/
|
||||
if (this.state === this.states.EDIT) {
|
||||
return;
|
||||
}
|
||||
this.changed = false;
|
||||
this.element.contentEditable = true;
|
||||
this.element.innerHTML = encodeHtmlEntities(this.getMarkdown());
|
||||
this.setState(this.states.EDIT);
|
||||
}
|
||||
|
||||
insertAtCursor(node) {
|
||||
var sel, range, html;
|
||||
sel = window.getSelection();
|
||||
range = sel.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
range.insertNode(node);
|
||||
range.setStartAfter(node);
|
||||
this.element.focus();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,584 +0,0 @@
|
|||
FroghatAPIv1 = {
|
||||
get: function(doc_id, callback) {
|
||||
(async () => {
|
||||
const raw = await fetch('/_/v1/get/' + doc_id, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
const res = await raw.json();
|
||||
if (res['code'] != 200) {
|
||||
console.error("APIv1 error: ", res)
|
||||
}
|
||||
callback(res);
|
||||
})();
|
||||
},
|
||||
|
||||
put: function(data, callback) {
|
||||
(async () => {
|
||||
const raw = await fetch('/_/v1/put/' + window.location.pathname, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'body': data
|
||||
}),
|
||||
});
|
||||
const res = await raw.json();
|
||||
if (res['code'] != 200) {
|
||||
console.error("APIv1 error: ", res)
|
||||
}
|
||||
callback(res);
|
||||
})();
|
||||
},
|
||||
|
||||
search: function(space, query, callback) {
|
||||
(async () => {
|
||||
const raw = await fetch('/_/v1/search/' + space, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'body': query
|
||||
}),
|
||||
});
|
||||
const res = await raw.json();
|
||||
if (res['code'] != 200) {
|
||||
console.error("APIv1 error: ", res)
|
||||
}
|
||||
callback(res);
|
||||
})();
|
||||
},
|
||||
};
|
||||
|
||||
class Froghat {
|
||||
constructor(settings) {
|
||||
/*
|
||||
* Create a new Froghat instance.
|
||||
*/
|
||||
this.api = settings.api || FroghatAPIv1;
|
||||
|
||||
this.element = document.getElementById(settings.editorId || 'froghat');
|
||||
|
||||
this.marked = marked;
|
||||
this.marked.use({
|
||||
breaks: false,
|
||||
gfm: true,
|
||||
});
|
||||
this.states = {
|
||||
VIEW: 'view',
|
||||
}
|
||||
this.cachedHTML = null;
|
||||
this.cachedMarkdown = null;
|
||||
this.state = null;
|
||||
this.changed = false;
|
||||
this.enabledPlugins = {};
|
||||
|
||||
settings.plugins.forEach(plugin => {
|
||||
this.enabledPlugins[plugin.name] = new plugin({name: plugin.name, wiki: this});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
run() {
|
||||
this.element.classList.add("loaded");
|
||||
this.view();
|
||||
}
|
||||
|
||||
plugins() {
|
||||
return Object.values(this.enabledPlugins).sort((a, b) => { a.precedence < b.precedence });
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
setState(newState) {
|
||||
this.state = newState;
|
||||
Object.values(this.states).forEach(state => {
|
||||
if (state == newState) {
|
||||
this.element.classList.add(state);
|
||||
} else {
|
||||
this.element.classList.remove(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
markdownToHTML(md) {
|
||||
var html = this.marked.parse(md);
|
||||
return html;
|
||||
}
|
||||
|
||||
getHTML(string) {
|
||||
/*
|
||||
* Convert the markdown source to HTML.
|
||||
*/
|
||||
if (this.changed || !this.cachedHTML) {
|
||||
this.cachedHTML = this.markdownToHTML(this.getMarkdown());
|
||||
}
|
||||
return this.cachedHTML;
|
||||
}
|
||||
|
||||
getMarkdown() {
|
||||
if (!this.cachedMarkdown) {
|
||||
this.cachedMarkdown = this.element.textContent;
|
||||
}
|
||||
return this.cachedMarkdown;
|
||||
}
|
||||
|
||||
|
||||
view() {
|
||||
/*
|
||||
* Convert the wiki read-only mode and display the current HTML.
|
||||
*/
|
||||
if (this.getState() === this.states.VIEW) {
|
||||
return;
|
||||
}
|
||||
this.element.innerHTML = this.getHTML();
|
||||
this.setState(this.states.VIEW);
|
||||
this.element.contentEditable = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FroghatPlugin {
|
||||
|
||||
constructor(settings) {
|
||||
this.name = settings.name;
|
||||
this.wiki = settings.wiki;
|
||||
this.precedence = 50;
|
||||
};
|
||||
|
||||
setEditable() {
|
||||
};
|
||||
|
||||
toMarkdown(html) {
|
||||
return html;
|
||||
};
|
||||
|
||||
toHTML(md) {
|
||||
return md;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
WIDGETS = {};
|
||||
|
||||
function loadWidget(name, callback) {
|
||||
if (Object.keys(WIDGETS).indexOf(name) == -1) {
|
||||
(async () => {
|
||||
await FroghatAPIv1.search("Widget", name, (res) => {
|
||||
if (res.code == 200) {
|
||||
function block(prefix) {
|
||||
return RegExp('##\\s*' + prefix + '.*?```\\w*(.+?)```', 'gims');
|
||||
};
|
||||
var html = res.response[0].body;
|
||||
var proc = block("Processor").exec(html)[1].trim();
|
||||
if (!proc) {
|
||||
proc = function(token, widget) {
|
||||
var name = token.keywords.split(" ").slice(1).join(" ");
|
||||
var ret = '';
|
||||
eval("ret = `" + widget.template + "`");
|
||||
return ret;
|
||||
}
|
||||
} else {
|
||||
eval(`proc = ${proc}`);
|
||||
}
|
||||
WIDGETS[name] = {
|
||||
template: block("Template").exec(html)[1],
|
||||
css: block("CSS").exec(html)[1],
|
||||
processor: proc
|
||||
};
|
||||
} else {
|
||||
WIDGETS[name] = {
|
||||
template: "",
|
||||
css: "",
|
||||
processor: function() { return `Invalid Widget: "${name}"` },
|
||||
};
|
||||
}
|
||||
if (callback) {
|
||||
callback(WIDGETS[name]);
|
||||
}
|
||||
});
|
||||
})();
|
||||
} else {
|
||||
return WIDGETS[name];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MacroPlugin extends FroghatPlugin {
|
||||
|
||||
macros = {
|
||||
// image: {}
|
||||
|
||||
widget: {
|
||||
inline: true,
|
||||
editable: false,
|
||||
|
||||
toHTML: (token, node) => {
|
||||
var widgetName = token.keywords.split(" ")[0];
|
||||
var contents = '';
|
||||
var cached = loadWidget(widgetName, (widget) => {
|
||||
contents = widget.processor(token, widget);
|
||||
var targets = wiki.element.querySelectorAll(`[data-macro-name="widget"][data-keywords="${token.keywords}"]`);
|
||||
targets.forEach(widgetElement => {
|
||||
widgetElement.style = widget.css;
|
||||
widgetElement.innerHTML = contents;
|
||||
});
|
||||
wiki.cachedHTML = wiki.element.innerHTML;
|
||||
});
|
||||
var ret = node + (cached ? cached.processor(token, cached) : "");
|
||||
return ret;
|
||||
},
|
||||
postprocess: (html) => {
|
||||
const buf = document.createElement('div');
|
||||
buf.innerHTML = html;
|
||||
var targets = buf.querySelectorAll(`[data-macro-name="widget"]`);
|
||||
targets.forEach(widgetElement => {
|
||||
var widget = WIDGETS[widgetElement.dataset.keywords.split(" ")[0]];
|
||||
if (widget) {
|
||||
widgetElement.style = widget.css;
|
||||
}
|
||||
});
|
||||
return buf.innerHTML;
|
||||
},
|
||||
},
|
||||
|
||||
style: {
|
||||
inline: false,
|
||||
editable: true,
|
||||
toHTML: (token, node) => {
|
||||
return node.replace(/class="macro"/, `class="macro ${token.keywords}"`);
|
||||
}
|
||||
},
|
||||
|
||||
multiline: {
|
||||
inline: false,
|
||||
editable: true,
|
||||
fromHTML: (node, markdown) => {
|
||||
var content = node.innerHTML.replaceAll("<br>", "\0");
|
||||
content = content.replaceAll("{{{", "{{{");
|
||||
content = content.replaceAll("}}}", "}}}");
|
||||
content = encodeHtmlEntities(content);
|
||||
return `{{{${content}}}}`;
|
||||
},
|
||||
},
|
||||
|
||||
user: {
|
||||
inline: true,
|
||||
toHTML: (token, node) => {
|
||||
return node + document.querySelector("nav > ul > li.user > a:first-child").outerHTML;
|
||||
},
|
||||
},
|
||||
toc: {
|
||||
inline: true,
|
||||
element: 'aside',
|
||||
postprocess: (html) => {
|
||||
const subList = (depth) => {
|
||||
var li = document.createElement("li");
|
||||
var ul = document.createElement("ul");
|
||||
li.appendChild(ul);
|
||||
return li;
|
||||
};
|
||||
|
||||
var tocIndex = 0;
|
||||
|
||||
const buf = document.createElement('div');
|
||||
buf.innerHTML = html;
|
||||
buf.querySelectorAll('[data-macro-name="toc"]').forEach(tocElement => {
|
||||
|
||||
var params = {
|
||||
depth: tocElement.dataset.paramDepth || 3,
|
||||
keywords: tocElement.dataset.paramKeywords || "",
|
||||
};
|
||||
|
||||
const headings = buf.querySelectorAll("h2, h3, h4, h5, h6");
|
||||
|
||||
var header = document.createElement("h2");
|
||||
header.className = 'header';
|
||||
header.textContent = 'Table of Contents';
|
||||
tocElement.prepend(header);
|
||||
|
||||
const toc = document.createElement("ul");
|
||||
toc.setAttribute('role', 'list');
|
||||
|
||||
var lastDepth = null;
|
||||
var ul = toc;
|
||||
headings.forEach(heading => {
|
||||
var depth = parseInt(heading.nodeName[1]) - 1;
|
||||
if (depth > params.depth) {
|
||||
return;
|
||||
}
|
||||
if (lastDepth === null) {
|
||||
lastDepth = depth;
|
||||
}
|
||||
|
||||
var index = document.createElement("li");
|
||||
var ref = camelCase(heading.textContent).join("");
|
||||
index.innerHTML = `<a href="#${heading.id}">${heading.textContent}</a>`;
|
||||
|
||||
var list = null;
|
||||
if (depth > lastDepth) {
|
||||
list = subList(depth);
|
||||
ul.appendChild(list);
|
||||
ul = list.firstChild;
|
||||
} else if (depth < lastDepth) {
|
||||
var list = subList(depth);
|
||||
toc.appendChild(list);
|
||||
ul = list.firstChild;
|
||||
}
|
||||
ul.appendChild(index);
|
||||
lastDepth = depth;
|
||||
});
|
||||
|
||||
if (params.keywords) {
|
||||
params.keywords.split(" ").forEach(className => {
|
||||
if (className) {
|
||||
toc.classList.add(className);
|
||||
}
|
||||
});
|
||||
}
|
||||
tocElement.appendChild(toc);
|
||||
});
|
||||
return buf.innerHTML;
|
||||
},
|
||||
},
|
||||
npc: {
|
||||
toHTML: (settings) => {
|
||||
var name = camelCase(settings.keywords).join(" ");
|
||||
var target = name.replaceAll(" ", "");
|
||||
return `<a href="/NPC/${target}" data-npc-name="${name}">👤 ${name}</a>`;
|
||||
},
|
||||
fromHTML: (node) => {
|
||||
return `{{npc ${node.firstChild.dataset.npcName}}}`;
|
||||
},
|
||||
},
|
||||
spell: {
|
||||
toHTML: (settings) => {
|
||||
var name = camelCase(settings.keywords).join(" ");
|
||||
var target = name.replaceAll(" ", "");
|
||||
return `<a href="/Spell/${target}" data-spell-name="${name}">✨ ${name}</a>`;
|
||||
},
|
||||
fromHTML: (node) => {
|
||||
return `{{spell ${node.firstChild.dataset.spellName}}}`;
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
getTokens = (pattern, source) => {
|
||||
const matched = source.matchAll(pattern);
|
||||
const tokens = [];
|
||||
if (!matched) {
|
||||
return tokens;
|
||||
}
|
||||
matched.forEach(match => {
|
||||
if (!this.macros[match.groups.name]) {
|
||||
return;
|
||||
}
|
||||
const token = {
|
||||
type: 'macro',
|
||||
source: match[0],
|
||||
matched: match,
|
||||
macro: this.macros[match.groups.name],
|
||||
keywords: (match.groups.keywords || '').trim(),
|
||||
inline: this.macros[match.groups.name].inline,
|
||||
editable: this.macros[match.groups.name].editable || false,
|
||||
params: {},
|
||||
rendered: '',
|
||||
};
|
||||
if (match.groups.parameters) {
|
||||
var params = decodeHtmlEntities(match.groups.parameters.trim());
|
||||
params.matchAll(this.paramPattern).forEach(param => {
|
||||
if (param.groups) {
|
||||
var name = param.groups.name;
|
||||
token.params[name] = decodeHtmlEntities(param.groups.value.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
token.rendered = this.renderToken(token);
|
||||
tokens.push(token);
|
||||
});
|
||||
return tokens;
|
||||
}
|
||||
|
||||
renderToken = (token) => {
|
||||
const tag = token.macro.element || (token.inline ? 'span' : 'div');
|
||||
var node = `<${tag} class="macro" data-plugin-name="macro" data-macro-name="${token.matched.groups.name}"`;
|
||||
|
||||
for (var name in token.params) {
|
||||
node += ` data-param-${name}="${(token.params[name] || "").trim()}"`;
|
||||
}
|
||||
|
||||
if (token.keywords) {
|
||||
node += ` data-keywords="${token.keywords}"`;
|
||||
}
|
||||
|
||||
node += ` data-inline="${token.inline}"`;
|
||||
|
||||
node += ` data-editable="${token.editable}"`;
|
||||
|
||||
node += ">";
|
||||
|
||||
if (token.macro.toHTML) {
|
||||
node = token.macro.toHTML(token, node);
|
||||
}
|
||||
|
||||
if (token.inline) {
|
||||
node += `</${tag}>`;
|
||||
}
|
||||
|
||||
// preserve the wrapping element, unless it is a paragraph.
|
||||
if (token.matched.groups.wrap) {
|
||||
if (token.matched.groups.wrap != '<p>') {
|
||||
node = token.matched.groups.wrap + node + token.matched.groups.endwrap;
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
constructor(settings) {
|
||||
super(settings);
|
||||
|
||||
this.pattern = new RegExp(
|
||||
'(?<!`)(?<wrap><[^>]+?>)?' + // capture the enclosing HTML tag, if any
|
||||
'{{' + // start of the macro
|
||||
'(?<name>\\w+)' + // the macro name
|
||||
'(?<keywords>(?:\\s(?:\\s*(?:[\\w-](?![\\w-]+=))+))+)?' + // zero or more keywords separated by spaces
|
||||
'(?<parameters>[^}<]+)?' + // anything else before the closing
|
||||
'\\s*(?<closed>}})?' + // is the tag closed?
|
||||
'(?<endwrap>(?:>!\\<)*?<\\/[^>]+?>)?', // capture the enclosing HTML tag, if any
|
||||
'mg'
|
||||
);
|
||||
this.endPattern = /<p>}}\s*<\/p>/mg;
|
||||
this.paramPattern = /\s*(?<name>[^=]+)="(?<value>[^"]*)"/g;
|
||||
this.multilinePattern = /(?<!\{{3}[\s\n]*)\{{3}[\s\n]*(?<content>(.(?!\}{3})*)+?)[\s\n]*\}{3}/smg;
|
||||
|
||||
const plugin = this;
|
||||
|
||||
this.wiki.marked.use({
|
||||
extensions: [
|
||||
{
|
||||
name: 'heading',
|
||||
renderer(token) {
|
||||
var ref = camelCase(token.text).join("");
|
||||
return `<h${token.depth} id='${ref}'>${token.text}</h${token.depth}>`;
|
||||
}
|
||||
},
|
||||
],
|
||||
hooks: {
|
||||
preprocess: (source) => {
|
||||
const matched = source.matchAll(plugin.multilinePattern);
|
||||
var md = source;
|
||||
matched.forEach(match => {
|
||||
var wrapper = '<span class="macro" data-plugin-name="macro" data-macro-name="multiline" data-inline="true" style="display: inline-block;">';
|
||||
var content = decodeHtmlEntities(match.groups.content)
|
||||
.replaceAll("\n", "::BR::")
|
||||
.replaceAll('`', '::QU::')
|
||||
.replaceAll('{', '::OC::')
|
||||
.replaceAll('}', '::CC::');
|
||||
md = md.replaceAll(match[0], wrapper + '\0' + content + '\0</span>');
|
||||
});
|
||||
return md;
|
||||
},
|
||||
postprocess: (html) => {
|
||||
plugin.getTokens(plugin.pattern, html).forEach(token => {
|
||||
var pat = new RegExp('(?<!<pre>.+?)' + token.source, 'mg');
|
||||
html = html.replaceAll(pat, token.rendered);
|
||||
});
|
||||
html = html.replaceAll(plugin.endPattern, '</div>');
|
||||
Object.values(plugin.macros).forEach(macro => {
|
||||
if (macro.postprocess) {
|
||||
html = macro.postprocess(html);
|
||||
}
|
||||
});
|
||||
|
||||
html = html.replaceAll("::BR::", "<br>")
|
||||
.replaceAll('::QU::', '`')
|
||||
.replaceAll('::OC::', '{')
|
||||
.replaceAll('::CC::', '}');
|
||||
|
||||
// remove unsafe html tags
|
||||
return DOMPurify.sanitize(html, {});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setEditable() {
|
||||
const plugin = this;
|
||||
this.wiki.turndown.addRule('macros', {
|
||||
filter: function (node, options) {
|
||||
return ((node.nodeName === 'ASIDE' || node.nodeName === 'DIV' || node.nodeName === 'SPAN') && node.dataset.pluginName == 'macro')
|
||||
},
|
||||
replacement: function (content, node, options) {
|
||||
var macro = plugin.macros[node.getAttribute('data-macro-name')];
|
||||
|
||||
if (macro.fromHTML) {
|
||||
return macro.fromHTML(node, content);
|
||||
}
|
||||
|
||||
var md = '{{' + node.dataset.macroName;
|
||||
if (node.dataset.keywords) {
|
||||
md += " " + node.dataset.keywords;
|
||||
}
|
||||
|
||||
for (var paramName in node.dataset) {
|
||||
if (paramName.indexOf("param") != 0) {
|
||||
continue;
|
||||
}
|
||||
md += ` ${paramName.replace('param', '').toLowerCase()}="${node.dataset[paramName]}"`
|
||||
};
|
||||
|
||||
if (node.dataset.inline == "false") {
|
||||
md = `\n\n${md}\n\n`;
|
||||
md += plugin.wiki.htmlToMarkdown(node.innerHTML);
|
||||
md += "\n\n}}\n\n";
|
||||
} else {
|
||||
md += "}}";
|
||||
}
|
||||
|
||||
// replace nulls with line breaks, for the multiline macro
|
||||
md = md.replaceAll('\0', "\n");
|
||||
|
||||
return md;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function camelCase(words) {
|
||||
var output = [];
|
||||
words.trim().split(/\s+/g).forEach(word => {
|
||||
var lcWord = word.toLowerCase();
|
||||
output.push(lcWord.charAt(0).toUpperCase() + lcWord.slice(1));
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
function b64encode(input) {
|
||||
return new TextEncoder().encode(input).toBase64();
|
||||
}
|
||||
|
||||
function b64decode(input) {
|
||||
return new TextDecoder().decode(Uint8Array.fromBase64(input));
|
||||
}
|
||||
|
||||
function decodeHtmlEntities(html) {
|
||||
var txt = document.createElement("textarea");
|
||||
txt.innerHTML = html;
|
||||
return txt.value;
|
||||
}
|
||||
|
||||
function encodeHtmlEntities(str) {
|
||||
return str.replace(/[\u00A0-\u9999<>\&]/g, i => '&#'+i.charCodeAt(0)+';')
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -113,7 +113,6 @@ table {
|
|||
|
||||
th {
|
||||
border-bottom: 1px solid #000;
|
||||
padding: 3px;
|
||||
}
|
||||
th, td {
|
||||
padding: 2px;
|
||||
|
|
@ -207,157 +206,3 @@ footer {
|
|||
menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: block;
|
||||
float: left;
|
||||
margin:5px;
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: block;
|
||||
float: right;
|
||||
margin: 5px;
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
.center {
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.box {
|
||||
border-radius: 5px;
|
||||
border: 1px solid black;
|
||||
background: #DEDEDE;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.striped > table {
|
||||
background: #FAFAFA;
|
||||
}
|
||||
.striped > table tr:nth-child(even) {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.equal-widths > table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layout > table {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
.layout > table > thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table td table {
|
||||
max-width:95%;
|
||||
}
|
||||
|
||||
pre {
|
||||
border: 1px dashed black;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin: 5px;
|
||||
background: #EEE;
|
||||
}
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
border: 1px dashed black;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
background: #EEE;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
div.macro {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
[data-macro-name="toc"] {
|
||||
display: inline;
|
||||
float: left;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
margin-right: 2em;
|
||||
margin-bottom: 2em;
|
||||
border: 1px solid #000;
|
||||
padding: 0ch 2ch;
|
||||
}
|
||||
|
||||
[data-macro-name="toc"] ul {
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
padding-left: 0ch;
|
||||
font-weight: normal;
|
||||
margin-left: 0px;
|
||||
|
||||
}
|
||||
|
||||
[data-macro-name="toc"] ul ul {
|
||||
padding-left: 2ch;
|
||||
}
|
||||
|
||||
[data-macro-name="toc"] > ul:first-child {
|
||||
padding-left: 0ch;
|
||||
margin-left: 0px;
|
||||
}
|
||||
[data-macro-name="toc"] > ul:first-child > li {
|
||||
padding-left: 0ch;
|
||||
margin-left: 0px;
|
||||
}
|
||||
[data-macro-name="toc"] > ul:first-child > li {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
[data-macro-name="toc"] > ul > li {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
[data-macro-name="toc"] > ul > li {
|
||||
padding-left: 0ch;
|
||||
}
|
||||
|
||||
[data-macro-name="toc"] a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
[data-macro-name="toc"] a:hover {
|
||||
background: #CCC;
|
||||
}
|
||||
|
||||
[data-macro-name="toc"] .header {
|
||||
font-size: var(--default-font-size);
|
||||
width: fit-content;
|
||||
margin: 2ch auto 0 auto;
|
||||
}
|
||||
|
||||
#froghat {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#froghat.loaded {
|
||||
display: block;
|
||||
}
|
||||
#froghat.wysiwyg {
|
||||
display: none,
|
||||
}
|
||||
#froghat.view {
|
||||
}
|
||||
|
||||
#froghat.edit {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#froghat.wysiwyg {
|
||||
}
|
||||
|
||||
#froghat.wysiwyg .md {
|
||||
opacity: 0.5;
|
||||
}
|
||||
6
src/ttfrog/themes/default/static/viewer/toastui-editor-viewer.min.css
vendored
Normal file
6
src/ttfrog/themes/default/static/viewer/toastui-editor-viewer.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
24
src/ttfrog/themes/default/static/viewer/toastui-editor-viewer.min.js
vendored
Normal file
24
src/ttfrog/themes/default/static/viewer/toastui-editor-viewer.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9
src/ttfrog/themes/default/static/viewer/viewer.css
Normal file
9
src/ttfrog/themes/default/static/viewer/viewer.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
@import 'toastui-editor-viewer.min.css';
|
||||
|
||||
#viewer {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.toastui-editor-contents {
|
||||
font-size: var(--default-font-size);
|
||||
}
|
||||
6
src/ttfrog/themes/default/static/viewer/viewer.js
Normal file
6
src/ttfrog/themes/default/static/viewer/viewer.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
var viewer = new toastui.Editor({
|
||||
viewer: true,
|
||||
el: document.querySelector("#viewer"),
|
||||
usageStatistics: false,
|
||||
});
|
||||
viewer.setMarkdown(document.getElementById("data_form__body").value);
|
||||
|
|
@ -123,13 +123,10 @@ def view(table, path):
|
|||
clean_table = re.sub(r"[^a-zA-Z0-9]", "", unquote(table))
|
||||
clean_path = re.sub(r"[^a-zA-Z0-9]", "", unquote(path))
|
||||
if clean_table != table or clean_path != clean_path:
|
||||
app.log.warning(f"Invalid table/path: {table=}, {path=}. Redirecting to {clean_table}/{clean_path}")
|
||||
return redirect(url_for("view", table=clean_table, path=clean_path), 302)
|
||||
|
||||
app.log.debug(f"Looking for {table=}, {path=}")
|
||||
page, error = get_page(request.path, table=table, create_okay=True)
|
||||
if error:
|
||||
app.log.error(error)
|
||||
g.messages.append(str(error))
|
||||
return rendered(page)
|
||||
|
||||
|
|
@ -153,7 +150,6 @@ def put(table, path):
|
|||
if parent:
|
||||
parent.update(members=list(set(parent.members + [updated])))
|
||||
app.db.save(parent)
|
||||
app.log.debug(f"Saved page at uri {updated.uri}")
|
||||
return api_response(response=dict(updated))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import pytest
|
||||
|
|
@ -13,7 +12,7 @@ from ttfrog import schema
|
|||
@pytest.fixture
|
||||
def app():
|
||||
with TemporaryDirectory() as path:
|
||||
fixture_db = GrungDB.with_schema(schema, path=Path(path), storage=MemoryStorage)
|
||||
fixture_db = GrungDB.with_schema(schema, path=path, storage=MemoryStorage)
|
||||
ttfrog.app.load_config(defaults=None, IN_MEMORY_DB=1)
|
||||
ttfrog.app.initialize(db=fixture_db, force=True)
|
||||
yield ttfrog.app
|
||||
|
|
@ -22,6 +21,8 @@ def app():
|
|||
|
||||
def test_create(app):
|
||||
user = schema.User(name="john", email="john@foo", password="powerfulCat")
|
||||
assert user.uid
|
||||
assert user._metadata.fields["uid"].unique
|
||||
|
||||
# insert
|
||||
john_something = app.db.save(user)
|
||||
|
|
@ -31,6 +32,7 @@ def test_create(app):
|
|||
assert app.db.User.get(doc_id=last_insert_id) == john_something
|
||||
assert john_something.name == user.name
|
||||
assert john_something.email == user.email
|
||||
assert john_something.uid == user.uid
|
||||
|
||||
# update
|
||||
john_something.name = "james?"
|
||||
|
|
@ -40,7 +42,6 @@ def test_create(app):
|
|||
assert before_update != after_update
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_permissions(app):
|
||||
john = app.db.save(schema.User(name="john", email="john@foo", password="powerfulCat"))
|
||||
players = app.db.save(schema.Group(name="players", members=[john]))
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import pytest
|
||||
from grung.db import GrungDB
|
||||
from tinydb.storages import MemoryStorage
|
||||
|
||||
import ttfrog.app
|
||||
from ttfrog import schema
|
||||
from ttfrog.bootstrap import bootstrap
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
with TemporaryDirectory() as path:
|
||||
fixture_db = GrungDB.with_schema(schema, path=Path(path), storage=MemoryStorage)
|
||||
ttfrog.app.load_config(defaults=None, IN_MEMORY_DB=1)
|
||||
ttfrog.app.initialize(db=fixture_db, force=True)
|
||||
ttfrog.app.web.config.update({"TESTING": True})
|
||||
bootstrap()
|
||||
yield ttfrog.app
|
||||
ttfrog.app.db.truncate()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def routes(app):
|
||||
import ttfrog.web
|
||||
|
||||
return ttfrog.web
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(routes):
|
||||
return ttfrog.app.web.test_client()
|
||||
|
||||
|
||||
def test_get(client, routes):
|
||||
response = client.get(ttfrog.app.config.VIEW_URI)
|
||||
assert response.status_code == 200
|
||||
Loading…
Reference in New Issue
Block a user