mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 04:26:23 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user