@jvs-milkdown/components 1.0.0

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 (192) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/lib/__internal__/components/icon.d.ts +24 -0
  4. package/lib/__internal__/components/icon.d.ts.map +1 -0
  5. package/lib/__internal__/components/image-input.d.ts +17 -0
  6. package/lib/__internal__/components/image-input.d.ts.map +1 -0
  7. package/lib/__internal__/keep-alive.d.ts +2 -0
  8. package/lib/__internal__/keep-alive.d.ts.map +1 -0
  9. package/lib/__internal__/meta.d.ts +3 -0
  10. package/lib/__internal__/meta.d.ts.map +1 -0
  11. package/lib/__tests__/setup.d.ts +2 -0
  12. package/lib/__tests__/setup.d.ts.map +1 -0
  13. package/lib/code-block/config.d.ts +23 -0
  14. package/lib/code-block/config.d.ts.map +1 -0
  15. package/lib/code-block/index.d.ts +5 -0
  16. package/lib/code-block/index.d.ts.map +1 -0
  17. package/lib/code-block/index.js +4160 -0
  18. package/lib/code-block/index.js.map +1 -0
  19. package/lib/code-block/view/components/code-block.d.ts +16 -0
  20. package/lib/code-block/view/components/code-block.d.ts.map +1 -0
  21. package/lib/code-block/view/components/copy-button.d.ts +9 -0
  22. package/lib/code-block/view/components/copy-button.d.ts.map +1 -0
  23. package/lib/code-block/view/components/language-picker.d.ts +5 -0
  24. package/lib/code-block/view/components/language-picker.d.ts.map +1 -0
  25. package/lib/code-block/view/components/preview-panel.d.ts +9 -0
  26. package/lib/code-block/view/components/preview-panel.d.ts.map +1 -0
  27. package/lib/code-block/view/index.d.ts +3 -0
  28. package/lib/code-block/view/index.d.ts.map +1 -0
  29. package/lib/code-block/view/loader.d.ts +13 -0
  30. package/lib/code-block/view/loader.d.ts.map +1 -0
  31. package/lib/code-block/view/node-view.d.ts +40 -0
  32. package/lib/code-block/view/node-view.d.ts.map +1 -0
  33. package/lib/image-block/config.d.ts +16 -0
  34. package/lib/image-block/config.d.ts.map +1 -0
  35. package/lib/image-block/index.d.ts +7 -0
  36. package/lib/image-block/index.d.ts.map +1 -0
  37. package/lib/image-block/index.js +660 -0
  38. package/lib/image-block/index.js.map +1 -0
  39. package/lib/image-block/remark-plugin.d.ts +2 -0
  40. package/lib/image-block/remark-plugin.d.ts.map +1 -0
  41. package/lib/image-block/schema.d.ts +3 -0
  42. package/lib/image-block/schema.d.ts.map +1 -0
  43. package/lib/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.d.ts +2 -0
  44. package/lib/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.d.ts.map +1 -0
  45. package/lib/image-block/view/components/image-block.d.ts +18 -0
  46. package/lib/image-block/view/components/image-block.d.ts.map +1 -0
  47. package/lib/image-block/view/components/image-viewer.d.ts +3 -0
  48. package/lib/image-block/view/components/image-viewer.d.ts.map +1 -0
  49. package/lib/image-block/view/index.d.ts +3 -0
  50. package/lib/image-block/view/index.d.ts.map +1 -0
  51. package/lib/image-inline/components/image-inline.d.ts +18 -0
  52. package/lib/image-inline/components/image-inline.d.ts.map +1 -0
  53. package/lib/image-inline/config.d.ts +11 -0
  54. package/lib/image-inline/config.d.ts.map +1 -0
  55. package/lib/image-inline/index.d.ts +5 -0
  56. package/lib/image-inline/index.d.ts.map +1 -0
  57. package/lib/image-inline/index.js +377 -0
  58. package/lib/image-inline/index.js.map +1 -0
  59. package/lib/image-inline/view.d.ts +3 -0
  60. package/lib/image-inline/view.d.ts.map +1 -0
  61. package/lib/index.d.ts +2 -0
  62. package/lib/index.d.ts.map +1 -0
  63. package/lib/index.js +35 -0
  64. package/lib/index.js.map +1 -0
  65. package/lib/link-tooltip/command.d.ts +2 -0
  66. package/lib/link-tooltip/command.d.ts.map +1 -0
  67. package/lib/link-tooltip/configure.d.ts +3 -0
  68. package/lib/link-tooltip/configure.d.ts.map +1 -0
  69. package/lib/link-tooltip/edit/component.d.ts +11 -0
  70. package/lib/link-tooltip/edit/component.d.ts.map +1 -0
  71. package/lib/link-tooltip/edit/edit-configure.d.ts +3 -0
  72. package/lib/link-tooltip/edit/edit-configure.d.ts.map +1 -0
  73. package/lib/link-tooltip/edit/edit-view.d.ts +15 -0
  74. package/lib/link-tooltip/edit/edit-view.d.ts.map +1 -0
  75. package/lib/link-tooltip/index.d.ts +7 -0
  76. package/lib/link-tooltip/index.d.ts.map +1 -0
  77. package/lib/link-tooltip/index.js +2526 -0
  78. package/lib/link-tooltip/index.js.map +1 -0
  79. package/lib/link-tooltip/preview/component.d.ts +11 -0
  80. package/lib/link-tooltip/preview/component.d.ts.map +1 -0
  81. package/lib/link-tooltip/preview/preview-configure.d.ts +3 -0
  82. package/lib/link-tooltip/preview/preview-configure.d.ts.map +1 -0
  83. package/lib/link-tooltip/preview/preview-view.d.ts +14 -0
  84. package/lib/link-tooltip/preview/preview-view.d.ts.map +1 -0
  85. package/lib/link-tooltip/slices.d.ts +34 -0
  86. package/lib/link-tooltip/slices.d.ts.map +1 -0
  87. package/lib/link-tooltip/tooltips.d.ts +3 -0
  88. package/lib/link-tooltip/tooltips.d.ts.map +1 -0
  89. package/lib/link-tooltip/utils.d.ts +14 -0
  90. package/lib/link-tooltip/utils.d.ts.map +1 -0
  91. package/lib/list-item-block/component.d.ts +19 -0
  92. package/lib/list-item-block/component.d.ts.map +1 -0
  93. package/lib/list-item-block/config.d.ts +13 -0
  94. package/lib/list-item-block/config.d.ts.map +1 -0
  95. package/lib/list-item-block/index.d.ts +6 -0
  96. package/lib/list-item-block/index.d.ts.map +1 -0
  97. package/lib/list-item-block/index.js +2149 -0
  98. package/lib/list-item-block/index.js.map +1 -0
  99. package/lib/list-item-block/view.d.ts +3 -0
  100. package/lib/list-item-block/view.d.ts.map +1 -0
  101. package/lib/table-block/config.d.ts +8 -0
  102. package/lib/table-block/config.d.ts.map +1 -0
  103. package/lib/table-block/dnd/calc-drag-over.d.ts +3 -0
  104. package/lib/table-block/dnd/calc-drag-over.d.ts.map +1 -0
  105. package/lib/table-block/dnd/create-drag-handler.d.ts +5 -0
  106. package/lib/table-block/dnd/create-drag-handler.d.ts.map +1 -0
  107. package/lib/table-block/dnd/drag-over-handler.d.ts +3 -0
  108. package/lib/table-block/dnd/drag-over-handler.d.ts.map +1 -0
  109. package/lib/table-block/dnd/prepare-dnd-context.d.ts +3 -0
  110. package/lib/table-block/dnd/prepare-dnd-context.d.ts.map +1 -0
  111. package/lib/table-block/dnd/preview.d.ts +3 -0
  112. package/lib/table-block/dnd/preview.d.ts.map +1 -0
  113. package/lib/table-block/index.d.ts +5 -0
  114. package/lib/table-block/index.d.ts.map +1 -0
  115. package/lib/table-block/index.js +3961 -0
  116. package/lib/table-block/index.js.map +1 -0
  117. package/lib/table-block/view/component.d.ts +16 -0
  118. package/lib/table-block/view/component.d.ts.map +1 -0
  119. package/lib/table-block/view/drag.d.ts +7 -0
  120. package/lib/table-block/view/drag.d.ts.map +1 -0
  121. package/lib/table-block/view/index.d.ts +2 -0
  122. package/lib/table-block/view/index.d.ts.map +1 -0
  123. package/lib/table-block/view/operation.d.ts +11 -0
  124. package/lib/table-block/view/operation.d.ts.map +1 -0
  125. package/lib/table-block/view/pointer.d.ts +7 -0
  126. package/lib/table-block/view/pointer.d.ts.map +1 -0
  127. package/lib/table-block/view/types.d.ts +32 -0
  128. package/lib/table-block/view/types.d.ts.map +1 -0
  129. package/lib/table-block/view/utils.d.ts +21 -0
  130. package/lib/table-block/view/utils.d.ts.map +1 -0
  131. package/lib/table-block/view/view.d.ts +22 -0
  132. package/lib/table-block/view/view.d.ts.map +1 -0
  133. package/lib/tsconfig.tsbuildinfo +1 -0
  134. package/package.json +110 -0
  135. package/src/__internal__/components/icon.tsx +38 -0
  136. package/src/__internal__/components/image-input.tsx +182 -0
  137. package/src/__internal__/keep-alive.ts +3 -0
  138. package/src/__internal__/meta.ts +15 -0
  139. package/src/__tests__/setup.ts +6 -0
  140. package/src/code-block/config.ts +54 -0
  141. package/src/code-block/index.ts +12 -0
  142. package/src/code-block/view/components/code-block.tsx +170 -0
  143. package/src/code-block/view/components/copy-button.tsx +96 -0
  144. package/src/code-block/view/components/language-picker.tsx +239 -0
  145. package/src/code-block/view/components/preview-panel.tsx +79 -0
  146. package/src/code-block/view/index.ts +24 -0
  147. package/src/code-block/view/loader.ts +40 -0
  148. package/src/code-block/view/node-view.ts +310 -0
  149. package/src/image-block/config.ts +37 -0
  150. package/src/image-block/index.ts +18 -0
  151. package/src/image-block/remark-plugin.ts +51 -0
  152. package/src/image-block/schema.ts +71 -0
  153. package/src/image-block/view/components/__tests__/image-viewer.onImageLoadError.spec.tsx +42 -0
  154. package/src/image-block/view/components/image-block.tsx +80 -0
  155. package/src/image-block/view/components/image-viewer.tsx +186 -0
  156. package/src/image-block/view/index.ts +111 -0
  157. package/src/image-inline/components/image-inline.tsx +85 -0
  158. package/src/image-inline/config.ts +30 -0
  159. package/src/image-inline/index.ts +12 -0
  160. package/src/image-inline/view.ts +109 -0
  161. package/src/index.ts +1 -0
  162. package/src/link-tooltip/command.ts +19 -0
  163. package/src/link-tooltip/configure.ts +9 -0
  164. package/src/link-tooltip/edit/component.tsx +82 -0
  165. package/src/link-tooltip/edit/edit-configure.ts +29 -0
  166. package/src/link-tooltip/edit/edit-view.ts +165 -0
  167. package/src/link-tooltip/index.ts +19 -0
  168. package/src/link-tooltip/preview/component.tsx +87 -0
  169. package/src/link-tooltip/preview/preview-configure.ts +65 -0
  170. package/src/link-tooltip/preview/preview-view.ts +101 -0
  171. package/src/link-tooltip/slices.ts +69 -0
  172. package/src/link-tooltip/tooltips.ts +22 -0
  173. package/src/link-tooltip/utils.ts +56 -0
  174. package/src/list-item-block/component.tsx +133 -0
  175. package/src/list-item-block/config.ts +39 -0
  176. package/src/list-item-block/index.ts +13 -0
  177. package/src/list-item-block/view.ts +130 -0
  178. package/src/table-block/config.ts +53 -0
  179. package/src/table-block/dnd/calc-drag-over.ts +46 -0
  180. package/src/table-block/dnd/create-drag-handler.ts +99 -0
  181. package/src/table-block/dnd/drag-over-handler.ts +113 -0
  182. package/src/table-block/dnd/prepare-dnd-context.ts +46 -0
  183. package/src/table-block/dnd/preview.ts +58 -0
  184. package/src/table-block/index.ts +9 -0
  185. package/src/table-block/view/component.tsx +219 -0
  186. package/src/table-block/view/drag.ts +121 -0
  187. package/src/table-block/view/index.ts +1 -0
  188. package/src/table-block/view/operation.ts +148 -0
  189. package/src/table-block/view/pointer.ts +165 -0
  190. package/src/table-block/view/types.ts +35 -0
  191. package/src/table-block/view/utils.ts +192 -0
  192. package/src/table-block/view/view.ts +165 -0
