From 5381962211400d153607e5a31a77cc3483abc695 Mon Sep 17 00:00:00 2001 From: d3m1d0v Date: Fri, 4 Apr 2025 15:17:33 +0300 Subject: [PATCH] feat: added plugin to set image attributes --- package-lock.json | 28 +++--- package.json | 1 + src/transform/plugins/image-attrs.ts | 87 +++++++++++++++++++ src/transform/plugins/imsize/inline-styles.ts | 34 ++++++++ src/transform/plugins/imsize/plugin.ts | 26 +----- test/data/image-attrs/fixtures.txt | 56 ++++++++++++ test/image-attrs.test.ts | 16 ++++ 7 files changed, 211 insertions(+), 37 deletions(-) create mode 100644 src/transform/plugins/image-attrs.ts create mode 100644 src/transform/plugins/imsize/inline-styles.ts create mode 100644 test/data/image-attrs/fixtures.txt create mode 100644 test/image-attrs.test.ts diff --git a/package-lock.json b/package-lock.json index 6d018a71..5785d25a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@diplodoc/cut-extension": "^0.7.2", "@diplodoc/file-extension": "^0.2.1", "@diplodoc/tabs-extension": "^3.7.2", + "@diplodoc/utils": "^2.1.0", "chalk": "^4.1.2", "cheerio": "^1.0.0", "css": "^3.0.0", @@ -3092,19 +3093,6 @@ } } }, - "node_modules/@diplodoc/cut-extension/node_modules/@diplodoc/utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@diplodoc/utils/-/utils-2.0.1.tgz", - "integrity": "sha512-BmEpoWG2fzaBlbS0l7o/nYc4Ww9QXeQGzHM6fTFk6gPV0PRl55aBxMx+60VZn7rDhiKwAQX97yTElBzoznTt4g==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } - } - }, "node_modules/@diplodoc/directive": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@diplodoc/directive/-/directive-0.3.0.tgz", @@ -3175,6 +3163,20 @@ "integrity": "sha512-Yfrj12T3+/+lcitddoMDXQHbEu6jg0CBHzeUoRJJq7GjuwgeIqlb2i1326Gud4IZlC6uaW2GFprB0iJnGWn4DQ==", "dev": true }, + "node_modules/@diplodoc/utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@diplodoc/utils/-/utils-2.1.0.tgz", + "integrity": "sha512-1XfZSb0gPLqSRGwxlLHcXo4c59bcFomcEaDM5v2S/aFDhgNRfZgDGxWEbHwkIijfBB2rvFWuVgKzON0VDp2uqQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", diff --git a/package.json b/package.json index 663a5892..a255f7be 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@diplodoc/cut-extension": "^0.7.2", "@diplodoc/file-extension": "^0.2.1", "@diplodoc/tabs-extension": "^3.7.2", + "@diplodoc/utils": "^2.1.0", "chalk": "^4.1.2", "cheerio": "^1.0.0", "css": "^3.0.0", diff --git a/src/transform/plugins/image-attrs.ts b/src/transform/plugins/image-attrs.ts new file mode 100644 index 00000000..d1e71189 --- /dev/null +++ b/src/transform/plugins/image-attrs.ts @@ -0,0 +1,87 @@ +/* eslint-disable valid-jsdoc */ +import MarkdownIt from 'markdown-it'; +import {parseMdAttrs} from '@diplodoc/utils'; + +import {applyInlineStyling} from './imsize/inline-styles'; + +export type ImageAttributesPluginOptions = { + enableInlineStyling?: boolean; + /** + * Additional allowed attributes + * + * Attributes `width` and `height` always allowed + */ + allowedAttributes?: string[]; +}; + +const defaultAllowedAttrs = ['width', 'height'] as const; + +/** + * Plugin for parsing image node attributes. + * + * Example of markup: + * + * ```md + * ![alt](_images/image.png "title"){width=100 height=100} + * ``` + */ +export const imageAttrsPlugin: MarkdownIt.PluginWithOptions = ( + md, + opts = {}, +) => { + const allowedAttrs = new Set(defaultAllowedAttrs); + if (Array.isArray(opts.allowedAttributes)) { + for (const val of opts.allowedAttributes) { + allowedAttrs.add(val); + } + } + + md.core.ruler.push('image-attributes', (state) => { + for (const token of state.tokens) { + if (token.type !== 'inline') { + continue; + } + + const children = token.children || []; + for (let i = 0; i < children.length; i++) { + const imgToken = children[i]; + if (imgToken.type !== 'image') { + continue; + } + + const nextTextToken = children[i + 1]; + if (nextTextToken?.type !== 'text') { + continue; + } + + const res = parseMdAttrs( + md, + nextTextToken.content, + 0, + nextTextToken.content.length, + ); + if (!res) { + continue; + } + + nextTextToken.content = nextTextToken.content.slice(res.pos); + + for (const key of allowedAttrs) { + if (res.attrs[key]) { + if (key === 'class') { + const values = res.attrs[key]; + values.forEach((val) => imgToken.attrJoin(key, val)); + } else { + const value = res.attrs[key][0]; + imgToken.attrSet(key, value); + } + } + } + + if (opts.enableInlineStyling) { + applyInlineStyling(imgToken, state.env); + } + } + } + }); +}; diff --git a/src/transform/plugins/imsize/inline-styles.ts b/src/transform/plugins/imsize/inline-styles.ts new file mode 100644 index 00000000..be090f58 --- /dev/null +++ b/src/transform/plugins/imsize/inline-styles.ts @@ -0,0 +1,34 @@ +import type {Token} from 'markdown-it'; + +import {ImsizeAttr} from './const'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function applyInlineStyling(token: Token, env: any) { + let style: string | undefined = ''; + + const width = token.attrGet(ImsizeAttr.Width) || ''; + const height = token.attrGet(ImsizeAttr.Height) || ''; + + const widthWithPercent = width.includes('%'); + const heightWithPercent = height.includes('%'); + + if (width !== '') { + const widthString = widthWithPercent ? width : `${width}px`; + style += `width: ${widthString};`; + } + + if (height !== '') { + if (width !== '' && !heightWithPercent && !widthWithPercent) { + style += `aspect-ratio: ${width} / ${height};height: auto;`; + env.additionalOptionsCssWhiteList ??= {}; + env.additionalOptionsCssWhiteList['aspect-ratio'] = true; + } else { + const heightString = heightWithPercent ? height : `${height}px`; + style += `height: ${heightString};`; + } + } + + if (style) { + token.attrPush([ImsizeAttr.Style, style]); + } +} diff --git a/src/transform/plugins/imsize/plugin.ts b/src/transform/plugins/imsize/plugin.ts index 88483977..6de2c9ff 100644 --- a/src/transform/plugins/imsize/plugin.ts +++ b/src/transform/plugins/imsize/plugin.ts @@ -4,6 +4,7 @@ import type Token from 'markdown-it/lib/token'; import {ImsizeAttr} from './const'; import {parseImageSize} from './helpers'; +import {applyInlineStyling} from './inline-styles'; export type ImsizeOptions = { enableInlineStyling?: boolean; @@ -212,30 +213,7 @@ export const imageWithSize = (md: MarkdownIt, opts?: ImsizeOptions): ParserInlin } if (opts?.enableInlineStyling) { - let style: string | undefined = ''; - - const widthWithPercent = width.includes('%'); - const heightWithPercent = height.includes('%'); - - if (width !== '') { - const widthString = widthWithPercent ? width : `${width}px`; - style += `width: ${widthString};`; - } - - if (height !== '') { - if (width !== '' && !heightWithPercent && !widthWithPercent) { - style += `aspect-ratio: ${width} / ${height};height: auto;`; - state.env.additionalOptionsCssWhiteList ??= {}; - state.env.additionalOptionsCssWhiteList['aspect-ratio'] = true; - } else { - const heightString = heightWithPercent ? height : `${height}px`; - style += `height: ${heightString};`; - } - } - - if (style) { - token.attrs.push([ImsizeAttr.Style, style]); - } + applyInlineStyling(token, state.env); } } diff --git a/test/data/image-attrs/fixtures.txt b/test/data/image-attrs/fixtures.txt new file mode 100644 index 00000000..be31a04e --- /dev/null +++ b/test/data/image-attrs/fixtures.txt @@ -0,0 +1,56 @@ +Coverage. Image with attributes +. +![test]( x ){width=100 height=200} +. +

