diff --git a/_config.yml b/_config.yml index dd74651ad5..b5fe7db4c6 100644 --- a/_config.yml +++ b/_config.yml @@ -546,6 +546,21 @@ mermaid: wavedrom: enable: false +# Picture Caption tag +picture_caption: + picture_border_radius: + enable: false + radius_size: 5px + # Picture gap size between pictures. No strict validation for units; the `px` unit is also acceptable. + picture_gap: 0.5em + # Available caption position: top | bottom + caption_position: bottom + # Available caption alignment: left | center | right + caption_alignment: center + # Caption margin size between picture and caption. No strict validation for units; the `px` unit is also acceptable. + caption_margin: 0.5em + caption_font_size: 0.875em + # --------------------------------------------------------------- # Third Party Plugins & Services Settings # See: https://theme-next.js.org/docs/third-party-services/ diff --git a/scripts/tags/index.js b/scripts/tags/index.js index d0c4392744..b1ca3a9dff 100644 --- a/scripts/tags/index.js +++ b/scripts/tags/index.js @@ -48,6 +48,12 @@ const pdf = require('./pdf')(hexo); hexo.extend.tag.register('pdf', pdf); +const pictureCaption = require('./picture-caption')(hexo); + +hexo.extend.tag.register('picturecaption', pictureCaption); +hexo.extend.tag.register('piccap', pictureCaption); +hexo.extend.tag.register('pc', pictureCaption); + const postTabs = require('./tabs')(hexo); hexo.extend.tag.register('tabs', postTabs, true); diff --git a/scripts/tags/picture-caption.js b/scripts/tags/picture-caption.js new file mode 100644 index 0000000000..f86f2dab1e --- /dev/null +++ b/scripts/tags/picture-caption.js @@ -0,0 +1,76 @@ +/** + * picture-caption.js | https://theme-next.js.org/docs/tag-plugins/picture-caption + * Description: Insert an image with caption. + * Usage: + * {% picturecaption pictureUrl, captionText %} + * {% picturecaption pictureUrl, pictureWidth, pictureHeight, captionText, captionUrl, captionIcon, captionPosition, captionAlignment %} + */ + +'use strict'; + +module.exports = ctx => function(args) { + args = args.join(' ').split(','); + + const pictureUrl = args[0].trim(); + if (!pictureUrl) { + ctx.log.warn('Image URL can NOT be empty in picture-caption tag.'); + return ''; + } + + // Default value. + let pictureWidth = 'auto'; + let pictureHeight = 'auto'; + let captionText = ''; + let captionUrl = ''; + let captionIcon = ''; + const theme = ctx.theme.config; + let captionPosition = theme.picture_caption.caption_position; + let captionAlignment = theme.picture_caption.caption_alignment; + + // Determine the invocation method based on the number of parameters. + if (args.length === 2) { + captionText = args[1].trim() || 'Picture'; + } else if (args.length >= 3) { + // Take parameters from back to front. + captionAlignment = ['left', 'center', 'right'].includes(args[args.length - 1].trim()) ? args.pop().trim() : 'center'; + captionPosition = ['top', 'bottom'].includes(args[args.length - 1].trim()) ? args.pop().trim() : 'bottom'; + captionIcon = args.pop().trim(); // This value can be null. + captionUrl = args.pop().trim(); // This value can be null. + // Take parameters from front to back. + pictureWidth = args[1].trim() || 'auto'; + pictureHeight = args[2].trim() || 'auto'; + // Allow the captionText to contain English commas, and extract all characters starting from index 3 to the end. + captionText = args.slice(3).join(',') || 'Picture'; + } + + const style = `width: ${pictureWidth}; height: ${pictureHeight}; object-fit: contain;`; + + const imgTag = `${captionText || 'Picture'}`; + + // Handle captionUrl and captionIcon, default value is null, 'None/none' or empty string is considered as no link. + const hascaptionUrl = captionUrl && captionUrl.toLowerCase() !== 'none'; + const hascaptionIcon = captionIcon && captionIcon.toLowerCase() !== 'none'; + + let captionHtml = ''; + if (captionText) { + let captionContent = ''; + if (hascaptionIcon) { + if (!captionIcon.startsWith('fa')) { captionIcon = 'fa fa-' + captionIcon; } + captionContent = `${captionText}`; + } else { + captionContent = captionText; + } + if (hascaptionUrl) { + captionContent = `${captionContent}`; + } + captionHtml = `
${captionContent}
`; + } + + // Determine the order based on captionPosition. + const content + = captionPosition === 'bottom' + ? `${imgTag}${captionHtml}` + : `${captionHtml}${imgTag}`; + + return `
${content}
`; +}; diff --git a/source/css/_common/scaffolding/tags/index.styl b/source/css/_common/scaffolding/tags/index.styl index 1fe49fc64c..435d56715a 100644 --- a/source/css/_common/scaffolding/tags/index.styl +++ b/source/css/_common/scaffolding/tags/index.styl @@ -6,4 +6,5 @@ @import 'wavedrom'; @import 'note'; @import 'pdf'; +@import 'picture-caption'; @import 'tabs'; diff --git a/source/css/_common/scaffolding/tags/picture-caption.styl b/source/css/_common/scaffolding/tags/picture-caption.styl new file mode 100644 index 0000000000..fbe9a9b7c6 --- /dev/null +++ b/source/css/_common/scaffolding/tags/picture-caption.styl @@ -0,0 +1,44 @@ +.picture-caption-container { + display: inline-flex; + flex-direction: column; + margin-bottom: 20px; + vertical-align: bottom; +} + +.picture-caption-container + .picture-caption-container { + margin-left: unquote(hexo-config('picture_caption.picture_gap')); +} + +.picture-caption-container img { + margin: 0 !important; + if(hexo-config('picture_caption.picture_border_radius.enable')) + { + border-radius: unquote(hexo-config('picture_caption.picture_border_radius.radius_size')); + } +} + +.picture-caption-caption { + margin: 0; + line-height: 1; + font-size: unquote(hexo-config('picture_caption.caption_font_size')); +} + +.picture-caption-align-left { + text-align: left; +} + +.picture-caption-align-center { + text-align: center; +} + +.picture-caption-align-right { + text-align: right; +} + +.picture-caption-position-top .picture-caption-caption { + margin-bottom: unquote(hexo-config('picture_caption.caption_margin')); +} + +.picture-caption-position-bottom .picture-caption-caption { + margin-top: unquote(hexo-config('picture_caption.caption_margin')); +}