summaryrefslogtreecommitdiff
path: root/server/common/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/common/src')
-rw-r--r--server/common/src/grit.rs1086
-rw-r--r--server/common/src/lib.rs1
-rw-r--r--server/common/src/testdata/grit/base.grd48
-rw-r--r--server/common/src/testdata/grit/extra.grdp6
-rw-r--r--server/common/src/tests.rs230
5 files changed, 1371 insertions, 0 deletions
diff --git a/server/common/src/grit.rs b/server/common/src/grit.rs
new file mode 100644
index 0000000..273da8a
--- /dev/null
+++ b/server/common/src/grit.rs
@@ -0,0 +1,1086 @@
+#![allow(dead_code)]
+
+use anyhow::Error;
+use std::fs;
+use std::io::{BufReader, Read};
+use std::path::Path;
+use tokio::task::spawn_blocking;
+use xml::attribute::OwnedAttribute;
+use xml::reader::{EventReader, ParserConfig, XmlEvent};
+
+#[derive(Debug, PartialEq)]
+pub struct Grit {
+ pub current_release: u32,
+ pub latest_public_release: u32,
+
+ pub outputs: Outputs,
+ pub translations: Translations,
+ pub release: Release,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct Outputs {
+ pub output: Vec<IfOutput>,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct Output {
+ pub filename: String,
+ pub output_type: String,
+ pub lang: String,
+}
+
+#[derive(Debug, PartialEq)]
+pub enum IfOutput {
+ Output(Output),
+
+ If { expr: String, output: Vec<Output> },
+}
+
+#[derive(Debug, PartialEq)]
+pub struct Translations {
+ pub file: Vec<IfFile>,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct File {
+ pub path: String,
+ pub lang: String,
+}
+
+#[derive(Debug, PartialEq)]
+pub enum IfFile {
+ File(File),
+
+ If { expr: String, file: Vec<File> },
+}
+
+#[derive(Debug, PartialEq)]
+pub struct Release {
+ pub allow_pseudo: bool,
+ pub seq: u32,
+
+ pub messages: Messages,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct Messages {
+ pub fallback_to_english: bool,
+
+ pub messages: Vec<IfMessagePart>,
+}
+
+#[derive(Debug, PartialEq)]
+pub enum IfMessagePart {
+ Message(Message),
+
+ Part(PartRef),
+
+ If {
+ expr: String,
+
+ message: Vec<MessagePart>,
+ },
+}
+
+#[derive(Debug, PartialEq)]
+pub enum MessagePart {
+ Message(Message),
+
+ Part(PartRef),
+}
+
+#[derive(Debug, PartialEq)]
+pub struct PartRef {
+ pub file: String,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct Message {
+ pub desc: String,
+ pub name: String,
+ pub internal_comment: Option<String>,
+ pub meaning: Option<String>,
+
+ pub content: Vec<TextPlaceholder>,
+}
+
+#[derive(Debug, PartialEq)]
+pub enum TextPlaceholder {
+ Text(String),
+ Placeholder {
+ name: String,
+ content: String,
+ example: Option<String>,
+ },
+}
+
+#[derive(Debug, PartialEq)]
+pub enum IfMessage {
+ Message(Message),
+
+ If { expr: String, message: Vec<Message> },
+}
+
+#[derive(Debug, PartialEq)]
+pub struct GritPart {
+ pub messages: Vec<IfMessage>,
+}
+
+fn get_opt_attribute<'a>(attributes: &'a Vec<OwnedAttribute>, name: &str) -> Option<&'a str> {
+ for attribute in attributes {
+ if attribute.name.local_name == name {
+ return Some(attribute.value.as_str());
+ }
+ }
+ None
+}
+
+fn get_attribute<'a>(attributes: &'a Vec<OwnedAttribute>, name: &str) -> anyhow::Result<&'a str> {
+ get_opt_attribute(attributes, name).ok_or(Error::msg(format!("Expected attribute {name}")))
+}
+
+fn parse_grit_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<Grit> {
+ let current_release = get_attribute(attributes, "current_release")?.parse::<u32>()?;
+ let latest_public_release =
+ get_attribute(attributes, "latest_public_release")?.parse::<u32>()?;
+
+ let mut outputs: Option<Outputs> = None;
+ let mut translations: Option<Translations> = None;
+ let mut release: Option<Release> = None;
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "outputs" => {
+ outputs = Some(parse_outputs_element(&attributes, reader)?);
+ }
+ "translations" => {
+ translations = Some(parse_translations_element(&attributes, reader)?);
+ }
+ "release" => {
+ release = Some(parse_release_element(&attributes, reader)?);
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in grit",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "grit");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(Grit {
+ current_release,
+ latest_public_release,
+ outputs: outputs.ok_or(Error::msg("Expected outputs in grit"))?,
+ translations: translations.ok_or(Error::msg("Expected outputs in grit"))?,
+ release: release.ok_or(Error::msg("Expected outputs in grit"))?,
+ })
+}
+
+fn parse_outputs_element<R: Read>(
+ _attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<Outputs> {
+ let mut output = Vec::<IfOutput>::new();
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "if" => {
+ output.push(parse_if_output_element(&attributes, reader)?);
+ }
+ "output" => {
+ output.push(IfOutput::Output(parse_output_element(&attributes, reader)?));
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in outputs",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "outputs");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(Outputs { output })
+}
+
+fn parse_output_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<Output> {
+ let filename = get_attribute(attributes, "filename")?;
+ let output_type = get_attribute(attributes, "type")?;
+ let lang = get_attribute(attributes, "lang")?;
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes: _,
+ namespace: _,
+ } => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in output",
+ name.local_name
+ )));
+ }
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "output");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(Output {
+ filename: filename.to_string(),
+ output_type: output_type.to_string(),
+ lang: lang.to_string(),
+ })
+}
+
+fn parse_if_output_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<IfOutput> {
+ let expr = get_attribute(attributes, "expr")?;
+ let mut output = Vec::<Output>::new();
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "output" => {
+ output.push(parse_output_element(&attributes, reader)?);
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in outputs>if",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "if");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(IfOutput::If {
+ expr: expr.to_string(),
+ output,
+ })
+}
+
+fn parse_translations_element<R: Read>(
+ _attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<Translations> {
+ let mut file = Vec::<IfFile>::new();
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "if" => {
+ file.push(parse_if_file_element(&attributes, reader)?);
+ }
+ "file" => {
+ file.push(IfFile::File(parse_file_element(&attributes, reader)?));
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in translations",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "translations");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(Translations { file })
+}
+
+fn parse_file_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<File> {
+ let path = get_attribute(attributes, "path")?;
+ let lang = get_attribute(attributes, "lang")?;
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes: _,
+ namespace: _,
+ } => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in file",
+ name.local_name
+ )));
+ }
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "file");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(File {
+ path: path.to_string(),
+ lang: lang.to_string(),
+ })
+}
+
+fn parse_if_file_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<IfFile> {
+ let expr = get_attribute(attributes, "expr")?;
+ let mut file = Vec::<File>::new();
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "file" => {
+ file.push(parse_file_element(&attributes, reader)?);
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in outputs>if",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "if");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(IfFile::If {
+ expr: expr.to_string(),
+ file,
+ })
+}
+
+fn parse_release_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<Release> {
+ let allow_pseudo = get_attribute(attributes, "allow_pseudo")?.parse::<bool>()?;
+ let seq = get_attribute(attributes, "seq")?.parse::<u32>()?;
+
+ let mut messages: Option<Messages> = None;
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "messages" => {
+ messages = Some(parse_messages_element(&attributes, reader)?);
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in release",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "release");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(Release {
+ allow_pseudo,
+ seq,
+ messages: messages.ok_or(Error::msg("No messages in release"))?,
+ })
+}
+
+fn parse_messages_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<Messages> {
+ let fallback_to_english = get_attribute(attributes, "fallback_to_english")?.parse::<bool>()?;
+
+ let mut messages = Vec::<IfMessagePart>::new();
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "message" => {
+ messages.push(IfMessagePart::Message(parse_message_element(
+ &attributes,
+ reader,
+ )?));
+ }
+ "part" => {
+ messages.push(IfMessagePart::Part(parse_part_element(
+ &attributes,
+ reader,
+ )?));
+ }
+ "if" => {
+ messages.push(parse_if_message_part_element(&attributes, reader)?);
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in messages",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "messages");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(Messages {
+ fallback_to_english,
+ messages,
+ })
+}
+
+fn parse_message_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<Message> {
+ let desc = get_attribute(attributes, "desc")?;
+ let name = get_attribute(attributes, "name")?;
+ let internal_comment = get_opt_attribute(attributes, "internal_comment").map(|s| s.to_string());
+ let meaning = get_opt_attribute(attributes, "meaning").map(|s| s.to_string());
+
+ let mut content = Vec::<TextPlaceholder>::new();
+ let mut first = true;
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "ph" => {
+ first = false;
+ content.push(parse_placeholder_element(&attributes, reader)?);
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in file",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "message");
+ break;
+ }
+ XmlEvent::Characters(data) => content.push(TextPlaceholder::Text(if first {
+ first = false;
+ data.trim_start().to_string()
+ } else {
+ data
+ })),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ if !first {
+ match content.last_mut().unwrap() {
+ TextPlaceholder::Text(data) => {
+ data.truncate(data.trim_end().len());
+ if data.is_empty() {
+ content.pop();
+ }
+ }
+ TextPlaceholder::Placeholder {
+ name: _,
+ content: _,
+ example: _,
+ } => {}
+ }
+ }
+
+ Ok(Message {
+ desc: desc.to_string(),
+ name: name.to_string(),
+ internal_comment,
+ meaning,
+ content,
+ })
+}
+
+fn parse_if_message_part_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<IfMessagePart> {
+ let expr = get_attribute(attributes, "expr")?;
+
+ let mut message = Vec::<MessagePart>::new();
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "message" => {
+ message.push(MessagePart::Message(parse_message_element(
+ &attributes,
+ reader,
+ )?));
+ }
+ "part" => {
+ message.push(MessagePart::Part(parse_part_element(&attributes, reader)?));
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in messages>if",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "if");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(IfMessagePart::If {
+ expr: expr.to_string(),
+ message,
+ })
+}
+
+fn parse_if_message_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<IfMessage> {
+ let expr = get_attribute(attributes, "expr")?;
+
+ let mut message = Vec::<Message>::new();
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "message" => {
+ message.push(parse_message_element(&attributes, reader)?);
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in grit-part>if",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "if");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(IfMessage::If {
+ expr: expr.to_string(),
+ message,
+ })
+}
+
+fn parse_part_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<PartRef> {
+ let file = get_attribute(attributes, "file")?;
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes: _,
+ namespace: _,
+ } => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in part",
+ name.local_name
+ )));
+ }
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "part");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(PartRef {
+ file: file.to_string(),
+ })
+}
+
+fn parse_placeholder_element<R: Read>(
+ attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<TextPlaceholder> {
+ let name = get_attribute(attributes, "name")?;
+
+ let mut content = String::new();
+ let mut example: Option<String> = None;
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "ex" => {
+ if example.is_some() {
+ return Err(Error::msg("Multiple examples in placeholder"));
+ }
+ example = Some(parse_placeholder_example_element(&attributes, reader)?);
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in file",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "ph");
+ break;
+ }
+ XmlEvent::Characters(data) => {
+ if example.is_some() {
+ return Err(Error::msg("Text after example in placeholder"));
+ }
+ content.push_str(data.as_str());
+ }
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(TextPlaceholder::Placeholder {
+ name: name.to_string(),
+ content,
+ example,
+ })
+}
+
+fn parse_placeholder_example_element<R: Read>(
+ _attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<String> {
+ let mut content = String::new();
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes: _,
+ namespace: _,
+ } => {
+ return Err(Error::msg(format!("Unexpected {0} in ex", name.local_name)));
+ }
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "ex");
+ break;
+ }
+ XmlEvent::Characters(data) => {
+ content.push_str(data.as_str());
+ }
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(content)
+}
+
+fn parse_grit_part_element<R: Read>(
+ _attributes: &Vec<OwnedAttribute>,
+ reader: &mut EventReader<R>,
+) -> anyhow::Result<GritPart> {
+ let mut messages = Vec::<IfMessage>::new();
+
+ loop {
+ let event = reader.next()?;
+ match event {
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => match name.local_name.as_str() {
+ "message" => {
+ messages.push(IfMessage::Message(parse_message_element(
+ &attributes,
+ reader,
+ )?));
+ }
+ "if" => {
+ messages.push(parse_if_message_element(&attributes, reader)?);
+ }
+ _ => {
+ return Err(Error::msg(format!(
+ "Unexpected {0} in grit-part",
+ name.local_name
+ )));
+ }
+ },
+ XmlEvent::EndElement { name } => {
+ assert!(name.local_name == "grit-part");
+ break;
+ }
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::EndDocument => panic!("Unexpected EOD"),
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+
+ Ok(GritPart { messages })
+}
+
+pub async fn parse_grit(path: impl AsRef<Path>) -> anyhow::Result<Grit> {
+ let path = path.as_ref().to_path_buf();
+ spawn_blocking(move || {
+ let file = fs::File::open(path)?;
+ let reader = BufReader::new(file);
+ let mut ereader = ParserConfig::new()
+ .ignore_comments(true)
+ .whitespace_to_characters(true)
+ .cdata_to_characters(true)
+ .create_reader(reader);
+ let mut ret: Option<Grit> = None;
+ loop {
+ let event = ereader.next()?;
+ match event {
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => {
+ if name.local_name == "grit" {
+ ret = Some(parse_grit_element(&attributes, &mut ereader)?);
+ } else {
+ return Err(Error::msg("Document root != grit"));
+ }
+ }
+ XmlEvent::EndDocument => break,
+ XmlEvent::EndElement { name: _ } => panic!("Unexpected EoE"),
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+ Ok(ret.unwrap())
+ })
+ .await
+ .unwrap()
+}
+
+pub async fn parse_grit_part(path: impl AsRef<Path>) -> anyhow::Result<GritPart> {
+ let path = path.as_ref().to_path_buf();
+ spawn_blocking(move || {
+ let file = fs::File::open(path)?;
+ let reader = BufReader::new(file);
+ let mut ereader = ParserConfig::new()
+ .ignore_comments(true)
+ .whitespace_to_characters(true)
+ .cdata_to_characters(true)
+ .create_reader(reader);
+ let mut ret: Option<GritPart> = None;
+ loop {
+ let event = ereader.next()?;
+ match event {
+ XmlEvent::StartDocument {
+ version: _,
+ encoding: _,
+ standalone: _,
+ } => (),
+ XmlEvent::StartElement {
+ name,
+ attributes,
+ namespace: _,
+ } => {
+ if name.local_name == "grit-part" {
+ ret = Some(parse_grit_part_element(&attributes, &mut ereader)?);
+ } else {
+ return Err(Error::msg("Document root != grit-part"));
+ }
+ }
+ XmlEvent::EndDocument => break,
+ XmlEvent::EndElement { name: _ } => panic!("Unexpected EoE"),
+ XmlEvent::Characters(_) => (),
+
+ XmlEvent::ProcessingInstruction { name: _, data: _ } => (),
+ XmlEvent::CData(_) => (),
+ XmlEvent::Comment(_) => (),
+ XmlEvent::Whitespace(_) => (),
+ }
+ }
+ Ok(ret.unwrap())
+ })
+ .await
+ .unwrap()
+}
diff --git a/server/common/src/lib.rs b/server/common/src/lib.rs
index abea52e..b844371 100644
--- a/server/common/src/lib.rs
+++ b/server/common/src/lib.rs
@@ -1,6 +1,7 @@
pub mod fs_utils;
pub mod git;
pub mod git_socket;
+pub mod grit;
#[cfg(test)]
mod tests;
diff --git a/server/common/src/testdata/grit/base.grd b/server/common/src/testdata/grit/base.grd
new file mode 100644
index 0000000..2ddc74a
--- /dev/null
+++ b/server/common/src/testdata/grit/base.grd
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grit current_release="1" latest_public_release="0">
+ <outputs>
+ <output filename="values/strings.xml" type="android" lang="en" />
+ <output filename="values-en-rGB/strings.xml" type="android" lang="en-GB" />
+ <output filename="values-my/strings.xml" type="android" lang="my" />
+ <if expr="not pp_if('zawgyi_encoding')">
+ <output filename="values-my-rZG/strings.xml" type="android" lang="my-ZG" />
+ </if>
+ <output filename="values-sv/strings.xml" type="android" lang="sv" />
+ </outputs>
+
+ <translations>
+ <file path="translations/base_en_gb.xlf" lang="en-GB" />
+ <if expr="pp_if('zawgyi_encoding')">
+ <file path="translations/base_my-Zawgyi.xlf" lang="my" />
+ </if>
+ <if expr="not pp_if('zawgyi_encoding')">
+ <file path="translations/base_my.xlf" lang="my" />
+ <file path="translations/base_my-Zawgyi.xlf" lang="my-ZG" />
+ </if>
+ <file path="translations/base_sv.xlf" lang="sv" />
+ </translations>
+
+ <release allow_pseudo="false" seq="1">
+ <messages fallback_to_english="true">
+ <if expr="pp_ifdef('include_extra')">
+ <part file="extra.grdp" />
+ </if>
+ <message desc="Title which is shown on the main bookmarks view." name="IDS_BOOKMARKS_FRAGMENT_TITLE" internal_comment="section(bookmarks)">
+ Bookmarks
+ </message>
+ <message desc="Generic welcome string." name="IDS_GENERIC_WELCOME" internal_comment="section(eula)">
+ Welcome to <ph name="STRING">%1$s<ex>Opera</ex></ph>
+ </message>
+ <message desc="First startup information about the license and privacy terms." name="IDS_START_TERMS">
+ By using this application you are agreeing to Opera's <ph name="TOS_BEGIN">&lt;tos&gt;</ph>Terms of Service<ph name="TOS_END">&lt;/tos&gt;</ph>. Also, you can learn how Opera handles and protects your data in our <ph name="PRIVACY_BEGIN">&lt;privacy&gt;</ph>Privacy Statement<ph name="PRIVACY_END">&lt;/privacy&gt;</ph>.
+ </message>
+ <message desc="Message which is shown when one or more folders have been deleted from the bookmark list." name="IDS_BOOKMARKS_FOLDERS_DELETED">
+ {BOOKMARKS, plural,
+ one {<ph name="COUNT">%1$d<ex>1</ex></ph> folder deleted}
+ few {<ph name="COUNT">%1$d<ex>15</ex></ph> folders deleted}
+ many {<ph name="COUNT">%1$d<ex>100</ex></ph> folders deleted}
+ other {<ph name="COUNT">%1$d<ex>42</ex></ph> folders deleted}}
+ </message>
+ </messages>
+ </release>
+</grit>
diff --git a/server/common/src/testdata/grit/extra.grdp b/server/common/src/testdata/grit/extra.grdp
new file mode 100644
index 0000000..b4382a3
--- /dev/null
+++ b/server/common/src/testdata/grit/extra.grdp
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<grit-part>
+ <message name="IDS_EXTRA" desc="Extra title">
+ Extra title
+ </message>
+</grit-part>
diff --git a/server/common/src/tests.rs b/server/common/src/tests.rs
index 5a02119..fc4d7aa 100644
--- a/server/common/src/tests.rs
+++ b/server/common/src/tests.rs
@@ -5,6 +5,7 @@ use tokio::sync::OnceCell;
use crate::fs_utils;
use crate::git;
+use crate::grit;
#[tokio::test]
async fn test_fs_utils_create_dir_allow_existing() {
@@ -285,3 +286,232 @@ async fn test_git_bare_delete_branch() {
assert!(repo.delete_branch("other").await.is_ok());
assert!(repo.delete_branch("does-not-exist").await.is_err());
}
+
+#[tokio::test]
+async fn test_grit_parse_base() {
+ let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/testdata/grit/base.grd");
+ let grit = grit::parse_grit(path).await.unwrap();
+ assert_eq!(
+ grit,
+ grit::Grit {
+ current_release: 1,
+ latest_public_release: 0,
+ outputs: grit::Outputs {
+ output: vec![
+ grit::IfOutput::Output(
+ grit::Output {
+ filename: "values/strings.xml".to_string(),
+ output_type: "android".to_string(),
+ lang: "en".to_string(),
+ },
+ ),
+ grit::IfOutput::Output(
+ grit::Output {
+ filename: "values-en-rGB/strings.xml".to_string(),
+ output_type: "android".to_string(),
+ lang: "en-GB".to_string(),
+ },
+ ),
+ grit::IfOutput::Output(
+ grit::Output {
+ filename: "values-my/strings.xml".to_string(),
+ output_type: "android".to_string(),
+ lang: "my".to_string(),
+ },
+ ),
+ grit::IfOutput::If {
+ expr: "not pp_if('zawgyi_encoding')".to_string(),
+ output: vec![
+ grit::Output {
+ filename: "values-my-rZG/strings.xml".to_string(),
+ output_type: "android".to_string(),
+ lang: "my-ZG".to_string(),
+ },
+ ],
+ },
+ grit::IfOutput::Output(
+ grit::Output {
+ filename: "values-sv/strings.xml".to_string(),
+ output_type: "android".to_string(),
+ lang: "sv".to_string(),
+ },
+ ),
+ ],
+ },
+ translations: grit::Translations {
+ file: vec![
+ grit::IfFile::File(
+ grit::File {
+ path: "translations/base_en_gb.xlf".to_string(),
+ lang: "en-GB".to_string(),
+ },
+ ),
+ grit::IfFile::If {
+ expr: "pp_if('zawgyi_encoding')".to_string(),
+ file: vec![
+ grit::File {
+ path: "translations/base_my-Zawgyi.xlf".to_string(),
+ lang: "my".to_string(),
+ },
+ ],
+ },
+ grit::IfFile::If {
+ expr: "not pp_if('zawgyi_encoding')".to_string(),
+ file: vec![
+ grit::File {
+ path: "translations/base_my.xlf".to_string(),
+ lang: "my".to_string(),
+ },
+ grit::File {
+ path: "translations/base_my-Zawgyi.xlf".to_string(),
+ lang: "my-ZG".to_string(),
+ },
+ ],
+ },
+ grit::IfFile::File(
+ grit::File {
+ path: "translations/base_sv.xlf".to_string(),
+ lang: "sv".to_string(),
+ },
+ ),
+ ],
+ },
+ release: grit::Release {
+ allow_pseudo: false,
+ seq: 1,
+ messages: grit::Messages {
+ fallback_to_english: true,
+ messages: vec![
+ grit::IfMessagePart::If {
+ expr: "pp_ifdef('include_extra')".to_string(),
+ message: vec![
+ grit::MessagePart::Part(
+ grit::PartRef {
+ file: "extra.grdp".to_string(),
+ },
+ ),
+ ],
+ },
+ grit::IfMessagePart::Message(
+ grit::Message {
+ desc: "Title which is shown on the main bookmarks view.".to_string(),
+ name: "IDS_BOOKMARKS_FRAGMENT_TITLE".to_string(),
+ internal_comment: Some("section(bookmarks)".to_string()),
+ meaning: None,
+ content: vec![
+ grit::TextPlaceholder::Text("Bookmarks".to_string()),
+ ],
+ },
+ ),
+ grit::IfMessagePart::Message(
+ grit::Message {
+ desc: "Generic welcome string.".to_string(),
+ name: "IDS_GENERIC_WELCOME".to_string(),
+ internal_comment: Some("section(eula)".to_string()),
+ meaning: None,
+ content: vec![
+ grit::TextPlaceholder::Text("Welcome to ".to_string()),
+ grit::TextPlaceholder::Placeholder {
+ name: "STRING".to_string(),
+ content: "%1$s".to_string(),
+ example: Some("Opera".to_string()),
+ },
+ ],
+ },
+ ),
+ grit::IfMessagePart::Message(
+ grit::Message {
+ desc: "First startup information about the license and privacy terms.".to_string(),
+ name: "IDS_START_TERMS".to_string(),
+ internal_comment: None,
+ meaning: None,
+ content: vec![
+ grit::TextPlaceholder::Text(
+ "By using this application you are agreeing to Opera's ".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "TOS_BEGIN".to_string(),
+ content: "<tos>".to_string(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text(
+ "Terms of Service".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "TOS_END".to_string(),
+ content: "</tos>".to_string(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text(
+ ". Also, you can learn how Opera handles and protects your data in our ".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "PRIVACY_BEGIN".to_string(),
+ content: "<privacy>".to_string(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text(
+ "Privacy Statement".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "PRIVACY_END".to_string(),
+ content: "</privacy>".to_string(),
+ example: None,
+ },
+ grit::TextPlaceholder::Text(
+ ".".to_string(),
+ ),
+ ],
+ },
+ ),
+ grit::IfMessagePart::Message(
+ grit::Message {
+ desc: "Message which is shown when one or more folders have been deleted from the bookmark list.".to_string(),
+ name: "IDS_BOOKMARKS_FOLDERS_DELETED".to_string(),
+ internal_comment: None,
+ meaning: None,
+ content: vec![
+ grit::TextPlaceholder::Text(
+ "{BOOKMARKS, plural,\n one {".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "COUNT".to_string(),
+ content: "%1$d".to_string(),
+ example: Some("1".to_string()),
+ },
+ grit::TextPlaceholder::Text(
+ " folder deleted}\n few {".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "COUNT".to_string(),
+ content: "%1$d".to_string(),
+ example: Some("15".to_string()),
+ },
+ grit::TextPlaceholder::Text(
+ " folders deleted}\n many {".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "COUNT".to_string(),
+ content: "%1$d".to_string(),
+ example: Some("100".to_string()),
+ },
+ grit::TextPlaceholder::Text(
+ " folders deleted}\n other {".to_string(),
+ ),
+ grit::TextPlaceholder::Placeholder {
+ name: "COUNT".to_string(),
+ content: "%1$d".to_string(),
+ example: Some("42".to_string()),
+ },
+ grit::TextPlaceholder::Text(
+ " folders deleted}}".to_string(),
+ ),
+ ],
+ },
+ ),
+ ],
+ },
+ },
+ },
+ )
+}