Skip to content

Latest commit

 

History

History
 
 

README.md

Scripture Editor for Platform using USJ

Build Status CodeQL Github Tag

A Scripture editor React component that works on USJ Scripture data. A utility that converts USX to USJ is also included. It is expected that data conforms to USJ v3.1.

---
title: Scripture Data — Editor flow
---
graph TB
  DB[(DB)] <-- USX --> C
  C[USX-USJ converter] <-- USJ --> A
  A[USJ-Editor adapter] <-- Editor State --> Editor
Loading

Install

npm install @eten-tech-foundation/platform-editor

Usage

Note

  • Use the <Editorial /> component for an editor without commenting features.
  • Use the <Marginal /> component (DEPRECATED) for an editor with comments (comments appear in the margin).

Important

<Marginal /> is deprecated and will be removed in a future release.

import { EditorOptions, Editorial, EditorRef, usxStringToUsj, UsjNodeOptions } from "@eten-tech-foundation/platform-editor";
import { BookChapterControl } from "platform-bible-react";

const emptyUsx = '<usx version="3.1" />';
const usx = `<?xml version="1.0" encoding="utf-8"?>
<usx version="3.1">
  <book code="PSA" style="id">World English Bible (WEB)</book>
  <para style="mt1">The Psalms</para>
  <chapter number="1" style="c" sid="PSA 1" />
  <para style="q1">
    <verse number="1" style="v" sid="PSA 1:1" />Blessed is the man who doesn’t walk in the counsel of the wicked,</para>
  <para style="q2" vid="PSA 1:1">nor stand on the path of sinners,</para>
  <para style="q2" vid="PSA 1:1">nor sit in the seat of scoffers;<verse eid="PSA 1:1" /></para>
</usx>
`;
const defaultUsj = usxStringToUsj(emptyUsx);
const defaultScrRef = { book: "PSA", chapterNum: 1, verseNum: 1 };
const nodeOptions: UsjNodeOptions = { noteCallerOnClick: () => console.log("Note was clicked!") };
const options: EditorOptions = { isReadonly: false, textDirection: "ltr", nodes: nodeOptions };
// Word "man" inside first q1 of PSA 1:1.
const annotationRange1 = {
  start: { jsonPath: "$.content[3].content[1]", offset: 15 },
  end: { jsonPath: "$.content[3].content[1]", offset: 18 },
};
// Phrase "man who" inside first q1 of PSA 1:1.
const annotationRange2 = {
  start: { jsonPath: "$.content[3].content[1]", offset: 15 },
  end: { jsonPath: "$.content[3].content[1]", offset: 22 },
};
const cursorLocation = { start: { jsonPath: "$.content[3].content[1]", offset: 15 } };

export default function App() {
  const editorialRef = useRef<EditorRef | null>(null);
  const [scrRef, setScrRef] = useState(defaultScrRef);

  const handleUsjChange = useCallback((usj: Usj, comments: Comments | undefined) => console.log({ usj, comments }), []);

  // Simulate USJ updating after the editor is loaded.
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      editorialRef.current?.setUsj(usxStringToUsj(usx));
    }, 1000);
    return () => clearTimeout(timeoutId);
  }, []);

  // Add and remove annotations after USJ is loaded, and set cursor location.
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      editorialRef.current?.setAnnotation(annotationRange1, "spelling", "annotationId");
      editorialRef.current?.setAnnotation(annotationRange2, "grammar", "abc123");
      editorialRef.current?.removeAnnotation("spelling", "annotationId");
      editorialRef.current?.setSelection(cursorLocation);
    }, 3000);
    return () => clearTimeout(timeoutId);
  }, []);

  return (
    <>
      <div className="controls">
        <BookChapterControl scrRef={scrRef} handleSubmit={setScrRef} />
      </div>
      <Editorial
        ref={editorialRef}
        defaultUsj={defaultUsj}
        scrRef={scrRef}
        onScrRefChange={setScrRef}
        onUsjChange={handleUsjChange}
        options={options}
        logger={console}
      />
    </>
  );
}

Features

  • USJ editor with USX support
  • Read-only and edit mode
  • History - undo & redo
  • Cut, copy, paste, paste as plain text - context menu and keyboard shortcuts
  • Format block type - change <para> markers. The current implementation is a proof-of-concept and doesn't have all the markers available yet.
  • Insert markers - type '\' (backslash - configurable to another key) for a marker menu. If text is selected first the marker will apply to the selection if possible, e.g. use '\wj' to "red-letter" selected text.
  • Add comments to selected text, reply in comment threads, delete comments and threads (deprecated).
    • To enable comments use the <Marginal /> editor component (comments appear in the margin).
    • To use the editor without comments use the <Editorial /> component.
  • Add and remove different types of annotations. Style the different annotations types with CSS, e.g. style a spelling annotation with a red squiggly underline.
  • Get and set the cursor location or selection range.
  • Specify textDirection as "ltr", "rtl", or "auto" ("auto" is unlikely to be useful for minority languages).
  • Insert note at selection, e.g. footnote, cross-reference. If text is selected it will be used as the quote in a footnote.
  • BCV linkage - change the book/chapter/verse externally and the cursor moves; move the cursor and it updates the external book/chapter/verse
  • Nodes supported <book>, <chapter>, <verse>, <para>, <char>, <note>, <ms>
  • Nodes not yet supported <table>, <row>, <cell>, <sidebar>, <periph>, <figure>, <optbreak>, <ref>
  • Node options:
    • callback for when a <note> link is clicked
    • customize possible note callers list
  • Apply Delta Operation changes to the editor and see Delta Operations when changes are made in the editor. For use with realtime collaborative editing.

