mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
Rework RenderCardOutput::question/answer
Instead of flattening the output (which was missing FrontSide), alter the behaviour of render() instead. The non-partial output is now exposed via Protobuf, so the non-Python clients can take advantage of it.
This commit is contained in:
parent
84609cc505
commit
dc56a2ca7d
9 changed files with 213 additions and 117 deletions
|
@ -93,6 +93,10 @@ message EmptyCardsReport {
|
||||||
message RenderExistingCardRequest {
|
message RenderExistingCardRequest {
|
||||||
int64 card_id = 1;
|
int64 card_id = 1;
|
||||||
bool browser = 2;
|
bool browser = 2;
|
||||||
|
// If true, rendering will stop when an unknown filter is encountered,
|
||||||
|
// and caller will need to complete rendering. This is done to allow
|
||||||
|
// Python code to modify the rendering.
|
||||||
|
bool partial_render = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RenderUncommittedCardRequest {
|
message RenderUncommittedCardRequest {
|
||||||
|
@ -100,6 +104,10 @@ message RenderUncommittedCardRequest {
|
||||||
uint32 card_ord = 2;
|
uint32 card_ord = 2;
|
||||||
notetypes.Notetype.Template template = 3;
|
notetypes.Notetype.Template template = 3;
|
||||||
bool fill_empty = 4;
|
bool fill_empty = 4;
|
||||||
|
// If true, rendering will stop when an unknown filter is encountered,
|
||||||
|
// and caller will need to complete rendering. This is done to allow
|
||||||
|
// Python code to modify the rendering.
|
||||||
|
bool partial_render = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RenderUncommittedCardLegacyRequest {
|
message RenderUncommittedCardLegacyRequest {
|
||||||
|
@ -107,6 +115,10 @@ message RenderUncommittedCardLegacyRequest {
|
||||||
uint32 card_ord = 2;
|
uint32 card_ord = 2;
|
||||||
bytes template = 3;
|
bytes template = 3;
|
||||||
bool fill_empty = 4;
|
bool fill_empty = 4;
|
||||||
|
// If true, rendering will stop when an unknown filter is encountered,
|
||||||
|
// and caller will need to complete rendering. This is done to allow
|
||||||
|
// Python code to modify the rendering.
|
||||||
|
bool partial_render = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RenderCardResponse {
|
message RenderCardResponse {
|
||||||
|
|
|
@ -262,6 +262,7 @@ class TemplateRenderContext:
|
||||||
card_ord=self._card.ord,
|
card_ord=self._card.ord,
|
||||||
template=to_json_bytes(self._template),
|
template=to_json_bytes(self._template),
|
||||||
fill_empty=self._fill_empty,
|
fill_empty=self._fill_empty,
|
||||||
|
partial_render=True,
|
||||||
)
|
)
|
||||||
# when rendering card layout, the css changes have not been
|
# when rendering card layout, the css changes have not been
|
||||||
# committed; we need the current notetype instance instead
|
# committed; we need the current notetype instance instead
|
||||||
|
@ -269,7 +270,7 @@ class TemplateRenderContext:
|
||||||
else:
|
else:
|
||||||
# existing card (eg study mode)
|
# existing card (eg study mode)
|
||||||
out = self._col._backend.render_existing_card(
|
out = self._col._backend.render_existing_card(
|
||||||
card_id=self._card.id, browser=self._browser
|
card_id=self._card.id, browser=self._browser, partial_render=True
|
||||||
)
|
)
|
||||||
return PartiallyRenderedCard.from_proto(out)
|
return PartiallyRenderedCard.from_proto(out)
|
||||||
|
|
||||||
|
|
|
@ -257,7 +257,7 @@ impl RenderContext {
|
||||||
fn new(col: &mut Collection, card: &Card, note: &Note, notetype: &Notetype) -> Self {
|
fn new(col: &mut Collection, card: &Card, note: &Note, notetype: &Notetype) -> Self {
|
||||||
match notetype
|
match notetype
|
||||||
.get_template(card.template_idx)
|
.get_template(card.template_idx)
|
||||||
.and_then(|template| col.render_card(note, card, notetype, template, true))
|
.and_then(|template| col.render_card(note, card, notetype, template, true, true))
|
||||||
{
|
{
|
||||||
Ok(render) => RenderContext::Ok {
|
Ok(render) => RenderContext::Ok {
|
||||||
question: rendered_nodes_to_str(&render.qnodes),
|
question: rendered_nodes_to_str(&render.qnodes),
|
||||||
|
|
|
@ -90,7 +90,7 @@ impl crate::services::CardRenderingService for Collection {
|
||||||
&mut self,
|
&mut self,
|
||||||
input: anki_proto::card_rendering::RenderExistingCardRequest,
|
input: anki_proto::card_rendering::RenderExistingCardRequest,
|
||||||
) -> Result<anki_proto::card_rendering::RenderCardResponse> {
|
) -> Result<anki_proto::card_rendering::RenderCardResponse> {
|
||||||
self.render_existing_card(CardId(input.card_id), input.browser)
|
self.render_existing_card(CardId(input.card_id), input.browser, input.partial_render)
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ impl crate::services::CardRenderingService for Collection {
|
||||||
let ord = input.card_ord as u16;
|
let ord = input.card_ord as u16;
|
||||||
let fill_empty = input.fill_empty;
|
let fill_empty = input.fill_empty;
|
||||||
|
|
||||||
self.render_uncommitted_card(&mut note, &template, ord, fill_empty)
|
self.render_uncommitted_card(&mut note, &template, ord, fill_empty, input.partial_render)
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ impl crate::services::CardRenderingService for Collection {
|
||||||
let ord = input.card_ord as u16;
|
let ord = input.card_ord as u16;
|
||||||
let fill_empty = input.fill_empty;
|
let fill_empty = input.fill_empty;
|
||||||
|
|
||||||
self.render_uncommitted_card(&mut note, &template, ord, fill_empty)
|
self.render_uncommitted_card(&mut note, &template, ord, fill_empty, input.partial_render)
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -410,14 +410,14 @@ fn strip_html_inside_mathjax(text: &str) -> Cow<str> {
|
||||||
|
|
||||||
pub(crate) fn cloze_filter<'a>(text: &'a str, context: &RenderContext) -> Cow<'a, str> {
|
pub(crate) fn cloze_filter<'a>(text: &'a str, context: &RenderContext) -> Cow<'a, str> {
|
||||||
strip_html_inside_mathjax(
|
strip_html_inside_mathjax(
|
||||||
reveal_cloze_text(text, context.card_ord + 1, context.question_side).as_ref(),
|
reveal_cloze_text(text, context.card_ord + 1, context.frontside.is_none()).as_ref(),
|
||||||
)
|
)
|
||||||
.into_owned()
|
.into_owned()
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn cloze_only_filter<'a>(text: &'a str, context: &RenderContext) -> Cow<'a, str> {
|
pub(crate) fn cloze_only_filter<'a>(text: &'a str, context: &RenderContext) -> Cow<'a, str> {
|
||||||
reveal_cloze_text_only(text, context.card_ord + 1, context.question_side)
|
reveal_cloze_text_only(text, context.card_ord + 1, context.frontside.is_none())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -69,7 +69,8 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn card_record(&mut self, card: CardId, with_html: bool) -> Result<[String; 2]> {
|
fn card_record(&mut self, card: CardId, with_html: bool) -> Result<[String; 2]> {
|
||||||
let RenderCardOutput { qnodes, anodes, .. } = self.render_existing_card(card, false)?;
|
let RenderCardOutput { qnodes, anodes, .. } =
|
||||||
|
self.render_existing_card(card, false, false)?;
|
||||||
Ok([
|
Ok([
|
||||||
rendered_nodes_to_record_field(&qnodes, with_html, false),
|
rendered_nodes_to_record_field(&qnodes, with_html, false),
|
||||||
rendered_nodes_to_record_field(&anodes, with_html, true),
|
rendered_nodes_to_record_field(&anodes, with_html, true),
|
||||||
|
|
|
@ -9,11 +9,12 @@ use super::Notetype;
|
||||||
use super::NotetypeKind;
|
use super::NotetypeKind;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::template::field_is_empty;
|
use crate::template::field_is_empty;
|
||||||
use crate::template::flatten_nodes;
|
|
||||||
use crate::template::render_card;
|
use crate::template::render_card;
|
||||||
use crate::template::ParsedTemplate;
|
use crate::template::ParsedTemplate;
|
||||||
|
use crate::template::RenderCardRequest;
|
||||||
use crate::template::RenderedNode;
|
use crate::template::RenderedNode;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct RenderCardOutput {
|
pub struct RenderCardOutput {
|
||||||
pub qnodes: Vec<RenderedNode>,
|
pub qnodes: Vec<RenderedNode>,
|
||||||
pub anodes: Vec<RenderedNode>,
|
pub anodes: Vec<RenderedNode>,
|
||||||
|
@ -22,20 +23,31 @@ pub struct RenderCardOutput {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderCardOutput {
|
impl RenderCardOutput {
|
||||||
/// The question text, ignoring any unknown field replacements.
|
/// The question text. This is only valid to call when partial_render=false.
|
||||||
pub fn question(&self) -> Cow<str> {
|
pub fn question(&self) -> Cow<str> {
|
||||||
flatten_nodes(&self.qnodes)
|
match self.qnodes.as_slice() {
|
||||||
|
[RenderedNode::Text { text }] => text.into(),
|
||||||
|
_ => "not fully rendered".into(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The answer text, ignoring any unknown field replacements.
|
/// The answer text. This is only valid to call when partial_render=false.
|
||||||
pub fn answer(&self) -> Cow<str> {
|
pub fn answer(&self) -> Cow<str> {
|
||||||
flatten_nodes(&self.anodes)
|
match self.anodes.as_slice() {
|
||||||
|
[RenderedNode::Text { text }] => text.into(),
|
||||||
|
_ => "not fully rendered".into(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
/// Render an existing card saved in the database.
|
/// Render an existing card saved in the database.
|
||||||
pub fn render_existing_card(&mut self, cid: CardId, browser: bool) -> Result<RenderCardOutput> {
|
pub fn render_existing_card(
|
||||||
|
&mut self,
|
||||||
|
cid: CardId,
|
||||||
|
browser: bool,
|
||||||
|
partial_render: bool,
|
||||||
|
) -> Result<RenderCardOutput> {
|
||||||
let card = self.storage.get_card(cid)?.or_invalid("no such card")?;
|
let card = self.storage.get_card(cid)?.or_invalid("no such card")?;
|
||||||
let note = self
|
let note = self
|
||||||
.storage
|
.storage
|
||||||
|
@ -50,7 +62,7 @@ impl Collection {
|
||||||
}
|
}
|
||||||
.or_invalid("missing template")?;
|
.or_invalid("missing template")?;
|
||||||
|
|
||||||
self.render_card(¬e, &card, &nt, template, browser)
|
self.render_card(¬e, &card, &nt, template, browser, partial_render)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a card that may not yet have been added.
|
/// Render a card that may not yet have been added.
|
||||||
|
@ -62,6 +74,7 @@ impl Collection {
|
||||||
template: &CardTemplate,
|
template: &CardTemplate,
|
||||||
card_ord: u16,
|
card_ord: u16,
|
||||||
fill_empty: bool,
|
fill_empty: bool,
|
||||||
|
partial_render: bool,
|
||||||
) -> Result<RenderCardOutput> {
|
) -> Result<RenderCardOutput> {
|
||||||
let card = self.existing_or_synthesized_card(note.id, template.ord, card_ord)?;
|
let card = self.existing_or_synthesized_card(note.id, template.ord, card_ord)?;
|
||||||
let nt = self
|
let nt = self
|
||||||
|
@ -72,7 +85,7 @@ impl Collection {
|
||||||
fill_empty_fields(note, &template.config.q_format, &nt, &self.tr);
|
fill_empty_fields(note, &template.config.q_format, &nt, &self.tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.render_card(note, &card, &nt, template, false)
|
self.render_card(note, &card, &nt, template, false, partial_render)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn existing_or_synthesized_card(
|
fn existing_or_synthesized_card(
|
||||||
|
@ -102,6 +115,7 @@ impl Collection {
|
||||||
nt: &Notetype,
|
nt: &Notetype,
|
||||||
template: &CardTemplate,
|
template: &CardTemplate,
|
||||||
browser: bool,
|
browser: bool,
|
||||||
|
partial_render: bool,
|
||||||
) -> Result<RenderCardOutput> {
|
) -> Result<RenderCardOutput> {
|
||||||
let mut field_map = note.fields_map(&nt.fields);
|
let mut field_map = note.fields_map(&nt.fields);
|
||||||
|
|
||||||
|
@ -122,15 +136,16 @@ impl Collection {
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let (qnodes, anodes) = render_card(
|
let (qnodes, anodes) = render_card(RenderCardRequest {
|
||||||
qfmt,
|
qfmt,
|
||||||
afmt,
|
afmt,
|
||||||
&field_map,
|
field_map: &field_map,
|
||||||
card.template_idx,
|
card_ord: card.template_idx,
|
||||||
nt.is_cloze(),
|
is_cloze: nt.is_cloze(),
|
||||||
browser,
|
browser,
|
||||||
&self.tr,
|
tr: &self.tr,
|
||||||
)?;
|
partial_render,
|
||||||
|
})?;
|
||||||
Ok(RenderCardOutput {
|
Ok(RenderCardOutput {
|
||||||
qnodes,
|
qnodes,
|
||||||
anodes,
|
anodes,
|
||||||
|
@ -190,3 +205,30 @@ fn fill_empty_fields(note: &mut Note, qfmt: &str, nt: &Notetype, tr: &I18n) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::collection::CollectionBuilder;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_render_fully() -> Result<()> {
|
||||||
|
let mut col = CollectionBuilder::default().build()?;
|
||||||
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
||||||
|
let mut note = Note::new(&nt);
|
||||||
|
note.set_field(0, "front")?;
|
||||||
|
note.set_field(1, "back")?;
|
||||||
|
let out: RenderCardOutput =
|
||||||
|
col.render_uncommitted_card(&mut note, &nt.templates[0], 0, false, false)?;
|
||||||
|
assert_eq!(&out.question(), "front");
|
||||||
|
assert_eq!(&out.answer(), "front\n\n<hr id=answer>\n\nback");
|
||||||
|
|
||||||
|
// should work even if unknown filters are encountered
|
||||||
|
let mut tmpl = nt.templates[0].clone();
|
||||||
|
tmpl.config.q_format = "{{some_filter:Front}}".into();
|
||||||
|
let out = col.render_uncommitted_card(&mut note, &nt.templates[0], 0, false, false)?;
|
||||||
|
assert_eq!(&out.question(), "front");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ use crate::cloze::add_cloze_numbers_in_string;
|
||||||
use crate::error::AnkiError;
|
use crate::error::AnkiError;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::error::TemplateError;
|
use crate::error::TemplateError;
|
||||||
|
use crate::invalid_input;
|
||||||
use crate::template_filters::apply_filters;
|
use crate::template_filters::apply_filters;
|
||||||
|
|
||||||
pub type FieldMap<'a> = HashMap<&'a str, u16>;
|
pub type FieldMap<'a> = HashMap<&'a str, u16>;
|
||||||
|
@ -387,8 +388,14 @@ pub enum RenderedNode {
|
||||||
pub(crate) struct RenderContext<'a> {
|
pub(crate) struct RenderContext<'a> {
|
||||||
pub fields: &'a HashMap<&'a str, Cow<'a, str>>,
|
pub fields: &'a HashMap<&'a str, Cow<'a, str>>,
|
||||||
pub nonempty_fields: &'a HashSet<&'a str>,
|
pub nonempty_fields: &'a HashSet<&'a str>,
|
||||||
pub question_side: bool,
|
|
||||||
pub card_ord: u16,
|
pub card_ord: u16,
|
||||||
|
/// Should be set before rendering the answer, even if `partial_for_python`
|
||||||
|
/// is true.
|
||||||
|
pub frontside: Option<&'a str>,
|
||||||
|
/// If true, question/answer will not be fully rendered if an unknown filter
|
||||||
|
/// is encountered, and the frontend code will need to complete the
|
||||||
|
/// rendering.
|
||||||
|
pub partial_for_python: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParsedTemplate {
|
impl ParsedTemplate {
|
||||||
|
@ -418,62 +425,75 @@ fn render_into(
|
||||||
append_str_to_nodes(rendered_nodes, text);
|
append_str_to_nodes(rendered_nodes, text);
|
||||||
}
|
}
|
||||||
Replacement { key, .. } if key == "FrontSide" => {
|
Replacement { key, .. } if key == "FrontSide" => {
|
||||||
// defer FrontSide rendering to Python, as extra
|
if let Some(frontside) = &context.frontside {
|
||||||
// filters may be required
|
if context.partial_for_python {
|
||||||
rendered_nodes.push(RenderedNode::Replacement {
|
// defer FrontSide rendering to Python, as extra
|
||||||
field_name: (*key).to_string(),
|
// filters may be required
|
||||||
filters: vec![],
|
rendered_nodes.push(RenderedNode::Replacement {
|
||||||
current_text: "".into(),
|
field_name: (*key).to_string(),
|
||||||
});
|
filters: vec![],
|
||||||
}
|
current_text: "".into(),
|
||||||
Replacement { key, filters } if key.is_empty() && !filters.is_empty() => {
|
});
|
||||||
// if a filter is provided, we accept an empty field name to
|
} else {
|
||||||
// mean 'pass an empty string to the filter, and it will add
|
append_str_to_nodes(rendered_nodes, frontside);
|
||||||
// its own text'
|
}
|
||||||
rendered_nodes.push(RenderedNode::Replacement {
|
} else {
|
||||||
field_name: "".to_string(),
|
// Not valid on the question side
|
||||||
current_text: "".to_string(),
|
return Err(TemplateError::FieldNotFound {
|
||||||
filters: filters.clone(),
|
field: "FrontSide".into(),
|
||||||
})
|
filters: "".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Replacement { key, filters } => {
|
Replacement { key, filters } => {
|
||||||
// apply built in filters if field exists
|
if context.partial_for_python && key.is_empty() && !filters.is_empty() {
|
||||||
let (text, remaining_filters) = match context.fields.get(key.as_str()) {
|
// if a filter is provided, we accept an empty field name to
|
||||||
Some(text) => apply_filters(
|
// mean 'pass an empty string to the filter, and it will add
|
||||||
text,
|
// its own text'
|
||||||
filters
|
rendered_nodes.push(RenderedNode::Replacement {
|
||||||
.iter()
|
field_name: "".to_string(),
|
||||||
.map(|s| s.as_str())
|
current_text: "".to_string(),
|
||||||
.collect::<Vec<_>>()
|
filters: filters.clone(),
|
||||||
.as_slice(),
|
});
|
||||||
key,
|
} else {
|
||||||
context,
|
// apply built in filters if field exists
|
||||||
),
|
let (text, remaining_filters) = match context.fields.get(key.as_str()) {
|
||||||
None => {
|
Some(text) => apply_filters(
|
||||||
// unknown field encountered
|
text,
|
||||||
let filters_str = filters
|
filters
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.map(|s| s.as_str())
|
||||||
.cloned()
|
.collect::<Vec<_>>()
|
||||||
.chain(iter::once("".into()))
|
.as_slice(),
|
||||||
.collect::<Vec<_>>()
|
key,
|
||||||
.join(":");
|
context,
|
||||||
return Err(TemplateError::FieldNotFound {
|
),
|
||||||
field: (*key).to_string(),
|
None => {
|
||||||
filters: filters_str,
|
// unknown field encountered
|
||||||
|
let filters_str = filters
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.cloned()
|
||||||
|
.chain(iter::once("".into()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(":");
|
||||||
|
return Err(TemplateError::FieldNotFound {
|
||||||
|
field: (*key).to_string(),
|
||||||
|
filters: filters_str,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// fully processed?
|
||||||
|
if remaining_filters.is_empty() {
|
||||||
|
append_str_to_nodes(rendered_nodes, text.as_ref())
|
||||||
|
} else {
|
||||||
|
rendered_nodes.push(RenderedNode::Replacement {
|
||||||
|
field_name: (*key).to_string(),
|
||||||
|
filters: remaining_filters,
|
||||||
|
current_text: text.into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// fully processed?
|
|
||||||
if remaining_filters.is_empty() {
|
|
||||||
append_str_to_nodes(rendered_nodes, text.as_ref())
|
|
||||||
} else {
|
|
||||||
rendered_nodes.push(RenderedNode::Replacement {
|
|
||||||
field_name: (*key).to_string(),
|
|
||||||
filters: remaining_filters,
|
|
||||||
current_text: text.into(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Conditional { key, children } => {
|
Conditional { key, children } => {
|
||||||
|
@ -529,24 +549,6 @@ fn append_str_to_nodes(nodes: &mut Vec<RenderedNode>, text: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the resolved text of a list of nodes, ignoring any unknown
|
|
||||||
/// filters that were encountered.
|
|
||||||
pub(crate) fn flatten_nodes(nodes: &[RenderedNode]) -> Cow<str> {
|
|
||||||
match nodes {
|
|
||||||
[RenderedNode::Text { text }] => text.into(),
|
|
||||||
nodes => {
|
|
||||||
let mut buf = String::new();
|
|
||||||
for node in nodes {
|
|
||||||
match node {
|
|
||||||
RenderedNode::Text { text } => buf.push_str(text),
|
|
||||||
RenderedNode::Replacement { current_text, .. } => buf.push_str(current_text),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buf.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// True if provided text contains only whitespace and/or empty BR/DIV tags.
|
/// True if provided text contains only whitespace and/or empty BR/DIV tags.
|
||||||
pub(crate) fn field_is_empty(text: &str) -> bool {
|
pub(crate) fn field_is_empty(text: &str) -> bool {
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -583,22 +585,36 @@ where
|
||||||
// Rendering both sides
|
// Rendering both sides
|
||||||
//----------------------------------------
|
//----------------------------------------
|
||||||
|
|
||||||
#[allow(clippy::implicit_hasher)]
|
pub struct RenderCardRequest<'a> {
|
||||||
|
pub qfmt: &'a str,
|
||||||
|
pub afmt: &'a str,
|
||||||
|
pub field_map: &'a HashMap<&'a str, Cow<'a, str>>,
|
||||||
|
pub card_ord: u16,
|
||||||
|
pub is_cloze: bool,
|
||||||
|
pub browser: bool,
|
||||||
|
pub tr: &'a I18n,
|
||||||
|
pub partial_render: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_card(
|
pub fn render_card(
|
||||||
qfmt: &str,
|
RenderCardRequest {
|
||||||
afmt: &str,
|
qfmt,
|
||||||
field_map: &HashMap<&str, Cow<str>>,
|
afmt,
|
||||||
card_ord: u16,
|
field_map,
|
||||||
is_cloze: bool,
|
card_ord,
|
||||||
browser: bool,
|
is_cloze,
|
||||||
tr: &I18n,
|
browser,
|
||||||
|
tr,
|
||||||
|
partial_render: partial_for_python,
|
||||||
|
}: RenderCardRequest<'_>,
|
||||||
) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> {
|
) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> {
|
||||||
// prepare context
|
// prepare context
|
||||||
let mut context = RenderContext {
|
let mut context = RenderContext {
|
||||||
fields: field_map,
|
fields: field_map,
|
||||||
nonempty_fields: &nonempty_fields(field_map),
|
nonempty_fields: &nonempty_fields(field_map),
|
||||||
question_side: true,
|
frontside: None,
|
||||||
card_ord,
|
card_ord,
|
||||||
|
partial_for_python,
|
||||||
};
|
};
|
||||||
|
|
||||||
// question side
|
// question side
|
||||||
|
@ -630,7 +646,14 @@ pub fn render_card(
|
||||||
}
|
}
|
||||||
|
|
||||||
// answer side
|
// answer side
|
||||||
context.question_side = false;
|
context.frontside = if context.partial_for_python {
|
||||||
|
Some("")
|
||||||
|
} else {
|
||||||
|
let Some(RenderedNode::Text {text }) = &qnodes.get(0) else {
|
||||||
|
invalid_input!("should not happen: first node not text");
|
||||||
|
};
|
||||||
|
Some(text)
|
||||||
|
};
|
||||||
let anodes = ParsedTemplate::from_text(afmt)
|
let anodes = ParsedTemplate::from_text(afmt)
|
||||||
.and_then(|tmpl| tmpl.render(&context, tr))
|
.and_then(|tmpl| tmpl.render(&context, tr))
|
||||||
.map_err(|e| template_error_to_anki_error(e, false, browser, tr))?;
|
.map_err(|e| template_error_to_anki_error(e, false, browser, tr))?;
|
||||||
|
@ -873,6 +896,7 @@ mod test {
|
||||||
use crate::template::field_is_empty;
|
use crate::template::field_is_empty;
|
||||||
use crate::template::nonempty_fields;
|
use crate::template::nonempty_fields;
|
||||||
use crate::template::FieldRequirements;
|
use crate::template::FieldRequirements;
|
||||||
|
use crate::template::RenderCardRequest;
|
||||||
use crate::template::RenderContext;
|
use crate::template::RenderContext;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1081,8 +1105,9 @@ mod test {
|
||||||
let ctx = RenderContext {
|
let ctx = RenderContext {
|
||||||
fields: &map,
|
fields: &map,
|
||||||
nonempty_fields: &nonempty_fields(&map),
|
nonempty_fields: &nonempty_fields(&map),
|
||||||
question_side: true,
|
frontside: None,
|
||||||
card_ord: 1,
|
card_ord: 1,
|
||||||
|
partial_for_python: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::template::RenderedNode as FN;
|
use crate::template::RenderedNode as FN;
|
||||||
|
@ -1198,9 +1223,18 @@ mod test {
|
||||||
let tr = I18n::template_only();
|
let tr = I18n::template_only();
|
||||||
use crate::template::RenderedNode as FN;
|
use crate::template::RenderedNode as FN;
|
||||||
|
|
||||||
let qnodes = super::render_card("test{{E}}", "", &map, 1, false, false, &tr)
|
let qnodes = super::render_card(RenderCardRequest {
|
||||||
.unwrap()
|
qfmt: "test{{E}}",
|
||||||
.0;
|
afmt: "",
|
||||||
|
field_map: &map,
|
||||||
|
card_ord: 1,
|
||||||
|
is_cloze: false,
|
||||||
|
browser: false,
|
||||||
|
tr: &tr,
|
||||||
|
partial_render: true,
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
qnodes[0],
|
qnodes[0],
|
||||||
FN::Text {
|
FN::Text {
|
||||||
|
|
|
@ -19,8 +19,9 @@ use crate::text::strip_html;
|
||||||
/// Applies built in filters, returning the resulting text and remaining
|
/// Applies built in filters, returning the resulting text and remaining
|
||||||
/// filters.
|
/// filters.
|
||||||
///
|
///
|
||||||
/// The first non-standard filter that is encountered will terminate processing,
|
/// If [context.partial_for_python] is true, the first non-standard filter that
|
||||||
/// so non-standard filters must come at the end.
|
/// is encountered will terminate processing, so non-standard filters must come
|
||||||
|
/// at the end. If false, missing filters are ignored.
|
||||||
pub(crate) fn apply_filters<'a>(
|
pub(crate) fn apply_filters<'a>(
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
filters: &[&str],
|
filters: &[&str],
|
||||||
|
@ -46,11 +47,14 @@ pub(crate) fn apply_filters<'a>(
|
||||||
text = output.into();
|
text = output.into();
|
||||||
}
|
}
|
||||||
(false, _) => {
|
(false, _) => {
|
||||||
// unrecognized filter, return current text and remaining filters
|
// unrecognized filter
|
||||||
return (
|
if context.partial_for_python {
|
||||||
text,
|
// return current text and remaining filters
|
||||||
filters.iter().skip(idx).map(ToString::to_string).collect(),
|
return (
|
||||||
);
|
text,
|
||||||
|
filters.iter().skip(idx).map(ToString::to_string).collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,8 +240,9 @@ field</a>
|
||||||
let ctx = RenderContext {
|
let ctx = RenderContext {
|
||||||
fields: &Default::default(),
|
fields: &Default::default(),
|
||||||
nonempty_fields: &Default::default(),
|
nonempty_fields: &Default::default(),
|
||||||
question_side: false,
|
frontside: Some(""),
|
||||||
card_ord: 0,
|
card_ord: 0,
|
||||||
|
partial_for_python: true,
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
apply_filters("ignored", &["cloze", "type"], "Text", &ctx),
|
apply_filters("ignored", &["cloze", "type"], "Text", &ctx),
|
||||||
|
@ -251,8 +256,9 @@ field</a>
|
||||||
let mut ctx = RenderContext {
|
let mut ctx = RenderContext {
|
||||||
fields: &Default::default(),
|
fields: &Default::default(),
|
||||||
nonempty_fields: &Default::default(),
|
nonempty_fields: &Default::default(),
|
||||||
question_side: true,
|
frontside: None,
|
||||||
card_ord: 0,
|
card_ord: 0,
|
||||||
|
partial_for_python: true,
|
||||||
};
|
};
|
||||||
assert_eq!(strip_html(&cloze_filter(text, &ctx)).as_ref(), "[...] two");
|
assert_eq!(strip_html(&cloze_filter(text, &ctx)).as_ref(), "[...] two");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -263,7 +269,7 @@ field</a>
|
||||||
ctx.card_ord = 1;
|
ctx.card_ord = 1;
|
||||||
assert_eq!(strip_html(&cloze_filter(text, &ctx)).as_ref(), "one [hint]");
|
assert_eq!(strip_html(&cloze_filter(text, &ctx)).as_ref(), "one [hint]");
|
||||||
|
|
||||||
ctx.question_side = false;
|
ctx.frontside = Some("");
|
||||||
assert_eq!(strip_html(&cloze_filter(text, &ctx)).as_ref(), "one two");
|
assert_eq!(strip_html(&cloze_filter(text, &ctx)).as_ref(), "one two");
|
||||||
|
|
||||||
// if the provided ordinal did not match any cloze deletions,
|
// if the provided ordinal did not match any cloze deletions,
|
||||||
|
|
Loading…
Reference in a new issue