snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
This commit is contained in:
45
blocksuite/affine/inlines/preset/package.json
Normal file
45
blocksuite/affine/inlines/preset/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@blocksuite/affine-inline-preset",
|
||||
"description": "Inline preset for BlockSuite.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "yarn run -T tsc"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [],
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-inline-comment": "workspace:*",
|
||||
"@blocksuite/affine-inline-footnote": "workspace:*",
|
||||
"@blocksuite/affine-inline-latex": "workspace:*",
|
||||
"@blocksuite/affine-inline-link": "workspace:*",
|
||||
"@blocksuite/affine-inline-mention": "workspace:*",
|
||||
"@blocksuite/affine-inline-reference": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-rich-text": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@ctrl/tinycolor": "^4.1.0",
|
||||
"@toeverything/theme": "^1.1.23",
|
||||
"@types/hast": "^3.0.4",
|
||||
"collapse-white-space": "^2.1.0",
|
||||
"lit": "^3.2.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./store": "./src/store.ts",
|
||||
"./view": "./src/view.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.27.5"
|
||||
}
|
||||
21
blocksuite/affine/inlines/preset/src/adapters/extensions.ts
Normal file
21
blocksuite/affine/inlines/preset/src/adapters/extensions.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { HtmlInlineToDeltaAdapterExtensions } from './html/html-inline';
|
||||
import { InlineDeltaToHtmlAdapterExtensions } from './html/inline-delta';
|
||||
import { InlineDeltaToMarkdownAdapterExtensions } from './markdown/inline-delta';
|
||||
import { MarkdownInlineToDeltaAdapterExtensions } from './markdown/markdown-inline';
|
||||
import { NotionHtmlInlineToDeltaAdapterExtensions } from './notion-html/html-inline';
|
||||
|
||||
export const InlineAdapterExtensions: ExtensionType[] = [
|
||||
HtmlInlineToDeltaAdapterExtensions,
|
||||
InlineDeltaToHtmlAdapterExtensions,
|
||||
NotionHtmlInlineToDeltaAdapterExtensions,
|
||||
InlineDeltaToMarkdownAdapterExtensions,
|
||||
MarkdownInlineToDeltaAdapterExtensions,
|
||||
].flat();
|
||||
|
||||
export * from './html/html-inline';
|
||||
export * from './html/inline-delta';
|
||||
export * from './markdown/inline-delta';
|
||||
export * from './markdown/markdown-inline';
|
||||
export * from './notion-html/html-inline';
|
||||
@@ -0,0 +1,146 @@
|
||||
import { TinyColor } from '@ctrl/tinycolor';
|
||||
import { cssVarV2, darkThemeV2, lightThemeV2 } from '@toeverything/theme/v2';
|
||||
|
||||
type Rgb = { r: number; g: number; b: number };
|
||||
type Oklab = { l: number; a: number; b: number; chroma: number; hue: number };
|
||||
|
||||
const ACHROMATIC_CHROMA_THRESHOLD = 0.02;
|
||||
const DEFAULT_TEXT_LIGHTNESS_MIN = 0.4;
|
||||
const DEFAULT_TEXT_LIGHTNESS_MAX = 0.9;
|
||||
const MAX_COLOR_DISTANCE = 0.18;
|
||||
const MAX_CHROMA_DISTANCE = 0.12;
|
||||
const MAX_HUE_DISTANCE = 45;
|
||||
|
||||
const supportedTextColorNames = [
|
||||
'red',
|
||||
'orange',
|
||||
'yellow',
|
||||
'green',
|
||||
'teal',
|
||||
'blue',
|
||||
'purple',
|
||||
'grey',
|
||||
] as const;
|
||||
|
||||
export const parseCssColor = (value: string) => {
|
||||
const parsed = new TinyColor(value);
|
||||
if (!parsed.isValid) return null;
|
||||
const { r, g, b, a } = parsed.toRgb();
|
||||
return { r, g, b, alpha: a };
|
||||
};
|
||||
|
||||
const srgbToLinear = (channel: number) => {
|
||||
const value = channel / 255;
|
||||
return value <= 0.04045
|
||||
? value / 12.92
|
||||
: Math.pow((value + 0.055) / 1.055, 2.4);
|
||||
};
|
||||
|
||||
const rgbToOklab = ({ r, g, b }: Rgb): Oklab => {
|
||||
const red = srgbToLinear(r);
|
||||
const green = srgbToLinear(g);
|
||||
const blue = srgbToLinear(b);
|
||||
const l = Math.cbrt(
|
||||
0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue
|
||||
);
|
||||
const m = Math.cbrt(
|
||||
0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue
|
||||
);
|
||||
const s = Math.cbrt(
|
||||
0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue
|
||||
);
|
||||
const result = {
|
||||
l: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
||||
a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
||||
b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
||||
};
|
||||
return {
|
||||
...result,
|
||||
chroma: Math.hypot(result.a, result.b),
|
||||
hue: (Math.atan2(result.b, result.a) * 180) / Math.PI,
|
||||
};
|
||||
};
|
||||
|
||||
const supportedTextColors = supportedTextColorNames.map(name => ({
|
||||
name,
|
||||
cssVar: cssVarV2(`text/highlight/fg/${name}`),
|
||||
references: [
|
||||
lightThemeV2[`text/highlight/fg/${name}`],
|
||||
darkThemeV2[`text/highlight/fg/${name}`],
|
||||
].flatMap(color => {
|
||||
const parsed = parseCssColor(color);
|
||||
return parsed ? [rgbToOklab(parsed)] : [];
|
||||
}),
|
||||
}));
|
||||
|
||||
const colorDistance = (a: Oklab, b: Oklab) =>
|
||||
Math.hypot(a.l - b.l, a.a - b.a, a.b - b.b);
|
||||
|
||||
const hueDistance = (a: number, b: number) => {
|
||||
const distance = Math.abs(a - b) % 360;
|
||||
return Math.min(distance, 360 - distance);
|
||||
};
|
||||
|
||||
export const resolveNearestSupportedColor = (color: string): string | null => {
|
||||
const parsed = parseCssColor(color);
|
||||
if (!parsed || parsed.alpha < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const target = rgbToOklab(parsed);
|
||||
const achromatic = target.chroma < ACHROMATIC_CHROMA_THRESHOLD;
|
||||
if (
|
||||
achromatic &&
|
||||
(target.l < DEFAULT_TEXT_LIGHTNESS_MIN ||
|
||||
target.l > DEFAULT_TEXT_LIGHTNESS_MAX)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nearest:
|
||||
| {
|
||||
cssVar: string;
|
||||
distance: number;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
for (const supported of supportedTextColors) {
|
||||
if (achromatic !== (supported.name === 'grey')) {
|
||||
continue;
|
||||
}
|
||||
for (const reference of supported.references) {
|
||||
const distance = colorDistance(target, reference);
|
||||
if (
|
||||
distance > MAX_COLOR_DISTANCE ||
|
||||
(!achromatic &&
|
||||
(Math.abs(target.chroma - reference.chroma) > MAX_CHROMA_DISTANCE ||
|
||||
hueDistance(target.hue, reference.hue) > MAX_HUE_DISTANCE))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!nearest || distance < nearest.distance) {
|
||||
nearest = { cssVar: supported.cssVar, distance };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nearest?.cssVar ?? null;
|
||||
};
|
||||
|
||||
export const extractColorFromStyle = (
|
||||
style: string | undefined
|
||||
): string | null => {
|
||||
if (typeof style !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const declarations = style.split(';');
|
||||
for (const declaration of declarations) {
|
||||
const colon = declaration.indexOf(':');
|
||||
if (colon === -1) continue;
|
||||
const key = declaration.slice(0, colon).trim().toLowerCase();
|
||||
if (key === 'color') {
|
||||
return declaration.slice(colon + 1).trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,363 @@
|
||||
import {
|
||||
type HtmlAST,
|
||||
HtmlASTToDeltaExtension,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { collapseWhiteSpace } from 'collapse-white-space';
|
||||
import type { Element } from 'hast';
|
||||
|
||||
import {
|
||||
extractColorFromStyle,
|
||||
resolveNearestSupportedColor,
|
||||
} from './color-utils.js';
|
||||
|
||||
/**
|
||||
* Handle empty text nodes created by HTML parser for styling purposes.
|
||||
* These nodes typically contain only whitespace/newlines, for example:
|
||||
* ```json
|
||||
* {
|
||||
* "type": "text",
|
||||
* "value": "\n\n \n \n "
|
||||
* }
|
||||
* ```
|
||||
* We collapse and trim the whitespace to check if the node is truly empty,
|
||||
* and return an empty array in that case.
|
||||
*/
|
||||
const isEmptyText = (ast: HtmlAST): boolean => {
|
||||
return (
|
||||
ast.type === 'text' && collapseWhiteSpace(ast.value, { trim: true }) === ''
|
||||
);
|
||||
};
|
||||
|
||||
const isElement = (ast: HtmlAST): ast is Element => {
|
||||
return ast.type === 'element';
|
||||
};
|
||||
|
||||
const textLikeElementTags = new Set(['span', 'bdi', 'bdo', 'ins']);
|
||||
const listElementTags = new Set(['ol', 'ul']);
|
||||
const strongElementTags = new Set(['strong', 'b']);
|
||||
const italicElementTags = new Set(['i', 'em']);
|
||||
|
||||
/**
|
||||
* Check if the element is a strong element through style or tag
|
||||
* If the element tag is <strong>, <b> or the style is `font-weight: bold;`, or the font-weight is 500 or above,
|
||||
* we consider it as a strong element
|
||||
* @param ast - The HTML AST node to check
|
||||
* @returns `true` if the element is a strong element, `false` otherwise
|
||||
* @example
|
||||
* ```html
|
||||
* <strong>Hello</strong>
|
||||
* <b>Hello</b>
|
||||
* <span style="font-weight: bold;">Hello</span>
|
||||
* <span style="font-weight: 700;">Hello</span>
|
||||
* ```
|
||||
*/
|
||||
const isStrongElement = (ast: HtmlAST) => {
|
||||
if (!isElement(ast)) {
|
||||
return false;
|
||||
}
|
||||
const style =
|
||||
typeof ast.properties.style === 'string' ? ast.properties.style : '';
|
||||
|
||||
const isStrongTag = strongElementTags.has(ast.tagName);
|
||||
// Should exclude the case like <b style="font-weight: normal;">
|
||||
const isNotNormalFontWeight = !/font-weight:\s*normal/.test(style);
|
||||
const isBoldFontWeight = /font-weight:\s*(([5-9]\d{2})|bold)/.test(style);
|
||||
return (isStrongTag && isNotNormalFontWeight) || isBoldFontWeight;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the element is an italic element through style or tag
|
||||
* If the element tag is <i>, <em> or the style is `font-style: italic;`,
|
||||
* we consider it as an italic element
|
||||
* @param ast - The HTML AST node to check
|
||||
* @returns `true` if the element is an italic element, `false` otherwise
|
||||
* @example
|
||||
* ```html
|
||||
* <i>Hello</i>
|
||||
* <em>Hello</em>
|
||||
* <span style="font-style: italic;">Hello</span>
|
||||
* ```
|
||||
*/
|
||||
const isItalicElement = (ast: HtmlAST) => {
|
||||
if (!isElement(ast)) {
|
||||
return false;
|
||||
}
|
||||
const style =
|
||||
typeof ast.properties.style === 'string' ? ast.properties.style : '';
|
||||
const isItalicTag = italicElementTags.has(ast.tagName);
|
||||
const isItalicStyle = /font-style:\s*italic/.test(style);
|
||||
return isItalicTag || isItalicStyle;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the element is an underline element through style or tag
|
||||
* If the element tag is <u> or the style is `text-decoration: underline;`,
|
||||
* we consider it as an underline element
|
||||
* @param ast - The HTML AST node to check
|
||||
* @returns `true` if the element is an underline element, `false` otherwise
|
||||
* @example
|
||||
* ```html
|
||||
* <u>Hello</u>
|
||||
* <span style="text-decoration: underline;">Hello</span>
|
||||
* ```
|
||||
*/
|
||||
const isUnderlineElement = (ast: HtmlAST) => {
|
||||
if (!isElement(ast)) {
|
||||
return false;
|
||||
}
|
||||
const style =
|
||||
typeof ast.properties.style === 'string' ? ast.properties.style : '';
|
||||
const isUnderlineTag = ast.tagName === 'u';
|
||||
const isUnderlineStyle = /text-decoration:\s*underline/.test(style);
|
||||
return isUnderlineTag || isUnderlineStyle;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the element is a line-through element through style or tag
|
||||
* If the element tag is <del> or the style is `text-decoration: line-through;`,
|
||||
* we consider it as a line-through element
|
||||
* @param ast - The HTML AST node to check
|
||||
* @returns `true` if the element is a line-through element, `false` otherwise
|
||||
* @example
|
||||
* ```html
|
||||
* <del>Hello</del>
|
||||
* <span style="text-decoration: line-through;">Hello</span>
|
||||
* ```
|
||||
*/
|
||||
const isLineThroughElement = (ast: HtmlAST) => {
|
||||
if (!isElement(ast)) {
|
||||
return false;
|
||||
}
|
||||
const style =
|
||||
typeof ast.properties.style === 'string' ? ast.properties.style : '';
|
||||
const isLineThroughTag = ast.tagName === 'del';
|
||||
const isLineThroughStyle = /text-decoration:\s*line-through/.test(style);
|
||||
return isLineThroughTag || isLineThroughStyle;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the case like <span>Hello</span>
|
||||
* @param ast
|
||||
* @returns
|
||||
*/
|
||||
const isTextLikeElement = (ast: HtmlAST) => {
|
||||
if (!isElement(ast)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
textLikeElementTags.has(ast.tagName) &&
|
||||
!isStrongElement(ast) &&
|
||||
!isItalicElement(ast) &&
|
||||
!isUnderlineElement(ast) &&
|
||||
!isLineThroughElement(ast)
|
||||
);
|
||||
};
|
||||
|
||||
export const htmlTextToDeltaMatcher = HtmlASTToDeltaExtension({
|
||||
name: 'text',
|
||||
match: ast => ast.type === 'text',
|
||||
toDelta: (ast, context) => {
|
||||
if (!('value' in ast)) {
|
||||
return [];
|
||||
}
|
||||
const { options } = context;
|
||||
options.trim ??= false;
|
||||
|
||||
if (options.pre) {
|
||||
return [{ insert: ast.value }];
|
||||
}
|
||||
|
||||
if (isEmptyText(ast)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const value = options.trim
|
||||
? collapseWhiteSpace(ast.value, { trim: options.trim })
|
||||
: collapseWhiteSpace(ast.value);
|
||||
return value ? [{ insert: value }] : [];
|
||||
},
|
||||
});
|
||||
|
||||
export const htmlColorStyleElementToDeltaMatcher = HtmlASTToDeltaExtension({
|
||||
name: 'color-style-element',
|
||||
match: ast =>
|
||||
isElement(ast) &&
|
||||
ast.tagName === 'span' &&
|
||||
typeof ast.properties?.style === 'string' &&
|
||||
/color\s*:/i.test(ast.properties.style),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const baseOptions = { ...context.options, trim: false };
|
||||
// In preformatted contexts (e.g. code blocks) we don't keep inline colors.
|
||||
if (baseOptions.pre) {
|
||||
return ast.children.flatMap(child => context.toDelta(child, baseOptions));
|
||||
}
|
||||
const colorValue = extractColorFromStyle(
|
||||
typeof ast.properties?.style === 'string' ? ast.properties.style : ''
|
||||
);
|
||||
const mappedColor = colorValue
|
||||
? resolveNearestSupportedColor(colorValue)
|
||||
: null;
|
||||
const deltas = ast.children.flatMap(child =>
|
||||
context.toDelta(child, baseOptions).map(delta => {
|
||||
if (mappedColor) {
|
||||
delta.attributes = { ...delta.attributes, color: mappedColor };
|
||||
}
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
return deltas;
|
||||
},
|
||||
});
|
||||
|
||||
export const htmlTextLikeElementToDeltaMatcher = HtmlASTToDeltaExtension({
|
||||
name: 'text-like-element',
|
||||
match: ast => isTextLikeElement(ast),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false })
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const htmlListToDeltaMatcher = HtmlASTToDeltaExtension({
|
||||
name: 'list-element',
|
||||
match: ast => isElement(ast) && listElementTags.has(ast.tagName),
|
||||
toDelta: () => {
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
export const htmlStrongElementToDeltaMatcher = HtmlASTToDeltaExtension({
|
||||
name: 'strong-element',
|
||||
match: ast => isStrongElement(ast),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, bold: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const htmlItalicElementToDeltaMatcher = HtmlASTToDeltaExtension({
|
||||
name: 'italic-element',
|
||||
match: ast => isItalicElement(ast),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, italic: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const htmlCodeElementToDeltaMatcher = HtmlASTToDeltaExtension({
|
||||
name: 'code-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'code',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, code: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const htmlDelElementToDeltaMatcher = HtmlASTToDeltaExtension({
|
||||
name: 'del-element',
|
||||
match: ast => isLineThroughElement(ast),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, strike: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const htmlUnderlineElementToDeltaMatcher = HtmlASTToDeltaExtension({
|
||||
name: 'underline-element',
|
||||
match: ast => isUnderlineElement(ast),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, underline: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const htmlMarkElementToDeltaMatcher = HtmlASTToDeltaExtension({
|
||||
name: 'mark-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'mark',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const dataColor =
|
||||
typeof ast.properties?.dataColor === 'string'
|
||||
? ast.properties.dataColor
|
||||
: '';
|
||||
const colorName =
|
||||
dataColor &&
|
||||
/^(red|orange|yellow|green|teal|blue|purple|grey)$/.test(dataColor)
|
||||
? dataColor
|
||||
: 'yellow';
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, { trim: false }).map(delta => {
|
||||
delta.attributes = {
|
||||
...delta.attributes,
|
||||
background: `var(--affine-text-highlight-${colorName})`,
|
||||
};
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const htmlBrElementToDeltaMatcher = HtmlASTToDeltaExtension({
|
||||
name: 'br-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'br',
|
||||
toDelta: () => {
|
||||
return [{ insert: '\n' }];
|
||||
},
|
||||
});
|
||||
|
||||
export const HtmlInlineToDeltaAdapterExtensions = [
|
||||
htmlTextToDeltaMatcher,
|
||||
htmlColorStyleElementToDeltaMatcher,
|
||||
htmlTextLikeElementToDeltaMatcher,
|
||||
htmlStrongElementToDeltaMatcher,
|
||||
htmlItalicElementToDeltaMatcher,
|
||||
htmlCodeElementToDeltaMatcher,
|
||||
htmlDelElementToDeltaMatcher,
|
||||
htmlUnderlineElementToDeltaMatcher,
|
||||
htmlMarkElementToDeltaMatcher,
|
||||
htmlBrElementToDeltaMatcher,
|
||||
];
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { InlineHtmlAST } from '@blocksuite/affine-shared/adapters';
|
||||
import { InlineDeltaToHtmlAdapterExtension } from '@blocksuite/affine-shared/adapters';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
|
||||
export const boldDeltaToHtmlAdapterMatcher = InlineDeltaToHtmlAdapterExtension({
|
||||
name: 'bold',
|
||||
match: delta => !!delta.attributes?.bold,
|
||||
toAST: (_, context) => {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'strong',
|
||||
properties: {},
|
||||
children: [context.current],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const italicDeltaToHtmlAdapterMatcher =
|
||||
InlineDeltaToHtmlAdapterExtension({
|
||||
name: 'italic',
|
||||
match: delta => !!delta.attributes?.italic,
|
||||
toAST: (_, context) => {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'em',
|
||||
properties: {},
|
||||
children: [context.current],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const strikeDeltaToHtmlAdapterMatcher =
|
||||
InlineDeltaToHtmlAdapterExtension({
|
||||
name: 'strike',
|
||||
match: delta => !!delta.attributes?.strike,
|
||||
toAST: (_, context) => {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'del',
|
||||
properties: {},
|
||||
children: [context.current],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const inlineCodeDeltaToHtmlAdapterMatcher =
|
||||
InlineDeltaToHtmlAdapterExtension({
|
||||
name: 'inlineCode',
|
||||
match: delta => !!delta.attributes?.code,
|
||||
toAST: (_, context) => {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'code',
|
||||
properties: {},
|
||||
children: [context.current],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const underlineDeltaToHtmlAdapterMatcher =
|
||||
InlineDeltaToHtmlAdapterExtension({
|
||||
name: 'underline',
|
||||
match: delta => !!delta.attributes?.underline,
|
||||
toAST: (_, context) => {
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'u',
|
||||
properties: {},
|
||||
children: [context.current],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const highlightBackgroundDeltaToHtmlAdapterMatcher =
|
||||
InlineDeltaToHtmlAdapterExtension({
|
||||
name: 'highlight-background',
|
||||
match: delta => !!delta.attributes?.background,
|
||||
toAST: (delta, context, provider) => {
|
||||
const hast: InlineHtmlAST = {
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: {},
|
||||
children: [context.current],
|
||||
};
|
||||
if (!provider || !delta.attributes?.background) {
|
||||
return hast;
|
||||
}
|
||||
|
||||
const theme = provider.getOptional(ThemeProvider);
|
||||
if (!theme) {
|
||||
return hast;
|
||||
}
|
||||
|
||||
const backgroundVar = delta.attributes?.background.substring(
|
||||
'var('.length,
|
||||
delta.attributes?.background.indexOf(')')
|
||||
);
|
||||
const background = theme.getCssVariableColor(backgroundVar);
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'mark',
|
||||
properties: {
|
||||
style: `background-color: ${background};`,
|
||||
},
|
||||
children: [context.current],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const highlightColorDeltaToHtmlAdapterMatcher =
|
||||
InlineDeltaToHtmlAdapterExtension({
|
||||
name: 'highlight-color',
|
||||
match: delta => !!delta.attributes?.color,
|
||||
toAST: (delta, context, provider) => {
|
||||
const hast: InlineHtmlAST = {
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: {},
|
||||
children: [context.current],
|
||||
};
|
||||
if (!provider || !delta.attributes?.color) {
|
||||
return hast;
|
||||
}
|
||||
|
||||
const theme = provider.getOptional(ThemeProvider);
|
||||
if (!theme) {
|
||||
return hast;
|
||||
}
|
||||
|
||||
const colorVar = delta.attributes?.color.substring(
|
||||
'var('.length,
|
||||
delta.attributes?.color.indexOf(')')
|
||||
);
|
||||
const color = theme.getCssVariableColor(colorVar);
|
||||
return {
|
||||
type: 'element',
|
||||
tagName: 'mark',
|
||||
properties: {
|
||||
style: `color: ${color};background-color: transparent`,
|
||||
},
|
||||
children: [context.current],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const InlineDeltaToHtmlAdapterExtensions = [
|
||||
boldDeltaToHtmlAdapterMatcher,
|
||||
italicDeltaToHtmlAdapterMatcher,
|
||||
strikeDeltaToHtmlAdapterMatcher,
|
||||
underlineDeltaToHtmlAdapterMatcher,
|
||||
highlightBackgroundDeltaToHtmlAdapterMatcher,
|
||||
highlightColorDeltaToHtmlAdapterMatcher,
|
||||
inlineCodeDeltaToHtmlAdapterMatcher,
|
||||
];
|
||||
@@ -0,0 +1,57 @@
|
||||
import { InlineDeltaToMarkdownAdapterExtension } from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const boldDeltaToMarkdownAdapterMatcher =
|
||||
InlineDeltaToMarkdownAdapterExtension({
|
||||
name: 'bold',
|
||||
match: delta => !!delta.attributes?.bold,
|
||||
toAST: (_, context) => {
|
||||
const { current: currentMdast } = context;
|
||||
return {
|
||||
type: 'strong',
|
||||
children: [currentMdast],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const italicDeltaToMarkdownAdapterMatcher =
|
||||
InlineDeltaToMarkdownAdapterExtension({
|
||||
name: 'italic',
|
||||
match: delta => !!delta.attributes?.italic,
|
||||
toAST: (_, context) => {
|
||||
const { current: currentMdast } = context;
|
||||
return {
|
||||
type: 'emphasis',
|
||||
children: [currentMdast],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const strikeDeltaToMarkdownAdapterMatcher =
|
||||
InlineDeltaToMarkdownAdapterExtension({
|
||||
name: 'strike',
|
||||
match: delta => !!delta.attributes?.strike,
|
||||
toAST: (_, context) => {
|
||||
const { current: currentMdast } = context;
|
||||
return {
|
||||
type: 'delete',
|
||||
children: [currentMdast],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const inlineCodeDeltaToMarkdownAdapterMatcher =
|
||||
InlineDeltaToMarkdownAdapterExtension({
|
||||
name: 'inlineCode',
|
||||
match: delta => !!delta.attributes?.code,
|
||||
toAST: delta => ({
|
||||
type: 'inlineCode',
|
||||
value: delta.insert,
|
||||
}),
|
||||
});
|
||||
|
||||
export const InlineDeltaToMarkdownAdapterExtensions = [
|
||||
inlineCodeDeltaToMarkdownAdapterMatcher,
|
||||
boldDeltaToMarkdownAdapterMatcher,
|
||||
italicDeltaToMarkdownAdapterMatcher,
|
||||
strikeDeltaToMarkdownAdapterMatcher,
|
||||
];
|
||||
@@ -0,0 +1,98 @@
|
||||
import { MarkdownASTToDeltaExtension } from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const markdownTextToDeltaMatcher = MarkdownASTToDeltaExtension({
|
||||
name: 'text',
|
||||
match: ast => ast.type === 'text',
|
||||
toDelta: ast => {
|
||||
if (!('value' in ast)) {
|
||||
return [];
|
||||
}
|
||||
return [{ insert: ast.value }];
|
||||
},
|
||||
});
|
||||
|
||||
export const markdownInlineCodeToDeltaMatcher = MarkdownASTToDeltaExtension({
|
||||
name: 'inlineCode',
|
||||
match: ast => ast.type === 'inlineCode',
|
||||
toDelta: ast => {
|
||||
if (!('value' in ast)) {
|
||||
return [];
|
||||
}
|
||||
return [{ insert: ast.value, attributes: { code: true } }];
|
||||
},
|
||||
});
|
||||
|
||||
export const markdownStrongToDeltaMatcher = MarkdownASTToDeltaExtension({
|
||||
name: 'strong',
|
||||
match: ast => ast.type === 'strong',
|
||||
toDelta: (ast, context) => {
|
||||
if (!('children' in ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, bold: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const markdownEmphasisToDeltaMatcher = MarkdownASTToDeltaExtension({
|
||||
name: 'emphasis',
|
||||
match: ast => ast.type === 'emphasis',
|
||||
toDelta: (ast, context) => {
|
||||
if (!('children' in ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, italic: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const markdownDeleteToDeltaMatcher = MarkdownASTToDeltaExtension({
|
||||
name: 'delete',
|
||||
match: ast => ast.type === 'delete',
|
||||
toDelta: (ast, context) => {
|
||||
if (!('children' in ast)) {
|
||||
return [];
|
||||
}
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, strike: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const markdownListToDeltaMatcher = MarkdownASTToDeltaExtension({
|
||||
name: 'list',
|
||||
match: ast => ast.type === 'list',
|
||||
toDelta: () => [],
|
||||
});
|
||||
|
||||
export const markdownHtmlToDeltaMatcher = MarkdownASTToDeltaExtension({
|
||||
name: 'html',
|
||||
match: ast => ast.type === 'html',
|
||||
toDelta: (ast, context) => {
|
||||
if (!('value' in ast)) {
|
||||
return [];
|
||||
}
|
||||
return context?.htmlToDelta?.(ast.value) ?? [{ insert: ast.value }];
|
||||
},
|
||||
});
|
||||
|
||||
export const MarkdownInlineToDeltaAdapterExtensions = [
|
||||
markdownTextToDeltaMatcher,
|
||||
markdownInlineCodeToDeltaMatcher,
|
||||
markdownStrongToDeltaMatcher,
|
||||
markdownEmphasisToDeltaMatcher,
|
||||
markdownDeleteToDeltaMatcher,
|
||||
markdownListToDeltaMatcher,
|
||||
markdownHtmlToDeltaMatcher,
|
||||
];
|
||||
@@ -0,0 +1,256 @@
|
||||
import {
|
||||
HastUtils,
|
||||
type HtmlAST,
|
||||
NotionHtmlASTToDeltaExtension,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { collapseWhiteSpace } from 'collapse-white-space';
|
||||
import type { Element, Text } from 'hast';
|
||||
|
||||
const isElement = (ast: HtmlAST): ast is Element => {
|
||||
return ast.type === 'element';
|
||||
};
|
||||
|
||||
const isText = (ast: HtmlAST): ast is Text => {
|
||||
return ast.type === 'text';
|
||||
};
|
||||
|
||||
const listElementTags = new Set(['ol', 'ul']);
|
||||
const strongElementTags = new Set(['strong', 'b']);
|
||||
const italicElementTags = new Set(['i', 'em']);
|
||||
|
||||
const NotionInlineEquationToken = 'notion-text-equation-token';
|
||||
const NotionUnderlineStyleToken = 'border-bottom:0.05em solid';
|
||||
|
||||
export const notionHtmlTextToDeltaMatcher = NotionHtmlASTToDeltaExtension({
|
||||
name: 'text',
|
||||
match: ast => isText(ast),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isText(ast)) {
|
||||
return [];
|
||||
}
|
||||
const { options } = context;
|
||||
options.trim ??= true;
|
||||
if (options.pre || ast.value === ' ') {
|
||||
return [{ insert: ast.value }];
|
||||
}
|
||||
if (options.trim) {
|
||||
const value = collapseWhiteSpace(ast.value, { trim: options.trim });
|
||||
if (value) {
|
||||
return [{ insert: value }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (ast.value) {
|
||||
return [{ insert: collapseWhiteSpace(ast.value) }];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
export const notionHtmlSpanElementToDeltaMatcher =
|
||||
NotionHtmlASTToDeltaExtension({
|
||||
name: 'span-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'span',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { toDelta, options } = context;
|
||||
if (
|
||||
Array.isArray(ast.properties?.className) &&
|
||||
ast.properties?.className.includes(NotionInlineEquationToken)
|
||||
) {
|
||||
const latex = HastUtils.getTextContent(
|
||||
HastUtils.querySelector(ast, 'annotation')
|
||||
);
|
||||
return [{ insert: ' ', attributes: { latex } }];
|
||||
}
|
||||
|
||||
// Add underline style detection
|
||||
if (
|
||||
typeof ast.properties?.style === 'string' &&
|
||||
ast.properties?.style?.includes(NotionUnderlineStyleToken)
|
||||
) {
|
||||
return ast.children.flatMap(child =>
|
||||
context.toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, underline: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return ast.children.flatMap(child => toDelta(child, options));
|
||||
},
|
||||
});
|
||||
|
||||
export const notionHtmlListToDeltaMatcher = NotionHtmlASTToDeltaExtension({
|
||||
name: 'list-element',
|
||||
match: ast => isElement(ast) && listElementTags.has(ast.tagName),
|
||||
toDelta: () => {
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
export const notionHtmlStrongElementToDeltaMatcher =
|
||||
NotionHtmlASTToDeltaExtension({
|
||||
name: 'strong-element',
|
||||
match: ast => isElement(ast) && strongElementTags.has(ast.tagName),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, bold: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const notionHtmlItalicElementToDeltaMatcher =
|
||||
NotionHtmlASTToDeltaExtension({
|
||||
name: 'italic-element',
|
||||
match: ast => isElement(ast) && italicElementTags.has(ast.tagName),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, italic: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const notionHtmlCodeElementToDeltaMatcher =
|
||||
NotionHtmlASTToDeltaExtension({
|
||||
name: 'code-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'code',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, code: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const notionHtmlDelElementToDeltaMatcher = NotionHtmlASTToDeltaExtension(
|
||||
{
|
||||
name: 'del-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'del',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, strike: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const notionHtmlUnderlineElementToDeltaMatcher =
|
||||
NotionHtmlASTToDeltaExtension({
|
||||
name: 'underline-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'u',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes, underline: true };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const notionHtmlMarkElementToDeltaMatcher =
|
||||
NotionHtmlASTToDeltaExtension({
|
||||
name: 'mark-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'mark',
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast)) {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
return ast.children.flatMap(child =>
|
||||
toDelta(child, options).map(delta => {
|
||||
delta.attributes = { ...delta.attributes };
|
||||
return delta;
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const notionHtmlLiElementToDeltaMatcher = NotionHtmlASTToDeltaExtension({
|
||||
name: 'li-element',
|
||||
match: ast =>
|
||||
isElement(ast) &&
|
||||
ast.tagName === 'li' &&
|
||||
!!HastUtils.querySelector(ast, '.checkbox'),
|
||||
toDelta: (ast, context) => {
|
||||
if (!isElement(ast) || !HastUtils.querySelector(ast, '.checkbox')) {
|
||||
return [];
|
||||
}
|
||||
const { toDelta, options } = context;
|
||||
// Should ignore the children of to do list which is the checkbox and the space following it
|
||||
const checkBox = HastUtils.querySelector(ast, '.checkbox');
|
||||
const checkBoxIndex = ast.children.findIndex(child => child === checkBox);
|
||||
return ast.children
|
||||
.slice(checkBoxIndex + 2)
|
||||
.flatMap(child => toDelta(child, options));
|
||||
},
|
||||
});
|
||||
|
||||
export const notionHtmlBrElementToDeltaMatcher = NotionHtmlASTToDeltaExtension({
|
||||
name: 'br-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'br',
|
||||
toDelta: () => {
|
||||
return [{ insert: '\n' }];
|
||||
},
|
||||
});
|
||||
|
||||
export const notionHtmlStyleElementToDeltaMatcher =
|
||||
NotionHtmlASTToDeltaExtension({
|
||||
name: 'style-element',
|
||||
match: ast => isElement(ast) && ast.tagName === 'style',
|
||||
toDelta: () => {
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
export const NotionHtmlInlineToDeltaAdapterExtensions: ExtensionType[] = [
|
||||
notionHtmlTextToDeltaMatcher,
|
||||
notionHtmlSpanElementToDeltaMatcher,
|
||||
notionHtmlStrongElementToDeltaMatcher,
|
||||
notionHtmlItalicElementToDeltaMatcher,
|
||||
notionHtmlCodeElementToDeltaMatcher,
|
||||
notionHtmlDelElementToDeltaMatcher,
|
||||
notionHtmlUnderlineElementToDeltaMatcher,
|
||||
notionHtmlMarkElementToDeltaMatcher,
|
||||
notionHtmlListToDeltaMatcher,
|
||||
notionHtmlLiElementToDeltaMatcher,
|
||||
notionHtmlBrElementToDeltaMatcher,
|
||||
notionHtmlStyleElementToDeltaMatcher,
|
||||
];
|
||||
138
blocksuite/affine/inlines/preset/src/command/config.ts
Normal file
138
blocksuite/affine/inlines/preset/src/command/config.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
BoldIcon,
|
||||
CodeIcon,
|
||||
ItalicIcon,
|
||||
LinkIcon,
|
||||
StrikethroughIcon,
|
||||
UnderlineIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import { toggleLink } from '@blocksuite/affine-inline-link';
|
||||
import { type EditorHost, TextSelection } from '@blocksuite/std';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
import {
|
||||
isTextAttributeActive,
|
||||
toggleBold,
|
||||
toggleCode,
|
||||
toggleItalic,
|
||||
toggleStrike,
|
||||
toggleUnderline,
|
||||
} from './text-style.js';
|
||||
|
||||
export interface TextFormatConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: TemplateResult<1>;
|
||||
hotkey?: string;
|
||||
activeWhen: (host: EditorHost) => boolean;
|
||||
action: (host: EditorHost) => void;
|
||||
textChecker?: (host: EditorHost) => boolean;
|
||||
}
|
||||
|
||||
export const textFormatConfigs: TextFormatConfig[] = [
|
||||
{
|
||||
id: 'bold',
|
||||
name: 'Bold',
|
||||
icon: BoldIcon,
|
||||
hotkey: 'Mod-b',
|
||||
activeWhen: host => {
|
||||
const [result] = host.std.command
|
||||
.chain()
|
||||
.pipe(isTextAttributeActive, { key: 'bold' })
|
||||
.run();
|
||||
return result;
|
||||
},
|
||||
action: host => {
|
||||
host.std.command.chain().pipe(toggleBold).run();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'italic',
|
||||
name: 'Italic',
|
||||
icon: ItalicIcon,
|
||||
hotkey: 'Mod-i',
|
||||
activeWhen: host => {
|
||||
const [result] = host.std.command
|
||||
.chain()
|
||||
.pipe(isTextAttributeActive, { key: 'italic' })
|
||||
.run();
|
||||
return result;
|
||||
},
|
||||
action: host => {
|
||||
host.std.command.chain().pipe(toggleItalic).run();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'underline',
|
||||
name: 'Underline',
|
||||
icon: UnderlineIcon,
|
||||
hotkey: 'Mod-u',
|
||||
activeWhen: host => {
|
||||
const [result] = host.std.command
|
||||
.chain()
|
||||
.pipe(isTextAttributeActive, { key: 'underline' })
|
||||
.run();
|
||||
return result;
|
||||
},
|
||||
action: host => {
|
||||
host.std.command.chain().pipe(toggleUnderline).run();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'strike',
|
||||
name: 'Strikethrough',
|
||||
icon: StrikethroughIcon,
|
||||
hotkey: 'Mod-shift-s',
|
||||
activeWhen: host => {
|
||||
const [result] = host.std.command
|
||||
.chain()
|
||||
.pipe(isTextAttributeActive, { key: 'strike' })
|
||||
.run();
|
||||
return result;
|
||||
},
|
||||
action: host => {
|
||||
host.std.command.chain().pipe(toggleStrike).run();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'code',
|
||||
name: 'Code',
|
||||
icon: CodeIcon,
|
||||
hotkey: 'Mod-e',
|
||||
activeWhen: host => {
|
||||
const [result] = host.std.command
|
||||
.chain()
|
||||
.pipe(isTextAttributeActive, { key: 'code' })
|
||||
.run();
|
||||
return result;
|
||||
},
|
||||
action: host => {
|
||||
host.std.command.chain().pipe(toggleCode).run();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'link',
|
||||
name: 'Link',
|
||||
icon: LinkIcon,
|
||||
hotkey: 'Mod-k',
|
||||
activeWhen: host => {
|
||||
const [result] = host.std.command
|
||||
.chain()
|
||||
.pipe(isTextAttributeActive, { key: 'link' })
|
||||
.run();
|
||||
return result;
|
||||
},
|
||||
action: host => {
|
||||
host.std.command.chain().pipe(toggleLink).run();
|
||||
},
|
||||
// should check text length
|
||||
textChecker: host => {
|
||||
const textSelection = host.std.selection.find(TextSelection);
|
||||
if (!textSelection || textSelection.isCollapsed()) return false;
|
||||
|
||||
return Boolean(
|
||||
textSelection.from.length + (textSelection.to?.length ?? 0)
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
19
blocksuite/affine/inlines/preset/src/command/consts.ts
Normal file
19
blocksuite/affine/inlines/preset/src/command/consts.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// corresponding to `formatText` command
|
||||
import { TableModelFlavour } from '@blocksuite/affine-model';
|
||||
|
||||
export const FORMAT_TEXT_SUPPORT_FLAVOURS = [
|
||||
'affine:paragraph',
|
||||
'affine:list',
|
||||
'affine:code',
|
||||
];
|
||||
// corresponding to `formatBlock` command
|
||||
export const FORMAT_BLOCK_SUPPORT_FLAVOURS = [
|
||||
'affine:paragraph',
|
||||
'affine:list',
|
||||
'affine:code',
|
||||
];
|
||||
// corresponding to `formatNative` command
|
||||
export const FORMAT_NATIVE_SUPPORT_FLAVOURS = [
|
||||
'affine:database',
|
||||
TableModelFlavour,
|
||||
];
|
||||
81
blocksuite/affine/inlines/preset/src/command/delete-text.ts
Normal file
81
blocksuite/affine/inlines/preset/src/command/delete-text.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import { type Command, TextSelection } from '@blocksuite/std';
|
||||
import type { Text } from '@blocksuite/store';
|
||||
|
||||
export const deleteTextCommand: Command<{
|
||||
currentTextSelection?: TextSelection;
|
||||
textSelection?: TextSelection;
|
||||
}> = (ctx, next) => {
|
||||
const textSelection = ctx.textSelection ?? ctx.currentTextSelection;
|
||||
if (!textSelection) return;
|
||||
|
||||
const range = ctx.std.range.textSelectionToRange(textSelection);
|
||||
if (!range) return;
|
||||
const selectedElements = ctx.std.range.getSelectedBlockComponentsByRange(
|
||||
range,
|
||||
{
|
||||
mode: 'flat',
|
||||
}
|
||||
);
|
||||
|
||||
const { from, to } = textSelection;
|
||||
|
||||
const fromElement = selectedElements.find(el => from.blockId === el.blockId);
|
||||
if (!fromElement) return;
|
||||
|
||||
let fromText: Text | undefined;
|
||||
if (matchModels(fromElement.model, [RootBlockModel])) {
|
||||
fromText = fromElement.model.props.title;
|
||||
} else {
|
||||
fromText = fromElement.model.text;
|
||||
}
|
||||
if (!fromText) return;
|
||||
if (!to) {
|
||||
fromText.delete(from.index, from.length);
|
||||
ctx.std.selection.setGroup('note', [
|
||||
ctx.std.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: from.blockId,
|
||||
index: from.index,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
return next();
|
||||
}
|
||||
|
||||
const toElement = selectedElements.find(el => to.blockId === el.blockId);
|
||||
if (!toElement) return;
|
||||
|
||||
const toText = toElement.model.text;
|
||||
if (!toText) return;
|
||||
|
||||
fromText.delete(from.index, from.length);
|
||||
toText.delete(0, to.length);
|
||||
|
||||
fromText.join(toText);
|
||||
|
||||
selectedElements
|
||||
.filter(el => el.model.id !== fromElement.model.id)
|
||||
.forEach(el => {
|
||||
ctx.std.store.deleteBlock(el.model, {
|
||||
bringChildrenTo:
|
||||
el.model.id === toElement.model.id ? fromElement.model : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
ctx.std.selection.setGroup('note', [
|
||||
ctx.std.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: from.blockId,
|
||||
index: from.index,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
|
||||
next();
|
||||
};
|
||||
75
blocksuite/affine/inlines/preset/src/command/format-block.ts
Normal file
75
blocksuite/affine/inlines/preset/src/command/format-block.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { getSelectedBlocksCommand } from '@blocksuite/affine-shared/commands';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import type { BlockSelection, Command } from '@blocksuite/std';
|
||||
import {
|
||||
INLINE_ROOT_ATTR,
|
||||
type InlineRootElement,
|
||||
} from '@blocksuite/std/inline';
|
||||
|
||||
import { FORMAT_BLOCK_SUPPORT_FLAVOURS } from './consts.js';
|
||||
|
||||
// for block selection
|
||||
export const formatBlockCommand: Command<{
|
||||
currentBlockSelections?: BlockSelection[];
|
||||
blockSelections?: BlockSelection[];
|
||||
styles: AffineTextAttributes;
|
||||
mode?: 'replace' | 'merge';
|
||||
}> = (ctx, next) => {
|
||||
const blockSelections = ctx.blockSelections ?? ctx.currentBlockSelections;
|
||||
if (!blockSelections) {
|
||||
console.error(
|
||||
'`blockSelections` is required, you need to pass it in args or use `getBlockSelections` command before adding this command to the pipeline.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (blockSelections.length === 0) return;
|
||||
|
||||
const styles = ctx.styles;
|
||||
const mode = ctx.mode ?? 'merge';
|
||||
|
||||
const success = ctx.std.command
|
||||
.chain()
|
||||
.pipe(getSelectedBlocksCommand, {
|
||||
blockSelections,
|
||||
filter: el => FORMAT_BLOCK_SUPPORT_FLAVOURS.includes(el.model.flavour),
|
||||
types: ['block'],
|
||||
})
|
||||
.pipe((ctx, next) => {
|
||||
const { selectedBlocks } = ctx;
|
||||
if (!selectedBlocks) {
|
||||
console.error(
|
||||
'`selectedBlocks` is required, you need to pass it in args or use `getSelectedBlocksCommand` command before adding this command to the pipeline.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedInlineEditors = selectedBlocks.flatMap(el => {
|
||||
const inlineRoot = el.querySelector<
|
||||
InlineRootElement<AffineTextAttributes>
|
||||
>(`[${INLINE_ROOT_ATTR}]`);
|
||||
if (inlineRoot) {
|
||||
return inlineRoot.inlineEditor;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
selectedInlineEditors.forEach(inlineEditor => {
|
||||
inlineEditor.formatText(
|
||||
{
|
||||
index: 0,
|
||||
length: inlineEditor.yTextLength,
|
||||
},
|
||||
styles,
|
||||
{
|
||||
mode,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
next();
|
||||
})
|
||||
.run();
|
||||
|
||||
if (success) next();
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
BLOCK_ID_ATTR,
|
||||
type BlockComponent,
|
||||
type Command,
|
||||
} from '@blocksuite/std';
|
||||
import {
|
||||
INLINE_ROOT_ATTR,
|
||||
type InlineRootElement,
|
||||
} from '@blocksuite/std/inline';
|
||||
|
||||
import { FORMAT_NATIVE_SUPPORT_FLAVOURS } from './consts.js';
|
||||
|
||||
// for native range
|
||||
export const formatNativeCommand: Command<{
|
||||
range?: Range;
|
||||
styles: AffineTextAttributes;
|
||||
mode?: 'replace' | 'merge';
|
||||
}> = (ctx, next) => {
|
||||
const { styles, mode = 'merge' } = ctx;
|
||||
|
||||
let range = ctx.range;
|
||||
if (!range) {
|
||||
const selection = document.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
range = selection.getRangeAt(0);
|
||||
}
|
||||
if (!range) return;
|
||||
|
||||
const selectedInlineEditors = Array.from<InlineRootElement>(
|
||||
ctx.std.host.querySelectorAll(`[${INLINE_ROOT_ATTR}]`)
|
||||
)
|
||||
.filter(el => range?.intersectsNode(el))
|
||||
.filter(el => {
|
||||
const block = el.closest<BlockComponent>(`[${BLOCK_ID_ATTR}]`);
|
||||
if (block) {
|
||||
return FORMAT_NATIVE_SUPPORT_FLAVOURS.includes(block.model.flavour);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(el => el.inlineEditor);
|
||||
|
||||
selectedInlineEditors.forEach(inlineEditor => {
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return;
|
||||
|
||||
inlineEditor.formatText(inlineRange, styles, {
|
||||
mode,
|
||||
});
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
94
blocksuite/affine/inlines/preset/src/command/format-text.ts
Normal file
94
blocksuite/affine/inlines/preset/src/command/format-text.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { clearMarksOnDiscontinuousInput } from '@blocksuite/affine-rich-text';
|
||||
import { getSelectedBlocksCommand } from '@blocksuite/affine-shared/commands';
|
||||
import type {
|
||||
AffineTextAttributes,
|
||||
AffineTextStyleAttributes,
|
||||
} from '@blocksuite/affine-shared/types';
|
||||
import type { Command, TextSelection } from '@blocksuite/std';
|
||||
import {
|
||||
INLINE_ROOT_ATTR,
|
||||
type InlineRootElement,
|
||||
} from '@blocksuite/std/inline';
|
||||
|
||||
import { FORMAT_TEXT_SUPPORT_FLAVOURS } from './consts.js';
|
||||
|
||||
// for text selection
|
||||
export const formatTextCommand: Command<{
|
||||
currentTextSelection?: TextSelection;
|
||||
textSelection?: TextSelection;
|
||||
styles: AffineTextStyleAttributes;
|
||||
mode?: 'replace' | 'merge';
|
||||
}> = (ctx, next) => {
|
||||
const { styles, mode = 'merge' } = ctx;
|
||||
|
||||
const textSelection = ctx.textSelection ?? ctx.currentTextSelection;
|
||||
if (!textSelection) return;
|
||||
|
||||
const success = ctx.std.command
|
||||
.chain()
|
||||
.pipe(getSelectedBlocksCommand, {
|
||||
textSelection,
|
||||
filter: el => FORMAT_TEXT_SUPPORT_FLAVOURS.includes(el.model.flavour),
|
||||
types: ['text'],
|
||||
})
|
||||
.pipe((ctx, next) => {
|
||||
const { selectedBlocks } = ctx;
|
||||
if (!selectedBlocks) return;
|
||||
|
||||
const selectedInlineEditors = selectedBlocks.flatMap(el => {
|
||||
const inlineRoot = el.querySelector<
|
||||
InlineRootElement<AffineTextAttributes>
|
||||
>(`[${INLINE_ROOT_ATTR}]`);
|
||||
if (inlineRoot && inlineRoot.inlineEditor.getInlineRange()) {
|
||||
return inlineRoot.inlineEditor;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
selectedInlineEditors.forEach(inlineEditor => {
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return;
|
||||
|
||||
if (inlineRange.length === 0) {
|
||||
const delta = inlineEditor.getDeltaByRangeIndex(inlineRange.index);
|
||||
|
||||
inlineEditor.setMarks({
|
||||
...inlineEditor.marks,
|
||||
...Object.fromEntries(
|
||||
Object.entries(styles).map(([key, value]) => {
|
||||
if (typeof value === 'boolean') {
|
||||
return [
|
||||
key,
|
||||
(inlineEditor.marks &&
|
||||
inlineEditor.marks[key as keyof AffineTextAttributes]) ||
|
||||
(delta &&
|
||||
delta.attributes &&
|
||||
delta.attributes[key as keyof AffineTextAttributes])
|
||||
? null
|
||||
: value,
|
||||
];
|
||||
}
|
||||
return [key, value];
|
||||
})
|
||||
),
|
||||
});
|
||||
clearMarksOnDiscontinuousInput(inlineEditor);
|
||||
} else {
|
||||
inlineEditor.formatText(inlineRange, styles, {
|
||||
mode,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Promise.all(selectedBlocks.map(el => el.updateComplete))
|
||||
.then(() => {
|
||||
ctx.std.range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
next();
|
||||
})
|
||||
.run();
|
||||
|
||||
if (success) next();
|
||||
};
|
||||
22
blocksuite/affine/inlines/preset/src/command/index.ts
Normal file
22
blocksuite/affine/inlines/preset/src/command/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export type { TextFormatConfig } from './config.js';
|
||||
export { textFormatConfigs } from './config.js';
|
||||
export {
|
||||
FORMAT_BLOCK_SUPPORT_FLAVOURS,
|
||||
FORMAT_NATIVE_SUPPORT_FLAVOURS,
|
||||
FORMAT_TEXT_SUPPORT_FLAVOURS,
|
||||
} from './consts.js';
|
||||
export { deleteTextCommand } from './delete-text.js';
|
||||
export { formatBlockCommand } from './format-block.js';
|
||||
export { formatNativeCommand } from './format-native.js';
|
||||
export { formatTextCommand } from './format-text.js';
|
||||
export {
|
||||
getTextAttributes,
|
||||
isTextAttributeActive,
|
||||
toggleBold,
|
||||
toggleCode,
|
||||
toggleItalic,
|
||||
toggleStrike,
|
||||
toggleTextStyleCommand,
|
||||
toggleUnderline,
|
||||
} from './text-style.js';
|
||||
export { isFormatSupported } from './utils.js';
|
||||
110
blocksuite/affine/inlines/preset/src/command/text-style.ts
Normal file
110
blocksuite/affine/inlines/preset/src/command/text-style.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
getBlockSelectionsCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import type {
|
||||
AffineTextAttributes,
|
||||
AffineTextStyleAttributes,
|
||||
} from '@blocksuite/affine-shared/types';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
|
||||
import { formatBlockCommand } from './format-block.js';
|
||||
import { formatNativeCommand } from './format-native.js';
|
||||
import { formatTextCommand } from './format-text.js';
|
||||
import { getCombinedTextAttributes } from './utils.js';
|
||||
|
||||
export const toggleTextStyleCommand: Command<{
|
||||
key: Extract<
|
||||
keyof AffineTextStyleAttributes,
|
||||
'bold' | 'italic' | 'underline' | 'strike' | 'code'
|
||||
>;
|
||||
}> = (ctx, next) => {
|
||||
const { std, key } = ctx;
|
||||
const [active] = std.command
|
||||
.chain()
|
||||
.pipe(isTextAttributeActive, { key })
|
||||
.run();
|
||||
|
||||
const payload: {
|
||||
styles: AffineTextStyleAttributes;
|
||||
mode?: 'replace' | 'merge';
|
||||
} = {
|
||||
styles: {
|
||||
[key]: active ? null : true,
|
||||
},
|
||||
};
|
||||
|
||||
const [result] = std.command
|
||||
.chain()
|
||||
.try(chain => [
|
||||
chain.pipe(getTextSelectionCommand).pipe(formatTextCommand, payload),
|
||||
chain.pipe(getBlockSelectionsCommand).pipe(formatBlockCommand, payload),
|
||||
chain.pipe(formatNativeCommand, payload),
|
||||
])
|
||||
.run();
|
||||
|
||||
if (result) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const toggleTextStyleCommandWrapper = (
|
||||
key: Extract<
|
||||
keyof AffineTextStyleAttributes,
|
||||
'bold' | 'italic' | 'underline' | 'strike' | 'code'
|
||||
>
|
||||
): Command => {
|
||||
return (ctx, next) => {
|
||||
const [success] = ctx.std.command
|
||||
.chain()
|
||||
.pipe(toggleTextStyleCommand, { key })
|
||||
.run();
|
||||
if (success) next();
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleBold = toggleTextStyleCommandWrapper('bold');
|
||||
export const toggleItalic = toggleTextStyleCommandWrapper('italic');
|
||||
export const toggleUnderline = toggleTextStyleCommandWrapper('underline');
|
||||
export const toggleStrike = toggleTextStyleCommandWrapper('strike');
|
||||
export const toggleCode = toggleTextStyleCommandWrapper('code');
|
||||
|
||||
export const getTextAttributes: Command<
|
||||
{},
|
||||
{ textAttributes: AffineTextAttributes }
|
||||
> = (ctx, next) => {
|
||||
const [result, innerCtx] = getCombinedTextAttributes(
|
||||
ctx.std.command.chain()
|
||||
).run();
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return next({ textAttributes: innerCtx.textAttributes });
|
||||
};
|
||||
|
||||
export const isTextAttributeActive: Command<{
|
||||
key: keyof AffineTextAttributes;
|
||||
}> = (ctx, next) => {
|
||||
const key = ctx.key;
|
||||
const [result] = getCombinedTextAttributes(ctx.std.command.chain())
|
||||
.pipe((ctx, next) => {
|
||||
const { textAttributes } = ctx;
|
||||
|
||||
if (textAttributes && key in textAttributes) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.run();
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
226
blocksuite/affine/inlines/preset/src/command/utils.ts
Normal file
226
blocksuite/affine/inlines/preset/src/command/utils.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import {
|
||||
getBlockSelectionsCommand,
|
||||
getSelectedBlocksCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import type {
|
||||
AffineInlineEditor,
|
||||
AffineTextAttributes,
|
||||
} from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
BLOCK_ID_ATTR,
|
||||
type BlockComponent,
|
||||
type Chain,
|
||||
type InitCommandCtx,
|
||||
} from '@blocksuite/std';
|
||||
import {
|
||||
INLINE_ROOT_ATTR,
|
||||
type InlineEditor,
|
||||
type InlineRange,
|
||||
type InlineRootElement,
|
||||
} from '@blocksuite/std/inline';
|
||||
|
||||
import {
|
||||
FORMAT_BLOCK_SUPPORT_FLAVOURS,
|
||||
FORMAT_NATIVE_SUPPORT_FLAVOURS,
|
||||
FORMAT_TEXT_SUPPORT_FLAVOURS,
|
||||
} from './consts.js';
|
||||
|
||||
function getCombinedFormatFromInlineEditors(
|
||||
inlineEditors: [AffineInlineEditor, InlineRange | null][]
|
||||
): AffineTextAttributes {
|
||||
const formatArr: AffineTextAttributes[] = [];
|
||||
inlineEditors.forEach(([inlineEditor, inlineRange]) => {
|
||||
if (!inlineRange) return;
|
||||
|
||||
const format = inlineEditor.getFormat(inlineRange);
|
||||
formatArr.push(format);
|
||||
});
|
||||
|
||||
if (formatArr.length === 0) return {};
|
||||
|
||||
// format will be active only when all inline editors have the same format.
|
||||
return formatArr.reduce((acc, cur) => {
|
||||
const newFormat: AffineTextAttributes = {};
|
||||
for (const key in acc) {
|
||||
const typedKey = key as keyof AffineTextAttributes;
|
||||
if (acc[typedKey] === cur[typedKey]) {
|
||||
// This cast is secure because we have checked that the value of the key is the same.
|
||||
|
||||
newFormat[typedKey] = acc[typedKey] as any;
|
||||
}
|
||||
}
|
||||
return newFormat;
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedInlineEditors(
|
||||
blocks: BlockComponent[],
|
||||
filter: (
|
||||
inlineRoot: InlineRootElement<AffineTextAttributes>
|
||||
) => InlineEditor<AffineTextAttributes> | []
|
||||
) {
|
||||
return blocks.flatMap(el => {
|
||||
const inlineRoot = el.querySelector<
|
||||
InlineRootElement<AffineTextAttributes>
|
||||
>(`[${INLINE_ROOT_ATTR}]`);
|
||||
|
||||
if (inlineRoot) {
|
||||
return filter(inlineRoot);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function handleCurrentSelection(
|
||||
chain: Chain<InitCommandCtx>,
|
||||
handler: (
|
||||
type: 'text' | 'block' | 'native',
|
||||
inlineEditors: InlineEditor<AffineTextAttributes>[]
|
||||
) => { textAttributes: AffineTextAttributes } | boolean | void
|
||||
): Chain<InitCommandCtx & { textAttributes: AffineTextAttributes }> {
|
||||
return chain.try(chain => [
|
||||
// text selection, corresponding to `formatText` command
|
||||
chain
|
||||
.pipe(getTextSelectionCommand)
|
||||
.pipe(getSelectedBlocksCommand, {
|
||||
types: ['text'],
|
||||
filter: el => FORMAT_TEXT_SUPPORT_FLAVOURS.includes(el.model.flavour),
|
||||
})
|
||||
.pipe((ctx, next) => {
|
||||
const { selectedBlocks } = ctx;
|
||||
if (!selectedBlocks) {
|
||||
console.error(
|
||||
'`selectedBlocks` is required, you need to pass it in args or use `getSelectedBlocksCommand` command before adding this command to the pipeline.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedInlineEditors = getSelectedInlineEditors(
|
||||
selectedBlocks,
|
||||
inlineRoot => {
|
||||
const inlineRange = inlineRoot.inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return [];
|
||||
return inlineRoot.inlineEditor;
|
||||
}
|
||||
);
|
||||
|
||||
const result = handler('text', selectedInlineEditors);
|
||||
if (!result) return false;
|
||||
if (result === true) {
|
||||
return next();
|
||||
}
|
||||
return next(result);
|
||||
}),
|
||||
// block selection, corresponding to `formatBlock` command
|
||||
chain
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe(getSelectedBlocksCommand, {
|
||||
types: ['block'],
|
||||
filter: el => FORMAT_BLOCK_SUPPORT_FLAVOURS.includes(el.model.flavour),
|
||||
})
|
||||
.pipe((ctx, next) => {
|
||||
const { selectedBlocks } = ctx;
|
||||
if (!selectedBlocks) {
|
||||
console.error(
|
||||
'`selectedBlocks` is required, you need to pass it in args or use `getSelectedBlocksCommand` command before adding this command to the pipeline.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedInlineEditors = getSelectedInlineEditors(
|
||||
selectedBlocks,
|
||||
inlineRoot =>
|
||||
inlineRoot.inlineEditor.yTextLength > 0
|
||||
? inlineRoot.inlineEditor
|
||||
: []
|
||||
);
|
||||
|
||||
const result = handler('block', selectedInlineEditors);
|
||||
if (!result) return false;
|
||||
if (result === true) {
|
||||
return next();
|
||||
}
|
||||
return next(result);
|
||||
}),
|
||||
// native selection, corresponding to `formatNative` command
|
||||
chain.pipe((ctx, next) => {
|
||||
const selectedInlineEditors = Array.from<InlineRootElement>(
|
||||
ctx.std.host.querySelectorAll(`[${INLINE_ROOT_ATTR}]`)
|
||||
)
|
||||
.filter(el => {
|
||||
const selection = document.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return false;
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
return range.intersectsNode(el);
|
||||
})
|
||||
.filter(el => {
|
||||
const block = el.closest<BlockComponent>(`[${BLOCK_ID_ATTR}]`);
|
||||
if (block) {
|
||||
return FORMAT_NATIVE_SUPPORT_FLAVOURS.includes(block.model.flavour);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map((el): AffineInlineEditor => el.inlineEditor);
|
||||
|
||||
const result = handler('native', selectedInlineEditors);
|
||||
if (!result) return false;
|
||||
if (result === true) {
|
||||
return next();
|
||||
}
|
||||
return next(result);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export function getCombinedTextAttributes(chain: Chain<InitCommandCtx>) {
|
||||
return handleCurrentSelection(chain, (type, inlineEditors) => {
|
||||
if (type === 'text') {
|
||||
return {
|
||||
textAttributes: getCombinedFormatFromInlineEditors(
|
||||
inlineEditors.map(e => [e, e.getInlineRange()])
|
||||
),
|
||||
};
|
||||
}
|
||||
if (type === 'block') {
|
||||
return {
|
||||
textAttributes: getCombinedFormatFromInlineEditors(
|
||||
inlineEditors.map(e => [e, { index: 0, length: e.yTextLength }])
|
||||
),
|
||||
};
|
||||
}
|
||||
if (type === 'native') {
|
||||
return {
|
||||
textAttributes: getCombinedFormatFromInlineEditors(
|
||||
inlineEditors.map(e => [e, e.getInlineRange()])
|
||||
),
|
||||
};
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
export function isFormatSupported(chain: Chain<InitCommandCtx>) {
|
||||
return handleCurrentSelection(chain, (_type, inlineEditors) => {
|
||||
if (inlineEditors.length === 1) {
|
||||
const inlineEditor = inlineEditors[0];
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
|
||||
// support block selection
|
||||
if (!inlineRange) return true;
|
||||
|
||||
if (inlineRange.length !== 1) return true;
|
||||
|
||||
// skip embed node
|
||||
const delta = inlineEditor.getDeltaByRangeIndex(inlineRange.index + 1);
|
||||
if (!delta) return true;
|
||||
|
||||
const isEmbed = inlineEditor.isEmbed(delta);
|
||||
if (isEmbed) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
return inlineEditors.length > 0;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { CommentInlineSpecExtension } from '@blocksuite/affine-inline-comment';
|
||||
import { FootNoteInlineSpecExtension } from '@blocksuite/affine-inline-footnote';
|
||||
import { LatexInlineSpecExtension } from '@blocksuite/affine-inline-latex';
|
||||
import { LinkInlineSpecExtension } from '@blocksuite/affine-inline-link';
|
||||
import { MentionInlineSpecExtension } from '@blocksuite/affine-inline-mention';
|
||||
import { ReferenceInlineSpecExtension } from '@blocksuite/affine-inline-reference';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { InlineManagerExtension } from '@blocksuite/std/inline';
|
||||
|
||||
import {
|
||||
BackgroundInlineSpecExtension,
|
||||
BoldInlineSpecExtension,
|
||||
CodeInlineSpecExtension,
|
||||
ColorInlineSpecExtension,
|
||||
ItalicInlineSpecExtension,
|
||||
StrikeInlineSpecExtension,
|
||||
UnderlineInlineSpecExtension,
|
||||
} from './inline-spec';
|
||||
|
||||
export const DefaultInlineManagerExtension =
|
||||
InlineManagerExtension<AffineTextAttributes>({
|
||||
id: 'DefaultInlineManager',
|
||||
specs: [
|
||||
BoldInlineSpecExtension.identifier,
|
||||
ItalicInlineSpecExtension.identifier,
|
||||
UnderlineInlineSpecExtension.identifier,
|
||||
StrikeInlineSpecExtension.identifier,
|
||||
CodeInlineSpecExtension.identifier,
|
||||
BackgroundInlineSpecExtension.identifier,
|
||||
ColorInlineSpecExtension.identifier,
|
||||
LatexInlineSpecExtension.identifier,
|
||||
ReferenceInlineSpecExtension.identifier,
|
||||
LinkInlineSpecExtension.identifier,
|
||||
FootNoteInlineSpecExtension.identifier,
|
||||
MentionInlineSpecExtension.identifier,
|
||||
CommentInlineSpecExtension.identifier,
|
||||
],
|
||||
});
|
||||
11
blocksuite/affine/inlines/preset/src/effects.ts
Normal file
11
blocksuite/affine/inlines/preset/src/effects.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AffineText } from './nodes/affine-text';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('affine-text', AffineText);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-text': AffineText;
|
||||
}
|
||||
}
|
||||
10
blocksuite/affine/inlines/preset/src/index.ts
Normal file
10
blocksuite/affine/inlines/preset/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type * as RichTextEffects from '@blocksuite/affine-rich-text/effects';
|
||||
|
||||
declare type _GLOBAL_ = typeof RichTextEffects;
|
||||
|
||||
export * from './adapters/extensions';
|
||||
export * from './command';
|
||||
export * from './default-inline-manager';
|
||||
export * from './inline-spec';
|
||||
export * from './keymap';
|
||||
export * from './markdown';
|
||||
118
blocksuite/affine/inlines/preset/src/inline-spec.ts
Normal file
118
blocksuite/affine/inlines/preset/src/inline-spec.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
type InlineRootElement,
|
||||
InlineSpecExtension,
|
||||
} from '@blocksuite/std/inline';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { html } from 'lit';
|
||||
import { z } from 'zod';
|
||||
|
||||
export type AffineInlineRootElement = InlineRootElement<AffineTextAttributes>;
|
||||
|
||||
export const BoldInlineSpecExtension =
|
||||
InlineSpecExtension<AffineTextAttributes>({
|
||||
name: 'bold',
|
||||
schema: z.object({
|
||||
bold: z.literal(true).optional().nullable().catch(undefined),
|
||||
}),
|
||||
match: delta => {
|
||||
return !!delta.attributes?.bold;
|
||||
},
|
||||
renderer: ({ delta }) => {
|
||||
return html`<affine-text .delta=${delta}></affine-text>`;
|
||||
},
|
||||
});
|
||||
|
||||
export const ItalicInlineSpecExtension =
|
||||
InlineSpecExtension<AffineTextAttributes>({
|
||||
name: 'italic',
|
||||
schema: z.object({
|
||||
italic: z.literal(true).optional().nullable().catch(undefined),
|
||||
}),
|
||||
match: delta => {
|
||||
return !!delta.attributes?.italic;
|
||||
},
|
||||
renderer: ({ delta }) => {
|
||||
return html`<affine-text .delta=${delta}></affine-text>`;
|
||||
},
|
||||
});
|
||||
|
||||
export const UnderlineInlineSpecExtension =
|
||||
InlineSpecExtension<AffineTextAttributes>({
|
||||
name: 'underline',
|
||||
schema: z.object({
|
||||
underline: z.literal(true).optional().nullable().catch(undefined),
|
||||
}),
|
||||
match: delta => {
|
||||
return !!delta.attributes?.underline;
|
||||
},
|
||||
renderer: ({ delta }) => {
|
||||
return html`<affine-text .delta=${delta}></affine-text>`;
|
||||
},
|
||||
});
|
||||
|
||||
export const StrikeInlineSpecExtension =
|
||||
InlineSpecExtension<AffineTextAttributes>({
|
||||
name: 'strike',
|
||||
schema: z.object({
|
||||
strike: z.literal(true).optional().nullable().catch(undefined),
|
||||
}),
|
||||
match: delta => {
|
||||
return !!delta.attributes?.strike;
|
||||
},
|
||||
renderer: ({ delta }) => {
|
||||
return html`<affine-text .delta=${delta}></affine-text>`;
|
||||
},
|
||||
});
|
||||
|
||||
export const CodeInlineSpecExtension =
|
||||
InlineSpecExtension<AffineTextAttributes>({
|
||||
name: 'inline-code',
|
||||
schema: z.object({
|
||||
code: z.literal(true).optional().nullable().catch(undefined),
|
||||
}),
|
||||
match: delta => {
|
||||
return !!delta.attributes?.code;
|
||||
},
|
||||
renderer: ({ delta }) => {
|
||||
return html`<affine-text .delta=${delta}></affine-text>`;
|
||||
},
|
||||
});
|
||||
|
||||
export const BackgroundInlineSpecExtension =
|
||||
InlineSpecExtension<AffineTextAttributes>({
|
||||
name: 'background',
|
||||
schema: z.object({
|
||||
background: z.string().optional().nullable().catch(undefined),
|
||||
}),
|
||||
match: delta => {
|
||||
return !!delta.attributes?.background;
|
||||
},
|
||||
renderer: ({ delta }) => {
|
||||
return html`<affine-text .delta=${delta}></affine-text>`;
|
||||
},
|
||||
});
|
||||
|
||||
export const ColorInlineSpecExtension =
|
||||
InlineSpecExtension<AffineTextAttributes>({
|
||||
name: 'color',
|
||||
schema: z.object({
|
||||
color: z.string().optional().nullable().catch(undefined),
|
||||
}),
|
||||
match: delta => {
|
||||
return !!delta.attributes?.color;
|
||||
},
|
||||
renderer: ({ delta }) => {
|
||||
return html`<affine-text .delta=${delta}></affine-text>`;
|
||||
},
|
||||
});
|
||||
|
||||
export const InlineSpecExtensions: ExtensionType[] = [
|
||||
BoldInlineSpecExtension,
|
||||
ItalicInlineSpecExtension,
|
||||
UnderlineInlineSpecExtension,
|
||||
StrikeInlineSpecExtension,
|
||||
CodeInlineSpecExtension,
|
||||
BackgroundInlineSpecExtension,
|
||||
ColorInlineSpecExtension,
|
||||
];
|
||||
78
blocksuite/affine/inlines/preset/src/keymap/basic.ts
Normal file
78
blocksuite/affine/inlines/preset/src/keymap/basic.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
focusTextModel,
|
||||
getInlineEditorByModel,
|
||||
selectTextModel,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
BlockSelection,
|
||||
type BlockStdScope,
|
||||
TextSelection,
|
||||
type UIEventHandler,
|
||||
} from '@blocksuite/std';
|
||||
|
||||
export const textCommonKeymap = (
|
||||
std: BlockStdScope
|
||||
): Record<string, UIEventHandler> => {
|
||||
return {
|
||||
ArrowUp: () => {
|
||||
const text = std.selection.find(TextSelection);
|
||||
if (!text) return;
|
||||
const inline = getInlineEditorByModel(std, text.from.blockId);
|
||||
if (!inline) return;
|
||||
return !inline.isFirstLine(inline.getInlineRange());
|
||||
},
|
||||
ArrowDown: () => {
|
||||
const text = std.selection.find(TextSelection);
|
||||
if (!text) return;
|
||||
const inline = getInlineEditorByModel(std, text.from.blockId);
|
||||
if (!inline) return;
|
||||
return !inline.isLastLine(inline.getInlineRange());
|
||||
},
|
||||
Escape: ctx => {
|
||||
const text = std.selection.find(TextSelection);
|
||||
if (!text) return;
|
||||
|
||||
selectBlock(std, text.from.blockId);
|
||||
ctx.get('keyboardState').raw.stopPropagation();
|
||||
return true;
|
||||
},
|
||||
'Mod-a': ctx => {
|
||||
const text = std.selection.find(TextSelection);
|
||||
if (!text) return;
|
||||
|
||||
const model = std.store.getBlock(text.from.blockId)?.model;
|
||||
if (!model || !model.text) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
|
||||
if (
|
||||
text.from.index === 0 &&
|
||||
text.from.length === model.text.yText.length
|
||||
) {
|
||||
selectBlock(std, text.from.blockId);
|
||||
return true;
|
||||
}
|
||||
|
||||
selectTextModel(std, text.from.blockId, 0, model.text.yText.length);
|
||||
return true;
|
||||
},
|
||||
Enter: ctx => {
|
||||
const blocks = std.selection.filter(BlockSelection);
|
||||
const blockId = blocks.at(-1)?.blockId;
|
||||
|
||||
if (!blockId) return;
|
||||
const model = std.store.getBlock(blockId)?.model;
|
||||
if (!model || !model.text) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
focusTextModel(std, blockId, model.text.yText.length);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function selectBlock(std: BlockStdScope, blockId: string) {
|
||||
std.selection.setGroup('note', [
|
||||
std.selection.create(BlockSelection, { blockId }),
|
||||
]);
|
||||
}
|
||||
162
blocksuite/affine/inlines/preset/src/keymap/bracket.ts
Normal file
162
blocksuite/affine/inlines/preset/src/keymap/bracket.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { insertLinkedNode } from '@blocksuite/affine-inline-reference';
|
||||
import { CodeBlockModel } from '@blocksuite/affine-model';
|
||||
import { getInlineEditorByModel } from '@blocksuite/affine-rich-text';
|
||||
import { BRACKET_PAIRS } from '@blocksuite/affine-shared/consts';
|
||||
import { createDefaultDoc, matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BlockStdScope,
|
||||
TextSelection,
|
||||
type UIEventHandler,
|
||||
} from '@blocksuite/std';
|
||||
import type { InlineEditor } from '@blocksuite/std/inline';
|
||||
|
||||
export const bracketKeymap = (
|
||||
std: BlockStdScope
|
||||
): Record<string, UIEventHandler> => {
|
||||
const keymap = BRACKET_PAIRS.reduce(
|
||||
(acc, pair) => {
|
||||
return {
|
||||
...acc,
|
||||
[pair.right]: ctx => {
|
||||
const { store: doc, selection } = std;
|
||||
if (doc.readonly) return;
|
||||
|
||||
const textSelection = selection.find(TextSelection);
|
||||
if (!textSelection) return;
|
||||
const model = doc.getBlock(textSelection.from.blockId)?.model;
|
||||
if (!model) return;
|
||||
if (!matchModels(model, [CodeBlockModel])) return;
|
||||
const inlineEditor = getInlineEditorByModel(
|
||||
std,
|
||||
textSelection.from.blockId
|
||||
);
|
||||
if (!inlineEditor) return;
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return;
|
||||
const left = inlineEditor.yText.toString()[inlineRange.index - 1];
|
||||
const right = inlineEditor.yText.toString()[inlineRange.index];
|
||||
if (pair.left === left && pair.right === right) {
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
}
|
||||
},
|
||||
[pair.left]: ctx => {
|
||||
const { store: doc, selection } = std;
|
||||
if (doc.readonly) return;
|
||||
|
||||
const textSelection = selection.find(TextSelection);
|
||||
if (!textSelection) return;
|
||||
const model = doc.getBlock(textSelection.from.blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const isCodeBlock = matchModels(model, [CodeBlockModel]);
|
||||
// When selection is collapsed, only trigger auto complete in code block
|
||||
if (textSelection.isCollapsed() && !isCodeBlock) return;
|
||||
if (!textSelection.isInSameBlock()) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
|
||||
const inlineEditor = getInlineEditorByModel(
|
||||
std,
|
||||
textSelection.from.blockId
|
||||
);
|
||||
if (!inlineEditor) return;
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return;
|
||||
const selectedText = inlineEditor.yText
|
||||
.toString()
|
||||
.slice(inlineRange.index, inlineRange.index + inlineRange.length);
|
||||
if (!isCodeBlock && pair.name === 'square bracket') {
|
||||
// [[Selected text]] should automatically be converted to a Linked doc with the title "Selected text".
|
||||
// See https://github.com/toeverything/blocksuite/issues/2730
|
||||
const success = tryConvertToLinkedDoc(std, inlineEditor);
|
||||
if (success) return true;
|
||||
}
|
||||
inlineEditor.insertText(
|
||||
inlineRange,
|
||||
pair.left + selectedText + pair.right
|
||||
);
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: inlineRange.length,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
|
||||
return {
|
||||
...keymap,
|
||||
'`': ctx => {
|
||||
const { store: doc, selection } = std;
|
||||
if (doc.readonly) return;
|
||||
|
||||
const textSelection = selection.find(TextSelection);
|
||||
if (!textSelection || textSelection.isCollapsed()) return;
|
||||
if (!textSelection.isInSameBlock()) return;
|
||||
const model = doc.getBlock(textSelection.from.blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
const inlineEditor = getInlineEditorByModel(
|
||||
std,
|
||||
textSelection.from.blockId
|
||||
);
|
||||
if (!inlineEditor) return;
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return;
|
||||
inlineEditor.formatText(inlineRange, { code: true });
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index,
|
||||
length: inlineRange.length,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function tryConvertToLinkedDoc(std: BlockStdScope, inlineEditor: InlineEditor) {
|
||||
const root = std.store.root;
|
||||
if (!root) return false;
|
||||
const linkedDocWidgetEle = std.view.getWidget(
|
||||
'affine-linked-doc-widget',
|
||||
root.id
|
||||
);
|
||||
if (!linkedDocWidgetEle) return false;
|
||||
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return false;
|
||||
const text = inlineEditor.yText.toString();
|
||||
const left = text[inlineRange.index - 1];
|
||||
const right = text[inlineRange.index + inlineRange.length];
|
||||
const needConvert = left === '[' && right === ']';
|
||||
if (!needConvert) return false;
|
||||
|
||||
const docName = text.slice(
|
||||
inlineRange.index,
|
||||
inlineRange.index + inlineRange.length
|
||||
);
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 1,
|
||||
length: inlineRange.length + 2,
|
||||
});
|
||||
inlineEditor.setInlineRange({ index: inlineRange.index - 1, length: 0 });
|
||||
|
||||
const doc = createDefaultDoc(std.store.workspace, {
|
||||
title: docName,
|
||||
});
|
||||
insertLinkedNode({
|
||||
inlineEditor,
|
||||
docId: doc.id,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
29
blocksuite/affine/inlines/preset/src/keymap/format.ts
Normal file
29
blocksuite/affine/inlines/preset/src/keymap/format.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { type BlockStdScope, type UIEventHandler } from '@blocksuite/std';
|
||||
|
||||
import { textFormatConfigs } from '../command/index.js';
|
||||
|
||||
export const textFormatKeymap = (std: BlockStdScope) =>
|
||||
textFormatConfigs
|
||||
.filter(config => config.hotkey)
|
||||
.reduce(
|
||||
(acc, config) => {
|
||||
return {
|
||||
...acc,
|
||||
[config.hotkey as string]: ctx => {
|
||||
const { store: doc } = std;
|
||||
if (doc.readonly) return;
|
||||
|
||||
const allowed = config.textChecker?.(std.host) ?? true;
|
||||
if (!allowed) return;
|
||||
|
||||
const event = ctx.get('keyboardState').raw;
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
config.action(std.host);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
15
blocksuite/affine/inlines/preset/src/keymap/index.ts
Normal file
15
blocksuite/affine/inlines/preset/src/keymap/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { BlockStdScope, UIEventHandler } from '@blocksuite/std';
|
||||
|
||||
import { textCommonKeymap } from './basic.js';
|
||||
import { bracketKeymap } from './bracket.js';
|
||||
import { textFormatKeymap } from './format.js';
|
||||
|
||||
export const textKeymap = (
|
||||
std: BlockStdScope
|
||||
): Record<string, UIEventHandler> => {
|
||||
return {
|
||||
...textCommonKeymap(std),
|
||||
...bracketKeymap(std),
|
||||
...textFormatKeymap(std),
|
||||
};
|
||||
};
|
||||
284
blocksuite/affine/inlines/preset/src/markdown.ts
Normal file
284
blocksuite/affine/inlines/preset/src/markdown.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { LatexExtension } from '@blocksuite/affine-inline-latex';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
// inline markdown match rules:
|
||||
// covert: ***test*** + space
|
||||
// covert: ***t est*** + space
|
||||
// not convert: *** test*** + space
|
||||
// not convert: ***test *** + space
|
||||
// not convert: *** test *** + space
|
||||
|
||||
export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
{
|
||||
name: 'bolditalic',
|
||||
pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}\s$|.*\*{3}([^\s*])\*{3}\s$/,
|
||||
action: ({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
inlineRange,
|
||||
pattern,
|
||||
undoManager,
|
||||
}) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(
|
||||
-(targetText.length + 3 * 2 + 1),
|
||||
-1
|
||||
);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
inlineEditor.formatText(
|
||||
{
|
||||
index: startIndex,
|
||||
length: annotatedText.length,
|
||||
},
|
||||
{
|
||||
bold: true,
|
||||
italic: true,
|
||||
}
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 4,
|
||||
length: 4,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 3,
|
||||
});
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 6,
|
||||
length: 0,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'bold',
|
||||
pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}\s$|.*\*{2}([^\s*])\*{2}\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-(targetText.length + 2 * 2 + 1), -1);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
inlineEditor.formatText(
|
||||
{
|
||||
index: startIndex,
|
||||
length: annotatedText.length,
|
||||
},
|
||||
{
|
||||
bold: true,
|
||||
}
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 3,
|
||||
length: 3,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 4,
|
||||
length: 0,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'italic',
|
||||
pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}\s$|.*\*{1}([^\s*])\*{1}\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-(targetText.length + 1 * 2 + 1), -1);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
inlineEditor.formatText(
|
||||
{
|
||||
index: startIndex,
|
||||
length: annotatedText.length,
|
||||
},
|
||||
{
|
||||
italic: true,
|
||||
}
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 2,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 2,
|
||||
length: 0,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const StrikethroughExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'strikethrough',
|
||||
pattern: /.*~{2}([^\s][^~]*[^\s])~{2}\s$|.*~{2}([^\s~])~{2}\s$/,
|
||||
action: ({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
inlineRange,
|
||||
pattern,
|
||||
undoManager,
|
||||
}) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(
|
||||
-targetText.length - (2 * 2 + 1),
|
||||
-1
|
||||
);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
inlineEditor.formatText(
|
||||
{
|
||||
index: startIndex,
|
||||
length: annotatedText.length,
|
||||
},
|
||||
{
|
||||
strike: true,
|
||||
}
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 3,
|
||||
length: 3,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 2,
|
||||
});
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 4,
|
||||
length: 0,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const UnderthroughExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'underthrough',
|
||||
pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}\s$|.*~{1}([^\s~])~{1}\s$/,
|
||||
action: ({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
inlineRange,
|
||||
pattern,
|
||||
undoManager,
|
||||
}) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(
|
||||
-(targetText.length + 1 * 2 + 1),
|
||||
-1
|
||||
);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
inlineEditor.formatText(
|
||||
{
|
||||
index: startIndex,
|
||||
length: annotatedText.length,
|
||||
},
|
||||
{
|
||||
underline: true,
|
||||
}
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 2,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 1,
|
||||
});
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 2,
|
||||
length: 0,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const CodeExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'code',
|
||||
pattern: /.*`([^\s][^`]*[^\s])`\s$|.*`([^\s`])`\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-(targetText.length + 1 * 2 + 1), -1);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
inlineEditor.formatText(
|
||||
{
|
||||
index: startIndex,
|
||||
length: annotatedText.length,
|
||||
},
|
||||
{
|
||||
code: true,
|
||||
}
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 2,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 1,
|
||||
});
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 2,
|
||||
length: 0,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const MarkdownExtensions: ExtensionType[] = [
|
||||
BoldItalicMarkdown,
|
||||
BoldMarkdown,
|
||||
ItalicExtension,
|
||||
StrikethroughExtension,
|
||||
UnderthroughExtension,
|
||||
CodeExtension,
|
||||
LatexExtension,
|
||||
];
|
||||
35
blocksuite/affine/inlines/preset/src/nodes/affine-text.ts
Normal file
35
blocksuite/affine/inlines/preset/src/nodes/affine-text.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { affineTextStyles } from '@blocksuite/affine-shared/styles';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { ZERO_WIDTH_FOR_EMPTY_LINE } from '@blocksuite/std/inline';
|
||||
import type { DeltaInsert } from '@blocksuite/store';
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
export class AffineText extends ShadowlessElement {
|
||||
override render() {
|
||||
const style = this.delta.attributes
|
||||
? affineTextStyles(this.delta.attributes)
|
||||
: {};
|
||||
|
||||
// we need to avoid \n appearing before and after the span element, which will
|
||||
// cause the unexpected space
|
||||
if (this.delta.attributes?.code) {
|
||||
return html`<code style=${styleMap(style)}
|
||||
><v-text .str=${this.delta.insert}></v-text
|
||||
></code>`;
|
||||
}
|
||||
|
||||
// we need to avoid \n appearing before and after the span element, which will
|
||||
// cause the unexpected space
|
||||
return html`<span style=${styleMap(style)}
|
||||
><v-text .str=${this.delta.insert}></v-text
|
||||
></span>`;
|
||||
}
|
||||
|
||||
@property({ type: Object })
|
||||
accessor delta: DeltaInsert<AffineTextAttributes> = {
|
||||
insert: ZERO_WIDTH_FOR_EMPTY_LINE,
|
||||
};
|
||||
}
|
||||
15
blocksuite/affine/inlines/preset/src/store.ts
Normal file
15
blocksuite/affine/inlines/preset/src/store.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
type StoreExtensionContext,
|
||||
StoreExtensionProvider,
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
|
||||
import { InlineAdapterExtensions } from './adapters/extensions';
|
||||
|
||||
export class InlinePresetStoreExtension extends StoreExtensionProvider {
|
||||
override name = 'affine-inline-preset';
|
||||
|
||||
override setup(context: StoreExtensionContext) {
|
||||
super.setup(context);
|
||||
context.register(InlineAdapterExtensions);
|
||||
}
|
||||
}
|
||||
25
blocksuite/affine/inlines/preset/src/view.ts
Normal file
25
blocksuite/affine/inlines/preset/src/view.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
|
||||
import { DefaultInlineManagerExtension } from './default-inline-manager';
|
||||
import { effects } from './effects';
|
||||
import { InlineSpecExtensions } from './inline-spec';
|
||||
import { MarkdownExtensions } from './markdown';
|
||||
|
||||
export class InlinePresetViewExtension extends ViewExtensionProvider {
|
||||
override name = 'affine-inline-preset';
|
||||
|
||||
override effect() {
|
||||
super.effect();
|
||||
effects();
|
||||
}
|
||||
|
||||
override setup(context: ViewExtensionContext) {
|
||||
super.setup(context);
|
||||
context.register(DefaultInlineManagerExtension);
|
||||
context.register(InlineSpecExtensions);
|
||||
context.register(MarkdownExtensions);
|
||||
}
|
||||
}
|
||||
24
blocksuite/affine/inlines/preset/tsconfig.json
Normal file
24
blocksuite/affine/inlines/preset/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{ "path": "../../components" },
|
||||
{ "path": "../../ext-loader" },
|
||||
{ "path": "../comment" },
|
||||
{ "path": "../footnote" },
|
||||
{ "path": "../latex" },
|
||||
{ "path": "../link" },
|
||||
{ "path": "../mention" },
|
||||
{ "path": "../reference" },
|
||||
{ "path": "../../model" },
|
||||
{ "path": "../../rich-text" },
|
||||
{ "path": "../../shared" },
|
||||
{ "path": "../../../framework/std" },
|
||||
{ "path": "../../../framework/store" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user