@jvs-milkdown/components 1.2.13 → 1.2.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jvs-milkdown/components",
3
- "version": "1.2.13",
3
+ "version": "1.2.14",
4
4
  "keywords": [
5
5
  "milkdown",
6
6
  "milkdown plugin"
@@ -46,19 +46,24 @@
46
46
  "./table-block": {
47
47
  "types": "./lib/table-block/index.d.ts",
48
48
  "import": "./lib/table-block/index.js"
49
+ },
50
+ "./diff-block": {
51
+ "types": "./lib/diff-block/index.d.ts",
52
+ "import": "./lib/diff-block/index.js"
49
53
  }
50
54
  },
51
55
  "dependencies": {
56
+ "@codemirror/merge": "^6.12.1",
52
57
  "@floating-ui/dom": "^1.5.1",
53
- "@jvs-milkdown/core": "^1.2.13",
54
- "@jvs-milkdown/ctx": "^1.2.13",
55
- "@jvs-milkdown/exception": "^1.2.13",
56
- "@jvs-milkdown/plugin-tooltip": "^1.2.13",
57
- "@jvs-milkdown/preset-commonmark": "^1.2.13",
58
- "@jvs-milkdown/preset-gfm": "^1.2.13",
59
- "@jvs-milkdown/prose": "^1.2.13",
60
- "@jvs-milkdown/transformer": "^1.2.13",
61
- "@jvs-milkdown/utils": "^1.2.13",
58
+ "@jvs-milkdown/core": "^1.2.14",
59
+ "@jvs-milkdown/ctx": "^1.2.14",
60
+ "@jvs-milkdown/exception": "^1.2.14",
61
+ "@jvs-milkdown/plugin-tooltip": "^1.2.14",
62
+ "@jvs-milkdown/preset-commonmark": "^1.2.14",
63
+ "@jvs-milkdown/preset-gfm": "^1.2.14",
64
+ "@jvs-milkdown/prose": "^1.2.14",
65
+ "@jvs-milkdown/transformer": "^1.2.14",
66
+ "@jvs-milkdown/utils": "^1.2.14",
62
67
  "@types/lodash-es": "^4.17.12",
63
68
  "clsx": "^2.0.0",
64
69
  "dompurify": "^3.2.5",
@@ -104,6 +109,9 @@
104
109
  ],
105
110
  "table-block": [
106
111
  "./lib/table-block/index.d.ts"
112
+ ],
113
+ "diff-block": [
114
+ "./lib/diff-block/index.d.ts"
107
115
  ]
108
116
  }
109
117
  }
