tmp
This commit is contained in:
parent
241ec45430
commit
837ce735aa
|
@ -2,6 +2,9 @@
|
||||||
Article query interface
|
Article query interface
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from amanuensis.db import *
|
from amanuensis.db import *
|
||||||
|
@ -42,3 +45,10 @@ def create(
|
||||||
db.session.add(new_article)
|
db.session.add(new_article)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return new_article
|
return new_article
|
||||||
|
|
||||||
|
|
||||||
|
def try_from_public_id(db: DbContext, public_id: UUID) -> Optional[Article]:
|
||||||
|
"""Get an article by its public id."""
|
||||||
|
return db(
|
||||||
|
select(Article).where(Article.public_id == public_id)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
|
|
@ -33,11 +33,13 @@
|
||||||
// Stake a claim on the nonce, potentially overwriting a previous
|
// Stake a claim on the nonce, potentially overwriting a previous
|
||||||
// nonce value.
|
// nonce value.
|
||||||
const nonce_local = 1 + Math.random();
|
const nonce_local = 1 + Math.random();
|
||||||
|
console.log("ifNoFurtherChanges: timeout=" + timeout.toString() + ", nonce=" + nonce_local.toString());
|
||||||
nonce = nonce_local;
|
nonce = nonce_local;
|
||||||
// Wait to see if this call is overwritten in turn.
|
// Wait to see if this call is overwritten in turn.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (nonce == nonce_local)
|
if (nonce == nonce_local)
|
||||||
{
|
{
|
||||||
|
console.log("Executing for: " + nonce.toString());
|
||||||
callback();
|
callback();
|
||||||
nonce = 0;
|
nonce = 0;
|
||||||
}
|
}
|
||||||
|
@ -102,9 +104,9 @@
|
||||||
}).then(response => response.json()).then(updateState);
|
}).then(response => response.json()).then(updateState);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onContentChange()
|
function onContentChange(e, timeout=2000)
|
||||||
{
|
{
|
||||||
ifNoFurtherChanges(update, /* timeout: */ 2000);
|
ifNoFurtherChanges(update, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitArticle()
|
function submitArticle()
|
||||||
|
@ -140,7 +142,7 @@
|
||||||
if (e.ctrlKey && e.key == 's')
|
if (e.ctrlKey && e.key == 's')
|
||||||
{
|
{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onContentChange(0);
|
onContentChange(e, 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
{% else %}href="{{ url_for('lexicon.contents', lexicon_name=g.lexicon.name) }}"
|
{% else %}href="{{ url_for('lexicon.contents', lexicon_name=g.lexicon.name) }}"
|
||||||
{% endif %}>Contents</a>{% endblock %}
|
{% endif %}>Contents</a>{% endblock %}
|
||||||
{% block sb_editor %}<a
|
{% block sb_editor %}<a
|
||||||
href="{{ url_for('lexicon.editor.editor', lexicon_name=g.lexicon.name) }}"
|
href="{{ url_for('lexicon.editor.select', lexicon_name=g.lexicon.name) }}"
|
||||||
>Editor</a>{% endblock %}
|
>Editor</a>{% endblock %}
|
||||||
{% block sb_posts %}<a
|
{% block sb_posts %}<a
|
||||||
{% if current_page == "posts" %}class="current-page"
|
{% if current_page == "posts" %}class="current-page"
|
||||||
|
|
|
@ -1,23 +1,123 @@
|
||||||
from flask import Blueprint, render_template, g
|
import re
|
||||||
from sqlalchemy import select
|
|
||||||
|
from flask import Blueprint, render_template, g, abort, request
|
||||||
|
|
||||||
from amanuensis.backend import *
|
from amanuensis.backend import *
|
||||||
from amanuensis.db import *
|
from amanuensis.db import *
|
||||||
from amanuensis.parser.core import *
|
from amanuensis.parser import *
|
||||||
|
from amanuensis.parser.core import Renderable
|
||||||
from amanuensis.server.helpers import lexicon_param, player_required
|
from amanuensis.server.helpers import lexicon_param, player_required
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("editor", __name__, url_prefix="/editor", template_folder=".")
|
bp = Blueprint("editor", __name__, url_prefix="/editor", template_folder=".")
|
||||||
|
|
||||||
|
|
||||||
|
class PreviewHtmlRenderer(RenderableVisitor):
|
||||||
|
"""Parses stylistic markdown and stores citations as footnotes."""
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.citations: list = []
|
||||||
|
self.rendered: str = ""
|
||||||
|
|
||||||
|
# Translate the leaf spans to text
|
||||||
|
def TextSpan(self, span):
|
||||||
|
return span.innertext
|
||||||
|
|
||||||
|
def LineBreak(self, span):
|
||||||
|
return '<br>'
|
||||||
|
|
||||||
|
# Translate the simple container spans to text
|
||||||
|
def BoldSpan(self, span):
|
||||||
|
return f'<b>{"".join(span.recurse(self))}</b>'
|
||||||
|
|
||||||
|
def ItalicSpan(self, span):
|
||||||
|
return f'<i>{"".join(span.recurse(self))}</i>'
|
||||||
|
|
||||||
|
# Record citations in the visitor, then translate the span to text as an
|
||||||
|
# underline and footnote number
|
||||||
|
def CitationSpan(self, span):
|
||||||
|
self.citations.append({
|
||||||
|
"title": span.cite_target,
|
||||||
|
"type": "phantom"
|
||||||
|
})
|
||||||
|
return f'<u>{"".join(span.recurse(self))}</u>[{len(self.citations)}]'
|
||||||
|
|
||||||
|
# Translate the paragraph-level containers to their text contents
|
||||||
|
def BodyParagraph(self, span):
|
||||||
|
return f'<p>{"".join(span.recurse(self))}</p>'
|
||||||
|
|
||||||
|
def SignatureParagraph(self, span):
|
||||||
|
return (
|
||||||
|
'<hr><span class="signature"><p>'
|
||||||
|
f'{"".join(span.recurse(self))}'
|
||||||
|
'</p></span>'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return the visitor from the top-level article span after saving the full
|
||||||
|
# text parsed from the child spans
|
||||||
|
def ParsedArticle(self, span):
|
||||||
|
self.contents = '\n'.join(span.recurse(self))
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class ConstraintCheck(RenderableVisitor):
|
||||||
|
"""Analyzes an article for content-based constraint violations."""
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.word_count: int = 0
|
||||||
|
self.signatures: int = 0
|
||||||
|
self.tmp: bool = False
|
||||||
|
|
||||||
|
def TextSpan(self, span):
|
||||||
|
self.word_count += len(re.split(r'\s+', span.innertext.strip()))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def BoldSpan(self, span):
|
||||||
|
self.tmp = True
|
||||||
|
return self
|
||||||
|
|
||||||
|
def SignatureParagraph(self, span):
|
||||||
|
self.signatures += 1
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def constraint_check(parsed: Renderable):
|
||||||
|
check_result: ConstraintCheck = parsed.render(ConstraintCheck())
|
||||||
|
errors = []
|
||||||
|
errors.append({
|
||||||
|
"severity": 0,
|
||||||
|
"message": f"Word count: {check_result.word_count}"
|
||||||
|
})
|
||||||
|
if check_result.signatures < 1:
|
||||||
|
errors.append({
|
||||||
|
"severity": 1,
|
||||||
|
"message": "Missing signature paragraph"
|
||||||
|
})
|
||||||
|
if check_result.signatures > 1:
|
||||||
|
errors.append({
|
||||||
|
"severity": 1,
|
||||||
|
"message": "More than one signature paragraph"
|
||||||
|
})
|
||||||
|
if check_result.tmp:
|
||||||
|
errors.append({
|
||||||
|
"severity": 2,
|
||||||
|
"message": "Temporary error"
|
||||||
|
})
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/")
|
||||||
|
@lexicon_param
|
||||||
|
@player_required
|
||||||
|
def select(lexicon_name):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<uuid:article_id>")
|
@bp.get("/<uuid:article_id>")
|
||||||
@lexicon_param
|
@lexicon_param
|
||||||
@player_required
|
@player_required
|
||||||
def open(lexicon_name, article_id):
|
def open(lexicon_name, article_id):
|
||||||
db: DbContext = g.db
|
article = artiq.try_from_public_id(g.db, article_id)
|
||||||
article: Article = db(
|
if not article:
|
||||||
select(Article).where(Article.public_id == article_id)
|
return abort(404)
|
||||||
).scalar_one()
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"session.editor.jinja",
|
"session.editor.jinja",
|
||||||
lexicon_name=lexicon_name,
|
lexicon_name=lexicon_name,
|
||||||
|
@ -29,46 +129,39 @@ def open(lexicon_name, article_id):
|
||||||
@lexicon_param
|
@lexicon_param
|
||||||
@player_required
|
@player_required
|
||||||
def load(lexicon_name, article_id):
|
def load(lexicon_name, article_id):
|
||||||
db: DbContext = g.db
|
article = artiq.try_from_public_id(g.db, article_id)
|
||||||
article: Article = db(
|
if not article:
|
||||||
select(Article).where(Article.public_id == article_id)
|
return abort(404)
|
||||||
).scalar_one()
|
parsed = parse_raw_markdown(article.body)
|
||||||
citations = [
|
preview_result: PreviewHtmlRenderer = parsed.render(PreviewHtmlRenderer())
|
||||||
{'title': 'Citation Title', 'type': 'phantom'}
|
errors = constraint_check(parsed)
|
||||||
]
|
|
||||||
errors = [
|
|
||||||
{'severity': 0, 'message': "OK"},
|
|
||||||
{'severity': 1, 'message': "Warning"},
|
|
||||||
{'severity': 2, 'message': "Error"},
|
|
||||||
]
|
|
||||||
return {
|
return {
|
||||||
'title': article.title,
|
'title': article.title,
|
||||||
'rendered': article.body,
|
'rendered': preview_result.contents,
|
||||||
'state': article.state.value,
|
'state': article.state.value,
|
||||||
'ersatz': article.ersatz,
|
'ersatz': article.ersatz,
|
||||||
'citations': citations,
|
'citations': preview_result.citations,
|
||||||
'errors': errors,
|
'errors': errors,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<uuid:article_id>/update")
|
@bp.post("/<uuid:article_id>/update")
|
||||||
def update(lexicon_name, article_id):
|
def update(lexicon_name, article_id):
|
||||||
db: DbContext = g.db
|
article = artiq.try_from_public_id(g.db, article_id)
|
||||||
article: Article = db(
|
if not article:
|
||||||
select(Article).where(Article.public_id == article_id)
|
return abort(404)
|
||||||
).scalar_one()
|
article.title = request.json['title']
|
||||||
citations = [
|
article.body = request.json['body']
|
||||||
{'title': 'Citation Title', 'type': 'phantom'}
|
article.state = ArticleState(request.json['state'])
|
||||||
]
|
g.db.session.commit()
|
||||||
errors = [
|
parsed = parse_raw_markdown(article.body)
|
||||||
{'severity': 0, 'message': "OK"},
|
preview_result: PreviewHtmlRenderer = parsed.render(PreviewHtmlRenderer())
|
||||||
{'severity': 1, 'message': "Warning"},
|
errors = constraint_check(parsed)
|
||||||
]
|
|
||||||
return {
|
return {
|
||||||
'title': article.title,
|
'title': article.title,
|
||||||
'rendered': article.body,
|
'rendered': preview_result.contents,
|
||||||
'state': article.state.value,
|
'state': article.state.value,
|
||||||
'ersatz': article.ersatz,
|
'ersatz': article.ersatz,
|
||||||
'citations': citations,
|
'citations': preview_result.citations,
|
||||||
'errors': errors,
|
'errors': errors,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,187 +1,122 @@
|
||||||
"""
|
# """
|
||||||
Handler helper functions pertaining to the article editor
|
# Handler helper functions pertaining to the article editor
|
||||||
"""
|
# """
|
||||||
import json
|
# import json
|
||||||
import uuid
|
# import uuid
|
||||||
|
|
||||||
from flask import (
|
# 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 (
|
# from amanuensis.lexicon import (
|
||||||
get_player_characters,
|
# get_player_characters,
|
||||||
get_player_drafts,
|
# get_player_drafts,
|
||||||
get_draft,
|
# get_draft,
|
||||||
title_constraint_analysis,
|
# title_constraint_analysis,
|
||||||
content_constraint_analysis)
|
# content_constraint_analysis)
|
||||||
from amanuensis.models import LexiconModel
|
# from amanuensis.models import LexiconModel
|
||||||
from amanuensis.parser import (
|
# from amanuensis.parser import (
|
||||||
normalize_title,
|
# normalize_title,
|
||||||
parse_raw_markdown)
|
# parse_raw_markdown)
|
||||||
from amanuensis.parser.core import RenderableVisitor
|
# from amanuensis.parser.core import RenderableVisitor
|
||||||
|
|
||||||
|
|
||||||
class PreviewHtmlRenderer(RenderableVisitor):
|
# 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.citations = []
|
||||||
self.contents = ""
|
# self.contents = ""
|
||||||
|
|
||||||
def TextSpan(self, span):
|
# def TextSpan(self, span):
|
||||||
return span.innertext
|
# return span.innertext
|
||||||
|
|
||||||
def LineBreak(self, span):
|
# def LineBreak(self, span):
|
||||||
return '<br>'
|
# return '<br>'
|
||||||
|
|
||||||
def ParsedArticle(self, span):
|
# def ParsedArticle(self, span):
|
||||||
self.contents = '\n'.join(span.recurse(self))
|
# self.contents = '\n'.join(span.recurse(self))
|
||||||
return 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>'
|
||||||
|
|
||||||
def SignatureParagraph(self, span):
|
# def SignatureParagraph(self, span):
|
||||||
return (
|
# return (
|
||||||
'<hr><span class="signature"><p>'
|
# '<hr><span class="signature"><p>'
|
||||||
f'{"".join(span.recurse(self))}'
|
# f'{"".join(span.recurse(self))}'
|
||||||
'</p></span>'
|
# '</p></span>'
|
||||||
)
|
# )
|
||||||
|
|
||||||
def BoldSpan(self, span):
|
# def BoldSpan(self, span):
|
||||||
return f'<b>{"".join(span.recurse(self))}</b>'
|
# return f'<b>{"".join(span.recurse(self))}</b>'
|
||||||
|
|
||||||
def ItalicSpan(self, span):
|
# def ItalicSpan(self, span):
|
||||||
return f'<i>{"".join(span.recurse(self))}</i>'
|
# return f'<i>{"".join(span.recurse(self))}</i>'
|
||||||
|
|
||||||
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 = '[extant]'
|
# link_class = '[extant]'
|
||||||
else:
|
# else:
|
||||||
link_class = '[phantom]'
|
# link_class = '[phantom]'
|
||||||
else:
|
# else:
|
||||||
link_class = '[new]'
|
# link_class = '[new]'
|
||||||
self.citations.append(f'{span.cite_target} {link_class}')
|
# self.citations.append(f'{span.cite_target} {link_class}')
|
||||||
return f'<u>{"".join(span.recurse(self))}</u>[{len(self.citations)}]'
|
# return f'<u>{"".join(span.recurse(self))}</u>[{len(self.citations)}]'
|
||||||
|
|
||||||
|
|
||||||
def load_editor(lexicon: LexiconModel, aid: str):
|
# def update_draft(lexicon: LexiconModel, article_json):
|
||||||
"""
|
# """
|
||||||
Load the editor page
|
# Update a draft and perform analysis on it
|
||||||
"""
|
# """
|
||||||
if aid:
|
# # Check if the update is permitted
|
||||||
# Article specfied, load editor in edit mode
|
# aid = article_json.get('aid')
|
||||||
article = get_draft(lexicon, aid)
|
# article = get_draft(lexicon, aid)
|
||||||
if not article:
|
# if not article:
|
||||||
flash("Draft not found")
|
# raise ValueError("missing article")
|
||||||
return redirect(url_for('session.session', name=lexicon.cfg.name))
|
# if lexicon.cfg.character.get(article.character).player != current_user.uid:
|
||||||
# Check that the player owns this article
|
# return ValueError("bad user")
|
||||||
character = lexicon.cfg.character.get(article.character)
|
# if article.status.approved:
|
||||||
if character.player != current_user.uid:
|
# raise ValueError("bad status")
|
||||||
flash("Access forbidden")
|
|
||||||
return redirect(url_for('session.session', name=lexicon.cfg.name))
|
|
||||||
return render_template(
|
|
||||||
'session.editor.jinja',
|
|
||||||
character=character,
|
|
||||||
article=article,
|
|
||||||
jsonfmt=lambda obj: Markup(json.dumps(obj)))
|
|
||||||
|
|
||||||
# Article not specified, load editor in load mode
|
# # Perform the update
|
||||||
characters = list(get_player_characters(lexicon, current_user.uid))
|
# title = article_json.get('title')
|
||||||
articles = list(get_player_drafts(lexicon, current_user.uid))
|
# contents = article_json.get('contents')
|
||||||
return render_template(
|
# status = article_json.get('status')
|
||||||
'session.editor.jinja',
|
|
||||||
characters=characters,
|
|
||||||
articles=articles)
|
|
||||||
|
|
||||||
|
# parsed = parse_raw_markdown(contents)
|
||||||
|
|
||||||
def new_draft(lexicon: LexiconModel, cid: str):
|
# # HTML parsing
|
||||||
"""
|
# preview = parsed.render(PreviewHtmlRenderer(lexicon))
|
||||||
Create a new draft and open it in the editor
|
# # Constraint analysis
|
||||||
"""
|
# title_infos, title_warnings, title_errors = title_constraint_analysis(
|
||||||
if cid:
|
# lexicon, current_user, article.character, title)
|
||||||
new_aid = uuid.uuid4().hex
|
# content_infos, content_warnings, content_errors = content_constraint_analysis(
|
||||||
# TODO harden this
|
# lexicon, current_user, article.character, parsed)
|
||||||
character = lexicon.cfg.character.get(cid)
|
# if any(title_errors) or any(content_errors):
|
||||||
article = {
|
# status['ready'] = False
|
||||||
"version": "0",
|
|
||||||
"aid": new_aid,
|
|
||||||
"lexicon": lexicon.lid,
|
|
||||||
"character": cid,
|
|
||||||
"title": "",
|
|
||||||
"turn": 1,
|
|
||||||
"status": {
|
|
||||||
"ready": False,
|
|
||||||
"approved": False
|
|
||||||
},
|
|
||||||
"contents": f"\n\n{character.signature}",
|
|
||||||
}
|
|
||||||
filename = f"{cid}.{new_aid}"
|
|
||||||
with lexicon.ctx.draft.new(filename) as j:
|
|
||||||
j.update(article)
|
|
||||||
return redirect(url_for(
|
|
||||||
'session.editor',
|
|
||||||
name=lexicon.cfg.name,
|
|
||||||
cid=cid,
|
|
||||||
aid=new_aid))
|
|
||||||
|
|
||||||
# Character not specified
|
# # Article update
|
||||||
flash('Character not found')
|
# filename = f'{article.character}.{aid}'
|
||||||
return redirect(url_for('session.session', name=lexicon.cfg.name))
|
# 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
|
||||||
def update_draft(lexicon: LexiconModel, article_json):
|
# return {
|
||||||
"""
|
# 'title': draft.title,
|
||||||
Update a draft and perform analysis on it
|
# 'status': {
|
||||||
"""
|
# 'ready': draft.status.ready,
|
||||||
# Check if the update is permitted
|
# 'approved': draft.status.approved,
|
||||||
aid = article_json.get('aid')
|
# },
|
||||||
article = get_draft(lexicon, aid)
|
# 'rendered': preview.contents,
|
||||||
if not article:
|
# 'citations': preview.citations,
|
||||||
raise ValueError("missing article")
|
# 'info': title_infos + content_infos,
|
||||||
if lexicon.cfg.character.get(article.character).player != current_user.uid:
|
# 'warning': title_warnings + content_warnings,
|
||||||
return ValueError("bad user")
|
# 'error': title_errors + content_errors,
|
||||||
if article.status.approved:
|
# }
|
||||||
raise ValueError("bad status")
|
|
||||||
|
|
||||||
# Perform the update
|
|
||||||
title = article_json.get('title')
|
|
||||||
contents = article_json.get('contents')
|
|
||||||
status = article_json.get('status')
|
|
||||||
|
|
||||||
parsed = parse_raw_markdown(contents)
|
|
||||||
|
|
||||||
# HTML parsing
|
|
||||||
preview = parsed.render(PreviewHtmlRenderer(lexicon))
|
|
||||||
# Constraint analysis
|
|
||||||
title_infos, title_warnings, title_errors = title_constraint_analysis(
|
|
||||||
lexicon, current_user, article.character, title)
|
|
||||||
content_infos, content_warnings, content_errors = content_constraint_analysis(
|
|
||||||
lexicon, current_user, article.character, parsed)
|
|
||||||
if any(title_errors) or any(content_errors):
|
|
||||||
status['ready'] = False
|
|
||||||
|
|
||||||
# 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': title_infos + content_infos,
|
|
||||||
'warning': title_warnings + content_warnings,
|
|
||||||
'error': title_errors + content_errors,
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue