diff --git a/Cargo.lock b/Cargo.lock index 96f06f7..330b36a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4554,6 +4554,7 @@ dependencies = [ "katex-rs", "lazy_static", "pulldown-cmark", + "regex", "syntect", "web-sys", ] diff --git a/README.md b/README.md index 6004238..4da9332 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,30 @@ see [here](https://rambip.github.io/rust-web-markdown/onclick) ## Custom Components see [here](https://rambip.github.io/rust-web-markdown/custom-components) +Custom components allow you to embed interactive or custom-styled elements in your markdown. + +### Custom Component Naming Rules + +To be recognized as a custom component, tag names must follow these rules: + +1. **Uppercase start** - Tags starting with an uppercase letter (A-Z) are always treated as custom components + - Examples: ``, ``, `` + +2. **Lowercase with dash** - Tags starting with lowercase (a-z) must contain at least one dash (-) + - Examples: ``, ``, `` + +These rules ensure standard HTML tags like `
`, ``, and `

` are not confused with custom components. + +**Valid custom components:** +- `` ✓ (uppercase start) +- `` ✓ (lowercase start with dash) +- `` ✓ (uppercase, self-closing with attributes) + +**NOT custom components:** +- `

` ✗ (lowercase without dash - standard HTML) +- `` ✗ (lowercase without dash - standard HTML) +- `

` ✗ (lowercase without dash - standard HTML) + # Contribute PRs are **very much** appreciated. diff --git a/dioxus-markdown/README.md b/dioxus-markdown/README.md index d959107..422923a 100644 --- a/dioxus-markdown/README.md +++ b/dioxus-markdown/README.md @@ -28,6 +28,24 @@ You just need trunk and a web-browser to test them. The Yew version of these examples can run in the browser from the links in [the top level ReadMe](../README.md). +## Custom Components + +Custom components allow you to embed interactive Dioxus components in your markdown. + +### Custom Component Naming Rules + +To be recognized as a custom component, tag names must follow these rules: + +1. **Uppercase start** - Tags starting with an uppercase letter (A-Z) are always treated as custom components + - Examples: ``, ``, `` + +2. **Lowercase with dash** - Tags starting with lowercase (a-z) must contain at least one dash (-) + - Examples: ``, ``, `` + +These rules ensure standard HTML tags like `

`, ``, and `

` are not confused with custom components. + +See the [custom-components example](./examples/custom-components) for a complete working example. + # Changelog ## 0.1.0 diff --git a/dioxus-markdown/examples/custom-components/src/main.rs b/dioxus-markdown/examples/custom-components/src/main.rs index fcfbc23..a9a2dcb 100644 --- a/dioxus-markdown/examples/custom-components/src/main.rs +++ b/dioxus-markdown/examples/custom-components/src/main.rs @@ -17,11 +17,11 @@ A counter which modifies the document: ## Here is a Box: - + **I am in a blue box !** - + "#; /// A counter who's current count is not stored in the document. @@ -80,7 +80,7 @@ fn App() -> Element { }) }); - components.register("box", |props| { + components.register("custom-box", |props| { let children = props.children; Ok(rsx! { ColorBox { children } diff --git a/leptos-markdown/README.md b/leptos-markdown/README.md index 2a83e51..2ac1eb4 100644 --- a/leptos-markdown/README.md +++ b/leptos-markdown/README.md @@ -71,6 +71,22 @@ Try it [here](https://rambip.github.io/rust-web-markdown-markdown/onclick) This feature is still very experimental. But there is an example [here](https://rambip.github.io/rust-web-markdown-markdown/custom_component) +Custom components allow you to embed interactive Leptos components in your markdown. + +### Custom Component Naming Rules + +To be recognized as a custom component, tag names must follow these rules: + +1. **Uppercase start** - Tags starting with an uppercase letter (A-Z) are always treated as custom components + - Examples: ``, ``, `` + +2. **Lowercase with dash** - Tags starting with lowercase (a-z) must contain at least one dash (-) + - Examples: ``, ``, `` + +These rules ensure standard HTML tags like `

`, ``, and `

` are not confused with custom components. + +See the [custom-component example](./examples/custom-component) for a complete working example. + # Changelog ## 0.7.0 diff --git a/leptos-markdown/examples/custom-component/src/main.rs b/leptos-markdown/examples/custom-component/src/main.rs index 27dcf89..1d0dc82 100644 --- a/leptos-markdown/examples/custom-component/src/main.rs +++ b/leptos-markdown/examples/custom-component/src/main.rs @@ -50,11 +50,11 @@ static MARKDOWN_SOURCE: &str = r#" ## Here is a Box: - + **I am in a blue box !** - + "#; #[component] @@ -67,7 +67,7 @@ fn App() -> impl IntoView { }) }); - components.register("box", |props| { + components.register("custom-box", |props| { Ok(view! { {props.children} }) diff --git a/web-markdown/Cargo.toml b/web-markdown/Cargo.toml index 3ef993d..a005da3 100644 --- a/web-markdown/Cargo.toml +++ b/web-markdown/Cargo.toml @@ -16,6 +16,7 @@ syntect = { version = "5.0.0", default-features = false, features = [ ] } lazy_static = "1.4.0" pulldown-cmark = "0.13.0" +regex = "1.12" [target.'cfg(target_arch = "wasm32")'.dependencies] katex-rs = { version = "0.2", optional = true } diff --git a/web-markdown/src/component.rs b/web-markdown/src/component.rs index ace9d62..c3491c4 100644 --- a/web-markdown/src/component.rs +++ b/web-markdown/src/component.rs @@ -1,7 +1,44 @@ use std::collections::BTreeMap; -/// A custom non-native html element -/// defined inside markdown. +/// A custom non-native html element defined inside markdown. +/// +/// ## Custom Component Naming Rules +/// +/// Custom components are identified by their tag names, which must follow specific rules +/// to distinguish them from standard HTML tags: +/// +/// ### Valid Custom Component Names +/// +/// A tag name is considered a custom component if it meets any of these criteria: +/// +/// 1. **Starts with an uppercase letter (A-Z)** +/// - Examples: ``, ``, `` +/// - No dash required for uppercase names +/// +/// 2. **Starts with a lowercase letter (a-z) and contains at least one dash (-)** +/// - Examples: ``, ``, `` +/// - The dash distinguishes these from standard HTML tags +/// +/// This ensures that standard HTML tags (like `

`, ``, `

`) are never confused +/// with custom components. Custom component tags support start tags, end tags, self-closing +/// tags, and tags with attributes (e.g., ``). +/// +/// ### Examples +/// +/// ```markdown +/// +/// +/// +/// content +/// +/// **Bold text inside custom component** +/// +/// +/// +///

text
+/// text +///

paragraph

+/// ``` #[derive(Debug, PartialEq)] pub struct ComponentCall<'a> { /// Where in the larger document full_string starts. diff --git a/web-markdown/src/render.rs b/web-markdown/src/render.rs index 29760a8..b73e88e 100644 --- a/web-markdown/src/render.rs +++ b/web-markdown/src/render.rs @@ -189,21 +189,60 @@ where current_component: Option, } -/// Returns true if `raw_html`: -/// - starts with '<' -/// - ends with '>' -/// - does not have any '<' or '>' in between. +/// Returns true if `raw_html` appears to be a custom component tag. /// -/// TODO: -/// An string attribute can a ">" character. +/// A valid custom component tag must: +/// - Start with '<' +/// - End with '>' +/// - Have a tag name that either: +/// - Starts with an uppercase letter (A-Z), OR +/// - Starts with a lowercase letter (a-z) and contains at least one dash (-) +/// +/// This validation prevents standard HTML tags like `
`, ``, `

` from being +/// treated as custom components while allowing custom component names like: +/// - `` (uppercase start, no dash needed) +/// - `` (lowercase start, has dash) +/// - `` (uppercase start, has dash) +/// +/// The function also handles: +/// - Self-closing tags: `` +/// - Tags with attributes: `` +/// - Closing tags: `` +/// +/// Invalid HTML like `` is rejected because "and" is not valid attribute syntax. fn can_be_custom_component(raw_html: &str) -> bool { - let chars: Vec<_> = raw_html.trim().chars().collect(); - let len = chars.len(); - if len < 3 { - return false; - }; - let (fst, middle, last) = (chars[0], &chars[1..len - 1], chars[len - 1]); - fst == '<' && last == '>' && middle.iter().all(|c| c != &'<' && c != &'>') + lazy_static::lazy_static! { + // Regex patterns for custom component tags: + + // Simple tags: or or or + // ^< - starts with < + // /? - optional / for closing tags + // ([A-Z][A-Za-z0-9-]* - uppercase start with optional alphanumeric and dashes + // |[a-z][A-Za-z0-9]*-[A-Za-z0-9-]*) - OR lowercase start with at least one dash + // >$ - ends with > + static ref SIMPLE_TAG_RE: regex::Regex = regex::Regex::new( + r"^$" + ).unwrap(); + + // Self-closing tags: or + static ref SELF_CLOSING_RE: regex::Regex = regex::Regex::new( + r"^<([A-Z][A-Za-z0-9-]*|[a-z][A-Za-z0-9]*-[A-Za-z0-9-]*)/\s*>$" + ).unwrap(); + + // Tags with attributes: or + // After the tag name, we must have whitespace followed by content that contains '=' + // This rejects things like "" where there's no '=' + // Note: The regex is non-greedy and will match the smallest possible string, + // so escaped characters like < or > in attributes are allowed + static ref WITH_ATTRS_RE: regex::Regex = regex::Regex::new( + r"^$" + ).unwrap(); + } + + let s = raw_html.trim(); + + // Try to match with regex patterns + SIMPLE_TAG_RE.is_match(s) || SELF_CLOSING_RE.is_match(s) || WITH_ATTRS_RE.is_match(s) } impl<'a, 'callback, 'c, I, F> Iterator for Renderer<'a, 'callback, 'c, I, F> @@ -348,7 +387,7 @@ where }) } else { Some(match item { - Event::InlineHtml(ref x) => RenderEvent { + Event::InlineHtml(ref x) if can_be_custom_component(x) => RenderEvent { // FIXME: avoid clone custom_tag: Some(x.clone()), event: item, @@ -585,3 +624,100 @@ where }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_can_be_custom_component_uppercase_start() { + // Uppercase start should always be valid + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + } + + #[test] + fn test_can_be_custom_component_lowercase_with_dash() { + // Lowercase start with dash should be valid + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + assert!(can_be_custom_component("")); + } + + #[test] + fn test_can_be_custom_component_lowercase_no_dash() { + // Lowercase start without dash should be invalid (standard HTML tags) + assert!(!can_be_custom_component("

")); + assert!(!can_be_custom_component("")); + assert!(!can_be_custom_component("

")); + assert!(!can_be_custom_component("

")); + assert!(!can_be_custom_component("
")); + assert!(!can_be_custom_component("
")); + assert!(!can_be_custom_component("