Skip to content

Commit cb1bbfd

Browse files
committed
migrate YarnSpinner.Compiler/Project.cs
1 parent 261c95b commit cb1bbfd

6 files changed

Lines changed: 428 additions & 7 deletions

File tree

Cargo.lock

Lines changed: 27 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/compiler/Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ description = "Compiler for Yarn Spinner for Rust, the friendly tool for writing
1111

1212
[features]
1313
default = [ ]
14-
serde = [ "dep:serde", "bevy?/serialize", "yarnspinner_core/serde" ]
14+
serde = [ "dep:serde", "dep:serde_json", "bevy?/serialize", "yarnspinner_core/serde" ]
1515
bevy = [ "dep:bevy", "yarnspinner_core/bevy" ]
1616

1717
[dependencies]
@@ -22,9 +22,12 @@ yarnspinner_internal_shared = { path = "../internal_shared", version = "0.1.0" }
2222
yarnspinner_core = { path = "../core", version = "0.7.0" }
2323
annotate-snippets = "0.10"
2424
serde = { version = "1", features = [ "derive" ], optional = true }
25+
serde_json = { version = "1", optional = true }
2526
bevy = { version = "0.18", default-features = false, optional = true }
2627
rand = { version = "0.9", features = [ "small_rng" ] }
27-
crc32fast = "1.5.0"
28+
crc32fast = "1.5"
29+
globset = "0.4"
30+
walkdir = "2.5"
2831

