mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-22 20:41:50 +08:00
refactor(editor): unify directories naming (#11516)
**Directory Structure Changes** - Renamed multiple block-related directories by removing the "block-" prefix: - `block-attachment` → `attachment` - `block-bookmark` → `bookmark` - `block-callout` → `callout` - `block-code` → `code` - `block-data-view` → `data-view` - `block-database` → `database` - `block-divider` → `divider` - `block-edgeless-text` → `edgeless-text` - `block-embed` → `embed`
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { LatexMarkdownAdapterExtensions } from './markdown/index.js';
|
||||
import { LatexBlockNotionHtmlAdapterExtension } from './notion-html.js';
|
||||
import { LatexBlockPlainTextAdapterExtension } from './plain-text.js';
|
||||
|
||||
export const LatexBlockAdapterExtensions: ExtensionType[] = [
|
||||
LatexMarkdownAdapterExtensions,
|
||||
LatexBlockNotionHtmlAdapterExtension,
|
||||
LatexBlockPlainTextAdapterExtension,
|
||||
].flat();
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './markdown/index.js';
|
||||
export * from './notion-html.js';
|
||||
export * from './plain-text.js';
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { LatexBlockMarkdownAdapterExtension } from './markdown.js';
|
||||
import { LatexMarkdownPreprocessorExtension } from './preprocessor.js';
|
||||
|
||||
export * from './markdown.js';
|
||||
export * from './preprocessor.js';
|
||||
|
||||
export const LatexMarkdownAdapterExtensions: ExtensionType[] = [
|
||||
LatexMarkdownPreprocessorExtension,
|
||||
LatexBlockMarkdownAdapterExtension,
|
||||
];
|
||||
@@ -0,0 +1,55 @@
|
||||
import { LatexBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
const isLatexNode = (node: MarkdownAST) => node.type === 'math';
|
||||
|
||||
export const latexBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
|
||||
flavour: LatexBlockSchema.model.flavour,
|
||||
toMatch: o => isLatexNode(o.node),
|
||||
fromMatch: o => o.node.flavour === LatexBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const latex = 'value' in o.node ? o.node.value : '';
|
||||
const { walkerContext } = context;
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: 'affine:latex',
|
||||
props: {
|
||||
latex,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const latex =
|
||||
'latex' in o.node.props ? (o.node.props.latex as string) : '';
|
||||
const { walkerContext } = context;
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'math',
|
||||
value: latex,
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LatexBlockMarkdownAdapterExtension = BlockMarkdownAdapterExtension(
|
||||
latexBlockMarkdownAdapterMatcher
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
type MarkdownAdapterPreprocessor,
|
||||
MarkdownPreprocessorExtension,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
const latexPreprocessor: MarkdownAdapterPreprocessor = {
|
||||
name: 'latex',
|
||||
levels: ['block', 'slice', 'doc'],
|
||||
preprocess: content => {
|
||||
// Replace block-level LaTeX delimiters \[ \] with $$ $$
|
||||
const blockProcessedContent = content.replace(
|
||||
/\\\[(.*?)\\\]/gs,
|
||||
(_, equation) => `$$${equation}$$`
|
||||
);
|
||||
// Replace inline LaTeX delimiters \( \) with $ $
|
||||
const inlineProcessedContent = blockProcessedContent.replace(
|
||||
/\\\((.*?)\\\)/gs,
|
||||
(_, equation) => `$${equation}$`
|
||||
);
|
||||
return inlineProcessedContent;
|
||||
},
|
||||
};
|
||||
|
||||
export const LatexMarkdownPreprocessorExtension =
|
||||
MarkdownPreprocessorExtension(latexPreprocessor);
|
||||
@@ -0,0 +1,50 @@
|
||||
import { LatexBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockNotionHtmlAdapterExtension,
|
||||
type BlockNotionHtmlAdapterMatcher,
|
||||
HastUtils,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
export const latexBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatcher =
|
||||
{
|
||||
flavour: LatexBlockSchema.model.flavour,
|
||||
toMatch: o => {
|
||||
return (
|
||||
HastUtils.isElement(o.node) &&
|
||||
o.node.tagName === 'figure' &&
|
||||
!!HastUtils.querySelector(o.node, '.equation-container')
|
||||
);
|
||||
},
|
||||
fromMatch: () => false,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
if (!HastUtils.isElement(o.node)) {
|
||||
return;
|
||||
}
|
||||
const { walkerContext } = context;
|
||||
const latex = HastUtils.getTextContent(
|
||||
HastUtils.querySelector(o.node, 'annotation')
|
||||
);
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: LatexBlockSchema.model.flavour,
|
||||
props: {
|
||||
latex,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
walkerContext.skipAllChildren();
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {},
|
||||
};
|
||||
|
||||
export const LatexBlockNotionHtmlAdapterExtension =
|
||||
BlockNotionHtmlAdapterExtension(latexBlockNotionHtmlAdapterMatcher);
|
||||
@@ -0,0 +1,29 @@
|
||||
import { LatexBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockPlainTextAdapterExtension,
|
||||
type BlockPlainTextAdapterMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
const latexPrefix = 'LaTex, with value: ';
|
||||
|
||||
export const latexBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher = {
|
||||
flavour: LatexBlockSchema.model.flavour,
|
||||
toMatch: () => false,
|
||||
fromMatch: o => o.node.flavour === LatexBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const latex =
|
||||
'latex' in o.node.props ? (o.node.props.latex as string) : '';
|
||||
|
||||
const { textBuffer } = context;
|
||||
if (latex) {
|
||||
textBuffer.content += `${latexPrefix}${latex}`;
|
||||
textBuffer.content += '\n';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LatexBlockPlainTextAdapterExtension =
|
||||
BlockPlainTextAdapterExtension(latexBlockPlainTextAdapterMatcher);
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { LatexProps } from '@blocksuite/affine-model';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { LatexBlockComponent } from './latex-block.js';
|
||||
|
||||
export const insertLatexBlockCommand: Command<
|
||||
{
|
||||
latex?: string;
|
||||
place?: 'after' | 'before';
|
||||
removeEmptyLine?: boolean;
|
||||
selectedModels?: BlockModel[];
|
||||
},
|
||||
{
|
||||
insertedLatexBlockId: Promise<string>;
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
const { selectedModels, latex, place, removeEmptyLine, std } = ctx;
|
||||
if (!selectedModels?.length) return;
|
||||
|
||||
const targetModel =
|
||||
place === 'before'
|
||||
? selectedModels[0]
|
||||
: selectedModels[selectedModels.length - 1];
|
||||
|
||||
const latexBlockProps: Partial<LatexProps> & {
|
||||
flavour: 'affine:latex';
|
||||
} = {
|
||||
flavour: 'affine:latex',
|
||||
latex: latex ?? '',
|
||||
};
|
||||
|
||||
const result = std.store.addSiblingBlocks(
|
||||
targetModel,
|
||||
[latexBlockProps],
|
||||
place
|
||||
);
|
||||
if (result.length === 0) return;
|
||||
|
||||
if (removeEmptyLine && targetModel.text?.length === 0) {
|
||||
std.store.deleteBlock(targetModel);
|
||||
}
|
||||
|
||||
next({
|
||||
insertedLatexBlockId: std.host.updateComplete.then(async () => {
|
||||
if (!latex) {
|
||||
const blockComponent = std.view.getBlock(result[0]);
|
||||
if (blockComponent instanceof LatexBlockComponent) {
|
||||
await blockComponent.updateComplete;
|
||||
blockComponent.toggleEditor();
|
||||
}
|
||||
}
|
||||
return result[0];
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { insertInlineLatex } from '@blocksuite/affine-inline-latex';
|
||||
import {
|
||||
getSelectedModelsCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
|
||||
import { TeXIcon } from '@blocksuite/icons/lit';
|
||||
|
||||
import { insertLatexBlockCommand } from '../commands';
|
||||
import { LatexTooltip } from './tooltips';
|
||||
|
||||
export const latexSlashMenuConfig: SlashMenuConfig = {
|
||||
items: [
|
||||
{
|
||||
name: 'Inline equation',
|
||||
group: '0_Basic@8',
|
||||
description: 'Create a inline equation.',
|
||||
icon: TeXIcon(),
|
||||
tooltip: {
|
||||
figure: LatexTooltip(
|
||||
'Energy. Mass. Light. In a single equation,',
|
||||
'E=mc^2',
|
||||
false
|
||||
),
|
||||
caption: 'Inline equation',
|
||||
},
|
||||
searchAlias: ['inlineMath, inlineEquation', 'inlineLatex'],
|
||||
action: ({ std }) => {
|
||||
std.command
|
||||
.chain()
|
||||
.pipe(getTextSelectionCommand)
|
||||
.pipe(insertInlineLatex)
|
||||
.run();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Equation',
|
||||
description: 'Create a equation block.',
|
||||
icon: TeXIcon(),
|
||||
tooltip: {
|
||||
figure: LatexTooltip(
|
||||
'Create a equation via LaTeX.',
|
||||
String.raw`\frac{a}{b} \pm \frac{c}{d} = \frac{ad \pm bc}{bd}`,
|
||||
true
|
||||
),
|
||||
caption: 'Equation',
|
||||
},
|
||||
searchAlias: ['mathBlock, equationBlock', 'latexBlock'],
|
||||
group: '4_Content & Media@10',
|
||||
action: ({ std }) => {
|
||||
std.command
|
||||
.chain()
|
||||
.pipe(getSelectedModelsCommand)
|
||||
.pipe(insertLatexBlockCommand, {
|
||||
place: 'after',
|
||||
removeEmptyLine: true,
|
||||
})
|
||||
.run();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import katex from 'katex';
|
||||
import { html } from 'lit';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
|
||||
export const LatexTooltip = (
|
||||
str: string,
|
||||
latex: string,
|
||||
displayMode: boolean = false
|
||||
) =>
|
||||
html` <style>
|
||||
.latex-tooltip {
|
||||
background: ${unsafeCSSVarV2('layer/pureWhite')};
|
||||
border-radius: 2px;
|
||||
width: 170px;
|
||||
padding: 5px 5px 5px 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.latex-tooltip-content {
|
||||
width: 159px;
|
||||
color: #121212;
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
|
||||
.katex > math[display='block'] {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="latex-tooltip">
|
||||
<div class="latex-tooltip-content">
|
||||
<span>${str}</span>
|
||||
${unsafeHTML(
|
||||
katex.renderToString(latex, {
|
||||
displayMode,
|
||||
output: 'mathml',
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { LatexBlockComponent } from './latex-block';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('affine-latex', LatexBlockComponent);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type * as RemarkMathType from 'remark-math';
|
||||
|
||||
export * from './adapters';
|
||||
export * from './commands';
|
||||
export * from './latex-block';
|
||||
export * from './latex-spec';
|
||||
|
||||
// Global types
|
||||
declare type _GLOBAl = typeof RemarkMathType;
|
||||
@@ -0,0 +1,158 @@
|
||||
import { selectBlock } from '@blocksuite/affine-block-note';
|
||||
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
|
||||
import { createLitPortal } from '@blocksuite/affine-components/portal';
|
||||
import type { LatexBlockModel } from '@blocksuite/affine-model';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import type { Placement } from '@floating-ui/dom';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import katex from 'katex';
|
||||
import { html, render } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
|
||||
import { latexBlockStyles } from './styles.js';
|
||||
|
||||
export class LatexBlockComponent extends CaptionedBlockComponent<LatexBlockModel> {
|
||||
static override styles = latexBlockStyles;
|
||||
|
||||
private _editorAbortController: AbortController | null = null;
|
||||
|
||||
get editorPlacement(): Placement {
|
||||
return 'bottom';
|
||||
}
|
||||
|
||||
get isBlockSelected() {
|
||||
const blockSelection = this.selection.filter(BlockSelection);
|
||||
return blockSelection.some(
|
||||
selection => selection.blockId === this.model.id
|
||||
);
|
||||
}
|
||||
|
||||
override firstUpdated(props: Map<string, unknown>) {
|
||||
super.firstUpdated(props);
|
||||
|
||||
const { disposables } = this;
|
||||
|
||||
this._editorAbortController?.abort();
|
||||
this._editorAbortController = new AbortController();
|
||||
disposables.add(() => {
|
||||
this._editorAbortController?.abort();
|
||||
});
|
||||
|
||||
const katexContainer = this._katexContainer;
|
||||
if (!katexContainer) return;
|
||||
|
||||
disposables.add(
|
||||
effect(() => {
|
||||
const latex = this.model.props.latex$.value;
|
||||
|
||||
katexContainer.replaceChildren();
|
||||
// @ts-expect-error lit hack won't fix
|
||||
delete katexContainer['_$litPart$'];
|
||||
|
||||
if (latex.length === 0) {
|
||||
render(
|
||||
html`<span class="latex-block-empty-placeholder">Equation</span>`,
|
||||
katexContainer
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
katex.render(latex, katexContainer, {
|
||||
displayMode: true,
|
||||
output: 'mathml',
|
||||
});
|
||||
} catch {
|
||||
katexContainer.replaceChildren();
|
||||
// @ts-expect-error lit hack won't fix
|
||||
delete katexContainer['_$litPart$'];
|
||||
render(
|
||||
html`<span class="latex-block-error-placeholder"
|
||||
>Error equation</span
|
||||
>`,
|
||||
katexContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.addFromEvent(this, 'click', () => {
|
||||
// should not open editor or select block in readonly mode
|
||||
if (this.doc.readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isBlockSelected) {
|
||||
this.toggleEditor();
|
||||
} else {
|
||||
this.selectBlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeEditor(portal: HTMLDivElement) {
|
||||
portal.remove();
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
return html`
|
||||
<div contenteditable="false" class="latex-block-container">
|
||||
<div class="katex"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
selectBlock() {
|
||||
this.host.command.exec(selectBlock, {
|
||||
focusBlock: this,
|
||||
});
|
||||
}
|
||||
|
||||
toggleEditor() {
|
||||
const katexContainer = this._katexContainer;
|
||||
if (!katexContainer) return;
|
||||
|
||||
this._editorAbortController?.abort();
|
||||
this._editorAbortController = new AbortController();
|
||||
|
||||
this.selection.setGroup('note', []);
|
||||
|
||||
const portal = createLitPortal({
|
||||
template: html`<latex-editor-menu
|
||||
.std=${this.std}
|
||||
.latexSignal=${this.model.props.latex$}
|
||||
.abortController=${this._editorAbortController}
|
||||
></latex-editor-menu>`,
|
||||
container: this.host,
|
||||
computePosition: {
|
||||
referenceElement: this,
|
||||
placement: this.editorPlacement,
|
||||
autoUpdate: {
|
||||
animationFrame: true,
|
||||
},
|
||||
},
|
||||
closeOnClickAway: true,
|
||||
abortController: this._editorAbortController,
|
||||
shadowDom: false,
|
||||
portalStyles: {
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
},
|
||||
});
|
||||
|
||||
this._editorAbortController.signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
this.removeEditor(portal);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
|
||||
@query('.latex-block-container')
|
||||
private accessor _katexContainer!: HTMLDivElement;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-latex': LatexBlockComponent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
|
||||
import { BlockViewExtension } from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { LatexBlockAdapterExtensions } from './adapters/extension.js';
|
||||
import { latexSlashMenuConfig } from './configs/slash-menu.js';
|
||||
|
||||
export const LatexBlockSpec: ExtensionType[] = [
|
||||
BlockViewExtension('affine:latex', literal`affine-latex`),
|
||||
LatexBlockAdapterExtensions,
|
||||
SlashMenuConfigExtension('affine:latex', latexSlashMenuConfig),
|
||||
].flat();
|
||||
@@ -0,0 +1,40 @@
|
||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { css } from 'lit';
|
||||
|
||||
export const latexBlockStyles = css`
|
||||
.latex-block-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px 24px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.latex-block-container:hover {
|
||||
background: ${unsafeCSSVar('hoverColor')};
|
||||
}
|
||||
|
||||
.latex-block-error-placeholder {
|
||||
color: ${unsafeCSSVarV2('text/highlight/fg/red')};
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.latex-block-empty-placeholder {
|
||||
color: ${unsafeCSSVarV2('text/secondary')};
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
user-select: none;
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user