Compare commits

..

No commits in common. "2ddb1281c1c892ad30e15f8d16ddc46331917461" and "276a77b67e6b35caf9a19e92d70d5b609b2011a3" have entirely different histories.

6 changed files with 50 additions and 333 deletions

View File

@ -1,9 +1,9 @@
"""
Logic for managing documents.
Logic for operations that depend on a whole collection of documents.
"""
import os
from redstring.parser import load, DocumentTag, DocumentTab, Document, TabOptions, TagOptions
from redstring.parser import load, TagOptions, DocumentTag, TabOptions, DocumentTab, Document
def generate_index_document(directory: str) -> Document:
@ -17,18 +17,16 @@ def generate_index_document(directory: str) -> Document:
document: Document = load(f)
# Check if this document specifies a tab, and create it if necessary.
if category_tag := document.get_tag('category'):
category = category_tag.value
else:
category = document.get_tag('category')
if not category:
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.
if topic_tag := document.get_tag('topic'):
topic = topic_tag.value
else:
topic = document.get_tag('topic')
if not topic:
topic = 'uncategorized'
if '.' in topic:
topic, subtopic = topic.split('.', maxsplit=1)
@ -64,36 +62,8 @@ 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))
built_tags.append(DocumentTag(topic, value, TagOptions(), []))
built_tabs.append(DocumentTab(category, built_tags))
built_tabs.append(DocumentTab(category, built_tags, TabOptions()))
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, Optional
from typing import Any, List, IO
#
@ -23,9 +23,6 @@ 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."""
@ -63,10 +60,13 @@ class TagOptions:
_PRIVATE_KEY = 'private'
def __init__(self, **kwargs) -> None:
self.options: dict = OrderedDict(**kwargs)
def to_json(self):
return self.options
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)
@property
def hyperlink(self) -> bool:
@ -98,22 +98,10 @@ class DocumentSubtag:
"""
A keyvalue describing a document subject.
"""
def __init__(
self,
name: str,
value: str,
options: Optional[TagOptions] = None
) -> None:
def __init__(self, name: str, value: str, options: TagOptions) -> None:
self.name: str = name
self.value: str = value
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(),
)
self.options: TagOptions = options
class DocumentTag:
@ -124,43 +112,23 @@ class DocumentTag:
self,
name: str,
value: str,
options: Optional[TagOptions] = None,
subtags: Optional[List[DocumentSubtag]] = None
options: TagOptions,
subtags: List[DocumentSubtag]
) -> None:
self.name: str = name
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],
)
self.value = value
self.options = options
self.subtags = subtags
class DocumentTab:
"""
A division of tags within a document.
"""
def __init__(
self,
name: str,
tags: List[DocumentTag],
options: Optional[TabOptions] = None
) -> None:
self.name: str = name
def __init__(self, name: str, tags: List[DocumentTag], options: TabOptions) -> None:
self.name = name
self.tags: List[DocumentTag] = tags
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(),
)
self.options: TabOptions = options
def get_tag(self, name: str):
for tag in self.tags:
@ -168,11 +136,6 @@ 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:
"""
@ -181,9 +144,6 @@ 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__()
@ -200,11 +160,6 @@ 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
@ -221,6 +176,16 @@ 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.
@ -317,14 +282,6 @@ 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,13 +10,12 @@ from flask import (
Flask,
redirect,
render_template,
request,
safe_join,
url_for,
)
from redstring.library import generate_index_document, generate_default_document
from redstring.parser import load, dump, DocumentTab, DocumentTag, TagOptions, DocumentSubtag
from redstring.library import generate_index_document
from redstring.parser import load
CONFIG_ENVVAR = 'REDSTRING_CONFIG'
@ -24,18 +23,18 @@ CONFIG_ENVVAR = 'REDSTRING_CONFIG'
app = Flask(__name__)
@app.route('/', methods=['GET'])
@app.route('/')
def root():
return redirect(url_for('index'))
@app.route('/index/', methods=['GET'])
@app.route('/index/')
def index():
document = generate_index_document(current_app.config['root'])
return render_template('doc.jinja', document=document, index=True)
@app.route('/doc/<document_id>', methods=['GET'])
@app.route('/doc/<document_id>')
def document(document_id):
doc_path = safe_join(current_app.config['root'], f'{document_id}.json')
if not os.path.isfile(doc_path):
@ -45,57 +44,6 @@ 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,20 +101,12 @@
table.page-table td:nth-child(2) {
white-space: pre-wrap;
}
table.page-table a {
table.page-table td:nth-child(2) a {
color: #8af;
}
table.page-table a:visited {
table.page-table td:nth-child(2) 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 = document.get_tag_value('title', document.get_tag('id').value) -%}
{% set page_summary = document.get_tag_value('summary', '') %}
{% set page_title = 'tmp' -%}
{% set page_summary = 'tmpp' %}
{% 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,26 +38,22 @@ 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 #}
{# TODO: tag.interlink and tag.private support #}
{% macro make_tag_value(tag) -%}
{%- if tag.options.hyperlink -%}
<a href="{{ tag.value }}">{{ tag.value }}</a>
@ -66,19 +62,8 @@ window.onload = function () {
{%- endif -%}
{%- endmacro %}
{# TODO: tab.priority support #}
{# TODO: tab.priority and tab.private support #}
{% block page_content %}
<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 -%}
<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 %}
{% endblock page_content %}

View File

@ -1,135 +0,0 @@
{% 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 %}