From ecf2da847f493ef69c62fceb08a9686b31bc2745 Mon Sep 17 00:00:00 2001 From: makhnatkin Date: Wed, 22 Jan 2025 09:51:42 +0100 Subject: [PATCH 1/5] feat(core): added MarkupManager to manage markdown, integrated with ExtensionsManager, parser and serializer --- src/core/ExtensionsManager.ts | 35 +++++--- src/core/ParserTokensRegistry.ts | 10 ++- src/core/SerializerTokensRegistry.ts | 5 +- src/core/markdown/MarkdownParser.ts | 6 ++ src/core/markdown/MarkdownSerializer.js | 13 ++- src/core/markdown/MarkupManager.test.ts | 5 ++ src/core/markdown/MarkupManager.ts | 112 ++++++++++++++++++++++++ 7 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 src/core/markdown/MarkupManager.test.ts create mode 100644 src/core/markdown/MarkupManager.ts diff --git a/src/core/ExtensionsManager.ts b/src/core/ExtensionsManager.ts index 7ce5de49..27f88967 100644 --- a/src/core/ExtensionsManager.ts +++ b/src/core/ExtensionsManager.ts @@ -6,6 +6,7 @@ import {ExtensionBuilder} from './ExtensionBuilder'; import {ParserTokensRegistry} from './ParserTokensRegistry'; import {SchemaSpecRegistry} from './SchemaSpecRegistry'; import {SerializerTokensRegistry} from './SerializerTokensRegistry'; +import {MarkupManager} from './markdown/MarkupManager'; import {TransformFn} from './markdown/ProseMirrorTransformer'; import type {ActionSpec} from './types/actions'; import type { @@ -122,24 +123,32 @@ export class ExtensionsManager { }; private createDeps() { + const actions = new ActionsManager(); + const markupManager = new MarkupManager(); + const schema = this.#schemaRegistry.createSchema(); + const markupParser = this.#parserRegistry.createParser( + schema, + this.#mdForMarkup, + this.#pmTransformers, + markupManager, + ); + const textParser = this.#parserRegistry.createParser( + schema, + this.#mdForText, + this.#pmTransformers, + markupManager, + ); + const serializer = this.#serializerRegistry.createSerializer(markupManager); + this.#deps = { schema, - actions: new ActionsManager(), - markupParser: this.#parserRegistry.createParser( - schema, - this.#mdForMarkup, - this.#pmTransformers, - ), - textParser: this.#parserRegistry.createParser( - schema, - this.#mdForText, - this.#pmTransformers, - ), - serializer: this.#serializerRegistry.createSerializer(), + actions, + markupParser, + textParser, + serializer, }; } - private createDerived() { this.#plugins = this.#spec.plugins(this.#deps); Object.assign(this.#actions, this.#spec.actions(this.#deps)); diff --git a/src/core/ParserTokensRegistry.ts b/src/core/ParserTokensRegistry.ts index a8256363..f5bbdf32 100644 --- a/src/core/ParserTokensRegistry.ts +++ b/src/core/ParserTokensRegistry.ts @@ -2,6 +2,7 @@ import type MarkdownIt from 'markdown-it'; import type {Schema} from 'prosemirror-model'; import {MarkdownParser} from './markdown/MarkdownParser'; +import {MarkupManager} from './markdown/MarkupManager'; import {TransformFn} from './markdown/ProseMirrorTransformer'; import type {Parser, ParserToken} from './types/parser'; @@ -13,7 +14,12 @@ export class ParserTokensRegistry { return this; } - createParser(schema: Schema, tokenizer: MarkdownIt, pmTransformers: TransformFn[]): Parser { - return new MarkdownParser(schema, tokenizer, this.#tokens, pmTransformers); + createParser( + schema: Schema, + tokenizer: MarkdownIt, + pmTransformers: TransformFn[], + markupManager: MarkupManager, + ): Parser { + return new MarkdownParser(schema, tokenizer, this.#tokens, pmTransformers, markupManager); } } diff --git a/src/core/SerializerTokensRegistry.ts b/src/core/SerializerTokensRegistry.ts index dabd0156..787da688 100644 --- a/src/core/SerializerTokensRegistry.ts +++ b/src/core/SerializerTokensRegistry.ts @@ -1,4 +1,5 @@ import {MarkdownSerializer} from './markdown/MarkdownSerializer'; +import {MarkupManager} from './markdown/MarkupManager'; import type {Serializer, SerializerMarkToken, SerializerNodeToken} from './types/serializer'; export class SerializerTokensRegistry { @@ -15,7 +16,7 @@ export class SerializerTokensRegistry { return this; } - createSerializer(): Serializer { - return new MarkdownSerializer(this.#nodes, this.#marks); + createSerializer(markupManager: MarkupManager): Serializer { + return new MarkdownSerializer(this.#nodes, this.#marks, markupManager); } } diff --git a/src/core/markdown/MarkdownParser.ts b/src/core/markdown/MarkdownParser.ts index d0344a71..2b7cc8e1 100644 --- a/src/core/markdown/MarkdownParser.ts +++ b/src/core/markdown/MarkdownParser.ts @@ -6,6 +6,7 @@ import {Mark, MarkType, Node, NodeType, Schema} from 'prosemirror-model'; import {logger} from '../../logger'; import type {Parser, ParserToken} from '../types/parser'; +import {MarkupManager} from './MarkupManager'; import {ProseMirrorTransformer, TransformFn} from './ProseMirrorTransformer'; type TokenAttrs = {[name: string]: unknown}; @@ -25,18 +26,21 @@ export class MarkdownParser implements Parser { tokens: Record; tokenizer: MarkdownIt; pmTransformers: TransformFn[]; + markupManager: MarkupManager; constructor( schema: Schema, tokenizer: MarkdownIt, tokens: Record, pmTransformers: TransformFn[], + markupManager: MarkupManager, ) { this.schema = schema; this.marks = Mark.none; this.tokens = tokens; this.tokenizer = tokenizer; this.pmTransformers = pmTransformers; + this.markupManager = markupManager; } validateLink(url: string): boolean { @@ -57,6 +61,8 @@ export class MarkdownParser implements Parser { parse(text: string) { const time = Date.now(); + this.markupManager.setMarkup(text); + try { this.stack = [{type: this.schema.topNodeType, content: []}]; diff --git a/src/core/markdown/MarkdownSerializer.js b/src/core/markdown/MarkdownSerializer.js index d9926549..807a3385 100644 --- a/src/core/markdown/MarkdownSerializer.js +++ b/src/core/markdown/MarkdownSerializer.js @@ -3,6 +3,8 @@ // ::- A specification for serializing a ProseMirror document as // Markdown/CommonMark text. // prettier-ignore +import {MarkupManager} from "./MarkupManager"; + export class MarkdownSerializer { // :: (Object<(state: MarkdownSerializerState, node: Node, parent: Node, index: number)>, Object) // Construct a serializer with the given configuration. The `nodes` @@ -37,18 +39,23 @@ export class MarkdownSerializer { // outside the marks. This is necessary for emphasis marks as // CommonMark does not permit enclosing whitespace inside emphasis // marks, see: http://spec.commonmark.org/0.26/#example-330 - constructor(nodes, marks) { + constructor(nodes, marks, markupManager) { // :: Object<(MarkdownSerializerState, Node)> The node serializer // functions for this serializer. this.nodes = nodes; // :: Object The mark serializer info. this.marks = marks; + + this.markupManager = markupManager; } // :: (Node, ?Object) → string // Serialize the content of the given node to // [CommonMark](http://commonmark.org/). serialize(content, options) { + if (!this.markupManager.rawMarkdown) { + console.warn('[MarkdownSerializer] No raw markdown is set'); + } const state = new MarkdownSerializerState(this.nodes, this.marks, options); state.renderContent(content); return state.out; @@ -227,8 +234,8 @@ export class MarkdownSerializerState { } } - const inner = marks.length && marks[marks.length - 1]; const - noEsc = inner && this.marks[inner.type.name].escape === false; + const inner = marks.length && marks[marks.length - 1]; + const noEsc = inner && this.marks[inner.type.name].escape === false; const len = marks.length - (noEsc ? 1 : 0); // Try to reorder 'mixable' marks, such as em and strong, which diff --git a/src/core/markdown/MarkupManager.test.ts b/src/core/markdown/MarkupManager.test.ts new file mode 100644 index 00000000..a923591b --- /dev/null +++ b/src/core/markdown/MarkupManager.test.ts @@ -0,0 +1,5 @@ +import {MarkupManager} from './MarkupManager'; + +describe.skip('MarkupManager', () => { + it('should test ', () => {}); +}); diff --git a/src/core/markdown/MarkupManager.ts b/src/core/markdown/MarkupManager.ts new file mode 100644 index 00000000..7adfa632 --- /dev/null +++ b/src/core/markdown/MarkupManager.ts @@ -0,0 +1,112 @@ +import {EventEmitter} from 'events'; + +import type {Parser} from '../types/parser'; + +export interface IMarkupManager { + setMarkup(rawMarkdown: string): void; + setPos(tokenId: string, pos: [number, number]): void; + setParser(parser: Parser): void; + reset(): void; + on(event: string, listener: (...args: any[]) => void): void; +} + +export interface Logger { + log(message: string): void; + error(message: string): void; +} + +/** + * Validate if a value is a valid position array + */ +function isPosArray(pos: unknown): pos is [number, number] { + return Array.isArray(pos) && pos.length === 2 && pos.every((v) => typeof v === 'number'); +} + +export class MarkupManager extends EventEmitter implements IMarkupManager { + private _rawMarkdown = ''; + private _poses: Map = new Map(); + private _parser: Parser | null = null; + private readonly logger: Logger; + + constructor(logger?: Logger) { + super(); + this.logger = logger ?? console; + } + + /** + * Set raw markdown text and emit an event + */ + setMarkup(rawMarkdown: string): void { + if (typeof rawMarkdown !== 'string') { + this.logger.error('[MarkupManager] rawMarkdown must be a string'); + return; + } + this._rawMarkdown = rawMarkdown; + + this.emit('markupManager.markupChanged', {data: rawMarkdown}); + this.logger.log('[MarkupManager] Raw markdown set successfully'); + } + /** + * Get the stored raw markdown text + */ + get rawMarkdown(): string { + return this._rawMarkdown; + } + + /** + * Set the position of a token + */ + setPos(tokenId: string, pos: [number, number]): void { + if (!isPosArray(pos)) { + this.logger.error( + '[MarkupManager] Invalid position format. Must be [start, end] with numbers', + ); + return; + } + this._poses.set(tokenId, pos); + + this.logger.log(`[MarkupManager] Position for token ${tokenId} set to ${pos}`); + } + + /** + * Get the positions map + */ + get poses(): Map { + return this._poses; + } + + /** + * Set the parser instance + */ + setParser(parser: Parser): void { + this._parser = parser; + + this.logger.log('[MarkupManager] Parser instance set successfully'); + } + + /** + * Get the parser instance + */ + get parser(): Parser { + if (!this._parser) { + const error = new Error('[MarkupManager] Markdown Parser is not set'); + this.logger.error(`[MarkupManager] ${error.message}`); + this.emit('markupManager.error', {error}); + throw error; + } + + return this._parser; + } + + /** + * Reset the stored raw markdown, positions, and parser, and emit an event + */ + reset(): void { + this._rawMarkdown = ''; + this._poses.clear(); + this._parser = null; + + this.emit('markupManager.markupChanged', {data: null}); + this.logger.log('[MarkupManager] MarkupManager has been reset'); + } +} From 2f7dae83b6a22512bf26f2cfbdeddee48d05a074 Mon Sep 17 00:00:00 2001 From: makhnatkin Date: Wed, 22 Jan 2025 10:21:04 +0100 Subject: [PATCH 2/5] feat(core): fixed tests --- src/core/markdown/Markdown.test.ts | 5 +++++ src/core/markdown/MarkdownParser.test.ts | 4 ++++ src/core/markdown/MarkupManager.test.ts | 5 ----- 3 files changed, 9 insertions(+), 5 deletions(-) delete mode 100644 src/core/markdown/MarkupManager.test.ts diff --git a/src/core/markdown/Markdown.test.ts b/src/core/markdown/Markdown.test.ts index 82996bde..05f52469 100644 --- a/src/core/markdown/Markdown.test.ts +++ b/src/core/markdown/Markdown.test.ts @@ -11,9 +11,12 @@ import type {SerializerNodeToken} from '../types/serializer'; import {MarkdownParser} from './MarkdownParser'; import {MarkdownSerializer} from './MarkdownSerializer'; +import {MarkupManager} from './MarkupManager'; const {schema} = builder; schema.nodes['hard_break'].spec.isBreak = true; + +const markupManager = new MarkupManager(); const parser: Parser = new MarkdownParser( schema, new MarkdownIt('commonmark'), @@ -35,6 +38,7 @@ const parser: Parser = new MarkdownParser( code_inline: {type: 'mark', name: 'code', noCloseToken: true}, }, [], + markupManager, ); const serializer = new MarkdownSerializer( { @@ -85,6 +89,7 @@ const serializer = new MarkdownSerializer( strong: {open: '**', close: '**', mixable: true, expelEnclosingWhitespace: true}, code: {open: '`', close: '`', escape: false, expelEnclosingWhitespace: true}, }, + markupManager, ); const {doc, p, h1, h2, li, ul, ol, br, pre} = builder; diff --git a/src/core/markdown/MarkdownParser.test.ts b/src/core/markdown/MarkdownParser.test.ts index 0085275d..d93dbb33 100644 --- a/src/core/markdown/MarkdownParser.test.ts +++ b/src/core/markdown/MarkdownParser.test.ts @@ -5,8 +5,11 @@ import {doc, br as hardBreak, p, schema} from 'prosemirror-test-builder'; import type {Parser} from '../types/parser'; import {MarkdownParser} from './MarkdownParser'; +import {MarkupManager} from './MarkupManager'; const md = MarkdownIt('commonmark', {html: false, breaks: true}); +const markupManager = new MarkupManager(); + const testParser: Parser = new MarkdownParser( schema, md, @@ -16,6 +19,7 @@ const testParser: Parser = new MarkdownParser( softbreak: {type: 'node', name: 'hard_break'}, }, [], + markupManager, ); function parseWith(parser: Parser) { diff --git a/src/core/markdown/MarkupManager.test.ts b/src/core/markdown/MarkupManager.test.ts deleted file mode 100644 index a923591b..00000000 --- a/src/core/markdown/MarkupManager.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {MarkupManager} from './MarkupManager'; - -describe.skip('MarkupManager', () => { - it('should test ', () => {}); -}); From 28e2e9d0e678ac50cc24978e36672aac357fbdc0 Mon Sep 17 00:00:00 2001 From: makhnatkin Date: Wed, 22 Jan 2025 10:25:48 +0100 Subject: [PATCH 3/5] feat(core): fixed tests --- src/core/SerializerTokensRegistry.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/SerializerTokensRegistry.test.ts b/src/core/SerializerTokensRegistry.test.ts index 82b36b3c..71bebdba 100644 --- a/src/core/SerializerTokensRegistry.test.ts +++ b/src/core/SerializerTokensRegistry.test.ts @@ -1,28 +1,32 @@ import {SerializerTokensRegistry} from './SerializerTokensRegistry'; +import {MarkupManager} from './markdown/MarkupManager'; import type {SerializerTests} from './types/serializer'; describe('SerializerTokensRegistry', () => { it('should create empty serializer', () => { - const serializer = new SerializerTokensRegistry().createSerializer(); + const markupManager = new MarkupManager(); + const serializer = new SerializerTokensRegistry().createSerializer(markupManager); expect(typeof serializer.serialize).toBe('function'); }); it('should add nodes', () => { const nodeName = 'example_node'; + const markupManager = new MarkupManager(); const serializer = new SerializerTokensRegistry() .addNode(nodeName, () => {}) - .createSerializer() as SerializerTests; + .createSerializer(markupManager) as SerializerTests; expect(serializer.containsNode(nodeName)).toBe(true); }); it('should add marks', () => { const markName = 'example_mark'; + const markupManager = new MarkupManager(); const serializer = new SerializerTokensRegistry() .addMark(markName, {open: '1', close: '2'}) - .createSerializer() as SerializerTests; + .createSerializer(markupManager) as SerializerTests; expect(serializer.containsMark(markName)).toBe(true); }); From 2a105fd686c9d73064a3602e90b3c586bd75382f Mon Sep 17 00:00:00 2001 From: makhnatkin Date: Fri, 24 Jan 2025 12:08:48 +0100 Subject: [PATCH 4/5] feat(core): added entity ID saving in attrs for raw markup linking --- src/core/ExtensionsManager.ts | 22 +++++++++-- src/core/markdown/MarkdownParser.ts | 40 ++++++++++++++++++-- src/core/markdown/MarkupManager.ts | 57 +++++++++++++++++++++++++++-- 3 files changed, 109 insertions(+), 10 deletions(-) diff --git a/src/core/ExtensionsManager.ts b/src/core/ExtensionsManager.ts index 27f88967..f4850c39 100644 --- a/src/core/ExtensionsManager.ts +++ b/src/core/ExtensionsManager.ts @@ -29,6 +29,10 @@ type ExtensionsManagerOptions = { pmTransformers?: TransformFn[]; }; +// TODO: move to props +const RAW_MARKUP_TRACKED_TOKENS = ['yfm_table_open']; +const TRACKED_NODES_TYPES = ['yfm_table']; + export class ExtensionsManager { static process(extensions: Extension, options: ExtensionsManagerOptions) { return new this({extensions, options}).build(); @@ -54,6 +58,7 @@ export class ExtensionsManager { #actions: Record = {}; #nodeViews: Record = {}; #markViews: Record = {}; + #markupManager: MarkupManager = new MarkupManager(); constructor({extensions, options = {}}: ExtensionsManagerParams) { this.#extensions = extensions; @@ -73,6 +78,9 @@ export class ExtensionsManager { // TODO: add prefilled context this.#builder = new ExtensionBuilder(); + + this.#markupManager.setTrackedTokensTypes(RAW_MARKUP_TRACKED_TOKENS); + this.#markupManager.setTrackedNodesTypes(TRACKED_NODES_TYPES); } build() { @@ -106,6 +114,13 @@ export class ExtensionsManager { private processNode = (name: string, {spec, fromMd, toMd: toMd, view}: ExtensionNodeSpec) => { this.#schemaRegistry.addNode(name, spec); + + // Inject nodeId attr for tracked nodes types + if (this.#markupManager.isTrackedNodeType(name)) { + spec.attrs = spec.attrs || {}; + spec.attrs.nodeId = {default: null}; + } + this.#parserRegistry.addToken(fromMd.tokenName || name, fromMd.tokenSpec); this.#serializerRegistry.addNode(name, toMd); if (view) { @@ -124,22 +139,21 @@ export class ExtensionsManager { private createDeps() { const actions = new ActionsManager(); - const markupManager = new MarkupManager(); const schema = this.#schemaRegistry.createSchema(); const markupParser = this.#parserRegistry.createParser( schema, this.#mdForMarkup, this.#pmTransformers, - markupManager, + this.#markupManager, ); const textParser = this.#parserRegistry.createParser( schema, this.#mdForText, this.#pmTransformers, - markupManager, + this.#markupManager, ); - const serializer = this.#serializerRegistry.createSerializer(markupManager); + const serializer = this.#serializerRegistry.createSerializer(this.#markupManager); this.#deps = { schema, diff --git a/src/core/markdown/MarkdownParser.ts b/src/core/markdown/MarkdownParser.ts index 2b7cc8e1..08fde0d0 100644 --- a/src/core/markdown/MarkdownParser.ts +++ b/src/core/markdown/MarkdownParser.ts @@ -19,6 +19,18 @@ enum TokenType { default = 'default', } +/** + * Generate a unique token ID + */ +export function createUniqueId(prefix: string): string { + const randomLetters = Array.from( + {length: 5}, + () => String.fromCharCode(97 + Math.floor(Math.random() * 26)), // a-z + ).join(''); + + return `${prefix}-${randomLetters}${Date.now()}`; +} + export class MarkdownParser implements Parser { schema: Schema; stack: Array<{type: NodeType; attrs?: TokenAttrs; content: Array}> = []; @@ -69,6 +81,13 @@ export class MarkdownParser implements Parser { let mdItTokens; try { mdItTokens = this.tokenizer.parse(text, {}); + mdItTokens.forEach((token) => { + if (this.markupManager.isTrackedTokenType(token.type) && token.map) { + const tokenId = createUniqueId(token.type); + token.attrSet('tokenId', tokenId); + this.markupManager.setPos(tokenId, token.map); + } + }); } catch (err) { const e = err as Error; e.message = 'Unable to parse your markup. Please check for errors. ' + e.message; @@ -108,10 +127,25 @@ export class MarkdownParser implements Parser { tokenStream: Token[], i: number, ): TokenAttrs | undefined { - if (tokenSpec.getAttrs) return tokenSpec.getAttrs(token, tokenStream, i); - else if (tokenSpec.attrs instanceof Function) return tokenSpec.attrs(token); + let attrs: TokenAttrs | undefined = {}; + + if (tokenSpec.getAttrs) { + attrs = tokenSpec.getAttrs(token, tokenStream, i); + } else if (tokenSpec.attrs instanceof Function) { + attrs = tokenSpec.attrs(token); + } else { + attrs = tokenSpec.attrs; + } + + // Inject nodeId attr if the markdown token has tokenId + const tokdenId = token.attrGet('tokenId'); + if (tokdenId) { + attrs = attrs ?? {}; + attrs.nodeId = tokdenId; + } - return tokenSpec.attrs; + // TODO: @makhnatkin add cache + return attrs; } private getTokenSpec(token: Token) { diff --git a/src/core/markdown/MarkupManager.ts b/src/core/markdown/MarkupManager.ts index 7adfa632..372c0974 100644 --- a/src/core/markdown/MarkupManager.ts +++ b/src/core/markdown/MarkupManager.ts @@ -26,6 +26,10 @@ export class MarkupManager extends EventEmitter implements IMarkupManager { private _rawMarkdown = ''; private _poses: Map = new Map(); private _parser: Parser | null = null; + // TODO: move to _trackedTokens: {types: Set, nodes: Set} + private _trackedTokensTypes: Set = new Set(); + private _trackedNodesTypes: Set = new Set(); + private readonly logger: Logger; constructor(logger?: Logger) { @@ -33,6 +37,36 @@ export class MarkupManager extends EventEmitter implements IMarkupManager { this.logger = logger ?? console; } + /** + * Set the list of tokens types to track + */ + setTrackedTokensTypes(tokensTypes: string[]): void { + this._trackedTokensTypes = new Set(tokensTypes); + this.logger.log(`[MarkupManager] Tracked tokens types set to: ${tokensTypes.join(', ')}`); + } + + /** + * Set the list of tokens types to track + */ + setTrackedNodesTypes(nodesTypes: string[]): void { + this._trackedNodesTypes = new Set(nodesTypes); + this.logger.log(`[MarkupManager] Tracked nodes types set to: ${nodesTypes.join(', ')}`); + } + + /** + * Check if a token type is being tracked + */ + isTrackedTokenType(tokenType: string): boolean { + return this._trackedTokensTypes.has(tokenType); + } + + /** + * Check if a token type is being tracked + */ + isTrackedNodeType(nodeType: string): boolean { + return this._trackedNodesTypes.has(nodeType); + } + /** * Set raw markdown text and emit an event */ @@ -46,6 +80,23 @@ export class MarkupManager extends EventEmitter implements IMarkupManager { this.emit('markupManager.markupChanged', {data: rawMarkdown}); this.logger.log('[MarkupManager] Raw markdown set successfully'); } + + /** + * Get raw markdown for a specific token by its unique ID + */ + getMarkupByTokenId(tokenId: string): string | null { + const pos = this._poses.get(tokenId); + + if (!pos) { + this.logger.error(`[MarkupManager] No position found for ID: ${tokenId}`); + return null; + } + + const [start, end] = pos; + + return this._rawMarkdown.slice(start, end); + } + /** * Get the stored raw markdown text */ @@ -56,16 +107,16 @@ export class MarkupManager extends EventEmitter implements IMarkupManager { /** * Set the position of a token */ - setPos(tokenId: string, pos: [number, number]): void { + setPos(id: string, pos: [number, number] | null): void { if (!isPosArray(pos)) { this.logger.error( '[MarkupManager] Invalid position format. Must be [start, end] with numbers', ); return; } - this._poses.set(tokenId, pos); + this._poses.set(id, pos); - this.logger.log(`[MarkupManager] Position for token ${tokenId} set to ${pos}`); + this.logger.log(`[MarkupManager] Position for ID ${id} set to ${pos}`); } /** From 7a4fd87fccc3a56d27dd277d36433a79d663af78 Mon Sep 17 00:00:00 2001 From: makhnatkin Date: Fri, 24 Jan 2025 17:39:19 +0100 Subject: [PATCH 5/5] feat(core): added configuration options for MarkupManager --- src/core/Editor.ts | 14 ++++++++++++++ src/core/ExtensionsManager.ts | 17 ++++++++--------- src/core/markdown/MarkdownParser.ts | 16 +++++++++------- src/core/markdown/MarkupManager.ts | 21 +++++++++++++++++++-- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/core/Editor.ts b/src/core/Editor.ts index a7f44344..524ba2eb 100644 --- a/src/core/Editor.ts +++ b/src/core/Editor.ts @@ -7,6 +7,7 @@ import type {CommonEditor, ContentHandler, MarkupString} from '../common'; import type {ActionsManager} from './ActionsManager'; import {WysiwygContentHandler} from './ContentHandler'; import {ExtensionsManager} from './ExtensionsManager'; +import {MarkupManagerOptions} from './markdown/MarkupManager'; import {TransformFn} from './markdown/ProseMirrorTransformer'; import type {ActionStorage} from './types/actions'; import type {Extension} from './types/extension'; @@ -38,6 +39,17 @@ export type WysiwygEditorOptions = { onChange?: OnChange; /** Call only if document change */ onDocChange?: OnChange; + /** + * Configures MarkupManager to control storing and processing + * raw markup, including tracked tokens, nodes, and attributes. + */ + markupManagerOpts?: MarkupManagerOptions; +}; + +const defaultMarkupManagerOpts: MarkupManagerOptions = { + allowDynamicAttributesForTrackedEntities: true, + trackedTokensTypes: ['yfm_table_open'], + trackedNodesTypes: ['yfm_table'], }; export class WysiwygEditor implements CommonEditor, ActionStorage { @@ -77,6 +89,7 @@ export class WysiwygEditor implements CommonEditor, ActionStorage { mdPreset, linkify, pmTransformers, + markupManagerOpts = defaultMarkupManagerOpts, linkifyTlds, escapeConfig, onChange, @@ -96,6 +109,7 @@ export class WysiwygEditor implements CommonEditor, ActionStorage { mdOpts: {html: allowHTML, linkify, breaks: true, preset: mdPreset}, linkifyTlds, pmTransformers, + markupManagerOpts, }); const state = EditorState.create({ diff --git a/src/core/ExtensionsManager.ts b/src/core/ExtensionsManager.ts index f4850c39..933778ca 100644 --- a/src/core/ExtensionsManager.ts +++ b/src/core/ExtensionsManager.ts @@ -6,7 +6,7 @@ import {ExtensionBuilder} from './ExtensionBuilder'; import {ParserTokensRegistry} from './ParserTokensRegistry'; import {SchemaSpecRegistry} from './SchemaSpecRegistry'; import {SerializerTokensRegistry} from './SerializerTokensRegistry'; -import {MarkupManager} from './markdown/MarkupManager'; +import {MarkupManager, MarkupManagerOptions} from './markdown/MarkupManager'; import {TransformFn} from './markdown/ProseMirrorTransformer'; import type {ActionSpec} from './types/actions'; import type { @@ -27,12 +27,9 @@ type ExtensionsManagerOptions = { mdOpts?: MarkdownIt.Options & {preset?: PresetName}; linkifyTlds?: string | string[]; pmTransformers?: TransformFn[]; + markupManagerOpts?: MarkupManagerOptions; }; -// TODO: move to props -const RAW_MARKUP_TRACKED_TOKENS = ['yfm_table_open']; -const TRACKED_NODES_TYPES = ['yfm_table']; - export class ExtensionsManager { static process(extensions: Extension, options: ExtensionsManagerOptions) { return new this({extensions, options}).build(); @@ -58,7 +55,7 @@ export class ExtensionsManager { #actions: Record = {}; #nodeViews: Record = {}; #markViews: Record = {}; - #markupManager: MarkupManager = new MarkupManager(); + #markupManager: MarkupManager; constructor({extensions, options = {}}: ExtensionsManagerParams) { this.#extensions = extensions; @@ -79,8 +76,7 @@ export class ExtensionsManager { // TODO: add prefilled context this.#builder = new ExtensionBuilder(); - this.#markupManager.setTrackedTokensTypes(RAW_MARKUP_TRACKED_TOKENS); - this.#markupManager.setTrackedNodesTypes(TRACKED_NODES_TYPES); + this.#markupManager = new MarkupManager(options.markupManagerOpts); } build() { @@ -116,7 +112,10 @@ export class ExtensionsManager { this.#schemaRegistry.addNode(name, spec); // Inject nodeId attr for tracked nodes types - if (this.#markupManager.isTrackedNodeType(name)) { + if ( + this.#markupManager.isAllowDynamicAttributesForTrackedEntities() && + this.#markupManager.isTrackedNodeType(name) + ) { spec.attrs = spec.attrs || {}; spec.attrs.nodeId = {default: null}; } diff --git a/src/core/markdown/MarkdownParser.ts b/src/core/markdown/MarkdownParser.ts index 08fde0d0..d0999f0a 100644 --- a/src/core/markdown/MarkdownParser.ts +++ b/src/core/markdown/MarkdownParser.ts @@ -81,13 +81,15 @@ export class MarkdownParser implements Parser { let mdItTokens; try { mdItTokens = this.tokenizer.parse(text, {}); - mdItTokens.forEach((token) => { - if (this.markupManager.isTrackedTokenType(token.type) && token.map) { - const tokenId = createUniqueId(token.type); - token.attrSet('tokenId', tokenId); - this.markupManager.setPos(tokenId, token.map); - } - }); + if (this.markupManager.isAllowDynamicAttributesForTrackedEntities()) { + mdItTokens.forEach((token) => { + if (this.markupManager.isTrackedTokenType(token.type) && token.map) { + const tokenId = createUniqueId(token.type); + token.attrSet('tokenId', tokenId); + this.markupManager.setPos(tokenId, token.map); + } + }); + } } catch (err) { const e = err as Error; e.message = 'Unable to parse your markup. Please check for errors. ' + e.message; diff --git a/src/core/markdown/MarkupManager.ts b/src/core/markdown/MarkupManager.ts index 372c0974..eacb53ca 100644 --- a/src/core/markdown/MarkupManager.ts +++ b/src/core/markdown/MarkupManager.ts @@ -22,18 +22,28 @@ function isPosArray(pos: unknown): pos is [number, number] { return Array.isArray(pos) && pos.length === 2 && pos.every((v) => typeof v === 'number'); } +export interface MarkupManagerOptions { + trackedTokensTypes?: string[]; + trackedNodesTypes?: string[]; + allowDynamicAttributesForTrackedEntities?: boolean; +} + export class MarkupManager extends EventEmitter implements IMarkupManager { private _rawMarkdown = ''; private _poses: Map = new Map(); private _parser: Parser | null = null; - // TODO: move to _trackedTokens: {types: Set, nodes: Set} private _trackedTokensTypes: Set = new Set(); private _trackedNodesTypes: Set = new Set(); + private _allowDynamicAttributesForTrackedEntities = false; private readonly logger: Logger; - constructor(logger?: Logger) { + constructor(options: MarkupManagerOptions = {}, logger?: Logger) { super(); + this._trackedTokensTypes = new Set(options.trackedTokensTypes ?? []); + this._trackedNodesTypes = new Set(options.trackedNodesTypes ?? []); + this._allowDynamicAttributesForTrackedEntities = + options.allowDynamicAttributesForTrackedEntities ?? true; this.logger = logger ?? console; } @@ -67,6 +77,13 @@ export class MarkupManager extends EventEmitter implements IMarkupManager { return this._trackedNodesTypes.has(nodeType); } + /** + * Check if a token type is being tracked + */ + isAllowDynamicAttributesForTrackedEntities(): boolean { + return this._allowDynamicAttributesForTrackedEntities; + } + /** * Set raw markdown text and emit an event */