Strip down editor a bit and add basic article creation
This commit is contained in:
parent
c6cfe25e6d
commit
1e152851d0
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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, "<");
|
||||
para = para.replace(/>/g, ">");
|
||||
// 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> </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;
|
||||
|
@ -93,6 +93,7 @@ div.contentblock {
|
||||
padding: 10px;
|
||||
width: auto;
|
||||
border-radius: 5px;
|
||||
word-break: break-word;
|
||||
}
|
||||
div.contentblock h3 {
|
||||
margin: 0.3em 0;
|
||||
|
@ -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
|
||||
|
@ -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> </p>
|
||||
</div>
|
||||
<div id="preview-control" class="contentblock">
|
||||
<p>Article length:</p>
|
||||
<p> </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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()] %}
|
Loading…
Reference in New Issue
Block a user