Merge branch 'main' into shuuji3/feat/emoji-reactions

pull/3033/head
TAKAHASHI Shuuji 2025-08-14 02:01:10 +09:00 zatwierdzone przez GitHub
commit e174f23b66
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
24 zmienionych plików z 4643 dodań i 2374 usunięć

Wyświetl plik

@ -1,6 +1,6 @@
<script setup lang="ts">
const { as = 'div', active } = defineProps<{
as: any
as?: string
active: boolean
}>()

Wyświetl plik

@ -35,15 +35,16 @@ const containerClass = computed(() => {
'backdrop-blur': !getPreferences(userSettings, 'optimizeForLowPerformanceDevice'),
}"
>
<div flex justify-between px5 py2 :class="{ 'xl:hidden': $route.name !== 'tag' }" class="native:xl:flex" border="b base">
<div flex gap-3 items-center :overflow-hidden="!noOverflowHidden ? '' : false" py2 w-full>
<NuxtLink
v-if="backOnSmallScreen || back" flex="~ gap1" items-center btn-text p-0 xl:hidden
<div flex justify-between gap-2 min-h-53px px5 py1 :class="{ 'xl:hidden': $route.name !== 'tag' }" class="native:xl:flex" border="b base">
<div flex gap-2 items-center :overflow-hidden="!noOverflowHidden ? '' : false" w-full>
<button
v-if="backOnSmallScreen || back"
btn-text flex items-center ms="-3" p-3 xl:hidden
:aria-label="$t('nav.back')"
@click="$router.go(-1)"
>
<div i-ri:arrow-left-line class="rtl-flip" />
</NuxtLink>
<div text-lg i-ri:arrow-left-line class="rtl-flip" />
</button>
<div :truncate="!noOverflowHidden ? '' : false" flex w-full data-tauri-drag-region class="native-mac:justify-start native-mac:text-center">
<slot name="title" />
</div>

Wyświetl plik

@ -33,17 +33,16 @@ router.afterEach(() => {
{{ $t('app_name') }} <sup text-sm italic mt-1>{{ env === 'release' ? 'alpha' : env }}</sup>
</div>
</NuxtLink>
<div
hidden xl:flex items-center me-8 mt-2 gap-1
>
<CommonTooltip :content="$t('nav.back')">
<NuxtLink
<div hidden xl:flex items-center me-6 mt-2 gap-1>
<CommonTooltip :content="$t('nav.back')" :distance="0">
<button
type="button"
:aria-label="$t('nav.back')"
:class="{ 'pointer-events-none op0': !back || back === '/', 'xl:flex': $route.name !== 'tag' }"
btn-text p-3 :class="{ 'pointer-events-none op0': !back || back === '/', 'xl:flex': $route.name !== 'tag' }"
@click="$router.go(-1)"
>
<div text-xl i-ri:arrow-left-line class="rtl-flip" btn-text />
</NuxtLink>
<div text-xl i-ri:arrow-left-line class="rtl-flip" />
</button>
</CommonTooltip>
</div>
</div>

Wyświetl plik

@ -29,7 +29,7 @@ const emit = defineEmits<{
const { t } = useI18n()
const { threadItems, threadIsActive, publishThread } = threadComposer ?? useThreadComposer(draftKey)
const { threadItems, threadIsActive, publishThread, threadIsSending } = threadComposer ?? useThreadComposer(draftKey)
const draft = computed({
get: () => threadItems.value[draftItemIndex],
@ -236,6 +236,59 @@ function stopQuestionMarkPropagation(e: KeyboardEvent) {
if (e.key === '?')
e.stopImmediatePropagation()
}
const userSettings = useUserSettings()
const optimizeForLowPerformanceDevice = computed(() => getPreferences(userSettings.value, 'optimizeForLowPerformanceDevice'))
const languageDetectorInGlobalThis = 'LanguageDetector' in globalThis
let supportsLanguageDetector = !optimizeForLowPerformanceDevice.value && languageDetectorInGlobalThis && await (globalThis as any).LanguageDetector.availability() === 'available'
let languageDetector: { detect: (arg0: string, option: { signal: AbortSignal }) => any }
// If the API is supported, but the model not loaded yet
if (languageDetectorInGlobalThis && !supportsLanguageDetector) {
// trigger the model download
(globalThis as any).LanguageDetector.create().then((_languageDetector: { detect: (arg0: string) => any }) => {
supportsLanguageDetector = true
languageDetector = _languageDetector
})
}
function countLetters(text: string) {
const segmenter = new Intl.Segmenter('und', { granularity: 'grapheme' })
const letters = [...segmenter.segment(text)]
return letters.length
}
let detectLanguageAbortController = new AbortController()
const detectLanguage = useDebounceFn(async () => {
if (!supportsLanguageDetector) {
return
}
if (!languageDetector) {
// maybe we dont want to mess with this with abort....
languageDetector = await (globalThis as any).LanguageDetector.create()
}
// we stop previously running language detection process
detectLanguageAbortController.abort()
detectLanguageAbortController = new AbortController()
const text = htmlToText(editor.value?.getHTML() || '')
if (!text || countLetters(text) <= 5) {
draft.value.params.language = preferredLanguage.value
return
}
try {
const detectedLanguage = (await languageDetector.detect(text, { signal: detectLanguageAbortController.signal }))[0].detectedLanguage
draft.value.params.language = detectedLanguage === 'und' ? preferredLanguage.value : detectedLanguage.substring(0, 2)
}
catch (e) {
// if error or abort we end up there
if ((e as Error).name !== 'AbortError') {
console.error(e)
}
draft.value.params.language = preferredLanguage.value
}
}, 500)
</script>
<template>
@ -310,6 +363,7 @@ function stopQuestionMarkPropagation(e: KeyboardEvent) {
}"
@keydown="stopQuestionMarkPropagation"
@keydown.esc.prevent="editor?.commands.blur()"
@keyup="detectLanguage"
/>
</div>
@ -523,18 +577,18 @@ function stopQuestionMarkPropagation(e: KeyboardEvent) {
<button
v-if="!threadIsActive || isFinalItemOfThread"
btn-solid rounded-3 text-sm w-full flex="~ gap1" items-center md:w-fit class="publish-button"
:aria-disabled="isPublishDisabled || isExceedingCharacterLimit" aria-describedby="publish-tooltip"
:disabled="isPublishDisabled || isExceedingCharacterLimit"
:aria-disabled="isPublishDisabled || isExceedingCharacterLimit || threadIsSending" aria-describedby="publish-tooltip"
:disabled="isPublishDisabled || isExceedingCharacterLimit || threadIsSending"
@click="publish"
>
<span v-if="isSending" block animate-spin preserve-3d>
<span v-if="isSending || threadIsSending" block animate-spin preserve-3d>
<div block i-ri:loader-2-fill />
</span>
<span v-if="failedMessages.length" block>
<div block i-carbon:face-dizzy-filled />
</span>
<template v-if="threadIsActive">
<span>{{ $t('action.publish_thread') }} </span>
<span>{{ !threadIsSending ? $t('action.publish_thread') : $t('state.publishing') }} </span>
</template>
<template v-else>
<span v-if="draft.editingStatus">{{ $t('action.save_changes') }}</span>

Wyświetl plik

@ -11,7 +11,7 @@ const {
withAction?: boolean
}>()
const { translation } = useTranslation(status, getLanguageCode())
const { translation } = await useTranslation(status, getLanguageCode())
const emojisObject = useEmojisFallback(() => status.emojis)
const vnode = computed(() => {

Wyświetl plik

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { mastodon } from 'masto'
const { actions = true, older, newer, hasOlder, hasNewer, main, ...props } = defineProps<{
const { actions = true, older, newer, hasOlder, hasNewer, main, account, ...props } = defineProps<{
status: mastodon.v1.Status
followedTag?: string | null
actions?: boolean
@ -20,6 +20,7 @@ const { actions = true, older, newer, hasOlder, hasNewer, main, ...props } = def
// When looking into a detailed view of a post, we can simplify the replying badges
// to the main expanded post
main?: mastodon.v1.Status
account?: mastodon.v1.Account
}>()
const userSettings = useUserSettings()
@ -60,7 +61,10 @@ const timeago = useTimeAgo(() => status.value.createdAt, timeAgoOptions)
const isSelfReply = computed(() => status.value.inReplyToAccountId === status.value.account.id)
const collapseRebloggedBy = computed(() => rebloggedBy.value?.id === status.value.account.id)
const isDM = computed(() => status.value.visibility === 'direct')
const isPinned = computed(() => status.value.pinned)
const isPinned = computed(
() =>
!!props.status.pinned && account?.id === status.value.account.id,
)
const showUpperBorder = computed(() => newer && !directReply.value)
const showReplyTo = computed(() => !replyToMain.value && !directReply.value)

Wyświetl plik

@ -9,7 +9,7 @@ const {
toggle: _toggleTranslation,
translation,
enabled: isTranslationEnabled,
} = useTranslation(status, getLanguageCode())
} = await useTranslation(status, getLanguageCode())
const preferenceHideTranslation = usePreferences('hideTranslation')
const showButton = computed(() =>

Wyświetl plik

@ -39,11 +39,11 @@ function getFollowedTag(status: mastodon.v1.Status): string | null {
<template #default="{ item, older, newer, active }">
<template v-if="virtualScroller">
<DynamicScrollerItem :item="item" :active="active" tag="article">
<StatusCard :followed-tag="getFollowedTag(item)" :status="item" :context="context" :older="older" :newer="newer" />
<StatusCard :followed-tag="getFollowedTag(item)" :status="item" :context="context" :older="older" :newer="newer" :account="account" />
</DynamicScrollerItem>
</template>
<template v-else>
<StatusCard :followed-tag="getFollowedTag(item)" :status="item" :context="context" :older="older" :newer="newer" />
<StatusCard :followed-tag="getFollowedTag(item)" :status="item" :context="context" :older="older" :newer="newer" :account="account" />
</template>
</template>
<template v-if="context === 'account' " #done="{ items }">

Wyświetl plik

@ -104,11 +104,12 @@ export function parseMastodonHTML(
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/`/g, '&#96;')
.replace(/\*/g, '&ast;')
const classes = lang ? ` class="language-${lang}"` : ''
return `><pre><code${classes}>${code}</code></pre>`
})
.replace(/`([^`\n]*)`/g, (_1, raw) => {
return raw ? `<code>${htmlToText(raw).replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code>` : ''
return raw ? `<code>${htmlToText(raw).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\*/g, '&ast;')}</code>` : ''
})
}

Wyświetl plik

@ -72,7 +72,7 @@ export const statusVisibilities = [
},
{
value: 'unlisted',
icon: 'i-ri:lock-unlock-line',
icon: 'i-ri:moon-line',
},
{
value: 'private',

Wyświetl plik

@ -95,9 +95,9 @@ export async function toggleMuteAccount(relationship: mastodon.v1.Relationship,
relationship!.muting = !relationship!.muting
relationship = relationship!.muting
? await client.value.v1.accounts.$select(account.id).mute({
duration,
notifications,
})
duration,
notifications,
})
: await client.value.v1.accounts.$select(account.id).unmute()
}

Wyświetl plik

@ -42,6 +42,10 @@ export const supportedTranslationCodes = [
'zh',
] as const
const translationAPISupported = 'Translator' in globalThis && 'LanguageDetector' in globalThis
const anchorMarkupRegEx = /<a[^>]*>.*?<\/a>/g
export function getLanguageCode() {
let code = 'en'
const getCode = (code: string) => code.replace(/-.*$/, '')
@ -58,6 +62,13 @@ interface TranslationErr {
}
}
function replaceTranslatedLinksWithOriginal(text: string) {
return text.replace(anchorMarkupRegEx, (match) => {
const tagLink = anchorMarkupRegEx.exec(text)
return tagLink ? tagLink[0] : match
})
}
export async function translateText(text: string, from: string | null | undefined, to: string) {
const config = useRuntimeConfig()
const status = ref({
@ -65,7 +76,6 @@ export async function translateText(text: string, from: string | null | undefine
error: '',
text: '',
})
const regex = /<a[^>]*>.*?<\/a>/g
try {
const response = await ($fetch as any)(config.public.translateApi, {
method: 'POST',
@ -78,11 +88,7 @@ export async function translateText(text: string, from: string | null | undefine
},
}) as TranslationResponse
status.value.success = true
// replace the translated links with the original
status.value.text = response.translatedText.replace(regex, (match) => {
const tagLink = regex.exec(text)
return tagLink ? tagLink[0] : match
})
status.value.text = replaceTranslatedLinksWithOriginal(response.translatedText)
}
catch (err) {
// TODO: improve type
@ -102,17 +108,27 @@ const translations = new WeakMap<mastodon.v1.Status | mastodon.v1.StatusEdit, {
error: string
}>()
export function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEdit, to: string) {
export async function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEdit, to: string) {
if (!translations.has(status))
translations.set(status, reactive({ visible: false, text: '', success: false, error: '' }))
const translation = translations.get(status)!
const userSettings = useUserSettings()
const shouldTranslate = 'language' in status && status.language && status.language !== to
&& supportedTranslationCodes.includes(to as any)
&& supportedTranslationCodes.includes(status.language as any)
&& !userSettings.value.disabledTranslationLanguages.includes(status.language)
let shouldTranslate = false
if ('language' in status) {
shouldTranslate = typeof status.language === 'string' && status.language !== to && !userSettings.value.disabledTranslationLanguages.includes(status.language)
if (!translationAPISupported) {
shouldTranslate = shouldTranslate && supportedTranslationCodes.includes(to as any)
&& supportedTranslationCodes.includes(status.language as any)
}
else {
shouldTranslate = shouldTranslate && (await (globalThis as any).Translator.availability({
sourceLanguage: status.language,
targetLanguage: to,
})) !== 'unavailable'
}
}
const enabled = /*! !useRuntimeConfig().public.translateApi && */ shouldTranslate
async function toggle() {
@ -120,12 +136,57 @@ export function useTranslation(status: mastodon.v1.Status | mastodon.v1.StatusEd
return
if (!translation.text) {
const translated = await translateText(status.content, status.language, to)
let translated = {
value: {
error: '',
text: '',
success: false,
},
}
if (translationAPISupported && 'language' in status) {
let sourceLanguage = status.language
if (!sourceLanguage) {
const languageDetector = await (globalThis as any).LanguageDetector.create()
// Make sure HTML markup doesn't derail language detection.
const div = document.createElement('div')
div.innerHTML = status.content
// eslint-disable-next-line unicorn/prefer-dom-node-text-content
const detectedLanguages = await languageDetector.detect(div.innerText)
sourceLanguage = detectedLanguages[0].detectedLanguage
if (sourceLanguage === 'und') {
throw new Error('Could not detect source language.')
}
}
const translator = await (globalThis as any).Translator.create({
sourceLanguage,
targetLanguage: to,
})
try {
let text = await translator.translate(status.content)
text = replaceTranslatedLinksWithOriginal(text)
translated.value = {
error: '',
text,
success: true,
}
}
catch (error) {
translated.value = {
error: (error as Error).message,
text: '',
success: false,
}
}
}
else {
if ('language' in status) {
translated = await translateText(status.content, status.language, to)
}
}
translation.error = translated.value.error
translation.text = translated.value.text
translation.success = translated.value.success
}
translation.visible = !translation.visible
}

Wyświetl plik

@ -11,6 +11,8 @@ export function useThreadComposer(draftKey: string, initial?: () => DraftItem) {
*/
const threadIsActive = computed<boolean>(() => draftItems.value.length > 1)
const threadIsSending = ref(false)
/**
* Add an item to the thread
*/
@ -44,6 +46,7 @@ export function useThreadComposer(draftKey: string, initial?: () => DraftItem) {
async function publishThread() {
const allFailedMessages: Array<string> = []
const isAReplyThread = Boolean(draftItems.value[0].params.inReplyToId)
threadIsSending.value = true
let lastPublishedStatus: mastodon.v1.Status | null = null
let amountPublished = 0
@ -72,6 +75,7 @@ export function useThreadComposer(draftKey: string, initial?: () => DraftItem) {
}
// Remove all published items from the thread
draftItems.value.splice(0, amountPublished)
threadIsSending.value = false
// If we have errors, return them
if (allFailedMessages.length > 0)
@ -90,5 +94,6 @@ export function useThreadComposer(draftKey: string, initial?: () => DraftItem) {
addThreadItem,
removeThreadItem,
publishThread,
threadIsSending,
}
}

Wyświetl plik

@ -10,6 +10,6 @@ catch (err) {
<template>
<MainContent text-base grid gap-3 m3>
<img rounded-3 :src="instance.thumbnail.url">
<img v-if="instance !== undefined" rounded-3 :src="instance.thumbnail.url">
</MainContent>
</template>

Wyświetl plik

@ -13,6 +13,6 @@
},
"devDependencies": {
"@nuxt-themes/docus": "^1.15.1",
"nuxt": "^3.17.3"
"nuxt": "^3.18.1"
}
}

Wyświetl plik

@ -165,7 +165,8 @@
},
"preferences": {
"grayscale_mode": "Tema en escala de grises",
"hide_username_emojis_description": "Oculta, de la historia, los emojis en los nombres de usuario. Los emojis seguirán visibles en los perfiles."
"hide_username_emojis_description": "Oculta, de la historia, los emojis en los nombres de usuario. Los emojis seguirán visibles en los perfiles.",
"unmute_videos": "Sonido de video activado por defecto"
},
"profile": {
"appearance": {

Wyświetl plik

@ -226,7 +226,9 @@
"manage": "Administrar listas",
"modify_account": "Modificar listas con cuenta",
"remove_account": "Eliminar cuenta de la lista",
"save": "Guardar"
"save": "Guardar",
"search_following_desc": "Buscar personas a las que sigues",
"search_following_placeholder": "Buscar entre las personas a las que sigues"
},
"magic_keys": {
"dialog_header": "Atajos de teclado",
@ -334,10 +336,12 @@
"zen_mode": "Modo Zen"
},
"notification": {
"and": "y",
"favourited_post": "marcó como favorita tu publicación",
"followed_you": "te ha seguido",
"followed_you_count": "{0} personas te siguieron|{0} persona te siguió|{0} personas te siguieron",
"missing_type": "MISSING notification.type:",
"others": "{0} personas|{0} persona|{0} personas",
"reblogged_post": "retooteó tu publicación",
"reported": "{0} reportó {1}",
"request_to_follow": "ha solicitado seguirte",
@ -559,6 +563,7 @@
"label": "Preferencias",
"optimize_for_low_performance_device": "Optimizar para dispositivos de bajo rendimiento",
"title": "Funcionalidades experimentales",
"unmute_videos": "Sonido de vídeo activado por defecto",
"use_star_favorite_icon": "Utilizar icono de estrella para favoritos",
"user_picker": "Selector de usuarios",
"user_picker_description": "Muestra todos los avatares de las cuentas registradas en la parte inferior izquierda para que puedas cambiar rápidamente entre ellos.",
@ -637,7 +642,8 @@
"poll": {
"count": "{0} votos|{0} voto|{0} votos",
"ends": "finaliza {0}",
"finished": "finalizada {0}"
"finished": "finalizada {0}",
"update": "Actualizar encuesta"
},
"replying_to": "Respondiendo a {0}",
"show_full_thread": "Mostrar hilo completo",

Wyświetl plik

@ -1,5 +1,4 @@
import type { Ref } from 'vue'
import type { UnwrapNestedRefs } from 'vue'
import type { Ref, UnwrapNestedRefs } from 'vue'
export interface PwaInjection {
isInstalled: boolean

Wyświetl plik

@ -68,7 +68,7 @@
"@vueuse/motion": "2.2.6",
"@vueuse/nuxt": "^13.2.0",
"blurhash": "^2.0.5",
"browser-fs-access": "^0.35.0",
"browser-fs-access": "^0.38.0",
"cheerio": "^1.0.0",
"chroma-js": "^3.0.0",
"emoji-mart": "^5.5.2",
@ -117,7 +117,7 @@
"ws": "^8.15.1"
},
"devDependencies": {
"@antfu/eslint-config": "^4.13.1",
"@antfu/eslint-config": "^5.1.0",
"@antfu/ni": "^24.4.0",
"@types/chroma-js": "^3.1.1",
"@types/file-saver": "^2.0.7",
@ -127,23 +127,23 @@
"@types/wicg-file-system-access": "^2023.10.6",
"@types/ws": "^8.18.1",
"@unlazy/nuxt": "^0.12.4",
"@unocss/eslint-config": "^66.1.2",
"@unocss/eslint-config": "^66.4.1",
"@vue/test-utils": "2.4.6",
"bumpp": "^10.1.1",
"bumpp": "^10.2.2",
"consola": "^3.4.2",
"eslint": "^9.27.0",
"eslint": "^9.32.0",
"eslint-plugin-format": "^1.0.1",
"flat": "^6.0.1",
"fs-extra": "^11.3.0",
"lint-staged": "^15.5.2",
"nuxt": "^3.17.3",
"prettier": "^3.5.3",
"sharp": "^0.34.1",
"nuxt": "^3.18.1",
"prettier": "^3.6.2",
"sharp": "^0.34.3",
"sharp-ico": "^0.1.5",
"simple-git-hooks": "^2.13.0",
"tsx": "^4.19.4",
"simple-git-hooks": "^2.13.1",
"tsx": "^4.20.3",
"typescript": "^5.4.4",
"vitest": "3.1.3",
"vitest": "3.2.4",
"vue-tsc": "^2.1.6"
},
"pnpm": {
@ -152,9 +152,9 @@
}
},
"resolutions": {
"nuxt-component-meta": "0.11.0",
"unstorage": "^1.16.0",
"vitest": "3.1.3",
"nuxt-component-meta": "0.13.0",
"unstorage": "^1.16.1",
"vitest": "3.2.4",
"vue": "^3.5.4"
},
"simple-git-hooks": {

28
page-lifecycle.d.ts vendored
Wyświetl plik

@ -1,17 +1,17 @@
declare module 'page-lifecycle/dist/lifecycle.mjs' {
type PageLifecycleState = 'active' | 'passive' | 'hidden' | 'frozen' | 'terminated'
type PageLifecycleState = 'active' | 'passive' | 'hidden' | 'frozen' | 'terminated'
interface PageLifecycleEvent extends Event {
newState: PageLifecycleState
oldState: PageLifecycleState
}
interface PageLifecycle extends EventTarget {
get state(): PageLifecycleState
get pageWasDiscarded(): boolean
addUnsavedChanges: (id: symbol | any) => void
removeUnsavedChanges: (id: symbol | any) => void
addEventListener: (type: string, listener: (evt: PageLifecycleEvent) => void) => void
}
const lifecycle: PageLifecycle
export default lifecycle
interface PageLifecycleEvent extends Event {
newState: PageLifecycleState
oldState: PageLifecycleState
}
interface PageLifecycle extends EventTarget {
get state(): PageLifecycleState
get pageWasDiscarded(): boolean
addUnsavedChanges: (id: symbol | any) => void
removeUnsavedChanges: (id: symbol | any) => void
addEventListener: (type: string, listener: (evt: PageLifecycleEvent) => void) => void
}
const lifecycle: PageLifecycle
export default lifecycle
}

Plik diff jest za duży Load Diff

Wyświetl plik

@ -60,7 +60,7 @@ async function fetchAppInfo(origin: string, server: string) {
},
body: {
client_name: APP_NAME + (env !== 'release' ? ` (${env})` : ''),
website: 'https://elk.zone',
website: origin,
redirect_uris: getRedirectURI(origin, server),
scopes: 'read write follow push',
},

Wyświetl plik

@ -1,5 +1,12 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`content-rich > asterisk paris in code block 1`] = `"<p><pre class="code-block">1 * 2 * 3</pre></p>"`;
exports[`content-rich > asterisk paris in inline code 1`] = `
"<p><code>1 * 2 * 3</code></p>
"
`;
exports[`content-rich > block with backticks 1`] = `"<p><pre class="code-block">[(\`number string) (\`tag string)]</pre></p>"`;
exports[`content-rich > block with injected html, with a known language 1`] = `

Wyświetl plik

@ -186,6 +186,16 @@ describe('content-rich', () => {
`)
expect(formatted).toMatchSnapshot()
})
it ('asterisk paris in inline code', async () => {
const { formatted } = await render('<p>`1 * 2 * 3`</p>')
expect(formatted).toMatchSnapshot()
})
it ('asterisk paris in code block', async () => {
const { formatted } = await render('<p>```<br />1 * 2 * 3<br />```</p>')
expect(formatted).toMatchSnapshot()
})
})
describe('editor', () => {