UNPKG

@blocknote/core

Version:

A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.

207 lines (171 loc) 5.72 kB
/** * Instead of depending on the NPM package, we vendor this file from https://github.com/n1ru4l/toposort/blob/main/src/toposort.ts (MIT) * * There was a recent publish, despite not having been updated in 2 years, which is suspicious. * * This file is also simple enough that we can maintain it ourselves. */ export type DirectedAcyclicGraph = Map<string, Iterable<string>>; export type DependencyGraph = DirectedAcyclicGraph; export type TaskList = Array<Set<string>>; // Add more specific types for better type safety export type NodeId = string; export type DependencyMap = Map<NodeId, Set<NodeId>>; export function toposort(dag: DirectedAcyclicGraph): TaskList { const inDegrees = countInDegrees(dag); let { roots, nonRoots } = getRootsAndNonRoots(inDegrees); const sorted: TaskList = []; while (roots.size) { sorted.push(roots); const newRoots = new Set<NodeId>(); for (const root of roots) { const dependents = dag.get(root); if (!dependents) { // Handle case where node has no dependents continue; } for (const dependent of dependents) { const currentDegree = inDegrees.get(dependent); if (currentDegree === undefined) { // Handle case where dependent node is not in inDegrees continue; } const newDegree = currentDegree - 1; inDegrees.set(dependent, newDegree); if (newDegree === 0) { newRoots.add(dependent); } } } roots = newRoots; } nonRoots = getRootsAndNonRoots(inDegrees).nonRoots; if (nonRoots.size) { throw new Error( `Cycle(s) detected; toposort only works on acyclic graphs. Cyclic nodes: ${Array.from(nonRoots).join(", ")}`, ); } return sorted; } export function toposortReverse(deps: DependencyGraph): TaskList { const dag = reverse(deps); return toposort(dag); } type InDegrees = Map<NodeId, number>; function countInDegrees(dag: DirectedAcyclicGraph): InDegrees { const counts: InDegrees = new Map(); for (const [vx, dependents] of dag.entries()) { // Initialize count for current node if not present if (!counts.has(vx)) { counts.set(vx, 0); } for (const dependent of dependents) { const currentCount = counts.get(dependent) ?? 0; counts.set(dependent, currentCount + 1); } } return counts; } function getRootsAndNonRoots(counts: InDegrees) { const roots = new Set<NodeId>(); const nonRoots = new Set<NodeId>(); for (const [id, deg] of counts.entries()) { if (deg === 0) { roots.add(id); } else { nonRoots.add(id); } } return { roots, nonRoots }; } function reverse(deps: DirectedAcyclicGraph): DependencyGraph { const reversedDeps: DependencyMap = new Map(); for (const [name, dependsOn] of deps.entries()) { // Ensure the source node exists in the reversed map if (!reversedDeps.has(name)) { reversedDeps.set(name, new Set()); } for (const dependsOnName of dependsOn) { if (!reversedDeps.has(dependsOnName)) { reversedDeps.set(dependsOnName, new Set()); } reversedDeps.get(dependsOnName)!.add(name); } } return reversedDeps; } export function createDependencyGraph(): DependencyMap { return new Map(); } export function addDependency( graph: DependencyMap, from: NodeId, to: NodeId, ): DependencyMap { if (!graph.has(from)) { graph.set(from, new Set()); } graph.get(from)!.add(to); return graph; } export function removeDependency( graph: DependencyMap, from: NodeId, to: NodeId, ): boolean { const dependents = graph.get(from); if (!dependents) { return false; } return dependents.delete(to); } export function hasDependency( graph: DependencyMap, from: NodeId, to: NodeId, ): boolean { const dependents = graph.get(from); return dependents ? dependents.has(to) : false; } /** * Sorts a list of items by their dependencies * @returns A function which can retrieve the priority of an item */ export function sortByDependencies( items: { key: string; runsBefore?: ReadonlyArray<string> }[], ) { const dag = createDependencyGraph(); for (const item of items) { if (Array.isArray(item.runsBefore) && item.runsBefore.length > 0) { item.runsBefore.forEach((runBefore) => { addDependency(dag, item.key, runBefore); }); } else { addDependency(dag, "default", item.key); } } const sortedSpecs = toposortReverse(dag); const defaultIndex = sortedSpecs.findIndex((set) => set.has("default")); /** * The priority of an item is described relative to the "default" (an arbitrary string which can be used as the reference) * * Since items are topologically sorted, we can see what their relative position is to the "default" * Each layer away from the default is 10 priority points (arbitrarily chosen) * The default is fixed at 101 (1 point higher than any tiptap extension, giving priority to custom blocks than any defaults) * * This is a bit of a hack, but it's a simple way to ensure that custom items are always rendered with higher priority than default items * and that custom items are rendered in the order they are defined in the list */ /** * Retrieves the priority of an item based on its position in the topologically sorted list * @param key - The key of the item to get the priority of * @returns The priority of the item */ return (key: string) => { const index = sortedSpecs.findIndex((set) => set.has(key)); // the default index should map to 101 // one before the default index is 91 // one after is 111 return 91 + (index + defaultIndex) * 10; }; }