@joinezco/markdown-editor 0.0.1 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/dist/editor/extensions/codeblock.js +82 -31
- package/dist/editor/index.js +23 -0
- package/dist/editor/styles.js +89 -0
- package/package.json +3 -3
- package/public/fonts/UbuntuMonoNerdFont-Regular.ttf +0 -0
- package/public/snapshot.bin +0 -0
- package/src/App.css +0 -1
- package/src/lib/editor/extensions/codeblock.ts +91 -36
- package/src/lib/editor/index.ts +23 -0
- package/src/lib/editor/styles.ts +93 -0
- package/src/test/multiview-sync.test.ts +137 -0
- package/TEST_README.md +0 -359
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-be-focusable-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-create-an-editor-instance-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-have-initial-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-render-in-the-DOM-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Browser-specific-Features-should-handle-copy-and-paste-operations-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Browser-specific-Features-should-handle-undo-and-redo-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Editor-State-and-Updates-should-maintain-state-across-content-changes-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Editor-State-and-Updates-should-trigger-update-callbacks-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Error-Handling-should-handle-invalid-markdown-gracefully-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Error-Handling-should-handle-very-long-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Keyboard-Shortcuts-should-handle-Ctrl-B-for-bold-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Keyboard-Shortcuts-should-handle-Ctrl-I-for-italic-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-get-markdown-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-handle-empty-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-set-markdown-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-bold-text-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-code-blocks-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-headings-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-inline-code-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-italic-text-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-lists-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-task-lists-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Selection-and-Cursor-Management-should-handle-cursor-positioning-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Selection-and-Cursor-Management-should-set-and-get-selection-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-handle-line-breaks-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-handle-typing-at-different-positions-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-insert-text-at-cursor-position-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-code-blocks-without-language-specification-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-different-programming-languages-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-inline-code-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-render-code-blocks-with-syntax-highlighting-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Extension-Interactions-should-handle-multiple-extensions-working-together-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Extension-Interactions-should-maintain-editor-state-across-complex-operations-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-File-System-Integration-should-handle-file-references-in-code-blocks-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-File-System-Integration-should-maintain-file-system-state-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-auto-detect-URLs-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-handle-email-links-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-render-links-correctly-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Markdown-Storage-should-provide-markdown-storage-interface-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Markdown-Storage-should-sync-markdown-content-with-storage-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Slash-Commands-Extension-should-handle-heading-commands-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Slash-Commands-Extension-should-trigger-slash-commands-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Table-Extension-should-handle-table-navigation-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Table-Extension-should-render-tables-correctly-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-handle-nested-task-lists-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-render-task-lists-correctly-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-toggle-task-completion-1.png +0 -0
|
@@ -75,7 +75,6 @@ export const ExtendedCodeblock = Node.create({
|
|
|
75
75
|
},
|
|
76
76
|
// Render language back to HTML structure
|
|
77
77
|
renderHTML: attributes => {
|
|
78
|
-
console.log('renderHTML attributes', { attributes });
|
|
79
78
|
if (!attributes.language || attributes.language === 'plaintext') {
|
|
80
79
|
return {}; // No class needed for plaintext
|
|
81
80
|
}
|
|
@@ -148,37 +147,36 @@ export const ExtendedCodeblock = Node.create({
|
|
|
148
147
|
},
|
|
149
148
|
// Register input rules (e.g., ``` or ~~~ at the start of a line)
|
|
150
149
|
addInputRules() {
|
|
150
|
+
const parseLanguageAttributes = (input) => {
|
|
151
|
+
if (!input)
|
|
152
|
+
return { language: 'markdown' };
|
|
153
|
+
// If input contains a dot, treat it as a filename
|
|
154
|
+
if (input.includes('.')) {
|
|
155
|
+
const ext = input.split('.').pop()?.toLowerCase() || '';
|
|
156
|
+
const lang = extOrLanguageToLanguageId[ext] || 'markdown';
|
|
157
|
+
return { file: input, language: lang };
|
|
158
|
+
}
|
|
159
|
+
// Otherwise, check if it's a language name
|
|
160
|
+
const matchingLanguage = Object.entries(extOrLanguageToLanguageId).find(([ext, lang]) => {
|
|
161
|
+
return lang.includes(input) || ext === input;
|
|
162
|
+
});
|
|
163
|
+
if (matchingLanguage) {
|
|
164
|
+
return { language: matchingLanguage[1], file: null };
|
|
165
|
+
}
|
|
166
|
+
return { language: 'markdown' };
|
|
167
|
+
};
|
|
151
168
|
return [
|
|
169
|
+
// ```language + space — more specific, checked first
|
|
152
170
|
textblockTypeInputRule({
|
|
153
|
-
find: /^```([^\s`]+)
|
|
171
|
+
find: /^```([^\s`]+)\s$/,
|
|
154
172
|
type: this.type,
|
|
155
|
-
getAttributes: match =>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const ext = input.split('.').pop()?.toLowerCase() || '';
|
|
163
|
-
const lang = extOrLanguageToLanguageId[ext] || 'markdown';
|
|
164
|
-
return {
|
|
165
|
-
file: input,
|
|
166
|
-
language: lang,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
// Otherwise, check if it's a language name
|
|
170
|
-
const matchingLanguage = Object.entries(extOrLanguageToLanguageId).find(([ext, lang]) => {
|
|
171
|
-
return lang.includes(input) || ext === input;
|
|
172
|
-
});
|
|
173
|
-
if (matchingLanguage) {
|
|
174
|
-
return {
|
|
175
|
-
language: matchingLanguage[1],
|
|
176
|
-
file: null,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
// If no language match found, default to markdown
|
|
180
|
-
return { language: 'markdown' };
|
|
181
|
-
},
|
|
173
|
+
getAttributes: match => parseLanguageAttributes(match[1]?.trim()),
|
|
174
|
+
}),
|
|
175
|
+
// ``` alone — triggers immediately on the third backtick
|
|
176
|
+
textblockTypeInputRule({
|
|
177
|
+
find: /^```$/,
|
|
178
|
+
type: this.type,
|
|
179
|
+
getAttributes: () => ({ language: '' }),
|
|
182
180
|
}),
|
|
183
181
|
];
|
|
184
182
|
},
|
|
@@ -238,6 +236,14 @@ export const ExtendedCodeblock = Node.create({
|
|
|
238
236
|
main = state.doc.lineAt(main.head);
|
|
239
237
|
if (dir < 0 ? main.from > 0 : main.to < state.doc.length)
|
|
240
238
|
return false;
|
|
239
|
+
// ArrowUp from first line: focus toolbar instead of escaping to ProseMirror
|
|
240
|
+
if (dir < 0) {
|
|
241
|
+
const toolbarInput = cm.dom.querySelector('.cm-toolbar-input');
|
|
242
|
+
if (toolbarInput) {
|
|
243
|
+
toolbarInput.focus();
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
241
247
|
// @ts-ignore
|
|
242
248
|
let targetPos = getPos() + (dir < 0 ? 0 : node.nodeSize);
|
|
243
249
|
let selection = Selection.near(view.state.doc.resolve(targetPos), dir);
|
|
@@ -304,8 +310,34 @@ export const ExtendedCodeblock = Node.create({
|
|
|
304
310
|
// Reassign z-indexes for all codeblocks whenever a new one is created
|
|
305
311
|
// This ensures proper stacking order based on DOM position
|
|
306
312
|
reassignZIndexes();
|
|
307
|
-
//
|
|
308
|
-
|
|
313
|
+
// Handle ArrowUp from toolbar to escape to ProseMirror above
|
|
314
|
+
dom.addEventListener('keydown', (e) => {
|
|
315
|
+
const target = e.target;
|
|
316
|
+
if (target.classList.contains('cm-toolbar-input') && e.key === 'ArrowUp') {
|
|
317
|
+
// Check if the dropdown is open — if so, let toolbar handle it
|
|
318
|
+
const dropdown = dom.querySelector('.cm-search-results');
|
|
319
|
+
if (dropdown && dropdown.children.length > 0)
|
|
320
|
+
return;
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
e.stopPropagation();
|
|
323
|
+
// @ts-ignore
|
|
324
|
+
const pos = getPos();
|
|
325
|
+
if (pos !== undefined) {
|
|
326
|
+
let selection = Selection.near(view.state.doc.resolve(pos), -1);
|
|
327
|
+
let tr = view.state.tr.setSelection(selection).scrollIntoView();
|
|
328
|
+
view.dispatch(tr);
|
|
329
|
+
view.focus();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}, true);
|
|
333
|
+
// Track whether this codeblock was created empty (e.g. via ``` input rule)
|
|
334
|
+
const wasCreatedEmpty = !node.textContent && !node.attrs.file;
|
|
335
|
+
// Use the editor's filesystem if available (so codeblocks can resolve
|
|
336
|
+
// file references seeded into the same filesystem), otherwise fall back
|
|
337
|
+
// to a standalone worker.
|
|
338
|
+
const editorFs = editor.storage.persistence?.options?.fs;
|
|
339
|
+
const fsPromise = editorFs ? Promise.resolve(editorFs) : getFileSystemWorker();
|
|
340
|
+
fsPromise.then(fs => {
|
|
309
341
|
fsWorker = fs;
|
|
310
342
|
SearchIndex.get(fsWorker, '.codeblock/index.json').then(index => {
|
|
311
343
|
// Reconfigure with codeblock extension once fs is ready
|
|
@@ -325,6 +357,16 @@ export const ExtendedCodeblock = Node.create({
|
|
|
325
357
|
}),
|
|
326
358
|
]
|
|
327
359
|
}));
|
|
360
|
+
// If created via input rule (empty), focus toolbar and open dropdown
|
|
361
|
+
if (wasCreatedEmpty) {
|
|
362
|
+
requestAnimationFrame(() => {
|
|
363
|
+
const toolbarInput = cm.dom.querySelector('.cm-toolbar-input');
|
|
364
|
+
if (toolbarInput) {
|
|
365
|
+
toolbarInput.focus();
|
|
366
|
+
toolbarInput.click();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
328
370
|
});
|
|
329
371
|
}).catch(error => {
|
|
330
372
|
console.error('Failed to initialize filesystem worker:', error);
|
|
@@ -334,6 +376,15 @@ export const ExtendedCodeblock = Node.create({
|
|
|
334
376
|
return {
|
|
335
377
|
dom,
|
|
336
378
|
setSelection(anchor, head) {
|
|
379
|
+
// If the codeblock wasn't focused (entering from outside),
|
|
380
|
+
// direct to the toolbar input for keyboard navigation
|
|
381
|
+
if (!cm.hasFocus) {
|
|
382
|
+
const toolbarInput = cm.dom.querySelector('.cm-toolbar-input');
|
|
383
|
+
if (toolbarInput) {
|
|
384
|
+
toolbarInput.focus();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
337
388
|
cm.focus();
|
|
338
389
|
updating = true;
|
|
339
390
|
cm.dispatch({ selection: { anchor, head } });
|
package/dist/editor/index.js
CHANGED
|
@@ -11,6 +11,28 @@ import { ExtendedLink } from './extensions/link';
|
|
|
11
11
|
import { SlashCommands } from './extensions/slash-commands';
|
|
12
12
|
import { defaultSlashCommands } from './commands';
|
|
13
13
|
import { StyleModule } from 'style-mod';
|
|
14
|
+
// Override native caret blink speed on browsers that support caret-animation (Firefox 130+/Zen)
|
|
15
|
+
let caretBlinkInjected = false;
|
|
16
|
+
function injectCaretBlink() {
|
|
17
|
+
if (caretBlinkInjected)
|
|
18
|
+
return;
|
|
19
|
+
caretBlinkInjected = true;
|
|
20
|
+
const style = document.createElement('style');
|
|
21
|
+
style.textContent = `
|
|
22
|
+
@supports (caret-animation: manual) {
|
|
23
|
+
.ezco-mde .ProseMirror {
|
|
24
|
+
caret-animation: manual;
|
|
25
|
+
}
|
|
26
|
+
.ezco-mde .ProseMirror:focus {
|
|
27
|
+
animation: ezco-mde-caret-blink 1s step-end infinite;
|
|
28
|
+
}
|
|
29
|
+
@keyframes ezco-mde-caret-blink {
|
|
30
|
+
from, 50% { caret-color: currentColor; }
|
|
31
|
+
50.1%, to { caret-color: transparent; }
|
|
32
|
+
}
|
|
33
|
+
}`;
|
|
34
|
+
document.head.appendChild(style);
|
|
35
|
+
}
|
|
14
36
|
/**
|
|
15
37
|
* Create a Markdown-ready Tiptap Editor with default extensions and options.
|
|
16
38
|
*
|
|
@@ -63,6 +85,7 @@ export function createEditor(options = {}) {
|
|
|
63
85
|
editor.view.dom.classList.add('ezco-mde');
|
|
64
86
|
if (typeof document !== 'undefined') {
|
|
65
87
|
StyleModule.mount(document, styleModule);
|
|
88
|
+
injectCaretBlink();
|
|
66
89
|
}
|
|
67
90
|
return editor;
|
|
68
91
|
}
|
package/dist/editor/styles.js
CHANGED
|
@@ -17,6 +17,26 @@ export const styleModule = new StyleModule({
|
|
|
17
17
|
'--ezco-mde-bg-dark': '#1e1e1e',
|
|
18
18
|
'--ezco-mde-link-color': '#5861ff',
|
|
19
19
|
'--ezco-mde-link-color-hover': '#383ea3',
|
|
20
|
+
// Typography scale based on perfect fourth ratio (1.333)
|
|
21
|
+
'--ezco-mde-type-ratio': '1.25',
|
|
22
|
+
'--ezco-mde-base-font-size': '1rem',
|
|
23
|
+
'--ezco-mde-base-line-height': '1.5',
|
|
24
|
+
// Font sizes using modular scale
|
|
25
|
+
'--ezco-mde-text-xs': 'calc(var(--ezco-mde-base-font-size) / var(--ezco-mde-type-ratio))',
|
|
26
|
+
'--ezco-mde-text-sm': 'calc(var(--ezco-mde-text-xs) * var(--ezco-mde-type-ratio))',
|
|
27
|
+
'--ezco-mde-text-base': 'var(--ezco-mde-base-font-size)',
|
|
28
|
+
'--ezco-mde-text-lg': 'calc(var(--ezco-mde-text-base) * var(--ezco-mde-type-ratio))',
|
|
29
|
+
'--ezco-mde-text-xl': 'calc(var(--ezco-mde-text-lg) * var(--ezco-mde-type-ratio))',
|
|
30
|
+
'--ezco-mde-text-2xl': 'calc(var(--ezco-mde-text-xl) * var(--ezco-mde-type-ratio))',
|
|
31
|
+
'--ezco-mde-text-3xl': 'calc(var(--ezco-mde-text-2xl) * var(--ezco-mde-type-ratio))',
|
|
32
|
+
'--ezco-mde-text-4xl': 'calc(var(--ezco-mde-text-3xl) * var(--ezco-mde-type-ratio))',
|
|
33
|
+
// Line heights based on modular scale - inversely related to font size for better readability
|
|
34
|
+
'--ezco-mde-line-ratio': '1', // Smaller ratio for line height progression
|
|
35
|
+
'--ezco-mde-leading-loose': 'calc(var(--ezco-mde-base-line-height) * var(--ezco-mde-line-ratio))',
|
|
36
|
+
'--ezco-mde-leading-relaxed': 'var(--ezco-mde-base-line-height)',
|
|
37
|
+
'--ezco-mde-leading-normal': 'calc(var(--ezco-mde-base-line-height) / var(--ezco-mde-line-ratio))',
|
|
38
|
+
'--ezco-mde-leading-snug': 'calc(var(--ezco-mde-leading-normal) / var(--ezco-mde-line-ratio))',
|
|
39
|
+
'--ezco-mde-leading-tight': 'calc(var(--ezco-mde-leading-snug) / var(--ezco-mde-line-ratio))',
|
|
20
40
|
// Default to light mode, overridden by media query
|
|
21
41
|
'--ezco-mde-code-bg': 'var(--ezco-mde-code-bg-light)',
|
|
22
42
|
'--ezco-mde-bg': 'var(--ezco-mde-bg-light)',
|
|
@@ -33,6 +53,59 @@ export const styleModule = new StyleModule({
|
|
|
33
53
|
color: 'var(--ezco-mde-link-color-hover)',
|
|
34
54
|
cursor: 'pointer',
|
|
35
55
|
},
|
|
56
|
+
// Typography styles for Markdown elements using modular scale
|
|
57
|
+
'& h1': {
|
|
58
|
+
'font-size': 'var(--ezco-mde-text-4xl)',
|
|
59
|
+
'line-height': 'var(--ezco-mde-leading-tight)',
|
|
60
|
+
'margin': '0.67em 0',
|
|
61
|
+
'font-weight': 'bold',
|
|
62
|
+
},
|
|
63
|
+
'& h2': {
|
|
64
|
+
'font-size': 'var(--ezco-mde-text-3xl)',
|
|
65
|
+
'line-height': 'var(--ezco-mde-leading-tight)',
|
|
66
|
+
'margin': '0.75em 0 0.5em 0',
|
|
67
|
+
'font-weight': 'bold',
|
|
68
|
+
},
|
|
69
|
+
'& h3': {
|
|
70
|
+
'font-size': 'var(--ezco-mde-text-2xl)',
|
|
71
|
+
'line-height': 'var(--ezco-mde-leading-snug)',
|
|
72
|
+
'margin': '0.83em 0 0.5em 0',
|
|
73
|
+
'font-weight': 'bold',
|
|
74
|
+
},
|
|
75
|
+
'& h4': {
|
|
76
|
+
'font-size': 'var(--ezco-mde-text-xl)',
|
|
77
|
+
'line-height': 'var(--ezco-mde-leading-snug)',
|
|
78
|
+
'margin': '1em 0 0.5em 0',
|
|
79
|
+
'font-weight': 'bold',
|
|
80
|
+
},
|
|
81
|
+
'& h5': {
|
|
82
|
+
'font-size': 'var(--ezco-mde-text-lg)',
|
|
83
|
+
'line-height': 'var(--ezco-mde-leading-normal)',
|
|
84
|
+
'margin': '1.17em 0 0.5em 0',
|
|
85
|
+
'font-weight': 'bold',
|
|
86
|
+
},
|
|
87
|
+
'& h6': {
|
|
88
|
+
'font-size': 'var(--ezco-mde-text-base)',
|
|
89
|
+
'line-height': 'var(--ezco-mde-leading-normal)',
|
|
90
|
+
'margin': '1.33em 0 0.5em 0',
|
|
91
|
+
'font-weight': 'bold',
|
|
92
|
+
},
|
|
93
|
+
'& p': {
|
|
94
|
+
'font-size': 'var(--ezco-mde-text-base)',
|
|
95
|
+
'line-height': 'var(--ezco-mde-leading-relaxed)',
|
|
96
|
+
'margin': '1em 0',
|
|
97
|
+
},
|
|
98
|
+
'& blockquote': {
|
|
99
|
+
'font-size': 'var(--ezco-mde-text-base)',
|
|
100
|
+
'line-height': 'var(--ezco-mde-leading-relaxed)',
|
|
101
|
+
'margin': '1.5em 0',
|
|
102
|
+
'padding': '0 1em',
|
|
103
|
+
'border-left': '4px solid #ddd',
|
|
104
|
+
},
|
|
105
|
+
'& small': {
|
|
106
|
+
'font-size': 'var(--ezco-mde-text-sm)',
|
|
107
|
+
'line-height': 'var(--ezco-mde-leading-normal)',
|
|
108
|
+
},
|
|
36
109
|
// Codeblock styles
|
|
37
110
|
'& .cm-editor': {
|
|
38
111
|
margin: '2rem 0',
|
|
@@ -44,6 +117,8 @@ export const styleModule = new StyleModule({
|
|
|
44
117
|
background: 'var(--ezco-mde-code-bg)',
|
|
45
118
|
padding: '0.1em 0.3em',
|
|
46
119
|
'border-radius': '3px',
|
|
120
|
+
'-webkit-box-decoration-break': 'clone',
|
|
121
|
+
'box-decoration-break': 'clone',
|
|
47
122
|
},
|
|
48
123
|
// Table styles
|
|
49
124
|
'&.tableWrapper': {
|
|
@@ -113,6 +188,12 @@ export const styleModule = new StyleModule({
|
|
|
113
188
|
'& li > p': {
|
|
114
189
|
'margin-top': 0,
|
|
115
190
|
'margin-bottom': 0,
|
|
191
|
+
'font-size': 'var(--ezco-mde-text-base)',
|
|
192
|
+
'line-height': 'var(--ezco-mde-leading-relaxed)',
|
|
193
|
+
},
|
|
194
|
+
'& ol > li > p, & ul > li > p': {
|
|
195
|
+
'font-size': 'var(--ezco-mde-text-base)',
|
|
196
|
+
'line-height': 'var(--ezco-mde-leading-relaxed)',
|
|
116
197
|
},
|
|
117
198
|
// Task list styles
|
|
118
199
|
'& li[data-checked="true"]>div>p': {
|
|
@@ -149,5 +230,13 @@ export const styleModule = new StyleModule({
|
|
|
149
230
|
flex: 1
|
|
150
231
|
}
|
|
151
232
|
},
|
|
233
|
+
// Make task checkboxes visible when selected (Ctrl-A)
|
|
234
|
+
// Checkboxes don't natively show selection highlighting,
|
|
235
|
+
// so add an outline using the system Highlight color
|
|
236
|
+
'& ul[data-type="taskList"] li > label > input[type="checkbox"]': {
|
|
237
|
+
'&::selection': {
|
|
238
|
+
background: 'Highlight',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
152
241
|
}
|
|
153
242
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joinezco/markdown-editor",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@codemirror/state": "^6.5.2",
|
|
18
|
-
"@codemirror/view": "6.36.7",
|
|
18
|
+
"@codemirror/view": "^6.36.7",
|
|
19
19
|
"@tiptap/core": "^3.4.1",
|
|
20
20
|
"@tiptap/extension-link": "^3.4.1",
|
|
21
21
|
"@tiptap/extension-table": "^3.4.1",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"tippy.js": "^6.3.7",
|
|
36
36
|
"tiptap-markdown": "^0.8.10",
|
|
37
37
|
"vite-plugin-node-polyfills": "^0.24.0",
|
|
38
|
-
"@joinezco/codeblock": "0.0.
|
|
38
|
+
"@joinezco/codeblock": "0.0.10"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@eslint/js": "^9.22.0",
|
|
Binary file
|
package/public/snapshot.bin
CHANGED
|
Binary file
|
package/src/App.css
CHANGED
|
@@ -91,8 +91,6 @@ export const ExtendedCodeblock = Node.create({
|
|
|
91
91
|
// Render language back to HTML structure
|
|
92
92
|
renderHTML: attributes => {
|
|
93
93
|
|
|
94
|
-
console.log('renderHTML attributes', { attributes });
|
|
95
|
-
|
|
96
94
|
if (!attributes.language || attributes.language === 'plaintext') {
|
|
97
95
|
return {}; // No class needed for plaintext
|
|
98
96
|
}
|
|
@@ -172,41 +170,40 @@ export const ExtendedCodeblock = Node.create({
|
|
|
172
170
|
|
|
173
171
|
// Register input rules (e.g., ``` or ~~~ at the start of a line)
|
|
174
172
|
addInputRules() {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// If input contains a dot, treat it as a filename
|
|
186
|
-
if (input.includes('.')) {
|
|
187
|
-
const ext = input.split('.').pop()?.toLowerCase() || '';
|
|
188
|
-
const lang = extOrLanguageToLanguageId[ext as ExtensionOrLanguage] || 'markdown'
|
|
189
|
-
return {
|
|
190
|
-
file: input,
|
|
191
|
-
language: lang,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
173
|
+
const parseLanguageAttributes = (input: string | undefined) => {
|
|
174
|
+
if (!input) return { language: 'markdown' };
|
|
175
|
+
|
|
176
|
+
// If input contains a dot, treat it as a filename
|
|
177
|
+
if (input.includes('.')) {
|
|
178
|
+
const ext = input.split('.').pop()?.toLowerCase() || '';
|
|
179
|
+
const lang = extOrLanguageToLanguageId[ext as ExtensionOrLanguage] || 'markdown'
|
|
180
|
+
return { file: input, language: lang };
|
|
181
|
+
}
|
|
194
182
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
183
|
+
// Otherwise, check if it's a language name
|
|
184
|
+
const matchingLanguage = Object.entries(extOrLanguageToLanguageId).find(([ext, lang]) => {
|
|
185
|
+
return lang.includes(input) || ext === input;
|
|
186
|
+
})
|
|
199
187
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
file: null,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
188
|
+
if (matchingLanguage) {
|
|
189
|
+
return { language: matchingLanguage[1], file: null };
|
|
190
|
+
}
|
|
206
191
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
192
|
+
return { language: 'markdown' };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return [
|
|
196
|
+
// ```language + space — more specific, checked first
|
|
197
|
+
textblockTypeInputRule({
|
|
198
|
+
find: /^```([^\s`]+)\s$/,
|
|
199
|
+
type: this.type,
|
|
200
|
+
getAttributes: match => parseLanguageAttributes(match[1]?.trim()),
|
|
201
|
+
}),
|
|
202
|
+
// ``` alone — triggers immediately on the third backtick
|
|
203
|
+
textblockTypeInputRule({
|
|
204
|
+
find: /^```$/,
|
|
205
|
+
type: this.type,
|
|
206
|
+
getAttributes: () => ({ language: '' }),
|
|
210
207
|
}),
|
|
211
208
|
];
|
|
212
209
|
},
|
|
@@ -273,6 +270,16 @@ export const ExtendedCodeblock = Node.create({
|
|
|
273
270
|
if (!main.empty) return false
|
|
274
271
|
if (unit == "line") main = state.doc.lineAt(main.head)
|
|
275
272
|
if (dir < 0 ? main.from > 0 : main.to < state.doc.length) return false
|
|
273
|
+
|
|
274
|
+
// ArrowUp from first line: focus toolbar instead of escaping to ProseMirror
|
|
275
|
+
if (dir < 0) {
|
|
276
|
+
const toolbarInput = cm.dom.querySelector<HTMLInputElement>('.cm-toolbar-input');
|
|
277
|
+
if (toolbarInput) {
|
|
278
|
+
toolbarInput.focus();
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
276
283
|
// @ts-ignore
|
|
277
284
|
let targetPos = getPos() + (dir < 0 ? 0 : node.nodeSize)
|
|
278
285
|
let selection = Selection.near(view.state.doc.resolve(targetPos), dir)
|
|
@@ -346,8 +353,36 @@ export const ExtendedCodeblock = Node.create({
|
|
|
346
353
|
// This ensures proper stacking order based on DOM position
|
|
347
354
|
reassignZIndexes();
|
|
348
355
|
|
|
349
|
-
//
|
|
350
|
-
|
|
356
|
+
// Handle ArrowUp from toolbar to escape to ProseMirror above
|
|
357
|
+
dom.addEventListener('keydown', (e) => {
|
|
358
|
+
const target = e.target as HTMLElement;
|
|
359
|
+
if (target.classList.contains('cm-toolbar-input') && e.key === 'ArrowUp') {
|
|
360
|
+
// Check if the dropdown is open — if so, let toolbar handle it
|
|
361
|
+
const dropdown = dom.querySelector('.cm-search-results');
|
|
362
|
+
if (dropdown && dropdown.children.length > 0) return;
|
|
363
|
+
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
e.stopPropagation();
|
|
366
|
+
// @ts-ignore
|
|
367
|
+
const pos = getPos();
|
|
368
|
+
if (pos !== undefined) {
|
|
369
|
+
let selection = Selection.near(view.state.doc.resolve(pos), -1);
|
|
370
|
+
let tr = view.state.tr.setSelection(selection).scrollIntoView();
|
|
371
|
+
view.dispatch(tr);
|
|
372
|
+
view.focus();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}, true);
|
|
376
|
+
|
|
377
|
+
// Track whether this codeblock was created empty (e.g. via ``` input rule)
|
|
378
|
+
const wasCreatedEmpty = !node.textContent && !node.attrs.file;
|
|
379
|
+
|
|
380
|
+
// Use the editor's filesystem if available (so codeblocks can resolve
|
|
381
|
+
// file references seeded into the same filesystem), otherwise fall back
|
|
382
|
+
// to a standalone worker.
|
|
383
|
+
const editorFs = editor.storage.persistence?.options?.fs;
|
|
384
|
+
const fsPromise = editorFs ? Promise.resolve(editorFs) : getFileSystemWorker();
|
|
385
|
+
fsPromise.then(fs => {
|
|
351
386
|
fsWorker = fs;
|
|
352
387
|
SearchIndex.get(fsWorker, '.codeblock/index.json').then(index => {
|
|
353
388
|
// Reconfigure with codeblock extension once fs is ready
|
|
@@ -367,6 +402,17 @@ export const ExtendedCodeblock = Node.create({
|
|
|
367
402
|
}),
|
|
368
403
|
]
|
|
369
404
|
}));
|
|
405
|
+
|
|
406
|
+
// If created via input rule (empty), focus toolbar and open dropdown
|
|
407
|
+
if (wasCreatedEmpty) {
|
|
408
|
+
requestAnimationFrame(() => {
|
|
409
|
+
const toolbarInput = cm.dom.querySelector<HTMLInputElement>('.cm-toolbar-input');
|
|
410
|
+
if (toolbarInput) {
|
|
411
|
+
toolbarInput.focus();
|
|
412
|
+
toolbarInput.click();
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
370
416
|
})
|
|
371
417
|
}).catch(error => {
|
|
372
418
|
console.error('Failed to initialize filesystem worker:', error);
|
|
@@ -378,6 +424,15 @@ export const ExtendedCodeblock = Node.create({
|
|
|
378
424
|
return {
|
|
379
425
|
dom,
|
|
380
426
|
setSelection(anchor, head) {
|
|
427
|
+
// If the codeblock wasn't focused (entering from outside),
|
|
428
|
+
// direct to the toolbar input for keyboard navigation
|
|
429
|
+
if (!cm.hasFocus) {
|
|
430
|
+
const toolbarInput = cm.dom.querySelector<HTMLInputElement>('.cm-toolbar-input');
|
|
431
|
+
if (toolbarInput) {
|
|
432
|
+
toolbarInput.focus();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
381
436
|
cm.focus()
|
|
382
437
|
updating = true
|
|
383
438
|
cm.dispatch({ selection: { anchor, head } })
|
package/src/lib/editor/index.ts
CHANGED
|
@@ -13,6 +13,28 @@ import { SlashCommands } from './extensions/slash-commands';
|
|
|
13
13
|
import { defaultSlashCommands } from './commands';
|
|
14
14
|
import { StyleModule } from 'style-mod';
|
|
15
15
|
|
|
16
|
+
// Override native caret blink speed on browsers that support caret-animation (Firefox 130+/Zen)
|
|
17
|
+
let caretBlinkInjected = false;
|
|
18
|
+
function injectCaretBlink() {
|
|
19
|
+
if (caretBlinkInjected) return;
|
|
20
|
+
caretBlinkInjected = true;
|
|
21
|
+
const style = document.createElement('style');
|
|
22
|
+
style.textContent = `
|
|
23
|
+
@supports (caret-animation: manual) {
|
|
24
|
+
.ezco-mde .ProseMirror {
|
|
25
|
+
caret-animation: manual;
|
|
26
|
+
}
|
|
27
|
+
.ezco-mde .ProseMirror:focus {
|
|
28
|
+
animation: ezco-mde-caret-blink 1s step-end infinite;
|
|
29
|
+
}
|
|
30
|
+
@keyframes ezco-mde-caret-blink {
|
|
31
|
+
from, 50% { caret-color: currentColor; }
|
|
32
|
+
50.1%, to { caret-color: transparent; }
|
|
33
|
+
}
|
|
34
|
+
}`;
|
|
35
|
+
document.head.appendChild(style);
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
export type MarkdownEditorOptions = Partial<EditorOptions> & {
|
|
17
39
|
extensions?: Extension[];
|
|
18
40
|
fs?: FileSystemOptions;
|
|
@@ -77,6 +99,7 @@ export function createEditor(options: MarkdownEditorOptions = {}): MarkdownEdito
|
|
|
77
99
|
|
|
78
100
|
if (typeof document !== 'undefined') {
|
|
79
101
|
StyleModule.mount(document, styleModule);
|
|
102
|
+
injectCaretBlink();
|
|
80
103
|
}
|
|
81
104
|
return editor as MarkdownEditor;
|
|
82
105
|
}
|