@@ -0,0 +1,239 @@
1
+ import { computePosition } from '@floating-ui/dom'
2
+ import clsx from 'clsx'
3
+ import {
4
+ computed,
5
+ defineComponent,
6
+ ref,
7
+ h,
8
+ Fragment,
9
+ onMounted,
10
+ onUnmounted,
11
+ watch,
12
+ } from 'vue'
13
+
14
+ import type { CodeBlockProps } from './code-block'
15
+
16
+ import { Icon } from '../../../__internal__/components/icon'
17
+ import { keepAlive } from '../../../__internal__/keep-alive'
18
+
19
+ keepAlive(h, Fragment)
20
+
21
+ type LanguagePickerProps = Pick<
22
+ CodeBlockProps,
23
+ 'language' | 'config' | 'setLanguage' | 'getAllLanguages' | 'getReadOnly'
24
+ >
25
+
26
+ export const LanguagePicker = defineComponent<LanguagePickerProps>({
27
+ props: {
28
+ language: {
29
+ type: Object,
30
+ required: true,
31
+ },
32
+ getReadOnly: {
33
+ type: Function,
34
+ required: true,
35
+ },
36
+ config: {
37
+ type: Object,
38
+ required: true,
39
+ },
40
+ getAllLanguages: {
41
+ type: Function,
42
+ required: true,
43
+ },
44
+ setLanguage: {
45
+ type: Function,
46
+ required: true,
47
+ },
48
+ },
49
+ setup({ language, config, setLanguage, getAllLanguages, getReadOnly }) {
50
+ const triggerRef = ref<HTMLButtonElement>()
51
+ const showPicker = ref(false)
52
+ const searchRef = ref<HTMLInputElement>()
53
+ const pickerRef = ref<HTMLDivElement>()
54
+ const filter = ref('')
55
+
56
+ watch([showPicker, triggerRef, pickerRef], () => {
57
+ filter.value = ''
58
+ const picker = triggerRef.value
59
+ const languageList = pickerRef.value
60
+ if (!picker || !languageList) return
61
+
62
+ computePosition(picker, languageList, {
63
+ placement: 'bottom-start',
64
+ })
65
+ .then(({ x, y }) => {
66
+ Object.assign(languageList.style, {
67
+ left: `${x}px`,
68
+ top: `${y}px`,
69
+ })
70
+ })
71
+ .catch(console.error)
72
+ })
73
+
74
+ const onTogglePicker = (e: Event) => {
75
+ e.preventDefault()
76
+ e.stopPropagation()
77
+ if (getReadOnly()) return
78
+
79
+ const next = !showPicker.value
80
+ showPicker.value = next
81
+ if (next) {
82
+ setTimeout(() => searchRef.value?.focus(), 0)
83
+ }
84
+ }
85
+
86
+ const changeFilter = (e: Event) => {
87
+ const target = e.target as HTMLInputElement
88
+ filter.value = target.value
89
+ }
90
+
91
+ const onSearchKeydown = (e: Event) => {
92
+ if ((e as KeyboardEvent).key === 'Escape') filter.value = ''
93
+ }
94
+
95
+ const languages = computed(() => {
96
+ if (!showPicker.value) return []
97
+
98
+ const all = getAllLanguages() ?? []
99
+
100
+ const selected = all.find(
101
+ (languageInfo) =>
102
+ languageInfo.name.toLowerCase() === language.value.toLowerCase()
103
+ )
104
+
105
+ const filtered = all.filter((languageInfo) => {
106
+ const currentValue = filter.value.toLowerCase()
107
+
108
+ return (
109
+ (languageInfo.name.toLowerCase().includes(currentValue) ||
110
+ languageInfo.alias.some((alias) =>
111
+ alias.toLowerCase().includes(currentValue)
112
+ )) &&
113
+ languageInfo !== selected
114
+ )
115
+ })
116
+
117
+ if (filtered.length === 0) return []
118
+
119
+ if (!selected) return filtered
120
+
121
+ return [selected, ...filtered]
122
+ })
123
+
124
+ const clickHandler = (e: MouseEvent) => {
125
+ const target = e.target as HTMLElement
126
+
127
+ if (triggerRef.value && triggerRef.value.contains(target)) return
128
+
129
+ const picker = pickerRef.value
130
+ const trigger = triggerRef.value
131
+ if (!trigger || !picker) return
132
+
133
+ if (trigger.dataset.expanded !== 'true') return
134
+
135
+ if (!picker.contains(target)) showPicker.value = false
136
+ }
137
+
138
+ onMounted(() => {
139
+ window.addEventListener('click', clickHandler)
140
+ })
141
+
142
+ onUnmounted(() => {
143
+ window.removeEventListener('click', clickHandler)
144
+ })
145
+
146
+ return () => {
147
+ return (
148
+ <>
149
+ <button
150
+ type="button"
151
+ ref={triggerRef}
152
+ class="language-button"
153
+ onClick={onTogglePicker}
154
+ data-expanded={String(showPicker.value)}
155
+ >
156
+ {language.value || 'Text'}
157
+ <div class="expand-icon">
158
+ <Icon icon={config.expandIcon} />
159
+ </div>
160
+ </button>
161
+ <div ref={pickerRef} class="language-picker">
162
+ {showPicker.value ? (
163
+ <div class="list-wrapper">
164
+ <div class="search-box">
165
+ <div class="search-icon">
166
+ <Icon icon={config.searchIcon} />
167
+ </div>
168
+ <input
169
+ ref={searchRef}
170
+ class="search-input"
171
+ placeholder={config.searchPlaceholder}
172
+ value={filter.value}
173
+ onInput={changeFilter}
174
+ onKeydown={onSearchKeydown}
175
+ />
176
+ <div
177
+ class={clsx(
178
+ 'clear-icon',
179
+ filter.value.length === 0 && 'hidden'
180
+ )}
181
+ onMousedown={(e) => {
182
+ e.preventDefault()
183
+ filter.value = ''
184
+ }}
185
+ >
186
+ <Icon icon={config.clearSearchIcon} />
187
+ </div>
188
+ </div>
189
+ <ul
190
+ class="language-list"
191
+ role="listbox"
192
+ onKeydown={(e) => {
193
+ if (e.key === 'Enter') {
194
+ const active = document.activeElement
195
+ if (
196
+ active instanceof HTMLElement &&
197
+ active.dataset.language
198
+ )
199
+ setLanguage(active.dataset.language)
200
+ }
201
+ }}
202
+ >
203
+ {!languages.value.length ? (
204
+ <li class="language-list-item no-result">
205
+ {config.noResultText}
206
+ </li>
207
+ ) : (
208
+ languages.value.map((languageInfo) => (
209
+ <li
210
+ role="listitem"
211
+ tabindex="0"
212
+ class="language-list-item"
213
+ aria-selected={
214
+ languageInfo.name.toLowerCase() ===
215
+ language.value.toLowerCase()
216
+ }
217
+ data-language={languageInfo.name}
218
+ onClick={() => {
219
+ setLanguage(languageInfo.name)
220
+ showPicker.value = false
221
+ }}
222
+ >
223
+ {config.renderLanguage(
224
+ languageInfo.name,
225
+ languageInfo.name.toLowerCase() ===
226
+ language.value.toLowerCase()
227
+ )}
228
+ </li>
229
+ ))
230
+ )}
231
+ </ul>
232
+ </div>
233
+ ) : null}
234
+ </div>
235
+ </>
236
+ )
237
+ }
238
+ },
239
+ })
@@ -0,0 +1,79 @@
1
+ import DOMPurify from 'dompurify'
2
+ import { defineComponent, ref, watchEffect, type Ref, h, Fragment } from 'vue'
3
+
4
+ import type { CodeBlockProps } from './code-block'
5
+
6
+ import { keepAlive } from '../../../__internal__/keep-alive'
7
+
8
+ keepAlive(h, Fragment)
9
+
10
+ type PreviewPanelProps = Pick<
11
+ CodeBlockProps,
12
+ 'text' | 'language' | 'config'
13
+ > & {
14
+ previewOnlyMode: Ref<boolean>
15
+ preview: Ref<string | HTMLElement | null>
16
+ }
17
+
18
+ export const PreviewPanel = defineComponent<PreviewPanelProps>({
19
+ props: {
20
+ text: {
21
+ type: Object,
22
+ required: true,
23
+ },
24
+ language: {
25
+ type: Object,
26
+ required: true,
27
+ },
28
+ config: {
29
+ type: Object,
30
+ required: true,
31
+ },
32
+ previewOnlyMode: {
33
+ type: Object,
34
+ required: true,
35
+ },
36
+ preview: {
37
+ type: Object,
38
+ required: true,
39
+ },
40
+ },
41
+ setup(props) {
42
+ const { previewOnlyMode, config, preview } = props
43
+ const previewRef = ref<HTMLDivElement>()
44
+
45
+ watchEffect(() => {
46
+ const previewContainer = previewRef.value
47
+ if (!previewContainer) return
48
+
49
+ while (previewContainer.firstChild) {
50
+ previewContainer.removeChild(previewContainer.firstChild)
51
+ }
52
+
53
+ const previewContent = preview.value
54
+
55
+ if (
56
+ typeof previewContent === 'string' ||
57
+ previewContent instanceof Element
58
+ ) {
59
+ previewContainer.innerHTML = DOMPurify.sanitize(previewContent)
60
+ }
61
+ })
62
+
63
+ return () => {
64
+ if (!preview.value) return null
65
+
66
+ return (
67
+ <div class="preview-panel">
68
+ {!previewOnlyMode.value && (
69
+ <>
70
+ <div class="preview-divider" />
71
+ <div class="preview-label">{config.previewLabel}</div>
72
+ </>
73
+ )}
74
+ <div ref={previewRef} class="preview" />
75
+ </div>
76
+ )
77
+ }
78
+ },
79
+ })
@@ -0,0 +1,24 @@
1
+ import type { NodeViewConstructor } from '@jvs-milkdown/prose/view'
2
+
3
+ import { codeBlockSchema } from '@jvs-milkdown/preset-commonmark'
4
+ import { $view } from '@jvs-milkdown/utils'
5
+
6
+ import { withMeta } from '../../__internal__/meta'
7
+ import { codeBlockConfig } from '../config'
8
+ import { LanguageLoader } from './loader'
9
+ import { CodeMirrorBlock } from './node-view'
10
+
11
+ export const codeBlockView = $view(
12
+ codeBlockSchema.node,
13
+ (ctx): NodeViewConstructor => {
14
+ const config = ctx.get(codeBlockConfig.key)
15
+ const languageLoader = new LanguageLoader(config.languages)
16
+ return (node, view, getPos) =>
17
+ new CodeMirrorBlock(node, view, getPos, languageLoader, config)
18
+ }
19
+ )
20
+
21
+ withMeta(codeBlockView, {
22
+ displayName: 'NodeView<code-block>',
23
+ group: 'CodeBlock',
24
+ })
@@ -0,0 +1,40 @@
1
+ import type { LanguageDescription, LanguageSupport } from '@codemirror/language'
2
+
3
+ export interface LanguageInfo {
4
+ name: string
5
+ alias: readonly string[]
6
+ }
7
+
8
+ export class LanguageLoader {
9
+ private readonly map: Record<string, LanguageDescription>
10
+
11
+ constructor(private languages: LanguageDescription[]) {
12
+ this.map = {}
13
+
14
+ languages.forEach((language) => {
15
+ language.alias.forEach((alias) => {
16
+ this.map[alias] = language
17
+ })
18
+ })
19
+ }
20
+
21
+ getAll(): LanguageInfo[] {
22
+ return this.languages.map((language): LanguageInfo => {
23
+ return {
24
+ name: language.name,
25
+ alias: language.alias,
26
+ }
27
+ })
28
+ }
29
+
30
+ load(languageName: string): Promise<LanguageSupport | undefined> {
31
+ const languageMap = this.map
32
+ const language = languageMap[languageName.toLowerCase()]
33
+
34
+ if (!language) return Promise.resolve(undefined)
35
+
36
+ if (language.support) return Promise.resolve(language.support)
37
+
38
+ return language.load()
39
+ }
40
+ }
@@ -0,0 +1,310 @@
1
+ import type { Line, SelectionRange } from '@codemirror/state'
2
+ import type { Node } from '@jvs-milkdown/prose/model'
3
+ import type { EditorView, NodeView } from '@jvs-milkdown/prose/view'
4
+
5
+ import { Compartment, EditorState } from '@codemirror/state'
6
+ import {
7
+ EditorView as CodeMirror,
8
+ type KeyBinding,
9
+ type ViewUpdate,
10
+ keymap as cmKeymap,
11
+ drawSelection,
12
+ } from '@codemirror/view'
13
+ import { exitCode } from '@jvs-milkdown/prose/commands'
14
+ import { redo, undo } from '@jvs-milkdown/prose/history'
15
+ import { TextSelection } from '@jvs-milkdown/prose/state'
16
+ import { createApp, ref, watchEffect, type App, type WatchHandle } from 'vue'
17
+
18
+ import type { CodeBlockConfig } from '../config'
19
+ import type { LanguageLoader } from './loader'
20
+
21
+ import { CodeBlock } from './components/code-block'
22
+
23
+ export class CodeMirrorBlock implements NodeView {
24
+ dom: HTMLElement
25
+ cm: CodeMirror
26
+ app: App
27
+
28
+ selected = ref(false)
29
+ language = ref('')
30
+ text = ref('')
31
+
32
+ private updating = false
33
+ private languageName: string = ''
34
+ private disposeSelectedWatcher: WatchHandle
35
+
36
+ private readonly languageConf: Compartment
37
+ private readonly readOnlyConf: Compartment
38
+
39
+ constructor(
40
+ public node: Node,
41
+ public view: EditorView,
42
+ public getPos: () => number | undefined,
43
+ public loader: LanguageLoader,
44
+ public config: CodeBlockConfig
45
+ ) {
46
+ this.languageConf = new Compartment()
47
+ this.readOnlyConf = new Compartment()
48
+
49
+ this.cm = new CodeMirror({
50
+ doc: this.node.textContent,
51
+ root: this.view.root,
52
+ extensions: [
53
+ this.readOnlyConf.of(EditorState.readOnly.of(!this.view.editable)),
54
+ drawSelection(),
55
+ cmKeymap.of(this.codeMirrorKeymap()),
56
+ this.languageConf.of([]),
57
+ EditorState.changeFilter.of(() => this.view.editable),
58
+ ...config.extensions,
59
+ CodeMirror.updateListener.of(this.forwardUpdate),
60
+ ],
61
+ })
62
+
63
+ this.app = this.createApp()
64
+
65
+ this.dom = this.createDom(this.app)
66
+
67
+ this.disposeSelectedWatcher = watchEffect(() => {
68
+ const isSelected = this.selected.value
69
+ if (isSelected) {
70
+ this.dom.classList.add('selected')
71
+ } else {
72
+ this.dom.classList.remove('selected')
73
+ }
74
+ })
75
+
76
+ this.updateLanguage()
77
+ }
78
+
79
+ private forwardUpdate = (update: ViewUpdate) => {
80
+ if (this.updating || !this.cm.hasFocus) return
81
+ let offset = (this.getPos() ?? 0) + 1
82
+ const { main } = update.state.selection
83
+ const selFrom = offset + main.from
84
+ const selTo = offset + main.to
85
+ const pmSel = this.view.state.selection
86
+ if (update.docChanged || pmSel.from !== selFrom || pmSel.to !== selTo) {
87
+ const tr = this.view.state.tr
88
+ update.changes.iterChanges((fromA, toA, fromB, toB, text) => {
89
+ if (text.length)
90
+ tr.replaceWith(
91
+ offset + fromA,
92
+ offset + toA,
93
+ this.view.state.schema.text(text.toString())
94
+ )
95
+ else tr.delete(offset + fromA, offset + toA)
96
+ offset += toB - fromB - (toA - fromA)
97
+ })
98
+ tr.setSelection(TextSelection.create(tr.doc, selFrom, selTo))
99
+ this.view.dispatch(tr)
100
+ }
101
+ }
102
+
103
+ private createApp = () => {
104
+ return createApp(CodeBlock, {
105
+ text: this.text,
106
+ selected: this.selected,
107
+ codemirror: this.cm,
108
+ language: this.language,
109
+ getAllLanguages: this.getAllLanguages,
110
+ getReadOnly: () => !this.view.editable,
111
+ setLanguage: this.setLanguage,
112
+ config: this.config,
113
+ })
114
+ }
115
+
116
+ private createDom(app: App) {
117
+ const dom = document.createElement('div')
118
+ dom.className = 'milkdown-code-block'
119
+ this.text.value = this.node.textContent
120
+ app.mount(dom)
121
+ return dom
122
+ }
123
+
124
+ private updateLanguage() {
125
+ const languageName = this.node.attrs.language
126
+
127
+ if (languageName === this.languageName) return
128
+
129
+ this.language.value = languageName
130
+ const language = this.loader.load(languageName ?? '')
131
+
132
+ language
133
+ .then((lang) => {
134
+ if (lang) {
135
+ this.cm.dispatch({
136
+ effects: this.languageConf.reconfigure(lang),
137
+ })
138
+ this.languageName = languageName
139
+ }
140
+ })
141
+ .catch(console.error)
142
+ }
143
+
144
+ private codeMirrorKeymap = (): KeyBinding[] => {
145
+ const view = this.view
146
+ return [
147
+ { key: 'ArrowUp', run: () => this.maybeEscape('line', -1) },
148
+ { key: 'ArrowLeft', run: () => this.maybeEscape('char', -1) },
149
+ { key: 'ArrowDown', run: () => this.maybeEscape('line', 1) },
150
+ { key: 'ArrowRight', run: () => this.maybeEscape('char', 1) },
151
+ {
152
+ key: 'Mod-Enter',
153
+ run: () => {
154
+ if (!exitCode(view.state, view.dispatch)) return false
155
+
156
+ view.focus()
157
+ return true
158
+ },
159
+ },
160
+ { key: 'Mod-z', run: () => undo(view.state, view.dispatch) },
161
+ { key: 'Shift-Mod-z', run: () => redo(view.state, view.dispatch) },
162
+ { key: 'Mod-y', run: () => redo(view.state, view.dispatch) },
163
+ {
164
+ key: 'Backspace',
165
+ run: () => {
166
+ const ranges = this.cm.state.selection.ranges
167
+
168
+ if (ranges.length > 1) return false
169
+
170
+ const selection = ranges[0]
171
+
172
+ if (selection && (!selection.empty || selection.anchor > 0))
173
+ return false
174
+
175
+ if (this.cm.state.doc.lines >= 2) return false
176
+
177
+ const state = this.view.state
178
+ const pos = this.getPos() ?? 0
179
+ const tr = state.tr.replaceWith(
180
+ pos,
181
+ pos + this.node.nodeSize,
182
+ state.schema.nodes.paragraph!.createChecked({}, this.node.content)
183
+ )
184
+
185
+ tr.setSelection(TextSelection.near(tr.doc.resolve(pos)))
186
+
187
+ this.view.dispatch(tr)
188
+ this.view.focus()
189
+ return true
190
+ },
191
+ },
192
+ ]
193
+ }
194
+
195
+ private maybeEscape = (unit: 'line' | 'char', dir: -1 | 1): boolean => {
196
+ const { state } = this.cm
197
+ let main: SelectionRange | Line = state.selection.main
198
+ if (!main.empty) return false
199
+ if (unit === 'line') main = state.doc.lineAt(main.head)
200
+ if (dir < 0 ? main.from > 0 : main.to < state.doc.length) return false
201
+
202
+ const targetPos = (this.getPos() ?? 0) + (dir < 0 ? 0 : this.node.nodeSize)
203
+ const selection = TextSelection.near(
204
+ this.view.state.doc.resolve(targetPos),
205
+ dir
206
+ )
207
+ const tr = this.view.state.tr.setSelection(selection).scrollIntoView()
208
+ this.view.dispatch(tr)
209
+ this.view.focus()
210
+ return true
211
+ }
212
+
213
+ setSelection(anchor: number, head: number) {
214
+ if (!this.cm.dom.isConnected) return
215
+
216
+ this.cm.focus()
217
+ this.updating = true
218
+ this.cm.dispatch({ selection: { anchor, head } })
219
+ this.updating = false
220
+ }
221
+
222
+ update(node: Node) {
223
+ if (node.type !== this.node.type) return false
224
+
225
+ if (this.updating) return true
226
+
227
+ this.node = node
228
+ this.text.value = node.textContent
229
+ this.updateLanguage()
230
+ if (this.view.editable === this.cm.state.readOnly) {
231
+ this.cm.dispatch({
232
+ effects: this.readOnlyConf.reconfigure(
233
+ EditorState.readOnly.of(!this.view.editable)
234
+ ),
235
+ })
236
+ }
237
+
238
+ const change = computeChange(this.cm.state.doc.toString(), node.textContent)
239
+ if (change) {
240
+ this.updating = true
241
+ this.cm.dispatch({
242
+ changes: { from: change.from, to: change.to, insert: change.text },
243
+ scrollIntoView: true,
244
+ })
245
+ this.updating = false
246
+ }
247
+ return true
248
+ }
249
+
250
+ selectNode() {
251
+ this.selected.value = true
252
+ this.cm.focus()
253
+ }
254
+
255
+ deselectNode() {
256
+ this.selected.value = false
257
+ }
258
+
259
+ stopEvent() {
260
+ return true
261
+ }
262
+
263
+ destroy() {
264
+ this.app.unmount()
265
+ this.cm.destroy()
266
+ this.disposeSelectedWatcher()
267
+ }
268
+
269
+ setLanguage = (language: string) => {
270
+ this.view.dispatch(
271
+ this.view.state.tr.setNodeAttribute(
272
+ this.getPos() ?? 0,
273
+ 'language',
274
+ language
275
+ )
276
+ )
277
+ }
278
+
279
+ getAllLanguages = () => {
280
+ return this.loader.getAll()
281
+ }
282
+ }
283
+
284
+ function computeChange(
285
+ oldVal: string,
286
+ newVal: string
287
+ ): { from: number; to: number; text: string } | null {
288
+ if (oldVal === newVal) return null
289
+
290
+ let start = 0
291
+ let oldEnd = oldVal.length
292
+ let newEnd = newVal.length
293
+
294
+ while (
295
+ start < oldEnd &&
296
+ oldVal.charCodeAt(start) === newVal.charCodeAt(start)
297
+ )
298
+ ++start
299
+
300
+ while (
301
+ oldEnd > start &&
302
+ newEnd > start &&
303
+ oldVal.charCodeAt(oldEnd - 1) === newVal.charCodeAt(newEnd - 1)
304
+ ) {
305
+ oldEnd--
306
+ newEnd--
307
+ }
308
+
309
+ return { from: start, to: oldEnd, text: newVal.slice(start, newEnd) }
310
+ }