Skip to content
Merged
1 change: 1 addition & 0 deletions Cargo.lock

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

24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<MyComponent>`, `<Counter>`, `<DataTable>`

2. **Lowercase with dash** - Tags starting with lowercase (a-z) must contain at least one dash (-)
- Examples: `<my-component>`, `<data-table>`, `<custom-counter>`

These rules ensure standard HTML tags like `<div>`, `<span>`, and `<p>` are not confused with custom components.

**Valid custom components:**
- `<MyComponent>` ✓ (uppercase start)
- `<my-component>` ✓ (lowercase start with dash)
- `<Counter initial="5"/>` ✓ (uppercase, self-closing with attributes)

**NOT custom components:**
- `<div>` ✗ (lowercase without dash - standard HTML)
- `<span>` ✗ (lowercase without dash - standard HTML)
- `<p>` ✗ (lowercase without dash - standard HTML)

# Contribute

PRs are **very much** appreciated.
Expand Down
18 changes: 18 additions & 0 deletions dioxus-markdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<MyComponent>`, `<Counter>`, `<DataTable>`

2. **Lowercase with dash** - Tags starting with lowercase (a-z) must contain at least one dash (-)
- Examples: `<my-component>`, `<data-table>`, `<custom-counter>`

These rules ensure standard HTML tags like `<div>`, `<span>`, and `<p>` are not confused with custom components.

See the [custom-components example](./examples/custom-components) for a complete working example.

# Changelog

## 0.1.0
Expand Down
6 changes: 3 additions & 3 deletions dioxus-markdown/examples/custom-components/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ A counter which modifies the document:
<PersistedCounter value="5"/>

## Here is a Box:
<box>
<custom-box>

**I am in a blue box !**

</box>
</custom-box>
"#;

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

