Skip to content
Open
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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- `searchListPredicate` property: Allows to filter the complete list of search options at once.
- Following optional BlueprintJs properties are forwarded now to override default behaviour: `noResults`, `createNewItemRenderer` and `itemRenderer`
- `isValidNewOption` property: Checks if an input string is or can be turned into a valid new option.
- `<Markdown />`
- Added `cutOff` property to set maximum number of raw Markdown characters to render
- new `utils` methods:
- `truncateMarkdownDisplay`: helper function to iterate over `Markdown` renderings to improve the experienced `cutOff` value

### Fixed

Expand All @@ -31,6 +35,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- if you forward properties then they cannot have `Color` as type, use `ColorLike`
- `<MultiSelect />`
- by default, if no searchPredicate or searchListPredicate is defined, the filtering is done via case-insensitive multi-word filtering.
- `<StringPreviewContentBlobToggler />` uses now the `Markdown.cutOff` property
- this enables Markdown rendering even if the preview need to be shortened
- this may lead to slightly different preview lengths

### Deprecated

Expand Down Expand Up @@ -199,7 +206,7 @@ This is a major release, and it might be not compatible with your current usage
- Add `ModalContext` to track open/close state of all used application modals.
- Add `modalId` property to give a modal a unique ID for tracking purposes.
- `preventReactFlowEvents`: adds 'nopan', 'nowheel' and 'nodrag' classes to overlay classes in order to prevent react-flow to react to drag and pan actions in modals.
- new `utils` methods
- new `utils` methods
- `colorCalculateDistance()`: calculates the difference between 2 colors using the simple CIE76 formula
- `textToColorHash()`: calculates a color from a text string
- `reduceToText`: shrinks HTML content and React elements to plain text, used for `<TextReducer />`
Expand Down
18 changes: 15 additions & 3 deletions src/cmem/ContentBlobToggler/StringPreviewContentBlobToggler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";

import { utils } from "../../common";
import InlineText from "../../components/Typography/InlineText";
import { Markdown } from "../markdown/Markdown";
import { Markdown, markdownAllowedInlineElements } from "../markdown/Markdown";

import { ContentBlobToggler, ContentBlobTogglerProps } from "./ContentBlobToggler";

