diff --git a/amanuensis/parser/__init__.py b/amanuensis/parser/__init__.py index 9aa9584..1de2c5d 100644 --- a/amanuensis/parser/__init__.py +++ b/amanuensis/parser/__init__.py @@ -2,14 +2,16 @@ Module encapsulating all markdown parsing functionality. """ -from .analyze import FeatureCounter, GetCitations +from .analyze import ConstraintAnalysis, GetCitations +from .core import normalize_title from .helpers import titlesort, filesafe_title from .parsing import parse_raw_markdown from .render import PreviewHtmlRenderer, HtmlRenderer __all__ = [ - FeatureCounter.__name__, + ConstraintAnalysis.__name__, GetCitations.__name__, + normalize_title.__name__, titlesort.__name__, filesafe_title.__name__, parse_raw_markdown.__name__, diff --git a/amanuensis/parser/analyze.py b/amanuensis/parser/analyze.py index f581353..79ad079 100644 --- a/amanuensis/parser/analyze.py +++ b/amanuensis/parser/analyze.py @@ -4,6 +4,9 @@ for verification against constraints. """ import re +from typing import Iterable + +from amanuensis.models import LexiconModel from .core import RenderableVisitor @@ -21,12 +24,25 @@ class GetCitations(RenderableVisitor): return self -class FeatureCounter(RenderableVisitor): - def __init__(self): +class ConstraintAnalysis(RenderableVisitor): + def __init__(self, lexicon: LexiconModel): + self.info: Iterable[str] = [] + self.warning: Iterable[str] = [] + self.error: Iterable[str] = [] + self.word_count = 0 self.citation_count = 0 self.has_signature = False + def ParsedArticle(self, span): + # Execute over the article tree + span.recurse(self) + # Perform analysis + self.info.append(f'Word count: {self.word_count}') + if not self.has_signature: + self.warning.append('Missing signature') + return self + def TextSpan(self, span): self.word_count += len(re.split(r'\s+', span.innertext.strip())) return self diff --git a/amanuensis/parser/render.py b/amanuensis/parser/render.py index 03c2913..9313c07 100644 --- a/amanuensis/parser/render.py +++ b/amanuensis/parser/render.py @@ -5,10 +5,11 @@ readable formats. from typing import Iterable +from .core import RenderableVisitor from .helpers import filesafe_title -class HtmlRenderer(): +class HtmlRenderer(RenderableVisitor): """ Renders an article token tree into published article HTML. """ @@ -55,13 +56,15 @@ class HtmlRenderer(): return f'{"".join(span.recurse(self))}' -class PreviewHtmlRenderer(): +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 @@ -70,7 +73,8 @@ class PreviewHtmlRenderer(): return '
' def ParsedArticle(self, span): - return '\n'.join(span.recurse(self)) + self.contents = '\n'.join(span.recurse(self)) + return self def BodyParagraph(self, span): return f'

{"".join(span.recurse(self))}

' @@ -91,9 +95,10 @@ class PreviewHtmlRenderer(): def CitationSpan(self, span): if span.cite_target in self.article_map: if self.article_map.get(span.cite_target): - link_class = ' style="color:#0000ff"' + link_class = '[extant]' else: - link_class = ' style="color:#ff0000"' + link_class = '[phantom]' else: - link_class = ' style="color:#008000"' - return f'{"".join(span.recurse(self))}' + link_class = '[new]' + self.citations.append(f'{span.cite_target} {link_class}') + return f'{"".join(span.recurse(self))}[{len(self.citations)}]' diff --git a/amanuensis/resources/editor.css b/amanuensis/resources/editor.css index cc9f925..fb0e52b 100644 --- a/amanuensis/resources/editor.css +++ b/amanuensis/resources/editor.css @@ -51,15 +51,12 @@ div#editor-right { div#editor-right div.contentblock { margin: 10px 5px 10px 10px; } -div#editor-right p#editor-warnings { +span.message-warning { color: #808000; } -div#editor-right p#editor-errors { +span.message-error { color: #ff0000; } -span.new { - color: #008000; -} @media only screen and (max-width: 816px) { div#wrapper { max-width: 564px; diff --git a/amanuensis/resources/editor.js b/amanuensis/resources/editor.js index aed7f26..83ea028 100644 --- a/amanuensis/resources/editor.js +++ b/amanuensis/resources/editor.js @@ -7,7 +7,8 @@ function ifNoFurtherChanges(callback, timeout=2000) { nonce = nonce_local; setTimeout(() => { if (nonce == nonce_local) { - callback() + callback(); + nonce = 0; } }, timeout); } @@ -17,11 +18,6 @@ window.onload = function() { // Kill noscript message first document.getElementById("preview").innerHTML = "

 

"; - if (params.article != null) { - document.getElementById("editor-title").value = params.article.title; - document.getElementById("editor-content").value = params.article.contents; - } - onContentChange(0); }; @@ -30,10 +26,7 @@ function buildArticleObject() { var contents = document.getElementById("editor-content").value; return { aid: params.article.aid, - lexicon: params.article.lexicon, - character: params.article.character, title: title, - turn: params.article.turn, status: params.article.status, contents: contents }; @@ -47,11 +40,12 @@ function update(article) { req.onreadystatechange = function () { if (req.readyState == 4 && req.status == 200) { // Update internal state with the returned article object - params.article = req.response.article; + params.status = req.response.status; + 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.info); + updatePreview(req.response); } }; var payload = { article: article }; @@ -66,11 +60,31 @@ function updateEditorStatus() { submitButton.innerText = ready ? "Edit article" : "Submit article"; } -function updatePreview(info) { - var title = document.getElementById("editor-title").value; - var previewHtml = "

