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

View File

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

View File

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