diff --git a/frontend/appflowy_web_app/index.html b/frontend/appflowy_web_app/index.html index 6c77f0b7ba5e6..5c5190a596462 100644 --- a/frontend/appflowy_web_app/index.html +++ b/frontend/appflowy_web_app/index.html @@ -27,12 +27,12 @@ -
+ + + + diff --git a/frontend/appflowy_web_app/src/application/database-yjs/context.ts b/frontend/appflowy_web_app/src/application/database-yjs/context.ts index cbac5bbd45e22..e0eb34c3bdf57 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/context.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/context.ts @@ -5,6 +5,7 @@ import * as Y from 'yjs'; export interface DatabaseContextState { readOnly: boolean; + isDark?: boolean; databaseDoc: YDoc; viewId: string; rowDocMap: Y.Map; diff --git a/frontend/appflowy_web_app/src/components/database/Database.tsx b/frontend/appflowy_web_app/src/components/database/Database.tsx index 4b3964e240bb4..1c4eabe2f610d 100644 --- a/frontend/appflowy_web_app/src/components/database/Database.tsx +++ b/frontend/appflowy_web_app/src/components/database/Database.tsx @@ -11,13 +11,14 @@ import { DatabaseContextProvider } from './DatabaseContext'; export interface Database2Props extends ViewMetaProps { doc: YDoc; + isDark?: boolean; getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map; destroy: () => void }>; loadView?: (viewId: string) => Promise; navigateToView?: (viewId: string) => Promise; loadViewMeta?: (viewId: string) => Promise; } -function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView, ...viewMeta }: Database2Props) { +function Database({ doc, isDark, getViewRowsMap, navigateToView, loadViewMeta, loadView, ...viewMeta }: Database2Props) { const [search, setSearch] = useSearchParams(); const viewId = search.get('v') || viewMeta.viewId; @@ -73,6 +74,7 @@ function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView,
}> Promise; loadViewMeta?: (viewId: string) => Promise; loadView?: (viewId: string) => Promise; @@ -20,6 +21,7 @@ export const Document = ({ navigateToView, loadViewMeta, getViewRowsMap, + isDark, ...viewMeta }: DocumentProps) => { return ( @@ -28,6 +30,7 @@ export const Document = ({ }>
{ const { readOnly } = useEditorContext(); const codeDecorate = useDecorate(editor); - const decorate = useCallback( - (entry: NodeEntry) => { - return [...codeDecorate(entry)]; - }, - [codeDecorate] - ); - - const renderElement = useCallback( - (props: RenderElementProps) => ( + const renderElement = useCallback((props: RenderElementProps) => { + return ( }> - ), - [] - ); + ); + }, []); return ( <> { + const decoration = codeDecorate?.(entry); + + return decoration || []; + }} className={'px-16 outline-none focus:outline-none max-md:px-4'} renderLeaf={Leaf} renderElement={renderElement} diff --git a/frontend/appflowy_web_app/src/components/editor/Editor.tsx b/frontend/appflowy_web_app/src/components/editor/Editor.tsx index 245e6b0504f34..421dc4b68980c 100644 --- a/frontend/appflowy_web_app/src/components/editor/Editor.tsx +++ b/frontend/appflowy_web_app/src/components/editor/Editor.tsx @@ -9,8 +9,19 @@ export interface EditorProps extends EditorContextState { } export const Editor = memo(({ doc, layoutStyle = defaultLayoutStyle, ...props }: EditorProps) => { + const [codeGrammars, setCodeGrammars] = React.useState>({}); + + const handleAddCodeGrammars = React.useCallback((blockId: string, grammar: string) => { + setCodeGrammars((prev) => ({ ...prev, [blockId]: grammar })); + }, []); + return ( - + ); diff --git a/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx b/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx index db1276250eba7..1b2650dcfce99 100644 --- a/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx +++ b/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx @@ -17,7 +17,10 @@ export const defaultLayoutStyle: EditorLayoutStyle = { export interface EditorContextState { readOnly: boolean; + isDark: boolean; layoutStyle?: EditorLayoutStyle; + codeGrammars?: Record; + addCodeGrammars?: (blockId: string, grammar: string) => void; navigateToView?: (viewId: string) => Promise; loadViewMeta?: (viewId: string) => Promise; loadView?: (viewId: string) => Promise; @@ -27,6 +30,8 @@ export interface EditorContextState { export const EditorContext = createContext({ readOnly: true, layoutStyle: defaultLayoutStyle, + codeGrammars: {}, + isDark: false, }); export const EditorContextProvider = ({ children, ...props }: EditorContextState & { children: React.ReactNode }) => { diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.hooks.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.hooks.ts index b7bb3500af6f5..8f3b3f99de5e6 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.hooks.ts +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.hooks.ts @@ -1,11 +1,43 @@ import { CodeNode } from '@/components/editor/editor.type'; -import { useCallback } from 'react'; +import { useEditorContext } from '@/components/editor/EditorContext'; +import { useCallback, useEffect } from 'react'; import { ReactEditor, useSlateStatic } from 'slate-react'; import { Element as SlateElement, Transforms } from 'slate'; +const Prism = window.Prism; +const hljs = window.hljs; + export function useCodeBlock(node: CodeNode) { const language = node.data.language; const editor = useSlateStatic() as ReactEditor; + + const addCodeGrammars = useEditorContext().addCodeGrammars; + + useEffect(() => { + const path = ReactEditor.findPath(editor, node); + let detectedLanguage = language; + + if (!language) { + const codeSnippet = editor.string(path); + + detectedLanguage = hljs.highlightAuto(codeSnippet).language; + } + + const prismLanguage = Prism.languages[detectedLanguage.toLowerCase()]; + + if (!prismLanguage) { + const script = document.createElement('script'); + + script.src = `https://cdnjs.cloudflare.com/ajax/libs/prism/1.26.0/components/prism-${detectedLanguage.toLowerCase()}.min.js`; + document.head.appendChild(script); + script.onload = () => { + addCodeGrammars?.(node.blockId, detectedLanguage); + }; + } else { + addCodeGrammars?.(node.blockId, detectedLanguage); + } + }, [addCodeGrammars, editor, language, node]); + const handleChangeLanguage = useCallback( (newLang: string) => { const path = ReactEditor.findPath(editor, node); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx index deb17cc7e61c0..b26d735db55a1 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx @@ -57,3 +57,4 @@ export const CodeBlock = memo( }), (prevProps, nextProps) => JSON.stringify(prevProps.node) === JSON.stringify(nextProps.node) ); +export default CodeBlock; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/SelectLanguage.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/SelectLanguage.tsx index f249a19951cb1..c3c05ef36fb73 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/SelectLanguage.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/SelectLanguage.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; function SelectLanguage({ readOnly, - language = 'json', + language = 'Auto', }: { readOnly?: boolean; language: string; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/useDecorate.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/useDecorate.ts index 1ec1a2e980edb..4b94a31226690 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/useDecorate.ts +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/useDecorate.ts @@ -1,13 +1,18 @@ import { BlockType } from '@/application/collab.type'; -import { decorateCode } from '@/components/editor/components/blocks/code/utils'; +import { useEditorContext } from '@/components/editor/EditorContext'; +import { decorateCode } from './utils'; import { CodeNode } from '@/components/editor/editor.type'; -import { useCallback } from 'react'; +import { useMemo } from 'react'; import { BaseRange, Editor, NodeEntry, Element } from 'slate'; import { ReactEditor } from 'slate-react'; export function useDecorate(editor: ReactEditor) { - return useCallback( - (entry: NodeEntry): BaseRange[] => { + const grammars = useEditorContext().codeGrammars; + const isDark = useEditorContext().isDark; + + return useMemo(() => { + return (entry: NodeEntry): BaseRange[] => { + if (!entry) return []; const path = entry[1]; const blockEntry = editor.above({ @@ -20,14 +25,11 @@ export function useDecorate(editor: ReactEditor) { const block = blockEntry[0] as CodeNode; - if (block.type === BlockType.CodeBlock) { - const language = block.data.language; - - return decorateCode(entry, language, false); + if (block.type === BlockType.CodeBlock && grammars?.[block.blockId]) { + return decorateCode(entry, grammars[block.blockId], isDark); } return []; - }, - [editor] - ); + }; + }, [editor, grammars, isDark]); } diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/utils.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/utils.ts index 458d9e8d7bcf8..fb22c6ce9256b 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/utils.ts +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/utils.ts @@ -1,46 +1,9 @@ -import Prism from 'prismjs'; - -import 'prismjs/components/prism-bash'; -import 'prismjs/components/prism-basic'; -import 'prismjs/components/prism-c'; -import 'prismjs/components/prism-clojure'; -import 'prismjs/components/prism-cpp'; -import 'prismjs/components/prism-csp'; -import 'prismjs/components/prism-css'; -import 'prismjs/components/prism-dart'; -import 'prismjs/components/prism-elixir'; -import 'prismjs/components/prism-elm'; -import 'prismjs/components/prism-erlang'; -import 'prismjs/components/prism-fortran'; -import 'prismjs/components/prism-go'; -import 'prismjs/components/prism-graphql'; -import 'prismjs/components/prism-haskell'; -import 'prismjs/components/prism-java'; -import 'prismjs/components/prism-javascript'; -import 'prismjs/components/prism-json'; -import 'prismjs/components/prism-kotlin'; -import 'prismjs/components/prism-lisp'; -import 'prismjs/components/prism-lua'; -import 'prismjs/components/prism-markdown'; -import 'prismjs/components/prism-matlab'; -import 'prismjs/components/prism-ocaml'; -import 'prismjs/components/prism-perl'; -import 'prismjs/components/prism-php'; -import 'prismjs/components/prism-powershell'; -import 'prismjs/components/prism-python'; -import 'prismjs/components/prism-r'; -import 'prismjs/components/prism-ruby'; -import 'prismjs/components/prism-rust'; -import 'prismjs/components/prism-scala'; -import 'prismjs/components/prism-shell-session'; -import 'prismjs/components/prism-sql'; -import 'prismjs/components/prism-swift'; -import 'prismjs/components/prism-typescript'; -import 'prismjs/components/prism-xml-doc'; -import 'prismjs/components/prism-yaml'; - import { BaseRange, NodeEntry, Text, Path } from 'slate'; +const Prism = window.Prism; + +Prism.plugins.autoloader.languages_path = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.26.0/components/'; + const push_string = ( token: string | Prism.Token, path: Path, @@ -121,17 +84,21 @@ export const decorateCode = ([node, path]: NodeEntry, language: string, isDark: return ranges; } - try { - const tokens = Prism.tokenize(node.text, Prism.languages[language.toLowerCase()]); + const highlightCode = (code: string, language: string) => { + try { + const tokens = Prism.tokenize(code, language); + + let start = 0; - let start = 0; + for (const token of tokens) { + start = recurseTokenize(token, path, ranges, start) || 0; + } - for (const token of tokens) { - start = recurseTokenize(token, path, ranges, start) || 0; + return ranges; + } catch { + return ranges; } + }; - return ranges; - } catch { - return ranges; - } + return highlightCode(node.text, Prism.languages[language.toLowerCase()]); }; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx index 7a0f36b1340ce..8a500c16895c0 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx @@ -2,30 +2,27 @@ import Placeholder from '@/components/editor/components/blocks/text/Placeholder' import { useSlateStatic } from 'slate-react'; import { useStartIcon } from './StartIcon.hooks'; import { EditorElementProps, TextNode } from '@/components/editor/editor.type'; -import React, { forwardRef, memo, useMemo } from 'react'; +import React, { forwardRef, useMemo } from 'react'; -export const Text = memo( - forwardRef>( - ({ node, children, className: classNameProp, ...attributes }, ref) => { - const { hasStartIcon, renderIcon } = useStartIcon(node); - const editor = useSlateStatic(); - const isEmpty = editor.isEmpty(node); - const className = useMemo(() => { - const classList = ['text-element', 'relative', 'flex', 'w-full', 'whitespace-pre-wrap', 'break-word', 'px-1']; +export const Text = forwardRef>( + ({ node, children, className: classNameProp, ...attributes }, ref) => { + const { hasStartIcon, renderIcon } = useStartIcon(node); + const editor = useSlateStatic(); + const isEmpty = editor.isEmpty(node); + const className = useMemo(() => { + const classList = ['text-element', 'relative', 'flex', 'w-full', 'whitespace-pre-wrap', 'break-word', 'px-1']; - if (classNameProp) classList.push(classNameProp); - if (hasStartIcon) classList.push('has-start-icon'); - return classList.join(' '); - }, [classNameProp, hasStartIcon]); + if (classNameProp) classList.push(classNameProp); + if (hasStartIcon) classList.push('has-start-icon'); + return classList.join(' '); + }, [classNameProp, hasStartIcon]); - return ( - - {renderIcon()} - {isEmpty && } - {children} - - ); - } - ), - (prevProps, nextProps) => JSON.stringify(prevProps.node) === JSON.stringify(nextProps.node) + return ( + + {renderIcon()} + {isEmpty && } + {children} + + ); + } ); diff --git a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx index 79cfd15d791bb..597bf25469288 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx @@ -24,115 +24,111 @@ import { Formula } from '@/components/editor/components/leaf/formula'; import { Mention } from '@/components/editor/components/leaf/mention'; import { EditorElementProps, TextNode } from '@/components/editor/editor.type'; import { renderColor } from '@/utils/color'; -import React, { FC, memo, useMemo } from 'react'; +import React, { FC, useMemo } from 'react'; import { RenderElementProps } from 'slate-react'; -import isEqual from 'lodash-es/isEqual'; -export const Element = memo( - ({ - element: node, - attributes, - children, - }: RenderElementProps & { - element: EditorElementProps['node']; - }) => { - const Component = useMemo(() => { - switch (node.type) { - case BlockType.HeadingBlock: - return Heading; - case BlockType.TodoListBlock: - return TodoList; - case BlockType.ToggleListBlock: - return ToggleList; - case BlockType.Paragraph: - return Paragraph; - case BlockType.DividerBlock: - return DividerNode; - case BlockType.Page: - return Page; - case BlockType.QuoteBlock: - return Quote; - case BlockType.BulletedListBlock: - return BulletedList; - case BlockType.NumberedListBlock: - return NumberedList; - case BlockType.CodeBlock: - return CodeBlock; - case BlockType.CalloutBlock: - return Callout; - case BlockType.EquationBlock: - return MathEquation; - case BlockType.ImageBlock: - return ImageBlock; - case BlockType.OutlineBlock: - return Outline; - case BlockType.TableBlock: - return TableBlock; - case BlockType.TableCell: - return TableCellBlock; - case BlockType.GridBlock: - case BlockType.BoardBlock: - case BlockType.CalendarBlock: - return DatabaseBlock; - case BlockType.LinkPreview: - return LinkPreview; - default: - return UnSupportedBlock; - } - }, [node.type]) as FC; - - const InlineComponent = useMemo(() => { - switch (node.type) { - case InlineBlockType.Formula: - return Formula; - case InlineBlockType.Mention: - return Mention; - default: - return null; - } - }, [node.type]) as FC; +export const Element = ({ + element: node, + attributes, + children, +}: RenderElementProps & { + element: EditorElementProps['node']; +}) => { + const Component = useMemo(() => { + switch (node.type) { + case BlockType.HeadingBlock: + return Heading; + case BlockType.TodoListBlock: + return TodoList; + case BlockType.ToggleListBlock: + return ToggleList; + case BlockType.Paragraph: + return Paragraph; + case BlockType.DividerBlock: + return DividerNode; + case BlockType.Page: + return Page; + case BlockType.QuoteBlock: + return Quote; + case BlockType.BulletedListBlock: + return BulletedList; + case BlockType.NumberedListBlock: + return NumberedList; + case BlockType.CodeBlock: + return CodeBlock; + case BlockType.CalloutBlock: + return Callout; + case BlockType.EquationBlock: + return MathEquation; + case BlockType.ImageBlock: + return ImageBlock; + case BlockType.OutlineBlock: + return Outline; + case BlockType.TableBlock: + return TableBlock; + case BlockType.TableCell: + return TableCellBlock; + case BlockType.GridBlock: + case BlockType.BoardBlock: + case BlockType.CalendarBlock: + return DatabaseBlock; + case BlockType.LinkPreview: + return LinkPreview; + default: + return UnSupportedBlock; + } + }, [node.type]) as FC; - const className = useMemo(() => { - const data = (node.data as BlockData) || {}; - const align = data.align; + const InlineComponent = useMemo(() => { + switch (node.type) { + case InlineBlockType.Formula: + return Formula; + case InlineBlockType.Mention: + return Mention; + default: + return null; + } + }, [node.type]) as FC; - return `block-element relative flex rounded ${align ? `block-align-${align}` : ''}`; - }, [node.data]); + const className = useMemo(() => { + const data = (node.data as BlockData) || {}; + const align = data.align; - const style = useMemo(() => { - const data = (node.data as BlockData) || {}; + return `block-element relative flex rounded ${align ? `block-align-${align}` : ''}`; + }, [node.data]); - return { - backgroundColor: data.bgColor ? renderColor(data.bgColor) : undefined, - color: data.font_color ? renderColor(data.font_color) : undefined, - }; - }, [node.data]); + const style = useMemo(() => { + const data = (node.data as BlockData) || {}; - if (InlineComponent) { - return ( - - {children} - - ); - } + return { + backgroundColor: data.bgColor ? renderColor(data.bgColor) : undefined, + color: data.font_color ? renderColor(data.font_color) : undefined, + }; + }, [node.data]); - if (node.type === YjsEditorKey.text) { - return ( - - {children} - - ); - } + if (InlineComponent) { + return ( + + {children} + + ); + } + if (node.type === YjsEditorKey.text) { return ( - -
- - {children} - -
-
+ + {children} + ); - }, - (prevProps, nextProps) => isEqual(prevProps.element, nextProps.element) -); + } + + return ( + +
+ + {children} + +
+
+ ); +}; diff --git a/frontend/appflowy_web_app/src/components/error/NotFound.tsx b/frontend/appflowy_web_app/src/components/error/NotFound.tsx index a4627f153d50b..1d2810fdc0a39 100644 --- a/frontend/appflowy_web_app/src/components/error/NotFound.tsx +++ b/frontend/appflowy_web_app/src/components/error/NotFound.tsx @@ -25,7 +25,7 @@ const NotFound = () => {
{t('publish.createWithAppFlowy')}
-
+
{t('publish.fastWithAI')} {t('publish.tryItNow')}
diff --git a/frontend/appflowy_web_app/src/components/publish/CollabView.tsx b/frontend/appflowy_web_app/src/components/publish/CollabView.tsx index 92afe5077d979..d5ae4d9d1195e 100644 --- a/frontend/appflowy_web_app/src/components/publish/CollabView.tsx +++ b/frontend/appflowy_web_app/src/components/publish/CollabView.tsx @@ -2,6 +2,7 @@ import { ViewLayout, YDoc } from '@/application/collab.type'; import { ViewMeta } from '@/application/db/tables/view_metas'; import { usePublishContext } from '@/application/publish'; import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; +import { useAppThemeMode } from '@/components/app/useAppThemeMode'; import { Database } from '@/components/database'; import { useViewMeta } from '@/components/publish/useViewMeta'; import { ViewMetaProps } from 'src/components/view-meta'; @@ -15,7 +16,7 @@ export interface CollabViewProps { function CollabView({ doc }: CollabViewProps) { const { viewId, layout, icon, cover, layoutClassName, style, name } = useViewMeta(); - + const { isDark } = useAppThemeMode(); const View = useMemo(() => { switch (layout) { case ViewLayout.Document: @@ -30,6 +31,7 @@ function CollabView({ doc }: CollabViewProps) { }, [layout]) as React.FC< { doc: YDoc; + isDark: boolean; navigateToView?: (viewId: string) => Promise; loadViewMeta?: (viewId: string) => Promise; getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map; destroy: () => void }>; @@ -58,6 +60,7 @@ function CollabView({ doc }: CollabViewProps) { cover={cover} viewId={viewId} name={name} + isDark={isDark} />
); diff --git a/frontend/appflowy_web_app/src/vite-env.d.ts b/frontend/appflowy_web_app/src/vite-env.d.ts index 14109472ea0c8..2ec03900dbf05 100644 --- a/frontend/appflowy_web_app/src/vite-env.d.ts +++ b/frontend/appflowy_web_app/src/vite-env.d.ts @@ -18,4 +18,26 @@ interface Window { default: (message: string) => void; warning: (message: string) => void; }; + + Prism: { + tokenize: (text: string, grammar: Prism.Grammar) => Prism.Token[]; + languages: Record; + plugins: { + autoloader: { + languages_path: string; + }; + }; + }; + hljs: { + highlightAuto: (code: string) => { language: string }; + }; +} + +namespace Prism { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Token { + type: string; + content: string | Token[]; + length: number; + } }