diff --git a/jest.config.js b/jest.config.js index 0c5dc19..d5f5c3d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,9 +4,6 @@ module.exports = { testEnvironment: 'node', roots: ['/test'], testPathIgnorePatterns: ['/node_modules/', '/test/integration/'], - moduleNameMapper: { - '^(\\.{1,2}/.*)\\.js$': '$1', - }, transform: { '^.+\\.tsx?$': ['ts-jest', { tsconfig: { diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6568843 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from './ts'; diff --git a/src/ts/index.ts b/src/ts/index.ts new file mode 100644 index 0000000..6345f6d --- /dev/null +++ b/src/ts/index.ts @@ -0,0 +1,2 @@ +export * from "./ribbit"; +export * from "./hopdown"; diff --git a/src/ts/ribbit-editor.ts b/src/ts/ribbit-editor.ts index 578a5df..c326164 100644 --- a/src/ts/ribbit-editor.ts +++ b/src/ts/ribbit-editor.ts @@ -69,10 +69,10 @@ interface BlockRule { isList?: boolean; } -const HEADING_PATTERN = /^(?#{1,6}) /; -const BLOCKQUOTE_PATTERN = /^> /; +const HEADING_PATTERN = /^(?#{1,6}) /; +const BLOCKQUOTE_PATTERN = /^> /; const UNORDERED_LIST_PATTERN = /^[-*+] /; -const ORDERED_LIST_PATTERN = /^\d+\. /; +const ORDERED_LIST_PATTERN = /^\d+\. /; // Block rules in priority order. Paragraph is the implicit fallback. const BLOCK_RULES: BlockRule[] = [ @@ -200,6 +200,19 @@ export class RibbitEditor extends Ribbit { if (this.theme.features?.vim) { // TODO + /* + this.vim = new VimHandler((mode) => { + if (mode === 'normal') { + this.toolbar.disable(); + this.element.classList.add('vim-normal'); + this.element.classList.remove('vim-insert'); + } else { + this.toolbar.enable(); + this.element.classList.add('vim-insert'); + this.element.classList.remove('vim-normal'); + } + }); + */ } this.#bindEvents(); @@ -265,7 +278,6 @@ export class RibbitEditor extends Ribbit { /** * Switch to styled-source editing mode. Renders the current markdown * as a styled DOM (one block div per line) and enables contentEditable. - * The DOM is never rebuilt on mode switch — only CSS changes. * * editor.wysiwyg(); * // user now edits markdown directly with CSS rendering @@ -274,10 +286,14 @@ export class RibbitEditor extends Ribbit { if (this.getState() === this.states.WYSIWYG) { return; } - this.invalidateCache(); + // Capture markdown before building the styled DOM, so getMarkdown() + // in wysiwyg state reads from the live styled DOM rather than + // sourceMarkdown (which belongs to view state). + const markdown = this.getMarkdown(); + this.sourceMarkdown = null; this.collaboration?.connect(); this.element.innerHTML = ''; - this.element.appendChild(this.#markdownToStyledDOM(this.getMarkdown())); + this.element.appendChild(this.#markdownToStyledDOM(markdown)); this.element.contentEditable = 'true'; // Macro islands are non-editable; their source is in data-source for (const macroElement of Array.from(this.element.querySelectorAll('.macro'))) { @@ -291,9 +307,10 @@ export class RibbitEditor extends Ribbit { /** * Convert the editor's current styled DOM back to markdown. - * Because delimiter characters live in text nodes inside .md-delim - * spans, element.textContent == the original markdown source. - * No conversion needed [see STYLED_SOURCE_DESIGN.md §getMarkdown()]. + * In wysiwyg state reads directly from the styled DOM — because + * every delimiter lives in a .md-delim text node, textContent + * always equals the original markdown source [see STYLED_SOURCE_DESIGN.md]. + * In view state delegates to the base class which reads sourceMarkdown. * * const markdown = editor.getMarkdown(); // "**hello** world" */ @@ -305,12 +322,9 @@ export class RibbitEditor extends Ribbit { .map((block) => this.#blockToMarkdown(block as HTMLElement)) .join('\n'); } - // VIEW state: element contains rendered HTML — fall back to - // the cached markdown that was used to render it. - if (this.cachedMarkdown !== null) { - return this.cachedMarkdown; - } - return this.element.textContent || ''; + // VIEW state: delegate to base class, which reads sourceMarkdown + // (set by view() before rendering) or falls back to textContent. + return super.getMarkdown(); } /** @@ -355,7 +369,7 @@ export class RibbitEditor extends Ribbit { * and parses inline formatting for the remaining content. * * this.#buildBlock('## Hello **world**') - * //
+ * //
* // ## * // Hello * //
@@ -409,9 +423,9 @@ export class RibbitEditor extends Ribbit { // Stage 1: tokenise into raw-text segments and matched parts. // We walk all rules left-to-right, splitting segments as we go. // Each segment is either raw (unmatched) or a matched inline rule. - interface RawSegment { raw: true; text: string } - interface RuleMatch { raw: false; rule: InlineRule; content: string; fullMatch: string } - interface LinkMatch { raw: false; isLink: true; text: string; href: string; fullMatch: string } + interface RawSegment { raw: true; text: string } + interface RuleMatch { raw: false; rule: InlineRule; content: string; fullMatch: string } + interface LinkMatch { raw: false; isLink: true; text: string; href: string; fullMatch: string } type Segment = RawSegment | RuleMatch | LinkMatch; let segments: Segment[] = [{ raw: true, text }]; @@ -487,7 +501,7 @@ export class RibbitEditor extends Ribbit { if ('isLink' in segment) { // Link: [text](href) // All three parts go into .md-delim spans so textContent - // reproduces the full markdown [( href )] syntax + // reproduces the full markdown [text](href) syntax span.className = 'md-link'; span.appendChild(this.#makeDelimSpan('[')); const linkTextNode = document.createElement('span'); @@ -555,8 +569,7 @@ export class RibbitEditor extends Ribbit { /** * Handle Enter and Backspace ourselves; route all other keys to the - * block tag's handleKeydown if it has one. This replaces the old - * dispatchKeydown which routed through the full tag system [C14]. + * block tag's handleKeydown if it has one. */ #dispatchKeydown(event: KeyboardEvent): void { // Dispatch to the block tag's own key handler first, so that @@ -740,10 +753,6 @@ export class RibbitEditor extends Ribbit { range.collapse(true); return true; } - // Mutate remaining via closure — TypeScript doesn't allow - // reassigning a parameter across recursive calls cleanly, - // so we use the return-value protocol: false = not placed yet, - // the caller subtracts and recurses. return false; } let consumed = 0; @@ -759,7 +768,6 @@ export class RibbitEditor extends Ribbit { } else { const childLength = (child.textContent || '').length; if (remaining - consumed <= childLength) { - // Recurse into this subtree with adjusted remaining const placed = this.#walkForCaret(child, range, remaining - consumed); if (placed) { return true; diff --git a/src/ts/ribbit.ts b/src/ts/ribbit.ts index 54c89c1..117a686 100644 --- a/src/ts/ribbit.ts +++ b/src/ts/ribbit.ts @@ -38,8 +38,6 @@ export class Ribbit { api: unknown; element: HTMLElement; states: Record; - cachedHTML: string | null; - cachedMarkdown: string | null; state: string | null; theme: RibbitTheme; themes: ThemeManager; @@ -51,6 +49,12 @@ export class Ribbit { private emitter: RibbitEmitter; private macros: MacroDef[]; + // The markdown source as it existed before view() rendered it to HTML. + // Set by subclasses (RibbitEditor) before overwriting element.innerHTML. + // Allows getMarkdown() in view state to return the original source rather + // than textContent of the rendered HTML (which strips delimiters). + protected sourceMarkdown: string | null = null; + constructor(settings: RibbitSettings) { this.api = settings.api || null; this.element = document.getElementById(settings.editorId || 'ribbit')!; @@ -60,8 +64,6 @@ export class Ribbit { this.states = { VIEW: 'view', }; - this.cachedHTML = null; - this.cachedMarkdown = null; this.state = null; this.themes = new ThemeManager(defaultTheme, this.themesPath, (theme, previous) => { @@ -69,7 +71,6 @@ export class Ribbit { this.converter = theme.tags ? new HopDown({ tags: theme.tags, macros: this.macros }) : new HopDown({ macros: this.macros }); - this.cachedHTML = null; this.emitter.emit('themeChange', { current: theme, previous, @@ -112,14 +113,13 @@ export class Ribbit { settings.collaboration, { onRemoteUpdate: (content) => { - this.cachedMarkdown = content; - this.cachedHTML = null; + this.sourceMarkdown = content; if (this.getState() !== this.states.VIEW) { - this.element.innerHTML = this.getHTML(); + this.element.innerHTML = this.markdownToHTML(content); } this.emitter.emit('change', { markdown: content, - html: this.getHTML(), + html: this.markdownToHTML(content), }); }, onPeersChange: (peers) => { @@ -188,7 +188,7 @@ export class Ribbit { } /** - * Current mode name ('view', 'edit', or 'wysiwyg'). + * Current mode name ('view' or 'wysiwyg'). * * if (editor.getState() === 'wysiwyg') { ... } */ @@ -200,7 +200,7 @@ export class Ribbit { * Transition to a new mode. Updates CSS classes on the editor element * so themes can style each mode differently, and fires modeChange. * - * editor.setState('edit'); + * editor.setState('wysiwyg'); */ setState(newState: string): void { const previous = this.state; @@ -225,28 +225,26 @@ export class Ribbit { } /** - * Rendered HTML of the current content, cached until invalidated. + * Rendered HTML of the current content. * * document.getElementById('preview').innerHTML = viewer.getHTML(); */ getHTML(): string { - if (this.cachedHTML === null) { - this.cachedHTML = this.markdownToHTML(this.getMarkdown()); - } - return this.cachedHTML; + return this.markdownToHTML(this.getMarkdown()); } /** - * Raw markdown of the current content. In view mode this is the - * original text; in edit/wysiwyg mode it's derived from the DOM. + * Raw markdown of the current content. In view state reads from + * sourceMarkdown if set (preserved before rendering overwrote the + * element), otherwise falls back to element.textContent. * * fetch('/save', { body: editor.getMarkdown() }); */ getMarkdown(): string { - if (this.cachedMarkdown === null) { - this.cachedMarkdown = this.element.textContent || ''; + if (this.sourceMarkdown !== null) { + return this.sourceMarkdown; } - return this.cachedMarkdown; + return this.element.textContent || ''; } /** @@ -270,26 +268,20 @@ export class Ribbit { * editor.view(); */ view(): void { - if (this.getState() === this.states.VIEW) return; - this.invalidateCache(); + if (this.getState() === this.states.VIEW) { + return; + } + // Capture markdown before overwriting the element with rendered HTML. + // getMarkdown() on the base class reads element.textContent when + // sourceMarkdown is null — correct for the initial load case where + // the element contains raw markdown text. + this.sourceMarkdown = this.getMarkdown(); this.collaboration?.disconnect(); - this.element.innerHTML = this.getHTML(); + this.element.innerHTML = this.markdownToHTML(this.sourceMarkdown); this.setState(this.states.VIEW); this.element.contentEditable = 'false'; } - /** - * Force re-conversion on next getHTML()/getMarkdown() call. - * Call after programmatically changing element content. - * - * editor.element.innerHTML = newContent; - * editor.invalidateCache(); - */ - invalidateCache(): void { - this.cachedMarkdown = null; - this.cachedHTML = null; - } - /** * Request an advisory editing lock. Returns false if another user * holds the lock. Requires a collaboration transport. @@ -297,7 +289,9 @@ export class Ribbit { * if (await editor.lockForEditing()) { editor.wysiwyg(); } */ async lockForEditing(): Promise { - if (!this.collaboration) return false; + if (!this.collaboration) { + return false; + } return this.collaboration.lock(); } @@ -318,7 +312,9 @@ export class Ribbit { * await editor.forceLockEditing(); */ async forceLockEditing(): Promise { - if (!this.collaboration) return false; + if (!this.collaboration) { + return false; + } return this.collaboration.forceLock(); } @@ -329,7 +325,9 @@ export class Ribbit { * revisions.forEach(r => console.log(r.id, r.timestamp)); */ async listRevisions(): Promise { - if (!this.collaboration) return []; + if (!this.collaboration) { + return []; + } return this.collaboration.listRevisions(); } @@ -340,7 +338,9 @@ export class Ribbit { * if (rev) { console.log(rev.content); } */ async getRevision(id: string): Promise<(Revision & { content: string }) | null> { - if (!this.collaboration) return null; + if (!this.collaboration) { + return null; + } return this.collaboration.getRevision(id); } @@ -351,18 +351,22 @@ export class Ribbit { * await editor.restoreRevision('abc-123'); */ async restoreRevision(id: string): Promise { - if (!this.collaboration) return; + if (!this.collaboration) { + return; + } const revision = await this.collaboration.getRevision(id); - if (!revision) return; - this.cachedMarkdown = revision.content; - this.cachedHTML = this.markdownToHTML(revision.content); + if (!revision) { + return; + } + this.sourceMarkdown = revision.content; + const html = this.markdownToHTML(revision.content); this.collaboration.sendUpdate(revision.content); if (this.getState() !== this.states.VIEW) { - this.element.innerHTML = this.cachedHTML; + this.element.innerHTML = html; } this.emitter.emit('change', { markdown: revision.content, - html: this.cachedHTML, + html, }); } @@ -373,7 +377,9 @@ export class Ribbit { * const rev = await editor.createRevision({ label: 'v1.0' }); */ async createRevision(metadata?: RevisionMetadata): Promise { - if (!this.collaboration) return null; + if (!this.collaboration) { + return null; + } const revision = await this.collaboration.createRevision(this.getMarkdown(), metadata); if (revision) { this.emitter.emit('revisionCreated', { revision }); @@ -427,7 +433,7 @@ export function decodeHtmlEntities(html: string): string { /** * Encode characters that would be interpreted as HTML into numeric * entities. Used when displaying raw markdown in contentEditable - * (edit mode) so the browser doesn't parse it as markup. + * so the browser doesn't parse it as markup. * * encodeHtmlEntities('hi') // '<b>hi</b>' */ diff --git a/test/collaboration.test.ts b/test/collaboration.test.ts deleted file mode 100644 index ac88f9b..0000000 --- a/test/collaboration.test.ts +++ /dev/null @@ -1,491 +0,0 @@ -import { ribbit, resetDOM } from './setup'; - -const lib = ribbit(); - -function mockTransport() { - const receiveListeners: Array<(update: Uint8Array) => void> = []; - const lockListeners: Array<(holder: any) => void> = []; - return { - connected: false, - sent: [] as Uint8Array[], - locked: false, - connect() { - this.connected = true; - }, - disconnect() { - this.connected = false; - }, - send(update: Uint8Array) { - this.sent.push(update); - }, - onReceive(cb: (update: Uint8Array) => void) { - receiveListeners.push(cb); - }, - simulateRemote(content: string) { - const encoded = new TextEncoder().encode(content); - receiveListeners.forEach(cb => cb(encoded)); - }, - lock: async function() { - this.locked = true; - return true; - }, - unlock() { - this.locked = false; - }, - forceLock: async function() { - this.locked = true; - return true; - }, - onLockChange(cb: (holder: any) => void) { - lockListeners.push(cb); - }, - simulateLock(holder: any) { - lockListeners.forEach(cb => cb(holder)); - }, - }; -} - -function mockPresence() { - const listeners: Array<(peers: any[]) => void> = []; - return { - lastSent: null as any, - send(info: any) { - this.lastSent = info; - }, - onUpdate(cb: (peers: any[]) => void) { - listeners.push(cb); - }, - simulatePeers(peers: any[]) { - listeners.forEach(cb => cb(peers)); - }, - }; -} - -function mockRevisions() { - const store: any[] = []; - return { - store, - list: async () => store, - get: async (id: string) => store.find((rev: any) => rev.id === id), - create: async (content: string, meta?: any) => { - const rev = { - id: String(store.length + 1), - timestamp: new Date().toISOString(), - content, - ...meta, - }; - store.push(rev); - return rev; - }, - }; -} - -describe('CollaborationManager', () => { - beforeEach(() => resetDOM('initial')); - - it('does not create manager without settings', () => { - const editor = new lib.Editor({}); - editor.run(); - expect(editor.collaboration).toBeUndefined(); - }); - - it('creates manager with settings', () => { - const transport = mockTransport(); - const editor = new lib.Editor({ - collaboration: { - transport, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - expect(editor.collaboration).toBeDefined(); - }); - - describe('connection lifecycle', () => { - it('connects on wysiwyg', () => { - const transport = mockTransport(); - const editor = new lib.Editor({ - collaboration: { - transport, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - editor.wysiwyg(); - expect(transport.connected).toBe(true); - }); - - it('connects on edit', () => { - const transport = mockTransport(); - const editor = new lib.Editor({ - collaboration: { - transport, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - editor.edit(); - expect(transport.connected).toBe(true); - }); - - it('disconnects on view', () => { - const transport = mockTransport(); - const editor = new lib.Editor({ - collaboration: { - transport, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - editor.wysiwyg(); - editor.view(); - expect(transport.connected).toBe(false); - }); - }); - - describe('source mode pausing', () => { - it('pauses on entering source mode', () => { - const transport = mockTransport(); - const editor = new lib.Editor({ - collaboration: { - transport, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - editor.edit(); - expect(editor.collaboration!.isPaused()).toBe(true); - }); - - it('counts remote changes while paused', () => { - const transport = mockTransport(); - const editor = new lib.Editor({ - collaboration: { - transport, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - editor.edit(); - transport.simulateRemote('change 1'); - transport.simulateRemote('change 2'); - expect(editor.collaboration!.getRemoteChangeCount()).toBe(2); - }); - - it('fires remoteActivity event while paused', (done) => { - const transport = mockTransport(); - const editor = new lib.Editor({ - collaboration: { - transport, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - on: { - remoteActivity: ({ count }: any) => { - if (count === 1) { - done(); - } - }, - }, - }); - editor.run(); - editor.edit(); - transport.simulateRemote('change'); - }); - - it('resumes on switching to wysiwyg', () => { - const transport = mockTransport(); - const editor = new lib.Editor({ - collaboration: { - transport, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - editor.edit(); - editor.wysiwyg(); - expect(editor.collaboration!.isPaused()).toBe(false); - }); - }); - - describe('locking', () => { - it('lock returns true', async () => { - const transport = mockTransport(); - const editor = new lib.Editor({ - collaboration: { - transport, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - expect(await editor.lockForEditing()).toBe(true); - }); - - it('forceLock returns true', async () => { - const transport = mockTransport(); - const editor = new lib.Editor({ - collaboration: { - transport, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - expect(await editor.forceLockEditing()).toBe(true); - }); - - it('fires lockChange event', (done) => { - const transport = mockTransport(); - const editor = new lib.Editor({ - collaboration: { - transport, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - on: { - lockChange: ({ holder }: any) => { - if (holder?.userId === 'alice') { - done(); - } - }, - }, - }); - editor.run(); - transport.simulateLock({ - userId: 'alice', - displayName: 'Alice', - status: 'active', - lastActive: Date.now(), - }); - }); - }); - - describe('presence', () => { - it('sends cursor with status', () => { - const transport = mockTransport(); - const presence = mockPresence(); - const editor = new lib.Editor({ - collaboration: { - transport, - presence, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - color: '#f00', - }, - }, - }); - editor.run(); - editor.wysiwyg(); - editor.collaboration!.sendCursor(42); - expect(presence.lastSent.status).toBe('active'); - expect(presence.lastSent.cursor).toBe(42); - }); - - it('sends editing status when paused', () => { - const transport = mockTransport(); - const presence = mockPresence(); - const editor = new lib.Editor({ - collaboration: { - transport, - presence, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - editor.edit(); - editor.collaboration!.sendCursor(10); - expect(presence.lastSent.status).toBe('editing'); - }); - - it('applies idle status to peers', () => { - const transport = mockTransport(); - const presence = mockPresence(); - const editor = new lib.Editor({ - collaboration: { - transport, - presence, - idleTimeout: 100, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - presence.simulatePeers([ - { - userId: 'a', - displayName: 'A', - status: 'active', - lastActive: Date.now() - 200, - }, - { - userId: 'b', - displayName: 'B', - status: 'active', - lastActive: Date.now(), - }, - ]); - const peers = editor.collaboration!.getPeers(); - expect(peers[0].status).toBe('idle'); - expect(peers[1].status).toBe('active'); - }); - }); - - describe('revisions', () => { - it('lists revisions', async () => { - const transport = mockTransport(); - const revisions = mockRevisions(); - await revisions.create('v1', { author: 'test' }); - const editor = new lib.Editor({ - collaboration: { - transport, - revisions, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - const list = await editor.listRevisions(); - expect(list).toHaveLength(1); - }); - - it('creates revision', async () => { - const transport = mockTransport(); - const revisions = mockRevisions(); - const editor = new lib.Editor({ - collaboration: { - transport, - revisions, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - const rev = await editor.createRevision({ - author: 'test', - summary: 'test rev', - }); - expect(rev).toBeDefined(); - expect(revisions.store).toHaveLength(1); - }); - - it('restores revision', async () => { - const transport = mockTransport(); - const revisions = mockRevisions(); - await revisions.create('old content', { author: 'test' }); - const editor = new lib.Editor({ - collaboration: { - transport, - revisions, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - }); - editor.run(); - editor.wysiwyg(); - await editor.restoreRevision('1'); - expect(editor.getMarkdown()).toBe('old content'); - }); - - it('fires revisionCreated event', async () => { - const transport = mockTransport(); - const revisions = mockRevisions(); - let fired = false; - const editor = new lib.Editor({ - collaboration: { - transport, - revisions, - user: { - userId: 'test', - displayName: 'Test', - status: 'active', - lastActive: Date.now(), - }, - }, - on: { - revisionCreated: () => { - fired = true; - }, - }, - }); - editor.run(); - await editor.createRevision({ author: 'test' }); - expect(fired).toBe(true); - }); - }); -}); diff --git a/test/custom-tags.test.ts b/test/custom-tags.test.ts index 6f2ea79..76827b7 100644 --- a/test/custom-tags.test.ts +++ b/test/custom-tags.test.ts @@ -1,4 +1,5 @@ import { ribbit, resetDOM } from './setup'; +import { HopDown } from '../src'; const lib = ribbit(); @@ -25,7 +26,7 @@ describe('Custom block tags', () => { selector: 'DETAILS', toMarkdown: (element: any, convert: any) => '\n\n|||\n' + convert.children(element).trim() + '\n|||\n\n', }; - const converter = new lib.HopDown({ + const converter = new HopDown({ tags: { 'DETAILS': spoiler, ...lib.defaultTags, @@ -38,15 +39,15 @@ describe('Custom block tags', () => { describe('HopDown({ exclude })', () => { it('excludes table', () => { - const converter = new lib.HopDown({ exclude: ['table'] }); + const converter = new HopDown({ exclude: ['table'] }); expect(converter.toHTML('| a |\n|---|\n| 1 |')).not.toContain(''); }); it('excludes code', () => { - const converter = new lib.HopDown({ exclude: ['code'] }); + const converter = new HopDown({ exclude: ['code'] }); expect(converter.toHTML('`code`')).toBe('

`code`

'); }); it('other tags still work', () => { - const converter = new lib.HopDown({ exclude: ['table'] }); + const converter = new HopDown({ exclude: ['table'] }); expect(converter.toHTML('**bold**')).toContain('bold'); }); }); @@ -59,7 +60,7 @@ describe('Collision detection', () => { htmlTag: 'span', precedence: 10, }); - expect(() => new lib.HopDown({ + expect(() => new HopDown({ tags: { ...lib.defaultTags, 'SPAN': bad, @@ -75,7 +76,7 @@ describe('Collision detection', () => { selector: 'STRONG', toMarkdown: () => '', }; - expect(() => new lib.HopDown({ + expect(() => new HopDown({ tags: { ...lib.defaultTags, 'STRONG': dup, @@ -98,7 +99,7 @@ describe('Collision detection', () => { }); // Remove default strikethrough to avoid collision with the custom S/DEL tags const { 'DEL,S,STRIKE': _, ...tagsWithoutStrikethrough } = lib.defaultTags; - expect(() => new lib.HopDown({ + expect(() => new HopDown({ tags: { ...tagsWithoutStrikethrough, 'S': short, diff --git a/test/editor.test.ts b/test/editor.test.ts index 9eff86f..d72821c 100644 --- a/test/editor.test.ts +++ b/test/editor.test.ts @@ -128,7 +128,7 @@ describe('RibbitEditor modes', () => { editor.run(); editor.wysiwyg(); editor.view(); - expect(modes).toEqual(['view', 'wysiwyg', 'edit', 'view']); + expect(modes).toEqual(['view', 'wysiwyg', 'view']); }); }); @@ -231,9 +231,7 @@ describe('Editor htmlToMarkdown', () => { it('returns markdown in view state', () => { resetDOM('**bold**'); const editor = new lib.Editor({}); - console.log(editor.getMarkdown()); editor.run(); - console.log(editor.getMarkdown()); expect(editor.getMarkdown()).toBe('**bold**'); }); diff --git a/test/hopdown.test.ts b/test/hopdown.test.ts index 4a51c4a..a869dc2 100644 --- a/test/hopdown.test.ts +++ b/test/hopdown.test.ts @@ -1,11 +1,14 @@ import { ribbit } from './setup'; const lib = ribbit(); -const hopdown = new lib.HopDown(); +const editor = new lib.Editor({}); +const hopdown = editor.converter; + const H = (md: string) => hopdown.toHTML(md); const M = (html: string) => hopdown.toMarkdown(html); const rt = (md: string) => M(H(md)); + describe('Markdown → HTML', () => { describe('inline formatting', () => { it('bold', () => expect(H('**bold**')).toBe('

bold

')); diff --git a/test/macros.test.ts b/test/macros.test.ts index 291b805..12e13a1 100644 --- a/test/macros.test.ts +++ b/test/macros.test.ts @@ -1,9 +1,7 @@ -import { ribbit } from './setup'; +import { ribbit, resetDOM } from './setup'; const lib = ribbit(); -const spacePattern = / /g; - const macros = [ { name: 'user', @@ -13,7 +11,7 @@ const macros = [ name: 'npc', toHTML: ({ keywords }: any) => { const name = keywords.join(' '); - return '' + name + ''; + return '' + name + ''; }, }, { @@ -26,7 +24,9 @@ const macros = [ }, ]; -const converter = new lib.HopDown({ macros }); +const editor = new lib.Editor({macros: macros}); +const converter = editor.converter; + const H = (md: string) => converter.toHTML(md); const M = (html: string) => converter.toMarkdown(html);