diff --git a/packages/apps/client/src/ScriptEditor/Editor.tsx b/packages/apps/client/src/ScriptEditor/Editor.tsx index e2e412b..87d0bbe 100644 --- a/packages/apps/client/src/ScriptEditor/Editor.tsx +++ b/packages/apps/client/src/ScriptEditor/Editor.tsx @@ -15,7 +15,7 @@ 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' + + '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. ' diff --git a/packages/apps/client/src/lib/mdParser/constructs/emphasisAndStrong.ts b/packages/apps/client/src/lib/mdParser/constructs/emphasisAndStrong.ts index 7e7957a..eae9575 100644 --- a/packages/apps/client/src/lib/mdParser/constructs/emphasisAndStrong.ts +++ b/packages/apps/client/src/lib/mdParser/constructs/emphasisAndStrong.ts @@ -1,18 +1,18 @@ -import { NodeConstruct, ParserState } from '..' -import { EmphasisNode, StrongNode } from '../astNodes' +import { NodeConstruct, ParserState, CharHandlerResult } from '../parserState' +import { EmphasisNode, StrongNode, Node, ParentNodeBase } from '../astNodes' export function emphasisAndStrong(): NodeConstruct { - function emphasisOrStrong(char: string, state: ParserState) { + function emphasisOrStrong(char: string, state: ParserState): CharHandlerResult | void { 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.nodeCursor && isEmphasisOrStrongNode(state.nodeCursor)) { + if (state.nodeCursor.code.startsWith(char)) { if (state.peek() === char) { state.consume() } state.flushBuffer() state.popNode() - return false + return CharHandlerResult.StopProcessingNoBuffer } } @@ -22,7 +22,7 @@ export function emphasisAndStrong(): NodeConstruct { if (state.peek() === char) { type = 'strong' - state.consume() + char += state.consume() } const emphasisOrStrongNode: EmphasisNode | StrongNode = { @@ -32,7 +32,7 @@ export function emphasisAndStrong(): NodeConstruct { } state.pushNode(emphasisOrStrongNode) - return false + return CharHandlerResult.StopProcessingNoBuffer } return { @@ -43,3 +43,7 @@ export function emphasisAndStrong(): NodeConstruct { }, } } + +function isEmphasisOrStrongNode(node: Node | ParentNodeBase): node is EmphasisNode | StrongNode { + return node.type === 'emphasis' || node.type === 'strong' +} diff --git a/packages/apps/client/src/lib/mdParser/constructs/escape.ts b/packages/apps/client/src/lib/mdParser/constructs/escape.ts index 2b1e6bf..a220468 100644 --- a/packages/apps/client/src/lib/mdParser/constructs/escape.ts +++ b/packages/apps/client/src/lib/mdParser/constructs/escape.ts @@ -1,14 +1,15 @@ -import { NodeConstruct, ParserState } from '..' +import { CharHandlerResult, NodeConstruct, ParserState } from '../parserState' export function escape(): NodeConstruct { - function escapeChar(_: string, state: ParserState) { + function escapeChar(_: string, state: ParserState): CharHandlerResult | void { state.dataStore['inEscape'] = true + return CharHandlerResult.StopProcessingNoBuffer } - function passthroughChar(_: string, state: ParserState) { + function passthroughChar(_: string, state: ParserState): CharHandlerResult | void { if (state.dataStore['inEscape'] !== true) return state.dataStore['inEscape'] = false - return true + return CharHandlerResult.StopProcessing } return { diff --git a/packages/apps/client/src/lib/mdParser/constructs/paragraph.ts b/packages/apps/client/src/lib/mdParser/constructs/paragraph.ts index 94e7743..a824b0b 100644 --- a/packages/apps/client/src/lib/mdParser/constructs/paragraph.ts +++ b/packages/apps/client/src/lib/mdParser/constructs/paragraph.ts @@ -1,4 +1,4 @@ -import { NodeConstruct, ParserState } from '..' +import { NodeConstruct, ParserState, CharHandlerResult } from '../parserState' import { ParagraphNode } from '../astNodes' export function paragraph(): NodeConstruct { @@ -11,7 +11,7 @@ export function paragraph(): NodeConstruct { state.replaceStack(newParagraph) } - function paragraphEnd(char: string, state: ParserState) { + function paragraphEnd(char: string, state: ParserState): CharHandlerResult { if (state.nodeCursor === null) { paragraphStart(char, state) } @@ -19,7 +19,7 @@ export function paragraph(): NodeConstruct { state.flushBuffer() state.nodeCursor = null - return false + return CharHandlerResult.StopProcessingNoBuffer } return { diff --git a/packages/apps/client/src/lib/mdParser/constructs/reverse.ts b/packages/apps/client/src/lib/mdParser/constructs/reverse.ts index 641e0b7..75957bd 100644 --- a/packages/apps/client/src/lib/mdParser/constructs/reverse.ts +++ b/packages/apps/client/src/lib/mdParser/constructs/reverse.ts @@ -1,14 +1,14 @@ -import { NodeConstruct, ParserState } from '..' +import { NodeConstruct, ParserState, CharHandlerResult } from '../parserState' import { ReverseNode } from '../astNodes' export function reverse(): NodeConstruct { - function reverse(char: string, state: ParserState) { + function reverse(char: string, state: ParserState): CharHandlerResult | void { 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 + return CharHandlerResult.StopProcessingNoBuffer } } @@ -23,7 +23,7 @@ export function reverse(): NodeConstruct { } state.pushNode(reverseNode) - return false + return CharHandlerResult.StopProcessingNoBuffer } return { diff --git a/packages/apps/client/src/lib/mdParser/index.ts b/packages/apps/client/src/lib/mdParser/index.ts index 69bc062..3080711 100644 --- a/packages/apps/client/src/lib/mdParser/index.ts +++ b/packages/apps/client/src/lib/mdParser/index.ts @@ -3,20 +3,7 @@ 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 -} +import { CharHandler, CharHandlerResult, NodeConstruct, ParserState } from './parserState' export class ParserStateImpl implements ParserState { readonly nodeStack: ParentNodeBase[] = [] @@ -60,27 +47,15 @@ export class ParserStateImpl implements ParserState { return this.text[this.charCursor + 1] } consume = () => { + if (this.text[this.charCursor + 1] === undefined) throw new Error('No more text available to parse') this.charCursor++ + return this.text[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) +export type Parser = (text: string) => RootNode +export default function createParser(): Parser { const nodeConstructs: NodeConstruct[] = [paragraph(), escape(), emphasisAndStrong(), reverse()] const charHandlers: Record = {} @@ -92,36 +67,47 @@ export function astFromMarkdownish(text: string): RootNode { } } - function runAll(handlers: CharHandler[], char: string, state: ParserState): void | undefined | boolean { + function runAll(handlers: CharHandler[], char: string, state: ParserState): void | undefined | CharHandlerResult { for (const handler of handlers) { const result = handler(char, state) - if (typeof result === 'boolean') return result + if (typeof result === 'number') 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 + return function astFromMarkdownish(text: string): RootNode { + performance.mark('astFromMarkdownishBegin') + + const document: RootNode = { + type: 'root', + children: [], } - if (!preventOthers && charHandlers[char]) { - const result = runAll(charHandlers[char], char, state) - if (result === false) continue - if (result === true) preventOthers = true + + const state = new ParserStateImpl(document, text) + + 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 === CharHandlerResult.StopProcessingNoBuffer) continue + if (result === CharHandlerResult.StopProcessing) preventOthers = true + } + if (!preventOthers && charHandlers[char]) { + const result = runAll(charHandlers[char], char, state) + if (result === CharHandlerResult.StopProcessingNoBuffer) continue + if (result === CharHandlerResult.StopProcessing) preventOthers = true + } + state.buffer += char } - state.buffer += char - } - if (charHandlers['end']) runAll(charHandlers['end'], 'end', state) + if (charHandlers['end']) runAll(charHandlers['end'], 'end', state) - performance.mark('astFromMarkdownishEnd') + performance.mark('astFromMarkdownishEnd') - console.log(performance.measure('astFromMarkdownish', 'astFromMarkdownishBegin', 'astFromMarkdownishEnd')) + console.log(performance.measure('astFromMarkdownish', 'astFromMarkdownishBegin', 'astFromMarkdownishEnd')) - console.log(document) + console.log(document) - return document + return document + } } diff --git a/packages/apps/client/src/lib/mdParser/parserState.ts b/packages/apps/client/src/lib/mdParser/parserState.ts new file mode 100644 index 0000000..d5331ef --- /dev/null +++ b/packages/apps/client/src/lib/mdParser/parserState.ts @@ -0,0 +1,41 @@ +import { ParentNodeBase } from './astNodes' + +export interface ParserState { + /** The current stack of nodes as set up leading to the current character */ + readonly nodeStack: ParentNodeBase[] + /** The top of the nodeStack */ + nodeCursor: ParentNodeBase | null + /** A buffer to collect characters that don't create or mutate nodes (text) */ + buffer: string + /** The position of the current character */ + charCursor: number + /** An dataStore that can be read and written to by NodeConstruct Handlers. */ + readonly dataStore: Record + /** Create a new text node and append as a child to the node under nodeCursor */ + flushBuffer(): void + /** Append a new child node to the node at the top of the nodeStack, and push it onto the nodeStack */ + pushNode(node: ParentNodeBase): void + /** Pop a ParentNode from the nodeStack */ + popNode(): void + /** Append a new child node to the root node and clear the stack */ + replaceStack(node: ParentNodeBase): void + /** Get the character immediately after the current one */ + peek(): string | undefined + /** Move the charCursor to the next character */ + consume(): string | undefined +} + +export enum CharHandlerResult { + /** Stop all processing of this character and append it to the text buffer */ + StopProcessing = 1, + /** Stop all processing of this character and don't append it to the text buffer */ + StopProcessingNoBuffer = 2, +} + +export type CharHandler = (char: string, state: ParserState) => void | undefined | CharHandlerResult + +/** A NodeConstruct is a set of character handlers that process a type of a node */ +export interface NodeConstruct { + name?: string + char: Record +} diff --git a/packages/apps/client/src/lib/prosemirrorDoc.ts b/packages/apps/client/src/lib/prosemirrorDoc.ts index 26bcba1..38ee7b4 100644 --- a/packages/apps/client/src/lib/prosemirrorDoc.ts +++ b/packages/apps/client/src/lib/prosemirrorDoc.ts @@ -1,9 +1,10 @@ import { Node as ProsemirrorNode } from 'prosemirror-model' import { schema } from '../ScriptEditor/scriptSchema' import { Node as MdAstNode } from './mdParser/astNodes' -import { astFromMarkdownish } from './mdParser' +import createMdParser from './mdParser' export function fromMarkdown(text: string): ProsemirrorNode[] { + const astFromMarkdownish = createMdParser() const ast = astFromMarkdownish(text) return traverseMdAstNodes(ast.children)