diff --git a/components/admin/AdminMarkdownEditor.vue b/components/admin/AdminMarkdownEditor.vue
index c287583..250c3f0 100644
--- a/components/admin/AdminMarkdownEditor.vue
+++ b/components/admin/AdminMarkdownEditor.vue
@@ -639,7 +639,7 @@ watch(activeMode, (mode) => {
}
nextTick(() => {
- previewRendererRef.value?.focusEditableAtLine(previewFocus.line, 0, 'auto', previewFocus.offset)
+ previewRendererRef.value?.focusEditableAtLine(previewFocus.line, 0, 'auto', previewFocus.offset, 'center')
})
})
})
diff --git a/components/content/ContentMarkdownRenderer.vue b/components/content/ContentMarkdownRenderer.vue
index 38a6f58..4102177 100644
--- a/components/content/ContentMarkdownRenderer.vue
+++ b/components/content/ContentMarkdownRenderer.vue
@@ -20,7 +20,7 @@ import {
} from '../../lib/markdown-live-edit.js'
import { buildCodeBlockLines, parseCodeFenceLine } from '../../lib/markdown-code-block.js'
import { buildToggleBlockLines } from '../../lib/markdown-toggle.js'
-import { parseCalloutOptions } from '../../lib/markdown-callout.js'
+import { CALLOUT_BACKGROUND_OPTIONS, parseCalloutOptions } from '../../lib/markdown-callout.js'
import ContentMarkdownCodeBlockEditor from './ContentMarkdownCodeBlockEditor.vue'
import ProseCodeBlock from './ProseCodeBlock.vue'
import ContentMarkdownCalloutEditor from './ContentMarkdownCalloutEditor.vue'
@@ -140,6 +140,7 @@ const createBlock = (type = 'paragraph', text = '', level = null, id = '', optio
calloutEmojiEnabled: options.calloutEmojiEnabled ?? true,
calloutEmoji: options.calloutEmoji || '๐ก',
calloutBackground: options.calloutBackground || 'blue',
+ quoteBackground: options.quoteBackground || 'pink',
codeLanguage: options.codeLanguage || '',
codeShowLineNumbers: options.codeShowLineNumbers !== false
})
@@ -161,6 +162,43 @@ const isQuoteMarkerLine = (line) => {
return trimmed === '>' || /^>\s/.test(trimmed)
}
+/**
+ * ์ธ์ฉ ๋ง์ปค๋ฅผ ์ ๊ฑฐํ ๋ณธ๋ฌธ์ ๋ฐํํ๋ค.
+ * @param {string} line - ๋งํฌ๋ค์ด ํ
+ * @returns {string} ์ธ์ฉ ๋ณธ๋ฌธ
+ */
+const getQuoteLineBody = (line) => String(line ?? '').trim().replace(/^>\s?/, '')
+
+/**
+ * ์ธ์ฉ ์ต์
์ค์ ํ์ฑํ๋ค.
+ * @param {string} value - ์ธ์ฉ ๋ณธ๋ฌธ ์ค
+ * @returns {{ quoteBackground: string }|null} ์ธ์ฉ ์ต์
+ */
+const parseQuoteOptions = (value) => {
+ const raw = String(value ?? '').trim()
+ const bracketMatch = raw.match(/^\[!(.+)\]$/)
+ const braceMatch = raw.match(/^\{(.+)\}$/)
+ const optionSource = bracketMatch?.[1] || braceMatch?.[1] || ''
+
+ if (!optionSource) {
+ return null
+ }
+
+ const tokens = optionSource.trim().split(/\s+/)
+ let quoteBackground = ''
+
+ tokens.forEach((token) => {
+ const [key, rawOptionValue] = token.split('=')
+ const optionValue = String(rawOptionValue || '').trim()
+
+ if (key?.toLowerCase() === 'bg' && CALLOUT_BACKGROUND_OPTIONS.includes(optionValue)) {
+ quoteBackground = optionValue
+ }
+ })
+
+ return quoteBackground ? { quoteBackground } : null
+}
+
/**
* ๋ถ๋ฆฟ ๋ชฉ๋ก ๋ง์ปค ์ค์ธ์ง ํ์ธํ๋ค.
* @param {string} line - ๋งํฌ๋ค์ด ํ
@@ -624,15 +662,24 @@ const parseMarkdownBlocks = (markdown) => {
if (isQuoteMarkerLine(line)) {
const startLine = index
- const quoteLines = []
+ const rawQuoteLines = []
while (index < lines.length && isQuoteMarkerLine(lines[index])) {
- quoteLines.push(lines[index].trim().replace(/^>\s?/, ''))
+ rawQuoteLines.push(getQuoteLineBody(lines[index]))
index += 1
}
+ const quoteOptions = parseQuoteOptions(rawQuoteLines[0])
+ const quoteLines = quoteOptions ? rawQuoteLines.slice(1) : rawQuoteLines
+ const contentStartLine = startLine + (quoteOptions ? 1 : 0)
+
blocks.push(attachSourceRange(
- createBlock('quote', quoteLines.join('\n'), null, `block-${blocks.length}`),
+ createBlock('quote', (quoteLines.length ? quoteLines : ['']).join('\n'), null, `block-${blocks.length}`, {
+ ...(quoteOptions || {}),
+ meta: {
+ quoteContentStartLine: contentStartLine
+ }
+ }),
startLine,
index - 1
))
@@ -785,9 +832,10 @@ watch(() => props.content, () => {
* @param {number} [attempt=0] - DOM ํ์ ์ฌ์๋ ํ์
* @param {'start'|'end'|'auto'} [cursorPosition='auto'] - ์ปค์ ์์น
* @param {number|null} [caretOffset=null] - ํ
์คํธ ์คํ์
+ * @param {'nearest'|'center'} [scrollBlock='nearest'] - ์คํฌ๋กค ์ ๋ ฌ ์์น
* @returns {void}
*/
-const focusEditableAtLine = (lineIndex, attempt = 0, cursorPosition = 'auto', caretOffset = null) => {
+const focusEditableAtLine = (lineIndex, attempt = 0, cursorPosition = 'auto', caretOffset = null, scrollBlock = 'nearest') => {
if (!import.meta.client) {
return
}
@@ -803,7 +851,7 @@ const focusEditableAtLine = (lineIndex, attempt = 0, cursorPosition = 'auto', ca
if (!element) {
if (attempt < 8) {
requestAnimationFrame(() => {
- focusEditableAtLine(lineIndex, attempt + 1, cursorPosition, caretOffset)
+ focusEditableAtLine(lineIndex, attempt + 1, cursorPosition, caretOffset, scrollBlock)
})
}
@@ -823,28 +871,31 @@ const focusEditableAtLine = (lineIndex, attempt = 0, cursorPosition = 'auto', ca
element.focus({ preventScroll: true })
if (element.getAttribute('contenteditable') !== 'true') {
- element.scrollIntoView({ block: 'nearest', inline: 'nearest' })
+ element.scrollIntoView({ block: scrollBlock, inline: 'nearest' })
return
}
if (typeof caretOffset === 'number' && caretOffset >= 0) {
setEditableCaretOffset(/** @type {HTMLElement} */ (element), caretOffset)
+ element.scrollIntoView({ block: scrollBlock, inline: 'nearest' })
return
}
if (cursorPosition === 'start' || (cursorPosition === 'auto' && (isBlankMarker || !line.trim()))) {
setEditableCaretOffset(/** @type {HTMLElement} */ (element), 0)
+ element.scrollIntoView({ block: scrollBlock, inline: 'nearest' })
return
}
if (cursorPosition === 'end') {
const text = readEditableTextFromElement(/** @type {HTMLElement} */ (element))
setEditableCaretOffset(/** @type {HTMLElement} */ (element), text.length)
+ element.scrollIntoView({ block: scrollBlock, inline: 'nearest' })
return
}
setEditableCaretOffset(/** @type {HTMLElement} */ (element), 0)
- element.scrollIntoView({ block: 'nearest', inline: 'nearest' })
+ element.scrollIntoView({ block: scrollBlock, inline: 'nearest' })
}
defineExpose({
@@ -1053,23 +1104,38 @@ const createEmptyListMarkerLine = (block, itemIndex) => {
}
/**
- * ์ธ์ฉ ๋ธ๋ก์ ์ค ๋จ์๋ก ๋ถ๋ฆฌํ๋ค.
+ * ์ธ์ฉ ๋ธ๋ก์ ๋ณธ๋ฌธ ์์ ์ค์ ๋ฐํํ๋ค.
* @param {Object} block - ์ธ์ฉ ๋ธ๋ก
- * @returns {string[]} ์ค ๋ชฉ๋ก
+ * @returns {number} ๋ณธ๋ฌธ ์์ ์ค
*/
-const getQuoteLines = (block) => {
- const lineCount = (block.meta.endLine ?? block.meta.startLine) - block.meta.startLine + 1
+const getQuoteContentStartLine = (block) => {
+ if (typeof block.meta?.quoteContentStartLine === 'number') {
+ return block.meta.quoteContentStartLine
+ }
+
+ return block.meta.startLine
+}
+
+/**
+ * ์ธ์ฉ ๋ธ๋ก์ ํธ์ง ๊ฐ๋ฅํ ์ค ๋จ์๋ก ๋ถ๋ฆฌํ๋ค.
+ * @param {Object} block - ์ธ์ฉ ๋ธ๋ก
+ * @returns {Array<{ text: string, sourceLine: number, sourceIndex: number }>} ์ค ๋ชฉ๋ก
+ */
+const getQuoteLineEntries = (block) => {
+ const contentStartLine = getQuoteContentStartLine(block)
+ const endLine = block.meta.endLine ?? block.meta.startLine
+ const lineCount = Math.max(1, endLine - contentStartLine + 1)
const fromText = String(block.text ?? '').split('\n')
while (fromText.length < lineCount) {
fromText.push('')
}
- if (!fromText.length) {
- return ['']
- }
-
- return fromText.slice(0, lineCount)
+ return fromText.slice(0, lineCount).map((text, index) => ({
+ text,
+ sourceLine: contentStartLine + index,
+ sourceIndex: contentStartLine + index - block.meta.startLine
+ }))
}
/**
@@ -2269,24 +2335,29 @@ onBeforeUnmount(() => {
-
+
{{ segment.text }}
{{ segment.text }}
diff --git a/components/content/ProseBlockquote.vue b/components/content/ProseBlockquote.vue
index 0d2adb0..5566734 100644
--- a/components/content/ProseBlockquote.vue
+++ b/components/content/ProseBlockquote.vue
@@ -1,10 +1,42 @@
@@ -12,10 +44,47 @@ defineProps({
class="prose-blockquote mb-2.5 text-[15px] leading-8"
:class="variant === 'alt'
? 'rounded-[14px] border border-[var(--site-line)] bg-[var(--site-panel)] px-6 py-5 italic text-[var(--site-text)]'
- : 'rounded-[10px] border-l-2 border-[#FF1A75] bg-[color-mix(in_srgb,#FF1A75_10%,#ffffff)] px-5 py-4 font-medium text-[#15171a]'"
+ : ['rounded-[10px] border-l-2 px-5 py-4 font-medium text-[#15171a]', backgroundClass]"
>
+
+
diff --git a/docs/changelog.md b/docs/changelog.md
index e6800df..f34220a 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,11 @@
# ์
๋ฐ์ดํธ ์์ฝ
+## v1.4.7
+
+- ๊ธ์ฐ๊ธฐ ๋ผ์ด๋ธ ๋ชจ๋์์ ๋ฌธ๋จ ์ด๋ ์ ์ธ๋ผ์ธ ๋งํฌ๋ค์ด ์์์ด ์ฌ๋ผ์ง๋ ๋ฌธ์ ๋ฅผ ์์ ํ๋ค.
+- ์ธ์ฉ ๋ธ๋ก์์ `> [!bg=yellow]` ํ์์ผ๋ก ๋ฐฐ๊ฒฝ์์ ์ง์ ํ ์ ์๋ค.
+- ์์ค ๋ชจ๋์์ ๋ผ์ด๋ธ ๋ชจ๋๋ก ์ ํํ ๋ ํ์ฌ ์ปค์ ์ค ์ฃผ๋ณ์ผ๋ก ์คํฌ๋กค๋๋๋ก ๋ณด์ ํ๋ค.
+
## v1.4.6
- ๊ด๋ฆฌ์ ์ฌ์ดํธ ์ค์ ์์ ๋ก๊ณ ์ ๋ฉ์ธ ์ปค๋ฒ ์ด๋ฏธ์ง๊ฐ ์ ์ฅ ๋ฒํผ์ ํตํด ๋ฐ์๋๋๋ก ์ ๋ฆฌํ๋ค.
diff --git a/docs/history.md b/docs/history.md
index 905bbaf..b6d6529 100644
--- a/docs/history.md
+++ b/docs/history.md
@@ -1,5 +1,11 @@
# ์์ฌ๊ฒฐ์ ์ด๋ ฅ
+## 2026-05-22 v1.4.7 โ ๋ผ์ด๋ธ ๋ชจ๋ ์ธ๋ผ์ธ ๋งํฌ๋ค์ด ์ง๋ ฌํ
+
+๋ผ์ด๋ธ ํธ์ง ์์ญ์ ํ๋ฉด์ ``ยท`` ๋ฑ์ผ๋ก ํ์๋์ง๋ง, blur ์ ์ ์ฅ ๊ฒฝ๋ก๊ฐ `textContent`๋ง ์ฝ์ผ๋ฉด `**`ยท`*` ๋ง์ปค๊ฐ ๋น ์ง๋ค. ๋ฌธ๋จ ์ด๋ ์ ์ด์ ๋ธ๋ก์ด blurยทcommit ๋๋ฏ๋ก ๋ฐฉํฅํค๋ง์ผ๋ก๋ ์์์ด ์ฌ๋ผ์ง ๊ฒ์ฒ๋ผ ๋ณด์๋ค. `readEditableTextFromElement`๊ฐ DOM ์ธ๋ผ์ธ ๋
ธ๋๋ฅผ ๋งํฌ๋ค์ด์ผ๋ก ๋ค์ ์ง๋ ฌํํ๋๋ก ์์ ํ๋ค.
+
+์ธ์ฉ ๋ธ๋ก์ ํ์ค `>` ๋ฌธ๋ฒ์ ์ ์งํ๋, ์ฒซ ์ค์ `> [!bg=yellow]`์ฒ๋ผ ์ต์
์ค์ ๋ ์ ์๊ฒ ํ๋ค. ์ fenced block์ ์ถ๊ฐํ์ง ์์ผ๋ฉด ๊ธฐ์กด ๋งํฌ๋ค์ด๊ณผ ํธํ๋๊ณ , ์ฝ์์์์ ์ด๋ฏธ ์ฐ๋ ๋ฐฐ๊ฒฝ ํ๋ฆฌ์
์ ๊ณต์ ํ ์ ์๋ค. ์์ค์์ ๋ผ์ด๋ธ๋ก ์ ํํ ๋๋ ์ปค์ ์ค์ ํฌ์ปค์ค๋ง ๋๊ณ ์คํฌ๋กคํ์ง ์๋ ๊ฒฝ๋ก๊ฐ ์์ด ํ๋ฉด ์์น๊ฐ ์ด๊ธ๋ฌ์ผ๋ฏ๋ก, ํด๋น ์ ํ์์๋ ๋์ ์ค์ ์ค์์ ๊ฐ๊น๊ฒ ์คํฌ๋กคํ๋ค.
+
## 2026-05-22 v1.4.6 โ ์ฌ์ดํธ ์ค์ ์ด๋ฏธ์ง ์ ์ฅ ํ๋ฆ ํต์ผ
๊ด๋ฆฌ์ ์ฌ์ดํธ ์ค์ ์ ์น์
๋ณ `ํธ์ง` ํ `์ ์ฅ`์ผ๋ก ๋ฐ์๋๋ ์ปจ์
์ด๋ฏ๋ก, ๋ก๊ณ ์
๋ก๋๋ DB๋ฅผ ์ฆ์ ๊ฐฑ์ ํ์ง ์๊ณ ์
๋ก๋๋ ํ์ผ URL๋ง ํผ์ ๋ฐ์ํ ๋ค ๊ธฐํ ์ค์ ์ ์ฅ ์ ํจ๊ป ์ ์ฅํ๋๋ก ์ ๋ฆฌํ๋ค. ํ ์ปค๋ฒ๋ ๊ณต๊ฐ ๋ผ์ดํธยท๋คํฌ ํ
๋ง์ ๋ฐ๋ผ ์ด๋ฏธ์ง ํค์ด ํฌ๊ฒ ๋ฌ๋ผ์ง ์ ์์ด ๊ธฐ์กด ๋ผ์ดํธ ์ด๋ฏธ์ง๋ฅผ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ ์งํ๋ฉด์ ๋คํฌ ์ ์ฉ URL์ ๋ณ๋ ์ปฌ๋ผ์ผ๋ก ์ถ๊ฐํ๋ค. ๋คํฌ ์ด๋ฏธ์ง๊ฐ ์์ผ๋ฉด ๊ธฐ์กด ์ด๋ฏธ์ง๋ก fallbackํด ๊ธฐ์กด ์ค์ ๊ณผ ๊ณต๊ฐ ํ๋ฉด ๋์์ ์ ์งํ๋ค.
diff --git a/docs/map.md b/docs/map.md
index 1945601..3c8d823 100644
--- a/docs/map.md
+++ b/docs/map.md
@@ -98,11 +98,11 @@
| ํ์ผ | ํ๋ฉด ์์น |
|------|-----------|
| components/content/ContentRenderer.vue | ๊ฒ์๋ฌผ/ํ์ด์ง ๋ณธ๋ฌธ |
-| components/content/ContentMarkdownRenderer.vue | ๋งํฌ๋ค์ด ๋ฌธ์์ด ๊ธฐ๋ฐ ๋ณธ๋ฌธ ๋ ๋๋ง, ๋ฌธ๋จ text-base(16px), ๋น ์ค spacer ๋ณด์กดยทhard break `
` ์ฒ๋ฆฌ, ํ์ฅ ๋ธ๋ก ํ์ฑ, ๋ผ์ด๋ธ ์ด๋ฏธ์งยท๊ฐค๋ฌ๋ฆฌ ๋๋๊ทธ ๋ณํฉยท์ถ๊ฐยท๋ถ๋ฆฌ UI, ๊ฐค๋ฌ๋ฆฌ ๋น์จ ๊ธฐ๋ฐ ํ ๋ ์ด์์, ๋ผ์ด๋ธ ๊ฐค๋ฌ๋ฆฌ ๊ฐ๋ณ ์ด๋ฏธ์ง ํธ์งยท์ญ์ , ๋ฆฌ์คํธ ๋ง์ปค ํ๋ ๊ณ์ด ํต์ผ |
+| components/content/ContentMarkdownRenderer.vue | ๋งํฌ๋ค์ด ๋ฌธ์์ด ๊ธฐ๋ฐ ๋ณธ๋ฌธ ๋ ๋๋ง, ๋ฌธ๋จ text-base(16px), ๋น ์ค spacer ๋ณด์กดยทhard break `
` ์ฒ๋ฆฌ, ํ์ฅ ๋ธ๋ก ํ์ฑ, ์ธ์ฉ ๋ฐฐ๊ฒฝ ์ต์
(`> [!bg=...]`), ๋ผ์ด๋ธ ์ด๋ฏธ์งยท๊ฐค๋ฌ๋ฆฌ ๋๋๊ทธ ๋ณํฉยท์ถ๊ฐยท๋ถ๋ฆฌ UI, ๊ฐค๋ฌ๋ฆฌ ๋น์จ ๊ธฐ๋ฐ ํ ๋ ์ด์์, ๋ผ์ด๋ธ ๊ฐค๋ฌ๋ฆฌ ๊ฐ๋ณ ์ด๋ฏธ์ง ํธ์งยท์ญ์ , ๋ฆฌ์คํธ ๋ง์ปค ํ๋ ๊ณ์ด ํต์ผ |
| components/content/ProseHeading.vue | h1~h6 ์ ๋ชฉ, ๊ธฐ๋ณธ mt-12 ์ ๊ฑฐ |
| components/content/ProseImage.vue | ๋ณธ๋ฌธ ๋ด ์ด๋ฏธ์ง, ๋ก๋ ์คํจยท๋น URL placeholder |
| components/content/ProseList.vue | ๋ชฉ๋ก |
-| components/content/ProseBlockquote.vue | ์ธ์ฉ๊ตฌ, ๋คํฌ๋ชจ๋ ๊ธฐ๋ณธ ์ธ์ฉ ํ
์คํธ ๊ฐ๋
์ฑ ๋ณด์ |
+| components/content/ProseBlockquote.vue | ์ธ์ฉ๊ตฌ, ์ฝ์์๊ณผ ๊ฐ์ ๋ฐฐ๊ฒฝ ํ๋ฆฌ์
, ๋คํฌ๋ชจ๋ ๊ธฐ๋ณธ ์ธ์ฉ ํ
์คํธ ๊ฐ๋
์ฑ ๋ณด์ |
| components/content/ProseCodeBlock.vue | ์ฝ๋ ๋ธ๋ก ๊ณตํต ์
ธ(๋คํฌ ๋ฐฐ๊ฒฝ, ์ค๋ฒํธ gutter, ๊ณต๊ฐ ๋ณต์ฌ ๋ฒํผ) |
| components/content/ContentMarkdownCodeBlockEditor.vue | ๋ผ์ด๋ธ ๋ชจ๋ ์ฝ๋ ๋ธ๋ก ์ธ๋ผ์ธ ํธ์ง(Languageยท์ค๋ฒํธ ํ ๊ธ) |
| components/content/ProseButton.vue | ๋ฒํผ |
diff --git a/docs/spec.md b/docs/spec.md
index 7630a90..cdf2851 100644
--- a/docs/spec.md
+++ b/docs/spec.md
@@ -543,7 +543,8 @@ components/content/
- ๋ผ์ด๋ธ ๋ชจ๋ ๋จ์ผ ์ด๋ฏธ์ง ๋ธ๋ก์ ๊ธฐ์กด ๊ฐค๋ฌ๋ฆฌ ์ด๋ฏธ์ง ์
์ ๋๋กญํ๋ฉด ํด๋น ์
๋ค์ ์ด๋ฏธ์ง๋ฅผ ์ถ๊ฐํ๊ณ ์๋ ๋จ์ผ ์ด๋ฏธ์ง ์ค์ ์ ๊ฑฐํ๋ค(`insert-image-to-gallery`).
- ๋ผ์ด๋ธ ๋ชจ๋ ๊ฐค๋ฌ๋ฆฌ ์ด๋ฏธ์ง๋ฅผ ๋ธ๋ก ์ฌ์ด ์์ ์ฝ์
์ (๋๋ ๋ฌธ์ ๋งจ ์๋ ์ฝ์
์ )์ ๋๋กญํ๋ฉด ํด๋น ์์น์ ๋จ์ผ ์ด๋ฏธ์ง ๋งํฌ๋ค์ด ์ค์ ์ฝ์
ํ๊ณ ๊ฐค๋ฌ๋ฆฌ์์ ์ ๊ฑฐํ๋ค(`extract-gallery-image`). ๊ฐค๋ฌ๋ฆฌ์ ์ด๋ฏธ์ง๊ฐ 1์ฅ๋ง ๋จ์ผ๋ฉด ๊ฐค๋ฌ๋ฆฌ ๋ธ๋ก์ ๋จ์ผ ์ด๋ฏธ์ง ์ค๋ก ๋ฐ๊พธ๊ณ , 0์ฅ์ด๋ฉด ๊ฐค๋ฌ๋ฆฌ ๋ธ๋ก์ ์ ๊ฑฐํ๋ค.
- `ProseImage`๋ URL์ด ๋น์ด ์๊ฑฐ๋ ๋ก๋์ ์คํจํด๋ ์ต์ ๋์ด placeholder์ ใ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ใ ์๋ด๋ฅผ ํ์ํด ๋ผ์ด๋ธ ๋ชจ๋์์ ๋ธ๋ก ์ ํยทํธ์ง์ด ๊ฐ๋ฅํ๋ค.
-- ๊ด๋ฆฌ์ **๋ผ์ด๋ธ ๋ชจ๋**(๋ฏธ๋ฆฌ๋ณด๊ธฐ) ์ธ๋ผ์ธ ํธ์ง: ๋ฌธ๋จยท๋น ์คยท์ ๋ชฉยท์ธ์ฉยท๋ชฉ๋กยท์ฝ๋ ๋ธ๋กยท์ฝ์์ยทํ ๊ธ์ ๋ ๋ ์คํ์ผ ๊ทธ๋๋ก contenteditable๋ก ์์ ํ๋ค. **Enter**ยท**Shift+Enter** ๋ชจ๋ ๋ค์ ๋ฌธ๋จ(๋ธ๋ก) ๋ถ๋ฆฌ. ๋ฌธ๋จ ์ `/`๋ก ์ฌ๋์ ๋ช
๋ น ๋ฉ๋ด(`/image`+Enter ์ด๋ฏธ์ง ์ฝ์
๋ฑ). **์์ค(์์ฑ) ๋ชจ๋** textarea์์๋ ๋์ผํ `/` ์ฌ๋์ ๋ฉ๋ด๋ฅผ ์ฌ์ฉํ๋ฉฐ, ์๋จ ๋งํฌ๋ค์ด ํด๋ฐ๋ ๋์ง ์๋๋ค. ์ฌ๋์ ๊ธฐ๋ณธ ์ ๋ชฉ์ **h2ยทh3ยทh4**๋ง ํ์ํ๋ฉฐ, ๋ณธ๋ฌธ **h1**์ `/h1` ๊ฒ์ ์์๋ง ์ฝ์
ํ๋ค(๊ฒ์๋ฌผ **์ ๋ชฉ ํ๋**๊ฐ ํ์ด์ง์ ์ ์ผํ h1). ์ฝ์์ ์ต์
์ ์ฒซ ์ค `:::callout emoji=๐ก bg=blue`์ฒ๋ผ `emoji`ยท`bg`(gray|blue|green|yellow|red|purple|pink)๋ก ์ง์ ํ๋ฉฐ, ๋ผ์ด๋ธ ๋ชจ๋์์๋ ์์ด์ฝ ํด๋ฆญ์ผ๋ก ๋ชจ๋ฌ์์ ํธ์งํ๋ค(์ด๋ชจ์ง 7์ข
ํ๋ฆฌ์
ยท๋ฐฐ๊ฒฝ์ ์ค์์น, ์ง์ ์
๋ ฅ ์์). ์ฝ๋ ๋ธ๋ก์ ` ```์ธ์ด`ยท`nolinenos`(์ค ๋ฒํธ ์จ๊น)๋ฅผ ์ง์ํ๋ค. ๋ผ์ด๋ธยท๊ณต๊ฐ ๋ชจ๋ `ProseCodeBlock`(`#15171a`, `px-4 py-3`, `text-sm leading-6`)์ผ๋ก ๋์ผํ๊ฒ ํ์ํ๋ค. ๋ผ์ด๋ธ ๋ชจ๋ ํธ๋ฒยทํฌ์ปค์ค ์ Language ์
๋ ฅยท์ค๋ฒํธ ํ ๊ธ์ด ๋ณด์ธ๋ค. ๊ณต๊ฐ ํ๋ฉด์๋ ์ธ์ด ๋ผ๋ฒจ ์ **๋ณต์ฌ** ๋ฒํผ์ผ๋ก ๋ณธ๋ฌธ์ ํด๋ฆฝ๋ณด๋์ ๋ฃ๋๋ค. ๋ณธ๋ฌธ ํ๋จ ํด๋ฆญ์ผ๋ก ์ ๋ฌธ๋จ์ ์ถ๊ฐํ๋ค.
+- ์ธ์ฉ(`>`) ๋ธ๋ก์ ์ฒซ ์ธ์ฉ ์ค์ `> [!bg=yellow]` ๋๋ `> {bg=yellow}` ์ต์
์ค์ ๋๋ฉด ํด๋น ์ค์ ์จ๊ธฐ๊ณ ๋ธ๋ก ๋ฐฐ๊ฒฝ์ ๋ฐ๊พผ๋ค. ์ง์ ๋ฐฐ๊ฒฝ ํ๋ฆฌ์
์ ์ฝ์์๊ณผ ๊ฐ์ `gray`, `blue`, `green`, `yellow`, `red`, `purple`, `pink`์ด๋ฉฐ, ์ต์
์ด ์์ผ๋ฉด ๊ธฐ์กด pink ๊ณ์ด ๊ธฐ๋ณธ ์ธ์ฉ ์คํ์ผ์ ์ด๋ค.
+- ๊ด๋ฆฌ์ **๋ผ์ด๋ธ ๋ชจ๋**(๋ฏธ๋ฆฌ๋ณด๊ธฐ) ์ธ๋ผ์ธ ํธ์ง: ๋ฌธ๋จยท๋น ์คยท์ ๋ชฉยท์ธ์ฉยท๋ชฉ๋กยท์ฝ๋ ๋ธ๋กยท์ฝ์์ยทํ ๊ธ์ ๋ ๋ ์คํ์ผ ๊ทธ๋๋ก contenteditable๋ก ์์ ํ๋ค. blurยท๋ฌธ๋จ ์ด๋(๋ฐฉํฅํค) ์ ํธ์ง ์์ญ์ ``ยท`` ๋ฑ์ `**`ยท`*` ๋งํฌ๋ค์ด์ผ๋ก ๋ค์ ์ง๋ ฌํํด ์ ์ฅํ๋ค. **Enter**ยท**Shift+Enter** ๋ชจ๋ ๋ค์ ๋ฌธ๋จ(๋ธ๋ก) ๋ถ๋ฆฌ. ๋ฌธ๋จ ์ `/`๋ก ์ฌ๋์ ๋ช
๋ น ๋ฉ๋ด(`/image`+Enter ์ด๋ฏธ์ง ์ฝ์
๋ฑ). **์์ค(์์ฑ) ๋ชจ๋** textarea์์๋ ๋์ผํ `/` ์ฌ๋์ ๋ฉ๋ด๋ฅผ ์ฌ์ฉํ๋ฉฐ, ์๋จ ๋งํฌ๋ค์ด ํด๋ฐ๋ ๋์ง ์๋๋ค. ์ฌ๋์ ๊ธฐ๋ณธ ์ ๋ชฉ์ **h2ยทh3ยทh4**๋ง ํ์ํ๋ฉฐ, ๋ณธ๋ฌธ **h1**์ `/h1` ๊ฒ์ ์์๋ง ์ฝ์
ํ๋ค(๊ฒ์๋ฌผ **์ ๋ชฉ ํ๋**๊ฐ ํ์ด์ง์ ์ ์ผํ h1). ์ฝ์์ ์ต์
์ ์ฒซ ์ค `:::callout emoji=๐ก bg=blue`์ฒ๋ผ `emoji`ยท`bg`(gray|blue|green|yellow|red|purple|pink)๋ก ์ง์ ํ๋ฉฐ, ๋ผ์ด๋ธ ๋ชจ๋์์๋ ์์ด์ฝ ํด๋ฆญ์ผ๋ก ๋ชจ๋ฌ์์ ํธ์งํ๋ค(์ด๋ชจ์ง 7์ข
ํ๋ฆฌ์
ยท๋ฐฐ๊ฒฝ์ ์ค์์น, ์ง์ ์
๋ ฅ ์์). ์ฝ๋ ๋ธ๋ก์ ` ```์ธ์ด`ยท`nolinenos`(์ค ๋ฒํธ ์จ๊น)๋ฅผ ์ง์ํ๋ค. ๋ผ์ด๋ธยท๊ณต๊ฐ ๋ชจ๋ `ProseCodeBlock`(`#15171a`, `px-4 py-3`, `text-sm leading-6`)์ผ๋ก ๋์ผํ๊ฒ ํ์ํ๋ค. ๋ผ์ด๋ธ ๋ชจ๋ ํธ๋ฒยทํฌ์ปค์ค ์ Language ์
๋ ฅยท์ค๋ฒํธ ํ ๊ธ์ด ๋ณด์ธ๋ค. ๊ณต๊ฐ ํ๋ฉด์๋ ์ธ์ด ๋ผ๋ฒจ ์ **๋ณต์ฌ** ๋ฒํผ์ผ๋ก ๋ณธ๋ฌธ์ ํด๋ฆฝ๋ณด๋์ ๋ฃ๋๋ค. ๋ณธ๋ฌธ ํ๋จ ํด๋ฆญ์ผ๋ก ์ ๋ฌธ๋จ์ ์ถ๊ฐํ๋ค.
- ์ด๋ฏธ์ง ํ์ผ์ ๋ถ์ฌ๋ฃ๊ฑฐ๋ ๋๋กญํ๋ฉด ๊ด๋ฆฌ์ ์
๋ก๋ API๋ก ์ ์ฅํ ๋ค ํ์ฌ ์ปค์ ์์น์ ์ด๋ฏธ์ง ๋๋ ๊ฐค๋ฌ๋ฆฌ ๋งํฌ๋ค์ด์ ์ฝ์
ํ๋ค.
- ํด๋ฐ `์ด๋ฏธ์ง`ยท`๊ฐค๋ฌ๋ฆฌ`๋ ๋ฏธ๋์ด ๋ชจ๋ฌ์ ์ฐ๋ค. ๋ชจ๋ฌ ๊ธฐ๋ณธ ํญ์ **๋ฏธ๋์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ**์ด๋ฉฐ **์
๋ก๋** ํญ์์ ๋๋๊ทธยทํ์ผ ์ ํ ํ ์ฆ์ ์ฝ์
ํ๋ค.
- ๋ฏธ๋์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ๋จ์ผ ์ด๋ฏธ์ง๋ฅผ ์ ํํ๋ฉด `` ํ์์ผ๋ก ์ฝ์
ํ๋ค.
@@ -616,7 +617,7 @@ components/content/
- ๋ผ์ด๋ธ/์คํ์ผ ๋ชจ๋์์ ์ ๋ชฉ ๋ธ๋ก Enter๋ ํ์ฌ ์ ๋ชฉ ๋ด์ฉ์ ์ ์ฅํ ๋ค ๋ฐ๋ก ์๋ ๋น ๋ฌธ๋จ์ ์ถ๊ฐํ๊ณ , ์๋ฌธ ๋งํฌ๋ค์ด ํธ์ง ์ํ๋ก ์ ํํ์ง ์๋๋ค.
- ๊ฒ์๋ฌผ ์์ฑ ํ๋ฉด ์๋จ ์ ๋ชฉ ์
๋ ฅ ํ Enter๋ ํ์ฌ ์๋ํฐ ๋ชจ๋๋ฅผ ์ ์งํ ์ฑ ๋ณธ๋ฌธ ์ฒซ ์ค(๋งํฌ๋ค์ด ์ฒซ ์ค์ด ์ ๋ชฉ์ด๋ฉด ๊ทธ ๋ค์ ์ค)๋ก ํฌ์ปค์ค๋ฅผ ์ฎ๊ธด๋ค.
- ์์ค ๋ชจ๋ ๋ผ์ธ ๋ฒํธ๋ ๋
ผ๋ฆฌ ์ค ์๋ฅผ ํ์ํ๋, ๊ธด ๋ฌธ์ฅ ์๋ ์ค๋ฐ๊ฟ์ผ๋ก textarea์ ํ ์ค ๋์ด๊ฐ ๋์ด๋๋ฉด ๋ผ์ธ ๋ฒํธ ์นธ๋ ๊ฐ์ ๋์ด๋ก ๋ง์ถ๋ค.
-- ์์ค ๋ชจ๋์์ ๋ผ์ด๋ธ ๋ชจ๋๋ก ์ ํํ๋ฉด ํ์ฌ textarea ์ปค์ ์ค๊ณผ ์ค ์ ์คํ์
์ ๊ธฐ์ค์ผ๋ก ๋์ํ๋ ๋ผ์ด๋ธ ํธ์ง ๋ธ๋ก์ ํฌ์ปค์ค๋ฅผ ๋๋ค. ์ด๋ ํ์ฌ ํ๋ฉด ์์น๋ฅผ ๋ถํ์ํ๊ฒ ๋งจ ์๋ ๋งจ ์๋๋ก ์ด๋ํ์ง ์๋๋ค.
+- ์์ค ๋ชจ๋์์ ๋ผ์ด๋ธ ๋ชจ๋๋ก ์ ํํ๋ฉด ํ์ฌ textarea ์ปค์ ์ค๊ณผ ์ค ์ ์คํ์
์ ๊ธฐ์ค์ผ๋ก ๋์ํ๋ ๋ผ์ด๋ธ ํธ์ง ๋ธ๋ก์ ํฌ์ปค์ค๋ฅผ ๋๊ณ , ๋์ ์ค์ด ํ๋ฉด ์ค์์ ๊ฐ๊น๊ฒ ๋ณด์ด๋๋ก ์คํฌ๋กคํ๋ค.
- ๋ผ์ด๋ธ ๋ชจ๋์์ ์์ค ๋ชจ๋๋ก ์ ํํ๋ฉด ํ์ฌ ํฌ์ปค์ค๋ ๋ธ๋ก ๋๋ ํ๋ฉด ์๋จ์ ๊ฐ๊น์ด ์๋ณธ ์ค์ ๊ธฐ์ค์ผ๋ก textarea ์ปค์์ ์คํฌ๋กค ์์น๋ฅผ ๋ณต์ํ๋ค.
- YouTube ์๋ฒ ๋ URL์ ๊ณต๊ฐ ํ๋ฉด์์ ๋ณธ๋ฌธ ํญ ๊ธฐ์ค 16:9 iframe์ผ๋ก ๋ ๋๋งํ๋ค.
- Twitter/X ๊ฒ์๋ฌผ URL(`twitter.com`ยท`x.com`ยท`mobile.twitter.com`, ๊ฒฝ๋ก์ `status` ํฌํจ)์ `platform.twitter.com/embed/Tweet.html` iframe์ผ๋ก ๋ ๋๋งํ๋ฉฐ, ํ
๋ง๋ `useThemeMode()`์ ๋๊ธฐํํ๋ค. X ๊ณต์ iframe์ ๋ด๋ถ ์ต๋ ํญ ๋๋ฌธ์ ๊ณต๊ฐ ํ๋ฉด์์๋ ์นด๋ ํญ์ ์ขํ ์ค์ ์ ๋ ฌํ๋ค.
diff --git a/docs/update.md b/docs/update.md
index d000a64..1290eac 100644
--- a/docs/update.md
+++ b/docs/update.md
@@ -1,5 +1,11 @@
# ์
๋ฐ์ดํธ ์ด๋ ฅ
+## v1.4.7
+
+- ๊ด๋ฆฌ์ ๊ธ์ฐ๊ธฐ ๋ผ์ด๋ธ ๋ชจ๋: ๋ฌธ๋จ ์ด๋(๋ฐฉํฅํค) ์ ๊ตต๊ฒยท๊ธฐ์ธ์ ๋ฑ ์ธ๋ผ์ธ ๋งํฌ๋ค์ด์ด ์ฌ๋ผ์ง๋ ๋ฌธ์ ์์ .
+- ์ฝํ
์ธ ๋ ๋๋ฌ: ์ธ์ฉ(`>`) ๋ธ๋ก์ `> [!bg=์์]` ์ต์
์ค์ ์ถ๊ฐํด ์ฝ์์๊ณผ ๊ฐ์ ๋ฐฐ๊ฒฝ ํ๋ฆฌ์
์ง์ ์ง์.
+- ๊ด๋ฆฌ์ ๊ธ์ฐ๊ธฐ: ์์ค ๋ชจ๋์์ ๋ผ์ด๋ธ ๋ชจ๋๋ก ์ ํํ ๋ ํ์ฌ ์ปค์ ์ค์ ๋ผ์ด๋ธ ํ๋ฉด ์ค์์ ๊ฐ๊น๊ฒ ์คํฌ๋กคํ๋๋ก ๋ณด์ .
+
## v1.4.6
- ๊ด๋ฆฌ์ ์ค์ : ๋ก๊ณ ์
๋ก๋๊ฐ ์ ์ฅ ๋ฒํผ ์์ด ์ฆ์ DB์ ๋ฐ์๋๋ ํ๋ฆ์ ํ์ผ ์
๋ก๋ ํ ์ ์ฅ ๋ฒํผ์ผ๋ก ๋ฐ์ํ๋๋ก ์์ .
diff --git a/lib/markdown-inline.js b/lib/markdown-inline.js
index 91a05e5..029f1c5 100644
--- a/lib/markdown-inline.js
+++ b/lib/markdown-inline.js
@@ -256,6 +256,30 @@ function* iterateEditableTextUnits(root) {
}
}
+/**
+ * contenteditable ๋ฃจํธ์ ๋จ์ผ ์์ ๋
ธ๋๋ฅผ ๋งํฌ๋ค์ด ์ธ๋ผ์ธ ๋ฌธ์์ด๋ก ์ง๋ ฌํํ๋ค.
+ * @param {Node} node - ์์ ๋
ธ๋
+ * @returns {string} ๋งํฌ๋ค์ด ์กฐ๊ฐ
+ */
+const readEditableChildNodeToMarkdown = (node) => {
+ if (node.nodeType === Node.TEXT_NODE) {
+ return node.textContent || ''
+ }
+
+ if (node.nodeType !== Node.ELEMENT_NODE) {
+ return ''
+ }
+
+ const element = /** @type {HTMLElement} */ (node)
+ const tagName = element.tagName.toLowerCase()
+
+ if (tagName === 'br') {
+ return '\n'
+ }
+
+ return convertHtmlInlineNodeToMarkdown(element)
+}
+
/**
* contenteditable ๋ฃจํธ์์ ํ
์คํธ๋ฅผ ์ฝ๋๋ค.
* @param {HTMLElement} root - contenteditable ๋ฃจํธ
@@ -268,13 +292,14 @@ export const readEditableTextFromElement = (root) => {
const parts = []
- for (const unit of iterateEditableTextUnits(root)) {
- if (unit.kind === 'text') {
- parts.push(unit.node?.textContent || '')
- continue
+ for (let index = 0; index < root.childNodes.length; index += 1) {
+ const node = root.childNodes[index]
+
+ if (node.nodeType === Node.ELEMENT_NODE && isEditableBlockBreak(/** @type {HTMLElement} */ (node), root) && index > 0) {
+ parts.push('\n')
}
- parts.push('\n')
+ parts.push(readEditableChildNodeToMarkdown(node))
}
return parts.join('').replace(/\u00a0/g, ' ').trimEnd()