Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): added MarkupManager to manage markdown, integrated with ExtensionsManager, parser and serializer #558

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions src/core/ExtensionsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -28,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();
Expand All @@ -53,6 +58,7 @@ export class ExtensionsManager {
#actions: Record<string, ActionSpec> = {};
#nodeViews: Record<string, NodeViewConstructor> = {};
#markViews: Record<string, MarkViewConstructor> = {};
#markupManager: MarkupManager = new MarkupManager();

constructor({extensions, options = {}}: ExtensionsManagerParams) {
this.#extensions = extensions;
Expand All @@ -72,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() {
Expand Down Expand Up @@ -105,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) {
Expand All @@ -122,24 +138,31 @@ export class ExtensionsManager {
};

private createDeps() {
const actions = new ActionsManager();

const schema = this.#schemaRegistry.createSchema();
const markupParser = this.#parserRegistry.createParser(
schema,
this.#mdForMarkup,
this.#pmTransformers,
this.#markupManager,
);
const textParser = this.#parserRegistry.createParser(
schema,
this.#mdForText,
this.#pmTransformers,
this.#markupManager,
);
const serializer = this.#serializerRegistry.createSerializer(this.#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));
Expand Down
10 changes: 8 additions & 2 deletions src/core/ParserTokensRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
}
}
10 changes: 7 additions & 3 deletions src/core/SerializerTokensRegistry.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Expand Down
5 changes: 3 additions & 2 deletions src/core/SerializerTokensRegistry.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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);
}
}
5 changes: 5 additions & 0 deletions src/core/markdown/Markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand All @@ -35,6 +38,7 @@ const parser: Parser = new MarkdownParser(
code_inline: {type: 'mark', name: 'code', noCloseToken: true},
},
[],
markupManager,
);
const serializer = new MarkdownSerializer(
{
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/core/markdown/MarkdownParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -16,6 +19,7 @@ const testParser: Parser = new MarkdownParser(
softbreak: {type: 'node', name: 'hard_break'},
},
[],
markupManager,
);

function parseWith(parser: Parser) {
Expand Down
46 changes: 43 additions & 3 deletions src/core/markdown/MarkdownParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -18,25 +19,40 @@ 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<Node>}> = [];
marks: readonly Mark[];
tokens: Record<string, ParserToken>;
tokenizer: MarkdownIt;
pmTransformers: TransformFn[];
markupManager: MarkupManager;

constructor(
schema: Schema,
tokenizer: MarkdownIt,
tokens: Record<string, ParserToken>,
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 {
Expand All @@ -57,12 +73,21 @@ export class MarkdownParser implements Parser {

parse(text: string) {
const time = Date.now();
this.markupManager.setMarkup(text);

try {
this.stack = [{type: this.schema.topNodeType, content: []}];

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;
Expand Down Expand Up @@ -102,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) {
Expand Down
13 changes: 10 additions & 3 deletions src/core/markdown/MarkdownSerializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading