Strip down editor a bit and add basic article creation

This commit is contained in:
Tim Van Baak 2020-02-22 08:54:24 -08:00
parent c6cfe25e6d
commit 1e152851d0
7 changed files with 252 additions and 196 deletions

View File

@ -81,3 +81,16 @@ class LexiconModel():
return [
char for char in self.character.values()
if uid is None or char.player == uid]
def get_drafts_for_player(self, uid):
chars = self.get_characters_for_player(uid=uid)
drafts_path = prepend('lexicon', self.id, 'draft')
drafts = []
for filename in os.listdir(drafts_path):
for char in chars:
if filename.startswith(str(char.cid)):
drafts.append(filename)
for i in range(len(drafts)):
with json_ro(drafts_path, drafts[i]) as a:
drafts[i] = a
return drafts

View File

@ -20,7 +20,7 @@ div#editor-left {
div#editor-left div.contentblock {
display: flex;
flex-direction: column;
margin: 10px 10px 10px 5px;
margin: 10px 5px 10px 10px;
width: 100%;
padding: 5px;
}
@ -29,6 +29,9 @@ div#editor-left div#editor-header {
display: flex;
justify-content: space-between;
}
div#editor-left div#editor-charselect {
margin: 5px;
}
div#editor-left input#editor-title {
font-size: 2em;
margin: 5px;

View File

