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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ __pycache__
.DS_Store
Thumbs.db

# Vitest
__screenshots__/
/.vitest-attachments/

# Playwright
/playwright/results/
/playwright/.cache/
Expand Down
123 changes: 123 additions & 0 deletions api-goldens/element-ng/markdown/index.api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
## Public API Report File for "@siemens/element-ng_markdown"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).

```ts

import * as _angular_core from '@angular/core';
import * as _angular_platform_browser from '@angular/platform-browser';
import { InputSignal } from '@angular/core';
import * as mdast from 'mdast';
import { Node as Node_2 } from 'mdast';
import { Parent } from 'mdast';
import { PluggableList } from 'unified';
import { Plugin as Plugin_2 } from 'unified';
import { Preset } from 'unified';
import { Processor } from 'unified';
import { Root } from 'mdast';
import { Signal } from '@angular/core';
import { TemplateRef } from '@angular/core';
import { Transformer as Transformer_2 } from 'unified';
import { Type } from '@angular/core';

// @public
export interface ComponentWithOptions<T> {
component: Type<T>;
options?: any;
}

// @public (undocumented)
export const makeSiMarkdownOptions: () => SiMarkdownOptions;

// @public
export interface PluginWithOptions {
// (undocumented)
options?: any;
// (undocumented)
plugin: UnifiedPlugin;
}

// @public
export class SiMarkdownComponent implements SiMarkdownControl {
readonly debug: _angular_core.InputSignal<boolean>;
readonly markdown: _angular_core.InputSignal<string>;
readonly options: _angular_core.InputSignal<SiMarkdownOptions | undefined>;
}

// @public
export interface SiMarkdownExtension {
codeTypes?: TypeHandler[];
plugins?: PluginWithOptions[];
types?: TypeHandler[];
}

// @public
export interface SiMarkdownExtensionComponent {
node: InputSignal<Node_2>;
// (undocumented)
options: InputSignal<any>;
parent: InputSignal<Parent>;
}

// @public
export class SiMarkdownFragmentComponent {
readonly children: _angular_core.Signal<mdast.RootContent[]>;
readonly node: _angular_core.InputSignal<Node_2>;
}

// @public (undocumented)
export type SiMarkdownHighlighter = ComponentWithOptions<SiMarkdownHighlighterComponent>;

// @public
export interface SiMarkdownHighlighterComponent {
code: InputSignal<string>;
language: InputSignal<string>;
// (undocumented)
options: InputSignal<any>;
updateLanguage: InputSignal<(lang?: string) => void>;
}

// @public (undocumented)
export class SiMarkdownInlineHtmlComponent implements SiMarkdownExtensionComponent {
// (undocumented)
readonly node: _angular_core.InputSignal<Node_2>;
// (undocumented)
readonly options: _angular_core.InputSignal<any>;
// (undocumented)
readonly parent: _angular_core.InputSignal<Parent>;
}

// @public
export class SiMarkdownOptions {
constructor();
// (undocumented)
getCodeTypeHandlers(): Map<string, TypeHandler>;
// (undocumented)
getHighlighter(): SiMarkdownHighlighter | undefined;
// (undocumented)
getTypeHandlers(): TypeHandler[];
installExtension(extension: SiMarkdownExtension): SiMarkdownOptions;
installUnifiedPlugin(plugin: UnifiedPlugin, options?: any): SiMarkdownOptions;
makeProcessor(): Processor<Root, undefined, undefined, undefined, undefined>;
setCodeHighlighter(highlighter?: SiMarkdownHighlighter): SiMarkdownOptions;
}

// @public
export class SiMarkdownTemplateDirective {
// (undocumented)
readonly nodeType: _angular_core.InputSignal<string>;
// (undocumented)
readonly template: TemplateRef<any>;
}

// @public
export interface TypeHandler extends ComponentWithOptions<SiMarkdownExtensionComponent> {
type: string;
}

// @public (undocumented)
export type UnifiedPlugin = Plugin_2<any> | Transformer_2<any> | Preset | PluggableList;

// (No @packageDocumentation comment for this package)

```
126 changes: 126 additions & 0 deletions docs/components/chat-messages/markdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Markdown

> **Note:** The markdown component is currently experimental and may undergo changes in future releases.

The **markdown** component renders Markdown content as Angular components. Use its options to configure extensions, code highlighting, and custom rendering behavior. Options can be shared between multiple component instances.

## Usage ---

Create the options once and pass the same instance to each `si-markdown` component that needs the configuration. The component supports GitHub Flavored Markdown by default, including tables, task lists, strikethrough, and autolinked URLs.

```ts
import { Component, signal } from '@angular/core';
import { makeSiMarkdownOptions, SiMarkdownComponent } from '@siemens/element-ng/markdown';
import { siMarkdownMathKaTeX } from '@siemens/element-ng/markdown/extensions/katex';
import { siMarkdownMermaid } from '@siemens/element-ng/markdown/extensions/mermaid';
import { siMarkdownHighlightJs } from '@siemens/element-ng/markdown/hightlighter/highlightjs';
import remarkGemoji from 'remark-gemoji';

@Component({
imports: [SiMarkdownComponent],
template: ` <si-markdown [markdown]="markdown()" [options]="markdownOptions" /> `
})
export class MarkdownExampleComponent {
protected readonly markdown = signal('# Release notes');
protected readonly markdownOptions = makeSiMarkdownOptions()
.setCodeHighlighter(siMarkdownHighlightJs({ autoDetectLanguage: true }))
.installExtension(siMarkdownMathKaTeX())
.installExtension(siMarkdownMermaid())
.installUnifiedPlugin(remarkGemoji);
}
```

