Markdown editor in progress + Login and session process completed

This commit is contained in:
2024-08-19 16:27:09 +02:00
parent aba56bb034
commit 2e92c389a2
74 changed files with 1305 additions and 313 deletions

View File

@@ -0,0 +1,105 @@
<style>
.editor
{
white-space: pre-line;
overflow: auto;
outline: none;
box-shadow: none !important;
}
</style>
<template>
<div class="editor" contenteditable>
<template
v-if="model && model.length > 0">
<MarkdownRenderer
v-if="node"
:key="key"
:node="node"
:proses="{
'a': LiveA,
'h1': LiveH1,
'h2': LiveH2,
'h3': LiveH3,
'h4': LiveH4,
'h5': LiveH5,
'h6': LiveH6,
'blockquote': LiveBlockquote,
}"
></MarkdownRenderer>
</template>
</div>
</template>
<script setup lang="ts">
import LiveA from "~/components/prose/live/LiveA.vue";
import LiveH1 from "~/components/prose/live/LiveH1.vue";
import LiveH2 from "~/components/prose/live/LiveH2.vue";
import LiveH3 from "~/components/prose/live/LiveH3.vue";
import LiveH4 from "~/components/prose/live/LiveH4.vue";
import LiveH5 from "~/components/prose/live/LiveH5.vue";
import LiveH6 from "~/components/prose/live/LiveH6.vue";
import LiveBlockquote from "~/components/prose/ProseBlockquote.vue";
import { hash } from 'ohash'
import { watch, computed } from 'vue'
import type { Root } from 'hast';
import { diffLines as diff } from 'diff';
const model = defineModel<string>();
const parser = useMarkdown();
const key = computed(() => hash(model.value));
const node = ref<Root>();
watch(model, async (value, old) => {
if(value && old)
{
if(node.value)
{
let content = "", line = 0, pos = -1, len = 0, child;
const d = diff(old, value);
const children = node.value?.children.filter(e => e.hasOwnProperty('position'));
for(let i = 0; i < d.length; i++)
{
if(d[i].added) //Nouvelle ligne
{
const next = d.length > i ? d[i + 1] : undefined;
if(pos === -1 && (!next || !next.removed)) //Nouvelle ligne
{
child = children.filter(e => e.position?.start.line <= line && e.position?.end.line >= line); //Je cherche tout les blocs qui était inclus dans les lignes éditées.
if(child.length > 0)
{
pos = child[0].position?.start.offset ?? 0; //Je pars du premier caractère du bloc
len += (child[child.length - 1].position?.end.offset ?? 0) + 1; //Je m'arrete au dernier caractère du bloc + le \n
}
}
len += d[i].value.length; // J'ajoute le nouveau nombre de caractère
}
else if(d[i].removed) //Ancienne ligne
{
child = children.filter(e => e.position?.start.line <= line + 1 && e.position?.end.line >= line + (d[i].count ?? 1)); //Je cherche tout les blocs qui était inclus dans les lignes éditées.
if(child.length > 0)
{
pos = child[0].position?.start.offset ?? 0; //Je pars du premier caractère du bloc
len += child[child.length - 1].position?.end.offset ?? 0 + 1; //Je m'arrete au dernier caractère du bloc
len -= d[i].value.length; //Je supprime l'ancien nombre de caractère
}
}
else
{
line += d[i].count ?? 0;
}
}
node.value = parser(value);
}
else
{
node.value = parser(value);
}
}
}, { immediate: true });
</script>

View File

@@ -8,6 +8,8 @@ const props = defineProps<Prop>();
const model = defineModel<string>();
const err = ref<string | boolean | undefined>(props.error);
watchEffect(() => err.value = props.error);
</script>
<template>

View File