2932
[target.'cfg(target_arch = "wasm32")'.dependencies]
3033
instant = { version = "0.1.12", features = [

crates/compiler/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub(crate) mod listeners;
1717
mod output;
1818
mod parser;
1919
pub(crate) mod parser_rule_context_ext;
20+
pub mod project;
2021
mod string_table_manager;
2122
pub(crate) mod token_ext;
2223
pub(crate) mod visitors;
@@ -34,6 +35,7 @@ pub mod prelude {
3435
compiler::{CompilationType, Compiler, File},
3536
listeners::{Diagnostic, DiagnosticSeverity, DiagnosticVec},
3637
output::*,
38+
project::*,
3739
};
3840
pub(crate) use yarnspinner_core::prelude::*;
3941
pub(crate) use yarnspinner_internal_shared::prelude::*;

crates/compiler/src/project.rs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
//! Adapted from <https://github.com/YarnSpinnerTool/YarnSpinner/blob/v2.5.0/YarnSpinner.Compiler/Project.cs>
2+
3+
use std::path::Path;
4+
use std::{collections::HashMap, path::PathBuf};
5+
6+
use globset::{Glob, GlobSet, GlobSetBuilder};
7+
#[cfg(feature = "serde")]
8+
use serde::{Deserialize, Serialize};
9+
use walkdir::WalkDir;
10+
11+
/// Current version of the .yarnproject
12+
pub const CURRENT_PROJECT_FILE_VERSION: u32 = 2;
13+
14+
/// Yarn Projects represent instructions on where to find Yarn scripts and
15+
/// associated assets, and how they should be compiled.
16+
#[derive(Debug, Clone)]
17+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18+
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
19+
pub struct Project {
20+
/// Gets or sets the file version of the project.
21+
///
22+
/// This value is required to be equal to [`CURRENT_PROJECT_FILE_VERSION`]
23+
pub project_file_version: u32,
24+
25+
/// Gets the path that the [`Project`]` was loaded from.
26+
///
27+
/// This value is not stored when the file is saved, but is instead
28+
/// determined when the file is loaded by [`self::load_from_file()`]
29+
#[cfg_attr(feature = "serde", serde(skip))]
30+
pub path: Option<PathBuf>,
31+
32+
/// Gets or sets the collection of file search patterns used to locate
33+
/// Yarn files that form this project.
34+
#[cfg_attr(
35+
feature = "serde",
36+
serde(rename = "sourceFiles", default = "default_source_files")
37+
)]
38+
pub source_file_patterns: Vec<String>,
39+
40+
/// Gets or sets the collection of file search patterns that should be
41+
/// excluded from this project.
42+
///
43+
/// If a file is matched by a pattern in `source_file_patterns`, and is also matched
44+
/// by a pattern in `exclude_file_patterns`, then it is not included in the value
45+
/// returned by `source_files()`
46+
#[cfg_attr(feature = "serde", serde(rename = "excludeFiles", default))]
47+
pub exclude_file_patterns: Vec<String>,
48+
49+
/// Gets or sets the collection of [`LocalizationInfo`]
50+
/// objects that store information about where localized data for this
51+
/// project is found.
52+
#[cfg_attr(feature = "serde", serde(default))]
53+
pub localisation: HashMap<String, LocalizationInfo>,
54+
55+
/// Gets or sets the base language of the project, as an IETF BCP-47
56+
/// language tag.
57+
///
58+
/// The base language is the language that the Yarn scripts is written in
59+
pub base_language: String,
60+
61+
/// Gets or sets the path to a JSON file containing command and function
62+
/// definitions that this project references.
63+
///
64+
/// Definitions files are used by editing tools to provide type
65+
/// information and other externally-defined data used by the Yarn scripts.
66+
#[cfg_attr(feature = "serde", serde(default))]
67+
pub definitions: Option<String>,
68+
69+
/// Gets or sets a dictionary containing instructions that control how
70+
/// the Yarn Spinner compiler should compile a project.
71+
///
72+
/// Note: Unsure about the type of options. No documentation in v2.5.0.
73+
#[cfg_attr(feature = "serde", serde(default))]
74+
pub compiler_options: HashMap<String, String>,
75+
}
76+
77+
fn default_source_files() -> Vec<String> {
78+
vec!["**/*.yarn".to_owned()]
79+
}
80+
81+
/// Stores the locations of where localized assets and a localized
82+
/// string table for a Yarn Project may be found.
83+
#[derive(Debug, Clone, PartialEq)]
84+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
85+
pub struct LocalizationInfo {
86+
/// Gets or sets the location at which localized assets may be found.
87+
#[cfg_attr(feature = "serde", serde(default))]
88+
pub assets: Option<String>,
89+
/// Gets or sets the location at which the localized string table may be found.
90+
#[cfg_attr(feature = "serde", serde(default))]
91+
pub strings: Option<String>,
92+
}
93+
94+
impl Default for Project {
95+
fn default() -> Self {
96+
Self {
97+
project_file_version: CURRENT_PROJECT_FILE_VERSION,
98+
path: Default::default(),
99+
source_file_patterns: default_source_files(),
100+
exclude_file_patterns: Default::default(),
101+
localisation: Default::default(),
102+
base_language: Default::default(),
103+
definitions: Default::default(),
104+
compiler_options: Default::default(),
105+
}
106+
}
107+
}
108+
109+
impl Project {
110+
/// Replaces the path used by the [`Project`].
111+
pub fn with_path(mut self, path: PathBuf) -> Self {
112+
self.path = Some(path);
113+
self
114+
}
115+
116+
/// Replaces the source files used by the [`Project`].
117+
pub fn with_source_files(mut self, source_files: Vec<String>) -> Self {
118+
self.source_file_patterns = source_files;
119+
self
120+
}
121+
122+
/// Loads and parses a [`Project`] from a file on disk.
123+
#[cfg(feature = "serde")]
124+
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
125+
let text =
126+
std::fs::read_to_string(&path).map_err(|e| format!("Cannot open project file: {e}"))?;
127+
128+
let mut project: Project =
129+
serde_json::from_str(&text).map_err(|e| format!("Invalid JSON: {e}"))?;
130+
131+
if project.project_file_version != CURRENT_PROJECT_FILE_VERSION {
132+
return Err(format!(
133+
"Incorrect project file version (expected {}, got {})",
134+
CURRENT_PROJECT_FILE_VERSION, project.project_file_version
135+
));
136+
}
137+
138+
project.path = Some(path.as_ref().to_path_buf());
139+
Ok(project)
140+
}
141+
142+
/// Gets a string containing JSON-formatted text that represents this [`Project`]
143+
#[cfg(feature = "serde")]
144+
pub fn to_json(&self) -> String {
145+
serde_json::to_string_pretty(self).unwrap()
146+
}
147+
148+
/// Saves a [`Project`] as JSON-formatted text to a file on disk
149+
#[cfg(feature = "serde")]
150+
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), String> {
151+
std::fs::write(path, self.to_json()).map_err(|e| format!("Cannot write project file: {e}"))
152+
}
153+
154+
/// Gets the path of the directory from which to start searching for .yarn
155+
/// files. This value is null if the directory does not exist on disk.
156+
fn search_directory(&self) -> Option<PathBuf> {
157+
let path = self.path.as_ref()?;
158+
159+
if path.is_dir() {
160+
// This project refers to a directory on disk.
161+
Some(path.clone())
162+
} else if path.is_file() {
163+
// This project refers to a .yarnproject on disk.
164+
path.parent().map(|p| p.to_path_buf())
165+
} else {
166+
// This project does not refer to a file on disk or to a directory.
167+
None
168+
}
169+
}
170+
171+
/// Build the [`GlobSet`] based on source files.
172+
fn build_source_globset(&self) -> GlobSet {
173+
let mut builder = GlobSetBuilder::new();
174+
175+
for pattern in &self.source_file_patterns {
176+
builder.add(Glob::new(pattern).unwrap());
177+
}
178+
179+
builder.build().unwrap()
180+
}
181+
182+
/// Build the [`GlobSet`] based on exclude files.
183+
fn build_exclude_globset(&self) -> GlobSet {
184+
let mut builder = GlobSetBuilder::new();
185+
186+
for pattern in &self.exclude_file_patterns {
187+
builder.add(Glob::new(pattern).unwrap());
188+
}
189+
190+
builder.build().unwrap()
191+
}
192+
193+
/// Gets the collection of Yarn files that should be used to compile the project.
194+
///
195+
/// This collection uses a [`GlobSet`] to find all files specified by `source_files`,
196+
/// excluding those that are specified by `exclude_files`.
197+
pub fn source_files(&self) -> Vec<PathBuf> {
198+
let Some(root) = self.search_directory() else {
199+
return Vec::new();
200+
};
201+
202+
let source_matcher = self.build_source_globset();
203+
let exclude_matcher = self.build_exclude_globset();
204+
205+
WalkDir::new(&root)
206+
.into_iter()
207+
.filter_map(Result::ok)
208+
.map(|e| e.path().to_path_buf())
209+
.filter(|path| source_matcher.is_match(path) && !exclude_matcher.is_match(path))
210+
.collect()
211+
}
212+
213+
/// Gets the path to the Definitions file, relative to this project's location.
214+
pub fn definitions_path(&self) -> Option<PathBuf> {
215+
let root = self.search_directory()?;
216+
let defs = self.definitions.as_ref()?;
217+
Some(root.join(defs))
218+
}
219+
220+
/// Gets a value indicating whether the given path is a path
221+
/// that is included in this project.
222+
pub fn is_matching_path<P: AsRef<Path>>(&self, path: P) -> bool {
223+
let Some(root) = self.search_directory() else {
224+
return false;
225+
};
226+
227+
let full_path = root.join(path);
228+
229+
let source_matcher = self.build_source_globset();
230+
let exclude_matcher = self.build_exclude_globset();
231+
source_matcher.is_match(&full_path) && !exclude_matcher.is_match(&full_path)
232+
}
233+
}

0 commit comments

Comments
 (0)