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
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import React from 'react';
import { EnrichedMarkdownTextStory } from '../EnrichedMarkdownTextStory';
import { storyMeta } from '../shared/storyMeta';
import {
admonitionStyledDefaults,
githubFlavorArgTypes,
numberControl,
type AdmonitionStyleControls,
} from '../shared/storybookMarkdownStyles';
import {
splitStyleControls,
toAdmonitionStyle,
} from '../shared/storybookStyleBuilders';
import type { StoryArgs, TextStory } from '../shared/storyTypes';

// The blockquote style controls plus a boolean toggle wired to
// md4cFlags.admonitions (on/off instead of an object).
type AdmonitionControls = AdmonitionStyleControls & {
admonitionsEnabled: boolean;
};

const ALL_TYPES_MARKDOWN = `> [!NOTE]
> Highlights information that users should take into account, even when skimming.

> [!TIP]
> Optional information to help a user be more successful.

> [!IMPORTANT]
> Key information users need to know to achieve their goal.

> [!WARNING]
> Urgent info that needs immediate user attention to avoid problems.

> [!CAUTION]
> Advises about risks or negative outcomes of certain actions.

> Control text showing default blockquote`;

const BLOCK_ELEMENTS_MARKDOWN = `> [!IMPORTANT]
> An admonition can hold many block elements:
>
> ## A heading inside the alert
>
> A paragraph with **bold**, _italic_ and a [link](https://swmansion.com).
>
> - first bullet
> - second bullet
>
> 1. ordered one
> 2. ordered two
>
> \`\`\`ts
> const answer = 42;
> \`\`\`
>
> | Column A | Column B |
> | -------- | -------- |
> | 1 | 2 |
>
> and a trailing paragraph.`;

const NESTED_MARKDOWN = `> [!WARNING]
> An outer warning alert.
>
> > [!TIP]
> > A tip nested inside the warning.
> >
> > > [!NOTE]
> > > And a note nested one level deeper - each level is its own themed container.`;

const argTypes = {
admonitionsEnabled: {
control: 'boolean',
description:
'md4cFlags.admonitions - turn the extension off to render plain blockquotes (also forced off when flavor="commonmark")',
},
fontSize: numberControl('markdownStyle.blockquote.fontSize', {
min: 12,
max: 24,
step: 1,
}),
borderWidth: numberControl('markdownStyle.blockquote.borderWidth', {
min: 1,
max: 8,
step: 1,
}),
gapWidth: numberControl('markdownStyle.blockquote.gapWidth', {
min: 0,
max: 32,
step: 2,
}),
borderRadius: numberControl('markdownStyle.blockquote.borderRadius', {
min: 0,
max: 16,
step: 1,
}),
padding: numberControl('markdownStyle.blockquote.padding', {
min: 0,
max: 32,
step: 2,
}),
noteColor: {
control: 'color',
description: 'markdownStyle.blockquote.admonitions.note.color',
},
noteBackgroundColor: {
control: 'color',
description: 'markdownStyle.blockquote.admonitions.note.backgroundColor',
},
tipColor: {
control: 'color',
description: 'markdownStyle.blockquote.admonitions.tip.color',
},
tipBackgroundColor: {
control: 'color',
description: 'markdownStyle.blockquote.admonitions.tip.backgroundColor',
},
importantColor: {
control: 'color',
description: 'markdownStyle.blockquote.admonitions.important.color',
},
importantBackgroundColor: {
control: 'color',
description:
'markdownStyle.blockquote.admonitions.important.backgroundColor',
},
warningColor: {
control: 'color',
description: 'markdownStyle.blockquote.admonitions.warning.color',
},
warningBackgroundColor: {
control: 'color',
description: 'markdownStyle.blockquote.admonitions.warning.backgroundColor',
},
cautionColor: {
control: 'color',
description: 'markdownStyle.blockquote.admonitions.caution.color',
},
cautionBackgroundColor: {
control: 'color',
description: 'markdownStyle.blockquote.admonitions.caution.backgroundColor',
},
};

function renderAdmonition(
title: string,
description: string,
args: StoryArgs<AdmonitionControls>
) {
const { admonitionsEnabled = true, ...styleArgs } = args;
const { controls, rest } = splitStyleControls(
styleArgs,
admonitionStyledDefaults
);
return (
<EnrichedMarkdownTextStory
title={title}
description={description}
{...rest}
md4cFlags={{ admonitions: admonitionsEnabled }}
style={{ blockquote: toAdmonitionStyle(controls) }}
/>
);
}

const flavorArgTypes = githubFlavorArgTypes(
'Admonitions require flavor="github". commonmark forces the extension off, so `> [!NOTE]` renders as a plain blockquote.'
);

const admonitionStoryBase = {
argTypes: { ...argTypes, ...flavorArgTypes },
args: {
...admonitionStyledDefaults,
admonitionsEnabled: true,
flavor: 'github' as const,
},
};

export default storyMeta('Block', 'Admonitions');

export const AllTypes: TextStory<AdmonitionControls> = {
...admonitionStoryBase,
args: {
...admonitionStoryBase.args,
markdown: ALL_TYPES_MARKDOWN,
},
render: (args) =>
renderAdmonition(
'Admonitions',
'Every GitHub alert type (note, tip, important, warning, caution), each a themed blockquote with an icon + title header. Toggle "admonitionsEnabled" off, or flip flavor to commonmark, to render them as plain blockquotes; tune the per-type colors via the controls.',
args
),
};

export const WithBlockElements: TextStory<AdmonitionControls> = {
...admonitionStoryBase,
args: {
...admonitionStoryBase.args,
markdown: BLOCK_ELEMENTS_MARKDOWN,
},
render: (args) =>
renderAdmonition(
'Admonition with Block Elements',
'An admonition holding a heading, lists, a fenced code block and a table - each rendered as its own segment inside the alert container.',
args
),
};

export const Nested: TextStory<AdmonitionControls> = {
...admonitionStoryBase,
args: {
...admonitionStoryBase.args,
markdown: NESTED_MARKDOWN,
},
render: (args) =>
renderAdmonition(
'Nested Admonition',
'Admonitions nested three levels deep - each level is its own recursive themed container.',
args
),
};
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,35 @@ export const blockquoteStyledDefaults: BlockquoteStyleControls = {
padding: 0,
};

// Admonitions inherit the blockquote geometry controls and add a per-type color
// pair. An empty background renders transparent (no fill).
export type AdmonitionStyleControls = BlockquoteStyleControls & {
noteColor: string;
noteBackgroundColor: string;
tipColor: string;
tipBackgroundColor: string;
importantColor: string;
importantBackgroundColor: string;
warningColor: string;
warningBackgroundColor: string;
cautionColor: string;
cautionBackgroundColor: string;
};

export const admonitionStyledDefaults: AdmonitionStyleControls = {
...blockquoteStyledDefaults,
noteColor: '#0969da',
noteBackgroundColor: '',
tipColor: '#1a7f37',
tipBackgroundColor: '',
importantColor: '#8250df',
importantBackgroundColor: '',
warningColor: '#9a6700',
warningBackgroundColor: '',
cautionColor: '#cf222e',
cautionBackgroundColor: '',
};