## Extensions ---

The following rendering support is included in every Markdown component:

| Rendering support | Purpose |
| ------------------------ | -------------------------------------------------------------- |
| GitHub Flavored Markdown | Parses tables, task lists, strikethrough, and autolinked URLs. |
| Inline HTML | Renders sanitized inline HTML. |
| Code blocks | Renders fenced code blocks without syntax highlighting. |

Optional integrations are configured through `SiMarkdownOptions`:

| Integration | Configuration | Purpose |
| ------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| [KaTeX](https://katex.org/) | `.installExtension(siMarkdownMathKaTeX())` | Parses and renders inline and block LaTeX math expressions. It accepts optional `remark-math` parser and KaTeX rendering options. |
| [Mermaid](https://mermaid.js.org/) | `.installExtension(siMarkdownMermaid())` | Renders fenced code blocks declared as `mermaid` as diagrams. It accepts optional Mermaid configuration. |
| [Highlight.js](https://highlightjs.org/) | `.setCodeHighlighter(siMarkdownHighlightJs())` | Adds syntax highlighting to fenced code blocks. It accepts Highlight.js options, including automatic language detection. |
| [Gemojis](https://github.com/github/gemoji) | `.installUnifiedPlugin(remarkGemoji)` | Converts emoji shortcodes such as `:rocket:` to emoji. |

You can also add a compatible [unified](https://unifiedjs.com/) or [remark](https://remark.js.org/) plugin with `.installUnifiedPlugin(plugin, options)`. This is useful for syntax that is not covered by the Element integrations, such as emoji shortcodes.

> **Bundle size:** KaTeX, Mermaid, Highlight.js, and additional `unified` plugins increase the application bundle size. Import and configure only the integrations your Markdown content requires.

## Custom extension ---

An extension can install `unified` plugin(s) and associate the AST node types produced by that plugin with Angular renderer components. The KaTeX integration is an example: `remark-math` parses Markdown math into `math` and `inlineMath` nodes, which are then rendered by the same component.

```ts
import { type KatexOptions } from 'katex';
import remarkMath, { type Options as RemarkMathOptions } from 'remark-math';
import { type SiMarkdownExtension } from '@siemens/element-ng/markdown';

import { MarkdownKatexComponent } from './markdown-katex.component';

export const markdownKaTeX = (
parseOptions?: RemarkMathOptions,
katexOptions?: KatexOptions
): SiMarkdownExtension => ({
plugins: [{ plugin: remarkMath, options: parseOptions }],
types: [
{ type: 'math', component: MarkdownKatexComponent, options: katexOptions },
{ type: 'inlineMath', component: MarkdownKatexComponent, options: katexOptions }
]
});
```

The renderer implements `SiMarkdownExtensionComponent`. Element supplies the parsed node, its parent, and the options provided in the extension definition as signal inputs.

```ts
import { Component, computed, inject, input } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { type KatexOptions, renderToString } from 'katex';
import { type Literal, type Node, type Parent } from 'mdast';
import { type SiMarkdownExtensionComponent } from '@siemens/element-ng/markdown';

@Component({
selector: 'app-markdown-katex',
template: '',
host: {
'[innerHTML]': 'html()'
}
})
export class MarkdownKatexComponent implements SiMarkdownExtensionComponent {
private readonly sanitizer = inject(DomSanitizer);

readonly node = input.required<Node>();
readonly parent = input.required<Parent>();
readonly options = input<KatexOptions>();

protected readonly html = computed(() => {
const expression = (this.node() as Literal).value;
return this.sanitizer.bypassSecurityTrustHtml(
renderToString(expression, {
...this.options(),
displayMode: this.node().type === 'math'
})
);
});
}
```

Register the extension with the component options:

```ts
protected readonly markdownOptions = makeSiMarkdownOptions().installExtension(markdownKaTeX());
```

## Code ---

<si-docs-component example="si-markdown/si-markdown" height="700"></si-docs-component>

<si-docs-api component="SiMarkdownComponent"></si-docs-api>

<si-docs-types></si-docs-types>
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ nav:
- Chat & messages:
- Chat input: 'components/chat-messages/chat-input.md'
- Chat message: 'components/chat-messages/chat-message.md'
- Markdown: 'components/chat-messages/markdown.md'
- Charts:
- Overview: 'components/charts/index.md'
- Bar chart: 'components/charts/bar-chart.md'
Expand Down
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,20 @@
"flag-icons": "7.5.0",
"google-libphonenumber": "3.2.44",
"gridstack": "12.6.0",
"highlight.js": "^11.11.1",
"katex": "^0.17.0",
"mermaid": "^11.16.0",
"ngx-image-cropper": "9.1.6",
"ol": "10.9.0",
"ol-ext": "4.0.38",
"ol-mapbox-style": "13.4.1",
"remark-gemoji": "^8.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",
"rxjs": "7.8.2",
"tslib": "2.8.1"
"tslib": "2.8.1",
"unified": "^11.0.5"
},
"devDependencies": {
"@angular-builders/custom-esbuild": "22.0.1",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
- paragraph:
- text: Here's a
- strong: simple response
- text: with basic formatting.
- text: with basic formatting and source citations.
- 'button "View source: Connected Devices Guide"'
- paragraph:
- text: You can use
- code: inline code
- text: "and create lists:"
- list:
- listitem: First item
- listitem: Second item
- paragraph:
- text: Production installations should follow the device and configuration guidance.
- 'button "View sources: Connected Devices Guide and 1 additional source"'
- group:
- button "Good response"
- button "Bad response"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading