wip: Add Entis GLS engine script

This commit is contained in:
2025-08-11 17:31:37 +08:00
parent a26498e381
commit 2be39c6ca7
11 changed files with 588 additions and 4 deletions

View File

@@ -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,

View File

@@ -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
View 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
}
}
}

View File

@@ -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 } => {

View File

@@ -0,0 +1,2 @@
//! Entis GLS engine Script
pub mod srcxml;

View 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(())
}
}

View File

@@ -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> =

View File

@@ -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,