export type CodeBlockStyleControls = {
fontSize: number;
fontFamily: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { MarkdownStyle } from 'react-native-enriched-markdown';
import type { StoryArgs } from './storyTypes';
import type {
AdmonitionStyleControls,
BlockquoteStyleControls,
CodeBlockStyleControls,
EmphasisStyleControls,
Expand Down Expand Up @@ -109,6 +110,39 @@ export function toBlockquoteStyle(
};
}

function admonitionColors(color: string, backgroundColor: string) {
return {
...(color ? { color } : {}),
...(backgroundColor ? { backgroundColor } : {}),
};
}

// Blockquote style with the admonition palette nested under it. Admonitions
// reuse the blockquote geometry and only theme colors per type.
export function toAdmonitionStyle(
controls: AdmonitionStyleControls
): NonNullable<MarkdownStyle['blockquote']> {
return {
...toBlockquoteStyle(controls),
admonitions: {
note: admonitionColors(controls.noteColor, controls.noteBackgroundColor),
tip: admonitionColors(controls.tipColor, controls.tipBackgroundColor),
important: admonitionColors(
controls.importantColor,
controls.importantBackgroundColor
),
warning: admonitionColors(
controls.warningColor,
controls.warningBackgroundColor
),
caution: admonitionColors(
controls.cautionColor,
controls.cautionBackgroundColor
),
},
};
}

export function toCodeBlockStyle(
controls: CodeBlockStyleControls
): NonNullable<MarkdownStyle['codeBlock']> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ static_assert(static_cast<int>(NodeType::Highlight) == 29,
"NodeType enum must stay in sync with Kotlin MarkdownASTNode.NodeType");
static_assert(static_cast<int>(NodeType::SoftBreak) == 30,
"NodeType enum must stay in sync with Kotlin MarkdownASTNode.NodeType");
static_assert(static_cast<int>(NodeType::Admonition) == 31,
"NodeType enum must stay in sync with Kotlin MarkdownASTNode.NodeType");

local_ref<JMarkdownASTNode> createJavaNode(const std::shared_ptr<MarkdownASTNode> &node) {
if (!node) {
Expand Down Expand Up @@ -69,6 +71,7 @@ Md4cFlags JMd4cFlags::toCppFlags() const {
static const auto highlightField = javaClassStatic()->getField<jboolean>("highlight");
static const auto permissiveAutolinksField = javaClassStatic()->getField<jboolean>("permissiveAutolinks");
static const auto hardSoftBreaksField = javaClassStatic()->getField<jboolean>("hardSoftBreaks");
static const auto admonitionsField = javaClassStatic()->getField<jboolean>("admonitions");

Md4cFlags flags;
flags.underline = getFieldValue(underlineField) == JNI_TRUE;
Expand All @@ -78,6 +81,7 @@ Md4cFlags JMd4cFlags::toCppFlags() const {
flags.highlight = getFieldValue(highlightField) == JNI_TRUE;
flags.permissiveAutolinks = getFieldValue(permissiveAutolinksField) == JNI_TRUE;
flags.hardSoftBreaks = getFieldValue(hardSoftBreaksField) == JNI_TRUE;
flags.admonitions = getFieldValue(admonitionsField) == JNI_TRUE;
return flags;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ data class MarkdownASTNode(
Subscript,
Highlight,
SoftBreak,
Admonition,
}

fun getAttribute(key: String): String? = attributes[key]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class Md4cFlags(
val highlight: Boolean = false,
val permissiveAutolinks: Boolean = true,
val hardSoftBreaks: Boolean = false,
val admonitions: Boolean = false,
) {
companion object {
val DEFAULT = Md4cFlags()
Expand Down
19 changes: 19 additions & 0 deletions packages/core/cpp/parser/MD4CParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class MD4CParser::Impl {
static const std::string ATTR_IS_TASK;
static const std::string ATTR_TASK_CHECKED;
static const std::string ATTR_START;
static const std::string ATTR_ADMONITION_TYPE;

void reset(size_t estimatedDepth) {
root = std::make_shared<MarkdownASTNode>(NodeType::Document);
Expand Down Expand Up @@ -109,6 +110,19 @@ class MD4CParser::Impl {
break;
}

case MD_BLOCK_ADMONITION: {
auto node = std::make_shared<MarkdownASTNode>(NodeType::Admonition);
if (detail) {
auto *adm = static_cast<MD_BLOCK_ADMONITION_DETAIL *>(detail);
std::string admonitionType = impl->getAttributeText(&adm->type);
if (!admonitionType.empty()) {
node->setAttribute(ATTR_ADMONITION_TYPE, admonitionType);
}
}
impl->pushNode(node);
break;
}

case MD_BLOCK_UL: {
impl->pushNode(std::make_shared<MarkdownASTNode>(NodeType::UnorderedList));
break;
Expand Down Expand Up @@ -486,6 +500,7 @@ bool isBlockNode(const MarkdownASTNode &node) {
case NodeType::Paragraph:
case NodeType::Heading:
case NodeType::Blockquote:
case NodeType::Admonition:
case NodeType::UnorderedList:
case NodeType::OrderedList:
case NodeType::ListItem:
Expand Down Expand Up @@ -611,6 +626,9 @@ std::shared_ptr<MarkdownASTNode> MD4CParser::parse(const std::string &markdown,
if (md4cFlags.hardSoftBreaks) {
flags |= MD_FLAG_HARD_SOFT_BREAKS;
}
if (md4cFlags.admonitions) {
flags |= MD_FLAG_ADMONITIONS;
}

// Configure MD4C parser with callbacks
MD_PARSER parser = {
Expand Down Expand Up @@ -647,5 +665,6 @@ const std::string MD4CParser::Impl::ATTR_LANGUAGE = "language";
const std::string MD4CParser::Impl::ATTR_IS_TASK = "isTask";
const std::string MD4CParser::Impl::ATTR_TASK_CHECKED = "taskChecked";
const std::string MD4CParser::Impl::ATTR_START = "start";
const std::string MD4CParser::Impl::ATTR_ADMONITION_TYPE = "admonitionType";

} // namespace Markdown
Loading
Loading