Compare commits

..

11 Commits

6 changed files with 333 additions and 50 deletions

View File

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

View File

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

View File

@ -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.")

View File

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

View File

@ -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>
@ -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 %}&#9492;{% else %}&#9500;{% 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 %}

View File

@ -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 -%}
&#9474;
{%- else -%}
<a href="/edit/{{ document.get_tag('id').value }}?add=subtag&tag={{ tag.name }}">&#9492; +</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 -%}
&#9474;
{%- else -%}
<a href="/edit/{{ document.get_tag('id').value }}?add=subtag&tag={{ tag.name }}">&#9492; +</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 %}