Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
6 changes: 6 additions & 0 deletions .changeset/parser-horizontal-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/message-parser": minor
"@rocket.chat/gazzodown": minor
---

Adds support for horizontal rules (thematic breaks) in the message parser. A line of 3 or more contiguous `-`, `*`, or `_` markers (with nothing else on the line) is parsed into a new `HORIZONTAL_RULE` block node and rendered with Fuselage's `Divider`. The node carries an optional `fallback` — a `[start, end]` offset span into the original source — so renderers without horizontal-rule support can slice the source to show the raw markup instead of dropping it, without duplicating the text into the AST. Spaced variants like `* * *` are intentionally not matched, to preserve the existing whitespace-emphasis behavior of `** **` / `__ __`.
8 changes: 8 additions & 0 deletions packages/gazzodown/src/Markup.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,12 @@ export const LineBreak: StoryObj<typeof Markup> = {
},
};

export const HorizontalRule: StoryObj<typeof Markup> = {
args: {
tokens: parse('above\n---\nbelow'),
},
};

export const Example: StoryObj<{ msg: string }> = {
render: ({ msg }) => {
const parseOptions: Options = { katex: { dollarSyntax: true, parenthesisSyntax: true }, colors: true, emoticons: true };
Expand Down Expand Up @@ -287,6 +293,8 @@ export const Example: StoryObj<{ msg: string }> = {
- [ ] Dishes
- [x] Laundry

---

> Cogito ergo sum.
> Sit amet, consectetur adipiscing elit.
> Donec eget ex euismod, euismod nisi euismod, vulputate nisi.
Expand Down
4 changes: 4 additions & 0 deletions packages/gazzodown/src/Markup.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Divider } from '@rocket.chat/fuselage';
import type * as MessageParser from '@rocket.chat/message-parser';
import { lazy, memo } from 'react';

Expand Down Expand Up @@ -65,6 +66,9 @@ const Markup = ({ tokens, source }: MarkupProps) => (
case 'LINE_BREAK':
return <br key={index} />;

case 'HORIZONTAL_RULE':
return <Divider key={index} />;

default: {
// Graceful degradation: blocks may carry a `fallback`. The current form is a
// `[start, end]` offset span into the source (sliced to render the raw markup);
Expand Down
25 changes: 23 additions & 2 deletions packages/message-parser/src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ export type LineBreak = {
value: undefined;
};

export type HorizontalRule = {
type: 'HORIZONTAL_RULE';
value: undefined;
// New form is a `[start, end]` offset span; the `Plain` form is kept in the
// type only to tolerate previously-persisted data at runtime.
fallback?: SourceRange | Plain;
};

export type KaTeX = {
type: 'KATEX';
value: string;
Expand Down Expand Up @@ -205,6 +213,7 @@ export type Types = {
LIST_ITEM: ListItem;
IMAGE: Image;
LINE_BREAK: LineBreak;
HORIZONTAL_RULE: HorizontalRule;
KATEX: KaTeX;
INLINE_KATEX: InlineKaTeX;
TIMESTAMP: Timestamp;
Expand All @@ -230,7 +239,8 @@ export type ASTNode =
| ChannelMention
| Emoji
| Color
| Tasks;
| Tasks
| HorizontalRule;

export type TypesKeys = keyof Types;

Expand All @@ -250,6 +260,17 @@ export type Inlines =
| Color
| InlineKaTeX;

export type Blocks = Code | Heading | Quote | SpoilerBlock | ListItem | Tasks | OrderedList | UnorderedList | LineBreak | KaTeX;
export type Blocks =
| Code
| Heading
| Quote
| SpoilerBlock
| ListItem
| Tasks
| OrderedList
| UnorderedList
| LineBreak
| KaTeX
| HorizontalRule;

export type Root = Array<Paragraph | Blocks> | [BigEmoji];
18 changes: 18 additions & 0 deletions packages/message-parser/src/grammar.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
image,
inlineCode,
inlineKatex,
horizontalRule,
italic,
katex,
lineBreak,
Expand Down Expand Up @@ -57,6 +58,7 @@ Blocks
= Blockquote
/ BlockSpoiler
/ Code
/ HorizontalRule
/ Heading
/ Tasks
/ OrderedList
Expand Down Expand Up @@ -239,6 +241,22 @@ KatexInlineEnd
*/
LineBreak = Space* EndOfLine { return lineBreak(); }

/**
*
* Horizontal Rule (thematic break)
* e.g: ---, ***, ___
*
* A line made up of 3+ contiguous copies of the same marker (-, * or _), with
* nothing else on the line (leading/trailing spaces allowed). Spaced variants
* (e.g. `* * *`) are intentionally not matched, to preserve the existing
* whitespace-emphasis behavior of `** **` / `__ __`.
*
*/
HorizontalRule = [ \t]* loc:HorizontalRuleMarkers [ \t]* (EndOfLine / !.) { return horizontalRule(loc); }

HorizontalRuleMarkers
= ("-" |3..| / "*" |3..| / "_" |3..|) { return [range().start, range().end]; }

/**
*
* Paragraph
Expand Down
7 changes: 7 additions & 0 deletions packages/message-parser/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
Link,
Timestamp,
SourceRange,
HorizontalRule,
} from './definitions';

const generate =
Expand Down Expand Up @@ -270,6 +271,12 @@ export const lineBreak = (): LineBreak => ({
value: undefined,
});

export const horizontalRule = (fallback?: SourceRange): HorizontalRule => ({
type: 'HORIZONTAL_RULE',
value: undefined,
...(fallback !== undefined && { fallback }),
});

export const katex = (content: string): KaTeX => ({
type: 'KATEX',
value: content,
Expand Down
6 changes: 6 additions & 0 deletions packages/message-parser/tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ export const lineBreak = () => ({
value: undefined,
});

export const horizontalRule = (fallback?: [number, number]) => ({
type: 'HORIZONTAL_RULE' as const,
value: undefined,
...(fallback !== undefined ? { fallback } : {}),
});

export const katex = (value: string) => ({
type: 'KATEX' as const,
value,
Expand Down
33 changes: 33 additions & 0 deletions packages/message-parser/tests/horizontalRule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { parse } from '../src';
import { bold, horizontalRule, paragraph, plain } from './helpers';

// `fallback` is the [start, end] offset span of the marker run in the source
test.each([
['---', [0, 3] as [number, number]],
['***', [0, 3] as [number, number]],
['___', [0, 3] as [number, number]],
['----------', [0, 10] as [number, number]],
[' --- ', [3, 6] as [number, number]],
])('parses %p as a horizontal rule', (input, range) => {
expect(parse(input)).toEqual([horizontalRule(range)]);
// the offsets slice back to the markers
expect(input.slice(range[0], range[1])).toBe(input.trim());
});

test('parses a horizontal rule between paragraphs', () => {
expect(parse('above\n---\nbelow')).toEqual([paragraph([plain('above')]), horizontalRule([6, 9]), paragraph([plain('below')])]);
});

test.each([
// fewer than three markers -> plain text
['--', [paragraph([plain('--')])]],
['**', [paragraph([plain('**')])]],
// emphasis must not be swallowed by the rule (parsed as a paragraph, not a rule)
['***bold italic***', [paragraph([plain('*'), bold([plain('bold italic')]), plain('*')])]],
// spaced markers are whitespace-emphasis (plain), not a rule
['** **', [paragraph([plain('** **')])]],
// list item, not a rule
['- item', [{ type: 'UNORDERED_LIST', value: [{ type: 'LIST_ITEM', value: [plain('item')] }] }]],
])('does not treat %p as a horizontal rule', (input, output) => {
expect(parse(input)).toEqual(output);
});
Loading