Skip to content

Commit b471e3a

Browse files
committed
feat: dynamically allow attribute values to be empty if elemnt and attribute is passed throug as options
1 parent a9c3756 commit b471e3a

File tree

5 files changed

+181
-13
lines changed

5 files changed

+181
-13
lines changed

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ On the other hand, the `customTextWrapper` parser function provides the followin
162162
- `value`: The value passed against the child element
163163

164164

165+
You can pass an object to `allowedEmptyAttributes` to retain empty attribute values for specific element types during HTML conversion.
166+
167+
**Note:**
168+
By default, if nothing is passed to `allowedEmptyAttributes`, we retain the `alt` attribute for `<img>` and `reference` (asset) element types, even when its value is empty, during HTML conversion.
169+
170+
165171
You can use the following customized JSON RTE Serializer code to convert your JSON RTE field data into HTML format.
166172

167173
```javascript
@@ -216,6 +222,10 @@ const htmlValue = jsonToHtml(
216222
return `<color data-color="${value}">${child}</color>`;
217223
},
218224
},
225+
allowedEmptyAttributes : {
226+
"p": ["dir"],
227+
"img" : ["width"]
228+
}
219229
}
220230
);
221231

src/toRedactor.tsx

+30-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import kebbab from 'lodash.kebabcase'
22
import isEmpty from 'lodash.isempty'
3-
import {IJsonToHtmlElementTags, IJsonToHtmlOptions, IJsonToHtmlTextTags} from './types'
3+
import {IJsonToHtmlElementTags, IJsonToHtmlOptions, IJsonToHtmlTextTags, IJsonToHtmlAllowedEmptyAttributes} from './types'
44
import isPlainObject from 'lodash.isplainobject'
55
import {replaceHtmlEntities, forbiddenAttrChars } from './utils'
66

@@ -213,11 +213,28 @@ const TEXT_WRAPPERS: IJsonToHtmlTextTags = {
213213
return `<span data-type='inlineCode'>${child}</span>`
214214
},
215215
}
216+
const ALLOWED_EMPTY_ATTRIBUTES: IJsonToHtmlAllowedEmptyAttributes = {
217+
img: ['alt'],
218+
reference: ['alt']
219+
}
220+
216221
export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string => {
217222
//TODO: optimize assign once per function call
218223
if(options?.customTextWrapper && !isEmpty(options.customTextWrapper)){
219224
Object.assign(TEXT_WRAPPERS,options.customTextWrapper)
220225
}
226+
if (options?.allowedEmptyAttributes && !isEmpty(options.allowedEmptyAttributes)) {
227+
Object.keys(options.allowedEmptyAttributes).forEach(key => {
228+
if (key === 'img' || key === 'reference') {
229+
ALLOWED_EMPTY_ATTRIBUTES[key] = [
230+
'alt',
231+
...(options.allowedEmptyAttributes?.[key] || [])
232+
];
233+
} else {
234+
ALLOWED_EMPTY_ATTRIBUTES[key] = options.allowedEmptyAttributes?.[key] ?? [];
235+
}
236+
});
237+
}
221238
if (jsonValue.hasOwnProperty('text')) {
222239
let text = jsonValue['text'].replace(/</g, '&lt;').replace(/>/g, '&gt;')
223240
if (jsonValue['break']) {
@@ -510,12 +527,20 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string
510527
if (forbiddenAttrChars.some(char => item[0].includes(char))) {
511528
return;
512529
}
513-
if((jsonValue['type'] === 'img' || (jsonValue['type'] === 'reference') && jsonValue.attrs['display-type'] === 'display' ) && item[0] === 'alt'){
514-
attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" `
515-
return;
516-
}
530+
if (ALLOWED_EMPTY_ATTRIBUTES.hasOwnProperty(jsonValue['type'])) {
531+
if (ALLOWED_EMPTY_ATTRIBUTES[jsonValue['type']].includes(item[0])) {
532+
// Check for 'display-type' attribute for reference type, as refernce is used for entries and assets
533+
if (jsonValue['type'] === 'reference' && jsonValue.attrs['display-type'] === 'display') {
534+
attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" `;
535+
return;
536+
}
537+
attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" `;
538+
return;
539+
}
540+
}
517541
return item[1] ? (item[1] !== '' ? (attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" `) : '') : ''
518542
})
543+
519544
attrs = (attrs.trim() ? ' ' : '') + attrs.trim()
520545
}
521546
if (jsonValue['type'] === 'table') {

src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ export interface IHtmlToJsonElementTags { [key: string]: (el:HTMLElement) => IHt
1414

1515
export interface IJsonToHtmlTextTags { [key: string]: (child:any, value:any) => string }
1616
export interface IJsonToHtmlElementTags { [key: string]: (attrs:string,child:string,jsonBlock:IAnyObject,extraProps?:object) => string }
17+
export interface IJsonToHtmlAllowedEmptyAttributes { [key: string]: string[]; }
1718
export interface IJsonToMarkdownElementTags{[key: string]: (attrsJson:IAnyObject,child:string) => string}
1819
export interface IJsonToMarkdownTextTags{ [key: string]: (child:any, value:any) => string }
1920
export interface IJsonToHtmlOptions {
2021
customElementTypes?: IJsonToHtmlElementTags,
2122
customTextWrapper?: IJsonToHtmlTextTags,
2223
allowNonStandardTypes?: boolean,
24+
allowedEmptyAttributes?: IJsonToHtmlAllowedEmptyAttributes,
2325
}

test/expectedJson.ts

+116-4
Original file line numberDiff line numberDiff line change
@@ -2206,8 +2206,13 @@ export default {
22062206

22072207
},
22082208
"RT-268":{
2209-
"html": `<img alt="" src="image_url.jpeg" width="100" style="width: 100; height: auto;" />`,
2210-
"json":
2209+
"html": [
2210+
`<img alt="" src="image_url.jpeg" width="100" style="width: 100; height: auto;" />`,
2211+
`<figure style="margin: 0"><img src="https://stag-images.csnonprod.com/v3/assets/blt3381770ff6804fa1/blt8c34458f407b3862/6572c368e7a0d4196d105010/compass-logo-v2-final.png" class="embedded-asset" content-type-uid="sys_assets" type="asset" alt="" asset-alt="compass-logo-v2-final.png" style="width: auto" data-sys-asset-filelink="https://stag-images.csnonprod.com/v3/assets/blt3381770ff6804fa1/blt8c34458f407b3862/6572c368e7a0d4196d105010/compass-logo-v2-final.png" data-sys-asset-uid="blt8c34458f407b3862" data-sys-asset-filename="compass-logo-v2-final.png" data-sys-asset-contenttype="image/png" data-sys-asset-alt="compass-logo-v2-final.png" sys-style-type="display"/></figure>`,
2212+
`<p dir="">This is for testing purpose</p>`,
2213+
`<img position="left" alt="" width="100" dirty="true" dir="" max-width="100" height="150" src="https://images.contentstack.io/v3/assets/blta29a98d37041ffc4/blt0f2e045a5f4ae8bd/646df9c6b8153a80eb810a6e/tony-litvyak-PzZQFFeRt54-unsplash.jpg" /><p dir="">This is for testing purpose</p>`
2214+
],
2215+
"json": [
22112216
{
22122217
"id": "a4794fb7214745a2a47fc24104b762f9",
22132218
"type": "docs",
@@ -2231,9 +2236,116 @@ export default {
22312236
]
22322237
}
22332238
]
2239+
},
2240+
{
2241+
"uid": "a59f9108e99747d4b3358d9e22b7c685",
2242+
"type": "doc",
2243+
"attrs": {
2244+
"dirty": true
2245+
},
2246+
"children": [
2247+
{
2248+
"uid": "a41aede53efe46018e00de52b6d0970e",
2249+
"type": "reference",
2250+
"attrs": {
2251+
"display-type": "display",
2252+
"asset-uid": "blt8c34458f407b3862",
2253+
"content-type-uid": "sys_assets",
2254+
"asset-link": "https://stag-images.csnonprod.com/v3/assets/blt3381770ff6804fa1/blt8c34458f407b3862/6572c368e7a0d4196d105010/compass-logo-v2-final.png",
2255+
"asset-name": "compass-logo-v2-final.png",
2256+
"asset-type": "image/png",
2257+
"type": "asset",
2258+
"class-name": "embedded-asset",
2259+
"alt": "",
2260+
"asset-alt": "compass-logo-v2-final.png",
2261+
"inline": false
2262+
},
2263+
"children": [
2264+
{
2265+
"text": ""
2266+
}
2267+
]
2268+
}
2269+
],
2270+
"_version": 2
2271+
},
2272+
{
2273+
"uid": "a59f9108e99747d4b3358d9e22b7c685",
2274+
"type": "doc",
2275+
"attrs": {
2276+
"dirty": true
2277+
},
2278+
"children": [
2279+
{
2280+
"uid": "8e7309d3c617401898f45c1c3ae62f1e",
2281+
"type": "p",
2282+
"attrs": {
2283+
"style": {},
2284+
"redactor-attributes": {},
2285+
"dir": ""
2286+
},
2287+
"children": [
2288+
{
2289+
"text": "This is for testing purpose"
2290+
}
2291+
]
2292+
}
2293+
],
2294+
"_version": 2
2295+
},
2296+
{
2297+
"uid": "a59f9108e99747d4b3358d9e22b7c685",
2298+
"type": "doc",
2299+
"attrs": {
2300+
"dirty": true
2301+
},
2302+
"children": [
2303+
{
2304+
"uid": "e22e5bcaa65b41beb3cc48a8d8cf175c",
2305+
"type": "img",
2306+
"attrs": {
2307+
"url": "https://images.contentstack.io/v3/assets/blta29a98d37041ffc4/blt0f2e045a5f4ae8bd/646df9c6b8153a80eb810a6e/tony-litvyak-PzZQFFeRt54-unsplash.jpg",
2308+
"width": 100,
2309+
"dirty": true,
2310+
"style": {
2311+
"text-align": "left",
2312+
"width": "100px",
2313+
"max-width": "100px",
2314+
"float": "left"
2315+
},
2316+
"redactor-attributes": {
2317+
"position": "left",
2318+
"alt": ""
2319+
},
2320+
"dir": "",
2321+
"alt": "",
2322+
"max-width": 100,
2323+
"height": 150
2324+
},
2325+
"children": [
2326+
{
2327+
"text": ""
2328+
}
2329+
]
2330+
},
2331+
{
2332+
"uid": "8e7309d3c617401898f45c1c3ae62f1e",
2333+
"type": "p",
2334+
"attrs": {
2335+
"style": {},
2336+
"redactor-attributes": {},
2337+
"dir": ""
2338+
},
2339+
"children": [
2340+
{
2341+
"text": "This is for testing purpose"
2342+
}
2343+
]
2344+
}
2345+
],
2346+
"_version": 2
22342347
}
2235-
2236-
2348+
]
22372349
}
22382350

