@ape-egg/vibe-compiler 4.1.2 → 4.1.4
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/native/vibe-compiler-darwin-arm64 +0 -0
- package/native/vibe-compiler-linux-x64 +0 -0
- package/package.json +1 -1
- package/src/Cargo.lock +1 -1
- package/src/Cargo.toml +1 -1
- package/src/compiler/name_binding_protect.rs +6 -35
- package/src/main.rs +1 -0
- package/src/parser/html.rs +129 -11
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ape-egg/vibe-compiler",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Optional Rust compiler for Vibe — pre-renders, inlines components, and optimizes iterations. The runtime never needs it.",
|
|
6
6
|
"homepage": "https://vibe.korte.kim",
|
package/src/Cargo.lock
CHANGED
package/src/Cargo.toml
CHANGED
|
@@ -18,12 +18,12 @@ pub const NAME_BIND_ATTR: &str = "data-vibe-namebind";
|
|
|
18
18
|
/// identical behaviour, no expression rewriting. Whitespace-free name-bindings already
|
|
19
19
|
/// survive as-is and are left untouched.
|
|
20
20
|
pub fn protect(html: &str) -> String {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let (masked, raw_blocks) = mask_raw_text(html);
|
|
21
|
+
crate::inert::protect(html, relocate_bindings)
|
|
22
|
+
}
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
fn relocate_bindings(html: &str) -> String {
|
|
25
|
+
tag_open_regex()
|
|
26
|
+
.replace_all(html, |caps: ®ex::Captures| {
|
|
27
27
|
let name = &caps[1];
|
|
28
28
|
let attrs = &caps[2];
|
|
29
29
|
let (kept, relocated) = relocate_unsafe(attrs);
|
|
@@ -41,9 +41,7 @@ pub fn protect(html: &str) -> String {
|
|
|
41
41
|
name, body, NAME_BIND_ATTR, relocated, self_close
|
|
42
42
|
)
|
|
43
43
|
})
|
|
44
|
-
.to_string()
|
|
45
|
-
|
|
46
|
-
restore_raw_text(&out, &raw_blocks)
|
|
44
|
+
.to_string()
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
/// Matches an opening tag and captures (name, attribute-section). Quoted values may
|
|
@@ -124,33 +122,6 @@ fn relocate_unsafe(attrs: &str) -> (String, String) {
|
|
|
124
122
|
(kept, relocated)
|
|
125
123
|
}
|
|
126
124
|
|
|
127
|
-
fn raw_text_regex() -> &'static Regex {
|
|
128
|
-
static RE: OnceLock<Regex> = OnceLock::new();
|
|
129
|
-
RE.get_or_init(|| Regex::new(r"(?is)<(script|style)\b[^>]*>.*?</(?:script|style)>").unwrap())
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
fn mask_raw_text(html: &str) -> (String, Vec<String>) {
|
|
133
|
-
let mut blocks = Vec::new();
|
|
134
|
-
let mut out = String::with_capacity(html.len());
|
|
135
|
-
let mut last = 0;
|
|
136
|
-
for mat in raw_text_regex().find_iter(html) {
|
|
137
|
-
out.push_str(&html[last..mat.start()]);
|
|
138
|
-
out.push_str(&format!("<!--VIBE_RAWTEXT_{}-->", blocks.len()));
|
|
139
|
-
blocks.push(html[mat.start()..mat.end()].to_string());
|
|
140
|
-
last = mat.end();
|
|
141
|
-
}
|
|
142
|
-
out.push_str(&html[last..]);
|
|
143
|
-
(out, blocks)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
fn restore_raw_text(html: &str, blocks: &[String]) -> String {
|
|
147
|
-
let mut out = html.to_string();
|
|
148
|
-
for (i, block) in blocks.iter().enumerate() {
|
|
149
|
-
out = out.replace(&format!("<!--VIBE_RAWTEXT_{}-->", i), block);
|
|
150
|
-
}
|
|
151
|
-
out
|
|
152
|
-
}
|
|
153
|
-
|
|
154
125
|
#[cfg(test)]
|
|
155
126
|
mod tests {
|
|
156
127
|
use super::*;
|
package/src/main.rs
CHANGED
package/src/parser/html.rs
CHANGED
|
@@ -4,6 +4,7 @@ use std::collections::{HashMap, HashSet};
|
|
|
4
4
|
use thiserror::Error;
|
|
5
5
|
|
|
6
6
|
use super::element::{Element, ElementCache};
|
|
7
|
+
use crate::inert::Inert;
|
|
7
8
|
|
|
8
9
|
#[derive(Error, Debug)]
|
|
9
10
|
pub enum ParseError {
|
|
@@ -178,7 +179,8 @@ impl HtmlParser {
|
|
|
178
179
|
return content.to_string(); // No custom elements to inline
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
let mut
|
|
182
|
+
let mut inert = Inert::new();
|
|
183
|
+
let mut result = inert.mask(content);
|
|
182
184
|
let mut changed = true;
|
|
183
185
|
let mut iterations = 0;
|
|
184
186
|
const MAX_ITERATIONS: usize = 10; // Reasonable limit for deeply nested custom elements
|
|
@@ -244,13 +246,13 @@ impl HtmlParser {
|
|
|
244
246
|
|
|
245
247
|
// Keep the wrapper for consistency with runtime (using generic <component> wrapper)
|
|
246
248
|
// No src attribute = wrapper won't be re-processed
|
|
247
|
-
let wrapper = format!("<component>{}</component>", replacement);
|
|
249
|
+
let wrapper = inert.mask(&format!("<component>{}</component>", replacement));
|
|
248
250
|
result.replace_range(start..end, &wrapper);
|
|
249
251
|
}
|
|
250
252
|
}
|
|
251
253
|
}
|
|
252
254
|
|
|
253
|
-
result
|
|
255
|
+
inert.restore(&result)
|
|
254
256
|
}
|
|
255
257
|
|
|
256
258
|
/// Recursively inline all <component> elements with their HTML content
|
|
@@ -262,7 +264,7 @@ impl HtmlParser {
|
|
|
262
264
|
/// the `<` of `</tag…>` (the slot-content boundary), `end` is just past its
|
|
263
265
|
/// `>`. Because the end tag may carry whitespace before `>` (`</tag\n>`), the
|
|
264
266
|
/// caller must use this `end` rather than assuming a `</tag>`-length tag.
|
|
265
|
-
fn find_matching_close(content: &str, after_open: usize, tag_name: &str) -> Option<(usize, usize)> {
|
|
267
|
+
pub(crate) fn find_matching_close(content: &str, after_open: usize, tag_name: &str) -> Option<(usize, usize)> {
|
|
266
268
|
// (?:\s|>) ensures <component> matches but not <component-foo> (hyphen is not \s or >)
|
|
267
269
|
let open_re = regex::Regex::new(&format!(r"<{}(?:\s|>|/>)", regex::escape(tag_name))).unwrap();
|
|
268
270
|
// `\s*>` tolerates whitespace before the `>` of an end tag — HTML allows
|
|
@@ -324,7 +326,8 @@ impl HtmlParser {
|
|
|
324
326
|
}
|
|
325
327
|
|
|
326
328
|
pub fn inline_single_component(&self, content: &str, component_src: &str, component_content: &str) -> String {
|
|
327
|
-
let mut
|
|
329
|
+
let mut inert = Inert::new();
|
|
330
|
+
let mut result = inert.mask(content);
|
|
328
331
|
|
|
329
332
|
// Normalize the src for matching
|
|
330
333
|
let normalized_src = if component_src.starts_with("http://") || component_src.starts_with("https://") {
|
|
@@ -373,7 +376,7 @@ impl HtmlParser {
|
|
|
373
376
|
// Iter-prop components stay as runtime `<component src>` tags (see
|
|
374
377
|
// is_iter_prop_root). The content is the same for every match, so skip all.
|
|
375
378
|
if is_iter_prop_root(component_content) {
|
|
376
|
-
return result;
|
|
379
|
+
return inert.restore(&result);
|
|
377
380
|
}
|
|
378
381
|
|
|
379
382
|
// Replace from end to start (sorted descending by start position)
|
|
@@ -389,15 +392,16 @@ impl HtmlParser {
|
|
|
389
392
|
replacement = replacement.replace("<slot/>", &slot_wrapped);
|
|
390
393
|
replacement = replacement.replace("<slot />", &slot_wrapped);
|
|
391
394
|
|
|
392
|
-
let wrapper = format!("<component>{}</component>", replacement);
|
|
395
|
+
let wrapper = inert.mask(&format!("<component>{}</component>", replacement));
|
|
393
396
|
result.replace_range(*start..*end, &wrapper);
|
|
394
397
|
}
|
|
395
398
|
|
|
396
|
-
result
|
|
399
|
+
inert.restore(&result)
|
|
397
400
|
}
|
|
398
401
|
|
|
399
402
|
fn inline_component_elements(&self, content: &str, external_cache: &HashMap<String, String>) -> String {
|
|
400
|
-
let mut
|
|
403
|
+
let mut inert = Inert::new();
|
|
404
|
+
let mut result = inert.mask(content);
|
|
401
405
|
|
|
402
406
|
// Match only the opening component tag — slot content is extracted via depth-counting
|
|
403
407
|
// to correctly handle slot content containing </div> or nested <component> elements.
|
|
@@ -471,7 +475,7 @@ impl HtmlParser {
|
|
|
471
475
|
replacement = replacement.replace("<slot/>", &slot_wrapped);
|
|
472
476
|
replacement = replacement.replace("<slot />", &slot_wrapped);
|
|
473
477
|
|
|
474
|
-
let wrapper = format!("<component>{}</component>", replacement);
|
|
478
|
+
let wrapper = inert.mask(&format!("<component>{}</component>", replacement));
|
|
475
479
|
result.replace_range(start..end, &wrapper);
|
|
476
480
|
applied = true;
|
|
477
481
|
break;
|
|
@@ -483,7 +487,7 @@ impl HtmlParser {
|
|
|
483
487
|
}
|
|
484
488
|
}
|
|
485
489
|
|
|
486
|
-
result
|
|
490
|
+
inert.restore(&result)
|
|
487
491
|
}
|
|
488
492
|
|
|
489
493
|
/// Parse component props from attributes string, in source order.
|
|
@@ -736,6 +740,10 @@ fn substitute_props(template: &str, props: &[(String, String)]) -> String {
|
|
|
736
740
|
|
|
737
741
|
/// Transform custom HTML elements to divs with classes
|
|
738
742
|
fn transform_custom_tags_to_divs(content: &str, reserved_elements: &[String]) -> String {
|
|
743
|
+
crate::inert::protect(content, |content| transform_custom_tags(content, reserved_elements))
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
fn transform_custom_tags(content: &str, reserved_elements: &[String]) -> String {
|
|
739
747
|
let mut result = content.to_string();
|
|
740
748
|
|
|
741
749
|
// Standard HTML5 elements (should not be transformed)
|
|
@@ -986,6 +994,116 @@ mod tests {
|
|
|
986
994
|
);
|
|
987
995
|
}
|
|
988
996
|
|
|
997
|
+
#[test]
|
|
998
|
+
fn component_tag_inside_script_string_is_not_inlined() {
|
|
999
|
+
let parser = HtmlParser::new(std::path::PathBuf::from("."));
|
|
1000
|
+
let page = concat!(
|
|
1001
|
+
r#"<page><script>const tpl = '<component src="/components/Badge.html"></component>';</script>"#,
|
|
1002
|
+
r#"<component src="/components/Badge.html"></component></page>"#,
|
|
1003
|
+
);
|
|
1004
|
+
let mut cache = HashMap::new();
|
|
1005
|
+
cache.insert("/components/Badge.html".to_string(), "<badge-pill>hi</badge-pill>".to_string());
|
|
1006
|
+
|
|
1007
|
+
let out = parser.inline_component_elements(page, &cache);
|
|
1008
|
+
|
|
1009
|
+
assert!(
|
|
1010
|
+
out.contains(r#"const tpl = '<component src="/components/Badge.html"></component>';"#),
|
|
1011
|
+
"script string literal was rewritten: {out}"
|
|
1012
|
+
);
|
|
1013
|
+
assert!(
|
|
1014
|
+
out.contains("<component><badge-pill>hi</badge-pill></component>"),
|
|
1015
|
+
"the real component was not inlined: {out}"
|
|
1016
|
+
);
|
|
1017
|
+
assert_eq!(out.matches("<badge-pill>").count(), 1, "template leaked into the script: {out}");
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
#[test]
|
|
1021
|
+
fn component_tag_inside_dehydrate_block_is_not_inlined() {
|
|
1022
|
+
let parser = HtmlParser::new(std::path::PathBuf::from("."));
|
|
1023
|
+
let page = concat!(
|
|
1024
|
+
r#"<docs><code vibe-dehydrate><component src="/components/Badge.html"></component></code>"#,
|
|
1025
|
+
r#"<component src="/components/Badge.html"></component></docs>"#,
|
|
1026
|
+
);
|
|
1027
|
+
let mut cache = HashMap::new();
|
|
1028
|
+
cache.insert("/components/Badge.html".to_string(), "<badge-pill>hi</badge-pill>".to_string());
|
|
1029
|
+
|
|
1030
|
+
let out = parser.inline_component_elements(page, &cache);
|
|
1031
|
+
|
|
1032
|
+
assert!(
|
|
1033
|
+
out.contains(r#"<code vibe-dehydrate><component src="/components/Badge.html"></component></code>"#),
|
|
1034
|
+
"dehydrated markup was inlined: {out}"
|
|
1035
|
+
);
|
|
1036
|
+
assert_eq!(out.matches("<badge-pill>").count(), 1, "template leaked into the dehydrate block: {out}");
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
#[test]
|
|
1040
|
+
fn component_tag_inside_pre_is_not_inlined() {
|
|
1041
|
+
let parser = HtmlParser::new(std::path::PathBuf::from("."));
|
|
1042
|
+
let page = concat!(
|
|
1043
|
+
r#"<docs><pre><component src="/components/Badge.html"></component></pre>"#,
|
|
1044
|
+
r#"<component src="/components/Badge.html"></component></docs>"#,
|
|
1045
|
+
);
|
|
1046
|
+
let mut cache = HashMap::new();
|
|
1047
|
+
cache.insert("/components/Badge.html".to_string(), "<badge-pill>hi</badge-pill>".to_string());
|
|
1048
|
+
|
|
1049
|
+
let out = parser.inline_component_elements(page, &cache);
|
|
1050
|
+
|
|
1051
|
+
assert!(
|
|
1052
|
+
out.contains(r#"<pre><component src="/components/Badge.html"></component></pre>"#),
|
|
1053
|
+
"markup inside <pre> was inlined: {out}"
|
|
1054
|
+
);
|
|
1055
|
+
assert_eq!(out.matches("<badge-pill>").count(), 1, "template leaked into the <pre>: {out}");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
#[test]
|
|
1059
|
+
fn custom_element_inside_script_string_is_not_inlined() {
|
|
1060
|
+
let mut parser = HtmlParser::new(std::path::PathBuf::from("."));
|
|
1061
|
+
parser.cache.insert(
|
|
1062
|
+
"badge".to_string(),
|
|
1063
|
+
Element::new(
|
|
1064
|
+
"badge".to_string(),
|
|
1065
|
+
std::path::PathBuf::from("badge.html"),
|
|
1066
|
+
"<badge-pill>hi</badge-pill>".to_string(),
|
|
1067
|
+
),
|
|
1068
|
+
);
|
|
1069
|
+
|
|
1070
|
+
let page = concat!(
|
|
1071
|
+
r#"<page><script>el.innerHTML = '<badge></badge>';</script>"#,
|
|
1072
|
+
r#"<badge></badge></page>"#,
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
let out = parser.inline_custom_elements(page);
|
|
1076
|
+
|
|
1077
|
+
assert!(
|
|
1078
|
+
out.contains(r#"el.innerHTML = '<badge></badge>';"#),
|
|
1079
|
+
"script string literal was rewritten: {out}"
|
|
1080
|
+
);
|
|
1081
|
+
assert_eq!(out.matches("<badge-pill>").count(), 1, "template leaked into the script: {out}");
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
#[test]
|
|
1085
|
+
fn custom_element_inside_dehydrate_block_is_not_inlined() {
|
|
1086
|
+
let mut parser = HtmlParser::new(std::path::PathBuf::from("."));
|
|
1087
|
+
parser.cache.insert(
|
|
1088
|
+
"badge".to_string(),
|
|
1089
|
+
Element::new(
|
|
1090
|
+
"badge".to_string(),
|
|
1091
|
+
std::path::PathBuf::from("badge.html"),
|
|
1092
|
+
"<badge-pill>hi</badge-pill>".to_string(),
|
|
1093
|
+
),
|
|
1094
|
+
);
|
|
1095
|
+
|
|
1096
|
+
let page = r#"<docs><code vibe-dehydrate><badge></badge></code><badge></badge></docs>"#;
|
|
1097
|
+
|
|
1098
|
+
let out = parser.inline_custom_elements(page);
|
|
1099
|
+
|
|
1100
|
+
assert!(
|
|
1101
|
+
out.contains(r#"<code vibe-dehydrate><badge></badge></code>"#),
|
|
1102
|
+
"dehydrated markup was inlined: {out}"
|
|
1103
|
+
);
|
|
1104
|
+
assert_eq!(out.matches("<badge-pill>").count(), 1, "template leaked into the dehydrate block: {out}");
|
|
1105
|
+
}
|
|
1106
|
+
|
|
989
1107
|
#[test]
|
|
990
1108
|
fn ordinary_component_still_inlines() {
|
|
991
1109
|
// Guard: a normal component (root is NOT an array-literal each) must still
|