@ape-egg/vibe-compiler 4.1.3 → 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.
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",
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
@@ -1599,7 +1599,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1599
1599
 
1600
1600
  [[package]]
1601
1601
  name = "vibe-compiler"
1602
- version = "4.1.1"
1602
+ version = "4.1.4"
1603
1603
  dependencies = [
1604
1604
  "clap",
1605
1605
  "colored",
package/src/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "vibe-compiler"
3
- version = "4.1.1"
3
+ version = "4.1.4"
4
4
  edition = "2021"
5
5
  description = "Vibe framework compiler - compiles Vibe source files into optimized output"
6
6
  authors = ["Kim Korte"]
@@ -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
- // <script>/<style> bodies are raw text — a `<` inside them must not be read as a
22
- // tag. Park them behind comment placeholders for the tag scan, restore after.
23
- let (masked, raw_blocks) = mask_raw_text(html);
21
+ crate::inert::protect(html, relocate_bindings)
22
+ }
24
23
 
25
- let out = tag_open_regex()
26
- .replace_all(&masked, |caps: &regex::Captures| {
24
+ fn relocate_bindings(html: &str) -> String {
25
+ tag_open_regex()
26
+ .replace_all(html, |caps: &regex::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
@@ -1,4 +1,5 @@
1
1
  mod config;
2
+ mod inert;
2
3
  mod parser;
3
4
  mod compiler;
4
5
 
@@ -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 result = content.to_string();
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 result = content.to_string();
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 result = content.to_string();
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