all tests pass but only if md-delim display: inline

This commit is contained in:
evilchili
2026-07-10 12:39:35 -07:00
parent 818ee418d5
commit c1dc49f0b3
23 changed files with 5109 additions and 6197 deletions
-491
View File
@@ -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);
});
});
});
+8 -7
View File
@@ -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('<table>');
});
it('excludes code', () => {
const converter = new lib.HopDown({ exclude: ['code'] });
const converter = new HopDown({ exclude: ['code'] });
expect(converter.toHTML('`code`')).toBe('<p>`code`</p>');
});
it('other tags still work', () => {
const converter = new lib.HopDown({ exclude: ['table'] });
const converter = new HopDown({ exclude: ['table'] });
expect(converter.toHTML('**bold**')).toContain('<strong>bold</strong>');
});
});
@@ -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,
+149
View File
@@ -0,0 +1,149 @@
function disambiguateAsteriskRuns(line: string): string {
const SENTINEL = '\u200C';
const ASTERISK_RUN = /\*{1,3}/g;
const stack: ('*' | '**')[] = [];
let result = '';
let lastIndex = 0;
let match: RegExpExecArray | null;
console.log(`DISAMBIGUATE: '${line}'`);
while ((match = ASTERISK_RUN.exec(line)) !== null) {
result += line.slice(lastIndex, match.index);
const run = match[0];
console.log(`DISAMBIGUATE: run == '${run}', stack == ${stack}`);
if (run.length === 3 && stack.length === 2) {
const innerCloser = stack.pop()!;
const outerCloser = stack.pop()!;
result += innerCloser + SENTINEL + outerCloser;
} else if (run.length === 3 && stack.length === 0) {
if (match.index == 0) {
result += '***';
} else {
result += '*' + SENTINEL + '**';
stack.push('**');
stack.push('*');
}
} else if (run.length === 3) {
result += run;
} else if (stack.length > 0 && stack[stack.length - 1] === run) {
stack.pop();
result += run;
} else {
stack.push(run as '*' | '**');
result += run;
}
lastIndex = match.index + run.length;
}
result += line.slice(lastIndex);
console.log(`DISAMBIGUATE: '${result}'`);
return result;
}
function assert(condition, message) {
if (!condition) { throw new Error(message); }
}
function rewrite(line: string): string {
const SENTINEL = '|' //'\u200C';
const ASTERISK_RUN = /\*{1,3}/g;
const stack: ('*' | '**' | '***')[] = [];
let result = '';
let lastIndex = 0;
let match: RegExpExecArray | null;
const bold = '**';
const italic = '*';
let lastSequence = '';
while ((match = ASTERISK_RUN.exec(line)) !== null) {
result += line.slice(lastIndex, match.index);
const sequence = match[0];
if (sequence.length === 3) {
// split *** into opening ** and *
if (stack.length === 0) {
result += sequence;
stack.push('***');
// closing ***, close the stack
} else if (stack.length === 1 && stack[0] === sequence) {
result = result.replace('***', bold + SENTINEL + italic);
result += italic + SENTINEL + bold;
stack.pop();
} else if (stack.length === 2) {
const inner = stack.pop();
const outer = stack.pop();
result += inner + SENTINEL + outer;
} else if (stack.length === 1) {
console.warn(`Cannot parsed line '${line}': invalid sequence ${sequence} with stack ${stack}!`);
} else {
console.warn(`UNHANDLED '${sequence}', last sequence '${lastSequence}'`);
}
} else if (stack.length) {
if (stack[stack.length - 1] === sequence) {
result += stack.pop();
} else if (stack.length === 1 && stack[0] === '***') {
const opener = stack[0].substring(0, stack[0].length - sequence.length);
result = result.replace('***', opener + SENTINEL + sequence);
result += sequence;
stack[0] = opener as '*' | '**';
} else {
result += sequence;
stack.push(sequence as '*' | '**');
}
} else {
stack.push(sequence as '*' | '**');
result += sequence;
}
lastIndex = match.index + sequence.length;
}
result += line.slice(lastIndex);
return result;
}
function test_it() {
console.log(rewrite('*italic **bold***'));
let cases = [
{
'input': '***bold-italic***',
'expected': '**|*bold-italic*|**'
},
{
'input': '***bold** italic*',
'expected': '*|**bold** italic*',
},
{
'input': '***italic* bold**',
'expected': '**|*italic* bold**',
},
{
'input': '**bold, *italic***',
'expected': '**bold, *italic*|**'
},
{
'input': '*italic, **bold***',
'expected': '*italic, **bold**|*'
},
];
for (const testCase of cases) {
let result = rewrite(testCase.input);
assert(result == testCase.expected, `'${result}' (${result.length}) != '${testCase.expected}' (${testCase.expected.length})`);
}
};
test_it();
+16 -31
View File
@@ -107,14 +107,6 @@ describe('RibbitEditor modes', () => {
expect(editor.element.contentEditable).toBe('true');
});
it('switches to edit', () => {
const editor = new lib.Editor({});
editor.run();
editor.wysiwyg();
editor.edit();
expect(editor.getState()).toBe('edit');
});
it('switches back to view', () => {
const editor = new lib.Editor({});
editor.run();
@@ -135,25 +127,10 @@ describe('RibbitEditor modes', () => {
});
editor.run();
editor.wysiwyg();
editor.edit();
editor.view();
expect(modes).toEqual(['view', 'wysiwyg', 'edit', 'view']);
expect(modes).toEqual(['view', 'wysiwyg', 'view']);
});
it('sourceMode disabled blocks edit', () => {
resetDOM();
const editor = new lib.Editor({
currentTheme: 'no-source',
themes: [{
name: 'no-source',
features: { sourceMode: false },
}],
});
editor.run();
editor.wysiwyg();
editor.edit();
expect(editor.getState()).toBe('wysiwyg');
});
});
describe('ThemeManager', () => {
@@ -229,7 +206,6 @@ describe('defaultTheme', () => {
it('has correct shape', () => {
expect(lib.defaultTheme.name).toBe('ribbit-default');
expect(lib.defaultTheme.tags).toBeDefined();
expect(lib.defaultTheme.features.sourceMode).toBe(true);
});
});
@@ -252,17 +228,26 @@ describe('Utility functions', () => {
});
describe('Editor htmlToMarkdown', () => {
beforeEach(() => resetDOM());
it('converts strong', () => {
it('returns markdown in view state', () => {
resetDOM('**bold**');
const editor = new lib.Editor({});
editor.run();
expect(editor.htmlToMarkdown('<strong>bold</strong>')).toBe('**bold**');
expect(editor.getMarkdown()).toBe('**bold**');
});
it('converts em', () => {
it('returns markdown in wysiwyg state', () => {
resetDOM('**bold**');
const editor = new lib.Editor({});
editor.run();
expect(editor.htmlToMarkdown('<em>italic</em>')).toBe('*italic*');
editor.wysiwyg();
expect(editor.getMarkdown()).toBe('**bold**');
});
it('round-trips inline formatting', () => {
resetDOM('hello **world** and *italic*');
const editor = new lib.Editor({});
editor.run();
editor.wysiwyg();
expect(editor.getMarkdown()).toBe('hello **world** and *italic*');
});
});
+4 -1
View File
@@ -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('<p><strong>bold</strong></p>'));
+14 -103
View File
@@ -1,106 +1,17 @@
/**
* Development server with livereload.
*
* Serves the test page and ribbit dist files. Watches src/ for
* changes, rebuilds automatically, and notifies connected browsers
* to reload via a simple EventSource stream.
*
* Run: npm run dev
*/
const { createServer } = require('./server');
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
var liveServer = require("live-server");
const PORT = 8080;
const WATCH_DIRS = [
path.join(__dirname, '..', '..', 'src'),
path.join(__dirname, '..', '..', 'test', 'integration'),
];
const DEBOUNCE_MS = 300;
var params = {
port: 5023,
host: "0.0.0.0",
open: true,
root: "test/integration",
mount: [
['/static', 'dist/ribbit'],
['/test', 'test/integration'],
],
logLevel: 2, // 0 = errors only, 1 = some, 2 = lots
};
const server = createServer(PORT);
const reloadClients = [];
// Patch the server to add the livereload endpoint
const originalServer = require('http').createServer;
const httpServer = server._server || (() => {
// Access the internal server by starting and intercepting
let captured = null;
const origListen = require('http').Server.prototype.listen;
require('http').Server.prototype.listen = function (...args) {
captured = this;
return origListen.apply(this, args);
};
server.start();
require('http').Server.prototype.listen = origListen;
return captured;
})();
// Simpler approach: create a standalone livereload server
const reloadServer = require('http').createServer((request, response) => {
response.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
});
reloadClients.push(response);
request.on('close', () => {
const index = reloadClients.indexOf(response);
if (index >= 0) {
reloadClients.splice(index, 1);
}
});
});
function notifyReload() {
for (const client of reloadClients) {
client.write('data: reload\n\n');
}
}
function rebuild() {
try {
console.log('\n🔨 Rebuilding...');
execSync('npm run build:js && npm run build:css', {
cwd: path.join(__dirname, '..', '..'),
stdio: 'pipe',
});
console.log('✅ Build complete');
notifyReload();
} catch (error) {
console.error('❌ Build failed:', error.stderr?.toString().slice(0, 500));
}
}
let debounceTimer = null;
function onFileChange(filename) {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
console.log(`📝 Changed: ${filename}`);
debounceTimer = setTimeout(rebuild, DEBOUNCE_MS);
}
// Watch source directories
for (const directory of WATCH_DIRS) {
if (fs.existsSync(directory)) {
fs.watch(directory, { recursive: true }, (eventType, filename) => {
if (filename && !filename.includes('node_modules')) {
onFileChange(filename);
}
});
}
}
server.start().then(() => {
reloadServer.listen(PORT + 1, () => {
console.log(`\n🐸 Ribbit dev server`);
console.log(` Editor: http://localhost:${PORT}`);
console.log(` Livereload: http://localhost:${PORT + 1} (EventSource)`);
console.log(` Watching: src/, test/integration/`);
console.log(`\n Add this to the page to enable livereload:`);
console.log(` <script>new EventSource('http://localhost:${PORT + 1}').onmessage = () => location.reload()</script>\n`);
});
});
console.log(`\n🐸 Ribbit dev server running on http://localhost:${params['port']}`);
liveServer.start(params);
+11 -29
View File
@@ -3,36 +3,23 @@
<head>
<meta charset="utf-8">
<title>Ribbit Integration Test Page</title>
<link rel="stylesheet" href="/ribbit/themes/ribbit-default/theme.css">
<style>
body { font-family: sans-serif; margin: 20px; }
#ribbit { border: 1px solid #ccc; padding: 20px; min-height: 200px; }
.ribbit-toolbar { background: #f5f5f5; border: 1px solid #ccc; padding: 4px; margin-bottom: 8px; }
.ribbit-toolbar ul { list-style: none; margin: 0; padding: 0; display: flex; gap: 2px; }
.ribbit-toolbar button { padding: 4px 8px; border: 1px solid #ddd; border-radius: 3px; background: white; cursor: pointer; font-size: 12px; }
.ribbit-toolbar button.active { background: #d0d0ff; }
.ribbit-toolbar button.disabled { opacity: 0.3; }
.ribbit-toolbar .spacer { width: 12px; }
.ribbit-dropdown { position: absolute; background: white; border: 1px solid #ccc; padding: 4px; }
.ribbit-dropdown button { display: block; width: 100%; }
</style>
<link rel="stylesheet" href="/static/themes/ribbit-default/theme.css">
</head>
<body>
<article id="ribbit">**bold** and *italic* and `code`
<main>
<article id="ribbit">
## Heading
| Type | To Get |
|------|--------|
| `*emphasis*` | *emphasis* |
| `**bold**` | **bold** |
| `abel](/link/address)` | [link label](/link/address) |
| ``inline`` | `inline` |
- list item 1
- list item 2
> a blockquote
| A | B |
|---|---|
| 1 | 2 |
</article>
</main>
<script src="/ribbit/ribbit.js"></script>
<script src="/static/ribbit.js"></script>
<script>
const editor = new ribbit.Editor({
on: {
@@ -42,10 +29,5 @@
editor.run();
window.__ribbitEditor = editor;
</script>
<script>
// Livereload — connects to dev server's EventSource endpoint.
// Silently fails if the dev server isn't running.
try { new EventSource('http://localhost:8081').onmessage = () => location.reload(); } catch(e) {}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -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 '<a href="/NPC/' + name.replace(spacePattern, '') + '">' + name + '</a>';
return '<a href="/NPC/' + name.replace(/ /g, '') + '">' + name + '</a>';
},
},
{
@@ -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);
-150
View File
@@ -1,150 +0,0 @@
import { ribbit, resetDOM } from './setup';
const lib = ribbit();
describe('VimHandler', () => {
beforeEach(() => resetDOM('hello world'));
it('starts in insert mode', () => {
const editor = new lib.Editor({
currentTheme: 'vim',
themes: [{
name: 'vim',
features: {
sourceMode: true,
vim: true,
},
tags: lib.defaultTags,
}],
});
editor.run();
editor.edit();
expect(editor.element.classList.contains('vim-insert')).toBe(true);
});
it('Esc enters normal mode', () => {
const editor = new lib.Editor({
currentTheme: 'vim',
themes: [{
name: 'vim',
features: {
sourceMode: true,
vim: true,
},
tags: lib.defaultTags,
}],
});
editor.run();
editor.edit();
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'Escape' }));
expect(editor.element.classList.contains('vim-normal')).toBe(true);
expect(editor.element.classList.contains('vim-insert')).toBe(false);
});
it('i returns to insert mode', () => {
const editor = new lib.Editor({
currentTheme: 'vim',
themes: [{
name: 'vim',
features: {
sourceMode: true,
vim: true,
},
tags: lib.defaultTags,
}],
});
editor.run();
editor.edit();
// Enter normal mode
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'Escape' }));
// Back to insert
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'i' }));
expect(editor.element.classList.contains('vim-insert')).toBe(true);
expect(editor.element.classList.contains('vim-normal')).toBe(false);
});
it('disables toolbar in normal mode', () => {
const editor = new lib.Editor({
autoToolbar: false,
currentTheme: 'vim',
themes: [{
name: 'vim',
features: {
sourceMode: true,
vim: true,
},
tags: lib.defaultTags,
}],
});
editor.run();
editor.toolbar.render();
editor.edit();
editor.toolbar.enable();
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'Escape' }));
const bold = editor.toolbar.buttons.get('bold');
expect(bold?.element?.classList.contains('disabled')).toBe(true);
});
it('re-enables toolbar in insert mode', () => {
const editor = new lib.Editor({
autoToolbar: false,
currentTheme: 'vim',
themes: [{
name: 'vim',
features: {
sourceMode: true,
vim: true,
},
tags: lib.defaultTags,
}],
});
editor.run();
editor.toolbar.render();
editor.edit();
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'Escape' }));
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'i' }));
const bold = editor.toolbar.buttons.get('bold');
expect(bold?.element?.classList.contains('disabled')).toBe(false);
});
it('detaches when leaving edit mode', () => {
const editor = new lib.Editor({
currentTheme: 'vim',
themes: [{
name: 'vim',
features: {
sourceMode: true,
vim: true,
},
tags: lib.defaultTags,
}],
});
editor.run();
editor.edit();
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'Escape' }));
expect(editor.element.classList.contains('vim-normal')).toBe(true);
editor.wysiwyg();
// vim classes should be gone after mode switch
expect(editor.element.classList.contains('vim-normal')).toBe(false);
expect(editor.element.classList.contains('vim-insert')).toBe(false);
});
it('only activates in edit mode', () => {
const editor = new lib.Editor({
currentTheme: 'vim',
themes: [{
name: 'vim',
features: {
sourceMode: true,
vim: true,
},
tags: lib.defaultTags,
}],
});
editor.run();
editor.wysiwyg();
// Esc in wysiwyg should not add vim classes
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'Escape' }));
expect(editor.element.classList.contains('vim-normal')).toBe(false);
});
});