@@ -13,15 +13,17 @@ const { data: navigation, execute, status, error } = await useFetch(() => `/api/
immediate: false,
});
if(route.params.projectId && project.value !== 0)
{
showing.value = true;
execute();
}
else
{
showing.value = false;
}
watch(route, () => {
if(route.params.projectId && project.value !== 0)
{
showing.value = true;
execute();
}
else
{
showing.value = false;
}
})
</script>
<template>

View File

@@ -1,89 +1,26 @@
<template>
<slot
:data="data?.data"
:body="data?.body"
:toc="data?.toc"
:excerpt="data?.excerpt"
:error="error"
>
<MDCRenderer
v-if="body"
:tag="tag"
:class="props.class"
:body="body"
:data="data?.data"
:unwrap="props.unwrap"
:components="{
a: ProseA,
h1: ProseH1,
h2: ProseH2,
h3: ProseH3,
h4: ProseH4,
h5: ProseH5,
h6: ProseH6,
blockquote: ProseBlockquote,
}"
/>
</slot>
<template
v-if="model && model.length > 0">
<MarkdownRenderer :key="key" v-if="node" :node="node"></MarkdownRenderer>
</template>
</template>
<script setup lang="ts">
import ProseA from "~/components/content/prose/ProseA.vue";
import ProseH1 from "~/components/content/prose/ProseH1.vue";
import ProseH2 from "~/components/content/prose/ProseH2.vue";
import ProseH3 from "~/components/content/prose/ProseH3.vue";
import ProseH4 from "~/components/content/prose/ProseH4.vue";
import ProseH5 from "~/components/content/prose/ProseH5.vue";
import ProseH6 from "~/components/content/prose/ProseH6.vue";
import ProseBlockquote from "~/components/content/prose/ProseBlockquote.vue";
import { hash } from 'ohash'
import { useAsyncData } from 'nuxt/app'
import { watch, computed } from 'vue'
import type { Root } from 'hast';
const props = defineProps({
tag: {
type: [String, Boolean],
default: 'div'
},
content: {
type: [String, Object],
required: true
},
excerpt: {
type: Boolean,
default: false
},
class: {
type: [String, Array, Object],
default: ''
},
unwrap: {
type: [Boolean, String],
default: false
}
})
const model = defineModel<number>({
default: 0,
});
const model = defineModel<string>();
const parser = useMarkdown();
const key = computed(() => hash(props.content))
const key = computed(() => hash(model.value));
const { data, refresh, error } = await useAsyncData(key.value, async () => {
const timer = performance.now();
if (typeof props.content !== 'string') {
model.value = performance.now() - timer;
return props.content
const node = ref<Root>();
watch(model, async () => {
if(model.value && model.value)
{
node.value = parser(model.value);
}
const result = await parser(props.content);
model.value = performance.now() - timer;
return result;
})
const body = computed(() => props.excerpt ? data.value?.excerpt : data.value?.body)
watch(() => props.content, () => {
refresh()
})
}, { immediate: true });
</script>

View File

@@ -0,0 +1,121 @@
<script lang="ts">
import type { Node, Text, Element, Comment, Root } from 'hast';
import { Text as HText, Comment as HComment } from 'vue';
import ProseP from '~/components/prose/ProseP.vue';
import ProseA from '~/components/prose/ProseA.vue';
import ProseBlockquote from '~/components/prose/ProseBlockquote.vue';
import ProseCode from '~/components/prose/ProseCode.vue';
import ProsePre from '~/components/prose/ProsePre.vue';
import ProseEm from '~/components/prose/ProseEm.vue';
import ProseH1 from '~/components/prose/ProseH1.vue';
import ProseH2 from '~/components/prose/ProseH2.vue';
import ProseH3 from '~/components/prose/ProseH3.vue';
import ProseH4 from '~/components/prose/ProseH4.vue';
import ProseH5 from '~/components/prose/ProseH5.vue';
import ProseH6 from '~/components/prose/ProseH6.vue';
import ProseHr from '~/components/prose/ProseHr.vue';
import ProseImg from '~/components/prose/ProseImg.vue';
import ProseUl from '~/components/prose/ProseUl.vue';
import ProseOl from '~/components/prose/ProseOl.vue';
import ProseLi from '~/components/prose/ProseLi.vue';
import ProseStrong from '~/components/prose/ProseStrong.vue';
import ProseTable from '~/components/prose/ProseTable.vue';
import ProseThead from '~/components/prose/ProseThead.vue';
import ProseTbody from '~/components/prose/ProseTbody.vue';
import ProseTd from '~/components/prose/ProseTd.vue';
import ProseTh from '~/components/prose/ProseTh.vue';
import ProseTr from '~/components/prose/ProseTr.vue';
import ProseScript from '~/components/prose/ProseScript.vue';
const proseList = {
"p": ProseP,
"a": ProseA,
"blockquote": ProseBlockquote,
"code": ProseCode,
"pre": ProsePre,
"em": ProseEm,
"h1": ProseH1,
"h2": ProseH2,
"h3": ProseH3,
"h4": ProseH4,
"h5": ProseH5,
"h6": ProseH6,
"hr": ProseHr,
"img": ProseImg,
"ul": ProseUl,
"ol": ProseOl,
"li": ProseLi,
"strong": ProseStrong,
"table": ProseTable,
"thead": ProseThead,
"tbody": ProseTbody,
"td": ProseTd,
"th": ProseTh,
"tr": ProseTr,
"script": ProseScript
};
export default defineComponent({
name: 'MarkdownRenderer',
props: {
node: {
type: Object,
required: true
},
proses: {
type: Object,
default: () => ({})
}
},
watch: {
node: {
handler: function(val, old) {
},
deep: true,
}
},
async setup(props) {
if(props.proses)
{
for(const prose of Object.keys(props.proses))
{
if(typeof props.proses[prose] === 'string')
props.proses[prose] = await resolveComponent(props.proses[prose]);
}
}
return { tags: Object.assign({}, proseList, props.proses) };
},
render(ctx: any) {
const { node, tags } = ctx;
const div = h('div', null, (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e));
return div;
}
});
function renderNode(node: Node, tags: Record<string, any>): VNode | undefined
{
if(node.type === 'text')
{
const text = node as Text;
if(text.value.length > 0 && text.value !== '\n')
return h(HText, (node as Text).value);
}
else if(node.type === 'comment')
{
const comment = node as Comment;
if(comment.value.length > 0 && comment.value !== '\n')
return h(HComment, (node as Comment).value);
}
else if(node.type === 'element')
{
const element = node as Element;
return h(tags[element.tagName], { ...element.properties, class: element.properties.className }, element.children.map(e => renderNode(e, tags)).filter(e => !!e));
}
return undefined;
}
</script>

View File

@@ -43,7 +43,7 @@ if(props.node.color !== undefined)
<div v-if="node.text?.length > 0" class="markdown-embed-content node-insert-event" style="">
<div class="markdown-preview-view markdown-rendered node-insert-event show-indentation-guide allow-fold-headings allow-fold-lists">
<div class="markdown-preview-sizer markdown-preview-section">
<Markdown :content="node.text"/>
<Markdown v-model="node.text"/>
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { Navigation } from '~/server/api/project/[projectId]/navigation.get';
import type { Navigation } from '~/types/api';
interface Props {
link: Navigation;

View File

@@ -8,7 +8,7 @@
<div class="markdown-preview-view markdown-rendered node-insert-event hide-title">
<div class="markdown-preview-sizer markdown-preview-section" style="padding-bottom: 0px;">
<h1 v-if="page[0]?.title">{{ page[0]?.title }}</h1>
<Markdown :content="page[0].content" />
<Markdown v-model="page[0].content" />
</div>
</div>
</div>

View File

@@ -0,0 +1,5 @@
<template>
<blockquote>
<slot />
</blockquote>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<code><slot /></code>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<em>
<slot />
</em>
</template>

View File

@@ -1,7 +1,11 @@
<template>
<h1 :id="id"><slot></slot></h1>
<h1 :id="id">
<slot />
</h1>
</template>
<script setup lang="ts">
const props = defineProps<{ id?: string }>()
</script>
const generate = computed(() => props.id)
</script>

View File

@@ -1,7 +1,11 @@
<template>
<h2 :id="id"><slot></slot></h2>
<h2 :id="id">
<slot />
</h2>
</template>
<script setup lang="ts">
const props = defineProps<{ id?: string }>()
</script>
const generate = computed(() => props.id)
</script>

View File

@@ -1,7 +1,11 @@
<template>
<h3 :id="id"><slot></slot></h3>
<h3 :id="id">
<slot />
</h3>
</template>
<script setup lang="ts">
const props = defineProps<{ id?: string }>()
</script>
const generate = computed(() => props.id)
</script>

View File

@@ -1,7 +1,11 @@
<template>
<h4 :id="id"><slot></slot></h4>
<h4 :id="id">
<slot />
</h4>
</template>
<script setup lang="ts">
const props = defineProps<{ id?: string }>()
</script>
const generate = computed(() => props.id)
</script>

View File

@@ -1,7 +1,11 @@
<template>
<h5 :id="id"><slot></slot></h5>
<h5 :id="id">
<slot />
</h5>
</template>
<script setup lang="ts">
const props = defineProps<{ id?: string }>()
</script>
const generate = computed(() => props.id)
</script>

View File

@@ -1,7 +1,11 @@
<template>
<h6 :id="id"><slot></slot></h6>
<h6 :id="id">
<slot />
</h6>
</template>
<script setup lang="ts">
const props = defineProps<{ id?: string }>()
</script>
const generate = computed(() => props.id)
</script>

View File

@@ -0,0 +1,3 @@
<template>
<hr>
</template>

View File

@@ -0,0 +1,42 @@
<template>
<img
:src="refinedSrc"
:alt="alt"
:width="width"
:height="height"
/>
</template>
<script setup lang="ts">
import { withTrailingSlash, withLeadingSlash, joinURL } from 'ufo'
import { useRuntimeConfig, computed, resolveComponent } from '#imports'
const props = defineProps({
src: {
type: String,
default: ''
},
alt: {
type: String,
default: ''
},
width: {
type: [String, Number],
default: undefined
},
height: {
type: [String, Number],
default: undefined
}
})
const refinedSrc = computed(() => {
if (props.src?.startsWith('/') && !props.src.startsWith('//')) {
const _base = withLeadingSlash(withTrailingSlash(useRuntimeConfig().app.baseURL))
if (_base !== '/' && !props.src.startsWith(_base)) {
return joinURL(_base, props.src)
}
}
return props.src
})
</script>

View File

@@ -0,0 +1,3 @@
<template>
<li><slot /></li>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<ol>
<slot />
</ol>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<p><slot /></p>
</template>

View File

@@ -0,0 +1,36 @@
<template>
<pre :class="$props.class"><slot /></pre>
</template>
<script setup lang="ts">
defineProps({
code: {
type: String,
default: ''
},
language: {
type: String,
default: null
},
filename: {
type: String,
default: null
},
highlights: {
type: Array as () => number[],
default: () => []
},
meta: {
type: String,
default: null
},
class: {
type: String,
default: null
}
})
</script>
<style>
pre code .line{display:block}
</style>

View File

@@ -0,0 +1,15 @@
<template>
<div v-if="isDev">
Rendering the <code>script</code> element is dangerous and is disabled by default. Consider implementing your own <code>ProseScript</code> element to have control over script rendering.
</div>
</template>
<script setup lang="ts">
defineProps({
src: {
type: String,
default: ''
}
})
const isDev = import.meta.dev
</script>

View File

@@ -0,0 +1,5 @@
<template>
<strong>
<slot />
</strong>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<table>
<slot />
</table>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<tbody>
<slot />
</tbody>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<td>
<slot />
</td>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<th>
<slot />
</th>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<thead>
<slot />
</thead>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<tr>
<slot />
</tr>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<ul>
<slot />
</ul>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<a>
<slot></slot>
</a>
</template>

View File

@@ -0,0 +1,12 @@
<template>
<span v-if="focused">> </span>
<blockquote ref="el" @focusin="focused = true" @focusout="focused = false">
<slot />
</blockquote>
</template>
<script setup lang="ts">
const focused = ref(false);
watch(focused, console.log);
</script>

View File

@@ -0,0 +1,3 @@
<template>
<span v-show="false"># </span><h1><slot></slot></h1>
</template>

View File

@@ -0,0 +1,12 @@
<template>
<span v-show="focused">## </span><h2><slot></slot></h2>
</template>
<script setup>
const props = defineProps({
focused: {
type: Boolean,
default: false
}
})
</script>

View File

@@ -0,0 +1,3 @@
<template>
<span v-show="false">### </span><h3><slot></slot></h3>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<span v-show="false">#### </span><h4><slot></slot></h4>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<span v-show="false">##### </span><h5><slot></slot></h5>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<span v-show="false">###### </span><h6><slot></slot></h6>
</template>