@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.
Files changed (60) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/dist/editor/extensions/codeblock.js +82 -31
  3. package/dist/editor/index.js +23 -0
  4. package/dist/editor/styles.js +89 -0
  5. package/package.json +3 -3
  6. package/public/fonts/UbuntuMonoNerdFont-Regular.ttf +0 -0
  7. package/public/snapshot.bin +0 -0
  8. package/src/App.css +0 -1
  9. package/src/lib/editor/extensions/codeblock.ts +91 -36
  10. package/src/lib/editor/index.ts +23 -0
  11. package/src/lib/editor/styles.ts +93 -0
  12. package/src/test/multiview-sync.test.ts +137 -0
  13. package/TEST_README.md +0 -359
  14. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-be-focusable-1.png +0 -0
  15. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-create-an-editor-instance-1.png +0 -0
  16. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-have-initial-content-1.png +0 -0
  17. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-render-in-the-DOM-1.png +0 -0
  18. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Browser-specific-Features-should-handle-copy-and-paste-operations-1.png +0 -0
  19. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Browser-specific-Features-should-handle-undo-and-redo-1.png +0 -0
  20. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Editor-State-and-Updates-should-maintain-state-across-content-changes-1.png +0 -0
  21. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Editor-State-and-Updates-should-trigger-update-callbacks-1.png +0 -0
  22. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Error-Handling-should-handle-invalid-markdown-gracefully-1.png +0 -0
  23. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Error-Handling-should-handle-very-long-content-1.png +0 -0
  24. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Keyboard-Shortcuts-should-handle-Ctrl-B-for-bold-1.png +0 -0
  25. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Keyboard-Shortcuts-should-handle-Ctrl-I-for-italic-1.png +0 -0
  26. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-get-markdown-content-1.png +0 -0
  27. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-handle-empty-content-1.png +0 -0
  28. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-set-markdown-content-1.png +0 -0
  29. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-bold-text-1.png +0 -0
  30. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-code-blocks-1.png +0 -0
  31. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-headings-1.png +0 -0
  32. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-inline-code-1.png +0 -0
  33. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-italic-text-1.png +0 -0
  34. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-lists-1.png +0 -0
  35. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-task-lists-1.png +0 -0
  36. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Selection-and-Cursor-Management-should-handle-cursor-positioning-1.png +0 -0
  37. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Selection-and-Cursor-Management-should-set-and-get-selection-1.png +0 -0
  38. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-handle-line-breaks-1.png +0 -0
  39. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-handle-typing-at-different-positions-1.png +0 -0
  40. package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-insert-text-at-cursor-position-1.png +0 -0
  41. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-code-blocks-without-language-specification-1.png +0 -0
  42. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-different-programming-languages-1.png +0 -0
  43. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-inline-code-1.png +0 -0
  44. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-render-code-blocks-with-syntax-highlighting-1.png +0 -0
  45. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Extension-Interactions-should-handle-multiple-extensions-working-together-1.png +0 -0
  46. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Extension-Interactions-should-maintain-editor-state-across-complex-operations-1.png +0 -0
  47. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-File-System-Integration-should-handle-file-references-in-code-blocks-1.png +0 -0
  48. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-File-System-Integration-should-maintain-file-system-state-1.png +0 -0
  49. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-auto-detect-URLs-1.png +0 -0
  50. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-handle-email-links-1.png +0 -0
  51. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-render-links-correctly-1.png +0 -0
  52. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Markdown-Storage-should-provide-markdown-storage-interface-1.png +0 -0
  53. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Markdown-Storage-should-sync-markdown-content-with-storage-1.png +0 -0
  54. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Slash-Commands-Extension-should-handle-heading-commands-1.png +0 -0
  55. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Slash-Commands-Extension-should-trigger-slash-commands-1.png +0 -0
  56. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Table-Extension-should-handle-table-navigation-1.png +0 -0
  57. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Table-Extension-should-render-tables-correctly-1.png +0 -0
  58. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-handle-nested-task-lists-1.png +0 -0
  59. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-render-task-lists-correctly-1.png +0 -0
  60. package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-toggle-task-completion-1.png +0 -0
@@ -0,0 +1,4 @@
1
+
2
+ > @joinezco/markdown-editor@0.0.3 build /home/theo/dev/mono/src/typescript/markdown-editor
3
+ > tsc -p tsconfig.lib.json
4
+
@@ -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`]+)?\s$/,
171
+ find: /^```([^\s`]+)\s$/,
154
172
  type: this.type,
155
- getAttributes: match => {
156
- const input = match[1]?.trim();
157
- console.log('input', { input });
158
- if (!input)
159
- return { language: 'markdown' };
160
- // If input contains a dot, treat it as a filename
161
- if (input.includes('.')) {
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
- // Initialize filesystem worker and update extensions asynchronously
308
- getFileSystemWorker().then(fs => {
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 } });
@@ -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
  }
@@ -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.1",
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.8"
38
+ "@joinezco/codeblock": "0.0.10"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@eslint/js": "^9.22.0",
Binary file
package/src/App.css CHANGED
@@ -133,7 +133,6 @@
133
133
  .settings-header {
134
134
  display: flex;
135
135
  align-items: center;
136
- justify-content: space-between;
137
136
  padding: 20px 24px;
138
137
  border-bottom: 1px solid #e2e8f0;
139
138
  }
@@ -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
- return [
176
- textblockTypeInputRule({
177
- find: /^```([^\s`]+)?\s$/,
178
- type: this.type,
179
- getAttributes: match => {
180
- const input = match[1]?.trim();
181
- console.log('input', { input });
182
-
183
- if (!input) return { language: 'markdown' };
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
- // Otherwise, check if it's a language name
196
- const matchingLanguage = Object.entries(extOrLanguageToLanguageId).find(([ext, lang]) => {
197
- return lang.includes(input) || ext === input;
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
- if (matchingLanguage) {
201
- return {
202
- language: matchingLanguage[1],
203
- file: null,
204
- };
205
- }
188
+ if (matchingLanguage) {
189
+ return { language: matchingLanguage[1], file: null };
190
+ }
206
191
 
207
- // If no language match found, default to markdown
208
- return { language: 'markdown' };
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
- // Initialize filesystem worker and update extensions asynchronously
350
- getFileSystemWorker().then(fs => {
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 } })
@@ -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
  }