Lexxy Documentation

repository·main·Indexed 22 days ago

https://github.com/basecamp/lexxy

Lexxy is a modern rich text editor for Ruby on Rails applications, built on the Lexical framework. It integrates with Rails Action Text to provide high-quality HTML semantics, Markdown support, code highlighting, and attachment previews for PDFs and videos. The library includes a flexible configuration hierarchy via Lexxy.configure, presets, and HTML attributes for the <lexxy-editor> element.

Tokens
23K
Snippets
95
Records
124
Agent score
78%

What's inside Lexxy

  1. Overview of Lexxy

    main

    Lexxy is a modern rich text editor designed specifically for Ruby on Rails applications. It is built on top of Meta's Lexical framework and focuses on producing high-quality HTML semantics (e.g., using real <p> tags).

    Key capabilities include:

    • Markdown Support: Includes keyboard shortcuts and auto-formatting when pasting Markdown.
    • Code Highlighting: Provides real-time syntax highlighting for code blocks.
    • Interactive Prompts: Supports configurable prompts for features like mentions, with various loading and filtering strategies.
    • Attachment Previews: Supports previewing attachments such as PDFs and Videos directly within the editor.
    • Action Text Integration: Works seamlessly with Rails Action Text by generating the same canonical HTML format required for attachments.
  2. Lexxy coding style: Comments and Conditionals

    main

    Comments

    Avoid comments that restate what the code does. Use comments only to explain why a specific workaround or non-obvious performance optimization exists, especially regarding browser quirks or ordering constraints in editor code.

    Conditional Returns

    Prefer expanded if/else blocks over guard clauses and ternaries to improve readability.

    Exceptions for Guard Clauses:

    • When the return is at the very beginning of a method.
    • When the method body is non-trivial (multiple lines) and the guard is a type check (e.g., checking a Lexical selection type).

    Avoid Ternaries: Avoid using ternaries for complex logic or multi-line assignments; use plain if/else instead.

    // Bad: Guard clause for simple logic
    edit() {
      if (!this.isEditable) return
      this.showEditor()
    }
    
    // Good: Expanded conditional
    edit() {
      if (this.isEditable) {
        this.showEditor()
      }
    }
    
    // Good: Guard clause for non-trivial body (e.g., Lexical type guard)
    editor.update(() => {
      const selection = $getSelection()
      if (!$isRangeSelection(selection)) return
    
      // ...several lines of logic that require a range selection...
    })
    
    // Bad: Ternary for assignment
    const [first, last] = from < to ? [from, to] : [to, from]
    
    // Good: Plain if/else
    let first, last
    if (from < to) {
      first = from
      last = to
    } else {
      first = to
      last = from
    }
  3. Lexxy coding style: Performance and Clarity

    main

    Prioritize code clarity and idiomatic patterns over speculative performance optimizations. Only optimize code if there is concrete evidence that it resides on a meaningful hot path (e.g., editor bootstrap, loading large documents, or large tables).

    Use the provided benchmark harness to validate optimizations before implementing them:

    yarn benchmark:browser
  4. How Lexxy's JavaScript architecture works

    main

    Lexxy's JavaScript is a set of plain ES modules centered around a custom element <lexxy-editor>.

    Instead of building directly on top of Lexical's patterns, Lexxy uses an Object-Oriented (OO) approach where specific controllers wrap the Lexical editor to provide a high-level, intention-revealing API.

    Key components include:

    • Controllers: Classes like Contents, Selection, and Clipboard that wrap Lexical logic.
    • Extensions: Self-contained, optional behaviors that subclass LexxyExtension.
    • Lexical: The underlying editor engine which Lexxy drives rather than follows.
  5. Implement trigger-based suggestions with Lexxy Prompts

    main

    Lexxy Prompts allow you to implement features like @mentions or /commands by triggering a suggestion menu based on specific text. When a user selects an item from the prompt, you can configure it to either:

    1. Insert as an Action Text custom attachment: Uses standard Rails Action Text to handle rendering or server-side processing via Signed Global IDs.
    2. Insert as free text: Simply places the text directly into the editor.

    Prompts support both inline loading (items defined directly in the HTML) and remote loading (items fetched from a src endpoint). Filtering can be performed locally or on the server.

  6. How Lexxy Extensions work

    main

    Lexxy Extensions are wrappers around Lexical Extensions that provide access to the Lexxy element (via this.editorElement) and the editor toolbar.

    An extension instance is initialized per editor using new MyLexxyExtension(lexxyElement). If you implement a custom constructor, you must pass the lexxyElement to super.

    To load an extension, the lexicalExtension property must return a truthy value. Lexxy provides a this.defineExtension(...) method on the base class to wrap Lexical's own defineExtension, ensuring version compatibility with the Lexical version bundled by Lexxy. If you need other Lexical utilities, they are re-exported via @37signals/lexxy.

    import { Lexical } from "@37signals/lexxy"
  7. Lexxy coding style: Defensive Programming

    main

    Lexxy follows a 'fail fast and loudly' philosophy.

    • Avoid swallowing errors: Do not use null fallbacks or optional chaining (?.) to paper over values that your logic guarantees should be present. Let the error raise so the underlying bug can be fixed.
    • Handle legitimate optional state: In the context of Lexical, certain states are legitimately optional (e.g., $getSelection() returning null when no selection exists). Guarding these is considered correctness, not unnecessary defensive programming.

    The Rule of Thumb: If you are using a fallback to avoid an error on a value your logic says must exist, you are likely masking a bug.

  8. How Lexxy handles preview generation

    main

    Generating previews for PDFs or videos is resource-intensive. Lexxy manages this by showing a file icon until the preview is ready. There are two strategies for detecting when a preview is ready, depending on your backend implementation:

    1. Default (Preload): If preview_status_url is not provided, Lexxy attempts to load the url into an off-screen Image element. When the image loads, the icon is swapped for the preview. This is best for backends that serve bytes directly (blocking or 404ing until ready).
    2. Opt-in (Polling): If preview_status_url is provided, Lexxy polls that URL using exponential backoff. This is best for backends that generate previews in the background (e.g., via Active Job) to avoid blocking request threads.
  9. Configure Lexxy editors using the configuration hierarchy

    main

    Lexxy editors are configured using a hierarchy of settings that resolve from least to most specific. This allows you to set broad defaults and override them for specific use cases or individual elements:

    1. Default options: Applied to every editor via Lexxy.configure({ default: { ... } }).
    2. Presets: Named configurations that extend the default preset. Opt-in using the preset attribute on the <lexxy-editor> element.
    3. HTML attributes: Individual options set directly on the <lexxy-editor> element. These have the highest precedence and override both defaults and presets.

    Important: You must call Lexxy.configure synchronously immediately after your import statement. Editor elements are registered after the import's call stack completes, so configuration must happen before the elements are processed.

    import * as Lexxy from "lexxy"
    
    // Must be called synchronously after import
    Lexxy.configure({
      default: {
        toolbar: false
      }
    })
  10. Lexxy project structure and composition

    main

    Lexxy is a dual-purpose project consisting of two main components:

    1. A JavaScript rich text editor: Built on top of Lexical.
    2. A Ruby gem: A thin layer that wires the JavaScript editor into Rails' Action Text.

    When contributing or extending the project, follow the STYLE.md guidelines as the source of truth, even if existing surrounding code deviates.

  11. Implement the Lexxy Extension lifecycle

    main

    Lexxy Extensions follow a specific lifecycle tied to the editor's connection state. Because a new extension instance is created on every editor connectedCallback, you must manage cleanup to prevent leaks or duplication.

    Lifecycle Methods

    • initializeToolbar(lexxyToolbar): Called when the editor toolbar is initialized. Use this to add buttons or attach listeners to the toolbar DOM.
    • dispose(): Called when the editor disconnects or reconnects. Use this to release DOM listeners, global listeners, observers, timers, or pending async state.