* Comment model * Framework, model, policy, presenter, api endpoint etc * Iteration, first pass of UI * fixes, refactors * Comment commands * comment socket support * typing indicators * comment component, styling * wip * right sidebar resize * fix: CMD+Enter submit * Add usePersistedState fix: Main page scrolling on comment highlight * drafts * Typing indicator * refactor * policies * Click thread to highlight Improve comment timestamps * padding * Comment menu v1 * Change comments to use editor * Basic comment editing * fix: Hide commenting button when disabled at team level * Enable opening sidebar without mark * Move selected comment to location state * Add comment delete confirmation * Add comment count to document meta * fix: Comment sidebar togglable Add copy link to comment * stash * Restore History changes * Refactor right sidebar to allow for comment animation * Update to new router best practices * stash * Various improvements * stash * Handle click outside * Fix incorrect placeholder in input fix: Input box appearing on other sessions erroneously * stash * fix: Don't leave orphaned child comments * styling * stash * Enable comment toggling again * Edit styling, merge conflicts * fix: Cannot navigate from insights to comments * Remove draft comment mark on click outside * Fix: Empty comment sidebar, tsc * Remove public toggle * fix: All comments are recessed fix: Comments should not be printed * fix: Associated mark should be removed on comment delete * Revert unused changes * Empty state, basic RTL support * Create dont toggle comment mark * Make it feel more snappy * Highlight active comment in text * fix animation * RTL support * Add reply CTA * Translations
227 lines
5.9 KiB
TypeScript
227 lines
5.9 KiB
TypeScript
import { PluginSimple } from "markdown-it";
|
|
import { keymap } from "prosemirror-keymap";
|
|
import { MarkdownParser, TokenConfig } from "prosemirror-markdown";
|
|
import { Schema } from "prosemirror-model";
|
|
import { EditorView } from "prosemirror-view";
|
|
import { Editor } from "~/editor";
|
|
import Mark from "../marks/Mark";
|
|
import Node from "../nodes/Node";
|
|
import Extension, { CommandFactory } from "./Extension";
|
|
import makeRules from "./markdown/rules";
|
|
import { MarkdownSerializer } from "./markdown/serializer";
|
|
|
|
export default class ExtensionManager {
|
|
extensions: (Node | Mark | Extension)[] = [];
|
|
|
|
constructor(
|
|
extensions: (
|
|
| Extension
|
|
| typeof Node
|
|
| typeof Mark
|
|
| typeof Extension
|
|
)[] = [],
|
|
editor?: Editor
|
|
) {
|
|
extensions.forEach((ext) => {
|
|
let extension;
|
|
|
|
if (typeof ext === "function") {
|
|
// @ts-expect-error We won't instantiate an abstract class
|
|
extension = new ext(editor?.props);
|
|
} else {
|
|
extension = ext;
|
|
}
|
|
|
|
if (editor) {
|
|
extension.bindEditor(editor);
|
|
}
|
|
|
|
this.extensions.push(extension);
|
|
});
|
|
}
|
|
|
|
get nodes() {
|
|
return this.extensions
|
|
.filter((extension) => extension.type === "node")
|
|
.reduce(
|
|
(nodes, node: Node) => ({
|
|
...nodes,
|
|
[node.name]: node.schema,
|
|
}),
|
|
{}
|
|
);
|
|
}
|
|
|
|
serializer() {
|
|
const nodes = this.extensions
|
|
.filter((extension) => extension.type === "node")
|
|
.reduce(
|
|
(nodes, extension: Node) => ({
|
|
...nodes,
|
|
[extension.name]: extension.toMarkdown,
|
|
}),
|
|
{}
|
|
);
|
|
|
|
const marks = this.extensions
|
|
.filter((extension) => extension.type === "mark")
|
|
.reduce(
|
|
(marks, extension: Mark) => ({
|
|
...marks,
|
|
[extension.name]: extension.toMarkdown,
|
|
}),
|
|
{}
|
|
);
|
|
|
|
return new MarkdownSerializer(nodes, marks);
|
|
}
|
|
|
|
parser({
|
|
schema,
|
|
rules,
|
|
plugins,
|
|
}: {
|
|
schema: Schema;
|
|
rules?: Record<string, any>;
|
|
plugins?: PluginSimple[];
|
|
}): MarkdownParser {
|
|
const tokens: Record<string, TokenConfig> = this.extensions
|
|
.filter(
|
|
(extension) => extension.type === "mark" || extension.type === "node"
|
|
)
|
|
.reduce((nodes, extension: Node | Mark) => {
|
|
const md = extension.parseMarkdown();
|
|
if (!md) {
|
|
return nodes;
|
|
}
|
|
|
|
return {
|
|
...nodes,
|
|
[extension.markdownToken || extension.name]: md,
|
|
};
|
|
}, {});
|
|
|
|
return new MarkdownParser(schema, makeRules({ rules, plugins }), tokens);
|
|
}
|
|
|
|
get marks() {
|
|
return this.extensions
|
|
.filter((extension) => extension.type === "mark")
|
|
.reduce(
|
|
(marks, { name, schema }: Mark) => ({
|
|
...marks,
|
|
[name]: schema,
|
|
}),
|
|
{}
|
|
);
|
|
}
|
|
|
|
get plugins() {
|
|
return this.extensions
|
|
.filter((extension) => "plugins" in extension)
|
|
.reduce((allPlugins, { plugins }) => [...allPlugins, ...plugins], []);
|
|
}
|
|
|
|
get rulePlugins() {
|
|
return this.extensions
|
|
.filter((extension) => "rulePlugins" in extension)
|
|
.reduce(
|
|
(allRulePlugins, { rulePlugins }) => [
|
|
...allRulePlugins,
|
|
...rulePlugins,
|
|
],
|
|
[]
|
|
);
|
|
}
|
|
|
|
keymaps({ schema }: { schema: Schema }) {
|
|
const keymaps = this.extensions
|
|
.filter((extension) => extension.keys)
|
|
.map((extension) =>
|
|
["node", "mark"].includes(extension.type)
|
|
? extension.keys({
|
|
type: schema[`${extension.type}s`][extension.name],
|
|
schema,
|
|
})
|
|
: (extension as Extension).keys({ schema })
|
|
);
|
|
|
|
return keymaps.map(keymap);
|
|
}
|
|
|
|
inputRules({ schema }: { schema: Schema }) {
|
|
const extensionInputRules = this.extensions
|
|
.filter((extension) => ["extension"].includes(extension.type))
|
|
.filter((extension) => extension.inputRules)
|
|
.map((extension: Extension) => extension.inputRules({ schema }));
|
|
|
|
const nodeMarkInputRules = this.extensions
|
|
.filter((extension) => ["node", "mark"].includes(extension.type))
|
|
.filter((extension) => extension.inputRules)
|
|
.map((extension) =>
|
|
extension.inputRules({
|
|
type: schema[`${extension.type}s`][extension.name],
|
|
schema,
|
|
})
|
|
);
|
|
|
|
return [...extensionInputRules, ...nodeMarkInputRules].reduce(
|
|
(allInputRules, inputRules) => [...allInputRules, ...inputRules],
|
|
[]
|
|
);
|
|
}
|
|
|
|
commands({ schema, view }: { schema: Schema; view: EditorView }) {
|
|
return this.extensions
|
|
.filter((extension) => extension.commands)
|
|
.reduce((allCommands, extension) => {
|
|
const { name, type } = extension;
|
|
const commands = {};
|
|
|
|
// @ts-expect-error FIXME
|
|
const value = extension.commands({
|
|
schema,
|
|
...(["node", "mark"].includes(type)
|
|
? {
|
|
type: schema[`${type}s`][name],
|
|
}
|
|
: {}),
|
|
});
|
|
|
|
const apply = (
|
|
callback: CommandFactory,
|
|
attrs: Record<string, any>
|
|
) => {
|
|
if (!view.editable) {
|
|
return false;
|
|
}
|
|
view.focus();
|
|
return callback(attrs)(view.state, view.dispatch, view);
|
|
};
|
|
|
|
const handle = (_name: string, _value: CommandFactory) => {
|
|
if (Array.isArray(_value)) {
|
|
commands[_name] = (attrs: Record<string, any>) =>
|
|
_value.forEach((callback) => apply(callback, attrs));
|
|
} else if (typeof _value === "function") {
|
|
commands[_name] = (attrs: Record<string, any>) =>
|
|
apply(_value, attrs);
|
|
}
|
|
};
|
|
|
|
if (typeof value === "object") {
|
|
Object.entries(value).forEach(([commandName, commandValue]) => {
|
|
handle(commandName, commandValue);
|
|
});
|
|
} else if (value) {
|
|
handle(name, value);
|
|
}
|
|
|
|
return {
|
|
...allCommands,
|
|
...commands,
|
|
};
|
|
}, {});
|
|
}
|
|
}
|