mirror of
https://github.com/neocturne/MinedMap.git
synced 2025-03-04 17:23:33 +01:00
world: implement decoding raw JSON text into a linear list of formatted strings
This commit is contained in:
parent
f78dd795ca
commit
61d456846a
3 changed files with 238 additions and 1 deletions
|
@ -1,7 +1,155 @@
|
|||
//! Newtype and helper methods for handling Minecraft Raw JSON Text
|
||||
|
||||
use std::{collections::VecDeque, sync::Arc};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
/// A span of formatted text
|
||||
///
|
||||
/// A [JSONText] consists of a tree of [FormattedText] nodes (canonically
|
||||
/// represented as a [FormattedTextTree], but other kinds are possible with
|
||||
/// is handled by [DeserializedText].
|
||||
///
|
||||
/// Formatting that is not set in a node is inherited from the parent.
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct FormattedText {
|
||||
#[serde(default)]
|
||||
/// Text content
|
||||
pub text: String,
|
||||
/// Text color
|
||||
pub color: Option<Arc<String>>,
|
||||
/// Bold formatting
|
||||
pub bold: Option<bool>,
|
||||
/// Italic formatting
|
||||
pub italic: Option<bool>,
|
||||
/// Underlines formatting
|
||||
pub underlined: Option<bool>,
|
||||
/// Strikethrough formatting
|
||||
pub strikethrough: Option<bool>,
|
||||
/// Obfuscated formatting
|
||||
pub obfuscated: Option<bool>,
|
||||
}
|
||||
|
||||
impl FormattedText {
|
||||
/// Fills in unset formatting fields from a parent node
|
||||
pub fn inherit(self, parent: &Self) -> Self {
|
||||
FormattedText {
|
||||
text: self.text,
|
||||
color: self.color.or_else(|| parent.color.clone()),
|
||||
bold: self.bold.or(parent.bold),
|
||||
italic: self.italic.or(parent.italic),
|
||||
underlined: self.underlined.or(parent.underlined),
|
||||
strikethrough: self.strikethrough.or(parent.strikethrough),
|
||||
obfuscated: self.obfuscated.or(parent.obfuscated),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A tree of [FormattedText] nodes
|
||||
///
|
||||
/// Each node including the root has a `text` and a list of children (`extra`).
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct FormattedTextTree {
|
||||
/// Root node content
|
||||
#[serde(flatten)]
|
||||
text: FormattedText,
|
||||
/// List of child trees
|
||||
#[serde(default)]
|
||||
extra: VecDeque<DeserializedText>,
|
||||
}
|
||||
|
||||
impl From<String> for FormattedTextTree {
|
||||
fn from(value: String) -> Self {
|
||||
FormattedTextTree {
|
||||
text: FormattedText {
|
||||
text: value,
|
||||
..Default::default()
|
||||
},
|
||||
extra: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List of [FormattedText]
|
||||
#[derive(Debug)]
|
||||
pub struct FormattedTextList(pub Vec<FormattedText>);
|
||||
|
||||
impl FormattedTextList {
|
||||
/// Returns `true` when [FormattedTextList] does not contain any text
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.iter().all(|text| text.text.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw deserialized [JSONText]
|
||||
///
|
||||
/// A [JSONText] can contain various different JSON types.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum DeserializedText {
|
||||
/// Unformatted string
|
||||
String(String),
|
||||
/// Unformatted number (will be converted to a string)
|
||||
Number(f32),
|
||||
/// Unformatted boolean (will be converted to a string)
|
||||
Boolean(bool),
|
||||
/// List of [DeserializedText]
|
||||
///
|
||||
/// The tail elements are appended as children of the head element.
|
||||
List(VecDeque<DeserializedText>),
|
||||
/// The canonical [FormattedTextTree] structure
|
||||
Object(FormattedTextTree),
|
||||
}
|
||||
|
||||
impl DeserializedText {
|
||||
/// Converts a [DeserializedText] into the regular [FormattedTextTree] format
|
||||
///
|
||||
/// Most variants are simply converted to strings. A list is handled by
|
||||
/// appending all tail elements to the `extra` field of the head.
|
||||
pub fn canonicalize(self) -> FormattedTextTree {
|
||||
match self {
|
||||
DeserializedText::Object(obj) => obj,
|
||||
DeserializedText::String(s) => FormattedTextTree::from(s),
|
||||
DeserializedText::Number(n) => FormattedTextTree::from(n.to_string()),
|
||||
DeserializedText::Boolean(b) => FormattedTextTree::from(b.to_string()),
|
||||
DeserializedText::List(mut list) => {
|
||||
let mut obj = list
|
||||
.pop_front()
|
||||
.map(|t| t.canonicalize())
|
||||
.unwrap_or_default();
|
||||
obj.extra.append(&mut list);
|
||||
obj
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the tree of [FormattedText] nodes into a linear list by
|
||||
/// copying formatting flags into each node.
|
||||
pub fn linearize(self, parent: &FormattedText) -> FormattedTextList {
|
||||
let obj = self.canonicalize();
|
||||
let mut ret = vec![obj.text.inherit(parent)];
|
||||
|
||||
for extra in obj.extra {
|
||||
ret.append(&mut extra.linearize(&ret[0]).0);
|
||||
}
|
||||
|
||||
FormattedTextList(ret)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DeserializedText {
|
||||
fn default() -> Self {
|
||||
DeserializedText::Object(FormattedTextTree::from(String::new()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Minecraft Raw JSON Text
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct JSONText(String);
|
||||
pub struct JSONText(pub String);
|
||||
|
||||
impl JSONText {
|
||||
/// Deserializes a [JSONText] into a [DeserializedText]
|
||||
pub fn deserialize(&self) -> DeserializedText {
|
||||
serde_json::from_str(&self.0).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,3 +5,4 @@ pub mod de;
|
|||
pub mod json_text;
|
||||
pub mod layer;
|
||||
pub mod section;
|
||||
pub mod sign;
|
||||
|
|
88
src/world/sign.rs
Normal file
88
src/world/sign.rs
Normal file
|
@ -0,0 +1,88 @@
|
|||
//! Processing of sign text
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{
|
||||
de,
|
||||
json_text::{FormattedText, FormattedTextList, JSONText},
|
||||
};
|
||||
|
||||
/// Version-independent reference to (front or back) sign text
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RawSignText<'a> {
|
||||
/// Lines of sign text
|
||||
///
|
||||
/// A regular sign always has 4 lines of text. The back of pre-1.20
|
||||
/// signs is represented as a [SignText] without any `messages`.
|
||||
pub messages: Vec<&'a JSONText>,
|
||||
/// Sign color
|
||||
///
|
||||
/// Defaults to "black".
|
||||
pub color: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> RawSignText<'a> {
|
||||
/// Decodes the [RawSignText] into a [SignText]
|
||||
pub fn decode(&self) -> SignText {
|
||||
let color = self.color.map(|c| Arc::new(c.to_owned()));
|
||||
let parent = FormattedText {
|
||||
color,
|
||||
..Default::default()
|
||||
};
|
||||
SignText(
|
||||
self.messages
|
||||
.iter()
|
||||
.map(|message| message.deserialize().linearize(&parent))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a de::BlockEntitySignV1_20Text> for RawSignText<'a> {
|
||||
fn from(value: &'a de::BlockEntitySignV1_20Text) -> Self {
|
||||
RawSignText {
|
||||
messages: value.messages.iter().collect(),
|
||||
color: value.color.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper methods for [de::BlockEntitySign]
|
||||
pub trait BlockEntitySignExt {
|
||||
/// Returns the front and back text of a sign in a version-indepentent format
|
||||
fn text(&self) -> (RawSignText, RawSignText);
|
||||
}
|
||||
|
||||
impl BlockEntitySignExt for de::BlockEntitySign {
|
||||
fn text(&self) -> (RawSignText, RawSignText) {
|
||||
match self {
|
||||
de::BlockEntitySign::V0 {
|
||||
text1,
|
||||
text2,
|
||||
text3,
|
||||
text4,
|
||||
color,
|
||||
} => (
|
||||
RawSignText {
|
||||
messages: vec![text1, text2, text3, text4],
|
||||
color: color.as_deref(),
|
||||
},
|
||||
Default::default(),
|
||||
),
|
||||
de::BlockEntitySign::V1_20 {
|
||||
front_text,
|
||||
back_text,
|
||||
} => (front_text.into(), back_text.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialized and linearized sign text
|
||||
pub struct SignText(pub Vec<FormattedTextList>);
|
||||
|
||||
impl SignText {
|
||||
/// Checks if all lines of the sign text are empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.iter().all(|line| line.is_empty())
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue