├── .gitignore ├── Cargo.toml ├── extract.sh ├── pack.sh ├── README.md ├── Cargo.lock └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | aaa.py -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "artemis_ast" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | serde = "1.0" 10 | serde_yaml = "0.9" 11 | clap = { version = "4.4.2", features = ["derive"] } 12 | anyhow = { version = "*", features = ["backtrace"] } 13 | -------------------------------------------------------------------------------- /extract.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if an input folder is provided 4 | if [ "$#" -ne 1 ]; then 5 | echo "Usage: $0 /path/to/input/folder" 6 | exit 1 7 | fi 8 | 9 | INPUT_DIR="$1" 10 | 11 | # Check if input folder exists 12 | if [ ! -d "$INPUT_DIR" ]; then 13 | echo "The provided input directory does not exist." 14 | exit 2 15 | fi 16 | 17 | # Loop through each .ast file in the input folder 18 | find "$INPUT_DIR" -type f -name "*.ast" | while read -r ast_file; do 19 | # Construct the output file path with .yaml extension 20 | output_file="${ast_file%.*}.yaml" 21 | 22 | # Call the artemis_ast tool 23 | ./target/release/artemis_ast extract "$ast_file" "$output_file" 24 | done 25 | -------------------------------------------------------------------------------- /pack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if an input folder is provided 4 | if [ "$#" -ne 2 ]; then 5 | echo "Usage: $0 /path/to/input/folder /output/folder" 6 | exit 1 7 | fi 8 | 9 | INPUT_DIR="$1" 10 | OUTPUT_DIR="$2" 11 | 12 | mkdir -p "$OUTPUT_DIR" 13 | 14 | # Check if input folder exists 15 | if [ ! -d "$INPUT_DIR" ]; then 16 | echo "The provided input directory does not exist." 17 | exit 2 18 | fi 19 | 20 | # Loop through each .ast file in the input folder 21 | find "$INPUT_DIR" -type f -name "*.ast" | while read -r ast_file; do 22 | # Construct the output file path with .yaml extension 23 | yaml_file="${ast_file%.*}.cn" 24 | 25 | filename=$(basename "$ast_file") 26 | new_path="$OUTPUT_DIR/$filename" 27 | 28 | # Call the artemis_ast tool 29 | ./target/release/artemis_ast merge "$ast_file" "$yaml_file" "$new_path" 30 | done 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Artemis AST Script Processor 2 | 3 | This utility offers a set of Rust functions to parse and manipulate Artemis AST-based scripts. 4 | 5 | ## Features 6 | 7 | 1. **Tokenization**: Converts raw script into a series of tokens. 8 | 2. **Parsing**: Transforms tokens into a structured AST represented as a HashMap. 9 | 3. **AST Pruning**: Removes unnecessary elements from the AST for a cleaner representation. 10 | 4. **Script Generation**: Converts the modified AST back into its script form. 11 | 5. **Scenario Extraction and Replacement**: Aids in replacing parts of the script based on your requirements. 12 | 13 | ## Getting Started 14 | 15 | ### Build 16 | 17 | 1. **Clone the Repository**: 18 | 19 | ```bash 20 | git clone https://github.com/your_username/ast-script-processor.git 21 | cd ast-script-processor 22 | ``` 23 | 24 | 2. **Build the Project**: 25 | 26 | With Rust installed, building is as simple as: 27 | 28 | ```bash 29 | cargo build --release 30 | ``` 31 | 32 | This will create an optimized executable in the `target/release` directory. 33 | 34 | ### Usage 35 | 36 | 1. Parse the AST: 37 | 38 | ```rust 39 | let ast = parse_ast("path/to/script.txt").unwrap(); 40 | ``` 41 | 42 | 2. Prune the AST: 43 | 44 | ```rust 45 | prune_ast(&mut ast); 46 | ``` 47 | 48 | 3. Convert the AST back to a script: 49 | 50 | ```rust 51 | let script = hashmap_to_script(&ast).unwrap(); 52 | ``` 53 | 54 | 4. Extract and replace scenario: 55 | 56 | ```rust 57 | replace_secnario(&ast, vec!["11", "22"]).unwrap(); 58 | ``` 59 | 60 | 61 | ## License 62 | 63 | [MIT](https://choosealicense.com/licenses/mit/) 64 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.5.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "utf8parse", 32 | ] 33 | 34 | [[package]] 35 | name = "anstyle" 36 | version = "1.0.3" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" 39 | 40 | [[package]] 41 | name = "anstyle-parse" 42 | version = "0.2.1" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" 45 | dependencies = [ 46 | "utf8parse", 47 | ] 48 | 49 | [[package]] 50 | name = "anstyle-query" 51 | version = "1.0.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 54 | dependencies = [ 55 | "windows-sys", 56 | ] 57 | 58 | [[package]] 59 | name = "anstyle-wincon" 60 | version = "2.1.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" 63 | dependencies = [ 64 | "anstyle", 65 | "windows-sys", 66 | ] 67 | 68 | [[package]] 69 | name = "anyhow" 70 | version = "1.0.75" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 73 | dependencies = [ 74 | "backtrace", 75 | ] 76 | 77 | [[package]] 78 | name = "artemis_ast" 79 | version = "0.1.0" 80 | dependencies = [ 81 | "anyhow", 82 | "clap", 83 | "serde", 84 | "serde_yaml", 85 | ] 86 | 87 | [[package]] 88 | name = "backtrace" 89 | version = "0.3.69" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 92 | dependencies = [ 93 | "addr2line", 94 | "cc", 95 | "cfg-if", 96 | "libc", 97 | "miniz_oxide", 98 | "object", 99 | "rustc-demangle", 100 | ] 101 | 102 | [[package]] 103 | name = "cc" 104 | version = "1.0.83" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 107 | dependencies = [ 108 | "libc", 109 | ] 110 | 111 | [[package]] 112 | name = "cfg-if" 113 | version = "1.0.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 116 | 117 | [[package]] 118 | name = "clap" 119 | version = "4.4.2" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" 122 | dependencies = [ 123 | "clap_builder", 124 | "clap_derive", 125 | ] 126 | 127 | [[package]] 128 | name = "clap_builder" 129 | version = "4.4.2" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" 132 | dependencies = [ 133 | "anstream", 134 | "anstyle", 135 | "clap_lex", 136 | "strsim", 137 | ] 138 | 139 | [[package]] 140 | name = "clap_derive" 141 | version = "4.4.2" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" 144 | dependencies = [ 145 | "heck", 146 | "proc-macro2", 147 | "quote", 148 | "syn", 149 | ] 150 | 151 | [[package]] 152 | name = "clap_lex" 153 | version = "0.5.1" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" 156 | 157 | [[package]] 158 | name = "colorchoice" 159 | version = "1.0.0" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 162 | 163 | [[package]] 164 | name = "equivalent" 165 | version = "1.0.1" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 168 | 169 | [[package]] 170 | name = "gimli" 171 | version = "0.28.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 174 | 175 | [[package]] 176 | name = "hashbrown" 177 | version = "0.14.0" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 180 | 181 | [[package]] 182 | name = "heck" 183 | version = "0.4.1" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 186 | 187 | [[package]] 188 | name = "indexmap" 189 | version = "2.0.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" 192 | dependencies = [ 193 | "equivalent", 194 | "hashbrown", 195 | ] 196 | 197 | [[package]] 198 | name = "itoa" 199 | version = "1.0.9" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 202 | 203 | [[package]] 204 | name = "libc" 205 | version = "0.2.147" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 208 | 209 | [[package]] 210 | name = "memchr" 211 | version = "2.6.3" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 214 | 215 | [[package]] 216 | name = "miniz_oxide" 217 | version = "0.7.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 220 | dependencies = [ 221 | "adler", 222 | ] 223 | 224 | [[package]] 225 | name = "object" 226 | version = "0.32.1" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 229 | dependencies = [ 230 | "memchr", 231 | ] 232 | 233 | [[package]] 234 | name = "proc-macro2" 235 | version = "1.0.66" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 238 | dependencies = [ 239 | "unicode-ident", 240 | ] 241 | 242 | [[package]] 243 | name = "quote" 244 | version = "1.0.33" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 247 | dependencies = [ 248 | "proc-macro2", 249 | ] 250 | 251 | [[package]] 252 | name = "rustc-demangle" 253 | version = "0.1.23" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 256 | 257 | [[package]] 258 | name = "ryu" 259 | version = "1.0.15" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 262 | 263 | [[package]] 264 | name = "serde" 265 | version = "1.0.188" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 268 | dependencies = [ 269 | "serde_derive", 270 | ] 271 | 272 | [[package]] 273 | name = "serde_derive" 274 | version = "1.0.188" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 277 | dependencies = [ 278 | "proc-macro2", 279 | "quote", 280 | "syn", 281 | ] 282 | 283 | [[package]] 284 | name = "serde_yaml" 285 | version = "0.9.25" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" 288 | dependencies = [ 289 | "indexmap", 290 | "itoa", 291 | "ryu", 292 | "serde", 293 | "unsafe-libyaml", 294 | ] 295 | 296 | [[package]] 297 | name = "strsim" 298 | version = "0.10.0" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 301 | 302 | [[package]] 303 | name = "syn" 304 | version = "2.0.32" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" 307 | dependencies = [ 308 | "proc-macro2", 309 | "quote", 310 | "unicode-ident", 311 | ] 312 | 313 | [[package]] 314 | name = "unicode-ident" 315 | version = "1.0.11" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 318 | 319 | [[package]] 320 | name = "unsafe-libyaml" 321 | version = "0.2.9" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" 324 | 325 | [[package]] 326 | name = "utf8parse" 327 | version = "0.2.1" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 330 | 331 | [[package]] 332 | name = "windows-sys" 333 | version = "0.48.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 336 | dependencies = [ 337 | "windows-targets", 338 | ] 339 | 340 | [[package]] 341 | name = "windows-targets" 342 | version = "0.48.5" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 345 | dependencies = [ 346 | "windows_aarch64_gnullvm", 347 | "windows_aarch64_msvc", 348 | "windows_i686_gnu", 349 | "windows_i686_msvc", 350 | "windows_x86_64_gnu", 351 | "windows_x86_64_gnullvm", 352 | "windows_x86_64_msvc", 353 | ] 354 | 355 | [[package]] 356 | name = "windows_aarch64_gnullvm" 357 | version = "0.48.5" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 360 | 361 | [[package]] 362 | name = "windows_aarch64_msvc" 363 | version = "0.48.5" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 366 | 367 | [[package]] 368 | name = "windows_i686_gnu" 369 | version = "0.48.5" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 372 | 373 | [[package]] 374 | name = "windows_i686_msvc" 375 | version = "0.48.5" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 378 | 379 | [[package]] 380 | name = "windows_x86_64_gnu" 381 | version = "0.48.5" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 384 | 385 | [[package]] 386 | name = "windows_x86_64_gnullvm" 387 | version = "0.48.5" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 390 | 391 | [[package]] 392 | name = "windows_x86_64_msvc" 393 | version = "0.48.5" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 396 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, path::{Path, PathBuf}}; 2 | use anyhow::{Result, anyhow, Ok}; 3 | use serde_yaml; 4 | use clap::{Parser, Subcommand}; 5 | 6 | #[derive(Debug)] 7 | enum Value { 8 | Integer(i64), 9 | Float(f64), 10 | String(String), 11 | Array(Vec), 12 | Dictionary(HashMap), 13 | SpContent(Option), 14 | } 15 | 16 | impl Value { 17 | pub fn as_string(&self) -> Option<&String> { 18 | match self { 19 | Value::String(s) => Some(s), 20 | _ => None, 21 | } 22 | } 23 | 24 | pub fn as_string_mut(&mut self) -> Option<&mut String> { 25 | match self { 26 | Value::String(s) => Some(s), 27 | _ => None, 28 | } 29 | } 30 | 31 | pub fn is_array(&self) -> bool { 32 | match self { 33 | Value::Array(_) => true, 34 | _ => false, 35 | } 36 | } 37 | 38 | pub fn as_array(&self) -> Option<&Vec> { 39 | match self { 40 | Value::Array(a) => Some(a), 41 | _ => None, 42 | } 43 | } 44 | 45 | pub fn as_array_mut(&mut self) -> Option<&mut Vec> { 46 | match self { 47 | Value::Array(a) => Some(a), 48 | _ => None, 49 | } 50 | } 51 | 52 | pub fn is_dictionary(&self) -> bool { 53 | match self { 54 | Value::Dictionary(_) => true, 55 | _ => false, 56 | } 57 | } 58 | 59 | pub fn as_dictionary(&self) -> Option<&HashMap> { 60 | match self { 61 | Value::Dictionary(d) => Some(d), 62 | _ => None, 63 | } 64 | } 65 | 66 | pub fn as_dictionary_mut(&mut self) -> Option<&mut HashMap> { 67 | match self { 68 | Value::Dictionary(d) => Some(d), 69 | _ => None, 70 | } 71 | } 72 | 73 | pub fn as_integer(&self) -> Option { 74 | match self { 75 | Value::Integer(i) => Some(*i), 76 | _ => None, 77 | } 78 | } 79 | 80 | pub fn as_float(&self) -> Option { 81 | match self { 82 | Value::Float(f) => Some(*f), 83 | _ => None, 84 | } 85 | } 86 | } 87 | 88 | 89 | #[derive(Debug, PartialEq, Clone)] 90 | enum Token { 91 | Equal, // "=" 92 | OpenBrace, // "{" 93 | CloseBrace, // "}" 94 | Comma, // "," 95 | Identifier(String), // "astver", "text" 等 96 | StringLiteral(String),// "2.0", "俺たちの新しい日常" 等 97 | IntegerLiteral(i64), // 整数 98 | FloatLiteral(f64), // 浮点数 99 | SpTagContent(Option), 100 | } 101 | 102 | fn tokenize(input: &str) -> Result> { 103 | let mut tokens = Vec::new(); 104 | let mut chars = input.chars().peekable(); 105 | 106 | while let Some(ch) = chars.next() { 107 | match ch { 108 | '=' => tokens.push(Token::Equal), 109 | '{' => tokens.push(Token::OpenBrace), 110 | '}' => tokens.push(Token::CloseBrace), 111 | ',' => tokens.push(Token::Comma), 112 | '"' => { 113 | let mut s = String::new(); 114 | while let Some(ch) = chars.peek() { 115 | match ch { 116 | '\\' => { 117 | chars.next(); // Consume the backslash 118 | if let Some(escaped) = chars.next() { 119 | match escaped { 120 | 'n' => s.push('\n'), 121 | 't' => s.push('\t'), 122 | '"' => s.push('"'), 123 | '\\' => s.push('\\'), 124 | _ => return Err(anyhow!("Unknown escape sequence")), 125 | } 126 | } else { 127 | return Err(anyhow!("Incomplete escape sequence")); 128 | } 129 | } 130 | '"' => { 131 | chars.next(); // skip the closing " 132 | break; 133 | } 134 | _ => s.push(chars.next().unwrap()), 135 | } 136 | } 137 | tokens.push(Token::StringLiteral(s)); 138 | } 139 | '[' => { 140 | chars.next(); 141 | let mut num_string = String::new(); 142 | loop { 143 | match chars.peek() { 144 | Some(&']') => { 145 | chars.next(); 146 | break; 147 | } 148 | Some(&ch) if ch.is_digit(10) => { 149 | num_string.push(ch); 150 | chars.next(); 151 | } 152 | None => return Err(anyhow!("Unexpected end of input while parsing sp content".to_string())), 153 | _ => break, 154 | } 155 | } 156 | if num_string.is_empty() { 157 | tokens.push(Token::SpTagContent(None)); 158 | } else { 159 | tokens.push(Token::SpTagContent(Some(num_string.parse::().unwrap()))); 160 | } 161 | } 162 | _ if ch.is_whitespace() || ch == '\n' || ch == '\r' => {} 163 | _ if ch.is_numeric() || (ch == '-' && chars.peek().map_or(false, |next| next.is_numeric())) => { 164 | let mut number = ch.to_string(); 165 | let mut is_float = false; 166 | while let Some(ch) = chars.peek() { 167 | if *ch == '.' { 168 | is_float = true; 169 | number.push(chars.next().unwrap()); 170 | } else if ch.is_numeric() { 171 | number.push(chars.next().unwrap()); 172 | } else { 173 | break; 174 | } 175 | } 176 | if is_float { 177 | tokens.push(Token::FloatLiteral(number.parse().unwrap())); 178 | } else { 179 | tokens.push(Token::IntegerLiteral(number.parse().unwrap())); 180 | } 181 | } 182 | _ if ch.is_alphanumeric() || ch == '_' => { 183 | let mut name = ch.to_string(); 184 | while let Some(ch) = chars.peek() { 185 | if ch.is_alphanumeric() || *ch == '_' { 186 | name.push(chars.next().unwrap()); 187 | } else { 188 | break; 189 | } 190 | } 191 | tokens.push(Token::Identifier(name)); 192 | } 193 | _ => return Err(anyhow!(format!("Unexpected character: {}", ch))), 194 | } 195 | } 196 | Ok(tokens) 197 | } 198 | 199 | 200 | fn parse_tokens(tokens: &[Token]) -> Result> { 201 | let mut index = 0; 202 | let mut result = HashMap::new(); 203 | 204 | while index < tokens.len() { 205 | match &tokens[index] { 206 | Token::Identifier(s) => { 207 | index += 1; 208 | if let Token::Equal = tokens[index] { 209 | index += 1; // Skip '=' 210 | let value = parse_value(tokens, &mut index)?; 211 | result.insert(s.clone(), value); 212 | } else { 213 | anyhow::bail!("Expected '=' after Identifier"); 214 | } 215 | }, 216 | // Token::SpTagContent(s) => { 217 | // index += 1; 218 | // if let Token::Equal = tokens[index] { 219 | // let value = parse_value(tokens, &mut index)?; 220 | // result.insert(s.clone(), value); 221 | // } else { 222 | // anyhow::bail!("Expected '=' after SpContent in root level"); 223 | // } 224 | // } 225 | _ => anyhow::bail!("Unexpected token at top level"), 226 | } 227 | } 228 | Ok(result) 229 | } 230 | 231 | fn parse_value(tokens: &[Token], index: &mut usize) -> Result { 232 | match &tokens[*index] { 233 | Token::OpenBrace => parse_array(tokens, index), 234 | Token::StringLiteral(s) => { 235 | *index += 1; 236 | Ok(Value::String(s.clone())) 237 | } 238 | Token::IntegerLiteral(i) => { 239 | *index += 1; 240 | Ok(Value::Integer(*i)) 241 | } 242 | Token::FloatLiteral(f) => { 243 | *index += 1; 244 | Ok(Value::Float(*f)) 245 | } 246 | Token::Identifier(s) => { 247 | *index += 1; 248 | if let Token::Equal = tokens[*index] { 249 | *index += 1; // Skip '=' 250 | let value = parse_value(tokens, index)?; 251 | let mut map = HashMap::new(); 252 | map.insert(s.clone(), value); 253 | Ok(Value::Dictionary(map)) 254 | } else { 255 | Ok(Value::String(s.clone())) 256 | } 257 | }, 258 | Token::SpTagContent(sp) => { 259 | *index += 1; 260 | if let Token::Equal = tokens[*index] { 261 | *index += 1; // Skip '=' 262 | let value = parse_value(tokens, index)?; 263 | let mut map = HashMap::new(); 264 | // hack 265 | let s = match sp { 266 | Some(sp) => format!("[{}]", sp), 267 | None => "[]".to_string(), 268 | }; 269 | map.insert(s, value); 270 | Ok(Value::Dictionary(map)) 271 | } else { 272 | Ok(Value::SpContent(*sp)) 273 | } 274 | } 275 | _ => anyhow::bail!(format!("Unexpected token: {:?}", tokens[*index])), 276 | } 277 | } 278 | 279 | 280 | fn parse_array(tokens: &[Token], index: &mut usize) -> Result { 281 | let mut values = Vec::new(); 282 | *index += 1; // Skip '{' 283 | 284 | loop { 285 | match &tokens[*index] { 286 | Token::CloseBrace => { 287 | *index += 1; 288 | return Ok(Value::Array(values)); 289 | } 290 | Token::Comma => { 291 | *index += 1; 292 | continue; 293 | } 294 | _ => { 295 | let value = parse_value(tokens, index)?; 296 | values.push(value); 297 | } 298 | } 299 | } 300 | } 301 | 302 | 303 | fn extract_secnario_toyaml(ast: &HashMap, output: impl AsRef) -> Result<()> { 304 | // extract all the text under the key "text" 305 | let ast_array = ast.get("ast") 306 | .ok_or(anyhow::anyhow!("ast key not found"))? 307 | .as_array() 308 | .ok_or(anyhow::anyhow!("ast is not a dictionary"))?; 309 | 310 | let mut all_texts = Vec::new(); 311 | 312 | for block_value in ast_array.iter() { 313 | let blocks = block_value.as_dictionary().ok_or(anyhow::anyhow!("block is not a dict"))?; 314 | for (block_key, block_dict) in blocks.iter() { 315 | if !block_key.starts_with("block_") { 316 | continue; 317 | } 318 | if let Some(block_items) = block_dict.as_array() { 319 | for block_item in block_items { 320 | if let Some(block_item) = block_item.as_dictionary() { 321 | if let Some(text_value) = block_item.get("text") { 322 | if let Some(text_array) = text_value.as_array() { 323 | for text_block in text_array.iter() { 324 | let ja_texts = text_block.as_dictionary(); 325 | if let Some(ja_texts) = ja_texts { 326 | if let Some(ja_texts) = ja_texts.get("ja") { 327 | if let Some(ja_texts) = ja_texts.as_array() { 328 | for subja in ja_texts { 329 | if let Some(subja) = subja.as_array() { 330 | for subj in subja.iter() { 331 | if let Some(subj) = subj.as_string() { 332 | all_texts.push(subj.to_string()); 333 | } 334 | } 335 | } 336 | } 337 | } 338 | } 339 | } 340 | 341 | } 342 | } 343 | } 344 | } 345 | } 346 | } 347 | } 348 | } 349 | 350 | let s = serde_yaml::to_string(&all_texts)?; 351 | // write to file 352 | std::fs::write(output, s)?; 353 | Ok(()) 354 | } 355 | 356 | fn extract_secnario(ast: &HashMap) -> Result> { 357 | // extract all the text under the key "text" 358 | let ast_array = ast.get("ast") 359 | .ok_or(anyhow::anyhow!("ast key not found"))? 360 | .as_array() 361 | .ok_or(anyhow::anyhow!("ast is not a dictionary"))?; 362 | 363 | let mut all_texts = Vec::new(); 364 | 365 | for block_value in ast_array.iter() { 366 | let blocks = block_value.as_dictionary().ok_or(anyhow::anyhow!("block is not a dict"))?; 367 | for (block_key, block_dict) in blocks.iter() { 368 | if !block_key.starts_with("block_") { 369 | continue; 370 | } 371 | if let Some(block_items) = block_dict.as_array() { 372 | for block_item in block_items { 373 | if let Some(block_item) = block_item.as_dictionary() { 374 | if let Some(text_value) = block_item.get("text") { 375 | if let Some(text_array) = text_value.as_array() { 376 | for text_block in text_array.iter() { 377 | let ja_texts = text_block.as_dictionary(); 378 | if let Some(ja_texts) = ja_texts { 379 | if let Some(ja_texts) = ja_texts.get("ja") { 380 | if let Some(ja_texts) = ja_texts.as_array() { 381 | for subja in ja_texts { 382 | if let Some(subja) = subja.as_array() { 383 | for subj in subja.iter() { 384 | if let Some(subj) = subj.as_string() { 385 | all_texts.push(subj.to_string()); 386 | } 387 | } 388 | } 389 | } 390 | } 391 | } 392 | } 393 | 394 | } 395 | } 396 | } 397 | } 398 | } 399 | } 400 | } 401 | } 402 | 403 | Ok(all_texts) 404 | } 405 | 406 | 407 | 408 | fn replace_secnario(ast: &mut HashMap, secnario: Vec) -> Result<()> { 409 | let mut scenario_iter = secnario.into_iter(); 410 | 411 | fn replace_text_in_ja(subja: &mut Value, scenario_iter: &mut impl Iterator) -> Result<()> { 412 | if let Some(subj) = subja.as_string_mut() { 413 | if let Some(new_str) = scenario_iter.next() { 414 | *subj = new_str; 415 | } else { 416 | return Err(anyhow::anyhow!("Ran out of strings in secnario.")); 417 | } 418 | } 419 | Ok(()) 420 | } 421 | 422 | fn replace_texts_in_block(block: &mut Value, scenario_iter: &mut impl Iterator) -> Result<()> { 423 | if let Some(block_dict) = block.as_dictionary_mut() { 424 | if let Some(text_array) = block_dict.get_mut("text").and_then(Value::as_array_mut) { 425 | for text_block in text_array { 426 | if let Some(ja_texts) = text_block.as_dictionary_mut().and_then(|dict| dict.get_mut("ja")).and_then(Value::as_array_mut) { 427 | for subja in ja_texts { 428 | replace_text_in_ja(subja, scenario_iter)?; 429 | } 430 | } 431 | } 432 | } 433 | } 434 | Ok(()) 435 | } 436 | 437 | if let Some(ast_array) = ast.get_mut("ast").and_then(Value::as_array_mut) { 438 | for block_value in ast_array { 439 | if let Some(blocks) = block_value.as_dictionary_mut() { 440 | for (_, block_dict) in blocks { 441 | if block_dict.is_dictionary() { 442 | replace_texts_in_block(block_dict, &mut scenario_iter)?; 443 | } 444 | } 445 | } 446 | } 447 | } 448 | 449 | if scenario_iter.next().is_some() { 450 | return Err(anyhow::anyhow!("Not all strings in secnario were used.")); 451 | } 452 | 453 | Ok(()) 454 | } 455 | 456 | 457 | 458 | fn parse_ast(filename: impl AsRef) -> Result> { 459 | let input = std::fs::read_to_string(filename)?; 460 | // hack 461 | if input.starts_with("[]") { 462 | return Ok(HashMap::new()); 463 | } 464 | 465 | let tokens = tokenize(&input)?; 466 | parse_tokens(&tokens) 467 | } 468 | 469 | 470 | fn read_yaml_as_strings(yaml_file: impl AsRef) -> Result> { 471 | let content = std::fs::read_to_string(yaml_file)?; 472 | let parsed: Vec = serde_yaml::from_str(&content)?; 473 | Ok(parsed) 474 | } 475 | 476 | 477 | fn value_to_script(value: &Value, indent_level: usize) -> Result { 478 | let indent = "\t".repeat(indent_level); 479 | let next_indent = "\t".repeat(indent_level + 1); 480 | 481 | match value { 482 | Value::String(s) => Ok(format!("\"{}\"", s)), 483 | Value::Float(f) => { 484 | if f.fract() == 0.0 { 485 | Ok(format!("{:.1}", f)) 486 | } else { 487 | Ok(f.to_string()) 488 | } 489 | }, 490 | Value::Integer(i) => Ok(i.to_string()), 491 | Value::Array(a) => { 492 | let contents: Result> = a.iter().map(|v| value_to_script(v, indent_level + 1)).collect(); 493 | contents.map(|c| format!("{{\n{}{}\n{}}}", 494 | next_indent, 495 | c.join(&format!(",\n{}", next_indent)), 496 | indent)) 497 | }, 498 | Value::Dictionary(d) => { 499 | let mut contents = Vec::new(); 500 | for (key, value) in d { 501 | let line = value_to_script(value, indent_level + 1)?; 502 | contents.push(format!("{}={}", key, line)); 503 | } 504 | Ok(format!("\n{}{}\n{}", next_indent, contents.join(&format!(",\n{}", next_indent)), indent)) 505 | } 506 | Value::SpContent(sp) => { 507 | let c = match sp { 508 | Some(sp) => format!("[{}]", sp), 509 | None => "[]".to_string(), 510 | }; 511 | Ok(c) 512 | }, 513 | } 514 | } 515 | 516 | 517 | 518 | fn reconstruct_script(ast: &HashMap) -> Result { 519 | let mut script = String::new(); 520 | 521 | for (key, value) in ast.iter() { 522 | script.push_str(key); 523 | script.push_str(" = "); 524 | script.push_str(&value_to_script(value, 0)?); 525 | script.push('\n'); 526 | } 527 | 528 | Ok(script) 529 | } 530 | 531 | 532 | fn prune_ast(ast: &mut HashMap) { 533 | if let Some(Value::Array(ast_array)) = ast.get_mut("ast") { 534 | for block_value in ast_array.iter_mut() { 535 | if let Value::Dictionary(blocks) = block_value { 536 | for (_, block_dict) in blocks.iter_mut() { 537 | if let Value::Array(block_items) = block_dict { 538 | let mut i = 0; 539 | while i != block_items.len() { 540 | match &mut block_items[i] { 541 | Value::Dictionary(item_dict) => { 542 | item_dict.retain(|key, _| key == "linknext" || key == "line"); 543 | i += 1; 544 | }, 545 | _ => { 546 | block_items.remove(i); 547 | } 548 | } 549 | } 550 | } 551 | } 552 | } 553 | } 554 | } 555 | } 556 | 557 | 558 | 559 | 560 | #[derive(Parser, Debug)] 561 | #[command(author, version, about, long_about = None)] 562 | struct Args { 563 | #[command(subcommand)] 564 | command: Commands, 565 | } 566 | 567 | 568 | #[derive(Subcommand, Debug)] 569 | enum Commands { 570 | /// Extract all secnario text to yaml 571 | Extract { input: PathBuf, output: PathBuf }, 572 | /// Prune the ast file, remove all secnario text (for steam release) 573 | Prune { input: PathBuf, output: PathBuf }, 574 | /// Merge corresponding secnario text back to ast file 575 | Merge { ast_input: PathBuf, yaml_input: PathBuf, output: PathBuf }, 576 | } 577 | 578 | 579 | fn build_replacement_map(original_texts: Vec, replacement_texts: Vec) -> HashMap { 580 | original_texts.into_iter().zip(replacement_texts.into_iter()).collect() 581 | } 582 | 583 | fn replace_strings_in_script(script: &str, replacements: &HashMap) -> Result { 584 | let mut output = String::new(); 585 | let mut used_replacements = HashMap::new(); 586 | 587 | for line in script.lines() { 588 | let mut new_line = line.to_string(); 589 | for (to_replace, replace_with) in replacements.iter() { 590 | // 替换带引号的字符串 591 | let quoted_to_replace = format!("\"{}\"", to_replace); 592 | let quoted_replace_with = format!("\"{}\"", replace_with); 593 | if new_line.contains("ed_to_replace) { 594 | new_line = new_line.replace("ed_to_replace, "ed_replace_with); 595 | used_replacements.insert(to_replace.clone(), true); 596 | } 597 | } 598 | output.push_str(&new_line); 599 | output.push('\n'); 600 | } 601 | 602 | // 确保所有替换都已完成 603 | if used_replacements.len() != replacements.len() { 604 | return Err(anyhow::anyhow!("Not all replacements were used!")); 605 | } 606 | 607 | Ok(output) 608 | } 609 | 610 | fn main() { 611 | let cli = Args::parse(); 612 | match &cli.command { 613 | Commands::Extract { input, output } => { 614 | println!("Extracting secnario text from {} to {}", input.display(), output.display()); 615 | let ast = parse_ast(input).unwrap(); 616 | if ast.is_empty() { 617 | return; 618 | } 619 | extract_secnario_toyaml(&ast, output).unwrap(); 620 | }, 621 | Commands::Prune { input, output } => { 622 | let mut ast = parse_ast(input).unwrap(); 623 | if ast.is_empty() { 624 | return; 625 | } 626 | prune_ast(&mut ast); 627 | let s = reconstruct_script(&ast).unwrap(); 628 | std::fs::write(output, s).unwrap(); 629 | }, 630 | Commands::Merge { ast_input, yaml_input, output } => { 631 | let mut ast = parse_ast(ast_input).unwrap(); 632 | if ast.is_empty() { 633 | return; 634 | } 635 | let old_secnario = extract_secnario(&ast).unwrap(); 636 | let secnario = read_yaml_as_strings(yaml_input).unwrap(); 637 | let rp = build_replacement_map(old_secnario, secnario); 638 | let s = replace_strings_in_script(&std::fs::read_to_string(ast_input).unwrap(), &rp).unwrap(); 639 | 640 | // replace_secnario(&mut ast, secnario).unwrap(); 641 | // let s = reconstruct_script(&ast).unwrap(); 642 | std::fs::write(output, s).unwrap(); 643 | } 644 | } 645 | 646 | } 647 | 648 | #[cfg(test)] 649 | mod tests { 650 | use super::*; 651 | 652 | #[test] 653 | fn test_parse_ast() { 654 | let input = r#"astver = 2.0 655 | ast = { 656 | block_00000 = { 657 | {"savetitle", text="俺たちの新しい日常"}, 658 | {"bg", time=2000, file="bg001a", path=":bg/"}, 659 | {"se", file="seアラーム", loop=1, id=1}, 660 | {"fg", ch="妃愛", size="no", mode=1, path=":fg/hiy[表情]/", file="hiy_nob0700", ex05="hiy_nob0000", face="b0032", head="hiy_nob", lv=2.2, id=20}, 661 | {"text"}, 662 | text = { 663 | vo = { 664 | {"vo", file="fem_hiy_00052", ch="hiy"}, 665 | }, 666 | ja = { 667 | { 668 | name = {"妃愛"}, 669 | "「お兄、あさー……むふー……」", 670 | {"rt2"}, 671 | }, 672 | }, 673 | }, 674 | linknext = "block_00001", 675 | line = 18, 676 | }, 677 | } 678 | "#; 679 | 680 | let tokens = tokenize(input).unwrap(); 681 | let _value = parse_tokens(&tokens).unwrap(); 682 | } 683 | 684 | 685 | fn read_yaml_as_strings2(yaml_file: &str) -> Result> { 686 | let parsed: Vec = serde_yaml::from_str(yaml_file)?; 687 | Ok(parsed) 688 | } 689 | 690 | #[test] 691 | fn test_prune_ast() { 692 | let input = r#"astver = 2.0 693 | ast = { 694 | block_00000 = { 695 | {"savetitle", text="俺たちの新しい日常"}, 696 | {"bg", time=2000, file="bg001a", path=":bg/"}, 697 | {"se", file="seアラーム", loop=1, id=1}, 698 | {"fg", ch="妃愛", size="no", mode=1, path=":fg/hiy[表情]/", file="hiy_nob0700", ex05="hiy_nob0000", face="b0032", head="hiy_nob", lv=2.2, id=20}, 699 | {"text"}, 700 | text = { 701 | vo = { 702 | {"vo", file="fem_hiy_00052", ch="hiy"}, 703 | }, 704 | ja = { 705 | { 706 | name = {"妃愛"}, 707 | "「お兄、あさー……むふー……」", 708 | {"rt2"}, 709 | }, 710 | }, 711 | }, 712 | linknext = "block_00001", 713 | line = 18, 714 | }, 715 | } 716 | "#; 717 | 718 | let tokens = tokenize(input).unwrap(); 719 | let mut value = parse_tokens(&tokens).unwrap(); 720 | prune_ast(&mut value); 721 | let s = reconstruct_script(&value).unwrap(); 722 | println!("{}", s); 723 | } 724 | 725 | #[test] 726 | fn test_reconstruct() { 727 | let input = r#"astver = 2.0 728 | ast = { 729 | block_00000 = { 730 | {"savetitle", text="俺たちの新しい日常"}, 731 | {"bg", time=2000, file="bg001a", path=":bg/"}, 732 | {"se", file="seアラーム", loop=1, id=1}, 733 | {"fg", ch="妃愛", size="no", mode=1, path=":fg/hiy[表情]/", file="hiy_nob0700", ex05="hiy_nob0000", face="b0032", head="hiy_nob", lv=2.2, id=20}, 734 | {"text"}, 735 | text = { 736 | vo = { 737 | {"vo", file="fem_hiy_00052", ch="hiy"}, 738 | }, 739 | ja = { 740 | { 741 | name = {"妃愛"}, 742 | "「お兄、あさー……むふー……」", 743 | {"rt2"}, 744 | }, 745 | }, 746 | }, 747 | linknext = "block_00001", 748 | line = 18, 749 | }, 750 | } 751 | "#; 752 | 753 | let tokens = tokenize(input).unwrap(); 754 | let value = parse_tokens(&tokens).unwrap(); 755 | let s = reconstruct_script(&value).unwrap(); 756 | println!("{}", s); 757 | } 758 | 759 | #[test] 760 | fn test_merge() { 761 | let input = r#"astver = 2.0 762 | ast = { 763 | block_00000 = { 764 | {"savetitle", text="俺たちの新しい日常"}, 765 | {"bg", time=2000, file="bg001a", path=":bg/"}, 766 | {"se", file="seアラーム", loop=1, id=1}, 767 | {"fg", ch="妃愛", size="no", mode=1, path=":fg/hiy[表情]/", file="hiy_nob0700", ex05="hiy_nob0000", face="b0032", head="hiy_nob", lv=2.2, id=20}, 768 | {"text"}, 769 | text = { 770 | vo = { 771 | {"vo", file="fem_hiy_00052", ch="hiy"}, 772 | }, 773 | ja = { 774 | { 775 | name = {"妃愛"}, 776 | "「お兄、あさー……むふー……」", 777 | {"rt2"}, 778 | }, 779 | }, 780 | }, 781 | linknext = "block_00001", 782 | line = 18, 783 | }, 784 | } 785 | "#; 786 | 787 | let tokens = tokenize(input).unwrap(); 788 | let _value = parse_tokens(&tokens).unwrap(); 789 | } 790 | } 791 | 792 | --------------------------------------------------------------------------------