components.register("box", |props| {
components.register("custom-box", |props| {
let children = props.children;
Ok(rsx! {
ColorBox { children }
Expand Down
16 changes: 16 additions & 0 deletions leptos-markdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<MyComponent>`, `<Counter>`, `<DataTable>`

2. **Lowercase with dash** - Tags starting with lowercase (a-z) must contain at least one dash (-)
- Examples: `<my-component>`, `<data-table>`, `<custom-counter>`

These rules ensure standard HTML tags like `<div>`, `<span>`, and `<p>` are not confused with custom components.

See the [custom-component example](./examples/custom-component) for a complete working example.

# Changelog

## 0.7.0
Expand Down
6 changes: 3 additions & 3 deletions leptos-markdown/examples/custom-component/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ static MARKDOWN_SOURCE: &str = r#"
<Counter initial="a"/>

## Here is a Box:
<box>
<custom-box>

**I am in a blue box !**

</box>
</custom-box>
"#;

#[component]
Expand All @@ -67,7 +67,7 @@ fn App() -> impl IntoView {
})
});

components.register("box", |props| {
components.register("custom-box", |props| {
Ok(view! {
<BlueBox>{props.children}</BlueBox>
})
Expand Down
1 change: 1 addition & 0 deletions web-markdown/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
41 changes: 39 additions & 2 deletions web-markdown/src/component.rs
Original file line number Diff line number Diff line change
@@ -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: `<MyComponent>`, `<Counter>`, `<DataTable>`
/// - No dash required for uppercase names
///
/// 2. **Starts with a lowercase letter (a-z) and contains at least one dash (-)**
/// - Examples: `<my-component>`, `<data-table>`, `<custom-counter>`
/// - The dash distinguishes these from standard HTML tags
///
/// This ensures that standard HTML tags (like `<div>`, `<span>`, `<p>`) are never confused
/// with custom components. Custom component tags support start tags, end tags, self-closing
/// tags, and tags with attributes (e.g., `<MyComponent key="value" name="test"/>`).
///
/// ### Examples
///
/// ```markdown
/// <!-- Valid custom components -->
/// <Counter initial="5"/>
/// <my-widget/>
/// <DataTable>content</DataTable>
/// <custom-box>
/// **Bold text inside custom component**
/// </custom-box>
///
/// <!-- NOT custom components (standard HTML) -->
/// <div>text</div>
/// <span>text</span>
/// <p>paragraph</p>
/// ```
#[derive(Debug, PartialEq)]
pub struct ComponentCall<'a> {
/// Where in the larger document full_string starts.
Expand Down
164 changes: 150 additions & 14 deletions web-markdown/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,21 +189,60 @@ where
current_component: Option<String>,
}

/// 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 `<div>`, `<span>`, `<p>` from being
/// treated as custom components while allowing custom component names like:
/// - `<MyComponent>` (uppercase start, no dash needed)
/// - `<my-component>` (lowercase start, has dash)
/// - `<My-Component>` (uppercase start, has dash)
///
/// The function also handles:
/// - Self-closing tags: `<My-Component/>`
/// - Tags with attributes: `<My-Component attr="value">`
/// - Closing tags: `</My-Component>`
///
/// Invalid HTML like `<Y and Y>` is rejected because "and" is not valid attribute syntax.
fn can_be_custom_component(raw_html: &str) -> bool {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be a lot better to use a regex.

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: <MyComponent> or </MyComponent> or <my-component> or </my-component>
// ^< - 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"^</?([A-Z][A-Za-z0-9-]*|[a-z][A-Za-z0-9]*-[A-Za-z0-9-]*)>$"
).unwrap();

// Self-closing tags: <MyComponent/> or <my-component/>
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: <MyComponent attr="value"> or <my-component attr="value"/>
// After the tag name, we must have whitespace followed by content that contains '='
// This rejects things like "<Y and Y>" where there's no '='
// Note: The regex is non-greedy and will match the smallest possible string,
// so escaped characters like &lt; or &gt; in attributes are allowed
static ref WITH_ATTRS_RE: regex::Regex = regex::Regex::new(
r"^</?([A-Z][A-Za-z0-9-]*|[a-z][A-Za-z0-9]*-[A-Za-z0-9-]*)\s+.*?=.*?/?\s*>$"
).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>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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("<MyComponent>"));
assert!(can_be_custom_component("<Counter>"));
assert!(can_be_custom_component("<DataTable>"));
assert!(can_be_custom_component("<MyComponent/>"));
assert!(can_be_custom_component("</MyComponent>"));
assert!(can_be_custom_component("<MyComponent attr=\"value\">"));
assert!(can_be_custom_component("<My-Component>"));
assert!(can_be_custom_component("<MY-COMPONENT>"));
}

#[test]
fn test_can_be_custom_component_lowercase_with_dash() {
// Lowercase start with dash should be valid
assert!(can_be_custom_component("<my-component>"));
assert!(can_be_custom_component("<data-table>"));
assert!(can_be_custom_component("<custom-counter>"));
assert!(can_be_custom_component("<my-component/>"));
assert!(can_be_custom_component("</my-component>"));
assert!(can_be_custom_component("<my-component attr=\"value\">"));
assert!(can_be_custom_component("<a-b>"));
assert!(can_be_custom_component("<my-custom-widget>"));
}

#[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("<div>"));
assert!(!can_be_custom_component("<span>"));
assert!(!can_be_custom_component("<p>"));
assert!(!can_be_custom_component("<section>"));
assert!(!can_be_custom_component("<article>"));
assert!(!can_be_custom_component("<header>"));
assert!(!can_be_custom_component("<footer>"));
assert!(!can_be_custom_component("<div/>"));
assert!(!can_be_custom_component("</div>"));
assert!(!can_be_custom_component("<div class=\"test\">"));
}

#[test]
fn test_can_be_custom_component_edge_cases() {
// Empty or invalid 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("text"));

// Missing brackets
assert!(!can_be_custom_component("MyComponent>"));
assert!(!can_be_custom_component("<MyComponent"));

// Whitespace handling
assert!(can_be_custom_component(" <MyComponent> "));
assert!(can_be_custom_component(" <my-component> "));
assert!(!can_be_custom_component(" <div> "));
}

#[test]
fn test_can_be_custom_component_with_attributes() {
// With attributes
assert!(can_be_custom_component(
"<Counter initial=\"5\" step=\"1\"/>"
));
assert!(can_be_custom_component(
"<my-widget data=\"test\" class=\"styled\"/>"
));
assert!(!can_be_custom_component(
"<div class=\"container\" id=\"main\">"
));
}

#[test]
fn test_can_be_custom_component_self_closing() {
// Self-closing tags
assert!(can_be_custom_component("<MyComponent/>"));
assert!(can_be_custom_component("<my-component/>"));
assert!(!can_be_custom_component("<div/>"));
assert!(!can_be_custom_component("<span/>"));
}

#[test]
fn test_can_be_custom_component_closing_tags() {
// Closing tags
assert!(can_be_custom_component("</MyComponent>"));
assert!(can_be_custom_component("</my-component>"));
assert!(!can_be_custom_component("</div>"));
assert!(!can_be_custom_component("</span>"));
}
}
17 changes: 17 additions & 0 deletions yew-markdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,20 @@ 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 Yew 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: `<MyComponent>`, `<Counter>`, `<DataTable>`

2. **Lowercase with dash** - Tags starting with lowercase (a-z) must contain at least one dash (-)
- Examples: `<my-component>`, `<data-table>`, `<custom-counter>`

These rules ensure standard HTML tags like `<div>`, `<span>`, and `<p>` are not confused with custom components.

See the [custom-components example](./examples/custom-components) for a complete working example.

Loading