Skip to content

Commit

Permalink
Export LiveSplit plugin (#191)
Browse files Browse the repository at this point in the history
* start working on livesplit logic

* split export implementation

* update documentation

* clean up

* update docs

* more clean up
  • Loading branch information
Pistonight authored Feb 16, 2024
1 parent 6ef1fd3 commit e42b82d
Show file tree
Hide file tree
Showing 27 changed files with 577 additions and 120 deletions.
28 changes: 15 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions compiler-base/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ serde = { version = "1.0.193", features = ["derive"] }
uni-path = "1.51.1"
serde_json = "1.0.108"
serde_yaml = "0.9.27"
base64 = "0.21.5"
base64 = "0.21.7"
regen-lang = "0.0.7"
tokio = { version = "1.32.0", features = [
tokio = { version = "1.36.0", features = [
"macros",
], optional = true}

Expand Down
2 changes: 2 additions & 0 deletions compiler-base/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

mod string_map;
pub use string_map::*;
mod xml_escape;
pub use xml_escape::*;

// re-exports
pub use uni_path::{Component, Path, PathBuf};
110 changes: 110 additions & 0 deletions compiler-base/src/util/xml_escape.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use std::borrow::Cow;

macro_rules! escape {
($escaped:ident, $bytes:literal, $s:ident, $i:ident, $len:literal) => {
match &mut $escaped {
None => {
let mut vec = Vec::with_capacity($s.len() + $len);
vec.extend_from_slice(&$s.as_bytes()[0..$i]);
vec.extend_from_slice($bytes);
$escaped = Some(vec);
}
Some(vec) => vec.extend_from_slice($bytes),
}
};
}

/// Escapes a string for XML.
///
/// This function escapes the following characters:
/// - `&` becomes `&`
/// - `<` becomes `&lt;`
/// - `>` becomes `&gt;`
/// - `"` becomes `&quot;`
/// - `'` becomes `&apos;`
pub fn xml_escape(s: &str) -> Cow<str> {
// An ASCII byte always represent a ASCII character
// so it is safe to treat the input as bytes
let mut escaped = None;
for (i, b) in s.bytes().enumerate() {
match b {
b'&' => {
escape!(escaped, b"&amp;", s, i, 5);
}
b'<' => {
escape!(escaped, b"&lt;", s, i, 5);
}
b'>' => {
escape!(escaped, b"&gt;", s, i, 5);
}
b'\'' => {
escape!(escaped, b"&apos;", s, i, 5);
}
b'"' => {
escape!(escaped, b"&quot;", s, i, 5);
}
_ => {
if let Some(vec) = &mut escaped {
vec.push(b);
}
}
}
}
match escaped {
Some(vec) => Cow::Owned(String::from_utf8(vec).unwrap()),
None => Cow::Borrowed(s),
}
}

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

#[test]
fn no_escape() {
let input = "This is a test.";
let expected = Cow::Borrowed(input);
let escaped = xml_escape(input);
assert_eq!(escaped, expected);
}

#[test]
fn no_escape_unicode() {
let input = "\u{0926}This is a test.";
let expected = Cow::Borrowed(input);
let escaped = xml_escape(input);
assert_eq!(escaped, expected);
}

#[test]
fn escape_amp() {
let input = "\u{4f60}&\u{597d}& Chips";
let expected: Cow<str> = Cow::Owned(String::from("\u{4f60}&amp;\u{597d}&amp; Chips"));
let escaped = xml_escape(input);
assert_eq!(escaped, expected);
}

#[test]
fn escape_lt_gt() {
let input = "2 < 3 and 3 > 2";
let expected: Cow<str> = Cow::Owned(String::from("2 &lt; 3 and 3 &gt; 2"));
let escaped = xml_escape(input);
assert_eq!(escaped, expected);
}

#[test]
fn escape_quotes() {
let input = r#"She said, "Hello, world!""#;
let expected: Cow<str> = Cow::Owned(String::from("She said, &quot;Hello, world!&quot;"));
let escaped = xml_escape(input);
assert_eq!(escaped, expected);
}

#[test]
fn escape_apos() {
let input = "It's a sunny day";
let expected: Cow<str> = Cow::Owned(String::from("It&apos;s a sunny day"));
let escaped = xml_escape(input);
assert_eq!(escaped, expected);
}
}
4 changes: 3 additions & 1 deletion compiler-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ serde = { version = "1.0.174", features = ["derive"] }
serde_json = "1.0.103"
serde_yaml = "0.9.25"
thiserror = "1.0.47"
tokio = { version = "1.32.0", features = [
tokio = { version = "1.36.0", features = [
"macros",
], optional = true}
instant = "0.1.12"
log = "0.4.20"
roman = "0.1.6"
flate2 = "1.0.28"
base64 = "0.21.7"

[dev-dependencies]
map-macro = "0.2.6"
Expand Down
33 changes: 0 additions & 33 deletions compiler-core/src/exec/exec_line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ pub struct ExecLine {
pub counter_text: Option<DocRichTextBlock>,
/// The notes
pub notes: Vec<DocNote>,
/// The split name, if different from text
pub split_name: Option<String>,
/// If the line text is a banner
pub is_banner: bool,
}
Expand Down Expand Up @@ -130,7 +128,6 @@ impl CompLine {
});
}

let split_name = self.split_name.map(|x| x.to_string());
ExecLine {
is_banner: self.is_banner,
section: section_number,
Expand All @@ -143,7 +140,6 @@ impl CompLine {
counter_text: self.counter_text,
notes: self.notes,
map_coords,
split_name,
}
}
}
Expand Down Expand Up @@ -462,35 +458,6 @@ mod test {
);
}

