Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: 🐛 extend from a factory cannot inject slice
- Loading branch information
1 parent
b724b16
commit 94235f4
Showing
19 changed files
with
542 additions
and
313 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<template> | ||
<Milkdown /> | ||
</template> | ||
|
||
<script lang="ts"> | ||
import { defineComponent } from 'vue'; | ||
import Milkdown from './Milkdown.vue'; | ||
export default defineComponent({ | ||
name: 'App', | ||
components: { | ||
Milkdown, | ||
}, | ||
}); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
/* Copyright 2021, Milkdown by Mirone. */ | ||
import { createSlice, editorViewCtx } from '@milkdown/core'; | ||
import { codeFence as originalCodeFence } from '@milkdown/preset-commonmark'; | ||
import { Fragment } from '@milkdown/prose/model'; | ||
import { RenderVue } from '@milkdown/vue'; | ||
|
||
const languageOptions = [ | ||
'', | ||
'javascript', | ||
'typescript', | ||
'bash', | ||
'sql', | ||
'json', | ||
'html', | ||
'css', | ||
'c', | ||
'cpp', | ||
'java', | ||
'ruby', | ||
'python', | ||
'go', | ||
'rust', | ||
'markdown', | ||
]; | ||
|
||
const id = 'fence'; | ||
|
||
export const languageListSlice = createSlice([] as string[], 'languageList'); | ||
|
||
export const codeFence: (view: ReturnType<RenderVue>) => typeof originalCodeFence = (view) => | ||
originalCodeFence.extend( | ||
(original, utils, options) => { | ||
const languageList = options?.languageList || languageOptions; | ||
return { | ||
...original, | ||
schema: (ctx) => { | ||
return { | ||
...original.schema(ctx), | ||
attrs: { | ||
language: { | ||
default: '', | ||
}, | ||
filename: { | ||
default: '', | ||
}, | ||
fold: { | ||
default: true, | ||
}, | ||
showInput: { | ||
default: false, | ||
}, | ||
}, | ||
parseDOM: [ | ||
{ | ||
tag: 'div.code-fence-container', | ||
preserveWhitespace: 'full', | ||
getAttrs: (dom) => { | ||
if (!(dom instanceof HTMLElement)) { | ||
throw new Error('Parse DOM error.'); | ||
} | ||
const pre = dom.querySelector('pre'); | ||
return { | ||
language: pre?.dataset['language'], | ||
filename: pre?.dataset['filename'], | ||
}; | ||
}, | ||
getContent: (dom, schema) => { | ||
if (!(dom instanceof HTMLElement)) { | ||
throw new Error('Parse DOM error.'); | ||
} | ||
const textNode = schema.text(dom.querySelector('pre')?.textContent ?? ''); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return Fragment.from<any>(textNode); | ||
}, | ||
}, | ||
{ | ||
tag: 'pre', | ||
preserveWhitespace: 'full', | ||
getAttrs: (dom) => { | ||
if (!(dom instanceof HTMLElement)) { | ||
throw new Error('Parse DOM error.'); | ||
} | ||
return { language: dom.dataset['language'], filename: dom.dataset['filename'] }; | ||
}, | ||
}, | ||
], | ||
toDOM: (node) => { | ||
const select = document.createElement('select'); | ||
languageList.forEach((lang) => { | ||
const option = document.createElement('option'); | ||
option.value = lang; | ||
option.innerText = !lang ? '--' : lang; | ||
if (lang === node.attrs['language']) { | ||
option.selected = true; | ||
} | ||
select.appendChild(option); | ||
}); | ||
select.onchange = (e) => { | ||
const target = e.target; | ||
if (!(target instanceof HTMLSelectElement)) { | ||
return; | ||
} | ||
const view = ctx.get(editorViewCtx); | ||
if (!view.editable) { | ||
target.value = node.attrs['language']; | ||
return; | ||
} | ||
|
||
const { top, left } = target.getBoundingClientRect(); | ||
const result = view.posAtCoords({ top, left }); | ||
if (!result) return; | ||
|
||
const { tr } = view.state; | ||
|
||
view.dispatch( | ||
tr.setNodeMarkup(result.inside, undefined, { | ||
...node.attrs, | ||
language: target.value, | ||
}), | ||
); | ||
}; | ||
return [ | ||
'div', | ||
{ | ||
class: 'code-fence-container', | ||
}, | ||
['span', node.attrs['filename']], | ||
select, | ||
[ | ||
'pre', | ||
{ | ||
'data-language': node.attrs['language'], | ||
'data-filename': node.attrs['filename'], | ||
class: utils.getClassName(node.attrs, 'code-fence'), | ||
}, | ||
['code', { spellCheck: 'false' }, 0], | ||
], | ||
]; | ||
}, | ||
parseMarkdown: { | ||
match: ({ type }) => type === 'code', | ||
runner: (state, node, type) => { | ||
const meta = node['meta'] as string; | ||
let filename = ''; | ||
if (meta) { | ||
const match = meta.match(/^\[(\w+)\]$/); | ||
if (match) { | ||
filename = match[1] || ''; | ||
} | ||
} | ||
const language = node['lang'] as string; | ||
const value = node['value'] as string; | ||
state.openNode(type, { language, filename }); | ||
if (value) { | ||
state.addText(value); | ||
} | ||
state.closeNode(); | ||
}, | ||
}, | ||
toMarkdown: { | ||
match: (node) => node.type.name === id, | ||
runner: (state, node) => { | ||
const filename = node.attrs['filename']; | ||
const meta = filename ? `[${filename}]` : ''; | ||
state.addNode('code', undefined, node.content.firstChild?.text || '', { | ||
lang: node.attrs['language'], | ||
meta, | ||
}); | ||
}, | ||
}, | ||
}; | ||
}, | ||
view: (ctx) => { | ||
ctx.set(languageListSlice, languageList); | ||
return view(ctx); | ||
}, | ||
}; | ||
}, | ||
[languageListSlice], | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
<script lang="ts"> | ||
import { nodeMetadata } from '@milkdown/vue'; | ||
import { defineComponent, inject } from 'vue'; | ||
import { languageListSlice } from './CodeFence'; | ||
export default defineComponent({ | ||
name: 'CodeFence', | ||
}); | ||
</script> | ||
|
||
<script setup lang="ts"> | ||
import { ref } from 'vue'; | ||
const metadata = inject(nodeMetadata); | ||
const attrs = metadata?.node.attrs; | ||
const languages = metadata?.ctx.get(languageListSlice) ?? []; | ||
const filename = ref(attrs?.['filename'] ?? ''); | ||
const lang = attrs?.['language'] ?? ''; | ||
const showInput = ref(attrs?.['showInput'] ?? false); | ||
const onChange = (e: Event) => { | ||
const view = metadata?.view; | ||
const node = metadata?.node; | ||
const getPos = metadata?.getPos as () => number; | ||
const { target } = e; | ||
if (!(target instanceof HTMLSelectElement) || !view || !node) { | ||
return; | ||
} | ||
const { value } = target; | ||
console.log(target.value); | ||
if (!view.editable) { | ||
target.value = node.attrs['language']; | ||
return; | ||
} | ||
const { tr } = view.state; | ||
view.dispatch( | ||
tr.setNodeMarkup(getPos(), undefined, { | ||
...node.attrs, | ||
language: value, | ||
}), | ||
); | ||
}; | ||
const toggleInput = () => { | ||
const view = metadata?.view; | ||
const node = metadata?.node; | ||
const getPos = metadata?.getPos as () => number; | ||
if (!view || !node) { | ||
return; | ||
} | ||
if (!view.editable) { | ||
return; | ||
} | ||
const show = node.attrs['showInput']; | ||
const { tr } = view.state; | ||
view.dispatch( | ||
tr.setNodeMarkup(getPos(), undefined, { | ||
...node.attrs, | ||
showInput: !show, | ||
}), | ||
); | ||
showInput.value = !show; | ||
}; | ||
const onKeydown = (e: KeyboardEvent) => { | ||
e.stopPropagation(); | ||
}; | ||
const onFilenameChange = (e: Event) => { | ||
const view = metadata?.view; | ||
const node = metadata?.node; | ||
const getPos = metadata?.getPos as () => number; | ||
const { target } = e; | ||
if (!(target instanceof HTMLInputElement)) return; | ||
if (!view || !node) { | ||
return; | ||
} | ||
if (!view.editable) { | ||
return; | ||
} | ||
const { value } = target; | ||
const { tr } = view.state; | ||
view.dispatch( | ||
tr.setNodeMarkup(getPos(), undefined, { | ||
...node.attrs, | ||
filename: value, | ||
showInput: false, | ||
}), | ||
); | ||
showInput.value = false; | ||
filename.value = value; | ||
}; | ||
</script> | ||
|
||
<template> | ||
<div class="code-fence"> | ||
<div class="control"> | ||
<span class="filename"> | ||
{{ filename }} | ||
<input v-if="showInput" @keydown="onKeydown" @change="onFilenameChange" /> | ||
<button v-else @click="toggleInput">edit</button> | ||
</span> | ||
<select @change="onChange"> | ||
<option v-for="language in languages" :selected="language === lang" :value="language"> | ||
{{ language }} | ||
</option> | ||
</select> | ||
</div> | ||
<div class="code"> | ||
<slot /> | ||
</div> | ||
</div> | ||
</template> | ||
|
||
<style scoped> | ||
.code-fence { | ||
border: 1px solid #ccc; | ||
padding: 10px; | ||
} | ||
.control { | ||
margin-bottom: 10px; | ||
display: flex; | ||
justify-content: space-between; | ||
} | ||
.code { | ||
background: #ccc; | ||
margin: 0 auto !important; | ||
white-space: pre; | ||
border-radius: 4px; | ||
padding: 10px; | ||
} | ||
</style> |
Oops, something went wrong.
94235f4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
milkdown – ./
milkdown-git-main-saul-mirone.vercel.app
www.milkdown.dev
milkdown.dev
milkdown.vercel.app
milkdown-saul-mirone.vercel.app