Add skeleton draft analysis

This commit is contained in:
Tim Van Baak 2020-04-25 21:52:54 -07:00
parent 5ead3c02a8
commit fc9c344a1d
8 changed files with 121 additions and 75 deletions

View File

@ -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__,

View File

@ -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

View File

@ -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'<a href="{link}"{link_class}>{"".join(span.recurse(self))}</a>'
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 '<br>'
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'<p>{"".join(span.recurse(self))}</p>'
@ -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'<a href="#"{link_class}>{"".join(span.recurse(self))}</a>'
link_class = '[new]'
self.citations.append(f'{span.cite_target} {link_class}')
return f'<u>{"".join(span.recurse(self))}</u>[{len(self.citations)}]'

View File

@ -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;

View File

@ -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 = "<p>&nbsp;</p>";
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 = "<h1>" + title + "</h1>\n" + info.rendered;
function updatePreview(response) {
var previewHtml = "<h1>" + response.title + "</h1>\n" + response.rendered;
document.getElementById("preview").innerHTML = previewHtml;
document.getElementById("preview-control").innerHTML = info.word_count;
var citations = "<ol>";
for (var i = 0; i < response.citations.length; i++) {
citations += "<li>" + response.citations[i] + "</li>";
}
citations += "</ol>";
document.getElementById("preview-citations").innerHTML = citations;
var info = "";
for (var i = 0; i < response.info.length; i++) {
info += "<span class=\"message-info\">" + response.info[i] + "</span><br>";
}
var warning = "";
for (var i = 0; i < response.warning.length; i++) {
warning += "<span class=\"message-warning\">" + response.warning[i] + "</span><br>";
}
var error = "";
for (var i = 0; i < response.error.length; i++) {
error += "<span class=\"message-error\">" + response.error[i] + "</span><br>";
}
var control = info + "<br>" + warning + "<br>" + 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?";
}
});

View File

@ -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

View File

@ -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:
status = article_json.get('status')
parsed = parse_raw_markdown(contents)
# HTML parsing
rendered_html = parsed.render(PreviewHtmlRenderer(lexicon))
preview = 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
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 {
'article': article,
'info': {
'rendered': rendered_html,
#'word_count': features.word_count,
'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,
}
}
return {}

View File

@ -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 %}
};
</script>
@ -94,8 +97,8 @@
<button>]</button>
<button>~</button>
</div>
<input id="editor-title" placeholder="Title" oninput="onContentChange()" disabled>
<textarea id="editor-content" class="fullwidth" oninput="onContentChange()" disabled></textarea>
<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 %}
</div>
</div>