22392351
}

test/toRedactor.test.ts

+23-4
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,29 @@ describe("Testing json to html conversion", () => {
292292
expect(html).toBe(`<img alt="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." src="https://images.contentstack.io/v3/assets/blt7359e2a55efae483/bltea2a11144a2c68b5/63c08b7f438f80612c397994/CS_Infographics_ForresterReport_Data_3_1200x628_(1).png" position="center" width="641" style="width: 641; height: auto;" />`)
293293
})
294294

295-
test(' should retain empty string value for alt attribute', () => {
296-
const json = expectedValue['RT-268'].json;
297-
const html = toRedactor(json);
298-
expect(html).toBe(expectedValue['RT-268'].html);
295+
describe("RT-268", ()=>{
296+
it(' should retain empty string value for alt attribute for img type', () => {
297+
const json = expectedValue['RT-268'].json[0];
298+
const html = toRedactor(json);
299+
expect(html).toBe(expectedValue['RT-268'].html[0]);
300+
})
301+
it(' should retain empty string value for alt attribute for asset reference', () => {
302+
const json = expectedValue['RT-268'].json[1];
303+
const html = toRedactor(json);
304+
expect(html).toBe(expectedValue['RT-268'].html[1]);
305+
})
306+
it(' should retain empty string value for attributes passed through "allowedEmptyAttributes" prop', () => {
307+
const json = expectedValue['RT-268'].json[2];
308+
const html = toRedactor(json, {allowedEmptyAttributes: { p: ["dir"]} });
309+
expect(html).toBe(expectedValue['RT-268'].html[2]);
310+
})
311+
it(' should retain empty string value for attributes passed through "allowedEmptyAttributes" prop, where alt is empty too (default empty)', () => {
312+
const json = expectedValue['RT-268'].json[3];
313+
const html = toRedactor(json, {allowedEmptyAttributes: { "img": ['dir'],"p": ["dir"]} });
314+
expect(html).toBe(expectedValue['RT-268'].html[3]);
299315
})
316+
317+
})
318+
300319
})
301320

0 commit comments

Comments
 (0)