Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions packages/platform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ const nodeOptions: UsjNodeOptions = { [immutableNoteCallerNodeName]: { onClick:
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 },
start: { jsonPathIndexes: [3, 1], offset: 15 },
end: { jsonPathIndexes: [3, 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 },
start: { jsonPathIndexes: [3, 1], offset: 15 },
end: { jsonPathIndexes: [3, 1], offset: 22 },
};
const cursorLocation = { start: { jsonPath: "$.content[3].content[1]", offset: 15 } };
const cursorLocation = { start: { jsonPathIndexes: [3, 1], offset: 15 } };

export default function App() {
const marginalRef = useRef<MarginalRef | null>(null);
Expand Down Expand Up @@ -142,6 +142,7 @@ If using the **commenting features** in the `<Marginal />` component:
- 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.
- To enable comments use the `<Marginal />` editor component (comments appear in the margin).
- To use the editor without comments use the `<Editorial />` component.
Expand Down
12 changes: 6 additions & 6 deletions packages/platform/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@ const nodeOptions: UsjNodeOptions = {
};
// Word "man" inside first q1 of PSA 1:1.
const annotationRange1 = {
start: { jsonPath: "$.content[10].content[2]", offset: 15 },
end: { jsonPath: "$.content[10].content[2]", offset: 18 },
start: { jsonPathIndexes: [10, 2], offset: 15 },
end: { jsonPathIndexes: [10, 2], offset: 18 },
};
// Phrase "man who" inside first q1 of PSA 1:1.
const annotationRange2 = {
start: { jsonPath: "$.content[10].content[2]", offset: 15 },
end: { jsonPath: "$.content[10].content[2]", offset: 22 },
start: { jsonPathIndexes: [10, 2], offset: 15 },
end: { jsonPathIndexes: [10, 2], offset: 22 },
};
// Word "stand" inside first q2 of PSA 1:1.
const annotationRange3 = {
start: { jsonPath: "$.content[11].content[0]", offset: 4 },
end: { jsonPath: "$.content[11].content[0]", offset: 9 },
start: { jsonPathIndexes: [11, 0], offset: 4 },
end: { jsonPathIndexes: [11, 0], offset: 9 },
};
const defaultAnnotations: Annotations = {
annotation1: {
Expand Down
11 changes: 7 additions & 4 deletions packages/platform/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
export { default as Editorial } from "./Editorial";
export { default as Marginal, type MarginalRef } from "./marginal/Marginal";
export type { LoggerBasic } from "shared/adaptors/logger-basic.model";
export type { AnnotationRange, SelectionRange } from "shared-react/annotation/selection.model";
export type { TextDirection } from "shared-react/plugins/text-direction.model";
export { immutableNoteCallerNodeName } from "shared-react/nodes/scripture/usj/ImmutableNoteCallerNode";
export { type UsjNodeOptions } from "shared-react/nodes/scripture/usj/usj-node-options.model";
export type { UsjNodeOptions } from "shared-react/nodes/scripture/usj/usj-node-options.model";
export {
type ViewOptions,
DEFAULT_VIEW_MODE,
getViewOptions,
viewOptionsToMode,
} from "./editor/adaptors/view-options.utils";
export { type EditorOptions, type EditorRef } from "./editor/Editor";
export { type ViewMode } from "./editor/adaptors/view-mode.model";
export { type Comments } from "./marginal/comments/commenting";
export type { EditorOptions, EditorRef } from "./editor/Editor";
export type { ViewMode } from "./editor/adaptors/view-mode.model";
export type { Comments } from "./marginal/comments/commenting";
4 changes: 3 additions & 1 deletion packages/shared-react/annotation/selection.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export type UsjLocation = {
jsonPath: string;
/* JsonPath indexes of the location in the USJ, e.g. JsonPath "$.content[1].content[2]" has indexes `[1, 2]` */
jsonPathIndexes: number[];
/* Offset of the location in the text */
offset: number;
};

Expand Down
23 changes: 4 additions & 19 deletions packages/shared-react/annotation/selection.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,6 @@ import {
import { AnnotationRange, SelectionRange, UsjLocation } from "./selection.model";
import { $isTypedMarkNode } from "shared/nodes/features/TypedMarkNode";

const JSON_PATH_START = "$";
const JSON_PATH_CONTENT = ".content[";

function extractJsonPathIndexes(jsonPath: string): number[] {
const path = jsonPath.split(JSON_PATH_CONTENT);
if (path.shift() !== JSON_PATH_START)
throw new Error(`extractJsonPathIndexes: jsonPath didn't start with '${JSON_PATH_START}'`);

const indexes = path.map((str) => parseInt(str, 10));
return indexes;
}

/**
* Find the text node that contains the location offset. Check if the offset fits within the current
* text node, if it doesn't check in the next nodes ignoring the TypedMarkNodes but looking inside
Expand Down Expand Up @@ -60,9 +48,8 @@ function $findTextNodeInMarks(
function $getNodeFromLocation(
location: UsjLocation,
): [LexicalNode | undefined, number | undefined] {
const indexes = extractJsonPathIndexes(location.jsonPath);
let currentNode: LexicalNode | undefined = $getRoot();
for (const index of indexes) {
for (const index of location.jsonPathIndexes) {
if (!currentNode || !$isElementNode(currentNode)) return [undefined, undefined];

currentNode = currentNode.getChildAtIndex(index) ?? undefined;
Expand Down Expand Up @@ -114,19 +101,17 @@ export function $getRangeFromSelection(
}

function getLocationFromNode(node: LexicalNode, offset: number): UsjLocation {
const indexes: number[] = [];
const jsonPathIndexes: number[] = [];
let current: LexicalNode | null = node;
while (current?.getParent()) {
const parent: ElementNode | null = current.getParent();
if (parent) {
const index = parent?.getChildren().indexOf(current);
if (index >= 0) indexes.unshift(index);
if (index >= 0) jsonPathIndexes.unshift(index);
}
current = parent;
}
const jsonPath =
JSON_PATH_START + JSON_PATH_CONTENT + indexes.join("]" + JSON_PATH_CONTENT) + "]";
return { jsonPath, offset };
return { jsonPathIndexes, offset };
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/utilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
"require": "./dist/index.cjs"
}
},
"files": [
Expand Down
25 changes: 25 additions & 0 deletions packages/utilities/src/converters/usj/jsonpath-indexes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { indexesFromUsjJsonPath, usjJsonPathFromIndexes } from "./jsonpath-indexes";

describe("USJ JSONPath Indexes", () => {
it("should convert indexes from JSONPath", () => {
const jsonPath = "$.content[1].content[2]";

const indexes = indexesFromUsjJsonPath(jsonPath);

expect(indexes).toEqual([1, 2]);
});

it("should throw if JSONPath doesn't start with $", () => {
const jsonPath = ".content[1].content[2]";

expect(() => indexesFromUsjJsonPath(jsonPath)).toThrow();
});

it("should convert JSONPath from indexes", () => {
const indexes = [1, 2];

const jsonPath = usjJsonPathFromIndexes(indexes);

expect(jsonPath).toEqual("$.content[1].content[2]");
});
});
28 changes: 28 additions & 0 deletions packages/utilities/src/converters/usj/jsonpath-indexes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const JSON_PATH_START = "$";
const JSON_PATH_CONTENT = ".content[";

/**
* Converts a USJ JSONPath string into an array of indexes.
*
* @param jsonPath - The USJ JSONPath string to convert. It must start with `$` and contain `.content[index]` segments.
* @returns An array of numeric indexes extracted from the JSONPath.
* @throws Will throw an error if the JSONPath does not start with `$`.
*/
export function indexesFromUsjJsonPath(jsonPath: string): number[] {
const path = jsonPath.split(JSON_PATH_CONTENT);
if (path.shift() !== JSON_PATH_START)
throw new Error(`indexesFromJsonPath: jsonPath didn't start with '${JSON_PATH_START}'`);

const indexes = path.map((str) => parseInt(str, 10));
return indexes;
}

/**
* Converts an array of indexes into a USJ JSONPath string.
*
* @param indexes - An array of numeric indexes to convert.
* @returns A USJ JSONPath string constructed from the indexes.
*/
export function usjJsonPathFromIndexes(indexes: number[]): string {
return indexes.reduce((path, index) => `${path}${JSON_PATH_CONTENT}${index}]`, JSON_PATH_START);
}
1 change: 1 addition & 0 deletions packages/utilities/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export {
} from "./converters/usj/usj.model";
export { usxStringToUsj } from "./converters/usj/usx-to-usj";
export { usjToUsxString } from "./converters/usj/usj-to-usx";
export { indexesFromUsjJsonPath, usjJsonPathFromIndexes } from "./converters/usj/jsonpath-indexes";