1use oxc_allocator::Allocator;
2use oxc_parser::Parser;
3use oxc_span::SourceType;
4use serde::Serialize;
5use std::borrow::Cow;
6
7const EDITOR_RECOVERY_IDENTIFIER: &str = "omenaEditorRecovery";
8
9pub(crate) struct RecoveredEditorSourceV0 {
10 pub source: String,
11 pub trusted_byte_end: usize,
12}
13
14pub(crate) fn recover_panicked_editor_source(
19 source: &str,
20 source_type: SourceType,
21 diagnostic_offsets: &[usize],
22) -> Option<RecoveredEditorSourceV0> {
23 const REPAIRS: &[&str] = &[
24 EDITOR_RECOVERY_IDENTIFIER,
25 "omenaEditorRecovery)} />;",
26 "omenaEditorRecovery')} />;",
27 "omenaEditorRecovery\")} />;",
28 "omenaEditorRecovery`)} />;",
29 "omenaEditorRecovery']",
30 "omenaEditorRecovery\"]",
31 "omenaEditorRecovery`]",
32 ];
33
34 let line_ends = source
35 .match_indices('\n')
36 .map(|(index, _)| {
37 if index > 0 && source.as_bytes()[index - 1] == b'\r' {
38 index - 1
39 } else {
40 index
41 }
42 })
43 .collect::<Vec<_>>();
44 let mut insertion_points = line_ends.iter().rev().take(16).copied().collect::<Vec<_>>();
45 for diagnostic_offset in diagnostic_offsets {
46 let diagnostic_offset = (*diagnostic_offset).min(source.len());
47 if let Some(previous_line_end) = line_ends
48 .iter()
49 .copied()
50 .rev()
51 .find(|line_end| *line_end <= diagnostic_offset)
52 {
53 insertion_points.push(previous_line_end);
54 }
55 let line_end = line_ends
56 .iter()
57 .copied()
58 .find(|line_end| *line_end >= diagnostic_offset)
59 .unwrap_or(source.len());
60 insertion_points.push(line_end);
61 }
62 insertion_points.push(source.len());
63 insertion_points.sort_unstable();
64 insertion_points.dedup();
65
66 for insertion_point in insertion_points.into_iter().rev() {
67 for repair in REPAIRS {
68 let mut candidate = String::with_capacity(source.len() + repair.len());
69 candidate.push_str(&source[..insertion_point]);
70 candidate.push_str(repair);
71 candidate.push_str(&source[insertion_point..]);
72 let allocator = Allocator::default();
73 if !Parser::new(&allocator, candidate.as_str(), source_type)
74 .parse()
75 .panicked
76 {
77 return Some(RecoveredEditorSourceV0 {
78 source: candidate,
79 trusted_byte_end: insertion_point,
80 });
81 }
82 }
83 }
84 None
85}
86
87pub(crate) trait SourceLanguageParserV0 {
88 fn parser_id(&self) -> &'static str;
89 fn language(&self) -> &'static str;
90 fn language_aliases(&self) -> Vec<&'static str>;
91 fn projection_kind(&self) -> &'static str;
92 fn source_type(&self, source_path: &str) -> SourceType;
93 fn project<'a>(&self, source: &'a str) -> Cow<'a, str>;
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97enum SourceLanguageParserKindV0 {
98 OxcTsx,
99 VueSfcScript,
100 HtmlScript,
101 SvelteComponentScript,
102 AstroComponentScript,
103 MarkdownFencedCode,
104 ServerTemplateMarkup,
105}
106
107impl SourceLanguageParserV0 for SourceLanguageParserKindV0 {
108 fn parser_id(&self) -> &'static str {
109 match self {
110 Self::OxcTsx => "oxcTsxSourceLanguageParserV0",
111 Self::VueSfcScript => "vueSfcScriptProjectionParserV0",
112 Self::HtmlScript => "htmlScriptProjectionParserV0",
113 Self::SvelteComponentScript => "svelteComponentScriptProjectionParserV0",
114 Self::AstroComponentScript => "astroComponentScriptProjectionParserV0",
115 Self::MarkdownFencedCode => "markdownFencedCodeProjectionParserV0",
116 Self::ServerTemplateMarkup => "serverTemplateMarkupProjectionParserV0",
117 }
118 }
119
120 fn language(&self) -> &'static str {
121 match self {
122 Self::OxcTsx => "tsx",
123 Self::VueSfcScript => "vue",
124 Self::HtmlScript => "html",
125 Self::SvelteComponentScript => "svelte",
126 Self::AstroComponentScript => "astro",
127 Self::MarkdownFencedCode => "markdown",
128 Self::ServerTemplateMarkup => "server-template",
129 }
130 }
131
132 fn language_aliases(&self) -> Vec<&'static str> {
133 match self {
134 Self::OxcTsx => vec![
135 "typescriptreact",
136 "javascriptreact",
137 "typescript",
138 "javascript",
139 ],
140 Self::MarkdownFencedCode => vec!["mdx"],
141 Self::ServerTemplateMarkup => vec![
142 "liquid",
143 "twig",
144 "nunjucks",
145 "handlebars",
146 "erb",
147 "ejs",
148 "django-html",
149 "jinja",
150 "html-eex",
151 "heex",
152 ],
153 _ => Vec::new(),
154 }
155 }
156
157 fn projection_kind(&self) -> &'static str {
158 match self {
159 Self::OxcTsx => "identityOxc",
160 Self::VueSfcScript => "bytePreservingScriptBlocks",
161 Self::HtmlScript => "bytePreservingScriptBlocks",
162 Self::SvelteComponentScript => "bytePreservingScriptBlocks",
163 Self::AstroComponentScript => "bytePreservingFrontmatterAndScriptBlocks",
164 Self::MarkdownFencedCode => "bytePreservingFencedCodeBlocks",
165 Self::ServerTemplateMarkup => "bytePreservingTemplateMarkupScan",
166 }
167 }
168
169 fn source_type(&self, source_path: &str) -> SourceType {
170 match self {
171 Self::OxcTsx => {
172 SourceType::from_path(source_path).unwrap_or_else(|_| SourceType::tsx())
173 }
174 Self::VueSfcScript
175 | Self::HtmlScript
176 | Self::SvelteComponentScript
177 | Self::AstroComponentScript
178 | Self::MarkdownFencedCode
179 | Self::ServerTemplateMarkup => SourceType::tsx(),
180 }
181 }
182
183 fn project<'a>(&self, source: &'a str) -> Cow<'a, str> {
184 match self {
185 Self::OxcTsx => Cow::Borrowed(source),
186 Self::VueSfcScript => Cow::Owned(project_tag_contents_to_typescript_source(
187 source,
188 "<script",
189 "</script>",
190 )),
191 Self::HtmlScript => Cow::Owned(project_tag_contents_to_typescript_source(
192 source,
193 "<script",
194 "</script>",
195 )),
196 Self::SvelteComponentScript => Cow::Owned(project_tag_contents_to_typescript_source(
197 source,
198 "<script",
199 "</script>",
200 )),
201 Self::AstroComponentScript => {
202 Cow::Owned(project_astro_component_to_typescript_source(source))
203 }
204 Self::MarkdownFencedCode => Cow::Owned(project_markdown_to_typescript_source(source)),
205 Self::ServerTemplateMarkup => Cow::Owned(String::new()),
206 }
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
211#[serde(rename_all = "camelCase")]
212pub struct SourceLanguageParserBoundarySummaryV0 {
213 pub schema_version: &'static str,
214 pub product: &'static str,
215 pub parser_count: usize,
216 pub parsers: Vec<SourceLanguageParserDescriptorV0>,
217 pub external_parser_abi_stable: bool,
218 pub ready_surfaces: Vec<&'static str>,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
222#[serde(rename_all = "camelCase")]
223pub struct SourceLanguageParserDescriptorV0 {
224 pub parser_id: &'static str,
225 pub language: &'static str,
226 pub language_aliases: Vec<&'static str>,
227 pub projection_kind: &'static str,
228 pub fixture_witnessed: bool,
229}
230
231pub fn summarize_omena_bridge_source_language_parser_boundary_v0()
232-> SourceLanguageParserBoundarySummaryV0 {
233 let parsers = [
234 SourceLanguageParserKindV0::OxcTsx,
235 SourceLanguageParserKindV0::VueSfcScript,
236 SourceLanguageParserKindV0::HtmlScript,
237 SourceLanguageParserKindV0::SvelteComponentScript,
238 SourceLanguageParserKindV0::AstroComponentScript,
239 SourceLanguageParserKindV0::MarkdownFencedCode,
240 SourceLanguageParserKindV0::ServerTemplateMarkup,
241 ]
242 .into_iter()
243 .map(|parser| SourceLanguageParserDescriptorV0 {
244 parser_id: parser.parser_id(),
245 language: parser.language(),
246 language_aliases: parser.language_aliases(),
247 projection_kind: parser.projection_kind(),
248 fixture_witnessed: true,
249 })
250 .collect::<Vec<_>>();
251
252 SourceLanguageParserBoundarySummaryV0 {
253 schema_version: "0",
254 product: "omena-bridge.source-language-parser-boundary",
255 parser_count: parsers.len(),
256 parsers,
257 external_parser_abi_stable: false,
258 ready_surfaces: vec![
259 "sourceLanguageParserV0",
260 "oxcTsxParserBoundary",
261 "vueSfcScriptProjection",
262 "htmlScriptProjection",
263 "svelteComponentScriptProjection",
264 "astroComponentScriptProjection",
265 "markdownFencedCodeProjection",
266 "mdxFencedCodeProjection",
267 "serverTemplateMarkupScan",
268 "serverTemplateInterpolationScan",
269 "serverTemplateLanguageAliases",
270 ],
271 }
272}
273
274pub(crate) fn is_vue_source(source_path: &str, source_language: Option<&str>) -> bool {
275 source_language == Some("vue") || source_path.ends_with(".vue")
276}
277
278pub(crate) fn is_html_source(source_path: &str, source_language: Option<&str>) -> bool {
279 source_language == Some("html")
280 || source_path.ends_with(".html")
281 || source_path.ends_with(".htm")
282}
283
284pub(crate) fn is_svelte_source(source_path: &str, source_language: Option<&str>) -> bool {
285 source_language == Some("svelte") || source_path.ends_with(".svelte")
286}
287
288pub(crate) fn is_astro_source(source_path: &str, source_language: Option<&str>) -> bool {
289 source_language == Some("astro") || source_path.ends_with(".astro")
290}
291
292pub(crate) fn is_markdown_source(source_path: &str, source_language: Option<&str>) -> bool {
293 source_language == Some("markdown")
294 || source_language == Some("mdx")
295 || source_path.ends_with(".md")
296 || source_path.ends_with(".mdx")
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub(crate) enum ServerTemplateDelimiterFamilyV0 {
301 LiquidLike,
302 ErbLike,
303 Handlebars,
304}
305
306pub(crate) fn server_template_delimiter_family(
307 source_path: &str,
308 source_language: Option<&str>,
309) -> Option<ServerTemplateDelimiterFamilyV0> {
310 if matches!(
311 source_language,
312 Some("liquid" | "twig" | "nunjucks" | "django-html" | "jinja")
313 ) || source_path.ends_with(".liquid")
314 || source_path.ends_with(".twig")
315 || source_path.ends_with(".njk")
316 || source_path.ends_with(".nunjucks")
317 {
318 return Some(ServerTemplateDelimiterFamilyV0::LiquidLike);
319 }
320 if matches!(source_language, Some("erb" | "ejs" | "html-eex" | "heex"))
321 || source_path.ends_with(".erb")
322 || source_path.ends_with(".ejs")
323 || source_path.ends_with(".html.eex")
324 || source_path.ends_with(".heex")
325 {
326 return Some(ServerTemplateDelimiterFamilyV0::ErbLike);
327 }
328 if source_language == Some("handlebars")
329 || source_path.ends_with(".hbs")
330 || source_path.ends_with(".handlebars")
331 {
332 return Some(ServerTemplateDelimiterFamilyV0::Handlebars);
333 }
334 None
335}
336
337pub(crate) fn is_server_template_source(source_path: &str, source_language: Option<&str>) -> bool {
338 matches!(
339 source_language,
340 Some(
341 "liquid"
342 | "twig"
343 | "nunjucks"
344 | "handlebars"
345 | "erb"
346 | "ejs"
347 | "django-html"
348 | "jinja"
349 | "html-eex"
350 | "heex"
351 )
352 ) || server_template_delimiter_family(source_path, source_language).is_some()
353}
354
355fn source_language_parser_for_path(
356 source_path: &str,
357 source_language: Option<&str>,
358) -> SourceLanguageParserKindV0 {
359 if is_vue_source(source_path, source_language) {
360 SourceLanguageParserKindV0::VueSfcScript
361 } else if is_html_source(source_path, source_language) {
362 SourceLanguageParserKindV0::HtmlScript
363 } else if is_svelte_source(source_path, source_language) {
364 SourceLanguageParserKindV0::SvelteComponentScript
365 } else if is_astro_source(source_path, source_language) {
366 SourceLanguageParserKindV0::AstroComponentScript
367 } else if is_markdown_source(source_path, source_language) {
368 SourceLanguageParserKindV0::MarkdownFencedCode
369 } else if is_server_template_source(source_path, source_language) {
370 SourceLanguageParserKindV0::ServerTemplateMarkup
371 } else {
372 SourceLanguageParserKindV0::OxcTsx
373 }
374}
375
376pub(crate) fn project_source_for_language<'a>(
377 source_path: &str,
378 source: &'a str,
379 source_language: Option<&str>,
380) -> Cow<'a, str> {
381 source_language_parser_for_path(source_path, source_language).project(source)
382}
383
384pub(crate) fn source_type_for_language(
385 source_path: &str,
386 source_language: Option<&str>,
387) -> SourceType {
388 source_language_parser_for_path(source_path, source_language).source_type(source_path)
389}
390
391#[cfg(test)]
392fn project_vue_sfc_script_to_typescript_source(source: &str) -> String {
393 project_tag_contents_to_typescript_source(source, "<script", "</script>")
394}
395
396#[cfg(test)]
397fn project_html_script_to_typescript_source(source: &str) -> String {
398 project_tag_contents_to_typescript_source(source, "<script", "</script>")
399}
400
401#[cfg(test)]
402fn project_svelte_component_script_to_typescript_source(source: &str) -> String {
403 project_tag_contents_to_typescript_source(source, "<script", "</script>")
404}
405
406#[cfg(test)]
407fn project_astro_component_script_to_typescript_source(source: &str) -> String {
408 project_astro_component_to_typescript_source(source)
409}
410
411#[cfg(test)]
412fn project_markdown_fenced_code_to_typescript_source(source: &str) -> String {
413 project_markdown_to_typescript_source(source)
414}
415
416fn project_tag_contents_to_typescript_source(
417 source: &str,
418 open_tag: &str,
419 close_tag: &str,
420) -> String {
421 project_ranges_to_typescript_source(source, tag_content_ranges(source, open_tag, close_tag))
422}
423
424fn project_astro_component_to_typescript_source(source: &str) -> String {
425 let mut ranges = Vec::new();
426 if let Some(range) = astro_frontmatter_range(source) {
427 ranges.push(range);
428 }
429 ranges.extend(tag_content_ranges(source, "<script", "</script>"));
430 project_ranges_to_typescript_source(source, ranges)
431}
432
433fn project_markdown_to_typescript_source(source: &str) -> String {
434 project_ranges_to_typescript_source(source, markdown_typescript_fence_ranges(source))
435}
436
437fn markdown_typescript_fence_ranges(source: &str) -> Vec<(usize, usize)> {
438 let mut ranges = Vec::new();
439 let mut open_fence: Option<(char, usize, usize)> = None;
440 let mut offset = 0usize;
441
442 for line in source.split_inclusive('\n') {
443 let line_start = offset;
444 let line_end = offset + line.len();
445 let line_without_newline = line.trim_end_matches(['\r', '\n']);
446 let leading_spaces = line_without_newline
447 .chars()
448 .take_while(|ch| *ch == ' ')
449 .count();
450 let trimmed = line_without_newline.trim_start_matches(' ');
451 if leading_spaces <= 3 {
452 if let Some((fence_char, fence_len, content_start)) = open_fence {
453 if markdown_fence_marker(trimmed).is_some_and(|(candidate_char, candidate_len)| {
454 candidate_char == fence_char && candidate_len >= fence_len
455 }) {
456 ranges.push((content_start, line_start));
457 open_fence = None;
458 }
459 } else if let Some((fence_char, fence_len)) = markdown_fence_marker(trimmed) {
460 let language = trimmed[fence_len..].trim();
461 if markdown_fence_language_is_typescript(language) {
462 open_fence = Some((fence_char, fence_len, line_end));
463 }
464 }
465 }
466 offset = line_end;
467 }
468
469 if let Some((_, _, content_start)) = open_fence {
470 ranges.push((content_start, source.len()));
471 }
472 ranges
473}
474
475fn markdown_fence_marker(line: &str) -> Option<(char, usize)> {
476 let mut chars = line.chars();
477 let fence_char = chars.next()?;
478 if fence_char != '`' && fence_char != '~' {
479 return None;
480 }
481 let fence_len = 1 + chars.take_while(|ch| *ch == fence_char).count();
482 if fence_len >= 3 {
483 Some((fence_char, fence_len))
484 } else {
485 None
486 }
487}
488
489fn markdown_fence_language_is_typescript(language: &str) -> bool {
490 let normalized = language
491 .split_whitespace()
492 .next()
493 .unwrap_or_default()
494 .trim_start_matches('{')
495 .trim_end_matches('}')
496 .to_ascii_lowercase();
497 matches!(
498 normalized.as_str(),
499 "ts" | "tsx"
500 | "typescript"
501 | "typescriptreact"
502 | "js"
503 | "jsx"
504 | "javascript"
505 | "javascriptreact"
506 )
507}
508
509pub(crate) fn tag_content_ranges(
510 source: &str,
511 open_tag: &str,
512 close_tag: &str,
513) -> Vec<(usize, usize)> {
514 let lower = source.to_ascii_lowercase();
515 let mut cursor = 0usize;
516 let mut ranges = Vec::new();
517
518 while let Some(relative_start) = lower[cursor..].find(open_tag) {
519 let tag_start = cursor + relative_start;
520 let Some(relative_tag_end) = lower[tag_start..].find('>') else {
521 break;
522 };
523 let content_start = tag_start + relative_tag_end + 1;
524 let Some(relative_close_start) = lower[content_start..].find(close_tag) else {
525 break;
526 };
527 let content_end = content_start + relative_close_start;
528 ranges.push((content_start, content_end));
529 cursor = content_end + close_tag.len();
530 }
531 ranges
532}
533
534fn astro_frontmatter_range(source: &str) -> Option<(usize, usize)> {
535 if !source.starts_with("---") {
536 return None;
537 }
538 let content_start = source[3..].find('\n')? + 4;
539 let relative_close = source[content_start..].find("\n---")?;
540 Some((content_start, content_start + relative_close))
541}
542
543fn project_ranges_to_typescript_source(
544 source: &str,
545 ranges: impl IntoIterator<Item = (usize, usize)>,
546) -> String {
547 let mut keep = vec![false; source.len()];
548 for (start, end) in ranges {
549 for item in keep.iter_mut().take(end).skip(start) {
550 *item = true;
551 }
552 }
553
554 let mut projected = String::with_capacity(source.len());
555 for (index, ch) in source.char_indices() {
556 if ch == '\n' {
557 projected.push('\n');
558 } else if keep[index] {
559 projected.push(ch);
560 } else {
561 for _ in 0..ch.len_utf8() {
562 projected.push(' ');
563 }
564 }
565 }
566 projected
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 #[test]
574 fn vue_sfc_projection_preserves_byte_offsets_and_script_text() {
575 let source = "<template>한글</template>\n<script setup lang=\"ts\">\nconst styles = useCssModule();\n</script>\n<style module>.root {}</style>\n";
576 let projected = project_vue_sfc_script_to_typescript_source(source);
577
578 assert_eq!(projected.len(), source.len());
579 assert_eq!(
580 projected.find("styles = useCssModule"),
581 source.find("styles = useCssModule")
582 );
583 assert!(!projected.contains("한글"));
584 assert!(!projected.contains(".root"));
585 }
586
587 #[test]
588 fn html_projection_preserves_script_import_offsets() {
589 let source = "<main>ignored</main>\n<script type=\"module\">\nimport styles from \"./App.module.scss\";\n</script>\n";
590 let projected = project_html_script_to_typescript_source(source);
591
592 assert_eq!(projected.len(), source.len());
593 assert_eq!(
594 projected.find("import styles"),
595 source.find("import styles")
596 );
597 assert!(!projected.contains("ignored"));
598 }
599
600 #[test]
601 fn svelte_projection_preserves_script_import_offsets() {
602 let source = "<script lang=\"ts\">\nimport styles from \"./Card.module.scss\";\nexport const root = styles.root;\n</script>\n<section>ignored</section>\n<style>.root { color: red; }</style>\n";
603 let projected = project_svelte_component_script_to_typescript_source(source);
604
605 assert_eq!(projected.len(), source.len());
606 assert_eq!(
607 projected.find("import styles"),
608 source.find("import styles")
609 );
610 assert_eq!(projected.find("styles.root"), source.find("styles.root"));
611 assert!(!projected.contains("ignored"));
612 assert!(!projected.contains("color: red"));
613 }
614
615 #[test]
616 fn astro_projection_preserves_frontmatter_and_script_import_offsets() {
617 let source = "---\nimport styles from \"./Card.module.scss\";\nconst root = styles.root;\n---\n<div class={root}>ignored</div>\n<script>\nconst local = styles.root;\n</script>\n<style>.root { color: red; }</style>\n";
618 let projected = project_astro_component_script_to_typescript_source(source);
619
620 assert_eq!(projected.len(), source.len());
621 assert_eq!(
622 projected.find("import styles"),
623 source.find("import styles")
624 );
625 assert_eq!(projected.find("const local"), source.find("const local"));
626 assert!(!projected.contains("ignored"));
627 assert!(!projected.contains("color: red"));
628 }
629
630 #[test]
631 fn markdown_projection_preserves_typescript_fenced_code_offsets() {
632 let source = "# Notes\n\nignored text\n\n```tsx\nimport styles from \"./Card.module.scss\";\nconst root = styles.root;\n```\n\n```css\n.root { color: red; }\n```\n";
633 let projected = project_markdown_fenced_code_to_typescript_source(source);
634
635 assert_eq!(projected.len(), source.len());
636 assert_eq!(
637 projected.find("import styles"),
638 source.find("import styles")
639 );
640 assert_eq!(projected.find("styles.root"), source.find("styles.root"));
641 assert!(!projected.contains("ignored text"));
642 assert!(!projected.contains("color: red"));
643 }
644
645 #[test]
646 fn source_language_parser_boundary_lists_fixture_witnessed_v0_parsers() {
647 let summary = summarize_omena_bridge_source_language_parser_boundary_v0();
648
649 assert_eq!(
650 summary.product,
651 "omena-bridge.source-language-parser-boundary"
652 );
653 assert_eq!(summary.parser_count, 7);
654 assert!(!summary.external_parser_abi_stable);
655 assert!(summary.parsers.iter().any(|parser| {
656 parser.parser_id == "oxcTsxSourceLanguageParserV0" && parser.fixture_witnessed
657 }));
658 assert!(summary.parsers.iter().any(|parser| {
659 parser.parser_id == "htmlScriptProjectionParserV0" && parser.language == "html"
660 }));
661 assert!(summary.parsers.iter().any(|parser| {
662 parser.parser_id == "svelteComponentScriptProjectionParserV0"
663 && parser.language == "svelte"
664 }));
665 assert!(summary.parsers.iter().any(|parser| {
666 parser.parser_id == "astroComponentScriptProjectionParserV0"
667 && parser.language == "astro"
668 }));
669 assert!(summary.parsers.iter().any(|parser| {
670 parser.parser_id == "markdownFencedCodeProjectionParserV0"
671 && parser.language == "markdown"
672 && parser.language_aliases == vec!["mdx"]
673 }));
674 assert!(summary.parsers.iter().any(|parser| {
675 parser.parser_id == "serverTemplateMarkupProjectionParserV0"
676 && parser.language == "server-template"
677 && parser.language_aliases.contains(&"liquid")
678 && parser.language_aliases.contains(&"heex")
679 }));
680 assert!(summary.ready_surfaces.contains(&"mdxFencedCodeProjection"));
681 assert!(
682 summary
683 .ready_surfaces
684 .contains(&"serverTemplateInterpolationScan")
685 );
686 assert!(
687 summary
688 .ready_surfaces
689 .contains(&"serverTemplateLanguageAliases")
690 );
691 }
692
693 #[test]
694 fn server_template_projection_keeps_script_extraction_inert() {
695 let projected = project_source_for_language(
696 "page.liquid",
697 r#"<main class="{{ modifier }}">content</main>"#,
698 Some("liquid"),
699 );
700
701 assert_eq!(projected.as_ref(), "");
702 }
703}