Skip to content

Commit

Permalink
Make mark rendering lazier
Browse files Browse the repository at this point in the history
  • Loading branch information
smoores-dev committed Aug 7, 2023
1 parent 7f74ac5 commit df3033a
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 61 deletions.
195 changes: 136 additions & 59 deletions src/components/NodeView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DOMOutputSpec, Node } from "prosemirror-model";
import { DOMOutputSpec, Mark, Node } from "prosemirror-model";
import { NodeSelection } from "prosemirror-state";
import React, {
ForwardRefExoticComponent,
Expand All @@ -13,19 +13,112 @@ import React, {

import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js";
import { NodeViewContext } from "../contexts/NodeViewContext.js";
import { ReactWidgetType } from "../decorations/ReactWidgetType.js";
import { NodeViewDesc, ViewDesc, iterDeco } from "../descriptors/ViewDesc.js";
import {
NodeViewDesc,
ViewDesc,
iterDeco,
sameOuterDeco,
} from "../descriptors/ViewDesc.js";
import {
DecorationInternal,
DecorationSourceInternal,
NonWidgetType,
ReactWidgetDecoration,
} from "../prosemirror-internal/DecorationInternal.js";

import { MarkView } from "./MarkView.js";
import { NodeViewComponentProps } from "./NodeViewComponentProps.js";
import { OutputSpec } from "./OutputSpec.js";
import { TextNodeView } from "./TextNodeView.js";
import { TrailingHackView } from "./TrailingHackView.js";
import { WidgetView } from "./WidgetView.js";

type QueuedNodes = {
outerDeco: readonly DecorationInternal[];
sharedMarks: readonly Mark[];
innerPos: number;
nodes: { node: Node; innerDeco: DecorationSourceInternal; offset: number }[];
};

function renderQueuedNodes({
outerDeco,
sharedMarks,
innerPos,
nodes,
}: QueuedNodes) {
const childElements = nodes.map(({ node, innerDeco, offset }) => {
const childPos = innerPos + offset;
const nodeElement = node.isText ? (
<ChildDescriptorsContext.Consumer>
{(siblingDescriptors) => (
<TextNodeView
node={node}
pos={childPos}
siblingDescriptors={siblingDescriptors}
decorations={outerDeco}
/>
)}
</ChildDescriptorsContext.Consumer>
) : (
<NodeView
node={node}
pos={childPos}
decorations={outerDeco}
innerDecorations={innerDeco}
/>
);

const uniqueMarks: Mark[] = node.marks.filter(
(mark) => !mark.isInSet(sharedMarks)
);
const markedElement = uniqueMarks.reduce(
(element, mark) => <MarkView mark={mark}>{element}</MarkView>,
nodeElement
);

return cloneElement(markedElement, { key: childPos });
});

const childElement = outerDeco.reduce(
(element, deco) => {
const {
nodeName,
class: className,
style: _,
...attrs
} = (deco.type as NonWidgetType).attrs;

if (nodeName) {
return createElement(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
nodeName!,
{
className,
...attrs,
},
element
);
}
if (Array.isArray(element)) {
return (
<>{element.map((el) => cloneElement(el, { className, ...attrs }))}</>
);
}
return cloneElement(element, {
className,
...attrs,
});
},

sharedMarks.reduce(
(element, mark) => <MarkView mark={mark}>{element}</MarkView>,
<>{childElements}</>
)
);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return cloneElement(childElement, { key: innerPos + nodes[0]!.offset });
}

type Props = {
node: Node;
Expand Down Expand Up @@ -76,74 +169,58 @@ export function NodeView({
const children: ReactNode[] = [];
const innerPos = pos + 1;

let queuedChildNodes: QueuedNodes = {
outerDeco: [],
sharedMarks: [],
innerPos,
nodes: [],
};

iterDeco(
node,
innerDecorations,
(widget, offset, index) => {
if (queuedChildNodes.nodes.length) {
children.push(renderQueuedNodes(queuedChildNodes));
queuedChildNodes = {
outerDeco: [],
sharedMarks: [],
innerPos,
nodes: [],
};
}
children.push(
createElement((widget.type as ReactWidgetType).Component, {
key: `${innerPos + offset}-${index}`,
})
<WidgetView
key={`${innerPos + offset}-${index}`}
widget={widget as ReactWidgetDecoration}
/>
);
},
(childNode, outerDeco, innerDeco, offset) => {
const childPos = innerPos + offset;
const nodeElement = childNode.isText ? (
<ChildDescriptorsContext.Consumer>
{(siblingDescriptors) => (
<TextNodeView
node={childNode}
pos={childPos}
siblingDescriptors={siblingDescriptors}
decorations={outerDeco}
/>
)}
</ChildDescriptorsContext.Consumer>
) : (
<NodeView
node={childNode}
pos={childPos}
decorations={outerDeco}
innerDecorations={innerDeco}
/>
);

const childElement = outerDeco.reduce(
(element, deco) => {
const {
nodeName,
class: className,
style: _,
...attrs
} = (deco.type as NonWidgetType).attrs;

if (nodeName) {
return createElement(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
nodeName!,
{
className,
...attrs,
},
element
);
}
return cloneElement(element, {
className,
...attrs,
});
},

childNode.marks.reduce(
(element, mark) => <MarkView mark={mark}>{element}</MarkView>,
nodeElement
)
const sharedMarks = childNode.marks.filter((mark, index) =>
queuedChildNodes.sharedMarks[index]?.eq(mark)
);

children.push(cloneElement(childElement, { key: childPos }));
if (
!sharedMarks.length ||
!sameOuterDeco(queuedChildNodes.outerDeco, outerDeco)
) {
if (queuedChildNodes.nodes.length) {
children.push(renderQueuedNodes(queuedChildNodes));
}
queuedChildNodes.outerDeco = outerDeco;
queuedChildNodes.nodes = [{ node: childNode, innerDeco, offset }];
queuedChildNodes.sharedMarks = childNode.marks;
} else {
queuedChildNodes.nodes.push({ node: childNode, innerDeco, offset });
queuedChildNodes.sharedMarks = sharedMarks;
}
}
);

if (queuedChildNodes.nodes.length) {
children.push(renderQueuedNodes(queuedChildNodes));
}

if (!children.length) {
children.push(<TrailingHackView key={innerPos} pos={innerPos} />);
}
Expand Down
45 changes: 44 additions & 1 deletion src/components/__tests__/EditorView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { render } from "@testing-library/react";
import { EditorState } from "prosemirror-state";
import { doc, li, p, schema, strong, ul } from "prosemirror-test-builder";
import { doc, em, li, p, schema, strong, ul } from "prosemirror-test-builder";
import React from "react";

import { useView } from "../../hooks/useView.js";
Expand Down Expand Up @@ -90,4 +90,47 @@ describe("EditorView", () => {
}
render(<TestEditor />);
});

it("can bias DOM position queries to enter nodes", () => {
const state = EditorState.create({
doc: doc(p(em(strong("a"), "b"), "c")),
});

function Test() {
useView((view) => {
function get(pos: number, bias: number) {
const r = view.domAtPos(pos, bias);
return (
(r.node.nodeType == 1 ? r.node.nodeName : r.node.nodeValue) +
"@" +
r.offset
);
}

expect(get(1, 0)).toBe("P@0");
expect(get(1, -1)).toBe("P@0");
expect(get(1, 1)).toBe("a@0");
expect(get(2, -1)).toBe("a@1");
expect(get(2, 0)).toBe("EM@1");
expect(get(2, 1)).toBe("b@0");
expect(get(3, -1)).toBe("b@1");
expect(get(3, 0)).toBe("P@1");
expect(get(3, 1)).toBe("c@0");
expect(get(4, -1)).toBe("c@1");
expect(get(4, 0)).toBe("P@2");
expect(get(4, 1)).toBe("P@2");
});

return null;
}

function TestEditor() {
return (
<EditorView defaultState={state}>
<Test></Test>
</EditorView>
);
}
render(<TestEditor />);
});
});
2 changes: 1 addition & 1 deletion src/descriptors/ViewDesc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
isEquivalentPosition,
} from "../prosemirror-internal/dom.js";

function sameOuterDeco(
export function sameOuterDeco(
a: readonly DecorationInternal[],
b: readonly DecorationInternal[]
) {
Expand Down

0 comments on commit df3033a

Please sign in to comment.