게시물 HTML iframe 블록 추가

This commit is contained in:
2026-06-17 09:52:56 +09:00
parent 6c4064d8b5
commit d41cb71d40
17 changed files with 211 additions and 45 deletions

View File

@@ -2,6 +2,7 @@ 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 블록 시작 줄 인덱스를 찾는다.
@@ -198,7 +199,7 @@ const resolveGalleryBlock = (lines, currentLine) => {
const resolveEmbedBlock = (lines, currentLine) => {
const standaloneUrl = String(lines[currentLine] || '').trim()
if (isStandaloneUrlLine(standaloneUrl) && !isStandaloneImageUrlLine(standaloneUrl)) {
if ((isStandaloneUrlLine(standaloneUrl) && !isStandaloneImageUrlLine(standaloneUrl)) || isIframeEmbedMarkup(standaloneUrl)) {
return {
kind: 'embed',
startLine: currentLine,

View File

@@ -0,0 +1,87 @@
/**
* iframe 속성 문자열을 객체로 변환한다.
* @param {string} value - iframe 여는 태그의 속성 문자열
* @returns {Record<string, string>} 속성 객체
*/
const parseIframeAttributes = (value) => {
const attributes = {}
const attributePattern = /([:\w-]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/g
let match = attributePattern.exec(String(value || ''))
while (match) {
attributes[match[1].toLowerCase()] = match[3] ?? match[4] ?? match[5] ?? ''
match = attributePattern.exec(String(value || ''))
}
return attributes
}
/**
* 외부 iframe src로 사용할 수 있는 URL인지 확인한다.
* @param {string} value - 검사할 URL
* @returns {boolean} 허용 여부
*/
const isSafeIframeSrc = (value) => {
try {
const parsedUrl = new URL(value)
return ['http:', 'https:'].includes(parsedUrl.protocol)
} catch {
return false
}
}
/**
* iframe 숫자 속성을 정리한다.
* @param {string} value - 원본 값
* @returns {string} 정리된 값
*/
const sanitizeSizeValue = (value) => {
const text = String(value || '').trim()
return /^\d{2,5}$/.test(text) ? text : ''
}
/**
* iframe allow 속성을 정리한다.
* @param {string} value - 원본 값
* @returns {string} 정리된 값
*/
const sanitizeAllowValue = (value) => String(value || '')
.replace(/[^\w\s;*.:/-]/g, '')
.trim()
/**
* iframe HTML 코드인지 확인한다.
* @param {string} value - 검사할 문자열
* @returns {boolean} iframe 코드 여부
*/
export const isIframeEmbedMarkup = (value) => /^<iframe\b[\s\S]*?(?:<\/iframe>\s*)?$/i.test(String(value || '').trim())
/**
* iframe HTML 코드에서 렌더링 가능한 정보만 추출한다.
* @param {string} value - iframe HTML 코드
* @returns {{ src: string, title: string, allow: string, width: string, height: string, allowFullscreen: boolean }|null} iframe 정보
*/
export const parseIframeEmbedMarkup = (value) => {
const source = String(value || '').trim()
const opener = source.match(/^<iframe\b([^>]*)>/i)
if (!opener) {
return null
}
const attributes = parseIframeAttributes(opener[1])
const src = attributes.src || ''
if (!isSafeIframeSrc(src)) {
return null
}
return {
src,
title: attributes.title || 'Embedded content',
allow: sanitizeAllowValue(attributes.allow || 'fullscreen; picture-in-picture; encrypted-media; web-share'),
width: sanitizeSizeValue(attributes.width),
height: sanitizeSizeValue(attributes.height),
allowFullscreen: /\ballowfullscreen\b/i.test(opener[1])
}
}

View File

@@ -117,9 +117,9 @@ export const MARKDOWN_SLASH_COMMANDS = [
},
{
id: 'embed',
label: '임베드',
description: 'YouTube·X 등 외부 URL 한 줄',
keywords: ['embed', 'youtube', 'link', '임베드', '유튜브'],
label: 'HTML',
description: 'URL 또는 iframe HTML',
keywords: ['html', 'iframe', 'embed', 'youtube', 'link', '임베드', '유튜브'],
action: 'lines',
lines: ['https://']
},