mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
more template rendering tweaks
- The front and back are rendered in one call now. If the front side contains no custom filters, we can bake {{FrontSide}} into the rear side. If it did contain custom filters, we return the partially complete rear template instead, and the calling code can inject the FrontSide in after it has been fully rendered. - Instead of modifying "cloze" into something like "cq-2", the card ordinal and whether we're rendering the question or answer are now passed in to the rendering filters as context. - The Rust code doesn't need to support filter names split on '-' anymore. - Drop the "Show" part of hint descriptions so i18n support can be deferred. - Ignore blank filter names caused by user using two colons instead of one. - Fixed hint field and text transposition.
This commit is contained in:
parent
be70997e5a
commit
9bb0348fdd
8 changed files with 330 additions and 167 deletions
|
@ -14,7 +14,7 @@ message BackendInput {
|
||||||
Empty deck_tree = 18;
|
Empty deck_tree = 18;
|
||||||
FindCardsIn find_cards = 19;
|
FindCardsIn find_cards = 19;
|
||||||
BrowserRowsIn browser_rows = 20;
|
BrowserRowsIn browser_rows = 20;
|
||||||
RenderTemplateIn render_template = 21;
|
RenderCardIn render_card = 21;
|
||||||
|
|
||||||
PlusOneIn plus_one = 2046; // temporary, for testing
|
PlusOneIn plus_one = 2046; // temporary, for testing
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ message BackendOutput {
|
||||||
DeckTreeOut deck_tree = 18;
|
DeckTreeOut deck_tree = 18;
|
||||||
FindCardsOut find_cards = 19;
|
FindCardsOut find_cards = 19;
|
||||||
BrowserRowsOut browser_rows = 20;
|
BrowserRowsOut browser_rows = 20;
|
||||||
RenderTemplateOut render_template = 21;
|
RenderCardOut render_card = 21;
|
||||||
|
|
||||||
PlusOneOut plus_one = 2046; // temporary, for testing
|
PlusOneOut plus_one = 2046; // temporary, for testing
|
||||||
|
|
||||||
|
@ -128,13 +128,16 @@ message BrowserRowsOut {
|
||||||
repeated string sort_fields = 1;
|
repeated string sort_fields = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RenderTemplateIn {
|
message RenderCardIn {
|
||||||
string template_text = 1;
|
string question_template = 1;
|
||||||
map<string,string> fields = 2;
|
string answer_template = 2;
|
||||||
|
map<string,string> fields = 3;
|
||||||
|
int32 card_ordinal = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RenderTemplateOut {
|
message RenderCardOut {
|
||||||
repeated RenderedTemplateNode nodes = 1;
|
repeated RenderedTemplateNode question_nodes = 1;
|
||||||
|
repeated RenderedTemplateNode answer_nodes = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RenderedTemplateNode {
|
message RenderedTemplateNode {
|
||||||
|
|
|
@ -30,7 +30,7 @@ from anki.rsbackend import RustBackend
|
||||||
from anki.sched import Scheduler as V1Scheduler
|
from anki.sched import Scheduler as V1Scheduler
|
||||||
from anki.schedv2 import Scheduler as V2Scheduler
|
from anki.schedv2 import Scheduler as V2Scheduler
|
||||||
from anki.tags import TagManager
|
from anki.tags import TagManager
|
||||||
from anki.template import render_qa_from_field_map
|
from anki.template import render_card
|
||||||
from anki.types import NoteType, QAData, Template
|
from anki.types import NoteType, QAData, Template
|
||||||
from anki.utils import (
|
from anki.utils import (
|
||||||
devMode,
|
devMode,
|
||||||
|
@ -666,7 +666,7 @@ where c.nid = n.id and c.id in %s group by nid"""
|
||||||
fields = runFilter("mungeFields", fields, model, data, self)
|
fields = runFilter("mungeFields", fields, model, data, self)
|
||||||
|
|
||||||
# render fields
|
# render fields
|
||||||
qatext = render_qa_from_field_map(self, qfmt, afmt, fields, card_ord)
|
qatext = render_card(self, qfmt, afmt, fields, card_ord)
|
||||||
ret: Dict[str, Any] = dict(q=qatext[0], a=qatext[1], id=card_id)
|
ret: Dict[str, Any] = dict(q=qatext[0], a=qatext[1], id=card_id)
|
||||||
|
|
||||||
# allow add-ons to modify the generated result
|
# allow add-ons to modify the generated result
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
# pylint: skip-file
|
# pylint: skip-file
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Union
|
from typing import Dict, List, Tuple, Union
|
||||||
|
|
||||||
import ankirspy # pytype: disable=import-error
|
import ankirspy # pytype: disable=import-error
|
||||||
|
|
||||||
|
@ -53,6 +53,27 @@ class TemplateReplacement:
|
||||||
filters: List[str]
|
filters: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
TemplateReplacementList = List[Union[str, TemplateReplacement]]
|
||||||
|
|
||||||
|
|
||||||
|
def proto_replacement_list_to_native(
|
||||||
|
nodes: List[pb.RenderedTemplateNode],
|
||||||
|
) -> TemplateReplacementList:
|
||||||
|
results: TemplateReplacementList = []
|
||||||
|
for node in nodes:
|
||||||
|
if node.WhichOneof("value") == "text":
|
||||||
|
results.append(node.text)
|
||||||
|
else:
|
||||||
|
results.append(
|
||||||
|
TemplateReplacement(
|
||||||
|
field_name=node.replacement.field_name,
|
||||||
|
current_text=node.replacement.current_text,
|
||||||
|
filters=list(node.replacement.filters),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
class RustBackend:
|
class RustBackend:
|
||||||
def __init__(self, path: str):
|
def __init__(self, path: str):
|
||||||
self._backend = ankirspy.Backend(path)
|
self._backend = ankirspy.Backend(path)
|
||||||
|
@ -105,26 +126,21 @@ class RustBackend:
|
||||||
)
|
)
|
||||||
).sched_timing_today
|
).sched_timing_today
|
||||||
|
|
||||||
def render_template(
|
def render_card(
|
||||||
self, template: str, fields: Dict[str, str]
|
self, qfmt: str, afmt: str, fields: Dict[str, str], card_ord: int
|
||||||
) -> List[Union[str, TemplateReplacement]]:
|
) -> Tuple[TemplateReplacementList, TemplateReplacementList]:
|
||||||
out = self._run_command(
|
out = self._run_command(
|
||||||
pb.BackendInput(
|
pb.BackendInput(
|
||||||
render_template=pb.RenderTemplateIn(
|
render_card=pb.RenderCardIn(
|
||||||
template_text=template, fields=fields
|
question_template=qfmt,
|
||||||
|
answer_template=afmt,
|
||||||
|
fields=fields,
|
||||||
|
card_ordinal=card_ord,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).render_template
|
).render_card
|
||||||
results: List[Union[str, TemplateReplacement]] = []
|
|
||||||
for node in out.nodes:
|
qnodes = proto_replacement_list_to_native(out.question_nodes) # type: ignore
|
||||||
if node.WhichOneof("value") == "text":
|
anodes = proto_replacement_list_to_native(out.answer_nodes) # type: ignore
|
||||||
results.append(node.text)
|
|
||||||
else:
|
return (qnodes, anodes)
|
||||||
results.append(
|
|
||||||
TemplateReplacement(
|
|
||||||
field_name=node.replacement.field_name,
|
|
||||||
current_text=node.replacement.current_text,
|
|
||||||
filters=list(node.replacement.filters),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return results
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ unrecognized filter. The remaining filters are returned to Python,
|
||||||
and applied using the hook system. For example,
|
and applied using the hook system. For example,
|
||||||
{{myfilter:hint:text:Field}} will apply the built in text and hint filters,
|
{{myfilter:hint:text:Field}} will apply the built in text and hint filters,
|
||||||
and then attempt to apply myfilter. If no add-ons have provided the filter,
|
and then attempt to apply myfilter. If no add-ons have provided the filter,
|
||||||
the text is not modified.
|
the filter is skipped.
|
||||||
|
|
||||||
Add-ons can register a filter by adding a hook to "fmod_<filter name>".
|
Add-ons can register a filter by adding a hook to "fmod_<filter name>".
|
||||||
As standard filters will not be run after a custom filter, it is up to the
|
As standard filters will not be run after a custom filter, it is up to the
|
||||||
|
@ -29,23 +29,14 @@ template_legacy.py file.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Dict, List, Tuple, Union
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
from anki.hooks import runFilter
|
from anki.hooks import runFilter
|
||||||
from anki.rsbackend import TemplateReplacement
|
from anki.rsbackend import TemplateReplacementList
|
||||||
from anki.sound import stripSounds
|
|
||||||
|
|
||||||
|
|
||||||
def render_template(
|
def render_card(
|
||||||
col: anki.storage._Collection, format: str, fields: Dict[str, str]
|
|
||||||
) -> str:
|
|
||||||
"Render a single template."
|
|
||||||
rendered = col.backend.render_template(format, fields)
|
|
||||||
return apply_custom_filters(rendered, fields)
|
|
||||||
|
|
||||||
|
|
||||||
def render_qa_from_field_map(
|
|
||||||
col: anki.storage._Collection,
|
col: anki.storage._Collection,
|
||||||
qfmt: str,
|
qfmt: str,
|
||||||
afmt: str,
|
afmt: str,
|
||||||
|
@ -53,39 +44,38 @@ def render_qa_from_field_map(
|
||||||
card_ord: int,
|
card_ord: int,
|
||||||
) -> Tuple[str, str]:
|
) -> Tuple[str, str]:
|
||||||
"Renders the provided templates, returning rendered q & a text."
|
"Renders the provided templates, returning rendered q & a text."
|
||||||
# question
|
|
||||||
format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (card_ord + 1), qfmt)
|
|
||||||
format = format.replace("<%cloze:", "<%%cq:%d:" % (card_ord + 1))
|
|
||||||
qtext = render_template(col, format, fields)
|
|
||||||
|
|
||||||
# answer
|
(qnodes, anodes) = col.backend.render_card(qfmt, afmt, fields, card_ord)
|
||||||
format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (card_ord + 1), afmt)
|
|
||||||
format = format.replace("<%cloze:", "<%%ca:%d:" % (card_ord + 1))
|
qtext = apply_custom_filters(qnodes, fields, front_side=None)
|
||||||
fields["FrontSide"] = stripSounds(qtext)
|
atext = apply_custom_filters(anodes, fields, front_side=qtext)
|
||||||
atext = render_template(col, format, fields)
|
|
||||||
|
|
||||||
return qtext, atext
|
return qtext, atext
|
||||||
|
|
||||||
|
|
||||||
def apply_custom_filters(
|
def apply_custom_filters(
|
||||||
rendered: List[Union[str, TemplateReplacement]], fields: Dict[str, str]
|
rendered: TemplateReplacementList, fields: Dict[str, str], front_side: Optional[str]
|
||||||
) -> str:
|
) -> str:
|
||||||
"Complete rendering by applying any pending custom filters."
|
"Complete rendering by applying any pending custom filters."
|
||||||
|
# template already fully rendered?
|
||||||
|
if len(rendered) == 1 and isinstance(rendered[0], str):
|
||||||
|
return rendered[0]
|
||||||
|
|
||||||
res = ""
|
res = ""
|
||||||
for node in rendered:
|
for node in rendered:
|
||||||
if isinstance(node, str):
|
if isinstance(node, str):
|
||||||
res += node
|
res += node
|
||||||
else:
|
else:
|
||||||
|
# do we need to inject in FrontSide?
|
||||||
|
if node.field_name == "FrontSide" and front_side is not None:
|
||||||
|
node.current_text = front_side
|
||||||
|
|
||||||
res += apply_field_filters(
|
res += apply_field_filters(
|
||||||
node.field_name, node.current_text, fields, node.filters
|
node.field_name, node.current_text, fields, node.filters
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
# Filters
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
|
|
||||||
def apply_field_filters(
|
def apply_field_filters(
|
||||||
field_name: str, field_text: str, fields: Dict[str, str], filters: List[str]
|
field_name: str, field_text: str, fields: Dict[str, str], filters: List[str]
|
||||||
) -> str:
|
) -> str:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from anki.template_legacy import _removeFormattingFromMathjax
|
from anki.template_legacy import _removeFormattingFromMathjax
|
||||||
|
from tests.shared import getEmptyCol
|
||||||
|
|
||||||
|
|
||||||
def test_remove_formatting_from_mathjax():
|
def test_remove_formatting_from_mathjax():
|
||||||
|
@ -14,3 +15,17 @@ def test_remove_formatting_from_mathjax():
|
||||||
|
|
||||||
txt = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
|
txt = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
|
||||||
assert _removeFormattingFromMathjax(txt, 1) == (r"\(a\) {{c1::b}} \[ {{C1::c}} \]")
|
assert _removeFormattingFromMathjax(txt, 1) == (r"\(a\) {{c1::b}} \[ {{C1::c}} \]")
|
||||||
|
|
||||||
|
|
||||||
|
def test_deferred_frontside():
|
||||||
|
d = getEmptyCol()
|
||||||
|
m = d.models.current()
|
||||||
|
m["tmpls"][0]["qfmt"] = "{{custom:Front}}"
|
||||||
|
d.models.save(m)
|
||||||
|
|
||||||
|
f = d.newNote()
|
||||||
|
f["Front"] = "xxtest"
|
||||||
|
f["Back"] = ""
|
||||||
|
d.addNote(f)
|
||||||
|
|
||||||
|
assert "xxtest" in f.cards()[0].a()
|
||||||
|
|
|
@ -7,7 +7,8 @@ use crate::backend_proto::RenderedTemplateReplacement;
|
||||||
use crate::err::{AnkiError, Result};
|
use crate::err::{AnkiError, Result};
|
||||||
use crate::sched::sched_timing_today;
|
use crate::sched::sched_timing_today;
|
||||||
use crate::template::{
|
use crate::template::{
|
||||||
without_legacy_template_directives, FieldMap, FieldRequirements, ParsedTemplate, RenderedNode,
|
render_card, without_legacy_template_directives, FieldMap, FieldRequirements, ParsedTemplate,
|
||||||
|
RenderedNode,
|
||||||
};
|
};
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
@ -96,7 +97,7 @@ impl Backend {
|
||||||
Value::DeckTree(_) => todo!(),
|
Value::DeckTree(_) => todo!(),
|
||||||
Value::FindCards(_) => todo!(),
|
Value::FindCards(_) => todo!(),
|
||||||
Value::BrowserRows(_) => todo!(),
|
Value::BrowserRows(_) => todo!(),
|
||||||
Value::RenderTemplate(input) => OValue::RenderTemplate(self.render_template(input)?),
|
Value::RenderCard(input) => OValue::RenderCard(self.render_template(input)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,26 +161,26 @@ impl Backend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_template(&self, input: pt::RenderTemplateIn) -> Result<pt::RenderTemplateOut> {
|
fn render_template(&self, input: pt::RenderCardIn) -> pt::RenderCardOut {
|
||||||
|
// convert string map to &str
|
||||||
let fields: HashMap<_, _> = input
|
let fields: HashMap<_, _> = input
|
||||||
.fields
|
.fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| (k.as_ref(), v.as_ref()))
|
.map(|(k, v)| (k.as_ref(), v.as_ref()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let normalized = without_legacy_template_directives(&input.template_text);
|
// render
|
||||||
match ParsedTemplate::from_text(normalized.as_ref()) {
|
let (qnodes, anodes) = render_card(
|
||||||
Ok(tmpl) => {
|
&input.question_template,
|
||||||
let nodes = tmpl.render(&fields);
|
&input.answer_template,
|
||||||
let out_nodes = nodes
|
&fields,
|
||||||
.into_iter()
|
input.card_ordinal as u16,
|
||||||
.map(|n| pt::RenderedTemplateNode {
|
);
|
||||||
value: Some(rendered_node_to_proto(n)),
|
|
||||||
})
|
// return
|
||||||
.collect();
|
pt::RenderCardOut {
|
||||||
Ok(pt::RenderTemplateOut { nodes: out_nodes })
|
question_nodes: rendered_nodes_to_proto(qnodes),
|
||||||
}
|
answer_nodes: rendered_nodes_to_proto(anodes),
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,6 +189,15 @@ fn ords_hash_to_set(ords: HashSet<u16>) -> Vec<u32> {
|
||||||
ords.iter().map(|ord| *ord as u32).collect()
|
ords.iter().map(|ord| *ord as u32).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rendered_nodes_to_proto(nodes: Vec<RenderedNode>) -> Vec<pt::RenderedTemplateNode> {
|
||||||
|
nodes
|
||||||
|
.into_iter()
|
||||||
|
.map(|n| pt::RenderedTemplateNode {
|
||||||
|
value: Some(rendered_node_to_proto(n)),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn rendered_node_to_proto(node: RenderedNode) -> pt::rendered_template_node::Value {
|
fn rendered_node_to_proto(node: RenderedNode) -> pt::rendered_template_node::Value {
|
||||||
match node {
|
match node {
|
||||||
RenderedNode::Text { text } => pt::rendered_template_node::Value::Text(text),
|
RenderedNode::Text { text } => pt::rendered_template_node::Value::Text(text),
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
use crate::err::{AnkiError, Result};
|
use crate::err::{AnkiError, Result};
|
||||||
use crate::template_filters::apply_filters;
|
use crate::template_filters::apply_filters;
|
||||||
|
use crate::text::strip_sounds;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use nom;
|
use nom;
|
||||||
use nom::branch::alt;
|
use nom::branch::alt;
|
||||||
|
@ -126,21 +127,6 @@ enum ParsedNode<'a> {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ParsedTemplate<'a>(Vec<ParsedNode<'a>>);
|
pub struct ParsedTemplate<'a>(Vec<ParsedNode<'a>>);
|
||||||
|
|
||||||
static ALT_HANDLEBAR_DIRECTIVE: &str = "{{=<% %>=}}";
|
|
||||||
|
|
||||||
/// Convert legacy alternate syntax to standard syntax.
|
|
||||||
pub fn without_legacy_template_directives(text: &str) -> Cow<str> {
|
|
||||||
if text.trim_start().starts_with(ALT_HANDLEBAR_DIRECTIVE) {
|
|
||||||
text.trim_start()
|
|
||||||
.trim_start_matches(ALT_HANDLEBAR_DIRECTIVE)
|
|
||||||
.replace("<%", "{{")
|
|
||||||
.replace("%>", "}}")
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
text.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParsedTemplate<'_> {
|
impl ParsedTemplate<'_> {
|
||||||
/// Create a template from the provided text.
|
/// Create a template from the provided text.
|
||||||
///
|
///
|
||||||
|
@ -199,6 +185,24 @@ fn parse_inner<'a, I: Iterator<Item = Result<Token<'a>>>>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy support
|
||||||
|
//----------------------------------------
|
||||||
|
|
||||||
|
static ALT_HANDLEBAR_DIRECTIVE: &str = "{{=<% %>=}}";
|
||||||
|
|
||||||
|
/// Convert legacy alternate syntax to standard syntax.
|
||||||
|
pub fn without_legacy_template_directives(text: &str) -> Cow<str> {
|
||||||
|
if text.trim_start().starts_with(ALT_HANDLEBAR_DIRECTIVE) {
|
||||||
|
text.trim_start()
|
||||||
|
.trim_start_matches(ALT_HANDLEBAR_DIRECTIVE)
|
||||||
|
.replace("<%", "{{")
|
||||||
|
.replace("%>", "}}")
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
text.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Checking if template is empty
|
// Checking if template is empty
|
||||||
//----------------------------------------
|
//----------------------------------------
|
||||||
|
|
||||||
|
@ -260,17 +264,24 @@ pub enum RenderedNode {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct RenderContext<'a> {
|
||||||
|
pub fields: &'a HashMap<&'a str, &'a str>,
|
||||||
|
pub nonempty_fields: &'a HashSet<&'a str>,
|
||||||
|
pub question_side: bool,
|
||||||
|
pub card_ord: u16,
|
||||||
|
pub front_text: Option<Cow<'a, str>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl ParsedTemplate<'_> {
|
impl ParsedTemplate<'_> {
|
||||||
/// Render the template with the provided fields.
|
/// Render the template with the provided fields.
|
||||||
///
|
///
|
||||||
/// Replacements that use only standard filters will become part of
|
/// Replacements that use only standard filters will become part of
|
||||||
/// a text node. If a non-standard filter is encountered, a partially
|
/// a text node. If a non-standard filter is encountered, a partially
|
||||||
/// rendered Replacement is returned for the calling code to complete.
|
/// rendered Replacement is returned for the calling code to complete.
|
||||||
pub fn render(&self, fields: &HashMap<&str, &str>) -> Vec<RenderedNode> {
|
fn render(&self, context: &RenderContext) -> Vec<RenderedNode> {
|
||||||
let mut rendered = vec![];
|
let mut rendered = vec![];
|
||||||
let nonempty = nonempty_fields(fields);
|
|
||||||
|
|
||||||
render_into(&mut rendered, self.0.as_ref(), fields, &nonempty);
|
render_into(&mut rendered, self.0.as_ref(), context);
|
||||||
|
|
||||||
rendered
|
rendered
|
||||||
}
|
}
|
||||||
|
@ -279,8 +290,7 @@ impl ParsedTemplate<'_> {
|
||||||
fn render_into(
|
fn render_into(
|
||||||
rendered_nodes: &mut Vec<RenderedNode>,
|
rendered_nodes: &mut Vec<RenderedNode>,
|
||||||
nodes: &[ParsedNode],
|
nodes: &[ParsedNode],
|
||||||
fields: &HashMap<&str, &str>,
|
context: &RenderContext,
|
||||||
nonempty: &HashSet<&str>,
|
|
||||||
) {
|
) {
|
||||||
use ParsedNode::*;
|
use ParsedNode::*;
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
|
@ -288,9 +298,28 @@ fn render_into(
|
||||||
Text(text) => {
|
Text(text) => {
|
||||||
append_str_to_nodes(rendered_nodes, text);
|
append_str_to_nodes(rendered_nodes, text);
|
||||||
}
|
}
|
||||||
|
Replacement {
|
||||||
|
key: key @ "FrontSide",
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if let Some(front_side) = &context.front_text {
|
||||||
|
// a fully rendered front side is available, so we can
|
||||||
|
// bake it into the output
|
||||||
|
append_str_to_nodes(rendered_nodes, front_side.as_ref());
|
||||||
|
} else {
|
||||||
|
// the front side contains unknown filters, and must
|
||||||
|
// be completed by the Python code
|
||||||
|
rendered_nodes.push(RenderedNode::Replacement {
|
||||||
|
field_name: (*key).to_string(),
|
||||||
|
filters: vec![],
|
||||||
|
current_text: "".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Replacement { key, filters } => {
|
Replacement { key, filters } => {
|
||||||
let (text, remaining_filters) = match fields.get(key) {
|
// apply built in filters if field exists
|
||||||
Some(text) => apply_filters(text, filters, key),
|
let (text, remaining_filters) = match context.fields.get(key) {
|
||||||
|
Some(text) => apply_filters(text, filters, key, context),
|
||||||
None => (unknown_field_message(key, filters).into(), vec![]),
|
None => (unknown_field_message(key, filters).into(), vec![]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -306,13 +335,13 @@ fn render_into(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Conditional { key, children } => {
|
Conditional { key, children } => {
|
||||||
if nonempty.contains(key) {
|
if context.nonempty_fields.contains(key) {
|
||||||
render_into(rendered_nodes, children.as_ref(), fields, nonempty);
|
render_into(rendered_nodes, children.as_ref(), context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NegatedConditional { key, children } => {
|
NegatedConditional { key, children } => {
|
||||||
if !nonempty.contains(key) {
|
if !context.nonempty_fields.contains(key) {
|
||||||
render_into(rendered_nodes, children.as_ref(), fields, nonempty);
|
render_into(rendered_nodes, children.as_ref(), context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -378,6 +407,53 @@ fn unknown_field_message(field_name: &str, filters: &[&str]) -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rendering both sides
|
||||||
|
//----------------------------------------
|
||||||
|
|
||||||
|
#[allow(clippy::implicit_hasher)]
|
||||||
|
pub fn render_card(
|
||||||
|
qfmt: &str,
|
||||||
|
afmt: &str,
|
||||||
|
field_map: &HashMap<&str, &str>,
|
||||||
|
card_ord: u16,
|
||||||
|
) -> (Vec<RenderedNode>, Vec<RenderedNode>) {
|
||||||
|
// prepare context
|
||||||
|
let mut context = RenderContext {
|
||||||
|
fields: field_map,
|
||||||
|
nonempty_fields: &nonempty_fields(field_map),
|
||||||
|
question_side: true,
|
||||||
|
card_ord,
|
||||||
|
front_text: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// question side
|
||||||
|
let qnorm = without_legacy_template_directives(qfmt);
|
||||||
|
let qnodes = match ParsedTemplate::from_text(qnorm.as_ref()) {
|
||||||
|
Ok(tmpl) => tmpl.render(&context),
|
||||||
|
Err(e) => vec![RenderedNode::Text {
|
||||||
|
text: format!("{:?}", e),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
// if the question side didn't have any unknown filters, we can pass
|
||||||
|
// FrontSide in now
|
||||||
|
if let [RenderedNode::Text { ref text }] = *qnodes.as_slice() {
|
||||||
|
context.front_text = Some(strip_sounds(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// answer side
|
||||||
|
context.question_side = false;
|
||||||
|
let anorm = without_legacy_template_directives(afmt);
|
||||||
|
let anodes = match ParsedTemplate::from_text(anorm.as_ref()) {
|
||||||
|
Ok(tmpl) => tmpl.render(&context),
|
||||||
|
Err(e) => vec![RenderedNode::Text {
|
||||||
|
text: format!("{:?}", e),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
(qnodes, anodes)
|
||||||
|
}
|
||||||
|
|
||||||
// Field requirements
|
// Field requirements
|
||||||
//----------------------------------------
|
//----------------------------------------
|
||||||
|
|
||||||
|
@ -437,7 +513,11 @@ impl ParsedTemplate<'_> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{FieldMap, ParsedNode::*, ParsedTemplate as PT};
|
use super::{FieldMap, ParsedNode::*, ParsedTemplate as PT};
|
||||||
use crate::template::{field_is_empty, without_legacy_template_directives, FieldRequirements};
|
use crate::template::{
|
||||||
|
field_is_empty, nonempty_fields, render_card, without_legacy_template_directives,
|
||||||
|
FieldRequirements, RenderContext, RenderedNode,
|
||||||
|
};
|
||||||
|
use crate::text::strip_html;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::iter::FromIterator;
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
@ -568,15 +648,23 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_render() {
|
fn test_render_single() {
|
||||||
let map: HashMap<_, _> = vec![("F", "f"), ("B", "b"), ("E", " ")]
|
let map: HashMap<_, _> = vec![("F", "f"), ("B", "b"), ("E", " ")]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let ctx = RenderContext {
|
||||||
|
fields: &map,
|
||||||
|
nonempty_fields: &nonempty_fields(&map),
|
||||||
|
question_side: true,
|
||||||
|
card_ord: 1,
|
||||||
|
front_text: None,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::template::RenderedNode as FN;
|
use crate::template::RenderedNode as FN;
|
||||||
let mut tmpl = PT::from_text("{{B}}A{{F}}").unwrap();
|
let mut tmpl = PT::from_text("{{B}}A{{F}}").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tmpl.render(&map),
|
tmpl.render(&ctx),
|
||||||
vec![FN::Text {
|
vec![FN::Text {
|
||||||
text: "bAf".to_owned()
|
text: "bAf".to_owned()
|
||||||
},]
|
},]
|
||||||
|
@ -584,12 +672,12 @@ mod test {
|
||||||
|
|
||||||
// empty
|
// empty
|
||||||
tmpl = PT::from_text("{{#E}}A{{/E}}").unwrap();
|
tmpl = PT::from_text("{{#E}}A{{/E}}").unwrap();
|
||||||
assert_eq!(tmpl.render(&map), vec![]);
|
assert_eq!(tmpl.render(&ctx), vec![]);
|
||||||
|
|
||||||
// missing
|
// missing
|
||||||
tmpl = PT::from_text("{{^M}}A{{/M}}").unwrap();
|
tmpl = PT::from_text("{{^M}}A{{/M}}").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tmpl.render(&map),
|
tmpl.render(&ctx),
|
||||||
vec![FN::Text {
|
vec![FN::Text {
|
||||||
text: "A".to_owned()
|
text: "A".to_owned()
|
||||||
},]
|
},]
|
||||||
|
@ -598,7 +686,7 @@ mod test {
|
||||||
// nested
|
// nested
|
||||||
tmpl = PT::from_text("{{^E}}1{{#F}}2{{#B}}{{F}}{{/B}}{{/F}}{{/E}}").unwrap();
|
tmpl = PT::from_text("{{^E}}1{{#F}}2{{#B}}{{F}}{{/B}}{{/F}}{{/E}}").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tmpl.render(&map),
|
tmpl.render(&ctx),
|
||||||
vec![FN::Text {
|
vec![FN::Text {
|
||||||
text: "12f".to_owned()
|
text: "12f".to_owned()
|
||||||
},]
|
},]
|
||||||
|
@ -607,7 +695,7 @@ mod test {
|
||||||
// unknown filters
|
// unknown filters
|
||||||
tmpl = PT::from_text("{{one:two:B}}").unwrap();
|
tmpl = PT::from_text("{{one:two:B}}").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tmpl.render(&map),
|
tmpl.render(&ctx),
|
||||||
vec![FN::Replacement {
|
vec![FN::Replacement {
|
||||||
field_name: "B".to_owned(),
|
field_name: "B".to_owned(),
|
||||||
filters: vec!["two".to_string(), "one".to_string()],
|
filters: vec!["two".to_string(), "one".to_string()],
|
||||||
|
@ -616,9 +704,10 @@ mod test {
|
||||||
);
|
);
|
||||||
|
|
||||||
// partially unknown filters
|
// partially unknown filters
|
||||||
tmpl = PT::from_text("{{one:text:B}}").unwrap();
|
// excess colons are ignored
|
||||||
|
tmpl = PT::from_text("{{one::text:B}}").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tmpl.render(&map),
|
tmpl.render(&ctx),
|
||||||
vec![FN::Replacement {
|
vec![FN::Replacement {
|
||||||
field_name: "B".to_owned(),
|
field_name: "B".to_owned(),
|
||||||
filters: vec!["one".to_string()],
|
filters: vec!["one".to_string()],
|
||||||
|
@ -629,7 +718,7 @@ mod test {
|
||||||
// known filter
|
// known filter
|
||||||
tmpl = PT::from_text("{{text:B}}").unwrap();
|
tmpl = PT::from_text("{{text:B}}").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tmpl.render(&map),
|
tmpl.render(&ctx),
|
||||||
vec![FN::Text {
|
vec![FN::Text {
|
||||||
text: "b".to_owned()
|
text: "b".to_owned()
|
||||||
}]
|
}]
|
||||||
|
@ -638,7 +727,7 @@ mod test {
|
||||||
// unknown field
|
// unknown field
|
||||||
tmpl = PT::from_text("{{X}}").unwrap();
|
tmpl = PT::from_text("{{X}}").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tmpl.render(&map),
|
tmpl.render(&ctx),
|
||||||
vec![FN::Text {
|
vec![FN::Text {
|
||||||
text: "{unknown field X}".to_owned()
|
text: "{unknown field X}".to_owned()
|
||||||
}]
|
}]
|
||||||
|
@ -647,10 +736,44 @@ mod test {
|
||||||
// unknown field with filters
|
// unknown field with filters
|
||||||
tmpl = PT::from_text("{{foo:text:X}}").unwrap();
|
tmpl = PT::from_text("{{foo:text:X}}").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tmpl.render(&map),
|
tmpl.render(&ctx),
|
||||||
vec![FN::Text {
|
vec![FN::Text {
|
||||||
text: "{unknown field foo:text:X}".to_owned()
|
text: "{unknown field foo:text:X}".to_owned()
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_complete_template(nodes: &Vec<RenderedNode>) -> Option<&str> {
|
||||||
|
if let [RenderedNode::Text { ref text }] = nodes.as_slice() {
|
||||||
|
Some(text.as_str())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_render_full() {
|
||||||
|
// make sure front and back side renders cloze differently
|
||||||
|
let fmt = "{{cloze:Text}}";
|
||||||
|
let clozed_text = "{{c1::one}} {{c2::two::hint}}";
|
||||||
|
let map: HashMap<_, _> = vec![("Text", clozed_text)].into_iter().collect();
|
||||||
|
|
||||||
|
let (qnodes, anodes) = render_card(fmt, fmt, &map, 0);
|
||||||
|
assert_eq!(
|
||||||
|
strip_html(get_complete_template(&qnodes).unwrap()),
|
||||||
|
"[...] two"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
strip_html(get_complete_template(&anodes).unwrap()),
|
||||||
|
"one two"
|
||||||
|
);
|
||||||
|
|
||||||
|
// FrontSide should render if only standard modifiers were used
|
||||||
|
let (_qnodes, anodes) = render_card("{{kana:text:Text}}", "{{FrontSide}}", &map, 1);
|
||||||
|
assert_eq!(get_complete_template(&anodes).unwrap(), clozed_text);
|
||||||
|
|
||||||
|
// But if a custom modifier was used, it's deferred to the Python code
|
||||||
|
let (_qnodes, anodes) = render_card("{{custom:Text}}", "{{FrontSide}}", &map, 1);
|
||||||
|
assert_eq!(get_complete_template(&anodes).is_none(), true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::template::RenderContext;
|
||||||
use crate::text::strip_html;
|
use crate::text::strip_html;
|
||||||
use blake3::Hasher;
|
use blake3::Hasher;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
@ -18,6 +19,7 @@ pub(crate) fn apply_filters<'a>(
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
filters: &[&str],
|
filters: &[&str],
|
||||||
field_name: &str,
|
field_name: &str,
|
||||||
|
context: &RenderContext,
|
||||||
) -> (Cow<'a, str>, Vec<String>) {
|
) -> (Cow<'a, str>, Vec<String>) {
|
||||||
let mut text: Cow<str> = text.into();
|
let mut text: Cow<str> = text.into();
|
||||||
|
|
||||||
|
@ -29,7 +31,7 @@ pub(crate) fn apply_filters<'a>(
|
||||||
};
|
};
|
||||||
|
|
||||||
for (idx, &filter_name) in filters.iter().enumerate() {
|
for (idx, &filter_name) in filters.iter().enumerate() {
|
||||||
match apply_filter(filter_name, text.as_ref(), field_name) {
|
match apply_filter(filter_name, text.as_ref(), field_name, context) {
|
||||||
(true, None) => {
|
(true, None) => {
|
||||||
// filter did not change text
|
// filter did not change text
|
||||||
}
|
}
|
||||||
|
@ -55,24 +57,26 @@ pub(crate) fn apply_filters<'a>(
|
||||||
///
|
///
|
||||||
/// Returns true if filter was valid.
|
/// Returns true if filter was valid.
|
||||||
/// Returns string if input text changed.
|
/// Returns string if input text changed.
|
||||||
fn apply_filter<'a>(filter_name: &str, text: &'a str, field_name: &str) -> (bool, Option<String>) {
|
fn apply_filter<'a>(
|
||||||
|
filter_name: &str,
|
||||||
|
text: &'a str,
|
||||||
|
field_name: &str,
|
||||||
|
context: &RenderContext,
|
||||||
|
) -> (bool, Option<String>) {
|
||||||
let output_text = match filter_name {
|
let output_text = match filter_name {
|
||||||
"text" => strip_html(text),
|
"text" => strip_html(text),
|
||||||
"furigana" => furigana_filter(text),
|
"furigana" => furigana_filter(text),
|
||||||
"kanji" => kanji_filter(text),
|
"kanji" => kanji_filter(text),
|
||||||
"kana" => kana_filter(text),
|
"kana" => kana_filter(text),
|
||||||
other => {
|
"type" => type_filter(field_name),
|
||||||
let split: Vec<_> = other.splitn(2, '-').collect();
|
"type-cloze" => type_cloze_filter(field_name),
|
||||||
let base = split[0];
|
|
||||||
let filter_args = *split.get(1).unwrap_or(&"");
|
|
||||||
match base {
|
|
||||||
"type" => type_filter(text, filter_args, field_name),
|
|
||||||
"hint" => hint_filter(text, field_name),
|
"hint" => hint_filter(text, field_name),
|
||||||
"cq" => cloze_filter(text, filter_args, true),
|
"cloze" => cloze_filter(text, context),
|
||||||
"ca" => cloze_filter(text, filter_args, false),
|
// an empty filter name (caused by using two colons) is ignored
|
||||||
|
"" => text.into(),
|
||||||
|
_ => {
|
||||||
// unrecognized filter
|
// unrecognized filter
|
||||||
_ => return (false, None),
|
return (false, None);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -126,7 +130,7 @@ mod mathjax_caps {
|
||||||
pub const CLOSING_TAG: usize = 3;
|
pub const CLOSING_TAG: usize = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reveal_cloze_text(text: &str, ord: u16, question: bool) -> Cow<str> {
|
fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow<str> {
|
||||||
let output = CLOZE.replace_all(text, |caps: &Captures| {
|
let output = CLOZE.replace_all(text, |caps: &Captures| {
|
||||||
let captured_ord = caps
|
let captured_ord = caps
|
||||||
.get(cloze_caps::ORD)
|
.get(cloze_caps::ORD)
|
||||||
|
@ -135,7 +139,7 @@ fn reveal_cloze_text(text: &str, ord: u16, question: bool) -> Cow<str> {
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
if captured_ord != ord {
|
if captured_ord != cloze_ord {
|
||||||
// other cloze deletions are unchanged
|
// other cloze deletions are unchanged
|
||||||
return caps.get(cloze_caps::TEXT).unwrap().as_str().to_owned();
|
return caps.get(cloze_caps::TEXT).unwrap().as_str().to_owned();
|
||||||
}
|
}
|
||||||
|
@ -173,9 +177,10 @@ fn strip_html_inside_mathjax(text: &str) -> Cow<str> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cloze_filter<'a>(text: &'a str, filter_args: &str, question: bool) -> Cow<'a, str> {
|
fn cloze_filter<'a>(text: &'a str, context: &RenderContext) -> Cow<'a, str> {
|
||||||
let cloze_ord = filter_args.parse().unwrap_or(0);
|
strip_html_inside_mathjax(
|
||||||
strip_html_inside_mathjax(reveal_cloze_text(text, cloze_ord, question).as_ref())
|
reveal_cloze_text(text, context.card_ord + 1, context.question_side).as_ref(),
|
||||||
|
)
|
||||||
.into_owned()
|
.into_owned()
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
@ -239,16 +244,14 @@ fn furigana_filter(text: &str) -> Cow<str> {
|
||||||
//----------------------------------------
|
//----------------------------------------
|
||||||
|
|
||||||
/// convert to [[type:...]] for the gui code to process
|
/// convert to [[type:...]] for the gui code to process
|
||||||
fn type_filter<'a>(_text: &'a str, filter_args: &str, field_name: &str) -> Cow<'a, str> {
|
fn type_filter<'a>(field_name: &str) -> Cow<'a, str> {
|
||||||
if filter_args.is_empty() {
|
format!("[[type:{}]]", field_name).into()
|
||||||
format!("[[type:{}]]", field_name)
|
}
|
||||||
} else {
|
|
||||||
format!("[[type:{}:{}]]", filter_args, field_name)
|
fn type_cloze_filter<'a>(field_name: &str) -> Cow<'a, str> {
|
||||||
}
|
format!("[[type:cloze:{}]]", field_name).into()
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixme: i18n
|
|
||||||
fn hint_filter<'a>(text: &'a str, field_name: &str) -> Cow<'a, str> {
|
fn hint_filter<'a>(text: &'a str, field_name: &str) -> Cow<'a, str> {
|
||||||
if text.trim().is_empty() {
|
if text.trim().is_empty() {
|
||||||
return text.into();
|
return text.into();
|
||||||
|
@ -267,9 +270,9 @@ onclick="this.style.display='none';
|
||||||
document.getElementById('hint{}').style.display='block';
|
document.getElementById('hint{}').style.display='block';
|
||||||
return false;">
|
return false;">
|
||||||
{}</a>
|
{}</a>
|
||||||
<div id="hint{}" class=hint style="display: none">Show {}</div>
|
<div id="hint{}" class=hint style="display: none">{}</div>
|
||||||
"##,
|
"##,
|
||||||
id, text, id, field_name
|
id, field_name, id, text
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
@ -279,9 +282,10 @@ return false;">
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use crate::template::RenderContext;
|
||||||
use crate::template_filters::{
|
use crate::template_filters::{
|
||||||
apply_filters, cloze_filter, furigana_filter, hint_filter, kana_filter, kanji_filter,
|
apply_filters, cloze_filter, furigana_filter, hint_filter, kana_filter, kanji_filter,
|
||||||
type_filter,
|
type_cloze_filter, type_filter,
|
||||||
};
|
};
|
||||||
use crate::text::strip_html;
|
use crate::text::strip_html;
|
||||||
|
|
||||||
|
@ -305,21 +309,25 @@ mod test {
|
||||||
onclick="this.style.display='none';
|
onclick="this.style.display='none';
|
||||||
document.getElementById('hint83fe48607f0f3a66').style.display='block';
|
document.getElementById('hint83fe48607f0f3a66').style.display='block';
|
||||||
return false;">
|
return false;">
|
||||||
foo</a>
|
field</a>
|
||||||
<div id="hint83fe48607f0f3a66" class=hint style="display: none">Show field</div>
|
<div id="hint83fe48607f0f3a66" class=hint style="display: none">foo</div>
|
||||||
"##
|
"##
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_type() {
|
fn test_type() {
|
||||||
assert_eq!(type_filter("ignored", "", "Front"), "[[type:Front]]");
|
assert_eq!(type_filter("Front"), "[[type:Front]]");
|
||||||
|
assert_eq!(type_cloze_filter("Front"), "[[type:cloze:Front]]");
|
||||||
|
let ctx = RenderContext {
|
||||||
|
fields: &Default::default(),
|
||||||
|
nonempty_fields: &Default::default(),
|
||||||
|
question_side: false,
|
||||||
|
card_ord: 0,
|
||||||
|
front_text: None,
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
type_filter("ignored", "cloze", "Front"),
|
apply_filters("ignored", &["cloze", "type"], "Text", &ctx),
|
||||||
"[[type:cloze:Front]]"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
apply_filters("ignored", &["cloze", "type"], "Text"),
|
|
||||||
("[[type:cloze:Text]]".into(), vec![])
|
("[[type:cloze:Text]]".into(), vec![])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -327,25 +335,23 @@ foo</a>
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cloze() {
|
fn test_cloze() {
|
||||||
let text = "{{c1::one}} {{c2::two::hint}}";
|
let text = "{{c1::one}} {{c2::two::hint}}";
|
||||||
|
let mut ctx = RenderContext {
|
||||||
|
fields: &Default::default(),
|
||||||
|
nonempty_fields: &Default::default(),
|
||||||
|
question_side: true,
|
||||||
|
card_ord: 0,
|
||||||
|
front_text: None,
|
||||||
|
};
|
||||||
|
assert_eq!(strip_html(&cloze_filter(text, &ctx)).as_ref(), "[...] two");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
strip_html(&cloze_filter(text, "1", true)).as_ref(),
|
cloze_filter(text, &ctx),
|
||||||
"[...] two"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
strip_html(&cloze_filter(text, "2", true)).as_ref(),
|
|
||||||
"one [hint]"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
strip_html(&cloze_filter(text, "1", false)).as_ref(),
|
|
||||||
"one two"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
cloze_filter(text, "1", false),
|
|
||||||
"<span class=cloze>one</span> two"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
cloze_filter(text, "1", true),
|
|
||||||
"<span class=cloze>[...]</span> two"
|
"<span class=cloze>[...]</span> two"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ctx.card_ord = 1;
|
||||||
|
assert_eq!(strip_html(&cloze_filter(text, &ctx)).as_ref(), "one [hint]");
|
||||||
|
|
||||||
|
ctx.question_side = false;
|
||||||
|
assert_eq!(strip_html(&cloze_filter(text, &ctx)).as_ref(), "one two");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue