Skip to content

Commit

Permalink
add proper literal merging
Browse files Browse the repository at this point in the history
  • Loading branch information
arlyon committed Aug 19, 2022
1 parent 65a0f08 commit c2b3a56
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 31 deletions.
1 change: 1 addition & 0 deletions src/lib.rs
Expand Up @@ -6,6 +6,7 @@ mod parse;
mod plugin;
#[cfg(test)]
mod test;
mod util;

use config::TailwindConfig;
use swc_ecmascript::ast::{
Expand Down
6 changes: 2 additions & 4 deletions src/parse/from.rs
Expand Up @@ -9,6 +9,7 @@ use swc_ecma_visit::swc_ecma_ast::PropOrSpread;
use swc_ecma_visit::swc_ecma_ast::Str;

use crate::config::TailwindTheme;
use crate::util::merge_literals;

use super::literal::parse_literal;
use super::nom::Directive;
Expand All @@ -19,10 +20,7 @@ pub fn literal_from_directive<'a>(val: Directive<'a>, theme: &TailwindTheme) ->
val.exps
.into_iter()
.map(|e| literal_from_exp(e, theme))
.reduce(|mut acc, mut curr| {
acc.props.append(&mut curr.props);
acc
})
.reduce(merge_literals)
.unwrap_or_else(|| ObjectLit {
span: DUMMY_SP,
props: vec![],
Expand Down
29 changes: 2 additions & 27 deletions src/plugin.rs
@@ -1,14 +1,12 @@
use std::collections::HashMap;

use itertools::Itertools;
use swc_common::DUMMY_SP;
use swc_ecma_visit::swc_ecma_ast::{
Expr, KeyValueProp, Lit, ObjectLit, Prop, PropName, PropOrSpread, Str,
};
use swc_ecma_visit::swc_ecma_ast::ObjectLit;

use crate::{
config::TailwindTheme,
infer::{infer_type, Type},
util::to_lit,
};

fn simple_lookup(hashmap: &HashMap<&str, &str>, search: &str, output: &str) -> Option<ObjectLit> {
Expand Down Expand Up @@ -196,26 +194,3 @@ pub fn m(rest: &str, theme: &TailwindTheme) -> Option<ObjectLit> {
pub fn z(rest: &str, theme: &TailwindTheme) -> Option<ObjectLit> {
simple_lookup(&theme.z_index, rest, "z-index")
}

fn to_lit(items: &[(&str, &str)]) -> ObjectLit {
ObjectLit {
span: DUMMY_SP,
props: items
.iter()
.map(|(key, value)| {
PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Str(Str {
span: DUMMY_SP,
raw: None,
value: (*key).into(),
}),
value: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
raw: None,
value: (*value).into(),
}))),
})))
})
.collect(),
}
}
14 changes: 14 additions & 0 deletions src/test.rs
Expand Up @@ -7,6 +7,7 @@ fn test_visitor() -> TransformVisitor<'static> {
let mut t = TransformVisitor::default();

t.config.theme.spacing.insert("4", "1rem");
t.config.theme.colors.insert("black", "black");

t
}
Expand Down Expand Up @@ -84,3 +85,16 @@ test!(
// Output codes after transformed with plugin
r#"<Test css={[{"height": "1rem"}]} />"#
);

test!(
swc_ecma_parser::Syntax::Typescript(swc_ecma_parser::TsConfig {
tsx: true,
..Default::default()
}),
|_| as_folder(test_visitor()),
multiple_mod,
// Input codes
r#"<Test tw="hover:h-4 hover:text-black" />"#,
// Output codes after transformed with plugin
r#"<Test css={{"&:hover": {"height": "1rem", "color": "black"}}} />"#
);
188 changes: 188 additions & 0 deletions src/util.rs
@@ -0,0 +1,188 @@
use std::collections::HashMap;

use swc_atoms::JsWord;
use swc_common::DUMMY_SP;
use swc_ecma_visit::swc_ecma_ast::{
Expr, KeyValueProp, Lit, ObjectLit, Prop, PropName, PropOrSpread, Str,
};

enum KeyStrategy {
Merge,
Override,
}