@ -1,181 +1,85 @@
function onContentChange() {
// Get the new content
var articleTitle = document.getElementById("editor-title").value;
var articleBody = document.getElementById("editor-content").value;
// Pass the draft text to the parser to get the preview html and citations
var parseResult = parseLexipythonMarkdown(articleBody);
// Build the citation block
var citeblockContent = makeCiteblock(parseResult);
// Compute warnings and build the control block
var controlContent = checkWarnings(parseResult);
// Fill in the content blocks
document.getElementById("preview").innerHTML = (
"<h1>" + articleTitle + "</h1>\n"
+ parseResult.html);
document.getElementById("preview-citations").innerHTML = citeblockContent;
document.getElementById("preview-control").innerHTML = controlContent;
}
function parseLexipythonMarkdown(text) {
// Prepare return values
var result = {
html: "",
citations: [],
hasSignature: false,
};
// Parse the content by paragraph, extracting the citations
var paras = text.trim().split(/\n\n+/);
citationList = [];
formatId = 1;
for (var i = 0; i < paras.length; i++) {
var para = paras[i];
// Escape angle brackets
para = para.replace(/</g, "&lt;");
para = para.replace(/>/g, "&gt;");
// Replace bold and italic marks with tags
para = para.replace(/\/\/([^\/]+)\/\//g, "<i>$1</i>");
para = para.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
// Replace \\LF with <br>LF
para = para.replace(/\\\\\n/g, "<br>\n");
// Abstract citations into the citation record
linkMatch = para.match(/\[\[(([^|\[\]]+)\|)?([^|\[\]]+)\]\]/);
while (linkMatch != null) {
// Identify the citation text and cited article
citeText = linkMatch[2] != null ? linkMatch[2] : linkMatch[3];
citeTitle = linkMatch[3].charAt(0).toUpperCase() + linkMatch[3].slice(1);
// Record the citation
result.citations.push({
id: formatId,
citeText: citeText,
citeTitle: citeTitle,
});
// Stitch the cite text in place of the citation, plus a cite number
para =
para.slice(0, linkMatch.index) +
"<a href=\"#\">" +
citeText +
"</a>" +
"<sup>" +
formatId.toString() +
"</sup>" +
para.slice(linkMatch.index + linkMatch[0].length);
formatId += 1; // Increment to the next format id
linkMatch = para.match(/\[\[(([^|\[\]]+)\|)?([^|\[\]]+)\]\]/);
}
// Mark signature paragraphs with a classed span
if (para.length > 0 && para[0] == "~") {
para = "<hr><span class=\"signature\"><p>" + para.slice(1) + "</p></span>";
result.hasSignature = true;
} else {
para = "<p>" + para + "</p>\n";
}
result.html += para;
}
if (citationList.length > 0) {
content += "<p><i>The following articles will be cited:</i></p>\n";
for (var i = 0; i < citationList.length; i++) {
content += "<p>" + citationList[i] + "</p>\n";
}
}
return result;
}
function makeCiteblock(parseResult) {
var citeTexts = []
for (var i = 0; i < parseResult.citations.length; i++) {
var cite = parseResult.citations[i];
citeTexts.push("[" + cite.id.toString() + "] " + cite.citeTitle);
}
return citeTexts.join(" / ");
}
function checkWarnings(parseResult) {
var result = {
errors: [],
warnings: [],
};
if (!parseResult.hasSignature) {
result.warnings.push("Article has no signature.");
}
// Self-citation
// TODO
// Citation targets
// TODO
if (params.citation.min_total != null &&
parseResult.citations.length < params.citation.min_total) {
result.errors.push("Article must have a minimum of " +
params.citation.min_total + " citations.");
}
if (params.citation.max_total != null &&
parseResult.citations.length > params.citation.max_total) {
result.errors.push("Article cannot have more than " +
params.citation.max_total + " citations.");
}
// TODO
// Word limits
var wordCount = (parseResult.html
// Delete all HTML tags
.replace(/<[^>]+>/g, "")
.trim()
.split(/\s+/)
.length);
if (params.wordLimit.hard != null && wordCount > params.wordLimit.hard) {
result.errors.push("Article must be shorter than " + params.wordLimit.hard + " words.");
} else if (params.wordLimit.soft != null && wordCount > params.wordLimit.soft) {
result.warnings.push("Article should be shorter than " + params.wordLimit.soft + " words.");
}
var controlContent = "";
controlContent += "<p>Word count: " + wordCount + "</p>";
if (result.errors.length > 0) {
controlContent += "<p id=\"editor-errors\">";
for (var i = 0; i < result.errors.length; i++) {
controlContent += result.errors[i] + "<br>";
}
controlContent += "</p>";
}
if (result.warnings.length > 0) {
controlContent += "<p id=\"editor-warnings\">";
for (var i = 0; i < result.warnings.length; i++) {
controlContent += result.warnings[i] + "<br>";
}
controlContent += "</p>";
}
return controlContent;
}
// Parse the article content and update the preview pane
// function download() {
// var articlePlayer = document.getElementById("article-player").value;
// var articleTurn = document.getElementById("article-turn").value;
// var articleTitle = document.getElementById("article-title").value;
// var articleBody = document.getElementById("article-body").value;
// var articleText =
// "# Player: " + articlePlayer + "\n" +
// "# Turn: " + articleTurn + "\n" +
// "# Title: " + articleTitle + "\n" +
// "\n" +
// articleBody;
// var articleFilename = articleTitle.toLowerCase().replace(/[^a-z0-9- ]/g, "").replace(/ +/g, "-");
// var downloader = document.createElement("a");
// downloader.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(articleText));
// downloader.setAttribute("download", articleFilename);
// if (document.createEvent) {
// var event = document.createEvent("MouseEvents");
// event.initEvent("click", true, true);
// downloader.dispatchEvent(event);
// } else {
// downloader.click();
// }
// }
window.onload = function() {
document.getElementById("editor-content").value = "\n\n" + params.default_signature;
this.onContentChange();
// Editor state
var loadedArticleInfo = {
aid: null,
lexicon: null,
character: null,
title: null,
turn: null,
status: {
ready: null,
approved: null,
},
contents: null,
};
// Reduce unnecessary requests by checking for no further changes being made
// before updating in response to a change.
var nonce = 0;
function ifNoFurtherChanges(callback) {
var nonce_local = Math.random();
nonce = nonce_local;
setTimeout(() => {
if (nonce == nonce_local) {
callback()
}
}, 2500);
}
// Initialize editor
window.onload = function() {
loadedArticleInfo.character = params.characterId;
document.getElementById("preview").innerHTML = "<p>&nbsp;</p>";
// document.getElementById("editor-content").value = "\n\n" + params.default_signature;
// this.onContentChange();
};
function getArticleObj() {
// aid
// lexicon
// character
// title
// turn
// status
// contents
}
function update(article) {
var req = new XMLHttpRequest();
req.open("POST", params.updateURL, true);
req.setRequestHeader("Content-type", "application/json");
req.onreadystatechange = function () {
if (req.readyState == 4 && req.status == 200)
return;
};
req.send(article)
}
function onContentChange() {
setTimeout(() => {
if (nonce == nonce_local) {
// Get the new content
var articleTitle = document.getElementById("editor-title").value;
var articleBody = document.getElementById("editor-content").value;
// Pass the draft text to the parser to get the preview html and citations
var parseResult = parseLexipythonMarkdown(articleBody);
// Build the citation block
var citeblockContent = makeCiteblock(parseResult);
// Compute warnings and build the control block
var controlContent = checkWarnings(parseResult);
// Fill in the content blocks
document.getElementById("preview").innerHTML = (
"<h1>" + articleTitle + "</h1>\n"
+ parseResult.html);
document.getElementById("preview-citations").innerHTML = citeblockContent;
document.getElementById("preview-control").innerHTML = controlContent;
}
}, 3000);
}
window.addEventListener("beforeunload", function(e) {
var content = document.getElementById("editor-content").value
var hasText = content.length > 0 && content != "\n\n" + params.default_signature;

View File

@ -93,6 +93,7 @@ div.contentblock {
padding: 10px;
width: auto;
border-radius: 5px;
word-break: break-word;
}
div.contentblock h3 {
margin: 0.3em 0;

View File

@ -4,7 +4,7 @@ from flask import (
Blueprint, render_template, url_for, redirect, g, flash, request, Markup)
from flask_login import login_required, current_user
from amanuensis.config import json_ro, open_ex
from amanuensis.config import json_ro, open_ex, prepend
from amanuensis.config.loader import ReadOnlyOrderedDict
from amanuensis.lexicon.manage import valid_add, add_player, add_character
from amanuensis.server.forms import (
@ -14,6 +14,10 @@ from amanuensis.server.helpers import (
player_required_if_not_public)
def jsonfmt(obj):
return Markup(json.dumps(obj))
def get_bp():
"""Create a blueprint for lexicon pages"""
bp = Blueprint('lexicon', __name__, url_prefix='/lexicon/<name>')
@ -138,11 +142,98 @@ def get_bp():
@lexicon_param
@player_required
def editor(name):
"""
cases:
- neither cid nor aid: load all chars and articles
- cid: list articles just for cid
- aid:
"""
cid = request.args.get('cid')
if not cid:
# Character not specified, load all characters and articles
# and return render_template
characters = [
char for char in g.lexicon.character.values()
if char.player == current_user.id
]
articles = [
article for article in g.lexicon.get_drafts_for_player(uid=current_user.id)
if any([article.character == char.cid for char in characters])
]
return render_template(
'lexicon/editor.html',
characters=characters,
articles=articles,
jsonfmt=jsonfmt)
character = g.lexicon.character.get(cid)
if not character:
# Character was specified, but id was invalid
flash("Character not found")
return redirect(url_for('lexicon.session', name=name))
if character.player != current_user.id:
# Player doesn't control this character
flash("Access forbidden")
return redirect(url_for('lexicon.session', name=name))
aid = request.args.get('aid')
if not aid:
# Character specified but not article, load character articles
# and retuen r_t
articles = [
article for article in g.lexicon.get_drafts_for_player(uid=current_user.id)
if article.character == character.cid
]
return render_template(
'lexicon/editor.html',
character=character,
articles=articles,
jsonfmt=jsonfmt)
filename = f'{cid}.{aid}.json'
path = prepend('lexicon', g.lexicon.id, 'draft', filename)
import os
if not os.path.isfile(path):
flash("Draft not found")
return redirect(url_for('lexicon.session', name=name))
with json_ro(path) as a:
article = a
return render_template(
'lexicon/editor.html',
current_turn=Markup(json.dumps(g.lexicon.turn.current)),
citation=Markup(json.dumps(dict(g.lexicon.article.citation))),
word_limit=Markup(json.dumps(dict(g.lexicon.article.word_limit))),
addendum=Markup(json.dumps(dict(g.lexicon.article.citation))))
character=character,
article=article,
jsonfmt=jsonfmt)
@bp.route('/session/editor/new', methods=['GET'])
@lexicon_param
@player_required
def editor_new(name):
import uuid
new_aid = uuid.uuid4().hex
cid = request.args.get("cid")
article = {
"version": "0",
"aid": new_aid,
"lexicon": g.lexicon.id,
"character": cid,
"title": "",
"turn": 1,
"status": {
"ready": False,
"approved": False
},
"contents": ""
}
filename = f"{cid}.{new_aid}.json"
with open_ex('lexicon', g.lexicon.id, 'draft', filename, mode='w') as f:
json.dump(article, f)
return redirect(url_for('lexicon.editor', name=name, cid=cid, aid=new_aid))
@bp.route('/session/editor/update', methods=['POST'])
@lexicon_param
@player_required
def editor_update(name):
pass
return bp

View File

@ -1,6 +1,5 @@
{% set characters = g.lexicon.get_characters_for_player(current_user.id) %}
{% if not characters %}
{% set characters = [g.lexicon.character.default ] %}
{% if character and not article %}
{% set characters = [character] %}
{% endif %}
<!DOCTYPE html>
<html>
@ -12,11 +11,17 @@
<link rel="stylesheet" href="{{ url_for('static', filename='editor.css') }}">
<script>
params = {
current_turn: {{ current_turn }},
default_signature: "{{ characters[0].signature }}",
citation: {{ citation }},
wordLimit: {{ word_limit }},
addendum: {{ addendum }},
updateURL: "{{ url_for('lexicon.editor_update', name=g.lexicon.name) }}",
{% if character %}
character: {{ jsonfmt(character) }},
{% else %}
character: null,
{% endif %}
{% if article %}
article: {{ jsonfmt(character) }},
{% else %}
article: null,
{% endif %}
};
</script>
<script type="text/javascript" src="{{ url_for('static', filename='editor.js') }}"></script>
@ -26,17 +31,51 @@
<div id="editor-left" class="column">
<div class="contentblock">
<div id="editor-header">
<select id="editor-character">
<a href="{{ url_for('lexicon.session', name=g.lexicon.name) }}">
{{ g.lexicon.title }}
</a>
<!-- <select id="editor-character">
{% for char in g.lexicon.get_characters_for_player(current_user.id) %}
<option value="{{ char.cid }}">{{ char.name }}</option>
{% endfor %}
</select>
</select> -->
<span>
<b>{{ current_user.username }}</b>
<b>
{% if character %}
{{ character.name }} /
{% endif %}
{{ current_user.username }}
</b>
</span>
</div>
{% for char in characters %}
<div id="editor-charselect">
<a href="{{ url_for('lexicon.editor', name=g.lexicon.name, cid=char.cid) }}">
<b>{{ char.name }}</b>
</a>
<ul>
{% for article in articles %}
{% if article.character == char.cid %}
<li>
<a href="{{ url_for('lexicon.editor', name=g.lexicon.name, cid=char.cid, aid=article.aid) }}">
{{ article.title if article.title.strip() else "Untitled" }}
</a>
</li>
{% endif %}
{% endfor %}
<li>
<a href="{{ url_for('lexicon.editor_new', name=g.lexicon.name, cid=char.cid) }}">
New
</a>
</li>
</ul>
</div>
{% endfor %}
{% if article %}
<input id="editor-title" placeholder="Title" oninput="onContentChange()">
<textarea id="editor-content" class="fullwidth" oninput="onContentChange()"></textarea>
<textarea id="editor-content" class="fullwidth" oninput="onContentChange()">
</textarea>
{% endif %}
</div>
</div>
<div id="editor-right" class="column">
@ -44,10 +83,10 @@
<p>This editor requires Javascript to function.</p>
</div>
<div id="preview-citations" class="contentblock">
<p>[1]</p>
<p>&nbsp;</p>
</div>
<div id="preview-control" class="contentblock">
<p>Article length:</p>
<p>&nbsp;</p>
</div>
</div>
</div>

View File

@ -45,6 +45,11 @@
<a href="{{ url_for('lexicon.character', name=g.lexicon.name) }}">Create a character</a>
</li>
{% endif %}
<li>
<a href="{{ url_for('lexicon.editor', name=g.lexicon.name) }}">
Article editor
</a>
</li>
</ul>
{% endblock %}
{% set template_content_blocks = template_content_blocks + [self.main()] %}