Skip to content

Commit

Permalink
fixes #3: Implement sort.
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexandre Leblanc committed Jan 28, 2024
1 parent d26624b commit 5aafa9f
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 28 deletions.
10 changes: 10 additions & 0 deletions example_unfinished.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,23 @@
<name>UiContext</name>
<message>
<source>Name</source>
<location line="456" filename="ui_main.cpp" />
<location line="10" filename="ui_potato_viewer.cpp" />
<location line="321" filename="ui_main.cpp" />
<location line="11" filename="ui_potato_viewer.cpp" />
<comment>An example entry for Name</comment>
<translation type="unfinished"></translation>
</message>
<message>
<location line="10" filename="ui_potato_viewer.cpp" />
<location line="144" filename="ui_main.cpp" />
<source>This is just a Sample</source>
<translation>Dies ist nur ein Beispiel</translation>
</message>
<message>
<source>Practice more</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>CodeContext</name>
Expand Down
88 changes: 82 additions & 6 deletions src/sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub fn sort_main(args: &SortArgs) -> Result<(), String> {
match nodes {
Ok(mut ts_node) => {
sort_ts_node(&mut ts_node);
Ok(())
write_ts_to_output(&args, &ts_node)
}
Err(e) => Err(format!(
"Could not parse input file \"{}\". Error: {e:?}.",
Expand All @@ -37,11 +37,26 @@ pub fn sort_main(args: &SortArgs) -> Result<(), String> {
}
}

fn sort_ts_node(_ts_node: &mut TSNode) {
todo!();
/// Sorts the TS document with the following rules:
/// 1. Context comes before no-context messages.
/// 2. Context are ordered by name.
/// 3. Messages are ordered by filename then by line.
fn sort_ts_node(ts_node: &mut TSNode) {
let contexts = &mut ts_node.contexts;
contexts.sort();
contexts.iter_mut().for_each(|context| {
context.messages.sort();
context
.messages
.iter_mut()
.for_each(|message| message.locations.sort());
});
}

fn write_ts_file(args: &SortArgs, node: &TSNode) -> Result<(), String> {
/// Writes the output TS file to the specified output (file or stdout).
/// This writer will auto indent/pretty print. It will always expand empty nodes, e.g.
/// `<name></name>` instead of `<name/>`.
fn write_ts_to_output(args: &SortArgs, node: &TSNode) -> Result<(), String> {
let mut inner_writer: BufWriter<Box<dyn Write>> = match &args.output_path {
None => BufWriter::new(Box::new(std::io::stdout().lock())),
Some(output_path) => match std::fs::File::options()
Expand All @@ -58,7 +73,8 @@ fn write_ts_file(args: &SortArgs, node: &TSNode) -> Result<(), String> {
},
};

let mut output_buffer = String::from("<!DOCTYPE TS>\n");
let mut output_buffer =
String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE TS>\n");
let mut ser = quick_xml::se::Serializer::new(&mut output_buffer);
ser.indent(' ', 2).expand_empty_elements(true);

Expand All @@ -74,6 +90,65 @@ fn write_ts_file(args: &SortArgs, node: &TSNode) -> Result<(), String> {
}
}

#[cfg(test)]
mod sort_test {
use super::*;
use quick_xml;

#[test]
fn sort_ts_node_ts() {
let reader_nosort = quick_xml::Reader::from_file("example_unfinished.xml")
.expect("Couldn't open example_unfinished test file");
let mut data_nosort: TSNode =
quick_xml::de::from_reader(reader_nosort.into_inner()).expect("Parsable");

sort_ts_node(&mut data_nosort);

// Validate context ordering
assert_eq!(data_nosort.contexts[0].name, Some("CodeContext".to_owned()));
assert_eq!(data_nosort.contexts[1].name, Some("UiContext".to_owned()));

// Validate message ordering
let messages = &data_nosort.contexts[1].messages;
assert_eq!(messages[0].source, Some("This is just a Sample".to_owned()));
assert_eq!(messages[1].source, Some("Name".to_owned()));
assert_eq!(messages[2].source, Some("Practice more".to_owned()));

// Validate locations ordering
assert_eq!(
messages[0].locations[0].filename,
Some("ui_main.cpp".to_owned())
);
assert_eq!(messages[0].locations[0].line, Some(144));
assert_eq!(
messages[0].locations[1].filename,
Some("ui_potato_viewer.cpp".to_owned())
);
assert_eq!(messages[0].locations[1].line, Some(10));

assert_eq!(
messages[1].locations[0].filename,
Some("ui_main.cpp".to_owned())
);
assert_eq!(messages[1].locations[0].line, Some(321));
assert_eq!(
messages[1].locations[1].filename,
Some("ui_main.cpp".to_owned())
);
assert_eq!(messages[1].locations[1].line, Some(456));
assert_eq!(
messages[1].locations[2].filename,
Some("ui_potato_viewer.cpp".to_owned())
);
assert_eq!(messages[1].locations[2].line, Some(10));
assert_eq!(
messages[1].locations[3].filename,
Some("ui_potato_viewer.cpp".to_owned())
);
assert_eq!(messages[1].locations[3].line, Some(11));
}
}

#[cfg(test)]
mod write_file_test {
use super::*;
Expand All @@ -89,12 +164,13 @@ mod write_file_test {
input_path: "whatever".to_owned(),
output_path: Some("test_result_write_to_ts.xml".to_owned()),
};
write_ts_file(&args, &data).expect("Output");
write_ts_to_output(&args, &data).expect("Output");

let f = quick_xml::Reader::from_file("test_result_write_to_ts.xml")
.expect("Couldn't open output test file");

let output_data: TSNode = quick_xml::de::from_reader(f.into_inner()).expect("Parsable");
std::fs::remove_file("test_result_write_to_ts.xml").expect("Test should clean test file.");
assert_eq!(data, output_data);
}
}
132 changes: 110 additions & 22 deletions src/ts_definition.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
// https://doc.qt.io/qt-6/linguist-ts-file-format.html
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;

// This file defines the schema matching (or trying to match?) Qt's XSD
// Eventually when a proper Rust code generator exists it would be great to use that instead.
// For now they can't handle Qt's semi-weird XSD.
// https://doc.qt.io/qt-6/linguist-ts-file-format.html

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename = "TS")]
Expand All @@ -10,8 +15,8 @@ pub struct TSNode {
source_language: Option<String>,
#[serde(rename = "@language", skip_serializing_if = "Option::is_none")]
language: Option<String>,
#[serde(rename = "context", skip_serializing_if = "Option::is_none")]
contexts: Option<Vec<ContextNode>>,
#[serde(rename = "context", skip_serializing_if = "Vec::is_empty", default)]
pub contexts: Vec<ContextNode>,
#[serde(skip_serializing_if = "Option::is_none")]
messages: Option<Vec<MessageNode>>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -26,12 +31,12 @@ pub struct TSNode {
translatorcomment: Option<String>,
}

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[derive(Debug, Eq, Deserialize, Serialize, PartialEq)]
pub struct ContextNode {
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
pub name: Option<String>,
#[serde(rename = "message")]
messages: Vec<MessageNode>,
pub messages: Vec<MessageNode>,
#[serde(skip_serializing_if = "Option::is_none")]
comment: Option<String>,
#[serde(rename = "@encoding", skip_serializing_if = "Option::is_none")]
Expand All @@ -49,16 +54,16 @@ pub struct Dependency {
catalog: String,
}

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[derive(Debug, Eq, Deserialize, Serialize, PartialEq)]
pub struct MessageNode {
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<String>,
pub source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
oldsource: Option<String>, // Result of merge
#[serde(skip_serializing_if = "Option::is_none")]
translation: Option<TranslationNode>,
#[serde(skip_serializing_if = "Option::is_none")]
location: Option<Vec<LocationNode>>,
#[serde(skip_serializing_if = "Vec::is_empty", rename = "location", default)]
pub locations: Vec<LocationNode>,
#[serde(skip_serializing_if = "Option::is_none")]
comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -76,7 +81,7 @@ pub struct MessageNode {
// todo: extra-something
}

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[derive(Debug, Eq, Deserialize, Serialize, PartialEq)]
pub struct TranslationNode {
// Did not find a way to make it an enum
// Therefore: either you have a `translation_simple` or a `numerus_forms`, but not both.
Expand All @@ -92,39 +97,122 @@ pub struct TranslationNode {
userdata: Option<String>, // deprecated
}

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[derive(Debug, Eq, Deserialize, Serialize, PartialEq)]
pub struct LocationNode {
#[serde(rename = "@line", skip_serializing_if = "Option::is_none")]
line: Option<u32>,
#[serde(rename = "@filename", skip_serializing_if = "Option::is_none")]
filename: Option<String>,
pub filename: Option<String>,
#[serde(rename = "@line", skip_serializing_if = "Option::is_none")]
pub line: Option<u32>,
}

#[derive(Debug, Deserialize, Serialize, PartialEq)]
#[derive(Debug, Eq, Deserialize, Serialize, PartialEq)]
pub struct NumerusFormNode {
#[serde(default, rename = "$value", skip_serializing_if = "String::is_empty")]
text: String,
#[serde(rename = "@variants", skip_serializing_if = "Option::is_none")]
filename: Option<String>, // "yes", "no"
}

impl PartialOrd<Self> for MessageNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let min_self = self
.locations
.iter()
.min_by_key(|location| (location.filename.as_ref(), location.line))
.map(|location| (location.filename.as_ref(), location.line.as_ref()))
.unwrap_or_default();

let min_other = other
.locations
.iter()
.min_by_key(|location| (location.filename.as_ref(), location.line))
.map(|location| (location.filename.as_ref(), location.line.as_ref()))
.unwrap_or_default();

// Counterintuitive, but we want to have locationless message at the end:
// handle `None` differently from default.
if min_self.0 == None && min_other.0 != None {
Some(Ordering::Greater)
} else if min_self.0 == min_other.0 && min_self.1 == None && min_other.1 != None {
Some(Ordering::Greater)
} else {
min_self.partial_cmp(&min_other)
}
}
}

impl Ord for MessageNode {
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(&other)
.expect("PartialOrd should always return a value for MessageNode")
}
}

impl Ord for LocationNode {
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(&other)
.expect("PartialOrd should always return a value for LocationNode")
}
}

impl PartialOrd<Self> for LocationNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match self
.filename
.as_ref()
.unwrap_or(&"".to_owned())
.to_lowercase()
.partial_cmp(
&other
.filename
.as_ref()
.unwrap_or(&"".to_owned())
.to_lowercase(),
)
.expect("LocationNode::filename should have an ordering")
{
Ordering::Less => Some(Ordering::Less),
Ordering::Greater => Some(Ordering::Greater),
Ordering::Equal => self.line.partial_cmp(&other.line),
}
}
}

