Add skeleton draft analysis
This commit is contained in:
parent
5ead3c02a8
commit
fc9c344a1d
|
@ -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__,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}]'
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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> </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?";
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue