klenod-plugin-css 0.0.1

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.
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "native"
3
+ version = "0.0.1"
4
+ edition = "2021"
5
+
6
+ [lib]
7
+ name = "native"
8
+ crate-type = ["cdylib"]
9
+
10
+ [dependencies]
11
+ base64 = "0.22.1"
12
+ cssparser = "0.37.0"
13
+ lightningcss = { version = "1.0.0-alpha.70", features = ["serde", "visitor", "sourcemap"] }
14
+ magnus = "0.8.2"
15
+ parcel_sourcemap = { version = "2.1.1", features = ["json"] }
16
+ serde_json = "1.0.149"
17
+ sha2 = "0.10.9"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("klenod/plugin/css/native")
@@ -0,0 +1,346 @@
1
+ use lightningcss::dependencies::DependencyOptions;
2
+ use lightningcss::{
3
+ properties::{css_modules::Specifier, custom::Variable},
4
+ selector::{Component, Selector},
5
+ stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet},
6
+ values::ident::DashedIdent,
7
+ visitor::{Visit, VisitTypes, Visitor},
8
+ };
9
+ use magnus::{
10
+ exception::ExceptionClass, function, gc::register_mark_object, prelude::*, value::Lazy, Error,
11
+ RHash, RModule, Ruby, TryConvert,
12
+ };
13
+ use parcel_sourcemap::SourceMap;
14
+ use std::{collections::HashMap, convert::Infallible};
15
+
16
+ static PARSE_ERROR: Lazy<ExceptionClass> = Lazy::new(|ruby| {
17
+ let ex = ruby
18
+ .class_object()
19
+ .const_get::<_, RModule>("Klenod")
20
+ .unwrap()
21
+ .const_get::<_, RModule>("Build")
22
+ .unwrap()
23
+ .const_get::<_, RModule>("Plugins")
24
+ .unwrap()
25
+ .const_get::<_, RModule>("CSSPlugin")
26
+ .unwrap()
27
+ .const_get("ParseError")
28
+ .unwrap();
29
+ register_mark_object(ex);
30
+ ex
31
+ });
32
+
33
+ #[derive(Clone)]
34
+ struct TransformOptions {
35
+ component: String,
36
+ hash: String,
37
+ transform_names: bool,
38
+ class_pattern: String,
39
+ tag_pattern: String,
40
+ variable_pattern: String,
41
+ local_css_variables: bool,
42
+ }
43
+
44
+ struct TransformNamesVisitor<'a> {
45
+ options: TransformOptions,
46
+ classes: &'a mut HashMap<String, String>,
47
+ elements: &'a mut HashMap<String, String>,
48
+ variables: &'a mut HashMap<String, String>,
49
+ }
50
+
51
+ impl<'a, 'i> Visitor<'i> for TransformNamesVisitor<'a> {
52
+ type Error = Infallible;
53
+
54
+ fn visit_types(&self) -> VisitTypes {
55
+ VisitTypes::RULES | VisitTypes::DASHED_IDENTS | VisitTypes::VARIABLES
56
+ }
57
+
58
+ fn visit_selector(&mut self, selector: &mut Selector<'i>) -> Result<(), Self::Error> {
59
+ for component in selector.iter_mut_raw_match_order() {
60
+ match component {
61
+ Component::Class(class_name) => {
62
+ let original = class_name.to_string();
63
+ let formatted = format_selector(
64
+ &self.options.class_pattern,
65
+ &self.options.component,
66
+ &original,
67
+ &self.options.hash,
68
+ );
69
+ self.classes.insert(original, formatted.clone());
70
+ if self.options.transform_names {
71
+ *component = Component::Class(formatted.into());
72
+ }
73
+ }
74
+ Component::LocalName(local_name) => {
75
+ let original = local_name.name.to_string();
76
+ let formatted = format_selector(
77
+ &self.options.tag_pattern,
78
+ &self.options.component,
79
+ &original,
80
+ &self.options.hash,
81
+ );
82
+ self.elements.insert(original, formatted.clone());
83
+ if self.options.transform_names {
84
+ *component = Component::Class(formatted.into());
85
+ }
86
+ }
87
+ _ => {}
88
+ }
89
+ }
90
+
91
+ Ok(())
92
+ }
93
+
94
+ fn visit_dashed_ident(&mut self, ident: &mut DashedIdent) -> Result<(), Self::Error> {
95
+ if !self.options.local_css_variables {
96
+ return Ok(());
97
+ }
98
+
99
+ let original = ident.0.to_string();
100
+ let formatted = format_variable(
101
+ &self.options.variable_pattern,
102
+ &self.options.component,
103
+ &original,
104
+ &self.options.hash,
105
+ );
106
+ self.variables.insert(original, formatted.clone());
107
+ if self.options.transform_names {
108
+ ident.0 = formatted.into();
109
+ }
110
+
111
+ Ok(())
112
+ }
113
+
114
+ fn visit_variable(&mut self, variable: &mut Variable) -> Result<(), Self::Error> {
115
+ if !matches!(
116
+ variable.name.from,
117
+ Some(Specifier::File(_)) | Some(Specifier::Global)
118
+ ) {
119
+ variable.visit_children(self)?;
120
+ } else if let Some(fallback) = &mut variable.fallback {
121
+ fallback.visit(self)?;
122
+ }
123
+
124
+ Ok(())
125
+ }
126
+ }
127
+
128
+ fn transform_native(
129
+ ruby: &Ruby,
130
+ source: String,
131
+ filename: String,
132
+ options: RHash,
133
+ ) -> Result<RHash, Error> {
134
+ let minify = hash_fetch_bool(ruby, options, "minify", true)?;
135
+ let transform_names = hash_fetch_bool(ruby, options, "transform_names", true)?;
136
+ let local_css_variables = hash_fetch_bool(ruby, options, "local_css_variables", false)?;
137
+ let class_pattern =
138
+ hash_fetch_string(ruby, options, "class_pattern", "[component].[local]?[hash]")?;
139
+ let tag_pattern =
140
+ hash_fetch_string(ruby, options, "tag_pattern", "[component]_[local]?[hash]")?;
141
+ let variable_pattern = hash_fetch_string(
142
+ ruby,
143
+ options,
144
+ "variable_pattern",
145
+ "[component]-[local]?[hash]",
146
+ )?;
147
+ let mut stylesheet = parse_stylesheet(ruby, &filename, &source, local_css_variables)?;
148
+ let mut classes = HashMap::new();
149
+ let mut elements = HashMap::new();
150
+ let mut variables = HashMap::new();
151
+ let selector_options = TransformOptions {
152
+ component: component_name(&filename),
153
+ hash: hash(&source),
154
+ transform_names,
155
+ class_pattern,
156
+ tag_pattern,
157
+ variable_pattern,
158
+ local_css_variables,
159
+ };
160
+
161
+ let _ = stylesheet.visit(&mut TransformNamesVisitor {
162
+ options: selector_options,
163
+ classes: &mut classes,
164
+ elements: &mut elements,
165
+ variables: &mut variables,
166
+ });
167
+
168
+ let mut source_map = SourceMap::new("/");
169
+ source_map.add_source(&filename);
170
+ source_map.set_source_content(0, &source).unwrap();
171
+
172
+ if minify {
173
+ stylesheet.minify(MinifyOptions::default()).unwrap();
174
+ }
175
+
176
+ let result = stylesheet
177
+ .to_css(PrinterOptions {
178
+ analyze_dependencies: Some(DependencyOptions::default()),
179
+ minify,
180
+ source_map: Some(&mut source_map),
181
+ ..PrinterOptions::default()
182
+ })
183
+ .unwrap();
184
+
185
+ let hash = ruby.hash_new();
186
+ hash.aset("code", result.code)?;
187
+ hash.aset("classes", string_map_to_hash(ruby, classes)?)?;
188
+ hash.aset("elements", string_map_to_hash(ruby, elements)?)?;
189
+ hash.aset(
190
+ "serialized_variables",
191
+ serialized_variable_map_to_hash(ruby, &variables)?,
192
+ )?;
193
+ hash.aset("variables", string_map_to_hash(ruby, variables)?)?;
194
+ hash.aset("source_map", source_map.to_json(None).ok())?;
195
+ hash.aset(
196
+ "dependencies",
197
+ serde_json::from_str::<serde_json::Value>(
198
+ &serde_json::to_string(&result.dependencies.unwrap()).unwrap(),
199
+ )
200
+ .unwrap()
201
+ .to_string(),
202
+ )?;
203
+ hash.aset(
204
+ "exports",
205
+ serde_json::from_str::<serde_json::Value>(
206
+ &serde_json::to_string(&result.exports.unwrap()).unwrap(),
207
+ )
208
+ .unwrap()
209
+ .to_string(),
210
+ )?;
211
+ hash.aset(
212
+ "references",
213
+ serde_json::from_str::<serde_json::Value>(
214
+ &serde_json::to_string(&result.references.unwrap_or_default()).unwrap(),
215
+ )
216
+ .unwrap()
217
+ .to_string(),
218
+ )?;
219
+ Ok(hash)
220
+ }
221
+
222
+ fn minify_native(ruby: &Ruby, source: String, filename: String) -> Result<String, Error> {
223
+ let mut stylesheet = parse_stylesheet(ruby, &filename, &source, false)?;
224
+ stylesheet.minify(MinifyOptions::default()).unwrap();
225
+
226
+ Ok(stylesheet
227
+ .to_css(PrinterOptions {
228
+ analyze_dependencies: Some(DependencyOptions::default()),
229
+ minify: true,
230
+ ..PrinterOptions::default()
231
+ })
232
+ .unwrap()
233
+ .code)
234
+ }
235
+
236
+ fn serialize_native(ruby: &Ruby, source: String, filename: String) -> Result<String, Error> {
237
+ let stylesheet = parse_stylesheet(ruby, &filename, &source, false)?;
238
+ Ok(serde_json::to_string(&stylesheet).unwrap())
239
+ }
240
+
241
+ fn parse_stylesheet<'i>(
242
+ ruby: &Ruby,
243
+ filename: &str,
244
+ source: &'i str,
245
+ local_css_variables: bool,
246
+ ) -> Result<StyleSheet<'i>, Error> {
247
+ StyleSheet::parse(
248
+ source,
249
+ ParserOptions {
250
+ filename: filename.to_string(),
251
+ css_modules: Some(lightningcss::css_modules::Config {
252
+ pattern: lightningcss::css_modules::Pattern::parse("[local]").unwrap(),
253
+ dashed_idents: local_css_variables,
254
+ animation: false,
255
+ grid: false,
256
+ container: false,
257
+ custom_idents: false,
258
+ pure: false,
259
+ }),
260
+ ..ParserOptions::default()
261
+ },
262
+ )
263
+ .map_err(|error| Error::new(ruby.get_inner(&PARSE_ERROR), error.to_string()))
264
+ }
265
+
266
+ fn hash_fetch_bool(_ruby: &Ruby, hash: RHash, key: &str, default: bool) -> Result<bool, Error> {
267
+ match hash.get(key) {
268
+ Some(value) => bool::try_convert(value),
269
+ None => Ok(default),
270
+ }
271
+ }
272
+
273
+ fn hash_fetch_string(_ruby: &Ruby, hash: RHash, key: &str, default: &str) -> Result<String, Error> {
274
+ match hash.get(key) {
275
+ Some(value) => String::try_convert(value),
276
+ None => Ok(default.to_string()),
277
+ }
278
+ }
279
+
280
+ fn string_map_to_hash(ruby: &Ruby, map: HashMap<String, String>) -> Result<RHash, Error> {
281
+ let hash = ruby.hash_new();
282
+ for (key, value) in map {
283
+ hash.aset(key, value)?;
284
+ }
285
+ Ok(hash)
286
+ }
287
+
288
+ fn serialized_variable_map_to_hash(
289
+ ruby: &Ruby,
290
+ map: &HashMap<String, String>,
291
+ ) -> Result<RHash, Error> {
292
+ let hash = ruby.hash_new();
293
+ for (key, value) in map {
294
+ let mut serialized = String::new();
295
+ cssparser::serialize_identifier(value, &mut serialized).unwrap();
296
+ hash.aset(key.clone(), serialized)?;
297
+ }
298
+ Ok(hash)
299
+ }
300
+
301
+ fn component_name(filename: &str) -> String {
302
+ std::path::Path::new(filename)
303
+ .with_extension("")
304
+ .display()
305
+ .to_string()
306
+ }
307
+
308
+ fn format_selector(pattern: &str, component: &str, local: &str, hash: &str) -> String {
309
+ pattern
310
+ .replace("[component]", component)
311
+ .replace("[local]", local)
312
+ .replace("[hash]", hash)
313
+ }
314
+
315
+ fn format_variable(pattern: &str, component: &str, local: &str, hash: &str) -> String {
316
+ format!(
317
+ "--{}",
318
+ format_selector(pattern, component, local.trim_start_matches("--"), hash)
319
+ )
320
+ }
321
+
322
+ fn hash(source: &str) -> String {
323
+ use base64::{engine::general_purpose, Engine as _};
324
+ use sha2::{Digest, Sha256};
325
+
326
+ let mut hasher = Sha256::new();
327
+ hasher.update(source);
328
+ general_purpose::URL_SAFE_NO_PAD
329
+ .encode(hasher.finalize())
330
+ .get(0..8)
331
+ .unwrap()
332
+ .to_string()
333
+ }
334
+
335
+ #[magnus::init]
336
+ fn init(ruby: &Ruby) -> Result<(), Error> {
337
+ let klenod = ruby.define_module("Klenod")?;
338
+ let build = klenod.define_module("Build")?;
339
+ let plugins = build.define_module("Plugins")?;
340
+ let css_plugin = plugins.define_module("CSSPlugin")?;
341
+ let native = css_plugin.define_module("Native")?;
342
+ native.define_singleton_method("transform_native", function!(transform_native, 3))?;
343
+ native.define_singleton_method("minify_native", function!(minify_native, 2))?;
344
+ native.define_singleton_method("serialize_native", function!(serialize_native, 2))?;
345
+ Ok(())
346
+ }