Skip to content

Commit

Permalink
Merge branch 'strings'
Browse files Browse the repository at this point in the history
  • Loading branch information
ackwell committed Aug 23, 2024
2 parents 51ed033 + 3f8552f commit 3f31469
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 14 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

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

13 changes: 11 additions & 2 deletions src/http/api1/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,18 @@ use super::error;
/// field. Allows one query to access data for multiple languages. `language`
/// accepts any valid `LanguageString`.
///
/// - `@as(raw)`: Prevents further processing, such as sheet relations, being
/// - `@as(<format>)`: Overrides the default output format for the decorated
/// field.
///
/// Currently accepted `format`s for `@as`:
///
/// - `raw`: Prevents further processing, such as sheet relations, being
/// performed on the decorated field. Has no effect on regular scalar fields.
///
/// - `html`: Formats a string field as rich HTML. Invalid on non-string
/// fields. Output will be a valid HTML fragment, however no stability
/// guarantees are made over the precise markup used.
///
/// Nested fields may be selected using dot notation, i.e. `a.b` will select the
/// field `b` contained in the struct `a`.
///
Expand Down Expand Up @@ -348,8 +357,8 @@ fn language(input: &str) -> IResult<&str, excel::Language> {

fn read_as(input: &str) -> IResult<&str, read::As> {
alt((
//
value(read::As::Raw, tag("raw")),
value(read::As::Html, tag("html")),
))(input)
}

Expand Down
1 change: 1 addition & 0 deletions src/http/api1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod query;
mod read;
mod search;
mod sheet;
mod string;
mod value;
mod version;

Expand Down
45 changes: 41 additions & 4 deletions src/http/api1/read.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::{collections::HashMap, sync::Arc};
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};

use aide::OperationIo;
use anyhow::anyhow;
Expand All @@ -8,16 +11,17 @@ use axum::{
http::request::Parts,
Extension, RequestPartsExt,
};
use ironworks::{excel, file::exh};
use ironworks::{excel, file::exh, sestring::format::Input};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::{http::service, read, schema, utility::anyhow::Anyhow};
use crate::{http::service, read, schema, utility::anyhow::Anyhow, version::VersionKey};

use super::{
error::{Error, Result},
extract::{Query, VersionQuery},
filter::FilterString,
string::build_input,
value::ValueString,
};

Expand Down Expand Up @@ -74,13 +78,35 @@ impl RowResult {
read::Value::Scalar(excel::Field::U32(14)),
)])),
excel::Language::English,
Input::new().into(),
),
// TODO: should this have an example?
transient: None,
}
}
}

#[derive(Debug, Default, Clone)]
pub struct RowReaderState {
string_input: Arc<RwLock<HashMap<VersionKey, Arc<Input>>>>,
}

impl RowReaderState {
fn input(&self, version: VersionKey, excel: &excel::Excel) -> Result<Arc<Input>> {
let inputs = self.string_input.read().expect("poisoned");
if let Some(input) = inputs.get(&version) {
return Ok(input.clone());
}

drop(inputs);
let mut inputs_mut = self.string_input.write().expect("poisoned");
let input = Arc::new(build_input(excel)?);
inputs_mut.insert(version, input.clone());

Ok(input)
}
}

#[derive(OperationIo)]
#[aide(input_with = "Query<RowReaderQuery>")]
pub struct RowReader {
Expand All @@ -91,6 +117,7 @@ pub struct RowReader {
pub language: excel::Language,
fields: read::Filter,
transient: Option<read::Filter>,
string_input: Arc<Input>,
}

// todo maybe an extra bit of state requirements on this for the filters? that would allow the filters to be wired up per-handler i think. not sure how that aligns with existing state though
Expand Down Expand Up @@ -119,6 +146,12 @@ where
.await
.map_err(|error| Error::Other(error.into()))?;

// TODO: again - how can i get this in the typechecked state?
let Extension(state) = parts
.extract::<Extension<RowReaderState>>()
.await
.map_err(|error| Error::Other(error.into()))?;

let excel = data.version(version_key)?.excel();

// TODO: should this be a bit like versionquery for the schema shit?
Expand All @@ -129,6 +162,8 @@ where
.map(excel::Language::from)
.unwrap_or_else(|| read.default_language());

let string_input = state.input(version_key, &excel)?;

let fields = query
.fields
.or_else(|| config.fields.get(&schema_specifier.source).cloned())
Expand All @@ -155,6 +190,7 @@ where
language,
fields,
transient,
string_input,
})
}
}
Expand All @@ -180,6 +216,7 @@ impl RowReader {
depth,
)?,
self.language,
self.string_input.clone(),
);