Styling

This npm package does not include styling so you need to style the editor component to suit your application. A good place to start is to copy the CSS from this repo:

For icon assets for the editor referenced in editor.css (the license file is included):

If using the commenting features in the <Marginal /> component:

Annotation Styles

Annotations are added with a specific type via the editor's reference API (see Editorial Ref). This type can then be used to apply custom CSS styles (e.g., a green squiggly underline for a "grammar" type annotation). The CSS classname for an annotation takes the form of .${annotationPrefix}-external-${type}, where type is the string you pass to the setAnnotation() method and annotationPrefix is set by config.theme.typedMark (defaults to "editor-typed-mark"). If annotations overlap with each other an additional CSS classname is added where annotationPrefix is set by config.theme.typedMarkOverlap (defaults to "editor-typed-markOverlap").

For example, if an annotation of type "grammar" is overlapping it will have both CSS classnames editor-typed-mark-external-grammar and editor-typed-markOverlap-external-grammar. If it's not overlapping it still has the first classname. Annotations and comments are the same when considering if it's overlapping.

Comment Styles

These follow a similar patter to Annotation Styles. If a comment is overlapping it will have both CSS classnames editor-typed-mark-internal-comment and editor-typed-markOverlap-internal-comment. If it's not overlapping it still has the first classname. Annotations and comments are the same when considering if it's overlapping.

<Editorial /> API

Editorial Properties

Controls initial data, BCV linkage, and change callbacks. Self-evident props (options, onSelectionChange, logger) are omitted — see the full reference below.

Prop Note
defaultUsj Uncontrolled: only sets initial value; external changes after mount are ignored
scrRef + onScrRefChange Both required together for BCV linkage; cursor moves when ref changes and ref updates when cursor moves
onUsjChange Optionally receives delta ops for collaborative editing
onStateChange Yields canUndo, canRedo, current blockMarker and contextMarker

Full API reference: platform-editor.api.md

Editorial Ref

Programmatic control over the editor. Self-evident methods (focus, undo, redo, cut, copy, paste, pastePlainText, getUsj, setUsj, formatPara, getElementByKey, selectNote, getNoteOps) are omitted — see the full reference below.

Method Note
getSelection / setSelection Uses json-path; assumes no comment Milestone nodes in the USJ
setAnnotation / removeAnnotation Ephemeral — not persisted; same json-path caveat
applyUpdate / replaceEmbedUpdate EXPERIMENTAL: real-time collaborative editing
insertMarker Replicates marker menu; throws if readonly, scrRef not provided, or marker is unsupported
insertNote Deprecated — use insertMarker
toolbarEndRef Internal use only: for dynamically adding toolbar controls

Full API reference: platform-editor.api.md

Editorial Options

Self-evident options (isReadonly, hasSpellCheck, textDirection, nodes) are omitted — see the full reference below.

Option Note
hasExternalUI Disables the built-in toolbar and marker menu
markerMenuTrigger Defaults to \; has no effect when hasExternalUI is set
contextMenu Append custom items to the built-in context menu
view EXPERIMENTAL: only formatted view is functional
debug Shows Lexical TreeView

Full API reference: platform-editor.api.md

Node Options

Set in EditorOptions.nodes. Self-evident options are omitted — see the full reference below.

Option Note
noteCallers Defaults to ["a"…"z"]; override for vernacular scripts
noteCallerOnClick Callback receives getCaller/setCaller; use GENERATOR_NOTE_CALLER / HIDDEN_NOTE_CALLER constants to toggle caller type

Full API reference: platform-editor.api.md

<Marginal /> API (DEPRECATED)

These are the same as Editorial except where noted below. See Editorial API.

Marginal deprecation and migration

<Marginal /> is in maintenance mode. The component continues to ship for backwards compatibility, but it will be removed in a future release. Prefer <Editorial /> or an alternative commenting workflow if you can.

If you must continue using <Marginal />, watch release notes for the removal timeline and plan a migration away from the margin-based commenting experience.

Marginal Properties

Inherits all Editorial Properties. Non-obvious additions:

Prop Note
onCommentChange Fires when comments change independently of USJ
onUsjChange Re-declared: adds comments: Comments | undefined as 2nd argument
showCommentsContainerRef Overrides where the "show comments" button renders

Full API reference: platform-editor.api.md

Marginal Ref

Inherits all Editorial Ref methods. Non-obvious additions:

Method Note
setComments Programmatically load comments without a USJ change

Full API reference: platform-editor.api.md

Demo and Collaborative Web Development Environment

Thanks to CodeSandbox for the instant dev environment: https://codesandbox.io/p/github/eten-tech-foundation/scripture-editors/main

This package is the third tab (dev:platform:5175).

OR

To run the demo app locally, first follow the Developer Quick Start, but instead of running the last step, instead run:

nx dev platform

Develop in App

To develop an editor in a target application you can use yalc to link the editor in without having to publish to NPM every time something changes.

  1. In this monorepo, publish the editor to yalc, e.g.:
    nx devpub platform-editor
  2. In the target application repo, link from yalc:
    yalc link @eten-tech-foundation/platform-editor
  3. In this monorepo, make changes and re-publish the editor (see step 1).
  4. When you have finished developing in the target application repo, unlink from yalc:
    yalc remove @eten-tech-foundation/platform-editor && npm i

License

MIT © ETEN Tech Foundation