/**
* Inserts all the keys / values from b into a.
* On clash:
* - if both values are objects, merge
* - otherwise, overwrite with b
*
* todo(arlyon): this could be slow for many keys
*/
pub fn merge_literals(mut a: ObjectLit, b: ObjectLit) -> ObjectLit {
let mut strategies: HashMap<JsWord, (usize, KeyStrategy)> = Default::default();

for (idx, prop) in a.props.iter().enumerate() {
if let PropOrSpread::Prop(box Prop::KeyValue(KeyValueProp {
key: PropName::Str(Str { value: name, .. }),
value: box value,
})) = prop
{
strategies.insert(
name.to_owned(),
(
idx,
match value {
Expr::Object(_) => KeyStrategy::Merge,
_ => KeyStrategy::Override,
},
),
);
}
}

for prop in b.props {
if let PropOrSpread::Prop(box Prop::KeyValue(KeyValueProp {
key: PropName::Str(Str { value: name, .. }),
..
})) = &prop
{
match strategies.get(name) {
Some((idx, KeyStrategy::Merge)) => {
if let (
Some(PropOrSpread::Prop(box Prop::KeyValue(KeyValueProp {
value: box Expr::Object(lit1),
..
}))),
PropOrSpread::Prop(box Prop::KeyValue(KeyValueProp {
value: box Expr::Object(lit2),
..
})),
) = (a.props.get_mut(*idx), prop)
{
let temp = std::mem::replace(lit1, to_lit(&[]));
std::mem::replace(lit1, merge_literals(temp, lit2));
}
}
Some((idx, KeyStrategy::Override)) => a.props.insert(*idx, prop),
_ => a.props.push(prop),
}
}
}

a
}

pub fn to_lit(items: &[(&str, &str)]) -> ObjectLit {
ObjectLit {
span: DUMMY_SP,
props: items
.iter()
.map(|(key, value)| {
PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Str(Str {
span: DUMMY_SP,
raw: None,
value: (*key).into(),
}),
value: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
raw: None,
value: (*value).into(),
}))),
})))
})
.collect(),
}
}

#[cfg(test)]
mod test {
use swc_common::DUMMY_SP;
use swc_ecma_visit::swc_ecma_ast::{
Expr, KeyValueProp, Lit, ObjectLit, Prop, PropName, PropOrSpread, Str,
};

use crate::util::{merge_literals, to_lit};

#[test]
fn a_and_b_merges() {
let a = to_lit(&[("a", "value")]);
let b = to_lit(&[("b", "value")]);
let c = merge_literals(a, b);
assert!(c.props.len() == 2);
}

#[test]
fn empty_b_does_nothing() {
let a = ObjectLit {
span: DUMMY_SP,
props: vec![],
};

let b = ObjectLit {
span: DUMMY_SP,
props: vec![],
};

let c = merge_literals(a.clone(), b);

assert!(a.eq(&c));
}

#[test]
fn override_b_replaces() {
let a = to_lit(&[("replace", "a")]);
let b = to_lit(&[("replace", "b")]);

let c = merge_literals(a.clone(), b);

if let PropOrSpread::Prop(box Prop::KeyValue(KeyValueProp {
value: box Expr::Lit(Lit::Str(Str { value, .. })),
..
})) = &c.props[0]
{
assert!("b".eq(&*value))
} else {
panic!("fail")
}
}

#[test]
fn mergeable_b_merges() {
let a = ObjectLit {
span: DUMMY_SP,
props: vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
value: Box::new(Expr::Object(to_lit(&[("a", "value")]))),
key: PropName::Str(Str {
raw: None,
span: DUMMY_SP,
value: "merge".into(),
}),
})))],
};
let b = ObjectLit {
span: DUMMY_SP,
props: vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
value: Box::new(Expr::Object(to_lit(&[("b", "value")]))),
key: PropName::Str(Str {
raw: None,
span: DUMMY_SP,
value: "merge".into(),
}),
})))],
};

let c = merge_literals(a.clone(), b);

if let PropOrSpread::Prop(box Prop::KeyValue(KeyValueProp {
value: box Expr::Object(ObjectLit { props, .. }),
..
})) = &c.props[0]
{
assert!(props.len() == 2)
} else {
panic!("fail")
}
}
}

0 comments on commit c2b3a56

Please sign in to comment.