diff --git a/.changeset/tough-baths-hunt.md b/.changeset/tough-baths-hunt.md new file mode 100644 index 00000000000..283b548df7e --- /dev/null +++ b/.changeset/tough-baths-hunt.md @@ -0,0 +1,8 @@ +--- +'mermaid': patch +--- + +fix: prevent escaping `<` and `&` when `htmlLabels: false` + +user: @aloisklink +user: @BambioGaming diff --git a/cypress/integration/rendering/flowchart-v2.spec.js b/cypress/integration/rendering/flowchart-v2.spec.js index bcee3d899e3..801a1256a0d 100644 --- a/cypress/integration/rendering/flowchart-v2.spec.js +++ b/cypress/integration/rendering/flowchart-v2.spec.js @@ -71,6 +71,15 @@ describe('Flowchart v2', () => { { htmlLabels: false, flowchart: { htmlLabels: false } } ); }); + it('5a: angle brackets should be work without html labels', () => { + imgSnapshotTest( + `flowchart TD + a["**Plain text**:\n 5 > 3 && 2 < 4"] + b["\`**Markdown**:
5 > 3 && 2 < 4\`"] + `, + { htmlLabels: false } + ); + }); it('6: should render non-escaped with html labels', () => { imgSnapshotTest( `flowchart TD diff --git a/packages/mermaid/src/diagrams/class/shapeUtil.ts b/packages/mermaid/src/diagrams/class/shapeUtil.ts index 94c8f817aef..967b91b2b66 100644 --- a/packages/mermaid/src/diagrams/class/shapeUtil.ts +++ b/packages/mermaid/src/diagrams/class/shapeUtil.ts @@ -146,7 +146,7 @@ async function addText( numberOfLines = text.children.length; const textChild = text.children[0]; - if (text.textContent === '' || text.textContent.includes('>')) { + if (text.textContent === '' || text.textContent!.includes('>')) { textChild.textContent = textContent[0] + textContent.substring(1).replaceAll('>', '>').replaceAll('<', '<').trim(); diff --git a/packages/mermaid/src/rendering-util/createText.spec.ts b/packages/mermaid/src/rendering-util/createText.spec.ts index dd7bc00b644..0a46222682d 100644 --- a/packages/mermaid/src/rendering-util/createText.spec.ts +++ b/packages/mermaid/src/rendering-util/createText.spec.ts @@ -1,7 +1,8 @@ +import { select } from 'd3'; import { describe, expect, it } from 'vitest'; import { sanitizeText } from '../diagram-api/diagramAPI.js'; import mermaid from '../mermaid.js'; -import { replaceIconSubstring } from './createText.js'; +import { createText, replaceIconSubstring } from './createText.js'; describe('replaceIconSubstring', () => { it('converts FontAwesome icon notations to HTML tags', async () => { @@ -61,3 +62,41 @@ describe('replaceIconSubstring', () => { expect(output).toContain(expected); }); }); + +describe('createText', () => { + beforeEach(() => { + // JSDom has no SVGTSpanElement, so we need to mock getComputedTextLength to avoid errors in createText + const mock = vi.mockObject(window.SVGElement.prototype); + (mock as unknown as SVGTSpanElement).getComputedTextLength = vi.fn(() => 123.456); + }); + + it.for([ + { + useHtmlLabels: false, + markdown: true, + }, + { + useHtmlLabels: true, + markdown: true, + }, + { + useHtmlLabels: false, + markdown: false, + }, + { + useHtmlLabels: true, + markdown: false, + }, + ])( + 'decodes HTML entities in text when useHtmlLabels is $useHtmlLabels and markdown is $markdown', + async ({ useHtmlLabels, markdown }) => { + const input = '5 > 3 && 2 < 4'; + const expected = '5 > 3 && 2 < 4'; + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const svgGroup = svg.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'g')); + const output = await createText(select(svgGroup), input, { useHtmlLabels, markdown }); + expect(output.textContent).toEqual(expected); + } + ); +}); diff --git a/packages/mermaid/src/rendering-util/createText.ts b/packages/mermaid/src/rendering-util/createText.ts index 165fa77bd97..bdb777cab73 100644 --- a/packages/mermaid/src/rendering-util/createText.ts +++ b/packages/mermaid/src/rendering-util/createText.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// @ts-nocheck TODO: Fix types import { select } from 'd3'; import type { MermaidConfig } from '../config.type.js'; import type { SVGGroup } from '../diagram-api/types.js'; @@ -17,8 +15,12 @@ import { getIconSVG, isIconAvailable } from './icons.js'; import { splitLineToFitWidth } from './splitText.js'; import type { MarkdownLine, MarkdownWord } from './types.js'; import { getConfig } from '../config.js'; +import type { D3Selection } from '../types.js'; -function applyStyle(dom, styleFn) { +function applyStyle( + dom: d3.Selection, + styleFn?: Parameters[1] +) { if (styleFn) { dom.attr('style', styleFn); } @@ -28,10 +30,10 @@ function applyStyle(dom, styleFn) { const maxSafeSizeForWidth = 16384; async function addHtmlSpan( - element, - node, - width, - classes, + element: D3Selection, + node: { label: string; labelStyle: string; isNode: boolean }, + width: number, + classes: string, addBackground = false, // TODO: Make config mandatory config: MermaidConfig = getConfig() @@ -42,7 +44,7 @@ async function addHtmlSpan( fo.attr('width', `${Math.min(10 * width, maxSafeSizeForWidth)}px`); fo.attr('height', `${Math.min(10 * width, maxSafeSizeForWidth)}px`); - const div = fo.append('xhtml:div'); + const div = fo.append('xhtml:div'); const sanitizedLabel = hasKatex(node.label) ? await renderKatexSanitized(node.label.replace(common.lineBreakRegex, '\n'), config) : sanitizeText(node.label, config); @@ -65,15 +67,15 @@ async function addHtmlSpan( div.attr('class', 'labelBkg'); } - let bbox = div.node().getBoundingClientRect(); + let bbox = div.node()!.getBoundingClientRect(); if (bbox.width === width) { div.style('display', 'table'); div.style('white-space', 'break-spaces'); div.style('width', width + 'px'); - bbox = div.node().getBoundingClientRect(); + bbox = div.node()!.getBoundingClientRect(); } - return fo.node(); + return fo.node()!; } /** @@ -84,7 +86,11 @@ async function addHtmlSpan( * @param lineHeight - The line height value for the text. * @returns The created tspan element. */ -function createTspan(textElement: any, lineIndex: number, lineHeight: number) { +function createTspan( + textElement: D3Selection, + lineIndex: number, + lineHeight: number +) { return textElement .append('tspan') .attr('class', 'text-outer-tspan') @@ -93,11 +99,15 @@ function createTspan(textElement: any, lineIndex: number, lineHeight: number) { .attr('dy', lineHeight + 'em'); } -function computeWidthOfText(parentNode: any, lineHeight: number, line: MarkdownLine): number { +function computeWidthOfText( + parentNode: D3Selection, + lineHeight: number, + line: MarkdownLine +): number { const testElement = parentNode.append('text'); const testSpan = createTspan(testElement, 1, lineHeight); updateTextContentAndStyles(testSpan, line); - const textLength = testSpan.node().getComputedTextLength(); + const textLength = testSpan.node()!.getComputedTextLength(); testElement.remove(); return textLength; } @@ -128,7 +138,7 @@ export function computeDimensionOfText( */ function createFormattedText( width: number, - g: any, + g: D3Selection, structuredText: MarkdownWord[][], addBackground = false ) { @@ -153,7 +163,7 @@ function createFormattedText( } } if (addBackground) { - const bbox = textElement.node().getBBox(); + const bbox = textElement.node()!.getBBox(); const padding = 2; bkg .attr('x', bbox.x - padding) @@ -161,12 +171,38 @@ function createFormattedText( .attr('width', bbox.width + 2 * padding) .attr('height', bbox.height + 2 * padding); - return labelGroup.node(); + return labelGroup.node()!; } else { - return textElement.node(); + return textElement.node()!; } } +/** + * Our HTML code uses `.innerHTML` to apply the text, + * however our plain text SVG code uses `.textContent` to apply the text, + * which means that HTML entities are not decoded in SVG text. + * + * This means that we need to decode any HTML entities that `sanitizeText` encodes. + * + * TODO: If we're using `.textContent`, we can probably skip sanitization entirely. + */ +function decodeHTMLEntities(text: string): string { + // We only need to decode the few entries that `sanitizeText` encodes. + const regex = /&(amp|lt|gt);/g; + return text.replace(regex, (match, entity) => { + switch (entity) { + case 'amp': + return '&'; + case 'lt': + return '<'; + case 'gt': + return '>'; + default: + return match; + } + }); +} + /** * Updates the text content and styles of the given tspan element based on the * provided wrappedLine data. @@ -174,7 +210,10 @@ function createFormattedText( * @param tspan - The tspan element to update. * @param wrappedLine - The line data to apply to the tspan element. */ -function updateTextContentAndStyles(tspan: any, wrappedLine: MarkdownWord[]) { +function updateTextContentAndStyles( + tspan: D3Selection, + wrappedLine: MarkdownWord[] +) { tspan.text(''); wrappedLine.forEach((word, index) => { @@ -184,10 +223,10 @@ function updateTextContentAndStyles(tspan: any, wrappedLine: MarkdownWord[]) { .attr('class', 'text-inner-tspan') .attr('font-weight', word.type === 'strong' ? 'bold' : 'normal'); if (index === 0) { - innerTspan.text(word.content); + innerTspan.text(decodeHTMLEntities(word.content)); } else { // TODO: check what joiner to use. - innerTspan.text(' ' + word.content); + innerTspan.text(' ' + decodeHTMLEntities(word.content)); } }); } @@ -226,8 +265,22 @@ export async function replaceIconSubstring( // Note when using from flowcharts converting the API isNode means classes should be set accordingly. When using htmlLabels => to set classes to 'nodeLabel' when isNode=true otherwise 'edgeLabel' // When not using htmlLabels => to set classes to 'title-row' when isTitle=true otherwise 'title-row' +/** + * Creates a text element within the given SVG group element. + * + * If `markdown` is `true`, basic markdown syntax will be processed. + * Otherwise, if: + * - `useHtmlLabels` is `true`, the text will be sanitized and set in `` as HTML. + * - `useHtmlLabels` is `false`, the text will be added as a `` element using `.text` + * + * @param el - The parent SVG `` element to append the text element to. + * @param text - The text content to be displayed. + * @param options - Optional options + * @param config - Mermaid configuration object + * @returns The created text element, either a `` or a `` element depending on the options. + */ export const createText = async ( - el, + el: D3Selection, text = '', { style = '', @@ -273,7 +326,7 @@ export const createText = async ( return vertexNode; } else { //sometimes the user might add br tags with 1 or more spaces in between, so we need to replace them with
- const sanitizeBR = text.replace(//g, '
'); + const sanitizeBR = decodeEntities(text.replace(//g, '
')); const structuredText = markdown ? markdownToLines(sanitizeBR.replace('
', '
'), config) : nonMarkdownToLines(sanitizeBR); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/bowTieRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/bowTieRect.ts index d4b41103f8b..9429c020dca 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/bowTieRect.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/bowTieRect.ts @@ -75,8 +75,8 @@ export async function bowTieRect(parent: D3Selecti const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const w = bbox.width + node.padding + 20; - const h = bbox.height + node.padding; + const w = bbox.width + (node.padding ?? 0) + 20; + const h = bbox.height + (node.padding ?? 0); const ry = h / 2; const rx = ry / (2.5 + h / 50); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/card.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/card.ts index 4aaf9222a7a..0f004944dfa 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/card.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/card.ts @@ -19,9 +19,9 @@ export async function card(parent: D3Selection, node.labelStyle = labelStyles; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const h = bbox.height + node.padding; + const h = bbox.height + (node.padding ?? 0); const padding = 12; - const w = bbox.width + node.padding + padding; + const w = bbox.width + (node.padding ?? 0) + padding; const left = 0; const right = w; const top = -h; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/cylinder.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/cylinder.ts index 9d2cd63f6d7..f09653bc32e 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/cylinder.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/cylinder.ts @@ -54,10 +54,10 @@ export async function cylinder(parent: D3Selection const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); - const w = Math.max(bbox.width + node.padding, node.width ?? 0); + const w = Math.max(bbox.width + (node.padding ?? 0), node.width ?? 0); const rx = w / 2; const ry = rx / (2.5 + w / 50); - const h = Math.max(bbox.height + ry + node.padding, node.height ?? 0); + const h = Math.max(bbox.height + ry + (node.padding ?? 0), node.height ?? 0); let cylinder: D3Selection | D3Selection; const { cssStyles } = node; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/dividedRect.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/dividedRect.ts index 77df5815541..a25ccd39b57 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/dividedRect.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/dividedRect.ts @@ -12,8 +12,8 @@ export async function dividedRectangle( const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; const { shapeSvg, bbox, label } = await labelHelper(parent, node, getNodeClasses(node)); - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; + const w = bbox.width + (node.padding ?? 0); + const h = bbox.height + (node.padding ?? 0); const rectOffset = h * 0.2; const x = -w / 2; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/document.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/document.ts index d5ea3aa7620..5d59b463f71 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/document.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/document.ts @@ -54,10 +54,10 @@ export async function cylinder(parent: D3Selection const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const w = bbox.width + node.padding; + const w = bbox.width + (node.padding ?? 0); const rx = w / 2; const ry = rx / (2.5 + w / 50); - const h = bbox.height + ry + node.padding; + const h = bbox.height + ry + (node.padding ?? 0); let cylinder: D3Selection | D3Selection; const { cssStyles } = node; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts index 9dd4c90e2b2..adecf2e0c00 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/erBox.ts @@ -350,18 +350,18 @@ async function addText( ); // Undo work around now that text passed through correctly if (labelText.includes('<') || labelText.includes('>')) { - let child = text.children[0]; - child.textContent = child.textContent.replaceAll('<', '<').replaceAll('>', '>'); + let child: Element | ChildNode = text.children[0]; + child.textContent = child.textContent!.replaceAll('<', '<').replaceAll('>', '>'); while (child.childNodes[0]) { child = child.childNodes[0]; // Replace its text content - child.textContent = child.textContent.replaceAll('<', '<').replaceAll('>', '>'); + child.textContent = child.textContent!.replaceAll('<', '<').replaceAll('>', '>'); } } let bbox = text.getBBox(); if (evaluate(config.htmlLabels)) { - const div = text.children[0]; + const div = text.children[0] as HTMLDivElement; div.style.textAlign = 'start'; const dv = select(text); bbox = div.getBoundingClientRect(); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/hexagon.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/hexagon.ts index 52a4397a2f6..910cb7b6c01 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/hexagon.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/hexagon.ts @@ -30,9 +30,9 @@ export async function hexagon(parent: D3Selection< const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); const f = 4; - const h = bbox.height + node.padding; + const h = bbox.height + (node.padding ?? 0); const m = h / f; - const w = bbox.width + 2 * m + node.padding; + const w = bbox.width + 2 * m + (node.padding ?? 0); const points = [ { x: m, y: 0 }, { x: w - m, y: 0 }, diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts index 87adc4814b7..988d0794782 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/question.ts @@ -22,8 +22,8 @@ export async function question(parent: D3Selection node.labelStyle = labelStyles; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; + const w = bbox.width + (node.padding ?? 0); + const h = bbox.height + (node.padding ?? 0); const s = w + h; const adjustment = 0.5; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts index ed764f5eecc..ed6c43b9ba8 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts @@ -198,12 +198,11 @@ async function addText( if (!useHtmlLabels) { const textChild = text.children[0]; for (const child of textChild.children) { - child.textContent = child.textContent.replaceAll('>', '>').replaceAll('<', '<'); if (style) { child.setAttribute('style', style); } } - // Get the bounding box after the text update + // Get the bounding box after the style update bbox = text.getBBox(); // Add extra height so it is similar to the html labels bbox.height += 6; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/stadium.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/stadium.ts index 117176b197d..32b00191180 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/stadium.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/stadium.ts @@ -61,8 +61,8 @@ export async function stadium(parent: D3Selection< node.labelStyle = labelStyles; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const h = bbox.height + node.padding; - const w = bbox.width + h / 4 + node.padding; + const h = bbox.height + (node.padding ?? 0); + const w = bbox.width + h / 4 + (node.padding ?? 0); const radius = h / 2; const { cssStyles } = node; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/subroutine.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/subroutine.ts index ab24af29c87..746e684b837 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/subroutine.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/subroutine.ts @@ -37,9 +37,9 @@ export async function subroutine(parent: D3Selecti const { labelStyles, nodeStyles } = styles2String(node); node.labelStyle = labelStyles; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const halfPadding = (node?.padding || 0) / 2; - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; + const halfPadding = (node.padding ?? 0) / 2; + const w = bbox.width + (node.padding ?? 0); + const h = bbox.height + (node.padding ?? 0); const x = -bbox.width / 2 - halfPadding; const y = -bbox.height / 2 - halfPadding; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/text.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/text.ts index cf253fd1750..417b7b9409d 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/text.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/text.ts @@ -10,8 +10,8 @@ export async function text(parent: D3Selection, const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const totalWidth = Math.max(bbox.width + node.padding, node?.width || 0); - const totalHeight = Math.max(bbox.height + node.padding, node?.height || 0); + const totalWidth = Math.max(bbox.width + (node.padding ?? 0), node?.width || 0); + const totalHeight = Math.max(bbox.height + (node.padding ?? 0), node?.height || 0); const x = -totalWidth / 2; const y = -totalHeight / 2; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoid.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoid.ts index d0228d55bd8..1a0efee9a27 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoid.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/trapezoid.ts @@ -26,8 +26,8 @@ export async function trapezoid(parent: D3Selectio node.labelStyle = labelStyles; const { shapeSvg, bbox } = await labelHelper(parent, node, getNodeClasses(node)); - const w = bbox.width + node.padding; - const h = bbox.height + node.padding; + const w = bbox.width + (node.padding ?? 0); + const h = bbox.height + (node.padding ?? 0); const points = [ { x: (-3 * h) / 6, y: 0 }, { x: w + (3 * h) / 6, y: 0 }, diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.ts index 568a08c39cc..d06df9d97b1 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/util.ts @@ -63,7 +63,7 @@ export const labelHelper = async ( const halfPadding = (node?.padding ?? 0) / 2; if (useHtmlLabels) { - const div = text.children[0]; + const div = text.children[0] as HTMLDivElement; const dv = select(text); // if there are images, need to wait for them to load before getting the bounding box diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts index 0f1bfbecfd1..4a61a993716 100644 --- a/packages/mermaid/src/utils.ts +++ b/packages/mermaid/src/utils.ts @@ -918,6 +918,7 @@ export const encodeEntities = function (text: string): string { }; /** + * Partially reverts encoding done via {@link encodeEntities} * * @param text - text to be decoded * @returns