Compare commits
11 Commits
276a77b67e
...
2ddb1281c1
Author | SHA1 | Date |
---|---|---|
Tim Van Baak | 2ddb1281c1 | |
Tim Van Baak | d8f37f27f8 | |
Tim Van Baak | 44d64e356c | |
Tim Van Baak | fc74673bb2 | |
Tim Van Baak | 8bd8bdf269 | |
Tim Van Baak | f626c86f8a | |
Tim Van Baak | a51a60c171 | |
Tim Van Baak | e8fca58e71 | |
Tim Van Baak | c263d8b469 | |
Tim Van Baak | 396f8d405a | |
Tim Van Baak | 606537d959 |
|
@ -1,9 +1,9 @@
|
|||
"""
|
||||
Logic for operations that depend on a whole collection of documents.
|
||||
Logic for managing documents.
|
||||
"""
|
||||
import os
|
||||
|
||||
from redstring.parser import load, TagOptions, DocumentTag, TabOptions, DocumentTab, Document
|
||||
from redstring.parser import load, DocumentTag, DocumentTab, Document, TabOptions, TagOptions
|
||||
|
||||
|
||||
def generate_index_document(directory: str) -> Document:
|
||||
|
@ -17,16 +17,18 @@ def generate_index_document(directory: str) -> Document:
|
|||
document: Document = load(f)
|
||||
|
||||
# Check if this document specifies a tab, and create it if necessary.
|
||||
category = document.get_tag('category')
|
||||
if not category:
|
||||
if category_tag := document.get_tag('category'):
|
||||
category = category_tag.value
|
||||
else:
|
||||
category = 'index'
|
||||
if category not in categories:
|
||||
categories[category] = {}
|
||||
category_tab = categories[category]
|
||||
|
||||
# Check if this document specifies a topic, and create it if necessary.
|
||||
topic = document.get_tag('topic')
|
||||
if not topic:
|
||||
if topic_tag := document.get_tag('topic'):
|
||||
topic = topic_tag.value
|
||||
else:
|
||||
topic = 'uncategorized'
|
||||
if '.' in topic:
|
||||
topic, subtopic = topic.split('.', maxsplit=1)
|
||||
|
@ -62,8 +64,36 @@ def generate_index_document(directory: str) -> Document:
|
|||
docs = sorted(categories[category][topic], key=lambda x: x[0])
|
||||
doc_links = map(document_link, docs)
|
||||
value = '- ' + '<br>- '.join(doc_links)
|
||||
built_tags.append(DocumentTag(topic, value, TagOptions(), []))
|
||||
built_tags.append(DocumentTag(topic, value))
|
||||
|
||||
built_tabs.append(DocumentTab(category, built_tags, TabOptions()))
|
||||
built_tabs.append(DocumentTab(category, built_tags))
|
||||
|
||||
return Document(built_tabs)
|
||||
|
||||
|
||||
def generate_default_document(doc_id) -> Document:
|
||||
"""
|
||||
Generate a blank document.
|
||||
"""
|
||||
return Document(tabs=[
|
||||
DocumentTab(
|
||||
name='tags',
|
||||
tags=[
|
||||
DocumentTag('id', doc_id),
|
||||
DocumentTag('title', ''),
|
||||
DocumentTag('author', ''),
|
||||
DocumentTag('date', ''),
|
||||
DocumentTag('source', ''),
|
||||
DocumentTag('summary', ''),
|
||||
DocumentTag('category', 'index', TagOptions(private=True)),
|
||||
DocumentTag('topic', 'uncategorized', TagOptions(private=True)),
|
||||
]
|
||||
),
|
||||
DocumentTab(
|
||||
name='notes',
|
||||
tags=[
|
||||
DocumentTag('notes', ''),
|
||||
],
|
||||
options=TabOptions(private=True, hide_names=True)
|
||||
)
|
||||
])
|
||||
|
|
|
@ -4,7 +4,7 @@ Logic for reading and writing documents from files.
|
|||
import argparse
|
||||
from collections import OrderedDict
|
||||
import json
|
||||
from typing import Any, List, IO
|
||||
from typing import Any, List, IO, Optional
|
||||
|
||||
|
||||
#
|
||||
|
@ -23,6 +23,9 @@ class TabOptions:
|
|||
def __init__(self, **kwargs) -> None:
|
||||
self.options: dict = OrderedDict(**kwargs)
|
||||
|
||||
def to_json(self):
|
||||
return self.options
|
||||
|
||||
@property
|
||||
def priority(self) -> int:
|
||||
"""Priority determines tab order."""
|
||||
|
@ -60,13 +63,10 @@ class TagOptions:
|
|||
_PRIVATE_KEY = 'private'
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.options = OrderedDict(**kwargs)
|
||||
# Tag value is a hyperlink
|
||||
self.hyperlink: bool = kwargs.get('hyperlink', False)
|
||||
# Tag value contains redstring interlinks
|
||||
self.interlink: bool = kwargs.get('interlink', False)
|
||||
# Hide the tag from unauthenticated viewers
|
||||
self.private: bool = kwargs.get('private', False)
|
||||
self.options: dict = OrderedDict(**kwargs)
|
||||
|
||||
def to_json(self):
|
||||
return self.options
|
||||
|
||||
@property
|
||||
def hyperlink(self) -> bool:
|
||||
|
@ -98,10 +98,22 @@ class DocumentSubtag:
|
|||
"""
|
||||
A keyvalue describing a document subject.
|
||||
"""
|
||||
def __init__(self, name: str, value: str, options: TagOptions) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
value: str,
|
||||
options: Optional[TagOptions] = None
|
||||
) -> None:
|
||||
self.name: str = name
|
||||
self.value: str = value
|
||||
self.options: TagOptions = options
|
||||
self.options: TagOptions = options if options is not None else TagOptions()
|
||||
|
||||
def to_json(self):
|
||||
return OrderedDict(
|
||||
name=self.name,
|
||||
value=self.value,
|
||||
options=self.options.to_json(),
|
||||
)
|
||||
|
||||
|
||||
class DocumentTag:
|
||||
|
@ -112,23 +124,43 @@ class DocumentTag:
|
|||
self,
|
||||
name: str,
|
||||
value: str,
|
||||
options: TagOptions,
|
||||
subtags: List[DocumentSubtag]
|
||||
options: Optional[TagOptions] = None,
|
||||
subtags: Optional[List[DocumentSubtag]] = None
|
||||
) -> None:
|
||||
self.name: str = name
|
||||
self.value = value
|
||||
self.options = options
|
||||
self.subtags = subtags
|
||||
self.value: str = value
|
||||
self.options: TagOptions = options if options is not None else TagOptions()
|
||||
self.subtags: list = subtags if subtags is not None else []
|
||||
|
||||
def to_json(self):
|
||||
return OrderedDict(
|
||||
name=self.name,
|
||||
value=self.value,
|
||||
options=self.options.to_json(),
|
||||
subtags=[subtag.to_json() for subtag in self.subtags],
|
||||
)
|
||||
|
||||
|
||||
class DocumentTab:
|
||||
"""
|
||||
A division of tags within a document.
|
||||
"""
|
||||
def __init__(self, name: str, tags: List[DocumentTag], options: TabOptions) -> None:
|
||||
self.name = name
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
tags: List[DocumentTag],
|
||||
options: Optional[TabOptions] = None
|
||||
) -> None:
|
||||
self.name: str = name
|
||||
self.tags: List[DocumentTag] = tags
|
||||
self.options: TabOptions = options
|
||||
self.options: TabOptions = options if options is not None else TabOptions()
|
||||
|
||||
def to_json(self):
|
||||
return OrderedDict(
|
||||
name=self.name,
|
||||
tags=[tag.to_json() for tag in self.tags],
|
||||
options=self.options.to_json(),
|
||||
)
|
||||
|
||||
def get_tag(self, name: str):
|
||||
for tag in self.tags:
|
||||
|
@ -136,6 +168,11 @@ class DocumentTab:
|
|||
return tag
|
||||
return None
|
||||
|
||||
def get_tag_value(self, name: str, default: str):
|
||||
if tag := self.get_tag(name):
|
||||
return tag.value
|
||||
return default
|
||||
|
||||
|
||||
class Document:
|
||||
"""
|
||||
|
@ -144,6 +181,9 @@ class Document:
|
|||
def __init__(self, tabs: List[DocumentTab]) -> None:
|
||||
self.tabs: List[DocumentTab] = tabs
|
||||
|
||||
def to_json(self):
|
||||
return [tab.to_json() for tab in self.tabs]
|
||||
|
||||
def __iter__(self):
|
||||
return self.tabs.__iter__()
|
||||
|
||||
|
@ -160,6 +200,11 @@ class Document:
|
|||
return tag
|
||||
return None
|
||||
|
||||
def get_tag_value(self, name: str, default: str):
|
||||
if tag := self.get_tag(name):
|
||||
return tag.value
|
||||
return default
|
||||
|
||||
|
||||
#
|
||||
# Parsing functions
|
||||
|
@ -176,16 +221,6 @@ def load(fd: IO) -> Document:
|
|||
return parse_document_from_json(parsed_json)
|
||||
|
||||
|
||||
def loads(string: str) -> Document:
|
||||
"""
|
||||
Load a document from a string.
|
||||
"""
|
||||
parsed_json = json.loads(string, object_pairs_hook=OrderedDict)
|
||||
if not isinstance(parsed_json, list):
|
||||
raise ValueError('Parsing as document, expected list')
|
||||
return parse_document_from_json(parsed_json)
|
||||
|
||||
|
||||
def parse_document_from_json(parsed_json: list) -> Document:
|
||||
"""
|
||||
Parses JSON into a Document object.
|
||||
|
@ -282,6 +317,14 @@ def parse_subtag_from_json(subtag_json: dict) -> DocumentSubtag:
|
|||
return DocumentSubtag(name, value, options)
|
||||
|
||||
|
||||
def dump(doc: Document, fd: IO):
|
||||
"""
|
||||
Write a document to a file descriptor.
|
||||
"""
|
||||
dumped_json = doc.to_json()
|
||||
json.dump(dumped_json, fd, indent=2)
|
||||
|
||||
|
||||
#
|
||||
# CLI functions
|
||||
#
|
||||
|
|
|
@ -10,12 +10,13 @@ from flask import (
|
|||
Flask,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
safe_join,
|
||||
url_for,
|
||||
)
|
||||
|
||||
from redstring.library import generate_index_document
|
||||
from redstring.parser import load
|
||||
from redstring.library import generate_index_document, generate_default_document
|
||||
from redstring.parser import load, dump, DocumentTab, DocumentTag, TagOptions, DocumentSubtag
|
||||
|
||||
|
||||
CONFIG_ENVVAR = 'REDSTRING_CONFIG'
|
||||
|
@ -23,18 +24,18 @@ CONFIG_ENVVAR = 'REDSTRING_CONFIG'
|
|||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@app.route('/', methods=['GET'])
|
||||
def root():
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
@app.route('/index/')
|
||||
@app.route('/index/', methods=['GET'])
|
||||
def index():
|
||||
document = generate_index_document(current_app.config['root'])
|
||||
return render_template('doc.jinja', document=document, index=True)
|
||||
|
||||
|
||||
@app.route('/doc/<document_id>')
|
||||
@app.route('/doc/<document_id>', methods=['GET'])
|
||||
def document(document_id):
|
||||
doc_path = safe_join(current_app.config['root'], f'{document_id}.json')
|
||||
if not os.path.isfile(doc_path):
|
||||
|
@ -44,6 +45,57 @@ def document(document_id):
|
|||
return render_template('doc.jinja', document=doc, index=False)
|
||||
|
||||
|
||||
@app.route('/new/', methods=['GET'])
|
||||
def new():
|
||||
document_id = 'new'
|
||||
new_doc = generate_default_document(document_id)
|
||||
doc_path = safe_join(current_app.config['root'], f'{document_id}.json')
|
||||
with open(doc_path, 'w') as f:
|
||||
dump(new_doc, f)
|
||||
return redirect(url_for('document', document_id=document_id))
|
||||
|
||||
|
||||
@app.route('/edit/<document_id>', methods=['GET', 'POST'])
|
||||
def edit(document_id):
|
||||
# Load the document to edit
|
||||
doc_path = safe_join(current_app.config['root'], f'{document_id}.json')
|
||||
if not os.path.isfile(doc_path):
|
||||
return abort(404)
|
||||
with open(doc_path) as f:
|
||||
doc = load(f)
|
||||
|
||||
# Check for structural change requests
|
||||
if add := request.args.get('add'):
|
||||
if add == 'tab':
|
||||
new_tab = DocumentTab('newtab', [])
|
||||
doc.tabs.append(new_tab)
|
||||
with open(doc_path, 'w') as f:
|
||||
dump(doc, f)
|
||||
return redirect(url_for('edit', document_id=document_id))
|
||||
elif add == 'tag':
|
||||
if tab_name := request.args.get('tab'):
|
||||
tab = doc.get_tab(tab_name)
|
||||
new_tag = DocumentTag('tag', '', TagOptions(private=True))
|
||||
tab.tags.append(new_tag)
|
||||
with open(doc_path, 'w') as f:
|
||||
dump(doc, f)
|
||||
return redirect(url_for('edit', document_id=document_id))
|
||||
return abort(400)
|
||||
elif add == 'subtag':
|
||||
if tag_name := request.args.get('tag'):
|
||||
tag = doc.get_tag(tag_name)
|
||||
new_subtag = DocumentSubtag('subtag', '', TagOptions(private=True))
|
||||
tag.subtags.append(new_subtag)
|
||||
with open(doc_path, 'w') as f:
|
||||
dump(doc, f)
|
||||
return redirect(url_for('edit', document_id=document_id))
|
||||
return abort(400)
|
||||
|
||||
# Otherwise, return the editor page
|
||||
else:
|
||||
return render_template('edit.jinja', document=doc, index=False)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Run the redstring server.")
|
||||
parser.add_argument("--config", help="Config file path.")
|
||||
|
|
|
@ -101,12 +101,20 @@
|
|||
table.page-table td:nth-child(2) {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
table.page-table td:nth-child(2) a {
|
||||
table.page-table a {
|
||||
color: #8af;
|
||||
}
|
||||
table.page-table td:nth-child(2) a:visited {
|
||||
table.page-table a:visited {
|
||||
color: #88f;
|
||||
}
|
||||
|
||||
/* Edit page styling */
|
||||
input.tag-name {
|
||||
}
|
||||
textarea.tag-value {
|
||||
resize: none;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
{% block page_scripts %}{% endblock %}
|
||||
</head>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'base.jinja' %}
|
||||
|
||||
{% set page_title = 'tmp' -%}
|
||||
{% set page_summary = 'tmpp' %}
|
||||
{% set page_title = document.get_tag_value('title', document.get_tag('id').value) -%}
|
||||
{% set page_summary = document.get_tag_value('summary', '') %}
|
||||
|
||||
{% block page_scripts %}
|
||||
<script>
|
||||
|
@ -14,7 +14,7 @@ function selectTab(name) {
|
|||
.forEach(e => e.classList.remove("tab-down"));
|
||||
Array.from(document.getElementsByClassName("tab-page"))
|
||||
.forEach(e => e.classList.remove("tab-page-selected"));
|
||||
// Select the new tab and content
|
||||
// Select the new tab and content
|
||||
tab.classList.add("tab-down");
|
||||
let content = document.getElementById(name + "-page");
|
||||
content.classList.add("tab-page-selected");
|
||||
|
@ -38,22 +38,26 @@ window.onload = function () {
|
|||
<div id="{{ tab.name }}-page" class="tab-page{% if selected %} tab-page-selected{% endif %}">
|
||||
<table id="{{ tab.name }}-page-table" class="page-table">
|
||||
{% for tag in tab.tags %}
|
||||
{%- if not tag.options.private -%}
|
||||
<tr {% if tab.options.hide_names %}class="hide-tag-name"{% endif %}>
|
||||
<td>{{ tag.name }}</td>
|
||||
<td>{{ make_tag_value(tag) }}</td>
|
||||
</tr>
|
||||
{%- endif -%}
|
||||
{% for subtag in tag.subtags %}
|
||||
{%- if not tag.options.private and not subtag.options.private -%}
|
||||
<tr {% if tab.options.hide_names %}class="hide-tag-name"{% endif %}>
|
||||
<td>{% if loop.last %}└{% else %}├{% endif %} {{ subtag.name }}</td>
|
||||
<td>{{ make_tag_value(subtag) }}</td>
|
||||
</tr>
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# TODO: tag.interlink and tag.private support #}
|
||||
{# TODO: tag.interlink #}
|
||||
{% macro make_tag_value(tag) -%}
|
||||
{%- if tag.options.hyperlink -%}
|
||||
<a href="{{ tag.value }}">{{ tag.value }}</a>
|
||||
|
@ -62,8 +66,19 @@ window.onload = function () {
|
|||
{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{# TODO: tab.priority and tab.private support #}
|
||||
{# TODO: tab.priority support #}
|
||||
{% block page_content %}
|
||||
<div id="tabs">{% for tab in document %}{{ make_content_tab(tab, loop.first) }}{% endfor %}{% if not index %}<div id="index" class="tab tab-right"><a href="/index/">index</a></div>{% endif %}</div>
|
||||
{% for tab in document %}{{ make_tab_page(tab, loop.first) }}{% endfor %}
|
||||
<div id="tabs">
|
||||
{%- for tab in document -%}
|
||||
{%- if not tab.options.private -%}
|
||||
{{ make_content_tab(tab, loop.first) }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if not index -%}
|
||||
<div id="index" class="tab tab-right"><a href="/index/">index</a></div>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{% for tab in document -%}
|
||||
{{ make_tab_page(tab, loop.first) }}
|
||||
{%- endfor -%}
|
||||
{% endblock page_content %}
|
|
@ -0,0 +1,135 @@
|
|||
{% extends 'base.jinja' %}
|
||||
|
||||
{% set page_title = document.get_tag_value('title', document.get_tag('id').value) -%}
|
||||
{% set page_summary = document.get_tag_value('summary', '') %}
|
||||
|
||||
{% block page_scripts %}
|
||||
<script>
|
||||
var newTabCounter = 0;
|
||||
|
||||
function selectTab(name) {
|
||||
|
||||
let tab = document.getElementById("tab-" + name);
|
||||
if (tab)
|
||||
{
|
||||
// Unselect all tabs and content
|
||||
Array.from(document.getElementsByClassName("tab-content"))
|
||||
.forEach(e => e.classList.remove("tab-down"));
|
||||
Array.from(document.getElementsByClassName("tab-page"))
|
||||
.forEach(e => e.classList.remove("tab-page-selected"));
|
||||
// Select the new tab and content
|
||||
tab.classList.add("tab-down");
|
||||
let content = document.getElementById(name + "-page");
|
||||
content.classList.add("tab-page-selected");
|
||||
}
|
||||
}
|
||||
|
||||
function addTab() {
|
||||
let newTabName = "newtab" + ++newTabCounter;
|
||||
let tabsDiv = document.querySelector("#tabs");
|
||||
let wrapper = document.querySelector("#wrapper");
|
||||
let newTabTab = document.querySelector("#newtab");
|
||||
|
||||
// Add tab div
|
||||
let newTab = document.createElement("div");
|
||||
newTab.id = "tab-" + newTabName;
|
||||
newTab.classList = "tab tab-content";
|
||||
newTab.contenteditable = true;
|
||||
newTab.innerText = newTabName;
|
||||
newTab.onclick = function() { selectTab(newTabName); };
|
||||
tabsDiv.insertBefore(newTab, newTabTab);
|
||||
|
||||
// Add page div
|
||||
let newPage = document.createElement("div");
|
||||
newPage.id = newTabName + "-page";
|
||||
newPage.classList = "tag-page";
|
||||
let newPageTable = document.createElement("table");
|
||||
newPageTable.id = newTabName + "-page-table";
|
||||
newPageTable.classList = "page-table";
|
||||
wrapper.appendChild(newPage);
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
// Respect fragment as a tab selector shortcut
|
||||
if (window.location.hash) {
|
||||
selectTab(window.location.hash.substring(1));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock page_scripts %}
|
||||
|
||||
{% macro make_content_tab(tab, selected) -%}
|
||||
<div id="tab-{{ tab.name }}" class="tab tab-content{% if selected %} tab-down{% endif %}{% if index %} tab-right{% endif %}" onclick="javascript:selectTab('{{ tab.name }}')">{{ tab.name }}</div>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro make_tab_page(tab, selected) %}
|
||||
<div id="{{ tab.name }}-page" class="tab-page{% if selected %} tab-page-selected{% endif %}">
|
||||
<table id="{{ tab.name }}-page-table" class="page-table">
|
||||
|
||||
<tr>
|
||||
<td><i>tab name</i></td>
|
||||
<td><div contenteditable>{{ tab.name }}</div></td>
|
||||
</tr>
|
||||
|
||||
{% for tag in tab.tags %}
|
||||
<tr>
|
||||
{%- if tag.name == 'id' -%}
|
||||
<td>{{ tag.name }}</td>
|
||||
{%- else -%}
|
||||
<td><div contenteditable>{{ tag.name }}</div></td>
|
||||
{% endif %}
|
||||
<td><div contenteditable>{{ tag.value }}</div></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
{%- if tag.subtags -%}
|
||||
│
|
||||
{%- else -%}
|
||||
<a href="/edit/{{ document.get_tag('id').value }}?add=subtag&tag={{ tag.name }}">└ +</a>
|
||||
{%- endif -%}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
{% for subtag in tag.subtags %}
|
||||
<tr>
|
||||
<td><div contenteditable>{{ subtag.name }}</div></td>
|
||||
<td><div contenteditable>{{ subtag.value }}</div></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
{%- if not loop.last -%}
|
||||
│
|
||||
{%- else -%}
|
||||
<a href="/edit/{{ document.get_tag('id').value }}?add=subtag&tag={{ tag.name }}">└ +</a>
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
<tr>
|
||||
<td><a href="/edit/{{ document.get_tag('id').value }}?add=tag&tab={{ tab.name }}">Add tag</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{# TODO: tab.priority support #}
|
||||
{% block page_content %}
|
||||
<div id="tabs">
|
||||
{%- for tab in document -%}
|
||||
{{ make_content_tab(tab, loop.first) }}
|
||||
{%- endfor -%}
|
||||
<div id="newtab" class="tab"><a href="/edit/{{ document.get_tag('id').value }}?add=tab">+</div>
|
||||
<div id="index" class="tab tab-right"><a href="/index/">index</a></div>
|
||||
</div>
|
||||
{% for tab in document -%}
|
||||
{{ make_tab_page(tab, loop.first) }}
|
||||
{%- endfor -%}
|
||||
{% endblock page_content %}
|
Loading…
Reference in New Issue