Anki/rslib/src/backend.rs
Damien Elmes 9bb0348fdd 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.
2020-01-12 21:34:07 +10:00

214 lines
7.2 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::backend_proto as pt;
use crate::backend_proto::backend_input::Value;
use crate::backend_proto::RenderedTemplateReplacement;
use crate::err::{AnkiError, Result};
use crate::sched::sched_timing_today;
use crate::template::{
render_card, without_legacy_template_directives, FieldMap, FieldRequirements, ParsedTemplate,
RenderedNode,
};
use prost::Message;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
pub struct Backend {
#[allow(dead_code)]
path: PathBuf,
}
/// Convert an Anki error to a protobuf error.
impl std::convert::From<AnkiError> for pt::BackendError {
fn from(err: AnkiError) -> Self {
use pt::backend_error::Value as V;
let value = match err {
AnkiError::InvalidInput { info } => V::InvalidInput(pt::InvalidInputError { info }),
AnkiError::TemplateParseError { info } => {
V::TemplateParse(pt::TemplateParseError { info })
}
};
pt::BackendError { value: Some(value) }
}
}
// Convert an Anki error to a protobuf output.
impl std::convert::From<AnkiError> for pt::backend_output::Value {
fn from(err: AnkiError) -> Self {
pt::backend_output::Value::Error(err.into())
}
}
impl Backend {
pub fn new<P: Into<PathBuf>>(path: P) -> Backend {
Backend { path: path.into() }
}
/// Decode a request, process it, and return the encoded result.
pub fn run_command_bytes(&mut self, req: &[u8]) -> Vec<u8> {
let mut buf = vec![];
let req = match pt::BackendInput::decode(req) {
Ok(req) => req,
Err(_e) => {
// unable to decode
let err = AnkiError::invalid_input("couldn't decode backend request");
let output = pt::BackendOutput {
value: Some(err.into()),
};
output.encode(&mut buf).expect("encode failed");
return buf;
}
};
let resp = self.run_command(req);
resp.encode(&mut buf).expect("encode failed");
buf
}
fn run_command(&self, input: pt::BackendInput) -> pt::BackendOutput {
let oval = if let Some(ival) = input.value {
match self.run_command_inner(ival) {
Ok(output) => output,
Err(err) => err.into(),
}
} else {
AnkiError::invalid_input("unrecognized backend input value").into()
};
pt::BackendOutput { value: Some(oval) }
}
fn run_command_inner(
&self,
ival: pt::backend_input::Value,
) -> Result<pt::backend_output::Value> {
use pt::backend_output::Value as OValue;
Ok(match ival {
Value::TemplateRequirements(input) => {
OValue::TemplateRequirements(self.template_requirements(input)?)
}
Value::PlusOne(input) => OValue::PlusOne(self.plus_one(input)?),
Value::SchedTimingToday(input) => {
OValue::SchedTimingToday(self.sched_timing_today(input))
}
Value::DeckTree(_) => todo!(),
Value::FindCards(_) => todo!(),
Value::BrowserRows(_) => todo!(),
Value::RenderCard(input) => OValue::RenderCard(self.render_template(input)),
})
}
fn plus_one(&self, input: pt::PlusOneIn) -> Result<pt::PlusOneOut> {
let num = input.num + 1;
Ok(pt::PlusOneOut { num })
}
fn template_requirements(
&self,
input: pt::TemplateRequirementsIn,
) -> Result<pt::TemplateRequirementsOut> {
let map: FieldMap = input
.field_names_to_ordinals
.iter()
.map(|(name, ord)| (name.as_str(), *ord as u16))
.collect();
// map each provided template into a requirements list
use crate::backend_proto::template_requirement::Value;
let all_reqs = input
.template_front
.into_iter()
.map(|template| {
let normalized = without_legacy_template_directives(&template);
if let Ok(tmpl) = ParsedTemplate::from_text(normalized.as_ref()) {
// convert the rust structure into a protobuf one
let val = match tmpl.requirements(&map) {
FieldRequirements::Any(ords) => Value::Any(pt::TemplateRequirementAny {
ords: ords_hash_to_set(ords),
}),
FieldRequirements::All(ords) => Value::All(pt::TemplateRequirementAll {
ords: ords_hash_to_set(ords),
}),
FieldRequirements::None => Value::None(pt::Empty {}),
};
Ok(pt::TemplateRequirement { value: Some(val) })
} else {
// template parsing failures make card unsatisfiable
Ok(pt::TemplateRequirement {
value: Some(Value::None(pt::Empty {})),
})
}
})
.collect::<Result<Vec<_>>>()?;
Ok(pt::TemplateRequirementsOut {
requirements: all_reqs,
})
}
fn sched_timing_today(&self, input: pt::SchedTimingTodayIn) -> pt::SchedTimingTodayOut {
let today = sched_timing_today(
input.created_secs as i64,
input.created_mins_west,
input.now_secs as i64,
input.now_mins_west,
input.rollover_hour as i8,
);
pt::SchedTimingTodayOut {
days_elapsed: today.days_elapsed,
next_day_at: today.next_day_at,
}
}
fn render_template(&self, input: pt::RenderCardIn) -> pt::RenderCardOut {
// convert string map to &str
let fields: HashMap<_, _> = input
.fields
.iter()
.map(|(k, v)| (k.as_ref(), v.as_ref()))
.collect();
// render
let (qnodes, anodes) = render_card(
&input.question_template,
&input.answer_template,
&fields,
input.card_ordinal as u16,
);
// return
pt::RenderCardOut {
question_nodes: rendered_nodes_to_proto(qnodes),
answer_nodes: rendered_nodes_to_proto(anodes),
}
}
}
fn ords_hash_to_set(ords: HashSet<u16>) -> Vec<u32> {
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 {
match node {
RenderedNode::Text { text } => pt::rendered_template_node::Value::Text(text),
RenderedNode::Replacement {
field_name,
current_text,
filters,
} => pt::rendered_template_node::Value::Replacement(RenderedTemplateReplacement {
field_name,
current_text,
filters,
}),
}
}