Expand Down Expand Up @@ -57,7 +57,7 @@ export function StringPreviewContentBlobToggler({
startExtended,
useOnly,
renderPreviewAsMarkdown = false,
allowedHtmlElementsInPreview,
allowedHtmlElementsInPreview = markdownAllowedInlineElements,
noTogglerContentSuffix,
firstNonEmptyLineOnly,
...otherContentBlobTogglerProps
Expand Down Expand Up @@ -90,7 +90,19 @@ export function StringPreviewContentBlobToggler({
previewMaxLength &&
utils.reduceToText(previewContent, { decodeHtmlEntities: true }).length > previewMaxLength
) {
previewContent = utils.reduceToText(previewContent, { decodeHtmlEntities: true }).slice(0, previewMaxLength);
previewContent = renderPreviewAsMarkdown
? utils.truncateMarkdownDisplay(
<Markdown
key="markdown-content"
allowedElements={allowedHtmlElementsInPreview}
cutOff={previewMaxLength}
cutOffSuffix={""}
>
{previewString}
</Markdown>,
{ decodeHtmlEntities: true },
)
: utils.reduceToText(previewContent, { decodeHtmlEntities: true }).slice(0, previewMaxLength);
enableToggler = true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const Template: StoryFn<typeof StringPreviewContentBlobToggler> = (args) => (
);

const initialTeststring =
"A library for GUI elements.\nIn order to create graphical user interfaces, please have look at the documentation at [Github](https://github.com/eccenca/gui-elements).";
"# A library for [GUI elements](https://github.com/eccenca/gui-elements).\nIn order to create graphical user interfaces, please\n* have look at the documentation at [Github](https://github.com/eccenca/gui-elements).";

export const Default = Template.bind({});
Default.args = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@ describe("StringPreviewContentBlobToggler", () => {
{...(StringPreviewContentBlobTogglerStory.args as StringPreviewContentBlobTogglerProps)}
/>,
);
textMustExist(queryByText, "A library for GUI elements.");
textMustNotExist(
queryByText,
"In order to create graphical user interfaces, please have look at the documentation at",
);
textMustExist(queryByText, "A library for");
textMustNotExist(queryByText, "documentation at");
textMustExist(queryByText, "show more");
});
it("should display full view if `startExtended` is enabled, and show toggler to reduce", () => {
Expand All @@ -37,10 +34,8 @@ describe("StringPreviewContentBlobToggler", () => {
startExtended
/>,
);
textMustExist(
queryByText,
"In order to create graphical user interfaces, please have look at the documentation at",
);
textMustExist(queryByText, "In order to create graphical user interfaces, please");
textMustExist(queryByText, "have look at the documentation at");
textMustExist(queryByText, "show less");
});
it('should display only first content line on `useOnly={"firstNonEmptyLine"}`', () => {
Expand All @@ -50,7 +45,7 @@ describe("StringPreviewContentBlobToggler", () => {
useOnly={"firstNonEmptyLine"}
/>,
);
textMustExist(queryByText, "A library for GUI elements.");
textMustExist(queryByText, "A library for");
textMustNotExist(queryByText, "In order to create");
});
it('should use first Markdown paragraph as preview content on `useOnly={"firstMarkdownSection"}` but shorten it', () => {
Expand All @@ -60,9 +55,9 @@ describe("StringPreviewContentBlobToggler", () => {
useOnly={"firstMarkdownSection"}
/>,
);
textMustExist(queryByText, "A library for GUI elements.");
textMustExist(queryByText, "A library for");
textMustExist(queryByText, "In order to create");
textMustNotExist(queryByText, "please have look at the documentation at");
textMustNotExist(queryByText, "documentation at");
});
it("should display full preview and no toggler if content is short enough", () => {
const { queryByText } = render(
Expand All @@ -71,11 +66,9 @@ describe("StringPreviewContentBlobToggler", () => {
previewMaxLength={144}
/>,
);
textMustExist(queryByText, "A library for GUI elements.");
textMustExist(
queryByText,
"In order to create graphical user interfaces, please have look at the documentation at",
);
textMustExist(queryByText, "A library for");
textMustExist(queryByText, "In order to create graphical user interfaces, please");
textMustExist(queryByText, "have look at the documentation at");
textMustNotExist(queryByText, "https://github.com/"); // test if Markdown was rendered
textMustNotExist(queryByText, "show more");
});
Expand All @@ -87,11 +80,7 @@ describe("StringPreviewContentBlobToggler", () => {
renderPreviewAsMarkdown={false}
/>,
);
textMustExist(queryByText, "A library for GUI elements.");
textMustExist(
queryByText,
"In order to create graphical user interfaces, please have look at the documentation at",
);
textMustExist(queryByText, "A library for [GUI elements]"); // raw Markdown link syntax visible
textMustExist(queryByText, "https://github.com/"); // test if Markdown was rendered
textMustExist(queryByText, "show more");
});
Expand Down
38 changes: 38 additions & 0 deletions src/cmem/markdown/Markdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,41 @@ A line with some <strong>HTML code</strong> inside.
[^1]: This is the text related to the the footnote referrer.
`,
};

export const CutOff = Template.bind({});

CutOff.args = {
children: `
This component renders Markdown content safely. It supports **GitHub Flavoured Markdown**, syntax highlighting for code blocks, and definition lists.

You can:
* configure _link targets_
* add custom __rehype__ plugins
* and filter content through an allowed elements list

A third paragraph that will not appear once the cutOff limit is reached.
`,
cutOff: 300,
};

export const CutOffWithCodeFence = Template.bind({});

CutOffWithCodeFence.args = {
children: `
A short paragraph before the code block.

Here is an important code example:

\`\`\`json
{
"host": "localhost",
"port": 8080,
"debug": true
}
\`\`\`

This paragraph comes after the code block and should not appear when the cutOff limit falls inside the fence above.
`,
cutOff: 110,
cutOffSuffix: "...",
};
102 changes: 66 additions & 36 deletions src/cmem/markdown/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { TestableComponent } from "../../components";
import { HtmlContentBlock, HtmlContentBlockProps } from "../../components/Typography";
import { CLASSPREFIX as eccgui } from "../../configuration/constants";

import utils from "./markdown.utils";

const DEFAULT_CUTOFF_SUFFIX = "...";

export interface MarkdownProps extends TestableComponent {
children: string;
/**
Expand Down Expand Up @@ -47,8 +51,63 @@ export interface MarkdownProps extends TestableComponent {
* Configure the `HtmlContentBlock` component that is automatically used as wrapper for the parsed Markdown content.
*/
htmlContentBlockProps?: Omit<HtmlContentBlockProps, "children" | "className" | "data-test-id">;
/**
* Maximum number of raw Markdown characters to render.
* Content exceeding this limit is truncated at the nearest safe paragraph
* boundary (or word boundary as fallback) to preserve Markdown structure.
* No truncation when absent or ≤ 0.
*/
cutOff?: number;
/**
* Text appended as a trailing paragraph when content is truncated by `cutOff`.
* Set to `""` to suppress the indicator entirely.
* Defaults to `"..."`.
*/
cutOffSuffix?: string;
}

export const markdownAllowedInlineElements = [
// default markdown
"a",
"code",
"em",
"img",
"strong",
// gfm (Github Flavoured Markdown) extensions
"del",
// other stuff
"mark",
];

export const markdownAllowedBlockElements = [
// default markdown
"blockquote",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"hr",
"li",
"ol",
"p",
"pre",
"ul",
// gfm (Github Flavoured Markdown) extensions
"input",
"table",
"tbody",
"td",
"th",
"thead",
"tr",
// other stuff
"dl",
"dt",
"dd",
];

const configDefault = {
/*
Using React Markdown configuration
Expand All @@ -58,41 +117,7 @@ const configDefault = {
remarkPlugins: [remarkGfm, remarkDefinitionList] as PluggableList,
// @see https://github.com/rehypejs/rehype/blob/main/doc/plugins.md#list-of-plugins
rehypePlugins: [] as PluggableList,
allowedElements: [
// default markdown
"a",
"blockquote",
"code",
"em",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"hr",
"img",
"li",
"ol",
"p",
"pre",
"strong",
"ul",
// gfm (Github Flavoured Markdown) extensions
"del",
"input",
"table",
"tbody",
"td",
"th",
"thead",
"tr",
// other stuff
"mark",
"dl",
"dt",
"dd",
],
allowedElements: [...markdownAllowedInlineElements, ...markdownAllowedBlockElements],
// remove all unwanted HTML markup
unwrapDisallowed: true,
// show escaped HTML
Expand All @@ -109,8 +134,13 @@ export const Markdown = ({
reHypePlugins,
linkTargetName = "_mdref",
htmlContentBlockProps,
cutOff,
cutOffSuffix = DEFAULT_CUTOFF_SUFFIX,
...otherProps
}: MarkdownProps) => {
const renderContent =
cutOff !== undefined && cutOff > 0 ? utils.truncateMarkdown(children, cutOff, cutOffSuffix) : children;

const configHtmlExternalLinks = {
rel: ["nofollow"],
target: linkTargetName,
Expand All @@ -136,7 +166,7 @@ export const Markdown = ({
: {};

const reactMarkdownProperties = {
children: children.trim(),
children: renderContent.trim(),
...configDefault,
...configHtml,
...configTextOnly,
Expand Down
Loading
Loading