test

+. +. +![test]( x ){width=100} +. +

test

+. +. +![test]( x ){height=200} +. +

test

+. +. +![test]( x "title"){width=100 height=200} +. +

test

+. +. +![test](http://this.is.test.jpg){width=100 height=200} +. +

test

+. +. +![test](){width=100 height=200} +. +

test

+. +. +![test](test){width="100%"} +. +

test

+. +. +![test](test){height="100%"} +. +

test

+. +. +![test](test){width="100%" height="100%"} +. +

test

+. +. +![test](test){width="100%" height=200} +. +

test

+. +. +![test](test){width=100 height="100%"} +. +

test

+. diff --git a/test/image-attrs.test.ts b/test/image-attrs.test.ts new file mode 100644 index 00000000..e2027bd6 --- /dev/null +++ b/test/image-attrs.test.ts @@ -0,0 +1,16 @@ +import {join} from 'node:path'; +import MarkdownIt from 'markdown-it'; + +import {ImageAttributesPluginOptions, imageAttrsPlugin} from '../src/transform/plugins/image-attrs'; + +const generate = require('markdown-it-testgen'); + +describe('image with attributes (inlineStyling is enabled)', () => { + const md = new MarkdownIt({ + html: true, + linkify: false, + typographer: false, + }).use(imageAttrsPlugin, {enableInlineStyling: true}); + + generate(join(__dirname, 'data/image-attrs/fixtures.txt'), md); +});