@@ -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 = / < i m g \s + ( [ ^ > ] * ?) \/ ? > / gi;
93+
94+ return html . replace ( imgRegex , ( match , attrs ) => {
95+ // src 속성 추출
96+ const srcMatch = / s r c = [ " ' ] ( [ ^ " ' ] + ) [ " ' ] / i. exec ( attrs ) ;
97+ if ( ! srcMatch ) return match ;
98+
99+ const src = srcMatch [ 1 ] ;
100+
101+ // alt 속성 추출
102+ const altMatch = / a l t = [ " ' ] ( [ ^ " ' ] * ) [ " ' ] / 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 이미지 () 처리
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