From 1e152851d03c73b376323cbac2b513c3867c5e28 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sat, 22 Feb 2020 08:54:24 -0800 Subject: [PATCH] Strip down editor a bit and add basic article creation --- amanuensis/lexicon/__init__.py | 13 ++ amanuensis/resources/editor.css | 5 +- amanuensis/resources/editor.js | 256 +++++++--------------- amanuensis/resources/page.css | 1 + amanuensis/server/lexicon.py | 101 ++++++++- amanuensis/templates/lexicon/editor.html | 67 ++++-- amanuensis/templates/lexicon/session.html | 5 + 7 files changed, 252 insertions(+), 196 deletions(-) diff --git a/amanuensis/lexicon/__init__.py b/amanuensis/lexicon/__init__.py index ce46d2a..86ad1af 100644 --- a/amanuensis/lexicon/__init__.py +++ b/amanuensis/lexicon/__init__.py @@ -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 diff --git a/amanuensis/resources/editor.css b/amanuensis/resources/editor.css index 1944b64..c049764 100644 --- a/amanuensis/resources/editor.css +++ b/amanuensis/resources/editor.css @@ -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; diff --git a/amanuensis/resources/editor.js b/amanuensis/resources/editor.js index 4052bea..1c45372 100644 --- a/amanuensis/resources/editor.js +++ b/amanuensis/resources/editor.js @@ -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 = ( - "

" + articleTitle + "

\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, ">"); - // Replace bold and italic marks with tags - para = para.replace(/\/\/([^\/]+)\/\//g, "$1"); - para = para.replace(/\*\*([^*]+)\*\*/g, "$1"); - // Replace \\LF with
LF - para = para.replace(/\\\\\n/g, "
\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) + - "" + - citeText + - "" + - "" + - formatId.toString() + - "" + - 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 = "

" + para.slice(1) + "

"; - result.hasSignature = true; - } else { - para = "

" + para + "

\n"; - } - result.html += para; - } - if (citationList.length > 0) { - content += "

The following articles will be cited:

\n"; - for (var i = 0; i < citationList.length; i++) { - content += "

" + citationList[i] + "

\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 += "

Word count: " + wordCount + "

"; - if (result.errors.length > 0) { - controlContent += "

"; - for (var i = 0; i < result.errors.length; i++) { - controlContent += result.errors[i] + "
"; - } - controlContent += "

"; - } - if (result.warnings.length > 0) { - controlContent += "

"; - for (var i = 0; i < result.warnings.length; i++) { - controlContent += result.warnings[i] + "
"; - } - controlContent += "

"; - } - 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 = "

 

"; + + // 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 = ( + "

" + articleTitle + "

\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; diff --git a/amanuensis/resources/page.css b/amanuensis/resources/page.css index 8102ad2..b22db7b 100644 --- a/amanuensis/resources/page.css +++ b/amanuensis/resources/page.css @@ -93,6 +93,7 @@ div.contentblock { padding: 10px; width: auto; border-radius: 5px; + word-break: break-word; } div.contentblock h3 { margin: 0.3em 0; diff --git a/amanuensis/server/lexicon.py b/amanuensis/server/lexicon.py index dbbf423..ce1cd1c 100644 --- a/amanuensis/server/lexicon.py +++ b/amanuensis/server/lexicon.py @@ -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/') @@ -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 diff --git a/amanuensis/templates/lexicon/editor.html b/amanuensis/templates/lexicon/editor.html index 6b0a2c2..db2b0ed 100644 --- a/amanuensis/templates/lexicon/editor.html +++ b/amanuensis/templates/lexicon/editor.html @@ -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 %} @@ -12,11 +11,17 @@ @@ -26,17 +31,51 @@
- - + + {% endif %}
@@ -44,10 +83,10 @@

This editor requires Javascript to function.

-

[1]

+

 

-

Article length:

+

 

diff --git a/amanuensis/templates/lexicon/session.html b/amanuensis/templates/lexicon/session.html index e5dcb1d..981eaa2 100644 --- a/amanuensis/templates/lexicon/session.html +++ b/amanuensis/templates/lexicon/session.html @@ -45,6 +45,11 @@ Create a character {% endif %} +
  • + + Article editor + +
  • {% endblock %} {% set template_content_blocks = template_content_blocks + [self.main()] %} \ No newline at end of file