Compare commits

...

2 Commits

12 changed files with 494 additions and 473 deletions

View File

@ -2,7 +2,10 @@
Article query interface Article query interface
""" """
from sqlalchemy import select from typing import Optional
from uuid import UUID
from sqlalchemy import select, update
from amanuensis.db import * from amanuensis.db import *
from amanuensis.errors import ArgumentError, BackendArgumentTypeError from amanuensis.errors import ArgumentError, BackendArgumentTypeError
@ -42,3 +45,20 @@ def create(
db.session.add(new_article) db.session.add(new_article)
db.session.commit() db.session.commit()
return new_article return new_article
def try_from_public_id(db: DbContext, public_id: UUID) -> Optional[Article]:
"""Get an article by its public id."""
return db(
select(Article).where(Article.public_id == public_id)
).scalar_one_or_none()
def update_state(db: DbContext, article_id: int, title: str, body: str, state: ArticleState):
"""Update an article."""
db(
update(Article)
.where(Article.id == article_id)
.values(title=title, body=body, state=state)
)
db.session.commit()

View File

View File

@ -0,0 +1,73 @@
import re
from typing import Sequence
from amanuensis.parser import *
class ConstraintCheck(RenderableVisitor):
"""Analyzes an article for content-based constraint violations."""
def __init__(self) -> None:
self.word_count: int = 0
self.signatures: int = 0
self.tmp: bool = False
def TextSpan(self, span):
self.word_count += len(re.split(r'\s+', span.innertext.strip()))
return self
def BoldSpan(self, span):
self.tmp = True
return self
def SignatureParagraph(self, span):
self.signatures += 1
return self
class ConstraintMessage:
INFO = 0
WARNING = 1
ERROR = 2
def __init__(self, severity: int, message: str) -> None:
self.severity = severity
self.message = message
@staticmethod
def info(message) -> "ConstraintMessage":
return ConstraintMessage(ConstraintMessage.INFO, message)
@staticmethod
def warning(message) -> "ConstraintMessage":
return ConstraintMessage(ConstraintMessage.WARNING, message)
@staticmethod
def error(message) -> "ConstraintMessage":
return ConstraintMessage(ConstraintMessage.ERROR, message)
@property
def is_error(self) -> bool:
return self.severity == ConstraintMessage.ERROR
def json(self):
return {"severity": self.severity, "message": self.message}
def constraint_check(parsed: Renderable) -> Sequence[ConstraintMessage]:
check_result: ConstraintCheck = parsed.render(ConstraintCheck())
messages = []
# I: Word count
messages.append(ConstraintMessage.info(f"Word count: {check_result.word_count}"))
# W: Check signature count
if check_result.signatures < 1:
messages.append(ConstraintMessage.warning("Missing signature paragraph"))
if check_result.signatures > 1:
messages.append(ConstraintMessage.warning("More than one signature paragraph"))
# E: tmp to test errors
if check_result.tmp:
messages.append(ConstraintMessage.error("Temporary error"))
return messages

View File

@ -2,11 +2,12 @@
Module encapsulating all markdown parsing functionality. Module encapsulating all markdown parsing functionality.
""" """
from .core import RenderableVisitor from .core import RenderableVisitor, Renderable
from .helpers import normalize_title, filesafe_title, titlesort from .helpers import normalize_title, filesafe_title, titlesort
from .parsing import parse_raw_markdown from .parsing import parse_raw_markdown
__all__ = [ __all__ = [
"Renderable",
"RenderableVisitor", "RenderableVisitor",
"normalize_title", "normalize_title",
"filesafe_title", "filesafe_title",

View File

@ -1,76 +1,75 @@
html, body { html, body {
height: 100%; height: 100%;
margin: 0px; margin: 0px;
} }
div#wrapper { div#wrapper {
max-width: 1128px; height: 100%;
height: 100%; display: flex;
display:flex; flex-direction: row;
flex-direction: row; align-items: stretch;
align-items: stretch;
} }
div.column { div.column {
width: 50%; width: 50%;
} }
div#editor-left { div#editor-left {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
} }
div#editor-left section { div#editor-left section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 10px 5px 10px 10px; margin: 10px 5px 10px 10px;
width: 100%; width: 100%;
padding: 5px; padding: 5px;
} }
div#editor-left div#editor-header { div#editor-left div#editor-header {
margin: 5px; margin: 5px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
div#editor-left div#editor-charselect { div#editor-left div#editor-charselect {
margin: 5px; margin: 5px;
} }
div#editor-left div#editor-buttons { div#editor-left div#editor-buttons {
margin: 5px; margin: 5px;
} }
div#editor-left input#editor-title { div#editor-left input#editor-title {
font-size: 2em; font-size: 2em;
margin: 5px; margin: 5px;
} }
textarea#editor-content { textarea#editor-content {
margin: 5px; margin: 5px;
resize: none; resize: none;
flex-grow: 1; flex-grow: 1;
width: initial; width: initial;
} }
div#editor-right { div#editor-right {
overflow-y: scroll; overflow-y: scroll;
} }
div#editor-right section { div#editor-right section {
margin: 10px 5px 10px 10px; margin: 10px 5px 10px 10px;
} }
span.message-warning { span.message-warning {
color: #808000; color: #808000;
} }
span.message-error { span.message-error {
color: #ff0000; color: #ff0000;
} }
@media only screen and (max-width: 816px) { @media only screen and (max-width: 816px) {
div#wrapper { div#wrapper {
max-width: 564px; max-width: 564px;
margin: 0 auto; margin: 0 auto;
padding: inherit; padding: inherit;
flex-direction: column; flex-direction: column;
} }
div.column { div.column {
width: 100%; width: 100%;
} }
div#editor-left { div#editor-left {
height: 100%; height: 100%;
} }
div#editor-right { div#editor-right {
overflow-y: inherit; overflow-y: inherit;
} }
} }

