New HyperMD implementation with custom behaviour.

This commit is contained in:
2025-02-25 15:55:30 +01:00
parent 6abc467a43
commit 0b97e9a295
18 changed files with 1545 additions and 407 deletions

View File

@@ -1,107 +1,157 @@
<script lang="ts">
const External = Annotation.define<boolean>();
import type { Editor, EditorConfiguration, EditorChange } from 'codemirror';
import { changeEnd } from 'codemirror';
import { fromTextArea } from 'hypermd';
import '#shared/hypermd.extend';
function onChange(cm: Editor, change: EditorChange)
{
if (changeEnd(change).line == cm.lastLine())
updateBottomMargin(cm);
}
function updateBottomMargin(cm: Editor)
{
let padding = "";
if (cm.lineCount() > 1)
{
//@ts-ignore
let totalH = cm.display.scroller.clientHeight - 30, lastLineH = cm.getLineHandle(cm.lastLine()).height;
padding = (totalH / 2 - lastLineH) + "px";
}
if (cm.state.scrollPastEndPadding != padding)
{
cm.state.scrollPastEndPadding = padding;
cm.display.lineSpace.parentNode.style.paddingBottom = padding;
}
}
</script>
<script setup lang="ts">
import { dropCursor, crosshairCursor, keymap, EditorView, ViewUpdate, placeholder as placeholderExtension } from '@codemirror/view';
import { Annotation, EditorState } from '@codemirror/state';
import { indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap } from '@codemirror/language';
import { history, defaultKeymap, historyKeymap } from '@codemirror/commands';
import { search, searchKeymap } from '@codemirror/search';
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
import { lintKeymap } from '@codemirror/lint';
const editor = useTemplateRef('editor');
const view = ref<EditorView>();
const state = ref<EditorState>();
const { placeholder, autofocus = false } = defineProps<{
const { placeholder, autofocus = false, gutters = true, format = 'd-any' } = defineProps<{
placeholder?: string
autofocus?: boolean
gutters?: boolean
format?: 'hypermd' | 'd-any'
}>();
const model = defineModel<string>();
const editor = ref<Editor>();
const input = useTemplateRef('input');
onMounted(() => {
if(editor.value)
if(input.value)
{
state.value = EditorState.create({
doc: model.value,
extensions: [
history(),
search(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
crosshairCursor(),
placeholderExtension(placeholder ?? ''),
EditorView.lineWrapping,
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
]),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
{
model.value = viewUpdate.state.doc.toString();
}
}),
EditorView.contentAttributes.of({spellcheck: "true"}),
]
});
view.value = new EditorView({
state: state.value,
parent: editor.value,
});
const e = editor.value = fromTextArea(input.value, {
mode: {
name: format,
hashtag: true,
toc: false,
math: false,
orgModeMarkup: false,
tokenTypeOverrides: {
hr: "line-HyperMD-hr line-background-HyperMD-hr-bg hr",
list1: "list-1",
list2: "list-2",
list3: "list-3",
code: "inline-code",
hashtag: "hashtag meta"
}
},
spellcheck: true,
autofocus: autofocus,
lineNumbers: false,
showCursorWhenSelecting: true,
indentUnit: 4,
autoCloseBrackets: true,
foldGutter: gutters,
theme: 'custom'
} as EditorConfiguration);
if(autofocus)
{
view.value.focus();
}
}
})
onBeforeUnmount(() => {
if (view.value) {
view.value?.destroy()
view.value = undefined
e.setValue(model.value ?? '');
updateBottomMargin(e);
e.on('change', onChange);
e.on('change', (cm: Editor, change: EditorChange) => model.value = cm.getValue());
e.on('refresh', updateBottomMargin);
}
});
watchEffect(() => {
if (model.value === void 0) {
return;
}
const currentValue = view.value ? view.value.state.doc.toString() : "";
if (view.value && model.value !== currentValue) {
view.value.dispatch({
changes: { from: 0, to: currentValue.length, insert: model.value || "" },
annotations: [External.of(true)],
});
const value = editor.value?.getValue();
if (editor.value && model.value !== value) {
editor.value.setValue(model.value ?? '');
editor.value.clearHistory();
}
});
</script>
<template>
<div ref="editor" class="flex flex-1 w-full justify-stretch items-stretch border border-light-35 dark:border-dark-35 caret-light-100 dark:caret-dark-100 py-2 px-1.5 font-sans text-base" />
<div :class="{ 'cancel-gutters': !gutters }" class="flex flex-1 w-full justify-stretch items-stretch !font-sans !text-base">
<textarea ref="input" class="hidden"></textarea>
</div>
</template>
<style>
.cm-editor
.CodeMirror
{
@apply bg-transparent;
@apply flex-1;
@apply flex-1 h-full;
@apply font-sans;
@apply text-light-100 dark:text-dark-100;
}
.cm-editor .cm-content
.cancel-gutters .CodeMirror-gutters
{
@apply caret-light-100;
@apply dark:caret-dark-100;
@apply hidden;
}
.cancel-gutters .CodeMirror-sizer
{
@apply ms-2;
}
.CodeMirror-gutters
{
@apply bg-transparent;
@apply border-transparent;
}
.CodeMirror-gutter-wrapper
{
@apply absolute top-0 bottom-0;
@apply flex justify-center items-center;
}
.CodeMirror-foldmarker
{
@apply text-light-100;
@apply dark:text-dark-100;
@apply ps-3;
text-shadow: none;
}
.hmd-inactive-line .cm-formatting-header, .hmd-inactive-line .cm-formatting-link, .hmd-inactive-line .cm-link-has-alias, .hmd-inactive-line .cm-link-alias-pipe
{
@apply hidden;
}
.CodeMirror-line
{
@apply text-base;
}
.CodeMirror-cursor
{
@apply border-light-100 dark:border-dark-100;
}
.CodeMirror-selected
{
@apply bg-light-35 dark:bg-dark-35;
}
.HyperMD-list-line-1 {
@apply !ps-0;
}
.HyperMD-list-line-2 {
@apply !ps-6;
}
.HyperMD-list-line-3 {
@apply !ps-12;
}
</style>

View File

@@ -25,8 +25,8 @@
<span @mousedown.left="(e) => resizeNode(e, 1, 0, -1, 1)" id="sw" class="cursor-sw-resize absolute -bottom-4 -left-4 w-8 h-8"></span> <!-- South West -->
</div>
</div>
<div v-else style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 overflow-hidden contain-strict w-full h-full flex" >
<Editor v-model="node.text" autofocus />
<div v-else style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 overflow-hidden contain-strict w-full h-full flex py-2" >
<Editor v-model="node.text" autofocus :gutters="false"/>
</div>
<div v-if="!editing && node.type === 'group' && node.label !== undefined" @click.left="(e) => selectNode(e)" @dblclick.left="(e) => editNode(e)" :class="style.border" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin">{{ node.label }}</div>
<input v-else-if="editing && node.type === 'group'" v-model="node.label" @click="e => e.stopImmediatePropagation()" v-autofocus :class="[style.border, style.outline]" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] appearance-none bg-transparent outline-4 mb-2 px-2 py-1 font-thin min-w-4" />

