From 4264c10da3030a87c1185301886da47465aa2f44 Mon Sep 17 00:00:00 2001 From: Shreya Kamble Date: Thu, 6 Mar 2025 12:28:27 +0530 Subject: [PATCH 1/4] fix: retain empty strings value for alt attr for img and asset while conversion to html --- src/toRedactor.tsx | 10 +++++++--- test/expectedJson.ts | 30 ++++++++++++++++++++++++++++++ test/toRedactor.test.ts | 7 +++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/toRedactor.tsx b/src/toRedactor.tsx index ca5abde..1b8c7b5 100644 --- a/src/toRedactor.tsx +++ b/src/toRedactor.tsx @@ -506,11 +506,15 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string delete attrsJson['url'] } delete attrsJson['redactor-attributes'] - Object.entries(attrsJson).forEach((key) => { - if (forbiddenAttrChars.some(char => key[0].includes(char))) { + Object.entries(attrsJson).forEach((item) => { + if (forbiddenAttrChars.some(char => item[0].includes(char))) { return; } - return key[1] ? (key[1] !== '' ? (attrs += `${key[0]}="${replaceHtmlEntities(key[1])}" `) : '') : '' + if((jsonValue['type'] === 'img' || (jsonValue['type'] === 'reference') && jsonValue.attrs['display-type'] === 'display' ) && item[0] === 'alt'){ + attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" ` + return; + } + return item[1] ? (item[1] !== '' ? (attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" `) : '') : '' }) attrs = (attrs.trim() ? ' ' : '') + attrs.trim() } diff --git a/test/expectedJson.ts b/test/expectedJson.ts index a77a1d5..0c79455 100644 --- a/test/expectedJson.ts +++ b/test/expectedJson.ts @@ -2204,6 +2204,36 @@ export default { ] + }, + "RT-268":{ + "html": ``, + "json": + { + "id": "a4794fb7214745a2a47fc24104b762f9", + "type": "docs", + "children": [ + { + "type": "img", + "attrs": { + "url": "image_url.jpeg", + "redactor-attributes": { + "alt": "", + "src": "image_url.jpeg", + "width": "100" + }, + "width": "100" + }, + "uid": "18ff239605014dcaaa23c705caf99403", + "children": [ + { + "text": "" + } + ] + } + ] + } + + } } \ No newline at end of file diff --git a/test/toRedactor.test.ts b/test/toRedactor.test.ts index adbb8aa..4801710 100644 --- a/test/toRedactor.test.ts +++ b/test/toRedactor.test.ts @@ -3,6 +3,7 @@ import isEqual from "lodash.isequal" import expectedValue from "./expectedJson" import { imageAssetData } from "./testingData" +import exp from "constants" describe("Testing json to html conversion", () => { it("heading conversion", () => { @@ -292,5 +293,11 @@ describe("Testing json to html conversion", () => { const html = toRedactor(json); expect(html).toBe(`Infographic showing 3 results from Forrester study of Contentstack CMS: $3M increase in profit, $507.3K productivity savings and $2.0M savings due to reduced time to publish.`) }) + + test(' should retain empty string value for alt attribute', () => { + const json = expectedValue['RT-268'].json; + const html = toRedactor(json); + expect(html).toBe(expectedValue['RT-268'].html); + }) }) From a9c37565954f64b41b1473118ccdc59180b5f262 Mon Sep 17 00:00:00 2001 From: Shreya Kamble Date: Thu, 6 Mar 2025 12:43:20 +0530 Subject: [PATCH 2/4] chore: remove unnecessary imports --- test/toRedactor.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/toRedactor.test.ts b/test/toRedactor.test.ts index 4801710..6246b3e 100644 --- a/test/toRedactor.test.ts +++ b/test/toRedactor.test.ts @@ -1,9 +1,7 @@ import { toRedactor } from "../src/toRedactor" -import isEqual from "lodash.isequal" - import expectedValue from "./expectedJson" import { imageAssetData } from "./testingData" -import exp from "constants" + describe("Testing json to html conversion", () => { it("heading conversion", () => { From b471e3afd426222fa8878b58a111628076bea452 Mon Sep 17 00:00:00 2001 From: Shreya Kamble Date: Mon, 10 Mar 2025 17:43:21 +0530 Subject: [PATCH 3/4] feat: dynamically allow attribute values to be empty if elemnt and attribute is passed throug as options --- README.md | 10 ++++ src/toRedactor.tsx | 35 ++++++++++-- src/types.ts | 2 + test/expectedJson.ts | 120 ++++++++++++++++++++++++++++++++++++++-- test/toRedactor.test.ts | 27 +++++++-- 5 files changed, 181 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 726a853..55df54b 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,12 @@ On the other hand, the `customTextWrapper` parser function provides the followin - `value`: The value passed against the child element +You can pass an object to `allowedEmptyAttributes` to retain empty attribute values for specific element types during HTML conversion. + +**Note:** +By default, if nothing is passed to `allowedEmptyAttributes`, we retain the `alt` attribute for `` and `reference` (asset) element types, even when its value is empty, during HTML conversion. + + You can use the following customized JSON RTE Serializer code to convert your JSON RTE field data into HTML format. ```javascript @@ -216,6 +222,10 @@ const htmlValue = jsonToHtml( return `${child}`; }, }, + allowedEmptyAttributes : { + "p": ["dir"], + "img" : ["width"] + } } ); diff --git a/src/toRedactor.tsx b/src/toRedactor.tsx index 1b8c7b5..6f3cf9f 100644 --- a/src/toRedactor.tsx +++ b/src/toRedactor.tsx @@ -1,6 +1,6 @@ import kebbab from 'lodash.kebabcase' import isEmpty from 'lodash.isempty' -import {IJsonToHtmlElementTags, IJsonToHtmlOptions, IJsonToHtmlTextTags} from './types' +import {IJsonToHtmlElementTags, IJsonToHtmlOptions, IJsonToHtmlTextTags, IJsonToHtmlAllowedEmptyAttributes} from './types' import isPlainObject from 'lodash.isplainobject' import {replaceHtmlEntities, forbiddenAttrChars } from './utils' @@ -213,11 +213,28 @@ const TEXT_WRAPPERS: IJsonToHtmlTextTags = { return `${child}` }, } +const ALLOWED_EMPTY_ATTRIBUTES: IJsonToHtmlAllowedEmptyAttributes = { + img: ['alt'], + reference: ['alt'] +} + export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string => { //TODO: optimize assign once per function call if(options?.customTextWrapper && !isEmpty(options.customTextWrapper)){ Object.assign(TEXT_WRAPPERS,options.customTextWrapper) } + if (options?.allowedEmptyAttributes && !isEmpty(options.allowedEmptyAttributes)) { + Object.keys(options.allowedEmptyAttributes).forEach(key => { + if (key === 'img' || key === 'reference') { + ALLOWED_EMPTY_ATTRIBUTES[key] = [ + 'alt', + ...(options.allowedEmptyAttributes?.[key] || []) + ]; + } else { + ALLOWED_EMPTY_ATTRIBUTES[key] = options.allowedEmptyAttributes?.[key] ?? []; + } + }); + } if (jsonValue.hasOwnProperty('text')) { let text = jsonValue['text'].replace(//g, '>') if (jsonValue['break']) { @@ -510,12 +527,20 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string if (forbiddenAttrChars.some(char => item[0].includes(char))) { return; } - if((jsonValue['type'] === 'img' || (jsonValue['type'] === 'reference') && jsonValue.attrs['display-type'] === 'display' ) && item[0] === 'alt'){ - attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" ` - return; - } + if (ALLOWED_EMPTY_ATTRIBUTES.hasOwnProperty(jsonValue['type'])) { + if (ALLOWED_EMPTY_ATTRIBUTES[jsonValue['type']].includes(item[0])) { + // Check for 'display-type' attribute for reference type, as refernce is used for entries and assets + if (jsonValue['type'] === 'reference' && jsonValue.attrs['display-type'] === 'display') { + attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" `; + return; + } + attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" `; + return; + } + } return item[1] ? (item[1] !== '' ? (attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" `) : '') : '' }) + attrs = (attrs.trim() ? ' ' : '') + attrs.trim() } if (jsonValue['type'] === 'table') { diff --git a/src/types.ts b/src/types.ts index adb3785..e531ecf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,10 +14,12 @@ export interface IHtmlToJsonElementTags { [key: string]: (el:HTMLElement) => IHt export interface IJsonToHtmlTextTags { [key: string]: (child:any, value:any) => string } export interface IJsonToHtmlElementTags { [key: string]: (attrs:string,child:string,jsonBlock:IAnyObject,extraProps?:object) => string } +export interface IJsonToHtmlAllowedEmptyAttributes { [key: string]: string[]; } export interface IJsonToMarkdownElementTags{[key: string]: (attrsJson:IAnyObject,child:string) => string} export interface IJsonToMarkdownTextTags{ [key: string]: (child:any, value:any) => string } export interface IJsonToHtmlOptions { customElementTypes?: IJsonToHtmlElementTags, customTextWrapper?: IJsonToHtmlTextTags, allowNonStandardTypes?: boolean, + allowedEmptyAttributes?: IJsonToHtmlAllowedEmptyAttributes, } diff --git a/test/expectedJson.ts b/test/expectedJson.ts index 0c79455..b2b0d87 100644 --- a/test/expectedJson.ts +++ b/test/expectedJson.ts @@ -2206,8 +2206,13 @@ export default { }, "RT-268":{ - "html": ``, - "json": + "html": [ + ``, + `
`, + `

This is for testing purpose

`, + `

This is for testing purpose

` + ], + "json": [ { "id": "a4794fb7214745a2a47fc24104b762f9", "type": "docs", @@ -2231,9 +2236,116 @@ export default { ] } ] + }, + { + "uid": "a59f9108e99747d4b3358d9e22b7c685", + "type": "doc", + "attrs": { + "dirty": true + }, + "children": [ + { + "uid": "a41aede53efe46018e00de52b6d0970e", + "type": "reference", + "attrs": { + "display-type": "display", + "asset-uid": "blt8c34458f407b3862", + "content-type-uid": "sys_assets", + "asset-link": "https://stag-images.csnonprod.com/v3/assets/blt3381770ff6804fa1/blt8c34458f407b3862/6572c368e7a0d4196d105010/compass-logo-v2-final.png", + "asset-name": "compass-logo-v2-final.png", + "asset-type": "image/png", + "type": "asset", + "class-name": "embedded-asset", + "alt": "", + "asset-alt": "compass-logo-v2-final.png", + "inline": false + }, + "children": [ + { + "text": "" + } + ] + } + ], + "_version": 2 + }, + { + "uid": "a59f9108e99747d4b3358d9e22b7c685", + "type": "doc", + "attrs": { + "dirty": true + }, + "children": [ + { + "uid": "8e7309d3c617401898f45c1c3ae62f1e", + "type": "p", + "attrs": { + "style": {}, + "redactor-attributes": {}, + "dir": "" + }, + "children": [ + { + "text": "This is for testing purpose" + } + ] + } + ], + "_version": 2 + }, + { + "uid": "a59f9108e99747d4b3358d9e22b7c685", + "type": "doc", + "attrs": { + "dirty": true + }, + "children": [ + { + "uid": "e22e5bcaa65b41beb3cc48a8d8cf175c", + "type": "img", + "attrs": { + "url": "https://images.contentstack.io/v3/assets/blta29a98d37041ffc4/blt0f2e045a5f4ae8bd/646df9c6b8153a80eb810a6e/tony-litvyak-PzZQFFeRt54-unsplash.jpg", + "width": 100, + "dirty": true, + "style": { + "text-align": "left", + "width": "100px", + "max-width": "100px", + "float": "left" + }, + "redactor-attributes": { + "position": "left", + "alt": "" + }, + "dir": "", + "alt": "", + "max-width": 100, + "height": 150 + }, + "children": [ + { + "text": "" + } + ] + }, + { + "uid": "8e7309d3c617401898f45c1c3ae62f1e", + "type": "p", + "attrs": { + "style": {}, + "redactor-attributes": {}, + "dir": "" + }, + "children": [ + { + "text": "This is for testing purpose" + } + ] + } + ], + "_version": 2 } - - + ] } } \ No newline at end of file diff --git a/test/toRedactor.test.ts b/test/toRedactor.test.ts index 6246b3e..86a5203 100644 --- a/test/toRedactor.test.ts +++ b/test/toRedactor.test.ts @@ -292,10 +292,29 @@ describe("Testing json to html conversion", () => { expect(html).toBe(`Infographic showing 3 results from Forrester study of Contentstack CMS: $3M increase in profit, $507.3K productivity savings and $2.0M savings due to reduced time to publish.`) }) - test(' should retain empty string value for alt attribute', () => { - const json = expectedValue['RT-268'].json; - const html = toRedactor(json); - expect(html).toBe(expectedValue['RT-268'].html); + describe("RT-268", ()=>{ + it(' should retain empty string value for alt attribute for img type', () => { + const json = expectedValue['RT-268'].json[0]; + const html = toRedactor(json); + expect(html).toBe(expectedValue['RT-268'].html[0]); + }) + it(' should retain empty string value for alt attribute for asset reference', () => { + const json = expectedValue['RT-268'].json[1]; + const html = toRedactor(json); + expect(html).toBe(expectedValue['RT-268'].html[1]); + }) + it(' should retain empty string value for attributes passed through "allowedEmptyAttributes" prop', () => { + const json = expectedValue['RT-268'].json[2]; + const html = toRedactor(json, {allowedEmptyAttributes: { p: ["dir"]} }); + expect(html).toBe(expectedValue['RT-268'].html[2]); + }) + it(' should retain empty string value for attributes passed through "allowedEmptyAttributes" prop, where alt is empty too (default empty)', () => { + const json = expectedValue['RT-268'].json[3]; + const html = toRedactor(json, {allowedEmptyAttributes: { "img": ['dir'],"p": ["dir"]} }); + expect(html).toBe(expectedValue['RT-268'].html[3]); }) + + }) + }) From e5e0f672c65e88447d15c31e107f07f4efde439b Mon Sep 17 00:00:00 2001 From: Shreya Kamble Date: Thu, 13 Mar 2025 12:05:13 +0530 Subject: [PATCH 4/4] chore: code optimisation --- src/toRedactor.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/toRedactor.tsx b/src/toRedactor.tsx index 6f3cf9f..a99585c 100644 --- a/src/toRedactor.tsx +++ b/src/toRedactor.tsx @@ -227,7 +227,7 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string Object.keys(options.allowedEmptyAttributes).forEach(key => { if (key === 'img' || key === 'reference') { ALLOWED_EMPTY_ATTRIBUTES[key] = [ - 'alt', + ...ALLOWED_EMPTY_ATTRIBUTES[key], ...(options.allowedEmptyAttributes?.[key] || []) ]; } else { @@ -523,24 +523,21 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string delete attrsJson['url'] } delete attrsJson['redactor-attributes'] + Object.entries(attrsJson).forEach((item) => { if (forbiddenAttrChars.some(char => item[0].includes(char))) { return; } - if (ALLOWED_EMPTY_ATTRIBUTES.hasOwnProperty(jsonValue['type'])) { - if (ALLOWED_EMPTY_ATTRIBUTES[jsonValue['type']].includes(item[0])) { - // Check for 'display-type' attribute for reference type, as refernce is used for entries and assets - if (jsonValue['type'] === 'reference' && jsonValue.attrs['display-type'] === 'display') { + + if (ALLOWED_EMPTY_ATTRIBUTES.hasOwnProperty(jsonValue['type']) && ALLOWED_EMPTY_ATTRIBUTES[jsonValue['type']].includes(item[0])) { + if ( jsonValue['type'] !== 'reference' || (jsonValue['type'] === 'reference' && jsonValue.attrs['display-type'] === 'display')) { attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" `; return; - } - attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" `; - return; - } + } } return item[1] ? (item[1] !== '' ? (attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" `) : '') : '' }) - + attrs = (attrs.trim() ? ' ' : '') + attrs.trim() } if (jsonValue['type'] === 'table') {