View File

@ -1,126 +1,152 @@
// Reduce unnecessary requests by checking for no further changes being made (function(){
// before updating in response to a change. /** Article submission state. */
var nonce = 0; const ArticleState = {
DRAFT: 0,
SUBMITTED: 1,
APPROVED: 2
};
function ifNoFurtherChanges(callback, timeout=2000) { /** Article state to be tracked in addition to the editable content. */
var nonce_local = Math.random(); var article = {
nonce = nonce_local; state: undefined,
setTimeout(() => { ersatz: false
if (nonce == nonce_local) { };
callback();
nonce = 0;
}
}, timeout);
}
// Read data out of params and initialize editor /** Article content as last received from the server. */
window.onload = function() { var preview = {
// Kill noscript message first title: undefined,
document.getElementById("preview").innerHTML = "<p>&nbsp;</p>"; rendered: undefined,
citations: [],
messages: []
}
if (document.body.contains(document.getElementById("editor-content"))) { /** The nonce of the last-made update request. */
onContentChange(0); let nonce = 0;
}
};
function buildArticleObject() { /**
var title = document.getElementById("editor-title").value; * Update request debounce wrapper that executes the callback if no further
var contents = document.getElementById("editor-content").value; * calls are made during the timeout period. If a new call is made, any
return { * previous calls are skipped.
aid: params.article.aid, */
title: title, function ifNoFurtherChanges(callback, timeout)
status: params.article.status, {
contents: contents // Stake a claim on the nonce, potentially overwriting a previous
}; // nonce value.
} const nonce_local = 1 + Math.random();
nonce = nonce_local;
// Wait to see if this call is overwritten in turn.
setTimeout(() => {
if (nonce == nonce_local)
{
callback();
nonce = 0;
}
}, timeout);
}
function update(article) { /** Update the editor controls and preview to match the current state. */
var req = new XMLHttpRequest(); function refreshEditor()
req.open("POST", params.updateURL, true); {
req.setRequestHeader("Content-type", "application/json"); // Enable or disable controls
req.responseType = "json"; const isEditable = article.state == ArticleState.DRAFT;
req.onreadystatechange = function () { const blocked = preview.messages.filter(msg => msg.severity == 2).length > 0;
if (req.readyState == 4 && req.status == 200) { document.getElementById("editor-title").disabled = !isEditable;
// Update internal state with the returned article object document.getElementById("editor-content").disabled = !isEditable;
params.status = req.response.status; document.getElementById("button-submit").innerText = isEditable ? "Submit article" : "Edit article";
params.errors = req.response.error.length; document.getElementById("button-submit").disabled = blocked;
document.getElementById("editor-title").value = req.response.title;
// Set editor editability based on article status
updateEditorStatus();
// Update the preview with the parse information
updatePreview(req.response);
}
};
var payload = { article: article };
req.send(JSON.stringify(payload));
}
function updateEditorStatus() { // Update the preview
var ready = !!params.status.ready || !!params.status.approved; const previewHtml = "<h1>" + preview.title + "</h1>\n" + preview.rendered;
document.getElementById("editor-title").disabled = ready; document.getElementById("preview").innerHTML = previewHtml;
document.getElementById("editor-content").disabled = ready;
var hasErrors = params.errors > 0;
var submitButton = document.getElementById("button-submit");
submitButton.innerText = ready ? "Edit article" : "Submit article";
submitButton.disabled = hasErrors;
}
function updatePreview(response) { // Fill in the citation block
var previewHtml = "<h1>" + response.title + "</h1>\n" + response.rendered; let citations = "<ol>";
document.getElementById("preview").innerHTML = previewHtml; preview.citations.forEach(cit => citations += "<li>" + JSON.stringify(cit) + "</li>");
citations += "</ol>";
document.getElementById("preview-citations").innerHTML = citations;
var citations = "<ol>"; // Fill in the status message block
for (var i = 0; i < response.citations.length; i++) { let statuses = "<ol>";
citations += "<li>" + response.citations[i] + "</li>"; preview.messages.forEach(err => statuses += "<li>" + JSON.stringify(err) + "</li>");
} statuses += "<ol>";
citations += "</ol>"; document.getElementById("preview-control").innerHTML = statuses;
document.getElementById("preview-citations").innerHTML = citations; }
var info = ""; /** Update the current state with the given data and refresh the editor. */
for (var i = 0; i < response.info.length; i++) { function updateState(data)
info += "<span class=\"message-info\">" + response.info[i] + "</span><br>"; {
} article.state = data.state;
var warning = ""; article.ersatz = data.ersatz;
for (var i = 0; i < response.warning.length; i++) { preview.title = data.title;
warning += "<span class=\"message-warning\">" + preview.rendered = data.rendered;
response.warning[i] + "</span><br>"; preview.citations = data.citations;
} preview.messages = data.messages;
var error = ""; refreshEditor();
for (var i = 0; i < response.error.length; i++) { }
error += "<span class=\"message-error\">" + response.error[i] + "</span><br>";
}
var control = info + warning + error;
document.getElementById("preview-control").innerHTML = control;
}
function onContentChange(timeout=2000) { /** Send the article's current content to the server. */
ifNoFurtherChanges(() => { function update()
var article = buildArticleObject(); {
update(article); const updateUrl = document.body.dataset.amanuensisUpdateUrl;
}, timeout); const data = {
} title: document.getElementById("editor-title").value,
body: document.getElementById("editor-content").value,
state: article.state
};
fetch(updateUrl, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
}).then(response => response.json()).then(updateState);
}
function submitArticle() { function onContentChange(e, timeout=2000)
ifNoFurtherChanges(() => { {
params.article.status.ready = !params.article.status.ready; ifNoFurtherChanges(update, timeout);
var article = buildArticleObject(); }
update(article);
}, 0);
}
window.addEventListener("beforeunload", function(e) { function submitArticle()
if (nonce != 0) { {
e.returnValue = "Are you sure?"; ifNoFurtherChanges(() => {
} article.state = ArticleState.SUBMITTED;
}); update();
},
/* timeout: */ 0);
}
window.addEventListener("keydown", function(event) { /** Initialize the editor on page load. */
if (event.ctrlKey || event.metaKey) function initializeEditor()
{ {
if (String.fromCharCode(event.which).toLowerCase() == 's') // Kill the noscript message
{ document.getElementById("preview").innerHTML = "<p>Loading...</p>";
event.preventDefault(); document.getElementById("preview-citations").innerHTML = "<p>Loading...</p>";
onContentChange(0); document.getElementById("preview-control").innerHTML = "<p>Loading...</p>";
}
} // Hook up the controls
}); document.getElementById("button-submit").onclick = submitArticle;
document.getElementById("editor-title").oninput = onContentChange;
document.getElementById("editor-content").oninput = onContentChange;
window.addEventListener("beforeunload", e =>
{
if (nonce > 0)
{
e.returnValue = "Are you sure?";
}
});
window.addEventListener("keydown", e =>
{
if (e.ctrlKey && e.key == 's')
{
e.preventDefault();
onContentChange(e, 0);
}
});
// Get the article status information.
const updateUrl = document.body.dataset.amanuensisUpdateUrl;
fetch(updateUrl).then(response => response.json()).then(updateState);
}
window.onload = initializeEditor;
})();

