mirror of
				https://github.com/slint-ui/slint.git
				synced 2025-10-30 03:27:22 +00:00 
			
		
		
		
	 88c0323e6c
			
		
	
	
		88c0323e6c
		
	
	
	
		
			
	
		
	
	
		
			Some checks are pending
		
		
	
	autofix.ci / format_fix (push) Waiting to run
				
			autofix.ci / lint_typecheck (push) Waiting to run
				
			CI / files-changed (push) Waiting to run
				
			CI / build_and_test (--exclude bevy-example, ubuntu-22.04, 1.85) (push) Blocked by required conditions
				
			CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, --exclude bevy-example, windows-2022, 1.85) (push) Blocked by required conditions
				
			CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, macos-14, stable) (push) Blocked by required conditions
				
			CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, windows-2022, beta) (push) Blocked by required conditions
				
			CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, windows-2022, stable) (push) Blocked by required conditions
				
			CI / build_and_test (ubuntu-22.04, nightly) (push) Blocked by required conditions
				
			CI / node_test (macos-14) (push) Blocked by required conditions
				
			CI / node_test (ubuntu-22.04) (push) Blocked by required conditions
				
			CI / node_test (windows-2022) (push) Blocked by required conditions
				
			CI / python_test (macos-14) (push) Blocked by required conditions
				
			CI / python_test (ubuntu-22.04) (push) Blocked by required conditions
				
			CI / python_test (windows-2022) (push) Blocked by required conditions
				
			CI / wasm (push) Blocked by required conditions
				
			CI / wasm_demo (push) Blocked by required conditions
				
			CI / tree-sitter (push) Blocked by required conditions
				
			CI / updater_test (0.3.0) (push) Blocked by required conditions
				
			CI / fmt_test (push) Blocked by required conditions
				
			CI / esp-idf-quick (push) Blocked by required conditions
				
			CI / android (push) Blocked by required conditions
				
			CI / miri (push) Blocked by required conditions
				
			CI / cpp_test_driver (macos-13) (push) Blocked by required conditions
				
			CI / cpp_test_driver (ubuntu-22.04) (push) Blocked by required conditions
				
			CI / cpp_test_driver (windows-2022) (push) Blocked by required conditions
				
			CI / cpp_cmake (macos-14, 1.85) (push) Blocked by required conditions
				
			CI / cpp_cmake (ubuntu-22.04, stable) (push) Blocked by required conditions
				
			CI / cpp_cmake (windows-2022, nightly) (push) Blocked by required conditions
				
			CI / cpp_package_test (push) Blocked by required conditions
				
			CI / vsce_build_test (push) Blocked by required conditions
				
			CI / mcu (pico-st7789, thumbv6m-none-eabi) (push) Blocked by required conditions
				
			CI / mcu (pico2-st7789, thumbv8m.main-none-eabihf) (push) Blocked by required conditions
				
			CI / mcu (stm32h735g, thumbv7em-none-eabihf) (push) Blocked by required conditions
				
			CI / mcu-embassy (push) Blocked by required conditions
				
			CI / ffi_32bit_build (push) Blocked by required conditions
				
			CI / docs (push) Blocked by required conditions
				
			CI / test-figma-inspector (push) Blocked by required conditions
				
			
		
			
				
	
	
		
			505 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			505 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| // Copyright © SixtyFPS GmbH <info@slint.dev>
 | |
| // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
 | |
| 
 | |
| /*!
 | |
| The module responsible for the code generation.
 | |
| 
 | |
| There is one sub module for every language
 | |
| */
 | |
| 
 | |
| // cSpell: ignore deque subcomponent
 | |
| 
 | |
| use smol_str::SmolStr;
 | |
| use std::collections::{BTreeSet, HashSet, VecDeque};
 | |
| use std::rc::{Rc, Weak};
 | |
| 
 | |
| use crate::expression_tree::{BindingExpression, Expression};
 | |
| use crate::langtype::ElementType;
 | |
| use crate::namedreference::NamedReference;
 | |
| use crate::object_tree::{Component, Document, ElementRc};
 | |
| use crate::CompilerConfiguration;
 | |
| 
 | |
