@ape-egg/vibe 2.1.22 → 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/README.md +112 -5
- package/boot.js +4 -4
- package/component.js +27 -29
- package/hot-module-refresh.js +4 -4
- package/index.js +26 -17
- package/llms.txt +36 -5
- package/package.json +20 -14
- package/runtime/affected.js +159 -36
- package/runtime/cleanup.js +45 -1
- package/runtime/component.js +360 -98
- package/runtime/conditionals.js +111 -14
- package/runtime/debug.js +24 -0
- package/runtime/dispatch.js +172 -0
- package/runtime/hydrate.js +277 -110
- package/runtime/index.js +189 -65
- package/runtime/iterate.js +125 -50
- package/runtime/iteration-utils.js +59 -8
- package/runtime/manifest.js +77 -2
- package/runtime/parse.js +81 -11
- package/runtime/pre-compiled-iterations.js +19 -6
- package/runtime/pre-compiled-manifest.js +13 -4
- package/runtime/staging.js +153 -0
- package/runtime/state.js +31 -0
- package/runtime/tracking.js +173 -0
- package/runtime/utils.js +155 -78
- package/spa.js +206 -0
- package/vibe.css +8 -4
- package/CHANGELOG.md +0 -1159
- package/ROADMAP.md +0 -397
- package/compiler/bin/vibe-compile.js +0 -121
- package/compiler/native/.gitkeep +0 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +0 -2023
- package/compiler/src/Cargo.toml +0 -38
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +0 -241
- package/compiler/src/compiler/binding_case.rs +0 -88
- package/compiler/src/compiler/compile.rs +0 -2522
- package/compiler/src/compiler/component_tagger.rs +0 -469
- package/compiler/src/compiler/iteration_optimizer.rs +0 -455
- package/compiler/src/compiler/js_analyzer.rs +0 -715
- package/compiler/src/compiler/manifest_builder.rs +0 -693
- package/compiler/src/compiler/mod.rs +0 -15
- package/compiler/src/compiler/name_binding_protect.rs +0 -207
- package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
- package/compiler/src/compiler/state_extractor.rs +0 -263
- package/compiler/src/compiler/value_stamper.rs +0 -921
- package/compiler/src/compiler/watcher.rs +0 -1147
- package/compiler/src/config.rs +0 -239
- package/compiler/src/main.rs +0 -347
- package/compiler/src/parser/element.rs +0 -96
- package/compiler/src/parser/html.rs +0 -1004
- package/compiler/src/parser/mod.rs +0 -8
- package/runtime/pre-compiled-manifest.test.mjs +0 -58
- package/runtime/scope.js +0 -50
- package/test-results/.last-run.json +0 -4
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
use regex::Regex;
|
|
2
|
-
use serde::{Serialize, Deserialize};
|
|
3
|
-
use std::collections::HashMap;
|
|
4
|
-
|
|
5
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
6
|
-
#[serde(rename_all = "camelCase")]
|
|
7
|
-
pub struct CompiledIteration {
|
|
8
|
-
pub item_alias: String,
|
|
9
|
-
pub index_alias: String,
|
|
10
|
-
pub state_path: String,
|
|
11
|
-
pub batch_fn: String,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
15
|
-
pub struct IterationOptimizations {
|
|
16
|
-
pub iterations: HashMap<String, CompiledIteration>,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/// Generate compiled batch functions for iterations in HTML
|
|
20
|
-
pub fn build_iteration_optimizations(html: &str) -> Option<IterationOptimizations> {
|
|
21
|
-
let iterations = extract_and_compile_iterations(html);
|
|
22
|
-
|
|
23
|
-
if iterations.is_empty() {
|
|
24
|
-
None
|
|
25
|
-
} else {
|
|
26
|
-
Some(IterationOptimizations { iterations })
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/// Extract <!-- each --> blocks and compile them to batch functions
|
|
31
|
-
fn extract_and_compile_iterations(html: &str) -> HashMap<String, CompiledIteration> {
|
|
32
|
-
let mut result = HashMap::new();
|
|
33
|
-
|
|
34
|
-
// Match: <!-- each arrayPath as item, index -->
|
|
35
|
-
let each_re = Regex::new(r"<!--\s*each\s+([^\s]+)\s+as\s+([^\s,]+)(?:\s*,\s*([^\s]+))?\s*-->").unwrap();
|
|
36
|
-
|
|
37
|
-
let mut search_start = 0;
|
|
38
|
-
|
|
39
|
-
while let Some(start_match) = each_re.find_at(html, search_start) {
|
|
40
|
-
let captures = each_re.captures(&html[start_match.start()..]).unwrap();
|
|
41
|
-
let state_path = captures.get(1).unwrap().as_str();
|
|
42
|
-
let item_alias = captures.get(2).unwrap().as_str();
|
|
43
|
-
let index_alias = captures.get(3).map(|m| m.as_str()).unwrap_or("index");
|
|
44
|
-
|
|
45
|
-
let template_start = start_match.end();
|
|
46
|
-
|
|
47
|
-
// Find matching <!-- /each --> using depth counting
|
|
48
|
-
if let Some((end_pos, end_after)) = find_matching_each_end(html, template_start) {
|
|
49
|
-
let template_html = &html[template_start..end_pos];
|
|
50
|
-
|
|
51
|
-
// Skip if this iteration is inside a conditional (parent won't be compiled)
|
|
52
|
-
if is_in_conditional_context(html, start_match.start()) {
|
|
53
|
-
search_start = end_after;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Check if template has nested structures (skip if so)
|
|
58
|
-
if has_nested_structures(template_html) {
|
|
59
|
-
search_start = end_after;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Raw-HTML bindings can't ride a template literal: a batch function
|
|
64
|
-
// interpolates RawHtml via toString(), which escapes. Skip the batch
|
|
65
|
-
// function so the runtime renders these rows through clone+hydrate —
|
|
66
|
-
// the one place that implements $.unsafe semantics (innerHTML +
|
|
67
|
-
// inert subtree).
|
|
68
|
-
if template_html.contains("$.unsafe(") {
|
|
69
|
-
search_start = end_after;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Generate hash for this template
|
|
74
|
-
let hash = generate_template_hash(template_html);
|
|
75
|
-
|
|
76
|
-
// Compile template to batch function
|
|
77
|
-
let batch_fn = compile_template_to_batch_fn(
|
|
78
|
-
template_html,
|
|
79
|
-
item_alias,
|
|
80
|
-
index_alias,
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
result.insert(hash, CompiledIteration {
|
|
84
|
-
item_alias: item_alias.to_string(),
|
|
85
|
-
index_alias: index_alias.to_string(),
|
|
86
|
-
state_path: state_path.to_string(),
|
|
87
|
-
batch_fn,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
search_start = end_after;
|
|
91
|
-
} else {
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
result
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/// Check if template contains conditionals (we skip these)
|
|
100
|
-
/// Nested iterations are supported, but not if parent has conditionals
|
|
101
|
-
fn has_nested_structures(template: &str) -> bool {
|
|
102
|
-
template.contains("<!-- if")
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/// Check if we're inside a non-compiled parent iteration
|
|
106
|
-
/// This happens when the template contains conditionals that prevent compilation
|
|
107
|
-
fn is_in_conditional_context(html: &str, pos: usize) -> bool {
|
|
108
|
-
// Look backwards from current position for unclosed conditionals
|
|
109
|
-
let before = &html[..pos];
|
|
110
|
-
let if_count = before.matches("<!-- if").count();
|
|
111
|
-
let endif_count = before.matches("<!-- /if").count();
|
|
112
|
-
if_count > endif_count
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/// Generate a stable hash for a template
|
|
116
|
-
fn generate_template_hash(template: &str) -> String {
|
|
117
|
-
use std::collections::hash_map::DefaultHasher;
|
|
118
|
-
use std::hash::{Hash, Hasher};
|
|
119
|
-
|
|
120
|
-
let mut hasher = DefaultHasher::new();
|
|
121
|
-
template.trim().hash(&mut hasher);
|
|
122
|
-
let hash = hasher.finish();
|
|
123
|
-
format!("iter_{:x}", hash)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/// DOM element properties — set on the element, kept as value attributes in
|
|
127
|
-
/// batch output. Mirrors DOM_PROPERTIES in runtime/constants.js.
|
|
128
|
-
const DOM_PROPERTIES: &[&str] = &["value", "checked", "selected"];
|
|
129
|
-
|
|
130
|
-
/// Attributes that keep their string value verbatim (never boolean-coerced).
|
|
131
|
-
/// Mirrors VALUE_ATTRS in runtime/constants.js — the two lists must stay in
|
|
132
|
-
/// lockstep or batch and clone paths render different attributes.
|
|
133
|
-
const VALUE_ATTRS: &[&str] = &[
|
|
134
|
-
"class", "style", "id", "title", "lang", "dir", "tabindex", "accesskey",
|
|
135
|
-
"slot", "part", "is", "nonce", "popover", "anchor",
|
|
136
|
-
"contenteditable", "draggable", "spellcheck", "translate",
|
|
137
|
-
"autocapitalize", "inputmode", "enterkeyhint", "virtualkeyboardpolicy",
|
|
138
|
-
"href", "src", "action", "cite", "data", "poster", "srcset",
|
|
139
|
-
"imagesrcset", "formaction", "ping", "usemap", "manifest", "codebase",
|
|
140
|
-
"name", "type", "value", "placeholder", "pattern", "min", "max", "step",
|
|
141
|
-
"minlength", "maxlength", "size", "accept", "autocomplete", "list",
|
|
142
|
-
"form", "formmethod", "formtarget", "formenctype", "wrap", "method",
|
|
143
|
-
"enctype", "for", "dirname",
|
|
144
|
-
"alt", "label", "summary", "abbr",
|
|
145
|
-
"width", "height", "cols", "rows", "span", "rowspan", "colspan",
|
|
146
|
-
"low", "high", "optimum",
|
|
147
|
-
"target", "rel", "hreflang", "download", "as", "media", "charset",
|
|
148
|
-
"crossorigin", "integrity", "loading", "decoding", "fetchpriority",
|
|
149
|
-
"referrerpolicy", "blocking", "imagesizes", "sizes",
|
|
150
|
-
"preload", "kind", "srclang",
|
|
151
|
-
"content", "http-equiv",
|
|
152
|
-
"sandbox", "allow", "srcdoc", "credentialless",
|
|
153
|
-
"headers", "scope",
|
|
154
|
-
"datetime",
|
|
155
|
-
"coords", "shape",
|
|
156
|
-
];
|
|
157
|
-
|
|
158
|
-
/// Whether an attribute keeps its string value (vs boolean coercion).
|
|
159
|
-
/// Mirrors isValueStyleAttr in runtime/iterate.js.
|
|
160
|
-
/// Turn ` data-vibe-namebind="@[A]@[B]"` back into ` @[A]="" @[B]=""` so each relocated
|
|
161
|
-
/// binding is rendered as a name-binding (` resolvedName=""`) rather than having its
|
|
162
|
-
/// resolved value written into the transport attribute. See name_binding_protect.
|
|
163
|
-
fn restore_relocated_name_bindings(template: &str) -> String {
|
|
164
|
-
let wrapper = Regex::new(
|
|
165
|
-
r#"\s+data-vibe-namebind="((?:@\[(?:[^\[\]'"]|\[[^\]]*\]|'[^']*'|"[^"]*")+\])+)""#,
|
|
166
|
-
)
|
|
167
|
-
.unwrap();
|
|
168
|
-
let one = Regex::new(r#"@\[(?:[^\[\]'"]|\[[^\]]*\]|'[^']*'|"[^"]*")+\]"#).unwrap();
|
|
169
|
-
wrapper
|
|
170
|
-
.replace_all(template, |caps: ®ex::Captures| {
|
|
171
|
-
let mut s = String::new();
|
|
172
|
-
for m in one.find_iter(&caps[1]) {
|
|
173
|
-
s.push(' ');
|
|
174
|
-
s.push_str(m.as_str());
|
|
175
|
-
s.push_str("=\"\"");
|
|
176
|
-
}
|
|
177
|
-
s
|
|
178
|
-
})
|
|
179
|
-
.to_string()
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
fn is_value_style_attr(name: &str) -> bool {
|
|
183
|
-
VALUE_ATTRS.contains(&name)
|
|
184
|
-
|| name.starts_with("data-")
|
|
185
|
-
|| name.starts_with("aria-")
|
|
186
|
-
|| name.starts_with("on")
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/// Escape a static text chunk for a JS template-literal context.
|
|
190
|
-
fn escape_tpl_text(text: &str) -> String {
|
|
191
|
-
text.replace('\\', "\\\\")
|
|
192
|
-
.replace('`', "\\`")
|
|
193
|
-
.replace("${", "\\${")
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/// Compile template HTML to a batch function string
|
|
197
|
-
/// Supports nested <!-- each --> blocks
|
|
198
|
-
fn compile_template_to_batch_fn(
|
|
199
|
-
template: &str,
|
|
200
|
-
item_alias: &str,
|
|
201
|
-
index_alias: &str,
|
|
202
|
-
) -> String {
|
|
203
|
-
// Normalize static boolean attributes: <div attr=""> → <div attr>
|
|
204
|
-
// (matches CSS attribute selectors). Runs on raw template text, before
|
|
205
|
-
// emission — emitted JS below also contains attr="" inside string
|
|
206
|
-
// literals, which must not be touched.
|
|
207
|
-
let boolean_attr_re = Regex::new(r#"(\w+)="""#).unwrap();
|
|
208
|
-
let cleaned = boolean_attr_re.replace_all(template, "$1");
|
|
209
|
-
|
|
210
|
-
let body = emit_template_literal(&cleaned);
|
|
211
|
-
|
|
212
|
-
format!(
|
|
213
|
-
r#"(arr, $) => {{ let html = ''; const len = arr.length; for (let {index} = 0; {index} < len; {index}++) {{ const {item} = arr[{index}]; html += `{body}`; }} return html; }}"#,
|
|
214
|
-
index = index_alias,
|
|
215
|
-
item = item_alias,
|
|
216
|
-
body = body
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/// Emit the body of a JS template literal for a template chunk:
|
|
221
|
-
/// - static text escaped for template-literal context
|
|
222
|
-
/// - attribute pure bindings classified like hydrate.js / compileBatchFn:
|
|
223
|
-
/// DOM properties and value-style attributes keep `attr="${expr}"`,
|
|
224
|
-
/// boolean-coerced attributes become `${(expr) ? ' attr=""' : ''}` so the
|
|
225
|
-
/// attribute is ABSENT when falsy and present-empty when truthy
|
|
226
|
-
/// - remaining @[expr] bindings (text content, partial attribute values)
|
|
227
|
-
/// become `${expr}` interpolations
|
|
228
|
-
/// - nested <!-- each --> blocks become inline IIFE loops, recursively
|
|
229
|
-
///
|
|
230
|
-
/// Anything the runtime clone+hydrate path renders, this output has to render
|
|
231
|
-
/// identically — it is the compiled twin of compileBatchFn in runtime/iterate.js.
|
|
232
|
-
fn emit_template_literal(template: &str) -> String {
|
|
233
|
-
// Restore name-bindings the compiler relocated into `data-vibe-namebind` (their
|
|
234
|
-
// expression has whitespace, so it could not be an HTML attribute name) back to the
|
|
235
|
-
// `@[expr]=""` form the binding pass below renders as ` resolvedName=""`. The compiled
|
|
236
|
-
// twin of the same step in runtime/iterate.js compileBatchFn.
|
|
237
|
-
let template = &restore_relocated_name_bindings(template);
|
|
238
|
-
|
|
239
|
-
let each_re = Regex::new(r"<!--\s*each\s+([^\s]+)\s+as\s+([^\s,]+)(?:\s*,\s*([^\s]+))?\s*-->").unwrap();
|
|
240
|
-
let end_re = Regex::new(r"<!--\s*/each\s*-->").unwrap();
|
|
241
|
-
let attr_binding_re = Regex::new(r#"(\s)([\w-]+)="@\[([^\]]+)\]""#).unwrap();
|
|
242
|
-
let binding_re = Regex::new(r"@\[([^\]]+)\]").unwrap();
|
|
243
|
-
|
|
244
|
-
let mut out = String::new();
|
|
245
|
-
let mut pos = 0;
|
|
246
|
-
|
|
247
|
-
while pos < template.len() {
|
|
248
|
-
let next_each = each_re.find_at(template, pos);
|
|
249
|
-
let next_attr = attr_binding_re.find_at(template, pos);
|
|
250
|
-
let next_binding = binding_re.find_at(template, pos);
|
|
251
|
-
|
|
252
|
-
// Earliest match wins; attr-binding outranks plain binding at the same
|
|
253
|
-
// region (the plain regex would match inside the attr form).
|
|
254
|
-
let candidates = [
|
|
255
|
-
next_each.map(|m| (m.start(), 0u8)),
|
|
256
|
-
next_attr.map(|m| (m.start(), 1u8)),
|
|
257
|
-
next_binding.map(|m| (m.start(), 2u8)),
|
|
258
|
-
];
|
|
259
|
-
let Some(&(start, kind)) = candidates
|
|
260
|
-
.iter()
|
|
261
|
-
.flatten()
|
|
262
|
-
.min_by_key(|(s, k)| (*s, *k))
|
|
263
|
-
else {
|
|
264
|
-
out.push_str(&escape_tpl_text(&template[pos..]));
|
|
265
|
-
break;
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
out.push_str(&escape_tpl_text(&template[pos..start]));
|
|
269
|
-
|
|
270
|
-
match kind {
|
|
271
|
-
0 => {
|
|
272
|
-
// Nested <!-- each --> → inline IIFE loop
|
|
273
|
-
let m = next_each.unwrap();
|
|
274
|
-
let caps = each_re.captures(&template[m.start()..]).unwrap();
|
|
275
|
-
let arr = caps.get(1).unwrap().as_str();
|
|
276
|
-
let item = caps.get(2).unwrap().as_str();
|
|
277
|
-
let idx = caps.get(3).map(|c| c.as_str()).unwrap_or("index");
|
|
278
|
-
|
|
279
|
-
if let Some((end_pos, _)) = find_matching_each_end(template, m.end()) {
|
|
280
|
-
let inner = emit_template_literal(&template[m.end()..end_pos]);
|
|
281
|
-
out.push_str(&format!(
|
|
282
|
-
"${{(() => {{ let inner = ''; const len_{idx} = {arr}.length; for (let {idx} = 0; {idx} < len_{idx}; {idx}++) {{ const {item} = {arr}[{idx}]; inner += `{inner_body}`; }} return inner; }})()}}",
|
|
283
|
-
idx = idx,
|
|
284
|
-
arr = arr,
|
|
285
|
-
item = item,
|
|
286
|
-
inner_body = inner
|
|
287
|
-
));
|
|
288
|
-
let end_match = end_re.find_at(template, end_pos).unwrap();
|
|
289
|
-
pos = end_match.end();
|
|
290
|
-
} else {
|
|
291
|
-
// Unbalanced each — emit as text and move on
|
|
292
|
-
out.push_str(&escape_tpl_text(&template[m.start()..m.end()]));
|
|
293
|
-
pos = m.end();
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
1 => {
|
|
297
|
-
// Attribute pure binding: attr="@[expr]"
|
|
298
|
-
let m = next_attr.unwrap();
|
|
299
|
-
let caps = attr_binding_re.captures(&template[m.start()..]).unwrap();
|
|
300
|
-
let ws = caps.get(1).unwrap().as_str();
|
|
301
|
-
let name = caps.get(2).unwrap().as_str();
|
|
302
|
-
let expr = caps.get(3).unwrap().as_str();
|
|
303
|
-
let name_lc = name.to_lowercase();
|
|
304
|
-
|
|
305
|
-
if DOM_PROPERTIES.contains(&name_lc.as_str()) || is_value_style_attr(&name_lc) {
|
|
306
|
-
out.push_str(&format!("{ws}{name}=\"${{{expr}}}\""));
|
|
307
|
-
} else {
|
|
308
|
-
out.push_str(&format!("${{({expr}) ? ' {name}=\"\"' : ''}}"));
|
|
309
|
-
}
|
|
310
|
-
pos = m.end();
|
|
311
|
-
}
|
|
312
|
-
_ => {
|
|
313
|
-
// Plain binding: text content or partial attribute value
|
|
314
|
-
let m = next_binding.unwrap();
|
|
315
|
-
let caps = binding_re.captures(&template[m.start()..]).unwrap();
|
|
316
|
-
out.push_str(&format!("${{{}}}", caps.get(1).unwrap().as_str()));
|
|
317
|
-
pos = m.end();
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
out
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/// Find matching <!-- /each --> comment using depth counting
|
|
326
|
-
/// Returns (position_before_comment, position_after_comment)
|
|
327
|
-
fn find_matching_each_end(html: &str, start_pos: usize) -> Option<(usize, usize)> {
|
|
328
|
-
let each_re = Regex::new(r"<!--\s*each\s+").unwrap();
|
|
329
|
-
let end_re = Regex::new(r"<!--\s*/each\s*-->").unwrap();
|
|
330
|
-
|
|
331
|
-
let mut depth = 1;
|
|
332
|
-
let mut search_pos = start_pos;
|
|
333
|
-
|
|
334
|
-
while depth > 0 {
|
|
335
|
-
let next_start = each_re.find_at(html, search_pos);
|
|
336
|
-
let next_end = end_re.find_at(html, search_pos);
|
|
337
|
-
|
|
338
|
-
match (next_start, next_end) {
|
|
339
|
-
(Some(start_match), Some(end_match)) if start_match.start() < end_match.start() => {
|
|
340
|
-
// Found nested <!-- each --> before <!-- /each -->
|
|
341
|
-
depth += 1;
|
|
342
|
-
search_pos = start_match.end();
|
|
343
|
-
}
|
|
344
|
-
(_, Some(end_match)) => {
|
|
345
|
-
// Found <!-- /each -->
|
|
346
|
-
depth -= 1;
|
|
347
|
-
if depth == 0 {
|
|
348
|
-
return Some((end_match.start(), end_match.end()));
|
|
349
|
-
}
|
|
350
|
-
search_pos = end_match.end();
|
|
351
|
-
}
|
|
352
|
-
_ => return None, // No matching end found
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
None
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
#[cfg(test)]
|
|
360
|
-
mod tests {
|
|
361
|
-
use super::*;
|
|
362
|
-
|
|
363
|
-
#[test]
|
|
364
|
-
fn test_simple_iteration() {
|
|
365
|
-
let html = r#"
|
|
366
|
-
<!-- each items as item -->
|
|
367
|
-
<li>@[item.name]</li>
|
|
368
|
-
<!-- /each -->
|
|
369
|
-
"#;
|
|
370
|
-
|
|
371
|
-
let opts = build_iteration_optimizations(html);
|
|
372
|
-
assert!(opts.is_some());
|
|
373
|
-
|
|
374
|
-
let iterations = &opts.unwrap().iterations;
|
|
375
|
-
assert_eq!(iterations.len(), 1);
|
|
376
|
-
|
|
377
|
-
let (_, compiled) = iterations.iter().next().unwrap();
|
|
378
|
-
assert_eq!(compiled.item_alias, "item");
|
|
379
|
-
assert_eq!(compiled.index_alias, "index");
|
|
380
|
-
assert_eq!(compiled.state_path, "items");
|
|
381
|
-
assert!(compiled.batch_fn.contains("html += `"));
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
#[test]
|
|
385
|
-
fn test_iteration_with_index() {
|
|
386
|
-
let html = r#"
|
|
387
|
-
<!-- each items as item, idx -->
|
|
388
|
-
<li>[@[idx]] @[item.name]</li>
|
|
389
|
-
<!-- /each -->
|
|
390
|
-
"#;
|
|
391
|
-
|
|
392
|
-
let opts = build_iteration_optimizations(html);
|
|
393
|
-
let iterations = &opts.unwrap().iterations;
|
|
394
|
-
let (_, compiled) = iterations.iter().next().unwrap();
|
|
395
|
-
assert_eq!(compiled.index_alias, "idx");
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
#[test]
|
|
399
|
-
fn test_boolean_attr_binding_emits_conditional_presence() {
|
|
400
|
-
let html = r#"
|
|
401
|
-
<!-- each items as item -->
|
|
402
|
-
<attr-target open="@[flag]" href="@[item.url]" value="@[item.v]"></attr-target>
|
|
403
|
-
<!-- /each -->
|
|
404
|
-
"#;
|
|
405
|
-
|
|
406
|
-
let opts = build_iteration_optimizations(html);
|
|
407
|
-
let iterations = &opts.unwrap().iterations;
|
|
408
|
-
let (_, compiled) = iterations.iter().next().unwrap();
|
|
409
|
-
|
|
410
|
-
// Boolean-coerced attribute: absent when falsy, present-empty when truthy
|
|
411
|
-
assert!(compiled.batch_fn.contains(r#"${(flag) ? ' open=""' : ''}"#));
|
|
412
|
-
// Value-style attribute keeps its string value
|
|
413
|
-
assert!(compiled.batch_fn.contains(r#"href="${item.url}""#));
|
|
414
|
-
// DOM property keeps its value form
|
|
415
|
-
assert!(compiled.batch_fn.contains(r#"value="${item.v}""#));
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
#[test]
|
|
419
|
-
fn test_unsafe_template_skips_batch_fn() {
|
|
420
|
-
let html = r#"
|
|
421
|
-
<!-- each rows as row -->
|
|
422
|
-
<li>@[$.unsafe(row.markup)]</li>
|
|
423
|
-
<!-- /each -->
|
|
424
|
-
"#;
|
|
425
|
-
|
|
426
|
-
// RawHtml semantics can't ride a template literal — no batch function
|
|
427
|
-
assert!(build_iteration_optimizations(html).is_none());
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
#[test]
|
|
431
|
-
fn test_nested_iteration_compiled() {
|
|
432
|
-
let html = r#"
|
|
433
|
-
<!-- each categories as cat -->
|
|
434
|
-
<div>
|
|
435
|
-
<!-- each cat.items as item -->
|
|
436
|
-
<span>@[item]</span>
|
|
437
|
-
<!-- /each -->
|
|
438
|
-
</div>
|
|
439
|
-
<!-- /each -->
|
|
440
|
-
"#;
|
|
441
|
-
|
|
442
|
-
let opts = build_iteration_optimizations(html);
|
|
443
|
-
// Should compile nested iterations
|
|
444
|
-
assert!(opts.is_some());
|
|
445
|
-
|
|
446
|
-
let iterations = &opts.unwrap().iterations;
|
|
447
|
-
assert_eq!(iterations.len(), 1);
|
|
448
|
-
|
|
449
|
-
let (_, compiled) = iterations.iter().next().unwrap();
|
|
450
|
-
// Should contain nested loop code with template literals
|
|
451
|
-
assert!(compiled.batch_fn.contains("cat.items"));
|
|
452
|
-
assert!(compiled.batch_fn.contains("let inner = ''"));
|
|
453
|
-
assert!(compiled.batch_fn.contains("inner += `"));
|
|
454
|
-
}
|
|
455
|
-
}
|