Skip to content

Commit 4342fcf

Browse files
authored
Merge pull request #35 from rambip/copilot/fix-can-be-custom-component-function
Fix custom component tag validation to distinguish from standard HTML
2 parents 99e9c34 + 4e01130 commit 4342fcf

11 files changed

Lines changed: 277 additions & 25 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,30 @@ see [here](https://rambip.github.io/rust-web-markdown/onclick)
3434
## Custom Components
3535
see [here](https://rambip.github.io/rust-web-markdown/custom-components)
3636

37+
Custom components allow you to embed interactive or custom-styled elements in your markdown.
38+
39+
### Custom Component Naming Rules
40+
41+
To be recognized as a custom component, tag names must follow these rules:
42+
43+
1. **Uppercase start** - Tags starting with an uppercase letter (A-Z) are always treated as custom components
44+
- Examples: `<MyComponent>`, `<Counter>`, `<DataTable>`
45+
46+
2. **Lowercase with dash** - Tags starting with lowercase (a-z) must contain at least one dash (-)
47+
- Examples: `<my-component>`, `<data-table>`, `<custom-counter>`
48+
49+
These rules ensure standard HTML tags like `<div>`, `<span>`, and `<p>` are not confused with custom components.
50+
51+
**Valid custom components:**
52+
- `<MyComponent>` ✓ (uppercase start)
53+
- `<my-component>` ✓ (lowercase start with dash)
54+
- `<Counter initial="5"/>` ✓ (uppercase, self-closing with attributes)
55+
56+
**NOT custom components:**
57+
- `<div>` ✗ (lowercase without dash - standard HTML)
58+
- `<span>` ✗ (lowercase without dash - standard HTML)
59+
- `<p>` ✗ (lowercase without dash - standard HTML)
60+
3761
# Contribute
3862

3963
PRs are **very much** appreciated.

dioxus-markdown/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,24 @@ You just need trunk and a web-browser to test them.
2828

2929
The Yew version of these examples can run in the browser from the links in [the top level ReadMe](../README.md).
3030

31+
## Custom Components
32+
33+
Custom components allow you to embed interactive Dioxus components in your markdown.
34+
35+
### Custom Component Naming Rules
36+
37+
To be recognized as a custom component, tag names must follow these rules:
38+
39+
1. **Uppercase start** - Tags starting with an uppercase letter (A-Z) are always treated as custom components
40+
- Examples: `<MyComponent>`, `<Counter>`, `<DataTable>`
41+
42+
2. **Lowercase with dash** - Tags starting with lowercase (a-z) must contain at least one dash (-)
43+
- Examples: `<my-component>`, `<data-table>`, `<custom-counter>`
44+
45+
These rules ensure standard HTML tags like `<div>`, `<span>`, and `<p>` are not confused with custom components.
46+
47+
See the [custom-components example](./examples/custom-components) for a complete working example.
48+
3149
# Changelog
3250

3351
## 0.1.0

dioxus-markdown/examples/custom-components/src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ A counter which modifies the document:
1717
<PersistedCounter value="5"/>
1818
1919
## Here is a Box:
20-
<box>
20+
<custom-box>
2121
2222
**I am in a blue box !**
2323
24-
</box>
24+
</custom-box>
2525
"#;
2626

2727
/// A counter who's current count is not stored in the document.
@@ -80,7 +80,7 @@ fn App() -> Element {
8080
})
8181
});
8282

