Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 85 additions & 5 deletions crates/oxide/src/extractor/pre_processors/vue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ use regex::Regex;
use std::sync;

static TEMPLATE_REGEX: sync::LazyLock<Regex> = sync::LazyLock::new(|| {
Regex::new(r#"<template lang=['"]([^"']*)['"]>([\s\S]*)<\/template>"#).unwrap()
Regex::new(
r#"(?x)
<template\s+
# Consume quoted values whole so embedded lang attributes are ignored.
(?:(?:[^>"']|"[^"]*"|'[^']*')*\s+)?
lang\s*=\s*(?:"([^"]*)"|'([^']*)')
# A closing angle bracket inside a quoted value does not end the tag.
(?:[^>"']|"[^"]*"|'[^']*')*>
([\s\S]*)</template>
"#,
)
.unwrap()
});

#[derive(Debug, Default)]
Expand All @@ -17,10 +28,16 @@ impl PreProcessor for Vue {

// Only process template tags if content is valid UTF-8
if let Ok(content_as_str) = std::str::from_utf8(content) {
for (_, [lang, body]) in TEMPLATE_REGEX
.captures_iter(content_as_str)
.map(|c| c.extract())
{
for captures in TEMPLATE_REGEX.captures_iter(content_as_str) {
let lang = captures
.get(1)
.or_else(|| captures.get(2))
.expect("template lang capture should exist")
.as_str();
let body = captures
.get(3)
.expect("template body capture should exist")
.as_str();
let replaced = pre_process_input(body.as_bytes().to_vec(), lang);
result = result.replace(body, replaced);
}
Expand All @@ -46,6 +63,69 @@ mod tests {
Vue::test_extract_contains(input, vec!["bg-neutral-900", "text-red-500"]);
}

#[test]
fn test_vue_template_pug_with_attributes_and_whitespace() {
for input in [
r#"
<template data-test="true" lang="pug">
.bg-neutral-900.text-red-500 This is a test.
</template>
"#,
r#"
<template
lang = 'pug'
data-test="true"
>
.bg-neutral-900.text-red-500 This is a test.
</template>
"#,
] {
Vue::test_extract_contains(input, vec!["bg-neutral-900", "text-red-500"]);
}
}

#[test]
fn test_vue_template_does_not_treat_data_lang_as_lang() {
let input = r#"
<template data-lang="pug">
.bg-neutral-900.text-red-500 This is a test.
</template>
"#;

Vue::test(input, input);
}

#[test]
fn test_vue_template_does_not_treat_quoted_values_as_lang() {
for attributes in [
r#"data-config=" lang='pug'""#,
r#"data-config=' lang="pug"'"#,
r#"lang="html" data-config=" lang='pug'""#,
r#"data-config=' lang="pug"' lang="html""#,
] {
let input = format!(r#"<template {attributes}><div class="foo.bar"></div></template>"#);

Vue::test(&input, &input);
}
}

#[test]
fn test_vue_template_pug_with_quoted_attribute_values() {
for attributes in [
r#"data-config=" lang='html'" lang="pug""#,
r#"lang='pug' data-config=' lang="html"'"#,
r#"data-config="a > b.c" lang='pug'"#,
r#"lang="pug" data-config='a > b.c'"#,
r#"functional data-test=true lang="pug""#,
] {
let input = format!("<template {attributes}>.flex.items-center</template>");
let expected = format!("<template {attributes}> flex items-center</template>");

Vue::test(&input, &expected);
Vue::test_extract_contains(&input, vec!["flex", "items-center"]);
}
}

#[test]
fn test_invalid_utf8_does_not_panic() {
// Invalid UTF-8 sequence: 0x80 is a continuation byte without a leading byte
Expand Down