@@ -0,0 +1,42 @@
1
+ import type { Extension } from '@codemirror/state'
2
+
3
+ import { $ctx } from '@jvs-milkdown/utils'
4
+
5
+ import { withMeta } from '../__internal__/meta'
6
+
7
+ export interface DiffBlockConfig {
8
+ extensions: Extension[]
9
+ languages: Array<string>
10
+ renderLanguage: (language: string, selected: boolean) => HTMLElement
11
+ theme: string
12
+ }
13
+
14
+ export const defaultConfig: DiffBlockConfig = {
15
+ extensions: [],
16
+ languages: [
17
+ 'text',
18
+ 'typescript',
19
+ 'javascript',
20
+ 'html',
21
+ 'css',
22
+ 'json',
23
+ 'markdown',
24
+ ],
25
+ renderLanguage: (language, selected) => {
26
+ const span = document.createElement('span')
27
+ span.className = 'milkdown-diff-block-language'
28
+ span.textContent = language
29
+ if (selected) {
30
+ span.classList.add('selected')
31
+ }
32
+ return span
33
+ },
34
+ theme: 'dark',
35
+ }
36
+
37
+ export const diffBlockConfig = $ctx(defaultConfig, 'diffBlockConfig')
38
+
39
+ withMeta(diffBlockConfig, {
40
+ displayName: 'Ctx<diffBlockConfig>',
41
+ group: 'DiffBlock',
42
+ })
@@ -0,0 +1,19 @@
1
+ import type { MilkdownPlugin } from '@jvs-milkdown/ctx'
2
+
3
+ import { diffBlockConfig } from './config'
4
+ import { diffBlockSchema } from './schema'
5
+ import { diffBlockView } from './view'
6
+
7
+ export * from './config'
8
+ export * from './schema'
9
+ export * from './view'
10
+ export * from './remark-plugin'
11
+
12
+ import { remarkDiffBlockPlugin } from './remark-plugin'
13
+
14
+ export const diffBlock: MilkdownPlugin[] = [
15
+ diffBlockConfig,
16
+ remarkDiffBlockPlugin,
17
+ diffBlockSchema,
18
+ diffBlockView,
19
+ ].flat()
@@ -0,0 +1,59 @@
1
+ import type { Node } from '@jvs-milkdown/transformer'
2
+
3
+ import { $remark } from '@jvs-milkdown/utils'
4
+ import { visit } from 'unist-util-visit'
5
+
6
+ import { withMeta } from '../__internal__/meta'
7
+
8
+ function visitDiffBlock(ast: Node) {
9
+ return visit(
10
+ ast,
11
+ 'code',
12
+ (
13
+ node: Node & { lang?: string; value?: string },
14
+ index: number,
15
+ parent: Node & { children: Node[] }
16
+ ) => {
17
+ if (node.lang !== 'diff_block') return
18
+
19
+ let originalText = ''
20
+ let modifiedText = node.value || ''
21
+ let language = 'text'
22
+
23
+ try {
24
+ if (modifiedText.startsWith('{')) {
25
+ const parsed = JSON.parse(modifiedText)
26
+ originalText = parsed.originalText || ''
27
+ modifiedText = parsed.modifiedText || ''
28
+ language = parsed.language || 'text'
29
+ }
30
+ } catch {
31
+ // fallback
32
+ }
33
+
34
+ const newNode = {
35
+ type: 'diff_block',
36
+ originalText,
37
+ modifiedText,
38
+ language,
39
+ }
40
+
41
+ parent.children.splice(index, 1, newNode as any)
42
+ }
43
+ )
44
+ }
45
+
46
+ export const remarkDiffBlockPlugin = $remark(
47
+ 'remark-diff-block',
48
+ () => () => visitDiffBlock
49
+ )
50
+
51
+ withMeta(remarkDiffBlockPlugin.plugin, {
52
+ displayName: 'Remark<remarkDiffBlock>',
53
+ group: 'DiffBlock',
54
+ })
55
+
56
+ withMeta(remarkDiffBlockPlugin.options, {
57
+ displayName: 'RemarkConfig<remarkDiffBlock>',
58
+ group: 'DiffBlock',
59
+ })
@@ -0,0 +1,86 @@
1
+ import { expectDomTypeError } from '@jvs-milkdown/exception'
2
+ import { $nodeSchema } from '@jvs-milkdown/utils'
3
+
4
+ import { withMeta } from '../__internal__/meta'
5
+
6
+ export const DIFF_BLOCK_DATA_TYPE = 'diff-block'
7
+
8
+ export const diffBlockSchema = $nodeSchema('diff_block', () => {
9
+ return {
10
+ content: 'text*',
11
+ marks: '',
12
+ group: 'block',
13
+ selectable: true,
14
+ isolating: true,
15
+ code: true,
16
+ attrs: {
17
+ originalText: { default: '' },
18
+ modifiedText: { default: '' },
19
+ language: { default: 'text' },
20
+ },
21
+ parseDOM: [
22
+ {
23
+ tag: `div[data-type="${DIFF_BLOCK_DATA_TYPE}"]`,
24
+ preserveWhitespace: 'full',
25
+ getAttrs: (dom) => {
26
+ if (!(dom instanceof HTMLElement)) throw expectDomTypeError(dom)
27
+
28
+ return {
29
+ originalText: dom.getAttribute('data-original-text') || '',
30
+ modifiedText: dom.getAttribute('data-modified-text') || '',
31
+ language: dom.getAttribute('data-language') || 'text',
32
+ }
33
+ },
34
+ },
35
+ ],
36
+ toDOM: (node) => {
37
+ return [
38
+ 'div',
39
+ {
40
+ 'data-type': DIFF_BLOCK_DATA_TYPE,
41
+ 'data-original-text': node.attrs.originalText,
42
+ 'data-modified-text': node.attrs.modifiedText,
43
+ 'data-language': node.attrs.language,
44
+ class: 'milkdown-diff-block',
45
+ },
46
+ ['code', { spellcheck: 'false' }, 0],
47
+ ]
48
+ },
49
+ parseMarkdown: {
50
+ match: ({ type }) => type === 'diff_block',
51
+ runner: (state, node, type) => {
52
+ const originalText = (node as any).originalText as string
53
+ const modifiedText = (node as any).modifiedText as string
54
+ const language = (node as any).language as string
55
+
56
+ state.addNode(type, {
57
+ originalText: originalText || '',
58
+ modifiedText: modifiedText || '',
59
+ language: language || 'text',
60
+ })
61
+ },
62
+ },
63
+ toMarkdown: {
64
+ match: (node) => node.type.name === 'diff_block',
65
+ runner: (state, node) => {
66
+ state.addNode('code', undefined, undefined, {
67
+ lang: 'diff_block',
68
+ value: JSON.stringify(
69
+ {
70
+ originalText: node.attrs.originalText,
71
+ modifiedText: node.attrs.modifiedText,
72
+ language: node.attrs.language,
73
+ },
74
+ null,
75
+ 2
76
+ ),
77
+ })
78
+ },
79
+ },
80
+ }
81
+ })
82
+
83
+ withMeta(diffBlockSchema.node, {
84
+ displayName: 'NodeSchema<diff_block>',
85
+ group: 'DiffBlock',
86
+ })
@@ -0,0 +1,22 @@
1
+ import type { NodeViewConstructor } from '@jvs-milkdown/prose/view'
2
+
3
+ import { $view } from '@jvs-milkdown/utils'
4
+
5
+ import { withMeta } from '../../__internal__/meta'
6
+ import { diffBlockConfig } from '../config'
7
+ import { diffBlockSchema } from '../schema'
8
+ import { DiffBlockNodeView } from './node-view'
9
+
10
+ export const diffBlockView = $view(
11
+ diffBlockSchema.node,
12
+ (ctx): NodeViewConstructor => {
13
+ const config = ctx.get(diffBlockConfig.key)
14
+ return (node, view, getPos) =>
15
+ new DiffBlockNodeView(node, view, getPos, config)
16
+ }
17
+ )
18
+
19
+ withMeta(diffBlockView, {
20
+ displayName: 'NodeView<diff_block>',
21
+ group: 'DiffBlock',
22
+ })
@@ -0,0 +1,154 @@
1
+ import type { Node } from '@jvs-milkdown/prose/model'
2
+ import type { EditorView, NodeView } from '@jvs-milkdown/prose/view'
3
+
4
+ import { unifiedMergeView } from '@codemirror/merge'
5
+ import { Compartment, EditorState } from '@codemirror/state'
6
+ import {
7
+ EditorView as CodeMirror,
8
+ type ViewUpdate,
9
+ drawSelection,
10
+ } from '@codemirror/view'
11
+ import { ref, watchEffect, type WatchHandle } from 'vue'
12
+
13
+ import type { DiffBlockConfig } from '../config'
14
+
15
+ export class DiffBlockNodeView implements NodeView {
16
+ dom: HTMLElement
17
+ cm: CodeMirror
18
+
19
+ selected = ref(false)
20
+
21
+ private updating = false
22
+ private disposeSelectedWatcher: WatchHandle
23
+
24
+ private readonly readOnlyConf: Compartment
25
+
26
+ constructor(
27
+ public node: Node,
28
+ public view: EditorView,
29
+ public getPos: () => number | undefined,
30
+ public config: DiffBlockConfig
31
+ ) {
32
+ this.readOnlyConf = new Compartment()
33
+
34
+ this.cm = new CodeMirror({
35
+ doc: this.node.attrs.modifiedText || '',
36
+ root: this.view.root,
37
+ extensions: [
38
+ this.readOnlyConf.of(EditorState.readOnly.of(true)),
39
+ drawSelection(),
40
+ unifiedMergeView({
41
+ original: this.node.attrs.originalText || '',
42
+ mergeControls: true,
43
+ }),
44
+ CodeMirror.updateListener.of(this.forwardUpdate),
45
+ ...config.extensions,
46
+ ],
47
+ })
48
+
49
+ this.dom = document.createElement('div')
50
+ this.dom.className = 'milkdown-diff-block'
51
+ this.dom.appendChild(this.cm.dom)
52
+
53
+ this.disposeSelectedWatcher = watchEffect(() => {
54
+ const isSelected = this.selected.value
55
+ if (isSelected) {
56
+ this.dom.classList.add('selected')
57
+ } else {
58
+ this.dom.classList.remove('selected')
59
+ }
60
+ })
61
+ }
62
+
63
+ private forwardUpdate = (update: ViewUpdate) => {
64
+ if (this.updating) return
65
+ if (!update.docChanged) return
66
+
67
+ // If the doc changed via CodeMirror merge controls (accept/reject),
68
+ // sync it back to ProseMirror's modifiedText attribute.
69
+ const pos = this.getPos()
70
+ if (pos == null) return
71
+
72
+ const newText = update.state.doc.toString()
73
+ const tr = this.view.state.tr.setNodeAttribute(pos, 'modifiedText', newText)
74
+ this.view.dispatch(tr)
75
+ }
76
+
77
+ setSelection(anchor: number, head: number) {
78
+ if (!this.cm.dom.isConnected) return
79
+
80
+ this.cm.focus()
81
+ this.updating = true
82
+ this.cm.dispatch({ selection: { anchor, head } })
83
+ this.updating = false
84
+ }
85
+
86
+ update(node: Node) {
87
+ if (node.type !== this.node.type) return false
88
+
89
+ if (this.updating) return true
90
+
91
+ this.node = node
92
+
93
+ const modifiedText = node.attrs.modifiedText || ''
94
+
95
+ // Reconfigure unifiedMergeView with the current original text if needed
96
+ // (We might need to recreate it if originalText changes, but typically it doesn't change from outside)
97
+
98
+ const change = computeChange(this.cm.state.doc.toString(), modifiedText)
99
+ if (change) {
100
+ this.updating = true
101
+ this.cm.dispatch({
102
+ changes: { from: change.from, to: change.to, insert: change.text },
103
+ })
104
+ this.updating = false
105
+ }
106
+ return true
107
+ }
108
+
109
+ selectNode() {
110
+ this.selected.value = true
111
+ this.cm.focus()
112
+ }
113
+
114
+ deselectNode() {
115
+ this.selected.value = false
116
+ }
117
+
118
+ stopEvent() {
119
+ return true
120
+ }
121
+
122
+ destroy() {
123
+ this.cm.destroy()
124
+ this.disposeSelectedWatcher()
125
+ }
126
+ }
127
+
128
+ function computeChange(
129
+ oldVal: string,
130
+ newVal: string
131
+ ): { from: number; to: number; text: string } | null {
132
+ if (oldVal === newVal) return null
133
+
134
+ let start = 0
135
+ let oldEnd = oldVal.length
136
+ let newEnd = newVal.length
137
+
138
+ while (
139
+ start < oldEnd &&
140
+ oldVal.charCodeAt(start) === newVal.charCodeAt(start)
141
+ )
142
+ ++start
143
+
144
+ while (
145
+ oldEnd > start &&
146
+ newEnd > start &&
147
+ oldVal.charCodeAt(oldEnd - 1) === newVal.charCodeAt(newEnd - 1)
148
+ ) {
149
+ oldEnd--
150
+ newEnd--
151
+ }
152
+
153
+ return { from: start, to: oldEnd, text: newVal.slice(start, newEnd) }
154
+ }
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { Icon } from './__internal__/components/icon'
2
+ export * from './diff-block'
@@ -467,14 +467,14 @@ export const TableBlock = defineComponent<TableBlockProps>({
467
467
  let shouldUpdate = false
468
468
  for (const mut of mutations) {
469
469
  if (mut.type === 'childList') {
470
- for (const node of mut.addedNodes) {
470
+ for (const node of Array.from(mut.addedNodes)) {
471
471
  if (
472
472
  node instanceof HTMLElement &&
473
473
  ['TR', 'TD', 'TH', 'TBODY'].includes(node.nodeName)
474
474
  )
475
475
  shouldUpdate = true
476
476
  }
477
- for (const node of mut.removedNodes) {
477
+ for (const node of Array.from(mut.removedNodes)) {
478
478
  if (
479
479
  node instanceof HTMLElement &&
480
480
  ['TR', 'TD', 'TH', 'TBODY'].includes(node.nodeName)