tag to a tag
*/
const inlineElements = new Set(['b']);
-const blockElements = new Set([
- 'div',
- 'p',
- 'h1',
- 'h2',
- 'h3',
- 'h4',
- 'h5',
- 'h6',
- 'ul',
- 'ol',
- 'li',
- 'blockquote',
- 'pre',
-]);
-
export const rehypeInlineToBlock: Plugin<[], Root> = () => {
return tree => {
visit(tree, 'element', node => {
@@ -31,7 +17,8 @@ export const rehypeInlineToBlock: Plugin<[], Root> = () => {
if (inlineElements.has(node.tagName)) {
// Check if the node has a block element child
const hasBlockChild = node.children.some(
- child => child.type === 'element' && blockElements.has(child.tagName)
+ child =>
+ child.type === 'element' && HastUtils.isTagBlock(child.tagName)
);
if (hasBlockChild) {
diff --git a/blocksuite/affine/shared/src/adapters/html/rehype-plugins/wrap-inline-element.ts b/blocksuite/affine/shared/src/adapters/html/rehype-plugins/wrap-inline-element.ts
new file mode 100644
index 000000000..7eb5f0774
--- /dev/null
+++ b/blocksuite/affine/shared/src/adapters/html/rehype-plugins/wrap-inline-element.ts
@@ -0,0 +1,79 @@
+import type { Element, ElementContent, Root } from 'hast';
+import type { Plugin } from 'unified';
+import { visit } from 'unist-util-visit';
+
+import { HastUtils } from '../../utils/hast';
+
+/**
+ * In some cases, the inline elements are wrapped in a div tag mixed with block elements
+ * We need to wrap them in a p tag to avoid the inline elements being treated as a block element
+ */
+export const rehypeWrapInlineElements: Plugin<[], Root> = () => {
+ return tree => {
+ visit(tree, 'element', (node: Element) => {
+ if (node.tagName === 'div') {
+ // First check if we have a mix of inline and block elements
+ let hasInline = false;
+ let hasBlock = false;
+ for (const child of node.children) {
+ if (child.type === 'element') {
+ if (HastUtils.isElementInline(child)) {
+ hasInline = true;
+ } else if (HastUtils.isTagBlock(child.tagName)) {
+ hasBlock = true;
+ }
+ if (hasInline && hasBlock) break;
+ }
+ }
+
+ // Only process if we have both inline and block elements
+ if (hasInline && hasBlock) {
+ const newChildren: ElementContent[] = [];
+ let currentInlineGroup: ElementContent[] = [];
+
+ for (const child of node.children) {
+ if (child.type === 'element') {
+ const elementChild = child;
+ if (HastUtils.isElementInline(elementChild)) {
+ // Add to current inline group
+ currentInlineGroup.push(elementChild);
+ } else if (HastUtils.isTagBlock(elementChild.tagName)) {
+ // If we have accumulated inline elements, wrap them in a p tag
+ if (currentInlineGroup.length > 0) {
+ newChildren.push({
+ type: 'element',
+ tagName: 'p',
+ properties: {},
+ children: currentInlineGroup,
+ });
+ currentInlineGroup = [];
+ }
+ // Add the block element as is
+ newChildren.push(elementChild);
+ } else {
+ // For unknown elements, treat them as inline
+ currentInlineGroup.push(elementChild);
+ }
+ } else {
+ // For text nodes, treat them as inline content
+ currentInlineGroup.push(child);
+ }
+ }
+
+ // Handle any remaining inline elements at the end
+ if (currentInlineGroup.length > 0) {
+ newChildren.push({
+ type: 'element',
+ tagName: 'p',
+ properties: {},
+ children: currentInlineGroup,
+ });
+ }
+
+ // Replace the original children with the new structure
+ node.children = newChildren;
+ }
+ }
+ });
+ };
+};
diff --git a/blocksuite/affine/shared/src/adapters/utils/hast.ts b/blocksuite/affine/shared/src/adapters/utils/hast.ts
index c34e7a6cf..6f7d60755 100644
--- a/blocksuite/affine/shared/src/adapters/utils/hast.ts
+++ b/blocksuite/affine/shared/src/adapters/utils/hast.ts
@@ -2,6 +2,86 @@ import type { Element, ElementContent, Text } from 'hast';
import type { HtmlAST } from '../types/hast.js';
+// Block elements that html adapter supports
+const blockElements = [
+ 'div',
+ 'p',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'ul',
+ 'ol',
+ 'li',
+ 'blockquote',
+ 'pre',
+];
+
+const blockElementsSet = new Set(blockElements);
+
+// Phrasing content
+const inlineElements = [
+ 'a',
+ 'abbr',
+ 'audio',
+ 'b',
+ 'bdi',
+ 'bdo',
+ 'br',
+ 'button',
+ 'canvas',
+ 'cite',
+ 'code',
+ 'data',
+ 'datalist',
+ 'del',
+ 'dfn',
+ 'em',
+ 'embed',
+ 'i',
+ // 'iframe' is not included because it needs special handling
+ // 'img' is not included because it needs special handling
+ 'input',
+ 'ins',
+ 'kbd',
+ 'label',
+ 'link',
+ 'map',
+ 'mark',
+ 'math',
+ 'meta',
+ 'meter',
+ 'noscript',
+ 'object',
+ 'output',
+ 'picture',
+ 'progress',
+ 'q',
+ 'ruby',
+ 's',
+ 'samp',
+ 'script',
+ 'select',
+ 'slot',
+ 'small',
+ 'span',
+ 'strong',
+ 'sub',
+ 'sup',
+ 'svg',
+ 'template',
+ 'textarea',
+ 'time',
+ 'u',
+ 'var',
+ 'video',
+ 'wbr',
+];
+
+const inlineElementsSet = new Set(inlineElements);
+
const isElement = (ast: HtmlAST): ast is Element => {
return ast.type === 'element';
};
@@ -53,66 +133,12 @@ const getTextChildrenOnlyAst = (ast: Element): Element => {
};
};
+const isTagBlock = (tagName: string): boolean => {
+ return blockElementsSet.has(tagName);
+};
+
const isTagInline = (tagName: string): boolean => {
- // Phrasing content
- const inlineElements = [
- 'a',
- 'abbr',
- 'audio',
- 'b',
- 'bdi',
- 'bdo',
- 'br',
- 'button',
- 'canvas',
- 'cite',
- 'code',
- 'data',
- 'datalist',
- 'del',
- 'dfn',
- 'em',
- 'embed',
- 'i',
- // 'iframe' is not included because it needs special handling
- // 'img' is not included because it needs special handling
- 'input',
- 'ins',
- 'kbd',
- 'label',
- 'link',
- 'map',
- 'mark',
- 'math',
- 'meta',
- 'meter',
- 'noscript',
- 'object',
- 'output',
- 'picture',
- 'progress',
- 'q',
- 'ruby',
- 's',
- 'samp',
- 'script',
- 'select',
- 'slot',
- 'small',
- 'span',
- 'strong',
- 'sub',
- 'sup',
- 'svg',
- 'template',
- 'textarea',
- 'time',
- 'u',
- 'var',
- 'video',
- 'wbr',
- ];
- return inlineElements.includes(tagName);
+ return inlineElementsSet.has(tagName);
};
const isElementInline = (element: Element): boolean => {
@@ -263,4 +289,7 @@ export const HastUtils = {
querySelector,
flatNodes,
isParagraphLike,
+ isTagBlock,
+ isTagInline,
+ isElementInline,
};