diff --git a/docs/syntax/diagrams.md b/docs/syntax/diagrams.md index aeb0730ff..d56e0bf93 100644 --- a/docs/syntax/diagrams.md +++ b/docs/syntax/diagrams.md @@ -181,106 +181,77 @@ erDiagram ### Complex flowchart -::::::{tab-set} +:::::{tab-set} -:::::{tab-item} Source +::::{tab-item} Source ````markdown ```mermaid -graph TB - A["Painless Operators"] - - B["General"] - C["Numeric"] - D["Boolean"] - E["Reference"] - F["Array"] - - B1["Control expression flow and
value assignment"] - C1["Mathematical operations and
bit manipulation"] - D1["Boolean logic and
conditional evaluation"] - E1["Object interaction and
safe data access"] - F1["Array manipulation and
element access"] - - B2["Precedence ( )
Function Call ( )
Cast ( )
Conditional ? :
Elvis ?:
Assignment =
Compound Assignment $="] - C2["Post/Pre Increment ++
Post/Pre Decrement --
Unary +/-
Bitwise Not ~
Multiplication *
Division /
Remainder %
Addition +
Subtraction -
Shift <<, >>, >>>
Bitwise And &
Bitwise Xor ^
Bitwise Or |"] - D2["Boolean Not !
Comparison >, >=, <, <=
Instanceof instanceof
Equality ==, !=
Identity ===, !==
Boolean Xor ^
Boolean And &&
Boolean Or ||"] - E2["Method Call . ( )
Field Access .
Null Safe ?.
New Instance new ( )
String Concatenation +
List/Map Init [ ], [ : ]
List/Map Access [ ]"] - F2["Array Init [ ] { }
Array Access [ ]
Array Length .length
New Array new [ ]"] - - A --> B & C & D & E & F - B --> B1 - C --> C1 - D --> D1 - E --> E1 - F --> F1 - B1 --> B2 - C1 --> C2 - D1 --> D2 - E1 --> E2 - F1 --> F2 - - classDef rootNode fill:#0B64DD,stroke:#101C3F,stroke-width:2px,color:#fff - classDef categoryBox fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#343741 - classDef descBox fill:#48EFCF,stroke:#343741,stroke-width:2px,color:#343741 - classDef exampleBox fill:#f5f7fa,stroke:#343741,stroke-width:2px,color:#343741 - - class A rootNode - class B,C,D,E,F categoryBox - class B1,C1,D1,E1,F1 descBox - class B2,C2,D2,E2,F2 exampleBox +flowchart TD + subgraph t2["T2"] + direction TB + enterprise_app["Enterprise application"] + + subgraph subscription["Subscription"] + subgraph rg["Resource Group"] + event_hub["event hub"] + storage_account["storage account"] + end + spacer1[" "]:::hidden + spacer2[" "]:::hidden + spacer1 ~~~ spacer2 + end + enterprise_app -- "Azure Event Hubs
Data Receiver" --> event_hub + enterprise_app -- "Storage Blob Data
Contributor" --> storage_account + end + + app_reg --> enterprise_app + + subgraph t1["T1"] + direction TB + app_reg["App registration"] + client_secret["Client secret"] + app_reg --> client_secret + end + + classDef hidden fill:none,stroke:none,color:transparent,width:0px; ``` ```` -::::: +:::: -:::::{tab-item} Rendered +::::{tab-item} Rendered ```mermaid -graph TB - A["Painless Operators"] - - B["General"] - C["Numeric"] - D["Boolean"] - E["Reference"] - F["Array"] - - B1["Control expression flow and
value assignment"] - C1["Mathematical operations and
bit manipulation"] - D1["Boolean logic and
conditional evaluation"] - E1["Object interaction and
safe data access"] - F1["Array manipulation and
element access"] - - B2["Precedence ( )
Function Call ( )
Cast ( )
Conditional ? :
Elvis ?:
Assignment =
Compound Assignment $="] - C2["Post/Pre Increment ++
Post/Pre Decrement --
Unary +/-
Bitwise Not ~
Multiplication *
Division /
Remainder %
Addition +
Subtraction -
Shift <<, >>, >>>
Bitwise And &
Bitwise Xor ^
Bitwise Or |"] - D2["Boolean Not !
Comparison >, >=, <, <=
Instanceof instanceof
Equality ==, !=
Identity ===, !==
Boolean Xor ^
Boolean And &&
Boolean Or ||"] - E2["Method Call . ( )
Field Access .
Null Safe ?.
New Instance new ( )
String Concatenation +
List/Map Init [ ], [ : ]
List/Map Access [ ]"] - F2["Array Init [ ] { }
Array Access [ ]
Array Length .length
New Array new [ ]"] - - A --> B & C & D & E & F - B --> B1 - C --> C1 - D --> D1 - E --> E1 - F --> F1 - B1 --> B2 - C1 --> C2 - D1 --> D2 - E1 --> E2 - F1 --> F2 - - classDef rootNode fill:#0B64DD,stroke:#101C3F,stroke-width:2px,color:#fff - classDef categoryBox fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#343741 - classDef descBox fill:#48EFCF,stroke:#343741,stroke-width:2px,color:#343741 - classDef exampleBox fill:#f5f7fa,stroke:#343741,stroke-width:2px,color:#343741 - - class A rootNode - class B,C,D,E,F categoryBox - class B1,C1,D1,E1,F1 descBox - class B2,C2,D2,E2,F2 exampleBox +flowchart TD + subgraph t2["T2"] + direction TB + enterprise_app["Enterprise application"] + + subgraph subscription["Subscription"] + subgraph rg["Resource Group"] + event_hub["event hub"] + storage_account["storage account"] + end + spacer1[" "]:::hidden + spacer2[" "]:::hidden + spacer1 ~~~ spacer2 + end + enterprise_app -- "Azure Event Hubs
Data Receiver" --> event_hub + enterprise_app -- "Storage Blob Data
Contributor" --> storage_account + end + + app_reg --> enterprise_app + + subgraph t1["T1"] + direction TB + app_reg["App registration"] + client_secret["Client secret"] + app_reg --> client_secret + end + + classDef hidden fill:none,stroke:none,color:transparent,width:0px; ``` -::::: - -:::::: +:::: +::::: ## Interactive controls Mermaid diagrams include interactive controls that appear when you hover over the diagram: diff --git a/src/Elastic.Documentation.Site/Assets/mermaid.test.ts b/src/Elastic.Documentation.Site/Assets/mermaid.test.ts new file mode 100644 index 000000000..6a4ae1323 --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/mermaid.test.ts @@ -0,0 +1,302 @@ +import { normalizeToXml, sanitizeSvgNode } from './mermaid' + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +const SVG_NS = 'http://www.w3.org/2000/svg' + +const wrapSvg = (inner: string) => `${inner}` + +const createRect = () => ({ + width: 100, + height: 60, + top: 0, + left: 0, + bottom: 60, + right: 100, + x: 0, + y: 0, + toJSON: () => ({}), +}) + +const setVisibleRect = (element: HTMLElement) => { + Object.defineProperty(element, 'getBoundingClientRect', { + value: () => createRect(), + }) + Object.defineProperty(element, 'getClientRects', { + value: () => [createRect()], + }) +} + +class MockIntersectionObserver { + callback: IntersectionObserverCallback + observe = jest.fn() + unobserve = jest.fn() + disconnect = jest.fn() + + constructor(callback: IntersectionObserverCallback) { + this.callback = callback + } +} + +const setupMermaid = () => { + window.mermaid = { + initialize: jest.fn(), + render: jest.fn().mockResolvedValue({ + svg: ``, + }), + } +} + +const setupScriptLoad = () => + jest.spyOn(document.head, 'appendChild').mockImplementation((node) => { + if (node instanceof HTMLScriptElement && node.onload) { + node.onload(new Event('load')) + } + return node + }) + +// --------------------------------------------------------------------------- +// normalizeToXml +// --------------------------------------------------------------------------- + +describe('normalizeToXml', () => { + it('converts
to self-closing
', () => { + expect(normalizeToXml('
')).toBe('
') + }) + + it('converts
to self-closing
', () => { + expect(normalizeToXml('
')).toBe('
') + }) + + it('preserves attributes when self-closing', () => { + expect(normalizeToXml('
')).toBe('
') + expect(normalizeToXml('b')).toBe( + 'b' + ) + }) + + it('leaves already self-closed elements unchanged', () => { + expect(normalizeToXml('
')).toBe('
') + expect(normalizeToXml('
')).toBe('
') + }) + + it('replaces   with numeric entity', () => { + expect(normalizeToXml('a b')).toBe('a b') + }) + + it('is case-insensitive', () => { + expect(normalizeToXml('
')).toBe('
') + expect(normalizeToXml('
')).toBe('
') + }) +}) + +// --------------------------------------------------------------------------- +// sanitizeSvgNode +// --------------------------------------------------------------------------- + +describe('sanitizeSvgNode', () => { + it('removes ') + ) as Element + expect(node.querySelector('script')).toBeNull() + expect(node.querySelector('rect')).not.toBeNull() + }) + + it('removes on* event handler attributes', () => { + const node = sanitizeSvgNode( + wrapSvg('') + ) as Element + expect(node.querySelector('rect')?.hasAttribute('onclick')).toBe(false) + }) + + it('removes javascript: URLs from href', () => { + const node = sanitizeSvgNode( + wrapSvg('x') + ) as Element + expect(node.querySelector('a')?.hasAttribute('href')).toBe(false) + }) + + it('removes javascript: URLs with leading whitespace', () => { + const node = sanitizeSvgNode( + wrapSvg('x') + ) as Element + expect(node.querySelector('a')?.hasAttribute('href')).toBe(false) + }) + + it('preserves safe href values', () => { + const node = sanitizeSvgNode( + wrapSvg('x') + ) as Element + expect(node.querySelector('a')?.getAttribute('href')).toBe( + 'https://example.com' + ) + }) + + it('removes