/* This file is part of the Notesnook project (https://notesnook.com/) Copyright (C) 2023 Streetwriters (Private) Limited This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import { Step, StepResult } from "@tiptap/pm/transform"; import { Node, Schema, Fragment } from "@tiptap/pm/model"; import { Mapping } from "@tiptap/pm/transform"; export interface AttributeUpdate { pos: number; attrName: string; value: any; } export class BatchAttributeStep extends Step { constructor(public updates: AttributeUpdate[]) { super(); } apply(doc: Node): StepResult { const updateMap = new Map>(); for (const update of this.updates) { if (!updateMap.has(update.pos)) { updateMap.set(update.pos, new Map()); } updateMap.get(update.pos)!.set(update.attrName, update.value); } const newDoc = doc.copy(this.updateContent(doc.content, 0, updateMap)); return StepResult.ok(newDoc); } private updateContent( content: Fragment, startPos: number, updateMap: Map> ): Fragment { const nodes: Node[] = []; let pos = startPos; content.forEach((node: Node) => { const nodePos = pos; const attrs = updateMap.get(nodePos); if (attrs) { const newAttrs = { ...node.attrs }; for (const [attrName, value] of attrs.entries()) { newAttrs[attrName] = value; } const newContent = node.isLeaf ? node.content : this.updateContent(node.content, pos + 1, updateMap); nodes.push(node.type.create(newAttrs, newContent, node.marks)); } else { if (!node.isLeaf && node.content.size > 0) { const newContent = this.updateContent( node.content, pos + 1, updateMap ); nodes.push(node.copy(newContent)); } else { nodes.push(node); } } pos += node.nodeSize; }); return Fragment.from(nodes); } invert(doc: Node): Step { const inverseUpdates: AttributeUpdate[] = []; for (const update of this.updates) { const node = doc.nodeAt(update.pos); if (node) { inverseUpdates.push({ pos: update.pos, attrName: update.attrName, value: node.attrs[update.attrName] }); } } return new BatchAttributeStep(inverseUpdates); } map(mapping: Mapping): Step | null { const mappedUpdates: AttributeUpdate[] = []; for (const update of this.updates) { const mappedPos = mapping.map(update.pos); if (mappedPos === null || mappedPos === undefined) { continue; } mappedUpdates.push({ pos: mappedPos, attrName: update.attrName, value: update.value }); } if (mappedUpdates.length === 0) { return null; } return new BatchAttributeStep(mappedUpdates); } toJSON() { return { stepType: "batchAttribute", updates: this.updates }; } static fromJSON(_: Schema, json: any): BatchAttributeStep { return new BatchAttributeStep(json.updates); } } Step.jsonID("batchAttribute", BatchAttributeStep);