" + title + "

\n" + info.rendered; +function updatePreview(response) { + var previewHtml = "

" + response.title + "

\n" + response.rendered; document.getElementById("preview").innerHTML = previewHtml; - document.getElementById("preview-control").innerHTML = info.word_count; + + var citations = "
    "; + for (var i = 0; i < response.citations.length; i++) { + citations += "
  1. " + response.citations[i] + "
  2. "; + } + citations += "
"; + document.getElementById("preview-citations").innerHTML = citations; + + var info = ""; + for (var i = 0; i < response.info.length; i++) { + info += "" + response.info[i] + "
"; + } + var warning = ""; + for (var i = 0; i < response.warning.length; i++) { + warning += "" + response.warning[i] + "
"; + } + var error = ""; + for (var i = 0; i < response.error.length; i++) { + error += "" + response.error[i] + "
"; + } + var control = info + "
" + warning + "
" + error; + document.getElementById("preview-control").innerHTML = control; } function onContentChange(timeout=2000) { @@ -89,9 +103,7 @@ function submitArticle() { } window.addEventListener("beforeunload", function(e) { - var content = document.getElementById("editor-content").value - var hasText = content.length > 0 && content != params.article.contents; - if (hasText) { + if (nonce != 0) { e.returnValue = "Are you sure?"; } }); diff --git a/amanuensis/server/session/__init__.py b/amanuensis/server/session/__init__.py index 97ce4ae..0cb81f5 100644 --- a/amanuensis/server/session/__init__.py +++ b/amanuensis/server/session/__init__.py @@ -1,5 +1,3 @@ -import uuid - from flask import ( Blueprint, render_template, @@ -15,8 +13,7 @@ from amanuensis.lexicon import attempt_publish from amanuensis.models import LexiconModel from amanuensis.parser import ( parse_raw_markdown, - PreviewHtmlRenderer, - FeatureCounter) + PreviewHtmlRenderer) from amanuensis.server.forms import ( LexiconConfigForm, LexiconCharacterForm, @@ -201,4 +198,3 @@ def editor_update(name): lexicon: LexiconModel = g.lexicon article_json = request.json['article'] return update_draft(lexicon, article_json) - # only need to be sending around title, status, contents, aid diff --git a/amanuensis/server/session/editor.py b/amanuensis/server/session/editor.py index fba0cf5..d32e68f 100644 --- a/amanuensis/server/session/editor.py +++ b/amanuensis/server/session/editor.py @@ -8,12 +8,16 @@ 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 +from amanuensis.lexicon import ( + get_player_characters, + get_player_drafts, + get_draft) from amanuensis.models import LexiconModel from amanuensis.parser import ( + normalize_title, parse_raw_markdown, PreviewHtmlRenderer, - FeatureCounter) + ConstraintAnalysis) def load_editor(lexicon: LexiconModel, aid: str): @@ -22,16 +26,10 @@ def load_editor(lexicon: LexiconModel, aid: str): """ if aid: # Article specfied, load editor in edit mode - article_fn = None - for filename in lexicon.ctx.draft.ls(): - if filename.endswith(f'{aid}.json'): - article_fn = filename - break - if not article_fn: + article = get_draft(lexicon, aid) + if not article: flash("Draft not found") return redirect(url_for('session.session', name=lexicon.cfg.name)) - with lexicon.ctx.draft.read(article_fn) as a: - article = a # Check that the player owns this article character = lexicon.cfg.character.get(article.character) if character.player != current_user.uid: @@ -91,28 +89,45 @@ 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') - # TODO check if article can be updated - # article exists - # player owns article - # article is not already approved + 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') - if contents is not None: - parsed = parse_raw_markdown(contents) - # HTML parsing - rendered_html = parsed.render(PreviewHtmlRenderer(lexicon)) - # Constraint analysis - # features = parsed_draft.render(FeatureCounter()) TODO - filename = f'{article_json["character"]}.{article_json["aid"]}' - with lexicon.ctx.draft.edit(filename) as article: - # TODO - article.contents = contents - return { - 'article': article, - 'info': { - 'rendered': rendered_html, - #'word_count': features.word_count, - } - } - return {} + status = article_json.get('status') + + parsed = parse_raw_markdown(contents) + + # HTML parsing + preview = parsed.render(PreviewHtmlRenderer(lexicon)) + # Constraint analysis + analysis = parsed.render(ConstraintAnalysis(lexicon)) + + # 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': analysis.info, + 'warning': analysis.warning, + 'error': analysis.error, + } diff --git a/amanuensis/server/session/session.editor.jinja b/amanuensis/server/session/session.editor.jinja index 57fe23f..87f6245 100644 --- a/amanuensis/server/session/session.editor.jinja +++ b/amanuensis/server/session/session.editor.jinja @@ -18,9 +18,12 @@ character: null, {% endif %} {% if article %} - article: {{ jsonfmt(article) }}, + article: { + aid: {{ jsonfmt(article.aid) }}, + status: {{ jsonfmt(article.status) }}, + } {% else %} - article: null, + article: null {% endif %} }; @@ -94,8 +97,8 @@ - - + + {% endif %}