@tishlang/tish-format 2.39.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +4 -1
- package/bin/tish-format +0 -0
- package/crates/tish/src/cli_help.rs +28 -0
- package/crates/tish/src/main.rs +41 -5
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +21 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/tish.d.tish +4 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +5 -1
- package/crates/tish/tests/regr_typed_externs.rs +59 -0
- package/crates/tish/tests/scope_merge.rs +79 -0
- package/crates/tish_build_utils/src/lib.rs +27 -1
- package/crates/tish_builtins/Cargo.toml +14 -6
- package/crates/tish_builtins/src/array.rs +130 -54
- package/crates/tish_builtins/src/collections.rs +9 -5
- package/crates/tish_builtins/src/construct.rs +7 -1
- package/crates/tish_builtins/src/date.rs +15 -1
- package/crates/tish_builtins/src/globals.rs +65 -3
- package/crates/tish_builtins/src/helpers.rs +7 -3
- package/crates/tish_builtins/src/iterator.rs +5 -1
- package/crates/tish_builtins/src/lib.rs +3 -0
- package/crates/tish_builtins/src/math.rs +14 -0
- package/crates/tish_builtins/src/number.rs +72 -6
- package/crates/tish_builtins/src/object.rs +6 -1
- package/crates/tish_builtins/src/string.rs +13 -2
- package/crates/tish_builtins/src/symbol.rs +17 -17
- package/crates/tish_builtins/src/typedarrays.rs +9 -2
- package/crates/tish_bytecode/src/compiler.rs +76 -2
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +77 -2
- package/crates/tish_compile/src/codegen.rs +2154 -225
- package/crates/tish_compile/src/infer.rs +37 -41
- package/crates/tish_compile/src/lib.rs +13 -1
- package/crates/tish_compile/src/platform_resolve.rs +361 -0
- package/crates/tish_compile/src/resolve.rs +195 -11
- package/crates/tish_compile/src/schemes.rs +333 -0
- package/crates/tish_compile/src/types.rs +205 -34
- package/crates/tish_compile/tests/perf_codegen_320.rs +2 -2
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +3 -1
- package/crates/tish_compile/tests/platform_resolve_cli.rs +164 -0
- package/crates/tish_compile/tests/regr_556_param_name_collision.rs +40 -0
- package/crates/tish_compile/tests/regr_558_struct_array_write.rs +65 -0
- package/crates/tish_compile/tests/regr_558b_struct_field_read.rs +21 -0
- package/crates/tish_compile/tests/regr_558c_wrapped_struct_array.rs +64 -0
- package/crates/tish_compile/tests/regr_558d_native_vec_mutators.rs +156 -0
- package/crates/tish_compile/tests/regr_562_wrapped_struct_array_push.rs +50 -0
- package/crates/tish_compile/tests/regr_fixed_native_coercions.rs +69 -0
- package/crates/tish_compile/tests/regr_gba_numerics_gated.rs +58 -0
- package/crates/tish_compile/tests/regr_gba_struct_reflection.rs +106 -0
- package/crates/tish_compile/tests/regression/name_collision_lib.tish +6 -0
- package/crates/tish_compile/tests/regression/name_collision_main.tish +13 -0
- package/crates/tish_compile/tests/regression/struct_array_write.tish +22 -0
- package/crates/tish_compile/tests/regression/struct_field_return_read.tish +10 -0
- package/crates/tish_compiler_wasm/Cargo.toml +1 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +56 -6
- package/crates/tish_core/Cargo.toml +39 -4
- package/crates/tish_core/src/compat.rs +506 -0
- package/crates/tish_core/src/console_style.rs +23 -1
- package/crates/tish_core/src/json.rs +42 -10
- package/crates/tish_core/src/lib.rs +121 -25
- package/crates/tish_core/src/macros.rs +1 -1
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +8 -1
- package/crates/tish_core/src/value.rs +305 -54
- package/crates/tish_core/src/vmref.rs +17 -5
- package/crates/tish_eval/Cargo.toml +2 -0
- package/crates/tish_eval/src/eval.rs +90 -3
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_eval/src/value_convert.rs +316 -21
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_lsp/Cargo.toml +2 -0
- package/crates/tish_lsp/src/import_goto.rs +41 -14
- package/crates/tish_lsp/src/main.rs +18 -0
- package/crates/tish_native/src/build.rs +246 -0
- package/crates/tish_native/src/config.rs +20 -0
- package/crates/tish_native/src/lib.rs +16 -6
- package/crates/tish_parser/src/lib.rs +28 -0
- package/crates/tish_parser/src/parser.rs +25 -4
- package/crates/tish_runtime/src/lib.rs +55 -9
- package/crates/tish_runtime_gba/Cargo.lock +837 -0
- package/crates/tish_runtime_gba/Cargo.toml +16 -0
- package/crates/tish_runtime_gba/src/gba.rs +211 -0
- package/crates/tish_runtime_gba/src/lib.rs +804 -0
- package/crates/tish_vm/src/jit.rs +1446 -164
- package/crates/tish_vm/src/vm.rs +583 -173
- package/justfile +9 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -22,6 +22,11 @@ pub struct ResolvedNativeModule {
|
|
|
22
22
|
pub export_fn: String,
|
|
23
23
|
/// When false, omit `path = …` in the generated Cargo.toml (crate comes from `tish.rustDependencies` only).
|
|
24
24
|
pub use_path_dependency: bool,
|
|
25
|
+
/// The crate's own SOURCE directory (where its `Cargo.toml`/`src` live), when known — distinct from
|
|
26
|
+
/// `crate_path` (the importing project root). Used to find a `tish.d.tish` the crate ships (typed
|
|
27
|
+
/// externs). For a `cargo:` dep it's the resolved `rustDependencies` path; for a `tish.module`
|
|
28
|
+
/// package it's the package dir; `None` for registry deps with no local path.
|
|
29
|
+
pub source_dir: Option<PathBuf>,
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/// How codegen links a native import to Rust (`generateNativeWrapper` for `tish:*`; `cargo:*` always generated).
|
|
@@ -39,6 +44,18 @@ pub enum NativeModuleInit {
|
|
|
39
44
|
},
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
impl NativeModuleInit {
|
|
48
|
+
/// The Rust crate name whose functions back this module — the path for a direct typed call
|
|
49
|
+
/// (`<crate_name>::name_typed(..)`). Both variants carry it (Legacy directly, Generated as the
|
|
50
|
+
/// shim crate the generated namespace forwards to).
|
|
51
|
+
pub fn crate_name(&self) -> &str {
|
|
52
|
+
match self {
|
|
53
|
+
NativeModuleInit::Legacy { crate_name, .. } => crate_name,
|
|
54
|
+
NativeModuleInit::Generated { shim_crate, .. } => shim_crate,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
42
59
|
/// Extra native build inputs produced alongside Rust source (Cargo merge + optional wrapper).
|
|
43
60
|
#[derive(Debug, Clone)]
|
|
44
61
|
pub struct NativeBuildArtifacts {
|
|
@@ -122,6 +139,11 @@ pub fn resolve_native_modules(
|
|
|
122
139
|
if is_builtin_native_spec(&s) {
|
|
123
140
|
continue;
|
|
124
141
|
}
|
|
142
|
+
// `asset:` loads are baked into the ROM by the GBA codegen, not linked as native
|
|
143
|
+
// modules — they have no crate to resolve and no Value wrapper to generate.
|
|
144
|
+
if crate::schemes::is_scheme_import(&s) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
125
147
|
if !seen.insert(s.clone()) {
|
|
126
148
|
continue;
|
|
127
149
|
}
|
|
@@ -378,6 +400,17 @@ fn resolve_cargo_native_module(
|
|
|
378
400
|
let crate_path = project_root
|
|
379
401
|
.canonicalize()
|
|
380
402
|
.unwrap_or_else(|_| project_root.to_path_buf());
|
|
403
|
+
// The crate's own source dir, from a `rustDependencies` `path` (so we can find its `tish.d.tish`).
|
|
404
|
+
let source_dir = rust_deps
|
|
405
|
+
.get(&dep_key)
|
|
406
|
+
.and_then(|v| v.as_object())
|
|
407
|
+
.and_then(|o| o.get("path"))
|
|
408
|
+
.and_then(|p| p.as_str())
|
|
409
|
+
.map(|raw| {
|
|
410
|
+
let p = Path::new(raw);
|
|
411
|
+
let resolved = if p.is_absolute() { p.to_path_buf() } else { project_root.join(p) };
|
|
412
|
+
resolved.canonicalize().unwrap_or(resolved)
|
|
413
|
+
});
|
|
381
414
|
Ok(ResolvedNativeModule {
|
|
382
415
|
spec: spec.to_string(),
|
|
383
416
|
package_name: dep_key.clone(),
|
|
@@ -385,6 +418,7 @@ fn resolve_cargo_native_module(
|
|
|
385
418
|
crate_path,
|
|
386
419
|
export_fn,
|
|
387
420
|
use_path_dependency: false,
|
|
421
|
+
source_dir,
|
|
388
422
|
})
|
|
389
423
|
}
|
|
390
424
|
|
|
@@ -437,9 +471,11 @@ fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNati
|
|
|
437
471
|
spec: spec.to_string(),
|
|
438
472
|
package_name: raw_crate.clone(),
|
|
439
473
|
crate_name: raw_crate.replace('-', "_"),
|
|
440
|
-
crate_path,
|
|
474
|
+
crate_path: crate_path.clone(),
|
|
441
475
|
export_fn,
|
|
442
476
|
use_path_dependency: true,
|
|
477
|
+
// A tish.module package IS its own source dir — that's where any `tish.d.tish` lives.
|
|
478
|
+
source_dir: Some(crate_path),
|
|
443
479
|
})
|
|
444
480
|
}
|
|
445
481
|
|
|
@@ -561,7 +597,7 @@ pub fn infer_native_module_exports(program: &Program) -> HashMap<String, HashSet
|
|
|
561
597
|
..
|
|
562
598
|
} => {
|
|
563
599
|
let s = spec.as_ref();
|
|
564
|
-
if is_builtin_native_spec(s) {
|
|
600
|
+
if is_builtin_native_spec(s) || crate::schemes::is_scheme_import(s) {
|
|
565
601
|
continue;
|
|
566
602
|
}
|
|
567
603
|
map.entry(s.to_string())
|
|
@@ -709,6 +745,13 @@ fn find_package_dir(package_name: &str, project_root: &Path) -> Result<PathBuf,
|
|
|
709
745
|
{
|
|
710
746
|
return Ok(node_mod);
|
|
711
747
|
}
|
|
748
|
+
let apple_crate = search.join("tish-apple").join("crates").join(package_name);
|
|
749
|
+
if apple_crate.join("package.json").exists()
|
|
750
|
+
&& read_package_name(&apple_crate.join("package.json"))
|
|
751
|
+
== Some(package_name.to_string())
|
|
752
|
+
{
|
|
753
|
+
return Ok(apple_crate);
|
|
754
|
+
}
|
|
712
755
|
let sibling = search.join(package_name);
|
|
713
756
|
if sibling.join("package.json").exists()
|
|
714
757
|
&& read_package_name(&sibling.join("package.json")) == Some(package_name.to_string())
|
|
@@ -876,7 +919,7 @@ pub fn resolve_project_from_stdin(
|
|
|
876
919
|
|
|
877
920
|
for stmt in &program.statements {
|
|
878
921
|
if let Some(from) = stmt_module_dep(stmt) {
|
|
879
|
-
if is_native_import(from.as_ref()) {
|
|
922
|
+
if is_native_import(from.as_ref()) || is_scoped_native_import(from.as_ref(), from_dir) || crate::schemes::is_scheme_import(from.as_ref()) {
|
|
880
923
|
continue;
|
|
881
924
|
}
|
|
882
925
|
let dep_path = resolve_import_path(from.as_ref(), from_dir, &root_canon)?;
|
|
@@ -944,7 +987,7 @@ fn load_module_recursive(
|
|
|
944
987
|
let dir = canonical.parent().unwrap_or(Path::new("."));
|
|
945
988
|
for stmt in &program.statements {
|
|
946
989
|
if let Some(from) = stmt_module_dep(stmt) {
|
|
947
|
-
if is_native_import(from.as_ref()) {
|
|
990
|
+
if is_native_import(from.as_ref()) || is_scoped_native_import(from.as_ref(), dir) || crate::schemes::is_scheme_import(from.as_ref()) {
|
|
948
991
|
continue; // Native imports don't load files
|
|
949
992
|
}
|
|
950
993
|
let dep_path = resolve_import_path(from.as_ref(), dir, project_root)?;
|
|
@@ -971,7 +1014,13 @@ fn load_module_recursive(
|
|
|
971
1014
|
/// - cargo:… (Cargo `rustDependencies` + generated wrapper; Rust native backend)
|
|
972
1015
|
/// - ffi:… (a C-ABI cdylib loaded via `tish_ffi::load_module` — portable across backends)
|
|
973
1016
|
///
|
|
974
|
-
/// Scoped npm packages (`@scope/pkg`) are merged as Tish source
|
|
1017
|
+
/// Scoped npm packages (`@scope/pkg`) are **merged as Tish source** by default — they are NOT
|
|
1018
|
+
/// native imports and fall through to the module-merge resolver (`exports.tish` / `tish.module`
|
|
1019
|
+
/// path / `tish.source` / `index.tish`). A scoped package is only treated as a native Rust crate
|
|
1020
|
+
/// when it opts in via `tish.crate` / `tish.rustDependencies` in its package.json (the tish-apple
|
|
1021
|
+
/// crates). This split is why `@scope` is intentionally absent below (#38): matching the ecosystem's
|
|
1022
|
+
/// resolution for `import … from "@scope/pkg"` is interop, and the native-vs-merged axis is the one
|
|
1023
|
+
/// piece tish adds on top, keyed off the native markers rather than mere presence of `tish.module`.
|
|
975
1024
|
pub fn is_native_import(spec: &str) -> bool {
|
|
976
1025
|
// A leading `node:` (node:fs, node:fs/promises) resolves the same as the bare form.
|
|
977
1026
|
let spec = spec.strip_prefix("node:").unwrap_or(spec);
|
|
@@ -984,6 +1033,39 @@ pub fn is_native_import(spec: &str) -> bool {
|
|
|
984
1033
|
)
|
|
985
1034
|
}
|
|
986
1035
|
|
|
1036
|
+
// Import schemes (`asset:` and any project-declared prefix) are recognized via the active
|
|
1037
|
+
// [`crate::schemes`] registry — `crate::schemes::is_scheme_import(spec)`. Like native imports, a
|
|
1038
|
+
// scheme spec is NOT resolved as a `.tish` module or npm package; unlike them it links no crate and
|
|
1039
|
+
// generates no Value wrapper — the codegen injects the scheme's include macro and binds each name to
|
|
1040
|
+
// an i32 handle. See [`resolve_scheme_spec`] for the file-resolution half.
|
|
1041
|
+
|
|
1042
|
+
/// The `@scope/pkg` native opt-in described above (#38): true only when the package resolves and
|
|
1043
|
+
/// its package.json carries the native markers — `tish.module == true` (the native-module flag, a
|
|
1044
|
+
/// bool; a string is a merged-source entry path) AND `tish.crate` / `tish.rustDependencies` (the
|
|
1045
|
+
/// Rust-crate opt-in). Anything else — unresolvable, plain `tish.module: true` with no crate
|
|
1046
|
+
/// (#38), or a `.tish` source package like `@tishlang/pg` — falls through to the module-merge
|
|
1047
|
+
/// resolver. Package resolution walks up from `from_dir` (the importing module's directory), the
|
|
1048
|
+
/// same walk [`resolve_native_module`] later repeats from the project root.
|
|
1049
|
+
pub fn is_scoped_native_import(spec: &str, from_dir: &Path) -> bool {
|
|
1050
|
+
if !spec.starts_with('@') {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
let Ok(pkg_dir) = find_package_dir(spec, from_dir) else {
|
|
1054
|
+
return false;
|
|
1055
|
+
};
|
|
1056
|
+
let Ok(content) = std::fs::read_to_string(pkg_dir.join("package.json")) else {
|
|
1057
|
+
return false;
|
|
1058
|
+
};
|
|
1059
|
+
let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
|
|
1060
|
+
return false;
|
|
1061
|
+
};
|
|
1062
|
+
let Some(tish) = json.get("tish") else {
|
|
1063
|
+
return false;
|
|
1064
|
+
};
|
|
1065
|
+
tish.get("module").and_then(|v| v.as_bool()).unwrap_or(false)
|
|
1066
|
+
&& (tish.get("crate").is_some() || tish.get("rustDependencies").is_some())
|
|
1067
|
+
}
|
|
1068
|
+
|
|
987
1069
|
/// True for `ffi:…` specs (portable C-ABI cdylib extensions, loadable on every backend). The
|
|
988
1070
|
/// path after `ffi:` is resolved relative to the importing program and loaded with
|
|
989
1071
|
/// `tish_ffi::load_module`. Unlike `cargo:` (rust-AOT only), `ffi:` is allowed everywhere.
|
|
@@ -1070,6 +1152,63 @@ pub fn resolve_bare_spec(spec: &str, from_dir: &Path, _project_root: &Path) -> O
|
|
|
1070
1152
|
None
|
|
1071
1153
|
}
|
|
1072
1154
|
|
|
1155
|
+
/// Resolve a scheme import (`asset:relpath`, or any project-declared scheme) to a canonical spec.
|
|
1156
|
+
/// For a `resolve_file` scheme, the part after the colon is resolved against the importing module's
|
|
1157
|
+
/// directory (`from_dir`) to an absolute path that must exist — a missing file is a compile-time
|
|
1158
|
+
/// error, not a device panic — and the spec becomes `prefix:/abs/path` (the codegen feeds that
|
|
1159
|
+
/// absolute path to the scheme's include macro; agb's `resolve_path` takes absolute paths as-is, so
|
|
1160
|
+
/// no asset-copy step is needed). A trailing `@N` on the path (e.g. `font:face.ttf@7`) is stripped
|
|
1161
|
+
/// before canonicalize and reattached on the canonical spec so `{size}` is available at emit. A
|
|
1162
|
+
/// bare `font:path` (no `@N`) defaults to [`crate::schemes::FONT_DEFAULT_SIZE`]. A non-`resolve_file`
|
|
1163
|
+
/// scheme passes through unchanged.
|
|
1164
|
+
fn resolve_scheme_spec(spec: &str, from_dir: &Path) -> Result<String, String> {
|
|
1165
|
+
let (scheme_name, prefix, resolve_file) = crate::schemes::with_active(|reg| {
|
|
1166
|
+
reg.matches(spec)
|
|
1167
|
+
.map(|s| (s.name.clone(), s.prefix(), s.resolve_file))
|
|
1168
|
+
})
|
|
1169
|
+
.ok_or_else(|| format!("import '{}' is not a registered scheme", spec))?;
|
|
1170
|
+
let rest = spec.strip_prefix(&prefix).unwrap_or(spec);
|
|
1171
|
+
if !resolve_file {
|
|
1172
|
+
return Ok(spec.to_string());
|
|
1173
|
+
}
|
|
1174
|
+
if rest.is_empty() {
|
|
1175
|
+
return Err(format!(
|
|
1176
|
+
"{} import needs a path, e.g. import {{ player }} from '{}gfx/player.png'",
|
|
1177
|
+
prefix.trim_end_matches(':'),
|
|
1178
|
+
prefix
|
|
1179
|
+
));
|
|
1180
|
+
}
|
|
1181
|
+
let (file_rest, size_opt) = crate::schemes::split_path_size(rest);
|
|
1182
|
+
let path = Path::new(file_rest);
|
|
1183
|
+
let joined = if path.is_absolute() {
|
|
1184
|
+
path.to_path_buf()
|
|
1185
|
+
} else {
|
|
1186
|
+
from_dir.join(path)
|
|
1187
|
+
};
|
|
1188
|
+
let abs = joined.canonicalize().map_err(|e| {
|
|
1189
|
+
format!(
|
|
1190
|
+
"{} import '{}': cannot find '{}' (resolved from {}): {}",
|
|
1191
|
+
prefix.trim_end_matches(':'),
|
|
1192
|
+
spec,
|
|
1193
|
+
file_rest,
|
|
1194
|
+
from_dir.display(),
|
|
1195
|
+
e
|
|
1196
|
+
)
|
|
1197
|
+
})?;
|
|
1198
|
+
let mut out = format!("{}{}", prefix, abs.to_string_lossy());
|
|
1199
|
+
// `font:` always carries an explicit size in the canonical spec (explicit @N, or default 16).
|
|
1200
|
+
if scheme_name == "font" {
|
|
1201
|
+
let size = size_opt.unwrap_or(crate::schemes::FONT_DEFAULT_SIZE);
|
|
1202
|
+
out.push('@');
|
|
1203
|
+
out.push_str(&size.to_string());
|
|
1204
|
+
} else if let Some(size) = size_opt {
|
|
1205
|
+
// Other schemes may use `@N` + `{size}` later; preserve when present.
|
|
1206
|
+
out.push('@');
|
|
1207
|
+
out.push_str(&size.to_string());
|
|
1208
|
+
}
|
|
1209
|
+
Ok(out)
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1073
1212
|
/// Resolve an import specifier (e.g. "./foo.tish", "../lib/utils", "lattish") to an absolute path.
|
|
1074
1213
|
fn resolve_import_path(
|
|
1075
1214
|
spec: &str,
|
|
@@ -1091,8 +1230,12 @@ fn resolve_import_path(
|
|
|
1091
1230
|
spec, spec
|
|
1092
1231
|
));
|
|
1093
1232
|
}
|
|
1233
|
+
let ctx = crate::platform_resolve::resolve_context();
|
|
1234
|
+
if let Some(path) = crate::platform_resolve::resolve_with_platform(spec, from_dir, ctx) {
|
|
1235
|
+
return Ok(path);
|
|
1236
|
+
}
|
|
1094
1237
|
let base = from_dir.join(spec);
|
|
1095
|
-
//
|
|
1238
|
+
// Fallback: legacy single .tish probe when platform context is empty / file missing
|
|
1096
1239
|
let path = if base.extension().is_none() {
|
|
1097
1240
|
let with_ext = base.with_extension("tish");
|
|
1098
1241
|
if with_ext.exists() {
|
|
@@ -1155,7 +1298,7 @@ fn has_cycle_from(
|
|
|
1155
1298
|
) -> Result<bool, String> {
|
|
1156
1299
|
for stmt in &program.statements {
|
|
1157
1300
|
if let Some(from) = stmt_module_dep(stmt) {
|
|
1158
|
-
if is_native_import(from.as_ref()) {
|
|
1301
|
+
if is_native_import(from.as_ref()) || is_scoped_native_import(from.as_ref(), from_dir) || crate::schemes::is_scheme_import(from.as_ref()) {
|
|
1159
1302
|
continue;
|
|
1160
1303
|
}
|
|
1161
1304
|
let dep_path = resolve_import_path(from.as_ref(), from_dir, Path::new("."))?;
|
|
@@ -1837,10 +1980,20 @@ pub fn merge_modules(mut modules: Vec<ResolvedModule>) -> Result<MergedProgram,
|
|
|
1837
1980
|
from,
|
|
1838
1981
|
span,
|
|
1839
1982
|
} => {
|
|
1840
|
-
if is_native_import(from.as_ref())
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1983
|
+
if is_native_import(from.as_ref())
|
|
1984
|
+
|| is_scoped_native_import(from.as_ref(), dir)
|
|
1985
|
+
|| crate::schemes::is_scheme_import(from.as_ref())
|
|
1986
|
+
{
|
|
1987
|
+
// Normalize fs/http/process -> tish:fs etc. for Node compatibility.
|
|
1988
|
+
// For `asset:` imports, resolve the (relative) file path to an absolute
|
|
1989
|
+
// one now, while we still know the importing module's directory — the GBA
|
|
1990
|
+
// codegen emits `include_aseprite_inner!("<abspath>")` from that spec.
|
|
1991
|
+
let canonical_spec = if crate::schemes::is_scheme_import(from.as_ref()) {
|
|
1992
|
+
resolve_scheme_spec(from.as_ref(), dir)?
|
|
1993
|
+
} else {
|
|
1994
|
+
normalize_builtin_spec(from.as_ref())
|
|
1995
|
+
.unwrap_or_else(|| from.to_string())
|
|
1996
|
+
};
|
|
1844
1997
|
// Emit VarDecl with NativeModuleLoad for each specifier
|
|
1845
1998
|
for spec in specifiers {
|
|
1846
1999
|
match spec {
|
|
@@ -2090,3 +2243,34 @@ mod cargo_spec_tests {
|
|
|
2090
2243
|
);
|
|
2091
2244
|
}
|
|
2092
2245
|
}
|
|
2246
|
+
|
|
2247
|
+
#[cfg(test)]
|
|
2248
|
+
mod find_package_dir_tests {
|
|
2249
|
+
use super::find_package_dir;
|
|
2250
|
+
|
|
2251
|
+
#[test]
|
|
2252
|
+
fn prefers_tish_apple_crates_over_sibling() {
|
|
2253
|
+
let dir = tempfile::tempdir().expect("tempdir");
|
|
2254
|
+
let root = dir.path();
|
|
2255
|
+
// sibling decoy
|
|
2256
|
+
let sibling = root.join("tish-macos");
|
|
2257
|
+
std::fs::create_dir_all(&sibling).unwrap();
|
|
2258
|
+
std::fs::write(
|
|
2259
|
+
sibling.join("package.json"),
|
|
2260
|
+
r#"{"name":"tish-macos","tish":{"module":true,"crate":"tishlang_macos","export":"macos_object"}}"#,
|
|
2261
|
+
)
|
|
2262
|
+
.unwrap();
|
|
2263
|
+
// preferred apple path
|
|
2264
|
+
let apple = root.join("tish-apple").join("crates").join("tish-macos");
|
|
2265
|
+
std::fs::create_dir_all(&apple).unwrap();
|
|
2266
|
+
std::fs::write(
|
|
2267
|
+
apple.join("package.json"),
|
|
2268
|
+
r#"{"name":"tish-macos","tish":{"module":true,"crate":"tish-macos","export":"macos_object"}}"#,
|
|
2269
|
+
)
|
|
2270
|
+
.unwrap();
|
|
2271
|
+
let project = root.join("app");
|
|
2272
|
+
std::fs::create_dir_all(&project).unwrap();
|
|
2273
|
+
let hit = find_package_dir("tish-macos", &project).expect("resolve");
|
|
2274
|
+
assert_eq!(hit.canonicalize().unwrap(), apple.canonicalize().unwrap());
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
//! Import-scheme registry — the extension seam for custom import prefixes.
|
|
2
|
+
//!
|
|
3
|
+
//! An *import scheme* is a prefix like `asset:` that means "don't resolve this as a `.tish` module
|
|
4
|
+
//! or npm package; instead resolve it my way and inject my code." tish core ships **no** built-in
|
|
5
|
+
//! schemes — it is target-agnostic (see [`SchemeRegistry::builtin`]). Schemes are contributed by
|
|
6
|
+
//! cargo path-dependencies that ship a `tish.schemes.json` (this is how `tish-agb` provides
|
|
7
|
+
//! `asset:`/`sheet:`/`background:`/`wav:`/`map:` for free to any game that depends on it) and by a
|
|
8
|
+
//! project's own `package.json` → `tish.schemes`, with no changes to the compiler. This is the same
|
|
9
|
+
//! philosophy as `cargo:` imports (config-declared, zero core edits per crate), generalized.
|
|
10
|
+
//!
|
|
11
|
+
//! A scheme is described declaratively:
|
|
12
|
+
//! * `prefix` — the scheme name (`asset` → matches `asset:...`).
|
|
13
|
+
//! * `resolve_file` — resolve the rest as a project-relative file and validate it exists.
|
|
14
|
+
//! * `targets` — per emit-target ("gba", …) code templates: the `use`s + body of a generated
|
|
15
|
+
//! `mod`, an accessor, and a registration call. Templates substitute `{path}` (the resolved
|
|
16
|
+
//! absolute path, as a Rust string literal), `{mod}` (the generated module's identifier),
|
|
17
|
+
//! `{charset}` (used non-ASCII chars), and optionally `{size}` (from a trailing `@N` on the path).
|
|
18
|
+
//!
|
|
19
|
+
//! Only *file-baking* schemes are expressible this way today — the family `asset:` belongs to
|
|
20
|
+
//! (resolve a file → emit a macro + a registration + bind an i32 handle). Schemes needing
|
|
21
|
+
//! compile-time logic (e.g. parsing aseprite tags) will want a code-plugin surface later.
|
|
22
|
+
//!
|
|
23
|
+
//! The active registry is compile-scoped: [`set_active`] installs it at the start of a build and
|
|
24
|
+
//! the resolve/codegen predicates read it via [`with_active`]. It defaults to [`builtin`] so any
|
|
25
|
+
//! path that doesn't install one still sees the built-in schemes (matching prior behaviour).
|
|
26
|
+
|
|
27
|
+
use std::cell::RefCell;
|
|
28
|
+
use std::collections::HashMap;
|
|
29
|
+
use std::path::Path;
|
|
30
|
+
|
|
31
|
+
/// What an imported name binds to. Only `Handle` (an `i32` index into a runtime arena) exists
|
|
32
|
+
/// today; kept as an enum so richer bindings (typed structs, const data) can be added later.
|
|
33
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
34
|
+
pub enum BindKind {
|
|
35
|
+
/// The imported name is an `i32` handle — its index among the scheme's registered files.
|
|
36
|
+
Handle,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// One emit target's code templates for a scheme (e.g. the `gba` recipe for `asset:`).
|
|
40
|
+
#[derive(Clone, Debug)]
|
|
41
|
+
pub struct TargetEmit {
|
|
42
|
+
/// `use` lines placed at the top of the generated `mod`.
|
|
43
|
+
pub uses: Vec<String>,
|
|
44
|
+
/// The module body — typically the include macro. `{path}` → the file as a Rust string literal.
|
|
45
|
+
pub module_body: String,
|
|
46
|
+
/// A public accessor emitted inside the same `mod` (so it can see the macro's private statics).
|
|
47
|
+
pub accessor: String,
|
|
48
|
+
/// Registration call emitted in the entry before `run()`. `{mod}` → the generated module ident.
|
|
49
|
+
pub register: String,
|
|
50
|
+
/// What each imported name binds to.
|
|
51
|
+
pub bind: BindKind,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// A declaratively-described import scheme.
|
|
55
|
+
#[derive(Clone, Debug)]
|
|
56
|
+
pub struct SchemeDef {
|
|
57
|
+
/// The prefix WITHOUT the trailing colon (`asset`); matches specs starting `asset:`.
|
|
58
|
+
pub name: String,
|
|
59
|
+
/// Resolve the part after the colon as a project-relative file (canonicalize + validate exists).
|
|
60
|
+
pub resolve_file: bool,
|
|
61
|
+
/// Per emit-target code templates.
|
|
62
|
+
pub targets: HashMap<String, TargetEmit>,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
impl SchemeDef {
|
|
66
|
+
/// The spec prefix including the colon (`asset:`).
|
|
67
|
+
pub fn prefix(&self) -> String {
|
|
68
|
+
format!("{}:", self.name)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// The set of active import schemes for a build.
|
|
73
|
+
#[derive(Clone, Debug, Default)]
|
|
74
|
+
pub struct SchemeRegistry {
|
|
75
|
+
pub schemes: Vec<SchemeDef>,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
impl SchemeRegistry {
|
|
79
|
+
/// The schemes tish core ships with — **none**. tish core is target-agnostic and knows nothing
|
|
80
|
+
/// about agb/GBA; concrete schemes (like `asset:`) are contributed by the crates that implement
|
|
81
|
+
/// their runtime (see [`from_project`](Self::from_project)). Kept as the seam for any future
|
|
82
|
+
/// genuinely-generic built-in scheme.
|
|
83
|
+
pub fn builtin() -> Self {
|
|
84
|
+
SchemeRegistry {
|
|
85
|
+
schemes: Vec::new(),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// The active registry for a build: schemes contributed by cargo dependencies, then the
|
|
90
|
+
/// project's own, merged in that order (project overrides a dependency of the same name).
|
|
91
|
+
///
|
|
92
|
+
/// * **Dependency-contributed** — each `tish.rustDependencies` entry with a `path` may ship a
|
|
93
|
+
/// `tish.schemes.json` (`{ "schemes": { … } }`) in its crate dir. This is how tish-agb
|
|
94
|
+
/// provides `asset:` without any tish-core edits: a game that depends on tish-agb (as every
|
|
95
|
+
/// sprite game already does) gets the scheme for free, no per-game config.
|
|
96
|
+
/// * **Project-local** — `package.json` → `tish.schemes`, for one-off or overriding schemes.
|
|
97
|
+
///
|
|
98
|
+
/// A malformed entry is skipped, not fatal.
|
|
99
|
+
pub fn from_project(project_root: &Path) -> Self {
|
|
100
|
+
let mut reg = Self::builtin();
|
|
101
|
+
let tish = crate::resolve::read_project_tish_config(project_root);
|
|
102
|
+
// 1. Schemes contributed by cargo path-dependencies.
|
|
103
|
+
if let Some(deps) = tish.get("rustDependencies").and_then(|v| v.as_object()) {
|
|
104
|
+
for val in deps.values() {
|
|
105
|
+
let Some(path) = val
|
|
106
|
+
.as_object()
|
|
107
|
+
.and_then(|o| o.get("path"))
|
|
108
|
+
.and_then(|p| p.as_str())
|
|
109
|
+
else {
|
|
110
|
+
continue;
|
|
111
|
+
};
|
|
112
|
+
let p = Path::new(path);
|
|
113
|
+
let dep_dir = if p.is_absolute() {
|
|
114
|
+
p.to_path_buf()
|
|
115
|
+
} else {
|
|
116
|
+
project_root.join(p)
|
|
117
|
+
};
|
|
118
|
+
let manifest = dep_dir.join("tish.schemes.json");
|
|
119
|
+
if let Ok(content) = std::fs::read_to_string(&manifest) {
|
|
120
|
+
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
|
|
121
|
+
reg.merge_schemes(json.get("schemes"));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 2. Project-local schemes (override dependency-contributed ones of the same name).
|
|
127
|
+
reg.merge_schemes(tish.get("schemes"));
|
|
128
|
+
reg
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Merge a JSON `{ name: def, … }` schemes object into the registry; a same-named scheme
|
|
132
|
+
/// replaces the existing one.
|
|
133
|
+
fn merge_schemes(&mut self, schemes: Option<&serde_json::Value>) {
|
|
134
|
+
let Some(obj) = schemes.and_then(|v| v.as_object()) else {
|
|
135
|
+
return;
|
|
136
|
+
};
|
|
137
|
+
for (name, def) in obj {
|
|
138
|
+
if let Some(parsed) = parse_scheme_def(name, def) {
|
|
139
|
+
self.schemes.retain(|s| s.name != parsed.name);
|
|
140
|
+
self.schemes.push(parsed);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// The scheme whose prefix `spec` starts with, if any.
|
|
146
|
+
pub fn matches(&self, spec: &str) -> Option<&SchemeDef> {
|
|
147
|
+
self.schemes.iter().find(|s| spec.starts_with(&s.prefix()))
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Parse one `tish.schemes["name"]` JSON entry into a [`SchemeDef`]. Returns `None` (skip) on a
|
|
152
|
+
/// shape that doesn't describe a valid scheme.
|
|
153
|
+
fn parse_scheme_def(name: &str, def: &serde_json::Value) -> Option<SchemeDef> {
|
|
154
|
+
// The name is interpolated into a generated module identifier (`mod __scheme_<name>_<j>`) and
|
|
155
|
+
// used as the `<name>:` import prefix, so it must be a valid Rust identifier. A name like
|
|
156
|
+
// `sprite-sheet` would emit un-compilable Rust (a raw rustc error far from its cause); skip it
|
|
157
|
+
// instead, consistent with "a malformed entry is skipped, not fatal".
|
|
158
|
+
if !is_valid_scheme_name(name) {
|
|
159
|
+
return None;
|
|
160
|
+
}
|
|
161
|
+
let obj = def.as_object()?;
|
|
162
|
+
let resolve_file = obj.get("file").and_then(|v| v.as_bool()).unwrap_or(true);
|
|
163
|
+
let mut targets = HashMap::new();
|
|
164
|
+
if let Some(target_map) = obj.get("targets").and_then(|v| v.as_object()) {
|
|
165
|
+
for (target, spec) in target_map {
|
|
166
|
+
let s = spec.as_object()?;
|
|
167
|
+
let uses = s
|
|
168
|
+
.get("uses")
|
|
169
|
+
.and_then(|v| v.as_array())
|
|
170
|
+
.map(|a| {
|
|
171
|
+
a.iter()
|
|
172
|
+
.filter_map(|x| x.as_str().map(|s| s.to_string()))
|
|
173
|
+
.collect()
|
|
174
|
+
})
|
|
175
|
+
.unwrap_or_default();
|
|
176
|
+
let module_body = s.get("module").and_then(|v| v.as_str())?.to_string();
|
|
177
|
+
let accessor = s
|
|
178
|
+
.get("accessor")
|
|
179
|
+
.and_then(|v| v.as_str())
|
|
180
|
+
.unwrap_or("")
|
|
181
|
+
.to_string();
|
|
182
|
+
let register = s
|
|
183
|
+
.get("register")
|
|
184
|
+
.and_then(|v| v.as_str())
|
|
185
|
+
.unwrap_or("")
|
|
186
|
+
.to_string();
|
|
187
|
+
targets.insert(
|
|
188
|
+
target.clone(),
|
|
189
|
+
TargetEmit {
|
|
190
|
+
uses,
|
|
191
|
+
module_body,
|
|
192
|
+
accessor,
|
|
193
|
+
register,
|
|
194
|
+
bind: BindKind::Handle,
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
Some(SchemeDef {
|
|
200
|
+
name: name.to_string(),
|
|
201
|
+
resolve_file,
|
|
202
|
+
targets,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// A scheme name must be a valid Rust identifier: it becomes part of a generated module ident
|
|
207
|
+
/// (`__scheme_<name>_<j>`) and the `<name>:` import prefix. ASCII letter/underscore, then
|
|
208
|
+
/// letters/digits/underscores. (The same shape `types::is_struct_field_safe` enforces for native
|
|
209
|
+
/// struct field keys, kept local so `schemes` stays self-contained.)
|
|
210
|
+
fn is_valid_scheme_name(name: &str) -> bool {
|
|
211
|
+
let mut chars = name.chars();
|
|
212
|
+
match chars.next() {
|
|
213
|
+
Some(c) if c == '_' || c.is_ascii_alphabetic() => {}
|
|
214
|
+
_ => return false,
|
|
215
|
+
}
|
|
216
|
+
chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
thread_local! {
|
|
220
|
+
static ACTIVE: RefCell<SchemeRegistry> = RefCell::new(SchemeRegistry::builtin());
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// Install the registry for the current build. Call once at the start of a compile, before
|
|
224
|
+
/// resolution — every scheme predicate reads whatever is installed here.
|
|
225
|
+
pub fn set_active(registry: SchemeRegistry) {
|
|
226
|
+
ACTIVE.with(|c| *c.borrow_mut() = registry);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Read from the active registry.
|
|
230
|
+
pub fn with_active<R>(f: impl FnOnce(&SchemeRegistry) -> R) -> R {
|
|
231
|
+
ACTIVE.with(|c| f(&c.borrow()))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// True if `spec` is a registered import scheme (built-in or project-declared).
|
|
235
|
+
pub fn is_scheme_import(spec: &str) -> bool {
|
|
236
|
+
with_active(|r| r.matches(spec).is_some())
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Default bake size when `font:path` omits `@N` (most common native grid: monogram/alagard/m3x6).
|
|
240
|
+
pub const FONT_DEFAULT_SIZE: u32 = 16;
|
|
241
|
+
|
|
242
|
+
/// Split a scheme path rest into `(file_path, size)` when it ends with `@` + digits
|
|
243
|
+
/// (`face.ttf@7` → `("face.ttf", Some(7))`). Non-digit suffixes after `@` are left intact
|
|
244
|
+
/// (`foo@2x.png` is not a size).
|
|
245
|
+
pub fn split_path_size(rest: &str) -> (&str, Option<u32>) {
|
|
246
|
+
let Some((path, size_str)) = rest.rsplit_once('@') else {
|
|
247
|
+
return (rest, None);
|
|
248
|
+
};
|
|
249
|
+
if path.is_empty() || size_str.is_empty() || !size_str.bytes().all(|b| b.is_ascii_digit()) {
|
|
250
|
+
return (rest, None);
|
|
251
|
+
}
|
|
252
|
+
match size_str.parse::<u32>() {
|
|
253
|
+
Ok(n) if n > 0 => (path, Some(n)),
|
|
254
|
+
_ => (rest, None),
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Substitute `{path}` (the file as a Rust string literal), `{mod}` (the generated module ident),
|
|
259
|
+
/// `{charset}` (used non-ASCII chars as a Rust string literal), and `{size}` (plain integer from a
|
|
260
|
+
/// trailing `@N` on the path, e.g. `font:face.ttf@7`) in a template. `{path}` never includes `@N`.
|
|
261
|
+
pub fn render_template(template: &str, abspath: &str, module_ident: &str, charset: &str) -> String {
|
|
262
|
+
let (path, size) = split_path_size(abspath);
|
|
263
|
+
let size_str = size
|
|
264
|
+
.map(|n| n.to_string())
|
|
265
|
+
.unwrap_or_else(|| FONT_DEFAULT_SIZE.to_string());
|
|
266
|
+
template
|
|
267
|
+
.replace("{path}", &format!("{:?}", path))
|
|
268
|
+
.replace("{mod}", module_ident)
|
|
269
|
+
.replace("{charset}", &format!("{:?}", charset))
|
|
270
|
+
.replace("{size}", &size_str)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#[cfg(test)]
|
|
274
|
+
mod tests {
|
|
275
|
+
use super::*;
|
|
276
|
+
|
|
277
|
+
#[test]
|
|
278
|
+
fn scheme_name_must_be_a_rust_identifier() {
|
|
279
|
+
// Valid identifiers pass.
|
|
280
|
+
assert!(is_valid_scheme_name("asset"));
|
|
281
|
+
assert!(is_valid_scheme_name("sprite_sheet"));
|
|
282
|
+
assert!(is_valid_scheme_name("_x"));
|
|
283
|
+
assert!(is_valid_scheme_name("bg2"));
|
|
284
|
+
// Anything that would break the generated `mod __scheme_<name>_<j>` is rejected.
|
|
285
|
+
assert!(!is_valid_scheme_name("sprite-sheet")); // hyphen
|
|
286
|
+
assert!(!is_valid_scheme_name("")); // empty
|
|
287
|
+
assert!(!is_valid_scheme_name("2d")); // leading digit
|
|
288
|
+
assert!(!is_valid_scheme_name("a:b")); // colon
|
|
289
|
+
assert!(!is_valid_scheme_name("a b")); // space
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#[test]
|
|
293
|
+
fn parse_scheme_def_skips_invalid_name() {
|
|
294
|
+
let def = serde_json::json!({
|
|
295
|
+
"file": true,
|
|
296
|
+
"targets": { "gba": { "module": "// x", "register": "" } }
|
|
297
|
+
});
|
|
298
|
+
// A hyphenated name is skipped (returns None) rather than emitting un-compilable Rust.
|
|
299
|
+
assert!(parse_scheme_def("sprite-sheet", &def).is_none());
|
|
300
|
+
// The same def under a valid name parses.
|
|
301
|
+
assert!(parse_scheme_def("sheet", &def).is_some());
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#[test]
|
|
305
|
+
fn split_path_size_parses_trailing_at_digits() {
|
|
306
|
+
assert_eq!(split_path_size("face.ttf@7"), ("face.ttf", Some(7)));
|
|
307
|
+
assert_eq!(split_path_size("/abs/face.ttf@16"), ("/abs/face.ttf", Some(16)));
|
|
308
|
+
assert_eq!(split_path_size("face.ttf"), ("face.ttf", None));
|
|
309
|
+
// Not a size — non-digit suffix after @.
|
|
310
|
+
assert_eq!(split_path_size("foo@2x.png"), ("foo@2x.png", None));
|
|
311
|
+
assert_eq!(split_path_size("@7"), ("@7", None));
|
|
312
|
+
assert_eq!(split_path_size("face.ttf@0"), ("face.ttf@0", None));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#[test]
|
|
316
|
+
fn render_template_substitutes_size_and_strips_at_from_path() {
|
|
317
|
+
let out = render_template(
|
|
318
|
+
"pack!({path}, {size}, {charset}); //{mod}",
|
|
319
|
+
"/abs/face.ttf@7",
|
|
320
|
+
"__scheme_font_0",
|
|
321
|
+
"你好",
|
|
322
|
+
);
|
|
323
|
+
assert!(out.contains("pack!(\"/abs/face.ttf\", 7, \"你好\")"), "got: {out}");
|
|
324
|
+
assert!(out.contains("//__scheme_font_0"));
|
|
325
|
+
assert!(!out.contains("face.ttf@7"));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#[test]
|
|
329
|
+
fn render_template_defaults_size_when_missing() {
|
|
330
|
+
let out = render_template("x({size})", "/abs/face.ttf", "m", "");
|
|
331
|
+
assert_eq!(out, format!("x({})", FONT_DEFAULT_SIZE));
|
|
332
|
+
}
|
|
333
|
+
}
|