mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-21 20:11:43 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
import type { Element, ElementContent, Text } from 'hast';
|
||||
|
||||
import type { HtmlAST } from '../types/hast.js';
|
||||
|
||||
const isElement = (ast: HtmlAST): ast is Element => {
|
||||
return ast.type === 'element';
|
||||
};
|
||||
|
||||
const getTextContent = (ast: HtmlAST | undefined, defaultStr = ''): string => {
|
||||
if (!ast) {
|
||||
return defaultStr;
|
||||
}
|
||||
switch (ast.type) {
|
||||
case 'text': {
|
||||
return ast.value.replace(/\s+/g, ' ');
|
||||
}
|
||||
case 'element': {
|
||||
switch (ast.tagName) {
|
||||
case 'br': {
|
||||
return '\n';
|
||||
}
|
||||
}
|
||||
return ast.children.map(child => getTextContent(child)).join('');
|
||||
}
|
||||
}
|
||||
return defaultStr;
|
||||
};
|
||||
|
||||
const getElementChildren = (ast: HtmlAST | undefined): Element[] => {
|
||||
if (!ast) {
|
||||
return [];
|
||||
}
|
||||
if (ast.type === 'element') {
|
||||
return ast.children.filter(child => child.type === 'element') as Element[];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const getTextChildren = (ast: HtmlAST | undefined): Text[] => {
|
||||
if (!ast) {
|
||||
return [];
|
||||
}
|
||||
if (ast.type === 'element') {
|
||||
return ast.children.filter(child => child.type === 'text') as Text[];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const getTextChildrenOnlyAst = (ast: Element): Element => {
|
||||
return {
|
||||
...ast,
|
||||
children: getTextChildren(ast),
|
||||
};
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const isElementInline = (element: Element): boolean => {
|
||||
return (
|
||||
isTagInline(element.tagName) ||
|
||||
// Inline elements
|
||||
!!(
|
||||
typeof element.properties?.style === 'string' &&
|
||||
element.properties.style.match(/display:\s*inline/)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const getInlineElementsAndText = (ast: Element): (Element | Text)[] => {
|
||||
if (!ast || !ast.children) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return ast.children.filter((child): child is Element | Text => {
|
||||
if (child.type === 'text') {
|
||||
return true;
|
||||
}
|
||||
if (child.type === 'element' && child.tagName && isElementInline(child)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const getInlineOnlyElementAST = (ast: Element): Element => {
|
||||
return {
|
||||
...ast,
|
||||
children: getInlineElementsAndText(ast),
|
||||
};
|
||||
};
|
||||
|
||||
const querySelectorTag = (
|
||||
ast: HtmlAST,
|
||||
tagName: string
|
||||
): Element | undefined => {
|
||||
if (ast.type === 'element') {
|
||||
if (ast.tagName === tagName) {
|
||||
return ast;
|
||||
}
|
||||
for (const child of ast.children) {
|
||||
const result = querySelectorTag(child, tagName);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const querySelectorClass = (
|
||||
ast: HtmlAST,
|
||||
className: string
|
||||
): Element | undefined => {
|
||||
if (ast.type === 'element') {
|
||||
if (
|
||||
Array.isArray(ast.properties?.className) &&
|
||||
ast.properties.className.includes(className)
|
||||
) {
|
||||
return ast;
|
||||
}
|
||||
for (const child of ast.children) {
|
||||
const result = querySelectorClass(child, className);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const querySelectorId = (ast: HtmlAST, id: string): Element | undefined => {
|
||||
if (ast.type === 'element') {
|
||||
if (ast.properties.id === id) {
|
||||
return ast;
|
||||
}
|
||||
for (const child of ast.children) {
|
||||
const result = querySelectorId(child, id);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const querySelector = (ast: HtmlAST, selector: string): Element | undefined => {
|
||||
if (ast.type === 'root') {
|
||||
for (const child of ast.children) {
|
||||
const result = querySelector(child, selector);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else if (ast.type === 'element') {
|
||||
if (selector.startsWith('.')) {
|
||||
return querySelectorClass(ast, selector.slice(1));
|
||||
} else if (selector.startsWith('#')) {
|
||||
return querySelectorId(ast, selector.slice(1));
|
||||
} else {
|
||||
return querySelectorTag(ast, selector);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const flatNodes = (
|
||||
ast: HtmlAST,
|
||||
expression: (tagName: string) => boolean
|
||||
): HtmlAST => {
|
||||
if (ast.type === 'element') {
|
||||
const children = ast.children.map(child => flatNodes(child, expression));
|
||||
return {
|
||||
...ast,
|
||||
children: children.flatMap(child => {
|
||||
if (child.type === 'element' && expression(child.tagName)) {
|
||||
return child.children;
|
||||
}
|
||||
return child;
|
||||
}) as ElementContent[],
|
||||
};
|
||||
}
|
||||
return ast;
|
||||
};
|
||||
|
||||
// Check if it is a paragraph like element
|
||||
// https://html.spec.whatwg.org/#paragraph
|
||||
const isParagraphLike = (node: Element): boolean => {
|
||||
// Flex container
|
||||
return (
|
||||
(typeof node.properties?.style === 'string' &&
|
||||
node.properties.style.match(/display:\s*flex/) !== null) ||
|
||||
getElementChildren(node).every(child => isElementInline(child))
|
||||
);
|
||||
};
|
||||
|
||||
export const HastUtils = {
|
||||
isElement,
|
||||
getTextContent,
|
||||
getElementChildren,
|
||||
getTextChildren,
|
||||
getTextChildrenOnlyAst,
|
||||
getInlineOnlyElementAST,
|
||||
querySelector,
|
||||
flatNodes,
|
||||
isParagraphLike,
|
||||
};
|
||||
Reference in New Issue
Block a user