Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add image captions support #586

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/transform/plugins/images/collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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);
Expand Down
96 changes: 94 additions & 2 deletions src/transform/plugins/images/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,52 @@ type Opts = SVGOpts & ImageOpts;
const index: MarkdownItPluginCb<Opts> = (md, opts) => {
md.assets = [];

md.inline.ruler.after('image', 'image_caption', (state, silent) => {
osulyanov marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand All @@ -106,11 +152,15 @@ const index: MarkdownItPluginCb<Opts> = (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') || '';
Expand All @@ -124,10 +174,47 @@ const index: MarkdownItPluginCb<Opts> = (md, opts) => {

childrenTokens[j].attrSet('yfm_patched', '1');
}
j++;
}

j = 0;
const newTokens: Token[] = [];

while (j < childrenTokens.length) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace default token image with image_with_caption can be wery destructive for external plugins, which searches for image token only.

I propose to implement this in other way:

  • Check what image token has caption attr.
  • Then generate new token image_caption after this one.
  • Add renderer for image_caption which will generate all required a11y compatible html.
  • Convert caption attr on image to some class attr image.attrSet('class', image.attrGet('class') + ' yfm-image-with-caption')
  • Add some styles to main css.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I got rid of image_with_caption token

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdyt, does the current solution work or potentially could have a negative impact on other image-related plugins?
What about the CSS? Do I need to align it to the left?
Screenshot 2024-12-18 at 19 01 38

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++;
}
};
Expand All @@ -143,6 +230,11 @@ const index: MarkdownItPluginCb<Opts> = (md, opts) => {

return token.attrGet('content') || '';
};

md.renderer.rules.figure_open = () => '<figure>';
md.renderer.rules.figure_close = () => '</figure>';
md.renderer.rules.figcaption_open = () => '<figcaption>';
md.renderer.rules.figcaption_close = () => '</figcaption>';
};

export = index;
Loading