| #[cfg(feature = "cpp")]
 | |
| pub mod cpp;
 | |
| #[cfg(feature = "cpp")]
 | |
| pub mod cpp_live_preview;
 | |
| #[cfg(feature = "rust")]
 | |
| pub mod rust;
 | |
| #[cfg(feature = "rust")]
 | |
| pub mod rust_live_preview;
 | |
| 
 | |
| #[derive(Clone, Debug, PartialEq)]
 | |
| pub enum OutputFormat {
 | |
|     #[cfg(feature = "cpp")]
 | |
|     Cpp(cpp::Config),
 | |
|     #[cfg(feature = "rust")]
 | |
|     Rust,
 | |
|     Interpreter,
 | |
|     Llr,
 | |
| }
 | |
| 
 | |
| impl OutputFormat {
 | |
|     pub fn guess_from_extension(path: &std::path::Path) -> Option<Self> {
 | |
|         match path.extension().and_then(|ext| ext.to_str()) {
 | |
|             #[cfg(feature = "cpp")]
 | |
|             Some("cpp") | Some("cxx") | Some("h") | Some("hpp") => {
 | |
|                 Some(Self::Cpp(cpp::Config::default()))
 | |
|             }
 | |
|             #[cfg(feature = "rust")]
 | |
|             Some("rs") => Some(Self::Rust),
 | |
|             _ => None,
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl std::str::FromStr for OutputFormat {
 | |
|     type Err = String;
 | |
|     fn from_str(s: &str) -> Result<Self, Self::Err> {
 | |
|         match s {
 | |
|             #[cfg(feature = "cpp")]
 | |
|             "cpp" => Ok(Self::Cpp(cpp::Config::default())),
 | |
|             #[cfg(feature = "rust")]
 | |
|             "rust" => Ok(Self::Rust),
 | |
|             "llr" => Ok(Self::Llr),
 | |
|             _ => Err(format!("Unknown output format {s}")),
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| pub fn generate(
 | |
|     format: OutputFormat,
 | |
|     destination: &mut impl std::io::Write,
 | |
|     doc: &Document,
 | |
|     compiler_config: &CompilerConfiguration,
 | |
| ) -> std::io::Result<()> {
 | |
|     #![allow(unused_variables)]
 | |
|     #![allow(unreachable_code)]
 | |
| 
 | |
|     match format {
 | |
|         #[cfg(feature = "cpp")]
 | |
|         OutputFormat::Cpp(config) => {
 | |
|             let output = cpp::generate(doc, config, compiler_config)?;
 | |
|             write!(destination, "{output}")?;
 | |
|         }
 | |
|         #[cfg(feature = "rust")]
 | |
|         OutputFormat::Rust => {
 | |
|             let output = rust::generate(doc, compiler_config)?;
 | |
|             write!(destination, "{output}")?;
 | |
|         }
 | |
|         OutputFormat::Interpreter => {
 | |
|             return Err(std::io::Error::new(
 | |
|                 std::io::ErrorKind::Other,
 | |
|                 "Unsupported output format: The interpreter is not a valid output format yet.",
 | |
|             )); // Perhaps byte code in the future?
 | |
|         }
 | |
|         OutputFormat::Llr => {
 | |
|             let root = crate::llr::lower_to_item_tree::lower_to_item_tree(doc, compiler_config)?;
 | |
|             let mut output = String::new();
 | |
|             crate::llr::pretty_print::pretty_print(&root, &mut output).unwrap();
 | |
|             write!(destination, "{output}")?;
 | |
|         }
 | |
|     }
 | |
|     Ok(())
 | |
| }
 | |
| 
 | |
| /// A reference to this trait is passed to the [`build_item_tree`] function.
 | |
| /// It can be used to build the array for the item tree.
 | |
| pub trait ItemTreeBuilder {
 | |
|     /// Some state that contains the code on how to access some particular component
 | |
|     type SubComponentState: Clone;
 | |
| 
 | |
|     fn push_repeated_item(
 | |
|         &mut self,
 | |
|         item: &crate::object_tree::ElementRc,
 | |
|         repeater_count: u32,
 | |
|         parent_index: u32,
 | |
|         component_state: &Self::SubComponentState,
 | |
|     );
 | |
|     fn push_native_item(
 | |
|         &mut self,
 | |
|         item: &ElementRc,
 | |
|         children_offset: u32,
 | |
|         parent_index: u32,
 | |
|         component_state: &Self::SubComponentState,
 | |
|     );
 | |
|     /// Called when a component is entered, this allow to change the component_state.
 | |
|     /// The returned SubComponentState will be used for all the items within that component
 | |
|     fn enter_component(
 | |
|         &mut self,
 | |
|         item: &ElementRc,
 | |
|         sub_component: &Rc<Component>,
 | |
|         children_offset: u32,
 | |
|         component_state: &Self::SubComponentState,
 | |
|     ) -> Self::SubComponentState;
 | |
|     /// Called before the children of a component are entered.
 | |
|     fn enter_component_children(
 | |
|         &mut self,
 | |
|         item: &ElementRc,
 | |
|         repeater_count: u32,
 | |
|         component_state: &Self::SubComponentState,
 | |
|         sub_component_state: &Self::SubComponentState,
 | |
|     );
 | |
| }
 | |
| 
 | |
| /// Visit each item in order in which they should appear in the children tree array.
 | |
| pub fn build_item_tree<T: ItemTreeBuilder>(
 | |
|     root_component: &Rc<Component>,
 | |
|     initial_state: &T::SubComponentState,
 | |
|     builder: &mut T,
 | |
| ) {
 | |
|     if let Some(sub_component) = root_component.root_element.borrow().sub_component() {
 | |
|         assert!(root_component.root_element.borrow().children.is_empty());
 | |
|         let sub_compo_state =
 | |
|             builder.enter_component(&root_component.root_element, sub_component, 1, initial_state);
 | |
|         builder.enter_component_children(
 | |
|             &root_component.root_element,
 | |
|             0,
 | |
|             initial_state,
 | |
|             &sub_compo_state,
 | |
|         );
 | |
|         build_item_tree::<T>(sub_component, &sub_compo_state, builder);
 | |
|     } else {
 | |
|         let mut repeater_count = 0;
 | |
|         visit_item(initial_state, &root_component.root_element, 1, &mut repeater_count, 0, builder);
 | |
| 
 | |
|         visit_children(
 | |
|             initial_state,
 | |
|             &root_component.root_element.borrow().children,
 | |
|             root_component,
 | |
|             &root_component.root_element,
 | |
|             0,
 | |
|             0,
 | |
|             1,
 | |
|             1,
 | |
|             &mut repeater_count,
 | |
|             builder,
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     // Size of the element's children and grand-children including
 | |
|     // sub-component children, needed to allocate the correct amount of
 | |
|     // index spaces for sub-components.
 | |
|     fn item_sub_tree_size(e: &ElementRc) -> usize {
 | |
|         let mut count = e.borrow().children.len();
 | |
|         if let Some(sub_component) = e.borrow().sub_component() {
 | |
|             count += item_sub_tree_size(&sub_component.root_element);
 | |
|         }
 | |
|         for i in &e.borrow().children {
 | |
|             count += item_sub_tree_size(i);
 | |
|         }
 | |
|         count
 | |
|     }
 | |
| 
 | |
|     fn visit_children<T: ItemTreeBuilder>(
 | |
|         state: &T::SubComponentState,
 | |
|         children: &[ElementRc],
 | |
|         _component: &Rc<Component>,
 | |
|         parent_item: &ElementRc,
 | |
|         parent_index: u32,
 | |
|         relative_parent_index: u32,
 | |
|         children_offset: u32,
 | |
|         relative_children_offset: u32,
 | |
|         repeater_count: &mut u32,
 | |
|         builder: &mut T,
 | |
|     ) {
 | |
|         debug_assert_eq!(
 | |
|             relative_parent_index,
 | |
|             *parent_item.borrow().item_index.get().unwrap_or(&parent_index)
 | |
|         );
 | |
| 
 | |
|         // Suppose we have this:
 | |
|         // ```
 | |
|         // Button := Rectangle { /* some repeater here*/ }
 | |
|         // StandardButton := Button { /* no children */ }
 | |
|         // App := Dialog { StandardButton { /* no children */ }}
 | |
|         // ```
 | |
|         // The inlining pass ensures that *if* `StandardButton` had children, `Button` would be inlined, but that's not the case here.
 | |
|         //
 | |
|         // We are in the stage of visiting the Dialog's children and we'll end up visiting the Button's Rectangle because visit_item()
 | |
|         // on the StandardButton - a Dialog's child - follows all the way to the Rectangle as native item. We've also determine that
 | |
|         // StandardButton is a sub-component and we'll call visit_children() on it. Now we are here. However as `StandardButton` has no children,
 | |
|         // and therefore we would never recurse into `Button`'s children and thus miss the repeater. That is what this condition attempts to
 | |
|         // detect and chain the children visitation.
 | |
|         if children.is_empty() {
 | |
|             if let Some(nested_subcomponent) = parent_item.borrow().sub_component() {
 | |
|                 let sub_component_state = builder.enter_component(
 | |
|                     parent_item,
 | |
|                     nested_subcomponent,
 | |
|                     children_offset,
 | |
|                     state,
 | |
|                 );
 | |
|                 visit_children(
 | |
|                     &sub_component_state,
 | |
|                     &nested_subcomponent.root_element.borrow().children,
 | |
|                     nested_subcomponent,
 | |
|                     &nested_subcomponent.root_element,
 | |
|                     parent_index,
 | |
|                     relative_parent_index,
 | |
|                     children_offset,
 | |
|                     relative_children_offset,
 | |
|                     repeater_count,
 | |
|                     builder,
 | |
|                 );
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         let mut offset = children_offset + children.len() as u32;
 | |
| 
 | |
|         let mut sub_component_states = VecDeque::new();
 | |
| 
 | |
|         for child in children.iter() {
 | |
|             if let Some(sub_component) = child.borrow().sub_component() {
 | |
|                 let sub_component_state =
 | |
|                     builder.enter_component(child, sub_component, offset, state);
 | |
|                 visit_item(
 | |
|                     &sub_component_state,
 | |
|                     &sub_component.root_element,
 | |
|                     offset,
 | |
|                     repeater_count,
 | |
|                     parent_index,
 | |
|                     builder,
 | |
|                 );
 | |
|                 sub_component_states.push_back(sub_component_state);
 | |
|             } else {
 | |
|                 visit_item(state, child, offset, repeater_count, parent_index, builder);
 | |
|             }
 | |
|             offset += item_sub_tree_size(child) as u32;
 | |
|         }
 | |
| 
 | |
|         let mut offset = children_offset + children.len() as u32;
 | |
|         let mut relative_offset = relative_children_offset + children.len() as u32;
 | |
|         let mut index = children_offset;
 | |
|         let mut relative_index = relative_children_offset;
 | |
| 
 | |
|         for e in children.iter() {
 | |
|             if let Some(sub_component) = e.borrow().sub_component() {
 | |
|                 let sub_tree_state = sub_component_states.pop_front().unwrap();
 | |
|                 builder.enter_component_children(e, *repeater_count, state, &sub_tree_state);
 | |
|                 visit_children(
 | |
|                     &sub_tree_state,
 | |
|                     &sub_component.root_element.borrow().children,
 | |
|                     sub_component,
 | |
|                     &sub_component.root_element,
 | |
|                     index,
 | |
|                     0,
 | |
|                     offset,
 | |
|                     1,
 | |
|                     repeater_count,
 | |
|                     builder,
 | |
|                 );
 | |
|             } else {
 | |
|                 visit_children(
 | |
|                     state,
 | |
|                     &e.borrow().children,
 | |
|                     _component,
 | |
|                     e,
 | |
|                     index,
 | |
|                     relative_index,
 | |
|                     offset,
 | |
|                     relative_offset,
 | |
|                     repeater_count,
 | |
|                     builder,
 | |
|                 );
 | |
|             }
 | |
| 
 | |
|             index += 1;
 | |
|             relative_index += 1;
 | |
|             let size = item_sub_tree_size(e) as u32;
 | |
|             offset += size;
 | |
|             relative_offset += size;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     fn visit_item<T: ItemTreeBuilder>(
 | |
|         component_state: &T::SubComponentState,
 | |
|         item: &ElementRc,
 | |
|         children_offset: u32,
 | |
|         repeater_count: &mut u32,
 | |
|         parent_index: u32,
 | |
|         builder: &mut T,
 | |
|     ) {
 | |
|         if item.borrow().repeated.is_some() {
 | |
|             builder.push_repeated_item(item, *repeater_count, parent_index, component_state);
 | |
|             *repeater_count += 1;
 | |
|         } else {
 | |
|             let mut item = item.clone();
 | |
|             let mut component_state = component_state.clone();
 | |
|             while let Some((base, state)) = {
 | |
|                 let base = item.borrow().sub_component().map(|c| {
 | |
|                     (
 | |
|                         c.root_element.clone(),
 | |
|                         builder.enter_component(&item, c, children_offset, &component_state),
 | |
|                     )
 | |
|                 });
 | |
|                 base
 | |
|             } {
 | |
|                 item = base;
 | |
|                 component_state = state;
 | |
|             }
 | |
|             builder.push_native_item(&item, children_offset, parent_index, &component_state)
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// Will call the `handle_property` callback for every property that needs to be initialized.
 | |
| /// This function makes sure to call them in order so that if constant binding need to access
 | |
| /// constant properties, these are already initialized
 | |
| pub fn handle_property_bindings_init(
 | |
|     component: &Rc<Component>,
 | |
|     mut handle_property: impl FnMut(&ElementRc, &SmolStr, &BindingExpression),
 | |
| ) {
 | |
|     fn handle_property_inner(
 | |
|         component: &Weak<Component>,
 | |
|         elem: &ElementRc,
 | |
|         prop_name: &SmolStr,
 | |
|         binding_expression: &BindingExpression,
 | |
|         handle_property: &mut impl FnMut(&ElementRc, &SmolStr, &BindingExpression),
 | |
|         processed: &mut HashSet<NamedReference>,
 | |
|     ) {
 | |
|         if elem.borrow().is_component_placeholder {
 | |
|             return; // This element does not really exist!
 | |
|         }
 | |
|         let nr = NamedReference::new(elem, prop_name.clone());
 | |
|         if processed.contains(&nr) {
 | |
|             return;
 | |
|         }
 | |
|         processed.insert(nr);
 | |
|         if binding_expression.analysis.as_ref().is_some_and(|a| a.is_const) {
 | |
|             // We must first handle all dependent properties in case it is a constant property
 | |
| 
 | |
|             binding_expression.expression.visit_recursive(&mut |e| {
 | |
|                 if let Expression::PropertyReference(nr) = e {
 | |
|                     let elem = nr.element();
 | |
|                     if Weak::ptr_eq(&elem.borrow().enclosing_component, component) {
 | |
|                         if let Some(be) = elem.borrow().bindings.get(nr.name()) {
 | |
|                             handle_property_inner(
 | |
|                                 component,
 | |
|                                 &elem,
 | |
|                                 nr.name(),
 | |
|                                 &be.borrow(),
 | |
|                                 handle_property,
 | |
|                                 processed,
 | |
|                             );
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|             })
 | |
|         }
 | |
|         handle_property(elem, prop_name, binding_expression);
 | |
|     }
 | |
| 
 | |
|     let mut processed = HashSet::new();
 | |
|     crate::object_tree::recurse_elem(&component.root_element, &(), &mut |elem: &ElementRc, ()| {
 | |
|         for (prop_name, binding_expression) in &elem.borrow().bindings {
 | |
|             handle_property_inner(
 | |
|                 &Rc::downgrade(component),
 | |
|                 elem,
 | |
|                 prop_name,
 | |
|                 &binding_expression.borrow(),
 | |
|                 &mut handle_property,
 | |
|                 &mut processed,
 | |
|             );
 | |
|         }
 | |
|     });
 | |
| }
 | |
| 
 | |
| /// Call the given function for each constant property in the Component so one can set
 | |
| /// `set_constant` on it.
 | |
| pub fn for_each_const_properties(
 | |
|     component: &Rc<Component>,
 | |
|     mut f: impl FnMut(&ElementRc, &SmolStr),
 | |
| ) {
 | |
|     crate::object_tree::recurse_elem(&component.root_element, &(), &mut |elem: &ElementRc, ()| {
 | |
|         if elem.borrow().repeated.is_some() {
 | |
|             return;
 | |
|         }
 | |
|         let mut e = elem.clone();
 | |
|         let mut all_prop = BTreeSet::new();
 | |
|         loop {
 | |
|             all_prop.extend(
 | |
|                 e.borrow()
 | |
|                     .property_declarations
 | |
|                     .iter()
 | |
|                     .filter(|(_, x)| {
 | |
|                         x.property_type.is_property_type() &&
 | |
|                             !matches!( &x.property_type, crate::langtype::Type::Struct(s) if s.name.as_ref().is_some_and(|name| name.ends_with("::StateInfo")))
 | |
|                     })
 | |
|                     .map(|(k, _)| k.clone()),
 | |
|             );
 | |
|             match &e.clone().borrow().base_type {
 | |
|                 ElementType::Component(c) => {
 | |
|                     e = c.root_element.clone();
 | |
|                 }
 | |
|                 ElementType::Native(n) => {
 | |
|                     let mut n = n;
 | |
|                     loop {
 | |
|                         all_prop.extend(
 | |
|                             n.properties
 | |
|                                 .iter()
 | |
|                                 .filter(|(k, x)| {
 | |
|                                     x.ty.is_property_type()
 | |
|                                         && !k.starts_with("viewport-")
 | |
|                                         && k.as_str() != "commands"
 | |
|                                 })
 | |
|                                 .map(|(k, _)| k.clone()),
 | |
|                         );
 | |
|                         match n.parent.as_ref() {
 | |
|                             Some(p) => n = p,
 | |
|                             None => break,
 | |
|                         }
 | |
|                     }
 | |
|                     break;
 | |
|                 }
 | |
|                 ElementType::Builtin(_) => {
 | |
|                     unreachable!("builtin element should have been resolved")
 | |
|                 }
 | |
|                 ElementType::Global | ElementType::Error => break,
 | |
|             }
 | |
|         }
 | |
|         for c in all_prop {
 | |
|             if NamedReference::new(elem, c.clone()).is_constant() {
 | |
|                 f(elem, &c);
 | |
|             }
 | |
|         }
 | |
|     });
 | |
| }
 | |
| 
 | |
| /// Convert a ascii kebab string to pascal case
 | |
| pub fn to_pascal_case(str: &str) -> String {
 | |
|     let mut result = Vec::with_capacity(str.len());
 | |
|     let mut next_upper = true;
 | |
|     for x in str.as_bytes() {
 | |
|         if *x == b'-' {
 | |
|             next_upper = true;
 | |
|         } else if next_upper {
 | |
|             result.push(x.to_ascii_uppercase());
 | |
|             next_upper = false;
 | |
|         } else {
 | |
|             result.push(*x);
 | |
|         }
 | |
|     }
 | |
|     String::from_utf8(result).unwrap()
 | |
| }
 | |
| 
 | |
| /// Convert a ascii pascal case string to kebab case
 | |
| pub fn to_kebab_case(str: &str) -> String {
 | |
|     let mut result = Vec::with_capacity(str.len());
 | |
|     for x in str.as_bytes() {
 | |
|         if x.is_ascii_uppercase() {
 | |
|             if !result.is_empty() {
 | |
|                 result.push(b'-');
 | |
|             }
 | |
|             result.push(x.to_ascii_lowercase());
 | |
|         } else {
 | |
|             result.push(*x);
 | |
|         }
 | |
|     }
 | |
|     String::from_utf8(result).unwrap()
 | |
| }
 | |
| 
 | |
| #[test]
 | |
| fn case_conversions() {
 | |
|     assert_eq!(to_kebab_case("HelloWorld"), "hello-world");
 | |
|     assert_eq!(to_pascal_case("hello-world"), "HelloWorld");
 | |
| }
 |