├── blocks ├── x-json │ ├── .gitignore │ ├── babel.config.js │ ├── tsconfig.json │ ├── src │ │ ├── utils │ │ │ ├── is.ts │ │ │ └── transform.ts │ │ ├── types │ │ │ ├── interface.ts │ │ │ └── block.ts │ │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ ├── jest.config.js │ └── test │ │ └── transform │ │ └── batch.test.ts ├── x-plugin │ ├── src │ │ ├── index.ts │ │ └── index.scss │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ └── .gitignore ├── x-core │ ├── src │ │ ├── selection │ │ │ ├── utils │ │ │ │ └── constant.ts │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── model │ │ │ ├── utils │ │ │ │ └── weak-map.ts │ │ │ └── types │ │ │ │ ├── index.ts │ │ │ │ └── dom.ts │ │ ├── perform │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── lookup │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ │ └── marks.ts │ │ ├── global.d.ts │ │ ├── editor │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ │ └── constant.ts │ │ ├── event │ │ │ └── bus │ │ │ │ └── types.ts │ │ ├── plugin │ │ │ ├── types │ │ │ │ └── context.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── babel.config.js │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ ├── jest.config.js │ └── test │ │ ├── selection │ │ └── collapse.test.ts │ │ └── lookup │ │ └── marks.test.ts └── x-react │ ├── tsconfig.json │ ├── babel.config.js │ ├── src │ ├── global.d.ts │ ├── index.ts │ ├── hooks │ │ ├── use-readonly.tsx │ │ ├── use-layout-context.ts │ │ ├── use-editor.tsx │ │ ├── use-meta.ts │ │ └── use-composing.ts │ ├── styles │ │ └── editable.scss │ ├── model │ │ ├── portal.tsx │ │ └── ph.tsx │ └── preset │ │ └── zero.tsx │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ ├── .gitignore │ └── jest.config.js ├── packages ├── ot-json │ ├── .gitignore │ ├── test │ │ ├── global.d.ts │ │ └── text0-compose.test.ts │ ├── babel.config.js │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ ├── jest.config.js │ └── src │ │ ├── index.ts │ │ ├── utils.ts │ │ └── subtype.ts ├── tools │ ├── src │ │ └── index.tsx │ └── package.json ├── react │ ├── test │ │ ├── config │ │ │ ├── styles.ts │ │ │ ├── utils.ts │ │ │ └── debug.ts │ │ ├── hooks │ │ │ ├── ref.test.tsx │ │ │ └── callback.test.tsx │ │ └── basic │ │ │ └── dom.test.tsx │ ├── tsconfig.json │ ├── babel.config.js │ ├── src │ │ ├── utils │ │ │ ├── constant.ts │ │ │ ├── event.ts │ │ │ └── weak-map.ts │ │ ├── global.d.ts │ │ ├── hooks │ │ │ ├── use-readonly.tsx │ │ │ ├── use-editor.tsx │ │ │ └── use-composing.ts │ │ ├── model │ │ │ ├── portal.tsx │ │ │ ├── eol.tsx │ │ │ └── ph.tsx │ │ ├── plugin │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── modules │ │ │ │ └── priority.ts │ │ └── preset │ │ │ ├── text.tsx │ │ │ └── block-kit.tsx │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ ├── .gitignore │ └── jest.config.js ├── plugin │ ├── src │ │ ├── align │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── index.tsx │ │ ├── bold │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── emoji │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── styles │ │ │ │ └── index.scss │ │ ├── quote │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── styles │ │ │ │ └── index.scss │ │ ├── italic │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── strike │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── divider │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── styles │ │ │ │ └── index.scss │ │ ├── font-color │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── index.tsx │ │ ├── heading │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── styles │ │ │ │ └── index.scss │ │ ├── background │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── index.tsx │ │ ├── font-size │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── index.tsx │ │ ├── indent │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── underline │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── line-height │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── index.tsx │ │ ├── link │ │ │ ├── utils │ │ │ │ └── constant.ts │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ ├── styles │ │ │ │ └── index.scss │ │ │ └── view │ │ │ │ └── a.tsx │ │ ├── mention │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ ├── styles │ │ │ │ ├── index.scss │ │ │ │ └── suggest.scss │ │ │ └── utils │ │ │ │ └── constant.ts │ │ ├── shared │ │ │ ├── styles │ │ │ │ ├── selection.scss │ │ │ │ └── variable.scss │ │ │ ├── utils │ │ │ │ ├── event.ts │ │ │ │ ├── is.ts │ │ │ │ └── dom.ts │ │ │ ├── icons │ │ │ │ ├── text.tsx │ │ │ │ ├── line-height.tsx │ │ │ │ ├── font-size.tsx │ │ │ │ ├── justify.tsx │ │ │ │ ├── font-color.tsx │ │ │ │ └── emoji.tsx │ │ │ └── modules │ │ │ │ └── selection.ts │ │ ├── toolbar │ │ │ ├── styles │ │ │ │ ├── emoji.scss │ │ │ │ ├── line-height.scss │ │ │ │ ├── cut.scss │ │ │ │ ├── link.scss │ │ │ │ ├── float.scss │ │ │ │ └── index.scss │ │ │ ├── modules │ │ │ │ ├── cut.tsx │ │ │ │ ├── image.tsx │ │ │ │ ├── divider.tsx │ │ │ │ ├── quote.tsx │ │ │ │ ├── bold.tsx │ │ │ │ ├── italic.tsx │ │ │ │ ├── strike.tsx │ │ │ │ ├── inline-code.tsx │ │ │ │ ├── underline.tsx │ │ │ │ ├── order-list.tsx │ │ │ │ └── bullet-list.tsx │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── context │ │ │ │ └── provider.tsx │ │ ├── inline-code │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── styles │ │ │ │ └── index.scss │ │ ├── bullet-list │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ ├── utils │ │ │ │ └── is.ts │ │ │ ├── styles │ │ │ │ └── index.scss │ │ │ └── view │ │ │ │ └── list.tsx │ │ ├── order-list │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ ├── utils │ │ │ │ └── is.ts │ │ │ ├── styles │ │ │ │ └── index.scss │ │ │ └── view │ │ │ │ └── list.tsx │ │ ├── global.d.ts │ │ ├── image │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── styles │ │ │ │ ├── index.scss │ │ │ │ └── wrapper.scss │ │ └── shortcut │ │ │ └── types │ │ │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ └── .gitignore ├── core │ ├── src │ │ ├── event │ │ │ └── bus │ │ │ │ └── types.ts │ │ ├── editor │ │ │ └── utils │ │ │ │ └── constant.ts │ │ ├── rect │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ │ └── convert.ts │ │ ├── tracer │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── history │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── global.d.ts │ │ ├── lookup │ │ │ └── utils │ │ │ │ └── is.ts │ │ ├── command │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── input │ │ │ └── utils │ │ │ │ └── hot-key.ts │ │ ├── schema │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ │ └── is.ts │ │ ├── plugin │ │ │ └── types │ │ │ │ └── context.ts │ │ ├── clipboard │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ │ └── serialize.ts │ │ ├── model │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── log │ │ │ └── index.ts │ │ └── state │ │ │ ├── utils │ │ │ └── key.ts │ │ │ └── types │ │ │ └── index.ts │ ├── test │ │ ├── config │ │ │ ├── setup.ts │ │ │ └── debug.ts │ │ ├── state │ │ │ ├── status.test.ts │ │ │ └── content.test.ts │ │ ├── history │ │ │ ├── remote.test.ts │ │ │ └── merge.test.ts │ │ ├── plugin │ │ │ └── inject.test.ts │ │ ├── input │ │ │ └── delete.test.ts │ │ ├── lookup │ │ │ └── unicode.test.ts │ │ └── selection │ │ │ └── walker.test.ts │ ├── babel.config.js │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ └── jest.config.js ├── vue │ ├── tsconfig.json │ ├── src │ │ ├── utils │ │ │ ├── event.ts │ │ │ ├── constant.ts │ │ │ ├── is.ts │ │ │ ├── weak-map.ts │ │ │ ├── types.ts │ │ │ └── wrapper.ts │ │ ├── hooks │ │ │ ├── use-readonly.tsx │ │ │ └── use-editor.tsx │ │ ├── plugin │ │ │ ├── index.ts │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ └── modules │ │ │ │ └── priority.ts │ │ ├── preset │ │ │ ├── isolate.ts │ │ │ └── block-kit.ts │ │ ├── model │ │ │ └── eol.ts │ │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ ├── .gitignore │ └── package.json ├── delta │ ├── src │ │ ├── attributes │ │ │ ├── interface.ts │ │ │ ├── diff.ts │ │ │ ├── invert.ts │ │ │ ├── transform.ts │ │ │ └── compose.ts │ │ ├── cluster │ │ │ └── interface.ts │ │ ├── delta │ │ │ ├── interface.ts │ │ │ └── op.ts │ │ └── utils │ │ │ └── equal.ts │ ├── babel.config.js │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ ├── jest.config.js │ ├── test │ │ ├── delta │ │ │ └── op.test.ts │ │ └── utils │ │ │ ├── delta.test.ts │ │ │ └── clone.test.ts │ └── package.json └── utils │ ├── babel.config.js │ ├── test │ ├── env.test.ts │ ├── uuid.test.ts │ ├── decorator.test.ts │ ├── json.test.ts │ ├── regexp.test.ts │ ├── literal.test.ts │ ├── native.test.ts │ ├── storage.test.ts │ └── format.test.ts │ ├── tsconfig.json │ ├── src │ ├── global.d.ts │ ├── extract.ts │ ├── uuid.ts │ ├── native.ts │ ├── decorator.ts │ ├── constant.ts │ ├── literal.ts │ ├── vars.less │ ├── vars.scss │ ├── json.ts │ └── vars.css │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ └── jest.config.js ├── examples ├── website │ ├── script │ │ └── global.d.ts │ ├── public │ │ ├── favicon.ico │ │ └── vue.html │ ├── tsconfig.json │ ├── src │ │ ├── global.d.ts │ │ ├── react │ │ │ ├── components │ │ │ │ └── github.tsx │ │ │ ├── config │ │ │ │ └── schema.ts │ │ │ └── styles │ │ │ │ └── index.scss │ │ ├── stream │ │ │ ├── data.ts │ │ │ └── index.scss │ │ ├── blocks │ │ │ ├── config │ │ │ │ └── blocks.ts │ │ │ └── styles │ │ │ │ └── index.scss │ │ └── variable │ │ │ └── constant.ts │ └── README.md ├── stream │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ └── types │ │ │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ ├── README.md │ └── package.json └── variable │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── tsconfig.lib.json │ ├── src │ ├── utils │ │ ├── types.ts │ │ └── constant.ts │ └── index.ts │ ├── gulpfile.js │ └── README.md ├── .npmrc ├── .vscode └── settings.json ├── pnpm-workspace.yaml ├── .gitignore ├── .prettierrc.js ├── .stylelintrc.js ├── LICENSE ├── .github └── workflows │ ├── testing.yml │ └── deploy.yml └── .eslintrc.js /blocks/x-json/.gitignore: -------------------------------------------------------------------------------- 1 | *.data 2 | -------------------------------------------------------------------------------- /packages/ot-json/.gitignore: -------------------------------------------------------------------------------- 1 | *.data 2 | -------------------------------------------------------------------------------- /packages/tools/src/index.tsx: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /packages/react/test/config/styles.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /packages/ot-json/test/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ot-fuzzer"; 2 | -------------------------------------------------------------------------------- /blocks/x-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./index.scss"; 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /examples/website/script/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "copy-webpack-plugin"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/align/types/index.ts: -------------------------------------------------------------------------------- 1 | export const ALIGN_KEY = "align"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/bold/types/index.ts: -------------------------------------------------------------------------------- 1 | export const BOLD_KEY = "bold"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/emoji/types/index.ts: -------------------------------------------------------------------------------- 1 | export const EMOJI_KEY = "emoji"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/quote/types/index.ts: -------------------------------------------------------------------------------- 1 | export const QUOTE_KEY = "quote"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/italic/types/index.ts: -------------------------------------------------------------------------------- 1 | export const ITALIC_KEY = "italic"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/strike/types/index.ts: -------------------------------------------------------------------------------- 1 | export const STRIKE_KEY = "strike"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/divider/types/index.ts: -------------------------------------------------------------------------------- 1 | export const DIVIDER_KEY = "divider"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/font-color/types/index.ts: -------------------------------------------------------------------------------- 1 | export const FONT_COLOR_KEY = "color"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/heading/types/index.ts: -------------------------------------------------------------------------------- 1 | export const HEADING_KEY = "heading"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/background/types/index.ts: -------------------------------------------------------------------------------- 1 | export const BACKGROUND_KEY = "background"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/font-size/types/index.ts: -------------------------------------------------------------------------------- 1 | export const FONT_SIZE_KEY = "font-size"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/indent/types/index.ts: -------------------------------------------------------------------------------- 1 | export const INDENT_LEVEL_KEY = "indent-level"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/underline/types/index.ts: -------------------------------------------------------------------------------- 1 | export const UNDERLINE_KEY = "underline"; 2 | -------------------------------------------------------------------------------- /packages/core/src/event/bus/types.ts: -------------------------------------------------------------------------------- 1 | /** 事件类型扩展 */ 2 | export interface EventMapExtension {} 3 | -------------------------------------------------------------------------------- /packages/plugin/src/line-height/types/index.ts: -------------------------------------------------------------------------------- 1 | export const LINE_HEIGHT_KEY = "line-height"; 2 | -------------------------------------------------------------------------------- /packages/plugin/src/link/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const LINK_REG = /^https?:\/\/[^\s/$.?#].[^\s]*/i; 2 | -------------------------------------------------------------------------------- /packages/vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # https://pnpm.io/9.x/npmrc 2 | auto-install-peers=true 3 | registry=https://registry.npmmirror.com/ 4 | -------------------------------------------------------------------------------- /blocks/x-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/stream/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/variable/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/delta/src/attributes/interface.ts: -------------------------------------------------------------------------------- 1 | export interface AttributeMap { 2 | [key: string]: string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/plugin/src/emoji/styles/index.scss: -------------------------------------------------------------------------------- 1 | .block-kit-emoji { 2 | height: 1em; 3 | width: 1em; 4 | } 5 | -------------------------------------------------------------------------------- /packages/plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /blocks/x-core/src/selection/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const POINT_TYPE = { 2 | BLOCK: "B", 3 | TEXT: "T", 4 | } as const; 5 | -------------------------------------------------------------------------------- /blocks/x-plugin/src/index.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable selector-class-pattern */ 2 | ._useless { 3 | display: block; 4 | } 5 | -------------------------------------------------------------------------------- /examples/website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindRunnerMax/BlockKit/HEAD/examples/website/public/favicon.ico -------------------------------------------------------------------------------- /packages/core/test/config/setup.ts: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | Object.assign(globalThis, { document: null }); 3 | }; 4 | -------------------------------------------------------------------------------- /packages/plugin/src/mention/types/index.ts: -------------------------------------------------------------------------------- 1 | export const MENTION_KEY = "mention"; 2 | export const MENTION_NAME = "name"; 3 | -------------------------------------------------------------------------------- /packages/plugin/src/shared/styles/selection.scss: -------------------------------------------------------------------------------- 1 | .block-kit-embed-selected { 2 | box-shadow: 0 0 0 1px rgba(var(--blue-7), 0.9); 3 | } 4 | -------------------------------------------------------------------------------- /blocks/x-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends":"../../tsconfig.json", 3 | "include": [ 4 | "src/**/*", 5 | "test/**/*" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends":"../../tsconfig.json", 3 | "include": [ 4 | "src/**/*", 5 | "test/**/*" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/vue/src/utils/event.ts: -------------------------------------------------------------------------------- 1 | export const preventNativeEvent = (e: Event) => { 2 | e.preventDefault(); 3 | e.stopPropagation(); 4 | }; 5 | -------------------------------------------------------------------------------- /blocks/x-core/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"], 3 | }; 4 | -------------------------------------------------------------------------------- /blocks/x-json/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"], 3 | }; 4 | -------------------------------------------------------------------------------- /blocks/x-react/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/core/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/delta/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/react/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/utils/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/ot-json/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src/**/*"], 4 | "exclude": ["**/packages/**/*", "**/node_modules/**/*"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/styles/emoji.scss: -------------------------------------------------------------------------------- 1 | .arco-trigger.menu-toolbar-emoji-trigger { 2 | width: 352px; 3 | 4 | .scroll { 5 | max-height: 300px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/editor/utils/constant.ts: -------------------------------------------------------------------------------- 1 | import type { DeltaLike } from "@block-kit/delta"; 2 | 3 | export const BLOCK_LIKE: DeltaLike = { 4 | ops: [{ insert: "\n" }], 5 | }; 6 | -------------------------------------------------------------------------------- /packages/plugin/src/quote/styles/index.scss: -------------------------------------------------------------------------------- 1 | .block-kit-quote { 2 | border-left: 3px solid var(--color-border-2); 3 | color: var(--color-text-2); 4 | padding-left: 10px; 5 | } 6 | -------------------------------------------------------------------------------- /blocks/x-core/src/model/utils/weak-map.ts: -------------------------------------------------------------------------------- 1 | import type { BlockState } from "../../state/modules/block-state"; 2 | 3 | export const STATE_TO_RENDER = new WeakMap void>(); 4 | -------------------------------------------------------------------------------- /packages/react/src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const NO_CURSOR: React.CSSProperties = { 2 | width: 0, 3 | height: 0, 4 | color: "transparent", 5 | position: "absolute", 6 | } as const; 7 | -------------------------------------------------------------------------------- /packages/utils/test/env.test.ts: -------------------------------------------------------------------------------- 1 | import { IS_TEST } from "../src/env"; 2 | 3 | describe("env", () => { 4 | it("IS_TEST", () => { 5 | expect(IS_TEST).toBeTruthy(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/core/src/rect/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Rect = { 2 | top: number; 3 | bottom: number; 4 | left: number; 5 | right: number; 6 | height: number; 7 | width: number; 8 | }; 9 | -------------------------------------------------------------------------------- /examples/stream/src/index.ts: -------------------------------------------------------------------------------- 1 | export { DeltaComposer } from "./modules/delta-composer"; 2 | export { MdComposer } from "./modules/md-composer"; 3 | export { parseLexerToken } from "./utils/token"; 4 | -------------------------------------------------------------------------------- /packages/plugin/src/divider/styles/index.scss: -------------------------------------------------------------------------------- 1 | .block-kit-divider-container { 2 | padding: 6px 0; 3 | } 4 | 5 | .block-kit-divider { 6 | background-color: var(--color-fill-4); 7 | height: 1px; 8 | } 9 | -------------------------------------------------------------------------------- /blocks/x-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends":"../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*", 8 | "test/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /blocks/x-json/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends":"../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*", 8 | "test/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/tracer/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { RawRange } from "../../selection/modules/raw-range"; 2 | 3 | export type RawRangeRef = { 4 | current: RawRange | null; 5 | unpack: () => RawRange | null; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends":"../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*", 8 | "test/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/delta/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends":"../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*", 8 | "test/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/ot-json/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends":"../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*", 8 | "test/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/modules/cut.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/cut.scss"; 2 | 3 | import type { FC } from "react"; 4 | 5 | export const Cut: FC = () => { 6 | return
; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends":"../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*", 8 | "test/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /blocks/x-react/src/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | MSStream: boolean; 3 | } 4 | 5 | declare namespace NodeJS { 6 | interface ProcessEnv { 7 | NODE_ENV: "development" | "production" | "test"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/react/src/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | MSStream: boolean; 3 | } 4 | 5 | declare namespace NodeJS { 6 | interface ProcessEnv { 7 | NODE_ENV: "development" | "production" | "test"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/utils/src/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | MSStream: boolean; 3 | } 4 | 5 | declare namespace NodeJS { 6 | interface ProcessEnv { 7 | NODE_ENV: "development" | "production" | "test"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin/src/link/types/index.ts: -------------------------------------------------------------------------------- 1 | import { CLIENT_KEY } from "@block-kit/core"; 2 | 3 | export const LINK_KEY = "link"; 4 | export const LINK_BLANK_KEY = "link-blank"; 5 | export const LINK_TEMP_KEY = CLIENT_KEY + "temp-link"; 6 | -------------------------------------------------------------------------------- /packages/vue/src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from "vue"; 2 | 3 | export const NO_CURSOR: CSSProperties = { 4 | width: 0, 5 | height: 0, 6 | color: "transparent", 7 | position: "absolute", 8 | } as const; 9 | -------------------------------------------------------------------------------- /blocks/x-core/src/perform/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApplyOptions, BatchApplyChange } from "../../state/types"; 2 | 3 | /** 变更结果 */ 4 | export type PerformResult = { 5 | changes: BatchApplyChange; 6 | options?: ApplyOptions; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/plugin/src/inline-code/types/index.ts: -------------------------------------------------------------------------------- 1 | export const INLINE_CODE_KEY = "inline-code"; 2 | 3 | export const INLINE_CODE_START_CLASS = "block-kit-inline-code-start"; 4 | export const INLINE_CODE_END_CLASS = "block-kit-inline-code-end"; 5 | -------------------------------------------------------------------------------- /packages/plugin/src/bullet-list/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 无序列表插件标识 */ 2 | export const BULLET_LIST_KEY = "bullet-list"; 3 | /** 列表类型 key => 无序列表/有序列表 */ 4 | export const LIST_TYPE_KEY = "list"; 5 | /** 无序列表类型值 */ 6 | export const BULLET_LIST_TYPE = "bullet"; 7 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/styles/line-height.scss: -------------------------------------------------------------------------------- 1 | .block-kit-toolbar-dropdown { 2 | .block-kit-toolbar-height-item { 3 | position: relative; 4 | 5 | .arco-icon-check { 6 | left: 5px; 7 | position: absolute; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/vue/src/hooks/use-readonly.tsx: -------------------------------------------------------------------------------- 1 | import { inject } from "vue"; 2 | 3 | export const ReadonlyContext = Symbol(); 4 | 5 | export const useReadonly = () => { 6 | const readonly = inject(ReadonlyContext); 7 | 8 | return { readonly }; 9 | }; 10 | -------------------------------------------------------------------------------- /blocks/x-react/src/index.ts: -------------------------------------------------------------------------------- 1 | export { BlockModel } from "./model/block"; 2 | export { PortalModel } from "./model/portal"; 3 | export { TextModel } from "./model/text"; 4 | export { BlockKitX } from "./preset/block-kit"; 5 | export { EditableX } from "./preset/editable"; 6 | -------------------------------------------------------------------------------- /blocks/x-json/src/utils/is.ts: -------------------------------------------------------------------------------- 1 | import type { Op } from "@block-kit/ot-json"; 2 | 3 | import type { DeltaSubOp } from "../modules/subtype"; 4 | 5 | export const isTextDeltaOp = (op: Op): op is DeltaSubOp => { 6 | return op.p[0] === "delta" && op.t === "delta" && op.o; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/vue/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /blocks/x-core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /blocks/x-json/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /blocks/x-plugin/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /blocks/x-react/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/stream/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/variable/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/delta/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/ot-json/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/plugin/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/react/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "outDir": "dist/es", 7 | "declarationDir": "dist/es", 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/styles/cut.scss: -------------------------------------------------------------------------------- 1 | @import '../../shared/styles/variable'; 2 | 3 | .block-kit-menu-toolbar { 4 | .menu-toolbar-cut { 5 | background-color: var(--color-fill-3); 6 | box-sizing: border-box; 7 | margin: 4px 6px; 8 | width: 1px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/website/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare namespace NodeJS { 5 | interface ProcessEnv { 6 | NODE_ENV: "development" | "production" | "test"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/plugin/src/order-list/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 有序列表标识 */ 2 | export const ORDER_LIST_KEY = "order-list"; 3 | /** 有序列表类型值 */ 4 | export const ORDER_LIST_TYPE = "order"; 5 | /** 有序列表序号 */ 6 | export const LIST_START_KEY = "list-start"; 7 | /** 重新编号标识 */ 8 | export const LIST_RESTART_KEY = "list-restart"; 9 | -------------------------------------------------------------------------------- /blocks/x-core/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "commonjs", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /blocks/x-json/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "commonjs", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /blocks/x-plugin/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "ES2015", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /blocks/x-react/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "ES2015", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/stream/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "ES2015", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/variable/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "ES2015", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "commonjs", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/delta/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "commonjs", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/ot-json/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "commonjs", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/plugin/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "ES2015", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/react/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "commonjs", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/utils/test/uuid.test.ts: -------------------------------------------------------------------------------- 1 | import { getUniqueId } from "../src/uuid"; 2 | 3 | describe("id", () => { 4 | it("unique id length", () => { 5 | expect(getUniqueId()).toHaveLength(10); 6 | expect(getUniqueId(5)).toHaveLength(5); 7 | expect(getUniqueId(15)).toHaveLength(15); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "commonjs", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/vue/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "useTsconfigDeclarationDir": true, 4 | "compilerOptions": { 5 | "module": "commonjs", 6 | "baseUrl": ".", 7 | "outDir": "dist/lib", 8 | "declarationDir": "dist/lib", 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/plugin/src/shared/utils/event.ts: -------------------------------------------------------------------------------- 1 | export const PLUGIN_EVENTS = { 2 | SHORTCUT_MARKS_CHANGE: "SHORTCUT_MARKS_CHANGE", 3 | } as const; 4 | 5 | declare module "@block-kit/core/dist/es/event/bus/types" { 6 | interface EventMapExtension { 7 | [PLUGIN_EVENTS.SHORTCUT_MARKS_CHANGE]: null; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "arcoblue", 4 | "COMPAT", 5 | "CRDT", 6 | "Indexify", 7 | "numberify", 8 | "pako", 9 | "pnpm", 10 | "redoable", 11 | "rspack", 12 | "TSON", 13 | "undoable", 14 | "webrtc" 15 | ] 16 | } -------------------------------------------------------------------------------- /blocks/x-react/src/hooks/use-readonly.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext } from "react"; 2 | 3 | export const ReadonlyContext = createContext(false); 4 | ReadonlyContext.displayName = "Readonly"; 5 | 6 | export const useReadonly = () => { 7 | const readonly = React.useContext(ReadonlyContext); 8 | return { readonly }; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-readonly.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext } from "react"; 2 | 3 | export const ReadonlyContext = createContext(false); 4 | ReadonlyContext.displayName = "Readonly"; 5 | 6 | export const useReadonly = () => { 7 | const readonly = React.useContext(ReadonlyContext); 8 | return { readonly }; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/plugin/src/bullet-list/utils/is.ts: -------------------------------------------------------------------------------- 1 | import type { AttributeMap } from "@block-kit/delta"; 2 | 3 | import { BULLET_LIST_TYPE, LIST_TYPE_KEY } from "../types"; 4 | 5 | /** 6 | * 检查无序列表 7 | * @param attrs 8 | */ 9 | export const isBulletList = (attrs: AttributeMap) => { 10 | return attrs[LIST_TYPE_KEY] === BULLET_LIST_TYPE; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/plugin/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module JSX { 2 | interface IntrinsicElements { 3 | "em-emoji": React.DetailedHTMLProps, HTMLElement>; 4 | } 5 | } 6 | 7 | declare namespace NodeJS { 8 | interface ProcessEnv { 9 | NODE_ENV: "development" | "production" | "test"; 10 | VERSION: string; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/plugin/src/heading/styles/index.scss: -------------------------------------------------------------------------------- 1 | .block-kit-heading { 2 | font-weight: bold; 3 | } 4 | 5 | .block-kit-heading.h1 { 6 | font-size: 20px; 7 | margin: 10px 0; 8 | } 9 | 10 | .block-kit-heading.h2 { 11 | font-size: 16px; 12 | margin: 8px 0; 13 | } 14 | 15 | .block-kit-heading.h3 { 16 | font-size: 14px; 17 | margin: 7px 0; 18 | } 19 | -------------------------------------------------------------------------------- /blocks/x-core/src/lookup/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Op } from "@block-kit/delta"; 2 | 3 | export type OpMeta = { 4 | /** Op */ 5 | op: Op; 6 | /** Ops */ 7 | ops: Op[]; 8 | /** Op 索引 */ 9 | index: number; 10 | /** Op 长度 */ 11 | length: number; 12 | /** 节点的文本偏移 */ 13 | offset: number; 14 | /** 判断是否是 Leaf 的尾部 */ 15 | tail: boolean; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/plugin/src/link/styles/index.scss: -------------------------------------------------------------------------------- 1 | .block-kit-hyper-link { 2 | color: rgb(var(--link-6)); 3 | cursor: pointer; 4 | text-decoration: none; 5 | } 6 | 7 | .block-kit-hyper-link-wrap:hover { 8 | text-decoration: underline; 9 | text-decoration-color: rgb(var(--link-6)); 10 | text-decoration-skip-ink: none; 11 | text-underline-offset: 0.1em; 12 | } 13 | -------------------------------------------------------------------------------- /blocks/x-core/src/model/types/index.ts: -------------------------------------------------------------------------------- 1 | // Block 2 | export const X_BLOCK_KEY = "data-block"; 3 | export const X_TEXT_BLOCK_KEY = "data-text-block"; 4 | export const X_BLOCK_ID_KEY = "data-block-id"; 5 | export const X_BLOCK_TYPE_KEY = "data-block-type"; 6 | 7 | // Selection 8 | export const X_ZERO_KEY = "data-x-zero"; 9 | export const X_SELECTION_KEY = "data-selection"; 10 | -------------------------------------------------------------------------------- /packages/core/src/history/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Delta } from "@block-kit/delta"; 2 | 3 | import type { RawRange } from "../../selection/modules/raw-range"; 4 | 5 | export type StackItem = { 6 | delta: Delta; 7 | id: Set; 8 | range: RawRange | null; 9 | }; 10 | 11 | export type Stack = { 12 | undo: StackItem[]; 13 | redo: StackItem[]; 14 | }; 15 | -------------------------------------------------------------------------------- /blocks/x-core/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | Selection: typeof Selection["constructor"]; 4 | DataTransfer: typeof DataTransfer["constructor"]; 5 | Node: typeof Node["constructor"]; 6 | } 7 | } 8 | 9 | declare namespace NodeJS { 10 | interface ProcessEnv { 11 | NODE_ENV: "development" | "production" | "test"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | Selection: typeof Selection["constructor"]; 4 | DataTransfer: typeof DataTransfer["constructor"]; 5 | Node: typeof Node["constructor"]; 6 | } 7 | } 8 | 9 | declare namespace NodeJS { 10 | interface ProcessEnv { 11 | NODE_ENV: "development" | "production" | "test"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/utils/test/decorator.test.ts: -------------------------------------------------------------------------------- 1 | import { Bind } from "../src/decorator"; 2 | 3 | describe("decorator", () => { 4 | it("Bind", () => { 5 | class Test { 6 | @Bind 7 | method() { 8 | return this; 9 | } 10 | } 11 | const test = new Test(); 12 | const method = test.method; 13 | expect(method()).toBe(test); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/vue/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /blocks/x-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /blocks/x-react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /blocks/x-react/src/styles/editable.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable selector-no-qualifying-type */ 2 | 3 | div[data-block][data-placeholder]::before { 4 | color: #bbbfc4; 5 | content: attr(data-placeholder); 6 | height: 0; 7 | pointer-events: none; 8 | position: absolute; 9 | } 10 | 11 | div:not(div[data-block-type='ROOT']) > .block-kit-x-children { 12 | margin-left: 10px; 13 | } 14 | -------------------------------------------------------------------------------- /packages/plugin/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/plugin/src/order-list/utils/is.ts: -------------------------------------------------------------------------------- 1 | import type { AttributeMap } from "@block-kit/delta"; 2 | 3 | import { LIST_TYPE_KEY } from "../../bullet-list/types"; 4 | import { ORDER_LIST_TYPE } from "../types"; 5 | 6 | /** 7 | * 检查有序列表 8 | * @param attrs 9 | */ 10 | export const isOrderList = (attrs: AttributeMap) => { 11 | return attrs[LIST_TYPE_KEY] === ORDER_LIST_TYPE; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/variable/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import type { O } from "@block-kit/utils/dist/es/types"; 2 | import type React from "react"; 3 | 4 | export type EditablePluginOptions = { 5 | placeholders?: O.Map; 6 | onKeydown?: React.KeyboardEventHandler; 7 | }; 8 | 9 | export type SelectorPluginOptions = { 10 | selector?: O.Map; 11 | optionsWidth?: number; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/core/src/lookup/utils/is.ts: -------------------------------------------------------------------------------- 1 | import type { Point } from "../../selection/modules/point"; 2 | import type { LeafState } from "../../state/modules/leaf-state"; 3 | 4 | /** 5 | * 判断是否是 Leaf 的尾部 6 | * @param leaf 7 | * @param point 8 | */ 9 | export const isLeafOffsetTail = (leaf: LeafState | null, point: Point) => { 10 | return leaf && point.offset - leaf.offset - leaf.length >= 0; 11 | }; 12 | -------------------------------------------------------------------------------- /examples/variable/src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const VARS_KEY = "var-key"; 2 | export const VARS_VALUE_KEY = "var-value"; 3 | export const VARS_CLS_PREFIX = "editable-vars"; 4 | export const DATA_EDITABLE_KEY = "data-editable"; 5 | 6 | export const SEL_KEY = "sel-key"; 7 | export const SEL_VALUE_KEY = "sel-value"; 8 | export const SEL_CLS_PREFIX = "editable-sel"; 9 | export const SEL_OPTIONS_WIDTH = 100; 10 | -------------------------------------------------------------------------------- /blocks/x-react/src/hooks/use-layout-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | 3 | export const LayoutEffectContext = createContext<(id: string) => void>(() => null); 4 | LayoutEffectContext.displayName = "LayoutEffectContext"; 5 | 6 | export const useLayoutEffectContext = () => { 7 | const context = useContext(LayoutEffectContext); 8 | return { forceLayoutEffect: context }; 9 | }; 10 | -------------------------------------------------------------------------------- /examples/website/src/react/components/github.tsx: -------------------------------------------------------------------------------- 1 | import { IconGithub } from "@arco-design/web-react/icon"; 2 | import type { FC } from "react"; 3 | 4 | export const GitHubIcon: FC = () => { 5 | return ( 6 |
window.open("https://github.com/WindRunnerMax/BlockKit")} 9 | > 10 | 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/plugin/src/image/types/index.ts: -------------------------------------------------------------------------------- 1 | export const LOADING_STATUS = { 2 | LOADING: "1", 3 | SUCCESS: "2", 4 | FAIL: "3", 5 | } as const; 6 | 7 | export const MIN_WIDTH = 100; 8 | export const IMAGE_KEY = "image"; 9 | export const IMAGE_SRC = "src"; 10 | export const IMAGE_WIDTH = "width"; 11 | export const IMAGE_SCALE = "scale"; 12 | export const IMAGE_HEIGHT = "height"; 13 | export const IMAGE_STATUS = "status"; 14 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/utils" 3 | - "packages/tools" 4 | - "packages/delta" 5 | - "packages/ot-json" 6 | - "packages/core" 7 | - "packages/react" 8 | - "packages/plugin" 9 | - "packages/vue" 10 | 11 | - "blocks/x-json" 12 | - "blocks/x-core" 13 | - "blocks/x-react" 14 | - "blocks/x-plugin" 15 | 16 | - "examples/variable" 17 | - "examples/stream" 18 | - "examples/website" 19 | -------------------------------------------------------------------------------- /blocks/x-core/src/editor/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { EditorSchema, LOG_LEVEL } from "@block-kit/core"; 2 | import type { O } from "@block-kit/utils/dist/es/types"; 3 | import type { BlockMap } from "@block-kit/x-json"; 4 | 5 | export type EditorOptions = { 6 | /** 初始渲染数据 */ 7 | initial?: BlockMap; 8 | /** 日志等级 */ 9 | logLevel?: O.Values; 10 | /** 文本编辑器预设渲染规则 */ 11 | schema?: EditorSchema; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/react/test/config/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import { sleep } from "@block-kit/utils"; 3 | 4 | export const waitRenderComplete = (editor: Editor, sleepMs?: number): Promise => { 5 | return Promise.all([ 6 | new Promise(resolve => { 7 | editor.event.once("PAINT", () => resolve()); 8 | }), 9 | sleep(sleepMs || 0), 10 | ]) as unknown as Promise; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/core/src/command/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { AttributeMap } from "@block-kit/delta"; 2 | 3 | import type { Range } from "../../selection/modules/range"; 4 | 5 | export type CMDPayload = { 6 | value: string; 7 | range?: Range; 8 | attrs?: AttributeMap; 9 | [key: string]: unknown; 10 | }; 11 | 12 | export type CMDFunc = (data: CMDPayload) => void | Promise; 13 | 14 | export type EditorCMD = Record; 15 | -------------------------------------------------------------------------------- /blocks/x-json/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "ts"], 3 | moduleDirectories: ["node_modules", "src", "test"], 4 | moduleNameMapper: { 5 | "src/(.*)$": "/src/$1", 6 | }, 7 | transform: { 8 | "\\.ts$": "ts-jest", 9 | "\\.js$": "babel-jest", 10 | }, 11 | transformIgnorePatterns: ["/node_modules/"], 12 | collectCoverage: false, 13 | testMatch: ["/test/**/*.test.ts"], 14 | }; 15 | -------------------------------------------------------------------------------- /packages/vue/src/utils/is.ts: -------------------------------------------------------------------------------- 1 | import type { LineState } from "@block-kit/core"; 2 | 3 | export const isStrictEmptyLine = (line: LineState) => { 4 | const leaves = line.getLeaves(); 5 | if (!leaves.length) { 6 | return true; 7 | } 8 | if ( 9 | leaves.length === 1 && 10 | leaves[0].eol && 11 | (!leaves[0].op.attributes || !Object.keys(leaves[0].op.attributes).length) 12 | ) { 13 | return true; 14 | } 15 | return false; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/delta/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "ts"], 3 | moduleDirectories: ["node_modules", "src", "test"], 4 | moduleNameMapper: { 5 | "src/(.*)$": "/src/$1", 6 | }, 7 | transform: { 8 | "\\.ts$": "ts-jest", 9 | "\\.js$": "babel-jest", 10 | }, 11 | transformIgnorePatterns: ["/node_modules/"], 12 | collectCoverage: false, 13 | testMatch: ["/test/**/*.test.ts"], 14 | }; 15 | -------------------------------------------------------------------------------- /packages/ot-json/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "ts"], 3 | moduleDirectories: ["node_modules", "src", "test"], 4 | moduleNameMapper: { 5 | "src/(.*)$": "/src/$1", 6 | }, 7 | transform: { 8 | "\\.ts$": "ts-jest", 9 | "\\.js$": "babel-jest", 10 | }, 11 | transformIgnorePatterns: ["/node_modules/"], 12 | collectCoverage: false, 13 | testMatch: ["/test/**/*.test.ts"], 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # log 4 | *.log 5 | *.docx 6 | *.pdf 7 | 8 | # dependencies 9 | node_modules 10 | .pnp 11 | .pnp.js 12 | 13 | # testing 14 | coverage 15 | 16 | # production 17 | build 18 | dist 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "preserve", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "es5", 10 | "bracketSpacing": true, 11 | "arrowParens": "avoid", 12 | "requirePragma": false, 13 | "insertPragma": false, 14 | "proseWrap": "preserve", 15 | "htmlWhitespaceSensitivity": "ignore", 16 | "endOfLine": "lf", 17 | }; 18 | -------------------------------------------------------------------------------- /packages/plugin/src/mention/styles/index.scss: -------------------------------------------------------------------------------- 1 | .block-kit-mention-embed { 2 | display: inline-block; 3 | line-height: 1.3; 4 | text-decoration: inherit; 5 | 6 | .block-kit-mention-name { 7 | background-color: rgba(var(--arcoblue-6), 0.9); 8 | border-radius: 8px; 9 | box-sizing: border-box; 10 | color: var(--color-white); 11 | display: inline-block; 12 | margin: 0 3px; 13 | padding: 0 4px; 14 | text-decoration: inherit; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/plugin/src/mention/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const SUGGEST_OFFSET = 5; 2 | export const PANEL_WIDTH = 130; 3 | export const PANEL_HEIGHT = 150; 4 | 5 | export const DATA = [ 6 | "Alice", 7 | "Bob", 8 | "Charlie", 9 | "David", 10 | "Eve", 11 | "Frank", 12 | "Grace", 13 | "Heidi", 14 | "Ivan", 15 | "Judy", 16 | "Mallory", 17 | "Oscar", 18 | "Peggy", 19 | "Romeo", 20 | "Sybil", 21 | "Trudy", 22 | "Victor", 23 | "Walter", 24 | "Zoe", 25 | ]; 26 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/types/index.ts: -------------------------------------------------------------------------------- 1 | import { BOLD_KEY } from "../../bold/types"; 2 | import { INLINE_CODE_KEY } from "../../inline-code/types"; 3 | 4 | export const TOOLBAR_TYPES = [BOLD_KEY, INLINE_CODE_KEY] as const; 5 | export const TOOLBAR_KEY_SET = new Set(TOOLBAR_TYPES); 6 | 7 | export type ToolbarProps = { 8 | className?: string; 9 | children: React.ReactNode; 10 | styles?: React.CSSProperties; 11 | onRef?: React.MutableRefObject; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/delta/test/delta/op.test.ts: -------------------------------------------------------------------------------- 1 | import { getOpLength } from "../../src/delta/op"; 2 | 3 | describe("Op", () => { 4 | describe("length()", () => { 5 | it("delete", () => { 6 | expect(getOpLength({ delete: 5 })).toEqual(5); 7 | }); 8 | 9 | it("retain", () => { 10 | expect(getOpLength({ retain: 2 })).toEqual(2); 11 | }); 12 | 13 | it("insert text", () => { 14 | expect(getOpLength({ insert: "text" })).toEqual(4); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 |

4 | GitHub 5 | 6 | DEMO 7 | 8 | Usage 9 |

10 | 11 | ## Development 12 | 13 | ```bash 14 | pnpm install 15 | pnpm run --filter "@block-kit/website^..." build 16 | npm run dev 17 | ``` 18 | -------------------------------------------------------------------------------- /packages/utils/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "ts"], 3 | moduleDirectories: ["node_modules", "src", "test"], 4 | moduleNameMapper: { 5 | "src/(.*)$": "/src/$1", 6 | }, 7 | transform: { 8 | "\\.ts$": "ts-jest", 9 | "\\.js$": "babel-jest", 10 | }, 11 | transformIgnorePatterns: ["/node_modules/"], 12 | collectCoverage: false, 13 | testEnvironment: "jsdom", 14 | testMatch: ["/test/**/*.test.ts"], 15 | }; 16 | -------------------------------------------------------------------------------- /packages/utils/test/json.test.ts: -------------------------------------------------------------------------------- 1 | import { TSON } from "../src/json"; 2 | 3 | describe("JSON", () => { 4 | it("parse", () => { 5 | const str = '{"a":1,"b":"2","c":[3,"4"]}'; 6 | const data = TSON.decode(str); 7 | expect(data).toEqual({ a: 1, b: "2", c: [3, "4"] }); 8 | }); 9 | 10 | it("stringify", () => { 11 | const data = { a: 1, b: "2", c: [3, "4"] }; 12 | const str = TSON.encode(data); 13 | expect(str).toBe('{"a":1,"b":"2","c":[3,"4"]}'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/utils/src/extract.ts: -------------------------------------------------------------------------------- 1 | import { URI } from "./uri"; 2 | 3 | export class Extract { 4 | /** 5 | * 提取 Email 信息 6 | * @param str 7 | */ 8 | public static email(str: string) { 9 | const [name, domain] = str.split("@"); 10 | return { name, domain: domain || "" }; 11 | } 12 | 13 | /** 14 | * 从路径解析数据 15 | * @param path 16 | * @param template 17 | * @example ("/user/123", "/user/:id") => { id: "123" } 18 | */ 19 | public static path = URI.parsePathParams; 20 | } 21 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-editor.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import React, { createContext } from "react"; 3 | 4 | export const BlockKitContext = createContext(null); 5 | BlockKitContext.displayName = "BlockKit"; 6 | 7 | export const useEditorStatic = () => { 8 | const editor = React.useContext(BlockKitContext); 9 | 10 | if (!editor) { 11 | throw new Error("UseEditor must be used within a EditorContext"); 12 | } 13 | 14 | return { editor }; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/react/test/config/debug.ts: -------------------------------------------------------------------------------- 1 | import type { LineState } from "@block-kit/core"; 2 | 3 | export const echoLines = (lines: LineState[]) => { 4 | lines.forEach(echoLineState); 5 | }; 6 | 7 | export const echoLineState = (lineState: LineState) => { 8 | console.log("LineState", lineState.getOps()); 9 | }; 10 | 11 | export const stringifyHTML = (node: Node) => { 12 | const serializer = new XMLSerializer(); 13 | const html = serializer.serializeToString(node); 14 | console.log("HTML", html); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/plugin/src/shared/utils/is.ts: -------------------------------------------------------------------------------- 1 | import type { LineState } from "@block-kit/core"; 2 | import { isEOLOp } from "@block-kit/delta"; 3 | 4 | export const isEmptyLine = (line: LineState) => { 5 | const lastLeaf = line.getLastLeaf(); 6 | const leaves = line.getLeaves(); 7 | // 没有最后的叶子结点, 或者仅单个节点且最后的叶子结点是换行符 8 | return !lastLeaf || (leaves.length === 1 && isEOLOp(lastLeaf.op)); 9 | }; 10 | 11 | export const isKeyCode = (event: KeyboardEvent, code: number) => { 12 | return event.keyCode === code; 13 | }; 14 | -------------------------------------------------------------------------------- /blocks/x-react/src/hooks/use-editor.tsx: -------------------------------------------------------------------------------- 1 | import type { BlockEditor } from "@block-kit/x-core"; 2 | import React, { createContext } from "react"; 3 | 4 | export const BlockKitContext = createContext(null); 5 | BlockKitContext.displayName = "BlockKitX"; 6 | 7 | export const useEditorStatic = () => { 8 | const editor = React.useContext(BlockKitContext); 9 | 10 | if (!editor) { 11 | throw new Error("UseEditor must be used within a EditorContext"); 12 | } 13 | 14 | return { editor }; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/ot-json/test/text0-compose.test.ts: -------------------------------------------------------------------------------- 1 | import { text } from "../src"; 2 | 3 | describe("compose", () => { 4 | it("is sane", () => { 5 | expect(text.compose([], [])).toEqual([]); 6 | expect(text.compose([{ i: "x", p: 0 }], [])).toEqual([{ i: "x", p: 0 }]); 7 | expect(text.compose([], [{ i: "x", p: 0 }])).toEqual([{ i: "x", p: 0 }]); 8 | expect(text.compose([{ i: "y", p: 100 }], [{ i: "x", p: 0 }])).toEqual([ 9 | { i: "y", p: 100 }, 10 | { i: "x", p: 0 }, 11 | ]); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /blocks/x-core/src/event/bus/types.ts: -------------------------------------------------------------------------------- 1 | import type { EventBus, EventKeys, Listener as EventListener } from "@block-kit/utils"; 2 | 3 | import type { EventMap } from "./index"; 4 | 5 | /** 事件类型扩展 */ 6 | export interface EventMapExtension {} 7 | 8 | /** 内建事件组 */ 9 | export type InternalEvent = EventMap & EventMapExtension; 10 | 11 | /** 内建事件总线类型 */ 12 | export type InternalEventBus = EventBus; 13 | 14 | /** 事件监听方法 */ 15 | export type Listener> = EventListener; 16 | -------------------------------------------------------------------------------- /packages/core/test/config/debug.ts: -------------------------------------------------------------------------------- 1 | import type { LineState } from "../../src/state/modules/line-state"; 2 | 3 | export const echoLines = (lines: LineState[]) => { 4 | lines.forEach(echoLineState); 5 | }; 6 | 7 | export const echoLineState = (lineState: LineState) => { 8 | console.log("LineState", lineState.getOps()); 9 | }; 10 | 11 | export const stringifyHTML = (node: Node) => { 12 | const serializer = new XMLSerializer(); 13 | const html = serializer.serializeToString(node); 14 | console.log("HTML", html); 15 | }; 16 | -------------------------------------------------------------------------------- /examples/stream/README.md: -------------------------------------------------------------------------------- 1 | # Streaming 2 | 3 |

4 | GitHub 5 | 6 | DEMO 7 | 8 | Usage 9 |

10 | 11 | ## Development 12 | 13 | ```bash 14 | pnpm install 15 | # http://localhost:8080/streaming.html 16 | pnpm run --filter @block-kit/website dev 17 | ``` 18 | -------------------------------------------------------------------------------- /packages/plugin/src/order-list/styles/index.scss: -------------------------------------------------------------------------------- 1 | .block-kit-order-list { 2 | display: flex; 3 | list-style-type: none; 4 | margin-block-end: 0; 5 | margin-block-start: 0; 6 | margin-inline-end: 0; 7 | margin-inline-start: 0; 8 | padding-inline-start: 0; 9 | 10 | .block-kit-order-indicator { 11 | color: rgb(var(--arcoblue-6)); 12 | margin-left: 6px; 13 | margin-right: 6px; 14 | user-select: none; 15 | } 16 | 17 | .block-kit-order-item { 18 | display: block; 19 | flex: 1 0 0; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/react/src/utils/event.ts: -------------------------------------------------------------------------------- 1 | export const preventNativeEvent = (event: { 2 | preventDefault: Event["preventDefault"]; 3 | stopPropagation: Event["stopPropagation"]; 4 | stopImmediatePropagation?: Event["stopImmediatePropagation"]; 5 | }) => { 6 | event.preventDefault(); 7 | event.stopPropagation(); 8 | event.stopImmediatePropagation && event.stopImmediatePropagation(); 9 | }; 10 | 11 | export const preventReactEvent = (event: React.SyntheticEvent) => { 12 | preventNativeEvent(event); 13 | preventNativeEvent(event.nativeEvent); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/delta/src/cluster/interface.ts: -------------------------------------------------------------------------------- 1 | import type { Ops } from "../delta/interface"; 2 | 3 | export const BLOCK_TYPE = { 4 | /** 内容块类型 - Content */ 5 | C: "C", 6 | /** 列表块类型 - List */ 7 | L: "L", 8 | } as const; 9 | 10 | export type BlockOption = { 11 | ops?: Ops; 12 | id?: string; 13 | type?: string; 14 | }; 15 | 16 | export type BlockLike = Required; 17 | export type BlockSetLike = Record; 18 | export type DeltaLike = Omit; 19 | export type BlockSetOption = Record; 20 | -------------------------------------------------------------------------------- /blocks/x-core/src/editor/utils/constant.ts: -------------------------------------------------------------------------------- 1 | import { getId } from "@block-kit/utils"; 2 | import type { Blocks } from "@block-kit/x-json"; 3 | 4 | export const getInitialBlocks = (): Blocks => { 5 | const rootId = getId(); 6 | const textChildId = getId(); 7 | return { 8 | [rootId]: { 9 | id: rootId, 10 | data: { type: "ROOT", parent: "", children: [textChildId] }, 11 | version: 1, 12 | }, 13 | [textChildId]: { 14 | id: getId(), 15 | version: 1, 16 | data: { type: "text", parent: rootId, children: [], delta: [] }, 17 | }, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/plugin/src/bullet-list/styles/index.scss: -------------------------------------------------------------------------------- 1 | .block-kit-bullet-list { 2 | list-style-type: none; 3 | margin: 0; 4 | padding-left: 0; 5 | 6 | .block-kit-bullet-item::marker { 7 | color: rgb(var(--arcoblue-6)); 8 | margin-right: 0; 9 | text-align: unset !important; 10 | text-align-last: unset !important; 11 | } 12 | 13 | // ["●", "◯", "■"] => disc, circle, square 14 | @each $index, $type in 0 disc, 1 circle, 2 square { 15 | .block-kit-bullet-item.block-kit-li-level-#{$index} { 16 | list-style-type: $type; 17 | margin-left: 20px; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /blocks/x-core/src/plugin/types/context.ts: -------------------------------------------------------------------------------- 1 | import type { P } from "@block-kit/utils/dist/es/types"; 2 | import type { Properties } from "csstype"; 3 | 4 | import type { BlockState } from "../../state/modules/block-state"; 5 | 6 | /** 7 | * 块包裹状态 8 | */ 9 | export type WrapContext = { 10 | classList: string[]; 11 | blockState: BlockState; 12 | style: Properties; 13 | children?: P.Any; 14 | }; 15 | 16 | /** 17 | * 块渲染状态 18 | */ 19 | export type BlockContext = { 20 | key?: string; 21 | classList: string[]; 22 | blockState: BlockState; 23 | style: Properties; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/vue/src/utils/weak-map.ts: -------------------------------------------------------------------------------- 1 | import type { LeafState, LineState } from "@block-kit/core"; 2 | import type { VNode } from "vue"; 3 | 4 | /** 5 | * LeafState 与 Text 节点的映射 6 | * - 仅处理文本节点, 零宽字符节点暂不处理 7 | */ 8 | export const LEAF_TO_TEXT = new WeakMap(); 9 | 10 | /** 11 | * VNode 与 State 的映射 12 | * - 渲染时即刻加入映射, wrap 时即刻消费映射 13 | */ 14 | export const JSX_TO_STATE = new WeakMap(); 15 | 16 | /** 17 | * State 与 Wrapper Symbol 的映射 18 | * - 主要是取得已经处理过的节点, 避免重复处理 19 | */ 20 | export const STATE_TO_SYMBOL = new WeakMap(); 21 | -------------------------------------------------------------------------------- /packages/utils/src/uuid.ts: -------------------------------------------------------------------------------- 1 | /** 原始字符 */ 2 | const CHARTS = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789"; 3 | 4 | /** 5 | * 生成唯一 ID 6 | * @param {number} len ID 长度 7 | */ 8 | export const getUniqueId = (len: number = 10): string => { 9 | const chars = new Array(len - 1).fill(""); 10 | return ( 11 | // 保证首字符非数字 避免 querySelector 方法抛异常 12 | CHARTS[Math.floor(Math.random() * 52)] + 13 | chars.map(() => CHARTS[Math.floor(Math.random() * CHARTS.length)]).join("") 14 | ); 15 | }; 16 | 17 | /** 18 | * 生成唯一 ID 19 | * @param {number} len ID 长度 20 | */ 21 | export const getId = getUniqueId; 22 | -------------------------------------------------------------------------------- /blocks/x-core/src/plugin/index.ts: -------------------------------------------------------------------------------- 1 | import type { BlockEditor } from "../editor"; 2 | import type { CorePlugin } from "./modules/implement"; 3 | 4 | export class Plugin { 5 | /** key 块渲染映射 */ 6 | public map: Record = {}; 7 | 8 | /** 9 | * 构造函数 10 | * @param editor 11 | */ 12 | constructor(protected editor: BlockEditor) {} 13 | 14 | /** 15 | * 对外的插件注册方法 16 | * - 会在注册插件时进行 Hook 17 | */ 18 | public get register() { 19 | /** 20 | * 批量注册插件 21 | * - 仅支持单次批量注册 22 | * @param plugins 23 | */ 24 | function _register(this: Plugin) {} 25 | return _register; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /blocks/x-react/src/hooks/use-meta.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import type { Delta } from "@block-kit/delta"; 3 | import React, { createContext } from "react"; 4 | 5 | export type MetaProviderProps = { 6 | onCreateTextEditor: (delta: Delta) => Editor; 7 | }; 8 | 9 | export const MetaContext = createContext(null); 10 | MetaContext.displayName = "MetaContext"; 11 | 12 | export const useMetaStatic = () => { 13 | const meta = React.useContext(MetaContext); 14 | 15 | if (!meta) { 16 | throw new Error("UseMeta must be used within a MetaContext"); 17 | } 18 | 19 | return meta; 20 | }; 21 | -------------------------------------------------------------------------------- /blocks/x-json/src/types/interface.ts: -------------------------------------------------------------------------------- 1 | import type { Op } from "@block-kit/delta"; 2 | import type { ROOT_BLOCK } from "@block-kit/utils"; 3 | 4 | /** Block 类型基础属性 */ 5 | export interface BasicBlock { 6 | /** Block 类型 */ 7 | type: string; 8 | /** 9 | * Block 文本类型 10 | * - 重点关注, 该类型的存在意味着该 Block 是文本类型节点 11 | */ 12 | delta?: Op[]; 13 | /** Block 父节点 */ 14 | parent: string; 15 | /** Block 子节点 */ 16 | children: string[]; 17 | } 18 | 19 | /** Block 类型属性扩展 */ 20 | export interface BlockModule { 21 | root: { 22 | type: typeof ROOT_BLOCK; 23 | }; 24 | text: { 25 | type: "text"; 26 | delta: Op[]; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/plugin/src/bullet-list/view/list.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/index.scss"; 2 | 3 | import type { Editor } from "@block-kit/core"; 4 | import type { ReactLineContext } from "@block-kit/react"; 5 | import { cs } from "@block-kit/utils"; 6 | import type { FC } from "react"; 7 | 8 | export const BulletListView: FC<{ 9 | context: ReactLineContext; 10 | editor: Editor; 11 | level: number; 12 | }> = props => { 13 | const { level, children } = props; 14 | 15 | return ( 16 |
    17 |
  • {children}
  • 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/utils/test/regexp.test.ts: -------------------------------------------------------------------------------- 1 | import { RegExec } from "../src/regexp"; 2 | 3 | describe("regexp", () => { 4 | it("exec", () => { 5 | const result = RegExec.exec(/>(.+)?123"); 6 | expect(result).toEqual("123"); 7 | }); 8 | 9 | it("match", () => { 10 | const result = RegExec.match(/
(.+?)<\/div>/g, "
123
456
"); 11 | expect(result).toEqual(["123", "456"]); 12 | }); 13 | 14 | it("get", () => { 15 | const result1 = RegExec.get(["a", "b"], 0); 16 | const result2 = RegExec.get(["a", "b"], 2); 17 | expect(result1).toEqual("a"); 18 | expect(result2).toEqual(""); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /blocks/x-react/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "ts", "jsx", "tsx"], 3 | moduleDirectories: ["node_modules", "src", "test"], 4 | moduleNameMapper: { 5 | "^react$": require.resolve("react"), 6 | "^react-dom$": require.resolve("react-dom"), 7 | "\\.(css|less|scss|sass)$": "/test/config/styles.ts", 8 | }, 9 | transform: { 10 | "\\.tsx?$": "ts-jest", 11 | "\\.jsx?$": "babel-jest", 12 | }, 13 | transformIgnorePatterns: ["/node_modules/"], 14 | collectCoverage: false, 15 | testEnvironment: "jsdom", 16 | testMatch: ["/test/**/*.test.ts", "/test/**/*.test.tsx"], 17 | }; 18 | -------------------------------------------------------------------------------- /packages/react/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "ts", "jsx", "tsx"], 3 | moduleDirectories: ["node_modules", "src", "test"], 4 | moduleNameMapper: { 5 | "^react$": require.resolve("react"), 6 | "^react-dom$": require.resolve("react-dom"), 7 | "\\.(css|less|scss|sass)$": "/test/config/styles.ts", 8 | }, 9 | transform: { 10 | "\\.tsx?$": "ts-jest", 11 | "\\.jsx?$": "babel-jest", 12 | }, 13 | transformIgnorePatterns: ["/node_modules/"], 14 | collectCoverage: false, 15 | testEnvironment: "jsdom", 16 | testMatch: ["/test/**/*.test.ts", "/test/**/*.test.tsx"], 17 | }; 18 | -------------------------------------------------------------------------------- /packages/core/src/input/utils/hot-key.ts: -------------------------------------------------------------------------------- 1 | import { IS_MAC, KEY_CODE } from "@block-kit/utils"; 2 | 3 | export const CTRL = IS_MAC ? "metaKey" : "ctrlKey"; 4 | 5 | export const isArrowLeft = (e: KeyboardEvent) => e.keyCode === KEY_CODE.LEFT; 6 | export const isArrowRight = (e: KeyboardEvent) => e.keyCode === KEY_CODE.RIGHT; 7 | export const isArrowUp = (e: KeyboardEvent) => e.keyCode === KEY_CODE.UP; 8 | export const isArrowDown = (e: KeyboardEvent) => e.keyCode === KEY_CODE.DOWN; 9 | export const isUndo = (e: KeyboardEvent) => !e.shiftKey && e[CTRL] && e.keyCode === KEY_CODE.Z; 10 | export const isRedo = (e: KeyboardEvent) => e.shiftKey && e[CTRL] && e.keyCode === KEY_CODE.Z; 11 | -------------------------------------------------------------------------------- /packages/core/test/state/status.test.ts: -------------------------------------------------------------------------------- 1 | import { Delta } from "@block-kit/delta"; 2 | 3 | import { Editor } from "../../src/editor"; 4 | import { EditorState } from "../../src/state"; 5 | import { EDITOR_STATE } from "../../src/state/types"; 6 | 7 | describe("state status", () => { 8 | const delta = new Delta(); 9 | const editor = new Editor(); 10 | 11 | it("base", () => { 12 | const state = new EditorState(editor, delta); 13 | state.set(EDITOR_STATE.COMPOSING, true); 14 | expect(state.get(EDITOR_STATE.COMPOSING)).toEqual(true); 15 | state.set(EDITOR_STATE.COMPOSING, false); 16 | expect(state.get(EDITOR_STATE.COMPOSING)).toEqual(false); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/core/src/schema/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { O } from "@block-kit/utils/dist/es/types"; 2 | 3 | export type SchemaRule = { 4 | /** 5 | * 块级节点 6 | * - block: 独占一行的可编辑节点 7 | */ 8 | block?: boolean; 9 | /** 10 | * 行内节点 11 | * - inline + mark: 不追踪末尾 Mark 12 | * - inline + void: 行内 Void 节点 => Embed 13 | */ 14 | inline?: boolean; 15 | /** 16 | * 空节点 17 | * - void: 独占一行且不可编辑的节点 18 | * - void + inline: 行内 Void 节点 => Embed 19 | */ 20 | void?: boolean; 21 | /** 22 | * Mark 23 | * - mark: 输入时会自动追踪样式的节点 24 | * - mark + inline: 不追踪末尾 Mark 25 | */ 26 | mark?: boolean; 27 | }; 28 | 29 | export type EditorSchema = O.Map; 30 | -------------------------------------------------------------------------------- /packages/plugin/src/shortcut/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import type { Range } from "@block-kit/core"; 3 | import type { O, P } from "@block-kit/utils/dist/es/types"; 4 | 5 | export const SHORTCUT_KEY = "SHORTCUT_KEY"; 6 | 7 | /** 8 | * 快捷键处理函数 9 | * @param event 键盘事件 10 | * @param payload 携带参数 11 | * @returns - true 表示匹配成功并阻止后续处理 12 | * - 否则继续执行后续快捷键逻辑 13 | */ 14 | export type ShortcutFunc = ( 15 | event: KeyboardEvent, 16 | payload: { 17 | editor: Editor; 18 | keys: O.Map; 19 | sel: Range | null; 20 | } 21 | ) => true | P.Nil; 22 | 23 | /** 预设快捷键集合 */ 24 | export type ShortcutFuncMap = O.Map; 25 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/modules/image.tsx: -------------------------------------------------------------------------------- 1 | import { IconImage } from "@arco-design/web-react/icon"; 2 | import { cs, TRULY } from "@block-kit/utils"; 3 | import type { FC } from "react"; 4 | 5 | import { IMAGE_KEY } from "../../image/types"; 6 | import { useToolbarContext } from "../context/provider"; 7 | 8 | export const Image: FC = () => { 9 | const { refreshMarks, editor } = useToolbarContext(); 10 | 11 | return ( 12 |
{ 15 | editor.command.exec(IMAGE_KEY, { value: TRULY }); 16 | refreshMarks(); 17 | }} 18 | > 19 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/styles/link.scss: -------------------------------------------------------------------------------- 1 | .block-kit-link-popup { 2 | background-color: var(--color-bg-1); 3 | border-radius: 3px; 4 | box-shadow: 0 0 4px var(--color-border-2); 5 | padding: 10px 15px; 6 | position: relative; 7 | width: 330px; 8 | 9 | .arco-form-size-small .arco-form-label-item > label { 10 | font-size: 13px; 11 | } 12 | 13 | .arco-form-item { 14 | margin: 5px 0; 15 | } 16 | 17 | .block-kit-link-popup-button { 18 | bottom: 15px; 19 | position: absolute; 20 | right: 15px; 21 | 22 | .arco-btn { 23 | margin-left: 10px; 24 | } 25 | } 26 | 27 | .block-kit-link-popup-go { 28 | cursor: pointer; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/ot-json/src/index.ts: -------------------------------------------------------------------------------- 1 | import { JSONType } from "./json0"; 2 | import { TextType } from "./text0"; 3 | 4 | export { JSONType } from "./json0"; 5 | export type { Subtype, SubtypeOp } from "./subtype"; 6 | export { subtypes } from "./subtype"; 7 | export { TextType } from "./text0"; 8 | export type { 9 | ListDeleteOp, 10 | ListInsertOp, 11 | ListMoveOp, 12 | ListReplaceOp, 13 | NumberAddOp, 14 | ObjectDeleteOp, 15 | ObjectInsertOp, 16 | ObjectReplaceOp, 17 | Op, 18 | Side, 19 | Snapshot, 20 | TextOp, 21 | } from "./types"; 22 | export { clone as cloneSnapshot, SIDE } from "./utils"; 23 | 24 | export const json = new JSONType(); 25 | export const text = new TextType(); 26 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/modules/divider.tsx: -------------------------------------------------------------------------------- 1 | import { cs, TRULY } from "@block-kit/utils"; 2 | import type { FC } from "react"; 3 | 4 | import { DIVIDER_KEY } from "../../divider/types"; 5 | import { DividerIcon } from "../../shared/icons/divider"; 6 | import { useToolbarContext } from "../context/provider"; 7 | 8 | export const Divider: FC = () => { 9 | const { refreshMarks, editor } = useToolbarContext(); 10 | 11 | return ( 12 |
{ 15 | editor.command.exec(DIVIDER_KEY, { value: TRULY }); 16 | refreshMarks(); 17 | }} 18 | > 19 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /examples/website/src/react/config/schema.ts: -------------------------------------------------------------------------------- 1 | import type { EditorSchema } from "@block-kit/core"; 2 | 3 | export const SCHEMA: EditorSchema = { 4 | "bold": { mark: true }, 5 | "italic": { mark: true }, 6 | "underline": { mark: true }, 7 | "strike": { mark: true }, 8 | "inline-code": { mark: true, inline: true }, 9 | "link": { mark: true, inline: true }, 10 | "link-blank": { mark: true, inline: true }, 11 | "color": { mark: true }, 12 | "font-size": { mark: true }, 13 | "background": { mark: true }, 14 | "image": { block: true, void: true }, 15 | "mention": { void: true, inline: true }, 16 | "divider": { block: true, void: true }, 17 | "emoji": { void: true, inline: true }, 18 | }; 19 | -------------------------------------------------------------------------------- /examples/stream/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { MarkedToken } from "marked"; 2 | 3 | import type { MdComposer } from "../modules/md-composer"; 4 | 5 | export const DEFAULT_OPTIONS: TokenParserOptions = { 6 | depth: 0, 7 | parent: null, 8 | index: 0, 9 | }; 10 | 11 | export type TokenParserOptions = { 12 | /** 13 | * 递归深度 14 | */ 15 | depth: number; 16 | /** 17 | * 节点索引 18 | * - 所属父节点的第 i 个元素 19 | */ 20 | index: number; 21 | /** 22 | * 父节点 23 | */ 24 | parent: MarkedToken | null; 25 | /** 26 | * 列表深度 27 | * - 依赖 list_item 层级 28 | */ 29 | listLevel?: number; 30 | /** 31 | * Markdown 调度器 32 | * - 通常仅表格、代码块等节点需要传递 33 | */ 34 | mc?: MdComposer; 35 | }; 36 | -------------------------------------------------------------------------------- /blocks/x-json/test/transform/batch.test.ts: -------------------------------------------------------------------------------- 1 | import type { JSONOp } from "../../src"; 2 | import { normalizeBatchOps } from "../../src/utils/transform"; 3 | 4 | describe("transform batch", () => { 5 | it("normalize", () => { 6 | const ops: JSONOp[] = [ 7 | { p: ["a", "b", 1], ld: 1 }, 8 | { p: ["a", "b", 3], ld: 3 }, 9 | { p: ["a", "b", 2], ld: 2 }, 10 | { p: ["a", "b", 3], ld: 3 }, 11 | ]; 12 | const batch = normalizeBatchOps(ops); 13 | expect(batch[0]).toEqual({ p: ["a", "b", 1], ld: 1 }); 14 | expect(batch[1]).toEqual({ p: ["a", "b", 2], ld: 3 }); 15 | expect(batch[2]).toEqual({ p: ["a", "b", 1], ld: 2 }); 16 | expect(batch[3]).toEqual(undefined); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/plugin/src/image/styles/index.scss: -------------------------------------------------------------------------------- 1 | .block-kit-image-container { 2 | display: inline-block; 3 | position: relative; 4 | 5 | .block-kit-image { 6 | display: block; 7 | height: auto; 8 | max-height: 100%; 9 | max-width: 100%; 10 | object-fit: contain; 11 | } 12 | 13 | .block-kit-image-loading { 14 | align-items: center; 15 | background-color: rgba(var(--gray-3), 0.5); 16 | bottom: 0; 17 | box-sizing: border-box; 18 | color: var(--color-text-2); 19 | display: flex; 20 | font-size: 23px; 21 | justify-content: center; 22 | left: 0; 23 | padding: 1px; 24 | position: absolute; 25 | right: 0; 26 | top: 0; 27 | z-index: 2; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/modules/quote.tsx: -------------------------------------------------------------------------------- 1 | import { IconQuote } from "@arco-design/web-react/icon"; 2 | import { cs, TRULY } from "@block-kit/utils"; 3 | import type { FC } from "react"; 4 | 5 | import { QUOTE_KEY } from "../../quote/types"; 6 | import { useToolbarContext } from "../context/provider"; 7 | 8 | export const Quote: FC = () => { 9 | const { keys, refreshMarks, editor } = useToolbarContext(); 10 | 11 | return ( 12 |
{ 15 | editor.command.exec(QUOTE_KEY, { value: TRULY }); 16 | refreshMarks(); 17 | }} 18 | > 19 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/modules/bold.tsx: -------------------------------------------------------------------------------- 1 | import { IconBold } from "@arco-design/web-react/icon"; 2 | import { cs, NIL, TRULY } from "@block-kit/utils"; 3 | import type { FC } from "react"; 4 | 5 | import { BOLD_KEY } from "../../bold/types"; 6 | import { useToolbarContext } from "../context/provider"; 7 | 8 | export const Bold: FC = () => { 9 | const { keys, refreshMarks, editor } = useToolbarContext(); 10 | 11 | const onExec = () => { 12 | editor.command.exec(BOLD_KEY, { value: keys[BOLD_KEY] ? NIL : TRULY }); 13 | refreshMarks(); 14 | }; 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/delta/test/utils/delta.test.ts: -------------------------------------------------------------------------------- 1 | import { Delta, deltaEndsWith } from "../../src"; 2 | 3 | describe("delta", () => { 4 | const delta = new Delta().insert("123").insert("\n").insert("456").insert("\n"); 5 | 6 | it("delta ends with", () => { 7 | expect(deltaEndsWith(delta, "456\n")).toBe(true); 8 | expect(deltaEndsWith(delta, "\n456\n")).toBe(true); 9 | expect(deltaEndsWith(delta, "\n")).toBe(true); 10 | expect(deltaEndsWith(delta, "123\n456\n")).toBe(true); 11 | }); 12 | 13 | it("delta not ends with", () => { 14 | expect(deltaEndsWith(delta, "6")).toBe(false); 15 | expect(deltaEndsWith(delta, "123")).toBe(false); 16 | expect(deltaEndsWith(delta, "1123\n456\n")).toBe(false); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /blocks/x-core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "ts"], 3 | moduleDirectories: ["node_modules", "src", "test"], 4 | moduleNameMapper: { 5 | "src/(.*)$": "/src/$1", 6 | }, 7 | transform: { 8 | "\\.ts$": "ts-jest", 9 | "\\.js$": "babel-jest", 10 | }, 11 | transformIgnorePatterns: ["/node_modules/"], 12 | collectCoverage: false, 13 | testEnvironment: "jsdom", 14 | // https://jestjs.io/docs/configuration#globalsetup-string 15 | // globalSetup: "./test/config/setup.ts", // fn 16 | // https://jestjs.io/docs/configuration#setupfilesafterenv-array 17 | // setupFilesAfterEnv: ["./test/config/setup.ts"], // iife 18 | testMatch: ["/test/**/*.test.ts"], 19 | }; 20 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "ts"], 3 | moduleDirectories: ["node_modules", "src", "test"], 4 | moduleNameMapper: { 5 | "src/(.*)$": "/src/$1", 6 | }, 7 | transform: { 8 | "\\.ts$": "ts-jest", 9 | "\\.js$": "babel-jest", 10 | }, 11 | transformIgnorePatterns: ["/node_modules/"], 12 | collectCoverage: false, 13 | testEnvironment: "jsdom", 14 | // https://jestjs.io/docs/configuration#globalsetup-string 15 | // globalSetup: "./test/config/setup.ts", // fn 16 | // https://jestjs.io/docs/configuration#setupfilesafterenv-array 17 | // setupFilesAfterEnv: ["./test/config/setup.ts"], // iife 18 | testMatch: ["/test/**/*.test.ts"], 19 | }; 20 | -------------------------------------------------------------------------------- /packages/delta/src/delta/interface.ts: -------------------------------------------------------------------------------- 1 | import type { AttributeMap } from "../attributes/interface"; 2 | 3 | export interface Op { 4 | // Only one property out of {insert, delete, retain} will be present 5 | insert?: string; 6 | delete?: number; 7 | retain?: number; 8 | 9 | attributes?: AttributeMap; 10 | } 11 | 12 | export const OP_TYPES = { 13 | INSERT: "insert", 14 | RETAIN: "retain", 15 | DELETE: "delete", 16 | }; 17 | export const EOL = "\n"; 18 | export const EOL_OP: InsertOp = { insert: EOL }; 19 | 20 | export type Ops = Op[]; 21 | export type DeleteOp = { delete: number }; 22 | export type RetainOp = { retain: number; attributes?: AttributeMap }; 23 | export type InsertOp = { insert: string; attributes?: AttributeMap }; 24 | -------------------------------------------------------------------------------- /packages/plugin/src/inline-code/styles/index.scss: -------------------------------------------------------------------------------- 1 | .block-kit-inline-code { 2 | background-color: var(--color-fill-2); 3 | border-bottom-style: solid; 4 | border-color: var(--color-border-2); 5 | border-top-style: solid; 6 | border-width: 1px; 7 | box-sizing: border-box; 8 | padding-bottom: 1px; 9 | padding-top: 1px; 10 | } 11 | 12 | .block-kit-inline-code-start { 13 | border-bottom-left-radius: 3px; 14 | border-left-style: solid; 15 | border-top-left-radius: 3px; 16 | margin-left: 2px; 17 | padding-left: 2px; 18 | } 19 | 20 | .block-kit-inline-code-end { 21 | border-bottom-right-radius: 3px; 22 | border-right-style: solid; 23 | border-top-right-radius: 3px; 24 | margin-right: 2px; 25 | padding-right: 2px; 26 | } 27 | -------------------------------------------------------------------------------- /packages/delta/src/attributes/diff.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from "@block-kit/utils"; 2 | 3 | import type { AttributeMap } from "./interface"; 4 | 5 | /** 6 | * 对比属性 7 | * @param a 8 | * @param b 9 | */ 10 | export const diffAttributes = ( 11 | a: AttributeMap = {}, 12 | b: AttributeMap = {} 13 | ): AttributeMap | undefined => { 14 | if (!isObject(a)) a = {}; 15 | if (!isObject(b)) b = {}; 16 | const attributes = Object.keys(a) 17 | .concat(Object.keys(b)) 18 | .reduce((attrs, key) => { 19 | if (a[key] !== b[key]) { 20 | attrs[key] = b[key] === undefined ? "" : b[key]; 21 | } 22 | return attrs; 23 | }, {}); 24 | return Object.keys(attributes).length > 0 ? attributes : undefined; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/modules/italic.tsx: -------------------------------------------------------------------------------- 1 | import { IconItalic } from "@arco-design/web-react/icon"; 2 | import { cs, NIL, TRULY } from "@block-kit/utils"; 3 | import type { FC } from "react"; 4 | 5 | import { ITALIC_KEY } from "../../italic/types"; 6 | import { useToolbarContext } from "../context/provider"; 7 | 8 | export const Italic: FC = () => { 9 | const { keys, refreshMarks, editor } = useToolbarContext(); 10 | 11 | const onExec = () => { 12 | editor.command.exec(ITALIC_KEY, { value: keys[ITALIC_KEY] ? NIL : TRULY }); 13 | refreshMarks(); 14 | }; 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/styles/float.scss: -------------------------------------------------------------------------------- 1 | @keyframes fade-in { 2 | from { opacity: 0; } 3 | to { opacity: 1; } 4 | } 5 | 6 | .block-kit-float-toolbar { 7 | animation: fade-in 0.3s; 8 | background-color: var(--color-bg-2); 9 | border: 1px solid var(--color-border-1); 10 | border-radius: 3px; 11 | box-shadow: 1px 2px 4px var(--color-border-2); 12 | position: absolute; 13 | transform: translate(-50%, -100%); 14 | z-index: 99999; 15 | } 16 | 17 | .block-kit-menu-toolbar.block-kit-float-toolbar { 18 | border-bottom: unset; 19 | 20 | .menu-toolbar-item { 21 | color: var(--color-text-1); 22 | margin: 0 5px; 23 | } 24 | 25 | .menu-toolbar-item.kit-color-case { 26 | margin: 0; 27 | padding: 3px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/context/provider.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import type { Range } from "@block-kit/core"; 3 | import React from "react"; 4 | 5 | export type ToolbarContextType = { 6 | editor: Editor; 7 | refreshMarks: () => void; 8 | keys: Record; 9 | setKeys: (v: Record) => void; 10 | selection: Range | null; 11 | }; 12 | 13 | export const ToolbarContext = React.createContext({ 14 | keys: {}, 15 | setKeys: () => null, 16 | refreshMarks: () => null, 17 | editor: null as unknown as Editor, 18 | selection: null, 19 | }); 20 | ToolbarContext.displayName = "Toolbar"; 21 | 22 | export const useToolbarContext = () => React.useContext(ToolbarContext); 23 | -------------------------------------------------------------------------------- /packages/delta/src/delta/op.ts: -------------------------------------------------------------------------------- 1 | import type { DeleteOp, InsertOp, Op, RetainOp } from "./interface"; 2 | 3 | export const isRetainOp = (op: Op): op is RetainOp => { 4 | return op && typeof op.retain === "number"; 5 | }; 6 | export const isInsertOp = (op: Op): op is InsertOp => { 7 | return op && typeof op.insert === "string"; 8 | }; 9 | export const isDeleteOp = (op: Op): op is DeleteOp => { 10 | return op && typeof op.delete === "number"; 11 | }; 12 | 13 | export const getOpLength = (op: Op): number => { 14 | if (isInsertOp(op)) { 15 | return op.insert.length; 16 | } else if (isRetainOp(op)) { 17 | return op.retain; 18 | } else if (isDeleteOp(op)) { 19 | return op.delete; 20 | } 21 | console.trace("Unknown Op:", op); 22 | return 0; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/plugin/src/shared/icons/text.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | 3 | export const TextIcon: FC = () => ( 4 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/modules/strike.tsx: -------------------------------------------------------------------------------- 1 | import { IconStrikethrough } from "@arco-design/web-react/icon"; 2 | import { cs, NIL, TRULY } from "@block-kit/utils"; 3 | import type { FC } from "react"; 4 | 5 | import { STRIKE_KEY } from "../../strike/types"; 6 | import { useToolbarContext } from "../context/provider"; 7 | 8 | export const Strike: FC = () => { 9 | const { keys, refreshMarks, editor } = useToolbarContext(); 10 | 11 | return ( 12 |
{ 15 | editor.command.exec(STRIKE_KEY, { value: keys[STRIKE_KEY] ? NIL : TRULY }); 16 | refreshMarks(); 17 | }} 18 | > 19 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/react/src/utils/weak-map.ts: -------------------------------------------------------------------------------- 1 | import type { LeafState, LineState } from "@block-kit/core"; 2 | 3 | /** 4 | * LeafState 与 Text 节点的映射 5 | * - 该 WeakMap 仅处理文本类型的节点 6 | */ 7 | export const LEAF_TO_TEXT = new WeakMap(); 8 | 9 | /** 10 | * LeafState 与 ZeroText 节点的映射 11 | * - 该 WeakMap 仅处理零宽字符文本类型的节点 12 | */ 13 | export const LEAF_TO_ZERO_TEXT = new WeakMap(); 14 | 15 | /** 16 | * LeafState 与节点渲染方法的映射 17 | * - 结构性的脏节点问题, 需要重建 Leaf DOM, 重新 Mount 节点 18 | */ 19 | export const LEAF_TO_REMOUNT = new WeakMap void>(); 20 | 21 | /** 22 | * JSX.Element 与 State 的映射 23 | * - 渲染时即刻加入映射, wrap 时即刻消费映射 24 | */ 25 | export const JSX_TO_STATE = new WeakMap(); 26 | -------------------------------------------------------------------------------- /packages/plugin/src/shared/icons/line-height.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | 3 | export const LineHeightIcon: FC = () => ( 4 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /blocks/x-json/src/types/block.ts: -------------------------------------------------------------------------------- 1 | import type { Op as JSONOp } from "@block-kit/ot-json"; 2 | 3 | import type { BasicBlock, BlockModule } from "./interface"; 4 | 5 | /** Block 数据模块类型 */ 6 | export type BlockModuleField = BlockModule[keyof BlockModule]; 7 | 8 | /** Block 数据类型字段 */ 9 | export type BlockDataField = BasicBlock & BlockModuleField; 10 | 11 | /** Block 类型 */ 12 | export type Block = { 13 | id: string; 14 | version: number; 15 | data: BlockDataField; 16 | }; 17 | 18 | /** Block 数据集合 */ 19 | export type Blocks = Record; 20 | 21 | /** Block 数据集合 [Alias Blocks] */ 22 | export type BlockMap = Blocks; 23 | 24 | /** Block 变更 */ 25 | export type BlockChange = JSONOp[]; 26 | 27 | /** Blocks 变更 */ 28 | export type BlocksChange = Record; 29 | -------------------------------------------------------------------------------- /packages/core/src/rect/utils/convert.ts: -------------------------------------------------------------------------------- 1 | import type { Rect } from "../types"; 2 | 3 | /** 4 | * 将 DOMRect 转换为 Rect 5 | * @param rect 6 | */ 7 | export const fromDOMRect = (rect: DOMRect): Rect => { 8 | return { 9 | top: rect.top, 10 | bottom: rect.bottom, 11 | left: rect.left, 12 | right: rect.right, 13 | height: rect.height, 14 | width: rect.width, 15 | }; 16 | }; 17 | 18 | /** 19 | * 取 Rect 相对位置 20 | * @param rect 21 | * @param base 22 | */ 23 | export const relativeTo = (rect: Rect, base: Rect): Rect => { 24 | return { 25 | top: rect.top - base.top, 26 | bottom: rect.bottom - base.top, 27 | left: rect.left - base.left, 28 | right: rect.right - base.left, 29 | height: rect.height, 30 | width: rect.width, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/modules/inline-code.tsx: -------------------------------------------------------------------------------- 1 | import { IconCode } from "@arco-design/web-react/icon"; 2 | import { cs, NIL, TRULY } from "@block-kit/utils"; 3 | import type { FC } from "react"; 4 | 5 | import { INLINE_CODE_KEY } from "../../inline-code/types"; 6 | import { useToolbarContext } from "../context/provider"; 7 | 8 | export const InlineCode: FC = () => { 9 | const { keys, refreshMarks, editor } = useToolbarContext(); 10 | 11 | return ( 12 |
{ 15 | editor.command.exec(INLINE_CODE_KEY, { value: keys[INLINE_CODE_KEY] ? NIL : TRULY }); 16 | refreshMarks(); 17 | }} 18 | > 19 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/modules/underline.tsx: -------------------------------------------------------------------------------- 1 | import { IconUnderline } from "@arco-design/web-react/icon"; 2 | import { cs, NIL, TRULY } from "@block-kit/utils"; 3 | import type { FC } from "react"; 4 | 5 | import { UNDERLINE_KEY } from "../../underline/types"; 6 | import { useToolbarContext } from "../context/provider"; 7 | 8 | export const Underline: FC = () => { 9 | const { keys, refreshMarks, editor } = useToolbarContext(); 10 | 11 | return ( 12 |
{ 15 | editor.command.exec(UNDERLINE_KEY, { value: keys[UNDERLINE_KEY] ? NIL : TRULY }); 16 | refreshMarks(); 17 | }} 18 | > 19 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/react/src/model/portal.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import type { O } from "@block-kit/utils/dist/es/types"; 3 | import type { FC, ReactPortal } from "react"; 4 | import React, { Fragment, useState } from "react"; 5 | 6 | import { EDITOR_TO_PORTAL } from "../utils/mount-dom"; 7 | 8 | const PortalView: FC<{ editor: Editor }> = props => { 9 | const [portals, setPortals] = useState>({}); 10 | 11 | EDITOR_TO_PORTAL.set(props.editor, setPortals); 12 | 13 | return ( 14 | 15 | {Object.entries(portals).map(([key, node]) => ( 16 | {node} 17 | ))} 18 | 19 | ); 20 | }; 21 | 22 | /** Portal Model */ 23 | export const PortalModel = React.memo(PortalView); 24 | -------------------------------------------------------------------------------- /packages/utils/src/native.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 异步延迟 [非精准] 3 | * @param ms 毫秒 4 | */ 5 | export const sleep = (ms: number): Promise => { 6 | return new Promise(resolve => { 7 | let id: NodeJS.Timeout | null = null; 8 | id = setTimeout(() => resolve(id!), ms); 9 | }); 10 | }; 11 | 12 | /** 13 | * Go-Style 异步异常处理 14 | * @param promise 15 | */ 16 | export const to = ( 17 | promise: Promise 18 | ): Promise<[null, T] | [U, undefined]> => { 19 | return promise 20 | .then<[null, T]>((data: T) => [null, data]) 21 | .catch<[U, undefined]>((error: U) => { 22 | if (error instanceof Error === false) { 23 | return [new Error(String(error)) as U, undefined]; 24 | } 25 | return [error, undefined]; 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/utils/test/literal.test.ts: -------------------------------------------------------------------------------- 1 | import { Literal } from "../src/literal"; 2 | 3 | describe("literal", () => { 4 | it("escapeHtml", () => { 5 | const result = Literal.escapeHtml(""); 6 | expect(result).toEqual("<script>alert('xss')</script>"); 7 | }); 8 | 9 | it("compare", () => { 10 | expect(Literal.compare("a", "b")).toEqual(-1); 11 | expect(Literal.compare("a", "a")).toEqual(0); 12 | expect(Literal.compare("b", "a")).toEqual(1); 13 | expect(Literal.compare("a", "ab")).toBeLessThan(0); 14 | expect(Literal.compare("ab", "a")).toBeGreaterThan(0); 15 | }); 16 | 17 | it("numberify", () => { 18 | const result = Literal.numberify("abc"); 19 | expect(result).toEqual(294); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/vue/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import type { P } from "@block-kit/utils/dist/es/types"; 3 | import type { ComponentOptions, EmitsOptions, RenderFunction, SetupContext, SlotsType } from "vue"; 4 | 5 | export type FC< 6 | Props extends Record, 7 | E extends EmitsOptions = {}, 8 | S extends SlotsType = {} 9 | > = (props: Props, ctx: SetupContext) => RenderFunction | Promise; 10 | 11 | export type FCOptions< 12 | Props extends Record, 13 | E extends EmitsOptions = {}, 14 | EE extends string = string, 15 | S extends SlotsType = {} 16 | > = Pick & { 17 | props?: (keyof Props)[]; 18 | emits?: E | EE[]; 19 | slots?: S; 20 | }; 21 | -------------------------------------------------------------------------------- /blocks/x-react/src/model/portal.tsx: -------------------------------------------------------------------------------- 1 | import type { O } from "@block-kit/utils/dist/es/types"; 2 | import type { BlockEditor } from "@block-kit/x-core"; 3 | import type { FC, ReactPortal } from "react"; 4 | import React, { Fragment, useState } from "react"; 5 | 6 | import { EDITOR_TO_PORTAL } from "../utils/mount-dom"; 7 | 8 | const PortalView: FC<{ editor: BlockEditor }> = props => { 9 | const [portals, setPortals] = useState>({}); 10 | 11 | EDITOR_TO_PORTAL.set(props.editor, setPortals); 12 | 13 | return ( 14 | 15 | {Object.entries(portals).map(([key, node]) => ( 16 | {node} 17 | ))} 18 | 19 | ); 20 | }; 21 | 22 | /** Portal Model */ 23 | export const PortalModel = React.memo(PortalView); 24 | -------------------------------------------------------------------------------- /examples/variable/gulpfile.js: -------------------------------------------------------------------------------- 1 | const { src, dest, watch } = require("gulp"); 2 | const cleanCSS = require("gulp-clean-css"); 3 | const concat = require("gulp-concat"); 4 | const scss = require("gulp-sass")(require("sass")); 5 | 6 | function build() { 7 | try { 8 | return src(["./src/index.scss"]) 9 | .pipe(scss()) 10 | .pipe(concat("index.css")) 11 | .pipe(cleanCSS({ compatibility: "*" })) 12 | .pipe(dest("dist/style")); 13 | } catch (error) { 14 | console.log("Compile Error :", error); 15 | } 16 | } 17 | 18 | function watchFiles() { 19 | build(); 20 | watch("./src/**/*.scss", build); 21 | } 22 | 23 | const { argv } = require("process"); 24 | const isWatchMode = argv.includes("--watch") || argv.includes("-w"); 25 | exports.default = isWatchMode ? watchFiles : build; 26 | -------------------------------------------------------------------------------- /packages/plugin/src/shared/icons/font-size.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | 3 | export const FontSizeIcon: FC = () => ( 4 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["stylelint-config-standard", "stylelint-config-sass-guidelines"], 3 | ignoreFiles: [ 4 | "**/node_modules/**/*.*", 5 | "**/dist/**/*.*", 6 | "**/build/**/*.*", 7 | "**/coverage/**/*.*", 8 | "**/public/**/*.*", 9 | ], 10 | rules: { 11 | "no-descending-specificity": null, 12 | "color-function-notation": null, 13 | "alpha-value-notation": null, 14 | "no-empty-source": null, 15 | "max-nesting-depth": 6, 16 | "selector-max-compound-selectors": 6, 17 | "selector-class-pattern": "^[a-z][a-zA-Z0-9_-]+$", 18 | "selector-id-pattern": "^[a-z][a-zA-Z0-9_-]+$", 19 | "selector-pseudo-class-no-unknown": [ 20 | true, 21 | { 22 | "ignorePseudoClasses": ["global"], 23 | }, 24 | ], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/react/src/plugin/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { LeafContext, LeafState, LineContext, LineState } from "@block-kit/core"; 2 | 3 | /** 4 | * 包装行状态 5 | */ 6 | export type ReactWrapLineContext = { 7 | lineState: LineState; 8 | children?: React.ReactNode; 9 | }; 10 | 11 | /** 12 | * 包装叶子状态 13 | */ 14 | export type ReactWrapLeafContext = { 15 | leafState: LeafState; 16 | children?: React.ReactNode; 17 | }; 18 | 19 | /** 20 | * 行状态 21 | */ 22 | export interface ReactLineContext extends LineContext { 23 | children?: React.ReactNode; 24 | } 25 | 26 | /** 27 | * 叶子状态 28 | */ 29 | export interface ReactLeafContext extends LeafContext { 30 | children?: React.ReactNode; 31 | } 32 | 33 | /** 34 | * 包装类型 35 | */ 36 | export const WRAP_TYPE = { 37 | LINE: "wrapLine", 38 | LEAF: "wrapLeaf", 39 | } as const; 40 | -------------------------------------------------------------------------------- /packages/vue/src/plugin/index.ts: -------------------------------------------------------------------------------- 1 | import { CorePlugin } from "@block-kit/core"; 2 | 3 | import type { 4 | VueLeafContext, 5 | VueLineContext, 6 | VueNode, 7 | VueWrapLeafContext, 8 | VueWrapLineContext, 9 | } from "./types"; 10 | 11 | export abstract class EditorPlugin extends CorePlugin { 12 | /** 13 | * 渲染包装行节点 14 | * - 调度优先级值越大 DOM 结构在越外层 15 | */ 16 | public wrapLine?(children: VueWrapLineContext): VueNode; 17 | /** 18 | * 渲染包装叶子节点 19 | * - 调度优先级值越大 DOM 结构在越外层 20 | */ 21 | public wrapLeaf?(context: VueWrapLeafContext): VueNode; 22 | /** 23 | * 渲染行节点 24 | * - 调度优先级值越大 DOM 结构在越外层 25 | */ 26 | public renderLine?(context: VueLineContext): VueNode; 27 | /** 28 | * 渲染块级子节点 29 | * - 调度优先级值越大 DOM 结构在越外层 30 | */ 31 | public renderLeaf?(context: VueLeafContext): VueNode; 32 | } 33 | -------------------------------------------------------------------------------- /packages/delta/src/attributes/invert.ts: -------------------------------------------------------------------------------- 1 | import type { AttributeMap } from "./interface"; 2 | 3 | /** 4 | * 反转属性 5 | * - 以 base 为基础, 生成 attr 的反转属性, 即 attr 的目标是 base 6 | * - 如果 base 参数为空, 则表示 attr 是新增的属性, 反转结果均为删除属性 7 | * @param attr 8 | * @param base 9 | */ 10 | export const invertAttributes = ( 11 | attr: AttributeMap = {}, 12 | base: AttributeMap = {} 13 | ): AttributeMap => { 14 | const baseInverted = Object.keys(base).reduce((memo, key) => { 15 | if (base[key] !== attr[key] && attr[key] !== undefined) { 16 | memo[key] = base[key]; 17 | } 18 | return memo; 19 | }, {}); 20 | return Object.keys(attr).reduce((memo, key) => { 21 | if (attr[key] !== base[key] && base[key] === undefined) { 22 | memo[key] = ""; 23 | } 24 | return memo; 25 | }, baseInverted); 26 | }; 27 | -------------------------------------------------------------------------------- /examples/variable/src/index.ts: -------------------------------------------------------------------------------- 1 | export { EditableTextInput } from "./components/editable-input"; 2 | export { EditableInputPlugin } from "./modules/editable-plugin"; 3 | export { SelectorInputPlugin } from "./modules/selector-plugin"; 4 | export { 5 | SEL_CLS_PREFIX, 6 | SEL_KEY, 7 | SEL_VALUE_KEY, 8 | VARS_CLS_PREFIX, 9 | VARS_KEY, 10 | VARS_VALUE_KEY, 11 | } from "./utils/constant"; 12 | export type { EditablePluginOptions } from "./utils/types"; 13 | export type { EditorSchema } from "@block-kit/core"; 14 | export { Editor, EDITOR_EVENT, LOG_LEVEL } from "@block-kit/core"; 15 | export { Delta } from "@block-kit/delta"; 16 | export { Isolate } from "@block-kit/react"; 17 | export { BlockKit, Editable, EditorPlugin, useEditorStatic, useReadonly } from "@block-kit/react"; 18 | export { cs, preventNativeEvent } from "@block-kit/utils"; 19 | -------------------------------------------------------------------------------- /packages/core/test/history/remote.test.ts: -------------------------------------------------------------------------------- 1 | import { Delta } from "@block-kit/delta"; 2 | import { sleep } from "@block-kit/utils"; 3 | 4 | import { APPLY_SOURCE, Editor } from "../../src"; 5 | 6 | describe("history remote", () => { 7 | it("image upload", async () => { 8 | const editor = new Editor(); 9 | // @ts-expect-error protected readonly property 10 | editor.history.DELAY = 10; 11 | editor.state.apply(new Delta().insert(" ", { src: "blob" })); 12 | await sleep(20); 13 | editor.state.apply(new Delta().retain(1, { src: "http" }), { source: APPLY_SOURCE.REMOTE }); 14 | // @ts-expect-error protected readonly property 15 | const undoStack = editor.history.undoStack.map(it => it.delta); 16 | expect(undoStack.length).toEqual(1); 17 | expect(undoStack[0]).toEqual(new Delta().delete(1)); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/react/src/preset/text.tsx: -------------------------------------------------------------------------------- 1 | import { LEAF_STRING } from "@block-kit/core"; 2 | import { useMemoFn } from "@block-kit/utils/dist/es/hooks"; 3 | import type { FC } from "react"; 4 | 5 | export type TextProps = { 6 | children: string; 7 | onRef?: (ref: HTMLSpanElement | null) => void; 8 | }; 9 | 10 | /** 11 | * 文本节点 12 | * @param props 13 | */ 14 | export const Text: FC = props => { 15 | /** 16 | * 处理 ref 回调 17 | * - 需要保证引用不变, 否则会导致回调在 rerender 时被多次调用 null/span 状态 18 | * - https://18.react.dev/reference/react-dom/components/common#ref-callback 19 | */ 20 | const onRef = useMemoFn((dom: HTMLSpanElement | null) => { 21 | props.onRef && props.onRef(dom); 22 | }); 23 | 24 | return ( 25 | 26 | {props.children} 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/core/test/state/content.test.ts: -------------------------------------------------------------------------------- 1 | import { Delta } from "@block-kit/delta"; 2 | import { MutateDelta } from "@block-kit/delta"; 3 | 4 | import { Editor } from "../../src/editor"; 5 | 6 | describe("state content", () => { 7 | it("set content", () => { 8 | const editor = new Editor({ delta: new Delta().insert("123").insert("\n") }); 9 | editor.state.setContent(new Delta().insert("456").insert("\n")); 10 | expect(editor.state.toBlock()).toEqual(new MutateDelta().insert("456").insert("\n")); 11 | }); 12 | 13 | it("set content multi EOL", () => { 14 | const editor = new Editor({ delta: new Delta().insert("123").insert("\n") }); 15 | editor.state.setContent(new Delta().insert("456").insert("\n\n")); 16 | expect(editor.state.toBlock()).toEqual(new MutateDelta().insert("456").insertEOL().insertEOL()); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /blocks/x-json/src/utils/transform.ts: -------------------------------------------------------------------------------- 1 | import type { Op } from "@block-kit/ot-json"; 2 | import { SIDE } from "@block-kit/ot-json"; 3 | 4 | import { json } from "../modules/subtype"; 5 | 6 | /** 7 | * 批量 Op 归一化 8 | * - 归一化的操作才可以被直接应用 9 | * - 主体思路是假设 a 变更, 由其变更的影响来变换 b 操作 10 | * - 假设值为 abcd, 以 a 为基准, 变换 b/c/d, 然后以 b 为基准, 变换 c/d, 以此类推 11 | * @param ops 12 | */ 13 | export const normalizeBatchOps = (ops: Op[]) => { 14 | const copied: Op[] = ops.filter(op => op); 15 | for (let i = 0, len = copied.length; i < len; i++) { 16 | const base = copied[i]; 17 | if (!base) continue; 18 | for (let k = i + 1; k < len; k++) { 19 | const op = copied[k]; 20 | if (!op) continue; 21 | const nextOp = json.transform(op, base, SIDE.RIGHT); 22 | copied[k] = nextOp as Op; 23 | } 24 | } 25 | return copied.filter(Boolean); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/plugin/src/mention/styles/suggest.scss: -------------------------------------------------------------------------------- 1 | @import '../../shared/styles/variable'; 2 | 3 | .block-kit-suggest-panel { 4 | @include no-scrollbar; 5 | @include frame-region; 6 | 7 | border-radius: 5px; 8 | box-sizing: border-box; 9 | color: var(--color-text-1); 10 | font-size: 13px; 11 | overflow-y: auto; 12 | overscroll-behavior: contain; 13 | padding: 5px 7px; 14 | position: absolute; 15 | z-index: 999; 16 | 17 | .block-kit-suggest-item { 18 | border-radius: 4px; 19 | overflow: hidden; 20 | padding: 3px 5px; 21 | text-overflow: ellipsis; 22 | white-space: nowrap; 23 | 24 | &.active { 25 | background-color: var(--color-fill-2); 26 | } 27 | } 28 | 29 | .block-kit-suggest-empty { 30 | color: var(--color-text-3); 31 | margin-top: 10px; 32 | text-align: center; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/modules/order-list.tsx: -------------------------------------------------------------------------------- 1 | import { IconOrderedList } from "@arco-design/web-react/icon"; 2 | import { cs, TRULY } from "@block-kit/utils"; 3 | import type { FC } from "react"; 4 | 5 | import { ORDER_LIST_KEY } from "../../order-list/types"; 6 | import { isOrderList } from "../../order-list/utils/is"; 7 | import { useToolbarContext } from "../context/provider"; 8 | 9 | export const OrderList: FC = () => { 10 | const { keys, refreshMarks, editor } = useToolbarContext(); 11 | 12 | const isOrder = isOrderList(keys); 13 | 14 | return ( 15 |
{ 18 | editor.command.exec(ORDER_LIST_KEY, { value: TRULY }); 19 | refreshMarks(); 20 | }} 21 | > 22 | 23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-composing.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import { EDITOR_EVENT } from "@block-kit/core"; 3 | import { useEffect, useState } from "react"; 4 | 5 | export const useComposing = (editor: Editor) => { 6 | const [isComposing, setIsComposing] = useState(false); 7 | 8 | useEffect(() => { 9 | const onCompositionStart = () => setIsComposing(true); 10 | const onCompositionEnd = () => setIsComposing(false); 11 | editor.event.on(EDITOR_EVENT.COMPOSITION_START, onCompositionStart); 12 | editor.event.on(EDITOR_EVENT.COMPOSITION_END, onCompositionEnd); 13 | return () => { 14 | editor.event.off(EDITOR_EVENT.COMPOSITION_START, onCompositionStart); 15 | editor.event.off(EDITOR_EVENT.COMPOSITION_END, onCompositionEnd); 16 | }; 17 | }, [editor]); 18 | 19 | return { isComposing }; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/react/src/plugin/index.ts: -------------------------------------------------------------------------------- 1 | import { CorePlugin } from "@block-kit/core"; 2 | 3 | import type { 4 | ReactLeafContext, 5 | ReactLineContext, 6 | ReactWrapLeafContext, 7 | ReactWrapLineContext, 8 | } from "./types"; 9 | 10 | export abstract class EditorPlugin extends CorePlugin { 11 | /** 12 | * 渲染包装行节点 13 | * - 调度优先级值越大 DOM 结构在越外层 14 | */ 15 | public wrapLine?(children: ReactWrapLineContext): React.ReactNode; 16 | /** 17 | * 渲染包装叶子节点 18 | * - 调度优先级值越大 DOM 结构在越外层 19 | */ 20 | public wrapLeaf?(context: ReactWrapLeafContext): React.ReactNode; 21 | /** 22 | * 渲染行节点 23 | * - 调度优先级值越大 DOM 结构在越外层 24 | */ 25 | public renderLine?(context: ReactLineContext): React.ReactNode; 26 | /** 27 | * 渲染块级子节点 28 | * - 调度优先级值越大 DOM 结构在越外层 29 | */ 30 | public renderLeaf?(context: ReactLeafContext): React.ReactNode; 31 | } 32 | -------------------------------------------------------------------------------- /packages/utils/test/native.test.ts: -------------------------------------------------------------------------------- 1 | import { sleep, to } from "../src/native"; 2 | 3 | describe("native", () => { 4 | it("sleep", async () => { 5 | const ms = 100; 6 | const start = Date.now(); 7 | await sleep(ms); 8 | expect(Date.now() - start).toBeGreaterThanOrEqual(ms); 9 | }); 10 | 11 | it("to", async () => { 12 | const [err, res] = await to(Promise.resolve(1)); 13 | expect(err).toBeNull(); 14 | expect(res).toBe(1); 15 | }); 16 | 17 | it("to error", async () => { 18 | const [err, res] = await to(Promise.reject("error")); 19 | expect(err).toEqual(new Error("error")); 20 | expect(res).toBeUndefined(); 21 | }); 22 | 23 | it("to undefined", async () => { 24 | const [err, res] = await to(Promise.reject()); 25 | expect(err).toBeInstanceOf(Error); 26 | expect(res).toBeUndefined(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/vue/src/utils/wrapper.ts: -------------------------------------------------------------------------------- 1 | import type { VNode } from "vue"; 2 | 3 | import { JSX_TO_STATE, STATE_TO_SYMBOL } from "./weak-map"; 4 | 5 | /** 6 | * 根据属性获取唯一标识值 7 | * @param keys 8 | * @param element 9 | */ 10 | export const getWrapSymbol = (keys: string[], el: VNode | undefined): string | null => { 11 | if (!el) return null; 12 | const state = JSX_TO_STATE.get(el); 13 | const cache = state && STATE_TO_SYMBOL.get(state); 14 | if (cache || !state) return cache || null; 15 | const attrs = state.op.attributes; 16 | if (!attrs || !Object.keys(attrs).length || !keys.length) { 17 | return null; 18 | } 19 | const suite: string[] = []; 20 | for (const key of keys) { 21 | attrs[key] && suite.push(`${key}${attrs[key]}`); 22 | } 23 | const symbol = suite.join(""); 24 | STATE_TO_SYMBOL.set(state, symbol); 25 | return symbol; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/modules/bullet-list.tsx: -------------------------------------------------------------------------------- 1 | import { IconUnorderedList } from "@arco-design/web-react/icon"; 2 | import { cs, TRULY } from "@block-kit/utils"; 3 | import type { FC } from "react"; 4 | 5 | import { BULLET_LIST_KEY } from "../../bullet-list/types"; 6 | import { isBulletList } from "../../bullet-list/utils/is"; 7 | import { useToolbarContext } from "../context/provider"; 8 | 9 | export const BulletList: FC = () => { 10 | const { keys, refreshMarks, editor } = useToolbarContext(); 11 | 12 | const isBullet = isBulletList(keys); 13 | 14 | return ( 15 |
{ 18 | editor.command.exec(BULLET_LIST_KEY, { value: TRULY }); 19 | refreshMarks(); 20 | }} 21 | > 22 | 23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@block-kit/tools", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "sideEffects": false, 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": {}, 12 | "keywords": [], 13 | "author": "", 14 | "license": "MIT", 15 | "dependencies": { 16 | "docx": "7.8.2", 17 | "image-size": "1.1.1", 18 | "pdf-lib": "1.17.1", 19 | "pdfmake": "0.2.10", 20 | "quill-delta": "4.2.2" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "7.20.12", 24 | "@babel/preset-env": "7.20.2", 25 | "@babel/preset-typescript": "7.18.6", 26 | "@types/jest": "29.4.0", 27 | "@types/pdfkit": "0.13.4", 28 | "@types/pdfmake": "0.2.9", 29 | "babel-jest": "29.4.1", 30 | "jest": "29.4.1", 31 | "ts-jest": "29.0.5" 32 | } 33 | } -------------------------------------------------------------------------------- /blocks/x-core/src/selection/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { O } from "@block-kit/utils/dist/es/types"; 2 | 3 | import type { POINT_TYPE } from "../utils/constant"; 4 | 5 | /** 选区点类型 */ 6 | export type BlockType = O.Values; 7 | 8 | /** 块级选区点 */ 9 | export type BlockPoint = { 10 | id: string; 11 | type: typeof POINT_TYPE.BLOCK; 12 | }; 13 | 14 | /** 文本选区点 */ 15 | export type TextPoint = { 16 | id: string; 17 | type: typeof POINT_TYPE.TEXT; 18 | offset: number; 19 | }; 20 | 21 | /** 选区节点类型 */ 22 | export type RangePoint = BlockPoint | TextPoint; 23 | 24 | /** 块级元素类型 */ 25 | export type BlockEntry = BlockPoint; 26 | 27 | /** 文本元素类型 */ 28 | export type TextEntry = { 29 | id: string; 30 | type: typeof POINT_TYPE.TEXT; 31 | start: number; 32 | len: number; 33 | }; 34 | 35 | /** 选区元素类型 */ 36 | export type RangeEntry = BlockEntry | TextEntry; 37 | -------------------------------------------------------------------------------- /packages/delta/src/attributes/transform.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from "@block-kit/utils"; 2 | 3 | import type { AttributeMap } from "./interface"; 4 | 5 | /** 6 | * 转换属性 7 | * @param a 8 | * @param b 9 | * @param priority 10 | */ 11 | export const transformAttributes = ( 12 | a: AttributeMap | undefined, 13 | b: AttributeMap | undefined, 14 | priority = false 15 | ): AttributeMap | undefined => { 16 | if (!isObject(a)) return b; 17 | if (!isObject(b)) return undefined; 18 | // B simply overwrites us without priority 19 | if (!priority) return b; 20 | const attributes = Object.keys(b).reduce((attrs, key) => { 21 | if (a[key] === undefined) { 22 | // Null is a valid value 23 | attrs[key] = b[key]; 24 | } 25 | return attrs; 26 | }, {}); 27 | return Object.keys(attributes).length > 0 ? attributes : undefined; 28 | }; 29 | -------------------------------------------------------------------------------- /examples/variable/README.md: -------------------------------------------------------------------------------- 1 | # Variables 2 | 3 |

4 | GitHub 5 | 6 | DEMO 7 | 8 | Usage 9 | 10 | CodeSandbox 11 |

12 | 13 | ## Development 14 | 15 | ```bash 16 | pnpm install 17 | pnpm run --filter "@block-kit/variable^..." build 18 | npm run dev 19 | ``` 20 | 21 | ```bash 22 | # http://localhost:8080/variable.html 23 | pnpm run --filter @block-kit/website dev 24 | ``` 25 | 26 | ## Example 27 | 28 | image 29 | -------------------------------------------------------------------------------- /packages/plugin/src/shared/icons/justify.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | 3 | export const JustifyIcon: FC = () => ( 4 | 13 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /blocks/x-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { BlockEditor } from "./editor/index"; 2 | export type { EditorOptions } from "./editor/types"; 3 | export { EDITOR_EVENT } from "./event/bus"; 4 | export type { InternalEvent, Listener } from "./event/bus/types"; 5 | export { 6 | X_BLOCK_ID_KEY, 7 | X_BLOCK_KEY, 8 | X_BLOCK_TYPE_KEY, 9 | X_SELECTION_KEY, 10 | X_TEXT_BLOCK_KEY, 11 | X_ZERO_KEY, 12 | } from "./model/types"; 13 | export { STATE_TO_RENDER } from "./model/utils/weak-map"; 14 | export { Entry } from "./selection/modules/entry"; 15 | export { Point } from "./selection/modules/point"; 16 | export { Range } from "./selection/modules/range"; 17 | export { toModelPoint, toModelRange } from "./selection/utils/model"; 18 | export { normalizeModelRange } from "./selection/utils/normalize"; 19 | export { BlockState } from "./state/modules/block-state"; 20 | export { EDITOR_STATE } from "./state/types"; 21 | -------------------------------------------------------------------------------- /packages/react/src/model/eol.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor, LeafState } from "@block-kit/core"; 2 | import { LEAF_KEY } from "@block-kit/core"; 3 | import type { FC } from "react"; 4 | import React from "react"; 5 | 6 | import { ZeroSpace } from "../preset/zero"; 7 | import { LEAF_TO_ZERO_TEXT } from "../utils/weak-map"; 8 | 9 | const EOLView: FC<{ 10 | editor: Editor; 11 | leafState: LeafState; 12 | }> = props => { 13 | const { editor, leafState } = props; 14 | 15 | const setModel = (ref: HTMLSpanElement | null) => { 16 | if (ref) { 17 | editor.model.setLeafModel(ref, leafState); 18 | } 19 | }; 20 | 21 | return ( 22 | 23 | el && LEAF_TO_ZERO_TEXT.set(leafState, el)} /> 24 | 25 | ); 26 | }; 27 | 28 | /** EOL Model */ 29 | export const EOLModel = React.memo(EOLView); 30 | -------------------------------------------------------------------------------- /packages/plugin/src/image/styles/wrapper.scss: -------------------------------------------------------------------------------- 1 | .block-kit-image-preview { 2 | cursor: zoom-in; 3 | } 4 | 5 | .block-kit-image-resider { 6 | position: absolute; 7 | z-index: 1; 8 | 9 | &::after { 10 | background-color: var(--color-bg-1); 11 | border: 1px solid rgb(var(--arcoblue-6)); 12 | border-radius: 10px; 13 | content: ''; 14 | display: block; 15 | height: 10px; 16 | width: 10px; 17 | z-index: 1; 18 | } 19 | 20 | &[data-type='lt'] { 21 | cursor: nwse-resize; 22 | left: -5px; 23 | top: -5px; 24 | } 25 | 26 | &[data-type='rt'] { 27 | cursor: nesw-resize; 28 | right: -5px; 29 | top: -5px; 30 | } 31 | 32 | &[data-type='lb'] { 33 | bottom: -5px; 34 | cursor: nesw-resize; 35 | left: -5px; 36 | } 37 | 38 | &[data-type='rb'] { 39 | bottom: -5px; 40 | cursor: nwse-resize; 41 | right: -5px; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/plugin/types/context.ts: -------------------------------------------------------------------------------- 1 | import type { AttributeMap } from "@block-kit/delta"; 2 | import type { Op } from "@block-kit/delta"; 3 | import type { P } from "@block-kit/utils/dist/es/types"; 4 | import type { Properties } from "csstype"; 5 | 6 | import type { LeafState } from "../../state/modules/leaf-state"; 7 | import type { LineState } from "../../state/modules/line-state"; 8 | 9 | /** 10 | * 行状态 11 | */ 12 | export type LineContext = { 13 | classList: string[]; 14 | lineState: LineState; 15 | attributes: AttributeMap; 16 | style: Properties; 17 | children?: P.Any; 18 | }; 19 | 20 | /** 21 | * 叶子状态 22 | */ 23 | export type LeafContext = { 24 | op: Op; 25 | key?: string; 26 | classList: string[]; 27 | lineState: LineState; 28 | leafState: LeafState; 29 | attributes?: AttributeMap; 30 | style: Properties; 31 | children?: P.Any; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/core/test/plugin/inject.test.ts: -------------------------------------------------------------------------------- 1 | import { CorePlugin, Editor } from "../../src/index"; 2 | 3 | describe("plugin inject", () => { 4 | class Plugin extends CorePlugin { 5 | public key = "plugin"; 6 | public destroy(): void {} 7 | public match = () => true; 8 | } 9 | 10 | it("multi editor instance", () => { 11 | const editor1 = new Editor(); 12 | const editor2 = new Editor(); 13 | editor1.plugin.register([new Plugin()]); 14 | editor2.plugin.register([new Plugin()]); 15 | expect(CorePlugin.editor).toBeNull(); 16 | // @ts-expect-error editor 17 | const pluginEditor1 = editor1.plugin.current[0].editor; 18 | // @ts-expect-error editor 19 | const pluginEditor2 = editor2.plugin.current[0].editor; 20 | expect(pluginEditor1).toBe(editor1); 21 | expect(pluginEditor2).toBe(editor2); 22 | expect(pluginEditor1).not.toBe(pluginEditor2); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/vue/src/plugin/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { LeafContext, LeafState, LineContext, LineState } from "@block-kit/core"; 2 | import type { VNode } from "vue"; 3 | 4 | export type VueNode = VNode | VNode[] | string | number | boolean | undefined; 5 | 6 | /** 7 | * 包装行状态 8 | */ 9 | export type VueWrapLineContext = { 10 | lineState: LineState; 11 | children?: VueNode; 12 | }; 13 | 14 | /** 15 | * 包装叶子状态 16 | */ 17 | export type VueWrapLeafContext = { 18 | leafState: LeafState; 19 | children?: VueNode; 20 | }; 21 | 22 | /** 23 | * 行状态 24 | */ 25 | export interface VueLineContext extends LineContext { 26 | children?: VueNode; 27 | } 28 | 29 | /** 30 | * 叶子状态 31 | */ 32 | export interface VueLeafContext extends LeafContext { 33 | children?: VueNode; 34 | } 35 | 36 | /** 37 | * 包装类型 38 | */ 39 | export const WRAP_TYPE = { 40 | LINE: "wrapLine", 41 | LEAF: "wrapLeaf", 42 | } as const; 43 | -------------------------------------------------------------------------------- /examples/website/public/vue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Block Kit 8 | 9 | 10 | <% if (process.env.NODE_ENV === "production") { %> 11 | 17 | <% } else { %> 18 | 24 | <% } %> 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/vue/src/hooks/use-editor.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import { inject } from "vue"; 3 | 4 | export const BlockKitContext = Symbol(); 5 | 6 | export const useEditorStatic = () => { 7 | const editor = inject(BlockKitContext); 8 | 9 | if (!editor) { 10 | throw new Error("UseEditor must be used within a EditorContext"); 11 | } 12 | 13 | return { 14 | editor, 15 | rect: editor.rect, 16 | state: editor.state, 17 | event: editor.event, 18 | input: editor.input, 19 | model: editor.model, 20 | plugin: editor.plugin, 21 | schema: editor.schema, 22 | logger: editor.logger, 23 | lookup: editor.lookup, 24 | tracer: editor.tracer, 25 | command: editor.command, 26 | history: editor.history, 27 | perform: editor.perform, 28 | clipboard: editor.clipboard, 29 | selection: editor.selection, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/utils/src/decorator.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from "./is"; 2 | 3 | // ExperimentalDecorators 4 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html 5 | 6 | /** 7 | * Bind 事件绑定装饰器 8 | * @param {T} _ 9 | * @param {string} key 10 | * @param {PropertyDescriptor} descriptor 11 | */ 12 | export function Bind(_: T, key: string, descriptor: PropertyDescriptor): PropertyDescriptor { 13 | const originalMethod = descriptor.value; 14 | if (!isFunction(originalMethod)) { 15 | throw new TypeError(`${originalMethod} is not a function`); 16 | } 17 | 18 | return { 19 | configurable: true, 20 | get() { 21 | const boundFunction = originalMethod.bind(this); 22 | Object.defineProperty(this, key, { 23 | value: boundFunction, 24 | configurable: true, 25 | enumerable: false, 26 | }); 27 | return boundFunction; 28 | }, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /blocks/x-react/src/hooks/use-composing.ts: -------------------------------------------------------------------------------- 1 | import { EDITOR_EVENT } from "@block-kit/core"; 2 | import type { BlockEditor } from "@block-kit/x-core"; 3 | import { useEffect, useState } from "react"; 4 | 5 | export const useComposing = (editor: BlockEditor) => { 6 | const [isComposing, setIsComposing] = useState(false); 7 | 8 | useEffect(() => { 9 | const onCompositionStart = () => setIsComposing(true); 10 | const onCompositionEnd = () => setIsComposing(false); 11 | editor.event.on(EDITOR_EVENT.COMPOSITION_START, onCompositionStart); 12 | // 提前执行是为了避免在 CompositionEnd 事件触发后认为仍在组合输入状态 13 | editor.event.on(EDITOR_EVENT.COMPOSITION_END, onCompositionEnd, 90); 14 | return () => { 15 | editor.event.off(EDITOR_EVENT.COMPOSITION_START, onCompositionStart); 16 | editor.event.off(EDITOR_EVENT.COMPOSITION_END, onCompositionEnd); 17 | }; 18 | }, [editor]); 19 | 20 | return { isComposing }; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/vue/src/preset/isolate.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from "vue"; 2 | import { defineComponent, h } from "vue"; 3 | 4 | import { preventNativeEvent } from "../utils/event"; 5 | 6 | export type IsolateProps = { 7 | className?: string; 8 | style?: CSSProperties; 9 | }; 10 | 11 | /** 12 | * 独立节点嵌入 HOC 13 | * - 独立区域 完全隔离所有事件 14 | * @param props 15 | */ 16 | export const Isolate = /*#__PURE__*/ defineComponent({ 17 | name: "Isolate", 18 | props: ["className", "style"], 19 | setup: (props, { slots }) => { 20 | return () => 21 | h( 22 | "span", 23 | { 24 | class: props.className, 25 | style: { userSelect: "none", ...props.style }, 26 | contentEditable: false, 27 | onMouseDown: preventNativeEvent, 28 | onCopy: preventNativeEvent, 29 | }, 30 | slots.default ? slots.default() : void 0 31 | ); 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /packages/react/test/hooks/ref.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { act, render } from "@testing-library/react"; 3 | 4 | import { useStateRef } from "../../../utils/src/hooks"; 5 | 6 | describe("hooks ref", () => { 7 | it("state ref", async () => { 8 | let action!: readonly [number, (next: number) => void, React.MutableRefObject]; 9 | const App = () => { 10 | const _action = useStateRef(0); 11 | action = _action; 12 | return
{_action[0]}
; 13 | }; 14 | const dom = render(); 15 | const spy = jest.fn(); 16 | act(() => { 17 | dom.unmount(); 18 | const error = console.error; 19 | console.error = spy; 20 | action?.[1](10); 21 | console.error = error; 22 | }); 23 | expect(action?.[0]).toBe(0); 24 | expect(action?.[2].current).toBe(10); 25 | expect(spy).toHaveBeenCalledTimes(0); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/core/src/clipboard/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Delta, Op } from "@block-kit/delta"; 2 | import type { O } from "@block-kit/utils/dist/es/types"; 3 | 4 | export const TEXT_DOC = "application/x-block-kit"; 5 | 6 | /** Fragment(Delta) => HTML */ 7 | export type SerializeContext = { 8 | /** Op 基准 */ 9 | op: Op; 10 | /** HTML 目标 */ 11 | html: Node; 12 | }; 13 | 14 | /** Context => Clipboard */ 15 | export type CopyContext = { 16 | /** Delta 基准 */ 17 | delta: Delta; 18 | /** HTML 目标 */ 19 | html: Node; 20 | /** 额外内容 */ 21 | extra?: O.Map; 22 | }; 23 | 24 | /** HTML => Fragment(Delta) */ 25 | export type DeserializeContext = { 26 | /** Delta 目标 */ 27 | delta: Delta; 28 | /** HTML 基准 */ 29 | html: Node; 30 | /** FILE 基准 */ 31 | files?: File[]; 32 | }; 33 | 34 | /** Clipboard => Context */ 35 | export type PasteContext = { 36 | /** Delta 基准 */ 37 | delta: Delta; 38 | /** 粘贴事件 */ 39 | event: ClipboardEvent; 40 | }; 41 | -------------------------------------------------------------------------------- /packages/plugin/src/shared/utils/dom.ts: -------------------------------------------------------------------------------- 1 | import type { EventContext } from "@block-kit/utils"; 2 | 3 | /** 4 | * 阻止所有编辑器分发的事件 5 | * @param event 6 | * @param context 7 | */ 8 | export const preventContextEvent = (event: Event, context: EventContext) => { 9 | context.stop(); 10 | context.prevent(); 11 | event.preventDefault(); 12 | event.stopPropagation(); 13 | }; 14 | 15 | /** 16 | * 滚动到指定元素 17 | * @param container 18 | * @param child 19 | */ 20 | export const scrollIfNeeded = (container: HTMLDivElement, child: Element, buffer = 0) => { 21 | const rect = child.getBoundingClientRect(); 22 | const containerRect = container.getBoundingClientRect(); 23 | if (rect.bottom > containerRect.bottom) { 24 | container.scrollTop = container.scrollTop + rect.bottom - containerRect.bottom + buffer; 25 | } else if (rect.top < containerRect.top) { 26 | container.scrollTop = container.scrollTop - containerRect.top + rect.top - buffer; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /blocks/x-json/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { DeltaSubOp } from "./modules/subtype"; 2 | export { deltaType, json } from "./modules/subtype"; 3 | export type { 4 | Block, 5 | BlockChange, 6 | BlockDataField, 7 | BlockMap, 8 | BlockModuleField, 9 | Blocks, 10 | BlocksChange, 11 | } from "./types/block"; 12 | export type { BasicBlock, BlockModule } from "./types/interface"; 13 | export { isTextDeltaOp } from "./utils/is"; 14 | export { normalizeBatchOps } from "./utils/transform"; 15 | export { createBlockTreeWalker, createBlockTreeWalkerBFS } from "./utils/walker"; 16 | export type { Op as DeltaOp } from "@block-kit/delta"; 17 | export type { 18 | Op as JSONOp, 19 | ListDeleteOp, 20 | ListInsertOp, 21 | ListMoveOp, 22 | ListReplaceOp, 23 | NumberAddOp, 24 | ObjectDeleteOp, 25 | ObjectInsertOp, 26 | ObjectReplaceOp, 27 | Op, 28 | Side, 29 | Snapshot, 30 | TextOp, 31 | } from "@block-kit/ot-json"; 32 | export { cloneSnapshot } from "@block-kit/ot-json"; 33 | -------------------------------------------------------------------------------- /packages/ot-json/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { isObjectLike } from "@block-kit/utils"; 2 | import type { O } from "@block-kit/utils/dist/es/types"; 3 | 4 | export const SIDE = { 5 | LEFT: "left", 6 | RIGHT: "right", 7 | } as const; 8 | 9 | /** 10 | * 深克隆 11 | * - 仅支持 Snapshot 定义的类型 12 | * @param value Snapshot/Op 13 | * @param cache WeakSet 作用域缓存 14 | */ 15 | export const clone = (value: T, cache = new WeakSet()): T => { 16 | // 基本类型直接返回 17 | if (isObjectLike(value) === false) { 18 | return value; 19 | } 20 | // 检查是否已缓存, 如果已缓存, 直接返回原对象 21 | if (cache.has(value)) { 22 | return value as T; 23 | } 24 | // 缓存当前对象 25 | cache.add(value); 26 | // 处理数组 27 | if (Array.isArray(value)) { 28 | return value.map(it => clone(it, cache)) as T; 29 | } 30 | // 处理普通对象 31 | const clonedObj: Record = {}; 32 | for (const key of Object.keys(value)) { 33 | clonedObj[key] = clone(value[key], cache) as T; 34 | } 35 | return clonedObj as T; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/utils/src/constant.ts: -------------------------------------------------------------------------------- 1 | import { IS_MAC } from "./env"; 2 | 3 | /** 默认节点 */ 4 | export const ROOT_BLOCK = "ROOT" as const; 5 | 6 | /** 默认优先级 */ 7 | export const DEFAULT_PRIORITY = 100; 8 | 9 | /** 键盘键值 */ 10 | export const KEY_CODE = { 11 | BACKSPACE: 8, 12 | TAB: 9, 13 | ENTER: 13, 14 | SHIFT: 16, 15 | CTRL: 17, 16 | ALT: 18, 17 | ESC: 27, 18 | SPACE: 32, 19 | PAGE_UP: 33, 20 | PAGE_DOWN: 34, 21 | END: 35, 22 | HOME: 36, 23 | LEFT: 37, 24 | UP: 38, 25 | RIGHT: 39, 26 | DOWN: 40, 27 | DELETE: 46, 28 | Z: 90, 29 | Y: 89, 30 | A: 65, 31 | B: 66, 32 | I: 73, 33 | K: 75, 34 | U: 85, 35 | D2: 50, 36 | }; 37 | 38 | /** Truly */ 39 | export const TRULY = "true"; 40 | 41 | /** Falsy */ 42 | export const FALSY = "false"; 43 | 44 | /** 控制键 */ 45 | export const CTRL_KEY: "metaKey" | "ctrlKey" = IS_MAC ? "metaKey" : "ctrlKey"; 46 | 47 | /** 空函数 */ 48 | export const NOOP = () => null; 49 | 50 | /** NIL STRING */ 51 | export const NIL = ""; 52 | -------------------------------------------------------------------------------- /packages/delta/test/utils/clone.test.ts: -------------------------------------------------------------------------------- 1 | import type { BlockSetLike } from "../../src"; 2 | import { cloneBlockSetLike } from "../../src"; 3 | 4 | describe("clone", () => { 5 | const blockSet: BlockSetLike = { 6 | a: { ops: [{ insert: "123", attributes: { a: "1" } }], id: "a", type: "Z" }, 7 | b: { ops: [{ insert: "456" }], id: "b", type: "Z" }, 8 | }; 9 | const newBlockSet = cloneBlockSetLike(blockSet); 10 | 11 | it("equal object content", () => { 12 | expect(blockSet).toEqual(newBlockSet); 13 | }); 14 | 15 | it("diff object", () => { 16 | expect(blockSet === newBlockSet).toEqual(false); 17 | }); 18 | 19 | it("diff block delta", () => { 20 | expect(blockSet.a === newBlockSet.a).toEqual(false); 21 | expect(blockSet.b === newBlockSet.b).toEqual(false); 22 | }); 23 | 24 | it("diff block delta ops", () => { 25 | expect(blockSet.a.ops === newBlockSet.a.ops).toEqual(false); 26 | expect(blockSet.b.ops === newBlockSet.b.ops).toEqual(false); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/plugin/src/shared/styles/variable.scss: -------------------------------------------------------------------------------- 1 | @mixin full-screen { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | 6 | @mixin flex-center { 7 | align-items: center; 8 | display: flex; 9 | justify-content: center; 10 | } 11 | 12 | @mixin flex-align-center { 13 | align-items: center; 14 | display: flex; 15 | } 16 | 17 | @mixin flex-justify-center { 18 | display: flex; 19 | justify-content: center; 20 | } 21 | 22 | @mixin flex-justify-between { 23 | display: flex; 24 | justify-content: space-between; 25 | } 26 | 27 | @mixin border-radius-3 { 28 | border-radius: 3px; 29 | } 30 | 31 | @mixin frame-region { 32 | @include border-radius-3; 33 | 34 | background-color: var(--color-bg-1); 35 | border: 1px solid var(--color-border-2); 36 | box-shadow: 0 0 4px var(--color-border-2); 37 | } 38 | 39 | @mixin no-scrollbar { 40 | overflow: -moz-scrollbars-none; 41 | -ms-overflow-style: none; 42 | scrollbar-width: none; 43 | 44 | &::-webkit-scrollbar { 45 | display: none; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/plugin/src/order-list/view/list.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/index.scss"; 2 | 3 | import type { Editor } from "@block-kit/core"; 4 | import type { ReactLineContext } from "@block-kit/react"; 5 | import { cs, preventNativeEvent } from "@block-kit/utils"; 6 | import type { FC } from "react"; 7 | 8 | import { formatListLevel } from "../utils/format"; 9 | 10 | export const OrderListView: FC<{ 11 | context: ReactLineContext; 12 | editor: Editor; 13 | level: number; 14 | start: number; 15 | }> = props => { 16 | const { level, start, children } = props; 17 | 18 | return ( 19 |
    20 |
    25 | {formatListLevel(start, level)} 26 |
    27 |
  1. 28 | {children} 29 |
  2. 30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/react/src/model/ph.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor, LineState } from "@block-kit/core"; 2 | import { isEmptyLine, PLACEHOLDER_KEY } from "@block-kit/core"; 3 | import type { FC } from "react"; 4 | import React from "react"; 5 | 6 | import { useComposing } from "../hooks/use-composing"; 7 | 8 | /** 9 | * 占位符组件 10 | * - 抽离组件的主要目标是避免父组件的 LayoutEffect 执行 11 | */ 12 | export const Placeholder: FC<{ 13 | editor: Editor; 14 | lines: LineState[]; 15 | placeholder: React.ReactNode | undefined; 16 | }> = props => { 17 | const { isComposing } = useComposing(props.editor); 18 | 19 | return props.placeholder && 20 | !isComposing && 21 | props.lines.length === 1 && 22 | isEmptyLine(props.lines[0], true) ? ( 23 |
32 | {props.placeholder} 33 |
34 | ) : null; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/vue/src/model/eol.ts: -------------------------------------------------------------------------------- 1 | import type { Editor, LeafState } from "@block-kit/core"; 2 | import { LEAF_KEY } from "@block-kit/core"; 3 | import type { P } from "@block-kit/utils/dist/es/types"; 4 | import { defineComponent, h } from "vue"; 5 | 6 | import { ZeroSpace } from "../preset/zero"; 7 | 8 | export type EOLModelProps = { 9 | editor: Editor; 10 | leafState: LeafState; 11 | }; 12 | 13 | /** 14 | * EOL Model 15 | * @param props 16 | */ 17 | export const EOLModel = /*#__PURE__*/ defineComponent({ 18 | name: "EOLModel", 19 | props: ["editor", "leafState"], 20 | setup: props => { 21 | const setModel = (dom: P.Any) => { 22 | if (dom instanceof HTMLSpanElement) { 23 | props.editor.model.setLeafModel(dom, props.leafState); 24 | } 25 | }; 26 | 27 | return () => 28 | h( 29 | "span", 30 | { 31 | ref: setModel, 32 | [LEAF_KEY]: true, 33 | }, 34 | [h(ZeroSpace, { enter: true })] 35 | ); 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /packages/core/src/command/index.ts: -------------------------------------------------------------------------------- 1 | import type { CMDFunc, CMDPayload, EditorCMD } from "./types"; 2 | 3 | export class Command { 4 | /** 命令 Map */ 5 | protected commands: EditorCMD; 6 | 7 | /** 8 | * 构造函数 9 | */ 10 | constructor() { 11 | this.commands = {}; 12 | } 13 | 14 | /** 15 | * 销毁模块 16 | */ 17 | public destroy() { 18 | this.commands = {}; 19 | } 20 | 21 | /** 22 | * 获取所有命令 23 | */ 24 | public get() { 25 | return this.commands; 26 | } 27 | 28 | /** 29 | * 注册命令 30 | * @param key 31 | * @param fn 32 | */ 33 | public register(key: string, fn: CMDFunc) { 34 | this.commands[key] = fn; 35 | } 36 | 37 | /** 38 | * 执行命令 39 | * @param key 40 | * @param data 41 | */ 42 | public exec(key: string, data: CMDPayload) { 43 | return this.commands[key] && this.commands[key](data); 44 | } 45 | 46 | /** 47 | * 卸载命令 48 | * @param key 49 | */ 50 | public unregister(key: string) { 51 | delete this.commands[key]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/plugin/src/link/view/a.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/index.scss"; 2 | 3 | import type { AttributeMap } from "@block-kit/delta"; 4 | import { useReadonly } from "@block-kit/react"; 5 | import { CTRL_KEY } from "@block-kit/utils"; 6 | import type { FC } from "react"; 7 | 8 | import { LINK_BLANK_KEY, LINK_KEY } from "../types"; 9 | 10 | export const A: FC<{ 11 | attrs: AttributeMap; 12 | }> = props => { 13 | const { attrs } = props; 14 | const { readonly } = useReadonly(); 15 | 16 | const href = attrs[LINK_KEY]; 17 | const target = attrs[LINK_BLANK_KEY] ? "_blank" : "_self"; 18 | 19 | const onClick = (e: React.MouseEvent) => { 20 | e[CTRL_KEY as "ctrlKey"] && window.open(href, "_blank"); 21 | e.preventDefault(); 22 | }; 23 | 24 | return ( 25 | 32 | {props.children} 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/core/src/model/types/index.ts: -------------------------------------------------------------------------------- 1 | // Editor 2 | export const EDITOR_KEY = "data-block-kit-editor"; 3 | export const EDITABLE = "contenteditable"; 4 | 5 | // Line 6 | export const NODE_KEY = "data-node"; 7 | export const PLACEHOLDER_KEY = "data-placeholder"; 8 | 9 | // Leaf 10 | export const LEAF_KEY = "data-leaf"; 11 | export const LEAF_STRING = "data-string"; 12 | export const VOID_KEY = "data-void"; 13 | export const VOID_LEN_KEY = "data-void-len"; 14 | 15 | // Block 16 | export const BLOCK_KEY = "data-block"; 17 | export const BLOCK_ID_KEY = "data-block-id"; 18 | export const ISOLATED_KEY = "data-isolated"; 19 | 20 | // Space 21 | export const ZERO_ENTER_KEY = "data-zero-enter"; 22 | export const ZERO_SPACE_KEY = "data-zero-space"; 23 | export const ZERO_VOID_KEY = "data-zero-void"; 24 | export const ZERO_EMBED_KEY = "data-zero-embed"; 25 | export const ZERO_SYMBOL = "\u200B"; 26 | export const ZERO_NO_BREAK_SYMBOL = "\uFEFF"; 27 | 28 | // Keywords 29 | export const CLIENT_KEY = "_client_"; 30 | export const REF_KEY = "_ref_"; 31 | -------------------------------------------------------------------------------- /packages/plugin/src/shared/icons/font-color.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | 3 | export const FontColorIcon: FC = () => ( 4 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /packages/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | export { BlockKitContext, useEditorStatic } from "./hooks/use-editor"; 2 | export { ReadonlyContext, useReadonly } from "./hooks/use-readonly"; 3 | export { BlockModel } from "./model/block"; 4 | export { EOLModel } from "./model/eol"; 5 | export { LeafModel } from "./model/leaf"; 6 | export { LineModel } from "./model/line"; 7 | export { EditorPlugin } from "./plugin/index"; 8 | export { Priority } from "./plugin/modules/priority"; 9 | export { InjectWrapKeys } from "./plugin/modules/wrap"; 10 | export type { 11 | VueLeafContext, 12 | VueLineContext, 13 | VueWrapLeafContext, 14 | VueWrapLineContext, 15 | } from "./plugin/types"; 16 | export { BlockKit } from "./preset/block-kit"; 17 | export { Editable } from "./preset/editable"; 18 | export { Embed } from "./preset/embed"; 19 | export { Isolate } from "./preset/isolate"; 20 | export { Text } from "./preset/text"; 21 | export { Void } from "./preset/void"; 22 | export { ZeroSpace } from "./preset/zero"; 23 | export { preventNativeEvent } from "./utils/event"; 24 | -------------------------------------------------------------------------------- /packages/vue/src/preset/block-kit.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import { EDITOR_STATE } from "@block-kit/core"; 3 | import { defineComponent, provide, watchSyncEffect } from "vue"; 4 | 5 | import { BlockKitContext } from "../hooks/use-editor"; 6 | import { ReadonlyContext } from "../hooks/use-readonly"; 7 | 8 | export type BlockKitProps = { 9 | editor: Editor; 10 | readonly?: boolean; 11 | }; 12 | 13 | export const BlockKit = /*#__PURE__*/ defineComponent({ 14 | name: "BlockKit", 15 | props: ["editor", "readonly"], 16 | setup: (props, { slots }) => { 17 | watchSyncEffect(() => { 18 | if (props.editor.state.get(EDITOR_STATE.READONLY) !== props.readonly) { 19 | props.editor.state.set(EDITOR_STATE.READONLY, props.readonly || false); 20 | } 21 | }); 22 | 23 | provide(BlockKitContext, props.editor); 24 | provide(ReadonlyContext, !!props.readonly); 25 | 26 | return () => { 27 | return slots.default ? slots.default() : null; 28 | }; 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /packages/core/test/input/delete.test.ts: -------------------------------------------------------------------------------- 1 | import { Delta } from "@block-kit/delta"; 2 | 3 | import { Editor } from "../../src/editor"; 4 | import { LOG_LEVEL } from "../../src/log"; 5 | import { Range } from "../../src/selection/modules/range"; 6 | import { mountEditorViewModel } from "../config/view"; 7 | 8 | describe("input delete", () => { 9 | const delta = new Delta({ 10 | ops: [{ insert: "text1 text2 text3" }, { insert: "\n" }], 11 | }); 12 | const editor = new Editor({ delta, logLevel: LOG_LEVEL.INFO }); 13 | const { container } = mountEditorViewModel(editor); 14 | 15 | it("delete word", () => { 16 | const range = Range.fromTuple([0, 11], [0, 11]); 17 | editor.selection.set(range, true); 18 | const event = new InputEvent("beforeinput", { 19 | inputType: "deleteWordBackward", 20 | }); 21 | container.dispatchEvent(event); 22 | const newDelta = new Delta({ 23 | ops: [{ insert: "text1 text3" }, { insert: "\n" }], 24 | }); 25 | expect(editor.state.toBlock()).toEqual(newDelta); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/core/test/lookup/unicode.test.ts: -------------------------------------------------------------------------------- 1 | import { getFirstUnicodeLen, getLastUnicodeLen } from "../../src/lookup/utils/string"; 2 | 3 | describe("lookup unicode", () => { 4 | it("forward emoji length", () => { 5 | expect(getFirstUnicodeLen("")).toEqual(0); 6 | expect(getFirstUnicodeLen("1")).toEqual(1); 7 | expect(getFirstUnicodeLen("12")).toEqual(1); 8 | expect(getFirstUnicodeLen("1🧑‍🎨")).toEqual(1); 9 | expect(getFirstUnicodeLen("🧑11")).toEqual(2); 10 | expect(getFirstUnicodeLen("🧑‍🎨11")).toEqual(5); 11 | expect(getFirstUnicodeLen("👨‍👨‍👦‍👦11")).toEqual(11); 12 | }); 13 | 14 | it("backward emoji length", () => { 15 | expect(getLastUnicodeLen("")).toEqual(0); 16 | expect(getLastUnicodeLen("1")).toEqual(1); 17 | expect(getLastUnicodeLen("12")).toEqual(1); 18 | expect(getLastUnicodeLen("🧑‍🎨1")).toEqual(1); 19 | expect(getLastUnicodeLen("11🧑")).toEqual(2); 20 | expect(getLastUnicodeLen("11🧑‍🎨")).toEqual(5); 21 | expect(getLastUnicodeLen("11👨‍👨‍👦‍👦")).toEqual(11); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/vue/src/plugin/modules/priority.ts: -------------------------------------------------------------------------------- 1 | import type { CorePlugin } from "@block-kit/core"; 2 | import { PRIORITY_KEY } from "@block-kit/core"; 3 | import { DEFAULT_PRIORITY, isNumber } from "@block-kit/utils"; 4 | import type { O } from "@block-kit/utils/dist/es/types"; 5 | 6 | /** 7 | * 获取插件的优先级 8 | * @param key 9 | * @param plugin 10 | */ 11 | export const getPluginPriority = (key: string, plugin: CorePlugin): number => { 12 | const priorityKey = `${PRIORITY_KEY}${key}`; 13 | const priorityPlugin = plugin as O.Any; 14 | const priority = priorityPlugin[priorityKey]; 15 | return isNumber(priority) ? priority : DEFAULT_PRIORITY; 16 | }; 17 | 18 | /** 19 | * 优先级定义装饰器 20 | * - 兼容性实现, 非强制类型检查 21 | * @param priority 22 | */ 23 | export function Priority(priority: number) { 24 | return function (target: T, key: string, descriptor: PropertyDescriptor): PropertyDescriptor { 25 | const priorityKey = `${PRIORITY_KEY}${key}`; 26 | const plugin = target as O.Mixed; 27 | plugin[priorityKey] = priority; 28 | return descriptor; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /examples/website/src/stream/data.ts: -------------------------------------------------------------------------------- 1 | export const MARKDOWN = `# 测试文档 2 | 3 | 初始多行文本内容 4 | 初始多行文本内容 5 | 初始多行文本内容 6 | 7 | ## 二级标题 8 | **加粗文本** *斜体文本* ~~删除线~~ 9 | 10 | ### 三级标题 11 | 12 | - 无序列表项1 13 | - 无序列表项1.1 14 | - 无序列表项1.2 15 | - 无序列表项2 16 | 17 | ### 三级标题 18 | 19 | > 1. 有序列表A 20 | > 2. 有序列表B 21 | 22 | \`行内代码\` 和 [链接](https://example.com) 23 | `; 24 | 25 | export const getReadableMarkdown = () => { 26 | let start = 0; 27 | const options: UnderlyingDefaultSource = { 28 | start(controller) { 29 | const interval = setInterval(() => { 30 | const slice = Math.floor(Math.random() * 2) + 1; 31 | const end = start + slice; 32 | const fragment = MARKDOWN.slice(start, end).replace(/\n/g, "\\n"); 33 | controller.enqueue(fragment); 34 | start = end; 35 | if (start >= MARKDOWN.length) { 36 | clearInterval(interval); 37 | controller.close(); 38 | } 39 | }, 50); 40 | }, 41 | }; 42 | const readable = new ReadableStream(options); 43 | return readable; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/delta/src/attributes/compose.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from "@block-kit/utils"; 2 | 3 | import type { AttributeMap } from "./interface"; 4 | 5 | /** 6 | * 组合属性 7 | * @param a 8 | * @param b 9 | * @param keepEmpty 保留空属性 10 | */ 11 | export const composeAttributes = ( 12 | a: AttributeMap = {}, 13 | b: AttributeMap = {}, 14 | keepEmpty = false 15 | ): AttributeMap | undefined => { 16 | if (!isObject(a)) a = {}; 17 | if (!isObject(b)) b = {}; 18 | let attributes = { ...b }; 19 | if (!keepEmpty) { 20 | // Remove empty attributes 21 | attributes = Object.keys(attributes).reduce((copy, key) => { 22 | if (attributes[key] !== "" && attributes[key] !== null) { 23 | copy[key] = attributes[key]; 24 | } 25 | return copy; 26 | }, {}); 27 | } 28 | for (const key of Object.keys(a)) { 29 | // Compose a to b 30 | if (a[key] !== undefined && b[key] === undefined) { 31 | attributes[key] = a[key]; 32 | } 33 | } 34 | return Object.keys(attributes).length > 0 ? attributes : undefined; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/react/src/plugin/modules/priority.ts: -------------------------------------------------------------------------------- 1 | import type { CorePlugin } from "@block-kit/core"; 2 | import { PRIORITY_KEY } from "@block-kit/core"; 3 | import { DEFAULT_PRIORITY, isNumber } from "@block-kit/utils"; 4 | import type { O } from "@block-kit/utils/dist/es/types"; 5 | 6 | /** 7 | * 获取插件的优先级 8 | * @param key 9 | * @param plugin 10 | */ 11 | export const getPluginPriority = (key: string, plugin: CorePlugin): number => { 12 | const priorityKey = `${PRIORITY_KEY}${key}`; 13 | const priorityPlugin = plugin as O.Any; 14 | const priority = priorityPlugin[priorityKey]; 15 | return isNumber(priority) ? priority : DEFAULT_PRIORITY; 16 | }; 17 | 18 | /** 19 | * 优先级定义装饰器 20 | * - 兼容性实现, 非强制类型检查 21 | * @param priority 22 | */ 23 | export function Priority(priority: number) { 24 | return function (target: T, key: string, descriptor: PropertyDescriptor): PropertyDescriptor { 25 | const priorityKey = `${PRIORITY_KEY}${key}`; 26 | const plugin = target as O.Mixed; 27 | plugin[priorityKey] = priority; 28 | return descriptor; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /examples/website/src/blocks/config/blocks.ts: -------------------------------------------------------------------------------- 1 | import type { Blocks } from "@block-kit/x-json"; 2 | 3 | export const INIT: Blocks = { 4 | root: { 5 | id: "root", 6 | version: 1, 7 | data: { type: "ROOT", children: ["child1", "child2"], parent: "" }, 8 | }, 9 | child1: { 10 | id: "child1", 11 | version: 1, 12 | data: { 13 | type: "text", 14 | children: ["grandchild1"], 15 | delta: [{ insert: "child1" }], 16 | parent: "root", 17 | }, 18 | }, 19 | child2: { 20 | id: "child2", 21 | version: 1, 22 | data: { type: "text", children: [], delta: [{ insert: "child2" }], parent: "root" }, 23 | }, 24 | grandchild1: { 25 | id: "grandchild1", 26 | version: 1, 27 | data: { 28 | type: "text", 29 | children: ["grandgrandchild1"], 30 | delta: [{ insert: "grandchild1" }], 31 | parent: "child1", 32 | }, 33 | }, 34 | grandgrandchild1: { 35 | id: "grandgrandchild1", 36 | version: 1, 37 | data: { type: "text", children: [], delta: [{ insert: "grandgrandchild1" }], parent: "child1" }, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/plugin/src/align/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import type { AttributeMap } from "@block-kit/delta"; 3 | import type { ReactLineContext } from "@block-kit/react"; 4 | import { EditorPlugin } from "@block-kit/react"; 5 | import type { ReactNode } from "react"; 6 | 7 | import { ALIGN_KEY } from "./types"; 8 | 9 | export class AlignPlugin extends EditorPlugin { 10 | public key = ALIGN_KEY; 11 | public destroy(): void {} 12 | 13 | constructor(editor: Editor) { 14 | super(); 15 | editor.command.register(ALIGN_KEY, context => { 16 | const sel = editor.selection.get(); 17 | sel && editor.perform.applyLineMarks(sel, { [ALIGN_KEY]: context.value }); 18 | }); 19 | } 20 | 21 | public match(attrs: AttributeMap): boolean { 22 | return !!attrs[ALIGN_KEY]; 23 | } 24 | 25 | public renderLine(context: ReactLineContext): ReactNode { 26 | const attrs = context.attributes || {}; 27 | const align = attrs[ALIGN_KEY]; 28 | context.style.textAlign = align as typeof context.style.textAlign; 29 | return context.children; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /blocks/x-core/test/selection/collapse.test.ts: -------------------------------------------------------------------------------- 1 | import { Entry, Range } from "../../src"; 2 | 3 | describe("selection collapse", () => { 4 | it("range empty", () => { 5 | const range = new Range([]); 6 | expect(range.isCollapsed).toBe(true); 7 | }); 8 | 9 | it("range single block entry", () => { 10 | const entry = Entry.create("1", "B"); 11 | const range = new Range([entry]); 12 | expect(range.isCollapsed).toBe(true); 13 | }); 14 | 15 | it("range multi block entry", () => { 16 | const entry1 = Entry.create("1", "B"); 17 | const entry2 = Entry.create("2", "B"); 18 | const range = new Range([entry1, entry2]); 19 | expect(range.isCollapsed).toBe(false); 20 | }); 21 | 22 | it("range single text entry", () => { 23 | const entry = Entry.create("1", "T", 1, 2); 24 | const range = new Range([entry]); 25 | expect(range.isCollapsed).toBe(false); 26 | }); 27 | 28 | it("range single text entry", () => { 29 | const entry = Entry.create("1", "T", 1, 0); 30 | const range = new Range([entry]); 31 | expect(range.isCollapsed).toBe(true); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/core/src/schema/utils/is.ts: -------------------------------------------------------------------------------- 1 | import { isEOLOp } from "@block-kit/delta"; 2 | 3 | import type { LineState } from "../../state/modules/line-state"; 4 | 5 | /** 6 | * 判断 Block 行状态 7 | * - block + void: 独占一行的 Void 节点 8 | * @param op 9 | */ 10 | export const isBlockLine = (line: LineState | null): boolean => { 11 | if (!line) return false; 12 | const firstLeaf = line.getFirstLeaf(); 13 | return !!firstLeaf && firstLeaf.void && !firstLeaf.inline; 14 | }; 15 | 16 | /** 17 | * 判断行是否为空行 18 | * @param line 行状态 19 | * @param strict 严格模式下会增加对行属性的判断 20 | */ 21 | export const isEmptyLine = (line: LineState, strict = false) => { 22 | const leaves = line.getLeaves(); 23 | // 如果行没有叶子结点, 则认为是空行 24 | if (!leaves.length) return true; 25 | // 叶子结点数必须仅为单个节点, 否则认为非空行 26 | if (leaves.length !== 1) return false; 27 | const lastLeaf = line.getLastLeaf()!; 28 | const op = lastLeaf.op; 29 | // 末尾节点必须为 EOL Op, 否则认为非空行 30 | if (!isEOLOp(op)) return false; 31 | // 非严格模式下已经足够判断条件, 直接返回 32 | if (!strict) return true; 33 | const attrs = op.attributes; 34 | return !attrs || !Object.keys(attrs).length; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/core/src/log/index.ts: -------------------------------------------------------------------------------- 1 | export const LOG_LEVEL = { 2 | DEBUG: -1, 3 | INFO: 0, 4 | WARNING: 1, 5 | ERROR: 2, 6 | } as const; 7 | 8 | export class Logger { 9 | public constructor(protected level: number) {} 10 | 11 | set(level: number) { 12 | this.level = level; 13 | } 14 | 15 | debug(key: string, ...args: unknown[]) { 16 | if (this.level <= LOG_LEVEL.DEBUG) { 17 | console.log("DEBUG -", key, ...args); 18 | } 19 | } 20 | 21 | info(key: string, ...args: unknown[]) { 22 | if (this.level <= LOG_LEVEL.INFO) { 23 | console.log("Log -", key, ...args); 24 | } 25 | } 26 | 27 | warning(key: string, ...args: unknown[]) { 28 | if (this.level <= LOG_LEVEL.WARNING) { 29 | console.warn("Warning -", key, ...args); 30 | } 31 | } 32 | 33 | error(key: string, ...args: unknown[]) { 34 | if (this.level <= LOG_LEVEL.ERROR) { 35 | console.error("Error -", key, ...args); 36 | } 37 | } 38 | 39 | trace(key: string, ...args: unknown[]) { 40 | if (this.level <= LOG_LEVEL.ERROR) { 41 | console.trace("Trace -", key, ...args); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/utils/test/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { Storage } from "../src/storage"; 2 | 3 | describe("date-time", () => { 4 | beforeAll(() => { 5 | const data: Record = {}; 6 | const mock = { 7 | getItem: (key: string) => data[key], 8 | setItem: (key: string, value: string) => (data[key] = value), 9 | removeItem: (key: string) => delete data[key], 10 | }; 11 | Object.assign(global, { localStorage: mock, sessionStorage: mock }); 12 | }); 13 | 14 | it("save value", () => { 15 | const key = "save-value"; 16 | const value = { data: 1 }; 17 | Storage.local.set(key, value); 18 | expect(Storage.local.get(key)).toEqual(value); 19 | expect(Storage.local.getOrigin(key)).toBe('{"origin":{"data":1},"expire":null}'); 20 | }); 21 | 22 | it("save value with ttl", async () => { 23 | const key = "save-value-ttl"; 24 | const value = { data: 1 }; 25 | Storage.local.set(key, value, 10); 26 | expect(Storage.local.get(key)).toEqual(value); 27 | await new Promise(r => setTimeout(r, 20)); 28 | expect(Storage.local.get(key)).toBeNull(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Czy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/plugin/src/font-color/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import type { AttributeMap } from "@block-kit/delta"; 3 | import type { ReactLeafContext } from "@block-kit/react"; 4 | import { EditorPlugin } from "@block-kit/react"; 5 | import type { ReactNode } from "react"; 6 | 7 | import { FONT_COLOR_KEY } from "./types"; 8 | 9 | export class FontColorPlugin extends EditorPlugin { 10 | public key = FONT_COLOR_KEY; 11 | public destroy(): void {} 12 | 13 | constructor(editor: Editor) { 14 | super(); 15 | editor.command.register(FONT_COLOR_KEY, context => { 16 | const sel = editor.selection.get(); 17 | sel && editor.perform.applyMarks(sel, { [FONT_COLOR_KEY]: context.value }); 18 | }); 19 | } 20 | 21 | public match(attrs: AttributeMap): boolean { 22 | return !!attrs[FONT_COLOR_KEY]; 23 | } 24 | 25 | public renderLeaf(context: ReactLeafContext): ReactNode { 26 | const attrs = context.attributes || {}; 27 | const color = attrs[FONT_COLOR_KEY]; 28 | if (color) { 29 | context.style.color = color; 30 | } 31 | return context.children; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/website/src/variable/constant.ts: -------------------------------------------------------------------------------- 1 | import type { O } from "@block-kit/utils/dist/es/types"; 2 | import type { EditorSchema } from "@block-kit/variable"; 3 | import { Delta, SEL_KEY, SEL_VALUE_KEY } from "@block-kit/variable"; 4 | import { VARS_KEY, VARS_VALUE_KEY } from "@block-kit/variable"; 5 | 6 | export const SCHEMA: EditorSchema = { 7 | [VARS_KEY]: { void: true, inline: true }, 8 | [SEL_KEY]: { void: true, inline: true }, 9 | }; 10 | 11 | export const PLACEHOLDERS: O.Map = { 12 | role: "[职业]", 13 | theme: "[主题]", 14 | platform: "[平台: 如公众号、知乎、头条等]", 15 | }; 16 | 17 | export const SELECTOR: O.Map = { 18 | space: ["简练", "中等", "长篇"], 19 | }; 20 | 21 | export const DELTA = new Delta() 22 | .insert("我是一位") 23 | .insert(" ", { 24 | [VARS_KEY]: "role", 25 | [VARS_VALUE_KEY]: "博主", 26 | }) 27 | .insert(",帮我写一篇关于") 28 | .insert(" ", { 29 | [VARS_KEY]: "theme", 30 | }) 31 | .insert("的") 32 | .insert(" ", { 33 | [VARS_KEY]: "platform", 34 | }) 35 | .insert("文章,需要符合该平台写作风格,文章篇幅为") 36 | .insert(" ", { 37 | [SEL_KEY]: "space", 38 | [SEL_VALUE_KEY]: "中等", 39 | }) 40 | .insert("。"); 41 | -------------------------------------------------------------------------------- /examples/website/src/react/styles/index.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable selector-no-qualifying-type */ 2 | 3 | body { 4 | background-color: var(--color-fill-2); 5 | } 6 | 7 | body[arco-theme='dark'] { 8 | background-color: var(--color-bg-3); 9 | } 10 | 11 | .block-kit-editable-container { 12 | margin: 15px auto; 13 | position: relative; 14 | width: 800px; 15 | } 16 | 17 | .block-kit-editor-container { 18 | box-sizing: border-box; 19 | position: relative; 20 | 21 | .block-kit-toolbar { 22 | background-color: var(--color-bg-2); 23 | border-bottom: unset; 24 | border-bottom: 1px solid var(--color-fill-2); 25 | justify-content: center; 26 | position: sticky; 27 | top: 0; 28 | z-index: 100; 29 | } 30 | 31 | .block-kit-mount-dom { 32 | left: 0; 33 | position: absolute; 34 | top: 0; 35 | } 36 | 37 | .block-kit-editable { 38 | background-color: var(--color-bg-2); 39 | color: var(--color-text-1); 40 | padding: 10px 20px; 41 | } 42 | 43 | div[data-node] { 44 | line-height: 1.8; 45 | margin: 8px 0; 46 | } 47 | 48 | div[data-placeholder] { 49 | line-height: 1.8; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/plugin/src/line-height/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import type { AttributeMap } from "@block-kit/delta"; 3 | import type { ReactLineContext } from "@block-kit/react"; 4 | import { EditorPlugin } from "@block-kit/react"; 5 | import type { ReactNode } from "react"; 6 | 7 | import { LINE_HEIGHT_KEY } from "./types"; 8 | 9 | export class LineHeightPlugin extends EditorPlugin { 10 | public key = LINE_HEIGHT_KEY; 11 | public destroy(): void {} 12 | 13 | constructor(editor: Editor) { 14 | super(); 15 | editor.command.register(LINE_HEIGHT_KEY, context => { 16 | const sel = editor.selection.get(); 17 | sel && editor.perform.applyLineMarks(sel, { [LINE_HEIGHT_KEY]: context.value }); 18 | }); 19 | } 20 | 21 | public match(attrs: AttributeMap): boolean { 22 | return !!attrs[LINE_HEIGHT_KEY]; 23 | } 24 | 25 | public renderLine(context: ReactLineContext): ReactNode { 26 | const attrs = context.attributes || {}; 27 | const height = attrs[LINE_HEIGHT_KEY]; 28 | context.style.lineHeight = height as typeof context.style.lineHeight; 29 | return context.children; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/plugin/src/shared/modules/selection.ts: -------------------------------------------------------------------------------- 1 | import type { Editor, SelectionChangeEvent } from "@block-kit/core"; 2 | import { EDITOR_EVENT } from "@block-kit/core"; 3 | import { Bind } from "@block-kit/utils"; 4 | 5 | import type { SelectionHOC } from "../components/selection"; 6 | export class SelectionPlugin { 7 | /** id <-> React.ReactNode */ 8 | protected idToView: Map; 9 | 10 | constructor(public editor: Editor) { 11 | this.idToView = new Map(); 12 | editor.event.on(EDITOR_EVENT.SELECTION_CHANGE, this.onSelectionChange); 13 | } 14 | 15 | public destroy(): void { 16 | this.idToView.clear(); 17 | this.editor.event.off(EDITOR_EVENT.SELECTION_CHANGE, this.onSelectionChange); 18 | } 19 | 20 | public mountView(id: string, view: SelectionHOC) { 21 | this.idToView.set(id, view); 22 | } 23 | 24 | public unmountView(id: string) { 25 | this.idToView.delete(id); 26 | } 27 | 28 | @Bind 29 | protected onSelectionChange(e: SelectionChangeEvent) { 30 | const current = e.current; 31 | this.idToView.forEach(view => { 32 | view.onSelectionChange(current); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/plugin/src/background/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import type { AttributeMap } from "@block-kit/delta"; 3 | import type { ReactLeafContext } from "@block-kit/react"; 4 | import { EditorPlugin } from "@block-kit/react"; 5 | import type { ReactNode } from "react"; 6 | 7 | import { BACKGROUND_KEY } from "./types"; 8 | 9 | export class BackgroundPlugin extends EditorPlugin { 10 | public key = BACKGROUND_KEY; 11 | public destroy(): void {} 12 | 13 | constructor(editor: Editor) { 14 | super(); 15 | editor.command.register(BACKGROUND_KEY, context => { 16 | const sel = editor.selection.get(); 17 | sel && editor.perform.applyMarks(sel, { [BACKGROUND_KEY]: context.value }); 18 | }); 19 | } 20 | 21 | public match(attrs: AttributeMap): boolean { 22 | return !!attrs[BACKGROUND_KEY]; 23 | } 24 | 25 | public renderLeaf(context: ReactLeafContext): ReactNode { 26 | const attrs = context.attributes || {}; 27 | const background = attrs[BACKGROUND_KEY]; 28 | if (background) { 29 | context.style.backgroundColor = background; 30 | } 31 | return context.children; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/plugin/src/shared/icons/emoji.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | 3 | export const EmojiIcon: FC = () => ( 4 | 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /blocks/x-react/src/preset/zero.tsx: -------------------------------------------------------------------------------- 1 | import { ZERO_SPACE_KEY, ZERO_SYMBOL } from "@block-kit/core"; 2 | import { NO_CURSOR } from "@block-kit/react"; 3 | import { useMemoFn } from "@block-kit/utils/dist/es/hooks"; 4 | import { X_ZERO_KEY } from "@block-kit/x-core"; 5 | import type { FC } from "react"; 6 | 7 | export type ZeroSpaceProps = { 8 | /** 隐藏光标 */ 9 | hide?: boolean; 10 | /** 块占位节点 */ 11 | block?: boolean; 12 | /** 获取 DOM 引用 */ 13 | onRef?: (ref: HTMLSpanElement | null) => void; 14 | }; 15 | 16 | /** 17 | * 零宽字符组件 18 | * @param props 19 | */ 20 | export const ZeroSpace: FC = props => { 21 | /** 22 | * 处理 ref 回调 23 | * - 需要保证引用不变, 否则会导致回调在 rerender 时被多次调用 null/span 状态 24 | * - https://18.react.dev/reference/react-dom/components/common#ref-callback 25 | */ 26 | const onRef = useMemoFn((dom: HTMLSpanElement | null) => { 27 | props.onRef && props.onRef(dom); 28 | }); 29 | 30 | return ( 31 | 39 | {ZERO_SYMBOL} 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/core/test/history/merge.test.ts: -------------------------------------------------------------------------------- 1 | import { Delta } from "@block-kit/delta"; 2 | import { sleep } from "@block-kit/utils"; 3 | 4 | import { Editor } from "../../src/editor"; 5 | 6 | describe("history merge", () => { 7 | const editor = new Editor(); 8 | // @ts-expect-error protected readonly property 9 | editor.history.DELAY = 10; 10 | 11 | it("merge record", async () => { 12 | editor.state.apply(new Delta().insert("1")); // 1 13 | await sleep(20); 14 | const { id: id1 } = editor.state.apply(new Delta().retain(1).insert("2", { src: "blob" })); // 12 15 | await sleep(20); 16 | editor.state.apply(new Delta().retain(1).insert("3")); // 132 17 | await sleep(20); 18 | const { id: id2 } = editor.state.apply(new Delta().retain(2).retain(1, { src: "http" })); // 132 19 | editor.history.mergeRecord(id1, id2); 20 | // @ts-expect-error protected property 21 | const undoStack = editor.history.undoStack.map(it => it.delta); 22 | expect(undoStack[2]).toEqual(new Delta().retain(1).delete(1)); // 12 23 | expect(undoStack[1]).toEqual(new Delta().retain(1).delete(1)); // 1 24 | expect(undoStack[0]).toEqual(new Delta().delete(1)); // 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/plugin/src/font-size/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import type { AttributeMap } from "@block-kit/delta"; 3 | import type { ReactLeafContext } from "@block-kit/react"; 4 | import { EditorPlugin } from "@block-kit/react"; 5 | import { Facade } from "@block-kit/utils"; 6 | import type { ReactNode } from "react"; 7 | 8 | import { FONT_SIZE_KEY } from "./types"; 9 | 10 | export class FontSizePlugin extends EditorPlugin { 11 | public key = FONT_SIZE_KEY; 12 | public destroy(): void {} 13 | 14 | constructor(editor: Editor) { 15 | super(); 16 | editor.command.register(FONT_SIZE_KEY, context => { 17 | const sel = editor.selection.get(); 18 | sel && editor.perform.applyMarks(sel, { [FONT_SIZE_KEY]: context.value }); 19 | }); 20 | } 21 | 22 | public match(attrs: AttributeMap): boolean { 23 | return !!attrs[FONT_SIZE_KEY]; 24 | } 25 | 26 | public renderLeaf(context: ReactLeafContext): ReactNode { 27 | const attrs = context.attributes || {}; 28 | const size = attrs[FONT_SIZE_KEY]; 29 | if (size) { 30 | context.style.fontSize = Facade.pixelate(size) as string; 31 | } 32 | return context.children; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /blocks/x-core/src/model/types/dom.ts: -------------------------------------------------------------------------------- 1 | import { isDOMElement } from "@block-kit/utils"; 2 | 3 | import type { BlockEditor } from "../../editor"; 4 | import { X_ZERO_KEY } from "."; 5 | 6 | /** 7 | * 获取指定 Block 的起始位置的 Text 节点 8 | * @param editor 9 | * @param blockId 10 | */ 11 | export const getBlockStartZeroNode = (editor: BlockEditor, blockId: string): Text | null => { 12 | const block = editor.state.getBlock(blockId); 13 | const blockNode = block && block.getDOMNode(); 14 | const firstNode = blockNode && blockNode.firstElementChild; 15 | if (isDOMElement(firstNode) && firstNode.hasAttribute(X_ZERO_KEY)) { 16 | return firstNode.firstChild as Text; 17 | } 18 | return null; 19 | }; 20 | 21 | /** 22 | * 获取指定 Block 的末尾位置的 Text 节点 23 | * @param editor 24 | * @param blockId 25 | */ 26 | export const getBlockEndZeroNode = (editor: BlockEditor, blockId: string): Text | null => { 27 | const block = editor.state.getBlock(blockId); 28 | const blockNode = block && block.getDOMNode(); 29 | const lastNode = blockNode && blockNode.lastElementChild; 30 | if (isDOMElement(lastNode) && lastNode.hasAttribute(X_ZERO_KEY)) { 31 | return lastNode.firstChild as Text; 32 | } 33 | return null; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/utils/src/literal.ts: -------------------------------------------------------------------------------- 1 | export class Literal { 2 | /** 3 | * 字符串: 安全地使用 HTML 4 | * @param str 5 | */ 6 | public static escapeHtml(str: string) { 7 | const html = str 8 | .replace(/&/g, "&") 9 | .replace(//g, ">") 11 | .replace(/'/g, "'") 12 | .replace(/"/g, """); 13 | 14 | return html; 15 | } 16 | 17 | /** 18 | * 字符串: 字典序对比 19 | * @param a 20 | * @param b 21 | * @returns -1: a < b, 0: a = b, 1: a > b 22 | */ 23 | public static compare(a: string, b: string): number { 24 | const len = Math.min(a.length, b.length); 25 | for (let i = 0; i < len; i++) { 26 | if (a.charCodeAt(i) < b.charCodeAt(i)) return -1; 27 | if (a.charCodeAt(i) > b.charCodeAt(i)) return 1; 28 | } 29 | const diff = a.length - b.length; 30 | return diff < 0 ? -1 : diff > 0 ? 1 : 0; 31 | } 32 | 33 | /** 34 | * 字符串: 关联 Number 35 | * - 类似于 Hash, 但不处理 Hash 冲突 36 | * @param str 37 | */ 38 | public static numberify(str: string): number { 39 | let num = 0; 40 | for (let i = 0; i < str.length; i++) { 41 | num = num + str.charCodeAt(i); 42 | } 43 | return num; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/src/state/utils/key.ts: -------------------------------------------------------------------------------- 1 | import type { Object } from "@block-kit/utils"; 2 | 3 | export const NODE_TO_KEY = new WeakMap(); 4 | 5 | export class Key { 6 | /** 当前节点 id */ 7 | public id: string; 8 | /** 自动递增标识符 */ 9 | public static n = 0; 10 | 11 | /** 12 | * 构造函数 13 | */ 14 | constructor(preset?: string) { 15 | this.id = preset || `${Key.n++}`; 16 | } 17 | 18 | /** 19 | * 根据节点获取 id 20 | * @param node 21 | */ 22 | public static getId(node: Object.Any): string { 23 | let key = NODE_TO_KEY.get(node); 24 | if (!key) { 25 | key = new Key(); 26 | NODE_TO_KEY.set(node, key); 27 | } 28 | return key.id; 29 | } 30 | 31 | /** 32 | * 根据节点刷新 id 33 | * @param node 34 | */ 35 | public static refresh(node: Object.Any): string { 36 | const key = new Key(); 37 | NODE_TO_KEY.set(node, key); 38 | return key.id; 39 | } 40 | 41 | /** 42 | * 刷新 Key id 43 | * @param node 44 | * @param id 45 | */ 46 | public static update(node: Object.Any, id: string): string { 47 | const key = NODE_TO_KEY.get(node) || new Key(id); 48 | key.id = id; 49 | NODE_TO_KEY.set(node, key); 50 | return id; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/utils/test/format.test.ts: -------------------------------------------------------------------------------- 1 | import { Format } from "../src/format"; 2 | 3 | describe("format", () => { 4 | it("string", () => { 5 | expect(Format.string("{{0}}", ["data"])).toBe("data"); 6 | expect(Format.string("{{id}}", { id: "data" })).toBe("data"); 7 | }); 8 | 9 | it("number", () => { 10 | expect(Format.number(1123)).toBe("1,123"); 11 | }); 12 | 13 | it("bytes", () => { 14 | expect(Format.bytes(0)).toBe("0 B"); 15 | expect(Format.bytes(1024)).toBe("1.00 KB"); 16 | expect(Format.bytes(1025, 3)).toBe("1.001 KB"); 17 | expect(Format.bytes(1024 ** 2)).toBe("1.00 MB"); 18 | expect(Format.bytes(1024 ** 3)).toBe("1.00 GB"); 19 | expect(Format.bytes(1024 ** 4)).toBe("1.00 TB"); 20 | }); 21 | 22 | it("time", () => { 23 | expect(Format.time(2000, 3000)).toBe("1 second ago"); 24 | expect(Format.time(3000, 1000 * 60)).toBe("57 seconds ago"); 25 | expect(Format.time(3000, 1000 * 60 * 60)).toBe("59 minutes ago"); 26 | expect(Format.time(3000, 1000 * 60 * 60 * 24)).toBe("23 hours ago"); 27 | expect(Format.time(3000, 1000 * 60 * 60 * 24 * 30)).toBe("29 days ago"); 28 | expect(Format.time(3000, 1000 * 60 * 60 * 24 * 365)).toBe("12 months ago"); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/ot-json/src/subtype.ts: -------------------------------------------------------------------------------- 1 | import type { P } from "@block-kit/utils/dist/es/types"; 2 | 3 | import type { Path, Side } from "./types"; 4 | 5 | export type Subtype = { 6 | /** 子类型名 */ 7 | name: string; 8 | /** 规范地址 */ 9 | uri?: string; 10 | /** 反转变更 */ 11 | invert: (ops: P.Any, snapshot?: P.Any) => P.Any; 12 | /** 应用变更 */ 13 | apply: (snapshot: P.Any, ops: P.Any) => void; 14 | /** 组合变更 */ 15 | compose: (ops1: P.Any, ops2: P.Any) => P.Any; 16 | /** 操作变换 */ 17 | transform: (ops1: P.Any, ops2: P.Any, side?: Side) => P.Any; 18 | /** 规范化变更 */ 19 | normalize?: (ops: P.Any) => P.Any; 20 | /** 序列化变更 */ 21 | serialize?: (ops: P.Any) => string; 22 | /** 反序列化变更 */ 23 | deserialize?: (str: string) => P.Any; 24 | /** 操作变换光标 */ 25 | transformCursor?: (cursor: number, ops: P.Any, side?: Side) => number; 26 | }; 27 | 28 | export interface SubtypeOpMap { 29 | text: { p: number; i?: string; d?: string }; 30 | } 31 | 32 | /** 33 | * applies the subtype op o of type t(registered subtype) to the object at [path] 34 | */ 35 | export type SubtypeOp = { 36 | p: Path; 37 | t: T; 38 | o: SubtypeOpMap[T]; 39 | }; 40 | 41 | export const subtypes: Record = {}; 42 | -------------------------------------------------------------------------------- /packages/react/test/hooks/callback.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { sleep } from "@block-kit/utils"; 3 | import { act, render } from "@testing-library/react"; 4 | import type { Dispatch, SetStateAction } from "react"; 5 | 6 | import { useMemoFn, useSafeState } from "../../../utils/src/hooks"; 7 | 8 | describe("hooks callback", () => { 9 | it("memo fn", async () => { 10 | const spy = jest.fn(v => v); 11 | let callerCount = 0; 12 | let setCount!: Dispatch>; 13 | const App = () => { 14 | const [count, _setCount] = useSafeState(0); 15 | setCount = _setCount; 16 | const fn = useMemoFn(() => { 17 | callerCount = spy(count); 18 | }); 19 | return ( 20 |
21 | {count} 22 |
23 | ); 24 | }; 25 | const dom = render(); 26 | await act(async () => { 27 | setCount(1); 28 | await sleep(10); 29 | setCount(2); 30 | await sleep(10); 31 | }); 32 | dom.container.querySelector("[data-count]")?.click(); 33 | expect(spy).toHaveBeenCalledTimes(1); 34 | expect(callerCount).toBe(2); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/delta/src/utils/equal.ts: -------------------------------------------------------------------------------- 1 | import type { AttributeMap } from "../attributes/interface"; 2 | import type { Op } from "../delta/interface"; 3 | 4 | /** 5 | * 判断两个 AttributeMap 是否相等 6 | * @param o 7 | * @param t 8 | */ 9 | export const isEqualAttributes = (o: AttributeMap | undefined, t: AttributeMap | undefined) => { 10 | if (!o && !t) return true; 11 | const origin = o || {}; 12 | const target = t || {}; 13 | const originKeys = Object.keys(origin); 14 | const targetKeys = Object.keys(target); 15 | if (originKeys.length !== targetKeys.length) return false; 16 | for (const key of originKeys) { 17 | if (origin[key] !== target[key]) return false; 18 | } 19 | return true; 20 | }; 21 | 22 | /** 23 | * 判断两个 Op 是否相等 24 | * @param origin 25 | * @param target 26 | */ 27 | export const isEqualOp = (origin: Op | undefined, target: Op | undefined) => { 28 | if (origin === target) return true; 29 | if (!origin || !target) return false; 30 | if (origin.insert !== target.insert) return false; 31 | if (origin.delete !== target.delete) return false; 32 | if (origin.retain !== target.retain) return false; 33 | if (!isEqualAttributes(origin.attributes, target.attributes)) return false; 34 | return true; 35 | }; 36 | -------------------------------------------------------------------------------- /examples/website/src/blocks/styles/index.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable selector-no-qualifying-type */ 2 | 3 | body { 4 | background-color: var(--color-fill-2); 5 | } 6 | 7 | body[arco-theme='dark'] { 8 | background-color: var(--color-bg-3); 9 | } 10 | 11 | .block-kit-editable-container { 12 | margin: 15px auto; 13 | position: relative; 14 | width: 800px; 15 | } 16 | 17 | .block-kit-editor-container { 18 | box-sizing: border-box; 19 | position: relative; 20 | 21 | .block-kit-toolbar { 22 | background-color: var(--color-bg-2); 23 | border-bottom: unset; 24 | border-bottom: 1px solid var(--color-fill-2); 25 | justify-content: center; 26 | position: sticky; 27 | top: 0; 28 | z-index: 100; 29 | } 30 | 31 | .block-kit-mount-dom { 32 | left: 0; 33 | position: absolute; 34 | top: 0; 35 | } 36 | 37 | .block-kit-editable { 38 | background-color: var(--color-bg-2); 39 | color: var(--color-text-1); 40 | padding: 10px 20px; 41 | } 42 | 43 | div[data-node], 44 | div.block-kit-editable > div[data-placeholder] { 45 | line-height: 1.8; 46 | } 47 | 48 | div[data-text-block], 49 | div.block-kit-editable > div[data-placeholder] { 50 | padding: 3px 0; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /blocks/x-react/src/model/ph.tsx: -------------------------------------------------------------------------------- 1 | import { PLACEHOLDER_KEY } from "@block-kit/core"; 2 | import type { BlockEditor, BlockState } from "@block-kit/x-core"; 3 | import type { FC } from "react"; 4 | import React from "react"; 5 | 6 | import { useComposing } from "../hooks/use-composing"; 7 | 8 | /** 9 | * 占位符组件 10 | * - 抽离组件的主要目标是避免父组件的 LayoutEffect 执行 11 | */ 12 | export const Placeholder: FC<{ 13 | editor: BlockEditor; 14 | state: BlockState; 15 | placeholder: React.ReactNode | undefined; 16 | }> = props => { 17 | const { state } = props; 18 | const { isComposing } = useComposing(props.editor); 19 | 20 | let placeholder: JSX.Element | null = null; 21 | if ( 22 | props.placeholder && 23 | !isComposing && 24 | state.children.length === 1 && 25 | state.children[0]!.data.delta && 26 | !state.children[0]!.length 27 | ) { 28 | placeholder = props.placeholder as JSX.Element; 29 | } 30 | 31 | return placeholder ? ( 32 |
41 | {placeholder} 42 |
43 | ) : null; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/react/src/preset/block-kit.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor } from "@block-kit/core"; 2 | import { EDITOR_STATE } from "@block-kit/core"; 3 | import type { ReactNode } from "react"; 4 | import { useMemo } from "react"; 5 | 6 | import { BlockKitContext } from "../hooks/use-editor"; 7 | import { ReadonlyContext } from "../hooks/use-readonly"; 8 | import { PortalModel } from "../model/portal"; 9 | import { initWrapPlugins } from "../plugin/modules/wrap"; 10 | 11 | export type BlockKitProps = { 12 | editor: Editor; 13 | readonly?: boolean; 14 | children?: ReactNode; 15 | }; 16 | 17 | export const BlockKit: React.FC = props => { 18 | const { editor, readonly, children } = props; 19 | 20 | if (editor.state.get(EDITOR_STATE.READONLY) !== readonly) { 21 | editor.state.set(EDITOR_STATE.READONLY, readonly || false); 22 | } 23 | 24 | useMemo(() => { 25 | // 希望在 Editor 初始化后立即执行一次 26 | initWrapPlugins(editor); 27 | }, [editor]); 28 | 29 | return ( 30 | 31 | 32 | 33 | {children} 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@block-kit/vue", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./dist/lib/index.js", 6 | "types": "./dist/es/index.d.ts", 7 | "module": "./dist/es/index.js", 8 | "sideEffects": [ 9 | "**/*.css", 10 | "**/*.scss", 11 | "**/*.less" 12 | ], 13 | "files": [ 14 | "dist" 15 | ], 16 | "scripts": { 17 | "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.lib.json", 18 | "test": "", 19 | "lint:ts": "tsc -p tsconfig.build.json --noEmit", 20 | "lint:circular": "madge --extensions js --circular ./dist" 21 | }, 22 | "author": "WindRunnerMax", 23 | "license": "MIT", 24 | "keywords": [ 25 | "editor", 26 | "wysiwyg", 27 | "rich-text" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/WindRunnerMax/BlockKit.git" 32 | }, 33 | "homepage": "https://github.com/WindRunnerMax/BlockKit", 34 | "bugs": { 35 | "url": "https://github.com/WindRunnerMax/BlockKit/issues" 36 | }, 37 | "peerDependencies": { 38 | "vue": ">=3.2.31" 39 | }, 40 | "dependencies": { 41 | "@block-kit/core": "workspace: *", 42 | "@block-kit/delta": "workspace: *", 43 | "@block-kit/utils": "workspace: *" 44 | } 45 | } -------------------------------------------------------------------------------- /packages/utils/src/vars.less: -------------------------------------------------------------------------------- 1 | /** 2 | * @usage *.less; 3 | * 4 | * ```less 5 | * @import "/node_modules//dist/style/vars.less"; 6 | * .className { 7 | * .mixinName(); 8 | * } 9 | * ``` 10 | */ 11 | 12 | .no-scrollbar() { 13 | overflow: -moz-scrollbars-none; 14 | -ms-overflow-style: none; 15 | scrollbar-width: none; 16 | 17 | &::-webkit-scrollbar { 18 | display: none; 19 | } 20 | } 21 | 22 | .min-scrollbar() { 23 | // https://caniuse.com/?search=scrollbar-color 24 | scrollbar-color: #ddd transparent; 25 | scrollbar-width: thin; 26 | 27 | // https://caniuse.com/?search=-webkit-scrollbar 28 | &::-webkit-scrollbar { 29 | height: 6px; 30 | width: 6px; 31 | } 32 | 33 | &::-webkit-scrollbar-thumb { 34 | background: #ddd; 35 | border-radius: 6px; 36 | } 37 | 38 | &::-webkit-scrollbar-thumb:hover { 39 | background: #ccc; 40 | } 41 | } 42 | 43 | .frame-box() { 44 | background-color: var(--color-bg-1, #fff); 45 | border: 1px solid var(--color-border-2, rgb(229, 230, 235)); 46 | border-radius: 3px; 47 | box-shadow: 0 0 4px var(--color-border-2, rgb(229, 230, 235)); 48 | } 49 | 50 | .text-ellipsis() { 51 | overflow: hidden; 52 | text-overflow: ellipsis; 53 | white-space: nowrap; 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: project testing 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | project-testing: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 15 14 | steps: 15 | - name: checkout 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | persist-credentials: false 20 | 21 | - name: install node-v18 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: "18.20.8" 25 | 26 | - name: install dependencies 27 | run: | 28 | node -v 29 | npm install -g pnpm@8.11.0 30 | pnpm install --registry=https://registry.npmjs.org/ 31 | 32 | - name: build project 33 | run: | 34 | pnpm run --filter '*' build 35 | 36 | - name: lint typescript 37 | run: | 38 | pnpm run --filter '*' lint:ts 39 | 40 | - name: lint circular 41 | run: | 42 | npm -g install madge@8.0.0 --registry=https://registry.npmjs.org/ 43 | pnpm run --filter '*' lint:circular 44 | 45 | - name: unit testing 46 | run: | 47 | export TZ="Asia/Shanghai" 48 | pnpm run --filter '*' test 49 | -------------------------------------------------------------------------------- /packages/core/src/clipboard/utils/serialize.ts: -------------------------------------------------------------------------------- 1 | import { EOL } from "@block-kit/delta"; 2 | 3 | import { NODE_KEY } from "../../model/types"; 4 | 5 | /** 6 | * 序列化 HTML 到 文本 7 | * @param node 8 | * @param clone 9 | */ 10 | export const serializeHTML = (node: Node, clone = false): string => { 11 | const el = document.createElement("div"); 12 | el.appendChild(clone ? node.cloneNode(true) : node); 13 | return el.innerHTML; 14 | }; 15 | 16 | /** 17 | * 递归处理节点文本内容 18 | * @param node 19 | */ 20 | export const getTextContent = (node: Node): string => { 21 | if (node instanceof Text) { 22 | return node.textContent || ""; 23 | } 24 | const texts: string[] = []; 25 | node.childNodes.forEach(child => { 26 | texts.push(getTextContent(child)); 27 | }); 28 | if (node instanceof Element && node.getAttribute(NODE_KEY)) { 29 | texts.push(EOL); 30 | } 31 | return texts.join(""); 32 | }; 33 | 34 | /** 35 | * 获取节点的文本内容 36 | * @param node 37 | */ 38 | export const getFragmentText = (node: Node) => { 39 | const texts: string[] = []; 40 | Array.from(node.childNodes).forEach(it => { 41 | texts.push(getTextContent(it)); 42 | }); 43 | const res = texts.join(""); 44 | // 将文本最后的 \n 移除 45 | return res.endsWith(EOL) ? res.slice(0, -1) : res; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/react/test/basic/dom.test.tsx: -------------------------------------------------------------------------------- 1 | import { Editor, LEAF_KEY, LEAF_STRING, NODE_KEY } from "@block-kit/core"; 2 | import { Delta } from "@block-kit/delta"; 3 | import { render } from "@testing-library/react"; 4 | 5 | import { BlockKit, Editable } from "../../src"; 6 | 7 | //
8 | //
9 | //
10 | // 11 | // test 12 | // 13 | //
14 | //
15 | //
16 | 17 | describe("basic dom", () => { 18 | it("editor", () => { 19 | const delta = new Delta().insert("test").insertEOL(); 20 | const editor = new Editor({ delta }); 21 | const dom = render( 22 | 23 | 24 | 25 | ); 26 | const root = dom.container; 27 | const lines = root.querySelectorAll(`[${NODE_KEY}]`); 28 | const leaves = Array.from(lines).map(line => line.querySelectorAll(`[${LEAF_KEY}]`)); 29 | expect(lines.length).toBe(1); 30 | expect(leaves[0].length).toBe(1); 31 | expect(leaves[0][0].querySelector(`[${LEAF_STRING}]`)!.textContent).toBe("test"); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy gh-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | env: 9 | OUT_DIR: "examples/website/build" 10 | 11 | jobs: 12 | build-and-deploy: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 15 15 | steps: 16 | - name: checkout 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | persist-credentials: false 21 | 22 | - name: install node-v16 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: "16.16.0" 26 | 27 | - name: install dependencies 28 | run: | 29 | node -v 30 | npm install -g pnpm@8.11.0 31 | pnpm install --registry=https://registry.npmjs.org/ 32 | 33 | - name: build project 34 | run: | 35 | pnpm run --filter "@block-kit/website..." build 36 | matter="---\ntitle: Note\n---\n" 37 | echo -e "$matter" > $OUT_DIR/Notes.md 38 | cat NOTE.md >> $OUT_DIR/Notes.md 39 | 40 | - name: deploy project 41 | uses: JamesIves/github-pages-deploy-action@releases/v3 42 | with: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | BRANCH: gh-pages 45 | FOLDER: ${{ env.OUT_DIR }} 46 | -------------------------------------------------------------------------------- /packages/core/src/state/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { O } from "@block-kit/utils/dist/es/types"; 2 | 3 | import type { RawRange } from "../../selection/modules/raw-range"; 4 | 5 | export const EDITOR_STATE = { 6 | /** IME 组合状态 */ 7 | COMPOSING: "COMPOSING", 8 | /** 挂载状态 */ 9 | MOUNTED: "MOUNTED", 10 | /** 只读状态 */ 11 | READONLY: "READONLY", 12 | /** 鼠标按键状态 */ 13 | MOUSE_DOWN: "MOUSE_DOWN", 14 | /** 焦点状态(捕获) */ 15 | FOCUS: "FOCUS", 16 | /** 焦点状态(冒泡) */ 17 | FOCUSIN: "FOCUSIN", 18 | /** 渲染状态 */ 19 | PAINTING: "PAINTING", 20 | } as const; 21 | 22 | export const APPLY_SOURCE = { 23 | /** 用户触发 默认值 */ 24 | USER: "USER", 25 | /** 远程触发 协同值 */ 26 | REMOTE: "REMOTE", 27 | /** History 模块触发 */ 28 | HISTORY: "HISTORY", 29 | }; 30 | 31 | export type ApplyOptions = { 32 | /** 操作源 */ 33 | source?: O.Values; 34 | /** 当前 Raw Range Modal */ 35 | range?: RawRange; 36 | /** 自动变换光标 */ 37 | autoCaret?: boolean; 38 | /** 39 | * 阻止标准化 Delta 40 | * - 需要调用前确保数据正确性 41 | * - 相关规则参考 normalizeDelta 方法 42 | */ 43 | preventNormalize?: boolean; 44 | /** 自动记录到 History */ 45 | undoable?: boolean; 46 | /** 额外携带的信息 */ 47 | extra?: unknown; 48 | }; 49 | 50 | export type ApplyResult = { 51 | /** 操作 id */ 52 | id: string; 53 | }; 54 | -------------------------------------------------------------------------------- /packages/utils/src/vars.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @usage *.scss; 3 | * 4 | * ```scss 5 | * @import "/node_modules//dist/style/vars.scss"; 6 | * .className { 7 | * @include mixinName; 8 | * } 9 | * ``` 10 | */ 11 | 12 | @mixin no-scrollbar { 13 | overflow: -moz-scrollbars-none; 14 | -ms-overflow-style: none; 15 | scrollbar-width: none; 16 | 17 | &::-webkit-scrollbar { 18 | display: none; 19 | } 20 | } 21 | 22 | @mixin min-scrollbar { 23 | // https://caniuse.com/?search=scrollbar-color 24 | scrollbar-color: #ddd transparent; 25 | scrollbar-width: thin; 26 | 27 | // https://caniuse.com/?search=-webkit-scrollbar 28 | &::-webkit-scrollbar { 29 | height: 6px; 30 | width: 6px; 31 | } 32 | 33 | &::-webkit-scrollbar-thumb { 34 | background: #ddd; 35 | border-radius: 6px; 36 | } 37 | 38 | &::-webkit-scrollbar-thumb:hover { 39 | background: #ccc; 40 | } 41 | } 42 | 43 | @mixin frame-box { 44 | background-color: var(--color-bg-1, #fff); 45 | border: 1px solid var(--color-border-2, rgb(229, 230, 235)); 46 | border-radius: 3px; 47 | box-shadow: 0 0 4px var(--color-border-2, rgb(229, 230, 235)); 48 | } 49 | 50 | @mixin text-ellipsis { 51 | overflow: hidden; 52 | text-overflow: ellipsis; 53 | white-space: nowrap; 54 | } 55 | -------------------------------------------------------------------------------- /blocks/x-core/test/lookup/marks.test.ts: -------------------------------------------------------------------------------- 1 | import type { Blocks } from "@block-kit/x-json"; 2 | 3 | import { BlockEditor } from "../../src"; 4 | import { getOpMetaMarks } from "../../src/lookup/utils/marks"; 5 | 6 | const getBlocks = (): Blocks => ({ 7 | root: { 8 | id: "root", 9 | version: 1, 10 | data: { type: "ROOT", children: ["child1"], parent: "" }, 11 | }, 12 | child1: { 13 | id: "child1", 14 | version: 1, 15 | data: { 16 | type: "text", 17 | children: [], 18 | delta: [ 19 | { insert: "text", attributes: { inline: "true" } }, 20 | { insert: "text2", attributes: { bold: "true", inline: "true" } }, 21 | { insert: "text3", attributes: { bold: "true" } }, 22 | ], 23 | parent: "root", 24 | }, 25 | }, 26 | }); 27 | 28 | describe("lookup marks", () => { 29 | it("collect inline mark", () => { 30 | const editor = new BlockEditor({ 31 | initial: getBlocks(), 32 | schema: { 33 | bold: { mark: true }, 34 | inline: { mark: true, inline: true }, 35 | }, 36 | }); 37 | const meta = editor.lookup.getLeafAtOffset("child1", 5); 38 | const attributes = getOpMetaMarks(editor, meta!); 39 | expect(attributes).toEqual({ bold: "true", inline: "true" }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /examples/stream/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@block-kit/stream", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./dist/lib/index.js", 6 | "types": "./dist/es/index.d.ts", 7 | "module": "./dist/es/index.js", 8 | "sideEffects": [ 9 | "**/*.css", 10 | "**/*.scss", 11 | "**/*.less" 12 | ], 13 | "files": [ 14 | "dist" 15 | ], 16 | "scripts": { 17 | "dev": "concurrently 'tsc -p tsconfig.build.json -w'", 18 | "build": "rm -rf dist && tsc -p tsconfig.build.json && tsc -p tsconfig.lib.json", 19 | "lint:ts": "tsc -p tsconfig.build.json --noEmit", 20 | "lint:circular": "madge --extensions js --circular ./dist" 21 | }, 22 | "author": "WindRunnerMax", 23 | "license": "MIT", 24 | "keywords": [ 25 | "editor", 26 | "wysiwyg", 27 | "rich-text" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/WindRunnerMax/BlockKit.git" 32 | }, 33 | "homepage": "https://github.com/WindRunnerMax/BlockKit", 34 | "bugs": { 35 | "url": "https://github.com/WindRunnerMax/BlockKit/issues" 36 | }, 37 | "dependencies": { 38 | "marked": "15.0.8", 39 | "@block-kit/delta": "workspace: *", 40 | "@block-kit/utils": "workspace: *" 41 | }, 42 | "devDependencies": { 43 | "concurrently": "8.2.2" 44 | } 45 | } -------------------------------------------------------------------------------- /examples/website/src/stream/index.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable selector-no-qualifying-type */ 2 | 3 | body { 4 | overflow: -moz-scrollbars-none; 5 | -ms-overflow-style: none; 6 | scrollbar-width: none; 7 | 8 | &::-webkit-scrollbar { 9 | display: none; 10 | } 11 | } 12 | 13 | .github-link { 14 | color: var(--color-text-1); 15 | font-size: 23px; 16 | position: fixed; 17 | right: 12px; 18 | top: 6px; 19 | } 20 | 21 | .stream-markdown-container-wrapper { 22 | padding: 0 20px; 23 | padding-top: 60px; 24 | 25 | .stream-markdown-title { 26 | font-size: 32px; 27 | font-weight: bold; 28 | margin: 0 auto; 29 | margin-bottom: 50px; 30 | text-align: center; 31 | user-select: none; 32 | } 33 | } 34 | 35 | .stream-markdown-container { 36 | display: flex; 37 | gap: 10px; 38 | 39 | >div { 40 | width: 100%; 41 | } 42 | 43 | div[data-node] { 44 | line-height: 1.8; 45 | margin: 8px 0; 46 | } 47 | 48 | div[data-placeholder] { 49 | line-height: 1.8; 50 | } 51 | } 52 | 53 | .stream-markdown-content { 54 | > .arco-textarea { 55 | min-height: 200px; 56 | } 57 | } 58 | 59 | .stream-markdown-exec { 60 | margin-top: 10px; 61 | } 62 | 63 | .stream-markdown-editor { 64 | border: 1px solid var(--color-border-2); 65 | padding: 5px 10px; 66 | } 67 | -------------------------------------------------------------------------------- /packages/utils/src/json.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Decode JSON String To Object 3 | * @param {string} value 4 | * @returns {T | null} 5 | */ 6 | export const decode = (value: string): T | null => { 7 | try { 8 | return JSON.parse(value) as T; 9 | } catch (error) { 10 | console.log("Decode JSON Error:", error); 11 | } 12 | return null; 13 | }; 14 | 15 | /** 16 | * Encode JSON Object To String 17 | * @param {unknown} value 18 | * @returns {string | null} 19 | */ 20 | export const encode = (value: unknown): string | null => { 21 | try { 22 | return JSON.stringify(value); 23 | } catch (error) { 24 | console.log("Encode JSON Error:", error); 25 | } 26 | return null; 27 | }; 28 | 29 | export const TSON = { 30 | /** 31 | * Decode JSON String To Object 32 | * @param {string} value 33 | * @returns {T | null} 34 | */ 35 | decode: decode, 36 | /** 37 | * Encode JSON Object To String 38 | * @param {unknown} value 39 | * @returns {string | null} 40 | */ 41 | encode: encode, 42 | /** 43 | * Parse JSON String To Object 44 | * @param {string} value 45 | * @returns {T | null} 46 | */ 47 | parse: decode, 48 | /** 49 | * Stringify JSON Object To String 50 | * @param {unknown} value 51 | * @returns {string | null} 52 | */ 53 | stringify: encode, 54 | }; 55 | -------------------------------------------------------------------------------- /packages/utils/src/vars.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @usage global singleton global.css; 3 | * 4 | * ```jsx 5 | * import "/node_modules//dist/style/views.css"; 6 | *
7 | * ``` 8 | */ 9 | 10 | /* ------ 动画 ------ */ 11 | @keyframes fade-in { 12 | from { 13 | opacity: 0; 14 | } 15 | 16 | to { 17 | opacity: 1; 18 | } 19 | } 20 | 21 | /* ------ Flex ------ */ 22 | 23 | .flex-center { 24 | align-items: center; 25 | display: flex; 26 | justify-content: center; 27 | } 28 | 29 | .flex-align-center { 30 | align-items: center; 31 | display: flex; 32 | } 33 | 34 | .flex-justify-center { 35 | display: flex; 36 | justify-content: center; 37 | } 38 | 39 | .flex-justify-between { 40 | display: flex; 41 | justify-content: space-between; 42 | } 43 | 44 | /* ------ 文本 ------ */ 45 | 46 | .text-ellipsis { 47 | overflow: hidden; 48 | text-overflow: ellipsis; 49 | white-space: nowrap; 50 | } 51 | 52 | /* ------ 盒子 ------ */ 53 | 54 | .full-screen { 55 | height: 100%; 56 | width: 100%; 57 | } 58 | 59 | .frame-box { 60 | background-color: var(--color-bg-1, #fff); 61 | border: 1px solid var(--color-border-2, rgb(229, 230, 235)); 62 | border-radius: 3px; 63 | box-shadow: 0 0 4px var(--color-border-2, rgb(229, 230, 235)); 64 | } 65 | 66 | /* ------ --- ------ */ 67 | -------------------------------------------------------------------------------- /blocks/x-core/src/lookup/utils/marks.ts: -------------------------------------------------------------------------------- 1 | import type { AttributeMap } from "@block-kit/delta"; 2 | import { isEOLOp } from "@block-kit/delta"; 3 | 4 | import type { BlockEditor } from "../../editor"; 5 | import type { OpMeta } from "../types"; 6 | 7 | /** 8 | * 过滤需要追踪的属性 9 | * - mark: 输入时会自动追踪样式的节点 10 | * - mark + inline: 不追踪末尾 Mark 11 | * @param editor 编辑器实例 12 | * @param meta Op 元信息 13 | */ 14 | export const getOpMetaMarks = (editor: BlockEditor, meta: OpMeta): AttributeMap | undefined => { 15 | const { op, tail, ops } = meta; 16 | if (!op || !op.insert || !op.attributes || isEOLOp(op)) { 17 | return void 0; 18 | } 19 | const attrs = op.attributes; 20 | const keys = Object.keys(attrs); 21 | const result: AttributeMap = {}; 22 | for (const key of keys) { 23 | // 当前节点为 void 时, 不需要处理文本 24 | if (editor.schema.void.has(key)) { 25 | return void 0; 26 | } 27 | if (editor.schema.mark.has(key) && attrs[key]) { 28 | result[key] = attrs[key]; 29 | } 30 | if (tail && editor.schema.inline.has(key)) { 31 | const next = ops[meta.index + 1]; 32 | // 如果下个节点存在相同的属性, 则仍然需要追加属性 33 | if (next && next.attributes && next.attributes[key]) { 34 | continue; 35 | } 36 | delete result[key]; 37 | } 38 | } 39 | return Object.keys(result).length ? result : void 0; 40 | }; 41 | -------------------------------------------------------------------------------- /packages/delta/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@block-kit/delta", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./dist/lib/index.js", 6 | "types": "./dist/es/index.d.ts", 7 | "module": "./dist/es/index.js", 8 | "sideEffects": false, 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.lib.json", 14 | "test": "jest", 15 | "lint:ts": "npm run build -- --noEmit", 16 | "lint:circular": "madge --extensions js --circular ./dist" 17 | }, 18 | "author": "WindRunnerMax", 19 | "license": "MIT", 20 | "keywords": [ 21 | "editor", 22 | "wysiwyg", 23 | "rich-text" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/WindRunnerMax/BlockKit.git" 28 | }, 29 | "homepage": "https://github.com/WindRunnerMax/BlockKit", 30 | "bugs": { 31 | "url": "https://github.com/WindRunnerMax/BlockKit/issues" 32 | }, 33 | "dependencies": { 34 | "fast-diff": "1.3.0", 35 | "@block-kit/utils": "workspace: *" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "7.20.12", 39 | "@babel/preset-env": "7.20.2", 40 | "@babel/preset-typescript": "7.18.6", 41 | "@types/jest": "29.4.0", 42 | "babel-jest": "29.4.1", 43 | "jest": "29.4.1", 44 | "ts-jest": "29.1.2" 45 | } 46 | } -------------------------------------------------------------------------------- /packages/plugin/src/toolbar/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../shared/styles/variable'; 2 | 3 | .block-kit-menu-toolbar { 4 | border-bottom: 1px solid var(--color-border-3); 5 | display: flex; 6 | padding: 6px 5px; 7 | user-select: none; 8 | 9 | .menu-toolbar-item { 10 | align-items: center; 11 | border-radius: 3px; 12 | color: var(--color-text-1); 13 | cursor: pointer; 14 | display: flex; 15 | margin: 0 3px; 16 | padding: 3px 7px; 17 | 18 | &.active, 19 | &:hover { 20 | background-color: var(--color-fill-2); 21 | color: var(--color-text-1); 22 | } 23 | 24 | &.disable { 25 | background-color: unset; 26 | color: var(--color-text-3); 27 | } 28 | } 29 | 30 | .menu-toolbar-icon-down { 31 | font-size: 12px; 32 | margin-left: 3px; 33 | } 34 | } 35 | 36 | .block-kit-toolbar-dropdown { 37 | @include frame-region; 38 | 39 | color: var(--color-text-1); 40 | 41 | > .kit-toolbar-node { 42 | align-items: center; 43 | cursor: pointer; 44 | display: flex; 45 | font-size: 12px; 46 | height: 30px; 47 | justify-content: center; 48 | margin: 5px; 49 | width: 70px; 50 | 51 | > .arco-icon { 52 | font-size: 13px; 53 | } 54 | 55 | &:hover { 56 | background-color: var(--color-fill-2); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: "module", 4 | }, 5 | extends: ["eslint:recommended", "plugin:prettier/recommended"], 6 | overrides: [ 7 | { 8 | files: ["*.ts"], 9 | parser: "@typescript-eslint/parser", 10 | plugins: ["@typescript-eslint", "simple-import-sort"], 11 | extends: ["plugin:@typescript-eslint/recommended"], 12 | }, 13 | { 14 | files: ["*.tsx"], 15 | parser: "@typescript-eslint/parser", 16 | plugins: ["react", "react-hooks", "@typescript-eslint/eslint-plugin", "simple-import-sort"], 17 | extends: ["plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"], 18 | }, 19 | ], 20 | env: { 21 | browser: true, 22 | node: true, 23 | commonjs: true, 24 | es2021: true, 25 | }, 26 | ignorePatterns: ["node_modules", "build", "dist", "coverage", "public", "*.html"], 27 | rules: { 28 | "semi": "error", 29 | "quote-props": ["error", "consistent-as-needed"], 30 | "arrow-parens": ["error", "as-needed"], 31 | "no-var": "error", 32 | "prefer-const": "error", 33 | "no-console": "off", 34 | "@typescript-eslint/consistent-type-imports": "error", 35 | "simple-import-sort/imports": "error", 36 | "simple-import-sort/exports": "error", 37 | "no-shadow": "error", 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/core/test/selection/walker.test.ts: -------------------------------------------------------------------------------- 1 | import { Delta } from "@block-kit/delta"; 2 | 3 | import { Editor, LEAF_STRING, LOG_LEVEL, ZERO_SPACE_KEY } from "../../src"; 4 | import { createTextNodeWalker } from "../../src/selection/utils/native"; 5 | import { mountEditorViewModel } from "../config/view"; 6 | 7 | describe("walker", () => { 8 | it("text leaves", () => { 9 | const delta = new Delta({ 10 | ops: [ 11 | { insert: "text" }, 12 | { insert: " ", attributes: { mention: " ", text: "123456" } }, 13 | { insert: " ", attributes: { mention: " ", text: "789" } }, 14 | { insert: "\n" }, 15 | ], 16 | }); 17 | const editor = new Editor({ 18 | delta, 19 | logLevel: LOG_LEVEL.INFO, 20 | schema: { 21 | mention: { inline: true, void: true }, 22 | }, 23 | }); 24 | const { lineDOMs } = mountEditorViewModel(editor); 25 | const container = lineDOMs[0]; 26 | const walker = createTextNodeWalker(container); 27 | const selector = `span[${LEAF_STRING}], span[${ZERO_SPACE_KEY}]`; 28 | const leaves = container.querySelectorAll(selector); 29 | let i = 0; 30 | for (const [node, nextNode] of walker) { 31 | expect(node === leaves[i]).toBe(true); 32 | expect(nextNode === (leaves[i + 1] || null)).toBe(true); 33 | i++; 34 | } 35 | }); 36 | }); 37 | --------------------------------------------------------------------------------