Curvenote Editor

repository·main·Indexed 43 days ago

https://github.com/curvenote/editor

An interactive scientific WYSIWYG editor built with ProseMirror, React, and Redux. Designed for computational narratives, it supports LaTeX, MyST Markdown, and integrates with Jupyter and Sphinx. The project includes specialized packages such as @curvenote/editor (v0.17.6), prosemirror-autocomplete for trigger-based suggestions, prosemirror-codemark for enhanced inline code navigation, and @curvenote/prosemirror-utils for TypeScript-based selection and node utilities.

Tokens
38.7K
Snippets
159
Records
181
Agent score
55%

What's inside Curvenote Editor

  1. Overview of @curvenote/editor

    main

    @curvenote/editor is an interactive scientific editor designed to facilitate the creation of computational narratives. It is a rich, WYSIWYG, collaborative editor that integrates with LaTeX, various Markdown flavors, and the Jupyter and Sphinx ecosystems.

    Key features include:

    • Interactivity: Designed to bridge the gap between expressive technical writing and accessibility.
    • Scientific Focus: Supports citations, cross-references, and callouts that standard CommonMark markdown lacks.
    • Ecosystem Integration: Built to work alongside Jupyter and Sphinx workflows.
    • Collaboration: Supports real-time collaboration via middleware integrations (e.g., prosemirror-collab).
  2. Overview of @curvenote/schema

    main

    @curvenote/schema provides a typed schema designed for writing reactive scientific documents. It acts as an intermediate representation that can be translated between Markdown (specifically MyST-flavored), JSON (ProseMirror Document structure), LaTeX, and HTML.

    Key features include:

    • Interoperability: Uses fromMarkdown and toMarkdown for Markdown conversion.
    • Reactive Components: Supports specialized nodes like variable, display, dynamic, range, and switch for interactive scientific writing.
    • Web Component Output: Renders non-standard components as Web Components in HTML output, while using standard HTML for others.
    • ProseMirror Based: The internal representation is a ProseMirror Document structure.
  3. How prosemirror-codemark handles code mark navigation

    main

    The plugin solves the ambiguity of whether a cursor is 'inside' or 'outside' an inline code mark by using a Decoration that renders a .fake-cursor span.

    Key Navigation Behaviors:

    • Arrow Keys: Allows users to enter or exit code marks. For example, pressing the Right Arrow at the end of a code mark will move the cursor outside the mark.
    • Home/End/Modifiers: ctrl-a, cmd-left, and Home jump to the start of a line/document without the mark. ctrl-e, cmd-right, and End jump to the end without the mark.
    • Backspace/Delete: Handles deleting text within a mark or deleting the entire mark when the cursor is at the boundary.
    • Clicking: Clicking a new location in the editor defaults the cursor to being outside the code mark.
    • Selection: If a word is selected and the ` key is pressed, it wraps the selection in a code mark.
  4. Understand the @curvenote/editor architecture

    main

    The editor is built using a layered approach designed to support multiple editor instances (blocks) on a single page, similar to the Jupyter interface.

    Core Stack:

    • Engine: ProseMirror wrapped in a React component.
    • State Management: Redux is used for middleware.
    • Styling: Material-UI.
    • Reactivity: Powered by @curvenote/components and @curvenote/runtime (Web Components).
    • Type Safety: Fully typed with TypeScript.

    Note for single-editor implementations: If you only require one editor instance on a page, you may encounter unnecessary indexing and complexity due to the Redux integration, but this architecture is maintained to support complex layouts where multiple editor blocks coexist with comments or other DOM elements.

  5. How prosemirror-autocomplete works

    main

    The autocomplete function returns two ProseMirror plugins:

    1. A decoration plugin that wraps the trigger and filter text (e.g., [@][mention]).
    2. An InputRule plugin that listens for the defined triggers.

    You can configure the plugin using either individual event handlers (onOpen, onEnter, onArrow, onFilter, onClose) or a single reducer function. Note that you cannot use both simultaneously.

    import autocomplete, { Options } from 'prosemirror-autocomplete';
    
    const options: Options = {
      triggers: [
        { name: 'hashtag', trigger: '#' },
        { name: 'mention', trigger: '@' },
      ],
      // Option A: Individual handlers
      onOpen: ({ view, range, trigger, type }) => handleOpen(),
      onEnter: ({ view }) => handleSelect(),
      // ... other handlers
    };
    
    // Or Option B: A single reducer
    const options: Options = {
      triggers: [{ name: 'mention', trigger: '@' }],
      reducer: (action) => dispatch(action),
    };
    
    // Add to EditorView
    const view = new EditorView(editor, {
      state: EditorState.create({
        doc: DOMParser.fromSchema(schema).parse(content),
        plugins: [...autocomplete(options), ...otherPlugins],
      }),
    });
  6. Explore the @curvenote/editor monorepo packages

    main

    The repository is a monorepo managed with turborepo. It contains several specialized packages that can be used for building technical editors:

    • @curvenote/schema: The core schema for the editor, focused on interactive content, translation, and export.
    • prosemirror-autocomplete: A ProseMirror plugin that adds triggers for #hashtags, @mentions, /menus, and other complex autocompletions.
    • prosemirror-codemark: A ProseMirror plugin for manipulating and navigating inline code marks.
    • prosemirror-docx: A package for exporting ProseMirror schemas to Microsoft Word.
    • sidenotes: Provides reactive placement of comments with hooks for multiple inline references.
    • @curvenote/article: Provides CSS, styling components, and document layout.
    • @curvenote/components: Provides interactive widgets and web-components.
    • @curvenote/runtime: Provides client-side reactivity built on Redux.
  7. Get started with @curvenote/editor

    main

    To run the editor locally for development, clone the repository, install dependencies, and start the development server.

    For a detailed implementation example, refer to the packages/editor/demo/index.tsx file in the repository.

    git clone git@github.com:curvenote/editor.git
    cd editor
    npm install
    npm run start
  8. Use prosemirror-codemark in basic ProseMirror

    main

    To integrate the plugin into a standard ProseMirror setup, import the codemark function and its required CSS. You must provide the markType from your schema (typically schema.marks.code) to the codemark function, which returns an array of plugins.

    import codemark from 'prosemirror-codemark';
    import 'prosemirror-codemark/dist/codemark.css';
    
    // Pass the specific MarkType from your schema
    const plugins = codemark({ markType: schema.marks.code });
    
    const view = new EditorView(editor, {
      state: EditorState.create({
        doc,
        plugins: [...plugins, ...otherPlugins],
      }),
    });
  9. Install and run @curvenote/editor locally

    main

    To run the editor locally for development, clone the repository, install the dependencies using npm, and start the development server.

    git clone git@github.com:curvenote/editor.git
    cd editor
    npm install
    npm run start
  10. Style and Position Autocomplete Suggestions

    main

    The library does not provide a built-in UI. You must manage the visibility and positioning of your suggestion list (e.g., using popper.js or absolute positioning).

    By default, the library wraps the trigger in a <span> with the class .autocomplete. You can customize this by providing decorationAttrs in your trigger definition.

    Example positioning logic:

    function placeSuggestion(open: boolean) {
      suggestion.style.display = open ? 'block' : 'none';
      const rect = document.getElementsByClassName('autocomplete')[0].getBoundingClientRect();
      suggestion.style.top = `${rect.top + rect.height}px`;
      suggestion.style.left = `${rect.left}px`;
    }