mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-13 16:38:52 +08:00
wip: Add Entis GLS engine script
This commit is contained in:
@@ -333,6 +333,11 @@ pub struct Arg {
|
||||
/// Try use YAML format instead of JSON when custom exporting.
|
||||
/// By default, this is based on output type. But can be overridden by this option.
|
||||
pub custom_yaml: Option<bool>,
|
||||
#[cfg(feature = "entis-gls")]
|
||||
#[arg(long, global = true)]
|
||||
/// Entis GLS srcxml script language, used to extract messages from srcxml script.
|
||||
/// If not specified, the first language will be used.
|
||||
pub entis_gls_srcxml_lang: Option<String>,
|
||||
#[command(subcommand)]
|
||||
/// Command
|
||||
pub command: Command,
|
||||
|
||||
@@ -5,4 +5,6 @@ pub mod fancy_regex;
|
||||
pub mod io;
|
||||
#[cfg(feature = "emote-psb")]
|
||||
pub mod psb;
|
||||
#[cfg(feature = "markup5ever_rcdom")]
|
||||
pub mod rcdom;
|
||||
pub mod vec;
|
||||
|
||||
88
src/ext/rcdom.rs
Normal file
88
src/ext/rcdom.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
//! Extensions for markup5ever_rcdom crate.
|
||||
use anyhow::Result;
|
||||
use markup5ever::Attribute;
|
||||
use markup5ever_rcdom::{Node, NodeData};
|
||||
use std::cell::Ref;
|
||||
|
||||
/// Extensions for [Node]
|
||||
pub trait NodeExt {
|
||||
/// Checks if the node is an element with the given name.
|
||||
///
|
||||
/// This function ignore namespaces.
|
||||
fn is_element<S: AsRef<str> + ?Sized>(&self, name: &S) -> bool;
|
||||
/// Checks if the node is a processing instruction with the given name.
|
||||
fn is_processing_instruction<S: AsRef<str> + ?Sized>(&self, name: &S) -> bool;
|
||||
/// Returns an iterator over the attribute keys of the element node.
|
||||
///
|
||||
/// This function returns an empty iterator if the node is not an element.
|
||||
/// Only the local names of the attributes are returned, ignoring namespaces.
|
||||
fn element_attr_keys<'a>(&'a self) -> Result<Box<dyn Iterator<Item = String> + 'a>>;
|
||||
/// Gets the value of the attribute with the given name.
|
||||
///
|
||||
/// This function returns `Ok(None)` if the node is not an element or if the attribute does not exist.
|
||||
/// This function ignores namespaces and only checks the local name of the attribute.
|
||||
fn get_attr_value<S: AsRef<str> + ?Sized>(&self, name: &S) -> Result<Option<String>>;
|
||||
}
|
||||
|
||||
impl NodeExt for Node {
|
||||
fn is_element<S: AsRef<str> + ?Sized>(&self, name: &S) -> bool {
|
||||
match &self.data {
|
||||
NodeData::Element { name: ename, .. } => ename.local.as_ref() == name.as_ref(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_processing_instruction<S: AsRef<str> + ?Sized>(&self, name: &S) -> bool {
|
||||
match &self.data {
|
||||
NodeData::ProcessingInstruction { target, .. } => target.as_ref() == name.as_ref(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn element_attr_keys<'a>(&'a self) -> Result<Box<dyn Iterator<Item = String> + 'a>> {
|
||||
match &self.data {
|
||||
NodeData::Element { attrs, .. } => {
|
||||
let borrowed = attrs.try_borrow()?;
|
||||
let iter = AttrKeyIter { borrowed, pos: 0 };
|
||||
Ok(Box::new(iter))
|
||||
}
|
||||
_ => Ok(Box::new(std::iter::empty())),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_attr_value<S: AsRef<str> + ?Sized>(&self, name: &S) -> Result<Option<String>> {
|
||||
match &self.data {
|
||||
NodeData::Element { attrs, .. } => {
|
||||
let borrowed = attrs.try_borrow()?;
|
||||
if let Some(attr) = borrowed
|
||||
.iter()
|
||||
.find(|a| a.name.local.as_ref() == name.as_ref())
|
||||
{
|
||||
Ok(Some(attr.value.to_string()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AttrKeyIter<'a> {
|
||||
borrowed: Ref<'a, Vec<Attribute>>,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AttrKeyIter<'a> {
|
||||
type Item = String;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.pos < self.borrowed.len() {
|
||||
let attr = &self.borrowed[self.pos];
|
||||
self.pos += 1;
|
||||
Some(attr.name.local.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1751,6 +1751,8 @@ fn main() {
|
||||
.map(|s| s == types::OutputScriptType::Yaml)
|
||||
.unwrap_or(false)
|
||||
}),
|
||||
#[cfg(feature = "entis-gls")]
|
||||
entis_gls_srcxml_lang: arg.entis_gls_srcxml_lang.clone(),
|
||||
};
|
||||
match &arg.command {
|
||||
args::Command::Export { input, output } => {
|
||||
|
||||
2
src/scripts/entis_gls/mod.rs
Normal file
2
src/scripts/entis_gls/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
//! Entis GLS engine Script
|
||||
pub mod srcxml;
|
||||
206
src/scripts/entis_gls/srcxml.rs
Normal file
206
src/scripts/entis_gls/srcxml.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
//! Entis GLS engine XML Script (.srcxml)
|
||||
use crate::ext::io::*;
|
||||
use crate::ext::rcdom::*;
|
||||
use crate::scripts::base::*;
|
||||
use crate::types::*;
|
||||
use crate::utils::encoding::*;
|
||||
use anyhow::Result;
|
||||
use markup5ever_rcdom::{Handle, RcDom, SerializableHandle};
|
||||
use xml5ever::driver::parse_document;
|
||||
use xml5ever::serialize::serialize;
|
||||
use xml5ever::tendril::TendrilSink;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A builder for Entis GLS srcxml scripts.
|
||||
pub struct SrcXmlScriptBuilder {}
|
||||
|
||||
impl SrcXmlScriptBuilder {
|
||||
/// Creates a new instance of `SrcXmlScriptBuilder`.
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScriptBuilder for SrcXmlScriptBuilder {
|
||||
fn default_encoding(&self) -> Encoding {
|
||||
Encoding::Utf8
|
||||
}
|
||||
|
||||
fn build_script(
|
||||
&self,
|
||||
buf: Vec<u8>,
|
||||
_filename: &str,
|
||||
encoding: Encoding,
|
||||
_archive_encoding: Encoding,
|
||||
config: &ExtraConfig,
|
||||
_archive: Option<&Box<dyn Script>>,
|
||||
) -> Result<Box<dyn Script>> {
|
||||
Ok(Box::new(SrcXmlScript::new(buf, encoding, config)?))
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &'static [&'static str] {
|
||||
&["srcxml"]
|
||||
}
|
||||
|
||||
fn script_type(&self) -> &'static ScriptType {
|
||||
&ScriptType::EntisGls
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SrcXmlScript {
|
||||
decoded: String,
|
||||
handle: Handle,
|
||||
lang: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SrcXmlScript {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SrcXmlScript")
|
||||
.field("handle", &self.handle)
|
||||
.field("lang", &self.lang)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SrcXmlScript {
|
||||
pub fn new(buf: Vec<u8>, encoding: Encoding, config: &ExtraConfig) -> Result<Self> {
|
||||
let decoded = decode_to_string(encoding, &buf, false)?;
|
||||
let dom = parse_document(RcDom::default(), Default::default())
|
||||
.from_utf8()
|
||||
.one(decoded.as_bytes());
|
||||
{
|
||||
let error = dom.errors.try_borrow()?;
|
||||
for e in error.iter() {
|
||||
eprintln!("WARN: Error parsing srcxml: {}", e);
|
||||
crate::COUNTER.inc_warning();
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
decoded,
|
||||
handle: dom.document,
|
||||
lang: config.entis_gls_srcxml_lang.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Script for SrcXmlScript {
|
||||
fn default_output_script_type(&self) -> OutputScriptType {
|
||||
OutputScriptType::Json
|
||||
}
|
||||
|
||||
fn default_format_type(&self) -> FormatOptions {
|
||||
FormatOptions::None
|
||||
}
|
||||
|
||||
fn extract_messages(&self) -> Result<Vec<Message>> {
|
||||
let mut messages = Vec::new();
|
||||
let mut lang = self.lang.clone();
|
||||
for i in self.handle.children.try_borrow()?.iter() {
|
||||
if i.is_element("xscript") {
|
||||
for code in i.children.try_borrow()?.iter() {
|
||||
if code.is_element("code") {
|
||||
for ins in code.children.try_borrow()?.iter() {
|
||||
if ins.is_element("msg") {
|
||||
let lan = match lang.as_ref() {
|
||||
Some(l) => l.as_str(),
|
||||
None => {
|
||||
for attr in ins.element_attr_keys()? {
|
||||
if attr.starts_with("name_")
|
||||
|| attr.starts_with("text_")
|
||||
{
|
||||
lang = Some(attr[5..].to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
lang.as_ref().map(|s| s.as_str()).unwrap_or("")
|
||||
}
|
||||
};
|
||||
let name_ref = if lan.is_empty() {
|
||||
"name"
|
||||
} else {
|
||||
&format!("name_{}", lan)
|
||||
};
|
||||
let mut name = ins.get_attr_value(name_ref)?;
|
||||
if name.as_ref().is_some_and(|s| s.is_empty()) {
|
||||
name = None;
|
||||
}
|
||||
let text_ref = if lan.is_empty() {
|
||||
"text"
|
||||
} else {
|
||||
&format!("text_{}", lan)
|
||||
};
|
||||
let message = ins
|
||||
.get_attr_value(text_ref)?
|
||||
.ok_or(anyhow::anyhow!("text not found"))?;
|
||||
messages.push(Message { name, message })
|
||||
} else if ins.is_element("select") {
|
||||
for menu in ins.children.try_borrow()?.iter() {
|
||||
if menu.is_element("menu") {
|
||||
let lan = match lang.as_ref() {
|
||||
Some(l) => l.as_str(),
|
||||
None => {
|
||||
for attr in ins.element_attr_keys()? {
|
||||
if attr.starts_with("name_")
|
||||
|| attr.starts_with("text_")
|
||||
{
|
||||
lang = Some(attr[5..].to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
lang.as_ref().map(|s| s.as_str()).unwrap_or("")
|
||||
}
|
||||
};
|
||||
let text_ref = if lan.is_empty() {
|
||||
"text"
|
||||
} else {
|
||||
&format!("text_{}", lan)
|
||||
};
|
||||
let message = menu
|
||||
.get_attr_value(text_ref)?
|
||||
.ok_or(anyhow::anyhow!("text not found"))?;
|
||||
messages.push(Message {
|
||||
name: None,
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
fn import_messages<'a>(
|
||||
&'a self,
|
||||
_messages: Vec<Message>,
|
||||
mut file: Box<dyn WriteSeek + 'a>,
|
||||
encoding: Encoding,
|
||||
_replacement: Option<&'a ReplacementTable>,
|
||||
) -> Result<()> {
|
||||
let dom = parse_document(RcDom::default(), Default::default())
|
||||
.from_utf8()
|
||||
.one(self.decoded.as_bytes());
|
||||
let root = dom.document;
|
||||
if !encoding.is_utf8() {
|
||||
let mut childrens = root.children.try_borrow_mut()?;
|
||||
if childrens.len() > 1 && childrens[0].is_processing_instruction("xml") {
|
||||
childrens.remove(0);
|
||||
}
|
||||
}
|
||||
let doc: SerializableHandle = root.clone().into();
|
||||
let mut output = MemWriter::new();
|
||||
serialize(&mut output, &doc, Default::default())
|
||||
.map_err(|e| anyhow::anyhow!("Error serializing srcxml: {}", e))?;
|
||||
if encoding.is_utf8() {
|
||||
file.write_all(&output.data)?;
|
||||
return Ok(());
|
||||
}
|
||||
let s = decode_to_string(Encoding::Utf8, &output.data, true)?;
|
||||
let encoded = encode_string(encoding, &s, false)?;
|
||||
file.write_all(&encoded)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ pub mod bgi;
|
||||
pub mod cat_system;
|
||||
#[cfg(feature = "circus")]
|
||||
pub mod circus;
|
||||
#[cfg(feature = "entis-gls")]
|
||||
pub mod entis_gls;
|
||||
#[cfg(feature = "escude")]
|
||||
pub mod escude;
|
||||
#[cfg(feature = "ex-hibit")]
|
||||
@@ -102,6 +104,8 @@ lazy_static::lazy_static! {
|
||||
Box::new(circus::image::crxd::CrxdImageBuilder::new()),
|
||||
#[cfg(feature = "bgi-audio")]
|
||||
Box::new(bgi::audio::audio::BgiAudioBuilder::new()),
|
||||
#[cfg(feature = "entis-gls")]
|
||||
Box::new(entis_gls::srcxml::SrcXmlScriptBuilder::new()),
|
||||
];
|
||||
/// A list of all script extensions.
|
||||
pub static ref ALL_EXTS: Vec<String> =
|
||||
|
||||
17
src/types.rs
17
src/types.rs
@@ -36,6 +36,16 @@ impl Encoding {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the encoding is UTF8.
|
||||
pub fn is_utf8(&self) -> bool {
|
||||
match self {
|
||||
Self::Utf8 => true,
|
||||
#[cfg(windows)]
|
||||
Self::CodePage(code_page) => *code_page == 65001,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)]
|
||||
@@ -325,6 +335,10 @@ pub struct ExtraConfig {
|
||||
pub circus_crx_canvas: bool,
|
||||
/// Try use YAML format instead of JSON when custom exporting.
|
||||
pub custom_yaml: bool,
|
||||
#[cfg(feature = "entis-gls")]
|
||||
/// Entis GLS srcxml script language, used to extract messages from srcxml script.
|
||||
/// If not specified, the first language will be used.
|
||||
pub entis_gls_srcxml_lang: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)]
|
||||
@@ -409,6 +423,9 @@ pub enum ScriptType {
|
||||
#[cfg(feature = "circus-img")]
|
||||
/// Circus Differential Image
|
||||
CircusCrxd,
|
||||
#[cfg(feature = "entis-gls")]
|
||||
/// Entis GLS srcxml Script
|
||||
EntisGls,
|
||||
#[cfg(feature = "escude-arc")]
|
||||
/// Escude bin archive
|
||||
EscudeArc,
|
||||
|
||||
Reference in New Issue
Block a user