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 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: def generate_index_document(directory: str) -> Document:
@ -17,16 +17,18 @@ def generate_index_document(directory: str) -> Document:
document: Document = load(f) document: Document = load(f)
# Check if this document specifies a tab, and create it if necessary. # Check if this document specifies a tab, and create it if necessary.
category = document.get_tag('category') if category_tag := document.get_tag('category'):
if not category: category = category_tag.value
else:
category = 'index' category = 'index'
if category not in categories: if category not in categories:
categories[category] = {} categories[category] = {}
category_tab = categories[category] category_tab = categories[category]
# Check if this document specifies a topic, and create it if necessary. # Check if this document specifies a topic, and create it if necessary.
topic = document.get_tag('topic') if topic_tag := document.get_tag('topic'):
if not topic: topic = topic_tag.value
else:
topic = 'uncategorized' topic = 'uncategorized'
if '.' in topic: if '.' in topic:
topic, subtopic = topic.split('.', maxsplit=1) 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]) docs = sorted(categories[category][topic], key=lambda x: x[0])
doc_links = map(document_link, docs) doc_links = map(document_link, docs)
value = '- ' + '<br>- '.join(doc_links) 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) 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 import argparse
from collections import OrderedDict from collections import OrderedDict
import json 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: def __init__(self, **kwargs) -> None:
self.options: dict = OrderedDict(**kwargs) self.options: dict = OrderedDict(**kwargs)
def to_json(self):
return self.options
@property @property
def priority(self) -> int: def priority(self) -> int:
"""Priority determines tab order.""" """Priority determines tab order."""
@ -60,13 +63,10 @@ class TagOptions:
_PRIVATE_KEY = 'private' _PRIVATE_KEY = 'private'
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
self.options = OrderedDict(**kwargs) self.options: dict = OrderedDict(**kwargs)
# Tag value is a hyperlink
self.hyperlink: bool = kwargs.get('hyperlink', False) def to_json(self):
# Tag value contains redstring interlinks return self.options
self.interlink: bool = kwargs.get('interlink', False)
# Hide the tag from unauthenticated viewers
self.private: bool = kwargs.get('private', False)
@property @property
def hyperlink(self) -> bool: def hyperlink(self) -> bool:
@ -98,10 +98,22 @@ class DocumentSubtag:
""" """
A keyvalue describing a document subject. 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.name: str = name
self.value: str = value 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: class DocumentTag:
@ -112,23 +124,43 @@ class DocumentTag:
self, self,
name: str, name: str,
value: str, value: str,
options: TagOptions, options: Optional[TagOptions] = None,
subtags: List[DocumentSubtag] subtags: Optional[List[DocumentSubtag]] = None
) -> None: ) -> None:
self.name: str = name self.name: str = name
self.value = value self.value: str = value
self.options = options self.options: TagOptions = options if options is not None else TagOptions()
self.subtags = subtags 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: class DocumentTab:
""" """
A division of tags within a document. A division of tags within a document.
""" """
def __init__(self, name: str, tags: List[DocumentTag], options: TabOptions) -> None: def __init__(
self.name = name self,
name: str,
tags: List[DocumentTag],
options: Optional[TabOptions] = None
) -> None:
self.name: str = name
self.tags: List[DocumentTag] = tags 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): def get_tag(self, name: str):
for tag in self.tags: for tag in self.tags:
@ -136,6 +168,11 @@ class DocumentTab:
return tag return tag
return None return None
def get_tag_value(self, name: str, default: str):
if tag := self.get_tag(name):
return tag.value
return default
class Document: class Document:
""" """
@ -144,6 +181,9 @@ class Document:
def __init__(self, tabs: List[DocumentTab]) -> None: def __init__(self, tabs: List[DocumentTab]) -> None:
self.tabs: List[DocumentTab] = tabs self.tabs: List[DocumentTab] = tabs
def to_json(self):
return [tab.to_json() for tab in self.tabs]
def __iter__(self): def __iter__(self):
return self.tabs.__iter__() return self.tabs.__iter__()
@ -160,6 +200,11 @@ class Document:
return tag return tag
return None return None
def get_tag_value(self, name: str, default: str):
if tag := self.get_tag(name):
return tag.value
return default
# #
# Parsing functions # Parsing functions
@ -176,16 +221,6 @@ def load(fd: IO) -> Document:
return parse_document_from_json(parsed_json) 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: def parse_document_from_json(parsed_json: list) -> Document:
""" """
Parses JSON into a Document object. Parses JSON into a Document object.
@ -282,6 +317,14 @@ def parse_subtag_from_json(subtag_json: dict) -> DocumentSubtag:
return DocumentSubtag(name, value, options) 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 # CLI functions
# #

View File

@ -10,12 +10,13 @@ from flask import (
Flask, Flask,
redirect, redirect,
render_template, render_template,
request,
safe_join, safe_join,
url_for, url_for,
) )
from redstring.library import generate_index_document from redstring.library import generate_index_document, generate_default_document
from redstring.parser import load from redstring.parser import load, dump, DocumentTab, DocumentTag, TagOptions, DocumentSubtag
CONFIG_ENVVAR = 'REDSTRING_CONFIG' CONFIG_ENVVAR = 'REDSTRING_CONFIG'
@ -23,18 +24,18 @@ CONFIG_ENVVAR = 'REDSTRING_CONFIG'
app = Flask(__name__) app = Flask(__name__)
@app.route('/') @app.route('/', methods=['GET'])
def root(): def root():
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/index/') @app.route('/index/', methods=['GET'])
def index(): def index():
document = generate_index_document(current_app.config['root']) document = generate_index_document(current_app.config['root'])
return render_template('doc.jinja', document=document, index=True) 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): def document(document_id):
doc_path = safe_join(current_app.config['root'], f'{document_id}.json') doc_path = safe_join(current_app.config['root'], f'{document_id}.json')
if not os.path.isfile(doc_path): if not os.path.isfile(doc_path):
@ -44,6 +45,57 @@ def document(document_id):
return render_template('doc.jinja', document=doc, index=False) 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(): def main():
parser = argparse.ArgumentParser(description="Run the redstring server.") parser = argparse.ArgumentParser(description="Run the redstring server.")
parser.add_argument("--config", help="Config file path.") parser.add_argument("--config", help="Config file path.")

View File

@ -101,12 +101,20 @@
table.page-table td:nth-child(2) { table.page-table td:nth-child(2) {
white-space: pre-wrap; white-space: pre-wrap;
} }
table.page-table td:nth-child(2) a { table.page-table a {
color: #8af; color: #8af;
} }
table.page-table td:nth-child(2) a:visited { table.page-table a:visited {
color: #88f; color: #88f;
} }
/* Edit page styling */
input.tag-name {
}
textarea.tag-value {
resize: none;
width: 100%;
}
</style> </style>
{% block page_scripts %}{% endblock %} {% block page_scripts %}{% endblock %}
</head> </head>

View File

@ -1,7 +1,7 @@
{% extends 'base.jinja' %} {% extends 'base.jinja' %}
{% set page_title = 'tmp' -%} {% set page_title = document.get_tag_value('title', document.get_tag('id').value) -%}
{% set page_summary = 'tmpp' %} {% set page_summary = document.get_tag_value('summary', '') %}
{% block page_scripts %} {% block page_scripts %}
<script> <script>
@ -14,7 +14,7 @@ function selectTab(name) {
.forEach(e => e.classList.remove("tab-down")); .forEach(e => e.classList.remove("tab-down"));
Array.from(document.getElementsByClassName("tab-page")) Array.from(document.getElementsByClassName("tab-page"))
.forEach(e => e.classList.remove("tab-page-selected")); .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"); tab.classList.add("tab-down");
let content = document.getElementById(name + "-page"); let content = document.getElementById(name + "-page");
content.classList.add("tab-page-selected"); 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 %}"> <div id="{{ tab.name }}-page" class="tab-page{% if selected %} tab-page-selected{% endif %}">
<table id="{{ tab.name }}-page-table" class="page-table"> <table id="{{ tab.name }}-page-table" class="page-table">
{% for tag in tab.tags %} {% for tag in tab.tags %}
{%- if not tag.options.private -%}
<tr {% if tab.options.hide_names %}class="hide-tag-name"{% endif %}> <tr {% if tab.options.hide_names %}class="hide-tag-name"{% endif %}>
<td>{{ tag.name }}</td> <td>{{ tag.name }}</td>
<td>{{ make_tag_value(tag) }}</td> <td>{{ make_tag_value(tag) }}</td>
</tr> </tr>
{%- endif -%}
{% for subtag in tag.subtags %} {% 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 %}> <tr {% if tab.options.hide_names %}class="hide-tag-name"{% endif %}>
<td>{% if loop.last %}&#9492;{% else %}&#9500;{% endif %} {{ subtag.name }}</td> <td>{% if loop.last %}&#9492;{% else %}&#9500;{% endif %} {{ subtag.name }}</td>
<td>{{ make_tag_value(subtag) }}</td> <td>{{ make_tag_value(subtag) }}</td>
</tr> </tr>
{%- endif -%}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</table> </table>
</div> </div>
{% endmacro %} {% endmacro %}
{# TODO: tag.interlink and tag.private support #} {# TODO: tag.interlink #}
{% macro make_tag_value(tag) -%} {% macro make_tag_value(tag) -%}
{%- if tag.options.hyperlink -%} {%- if tag.options.hyperlink -%}
<a href="{{ tag.value }}">{{ tag.value }}</a> <a href="{{ tag.value }}">{{ tag.value }}</a>
@ -62,8 +66,19 @@ window.onload = function () {
{%- endif -%} {%- endif -%}
{%- endmacro %} {%- endmacro %}
{# TODO: tab.priority and tab.private support #} {# TODO: tab.priority support #}
{% block page_content %} {% 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> <div id="tabs">
{% for tab in document %}{{ make_tab_page(tab, loop.first) }}{% endfor %} {%- 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 %} {% 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 %}