@@ -2747,6 +2747,10 @@ defineExpose({
|
||||
* @returns {void}
|
||||
*/
|
||||
const closeMediaPicker = () => {
|
||||
if (isUploading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isMediaPickerOpen.value = false
|
||||
selectedMediaUrls.value = []
|
||||
activeMediaPickerTab.value = 'library'
|
||||
@@ -2999,7 +3003,7 @@ const handleFileInput = async (event, target) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 모달 업로드 탭에서 파일을 삽입한다.
|
||||
* 미디어 모달 업로드 탭에서 파일을 업로드하고 라이브러리 목록을 갱신한다.
|
||||
* @param {FileList|Array<File>} files - 업로드 파일 목록
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@@ -3008,10 +3012,6 @@ const uploadFromMediaModal = async (files) => {
|
||||
return
|
||||
}
|
||||
|
||||
const target = mediaPickerTarget.value === 'gallery' || mediaPickerTarget.value === 'active-gallery'
|
||||
? 'gallery'
|
||||
: mediaPickerTarget.value
|
||||
|
||||
isUploading.value = true
|
||||
|
||||
try {
|
||||
@@ -3020,8 +3020,10 @@ const uploadFromMediaModal = async (files) => {
|
||||
...uploadedFiles,
|
||||
...mediaItems.value
|
||||
])
|
||||
insertSelectedMediaItems(target === 'gallery' ? uploadedFiles : uploadedFiles.slice(0, 1))
|
||||
closeMediaPicker()
|
||||
selectedMediaUrls.value = []
|
||||
activeMediaPickerTab.value = 'library'
|
||||
mediaSearchQuery.value = ''
|
||||
showToast('success', '업로드가 완료되었습니다. 목록에서 파일을 선택해 삽입해 주세요.')
|
||||
} catch (error) {
|
||||
showToast('error', resolveUploadFetchErrorMessage(error))
|
||||
} finally {
|
||||
@@ -3360,7 +3362,12 @@ const handleKeydown = (event) => {
|
||||
{{ selectedMediaUrls.length }}개 선택됨
|
||||
</p>
|
||||
</div>
|
||||
<button class="admin-markdown-editor__media-close rounded px-3 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="closeMediaPicker">
|
||||
<button
|
||||
class="admin-markdown-editor__media-close rounded px-3 py-1.5 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2] disabled:cursor-not-allowed disabled:opacity-40"
|
||||
type="button"
|
||||
:disabled="isUploading"
|
||||
@click="closeMediaPicker"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
</header>
|
||||
@@ -3483,13 +3490,18 @@ const handleKeydown = (event) => {
|
||||
</div>
|
||||
|
||||
<footer v-if="activeMediaPickerTab === 'library'" class="admin-markdown-editor__media-footer flex items-center justify-end gap-2 border-t border-[#e3e6e8] px-5 py-4">
|
||||
<button class="admin-markdown-editor__media-cancel rounded px-4 py-2 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2]" type="button" @click="closeMediaPicker">
|
||||
<button
|
||||
class="admin-markdown-editor__media-cancel rounded px-4 py-2 text-sm font-semibold text-[#394047] hover:bg-[#eff1f2] disabled:cursor-not-allowed disabled:opacity-40"
|
||||
type="button"
|
||||
:disabled="isUploading"
|
||||
@click="closeMediaPicker"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
class="admin-markdown-editor__media-apply rounded bg-[#15171a] px-4 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-40"
|
||||
type="button"
|
||||
:disabled="selectedMediaUrls.length === 0"
|
||||
:disabled="isUploading || selectedMediaUrls.length === 0"
|
||||
@click="applyMediaSelection"
|
||||
>
|
||||
삽입
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
toAdminPostStoredTitle
|
||||
} from '../../lib/admin-post-title.js'
|
||||
import { normalizeMarkdownContent } from '../../lib/markdown-content-normalizer.js'
|
||||
import { DEFAULT_POST_TAG_LIMIT, normalizePostTagLimit } from '../../lib/post-tag-limit.js'
|
||||
|
||||
const props = defineProps({
|
||||
initialPost: {
|
||||
@@ -83,6 +84,9 @@ const publishModalExpandedSection = ref(null)
|
||||
const { data: adminTags } = useFetch('/admin/api/tags', {
|
||||
default: () => []
|
||||
})
|
||||
const { data: siteSettings } = useFetch('/admin/api/settings', {
|
||||
default: () => ({ postTagLimit: DEFAULT_POST_TAG_LIMIT })
|
||||
})
|
||||
|
||||
const defaultTagColor = '#15171a'
|
||||
/** @type {number} 한국어 본문 예상 읽기 속도(분당 공백 제외 문자 수) */
|
||||
@@ -459,6 +463,10 @@ const selectedTags = computed(() => parseTags(form.tagsText))
|
||||
|
||||
const selectedTagKeys = computed(() => new Set(selectedTags.value.map((tag) => tag.toLowerCase())))
|
||||
|
||||
const postTagLimit = computed(() => normalizePostTagLimit(siteSettings.value?.postTagLimit))
|
||||
|
||||
const canAddMoreTags = computed(() => selectedTags.value.length < postTagLimit.value)
|
||||
|
||||
const availableAdminTags = computed(() => Array.isArray(adminTags.value) ? adminTags.value : [])
|
||||
|
||||
const managedTagOptions = computed(() => availableAdminTags.value.filter((tag) => tag.tagType === 'managed'))
|
||||
@@ -469,6 +477,9 @@ const tagSuggestionOptions = computed(() => {
|
||||
|
||||
return sourceTags
|
||||
.filter((tag) => {
|
||||
if (!canAddMoreTags.value) {
|
||||
return false
|
||||
}
|
||||
if (!tag?.slug || selectedTagKeys.value.has(tag.slug.toLowerCase())) {
|
||||
return false
|
||||
}
|
||||
@@ -794,6 +805,12 @@ const addTagToken = (value) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedTagKeys.value.has(nextTag.toLowerCase()) && !canAddMoreTags.value) {
|
||||
tagInput.value = ''
|
||||
isTagSuggestionsOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const nextTags = [...selectedTags.value]
|
||||
if (!selectedTagKeys.value.has(nextTag.toLowerCase())) {
|
||||
nextTags.push(nextTag)
|
||||
@@ -872,6 +889,11 @@ const getTagSuggestionMeta = (tag) => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const openTagSuggestions = () => {
|
||||
if (!canAddMoreTags.value) {
|
||||
isTagSuggestionsOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
isTagSuggestionsOpen.value = true
|
||||
activeTagSuggestionIndex.value = 0
|
||||
tagInputRef.value?.focus()
|
||||
@@ -882,6 +904,11 @@ const openTagSuggestions = () => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const toggleTagSuggestions = () => {
|
||||
if (!canAddMoreTags.value) {
|
||||
isTagSuggestionsOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (isTagSuggestionsOpen.value) {
|
||||
isTagSuggestionsOpen.value = false
|
||||
return
|
||||
@@ -1829,7 +1856,10 @@ defineExpose({
|
||||
<div class="admin-post-form__field grid gap-1 text-sm">
|
||||
<span class="admin-post-form__label h-[22px] font-bold text-[#15171a]">Tags</span>
|
||||
<div class="admin-post-form__tag-combobox relative">
|
||||
<div class="admin-post-form__tag-editor flex min-h-[38px] w-full flex-wrap items-center gap-2 rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-1 transition-colors hover:border-[#c8ced3] focus-within:border-[#8e9cac]">
|
||||
<div
|
||||
class="admin-post-form__tag-editor flex min-h-[38px] w-full flex-wrap items-center gap-2 rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-1 transition-colors hover:border-[#c8ced3] focus-within:border-[#8e9cac]"
|
||||
:class="!canAddMoreTags ? 'opacity-85' : ''"
|
||||
>
|
||||
<span
|
||||
v-for="tag in selectedTags"
|
||||
:key="tag"
|
||||
@@ -1853,7 +1883,8 @@ defineExpose({
|
||||
v-model="tagInput"
|
||||
class="admin-post-form__tag-input min-w-0 flex-1 border-0 bg-transparent p-0 text-sm text-black outline-none placeholder:text-[#8e9cac]"
|
||||
type="text"
|
||||
placeholder="태그 입력"
|
||||
:placeholder="canAddMoreTags ? '태그 입력' : `최대 ${postTagLimit}개까지 선택됨`"
|
||||
:disabled="!canAddMoreTags"
|
||||
role="combobox"
|
||||
:aria-expanded="hasTagSuggestions"
|
||||
aria-autocomplete="list"
|
||||
@@ -1868,6 +1899,7 @@ defineExpose({
|
||||
class="admin-post-form__tag-dropdown-trigger ml-auto inline-flex size-8 shrink-0 items-center justify-center rounded text-[#15171a] transition-colors hover:bg-[#e3e6e8] focus-visible:outline focus-visible:ring-2 focus-visible:ring-[#8e9cac]"
|
||||
type="button"
|
||||
:aria-expanded="hasTagSuggestions"
|
||||
:disabled="!canAddMoreTags"
|
||||
aria-label="메인 태그 목록 열기"
|
||||
@mousedown.prevent="toggleTagSuggestions"
|
||||
>
|
||||
@@ -1877,6 +1909,9 @@ defineExpose({
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-[#8e9cac]">
|
||||
{{ selectedTags.length }} / {{ postTagLimit }}개 선택됨
|
||||
</p>
|
||||
<div
|
||||
v-if="hasTagSuggestions"
|
||||
class="admin-post-form__tag-suggestions absolute left-0 right-0 top-full z-40 mt-1 max-h-64 overflow-y-auto rounded-lg border border-[#d7dce0] bg-white py-1 text-sm shadow-[0_18px_50px_rgba(15,23,42,0.14)]"
|
||||
|
||||
@@ -87,6 +87,12 @@ const props = defineProps({
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- table -->
|
||||
<template v-else-if="commandId === 'table'">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 5.5h16M4 11.5h16M4 17.5h16M8.5 5.5v12M15.5 5.5v12" />
|
||||
<rect x="3" y="4" width="18" height="15" rx="1.5" stroke="currentColor" stroke-width="1.8" />
|
||||
</template>
|
||||
|
||||
<!-- code -->
|
||||
<template v-else-if="commandId === 'code'">
|
||||
<path
|
||||
|
||||
@@ -12,6 +12,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
requireChanges: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
defaultTagType: {
|
||||
type: String,
|
||||
default: 'general'
|
||||
@@ -29,6 +33,58 @@ const form = reactive({
|
||||
color: props.initialTag.color || '#15171a'
|
||||
})
|
||||
|
||||
/**
|
||||
* 태그 입력값을 저장 비교용 형태로 정규화한다.
|
||||
* @param {Object} tag - 태그 입력값
|
||||
* @returns {{ name: string, slug: string, description: string, sortOrder: number, color: string, tagType: string }} 정규화된 태그 입력값
|
||||
*/
|
||||
const normalizeTagPayload = (tag) => ({
|
||||
name: String(tag.name || '').trim(),
|
||||
slug: toSlug(tag.slug || tag.name || ''),
|
||||
description: String(tag.description || '').trim(),
|
||||
sortOrder: Number(tag.sortOrder ?? 0),
|
||||
color: String(tag.color || '#15171a'),
|
||||
tagType: String(tag.tagType || props.defaultTagType)
|
||||
})
|
||||
|
||||
/**
|
||||
* 현재 폼 입력값
|
||||
* @returns {{ name: string, slug: string, description: string, sortOrder: number, color: string, tagType: string }} 현재 저장 입력값
|
||||
*/
|
||||
const currentPayload = computed(() => normalizeTagPayload({
|
||||
name: form.name,
|
||||
slug: form.slug || form.name,
|
||||
description: form.description,
|
||||
sortOrder: props.initialTag.sortOrder ?? 0,
|
||||
color: form.color,
|
||||
tagType: props.initialTag.tagType || props.defaultTagType
|
||||
}))
|
||||
|
||||
/**
|
||||
* 최초 태그 입력값
|
||||
* @returns {{ name: string, slug: string, description: string, sortOrder: number, color: string, tagType: string }} 최초 저장 입력값
|
||||
*/
|
||||
const initialPayload = computed(() => normalizeTagPayload({
|
||||
name: props.initialTag.name || '',
|
||||
slug: props.initialTag.slug || props.initialTag.name || '',
|
||||
description: props.initialTag.description || '',
|
||||
sortOrder: props.initialTag.sortOrder ?? 0,
|
||||
color: props.initialTag.color || '#15171a',
|
||||
tagType: props.initialTag.tagType || props.defaultTagType
|
||||
}))
|
||||
|
||||
/**
|
||||
* 태그 입력값 변경 여부
|
||||
* @returns {boolean} 변경 여부
|
||||
*/
|
||||
const hasChanges = computed(() => JSON.stringify(currentPayload.value) !== JSON.stringify(initialPayload.value))
|
||||
|
||||
/**
|
||||
* 태그 저장 가능 여부
|
||||
* @returns {boolean} 저장 가능 여부
|
||||
*/
|
||||
const canSubmit = computed(() => !props.saving && (!props.requireChanges || hasChanges.value))
|
||||
|
||||
/**
|
||||
* 문자열을 URL 슬러그로 변환
|
||||
* @param {string} value - 원본 문자열
|
||||
@@ -62,14 +118,11 @@ const touchSlug = () => {
|
||||
* @returns {void}
|
||||
*/
|
||||
const submitTag = () => {
|
||||
emit('submit', {
|
||||
name: form.name.trim(),
|
||||
slug: toSlug(form.slug || form.name),
|
||||
description: form.description.trim(),
|
||||
sortOrder: props.initialTag.sortOrder ?? 0,
|
||||
color: form.color,
|
||||
tagType: props.initialTag.tagType || props.defaultTagType
|
||||
})
|
||||
if (!canSubmit.value) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('submit', currentPayload.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -132,7 +185,7 @@ const submitTag = () => {
|
||||
<button
|
||||
class="admin-tag-form__submit rounded bg-[#15171a] px-4 py-2 text-sm font-semibold text-white disabled:opacity-50"
|
||||
type="submit"
|
||||
:disabled="saving"
|
||||
:disabled="!canSubmit"
|
||||
>
|
||||
{{ saving ? '저장 중' : submitLabel }}
|
||||
</button>
|
||||
|
||||
@@ -467,6 +467,90 @@ const parseMediaMeta = (raw) => {
|
||||
return meta
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 행 후보인지 확인한다.
|
||||
* @param {string} line - 원본 줄
|
||||
* @returns {boolean} 표 행 후보 여부
|
||||
*/
|
||||
const isMarkdownTableRow = (line) => {
|
||||
const value = String(line || '').trim()
|
||||
return value.startsWith('|') && value.endsWith('|') && value.split('|').length >= 4
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 구분선 행인지 확인한다.
|
||||
* @param {string} line - 원본 줄
|
||||
* @returns {boolean} 구분선 여부
|
||||
*/
|
||||
const isMarkdownTableSeparator = (line) => {
|
||||
if (!isMarkdownTableRow(line)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return splitMarkdownTableRow(line).every((cell) => /^:?-{3,}:?$/.test(cell.trim()))
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 행을 셀 배열로 나눈다.
|
||||
* @param {string} line - 원본 줄
|
||||
* @returns {string[]} 셀 목록
|
||||
*/
|
||||
const splitMarkdownTableRow = (line) => String(line || '')
|
||||
.trim()
|
||||
.replace(/^\|/, '')
|
||||
.replace(/\|$/, '')
|
||||
.split('|')
|
||||
.map((cell) => cell.trim().replace(/\\\|/g, '|'))
|
||||
|
||||
/**
|
||||
* 표 정렬 행에서 셀 정렬 값을 파싱한다.
|
||||
* @param {string} cell - 구분선 셀
|
||||
* @returns {'left'|'center'|'right'} 정렬 값
|
||||
*/
|
||||
const parseMarkdownTableAlignment = (cell) => {
|
||||
const value = String(cell || '').trim()
|
||||
|
||||
if (value.startsWith(':') && value.endsWith(':')) {
|
||||
return 'center'
|
||||
}
|
||||
|
||||
if (value.endsWith(':')) {
|
||||
return 'right'
|
||||
}
|
||||
|
||||
return 'left'
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 블록을 파싱한다.
|
||||
* @param {string[]} lines - 전체 마크다운 줄
|
||||
* @param {number} startIndex - 표 시작 후보 줄
|
||||
* @returns {{headers: string[], alignments: string[], rows: string[][], endLine: number, nextIndex: number}|null} 표 메타
|
||||
*/
|
||||
const parseMarkdownTableBlock = (lines, startIndex) => {
|
||||
if (!isMarkdownTableRow(lines[startIndex]) || !isMarkdownTableSeparator(lines[startIndex + 1])) {
|
||||
return null
|
||||
}
|
||||
|
||||
const headers = splitMarkdownTableRow(lines[startIndex])
|
||||
const alignments = splitMarkdownTableRow(lines[startIndex + 1]).map(parseMarkdownTableAlignment)
|
||||
const rows = []
|
||||
let index = startIndex + 2
|
||||
|
||||
while (index < lines.length && isMarkdownTableRow(lines[index]) && !isMarkdownTableSeparator(lines[index])) {
|
||||
rows.push(splitMarkdownTableRow(lines[index]))
|
||||
index += 1
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
alignments,
|
||||
rows,
|
||||
endLine: index - 1,
|
||||
nextIndex: index
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 문자열을 렌더링용 블록 목록으로 변환
|
||||
* @param {string} markdown - 마크다운 문자열
|
||||
@@ -697,6 +781,24 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
continue
|
||||
}
|
||||
|
||||
const tableBlock = parseMarkdownTableBlock(lines, index)
|
||||
if (tableBlock) {
|
||||
const startLine = index
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('table', '', null, `block-${blocks.length}`, {
|
||||
meta: {
|
||||
headers: tableBlock.headers,
|
||||
alignments: tableBlock.alignments,
|
||||
rows: tableBlock.rows
|
||||
}
|
||||
}),
|
||||
startLine,
|
||||
tableBlock.endLine
|
||||
))
|
||||
index = tableBlock.nextIndex
|
||||
continue
|
||||
}
|
||||
|
||||
if (isQuoteMarkerLine(line)) {
|
||||
const startLine = index
|
||||
const rawQuoteLines = []
|
||||
@@ -1283,6 +1385,23 @@ const getHeadingEditableClass = (level) => {
|
||||
return `${base} text-[clamp(0.9rem,0.875rem+0.1vw,1rem)]`
|
||||
}
|
||||
|
||||
/**
|
||||
* 표 셀 정렬 클래스를 반환한다.
|
||||
* @param {string} alignment - 정렬 값
|
||||
* @returns {string} Tailwind 정렬 클래스
|
||||
*/
|
||||
const getTableCellAlignClass = (alignment) => {
|
||||
if (alignment === 'center') {
|
||||
return 'text-center'
|
||||
}
|
||||
|
||||
if (alignment === 'right') {
|
||||
return 'text-right'
|
||||
}
|
||||
|
||||
return 'text-left'
|
||||
}
|
||||
|
||||
/**
|
||||
* 제목 블록 마크다운 줄을 만든다.
|
||||
* @param {number} level - 제목 레벨
|
||||
@@ -3332,6 +3451,55 @@ onBeforeUnmount(() => {
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="block.type === 'table'"
|
||||
class="content-markdown-renderer__table-wrap my-6 overflow-x-auto"
|
||||
:data-source-line="block.meta.startLine"
|
||||
:data-source-line-end="block.meta.endLine"
|
||||
>
|
||||
<table class="content-markdown-renderer__table w-full min-w-[520px] border-collapse text-sm text-[var(--site-text)]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="(header, headerIndex) in block.meta.headers"
|
||||
:key="`${block.id}-table-head-${headerIndex}`"
|
||||
class="border border-line bg-[color-mix(in_srgb,var(--site-text)_6%,transparent)] px-3 py-2 font-semibold"
|
||||
:class="getTableCellAlignClass(block.meta.alignments?.[headerIndex])"
|
||||
>
|
||||
<template v-for="(segment, segmentIndex) in parseInlineSegments(header)" :key="`${block.id}-table-head-${headerIndex}-${segmentIndex}`">
|
||||
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
|
||||
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
|
||||
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
|
||||
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
|
||||
<code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
|
||||
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
|
||||
<template v-else>{{ segment.text }}</template>
|
||||
</template>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, rowIndex) in block.meta.rows" :key="`${block.id}-table-row-${rowIndex}`">
|
||||
<td
|
||||
v-for="(_, cellIndex) in block.meta.headers"
|
||||
:key="`${block.id}-table-cell-${rowIndex}-${cellIndex}`"
|
||||
class="border border-line px-3 py-2 align-top"
|
||||
:class="getTableCellAlignClass(block.meta.alignments?.[cellIndex])"
|
||||
>
|
||||
<template v-for="(segment, segmentIndex) in parseInlineSegments(row[cellIndex] || '')" :key="`${block.id}-table-cell-${rowIndex}-${cellIndex}-${segmentIndex}`">
|
||||
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
|
||||
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
|
||||
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
|
||||
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
|
||||
<code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
|
||||
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
|
||||
<template v-else>{{ segment.text }}</template>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ContentMarkdownCodeBlockEditor
|
||||
v-else-if="block.type === 'code' && interactive"
|
||||
:language="block.codeLanguage"
|
||||
|
||||
Reference in New Issue
Block a user