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. 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 .helpers import titlesort, filesafe_title
from .parsing import parse_raw_markdown from .parsing import parse_raw_markdown
from .render import PreviewHtmlRenderer, HtmlRenderer from .render import PreviewHtmlRenderer, HtmlRenderer
__all__ = [ __all__ = [
FeatureCounter.__name__, ConstraintAnalysis.__name__,
GetCitations.__name__, GetCitations.__name__,
normalize_title.__name__,
titlesort.__name__, titlesort.__name__,
filesafe_title.__name__, filesafe_title.__name__,
parse_raw_markdown.__name__, parse_raw_markdown.__name__,

View File

@ -4,6 +4,9 @@ for verification against constraints.
""" """
import re import re
from typing import Iterable
from amanuensis.models import LexiconModel
from .core import RenderableVisitor from .core import RenderableVisitor
@ -21,12 +24,25 @@ class GetCitations(RenderableVisitor):
return self return self
class FeatureCounter(RenderableVisitor): class ConstraintAnalysis(RenderableVisitor):
def __init__(self): def __init__(self, lexicon: LexiconModel):
self.info: Iterable[str] = []
self.warning: Iterable[str] = []
self.error: Iterable[str] = []
self.word_count = 0 self.word_count = 0
self.citation_count = 0 self.citation_count = 0
self.has_signature = False 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): def TextSpan(self, span):
self.word_count += len(re.split(r'\s+', span.innertext.strip())) self.word_count += len(re.split(r'\s+', span.innertext.strip()))
return self return self

View File

@ -5,10 +5,11 @@ readable formats.
from typing import Iterable from typing import Iterable
from .core import RenderableVisitor
from .helpers import filesafe_title from .helpers import filesafe_title
class HtmlRenderer(): class HtmlRenderer(RenderableVisitor):
""" """
Renders an article token tree into published article HTML. 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>' return f'<a href="{link}"{link_class}>{"".join(span.recurse(self))}</a>'
class PreviewHtmlRenderer(): class PreviewHtmlRenderer(RenderableVisitor):
def __init__(self, lexicon): def __init__(self, lexicon):
with lexicon.ctx.read('info') as info: with lexicon.ctx.read('info') as info:
self.article_map = { self.article_map = {
title: article.character title: article.character
for title, article in info.items() for title, article in info.items()
} }
self.citations = []
self.contents = ""
def TextSpan(self, span): def TextSpan(self, span):
return span.innertext return span.innertext
@ -70,7 +73,8 @@ class PreviewHtmlRenderer():
return '<br>' return '<br>'
def ParsedArticle(self, span): def ParsedArticle(self, span):
return '\n'.join(span.recurse(self)) self.contents = '\n'.join(span.recurse(self))
return self
def BodyParagraph(self, span): def BodyParagraph(self, span):
return f'<p>{"".join(span.recurse(self))}</p>' return f'<p>{"".join(span.recurse(self))}</p>'
@ -91,9 +95,10 @@ class PreviewHtmlRenderer():
def CitationSpan(self, span): def CitationSpan(self, span):
if span.cite_target in self.article_map: if span.cite_target in self.article_map:
if self.article_map.get(span.cite_target): if self.article_map.get(span.cite_target):
link_class = ' style="color:#0000ff"' link_class = '[extant]'
else: else:
link_class = ' style="color:#ff0000"' link_class = '[phantom]'
else: else:
link_class = ' style="color:#008000"' link_class = '[new]'
return f'<a href="#"{link_class}>{"".join(span.recurse(self))}</a>' 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 { div#editor-right div.contentblock {
margin: 10px 5px 10px 10px; margin: 10px 5px 10px 10px;
} }
div#editor-right p#editor-warnings { span.message-warning {
color: #808000; color: #808000;
} }
div#editor-right p#editor-errors { span.message-error {
color: #ff0000; color: #ff0000;
} }
span.new {
color: #008000;
}
@media only screen and (max-width: 816px) { @media only screen and (max-width: 816px) {
div#wrapper { div#wrapper {
max-width: 564px; max-width: 564px;

View File

@ -7,7 +7,8 @@ function ifNoFurtherChanges(callback, timeout=2000) {
nonce = nonce_local; nonce = nonce_local;
setTimeout(() => { setTimeout(() => {
if (nonce == nonce_local) { if (nonce == nonce_local) {
callback() callback();
nonce = 0;
} }
}, timeout); }, timeout);
} }
@ -17,11 +18,6 @@ window.onload = function() {
// Kill noscript message first // Kill noscript message first
document.getElementById("preview").innerHTML = "<p>&nbsp;</p>"; 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); onContentChange(0);
}; };
@ -30,10 +26,7 @@ function buildArticleObject() {
var contents = document.getElementById("editor-content").value; var contents = document.getElementById("editor-content").value;
return { return {
aid: params.article.aid, aid: params.article.aid,
lexicon: params.article.lexicon,
character: params.article.character,
title: title, title: title,
turn: params.article.turn,
status: params.article.status, status: params.article.status,
contents: contents contents: contents
}; };
@ -47,11 +40,12 @@ function update(article) {
req.onreadystatechange = function () { req.onreadystatechange = function () {
if (req.readyState == 4 && req.status == 200) { if (req.readyState == 4 && req.status == 200) {
// Update internal state with the returned article object // 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 // Set editor editability based on article status
updateEditorStatus(); updateEditorStatus();
// Update the preview with the parse information // Update the preview with the parse information
updatePreview(req.response.info); updatePreview(req.response);
} }
}; };
var payload = { article: article }; var payload = { article: article };
@ -66,11 +60,31 @@ function updateEditorStatus() {
submitButton.innerText = ready ? "Edit article" : "Submit article"; submitButton.innerText = ready ? "Edit article" : "Submit article";
} }
function updatePreview(info) { function updatePreview(response) {
var title = document.getElementById("editor-title").value; var previewHtml = "<h1>" + response.title + "</h1>\n" + response.rendered;
var previewHtml = "<h1>" + title + "</h1>\n" + info.rendered;
document.getElementById("preview").innerHTML = previewHtml; 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) { function onContentChange(timeout=2000) {
@ -89,9 +103,7 @@ function submitArticle() {
} }
window.addEventListener("beforeunload", function(e) { window.addEventListener("beforeunload", function(e) {
var content = document.getElementById("editor-content").value if (nonce != 0) {
var hasText = content.length > 0 && content != params.article.contents;
if (hasText) {
e.returnValue = "Are you sure?"; e.returnValue = "Are you sure?";
} }
}); });

View File

@ -1,5 +1,3 @@
import uuid
from flask import ( from flask import (
Blueprint, Blueprint,
render_template, render_template,
@ -15,8 +13,7 @@ from amanuensis.lexicon import attempt_publish
from amanuensis.models import LexiconModel from amanuensis.models import LexiconModel
from amanuensis.parser import ( from amanuensis.parser import (
parse_raw_markdown, parse_raw_markdown,
PreviewHtmlRenderer, PreviewHtmlRenderer)
FeatureCounter)
from amanuensis.server.forms import ( from amanuensis.server.forms import (
LexiconConfigForm, LexiconConfigForm,
LexiconCharacterForm, LexiconCharacterForm,
@ -201,4 +198,3 @@ def editor_update(name):
lexicon: LexiconModel = g.lexicon lexicon: LexiconModel = g.lexicon
article_json = request.json['article'] article_json = request.json['article']
return update_draft(lexicon, article_json) 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) flash, redirect, url_for, render_template, Markup)
from flask_login import current_user 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.models import LexiconModel
from amanuensis.parser import ( from amanuensis.parser import (
normalize_title,
parse_raw_markdown, parse_raw_markdown,
PreviewHtmlRenderer, PreviewHtmlRenderer,
FeatureCounter) ConstraintAnalysis)
def load_editor(lexicon: LexiconModel, aid: str): def load_editor(lexicon: LexiconModel, aid: str):
@ -22,16 +26,10 @@ def load_editor(lexicon: LexiconModel, aid: str):
""" """
if aid: if aid:
# Article specfied, load editor in edit mode # Article specfied, load editor in edit mode
article_fn = None article = get_draft(lexicon, aid)
for filename in lexicon.ctx.draft.ls(): if not article:
if filename.endswith(f'{aid}.json'):
article_fn = filename
break
if not article_fn:
flash("Draft not found") flash("Draft not found")
return redirect(url_for('session.session', name=lexicon.cfg.name)) 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 # Check that the player owns this article
character = lexicon.cfg.character.get(article.character) character = lexicon.cfg.character.get(article.character)
if character.player != current_user.uid: 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 Update a draft and perform analysis on it
""" """
# Check if the update is permitted
aid = article_json.get('aid') aid = article_json.get('aid')
# TODO check if article can be updated article = get_draft(lexicon, aid)
# article exists if not article:
# player owns article raise ValueError("missing article")
# article is not already approved 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') contents = article_json.get('contents')
if contents is not None: status = article_json.get('status')
parsed = parse_raw_markdown(contents) parsed = parse_raw_markdown(contents)
# HTML parsing # HTML parsing
rendered_html = parsed.render(PreviewHtmlRenderer(lexicon)) preview = parsed.render(PreviewHtmlRenderer(lexicon))
# Constraint analysis # Constraint analysis
# features = parsed_draft.render(FeatureCounter()) TODO analysis = parsed.render(ConstraintAnalysis(lexicon))
filename = f'{article_json["character"]}.{article_json["aid"]}'
with lexicon.ctx.draft.edit(filename) as article: # Article update
# TODO filename = f'{article.character}.{aid}'
article.contents = contents 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 { return {
'article': article, 'title': draft.title,
'info': { 'status': {
'rendered': rendered_html, 'ready': draft.status.ready,
#'word_count': features.word_count, '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, character: null,
{% endif %} {% endif %}
{% if article %} {% if article %}
article: {{ jsonfmt(article) }}, article: {
aid: {{ jsonfmt(article.aid) }},
status: {{ jsonfmt(article.status) }},
}
{% else %} {% else %}
article: null, article: null
{% endif %} {% endif %}
}; };
</script> </script>
@ -94,8 +97,8 @@
<button>]</button> <button>]</button>
<button>~</button> <button>~</button>
</div> </div>
<input id="editor-title" placeholder="Title" oninput="onContentChange()" disabled> <input id="editor-title" placeholder="Title" oninput="onContentChange()" disabled value="{{ article.title }}">
<textarea id="editor-content" class="fullwidth" oninput="onContentChange()" disabled></textarea> <textarea id="editor-content" class="fullwidth" oninput="onContentChange()" disabled>{{ article.contents }}</textarea>
{% endif %} {% endif %}
</div> </div>
</div> </div>