Add skeleton draft analysis
This commit is contained in:
parent
5ead3c02a8
commit
fc9c344a1d
|
@ -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__,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)}]'
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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> </p>";
|
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);
|
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?";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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)
|
|
||||||
# HTML parsing
|
parsed = parse_raw_markdown(contents)
|
||||||
rendered_html = parsed.render(PreviewHtmlRenderer(lexicon))
|
|
||||||
# Constraint analysis
|
# HTML parsing
|
||||||
# features = parsed_draft.render(FeatureCounter()) TODO
|
preview = parsed.render(PreviewHtmlRenderer(lexicon))
|
||||||
filename = f'{article_json["character"]}.{article_json["aid"]}'
|
# Constraint analysis
|
||||||
with lexicon.ctx.draft.edit(filename) as article:
|
analysis = parsed.render(ConstraintAnalysis(lexicon))
|
||||||
# TODO
|
|
||||||
article.contents = contents
|
# Article update
|
||||||
return {
|
filename = f'{article.character}.{aid}'
|
||||||
'article': article,
|
with lexicon.ctx.draft.edit(filename) as draft:
|
||||||
'info': {
|
draft.title = normalize_title(title)
|
||||||
'rendered': rendered_html,
|
draft.contents = contents
|
||||||
#'word_count': features.word_count,
|
draft.status.ready = status.get('ready', False)
|
||||||
}
|
|
||||||
}
|
# Return canonical information to editor
|
||||||
return {}
|
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,
|
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>
|
||||||
|
|
Loading…
Reference in New Issue