impl PartialOrd<Self> for ContextNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
// Contexts are generally module or classes names; let's assume they don't need any special collation treatment.
self.name
.as_ref()
.unwrap_or(&"".to_owned())
.to_lowercase()
.partial_cmp(&other.name.as_ref().unwrap_or(&"".to_owned()).to_lowercase())
}
}

impl Ord for ContextNode {
fn cmp(&self, other: &Self) -> Ordering {
// Contexts are generally module or classes names; let's assume they don't need any special collation treatment.
self.name.cmp(&other.name)
}
}

#[cfg(test)]
mod test {
use super::*;
use quick_xml;

// TODO: Data set. https://github.com/qt/qttranslations/
#[test]
fn parse_with_numerus_forms() {
let f =
quick_xml::Reader::from_file("example1.xml").expect("Couldn't open example1 test file");

let data: TSNode = quick_xml::de::from_reader(f.into_inner()).expect("Parsable");
assert_eq!(data.contexts.as_ref().unwrap().len(), 2);
assert_eq!(data.contexts.len(), 2);
assert_eq!(data.version.unwrap(), "2.1");
assert_eq!(data.source_language.unwrap(), "en");
assert_eq!(data.language.unwrap(), "sv");

let context1 = &data.contexts.as_ref().unwrap()[0];
let context1 = &data.contexts[0];
assert_eq!(context1.name.as_ref().unwrap(), "kernel/navigationpart");
assert_eq!(context1.messages.len(), 3);

Expand Down Expand Up @@ -180,16 +268,16 @@ mod test {
.expect("Couldn't open example1 test file");

let data: TSNode = quick_xml::de::from_reader(f.into_inner()).expect("Parsable");
assert_eq!(data.contexts.as_ref().unwrap().len(), 1);
assert_eq!(data.contexts.len(), 1);
assert_eq!(data.version.unwrap(), "1.1");
assert_eq!(data.source_language, None);
assert_eq!(data.language.unwrap(), "de");

let context1 = &data.contexts.as_ref().unwrap()[0];
let context1 = &data.contexts[0];
assert_eq!(context1.name.as_ref().unwrap(), "tst_QKeySequence");
assert_eq!(context1.messages.len(), 11);
let message_c1_2 = &context1.messages[2];
let locations = message_c1_2.location.as_ref().unwrap();
let locations = &message_c1_2.locations;
assert_eq!(locations.len(), 2);
assert_eq!(
locations[0].filename.as_ref().unwrap(),
Expand Down

0 comments on commit 5aafa9f

Please sign in to comment.