chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions
@@ -0,0 +1,37 @@
const fetchImage = async (url: string, init?: RequestInit, proxy?: string) => {
try {
if (!proxy) {
return await fetch(url, init);
}
if (url.startsWith('blob:')) {
return await fetch(url, init);
}
if (url.startsWith('data:')) {
return await fetch(url, init);
}
if (url.startsWith(window.location.origin)) {
return await fetch(url, init);
}
return await fetch(proxy + '?url=' + encodeURIComponent(url), init)
.then(res => {
if (!res.ok) {
throw new Error('Network response was not ok');
}
return res;
})
.catch(() => fetch(url, init));
} catch (error) {
console.warn('Error fetching image:', error);
return null;
}
};
const fetchable = (url: string) =>
url.startsWith('http:') ||
url.startsWith('https:') ||
url.startsWith('data:');
export const FetchUtils = {
fetchImage,
fetchable,
};
@@ -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,
};
@@ -0,0 +1,3 @@
export * from './fetch.js';
export * from './hast.js';
export * from './text.js';
@@ -0,0 +1,83 @@
import { isEqual } from '@blocksuite/global/utils';
import type { DeltaInsert } from '@blocksuite/inline';
const mergeDeltas = (
acc: DeltaInsert[],
cur: DeltaInsert,
options: { force?: boolean } = { force: false }
) => {
if (acc.length === 0) {
return [cur];
}
const last = acc[acc.length - 1];
if (options?.force) {
last.insert = last.insert + cur.insert;
last.attributes = Object.create(null);
return acc;
} else if (
typeof last.insert === 'string' &&
typeof cur.insert === 'string' &&
(isEqual(last.attributes, cur.attributes) ||
(last.attributes === undefined && cur.attributes === undefined))
) {
last.insert += cur.insert;
return acc;
}
return [...acc, cur];
};
const isNullish = (value: unknown) => value === null || value === undefined;
const createText = (s: string) => {
return {
'$blocksuite:internal:text$': true,
delta: s.length === 0 ? [] : [{ insert: s }],
};
};
const isText = (o: unknown) => {
if (
typeof o === 'object' &&
o !== null &&
'$blocksuite:internal:text$' in o
) {
return o['$blocksuite:internal:text$'] === true;
}
return false;
};
function toURLSearchParams(
params?: Partial<Record<string, string | string[]>>
) {
if (!params) return;
const items = Object.entries(params)
.filter(([_, v]) => !isNullish(v))
.filter(([_, v]) => {
if (typeof v === 'string') {
return v.length > 0;
}
if (Array.isArray(v)) {
return v.length > 0;
}
return false;
})
.map(([k, v]) => [k, Array.isArray(v) ? v.filter(v => v.length) : v]) as [
string,
string | string[],
][];
return new URLSearchParams(
items
.filter(([_, v]) => v.length)
.map(([k, v]) => [k, Array.isArray(v) ? v.join(',') : v])
);
}
export const TextUtils = {
mergeDeltas,
isNullish,
createText,
isText,
toURLSearchParams,
};