Skip to content

Commit 7f49f6a

Browse files
committed
feat: 마크다운 이미지 토큰 외 html img element 도 picture 태그로 처리하도록 변경
1 parent bc2a807 commit 7f49f6a

File tree

1 file changed

+91
-38
lines changed

1 file changed

+91
-38
lines changed

.vitepress/plugins/markdown-picture.ts

Lines changed: 91 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,70 @@ function fileExists(relativePath: string, markdownFilePath: string): boolean {
4242
}
4343
}
4444

45+
/**
46+
* img 태그를 picture 태그로 변환하는 헬퍼 함수
47+
*/
48+
function convertImgToPicture(src: string, alt: string, markdownPath: string): string {
49+
// 외부 URL이거나 svg, gif는 picture 태그로 변환하지 않음
50+
if (src.startsWith("http") || src.endsWith(".svg") || src.endsWith(".gif")) {
51+
return `<img src="${src}" alt="${alt}" loading="lazy" />`;
52+
}
53+
54+
// 이미지 포맷별 경로 생성
55+
const srcWebp = changeExtension(src, "webp");
56+
const srcAvif = changeExtension(src, "avif");
57+
const srcJpeg = changeExtension(src, "jpeg");
58+
59+
// 실제로 존재하는 파일만 source로 추가
60+
const sources: string[] = [];
61+
62+
// AVIF (최우선)
63+
if (fileExists(srcAvif, markdownPath)) {
64+
sources.push(`<source srcset="${srcAvif}" type="image/avif" />`);
65+
}
66+
// WebP (차선)
67+
if (fileExists(srcWebp, markdownPath)) {
68+
sources.push(`<source srcset="${srcWebp}" type="image/webp" />`);
69+
}
70+
// JPEG (폴백)
71+
if (fileExists(srcJpeg, markdownPath)) {
72+
sources.push(`<source srcset="${srcJpeg}" type="image/jpeg" />`);
73+
}
74+
75+
// 변환된 이미지가 없으면 원본만 사용
76+
if (sources.length === 0) {
77+
return `<img src="${src}" alt="${alt}" loading="lazy" />`;
78+
}
79+
80+
// picture 태그 생성 (원본을 최종 fallback으로 사용)
81+
return `<picture>
82+
${sources.join("\n ")}
83+
<img src="${src}" alt="${alt}" loading="lazy" />
84+
</picture>`;
85+
}
86+
87+
/**
88+
* HTML 문자열에서 img 태그를 찾아서 picture 태그로 변환
89+
*/
90+
function processHtmlImages(html: string, markdownPath: string): string {
91+
// <img> 태그를 찾는 정규식 (src와 alt 속성 추출)
92+
const imgRegex = /<img\s+([^>]*?)\/?>/gi;
93+
94+
return html.replace(imgRegex, (match, attrs) => {
95+
// src 속성 추출
96+
const srcMatch = /src=["']([^"']+)["']/i.exec(attrs);
97+
if (!srcMatch) return match;
98+
99+
const src = srcMatch[1];
100+
101+
// alt 속성 추출
102+
const altMatch = /alt=["']([^"']*)["']/i.exec(attrs);
103+
const alt = altMatch ? altMatch[1] : "";
104+
105+
return convertImgToPicture(src, alt, markdownPath);
106+
});
107+
}
108+
45109
/**
46110
* 마크다운의 이미지를 picture 태그로 변환하는 플러그인
47111
*/
@@ -52,6 +116,7 @@ export function markdownPicturePlugin(md: MarkdownIt) {
52116
return self.renderToken(tokens, idx, options);
53117
});
54118

119+
// 1. markdown 이미지 (![](src)) 처리
55120
md.renderer.rules.image = (tokens, idx, options, env, self) => {
56121
const token = tokens[idx];
57122
const srcIndex = token.attrIndex("src");
@@ -62,52 +127,40 @@ export function markdownPicturePlugin(md: MarkdownIt) {
62127

63128
const src = token.attrs![srcIndex][1];
64129
const alt = token.content;
130+
const markdownPath = env.path || "";
65131

66-
// 외부 URL이거나 svg, gif는 picture 태그로 변환하지 않음
67-
if (src.startsWith("http") || src.endsWith(".svg") || src.endsWith(".gif")) {
68-
return defaultRender(tokens, idx, options, env, self);
69-
}
70-
71-
// 이미지 포맷별 경로 생성
72-
const srcWebp = changeExtension(src, "webp");
73-
const srcAvif = changeExtension(src, "avif");
74-
const srcJpeg = changeExtension(src, "jpeg");
132+
return convertImgToPicture(src, alt, markdownPath);
133+
};
75134

76-
// 실제로 존재하는 파일만 source로 추가
77-
const sources: string[] = [];
135+
// 2. HTML inline 이미지 (<img>) 처리
136+
const defaultHtmlInline =
137+
md.renderer.rules.html_inline || md.renderer.renderToken.bind(md.renderer);
138+
md.renderer.rules.html_inline = (tokens, idx, options, env, self) => {
139+
const token = tokens[idx];
140+
const content = token.content;
78141
const markdownPath = env.path || "";
79142

80-
console.log(`\n🔍 이미지 처리 중:`);
81-
console.log(` - 원본: ${src}`);
82-
console.log(` - MD 파일: ${markdownPath}`);
83-
console.log(` - WebP: ${srcWebp} (존재: ${fileExists(srcWebp, markdownPath)})`);
84-
console.log(` - AVIF: ${srcAvif} (존재: ${fileExists(srcAvif, markdownPath)})`);
85-
console.log(` - JPEG: ${srcJpeg} (존재: ${fileExists(srcJpeg, markdownPath)})`);
86-
87-
// AVIF (최우선)
88-
if (fileExists(srcAvif, markdownPath)) {
89-
sources.push(`<source srcset="${srcAvif}" type="image/avif" />`);
90-
}
91-
// WebP (차선)
92-
if (fileExists(srcWebp, markdownPath)) {
93-
sources.push(`<source srcset="${srcWebp}" type="image/webp" />`);
94-
}
95-
// JPEG (폴백)
96-
if (fileExists(srcJpeg, markdownPath)) {
97-
sources.push(`<source srcset="${srcJpeg}" type="image/jpeg" />`);
143+
// img 태그가 있는 경우에만 처리
144+
if (content.includes("<img")) {
145+
return processHtmlImages(content, markdownPath);
98146
}
99147

100-
console.log(` - Sources 개수: ${sources.length}`);
148+
return defaultHtmlInline(tokens, idx, options, env, self);
149+
};
150+
151+
// 3. HTML block 이미지 처리
152+
const defaultHtmlBlock =
153+
md.renderer.rules.html_block || md.renderer.renderToken.bind(md.renderer);
154+
md.renderer.rules.html_block = (tokens, idx, options, env, self) => {
155+
const token = tokens[idx];
156+
const content = token.content;
157+
const markdownPath = env.path || "";
101158

102-
// 변환된 이미지가 없으면 원본만 사용
103-
if (sources.length === 0) {
104-
return `<img src="${src}" alt="${alt}" loading="lazy" />`;
159+
// img 태그가 있는 경우에만 처리
160+
if (content.includes("<img")) {
161+
return processHtmlImages(content, markdownPath);
105162
}
106163

107-
// picture 태그 생성 (원본을 최종 fallback으로 사용)
108-
return `<picture>
109-
${sources.join("\n ")}
110-
<img src="${src}" alt="${alt}" loading="lazy" />
111-
</picture>`;
164+
return defaultHtmlBlock(tokens, idx, options, env, self);
112165
};
113166
}

0 commit comments

Comments
 (0)