Marktion WYSIWYG Markdown Editor

repository·main·Indexed 43 days ago

https://github.com/microvoid/marktion

A WYSIWYG Markdown editor built on ProseMirror featuring real-time rendering, slash menus, bubble menus, and built-in AI integration via Vercel AI. Includes a ReactEditor component, an API client with OpenAPI support, and utility packages like use-channel and use-context-immer.

Tokens
30K
Snippets
140
Records
157
Agent score
55%

What's inside Marktion

  1. Quickstart: Use ReactEditor in a React component

    main

    Import ReactEditor and its corresponding CSS file to render a WYSIWYG Markdown editor. You can pass initial content via the content prop.

    import { ReactEditor } from 'marktion';
    import 'marktion/dist/style.css';
    
    function Editor() {
      return <ReactEditor content={`# Hello World`} />;
    }
  2. Enable type-aware ESLint rules for production

    main

    For production applications, it is recommended to enable type-aware linting by updating your ESLint configuration.

    1. Update parserOptions to include your tsconfig files:
    parserOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      project: ['./tsconfig.json', './tsconfig.node.json'],
      tsconfigRootDir: __dirname,
    },
    1. Update your extends list:
    • Replace plugin:@typescript-eslint/recommended with plugin:@typescript-eslint/recommended-type-checked or plugin:@typescript-eslint/strict-type-checked.
    • Optionally add plugin:@typescript-eslint/stylistic-type-checked.
    • Install eslint-plugin-react and add plugin:react/recommended and plugin:react/jsx-runtime to the extends list.
  3. Manage state with useDispatch and useContextSelector

    main

    For a more structured approach, use useDispatch to retrieve the dispatch function and useContextSelector to subscribe to specific parts of your context state. This allows components to re-render only when the selected slice of state changes.

    1. Define a state class.
    2. Create a context with an initial instance of that class.
    3. Use useDispatch to trigger updates via produce-style functions.
    4. Use useContextSelector with a selector function to extract specific values.
    5. Wrap your application in the context provider (which can be used as a component directly).
    class AppState {
      style = {
        theme: 'light',
        primary: 'tomato'
      };
    }
    
    const AppStateContext = createContext(new AppState());
    
    const setDarkMode = produce(draft => {
      draft.style.theme = 'dark';
    });
    
    const getThemeMode = (state: AppState) => {
      return state.style.theme;
    };
    
    function App() {
      const dispatch = useDispatch();
      const theme = useContextSelector(getThemeMode);
    
      const onToggle = () => {
        dispatch(setDarkMode);
      };
    
      return (
        <div>
          theme: {theme}
          <button onClick={onToggle}>Toggle Mode</button>
        </div>
      );
    }
    
    render(
      <AppStateContext>
        <App />
      </AppStateContext>,
      rootEl
    );
  4. Use use-channel for cross-component communication

    main

    The use-channel hook provides a mechanism for components to communicate via a shared channel. You can create a typed channel using createChannel<T>() and then consume it in components using the useChannel(channel) hook.

    To send data through a channel, call channel.message(data). To access the history of messages sent through the channel, use channel.messages(), which returns an array of the messages received.

    // 1. Define a typed channel
    export const EditArticleChannel = createChannel<Article[]>();
    
    // 2. Consume the channel in a component to listen for messages
    function Editor() {
      const channel = useChannel(EditArticleChannel);
    
      useEffect(() => {
        // Get the latest message from the channel
        const followArticle = channel.messages().pop();
        if (followArticle) {
          // Handle the new message
        }
      }, [channel]);
    
      // 3. Send a message through the channel
      const sendMessage = (data: Article[]) => {
        channel.message(data);
      };
    }
    
    // 4. Send a message from a different component
    function ArticleList() {
      const channel = useChannel(EditArticleChannel);
    
      const onEditItem = (item: Article[]) => {
        channel.message(item);
      };
    }
  5. Quick start with use-context-immer

    main

    To use use-context-immer, create a context and use useSelector to access specific slices of your state or the dispatch function. You can update the state by calling the dispatch function with a function that uses Immer-style mutations to modify the draft state.

    Note: The example uses useSelector and dispath (likely a typo for dispatch in the source) to interact with the context provider.

    const TodoContext = createContext();
    
    function Todo() {
      const todos = useSelector(TodoContext, ctx => ctx.model.todos);
      const dispath = useSelector(TodoContext, ctx => ctx.dispath);
    
      const onCommit = () => {
        dispath(model => {
          model.todos[0].check = true;
        })
      }
    }
    
    function App() {
      return <TodoContext.Provider><Todo /></TodoContext.Provider>
    }
  6. Use the AI Plugin with Marktion

    main

    Marktion supports an AI plugin based on Vercel AI. This provides a built-in AI chat interface that can be invoked by pressing Space at the start of a line.

    To use it, you must first create an AI router (refer to Vercel AI SDK documentation). You can then use the useAI hook to provide the plugin to the ReactEditor via the plugins prop.

    function Editor() {
      const ai = useAI({
        basePath: import.meta.env.VITE_OPENAI_BASE_URL
      });
    
      return <ReactEditor ref={editorRef} plugins={[ai.plugin]} />;
    }
  7. Initialize the Marktion Editor with plugins

    main

    To use the Marktion editor, initialize an EditorState with a list of plugins. You can then pass this state to the <Editor /> component. The onChange callback should be used to update the state when the editor content changes.

    Common plugins include toolbar(), bubble(), slash(), upload(), and placeholder(). You can also extend functionality by adding specialized plugins like AIToolkit().

    const defaultPlugins = [toolbar(), bubble(), slash(), upload(), placeholder()];
    
    function App() {
      const [state, setState] = useState(function () {
        return EditorState.create({
          plugins: [...defaultPlugins, AIToolkit()]
        });
      });
    
      return <Editor state={state} onChange={setState} />;
    }
  8. Expand ESLint configuration for type-aware linting

    main

    For production applications, it is recommended to enable type-aware lint rules in your ESLint configuration. This requires configuring parserOptions to point to your TypeScript configuration files and switching to type-checked plugin presets.

    Steps to enable type-aware linting:

    1. Update parserOptions in your ESLint config to include project and tsconfigRootDir.
    2. Replace standard TypeScript ESLint plugins with type-checked versions.
    3. (Optional) Add stylistic type-checked rules.
    4. (Optional) Add eslint-plugin-react for React-specific linting.
    // 1. Configure parserOptions
    parserOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      project: ['./tsconfig.json', './tsconfig.node.json'],
      tsconfigRootDir: __dirname,
    },
    
    // 2. Update extends list (example logic)
    // Replace 'plugin:@typescript-eslint/recommended'
    // With 'plugin:@typescript-eslint/recommended-type-checked' 
    // or 'plugin:@typescript-eslint/strict-type-checked'
    
    // 3. Optionally add:
    // 'plugin:@typescript-eslint/stylistic-type-checked'
    
    // 4. Add React plugins (requires eslint-plugin-react):
    // 'plugin:react/recommended'
    // 'plugin:react/jsx-runtime'