refactor(markdown): extract post-render effects into composable

Four components duplicated the watch -> nextTick -> listenToClick -> runTikz
shape. Collapse to useMarkdownPostRender, which also accepts optional flags
for Mermaid, Shikiji and image hydration so StackedNote no longer needs its
own inline conditional chain.
This commit is contained in:
Julien Calixte
2026-05-15 15:48:12 +02:00
parent 813f16655c
commit 33285d723a
5 changed files with 85 additions and 65 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, nextTick, onMounted, onUnmounted, toRefs, watch } from "vue" import { computed, onMounted, onUnmounted, toRefs, watch } from "vue"
import HeaderNote from "@/components/HeaderNote.vue" import HeaderNote from "@/components/HeaderNote.vue"
import SignInGithub from "@/components/SignInGithub.vue" import SignInGithub from "@/components/SignInGithub.vue"
@@ -7,7 +7,8 @@ import SkeletonLoader from "@/components/SkeletonLoader.vue"
import StackedNote from "@/components/StackedNote.vue" import StackedNote from "@/components/StackedNote.vue"
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook" import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
import { useLinks } from "@/hooks/useLinks.hook" import { useLinks } from "@/hooks/useLinks.hook"
import { markdownBuilder, runTikz } from "@/hooks/useMarkdown.hook" import { markdownBuilder } from "@/hooks/useMarkdown.hook"
import { useMarkdownPostRender } from "@/hooks/useMarkdownPostRender.hook"
import { useNoteView } from "@/hooks/useNoteView.hook" import { useNoteView } from "@/hooks/useNoteView.hook"
import { useResizeContainer } from "@/hooks/useResizeContainer.hook" import { useResizeContainer } from "@/hooks/useResizeContainer.hook"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook" import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
@@ -59,15 +60,10 @@ const renderedContent = computed(() =>
const isLoading = computed(() => renderedContent.value === undefined) const isLoading = computed(() => renderedContent.value === undefined)
const hasContent = computed(() => !!renderedContent.value) const hasContent = computed(() => !!renderedContent.value)
watch( useMarkdownPostRender(renderedContent, () => ".note-display", {
renderedContent, onReady: () => listenToClick(),
async () => { tikz: true
await nextTick() })
listenToClick()
void runTikz(".note-display .tikz")
},
{ immediate: true }
)
watch( watch(
[refProps.user, refProps.repo], [refProps.user, refProps.repo],

View File

@@ -2,7 +2,6 @@
import { import {
computed, computed,
defineAsyncComponent, defineAsyncComponent,
nextTick,
onMounted, onMounted,
ref, ref,
watch watch
@@ -11,14 +10,9 @@ import {
import { useEditionMode } from "@/hooks/useEditionMode" import { useEditionMode } from "@/hooks/useEditionMode"
import { useFile } from "@/hooks/useFile.hook" import { useFile } from "@/hooks/useFile.hook"
import { useGitHubContent } from "@/hooks/useGitHubContent.hook" import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
import { useImages } from "@/hooks/useImages.hook"
import { useLinks } from "@/hooks/useLinks.hook" import { useLinks } from "@/hooks/useLinks.hook"
import { import { renderCodeFile } from "@/hooks/useMarkdown.hook"
renderCodeFile, import { useMarkdownPostRender } from "@/hooks/useMarkdownPostRender.hook"
runMermaid,
runTikz,
useShikiji
} from "@/hooks/useMarkdown.hook"
import { useNoteFreshness } from "@/hooks/useNoteFreshness.hook" import { useNoteFreshness } from "@/hooks/useNoteFreshness.hook"
import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook" import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook" import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
@@ -145,30 +139,14 @@ watch(
const { mode, toggleMode } = useEditionMode() const { mode, toggleMode } = useEditionMode()
watch([content, mode], () => { useMarkdownPostRender(content, () => `.note-${sha.value}`, {
if (!content.value) { onReady: () => listenToClick(),
return tikz: true,
} mermaid: () => rawContent.value.includes("```mermaid"),
shikiji: () => isMarkdown.value && rawContent.value.includes("```"),
nextTick(() => { images: () =>
listenToClick() /\!\[.*?\]\(.*?\)/.test(rawContent.value) ? props.sha : null,
triggers: [mode]
if (/\!\[.*?\]\(.*?\)/.test(rawContent.value)) {
useImages(props.sha)
}
if (rawContent.value.includes("```mermaid")) {
runMermaid(`.note-${sha.value} .mermaid`)
}
if (rawContent.value.includes("```tikz")) {
void runTikz(`.note-${sha.value} .tikz`)
}
if (isMarkdown.value && rawContent.value.includes("```")) {
useShikiji()
}
})
}) })
const performSave = async (overrideSha?: string) => { const performSave = async (overrideSha?: string) => {

View File

@@ -1,11 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computedAsync } from "@vueuse/core" import { computedAsync } from "@vueuse/core"
import { computed, nextTick, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import { useRoute } from "vue-router" import { useRoute } from "vue-router"
import SkeletonLoader from "@/components/SkeletonLoader.vue" import SkeletonLoader from "@/components/SkeletonLoader.vue"
import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook" import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook"
import { markdownBuilder, runTikz } from "@/hooks/useMarkdown.hook" import { markdownBuilder } from "@/hooks/useMarkdown.hook"
import { useMarkdownPostRender } from "@/hooks/useMarkdownPostRender.hook"
import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook" import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook" import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
import { getAuthor } from "@/modules/atproto/getAuthor" import { getAuthor } from "@/modules/atproto/getAuthor"
@@ -76,15 +77,10 @@ const content = computed(() =>
: "" : ""
) )
watch( useMarkdownPostRender(content, () => `.note-${classNameId.value}`, {
content, onReady: () => listenToClick(),
async () => { tikz: true
await nextTick() })
listenToClick()
void runTikz(`.note-${classNameId.value} .tikz`)
},
{ immediate: true }
)
</script> </script>
<template> <template>

View File

@@ -0,0 +1,54 @@
import { nextTick, type Ref, toValue, watch } from "vue"
import { useImages } from "@/hooks/useImages.hook"
import {
runMermaid,
runTikz,
useShikiji
} from "@/hooks/useMarkdown.hook"
interface MarkdownPostRenderOptions {
onReady?: () => void
tikz?: boolean
mermaid?: () => boolean
shikiji?: () => boolean
images?: () => string | null | undefined
triggers?: Ref<unknown>[]
}
export const useMarkdownPostRender = (
contentRef: Ref<unknown>,
scopeSelector: () => string,
options: MarkdownPostRenderOptions = {}
) => {
const sources = [contentRef, ...(options.triggers ?? [])]
watch(
sources,
async () => {
if (!toValue(contentRef)) return
await nextTick()
options.onReady?.()
const scope = scopeSelector()
if (options.tikz) {
void runTikz(`${scope} .tikz`)
}
if (options.mermaid?.()) {
runMermaid(`${scope} .mermaid`)
}
if (options.shikiji?.()) {
void useShikiji()
}
const imagesSha = options.images?.()
if (imagesSha) {
useImages(imagesSha)
}
},
{ immediate: true }
)
}

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computedAsync } from "@vueuse/core" import { computedAsync } from "@vueuse/core"
import { useTitle } from "@vueuse/core" import { useTitle } from "@vueuse/core"
import { computed, nextTick, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import { useRouter } from "vue-router" import { useRouter } from "vue-router"
import HomeButton from "@/components/HomeButton.vue" import HomeButton from "@/components/HomeButton.vue"
@@ -9,7 +9,8 @@ import SkeletonLoader from "@/components/SkeletonLoader.vue"
import StackedPublicNote from "@/components/StackedPublicNote.vue" import StackedPublicNote from "@/components/StackedPublicNote.vue"
import ThemeSwap from "@/components/ThemeSwap.vue" import ThemeSwap from "@/components/ThemeSwap.vue"
import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook" import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook"
import { markdownBuilder, runTikz } from "@/hooks/useMarkdown.hook" import { markdownBuilder } from "@/hooks/useMarkdown.hook"
import { useMarkdownPostRender } from "@/hooks/useMarkdownPostRender.hook"
import { useResizeContainer } from "@/hooks/useResizeContainer.hook" import { useResizeContainer } from "@/hooks/useResizeContainer.hook"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook" import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
import { getAuthor } from "@/modules/atproto/getAuthor" import { getAuthor } from "@/modules/atproto/getAuthor"
@@ -115,15 +116,10 @@ const { stackedNotes, scrollToFocusedNote } = useRouteQueryStackedNotes()
const { listenToClick } = useATProtoLinks("note-display", { mainNoteId }) const { listenToClick } = useATProtoLinks("note-display", { mainNoteId })
useResizeContainer("note-container", stackedNotes) useResizeContainer("note-container", stackedNotes)
watch( useMarkdownPostRender(content, () => ".public-note-view .note-display", {
content, onReady: () => listenToClick(),
async () => { tikz: true
await nextTick() })
listenToClick()
void runTikz(".public-note-view .note-display .tikz")
},
{ immediate: true }
)
</script> </script>
<template> <template>