From 1174cd936f84a73c8b93c7883a306ddefb6f3ae1 Mon Sep 17 00:00:00 2001 From: osulyanov Date: Mon, 16 Dec 2024 13:15:25 +0300 Subject: [PATCH 1/3] feat: Add image capttions support --- src/transform/plugins/images/collect.ts | 4 +- src/transform/plugins/images/index.ts | 96 ++++++++++++++++++++++++- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/transform/plugins/images/collect.ts b/src/transform/plugins/images/collect.ts index f67e4671..43324bbf 100644 --- a/src/transform/plugins/images/collect.ts +++ b/src/transform/plugins/images/collect.ts @@ -27,7 +27,7 @@ const collect = (input: string, options: Options) => { const children = token.children || []; children.forEach((childToken) => { - if (childToken.type !== 'image') { + if (childToken.type !== 'image' && childToken.type !== 'image_with_caption') { return; } @@ -43,7 +43,7 @@ const collect = (input: string, options: Options) => { if (singlePage && !path.includes('_includes/')) { const newSrc = relative(root, resolveRelativePath(path, src)); - result = result.replace(src, newSrc); + result = result.replace(new RegExp(src, 'g'), newSrc); } copyFile(targetPath, targetDestPath); diff --git a/src/transform/plugins/images/index.ts b/src/transform/plugins/images/index.ts index be29205f..e9429517 100644 --- a/src/transform/plugins/images/index.ts +++ b/src/transform/plugins/images/index.ts @@ -92,6 +92,52 @@ type Opts = SVGOpts & ImageOpts; const index: MarkdownItPluginCb = (md, opts) => { md.assets = []; + md.inline.ruler.after('image', 'image_caption', (state, silent) => { + const pos = state.pos; + const max = state.posMax; + + if (state.tokens.length === 0 || state.tokens[state.tokens.length - 1].type !== 'image') { + return false; + } + if (state.src.charCodeAt(pos) !== 0x7b /* { */) { + return false; + } + + let found = false; + let curPos = pos + 1; + let captionText = ''; + + while (curPos < max) { + if (state.src.charCodeAt(curPos) === 0x7d /* } */) { + const content = state.src.slice(pos + 1, curPos).trim(); + const captionMatch = content.match(/^caption(?:="([^"]*)")?$/); + if (captionMatch) { + found = true; + captionText = captionMatch[1] || ''; + break; + } + } + curPos++; + } + + if (!found) { + return false; + } + + if (!silent) { + const token = state.tokens[state.tokens.length - 1]; + token.type = 'image_with_caption'; + if (captionText) { + token.attrSet('caption', captionText); + } + state.pos = curPos + 1; + return true; + } + + state.pos = curPos + 1; + return true; + }); + const plugin = (state: StateCore) => { const tokens = state.tokens; let i = 0; @@ -106,11 +152,15 @@ const index: MarkdownItPluginCb = (md, opts) => { let j = 0; while (j < childrenTokens.length) { - if (childrenTokens[j].type === 'image') { + if ( + childrenTokens[j].type === 'image' || + childrenTokens[j].type === 'image_with_caption' + ) { const didPatch = childrenTokens[j].attrGet('yfm_patched') || false; if (didPatch) { - return; + j++; + continue; } const imgSrc = childrenTokens[j].attrGet('src') || ''; @@ -124,10 +174,47 @@ const index: MarkdownItPluginCb = (md, opts) => { childrenTokens[j].attrSet('yfm_patched', '1'); } + j++; + } + + j = 0; + const newTokens: Token[] = []; + while (j < childrenTokens.length) { + if (childrenTokens[j].type === 'image_with_caption') { + const explicitCaption = childrenTokens[j].attrGet('caption'); + const title = childrenTokens[j].attrGet('title'); + const captionText = explicitCaption || title || ''; + + const figureOpen = new state.Token('figure_open', 'figure', 1); + const figureClose = new state.Token('figure_close', 'figure', -1); + + childrenTokens[j].type = 'image'; + + if (captionText) { + const captionOpen = new state.Token('figcaption_open', 'figcaption', 1); + const captionContent = new state.Token('text', '', 0); + captionContent.content = captionText; + const captionClose = new state.Token('figcaption_close', 'figcaption', -1); + + newTokens.push( + figureOpen, + childrenTokens[j], + captionOpen, + captionContent, + captionClose, + figureClose, + ); + } else { + newTokens.push(figureOpen, childrenTokens[j], figureClose); + } + } else { + newTokens.push(childrenTokens[j]); + } j++; } + tokens[i].children = newTokens; i++; } }; @@ -143,6 +230,11 @@ const index: MarkdownItPluginCb = (md, opts) => { return token.attrGet('content') || ''; }; + + md.renderer.rules.figure_open = () => '
'; + md.renderer.rules.figure_close = () => '
'; + md.renderer.rules.figcaption_open = () => '
'; + md.renderer.rules.figcaption_close = () => '
'; }; export = index; From c19de0f06e2d37d847f48dc1229d9d19dd500b37 Mon Sep 17 00:00:00 2001 From: osulyanov Date: Wed, 18 Dec 2024 18:45:22 +0300 Subject: [PATCH 2/3] feat: another implementation of image capture Don't use separate image_capture tag that potentially can break other image-related plugings --- src/transform/plugins/images/collect.ts | 2 +- src/transform/plugins/images/index.ts | 134 +++++++++--------------- 2 files changed, 53 insertions(+), 83 deletions(-) diff --git a/src/transform/plugins/images/collect.ts b/src/transform/plugins/images/collect.ts index 43324bbf..5ae55792 100644 --- a/src/transform/plugins/images/collect.ts +++ b/src/transform/plugins/images/collect.ts @@ -27,7 +27,7 @@ const collect = (input: string, options: Options) => { const children = token.children || []; children.forEach((childToken) => { - if (childToken.type !== 'image' && childToken.type !== 'image_with_caption') { + if (childToken.type !== 'image') { return; } diff --git a/src/transform/plugins/images/index.ts b/src/transform/plugins/images/index.ts index e9429517..37e37db9 100644 --- a/src/transform/plugins/images/index.ts +++ b/src/transform/plugins/images/index.ts @@ -87,56 +87,66 @@ function convertSvg( } } -type Opts = SVGOpts & ImageOpts; +function imageCaption(state: StateCore) { + const tokens = state.tokens; -const index: MarkdownItPluginCb = (md, opts) => { - md.assets = []; + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].type !== 'inline') { + continue; + } - md.inline.ruler.after('image', 'image_caption', (state, silent) => { - const pos = state.pos; - const max = state.posMax; + const childrenTokens = tokens[i].children || []; + const newTokens: Token[] = []; - if (state.tokens.length === 0 || state.tokens[state.tokens.length - 1].type !== 'image') { - return false; - } - if (state.src.charCodeAt(pos) !== 0x7b /* { */) { - return false; - } + for (let j = 0; j < childrenTokens.length; j++) { + const token = childrenTokens[j]; + + if (token.type === 'image') { + const attrs = token.attrs || []; + const hasCaptionAttr = attrs.some(([key]) => key === 'caption'); + + if (hasCaptionAttr) { + const captionAttr = attrs.find(([key]) => key === 'caption'); + const explicitCaption = captionAttr ? captionAttr[1] : ''; + const title = attrs.find(([key]) => key === 'title'); + const captionText = explicitCaption || (title ? title[1] : ''); - let found = false; - let curPos = pos + 1; - let captionText = ''; - - while (curPos < max) { - if (state.src.charCodeAt(curPos) === 0x7d /* } */) { - const content = state.src.slice(pos + 1, curPos).trim(); - const captionMatch = content.match(/^caption(?:="([^"]*)")?$/); - if (captionMatch) { - found = true; - captionText = captionMatch[1] || ''; - break; + const figureOpen = new state.Token('figure_open', 'figure', 1); + const figureClose = new state.Token('figure_close', 'figure', -1); + + if (captionText) { + const captionOpen = new state.Token('figcaption_open', 'figcaption', 1); + const captionContent = new state.Token('text', '', 0); + captionContent.content = captionText; + const captionClose = new state.Token('figcaption_close', 'figcaption', -1); + + newTokens.push( + figureOpen, + token, + captionOpen, + captionContent, + captionClose, + figureClose, + ); + } else { + newTokens.push(figureOpen, token, figureClose); + } + } else { + newTokens.push(token); } + } else { + newTokens.push(token); } - curPos++; } - if (!found) { - return false; - } + tokens[i].children = newTokens; + } +} - if (!silent) { - const token = state.tokens[state.tokens.length - 1]; - token.type = 'image_with_caption'; - if (captionText) { - token.attrSet('caption', captionText); - } - state.pos = curPos + 1; - return true; - } +type Opts = SVGOpts & ImageOpts; - state.pos = curPos + 1; - return true; - }); +const index: MarkdownItPluginCb = (md, opts) => { + md.assets = []; const plugin = (state: StateCore) => { const tokens = state.tokens; @@ -152,10 +162,7 @@ const index: MarkdownItPluginCb = (md, opts) => { let j = 0; while (j < childrenTokens.length) { - if ( - childrenTokens[j].type === 'image' || - childrenTokens[j].type === 'image_with_caption' - ) { + if (childrenTokens[j].type === 'image') { const didPatch = childrenTokens[j].attrGet('yfm_patched') || false; if (didPatch) { @@ -177,44 +184,6 @@ const index: MarkdownItPluginCb = (md, opts) => { j++; } - j = 0; - const newTokens: Token[] = []; - - while (j < childrenTokens.length) { - if (childrenTokens[j].type === 'image_with_caption') { - const explicitCaption = childrenTokens[j].attrGet('caption'); - const title = childrenTokens[j].attrGet('title'); - const captionText = explicitCaption || title || ''; - - const figureOpen = new state.Token('figure_open', 'figure', 1); - const figureClose = new state.Token('figure_close', 'figure', -1); - - childrenTokens[j].type = 'image'; - - if (captionText) { - const captionOpen = new state.Token('figcaption_open', 'figcaption', 1); - const captionContent = new state.Token('text', '', 0); - captionContent.content = captionText; - const captionClose = new state.Token('figcaption_close', 'figcaption', -1); - - newTokens.push( - figureOpen, - childrenTokens[j], - captionOpen, - captionContent, - captionClose, - figureClose, - ); - } else { - newTokens.push(figureOpen, childrenTokens[j], figureClose); - } - } else { - newTokens.push(childrenTokens[j]); - } - j++; - } - - tokens[i].children = newTokens; i++; } }; @@ -225,6 +194,7 @@ const index: MarkdownItPluginCb = (md, opts) => { md.core.ruler.push('images', plugin); } + md.core.ruler.push('image_caption', imageCaption); md.renderer.rules.image_svg = (tokens, index) => { const token = tokens[index]; From 98f3739c96b04c5febb8bc8ad78177545ade6561 Mon Sep 17 00:00:00 2001 From: osulyanov Date: Sun, 22 Dec 2024 10:59:48 +0300 Subject: [PATCH 3/3] chore: remove redundand code Renderer rules are not needed because markdown-it already has default HTML renderers for basic HTML tags --- src/transform/plugins/images/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/transform/plugins/images/index.ts b/src/transform/plugins/images/index.ts index 37e37db9..b833f060 100644 --- a/src/transform/plugins/images/index.ts +++ b/src/transform/plugins/images/index.ts @@ -200,11 +200,6 @@ const index: MarkdownItPluginCb = (md, opts) => { return token.attrGet('content') || ''; }; - - md.renderer.rules.figure_open = () => '
'; - md.renderer.rules.figure_close = () => '
'; - md.renderer.rules.figcaption_open = () => '
'; - md.renderer.rules.figcaption_close = () => '
'; }; export = index;