Skip to content
Merged
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<img>` 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
Expand Down Expand Up @@ -216,6 +222,10 @@ const htmlValue = jsonToHtml(
return `<color data-color="${value}">${child}</color>`;
},
},
allowedEmptyAttributes : {
"p": ["dir"],
"img" : ["width"]
}
}
);

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/json-rte-serializer",
"version": "2.1.0",
"version": "3.0.0",
"description": "This Package converts Html Document to Json and vice-versa.",
"main": "lib/index.js",
"module": "lib/index.mjs",
Expand Down
30 changes: 25 additions & 5 deletions src/toRedactor.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -105,7 +105,7 @@ const ELEMENT_TYPES: IJsonToHtmlElementTags = {
return `<div${attrs}>${child}</div>`
},
hr: (attrs: any, child: any) => {
return `<div data-type='hr' style='border-top: 3px solid #bbb'></div>`
return `<hr>`
},
span: (attrs: any, child: any) => {
return `<span${attrs}>${child}</span>`
Expand Down Expand Up @@ -213,11 +213,24 @@ const TEXT_WRAPPERS: IJsonToHtmlTextTags = {
return `<span data-type='inlineCode'>${child}</span>`
},
}
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 => {
ALLOWED_EMPTY_ATTRIBUTES[key] = [
...(ALLOWED_EMPTY_ATTRIBUTES[key] ?? []),
...(options.allowedEmptyAttributes?.[key] || [])
];
});
}
if (jsonValue.hasOwnProperty('text')) {
let text = jsonValue['text'].replace(/</g, '&lt;').replace(/>/g, '&gt;')
if (jsonValue['break']) {
Expand Down Expand Up @@ -506,12 +519,19 @@ 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 (ALLOWED_EMPTY_ATTRIBUTES.hasOwnProperty(jsonValue['type']) && ALLOWED_EMPTY_ATTRIBUTES[jsonValue['type']].includes(item[0])) {
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') {
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
145 changes: 144 additions & 1 deletion test/expectedJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ export default {
"htmlUpdated": "<p></p><img asset_uid=\"blt5523ee02703e39f5\" src=\"https://images.com/captain_pardip.jpg\" width=\"24.193548387096776\" height=\"auto\" style=\"width: 24.193548387096776; height: auto;height: auto;\" type=\"asset\" sys-style-type=\"download\"/><p></p><iframe src=\"https://www.***REMOVED***.com/embed/CSvFpBOe8eY\"></iframe><img asset_uid=\"blta2aad0332073026c\" src=\"https://images.com/logo_1.jpg\" height=\"auto\" type=\"asset\" sys-style-type=\"download\"/>"
},
"7": {
"html": "<p>this is <a href=\"link.com\" target=\"_self\">link</a></p><p></p><div data-type='hr' style='border-top: 3px solid #bbb'></div><p></p><p class=\"className\">paragraph with class</p><p id=\"id\">paragraph with id</p>",
"html": "<p>this is <a href=\"link.com\" target=\"_self\">link</a></p><p></p><hr><p></p><p class=\"className\">paragraph with class</p><p id=\"id\">paragraph with id</p>",
"json": [
{
"type": "p",
Expand Down Expand Up @@ -660,6 +660,7 @@ export default {
},
{
"type": "hr",
"attrs": {},
"uid": "699946dbb6b84ef583914eb92dcac44b",
"children": [
{
Expand Down Expand Up @@ -2204,6 +2205,148 @@ export default {

]

},
"RT-268":{
"html": [
`<img alt="" src="image_url.jpeg" width="100" style="width: 100; height: auto;" />`,
`<figure style="margin: 0"><img src="https://***REMOVED***.***REMOVED***.com/v3/assets/***REMOVED***1/***REMOVED***/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://***REMOVED***.***REMOVED***.com/v3/assets/***REMOVED***1/***REMOVED***/6572c368e7a0d4196d105010/compass-logo-v2-final.png" data-sys-asset-uid="***REMOVED***" 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>`,
`<p dir="">This is for testing purpose</p>`,
`<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>`
],
"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": ""
}
]
}
]
},
{
"uid": "a59f9108e99747d4b3358d9e22b7c685",
"type": "doc",
"attrs": {
"dirty": true
},
"children": [
{
"uid": "a41aede53efe46018e00de52b6d0970e",
"type": "reference",
"attrs": {
"display-type": "display",
"asset-uid": "***REMOVED***",
"content-type-uid": "sys_assets",
"asset-link": "https://***REMOVED***.***REMOVED***.com/v3/assets/***REMOVED***1/***REMOVED***/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
}
]
}

}
2 changes: 1 addition & 1 deletion test/fromRedactor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe("Testing html to json conversion", () => {
let htmlDoc = dom.window.document.querySelector('body')
let jsonValue = fromRedactor(htmlDoc)
let testResult = isEqual(omitdeep(jsonValue, "uid"), omitdeep(docWrapper(expectedValue[7].json), "uid"))
expect(testResult).toBe(true)
expect(omitdeep(jsonValue, "uid")).toStrictEqual(omitdeep(docWrapper(expectedValue[7].json), "uid"))
})

it("Embedded entry as link", () => {
Expand Down
28 changes: 26 additions & 2 deletions test/toRedactor.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { toRedactor } from "../src/toRedactor"
import isEqual from "lodash.isequal"

import expectedValue from "./expectedJson"
import { imageAssetData } from "./testingData"


describe("Testing json to html conversion", () => {
it("heading conversion", () => {
let jsonValue = expectedValue["2"].json
Expand Down Expand Up @@ -292,5 +291,30 @@ describe("Testing json to html conversion", () => {
const html = toRedactor(json);
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;" />`)
})

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]);
})

})

})

Loading