From 1c18b5e17f0936bf452efd150f214f12da091fba Mon Sep 17 00:00:00 2001 From: Jayesh Deorukhkar Date: Tue, 26 Sep 2023 19:27:21 +0530 Subject: [PATCH 1/8] feat: support for nested attrs --- src/fromRedactor.tsx | 24 ++++++++++++++++++------ src/toRedactor.tsx | 9 ++++++++- test/expectedJson.ts | 12 +++++++++++- test/fromRedactor.test.ts | 12 ++++++++++++ test/toRedactor.test.ts | 7 +++++++ 5 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/fromRedactor.tsx b/src/fromRedactor.tsx index 09c44fe..a7e8f32 100644 --- a/src/fromRedactor.tsx +++ b/src/fromRedactor.tsx @@ -232,13 +232,14 @@ export const fromRedactor = (el: any, options?:IHtmlToJsonOptions) : IAnyObject return jsx('element', { type: "doc", uid: generateId(), attrs: {} }, children) } if (options?.allowNonStandardTags && !Object.keys(ELEMENT_TAGS).includes(nodeName) && !Object.keys(TEXT_TAGS).includes(nodeName)) { - const attributes = el.attributes - const attribute = {} - Array.from(attributes).forEach((child: any) => { - attribute[child.nodeName] = child.nodeValue + const attributes = (el as HTMLElement).attributes + const attributeMap = {} + Array.from(attributes).forEach((attribute) => { + let { nodeName, value } = attribute + attributeMap[nodeName] = getNestedValueIfAvailable(value) }) console.warn(`${nodeName} is not a standard tag of JSON RTE.`) - return jsx('element', { type: nodeName.toLowerCase(), attrs: { ...attribute } }, children) + return jsx('element', { type: nodeName.toLowerCase(), attrs: { ...attributeMap } }, children) } const isEmbedEntry = el.attributes['data-sys-entry-uid']?.value const type = el.attributes['type']?.value @@ -791,4 +792,15 @@ const getFinalImageAttributes = ({elementAttrs, newChildren, extraAttrs, sizeAtt const imageAttrs = getImageAttributes(elementAttrs, childAttrs, extraAttrs); return imageAttrs -} \ No newline at end of file +} + +const getNestedValueIfAvailable = (value: string) => { + try { + if (typeof value === "string" && value.match(/^{|\[/i)) { + return JSON.parse(value); + } + return value + } catch { + return value; + } +}; diff --git a/src/toRedactor.tsx b/src/toRedactor.tsx index d937b42..b8791d6 100644 --- a/src/toRedactor.tsx +++ b/src/toRedactor.tsx @@ -2,6 +2,7 @@ import kebbab from 'lodash/kebabCase' import isEmpty from 'lodash/isEmpty' import {IJsonToHtmlElementTags, IJsonToHtmlOptions, IJsonToHtmlTextTags} from './types' +import { isPlainObject } from 'lodash' const ELEMENT_TYPES: IJsonToHtmlElementTags = { 'blockquote': (attrs: string, child: string) => { @@ -223,7 +224,13 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string if (options?.allowNonStandardTypes && !Object.keys(ELEMENT_TYPES).includes(jsonValue['type']) && jsonValue['type'] !== 'doc') { let attrs = '' Object.entries(jsonValue?.attrs|| {}).forEach(([key, val]) => { - attrs += val ? ` ${key}="${val}"` : ` ${key}`; + if(isPlainObject(val)){ + val = JSON.stringify(val) + attrs += ` ${key}='${val}'` + } + else{ + attrs += val ? ` ${key}="${val}"` : ` ${key}`; + } }) attrs = (attrs.trim() ? ' ' : '') + attrs.trim() console.warn(`${jsonValue['type']} is not a valid element type.`) diff --git a/test/expectedJson.ts b/test/expectedJson.ts index 659cf62..d2e0986 100644 --- a/test/expectedJson.ts +++ b/test/expectedJson.ts @@ -1745,5 +1745,15 @@ export default { ] } ] - } + }, + "nested-attrs": [ + { + json: {"type":"doc","uid":"uid","attrs":{},"children":[{"type":"hangout-module","attrs":{},"children":[{"type":"hangout-chat","attrs":{"from":"Paul, Addy","nested":{"to":"JD"}},"children":[{"type":"hangout-discussion","attrs":{},"children":[{"type":"hangout-message","attrs":{"from":"Paul","profile":"profile.png","datetime":"2013-07-17T12:02"},"children":[{"type":"p","uid":"uid","attrs":{},"children":[{"text":"Feelin' this Web Components thing."}]},{"type":"p","uid":"uid","attrs":{},"children":[{"text":"Heard of it?"}]}]}]}]},{"type":"hangout-chat","attrs":{},"children":[{"text":"Hi There!"}]}]}]}, + html : `

Feelin' this Web Components thing.

Heard of it?

Hi There!
` + }, + { + json: {"type":"doc","uid":"uid","attrs":{},"children":[{"type":"hangout-module","attrs":{},"children":[{"type":"hangout-chat","attrs":{"from":"Paul, Addy","nested-json":{"to":"Hello World","more-nesting":{"from":"Beautiful World"}}},"children":[{"type":"hangout-discussion","attrs":{},"children":[{"type":"hangout-message","attrs":{"from":"Paul","profile":"profile.png","datetime":"2013-07-17T12:02"},"children":[{"type":"p","uid":"uid","attrs":{},"children":[{"text":"Feelin' this Web Components thing."}]},{"type":"p","uid":"uid","attrs":{},"children":[{"text":"Heard of it?"}]}]}]}]},{"type":"hangout-chat","attrs":{},"children":[{"text":"Hi There!"}]}]}]}, + html : `

Feelin' this Web Components thing.

Heard of it?

Hi There!
` + }, + ] } \ No newline at end of file diff --git a/test/fromRedactor.test.ts b/test/fromRedactor.test.ts index 44918a3..ab286f1 100644 --- a/test/fromRedactor.test.ts +++ b/test/fromRedactor.test.ts @@ -15,6 +15,8 @@ const docWrapper = (children: any) => { const compareValue = (json1,json2) => { return isEqual(JSON.stringify(omitdeep(json1, "uid")), JSON.stringify(omitdeep(docWrapper(json2), "uid"))) } + +jest.mock('uuid', () => ({ v4: () => 'uid' })); describe("Testing html to json conversion", () => { it("paragraph conversion", () => { let html = "

This is test

" @@ -224,4 +226,14 @@ describe("Testing html to json conversion", () => { ], "type": "p" }])) expect(testResult).toBe(true) }) + + test("should convert stringified attrs to proper nested JSON attrs", () => { + for (const testCase of expectedValue["nested-attrs"]) { + const { json, html } = testCase; + const dom = new JSDOM(html); + let htmlDoc = dom.window.document.querySelector("body"); + const jsonValue = fromRedactor(htmlDoc, { allowNonStandardTags: true }); + expect(jsonValue).toStrictEqual(json); + } + }); }) \ No newline at end of file diff --git a/test/toRedactor.test.ts b/test/toRedactor.test.ts index f7564a5..aa55d0b 100644 --- a/test/toRedactor.test.ts +++ b/test/toRedactor.test.ts @@ -122,4 +122,11 @@ describe("Testing json to html conversion", () => { let testResult = isEqual(htmlValue, expectedValue["inline-classname-and-id"].html) expect(testResult).toBe(true) }) + test("should have stringified attrs for nested json", () => { + for (const testCase of expectedValue["nested-attrs"]) { + const { json, html } = testCase; + const htmlValue = toRedactor(json, { allowNonStandardTypes: true }); + expect(htmlValue).toBe(html); + } + }); }) \ No newline at end of file From 3cf16bfb3c93746333b4222bf5d651391abf1149 Mon Sep 17 00:00:00 2001 From: Jayesh Deorukhkar Date: Tue, 26 Sep 2023 19:30:05 +0530 Subject: [PATCH 2/8] feat: aaded optional chaining for image text-align style --- src/fromRedactor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fromRedactor.tsx b/src/fromRedactor.tsx index a7e8f32..7baa2ab 100644 --- a/src/fromRedactor.tsx +++ b/src/fromRedactor.tsx @@ -782,7 +782,7 @@ const getFinalImageAttributes = ({elementAttrs, newChildren, extraAttrs, sizeAtt sizeAttrs.width = newChildren[0].attrs.width.toString(); } - const childAttrs = { ...newChildren[0].attrs, ...sizeAttrs, style: { 'text-align': style['text-align'] }, caption: extraAttrs['caption'] } + const childAttrs = { ...newChildren[0].attrs, ...sizeAttrs, style: { 'text-align': style?.['text-align'] }, caption: extraAttrs['caption'] } extraAttrs = { ...extraAttrs, ...sizeAttrs } if (!childAttrs.caption) { From ba511e41eedbb41b619897d77916b41cf752af14 Mon Sep 17 00:00:00 2001 From: Jayesh Deorukhkar Date: Wed, 27 Sep 2023 13:53:40 +0530 Subject: [PATCH 3/8] feat: added proper type check for nodeValue --- src/fromRedactor.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/fromRedactor.tsx b/src/fromRedactor.tsx index 7baa2ab..a634916 100644 --- a/src/fromRedactor.tsx +++ b/src/fromRedactor.tsx @@ -235,9 +235,12 @@ export const fromRedactor = (el: any, options?:IHtmlToJsonOptions) : IAnyObject const attributes = (el as HTMLElement).attributes const attributeMap = {} Array.from(attributes).forEach((attribute) => { - let { nodeName, value } = attribute - attributeMap[nodeName] = getNestedValueIfAvailable(value) - }) + let { nodeName, nodeValue } = attribute; + if (typeof nodeValue === "string") { + nodeValue = getNestedValueIfAvailable(nodeValue); + } + attributeMap[nodeName] = nodeValue; + }); console.warn(`${nodeName} is not a standard tag of JSON RTE.`) return jsx('element', { type: nodeName.toLowerCase(), attrs: { ...attributeMap } }, children) } From 05cdda2d3e835e1fb3e12788f535fe432c7d679e Mon Sep 17 00:00:00 2001 From: Jayesh Deorukhkar Date: Wed, 27 Sep 2023 16:09:45 +0530 Subject: [PATCH 4/8] feat: added trim while checking for json string --- src/fromRedactor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fromRedactor.tsx b/src/fromRedactor.tsx index a634916..e3d15e7 100644 --- a/src/fromRedactor.tsx +++ b/src/fromRedactor.tsx @@ -799,7 +799,7 @@ const getFinalImageAttributes = ({elementAttrs, newChildren, extraAttrs, sizeAtt const getNestedValueIfAvailable = (value: string) => { try { - if (typeof value === "string" && value.match(/^{|\[/i)) { + if (typeof value === "string" && value.trim().match(/^{|\[/i)) { return JSON.parse(value); } return value From 40ccc1c50f992c359a298d7bb28d100ccc4a5e9f Mon Sep 17 00:00:00 2001 From: Jayesh Deorukhkar Date: Mon, 9 Oct 2023 12:11:53 +0530 Subject: [PATCH 5/8] test: added additional test cases for nested attrs --- src/fromRedactor.tsx | 2 +- test/fromRedactor.test.ts | 37 +++++++++++++++++++++++++++++++++++-- test/toRedactor.test.ts | 24 ++++++++++++++++++------ 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/fromRedactor.tsx b/src/fromRedactor.tsx index e3d15e7..ecf7779 100644 --- a/src/fromRedactor.tsx +++ b/src/fromRedactor.tsx @@ -797,7 +797,7 @@ const getFinalImageAttributes = ({elementAttrs, newChildren, extraAttrs, sizeAtt return imageAttrs } -const getNestedValueIfAvailable = (value: string) => { +export const getNestedValueIfAvailable = (value: string) => { try { if (typeof value === "string" && value.trim().match(/^{|\[/i)) { return JSON.parse(value); diff --git a/test/fromRedactor.test.ts b/test/fromRedactor.test.ts index ab286f1..1c5c3fb 100644 --- a/test/fromRedactor.test.ts +++ b/test/fromRedactor.test.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { fromRedactor } from "../src/fromRedactor" +import { fromRedactor, getNestedValueIfAvailable } from "../src/fromRedactor" import { JSDOM } from "jsdom" import { isEqual } from "lodash" import omitdeep from "omit-deep-lodash" @@ -227,6 +227,8 @@ describe("Testing html to json conversion", () => { expect(testResult).toBe(true) }) + describe("Nested attrs", () =>{ + test("should convert stringified attrs to proper nested JSON attrs", () => { for (const testCase of expectedValue["nested-attrs"]) { const { json, html } = testCase; @@ -236,4 +238,35 @@ describe("Testing html to json conversion", () => { expect(jsonValue).toStrictEqual(json); } }); -}) \ No newline at end of file + + test("should not convert stringify attrs when `allowNonStandardTags` is not true", () => { + const html = `

Hi There!

`; + const json = {"attrs": {}, "children": [{"attrs": {}, "children": [{"attrs": {"redactor-attributes": {"from": "Paul, Addy", "to": "[object Object]"}, "style": {}}, "children": [{"attrs": {"style": {}}, "text": "Hi There!"}], "type": "span", "uid": "uid"}], "type": "p", "uid": "uid"}], "type": "doc", "uid": "uid"}; + + const dom = new JSDOM(html); + let htmlDoc = dom.window.document.querySelector("body"); + const jsonValue = fromRedactor(htmlDoc); + expect(jsonValue).toStrictEqual(json); + }); + }) + +}) + + +describe('getNestedValueIfAvailable', () => { + + it('should return the input value when it\'s not a string containing JSON', () => { + expect(getNestedValueIfAvailable(10)).toBe(10); + expect(getNestedValueIfAvailable(null)).toBeNull(); + expect(getNestedValueIfAvailable('{ "name": "John", "age": }')).toBe('{ "name": "John", "age": }'); + expect(getNestedValueIfAvailable({ "name": "John", "age": 30})).toStrictEqual({ "name": "John", "age": 30}); + expect(getNestedValueIfAvailable('[Object Object]')).toBe('[Object Object]'); + }); + + it('should return the parsed JSON when the input value is a string containing JSON', () => { + const value = '{ "name": "John", "age": 30 }'; + const result = getNestedValueIfAvailable(value); + expect(result).toEqual({ name: "John", age: 30 }); + }); + +}); diff --git a/test/toRedactor.test.ts b/test/toRedactor.test.ts index aa55d0b..198b9c7 100644 --- a/test/toRedactor.test.ts +++ b/test/toRedactor.test.ts @@ -122,11 +122,23 @@ describe("Testing json to html conversion", () => { let testResult = isEqual(htmlValue, expectedValue["inline-classname-and-id"].html) expect(testResult).toBe(true) }) - test("should have stringified attrs for nested json", () => { - for (const testCase of expectedValue["nested-attrs"]) { - const { json, html } = testCase; - const htmlValue = toRedactor(json, { allowNonStandardTypes: true }); + + describe("Nested attrs", () => { + + test("should have stringified attrs for nested json", () => { + for (const testCase of expectedValue["nested-attrs"]) { + const { json, html } = testCase; + const htmlValue = toRedactor(json, { allowNonStandardTypes: true }); + expect(htmlValue).toBe(html); + } + }); + + test("should not convert to stringify attrs when `allowNonStandardTypes` is not true", () => { + const html = `This is HTML-formatted content.` + const json = {"type":"doc","attrs":{}, "children":[{"type":"aprimo","attrs":{ nestedAttrs: { "k1" : "v1"} },"children":[{"text":"This is HTML-formatted content."}]}]}; + + const htmlValue = toRedactor(json); expect(htmlValue).toBe(html); - } - }); + }); + }) }) \ No newline at end of file From 3900318c96dc87f9ab981c6b751e22459222e422 Mon Sep 17 00:00:00 2001 From: Jayesh Deorukhkar Date: Mon, 9 Oct 2023 19:53:16 +0530 Subject: [PATCH 6/8] chore: removed checks as per sre --- .github/workflows/sast-scan.yml | 11 ----------- .github/workflows/secrets-scan.yml | 11 ----------- 2 files changed, 22 deletions(-) delete mode 100644 .github/workflows/sast-scan.yml delete mode 100644 .github/workflows/secrets-scan.yml diff --git a/.github/workflows/sast-scan.yml b/.github/workflows/sast-scan.yml deleted file mode 100644 index f931630..0000000 --- a/.github/workflows/sast-scan.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: SAST Scan -on: - pull_request: - types: [opened, synchronize, reopened] -jobs: - security: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Horusec Scan - run: docker run -v /var/run/docker.sock:/var/run/docker.sock -v $(pwd):/src horuszup/horusec-cli:latest horusec start -p /src -P $(pwd) \ No newline at end of file diff --git a/.github/workflows/secrets-scan.yml b/.github/workflows/secrets-scan.yml deleted file mode 100644 index 1e8f176..0000000 --- a/.github/workflows/secrets-scan.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: Secrets Scan -on: - pull_request: - types: [opened, synchronize, reopened] -jobs: - security: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Gittyleaks - uses: gupy-io/gittyleaks-action@v0.1 \ No newline at end of file From f13dd7debede57e1e2b8a0acb839245083546b31 Mon Sep 17 00:00:00 2001 From: Jayesh Deorukhkar Date: Wed, 11 Oct 2023 12:22:08 +0530 Subject: [PATCH 7/8] chore: updated license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 1f50113..7b44fb1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2022 Contentstack +Copyright (c) 2022-2023 Contentstack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From fa960c26a85eba2308e427d16c5440dc8e00ca83 Mon Sep 17 00:00:00 2001 From: Jayesh Deorukhkar Date: Wed, 11 Oct 2023 15:01:38 +0530 Subject: [PATCH 8/8] chore: update package version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e14290..ffd20c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/json-rte-serializer", - "version": "2.0.1", + "version": "2.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@contentstack/json-rte-serializer", - "version": "2.0.1", + "version": "2.0.3", "license": "MIT", "dependencies": { "array-flat-polyfill": "^1.0.1", diff --git a/package.json b/package.json index 216768a..ca8e35e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/json-rte-serializer", - "version": "2.0.2", + "version": "2.0.3", "description": "This Package converts Html Document to Json and vice-versa.", "main": "lib/index.js", "types": "lib/index.d.ts",