View File

@@ -27,4 +27,10 @@ const { hash, pathname } = parseURL(href);
const { content } = useContent();
const overview = computed(() => content.value.find(e => e.path === pathname));
</script>
</script>
<style>
.cm-link {
@apply text-accent-blue inline-flex items-center cursor-pointer hover:text-opacity-85;
}
</style>

View File

@@ -2,4 +2,27 @@
<blockquote class="empty:before:hidden ps-4 my-4 relative before:absolute before:-top-1 before:-bottom-1 before:left-0 before:w-1 before:bg-light-30 dark:before:bg-dark-30" ref="el">
<slot />
</blockquote>
</template>
</template>
<style>
.HyperMD-quote
{
@apply before:hidden;
}
.HyperMD-quote.hmd-inactive-line
{
@apply before:block empty:before:!hidden !pb-2 !ps-4 !relative before:!absolute before:!-top-1 before:!-bottom-1 before:!left-0 before:!w-1 before:!bg-none before:!bg-light-30 dark:before:!bg-dark-30;
}
.HyperMD-quote.HyperMD-header
{
@apply before:!hidden;
}
.hmd-inactive-line .cm-formatting-quote
{
@apply !hidden;
}
.cm-quote
{
@apply text-light-100 dark:text-dark-100;
}
</style>

