From 6f75d784014e194db32e4172ae146d1a21fb9377 Mon Sep 17 00:00:00 2001 From: Vic Demuzere Date: Sat, 13 Jan 2024 13:47:15 +0100 Subject: [PATCH] Add support for a subset of SVG elements This enables us to embed SVG graphics in HTML documents. --- hypertext/Cargo.toml | 4 +- hypertext/src/attributes.rs | 109 +++++++++++++++ hypertext/src/html_elements.rs | 240 +++++++++++++++++++++++++++++++++ hypertext/src/lib.rs | 3 + hypertext/tests/main.rs | 31 +++++ 5 files changed, 386 insertions(+), 1 deletion(-) diff --git a/hypertext/Cargo.toml b/hypertext/Cargo.toml index 92532b6..44e048f 100644 --- a/hypertext/Cargo.toml +++ b/hypertext/Cargo.toml @@ -32,8 +32,10 @@ axum-core = { version = "0.4", optional = true } http = { version = "1", optional = true } [features] -default = ["alloc"] +default = ["alloc", "svg"] alloc = ["dep:html-escape", "dep:itoa", "dep:ryu"] axum = ["alloc", "dep:axum-core", "dep:http"] + +svg = [] diff --git a/hypertext/src/attributes.rs b/hypertext/src/attributes.rs index 0add484..2ef6c51 100644 --- a/hypertext/src/attributes.rs +++ b/hypertext/src/attributes.rs @@ -120,3 +120,112 @@ pub trait GlobalAttributes { /// Whether the element is to be translated when the page is localized. const translate: Attribute = Attribute; } + +/// Global SVG attributes. +/// +/// This trait must be in scope to use well-known attributes such as +/// [`class`](Self::class) and [`id`](Self::id). This trait is implemented +/// by every SVG element specified in [`crate::svg_elements`]. +/// +#[cfg(feature = "svg")] +#[allow(non_upper_case_globals, clippy::module_name_repetitions)] +pub trait GlobalSVGAttributes { + /// The XML namespace, only required on the outermost SVG element. + const xmlns: Attribute = Attribute; + + /// The class of the element. + const class: Attribute = Attribute; + + /// A unique identifier for the element. + const id: Attribute = Attribute; + + /// The language of the element. + const lang: Attribute = Attribute; + + /// The CSS styling to apply to the element. + const style: Attribute = Attribute; + + /// Customize the index of the element for sequential focus navigation. + const tabindex: Attribute = Attribute; +} + +/// Presentation SVG attributes. +/// +/// This trait must be in scope to use presentation attributes such as +/// [`fill`](Self::fill) and [`stroke`](Self::stroke). +#[cfg(feature = "svg")] +#[allow(non_upper_case_globals, clippy::module_name_repetitions)] +pub trait PresentationSVGAttributes { + + /// Defines or associates a clipping path with the element. + const clip_path: Attribute = Attribute; + + /// Works as the fill-rule attribute, but applies to clipPath defenitions. + /// Only applies to graphics elements contained within a clipPath element. + const clip_rule: Attribute = Attribute; + + /// Provides a potential indirect value for the fill, stroke, stop-color, flood-color and + /// lighting-color attributes. + const color: Attribute = Attribute; + + /// Specifies color space for gradient interpolations, color animations and alpha compositing. + const color_interpolation: Attribute = Attribute; + + /// Lets you control the rendering of graphical or container elements. + const display: Attribute = Attribute; + + /// Defines the color for shapes or text, or the final state of an animation. + const fill: Attribute = Attribute; + + /// Defines the opacity of the paint server (color, gradient, pattern, ..) of a shape. + const fill_opacity: Attribute = Attribute; + + /// Defines the algorithm to use to determine the *inside* part of a shape. + const fill_rule: Attribute = Attribute; + + /// Specifies the filter effects defined by the filter element that shall be applied.. + const filter: Attribute = Attribute; + + /// Mainly used to bind a given mask element with the element the attribute belongs to. + const mask: Attribute = Attribute; + + /// Specifies the transparency of an object or a group of objects. + const opacity: Attribute = Attribute; + + /// Provides hints to the renderer about what tradeoffs to make when rendering shapes. + const shape_rendering: Attribute = Attribute; + + /// Defines the color, gradient or pattern used to paint the outline of the shape. + const stroke: Attribute = Attribute; + + /// Defines the pattern of dashes and gaps used to paint the outline of the shape. + const stroke_dasharray: Attribute = Attribute; + + /// Defines the offset on the rendering of a dash array. + const stroke_dashoffset: Attribute = Attribute; + + /// Defines the shape to be used at the end of open subpaths. + const stroke_linecap: Attribute = Attribute; + + /// Defines the shape to be used at the corners of paths when they are stroked. + const stroke_linejoin: Attribute = Attribute; + + /// Defines the limit on the ratio of the miter length to the stroke-width used to draw a miter + /// join. When the limit is exceeded, the join is converted from a miter to a bevel. + const stroke_miterlimit: Attribute = Attribute; + + /// Defines the opacity of the paint server (color, gradient, pattern, ..) of a shape. + const stroke_opacity: Attribute = Attribute; + + /// Defines the width of the stroke to be applied to the shape. + const stroke_width: Attribute = Attribute; + + /// Defines a list of transform definitions that are applied to an element and its children. + const transform: Attribute = Attribute; + + /// Specifies the vector effect to use when drawing an object. + const vector_effect: Attribute = Attribute; + + /// Lets you control the visibility of graphical elements. + const visibility: Attribute = Attribute; +} diff --git a/hypertext/src/html_elements.rs b/hypertext/src/html_elements.rs index 529a3ad..b30c946 100644 --- a/hypertext/src/html_elements.rs +++ b/hypertext/src/html_elements.rs @@ -1217,3 +1217,243 @@ macro_rules! void { void! { area base br col embed hr img input link meta source track wbr } + +#[cfg(feature = "svg")] +macro_rules! svg_elements { + { + $( + $(#[$element_meta:meta])* + $element:ident $( + { + $( + $(#[$attr_meta:meta])* + $attr:ident + )* + } + )? + )* + } => { + $( + $(#[$element_meta])* + #[allow(non_camel_case_types)] + #[allow(missing_docs)] // TODO + #[derive(Debug, Clone, Copy)] + pub struct $element; + + impl $element { + $( + $( + $(#[$attr_meta])* + #[allow(non_upper_case_globals)] + #[allow(missing_docs)] // TODO + pub const $attr: crate::attributes::Attribute = crate::attributes::Attribute; + )* + )? + } + + impl crate::attributes::GlobalSVGAttributes for $element {} + )* + } +} + +#[cfg(feature = "svg")] +svg_elements! { + circle { + cx + cy + r + pathLength + } + + defs + + desc + + ellipse { + cx + cy + rx + ry + pathLength + } + + g + + image { + x + y + width + height + href + preserveAspectRatio + crossorigin + decoding + } + + line { + x1 + x2 + y1 + y2 + pathLength + } + + linearGradient { + gradientUnits + gradientTransform + href + spreadMethod + x1 + y1 + y2 + } + + mask { + width + height + maskContentUnits + maskUnits + x + y + } + + metadata + + path { + d + pathLength + } + + pattern { + width + height + href + patternContentUnits + patternTransform + patternUnits + preserveAspectRatio + viewBox + x + y + } + + polygon { + points + pathLength + } + + polyline { + points + pathLength + } + + radialGradient { + cx + cy + fr + fx + fy + gradientUnits + gradientTransform + href + r + spreadMethod + } + + rect { + x + y + width + height + rx + ry + pathLength + } + + stop { + offset + stop_color + stop_opacity + } + + svg { + baseProfile + preserveAspectRatio + version + viewBox + width + height + x + y + } + + symbol { + width + height + preserveAspectRatio + refX + refY + viewBox + x + y + } + + text { + x + y + dx + dy + rotate + lengthAdjust + textLength + } + + textPath { + href + lengthAdjust + method + spacing + startOffset + textLength + } + + tspan { + x + y + dx + dy + rotate + lengthAdjust + textLength + } + + r#use { + href + x + y + width + height + } + + view { + viewBox + preserveAspectRatio + } +} + +#[cfg(feature = "svg")] +void! { + circle ellipse image line path polygon polyline rect stop r#use view +} + +#[cfg(feature = "svg")] +macro_rules! presentation { + ($($el:ident)*) => { + $(impl crate::attributes::PresentationSVGAttributes for $el {})* + }; +} + +#[cfg(feature = "svg")] +presentation! { + a circle ellipse g image line path pattern polygon polyline rect stop symbol text textPath tspan r#use svg +} diff --git a/hypertext/src/lib.rs b/hypertext/src/lib.rs index 0c83629..97ff88d 100644 --- a/hypertext/src/lib.rs +++ b/hypertext/src/lib.rs @@ -108,6 +108,9 @@ pub mod html_elements; mod web; pub use attributes::{Attribute, GlobalAttributes}; + +#[cfg(feature = "svg")] +pub use attributes::{GlobalSVGAttributes, PresentationSVGAttributes}; /// Render static HTML using [`maud`] syntax. /// /// For details about the syntax, see [`maud!`]. diff --git a/hypertext/tests/main.rs b/hypertext/tests/main.rs index 38637fe..f06026b 100644 --- a/hypertext/tests/main.rs +++ b/hypertext/tests/main.rs @@ -40,3 +40,34 @@ fn readme() { assert_eq!(shopping_list_maud, shopping_list_rsx); } + +#[test] +#[cfg(feature = "svg")] +fn inline_svg() { + use hypertext::{html_elements, GlobalAttributes, GlobalSVGAttributes, PresentationSVGAttributes, Renderable}; + + // Icon from Bootstrap Icons + // https://icons.getbootstrap.com/icons/chat-dots/ + + let content_maud = hypertext::maud!{ + div #test { + svg .bi."bi-chat-dots" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" { + path d="M5 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0m4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0m3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2"; + path d="m2.165 15.803.02-.004c1.83-.363 2.948-.842 3.468-1.105A9 9 0 0 0 8 15c4.418 0 8-3.134 8-7s-3.582-7-8-7-8 3.134-8 7c0 1.76.743 3.37 1.97 4.6a10.4 10.4 0 0 1-.524 2.318l-.003.011a11 11 0 0 1-.244.637c-.079.186.074.394.273.362a22 22 0 0 0 .693-.125m.8-3.108a1 1 0 0 0-.287-.801C1.618 10.83 1 9.468 1 8c0-3.192 3.004-6 7-6s7 2.808 7 6-3.004 6-7 6a8 8 0 0 1-2.088-.272 1 1 0 0 0-.711.074c-.387.196-1.24.57-2.634.893a11 11 0 0 0 .398-2"; + } + } + } + .render(); + + let content_rsx = hypertext::rsx!{ +
+ + + + +
+ } + .render(); + + assert_eq!(content_maud, content_rsx); +}