83-
components.register("box", |props| {
83+
components.register("custom-box", |props| {
8484
let children = props.children;
8585
Ok(rsx! {
8686
ColorBox { children }

leptos-markdown/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,22 @@ Try it [here](https://rambip.github.io/rust-web-markdown-markdown/onclick)
7171
This feature is still very experimental.
7272
But there is an example [here](https://rambip.github.io/rust-web-markdown-markdown/custom_component)
7373

74+
Custom components allow you to embed interactive Leptos components in your markdown.
75+
76+
### Custom Component Naming Rules
77+
78+
To be recognized as a custom component, tag names must follow these rules:
79+
80+
1. **Uppercase start** - Tags starting with an uppercase letter (A-Z) are always treated as custom components
81+
- Examples: `<MyComponent>`, `<Counter>`, `<DataTable>`
82+
83+
2. **Lowercase with dash** - Tags starting with lowercase (a-z) must contain at least one dash (-)
84+
- Examples: `<my-component>`, `<data-table>`, `<custom-counter>`
85+
86+
These rules ensure standard HTML tags like `<div>`, `<span>`, and `<p>` are not confused with custom components.
87+
88+
See the [custom-component example](./examples/custom-component) for a complete working example.
89+
7490
# Changelog
7591

7692
## 0.7.0

leptos-markdown/examples/custom-component/src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ static MARKDOWN_SOURCE: &str = r#"
5050
<Counter initial="a"/>
5151
5252
## Here is a Box:
53-
<box>
53+
<custom-box>
5454
5555
**I am in a blue box !**
5656
57-
</box>
57+
</custom-box>
5858
"#;
5959

6060
#[component]
@@ -67,7 +67,7 @@ fn App() -> impl IntoView {
6767
})
6868
});
6969

70-
components.register("box", |props| {
70+
components.register("custom-box", |props| {
7171
Ok(view! {
7272
<BlueBox>{props.children}</BlueBox>
7373
})

web-markdown/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ syntect = { version = "5.0.0", default-features = false, features = [
1616
] }
1717
lazy_static = "1.4.0"
1818
pulldown-cmark = "0.13.0"
19+
regex = "1.12"
1920

2021
[target.'cfg(target_arch = "wasm32")'.dependencies]
2122
katex-rs = { version = "0.2", optional = true }

web-markdown/src/component.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,44 @@
11
use std::collections::BTreeMap;
22

3-
/// A custom non-native html element
4-
/// defined inside markdown.
3+
/// A custom non-native html element defined inside markdown.
4+
///
5+
/// ## Custom Component Naming Rules
6+
///
7+
/// Custom components are identified by their tag names, which must follow specific rules
8+
/// to distinguish them from standard HTML tags:
9+
///
10+
/// ### Valid Custom Component Names
11+
///
12+
/// A tag name is considered a custom component if it meets any of these criteria:
13+
///
14+
/// 1. **Starts with an uppercase letter (A-Z)**
15+
/// - Examples: `<MyComponent>`, `<Counter>`, `<DataTable>`
16+
/// - No dash required for uppercase names
17+
///
18+
/// 2. **Starts with a lowercase letter (a-z) and contains at least one dash (-)**
19+
/// - Examples: `<my-component>`, `<data-table>`, `<custom-counter>`
20+
/// - The dash distinguishes these from standard HTML tags
21+
///
22+
/// This ensures that standard HTML tags (like `<div>`, `<span>`, `<p>`) are never confused
23+
/// with custom components. Custom component tags support start tags, end tags, self-closing
24+
/// tags, and tags with attributes (e.g., `<MyComponent key="value" name="test"/>`).
25+
///
26+
/// ### Examples
27+
///
28+
/// ```markdown
29+
/// <!-- Valid custom components -->
30+
/// <Counter initial="5"/>
31+
/// <my-widget/>
32+
/// <DataTable>content</DataTable>
33+
/// <custom-box>
34+
/// **Bold text inside custom component**
35+
/// </custom-box>
36+
///
37+
/// <!-- NOT custom components (standard HTML) -->
38+
/// <div>text</div>
39+
/// <span>text</span>
40+
/// <p>paragraph</p>
41+
/// ```
542
#[derive(Debug, PartialEq)]
643
pub struct ComponentCall<'a> {
744
/// Where in the larger document full_string starts.

web-markdown/src/render.rs

Lines changed: 150 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -189,21 +189,60 @@ where
189189
current_component: Option<String>,
190190
}
191191

192-
/// Returns true if `raw_html`:
193-
/// - starts with '<'
194-
/// - ends with '>'
195-
/// - does not have any '<' or '>' in between.
192+
/// Returns true if `raw_html` appears to be a custom component tag.
196193
///
197-
/// TODO:
198-
/// An string attribute can a ">" character.
194+
/// A valid custom component tag must:
195+
/// - Start with '<'
196+
/// - End with '>'
197+
/// - Have a tag name that either:
198+
/// - Starts with an uppercase letter (A-Z), OR
199+
/// - Starts with a lowercase letter (a-z) and contains at least one dash (-)
200+
///
201+
/// This validation prevents standard HTML tags like `<div>`, `<span>`, `<p>` from being
202+
/// treated as custom components while allowing custom component names like:
203+
/// - `<MyComponent>` (uppercase start, no dash needed)
204+
/// - `<my-component>` (lowercase start, has dash)
205+
/// - `<My-Component>` (uppercase start, has dash)
206+
///
207+
/// The function also handles:
208+
/// - Self-closing tags: `<My-Component/>`
209+
/// - Tags with attributes: `<My-Component attr="value">`
210+
/// - Closing tags: `</My-Component>`
211+
///
212+
/// Invalid HTML like `<Y and Y>` is rejected because "and" is not valid attribute syntax.
199213
fn can_be_custom_component(raw_html: &str) -> bool {
200-
let chars: Vec<_> = raw_html.trim().chars().collect();
201-
let len = chars.len();
202-
if len < 3 {
203-
return false;
204-
};
205-
let (fst, middle, last) = (chars[0], &chars[1..len - 1], chars[len - 1]);
206-
fst == '<' && last == '>' && middle.iter().all(|c| c != &'<' && c != &'>')
214+
lazy_static::lazy_static! {
215+
// Regex patterns for custom component tags:
216+
217+
// Simple tags: <MyComponent> or </MyComponent> or <my-component> or </my-component>
218+
// ^< - starts with <
219+
// /? - optional / for closing tags
220+
// ([A-Z][A-Za-z0-9-]* - uppercase start with optional alphanumeric and dashes
221+
// |[a-z][A-Za-z0-9]*-[A-Za-z0-9-]*) - OR lowercase start with at least one dash
222+
// >$ - ends with >
223+
static ref SIMPLE_TAG_RE: regex::Regex = regex::Regex::new(
224+
r"^</?([A-Z][A-Za-z0-9-]*|[a-z][A-Za-z0-9]*-[A-Za-z0-9-]*)>$"
225+
).unwrap();
226+
227+
// Self-closing tags: <MyComponent/> or <my-component/>
228+
static ref SELF_CLOSING_RE: regex::Regex = regex::Regex::new(
229+
r"^<([A-Z][A-Za-z0-9-]*|[a-z][A-Za-z0-9]*-[A-Za-z0-9-]*)/\s*>$"
230+
).unwrap();
231+
232+
// Tags with attributes: <MyComponent attr="value"> or <my-component attr="value"/>
233+
// After the tag name, we must have whitespace followed by content that contains '='
234+
// This rejects things like "<Y and Y>" where there's no '='
235+
// Note: The regex is non-greedy and will match the smallest possible string,
236+
// so escaped characters like &lt; or &gt; in attributes are allowed
237+
static ref WITH_ATTRS_RE: regex::Regex = regex::Regex::new(
238+
r"^</?([A-Z][A-Za-z0-9-]*|[a-z][A-Za-z0-9]*-[A-Za-z0-9-]*)\s+.*?=.*?/?\s*>$"
239+
).unwrap();
240+
}
241+
242+
let s = raw_html.trim();
243+
244+
// Try to match with regex patterns
245+
SIMPLE_TAG_RE.is_match(s) || SELF_CLOSING_RE.is_match(s) || WITH_ATTRS_RE.is_match(s)
207246
}
208247

209248
impl<'a, 'callback, 'c, I, F> Iterator for Renderer<'a, 'callback, 'c, I, F>
@@ -348,7 +387,7 @@ where
348387
})
349388
} else {
350389
Some(match item {
351-
Event::InlineHtml(ref x) => RenderEvent {
390+
Event::InlineHtml(ref x) if can_be_custom_component(x) => RenderEvent {
352391
// FIXME: avoid clone
353392
custom_tag: Some(x.clone()),
354393
event: item,
@@ -585,3 +624,100 @@ where
585624
})
586625
}
587626
}
627+
628+
#[cfg(test)]
629+
mod tests {
630+
use super::*;
631+
632+
#[test]
633+
fn test_can_be_custom_component_uppercase_start() {
634+
// Uppercase start should always be valid
635+
assert!(can_be_custom_component("<MyComponent>"));
636+
assert!(can_be_custom_component("<Counter>"));
637+
assert!(can_be_custom_component("<DataTable>"));
638+
assert!(can_be_custom_component("<MyComponent/>"));
639+
assert!(can_be_custom_component("</MyComponent>"));
640+
assert!(can_be_custom_component("<MyComponent attr=\"value\">"));
641+
assert!(can_be_custom_component("<My-Component>"));
642+
assert!(can_be_custom_component("<MY-COMPONENT>"));
643+
}
644+
645+
#[test]
646+
fn test_can_be_custom_component_lowercase_with_dash() {
647+
// Lowercase start with dash should be valid
648+
assert!(can_be_custom_component("<my-component>"));
649+
assert!(can_be_custom_component("<data-table>"));
650+
assert!(can_be_custom_component("<custom-counter>"));
651+
assert!(can_be_custom_component("<my-component/>"));
652+
assert!(can_be_custom_component("</my-component>"));
653+
assert!(can_be_custom_component("<my-component attr=\"value\">"));
654+
assert!(can_be_custom_component("<a-b>"));
655+
assert!(can_be_custom_component("<my-custom-widget>"));
656+
}
657+
658+
#[test]
659+
fn test_can_be_custom_component_lowercase_no_dash() {
660+
// Lowercase start without dash should be invalid (standard HTML tags)
661+
assert!(!can_be_custom_component("<div>"));
662+
assert!(!can_be_custom_component("<span>"));
663+
assert!(!can_be_custom_component("<p>"));
664+
assert!(!can_be_custom_component("<section>"));
665+
assert!(!can_be_custom_component("<article>"));
666+
assert!(!can_be_custom_component("<header>"));
667+
assert!(!can_be_custom_component("<footer>"));
668+
assert!(!can_be_custom_component("<div/>"));
669+
assert!(!can_be_custom_component("</div>"));
670+
assert!(!can_be_custom_component("<div class=\"test\">"));
671+
}
672+
673+
#[test]
674+
fn test_can_be_custom_component_edge_cases() {
675+
// Empty or invalid tags
676+
assert!(!can_be_custom_component("<>"));
677+
assert!(!can_be_custom_component("</>"));
678+
assert!(!can_be_custom_component(""));
679+
assert!(!can_be_custom_component("< >"));
680+
assert!(!can_be_custom_component("text"));
681+
682+
// Missing brackets
683+
assert!(!can_be_custom_component("MyComponent>"));
684+
assert!(!can_be_custom_component("<MyComponent"));
685+
686+
// Whitespace handling
687+
assert!(can_be_custom_component(" <MyComponent> "));
688+
assert!(can_be_custom_component(" <my-component> "));
689+
assert!(!can_be_custom_component(" <div> "));
690+
}
691+
692+
#[test]
693+
fn test_can_be_custom_component_with_attributes() {
694+
// With attributes
695+
assert!(can_be_custom_component(
696+
"<Counter initial=\"5\" step=\"1\"/>"
697+
));
698+
assert!(can_be_custom_component(
699+
"<my-widget data=\"test\" class=\"styled\"/>"
700+
));
701+
assert!(!can_be_custom_component(
702+
"<div class=\"container\" id=\"main\">"
703+
));
704+
}
705+
706+
#[test]
707+
fn test_can_be_custom_component_self_closing() {
708+
// Self-closing tags
709+
assert!(can_be_custom_component("<MyComponent/>"));
710+
assert!(can_be_custom_component("<my-component/>"));
711+
assert!(!can_be_custom_component("<div/>"));
712+
assert!(!can_be_custom_component("<span/>"));
713+
}
714+
715+
#[test]
716+
fn test_can_be_custom_component_closing_tags() {
717+
// Closing tags
718+
assert!(can_be_custom_component("</MyComponent>"));
719+
assert!(can_be_custom_component("</my-component>"));
720+
assert!(!can_be_custom_component("</div>"));
721+
assert!(!can_be_custom_component("</span>"));
722+
}
723+
}

yew-markdown/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,20 @@ see [here](https://rambip.github.io/rust-web-markdown/onclick)
4141

4242
## Custom Components
4343
see [here](https://rambip.github.io/rust-web-markdown/custom_components)
44+
45+
Custom components allow you to embed interactive Yew components in your markdown.
46+
47+
### Custom Component Naming Rules
48+
49+
To be recognized as a custom component, tag names must follow these rules:
50+
51+
1. **Uppercase start** - Tags starting with an uppercase letter (A-Z) are always treated as custom components
52+
- Examples: `<MyComponent>`, `<Counter>`, `<DataTable>`
53+
54+
2. **Lowercase with dash** - Tags starting with lowercase (a-z) must contain at least one dash (-)
55+
- Examples: `<my-component>`, `<data-table>`, `<custom-counter>`
56+
57+
These rules ensure standard HTML tags like `<div>`, `<span>`, and `<p>` are not confused with custom components.
58+
59+
See the [custom-components example](./examples/custom-components) for a complete working example.
60+

0 commit comments

Comments
 (0)