/* * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import type { LineWithId } from '../../extensions/linemarker/types' import type { ArrayChange } from 'diff' import { diffArrays } from 'diff' type NewLine = string type LineChange = ArrayChange /** * Calculates ids for every line in a given text and memorized the state of the last given text. * It also assigns ids for new lines on every update. */ export class LineContentToLineIdMapper { private lastLines: LineWithId[] = [] private lastUsedLineId = 0 /** * Calculates a line id mapping for the given line based text by creating a diff * with the last lines code. * * @param newMarkdownContentLines The markdown content for which the line ids should be calculated * @return the calculated {@link LineWithId lines with unique ids} */ public updateLineMapping(newMarkdownContentLines: string[]): LineWithId[] { const lineDifferences = this.diffNewLinesWithLastLineKeys(newMarkdownContentLines) const newLineKeys = this.convertChangesToLinesWithIds(lineDifferences) this.lastLines = newLineKeys return newLineKeys } /** * Creates a diff between the given {@link string lines} and the existing {@link LineWithId lines with unique ids}. * The diff is based on the line content. * * @param lines The plain lines that describe the new state. * @return {@link LineChange line changes} that describe the difference between the given and the old lines. Because of the way the used diff-lib works, the ADDED lines will be tagged as "removed", because if two lines are the same the lib takes the line from the NEW lines, which results in a loss of the unique id. */ private diffNewLinesWithLastLineKeys(lines: string[]): LineChange[] { return diffArrays(lines, this.lastLines, { comparator: (left: NewLine | LineWithId, right: NewLine | LineWithId) => { const leftLine = (left as LineWithId).line ?? (left as NewLine) const rightLine = (right as LineWithId).line ?? (right as NewLine) return leftLine === rightLine } }) } /** * Converts the given {@link LineChange line changes} to {@link lines with unique ids}. * Only not changed or added lines will be processed. * * @param changes The {@link LineChange changes} whose lines should be converted. * @return The created or reused {@link LineWithId lines with ids} */ private convertChangesToLinesWithIds(changes: LineChange[]): LineWithId[] { return changes .filter( (change) => LineContentToLineIdMapper.changeIsNotChangingLines(change) || LineContentToLineIdMapper.changeIsAddingLines(change) ) .reduce( (previousLineKeys, currentChange) => [...previousLineKeys, ...this.convertChangeToLinesWithIds(currentChange)], [] as LineWithId[] ) } /** * Defines if the given {@link LineChange change} is neither adding or removing lines. * * @param change The {@link LineChange change} to check. * @return {@link true} if the given change is neither adding nor removing lines. */ private static changeIsNotChangingLines(change: LineChange): boolean { return change.added === undefined && change.removed === undefined } /** * Defines if the given {@link LineChange change} contains new, not existing lines. * * @param change The {@link LineChange change} to check. * @return {@link true} if the given change contains {@link NewLine new lines} */ private static changeIsAddingLines(change: LineChange): change is ArrayChange { return change.removed === true } /** * Converts the given {@link LineChange change} into {@link LineWithId lines with unique ids} by inspecting the contained lines. * This is done by either reusing the existing ids (if the line wasn't added), * or by assigning new, unused line ids. * * @param change The {@link LineChange change} whose lines should be converted. * @return The created or reused {@link LineWithId lines with ids} */ private convertChangeToLinesWithIds(change: LineChange): LineWithId[] { if (LineContentToLineIdMapper.changeIsAddingLines(change)) { return change.value.map((line) => { this.lastUsedLineId += 1 return { line: line, id: this.lastUsedLineId } }) } else { return change.value as LineWithId[] } } }