View File

@@ -1,3 +1,10 @@
<template>
<code><slot /></code>
</template>
<style>
.cm-inline-code
{
@apply !border-none !bg-transparent !text-light-100 dark:!text-dark-100 !p-0;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<h1 :id="parseId(id)" class="text-5xl font-thin mt-3 mb-8 first:pt-0 pt-2 relative lg:right-8 sm:right-4 right-2">
<h1 :id="parseId(id)" class="text-5xl font-thin mt-3 mb-8 first:pt-0 pt-2">
<slot />
</h1>
</template>
@@ -8,3 +8,14 @@
import { parseId } from '#shared/general.util';
const props = defineProps<{ id?: string }>()
</script>
<style>
.HyperMD-header-1
{
@apply text-5xl pt-4 pb-2 after:hidden;
}
.HyperMD-header-1 .cm-header
{
@apply font-thin;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<h2 :id="parseId(id)" class="text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2 relative sm:right-4 right-2">
<h2 :id="parseId(id)" class="text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2">
<slot />
</h2>
</template>
@@ -10,3 +10,14 @@ const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)
</script>
<style>
.HyperMD-header-2
{
@apply !text-4xl !pt-4 !pb-2 !ps-1 leading-loose after:hidden;
}
.HyperMD-header-2 .cm-header
{
@apply font-semibold;
}
</style>

View File

@@ -10,3 +10,14 @@ const props = defineProps<{ id?: string }>()
const generate = computed(() => props.id)
</script>
<style>
.HyperMD-header-3
{
@apply !text-2xl !font-bold !pt-1 after:!hidden;
}
.HyperMD-header-3 .cm-header
{
@apply font-bold;
}
</style>

View File

@@ -8,3 +8,15 @@
import { parseId } from '#shared/general.util';
const props = defineProps<{ id?: string }>()
</script>
<style>
.HyperMD-header-4
{
@apply !text-xl font-semibold pt-1 after:hidden;
font-variant: small-caps;
}
.HyperMD-header-4 .cm-header
{
@apply font-semibold;
}
</style>

View File

@@ -1,3 +1,10 @@
<template>
<Separator class="border-b border-light-35 dark:border-dark-35 m-4" />
</template>
<style>
.HyperMD-hr
{
@apply bg-light-35 dark:bg-dark-35 h-px;
}
</style>

View File

@@ -1,3 +1,22 @@
<template>
<li class="before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4"><slot /></li>
</template>
<style>
.HyperMD-list-line
{
@apply !py-1;
}
.HyperMD-list-line.hmd-inactive-line > span
{
@apply before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4;
}
.hmd-inactive-line .cm-formatting-list
{
@apply hidden;
}
.cm-hmd-list-indent
{
@apply !hidden;
}
</style>

View File

@@ -2,4 +2,15 @@
<span class="before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30">
<slot></slot>
</span>
</template>
</template>
<style>
.cm-hashtag.cm-hashtag-begin
{
@apply bg-accent-blue bg-opacity-10 text-accent-blue text-sm pb-0.5 ps-1 rounded-l-[12px] border border-r-0 border-accent-blue border-opacity-30;
}
.cm-hashtag.cm-hashtag-end
{
@apply bg-accent-blue bg-opacity-10 text-accent-blue text-sm pb-0.5 pe-1 rounded-r-[12px] !rounded-se-none border border-l-0 border-accent-blue border-opacity-30;
}
</style>