Files
sori.studio/composables/useContentMarkdownBlocks.js

108 lines
3.6 KiB
JavaScript

import { computed, unref } from 'vue'
import { getInArticleAdAfterBlockIds } from '../lib/content-in-article-ad.js'
import { getContentLightboxImages } from '../lib/content-gallery-images.js'
import { parseMarkdownBlocks } from '../lib/markdown-block-parser.js'
import { createHeadingIdFactory } from '../lib/markdown-toc.js'
/**
* 공개 Markdown 본문의 렌더링 블록 계산 상태를 만든다.
* @param {Object} options - 계산 옵션
* @param {import('vue').Ref<string>|string} options.content - Markdown 본문
* @param {import('vue').Ref<boolean>|boolean} options.interactive - 관리자 라이브 편집 여부
* @param {import('vue').Ref<string>|string} options.inArticleAdCode - 인아티클 광고 코드
* @returns {{
* blocks: import('vue').ComputedRef<Array<Object>>,
* normalizedInArticleAdCode: import('vue').ComputedRef<string>,
* inArticleAdAfterBlockIds: import('vue').ComputedRef<string[]>,
* headingIdsByBlock: import('vue').ComputedRef<Record<string, string>>,
* renderBlocks: import('vue').ComputedRef<Array<Object>>,
* tailInsertBeforeLine: import('vue').ComputedRef<number>,
* contentLightboxImages: import('vue').ComputedRef<Array<Object>>
* }} 본문 블록 계산 결과
*/
export const useContentMarkdownBlocks = (options) => {
const blocks = computed(() => parseMarkdownBlocks(String(unref(options.content) || '')))
const normalizedInArticleAdCode = computed(() => String(unref(options.inArticleAdCode) || '').trim())
/**
* 본문 인아티클 광고를 삽입할 블록 ID 목록을 반환한다.
* @returns {string[]} 삽입 기준 블록 ID 목록
*/
const inArticleAdAfterBlockIds = computed(() => {
if (unref(options.interactive) || !normalizedInArticleAdCode.value) {
return []
}
return getInArticleAdAfterBlockIds(blocks.value)
})
const headingIdsByBlock = computed(() => {
const createHeadingId = createHeadingIdFactory()
const ids = {}
for (const block of blocks.value) {
if (block.type === 'heading' && block.level >= 1 && block.level <= 3) {
ids[block.id] = createHeadingId(block.text)
}
}
return ids
})
/**
* 공개 본문 렌더링용 블록 목록을 반환한다.
* 인아티클 광고는 별도 v-if 대신 블록으로 삽입해 fragment 주석을 줄인다.
* @returns {Array<Object>} 렌더링 블록 목록
*/
const renderBlocks = computed(() => {
const sourceBlocks = blocks.value
const adAfterBlockIds = inArticleAdAfterBlockIds.value
if (unref(options.interactive) || !adAfterBlockIds.length) {
return sourceBlocks
}
const adAfterIdSet = new Set(adAfterBlockIds)
const result = []
for (const block of sourceBlocks) {
result.push(block)
if (adAfterIdSet.has(block.id)) {
result.push({
id: `in-article-ad-after-${block.id}`,
type: 'in-article-ad',
meta: {
afterBlockId: block.id
}
})
}
}
return result
})
/** @type {import('vue').ComputedRef<number>} 문서 맨 아래 단일 이미지 삽입 줄 */
const tailInsertBeforeLine = computed(() => {
const lastBlock = blocks.value[blocks.value.length - 1]
if (typeof lastBlock?.meta?.endLine === 'number') {
return lastBlock.meta.endLine + 1
}
return String(unref(options.content) || '').split('\n').length
})
const contentLightboxImages = computed(() => getContentLightboxImages(blocks.value))
return {
blocks,
normalizedInArticleAdCode,
inArticleAdAfterBlockIds,
headingIdsByBlock,
renderBlocks,
tailInsertBeforeLine,
contentLightboxImages
}
}