@ape-egg/vibe 2.3.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/README.md +14 -4
- package/boot.js +4 -4
- package/component.js +27 -29
- package/hot-module-refresh.js +4 -4
- package/index.js +10 -15
- package/llms.txt +8 -6
- package/package.json +19 -14
- package/runtime/affected.js +159 -36
- package/runtime/cleanup.js +45 -1
- package/runtime/component.js +312 -99
- package/runtime/conditionals.js +111 -14
- package/runtime/debug.js +24 -0
- package/runtime/dispatch.js +172 -0
- package/runtime/hydrate.js +251 -111
- package/runtime/index.js +180 -71
- package/runtime/iterate.js +125 -50
- package/runtime/iteration-utils.js +59 -8
- package/runtime/manifest.js +77 -2
- package/runtime/parse.js +69 -5
- 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 +77 -14
- package/vibe.css +8 -4
- package/CHANGELOG.md +0 -1196
- 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 -2880
- 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 -16
- package/compiler/src/compiler/name_binding_protect.rs +0 -207
- package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
- package/compiler/src/compiler/spa.rs +0 -477
- 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 -1278
- package/compiler/src/config.rs +0 -279
- package/compiler/src/main.rs +0 -358
- 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
package/compiler/src/Cargo.toml
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "vibe-compiler"
|
|
3
|
-
version = "2.2.0"
|
|
4
|
-
edition = "2021"
|
|
5
|
-
description = "Vibe framework compiler - compiles Vibe source files into optimized output"
|
|
6
|
-
authors = ["Kim Korte"]
|
|
7
|
-
license = "ISC"
|
|
8
|
-
|
|
9
|
-
[[bin]]
|
|
10
|
-
name = "vibe-compiler"
|
|
11
|
-
path = "main.rs"
|
|
12
|
-
|
|
13
|
-
[dependencies]
|
|
14
|
-
clap = { version = "4.4", features = ["derive"] }
|
|
15
|
-
serde = { version = "=1.0.210", features = ["derive"] }
|
|
16
|
-
serde_json = "1.0"
|
|
17
|
-
html5ever = "0.27"
|
|
18
|
-
markup5ever_rcdom = "0.3"
|
|
19
|
-
markup5ever = "0.12"
|
|
20
|
-
thiserror = "1.0"
|
|
21
|
-
regex = "1.10"
|
|
22
|
-
colored = "2.1"
|
|
23
|
-
ureq = { version = "2", features = ["tls"] }
|
|
24
|
-
glob = "0.3"
|
|
25
|
-
rquickjs = "0.6"
|
|
26
|
-
swc_common = "=0.40.1"
|
|
27
|
-
swc_ecma_parser = "=0.152.0"
|
|
28
|
-
swc_ecma_ast = "=0.121.0"
|
|
29
|
-
swc_ecma_visit = "=0.107.0"
|
|
30
|
-
notify = "6.1"
|
|
31
|
-
notify-debouncer-full = "0.3"
|
|
32
|
-
rayon = "1.10"
|
|
33
|
-
|
|
34
|
-
[profile.release]
|
|
35
|
-
opt-level = 3
|
|
36
|
-
lto = true
|
|
37
|
-
codegen-units = 1
|
|
38
|
-
strip = true
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
# Pre-Rendering Implementation: swc, QuickJS, and Manifest Restoration
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
This document describes how pre-rendering works in Vibe's compiler, the technical challenges encountered, and the solutions implemented. The goal is to pre-render HTML with actual state values (FOUC prevention) while preserving markers for runtime reactivity.
|
|
6
|
-
|
|
7
|
-
## Technologies Used
|
|
8
|
-
|
|
9
|
-
### swc (JavaScript AST Parser)
|
|
10
|
-
- **Purpose**: Parse JavaScript code to extract state variables and resolve imports
|
|
11
|
-
- **Use Case**: When HTML contains `import { menuSections } from '/index.js'`, we need to know what variables exist
|
|
12
|
-
- **Implementation**: `js_analyzer.rs` - traverses AST to find import declarations and build scope
|
|
13
|
-
|
|
14
|
-
### QuickJS (JavaScript Runtime)
|
|
15
|
-
- **Purpose**: Execute JavaScript to evaluate state values for pre-rendering
|
|
16
|
-
- **Use Case**: Transform `@[firstName]` → `"John"` by evaluating the state object
|
|
17
|
-
- **Implementation**: `value_stamper.rs` - creates JS context with state, evaluates expressions
|
|
18
|
-
|
|
19
|
-
## The Pre-Rendering Flow
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
1. Parse HTML → 2. Extract State → 3. Build Manifest → 4. Stamp Values
|
|
23
|
-
(html5ever) (swc + QuickJS) (before stamping) (QuickJS)
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
**Critical**: Manifest must be built BEFORE value stamping, otherwise templates contain pre-rendered values instead of markers.
|
|
27
|
-
|
|
28
|
-
## Problems Encountered and Solutions
|
|
29
|
-
|
|
30
|
-
### Problem 1: Manifest Build Order
|
|
31
|
-
**Symptom**: Manifest templates contained `"Vibe Compiled"` instead of `@[section.label]`
|
|
32
|
-
|
|
33
|
-
**Root Cause**: Manifest was being built AFTER value stamping:
|
|
34
|
-
```rust
|
|
35
|
-
// WRONG ORDER (old code)
|
|
36
|
-
let pre_rendered = stamper.stamp_html(html.to_string())?;
|
|
37
|
-
let manifest = manifest_builder.build_from_html(&pre_rendered, &state, iterations_as_is)?;
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
**Solution**: Reorder operations - build manifest first:
|
|
41
|
-
```rust
|
|
42
|
-
// CORRECT ORDER (compile.rs lines 556-565)
|
|
43
|
-
let state = StateExtractor::extract_from_html(html, &source_root.to_path_buf())?;
|
|
44
|
-
let manifest = manifest_builder.build_from_html(html, &state, iterations_as_is)?;
|
|
45
|
-
let pre_rendered = stamper.stamp_html(html.to_string())?;
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
**Files Modified**: `compile.rs`
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
### Problem 2: Multiline Script Tag Parsing
|
|
53
|
-
**Symptom**: State extraction logged "found 0-length scripts"
|
|
54
|
-
|
|
55
|
-
**Root Cause**: Regex didn't match newlines in multiline `<script>` blocks:
|
|
56
|
-
```rust
|
|
57
|
-
// WRONG (old code)
|
|
58
|
-
let script_regex = Regex::new(r#"<script[^>]*>(.*?)</script>"#).unwrap();
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
**Solution**: Add `(?s)` flag to make `.` match newlines:
|
|
62
|
-
```rust
|
|
63
|
-
// CORRECT (state_extractor.rs line 22)
|
|
64
|
-
let script_regex = Regex::new(r#"(?s)<script[^>]*>(.*?)</script>"#).unwrap();
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
**Files Modified**: `state_extractor.rs`
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
### Problem 3: Invalid Name Binding HTML Syntax
|
|
72
|
-
**Symptom**: Elements inserted via `insertBefore()` but immediately disappeared from DOM
|
|
73
|
-
|
|
74
|
-
**Root Cause**: html5ever serializer normalized name bindings to invalid HTML:
|
|
75
|
-
- Input: `<icon @[section.icon]>`
|
|
76
|
-
- html5ever output: `<icon @[section.icon]="">`
|
|
77
|
-
- Browsers reject attributes with dynamic names followed by `=""`
|
|
78
|
-
|
|
79
|
-
**Solution**: Post-process serialized HTML to strip `=""` from name bindings:
|
|
80
|
-
```rust
|
|
81
|
-
// manifest_builder.rs (serialize_nodes function)
|
|
82
|
-
let name_binding_fix = Regex::new(r#"(@\[[^\]]+\])="+"#).unwrap();
|
|
83
|
-
html = name_binding_fix.replace_all(&html, "$1").to_string();
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
**Shortcut Taken**: This is a post-processing hack rather than fixing html5ever's serialization logic. It works for standard cases but could theoretically fail if there are edge cases with quotes inside bindings.
|
|
87
|
-
|
|
88
|
-
**Files Modified**: `manifest_builder.rs`
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
### Problem 4: Wrong Iteration Comment Matching
|
|
93
|
-
**Symptom**: Restoration found nested `<!-- each section.items as item -->` instead of target `<!-- each menuSections as section -->`
|
|
94
|
-
|
|
95
|
-
**Root Cause**: Searched for first `<!-- each -->` comment without specificity
|
|
96
|
-
|
|
97
|
-
**Solution**: Store and match the exact iteration expression:
|
|
98
|
-
```rust
|
|
99
|
-
// manifest_builder.rs (lines 142-161)
|
|
100
|
-
if trimmed.starts_with("each ") {
|
|
101
|
-
let expression = trimmed.strip_prefix("each ").unwrap_or("").to_string();
|
|
102
|
-
// ... store expression in RestorationData
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
```javascript
|
|
107
|
-
// pre-compiled-manifest.js
|
|
108
|
-
const expectedComment = `each ${restoration.expression}`;
|
|
109
|
-
if (trimmed === expectedComment && !startComment) {
|
|
110
|
-
startComment = comment;
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
**Files Modified**: `manifest_builder.rs`, `pre-compiled-manifest.js`
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
### Problem 5: Wrong End Comment - Nested Iteration Depth (THE CRITICAL FIX)
|
|
119
|
-
**Symptom**:
|
|
120
|
-
- Menu rendered 25 times (5 × 5) instead of 5
|
|
121
|
-
- Restoration found wrong parent: `ACCORDION-CONTENT` instead of correct parent
|
|
122
|
-
- Console showed duplicate conditional comments
|
|
123
|
-
|
|
124
|
-
**Root Cause**:
|
|
125
|
-
HTML structure has nested iterations:
|
|
126
|
-
```html
|
|
127
|
-
<!-- each menuSections as section --> (line 57)
|
|
128
|
-
<nav-section>
|
|
129
|
-
<accordion>
|
|
130
|
-
<!-- each section.items as item -->
|
|
131
|
-
...
|
|
132
|
-
<!-- /each --> (line 98 - closes section.items, NOT menuSections!)
|
|
133
|
-
</accordion>
|
|
134
|
-
</nav-section>
|
|
135
|
-
<!-- 4 more nav-sections with their own nested iterations -->
|
|
136
|
-
<!-- /each --> (line 483 - actually closes menuSections)
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
The restoration code found the correct start comment but matched the FIRST `<!-- /each -->` (line 98), which closed a nested iteration. This caused:
|
|
140
|
-
1. Only removing the first nav-section (5 items total, leaving 4 = 20 items)
|
|
141
|
-
2. Inserting template 5 times (once per menuSection)
|
|
142
|
-
3. Each insertion added 5 items = 25 total
|
|
143
|
-
|
|
144
|
-
**Solution**: Track nesting depth and only match `/each` at depth 0:
|
|
145
|
-
```javascript
|
|
146
|
-
// pre-compiled-manifest.js
|
|
147
|
-
let depth = 0;
|
|
148
|
-
const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT);
|
|
149
|
-
while (walker.nextNode()) {
|
|
150
|
-
const comment = walker.currentNode;
|
|
151
|
-
const trimmed = comment.textContent.trim();
|
|
152
|
-
|
|
153
|
-
if (trimmed === expectedComment && !startComment) {
|
|
154
|
-
startComment = comment;
|
|
155
|
-
depth = 1; // We're now inside this iteration
|
|
156
|
-
} else if (startComment) {
|
|
157
|
-
if (trimmed.startsWith('each ')) {
|
|
158
|
-
depth++; // Entering nested iteration
|
|
159
|
-
} else if (trimmed === '/each') {
|
|
160
|
-
depth--; // Exiting an iteration
|
|
161
|
-
if (depth === 0) {
|
|
162
|
-
endComment = comment; // This closes OUR iteration
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
**Why This Works**:
|
|
171
|
-
- `depth = 1` when we enter `menuSections`
|
|
172
|
-
- `depth = 2` when we enter nested `section.items`
|
|
173
|
-
- `depth = 1` when we exit nested `section.items` (line 98)
|
|
174
|
-
- `depth = 0` when we exit `menuSections` (line 483) - **this is our match**
|
|
175
|
-
|
|
176
|
-
**Files Modified**: `pre-compiled-manifest.js`
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
## Edge Cases and Limitations
|
|
181
|
-
|
|
182
|
-
### Known Shortcuts
|
|
183
|
-
1. **Name binding fix**: Post-processing regex instead of proper html5ever configuration
|
|
184
|
-
2. **Comment parsing**: Assumes comments are well-formed (no nested comments in strings)
|
|
185
|
-
|
|
186
|
-
### Will This Work for Most Cases?
|
|
187
|
-
**Yes**, the solution is robust for standard use cases:
|
|
188
|
-
|
|
189
|
-
✅ **Works For**:
|
|
190
|
-
- Nested iterations at any depth
|
|
191
|
-
- Multiple iterations at the same level
|
|
192
|
-
- Conditionals inside iterations
|
|
193
|
-
- Iterations inside conditionals
|
|
194
|
-
- Mixed nesting patterns
|
|
195
|
-
|
|
196
|
-
⚠️ **Edge Cases**:
|
|
197
|
-
- Identical iteration expressions at multiple nesting levels (e.g., `<!-- each items as item -->` appears twice in nested structure)
|
|
198
|
-
- Solution: Expression matching will find the first one, depth tracking ensures correct pairing
|
|
199
|
-
- This is acceptable because the outer iteration would have a different parent element
|
|
200
|
-
|
|
201
|
-
- Comments inside JavaScript strings that look like iteration comments
|
|
202
|
-
- Example: `const x = "<!-- each items as item -->"` in a `<script>` tag
|
|
203
|
-
- This would confuse the comment walker
|
|
204
|
-
- Mitigation: Unlikely in practice, as state is extracted separately
|
|
205
|
-
|
|
206
|
-
- Name bindings with quotes inside: `<icon @[items["key"]]>`
|
|
207
|
-
- The regex fix might not handle all quote variations
|
|
208
|
-
- Mitigation: Rare in practice, typically use dot notation
|
|
209
|
-
|
|
210
|
-
### Future Improvements
|
|
211
|
-
1. **Proper html5ever integration**: Configure serializer to not add `=""` for dynamic attributes
|
|
212
|
-
2. **AST-based comment parsing**: Use html5ever's comment nodes instead of regex/text matching
|
|
213
|
-
3. **Unique iteration IDs**: Generate unique IDs for each iteration during compilation, use those for matching instead of expressions
|
|
214
|
-
|
|
215
|
-
## Testing Checklist
|
|
216
|
-
When modifying this code, verify:
|
|
217
|
-
- [ ] Simple iterations render correctly
|
|
218
|
-
- [ ] Nested iterations (2+ levels deep) render correctly
|
|
219
|
-
- [ ] Multiple iterations at same level render correctly
|
|
220
|
-
- [ ] Pre-rendered HTML shows actual values (no FOUC)
|
|
221
|
-
- [ ] Runtime finds markers and makes content reactive
|
|
222
|
-
- [ ] Menu navigation works (indicates correct event handlers)
|
|
223
|
-
- [ ] Console shows no errors during restoration
|
|
224
|
-
|
|
225
|
-
## File Summary
|
|
226
|
-
|
|
227
|
-
| File | Purpose | Key Changes |
|
|
228
|
-
|------|---------|-------------|
|
|
229
|
-
| `compile.rs` | Main compilation orchestration | Reordered manifest build before value stamping |
|
|
230
|
-
| `state_extractor.rs` | Extract state from `<script>` tags | Fixed multiline regex with `(?s)` flag |
|
|
231
|
-
| `manifest_builder.rs` | Build restoration manifest | Added expression field, fixed name binding serialization |
|
|
232
|
-
| `pre-compiled-manifest.js` | Runtime restoration logic | Added expression matching and depth tracking |
|
|
233
|
-
|
|
234
|
-
## Conclusion
|
|
235
|
-
|
|
236
|
-
The pre-rendering system now correctly handles nested iterations by:
|
|
237
|
-
1. Storing iteration expressions for precise matching
|
|
238
|
-
2. Tracking depth to find the correct closing comment
|
|
239
|
-
3. Building manifests before value stamping to preserve markers
|
|
240
|
-
|
|
241
|
-
The depth tracking solution is the critical piece that makes nested iterations work correctly. Without it, the restoration would match the wrong closing comment and corrupt the DOM structure.
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
use regex::Regex;
|
|
2
|
-
use std::collections::HashMap;
|
|
3
|
-
use std::sync::OnceLock;
|
|
4
|
-
|
|
5
|
-
/// The canonical `@[...]` binding matcher, shared with manifest_builder /
|
|
6
|
-
/// value_stamper. A binding body is any run of non-bracket chars, optionally
|
|
7
|
-
/// containing a single bracketed group (`items[0]`).
|
|
8
|
-
fn binding_regex() -> &'static Regex {
|
|
9
|
-
static RE: OnceLock<Regex> = OnceLock::new();
|
|
10
|
-
RE.get_or_init(|| Regex::new(r"@\[((?:[^\[\]]|\[[^\]]*\])+)\]").unwrap())
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/// html5ever lowercases attribute NAMES per the HTML spec. A name-binding lives
|
|
14
|
-
/// in attribute-name position (`<icon @[selectedEquipProps(uuid).element]>`), so
|
|
15
|
-
/// its expression is lowercased by every parse/serialize round-trip — the
|
|
16
|
-
/// identifier `selectedEquipProps` becomes `selectedequipprops`, undefined at
|
|
17
|
-
/// runtime. Attribute VALUES keep their case, so value-bindings are unaffected.
|
|
18
|
-
///
|
|
19
|
-
/// We can't tell html5ever to preserve case, so we restore it ourselves: snapshot
|
|
20
|
-
/// the original-cased `@[...]` bindings from the input BEFORE the round-trip, then
|
|
21
|
-
/// map them back over the output AFTER. The map is keyed by the lowercased binding
|
|
22
|
-
/// (what the round-trip produces) → original binding.
|
|
23
|
-
///
|
|
24
|
-
/// First-wins on a lowercase collision (two differently-cased bindings that
|
|
25
|
-
/// lowercase to the same key): rare, and either casing is a defensible restore.
|
|
26
|
-
pub fn capture(input: &str) -> HashMap<String, String> {
|
|
27
|
-
let mut map = HashMap::new();
|
|
28
|
-
for caps in binding_regex().captures_iter(input) {
|
|
29
|
-
let original = caps.get(0).unwrap().as_str();
|
|
30
|
-
let lower = original.to_ascii_lowercase();
|
|
31
|
-
if lower != original {
|
|
32
|
-
map.entry(lower).or_insert_with(|| original.to_string());
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
map
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/// Restore original casing of `@[...]` bindings in `output` using a map built by
|
|
39
|
-
/// [`capture`]. Only bindings whose lowercased form is a known key are rewritten;
|
|
40
|
-
/// everything else is left verbatim, so this is a no-op when nothing was lowered.
|
|
41
|
-
pub fn restore(output: &str, map: &HashMap<String, String>) -> String {
|
|
42
|
-
if map.is_empty() {
|
|
43
|
-
return output.to_string();
|
|
44
|
-
}
|
|
45
|
-
binding_regex()
|
|
46
|
-
.replace_all(output, |caps: ®ex::Captures| {
|
|
47
|
-
let matched = caps.get(0).unwrap().as_str();
|
|
48
|
-
match map.get(&matched.to_ascii_lowercase()) {
|
|
49
|
-
Some(original) => original.clone(),
|
|
50
|
-
None => matched.to_string(),
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
.to_string()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
#[cfg(test)]
|
|
57
|
-
mod tests {
|
|
58
|
-
use super::*;
|
|
59
|
-
|
|
60
|
-
#[test]
|
|
61
|
-
fn restores_lowercased_name_binding() {
|
|
62
|
-
// Post-prop-substitution name binding (parens added by substitute_props),
|
|
63
|
-
// as it enters an html5ever round-trip. html5ever lowercases the whole
|
|
64
|
-
// attr name; restore puts the original casing back.
|
|
65
|
-
let input = "<icon @[(selectedEquipProps(uuid)).element]></icon>";
|
|
66
|
-
let map = capture(input);
|
|
67
|
-
let lowered = "<icon @[(selectedequipprops(uuid)).element]></icon>";
|
|
68
|
-
let restored = restore(lowered, &map);
|
|
69
|
-
assert_eq!(restored, input);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
#[test]
|
|
73
|
-
fn leaves_value_bindings_untouched_when_already_correct() {
|
|
74
|
-
let input = r#"<card tinted="@[selectedEquipProps(uuid).element]"></card>"#;
|
|
75
|
-
let map = capture(input);
|
|
76
|
-
// Value bindings keep case through html5ever, so output already matches.
|
|
77
|
-
let restored = restore(input, &map);
|
|
78
|
-
assert_eq!(restored, input);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
#[test]
|
|
82
|
-
fn noop_when_no_uppercase() {
|
|
83
|
-
let input = "<icon @[tooltip.props.element]></icon>";
|
|
84
|
-
let map = capture(input);
|
|
85
|
-
assert!(map.is_empty());
|
|
86
|
-
assert_eq!(restore(input, &map), input);
|
|
87
|
-
}
|
|
88
|
-
}
|