Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bugs and refactor #9

Merged
merged 13 commits into from
Mar 31, 2023
15 changes: 15 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Using template from: https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings

# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto

# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.rs text eol=lf

# Declare files that will always have CRLF line endings on checkout.
# *.sln text eol=crlf

# Denote all files that are truly binary and should not be modified.
# *.png binary
# *.jpg binary
44 changes: 44 additions & 0 deletions build/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ time = "0.3"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
url = { version = "2.3", features = ["serde"] }
url = { version = "2", features = ["serde"] }
xml-rs = "0.8"

[dev-dependencies]
pretty_assertions = "1"
File renamed without changes.
137 changes: 114 additions & 23 deletions build/src/icon.rs → build/src/icon/mod.rs
Original file line number Diff line number Diff line change
@@ -1,44 +1,115 @@
use std::{fmt::Display, str::FromStr};
use std::{fmt::Display, path::Path, str::FromStr};

use anyhow::Result;
use heck::ToPascalCase;
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use xml::attribute::OwnedAttribute;

use crate::{feature::Feature, leptos::LeptosComponent, package::PackageType, svg::ParsedSvg};
use crate::{
feature::Feature,
leptos::LeptosComponent,
package::{Downloaded, Package, PackageType},
};

use self::svg::ParsedSvg;

mod svg;

#[derive(Debug)]
pub(crate) struct SvgIcon {
pub svg: ParsedSvg,
pub svg: svg::ParsedSvg,
pub categories: Vec<Category>,
pub component_name: String,
pub feature: Feature,
}

impl SvgIcon {
pub async fn new(
package: &Package<Downloaded>,
path: &Path,
size: Option<IconSize>,
mut categories: Vec<Category>,
) -> Result<Self> {
let file_stem = path.file_stem().unwrap().to_string_lossy(); // TODO: Error handling\

let (raw_name, size_from_name, cats_from_name) =
parse_raw_icon_name(package.ty, &file_stem);

if let Some(mut cats_from_name) = cats_from_name {
categories.append(&mut cats_from_name);
}

let feature = Feature {
name: feature_name(
raw_name,
size_from_name.or(size),
&categories,
&package.meta.short_name,
),
};

let svg = tokio::fs::read_to_string(path).await?;

Ok(SvgIcon {
svg: ParsedSvg::parse(svg.as_bytes())?,
categories,
feature,
})
}

/// This creates the Rust code for a leptos component representing a single icon.
/// Feature-gated by the given feature name.
///
/// TODO: Once https://github.com/leptos-rs/leptos/pull/748 is merged, use `::leptos::...` wherever possible and remove `use leptos::*` in main.rs.
pub(crate) fn create_leptos_icon_component(&self) -> Result<LeptosComponent> {
let feature_name: &str = &self.feature.name;
let component_name: &str = &self.component_name;
let component_name: &str = &self.feature.name;

let doc_comment = format!("This icon requires the feature `{feature_name}` to be enabled.");
let component_ident = Ident::new(component_name, Span::call_site());
let svg_attributes = attributes_token_stream(&self.svg.attributes)?;
let svg_content = &self.svg.content;
let svg_content: TokenStream =
self.svg.content.parse().map_err(|err| {
anyhow::anyhow!("Error parsing svg content into TokenStream: {err}")
})?;

let x_attribute = attribute_token_stream(&self.svg.svg_attributes.x)?;
let y_attribute = attribute_token_stream(&self.svg.svg_attributes.y)?;
let view_box_attribute = attribute_token_stream(&self.svg.svg_attributes.view_box)?;
let stroke_linecap_attribute =
attribute_token_stream(&self.svg.svg_attributes.stroke_linecap)?;
let stroke_linejoin_attribute =
attribute_token_stream(&self.svg.svg_attributes.stroke_linejoin)?;
let stroke_width_attribute = attribute_token_stream(&self.svg.svg_attributes.stroke_width)?;
// We are fine is stroke is not set for the svg.
let stroke_attribute = attribute_token_stream(&self.svg.svg_attributes.stroke)?;
// Fill should most likely always default to use the "currentColor".
let fill_attribute = attribute_token_stream_opt(&self.svg.svg_attributes.fill)?
.unwrap_or_else(|| quote!(fill = "currentColor"));
let style_attribute = self
.svg
.svg_attributes
.style
.clone()
.map(|it| it.value)
.unwrap_or_default();
// role="graphics-symbol" should be used for icons.
let role_attribute = attribute_token_stream_opt(&self.svg.svg_attributes.role)?
.unwrap_or_else(|| quote!(role = "graphics-symbol"));

let style_format_string = format!("{style_attribute} {{}}");

let tokens = quote! {
#[cfg(feature = #feature_name)]
#[doc = #doc_comment]
#[component]
pub fn #component_ident(
cx: Scope,
/// The size of the icon (The side length of the square surrounding the icon). Defaults to "1em".
/// The width of the icon (horizontal side length of the square surrounding the icon). Defaults to "1em".
#[prop(into, optional, default = String::from("1em"))]
width: String,
/// The height of the icon (vertical side length of the square surrounding the icon). Defaults to "1em".
#[prop(into, optional, default = String::from("1em"))]
size: String,
height: String,
/// HTML class attribute.
#[prop(into, optional)]
class: String,
Expand All @@ -48,24 +119,30 @@ impl SvgIcon {
/// HTML style attribute.
#[prop(into, optional)]
style: String,
/// Accessibility title.
#[prop(into, optional)]
/// ARIA accessibility title.
#[prop(into, optional, default = String::from(#component_name))]
title: String,
) -> impl IntoView {
view! { cx,
// As of https://stackoverflow.com/questions/18467982/are-svg-parameters-such-as-xmlns-and-version-needed, version and namespace information is not required for an inline-svg.
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
#svg_attributes
class=class
style=format!("color: {}; {}", color, style)
width=size.clone()
height=size
xmlns="http://www.w3.org/2000/svg"
inner_html=#svg_content
style=format!(#style_format_string, style)
#x_attribute
#y_attribute
width=width
height=height
#view_box_attribute
#stroke_linecap_attribute
#stroke_linejoin_attribute
#stroke_width_attribute
#stroke_attribute
#fill_attribute
#role_attribute
>
// Title should be the first child!
<title>{title}</title>
#svg_content
</svg>
}
}
Expand All @@ -76,7 +153,7 @@ impl SvgIcon {
}
}

pub(crate) struct IconMeta {
pub(crate) struct IconMetadata {
pub name: String, // Both the component and feature name!
pub categories: Vec<Category>,
}
Expand Down Expand Up @@ -211,8 +288,22 @@ pub(crate) fn parse_raw_icon_name(
}
}

fn attributes_token_stream(attributes: &[OwnedAttribute]) -> Result<TokenStream> {
attributes
fn attribute_token_stream_opt(attribute: &Option<OwnedAttribute>) -> Result<Option<TokenStream>> {
if let Some(attribute) = attribute {
let attribute_val = &attribute.value;
let attr_ident: TokenStream = attribute
.name
.local_name
.parse()
.map_err(|_| anyhow::anyhow!("could not convert attributes to token stream"))?;
Ok(Some(quote!(#attr_ident=#attribute_val)))
} else {
Ok(None)
}
}

fn attribute_token_stream(attribute: &Option<OwnedAttribute>) -> Result<TokenStream> {
attribute
.iter()
.map(|attribute| {
let attribute_val = &attribute.value;
Expand Down