// Try to read a transient row.
Expand All @@ -195,7 +232,7 @@ impl RowReader {
filter,
depth,
) {
Ok(value) => Some(ValueString(value, self.language)),
Ok(value) => Some(ValueString(value, self.language, self.string_input.clone())),
Err(read::Error::NotFound(_)) => None,
Err(error) => Err(error)?,
},
Expand Down
3 changes: 2 additions & 1 deletion src/http/api1/sheet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::{
use super::{
error::{Error, Result},
extract::{Path, Query, VersionQuery},
read::{RowReader, RowReaderConfig, RowResult},
read::{RowReader, RowReaderConfig, RowReaderState, RowResult},
};

#[derive(Debug, Clone, Deserialize)]
Expand All @@ -48,6 +48,7 @@ pub fn router(config: Config) -> ApiRouter<service::State> {
.layer(Extension(config.list))
.api_route("/:sheet/:row", get_with(row, row_docs))
.layer(Extension(config.entry))
.layer(Extension(RowReaderState::default()))
.layer(Extension(config.limit))
}

Expand Down
87 changes: 87 additions & 0 deletions src/http/api1/string.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::sync::OnceLock;

use aho_corasick::AhoCorasick;
use anyhow::Context;
use ironworks::{
excel::Excel,
sestring::{
format::{format, Color, ColorUsage, Input, Write},
Error as SeStringError, SeString,
},
};

use super::error::Result;

pub fn build_input(excel: &Excel) -> Result<Input> {
let mut input = Input::new();

// Someone put a bullet through me if I need to make this configurable.
let sheet = excel
.sheet("UIColor")
.context("failed to read UIColor sheet")?;

for row in sheet.into_iter() {
let [r, g, b, a] = row
.field(0)
.ok()
.and_then(|field| field.into_u32().ok())
.context("failed to read color field")?
.to_be_bytes();

input.add_color(ColorUsage::Foreground, row.row_id(), Color { r, g, b, a });
}

Ok(input)
}

pub fn as_html(string: SeString, input: &Input) -> Result<String, SeStringError> {
let mut writer = HtmlWriter::default();
format(string, input, &mut writer)?;
Ok(writer.buffer)
}

#[derive(Debug, Default)]
struct HtmlWriter {
buffer: String,
}

impl Write for HtmlWriter {
fn write_str(&mut self, str: &str) -> Result<(), SeStringError> {
// Probaly overkill for this but it'll be nice if I add more replacements.
static PATTERN: OnceLock<AhoCorasick> = OnceLock::new();
let pattern = PATTERN.get_or_init(|| {
AhoCorasick::new(["\n"]).expect("pattern construction should not fail")
});

let output = pattern.replace_all(str, &["<br>"]);

self.buffer.push_str(&output);
Ok(())
}

// Only paying attention to foreground for now. Anything more involved will
// need additional tracking for stacks and a lot more spans.

fn push_color(&mut self, usage: ColorUsage, color: Color) -> Result<(), SeStringError> {
if usage != ColorUsage::Foreground {
return Ok(());
}

let Color { r, g, b, a } = color;
let a = f32::from(a) / 255.;
self.buffer
.push_str(&format!(r#"<span style="color:rgba({r},{g},{b},{a});">"#));

Ok(())
}

fn pop_color(&mut self, usage: ColorUsage) -> Result<(), SeStringError> {
if usage != ColorUsage::Foreground {
return Ok(());
}

self.buffer.push_str("</span>");

Ok(())
}
}
33 changes: 29 additions & 4 deletions src/http/api1/value.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};

use ironworks::excel;
use ironworks::{excel, sestring};
use schemars::{
gen::SchemaGenerator,
schema::{InstanceType, Schema, SchemaObject},
};
use serde::ser::{Serialize, SerializeMap, SerializeSeq, SerializeStruct};
use serde::ser::{Error as SerError, Serialize, SerializeMap, SerializeSeq, SerializeStruct};

use crate::{read, utility::jsonschema::impl_jsonschema};

use super::string;

#[derive(Debug)]
pub struct ValueString(pub read::Value, pub excel::Language);
pub struct ValueString(
pub read::Value,
pub excel::Language,
pub Arc<sestring::format::Input>,
);

impl Serialize for ValueString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
Expand All @@ -20,6 +26,7 @@ impl Serialize for ValueString {
ValueReference {
value: &self.0,
language: self.1,
string_input: &self.2,
}
.serialize(serializer)
}
Expand All @@ -36,6 +43,7 @@ fn valuestring_schema(_generator: &mut SchemaGenerator) -> Schema {
struct ValueReference<'a> {
value: &'a read::Value,
language: excel::Language,
string_input: &'a sestring::format::Input,
}

impl Serialize for ValueReference<'_> {
Expand All @@ -46,6 +54,7 @@ impl Serialize for ValueReference<'_> {
use read::Value as V;
match self.value {
V::Array(values) => self.serialize_array(serializer, values),
V::Html(string) => self.serialize_html(serializer, string),
V::Icon(id) => self.serialize_icon(serializer, *id),
V::Reference(reference) => self.serialize_reference(serializer, reference),
V::Scalar(field) => self.serialize_scalar(serializer, field),
Expand All @@ -64,11 +73,25 @@ impl ValueReference<'_> {
sequence.serialize_element(&ValueReference {
value,
language: self.language,
string_input: self.string_input,
})?;
}
sequence.end()
}

fn serialize_html<S>(
&self,
serializer: S,
string: &sestring::SeString,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let output =
string::as_html(string.as_ref(), self.string_input).map_err(SerError::custom)?;
serializer.serialize_str(&output)
}

fn serialize_icon<S>(&self, serializer: S, id: i32) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
Expand Down Expand Up @@ -124,6 +147,7 @@ impl ValueReference<'_> {
&ValueReference {
value: fields,
language: self.language,
string_input: self.string_input,
},
)?;
state.end()
Expand Down Expand Up @@ -171,6 +195,7 @@ impl ValueReference<'_> {
&ValueReference {
value,
language: self.language,
string_input: self.string_input,
},
)?;
}
Expand Down
5 changes: 5 additions & 0 deletions src/read/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ pub struct StructEntry {
pub enum As {
Default,
Raw,
// NOTE: Passing this through read (and presumably json in future) really
// kinda reeks, but the alternative is having api1 store html/json state in
// some tree other than a filter while it gets read, which also kinda sucks.
// Would need some intermediary format.
Html,
}
Loading

0 comments on commit 3f31469

Please sign in to comment.