Skip to content

Commit

Permalink
Make shared mark concatenation recursive
Browse files Browse the repository at this point in the history
  • Loading branch information
smoores-dev committed Aug 7, 2023
1 parent 8ae0923 commit a22c5b5
Show file tree
Hide file tree
Showing 5 changed files with 305 additions and 316 deletions.
123 changes: 9 additions & 114 deletions src/components/DocNodeView.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,15 @@
import { Node } from "prosemirror-model";
import { DecorationSet } from "prosemirror-view";
import React, {
ForwardedRef,
ReactNode,
cloneElement,
createElement,
forwardRef,
useContext,
useImperativeHandle,
useLayoutEffect,
useRef,
} from "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 {
DecorationSourceInternal,
NonWidgetType,
} from "../prosemirror-internal/DecorationInternal.js";

import { MarkView } from "./MarkView.js";
import { NodeView } from "./NodeView.js";
import { TextNodeView } from "./TextNodeView.js";
import { useChildNodeViews } from "../hooks/useChildNodeViews.js";
import { useNodeViewDescriptor } from "../hooks/useNodeViewDescriptor.js";
import { DecorationSourceInternal } from "../prosemirror-internal/DecorationInternal.js";

type Props = {
node: Node;
Expand All @@ -35,9 +21,6 @@ export const DocNodeView = forwardRef(function DocNodeView(
{ node, contentEditable, decorations, ...props }: Props,
ref: ForwardedRef<HTMLDivElement | null>
) {
const { posToDesc, domToDesc } = useContext(NodeViewContext);
const siblingDescriptors = useContext(ChildDescriptorsContext);
const childDescriptors: ViewDesc[] = [];
const innerRef = useRef<HTMLDivElement | null>(null);

useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(
Expand All @@ -48,103 +31,15 @@ export const DocNodeView = forwardRef(function DocNodeView(
[]
);

useLayoutEffect(() => {
if (!innerRef.current) return;

const firstChildDesc = childDescriptors[0];

const desc = new NodeViewDesc(
undefined,
childDescriptors,
node,
[],
DecorationSet.empty,
innerRef.current,
firstChildDesc?.dom.parentElement ?? null,
innerRef.current,
posToDesc,
domToDesc
);
posToDesc.set(-1, desc);
domToDesc.set(innerRef.current, desc);
siblingDescriptors.push(desc);

for (const childDesc of childDescriptors) {
childDesc.parent = desc;
}
});

const children: ReactNode[] = [];
const innerPos = 0;

iterDeco(
const childDescriptors = useNodeViewDescriptor(
-1,
node,
decorations,
(widget, offset, index) => {
children.push(
createElement((widget.type as ReactWidgetType).Component, {
key: `${innerPos + offset}-${index}`,
})
);
},
(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
)
);

children.push(cloneElement(childElement, { key: childPos }));
}
innerRef,
decorations
);

const children = useChildNodeViews(-1, node, decorations);

return (
<div
ref={(element) => {
Expand Down
199 changes: 9 additions & 190 deletions src/components/NodeView.tsx
Original file line number Diff line number Diff line change
@@ -1,124 +1,23 @@
import { DOMOutputSpec, Mark, Node } from "prosemirror-model";
import { DOMOutputSpec, Node } from "prosemirror-model";
import { NodeSelection } from "prosemirror-state";
import React, {
ForwardRefExoticComponent,
ReactNode,
RefAttributes,
cloneElement,
createElement,
useContext,
useLayoutEffect,
useRef,
} from "react";

import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js";
import { NodeViewContext } from "../contexts/NodeViewContext.js";
import {
NodeViewDesc,
ViewDesc,
iterDeco,
sameOuterDeco,
} from "../descriptors/ViewDesc.js";
import { useChildNodeViews } from "../hooks/useChildNodeViews.js";
import { useNodeViewDescriptor } from "../hooks/useNodeViewDescriptor.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 All @@ -134,96 +33,16 @@ export function NodeView({
innerDecorations,
...props
}: Props) {
const { posToDesc, domToDesc, nodeViews, state } =
useContext(NodeViewContext);
const siblingDescriptors = useContext(ChildDescriptorsContext);
const childDescriptors: ViewDesc[] = [];
const { nodeViews, state } = useContext(NodeViewContext);
const domRef = useRef<HTMLElement | null>(null);

useLayoutEffect(() => {
if (!domRef.current) return;

const firstChildDesc = childDescriptors[0];

const desc = new NodeViewDesc(
undefined,
childDescriptors,
node,
[],
innerDecorations,
domRef.current,
firstChildDesc?.dom.parentElement ?? null,
domRef.current ?? domRef.current,
posToDesc,
domToDesc
);
posToDesc.set(pos, desc);
domToDesc.set(domRef.current, desc);
siblingDescriptors.push(desc);

for (const childDesc of childDescriptors) {
childDesc.parent = desc;
}
});

const children: ReactNode[] = [];
const innerPos = pos + 1;

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

iterDeco(
const childDescriptors = useNodeViewDescriptor(
pos,
node,
innerDecorations,
(widget, offset, index) => {
if (queuedChildNodes.nodes.length) {
children.push(renderQueuedNodes(queuedChildNodes));
queuedChildNodes = {
outerDeco: [],
sharedMarks: [],
innerPos,
nodes: [],
};
}
children.push(
<WidgetView
key={`${innerPos + offset}-${index}`}
widget={widget as ReactWidgetDecoration}
/>
);
},
(childNode, outerDeco, innerDeco, offset) => {
const sharedMarks = childNode.marks.filter((mark, index) =>
queuedChildNodes.sharedMarks[index]?.eq(mark)
);
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;
}
}
domRef,
innerDecorations
);

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

if (!children.length) {
children.push(<TrailingHackView key={innerPos} pos={innerPos} />);
}
const children = useChildNodeViews(pos, node, innerDecorations);

let element: JSX.Element | null = null;

Expand Down
Loading

0 comments on commit a22c5b5

Please sign in to comment.