Modify card rendering output to specify if rendered card is empty (#3890)

* modify render_card to return whether card was empty

* plumbing

* add flag to proto message

* plumbing: pass flag along to PartiallyRenderedCard

* add tests

* Use a custom return type for clarity (dae)
This commit is contained in:
llama 2025-03-31 18:51:28 +08:00 committed by GitHub
parent 1798620d64
commit ccab18b7ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 39 additions and 14 deletions

View file

@ -127,6 +127,7 @@ message RenderCardResponse {
repeated RenderedTemplateNode answer_nodes = 2; repeated RenderedTemplateNode answer_nodes = 2;
string css = 3; string css = 3;
bool latex_svg = 4; bool latex_svg = 4;
bool is_empty = 5;
} }
message RenderedTemplateNode { message RenderedTemplateNode {

View file

@ -60,6 +60,7 @@ class PartiallyRenderedCard:
anodes: TemplateReplacementList anodes: TemplateReplacementList
css: str css: str
latex_svg: bool latex_svg: bool
is_empty: bool
@classmethod @classmethod
def from_proto( def from_proto(
@ -68,7 +69,9 @@ class PartiallyRenderedCard:
qnodes = cls.nodes_from_proto(out.question_nodes) qnodes = cls.nodes_from_proto(out.question_nodes)
anodes = cls.nodes_from_proto(out.answer_nodes) anodes = cls.nodes_from_proto(out.answer_nodes)
return PartiallyRenderedCard(qnodes, anodes, out.css, out.latex_svg) return PartiallyRenderedCard(
qnodes, anodes, out.css, out.latex_svg, out.is_empty
)
@staticmethod @staticmethod
def nodes_from_proto( def nodes_from_proto(

View file

@ -219,6 +219,7 @@ impl From<RenderCardOutput> for anki_proto::card_rendering::RenderCardResponse {
answer_nodes: rendered_nodes_to_proto(o.anodes), answer_nodes: rendered_nodes_to_proto(o.anodes),
css: o.css, css: o.css,
latex_svg: o.latex_svg, latex_svg: o.latex_svg,
is_empty: o.is_empty,
} }
} }
} }

View file

@ -20,6 +20,7 @@ pub struct RenderCardOutput {
pub anodes: Vec<RenderedNode>, pub anodes: Vec<RenderedNode>,
pub css: String, pub css: String,
pub latex_svg: bool, pub latex_svg: bool,
pub is_empty: bool,
} }
impl RenderCardOutput { impl RenderCardOutput {
@ -136,7 +137,7 @@ impl Collection {
) )
}; };
let (qnodes, anodes) = render_card(RenderCardRequest { let response = render_card(RenderCardRequest {
qfmt, qfmt,
afmt, afmt,
field_map: &field_map, field_map: &field_map,
@ -147,10 +148,11 @@ impl Collection {
partial_render, partial_render,
})?; })?;
Ok(RenderCardOutput { Ok(RenderCardOutput {
qnodes, qnodes: response.qnodes,
anodes, anodes: response.anodes,
css: nt.config.css.clone(), css: nt.config.css.clone(),
latex_svg: nt.config.latex_svg, latex_svg: nt.config.latex_svg,
is_empty: response.is_empty,
}) })
} }

View file

@ -592,6 +592,13 @@ pub struct RenderCardRequest<'a> {
pub partial_render: bool, pub partial_render: bool,
} }
pub struct RenderCardResponse {
pub qnodes: Vec<RenderedNode>,
pub anodes: Vec<RenderedNode>,
pub is_empty: bool,
}
/// Returns `(qnodes, anodes, is_empty)`
pub fn render_card( pub fn render_card(
RenderCardRequest { RenderCardRequest {
qfmt, qfmt,
@ -603,7 +610,7 @@ pub fn render_card(
tr, tr,
partial_render: partial_for_python, partial_render: partial_for_python,
}: RenderCardRequest<'_>, }: RenderCardRequest<'_>,
) -> Result<(Vec<RenderedNode>, Vec<RenderedNode>)> { ) -> Result<RenderCardResponse> {
// prepare context // prepare context
let mut context = RenderContext { let mut context = RenderContext {
fields: field_map, fields: field_map,
@ -638,7 +645,11 @@ pub fn render_card(
}; };
if let Some(text) = empty_message { if let Some(text) = empty_message {
qnodes.push(RenderedNode::Text { text: text.clone() }); qnodes.push(RenderedNode::Text { text: text.clone() });
return Ok((qnodes, vec![RenderedNode::Text { text }])); return Ok(RenderCardResponse {
qnodes,
anodes: vec![RenderedNode::Text { text }],
is_empty: true,
});
} }
// answer side // answer side
@ -654,7 +665,11 @@ pub fn render_card(
.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))?;
Ok((qnodes, anodes)) Ok(RenderCardResponse {
qnodes,
anodes,
is_empty: false,
})
} }
fn cloze_is_empty(field_map: &HashMap<&str, Cow<str>>, card_ord: u16) -> bool { fn cloze_is_empty(field_map: &HashMap<&str, Cow<str>>, card_ord: u16) -> bool {
@ -1338,14 +1353,15 @@ mod test {
tr: &tr, tr: &tr,
partial_render: true, partial_render: true,
}; };
let qnodes = super::render_card(req.clone()).unwrap().0; let response = super::render_card(req.clone()).unwrap();
assert_eq!( assert_eq!(
qnodes[0], response.qnodes[0],
FN::Text { FN::Text {
text: "test".into() text: "test".into()
} }
); );
if let FN::Text { ref text } = qnodes[1] { assert!(response.is_empty);
if let FN::Text { ref text } = response.qnodes[1] {
assert!(text.contains("card is blank")); assert!(text.contains("card is blank"));
} else { } else {
unreachable!(); unreachable!();
@ -1354,9 +1370,9 @@ mod test {
// a popular card template expects {{FrontSide}} to resolve to an empty // a popular card template expects {{FrontSide}} to resolve to an empty
// string on the front side :-( // string on the front side :-(
req.qfmt = "{{FrontSide}}{{N}}"; req.qfmt = "{{FrontSide}}{{N}}";
let qnodes = super::render_card(req.clone()).unwrap().0; let response = super::render_card(req.clone()).unwrap();
assert_eq!( assert_eq!(
&qnodes, &response.qnodes,
&[ &[
FN::Replacement { FN::Replacement {
field_name: "FrontSide".into(), field_name: "FrontSide".into(),
@ -1366,8 +1382,10 @@ mod test {
FN::Text { text: "N".into() } FN::Text { text: "N".into() }
] ]
); );
assert!(!response.is_empty);
req.partial_render = false; req.partial_render = false;
let qnodes = super::render_card(req.clone()).unwrap().0; let response = super::render_card(req.clone()).unwrap();
assert_eq!(&qnodes, &[FN::Text { text: "N".into() }]); assert_eq!(&response.qnodes, &[FN::Text { text: "N".into() }]);
assert!(!response.is_empty);
} }
} }