diff --git a/packages/apps/client/src/ScriptEditor/Editor.tsx b/packages/apps/client/src/ScriptEditor/Editor.tsx index bfd4239..e2e412b 100644 --- a/packages/apps/client/src/ScriptEditor/Editor.tsx +++ b/packages/apps/client/src/ScriptEditor/Editor.tsx @@ -13,6 +13,13 @@ import { formatingKeymap } from './keymaps' import { deselectAll } from './commands/deselectAll' import { fromMarkdown } from '../lib/prosemirrorDoc' +const LOREM_IPSUM = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent sollicitudin ipsum at lacinia sodales. *Sed* in **pharetra _mauris_**, id facilisis nibh. Curabitur eget erat bibendum, aliquam ligula ac, interdum orci. Curabitur non mollis nibh. Pellentesque ultrices suscipit diam ac fermentum. Morbi id velit consectetur, auctor ligula scelerisque, vulputate ante. Nunc mattis consectetur eleifend. Aenean vestibulum porta mollis. Cras ultrices facilisis turpis, et vulputate felis tempor at. Aliquam ultricies commodo odio at vehicula. Curabitur lobortis lectus at lacus commodo tincidunt. Donec vulputate urna efficitur, vehicula urna vel, porttitor urna.\n' + + 'Duis congue molestie neque, et sollicitudin lacus porta eget. Etiam massa dui, cursus vitae lacus sit **amet**, aliquet bibendum elit. Morbi tincidunt quis metus ut luctus. Proin tincidunt suscipit vestibulum. In eu cursus quam. Praesent lacus mauris, euismod nec lacus in, tincidunt ultrices justo. Sed ac rhoncus quam. Praesent libero elit, convallis ut urna nec, interdum elementum diam. Pellentesque aliquet, mi vitae faucibus euismod, mauris lorem auctor felis, tincidunt bibendum erat nisl in nisi.\n' + + 'Donec ac rhoncus ex. Pellentesque eleifend ante id maximus *mollis*. Duis in mauris vel ligula venenatis gravida.\n\n\\*Mauris blandit arcu a lorem cursus ornare. Vestibulum at ligula vel nisi eleifend pretium. Vivamus et nunc scelerisque, suscipit dolor nec, ornare elit. Nam ut tristique est. Suspendisse sollicitudin tortor quam, eget cursus quam porttitor nec. Fusce convallis libero massa, a consequat tortor accumsan id. Pellentesque at diam sit amet tortor suscipit bibendum sed et elit. Etiam ac tellus tellus. Cras pulvinar sem et augue consequat mattis. \n' + + 'Duis mollis ut enim vitae lobortis. ~Nulla mi libero~, blandit sit amet congue eu, vehicula vel sem. Donec maximus lacus \\~ac nisi blandit sodales. Fusce sed lectus iaculis, tempus quam lacinia, gravida velit. In imperdiet, sem sit amet commodo eleifend, turpis tellus lobortis metus, et rutrum mi sapien vel nisl. Pellentesque at est non tortor efficitur tincidunt vitae in ex. In gravida pulvinar ligula eget pellentesque. Nullam viverra orci velit, at dictum diam imperdiet sit amet. Morbi consequat est vitae mi consequat fringilla. Phasellus pharetra turpis nulla, at molestie nunc hendrerit ut. \n' + + 'Aenean ut nulla ut diam imperdiet laoreet sed sed enim. **Vivamus bibendum** tempus metus ac consectetur. Aliquam ut nisl sed mauris sodales dignissim. Integer consectetur sapien quam, sit amet blandit quam cursus ac. Quisque vel convallis erat. Aliquam ac interdum nisi. Praesent id sapien vitae sem venenatis sollicitudin. ' + export function Editor({ initialValue, className, @@ -56,7 +63,7 @@ export function Editor({ [ schema.node(schema.nodes.lineTitle, undefined, [schema.text('Line title')]), ...fromMarkdown( - 'Raz _dwa **trzy**_. ~Cztery.~\n\nPięć _sześć_ siedem.\nRaz\n\n\n\n\n Some more ~Markdown **Here**~' + 'Raz _dwa **trzy**_. ~Cztery.~\n\nPięć _sześć_ siedem.\nRaz\n\n\n\nSome more ~Markdown **Here**~' ), ] ), @@ -65,10 +72,7 @@ export function Editor({ { lineId: randomId(), }, - [ - schema.node(schema.nodes.lineTitle, undefined, schema.text('Line title')), - schema.node(schema.nodes.paragraph, undefined, schema.text('Script...')), - ] + [schema.node(schema.nodes.lineTitle, undefined, schema.text('Line title')), ...fromMarkdown(LOREM_IPSUM)] ), schema.node( schema.nodes.line, diff --git a/packages/apps/client/src/lib/markdownishParser.ts b/packages/apps/client/src/lib/markdownishParser.ts deleted file mode 100644 index 876377e..0000000 --- a/packages/apps/client/src/lib/markdownishParser.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { assertType } from 'packages/shared/lib/dist' - -interface NodeBase { - type: string -} - -interface ParentNodeBase extends NodeBase { - children: Node[] -} - -interface RootNode extends ParentNodeBase { - type: 'root' -} - -interface ParagraphNode extends ParentNodeBase { - type: 'paragraph' -} - -interface TextNode extends NodeBase { - type: 'text' - value: string -} - -interface StrongNode extends ParentNodeBase { - type: 'strong' - code: string -} - -interface EmphasisNode extends ParentNodeBase { - type: 'emphasis' - code: string -} - -interface ReverseNode extends ParentNodeBase { - type: 'reverse' - code: string -} - -export type Node = RootNode | ParagraphNode | TextNode | StrongNode | EmphasisNode | ReverseNode - -export function astFromMarkdownish(text: string): RootNode { - const document: RootNode = { - type: 'root', - children: [], - } - const stack: ParentNodeBase[] = [] - let cursor: ParentNodeBase | null = null - let buffer = '' - - let i = 0 - - function flushBuffer() { - if (buffer === '') return - if (cursor === null) throw new Error('cursor === null assertion') - - cursor.children.push({ - type: 'text', - value: buffer, - }) - buffer = '' - } - - function paragraphStart() { - const newParagraph: ParagraphNode = { - type: 'paragraph', - children: [], - } - document.children.push(newParagraph) - cursor = newParagraph - stack.length = 0 - stack.push(cursor) - } - - function paragraphEnd() { - if (buffer === '') return - if (cursor === null) throw new Error('cursor === null assertion') - - flushBuffer() - cursor = null - } - - function peek() { - return text[i + 1] - } - - function emphasisOrStrong(char: string) { - if (cursor === null) throw new Error('cursor === null assertion') - if ((cursor.type === 'emphasis' || cursor.type === 'strong') && 'code' in cursor) { - if (cursor.code === char) { - if (peek() === char) { - i++ - } - - flushBuffer() - stack.pop() - cursor = stack[stack.length - 1] - return - } - } - - flushBuffer() - - let type: 'emphasis' | 'strong' = 'emphasis' - - if (peek() === char) { - type = 'strong' - i++ - } - - const emphasisOrStrong: EmphasisNode | StrongNode = { - type, - children: [], - code: char, - } - cursor.children.push(emphasisOrStrong) - stack.push(emphasisOrStrong) - cursor = emphasisOrStrong - } - - if (text.length > 0) { - paragraphStart() - } - - for (i = 0; i < text.length; i++) { - const char = text[i] - switch (char) { - case '\n': - paragraphEnd() - continue - case '*': - case '_': - emphasisOrStrong(char) - continue - } - if (cursor === null) paragraphStart() - buffer += char - } - - paragraphEnd() - - console.log(document) - - return document -} diff --git a/packages/apps/client/src/lib/mdParser/astNodes.ts b/packages/apps/client/src/lib/mdParser/astNodes.ts new file mode 100644 index 0000000..f7d7b9d --- /dev/null +++ b/packages/apps/client/src/lib/mdParser/astNodes.ts @@ -0,0 +1,37 @@ +interface NodeBase { + type: string +} + +export interface ParentNodeBase extends NodeBase { + children: Node[] +} + +export interface RootNode extends ParentNodeBase { + type: 'root' +} + +export interface ParagraphNode extends ParentNodeBase { + type: 'paragraph' +} + +export interface TextNode extends NodeBase { + type: 'text' + value: string +} + +export interface StrongNode extends ParentNodeBase { + type: 'strong' + code: string +} + +export interface EmphasisNode extends ParentNodeBase { + type: 'emphasis' + code: string +} + +export interface ReverseNode extends ParentNodeBase { + type: 'reverse' + code: string +} + +export type Node = RootNode | ParagraphNode | TextNode | StrongNode | EmphasisNode | ReverseNode diff --git a/packages/apps/client/src/lib/mdParser/constructs/emphasisAndStrong.ts b/packages/apps/client/src/lib/mdParser/constructs/emphasisAndStrong.ts new file mode 100644 index 0000000..7e7957a --- /dev/null +++ b/packages/apps/client/src/lib/mdParser/constructs/emphasisAndStrong.ts @@ -0,0 +1,45 @@ +import { NodeConstruct, ParserState } from '..' +import { EmphasisNode, StrongNode } from '../astNodes' + +export function emphasisAndStrong(): NodeConstruct { + function emphasisOrStrong(char: string, state: ParserState) { + if (state.nodeCursor === null) throw new Error('cursor === null assertion') + if ((state.nodeCursor.type === 'emphasis' || state.nodeCursor.type === 'strong') && 'code' in state.nodeCursor) { + if (state.nodeCursor.code === char) { + if (state.peek() === char) { + state.consume() + } + + state.flushBuffer() + state.popNode() + return false + } + } + + state.flushBuffer() + + let type: 'emphasis' | 'strong' = 'emphasis' + + if (state.peek() === char) { + type = 'strong' + state.consume() + } + + const emphasisOrStrongNode: EmphasisNode | StrongNode = { + type, + children: [], + code: char, + } + state.pushNode(emphasisOrStrongNode) + + return false + } + + return { + name: 'emphasisOrStrong', + char: { + '*': emphasisOrStrong, + _: emphasisOrStrong, + }, + } +} diff --git a/packages/apps/client/src/lib/mdParser/constructs/escape.ts b/packages/apps/client/src/lib/mdParser/constructs/escape.ts new file mode 100644 index 0000000..2b1e6bf --- /dev/null +++ b/packages/apps/client/src/lib/mdParser/constructs/escape.ts @@ -0,0 +1,21 @@ +import { NodeConstruct, ParserState } from '..' + +export function escape(): NodeConstruct { + function escapeChar(_: string, state: ParserState) { + state.dataStore['inEscape'] = true + } + + function passthroughChar(_: string, state: ParserState) { + if (state.dataStore['inEscape'] !== true) return + state.dataStore['inEscape'] = false + return true + } + + return { + name: 'escape', + char: { + '\\': escapeChar, + any: passthroughChar, + }, + } +} diff --git a/packages/apps/client/src/lib/mdParser/constructs/paragraph.ts b/packages/apps/client/src/lib/mdParser/constructs/paragraph.ts new file mode 100644 index 0000000..94e7743 --- /dev/null +++ b/packages/apps/client/src/lib/mdParser/constructs/paragraph.ts @@ -0,0 +1,33 @@ +import { NodeConstruct, ParserState } from '..' +import { ParagraphNode } from '../astNodes' + +export function paragraph(): NodeConstruct { + function paragraphStart(_: string, state: ParserState) { + if (state.nodeCursor !== null) return + const newParagraph: ParagraphNode = { + type: 'paragraph', + children: [], + } + state.replaceStack(newParagraph) + } + + function paragraphEnd(char: string, state: ParserState) { + if (state.nodeCursor === null) { + paragraphStart(char, state) + } + + state.flushBuffer() + state.nodeCursor = null + + return false + } + + return { + name: 'paragraph', + char: { + end: paragraphEnd, + '\n': paragraphEnd, + any: paragraphStart, + }, + } +} diff --git a/packages/apps/client/src/lib/mdParser/constructs/reverse.ts b/packages/apps/client/src/lib/mdParser/constructs/reverse.ts new file mode 100644 index 0000000..641e0b7 --- /dev/null +++ b/packages/apps/client/src/lib/mdParser/constructs/reverse.ts @@ -0,0 +1,35 @@ +import { NodeConstruct, ParserState } from '..' +import { ReverseNode } from '../astNodes' + +export function reverse(): NodeConstruct { + function reverse(char: string, state: ParserState) { + if (state.nodeCursor === null) throw new Error('cursor === null assertion') + if (state.nodeCursor.type === 'reverse' && 'code' in state.nodeCursor) { + if (state.nodeCursor.code === char) { + state.flushBuffer() + state.popNode() + return false + } + } + + state.flushBuffer() + + const type = 'reverse' + + const reverseNode: ReverseNode = { + type, + children: [], + code: char, + } + state.pushNode(reverseNode) + + return false + } + + return { + name: 'reverse', + char: { + '~': reverse, + }, + } +} diff --git a/packages/apps/client/src/lib/mdParser/index.ts b/packages/apps/client/src/lib/mdParser/index.ts new file mode 100644 index 0000000..69bc062 --- /dev/null +++ b/packages/apps/client/src/lib/mdParser/index.ts @@ -0,0 +1,127 @@ +import { ParentNodeBase, RootNode, Node } from './astNodes' +import { emphasisAndStrong } from './constructs/emphasisAndStrong' +import { escape } from './constructs/escape' +import { paragraph } from './constructs/paragraph' +import { reverse } from './constructs/reverse' + +export interface ParserState { + readonly nodeStack: ParentNodeBase[] + nodeCursor: ParentNodeBase | null + buffer: string + charCursor: number + readonly dataStore: Record + flushBuffer: () => void + pushNode: (node: ParentNodeBase) => void + popNode: () => void + replaceStack: (node: ParentNodeBase) => void + peek: () => string | undefined + consume: () => void +} + +export class ParserStateImpl implements ParserState { + readonly nodeStack: ParentNodeBase[] = [] + nodeCursor: ParentNodeBase | null = null + buffer: string = '' + charCursor: number = 0 + readonly dataStore: Record = {} + + constructor(private document: RootNode, private text: string) {} + + flushBuffer = () => { + if (this.buffer === '') return + if (this.nodeCursor === null) throw new Error('No node available to flush buffer.') + + this.nodeCursor.children.push({ + type: 'text', + value: this.buffer, + }) + this.buffer = '' + } + pushNode = (node: ParentNodeBase) => { + if (this.nodeCursor === null) { + this.nodeCursor = node + } else { + this.nodeCursor.children.push(node as Node) + } + this.nodeStack.push(node) + this.nodeCursor = node + } + popNode = () => { + this.nodeStack.pop() + this.nodeCursor = this.nodeStack[this.nodeStack.length - 1] + } + replaceStack = (node: ParentNodeBase) => { + this.document.children.push(node as Node) + this.nodeCursor = node + this.nodeStack.length = 0 + this.nodeStack.push(this.nodeCursor) + } + peek = () => { + return this.text[this.charCursor + 1] + } + consume = () => { + this.charCursor++ + } +} + +export type CharHandler = (char: string, state: ParserState) => void | undefined | boolean + +export interface NodeConstruct { + name?: string + char: Record +} + +export function astFromMarkdownish(text: string): RootNode { + performance.mark('astFromMarkdownishBegin') + + const document: RootNode = { + type: 'root', + children: [], + } + + const state = new ParserStateImpl(document, text) + + const nodeConstructs: NodeConstruct[] = [paragraph(), escape(), emphasisAndStrong(), reverse()] + + const charHandlers: Record = {} + + for (const construct of nodeConstructs) { + for (const [char, handler] of Object.entries(construct.char)) { + if (!charHandlers[char]) charHandlers[char] = [] + charHandlers[char].push(handler) + } + } + + function runAll(handlers: CharHandler[], char: string, state: ParserState): void | undefined | boolean { + for (const handler of handlers) { + const result = handler(char, state) + if (typeof result === 'boolean') return result + } + } + + for (state.charCursor = 0; state.charCursor < text.length; state.charCursor++) { + const char = text[state.charCursor] + let preventOthers = false + if (!preventOthers && charHandlers['any']) { + const result = runAll(charHandlers['any'], char, state) + if (result === false) continue + if (result === true) preventOthers = true + } + if (!preventOthers && charHandlers[char]) { + const result = runAll(charHandlers[char], char, state) + if (result === false) continue + if (result === true) preventOthers = true + } + state.buffer += char + } + + if (charHandlers['end']) runAll(charHandlers['end'], 'end', state) + + performance.mark('astFromMarkdownishEnd') + + console.log(performance.measure('astFromMarkdownish', 'astFromMarkdownishBegin', 'astFromMarkdownishEnd')) + + console.log(document) + + return document +} diff --git a/packages/apps/client/src/lib/prosemirrorDoc.ts b/packages/apps/client/src/lib/prosemirrorDoc.ts index 13367d2..26bcba1 100644 --- a/packages/apps/client/src/lib/prosemirrorDoc.ts +++ b/packages/apps/client/src/lib/prosemirrorDoc.ts @@ -1,6 +1,7 @@ import { Node as ProsemirrorNode } from 'prosemirror-model' import { schema } from '../ScriptEditor/scriptSchema' -import { astFromMarkdownish, Node as MdAstNode } from './markdownishParser' +import { Node as MdAstNode } from './mdParser/astNodes' +import { astFromMarkdownish } from './mdParser' export function fromMarkdown(text: string): ProsemirrorNode[] { const ast = astFromMarkdownish(text)