게시물 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

@@ -2,7 +2,6 @@ import { isImageUrl, parseImageMarkdownLine } from './markdown-image.js'
import { CALLOUT_BACKGROUND_OPTIONS, QUOTE_BACKGROUND_OPTIONS, parseCalloutOptions } from './markdown-callout.js'
import { parseCodeFenceLine } from './markdown-code-block.js'
import { parseToggleOpenerLine } from './markdown-toggle.js'
import { isIframeEmbedMarkup } from './markdown-iframe-embed.js'
/**
* fenced 블록 시작 줄 인덱스를 찾는다.
@@ -191,41 +190,40 @@ const resolveGalleryBlock = (lines, currentLine) => {
}
/**
* 임베드 fenced 블록을 파싱한다.
* 링크·iframe·HTML fenced 블록을 파싱한다.
* @param {string[]} lines - 본문 줄 목록
* @param {number} currentLine - 현재 줄
* @returns {{ kind: 'embed', startLine: number, endLine: number, url: string }|null}
* @returns {{ kind: 'link'|'iframe'|'html', startLine: number, endLine: number, url: string }|null}
*/
const resolveEmbedBlock = (lines, currentLine) => {
const standaloneUrl = String(lines[currentLine] || '').trim()
const resolveHtmlFamilyBlock = (lines, currentLine) => {
const blockTypes = [
{ opener: ':::link', kind: 'link' },
{ opener: ':::iframe', kind: 'iframe' },
{ opener: ':::html', kind: 'html' }
]
for (const blockType of blockTypes) {
const blockStart = findFencedBlockStart(lines, currentLine, blockType.opener)
if (blockStart === -1) {
continue
}
const blockEnd = findFencedBlockEnd(lines, blockStart)
if (blockEnd === -1 || currentLine > blockEnd) {
continue
}
if ((isStandaloneUrlLine(standaloneUrl) && !isStandaloneImageUrlLine(standaloneUrl)) || isIframeEmbedMarkup(standaloneUrl)) {
return {
kind: 'embed',
startLine: currentLine,
endLine: currentLine,
url: standaloneUrl
kind: blockType.kind,
startLine: blockStart,
endLine: blockEnd,
url: lines.slice(blockStart + 1, blockEnd).join('\n').trim()
}
}
const embedStart = findFencedBlockStart(lines, currentLine, ':::embed')
if (embedStart === -1) {
return null
}
const embedEnd = findFencedBlockEnd(lines, embedStart)
if (embedEnd === -1 || currentLine > embedEnd) {
return null
}
return {
kind: 'embed',
startLine: embedStart,
endLine: embedEnd,
url: lines.slice(embedStart + 1, embedEnd).join('\n').trim()
}
return null
}
/**
@@ -394,5 +392,5 @@ export const resolveActiveBlockContext = (markdown, lineIndex) => {
return toggle
}
return resolveEmbedBlock(lines, currentLine)
return resolveHtmlFamilyBlock(lines, currentLine)
}

View File

@@ -0,0 +1,35 @@
const DANGEROUS_TAG_NAMES = [
'script',
'iframe',
'object',
'embed',
'form',
'input',
'button',
'textarea',
'select',
'meta',
'link',
'base'
]
const dangerousTagPattern = new RegExp(`<\\s*(${DANGEROUS_TAG_NAMES.join('|')})\\b[\\s\\S]*?<\\s*\\/\\s*\\1\\s*>`, 'gi')
const dangerousSingleTagPattern = new RegExp(`<\\s*(${DANGEROUS_TAG_NAMES.join('|')})\\b[^>]*\\/?>`, 'gi')
/**
* 본문 HTML 블록에서 실행성 코드와 위험 속성을 제거한다.
* @param {string} value - 원본 HTML
* @returns {string} 정리된 HTML
*/
export const sanitizeHtmlBlock = (value) => String(value || '')
.replace(dangerousTagPattern, '')
.replace(dangerousSingleTagPattern, '')
.replace(/\s+on[a-z]+\s*=\s*"[^"]*"/gi, '')
.replace(/\s+on[a-z]+\s*=\s*'[^']*'/gi, '')
.replace(/\s+on[a-z]+\s*=\s*[^\s>]+/gi, '')
.replace(/\s+(href|src)\s*=\s*"javascript:[^"]*"/gi, '')
.replace(/\s+(href|src)\s*=\s*'javascript:[^']*'/gi, '')
.replace(/\s+(href|src)\s*=\s*javascript:[^\s>]+/gi, '')
.replace(/\s+style\s*=\s*"[^"]*expression\s*\([^"]*"/gi, '')
.replace(/\s+style\s*=\s*'[^']*expression\s*\([^']*'/gi, '')
.trim()

View File

@@ -116,12 +116,28 @@ export const MARKDOWN_SLASH_COMMANDS = [
lines: [':::toggle 제목', '', ':::']
},
{
id: 'embed',
id: 'html',
label: 'HTML',
description: 'URL 또는 iframe HTML',
keywords: ['html', 'iframe', 'embed', 'youtube', 'link', '임베드', '유튜브'],
description: '원하는 HTML 코드 삽입',
keywords: ['html', 'markup', 'tablehtml', 'html표', '코드', '마크업'],
action: 'lines',
lines: ['https://']
lines: [':::html', '', ':::']
},
{
id: 'link',
label: '링크',
description: 'URL 링크/임베드 카드',
keywords: ['link', 'url', 'embed', 'youtube', '링크', '주소', '임베드', '유튜브'],
action: 'lines',
lines: [':::link', 'https://', ':::']
},
{
id: 'iframe',
label: 'iframe',
description: '외부 iframe 코드 삽입',
keywords: ['iframe', 'frame', '아이프레임', 'html'],
action: 'lines',
lines: [':::iframe', '<iframe src="https://"></iframe>', ':::']
},
{
id: 'video',