mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-10 06:24:35 +09:00
Ref https://github.com/webstudio-is/webstudio-designer/issues/696 Refactored markdown parser to produce normalized instances and added multiple roots support for insert utility. Note: can be reviewed with hidden spaces
434 lines
12 KiB
TypeScript
434 lines
12 KiB
TypeScript
import { nanoid } from "nanoid";
|
|
import {
|
|
getStyleDeclKey,
|
|
Instance,
|
|
Instances,
|
|
InstancesItem,
|
|
Prop,
|
|
Props,
|
|
StyleDecl,
|
|
Styles,
|
|
StyleSource,
|
|
StyleSources,
|
|
StyleSourceSelection,
|
|
StyleSourceSelections,
|
|
} from "@webstudio-is/project-build";
|
|
import { getComponentMeta } from "@webstudio-is/react-sdk";
|
|
|
|
const traverseInstances = (
|
|
instance: Instance,
|
|
cb: (child: Instance, parent: Instance) => void
|
|
) => {
|
|
for (const child of instance.children) {
|
|
if (child.type === "text") {
|
|
continue;
|
|
}
|
|
if (child.type === "instance") {
|
|
cb(child, instance);
|
|
traverseInstances(child, cb);
|
|
}
|
|
}
|
|
};
|
|
|
|
export type InstancesIndex = {
|
|
rootInstanceId: undefined | Instance["id"];
|
|
instancesById: Map<Instance["id"], Instance>;
|
|
parentInstancesById: Map<Instance["id"], Instance>;
|
|
};
|
|
|
|
export const createInstancesIndex = (
|
|
rootInstance: undefined | Instance
|
|
): InstancesIndex => {
|
|
const instancesById = new Map<Instance["id"], Instance>();
|
|
const parentInstancesById = new Map<Instance["id"], Instance>();
|
|
if (rootInstance) {
|
|
// traverse skips root without parent
|
|
instancesById.set(rootInstance.id, rootInstance);
|
|
traverseInstances(rootInstance, (child, parent) => {
|
|
parentInstancesById.set(child.id, parent);
|
|
instancesById.set(child.id, child);
|
|
});
|
|
}
|
|
return {
|
|
rootInstanceId: rootInstance?.id,
|
|
instancesById,
|
|
parentInstancesById,
|
|
};
|
|
};
|
|
|
|
const isInstanceDroppable = (instance: Instance) => {
|
|
const meta = getComponentMeta(instance.component);
|
|
return meta?.type === "body" || meta?.type === "container";
|
|
};
|
|
|
|
export type DroppableTarget = {
|
|
parentId: Instance["id"];
|
|
position: number | "end";
|
|
};
|
|
|
|
export const findClosestDroppableTarget = (
|
|
instancesIndex: InstancesIndex,
|
|
instanceId: undefined | Instance["id"]
|
|
): undefined | DroppableTarget => {
|
|
const { instancesById, parentInstancesById } = instancesIndex;
|
|
|
|
if (instancesIndex.rootInstanceId === undefined) {
|
|
return;
|
|
}
|
|
// fallback to root instance
|
|
let droppableInstance = instancesById.get(
|
|
instanceId ?? instancesIndex.rootInstanceId
|
|
);
|
|
if (droppableInstance === undefined) {
|
|
return;
|
|
}
|
|
let position = -1;
|
|
while (isInstanceDroppable(droppableInstance) === false) {
|
|
const parentInstance = parentInstancesById.get(droppableInstance.id);
|
|
if (parentInstance === undefined) {
|
|
break;
|
|
}
|
|
// source of lookup in children
|
|
const sourceInstanceId = droppableInstance.id;
|
|
droppableInstance = parentInstance;
|
|
position = droppableInstance.children.findIndex(
|
|
(child) => child.type === "instance" && child.id === sourceInstanceId
|
|
);
|
|
}
|
|
|
|
return {
|
|
parentId: droppableInstance.id,
|
|
// put in the end when no position provided
|
|
position:
|
|
position === -1 ? droppableInstance.children.length : position + 1,
|
|
};
|
|
};
|
|
|
|
export const insertInstanceMutable = (
|
|
instancesIndex: InstancesIndex,
|
|
instance: Instance,
|
|
dropTarget?: DroppableTarget
|
|
) => {
|
|
if (dropTarget === undefined) {
|
|
return;
|
|
}
|
|
const parentInstance = instancesIndex.instancesById.get(dropTarget.parentId);
|
|
if (parentInstance === undefined) {
|
|
return;
|
|
}
|
|
const { position } = dropTarget;
|
|
if (position === "end") {
|
|
parentInstance.children.push(instance);
|
|
} else {
|
|
parentInstance.children.splice(position, 0, instance);
|
|
}
|
|
};
|
|
|
|
export const reparentInstanceMutable = (
|
|
instancesIndex: InstancesIndex,
|
|
instanceId: Instance["id"],
|
|
dropTarget: DroppableTarget
|
|
) => {
|
|
const prevParent = instancesIndex.parentInstancesById.get(instanceId);
|
|
const nextParent = instancesIndex.instancesById.get(dropTarget.parentId);
|
|
const instance = instancesIndex.instancesById.get(instanceId);
|
|
if (
|
|
prevParent === undefined ||
|
|
nextParent === undefined ||
|
|
instance === undefined
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const prevPosition = prevParent.children.findIndex(
|
|
(child) => child.type === "instance" && child.id === instanceId
|
|
);
|
|
if (prevPosition === -1) {
|
|
return;
|
|
}
|
|
|
|
// if parent is the same, we need to adjust the position
|
|
// to account for the removal of the instance.
|
|
let nextPosition = dropTarget.position;
|
|
if (
|
|
nextPosition !== "end" &&
|
|
prevParent.id === nextParent.id &&
|
|
prevPosition < nextPosition
|
|
) {
|
|
nextPosition -= 1;
|
|
}
|
|
|
|
prevParent.children.splice(prevPosition, 1);
|
|
if (nextPosition === "end") {
|
|
nextParent.children.push(instance);
|
|
} else {
|
|
nextParent.children.splice(nextPosition, 0, instance);
|
|
}
|
|
};
|
|
|
|
export const getInstanceAncestorsAndSelf = (
|
|
instancesIndex: InstancesIndex,
|
|
instanceId: Instance["id"]
|
|
) => {
|
|
const { instancesById, parentInstancesById } = instancesIndex;
|
|
const path = [];
|
|
let instance = instancesById.get(instanceId);
|
|
while (instance) {
|
|
path.unshift(instance);
|
|
instance = parentInstancesById.get(instance.id);
|
|
}
|
|
return path;
|
|
};
|
|
|
|
export const findClosestRichTextInstance = (
|
|
instancesIndex: InstancesIndex,
|
|
instanceId: Instance["id"]
|
|
) => {
|
|
return getInstanceAncestorsAndSelf(instancesIndex, instanceId)
|
|
.reverse()
|
|
.find(
|
|
(instance) => getComponentMeta(instance.component)?.type === "rich-text"
|
|
);
|
|
};
|
|
|
|
export const findParentInstance = (
|
|
instances: Instances,
|
|
instanceId: Instance["id"]
|
|
) => {
|
|
for (const instance of instances.values()) {
|
|
for (const child of instance.children) {
|
|
if (child.type === "id" && child.value === instanceId) {
|
|
return instance;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const traverseInstancesMap = (
|
|
instances: Map<Instance["id"], InstancesItem>,
|
|
instanceId: Instance["id"],
|
|
callback: (instance: InstancesItem) => void
|
|
) => {
|
|
const instance = instances.get(instanceId);
|
|
if (instance === undefined) {
|
|
return;
|
|
}
|
|
callback(instance);
|
|
for (const child of instance.children) {
|
|
if (child.type === "id") {
|
|
traverseInstancesMap(instances, child.value, callback);
|
|
}
|
|
}
|
|
};
|
|
|
|
export const findTreeInstances = (
|
|
instances: Instances,
|
|
rootInstanceId: Instance["id"]
|
|
) => {
|
|
const subtreeIds = new Set<Instance["id"]>();
|
|
traverseInstancesMap(instances, rootInstanceId, (instance) => {
|
|
subtreeIds.add(instance.id);
|
|
});
|
|
return subtreeIds;
|
|
};
|
|
|
|
export const cloneStyles = (
|
|
styles: Styles,
|
|
clonedStyleSourceIds: Map<Instance["id"], Instance["id"]>
|
|
) => {
|
|
const clonedStyles: StyleDecl[] = [];
|
|
for (const styleDecl of styles.values()) {
|
|
const styleSourceId = clonedStyleSourceIds.get(styleDecl.styleSourceId);
|
|
if (styleSourceId === undefined) {
|
|
continue;
|
|
}
|
|
clonedStyles.push({
|
|
...styleDecl,
|
|
styleSourceId,
|
|
});
|
|
}
|
|
return clonedStyles;
|
|
};
|
|
|
|
export const findSubtreeLocalStyleSources = (
|
|
subtreeIds: Set<Instance["id"]>,
|
|
styleSources: StyleSources,
|
|
styleSourceSelections: StyleSourceSelections
|
|
) => {
|
|
const localStyleSourceIds = new Set<StyleSource["id"]>();
|
|
for (const styleSource of styleSources.values()) {
|
|
if (styleSource.type === "local") {
|
|
localStyleSourceIds.add(styleSource.id);
|
|
}
|
|
}
|
|
|
|
const subtreeLocalStyleSourceIds = new Set<StyleSource["id"]>();
|
|
for (const { instanceId, values } of styleSourceSelections.values()) {
|
|
// skip selections outside of subtree
|
|
if (subtreeIds.has(instanceId) === false) {
|
|
continue;
|
|
}
|
|
// find only local style sources on selections
|
|
for (const styleSourceId of values) {
|
|
if (localStyleSourceIds.has(styleSourceId)) {
|
|
subtreeLocalStyleSourceIds.add(styleSourceId);
|
|
}
|
|
}
|
|
}
|
|
|
|
return subtreeLocalStyleSourceIds;
|
|
};
|
|
|
|
export const insertInstancesMutable = (
|
|
instances: Instances,
|
|
insertedInstances: InstancesItem[],
|
|
rootIds: Instance["id"][],
|
|
dropTarget: undefined | DroppableTarget
|
|
) => {
|
|
if (dropTarget === undefined) {
|
|
return;
|
|
}
|
|
const parentInstance = instances.get(dropTarget.parentId);
|
|
if (parentInstance === undefined) {
|
|
return;
|
|
}
|
|
|
|
let treeRootInstanceId: undefined | Instance["id"] = undefined;
|
|
for (const instance of insertedInstances) {
|
|
if (treeRootInstanceId === undefined) {
|
|
treeRootInstanceId = instance.id;
|
|
}
|
|
instances.set(instance.id, instance);
|
|
}
|
|
if (treeRootInstanceId === undefined) {
|
|
return;
|
|
}
|
|
|
|
const { position } = dropTarget;
|
|
const dropTargetChildren: InstancesItem["children"] = rootIds.map(
|
|
(instanceId) => ({
|
|
type: "id",
|
|
value: instanceId,
|
|
})
|
|
);
|
|
if (position === "end") {
|
|
parentInstance.children.push(...dropTargetChildren);
|
|
} else {
|
|
parentInstance.children.splice(position, 0, ...dropTargetChildren);
|
|
}
|
|
};
|
|
|
|
export const insertInstancesCopyMutable = (
|
|
instances: Instances,
|
|
copiedInstances: InstancesItem[],
|
|
dropTarget: undefined | DroppableTarget
|
|
) => {
|
|
const copiedInstanceIds = new Map<Instance["id"], Instance["id"]>();
|
|
const copiedInstancesWithNewIds: InstancesItem[] = [];
|
|
for (const instance of copiedInstances) {
|
|
const newInstanceId = nanoid();
|
|
copiedInstanceIds.set(instance.id, newInstanceId);
|
|
}
|
|
|
|
for (const instance of copiedInstances) {
|
|
copiedInstancesWithNewIds.push({
|
|
...instance,
|
|
id: copiedInstanceIds.get(instance.id) ?? instance.id,
|
|
children: instance.children.map((child) => {
|
|
if (child.type === "id") {
|
|
return {
|
|
type: "id",
|
|
value: copiedInstanceIds.get(child.value) ?? child.value,
|
|
};
|
|
}
|
|
return child;
|
|
}),
|
|
});
|
|
}
|
|
|
|
insertInstancesMutable(
|
|
instances,
|
|
copiedInstancesWithNewIds,
|
|
// consider the first instance as the root
|
|
[copiedInstancesWithNewIds[0].id],
|
|
dropTarget
|
|
);
|
|
|
|
return copiedInstanceIds;
|
|
};
|
|
|
|
export const insertStyleSourcesCopyMutable = (
|
|
styleSources: StyleSources,
|
|
copiedStyleSources: StyleSource[]
|
|
) => {
|
|
// store map of old ids to new ids to copy dependant data
|
|
const copiedStyleSourceIds = new Map<StyleSource["id"], StyleSource["id"]>();
|
|
for (const styleSource of copiedStyleSources) {
|
|
const newStyleSourceId = nanoid();
|
|
copiedStyleSourceIds.set(styleSource.id, newStyleSourceId);
|
|
styleSources.set(newStyleSourceId, {
|
|
...styleSource,
|
|
id: newStyleSourceId,
|
|
});
|
|
}
|
|
return copiedStyleSourceIds;
|
|
};
|
|
|
|
export const insertPropsCopyMutable = (
|
|
props: Props,
|
|
copiedProps: Prop[],
|
|
copiedInstanceIds: Map<Instance["id"], Instance["id"]>
|
|
) => {
|
|
for (const prop of copiedProps) {
|
|
const newInstanceId =
|
|
copiedInstanceIds.get(prop.instanceId) ?? prop.instanceId;
|
|
const newPropId = nanoid();
|
|
props.set(newPropId, {
|
|
...prop,
|
|
id: newPropId,
|
|
instanceId: newInstanceId,
|
|
});
|
|
}
|
|
};
|
|
|
|
export const insertStyleSourceSelectionsCopyMutable = (
|
|
styleSourceSelections: StyleSourceSelections,
|
|
copiedStyleSourceSelections: StyleSourceSelection[],
|
|
copiedInstanceIds: Map<Instance["id"], Instance["id"]>,
|
|
copiedStyleSourceIds: Map<StyleSource["id"], StyleSource["id"]>
|
|
) => {
|
|
for (const styleSourceSelection of copiedStyleSourceSelections) {
|
|
// preserve ids when no new id provided
|
|
// for example for non-local style source
|
|
const newInstanceId =
|
|
copiedInstanceIds.get(styleSourceSelection.instanceId) ??
|
|
styleSourceSelection.instanceId;
|
|
const newValues = styleSourceSelection.values.map(
|
|
(styleSourceId) =>
|
|
copiedStyleSourceIds.get(styleSourceId) ?? styleSourceId
|
|
);
|
|
styleSourceSelections.set(newInstanceId, {
|
|
instanceId: newInstanceId,
|
|
values: newValues,
|
|
});
|
|
}
|
|
};
|
|
|
|
export const insertStylesCopyMutable = (
|
|
styles: Styles,
|
|
copiedStyles: StyleDecl[],
|
|
copiedStyleSourceIds: Map<StyleSource["id"], StyleSource["id"]>
|
|
) => {
|
|
for (const styleDecl of copiedStyles) {
|
|
// preserve ids when no new id provided
|
|
const newStyleSourceId =
|
|
copiedStyleSourceIds.get(styleDecl.styleSourceId) ??
|
|
styleDecl.styleSourceId;
|
|
const styleDeclCopy = {
|
|
...styleDecl,
|
|
styleSourceId: newStyleSourceId,
|
|
};
|
|
styles.set(getStyleDeclKey(styleDeclCopy), styleDeclCopy);
|
|
}
|
|
};
|