View File

@ -15,6 +15,9 @@
{% if current_page == "contents" %}class="current-page" {% if current_page == "contents" %}class="current-page"
{% else %}href="{{ url_for('lexicon.contents', lexicon_name=g.lexicon.name) }}" {% else %}href="{{ url_for('lexicon.contents', lexicon_name=g.lexicon.name) }}"
{% endif %}>Contents</a>{% endblock %} {% endif %}>Contents</a>{% endblock %}
{% block sb_editor %}<a
href="{{ url_for('lexicon.editor.select', lexicon_name=g.lexicon.name) }}"
>Editor</a>{% endblock %}
{% block sb_posts %}<a {% block sb_posts %}<a
{% if current_page == "posts" %}class="current-page" {% if current_page == "posts" %}class="current-page"
{% else %}href="{{ url_for('lexicon.posts.list', lexicon_name=g.lexicon.name) }}" {% else %}href="{{ url_for('lexicon.posts.list', lexicon_name=g.lexicon.name) }}"
@ -35,6 +38,7 @@
{% set template_sidebar_rows = [ {% set template_sidebar_rows = [
self.sb_characters(), self.sb_characters(),
self.sb_contents(), self.sb_contents(),
self.sb_editor(),
self.sb_posts(), self.sb_posts(),
self.sb_rules(), self.sb_rules(),
self.sb_settings(), self.sb_settings(),

View File

@ -7,6 +7,7 @@ from amanuensis.errors import ArgumentError
from amanuensis.server.helpers import lexicon_param, player_required_if_not_public from amanuensis.server.helpers import lexicon_param, player_required_if_not_public
from .characters import bp as characters_bp from .characters import bp as characters_bp
from .editor import bp as editor_bp
from .forms import LexiconJoinForm from .forms import LexiconJoinForm
from .posts import bp as posts_bp from .posts import bp as posts_bp
from .settings import bp as settings_bp from .settings import bp as settings_bp
@ -16,6 +17,7 @@ bp = Blueprint(
"lexicon", __name__, url_prefix="/lexicon/<lexicon_name>", template_folder="." "lexicon", __name__, url_prefix="/lexicon/<lexicon_name>", template_folder="."
) )
bp.register_blueprint(characters_bp) bp.register_blueprint(characters_bp)
bp.register_blueprint(editor_bp)
bp.register_blueprint(posts_bp) bp.register_blueprint(posts_bp)
bp.register_blueprint(settings_bp) bp.register_blueprint(settings_bp)

View File

@ -0,0 +1,148 @@
import re
from flask import Blueprint, render_template, g, abort, request
from amanuensis.backend import *
from amanuensis.db import *
from amanuensis.lexicon.constraint import constraint_check
from amanuensis.parser import *
from amanuensis.server.helpers import lexicon_param, player_required
bp = Blueprint("editor", __name__, url_prefix="/editor", template_folder=".")
class PreviewHtmlRenderer(RenderableVisitor):
"""Parses stylistic markdown and stores citations as footnotes."""
def __init__(self) -> None:
self.citations: list = []
self.rendered: str = ""
# Translate the leaf spans to text
def TextSpan(self, span):
return span.innertext
def LineBreak(self, span):
return '<br>'
# Translate the simple container spans to text
def BoldSpan(self, span):
return f'<b>{"".join(span.recurse(self))}</b>'
def ItalicSpan(self, span):
return f'<i>{"".join(span.recurse(self))}</i>'
# Record citations in the visitor, then translate the span to text as an
# underline and footnote number
def CitationSpan(self, span):
self.citations.append({
"title": span.cite_target,
"type": "phantom"
})
return f'<u>{"".join(span.recurse(self))}</u>[{len(self.citations)}]'
# Translate the paragraph-level containers to their text contents
def BodyParagraph(self, span):
return f'<p>{"".join(span.recurse(self))}</p>'
def SignatureParagraph(self, span):
return (
'<hr><span class="signature"><p>'
f'{"".join(span.recurse(self))}'
'</p></span>'
)
# Return the visitor from the top-level article span after saving the full
# text parsed from the child spans
def ParsedArticle(self, span):
self.contents = '\n'.join(span.recurse(self))
return self
@bp.get("/")
@lexicon_param
@player_required
def select(lexicon_name):
return {}
@bp.get("/<uuid:article_id>")
@lexicon_param
@player_required
def open(lexicon_name, article_id):
article = artiq.try_from_public_id(g.db, article_id)
if not article:
return abort(404)
return render_template(
"session.editor.jinja",
lexicon_name=lexicon_name,
article=article,
)
@bp.get("/<uuid:article_id>/update")
@lexicon_param
@player_required
def load(lexicon_name, article_id):
# Get the article
article = artiq.try_from_public_id(g.db, article_id)
if not article:
return abort(404)
# Generate the preview HTML
parsed = parse_raw_markdown(article.body)
preview_result: PreviewHtmlRenderer = parsed.render(PreviewHtmlRenderer())
# Check article content against constraints
messages = constraint_check(parsed)
# Return the article information to the editor
msg_list = list([msg.json() for msg in messages])
return {
'title': article.title,
'rendered': preview_result.contents,
'state': article.state.value,
'ersatz': article.ersatz,
'citations': preview_result.citations,
'messages': msg_list,
}
@bp.post("/<uuid:article_id>/update")
def update(lexicon_name, article_id):
# Get the article
article = artiq.try_from_public_id(g.db, article_id)
if not article:
return abort(404)
# Extract the submitted content
new_title = request.json['title']
new_body = request.json['body']
new_state = ArticleState(request.json['state'])
# Generate the preview HTML from the submitted content
parsed = parse_raw_markdown(new_body)
preview_result: PreviewHtmlRenderer = parsed.render(PreviewHtmlRenderer())
# Check article content against constraints
messages = constraint_check(parsed)
# Block submission if the article is a draft with errors
has_errors = any([msg for msg in messages if msg.is_error])
if article.state == ArticleState.DRAFT and new_state != ArticleState.DRAFT and has_errors:
new_state = ArticleState.DRAFT
# Update the article with the submitted information
artiq.update_state(g.db, article.id, title=new_title, body=new_body, state=new_state)
updated_article = artiq.try_from_public_id(g.db, article_id)
# Return the article information to the editor
msg_list = list([msg.json() for msg in messages])
return {
'title': updated_article.title,
'rendered': preview_result.contents,
'state': updated_article.state.value,
'ersatz': updated_article.ersatz,
'citations': preview_result.citations,
'messages': msg_list,
}

View File

@ -1,187 +0,0 @@
"""
Handler helper functions pertaining to the article editor
"""
import json
import uuid
from flask import (
flash, redirect, url_for, render_template, Markup)
from flask_login import current_user
from amanuensis.lexicon import (
get_player_characters,
get_player_drafts,
get_draft,
title_constraint_analysis,
content_constraint_analysis)
from amanuensis.models import LexiconModel
from amanuensis.parser import (
normalize_title,
parse_raw_markdown)
from amanuensis.parser.core import RenderableVisitor
class PreviewHtmlRenderer(RenderableVisitor):
def __init__(self, lexicon):
with lexicon.ctx.read('info') as info:
self.article_map = {
title: article.character
for title, article in info.items()
}
self.citations = []
self.contents = ""
def TextSpan(self, span):
return span.innertext
def LineBreak(self, span):
return '<br>'
def ParsedArticle(self, span):
self.contents = '\n'.join(span.recurse(self))
return self
def BodyParagraph(self, span):
return f'<p>{"".join(span.recurse(self))}</p>'
def SignatureParagraph(self, span):
return (
'<hr><span class="signature"><p>'
f'{"".join(span.recurse(self))}'
'</p></span>'
)
def BoldSpan(self, span):
return f'<b>{"".join(span.recurse(self))}</b>'
def ItalicSpan(self, span):
return f'<i>{"".join(span.recurse(self))}</i>'
def CitationSpan(self, span):
if span.cite_target in self.article_map:
if self.article_map.get(span.cite_target):
link_class = '[extant]'
else:
link_class = '[phantom]'
else:
link_class = '[new]'
self.citations.append(f'{span.cite_target} {link_class}')
return f'<u>{"".join(span.recurse(self))}</u>[{len(self.citations)}]'
def load_editor(lexicon: LexiconModel, aid: str):
"""
Load the editor page
"""
if aid:
# Article specfied, load editor in edit mode
article = get_draft(lexicon, aid)
if not article:
flash("Draft not found")
return redirect(url_for('session.session', name=lexicon.cfg.name))
# Check that the player owns this article
character = lexicon.cfg.character.get(article.character)
if character.player != current_user.uid:
flash("Access forbidden")
return redirect(url_for('session.session', name=lexicon.cfg.name))
return render_template(
'session.editor.jinja',
character=character,
article=article,
jsonfmt=lambda obj: Markup(json.dumps(obj)))
# Article not specified, load editor in load mode
characters = list(get_player_characters(lexicon, current_user.uid))
articles = list(get_player_drafts(lexicon, current_user.uid))
return render_template(
'session.editor.jinja',
characters=characters,
articles=articles)
def new_draft(lexicon: LexiconModel, cid: str):
"""
Create a new draft and open it in the editor
"""
if cid:
new_aid = uuid.uuid4().hex
# TODO harden this
character = lexicon.cfg.character.get(cid)
article = {
"version": "0",
"aid": new_aid,
"lexicon": lexicon.lid,
"character": cid,
"title": "",
"turn": 1,
"status": {
"ready": False,
"approved": False
},
"contents": f"\n\n{character.signature}",
}
filename = f"{cid}.{new_aid}"
with lexicon.ctx.draft.new(filename) as j:
j.update(article)
return redirect(url_for(
'session.editor',
name=lexicon.cfg.name,
cid=cid,
aid=new_aid))
# Character not specified
flash('Character not found')
return redirect(url_for('session.session', name=lexicon.cfg.name))
def update_draft(lexicon: LexiconModel, article_json):
"""
Update a draft and perform analysis on it
"""
# Check if the update is permitted
aid = article_json.get('aid')
article = get_draft(lexicon, aid)
if not article:
raise ValueError("missing article")
if lexicon.cfg.character.get(article.character).player != current_user.uid:
return ValueError("bad user")
if article.status.approved:
raise ValueError("bad status")
# Perform the update
title = article_json.get('title')
contents = article_json.get('contents')
status = article_json.get('status')
parsed = parse_raw_markdown(contents)
# HTML parsing
preview = parsed.render(PreviewHtmlRenderer(lexicon))
# Constraint analysis
title_infos, title_warnings, title_errors = title_constraint_analysis(
lexicon, current_user, article.character, title)
content_infos, content_warnings, content_errors = content_constraint_analysis(
lexicon, current_user, article.character, parsed)
if any(title_errors) or any(content_errors):
status['ready'] = False
# Article update
filename = f'{article.character}.{aid}'
with lexicon.ctx.draft.edit(filename) as draft:
draft.title = normalize_title(title)
draft.contents = contents
draft.status.ready = status.get('ready', False)
# Return canonical information to editor
return {
'title': draft.title,
'status': {
'ready': draft.status.ready,
'approved': draft.status.approved,
},
'rendered': preview.contents,
'citations': preview.citations,
'info': title_infos + content_infos,
'warning': title_warnings + content_warnings,
'error': title_errors + content_errors,
}

View File

@ -1,121 +1,64 @@
{% if character and not article %}
{% set characters = [character] %}
{% endif %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Editor</title> <title>Editor</title>
<link rel="icon" type="image/png" href="{{ url_for('static', filename='amanuensis.png') }}"> <link rel="icon" type="image/png" href="{{ url_for('static', filename='amanuensis.png') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='page.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='page.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='editor.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='editor.css') }}">
<script> <script type="text/javascript" src="{{ url_for('static', filename='editor.js') }}"></script>
params = {
updateURL: "{{ url_for('session.editor_update', name=g.lexicon.cfg.name) }}",
{% if character %}
character: {{ jsonfmt(character) }},
{% else %}
character: null,
{% endif %}
{% if article %}
article: {
aid: {{ jsonfmt(article.aid) }},
status: {{ jsonfmt(article.status) }},
errors: 1,
}
{% else %}
article: null
{% endif %}
};
</script>
<script type="text/javascript" src="{{ url_for('static', filename='editor.js') }}"></script>
</head> </head>
<body> <body
<div id="wrapper"> data-amanuensis-update-url="{{ url_for('lexicon.editor.load', lexicon_name=lexicon_name, article_id=article.public_id) }}"
<div id="editor-left" class="column"> >
<section> <div id="wrapper">
{# Thin header bar #} <div id="editor-left" class="column">
<div id="editor-header"> <section>
{# Header always includes backlink to lexicon #} {# Thin header bar #}
<a href="{{ url_for('session.session', name=g.lexicon.cfg.name) }}"> <div id="editor-header">
{{ g.lexicon.title }} {# Header always includes backlink to lexicon #}
</a> <a href="{{ url_for('lexicon.contents', lexicon_name=lexicon_name) }}">
{# If article is not finalized, show button to submit and retract #} {{ g.lexicon.full_title }}
{% if article and not article.status.approved %} </a>
<button id="button-submit" onclick="submitArticle()" disabled>Submit article</button> {# If article is not finalized, show button to submit and retract #}
{% endif %} {# {% if article and not article.status.approved %} #}
{# Header always includes character/player info #} {% if article %}
<span> <button
<b> id="button-submit"
{% if character %} disabled
{{ character.name }} / >Submit article</button>
{% endif %} {% endif %}
{{ current_user.cfg.username }} {# Header always includes character/player info #}
</b> <span>{{ article.character.name }}</span>
</span> </div>
</div> {% if article %}
{# In load mode, `characters` is specified and `article` is #} {# <div id="editor-buttons">
{# not, and the main body of the editor column contains a #} Character literals:
{# list of articles that can be loaded. #} <button>*</button>
{% for char in characters %} <button>/</button>
<div id="editor-charselect"> <button>[</button>
<b>{{ char.name }}</b> <button>]</button>
<ul> <button>~</button>
{% for article in articles %} </div> #}
{% if article.character == char.cid %} <input id="editor-title" placeholder="Title" disabled value="{{ article.title }}">
<li> <textarea id="editor-content" class="fullwidth" disabled>
<a href="{{ url_for('session.editor', name=g.lexicon.cfg.name, aid=article.aid) }}"> {{- article.body -}}
{{ article.title if article.title.strip() else "Untitled" }}</a> </textarea>
<span> {% endif %}
{% if not article.status.ready %} </section>
[Draft] </div>
{% elif not article.status.approved %} <div id="editor-right" class="column">
[Pending] <section id="preview">
{% else %} <p>This editor requires Javascript to function.</p>
[Approved] </section>
{% endif %} <section id="preview-citations">
</span> <p>&nbsp;</p>
</li> </section>
{% endif %} <section id="preview-control">
{% endfor %} <p>&nbsp;</p>
<li> </fieldset>
<a href="{{ url_for('session.editor_new', name=g.lexicon.cfg.name, cid=char.cid) }}"> </div>
New </div>
</a>
</li>
</ul>
</div>
{% endfor %}
{# In edit mode, `article` is specified and `characters` is #}
{# not, and the editor pane contains the article editor. #}
{% if article %}
{# <div id="editor-buttons">
Character literals:
<button>*</button>
<button>/</button>
<button>[</button>
<button>]</button>
<button>~</button>
</div> #}
<input id="editor-title" placeholder="Title" oninput="onContentChange()" disabled value="{{ article.title }}">
<textarea id="editor-content" class="fullwidth" oninput="onContentChange()" disabled>
{# #}{{ article.contents }}{#
#}</textarea>
{% endif %}
</section>
</div>
<div id="editor-right" class="column">
<section id="preview">
<p>This editor requires Javascript to function.</p>
</div>
<section id="preview-citations">
<p>&nbsp;</p>
</div>
<section id="preview-control">
<p>&nbsp;</p>
</div>
</div>
</div>
</body> </body>
</html> </html>

View File

@ -8,17 +8,9 @@
<span style="color:#ff0000">{{ message }}</span><br> <span style="color:#ff0000">{{ message }}</span><br>
{% endfor %} {% endfor %}
{% for index in indexed %} {% for article in current_lexicon.articles %}
<b>{{ index }}</b> <p>{{ article.title }} - {{ article.public_id }}</p>
{% if indexed[index] %} <p>{{ article.body }}</p>
<ul>
{% for article in indexed[index] %}
<li><a href="{{ article.title|articlelink }}" class="{{ 'phantom' if not article.character else '' }}">
{{ article.title }}
</a></li>
{% endfor %}
</ul>
{% endif %}
{% endfor %} {% endfor %}
</section> </section>
{% endblock %} {% endblock %}