게시물 HTML 블록 분리

This commit is contained in:
2026-06-26 15:23:19 +09:00
parent c77cf1032b
commit 8e3bf4a874
16 changed files with 233 additions and 101 deletions

View File

@@ -35,9 +35,9 @@ import {
parseCalloutOptions
} from '../../lib/markdown-callout.js'
import { createHeadingIdFactory } from '../../lib/markdown-toc.js'
import { isIframeEmbedMarkup } from '../../lib/markdown-iframe-embed.js'
import ContentMarkdownCodeBlockEditor from './ContentMarkdownCodeBlockEditor.vue'
import ProseCodeBlock from './ProseCodeBlock.vue'
import ProseHtml from './ProseHtml.vue'
import ContentMarkdownCalloutEditor from './ContentMarkdownCalloutEditor.vue'
import ContentMarkdownToggleEditor from './ContentMarkdownToggleEditor.vue'
@@ -253,13 +253,13 @@ const isMarkdownBlockStart = (line) => {
trimmedLine === ':::video' ||
trimmedLine === ':::audio' ||
trimmedLine === ':::file' ||
trimmedLine === ':::embed' ||
trimmedLine === ':::link' ||
trimmedLine === ':::iframe' ||
trimmedLine === ':::html' ||
trimmedLine.startsWith(':::callout') ||
trimmedLine.startsWith(':::toggle') ||
trimmedLine.startsWith('```') ||
trimmedLine === '---' ||
isStandaloneUrlLine(trimmedLine) ||
isIframeEmbedMarkup(trimmedLine) ||
/^(#{1,6})\s+(.+)$/.test(trimmedLine) ||
trimmedLine.startsWith('> ') ||
/^- /.test(trimmedLine) ||
@@ -689,11 +689,12 @@ const parseMarkdownBlocks = (markdown) => {
continue
}
if (trimmedLine === ':::embed') {
if ([':::link', ':::iframe', ':::html'].includes(trimmedLine)) {
const startLine = index
const blockType = trimmedLine.replace(':::', '')
const { contentLines, endLine, nextIndex } = collectFencedLines(lines, index + 1)
blocks.push(attachSourceRange(
createBlock('embed', '', null, `block-${blocks.length}`, { url: contentLines.join('\n').trim() }),
createBlock(blockType, '', null, `block-${blocks.length}`, { url: contentLines.join('\n').trim() }),
startLine,
endLine
))
@@ -711,28 +712,6 @@ const parseMarkdownBlocks = (markdown) => {
continue
}
if (isStandaloneUrlLine(trimmedLine)) {
const startLine = index
blocks.push(attachSourceRange(
createBlock('embed', '', null, `block-${blocks.length}`, { url: trimmedLine }),
startLine,
startLine
))
index += 1
continue
}
if (isIframeEmbedMarkup(trimmedLine)) {
const startLine = index
blocks.push(attachSourceRange(
createBlock('embed', '', null, `block-${blocks.length}`, { url: trimmedLine }),
startLine,
startLine
))
index += 1
continue
}
const image = parseImageLine(trimmedLine)
if (image) {
@@ -3282,27 +3261,29 @@ onBeforeUnmount(() => {
:size="block.meta.size"
/>
<section
v-else-if="block.type === 'embed' && interactive"
v-else-if="['link', 'iframe', 'html'].includes(block.type) && interactive"
class="content-markdown-renderer__embed-live group relative mb-8 rounded-[12px] outline-none transition-shadow focus-within:ring-2 focus-within:ring-[var(--site-accent)] focus-within:ring-offset-2 focus-visible:ring-2 focus-visible:ring-[var(--site-accent)] focus-visible:ring-offset-2"
:data-source-line="block.meta.startLine"
data-preview-card-block="true"
tabindex="0"
role="group"
aria-label="HTML 블록"
:aria-label="block.type === 'link' ? '링크 블록' : block.type === 'iframe' ? 'iframe 블록' : 'HTML 블록'"
@mousedown.capture="focusPreviewCardBlock"
@keydown="onPreviewCardKeydown($event, block)"
>
<button
class="content-markdown-renderer__embed-delete content-markdown-renderer__preview-card-action absolute right-2 top-2 z-10 rounded bg-[#15171a]/85 px-2.5 py-1 text-xs font-semibold text-white opacity-0 shadow transition-opacity hover:bg-[#15171a] focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-[var(--site-accent)] group-hover:opacity-100 group-focus-within:opacity-100"
type="button"
aria-label="HTML 블록 삭제"
:aria-label="block.type === 'link' ? '링크 블록 삭제' : block.type === 'iframe' ? 'iframe 블록 삭제' : 'HTML 블록 삭제'"
@click.stop="deletePreviewCardBlock(block)"
>
삭제
</button>
<ProseEmbed :url="block.url" />
<ProseHtml v-if="block.type === 'html'" :html="block.url" />
<ProseEmbed v-else :url="block.url" />
</section>
<ProseEmbed v-else-if="block.type === 'embed'" :url="block.url" />
<ProseHtml v-else-if="block.type === 'html'" :html="block.url" />
<ProseEmbed v-else-if="block.type === 'link' || block.type === 'iframe'" :url="block.url" />
<div
v-else-if="block.type === 'gallery'"
class="content-markdown-renderer__gallery group relative my-8 flex flex-col gap-2 rounded-[12px] outline-none transition-shadow focus-within:ring-2 focus-within:ring-[var(--site-accent)] focus-within:ring-offset-2 focus-visible:ring-2 focus-visible:ring-[var(--site-accent)] focus-visible:ring-offset-2"

View File

@@ -0,0 +1,54 @@
<script setup>
import { sanitizeHtmlBlock } from '../../lib/markdown-html-block.js'
const props = defineProps({
html: {
type: String,
default: ''
}
})
const sanitizedHtml = computed(() => sanitizeHtmlBlock(props.html))
</script>
<template>
<div
v-if="sanitizedHtml"
class="prose-html prose-html-block mb-8 overflow-x-auto text-base leading-7 text-[var(--site-text)]"
v-html="sanitizedHtml"
/>
<p v-else class="prose-html prose-html-block mb-8 text-sm font-semibold text-[var(--site-muted)]">
표시할 수 있는 HTML이 없습니다.
</p>
</template>
<style scoped>
.prose-html :deep(table) {
width: 100%;
min-width: 520px;
border-collapse: collapse;
font-size: 0.875rem;
}
.prose-html :deep(th),
.prose-html :deep(td) {
border: 1px solid var(--site-line);
padding: 0.5rem 0.75rem;
vertical-align: top;
}
.prose-html :deep(th) {
background: color-mix(in srgb, var(--site-text) 6%, transparent);
font-weight: 700;
}
.prose-html :deep(p) {
margin-bottom: 0.625rem;
}
.prose-html :deep(a) {
color: var(--site-accent);
text-decoration: underline;
text-underline-offset: 0.18em;
}
</style>