Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f128bb6
fix(richtext)!: HTML and Markdown parsing for links and edge cases
maoberlehner Nov 7, 2025
104c75f
Merge branch 'main' into bugfix/richtext-html-parser
alexjoverm Feb 9, 2026
b1048b3
fix(richtext): convert 6 needed nodes to underscode case
alexjoverm Feb 9, 2026
055c4b6
Merge branch 'main' into bugfix/richtext-html-parser
alexjoverm Feb 12, 2026
6fc767c
chore: upgrade playground
alexjoverm Feb 12, 2026
8994436
chore: refactor marks and nodes into a centralized extensions helpers
alexjoverm Feb 12, 2026
c5855b8
chore: revamp the Richtext renderer to use extensions instead of
alexjoverm Feb 12, 2026
fb1be37
test: add edge cases to HTML tests from the registered support issues
alexjoverm Feb 12, 2026
7c6a64a
chore: Update packages/richtext/src/extensions/nodes.ts
alexjoverm Feb 12, 2026
3a32d14
chore: Update packages/richtext/playground/vanilla/.env
alexjoverm Feb 12, 2026
28fc859
chore: revert ignore
alexjoverm Feb 12, 2026
34d6d86
feat: add tiptapExtensions for modern overrides
alexjoverm Feb 12, 2026
e7c0dce
feat: generate linear list of richtext content
dipankarmaikap Feb 18, 2026
7bd2283
Merge branch main' into bugfix/richtext-html-parser
alexjoverm Feb 23, 2026
98e0811
feat!: remove custom resolvers in favor of tiptap extensions
alexjoverm Feb 23, 2026
bdc2fcd
chore: update playgrounds
alexjoverm Feb 23, 2026
df697eb
chore(richtext): add extra tests to cover both-ways functionality
alexjoverm Feb 24, 2026
2ca560c
test(richtext): clean up and purge all unnecessary tests
alexjoverm Feb 24, 2026
74555c2
qs: apply bugbot fixes
alexjoverm Feb 24, 2026
cbf6784
qs: add name and type as context
alexjoverm Feb 24, 2026
47bfdf3
test: ensure the right nodes, marks and extensions are the ones to be
alexjoverm Feb 24, 2026
457ce44
fix: mailto duplication issue
alexjoverm Feb 24, 2026
5c6bdaa
chore: update lockfile
alexjoverm Feb 24, 2026
603133e
qs: fix null outputs, add globals exclusions
alexjoverm Feb 24, 2026
c1963b2
qs: fix textStyle node, fix playgrounds
alexjoverm Feb 24, 2026
a654352
qs: lint playgrounds
alexjoverm Feb 24, 2026
6b0bfad
qs: add thead/tbody support, fix _id issue
alexjoverm Feb 24, 2026
a5c1dec
qs: fix id issue
alexjoverm Feb 24, 2026
a4edafb
chore: normalize null/undefined values, clean up personal data in tests
alexjoverm Feb 25, 2026
109b338
fix: prevent empty values on href, add asTag for better typing
alexjoverm Feb 25, 2026
d97f3c9
chore: fix test:types race condition by run it after build
alexjoverm Feb 25, 2026
45388cd
chore: revert
alexjoverm Feb 25, 2026
63438e4
chore: astro richtext playground update
dipankarmaikap Feb 26, 2026
7e651a5
refactor(astro): drop experimental richTextToHTML
dipankarmaikap Feb 26, 2026
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
29 changes: 26 additions & 3 deletions packages/richtext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,32 @@
"release:dry": "release-it --dry-run"
},
"dependencies": {
"markdown-it": "^14.1.0",
"markdown-it-github": "^0.5.0",
"node-html-parser": "^7.0.1"
"@tiptap/core": "^3.10.2",
"@tiptap/extension-blockquote": "^3.10.2",
"@tiptap/extension-bold": "^3.10.2",
"@tiptap/extension-code": "^3.10.2",
"@tiptap/extension-code-block": "^3.10.2",
"@tiptap/extension-details": "^3.10.2",
"@tiptap/extension-document": "^3.10.2",
"@tiptap/extension-emoji": "^3.10.2",
"@tiptap/extension-hard-break": "^3.10.2",
"@tiptap/extension-heading": "^3.10.2",
"@tiptap/extension-highlight": "^3.10.2",
"@tiptap/extension-horizontal-rule": "^3.10.2",
"@tiptap/extension-image": "^3.10.2",
"@tiptap/extension-italic": "^3.10.2",
"@tiptap/extension-link": "^3.10.2",
"@tiptap/extension-list": "^3.10.2",
"@tiptap/extension-paragraph": "^3.10.2",
"@tiptap/extension-strike": "^3.10.2",
"@tiptap/extension-subscript": "^3.10.2",
"@tiptap/extension-superscript": "^3.10.2",
"@tiptap/extension-table": "^3.10.2",
"@tiptap/extension-text": "^3.10.2",
"@tiptap/extension-text-style": "^3.10.2",
"@tiptap/extension-underline": "^3.10.2",
"@tiptap/html": "^3.10.2",
"markdown-it": "^14.1.0"
},
"devDependencies": {
"@arethetypeswrong/core": "^0.18.2",
Expand Down
153 changes: 42 additions & 111 deletions packages/richtext/src/html-parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { htmlToStoryblokRichtext } from './html-parser';
import { BlockTypes } from './types';
import Heading from '@tiptap/extension-heading';

describe('htmlToStoryblokRichtext', () => {
it('parses headings', () => {
Expand Down Expand Up @@ -62,10 +62,10 @@
type: 'doc',
content: [
{
type: 'bullet_list',
type: 'bulletList',
content: [
{
type: 'list_item',
type: 'listItem',
content: [
{
type: 'paragraph',
Expand All @@ -76,7 +76,7 @@
],
},
{
type: 'list_item',
type: 'listItem',
content: [
{
type: 'paragraph',
Expand All @@ -99,10 +99,10 @@
type: 'doc',
content: [
{
type: 'ordered_list',
type: 'orderedList',
content: [
{
type: 'list_item',
type: 'listItem',
content: [
{
type: 'paragraph',
Expand All @@ -113,7 +113,7 @@
],
},
{
type: 'list_item',
type: 'listItem',
content: [
{
type: 'paragraph',
Expand Down Expand Up @@ -183,7 +183,7 @@
type: 'doc',
content: [
{
type: 'code_block',
type: 'codeBlock',
attrs: { language: 'js' },
content: [
{ type: 'text', text: 'const foo = "bar";\nconsole.log(foo);\n' },
Expand All @@ -206,7 +206,7 @@
type: 'tableRow',
content: [
{
type: 'tableCell',
type: 'tableHeader',
content: [
{
type: 'paragraph',
Expand All @@ -216,7 +216,7 @@
attrs: { colspan: 1, rowspan: 1, colwidth: null },
},
{
type: 'tableCell',
type: 'tableHeader',
content: [
{
type: 'paragraph',
Expand All @@ -226,7 +226,7 @@
attrs: { colspan: 1, rowspan: 1, colwidth: null },
},
{
type: 'tableCell',
type: 'tableHeader',
content: [
{
type: 'paragraph',
Expand Down Expand Up @@ -389,43 +389,6 @@
});
});

it('distinguishes between anchor and link', () => {
const result = htmlToStoryblokRichtext(
'<a id="some-id">Anchor</a><a href="/home">Link</a>',
);
expect(result).toEqual({
type: 'doc',
content: [
{
text: 'Anchor',
type: 'text',
marks: [
{
type: 'anchor',
attrs: {
anchor: 'some-id',
},
content: [],
},
],
},
{
text: 'Link',
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: '/home',
},
content: [],
},
],
},
],
});
});

it('parses strikethrough (delete, s) marks', () => {
const html = '<p>This is <del>deleted text</del> <s>deleted text</s>.</p>';
const result = htmlToStoryblokRichtext(html);
Expand Down Expand Up @@ -495,7 +458,7 @@
],
},
{
type: 'horizontal_rule',
type: 'horizontalRule',
},
{
type: 'paragraph',
Expand All @@ -517,7 +480,7 @@
type: 'paragraph',
content: [
{ type: 'text', text: 'Line with a hard break here.' },
{ type: 'hard_break' },
{ type: 'hardBreak' },
{ type: 'text', text: 'Next line after break.' },
],
},
Expand Down Expand Up @@ -548,8 +511,8 @@
type: 'text',
text: 'bold and italic',
marks: [
{ type: 'italic' },
{ type: 'bold' },
{ type: 'italic' },
],
},
],
Expand All @@ -566,8 +529,8 @@
type: 'text',
text: 'italic',
marks: [
{ type: 'italic' },
{ type: 'bold' },
{ type: 'italic' },
],
},
],
Expand Down Expand Up @@ -658,10 +621,10 @@
},
},
{
type: 'italic',
type: 'bold',
},
{
type: 'bold',
type: 'italic',
},
],
text: 'bold and italic link',
Expand Down Expand Up @@ -758,7 +721,7 @@
const resultDefault = htmlToStoryblokRichtext(
'<p class="unsupported">Hello <a data-unsupported-custom-attribute="whatever" target="_blank" href="/home">world!</a></p>',
);
expect(resultDefault).toEqual({

Check failure on line 724 in packages/richtext/src/html-parser.test.ts

View workflow job for this annotation

GitHub Actions / build

src/html-parser.test.ts > htmlToStoryblokRichtext > does not preserve unsupported attributes by default

AssertionError: expected { type: 'doc', content: [ { …(2) } ] } to deeply equal { type: 'doc', content: [ { …(2) } ] } - Expected + Received @@ -12,10 +12,11 @@ "attrs": { "class": null, "href": "/home", "rel": "noopener noreferrer", "target": "_blank", + "title": null, }, "type": "link", }, ], "text": "world!", ❯ src/html-parser.test.ts:724:27
type: 'doc',
content: [
{
Expand All @@ -775,10 +738,11 @@
{
type: 'link',
attrs: {
class: null,
href: '/home',
rel: 'noopener noreferrer',
target: '_blank',
},
content: [],
},
],
},
Expand All @@ -795,7 +759,7 @@
allowCustomAttributes: true,
},
);
expect(resultAllowCustomAttributes).toEqual({

Check failure on line 762 in packages/richtext/src/html-parser.test.ts

View workflow job for this annotation

GitHub Actions / build

src/html-parser.test.ts > htmlToStoryblokRichtext > preserves custom attributes on <a> when allowCustomAttributes is true

AssertionError: expected { type: 'doc', content: [ { …(2) } ] } to deeply equal { type: 'doc', content: [ { …(2) } ] } - Expected + Received @@ -15,10 +15,11 @@ "data-supported-custom-attribute": "whatever", }, "href": "/home", "rel": "noopener noreferrer", "target": "_blank", + "title": null, }, "type": "link", }, ], "text": "world!", ❯ src/html-parser.test.ts:762:41
type: 'doc',
content: [
{
Expand All @@ -812,13 +776,14 @@
{
type: 'link',
attrs: {
class: null,
custom: {
'data-supported-custom-attribute': 'whatever',
},
href: '/home',
rel: 'noopener noreferrer',
target: '_blank',
},
content: [],
},
],
},
Expand All @@ -830,7 +795,7 @@

it('preserves styleOptions on inline elements', () => {
const resultStyleOptions = htmlToStoryblokRichtext(
'<p>foo <span class="style-1 invalid-style">bar</span></p><p>baz <span class="style-2">qux</span></p><p>corge <span class="style-3">grault</span> <a href="/home" class="style-1">Home</a></p>',
'<p>foo <span class="style-1 invalid-style">bar</span></p><p>baz <span class="style-2">qux</span></p><p>corge <span class="style-3">grault</span> <a href="/home" class="style-1 invalid-style">Home</a></p>',
{
styleOptions: [
{ name: 'Style1', value: 'style-1' },
Expand All @@ -838,7 +803,7 @@
],
},
);
expect(resultStyleOptions).toEqual({

Check failure on line 806 in packages/richtext/src/html-parser.test.ts

View workflow job for this annotation

GitHub Actions / build

src/html-parser.test.ts > htmlToStoryblokRichtext > preserves styleOptions on inline elements

AssertionError: expected { type: 'doc', …(1) } to deeply equal { type: 'doc', …(1) } - Expected + Received @@ -54,10 +54,11 @@ "attrs": { "class": "style-1", "href": "/home", "rel": null, "target": null, + "title": null, }, "type": "link", }, ], "text": "Home", ❯ src/html-parser.test.ts:806:32
type: 'doc',
content: [
{
Expand All @@ -856,7 +821,6 @@
attrs: {
class: 'style-1',
},
content: [],
},
],
text: 'bar',
Expand All @@ -878,7 +842,6 @@
attrs: {
class: 'style-2',
},
content: [],
},
],
text: 'qux',
Expand All @@ -890,33 +853,20 @@
content: [
{
type: 'text',
text: 'corge ',
},
{
type: 'text',
text: 'grault',
},
{
type: 'text',
text: ' ',
text: 'corge grault ',
},
{
text: 'Home',
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: '/home',
},
content: [],
},
{
type: 'styled',
attrs: {
class: 'style-1',
href: '/home',
rel: null,
target: null,
},
content: [],
},
],
},
Expand Down Expand Up @@ -945,36 +895,21 @@
expect(warn).toHaveBeenCalledWith(expect.stringContaining('[StoryblokRichText] - `class` "unsupported" on `<span>` can not be transformed to rich text.'));
});

it('throws an error when transformation is not supported', () => {
const unsupportedElements = [
'<div>Hello world!</div>',
'<iframe src="https://example.com"></iframe>',
'<script>alert("test")</script>',
];
for (const element of unsupportedElements) {
expect(() => htmlToStoryblokRichtext(element))
.toThrowError(/No resolver specified for tag "(div|iframe|script)"!/);
}
});

it('throws an error when the source HTML is invalid', () => {
const html = '<ul><li>Not closed!<p></ul>';
expect(() => htmlToStoryblokRichtext(html))
.toThrowError('Invalid HTML: The provided string could not be parsed. Common causes include unclosed or mismatched tags!');
});

it('allows using custom resolvers', () => {
const html = '<h2>Custom Heading</h2><div>Custom Div</div>';
it('allows using custom Tiptap extensions', () => {
const html = '<h2>Custom Heading</h2>';
const result = htmlToStoryblokRichtext(html, {
resolvers: {
h2: (_, content) => ({
type: BlockTypes.HEADING,
attrs: { level: 99 },
content,
}),
div: (_, content) => ({
type: BlockTypes.PARAGRAPH,
content,
tiptapExtensions: {
heading: Heading.extend({
addAttributes() {
return {
...this.parent?.(),
level: {
parseHTML: () => {
return 99;
},
},
};
},
}),
},
});
Expand All @@ -988,10 +923,6 @@
{ type: 'text', text: 'Custom Heading' },
],
},
{
type: 'paragraph',
content: [{ type: 'text', text: 'Custom Div' }],
},
],
});
});
Expand Down Expand Up @@ -1024,10 +955,10 @@
],
},
{
type: 'horizontal_rule',
type: 'horizontalRule',
},
{
type: 'code_block',
type: 'codeBlock',
attrs: {},
content: [{ type: 'text', text: 'Hello\nworld' }],
},
Expand Down
Loading
Loading