#[test]
fn test_split_name() {
let test_split_name = DocRichText(vec![
DocRichTextBlock {
tag: None,
text: "test1".to_string(),
link: None,
},
DocRichTextBlock {
tag: Some("something".to_string()),
text: " test ".to_string(),
link: None,
},
DocRichTextBlock {
tag: None,
text: "test3".to_string(),
link: None,
},
]);

let test_line = CompLine {
split_name: Some(test_split_name.clone()),
..Default::default()
};

let exec_line = test_line.exec(&Default::default(), 0, 0, &mut MapBuilder::default());
assert_eq!(exec_line.split_name.unwrap(), "test1 test test3");
}

#[test]
fn test_missing_icons() {
let test_line = CompLine {
Expand Down
44 changes: 44 additions & 0 deletions compiler-core/src/expo/blob.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use std::io;
use std::io::Write;

use base64::Engine;
use flate2::write::GzEncoder;
use flate2::Compression;

use crate::macros::derive_wasm;

/// The data in the export
#[derive(Debug, Clone)]
#[derive_wasm]
#[serde(tag = "type", content = "data")]
pub enum ExpoBlob {
/// UTF-8 text
Text(String),
/// Binary data encoded in base64
Base64(String),
/// Binary data gzipped and encoded in base64
Base64Gzip(String),
}

impl ExpoBlob {
/// Create a blob from UTF-8 string
pub fn from_utf8(s: String) -> Self {
Self::Text(s)
}

/// Create a blob from binary data, uncompressed
///
/// Useful if the binary data is small or is already compressed
pub fn from_bytes(b: &[u8]) -> Self {
let encoded = base64::engine::general_purpose::STANDARD.encode(b);
Self::Base64(encoded)
}

/// Create a blob from binary data, compressed with gzip format
pub fn from_bytes_gzipped(b: Vec<u8>) -> io::Result<Self> {
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&b)?;
let bytes = encoder.finish()?;
Ok(Self::from_bytes(&bytes))
}
}
20 changes: 12 additions & 8 deletions compiler-core/src/expo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ use crate::comp::CompDoc;
use crate::exec::ExecContext;
use crate::macros::derive_wasm;

mod blob;
pub use blob::*;

/// Output of the export phase
#[derive_wasm]
pub struct ExpoContext<'p> {
Expand Down Expand Up @@ -90,6 +93,7 @@ pub struct ExportRequest {
#[derive_wasm]
pub enum ExpoDoc {
/// Success output. Contains file name and the bytes
#[serde(rename_all = "camelCase")]
Success {
file_name: String,
file_content: ExpoBlob,
Expand All @@ -98,14 +102,14 @@ pub enum ExpoDoc {
Error(String),
}

/// The data in the export
#[derive(Debug, Clone)]
#[derive_wasm]
pub enum ExpoBlob {
/// UTF-8 text
Text(String),
/// Binary data encoded in base64
Base64(String),
#[macro_export]
macro_rules! export_error {
($msg:literal) => {
Ok(Some(ExpoDoc::Error($msg.to_string())))
};
($msg:expr) => {
Ok(Some(ExpoDoc::Error($msg)))
};
}

impl<'p> ExecContext<'p> {
Expand Down
Loading

0 comments on commit e42b82d

Please sign in to comment.