Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/components/markdown/__tests__/markdoc.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,42 @@ console.log('Hello');
expect(screen.getByText('Bold text')).toBeInTheDocument();
expect(screen.getByText('italic text')).toBeInTheDocument();
});

it('renders style tag with color', () => {
const content = '{% style color="red" %}red text{% /style %}';
render(<Markdown markdown={content} />);

const styledEl = screen.getByText('red text');
expect(styledEl).toBeInTheDocument();
expect(styledEl).toHaveStyle({ color: 'red' });
});

it('renders style tag with background color', () => {
const content = '{% style bg="yellow" %}highlighted text{% /style %}';
render(<Markdown markdown={content} />);

const styledEl = screen.getByText('highlighted text');
expect(styledEl).toBeInTheDocument();
expect(styledEl).toHaveStyle({ backgroundColor: 'yellow' });
});

it('renders style tag with both color and background', () => {
const content =
'{% style color="white" bg="blue" %}styled text{% /style %}';
render(<Markdown markdown={content} />);

const styledEl = screen.getByText('styled text');
expect(styledEl).toBeInTheDocument();
expect(styledEl).toHaveStyle({ color: 'white', backgroundColor: 'blue' });
});

it('renders nested style tags', () => {
const content =
'{% style color="blue" %}outer {% style color="red" %}inner{% /style %}{% /style %}';
render(<Markdown markdown={content} />);

const innerEl = screen.getByText('inner');
expect(innerEl).toBeInTheDocument();
expect(innerEl).toHaveStyle({ color: 'red' });
});
});
2 changes: 2 additions & 0 deletions src/components/markdown/markdoc-components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import InlineCode from './inline-code/inline-code';
import List from './list/list';
import SignalButton from './signal-button/signal-button';
import StartWorkflowButton from './start-workflow-button/start-workflow-button';
import Style from './style/style';

// Export all components that Markdoc can use
export const markdocComponents = {
Expand All @@ -17,6 +18,7 @@ export const markdocComponents = {
InlineCode,
Image,
Br,
Style,
};

export type MarkdocComponents = typeof markdocComponents;
13 changes: 13 additions & 0 deletions src/components/markdown/markdoc-components/style/style.markdoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const styleMarkdocSchema = {
render: 'Style',
attributes: {
color: {
type: String,
required: false,
},
bg: {
type: String,
required: false,
},
},
};
23 changes: 23 additions & 0 deletions src/components/markdown/markdoc-components/style/style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type StyleProps } from './style.types';

// Validates CSS color values to prevent injection of unexpected values.
// Accepts named colors, hex codes, and rgb/rgba/hsl/hsla functions.
function isValidCssColor(value: string): boolean {
return /^(#[0-9a-fA-F]{3,8}|rgb\(.*\)|rgba\(.*\)|hsl\(.*\)|hsla\(.*\)|[a-zA-Z]+)$/.test(
value.trim()
);
Comment on lines +6 to +8
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Security: CSS color validation regex is overly permissive with .*

The isValidCssColor regex uses .* inside the rgb()/rgba()/hsl()/hsla() patterns, which matches arbitrary content between the parentheses. For example, rgb(0,0,0;injected-property:value) would pass validation.

In practice, React's style prop sets values via the DOM element.style API (not innerHTML), so this can't be exploited for CSS injection. However, since the function is explicitly documented as a security validation layer, it should actually enforce valid values to match its stated purpose and serve as defense-in-depth.

Suggested fix:

function isValidCssColor(value: string): boolean {
  return /^(#[0-9a-fA-F]{3,8}|rgba?\([\d\s,.%]+\)|hsla?\([\d\s,.%]+\)|[a-zA-Z]+)$/.test(
    value.trim()
  );
}

Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion

}

export default function Style({ color, bg, children }: StyleProps) {
const cssStyle: React.CSSProperties = {};

if (color && isValidCssColor(color)) {
cssStyle.color = color;
}

if (bg && isValidCssColor(bg)) {
cssStyle.backgroundColor = bg;
}

return <span style={cssStyle}>{children}</span>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type ReactNode } from 'react';

export type StyleProps = {
color?: string;
bg?: string;
children?: ReactNode;
};
2 changes: 2 additions & 0 deletions src/components/markdown/markdoc-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import { inlineCodeMarkdocSchema } from './markdoc-components/inline-code/inline
import { listMarkdocSchema } from './markdoc-components/list/list.markdoc';
import { signalButtonMarkdocSchema } from './markdoc-components/signal-button/signal-button.markdoc';
import { startWorkflowButtonMarkdocSchema } from './markdoc-components/start-workflow-button/start-workflow-button.markdoc';
import { styleMarkdocSchema } from './markdoc-components/style/style.markdoc';

export const markdocConfig: Config = {
tags: {
signal: signalButtonMarkdocSchema,
start: startWorkflowButtonMarkdocSchema,
image: imageSchema,
br: brSchema,
style: styleMarkdocSchema,
},
nodes: {
// Standard HTML nodes
Expand Down
49 changes: 49 additions & 0 deletions src/views/docs/markdown/markdown-guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,55 @@ Image with 100px height:
Image with 100px width and 100px height:
{% image src="https://cadenceworkflow.io/assets/images/workflow-84ef76d93c7ff138714a0aa7c9b92841.png" alt="Image with 100px width and 100px height" width="100" height="100" /%}

## Styled Text

Use the \`{% style %}\` tag to apply custom text color or background color to any content.

### Color

Use the \`color\` attribute to set the text color:

\`\`\`
{% style color="red" %}red text{% /style %}
{% style color="#0070f3" %}blue text{% /style %}
\`\`\`

{% style color="red" %}red text{% /style %}

{% style color="green" %}green text{% /style %}

### Background

Use the \`bg\` attribute to set the background color:

\`\`\`
{% style bg="yellow" %}highlighted text{% /style %}
\`\`\`

{% style bg="yellow" %}highlighted text{% /style %}

### Combined

Use both \`color\` and \`bg\` together:

\`\`\`
{% style color="white" bg="blue" %}white text on blue background{% /style %}
\`\`\`

{% style color="white" bg="blue" %}white text on blue background{% /style %}

### Nesting

Tags can be nested for fine-grained control:

\`\`\`
{% style color="blue" %}
This is blue text, {% style color="red" %}this part is red{% /style %}, and back to blue.
{% /style %}
\`\`\`

{% style color="blue" %}This is blue text, {% style color="red" %}this part is red{% /style %}, and back to blue.{% /style %}

`;

export default content;