From 04660dce03ed0b38b591c1e2d51afac6ca2eaa0b Mon Sep 17 00:00:00 2001 From: Ashwini-Rathod Date: Tue, 4 Jun 2024 13:11:09 +0530 Subject: [PATCH 1/5] feat: added json to markdown serializer --- package-lock.json | 58 ++++++ package.json | 1 + src/index.tsx | 3 +- src/jsonToMarkdown.tsx | 445 +++++++++++++++++++++++++++++++++++++++++ src/types.ts | 2 + 5 files changed, 508 insertions(+), 1 deletion(-) create mode 100644 src/jsonToMarkdown.tsx diff --git a/package-lock.json b/package-lock.json index 2e2cfc8..4a0e36a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "lodash.isplainobject": "^4.0.6", "lodash.isundefined": "^3.0.1", "lodash.kebabcase": "^4.1.1", + "slate": "^0.103.0", "uuid": "^8.3.2" }, "devDependencies": { @@ -2673,6 +2674,15 @@ "node": ">=0.10.0" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -2762,6 +2772,14 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -4444,6 +4462,16 @@ "node": ">=8" } }, + "node_modules/slate": { + "version": "0.103.0", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.103.0.tgz", + "integrity": "sha512-eCUOVqUpADYMZ59O37QQvUdnFG+8rin0OGQAXNHvHbQeVJ67Bu0spQbcy621vtf8GQUXTEQBlk6OP9atwwob4w==", + "dependencies": { + "immer": "^10.0.3", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4629,6 +4657,11 @@ "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", "dev": true }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7017,6 +7050,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==" + }, "import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -7082,6 +7120,11 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, "is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -8391,6 +8434,16 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slate": { + "version": "0.103.0", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.103.0.tgz", + "integrity": "sha512-eCUOVqUpADYMZ59O37QQvUdnFG+8rin0OGQAXNHvHbQeVJ67Bu0spQbcy621vtf8GQUXTEQBlk6OP9atwwob4w==", + "requires": { + "immer": "^10.0.3", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -8528,6 +8581,11 @@ "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", "dev": true }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index a71ffaa..76c753f 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "lodash.isplainobject": "^4.0.6", "lodash.isundefined": "^3.0.1", "lodash.kebabcase": "^4.1.1", + "slate": "^0.103.0", "uuid": "^8.3.2" }, "files": [ diff --git a/src/index.tsx b/src/index.tsx index 4a1da63..d1e48ff 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,6 @@ import "array-flat-polyfill" import { fromRedactor } from "./fromRedactor" import { toRedactor } from "./toRedactor" +import {jsonToMarkdownSerializer} from './jsonToMarkdown' export * from "./types" -export { fromRedactor as htmlToJson, toRedactor as jsonToHtml } \ No newline at end of file +export { fromRedactor as htmlToJson, toRedactor as jsonToHtml, jsonToMarkdownSerializer as jsonToMarkdown } \ No newline at end of file diff --git a/src/jsonToMarkdown.tsx b/src/jsonToMarkdown.tsx new file mode 100644 index 0000000..1e0a0ba --- /dev/null +++ b/src/jsonToMarkdown.tsx @@ -0,0 +1,445 @@ +import {IJsonToMarkdownElementTags, IJsonToMarkdownTextTags} from './types' +import kebbab from 'lodash.kebabcase' +import {Node} from 'slate' + +const ELEMENT_TYPES: IJsonToMarkdownElementTags = { + 'blockquote': (attrs: string, child: string) => { + return ` + +> ${child}${attrs}` + }, + 'h1': (attrs: any, child: string) => { + return ` + +#${child}#` + }, + 'h2': (attrs: any, child: any) => { + return ` + +##${child}##` + }, + 'h3': (attrs: any, child: any) => { + return ` + +###${child}###` + }, + 'h4': (attrs: any, child: any) => { + return ` + +####${child}####` + }, + 'h5': (attrs: any, child: any) => { + return ` + +#####${child}#####` + }, + 'h6': (attrs: any, child: any) => { + return ` + +######${child}######` + }, + img: (attrs: any, child: any, attrsJson: any, figureStyles: any) => { + if (figureStyles.fieldsEdited.length === 0) { + return `` + } + let img = figureStyles.anchorLink ? `` : `` + let caption = figureStyles.caption + ? figureStyles.alignment === 'center' + ? `
${figureStyles.caption}
` + : `
${figureStyles.caption}
` + : '' + let align = figureStyles.position + ? `
${img}${caption}
` + : figureStyles.caption + ? `
${img}${caption}
` + : `${img}` + + return `${align}` + }, + p: (attrs: any, child: any) => { + return ` + +${child}` + }, + code: (attrs: any, child: any) => { + return ` + + ${child} ` + }, + ol: (attrs: any, child: any) => { + return `${child}` + }, + ul: (attrs: any, child: any) => { + return `${child}` + }, + li: (attrs: any, child: any) => { + return `${child}` + }, + a: (attrs: any, child: any, attrsJson: any) => { + return `[${child}](${attrsJson.href})` + }, + hr: (attrs: any, child: any) => { + return ` + +----------` + }, + span: (attrs: any, child: any) => { + return `${child}` + }, + div: (attrs: any, child: any) => { + return `${child}` + }, + reference: (attrs: any, child: any, attrsJson: any, extraAttrs: any): any => { + if(extraAttrs?.displayType === 'display') { + if(attrsJson) { + let assetAlt = attrsJson?.alt ? attrsJson.alt : 'enter image description here' + let assetURL = attrsJson?.['data-sys-asset-filelink'] ? attrsJson['data-sys-asset-filelink'] : '' + return ` + +![${assetAlt}] +(${assetURL})` + } + } + else if(extraAttrs?.displayType === 'link') { + if(attrsJson) { + return `[${child}](${attrsJson?.['href'] ? attrsJson['href'] : "#"})` + } + } + }, + fragment: (attrs: any, child: any) => { + return child + }, +} +const TEXT_WRAPPERS: IJsonToMarkdownTextTags = { + 'bold': (child: any, value: any) => { + return `**${child}**`; + }, + 'italic': (child: any, value: any) => { + return `*${child}*`; + }, + // 'underline': (child: any, value: any) => { + // return `${child}`; + // }, underline is not supported in markdown + 'strikethrough': (child: any, value: any) => { + return `~~${child}~~`; + }, + 'superscript': (child: any, value: any) => { + return `^${child}^`; + }, + 'subscript': (child: any, value: any) => { + return `~${child}~`; + }, + 'inlineCode': (child: any, value: any) => { + return `\`${child}\`` + }, +} + +const getOLOrULStringFromJson = (value: any) => { + if(value.type === 'ol'){ + let child = '' + let start = parseInt(value?.attrs?.start || 1) + Array.from(value.children).forEach((val: any, index) => { + child += `${index + start}. ${Node.string(val)}\n` + }) + return ` + +${child}` + } + if(value.type === 'ul') { + let child = '' + let symbol = value?.attrs?.listStyleType || '- ' + Array.from(value.children).forEach((val: any, index) => { + child += `${symbol}${Node.string(val)}\n` + }) + return ` + +${child}` + } +} + +export const jsonToMarkdownSerializer = (jsonValue: any): string => { + if (jsonValue.hasOwnProperty('text')) { + let text = jsonValue['text'].replace(//g, '>') + if (jsonValue['break']) { + text += `
` + } + if (jsonValue['classname'] || jsonValue['id']) { + if (jsonValue['classname'] && jsonValue['id']) { + text = `${text}` + } + else if (jsonValue['classname'] && !jsonValue['id']) { + text = `${text}` + } + else if (jsonValue['id'] && !jsonValue['classname']) { + text = `${text}` + } + } + if (jsonValue.text.includes('\n') && !jsonValue['break']) { + text = text.replace(/\n/g, '
') + } + Object.entries(jsonValue).forEach(([key, value]) => { + if (TEXT_WRAPPERS.hasOwnProperty(key)) { + text = TEXT_WRAPPERS[key](text, value) + } + }) + if (jsonValue['attrs']) { + const { style } = jsonValue['attrs'] + if (style) { + let attrsStyle = '' + if (style.color) { + attrsStyle = `color:${style.color};` + } + if (style["font-family"]) { + attrsStyle += `font-family:"${style.fontFamily}";` + } + if (style["font-size"]) { + attrsStyle += `font-size: ${style.fontSize};` + } + if (attrsStyle !== '') { + text = `${text}` + } + } + } + return text + } + let children: any = '' + if (jsonValue.children) { + children = Array.from(jsonValue.children).map((child) => jsonToMarkdownSerializer(child)) + if (jsonValue['type'] === 'blockquote') { + children = children.map((child: any) => { + if (child === '\n') { + return '
' + } + return child + }) + } + children = children.join('') + } + + if (ELEMENT_TYPES[jsonValue['type']]) { + let attrs = '' + let attrsJson: { [key: string]: any } = {} + let orgType + let figureStyles: any = { + fieldsEdited: [] + } + if (jsonValue.attrs) { + + let allattrs = JSON.parse(JSON.stringify(jsonValue.attrs)) + let style = '' + if (jsonValue.attrs["redactor-attributes"]) { + attrsJson = { ...allattrs["redactor-attributes"] } + } + if (jsonValue['type'] === 'reference' && jsonValue?.attrs?.default) { + orgType = "img" + let inline = '' + if (attrsJson['asset-link']) { + attrsJson['src'] = attrsJson['asset-link'] + delete attrsJson['asset-link'] + delete allattrs['asset-link'] + } + if (attrsJson['inline']) { + inline = `display: flow-root;margin:0` + delete attrsJson['width'] + delete attrsJson['style'] + } + if (attrsJson['position']) { + figureStyles.position = + attrsJson['position'] === 'center' + ? `style = "margin: auto; text-align: center;width: ${allattrs['width'] ? allattrs['width'] + '%' : 100 + '%' + };"` + : `style = "float: ${attrsJson['position']};${inline};width: ${allattrs['width'] ? allattrs['width'] + '%' : 100 + '%' + };max-width:${allattrs['max-width'] ? allattrs['max-width'] + '%' : 100 + '%'};"` + figureStyles.alignment = attrsJson['position'] + figureStyles.fieldsEdited.push(figureStyles.position) + delete attrsJson['position'] + attrsJson['width'] && delete attrsJson['width'] + attrsJson['style'] && delete attrsJson['style'] + attrsJson['height'] && delete attrsJson['height'] + attrsJson['max-width'] && delete attrsJson['max-width'] + allattrs['max-width'] && delete allattrs['max-width'] + allattrs['width'] && delete allattrs['width'] + if (allattrs["redactor-attributes"]) { + allattrs["redactor-attributes"]['width'] && delete allattrs["redactor-attributes"]['width'] + allattrs?.["redactor-attributes"]?.['style'] && delete allattrs["redactor-attributes"]['style'] + allattrs?.["redactor-attributes"]?.['max-width'] && delete allattrs["redactor-attributes"]['max-width'] + } + } + if (attrsJson['asset-caption']) { + figureStyles.caption = attrsJson['asset-caption'] + figureStyles.fieldsEdited.push(figureStyles.caption) + delete attrsJson['asset-caption'] + delete allattrs['asset-caption'] + } + if (attrsJson['link']) { + let anchor = '' + anchor = `href="${attrsJson['link']}"` + if (attrsJson['target']) { + anchor += ' target="_blank"' + } + figureStyles.anchorLink = `${anchor}` + figureStyles.fieldsEdited.push(figureStyles.anchorLink) + delete attrsJson['link'] + delete allattrs['link'] + } + delete allattrs['default'] + delete attrsJson['default'] + delete attrsJson['target'] + delete allattrs['asset-link'] + delete allattrs['asset-type'] + delete allattrs['display-type'] + + } + if (jsonValue['type'] === 'a') { + attrsJson['href'] = allattrs['url'] + } + if (allattrs['orgType']) { + orgType = allattrs['orgType'] + delete allattrs['orgType'] + } + if (allattrs['class-name']) { + attrsJson['class'] = allattrs['class-name'] + delete allattrs['class-name'] + } + if (attrsJson['width']) { + let width = attrsJson['width'] + if (width.slice(width.length - 1) === '%') { + style = `width: ${allattrs['width']}; height: ${attrsJson['height'] ? attrsJson['height'] : 'auto'};` + } else { + style = `width: ${allattrs['width'] + '%'}; height: ${attrsJson['height'] ? attrsJson['height'] : 'auto'};` + } + } else { + if (allattrs['width']) { + let width = String(allattrs['width']) + + if (width.slice(width.length - 1) === '%') { + allattrs['width'] = String(allattrs['width']) + } else { + allattrs['width'] = allattrs['width'] + '%' + } + // style = `width: ${allattrs['width']}; height: auto;` + } + } + if (allattrs['style'] && jsonValue['type'] !== 'img') { + Object.keys(allattrs['style']).forEach((key) => { + style += `${kebbab(key)}: ${allattrs.style[key]};` + }) + delete allattrs['style'] + } + if (allattrs['rows'] && allattrs['cols'] && allattrs['colWidths']) { + delete allattrs['rows'] + delete allattrs['cols'] + delete allattrs['colWidths'] + } + if (allattrs['disabledCols']) { + delete allattrs['disabledCols'] + } + if (allattrs['colSpan']) { + delete allattrs['colSpan'] + } + if (allattrs['rowSpan']) { + delete allattrs['rowSpan'] + } + + attrsJson = { ...attrsJson, ...allattrs, style: style } + if (jsonValue['type'] === 'reference') { + if (attrsJson['type'] === "entry") { + attrsJson['data-sys-entry-uid'] = allattrs['entry-uid'] + delete attrsJson['entry-uid'] + attrsJson['data-sys-entry-locale'] = allattrs['locale'] + delete attrsJson['locale'] + attrsJson['data-sys-content-type-uid'] = allattrs['content-type-uid'] + delete attrsJson['content-type-uid'] + attrsJson['sys-style-type'] = allattrs['display-type'] + delete attrsJson['display-type'] + } + + else if (attrsJson['type'] === "asset") { + attrsJson['data-sys-asset-filelink'] = allattrs['asset-link'] + delete attrsJson['asset-link'] + attrsJson['data-sys-asset-uid'] = allattrs['asset-uid'] + delete attrsJson['asset-uid'] + attrsJson['data-sys-asset-filename'] = allattrs['asset-name'] + delete attrsJson['asset-name'] + attrsJson['data-sys-asset-contenttype'] = allattrs['asset-type'] + delete attrsJson['asset-type'] + // + if (allattrs['asset-caption']) { + attrsJson['data-sys-asset-caption'] = allattrs['asset-caption'] + delete attrsJson['asset-caption'] + } + + if (allattrs['asset-alt']) { + attrsJson['data-sys-asset-alt'] = allattrs['asset-alt'] + delete attrsJson['aasset-alt'] + } + + if (allattrs['link']) { + attrsJson['data-sys-asset-link'] = allattrs['link'] + delete attrsJson['link'] + } + + if (allattrs['position']) { + attrsJson['data-sys-asset-position'] = allattrs['position'] + delete attrsJson['position'] + } + + if (allattrs['target']) { + attrsJson['data-sys-asset-isnewtab'] = allattrs['target'] === "_blank" + delete attrsJson['target'] + } + if (!attrsJson['sys-style-type']) { + attrsJson['sys-style-type'] = String(allattrs['asset-type']).indexOf('image') > -1 ? 'display' : 'download' + } + if (attrsJson?.["display-type"] === "display") { + const styleObj = jsonValue?.["attrs"]?.["style"] ?? {}; + if (!styleObj["width"]) { + styleObj["width"] = "auto"; + } + delete styleObj["float"]; + (attrsJson["style"] && typeof attrsJson["style"] === 'string') + ? (attrsJson["style"] += getStyleStringFromObject(styleObj)) : + (attrsJson["style"] = getStyleStringFromObject(styleObj)); + } + delete attrsJson['display-type'] + } + } + if (jsonValue['type'] === "style") { + delete attrsJson['style-text'] + } + + delete attrsJson['redactor-attributes'] + Object.entries(attrsJson).forEach((key) => { + return key[1] ? (key[1] !== '' ? (attrs += `${key[0]}="${key[1]}" `) : '') : '' + }) + attrs = (attrs.trim() ? ' ' : '') + attrs.trim() + } + + if(jsonValue['type'] === 'ol' || jsonValue['type'] === 'ul') { + //@ts-ignore + return getOLOrULStringFromJson(jsonValue) + } + + if (jsonValue['type'] === 'reference') { + figureStyles.displayType = jsonValue?.attrs?.["display-type"] + } + + if (jsonValue['type'] === 'span' && jsonValue.children.length === 1 && jsonValue.children[0].type === 'span') { + if (Object.keys(jsonValue.attrs).length === 0) { + return children + } + } + + attrs = (attrs.trim() ? ' ' : '') + attrs.trim() + + return ELEMENT_TYPES[orgType || jsonValue['type']](attrs, children, attrsJson, figureStyles) + } + return children +} + + +function getStyleStringFromObject(styleObj: { [key: string]: string }) { + return Object.keys(styleObj) + .map((key) => `${key}: ${styleObj[key]}`) + .join("; "); +} diff --git a/src/types.ts b/src/types.ts index f5c79a3..5c086a6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,8 @@ 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 IJsonToMarkdownElementTags{[key: string]: (attrs:string,child:string,attrsJson:IAnyObject,extraProps?:object) => string} +export interface IJsonToMarkdownTextTags{ [key: string]: (child:any, value:any) => string } export interface IJsonToHtmlOptions { customElementTypes?: IJsonToHtmlElementTags, customTextWrapper?: IJsonToHtmlTextTags, From a01f6e87d8cdab43a3efef8bf63738524bcde8e6 Mon Sep 17 00:00:00 2001 From: Ashwini-Rathod Date: Wed, 5 Jun 2024 09:54:48 +0530 Subject: [PATCH 2/5] chore: refactoring changes --- src/jsonToMarkdown.tsx | 335 ++++++----------------------------------- src/types.ts | 2 +- 2 files changed, 48 insertions(+), 289 deletions(-) diff --git a/src/jsonToMarkdown.tsx b/src/jsonToMarkdown.tsx index 1e0a0ba..5b8f996 100644 --- a/src/jsonToMarkdown.tsx +++ b/src/jsonToMarkdown.tsx @@ -1,9 +1,7 @@ import {IJsonToMarkdownElementTags, IJsonToMarkdownTextTags} from './types' -import kebbab from 'lodash.kebabcase' -import {Node} from 'slate' const ELEMENT_TYPES: IJsonToMarkdownElementTags = { - 'blockquote': (attrs: string, child: string) => { + 'blockquote': (attrs: any, child: any) => { return ` > ${child}${attrs}` @@ -38,23 +36,16 @@ const ELEMENT_TYPES: IJsonToMarkdownElementTags = { ######${child}######` }, - img: (attrs: any, child: any, attrsJson: any, figureStyles: any) => { - if (figureStyles.fieldsEdited.length === 0) { - return `` + img: (attrsJson: any, child: any) => { + if(attrsJson) { + let imageAlt = attrsJson?.['alt'] ? attrsJson['alt'] : 'enter image description here' + let imageURL = attrsJson?.['url'] ? attrsJson['url'] : '' + return ` + +![${imageAlt}] +(${imageURL})` } - let img = figureStyles.anchorLink ? `` : `` - let caption = figureStyles.caption - ? figureStyles.alignment === 'center' - ? `
${figureStyles.caption}
` - : `
${figureStyles.caption}
` - : '' - let align = figureStyles.position - ? `
${img}${caption}
` - : figureStyles.caption - ? `
${img}${caption}
` - : `${img}` - - return `${align}` + return '' }, p: (attrs: any, child: any) => { return ` @@ -75,8 +66,8 @@ ${child}` li: (attrs: any, child: any) => { return `${child}` }, - a: (attrs: any, child: any, attrsJson: any) => { - return `[${child}](${attrsJson.href})` + a: (attrsJson: any, child: any) => { + return `[${child}](${attrsJson.url})` }, hr: (attrs: any, child: any) => { return ` @@ -86,21 +77,18 @@ ${child}` span: (attrs: any, child: any) => { return `${child}` }, - div: (attrs: any, child: any) => { - return `${child}` - }, - reference: (attrs: any, child: any, attrsJson: any, extraAttrs: any): any => { - if(extraAttrs?.displayType === 'display') { + reference: (attrsJson: any, child: any): any => { + if(attrsJson?.['display-type'] === 'display') { if(attrsJson) { - let assetAlt = attrsJson?.alt ? attrsJson.alt : 'enter image description here' - let assetURL = attrsJson?.['data-sys-asset-filelink'] ? attrsJson['data-sys-asset-filelink'] : '' + let assetName = attrsJson?.['asset-name'] ? attrsJson['asset-name'] : 'enter image description here' + let assetURL = attrsJson?.['asset-link'] ? attrsJson['asset-link'] : '' return ` -![${assetAlt}] +![${assetName}] (${assetURL})` } } - else if(extraAttrs?.displayType === 'link') { + else if(attrsJson?.['display-type'] === 'link') { if(attrsJson) { return `[${child}](${attrsJson?.['href'] ? attrsJson['href'] : "#"})` } @@ -135,26 +123,42 @@ const TEXT_WRAPPERS: IJsonToMarkdownTextTags = { } const getOLOrULStringFromJson = (value: any) => { + let child = '' + let nestedListFound = false if(value.type === 'ol'){ - let child = '' let start = parseInt(value?.attrs?.start || 1) Array.from(value.children).forEach((val: any, index) => { - child += `${index + start}. ${Node.string(val)}\n` + if(val.hasOwnProperty('type') && val.type === 'li' && value.children[index + 1] && value.children[index + 1]?.type === 'ol'){ + let liChildren = jsonToMarkdownSerializer(val) + let nestedListChildren = getOLOrULStringFromJson(value.children[index + 1]) + let indentedNestedListChildren = nestedListChildren.split('\n').filter((child) => child.length).map((child) => ` ${child}`).join('\n') + child += `${index + start}. ${liChildren}\n${indentedNestedListChildren}\n` + nestedListFound = true + } + else if(val.hasOwnProperty('type') && val.type !== 'ol'){ + let liChildren = jsonToMarkdownSerializer(val) + child += `${nestedListFound ? (index + start - 1): index + start}. ${liChildren}\n` + } + }) - return ` - -${child}` } if(value.type === 'ul') { - let child = '' let symbol = value?.attrs?.listStyleType || '- ' Array.from(value.children).forEach((val: any, index) => { - child += `${symbol}${Node.string(val)}\n` + if(val.hasOwnProperty('type') && val.type === 'li' && value.children[index + 1] && value.children[index + 1]?.type === 'ol'){ + let liChildren = jsonToMarkdownSerializer(val) + let nestedListChildren = getOLOrULStringFromJson(value.children[index + 1]) + let indentedNestedListChildren = nestedListChildren.split('\n').filter((child) => child.length).map((child) => ` ${child}`).join('\n') + child += `${symbol}${liChildren}\n${indentedNestedListChildren}\n` + } + else if(val.hasOwnProperty('type') && val.type !== 'ol'){ + let liChildren = jsonToMarkdownSerializer(val) + child += `${symbol}${liChildren}\n` + } }) - return ` - -${child}` } + return ` +${child}` } export const jsonToMarkdownSerializer = (jsonValue: any): string => { @@ -163,17 +167,6 @@ export const jsonToMarkdownSerializer = (jsonValue: any): string => { if (jsonValue['break']) { text += `
` } - if (jsonValue['classname'] || jsonValue['id']) { - if (jsonValue['classname'] && jsonValue['id']) { - text = `${text}` - } - else if (jsonValue['classname'] && !jsonValue['id']) { - text = `${text}` - } - else if (jsonValue['id'] && !jsonValue['classname']) { - text = `${text}` - } - } if (jsonValue.text.includes('\n') && !jsonValue['break']) { text = text.replace(/\n/g, '
') } @@ -182,24 +175,6 @@ export const jsonToMarkdownSerializer = (jsonValue: any): string => { text = TEXT_WRAPPERS[key](text, value) } }) - if (jsonValue['attrs']) { - const { style } = jsonValue['attrs'] - if (style) { - let attrsStyle = '' - if (style.color) { - attrsStyle = `color:${style.color};` - } - if (style["font-family"]) { - attrsStyle += `font-family:"${style.fontFamily}";` - } - if (style["font-size"]) { - attrsStyle += `font-size: ${style.fontSize};` - } - if (attrsStyle !== '') { - text = `${text}` - } - } - } return text } let children: any = '' @@ -217,229 +192,13 @@ export const jsonToMarkdownSerializer = (jsonValue: any): string => { } if (ELEMENT_TYPES[jsonValue['type']]) { - let attrs = '' - let attrsJson: { [key: string]: any } = {} - let orgType - let figureStyles: any = { - fieldsEdited: [] - } - if (jsonValue.attrs) { - - let allattrs = JSON.parse(JSON.stringify(jsonValue.attrs)) - let style = '' - if (jsonValue.attrs["redactor-attributes"]) { - attrsJson = { ...allattrs["redactor-attributes"] } - } - if (jsonValue['type'] === 'reference' && jsonValue?.attrs?.default) { - orgType = "img" - let inline = '' - if (attrsJson['asset-link']) { - attrsJson['src'] = attrsJson['asset-link'] - delete attrsJson['asset-link'] - delete allattrs['asset-link'] - } - if (attrsJson['inline']) { - inline = `display: flow-root;margin:0` - delete attrsJson['width'] - delete attrsJson['style'] - } - if (attrsJson['position']) { - figureStyles.position = - attrsJson['position'] === 'center' - ? `style = "margin: auto; text-align: center;width: ${allattrs['width'] ? allattrs['width'] + '%' : 100 + '%' - };"` - : `style = "float: ${attrsJson['position']};${inline};width: ${allattrs['width'] ? allattrs['width'] + '%' : 100 + '%' - };max-width:${allattrs['max-width'] ? allattrs['max-width'] + '%' : 100 + '%'};"` - figureStyles.alignment = attrsJson['position'] - figureStyles.fieldsEdited.push(figureStyles.position) - delete attrsJson['position'] - attrsJson['width'] && delete attrsJson['width'] - attrsJson['style'] && delete attrsJson['style'] - attrsJson['height'] && delete attrsJson['height'] - attrsJson['max-width'] && delete attrsJson['max-width'] - allattrs['max-width'] && delete allattrs['max-width'] - allattrs['width'] && delete allattrs['width'] - if (allattrs["redactor-attributes"]) { - allattrs["redactor-attributes"]['width'] && delete allattrs["redactor-attributes"]['width'] - allattrs?.["redactor-attributes"]?.['style'] && delete allattrs["redactor-attributes"]['style'] - allattrs?.["redactor-attributes"]?.['max-width'] && delete allattrs["redactor-attributes"]['max-width'] - } - } - if (attrsJson['asset-caption']) { - figureStyles.caption = attrsJson['asset-caption'] - figureStyles.fieldsEdited.push(figureStyles.caption) - delete attrsJson['asset-caption'] - delete allattrs['asset-caption'] - } - if (attrsJson['link']) { - let anchor = '' - anchor = `href="${attrsJson['link']}"` - if (attrsJson['target']) { - anchor += ' target="_blank"' - } - figureStyles.anchorLink = `${anchor}` - figureStyles.fieldsEdited.push(figureStyles.anchorLink) - delete attrsJson['link'] - delete allattrs['link'] - } - delete allattrs['default'] - delete attrsJson['default'] - delete attrsJson['target'] - delete allattrs['asset-link'] - delete allattrs['asset-type'] - delete allattrs['display-type'] - - } - if (jsonValue['type'] === 'a') { - attrsJson['href'] = allattrs['url'] - } - if (allattrs['orgType']) { - orgType = allattrs['orgType'] - delete allattrs['orgType'] - } - if (allattrs['class-name']) { - attrsJson['class'] = allattrs['class-name'] - delete allattrs['class-name'] - } - if (attrsJson['width']) { - let width = attrsJson['width'] - if (width.slice(width.length - 1) === '%') { - style = `width: ${allattrs['width']}; height: ${attrsJson['height'] ? attrsJson['height'] : 'auto'};` - } else { - style = `width: ${allattrs['width'] + '%'}; height: ${attrsJson['height'] ? attrsJson['height'] : 'auto'};` - } - } else { - if (allattrs['width']) { - let width = String(allattrs['width']) - - if (width.slice(width.length - 1) === '%') { - allattrs['width'] = String(allattrs['width']) - } else { - allattrs['width'] = allattrs['width'] + '%' - } - // style = `width: ${allattrs['width']}; height: auto;` - } - } - if (allattrs['style'] && jsonValue['type'] !== 'img') { - Object.keys(allattrs['style']).forEach((key) => { - style += `${kebbab(key)}: ${allattrs.style[key]};` - }) - delete allattrs['style'] - } - if (allattrs['rows'] && allattrs['cols'] && allattrs['colWidths']) { - delete allattrs['rows'] - delete allattrs['cols'] - delete allattrs['colWidths'] - } - if (allattrs['disabledCols']) { - delete allattrs['disabledCols'] - } - if (allattrs['colSpan']) { - delete allattrs['colSpan'] - } - if (allattrs['rowSpan']) { - delete allattrs['rowSpan'] - } - - attrsJson = { ...attrsJson, ...allattrs, style: style } - if (jsonValue['type'] === 'reference') { - if (attrsJson['type'] === "entry") { - attrsJson['data-sys-entry-uid'] = allattrs['entry-uid'] - delete attrsJson['entry-uid'] - attrsJson['data-sys-entry-locale'] = allattrs['locale'] - delete attrsJson['locale'] - attrsJson['data-sys-content-type-uid'] = allattrs['content-type-uid'] - delete attrsJson['content-type-uid'] - attrsJson['sys-style-type'] = allattrs['display-type'] - delete attrsJson['display-type'] - } - - else if (attrsJson['type'] === "asset") { - attrsJson['data-sys-asset-filelink'] = allattrs['asset-link'] - delete attrsJson['asset-link'] - attrsJson['data-sys-asset-uid'] = allattrs['asset-uid'] - delete attrsJson['asset-uid'] - attrsJson['data-sys-asset-filename'] = allattrs['asset-name'] - delete attrsJson['asset-name'] - attrsJson['data-sys-asset-contenttype'] = allattrs['asset-type'] - delete attrsJson['asset-type'] - // - if (allattrs['asset-caption']) { - attrsJson['data-sys-asset-caption'] = allattrs['asset-caption'] - delete attrsJson['asset-caption'] - } - - if (allattrs['asset-alt']) { - attrsJson['data-sys-asset-alt'] = allattrs['asset-alt'] - delete attrsJson['aasset-alt'] - } - - if (allattrs['link']) { - attrsJson['data-sys-asset-link'] = allattrs['link'] - delete attrsJson['link'] - } - - if (allattrs['position']) { - attrsJson['data-sys-asset-position'] = allattrs['position'] - delete attrsJson['position'] - } - - if (allattrs['target']) { - attrsJson['data-sys-asset-isnewtab'] = allattrs['target'] === "_blank" - delete attrsJson['target'] - } - if (!attrsJson['sys-style-type']) { - attrsJson['sys-style-type'] = String(allattrs['asset-type']).indexOf('image') > -1 ? 'display' : 'download' - } - if (attrsJson?.["display-type"] === "display") { - const styleObj = jsonValue?.["attrs"]?.["style"] ?? {}; - if (!styleObj["width"]) { - styleObj["width"] = "auto"; - } - delete styleObj["float"]; - (attrsJson["style"] && typeof attrsJson["style"] === 'string') - ? (attrsJson["style"] += getStyleStringFromObject(styleObj)) : - (attrsJson["style"] = getStyleStringFromObject(styleObj)); - } - delete attrsJson['display-type'] - } - } - if (jsonValue['type'] === "style") { - delete attrsJson['style-text'] - } - - delete attrsJson['redactor-attributes'] - Object.entries(attrsJson).forEach((key) => { - return key[1] ? (key[1] !== '' ? (attrs += `${key[0]}="${key[1]}" `) : '') : '' - }) - attrs = (attrs.trim() ? ' ' : '') + attrs.trim() - } - if(jsonValue['type'] === 'ol' || jsonValue['type'] === 'ul') { - //@ts-ignore + //@ts-ignore return getOLOrULStringFromJson(jsonValue) } - if (jsonValue['type'] === 'reference') { - figureStyles.displayType = jsonValue?.attrs?.["display-type"] - } - - if (jsonValue['type'] === 'span' && jsonValue.children.length === 1 && jsonValue.children[0].type === 'span') { - if (Object.keys(jsonValue.attrs).length === 0) { - return children - } - } - - attrs = (attrs.trim() ? ' ' : '') + attrs.trim() - - return ELEMENT_TYPES[orgType || jsonValue['type']](attrs, children, attrsJson, figureStyles) + return ELEMENT_TYPES[jsonValue['type']](jsonValue['attrs'], children) } - return children -} - -function getStyleStringFromObject(styleObj: { [key: string]: string }) { - return Object.keys(styleObj) - .map((key) => `${key}: ${styleObj[key]}`) - .join("; "); + return children } diff --git a/src/types.ts b/src/types.ts index 5c086a6..adb3785 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,7 +14,7 @@ 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 IJsonToMarkdownElementTags{[key: string]: (attrs:string,child:string,attrsJson:IAnyObject,extraProps?:object) => 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, From bbc69d48d2495cb131f9ea9d45ea68c84cc2ea25 Mon Sep 17 00:00:00 2001 From: Ashwini-Rathod Date: Wed, 5 Jun 2024 19:14:19 +0530 Subject: [PATCH 3/5] chore: refactoring changes --- package-lock.json | 7 ++- package.json | 1 + src/jsonToMarkdown.tsx | 105 +++++++++++++++++++++++++++-------------- 3 files changed, 74 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a0e36a..34daa2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "array-flat-polyfill": "^1.0.1", + "lodash": "^4.17.21", "lodash.clonedeep": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isempty": "^4.4.0", @@ -3837,8 +3838,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.clonedeep": { "version": "4.5.0", @@ -7949,8 +7949,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.clonedeep": { "version": "4.5.0", diff --git a/package.json b/package.json index 76c753f..9d54d47 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ }, "dependencies": { "array-flat-polyfill": "^1.0.1", + "lodash": "^4.17.21", "lodash.clonedeep": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isempty": "^4.4.0", diff --git a/src/jsonToMarkdown.tsx b/src/jsonToMarkdown.tsx index 5b8f996..1a7b84b 100644 --- a/src/jsonToMarkdown.tsx +++ b/src/jsonToMarkdown.tsx @@ -1,10 +1,15 @@ import {IJsonToMarkdownElementTags, IJsonToMarkdownTextTags} from './types' +import {cloneDeep} from 'lodash' +import {Node} from 'slate' + +let listTypes = ['ol', 'ul'] +const elementsToAvoidWithinMarkdownTable = ['ol', 'ul', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'reference', 'img', 'fragment'] const ELEMENT_TYPES: IJsonToMarkdownElementTags = { 'blockquote': (attrs: any, child: any) => { return ` -> ${child}${attrs}` +> ${child}` }, 'h1': (attrs: any, child: string) => { return ` @@ -97,6 +102,42 @@ ${child}` fragment: (attrs: any, child: any) => { return child }, + table: (attrs: any, child: any) => { + return `${child}` + }, + tbody: (attrs: any, child: any) => { + return `${child}` + }, + thead: (attrs: any, child: any) => { + let tableBreak = '|' + if(attrs.cols) { + if(attrs.addEmptyThead) { + let tHeadChildren = '| ' + for(let i = 0; i < attrs.cols; i++) { + tHeadChildren += '| ' + tableBreak += ' ----- |' + } + return `${tHeadChildren}\n${tableBreak}\n` + } + else{ + for(let i = 0; i < attrs.cols; i++) { + tableBreak += ' ----- |' + } + return `${child}\n${tableBreak}\n` + } + } + + return `${child}` + }, + tr: (attrs: any, child: any) => { + return `| ${child}\n` + }, + td: (attrs: any, child: any) => { + return ` ${child.trim()} |` + }, + th: (attrs: any, child: any) => { + return ` ${child.trim()} |` + } } const TEXT_WRAPPERS: IJsonToMarkdownTextTags = { 'bold': (child: any, value: any) => { @@ -105,18 +146,9 @@ const TEXT_WRAPPERS: IJsonToMarkdownTextTags = { 'italic': (child: any, value: any) => { return `*${child}*`; }, - // 'underline': (child: any, value: any) => { - // return `${child}`; - // }, underline is not supported in markdown 'strikethrough': (child: any, value: any) => { return `~~${child}~~`; }, - 'superscript': (child: any, value: any) => { - return `^${child}^`; - }, - 'subscript': (child: any, value: any) => { - return `~${child}~`; - }, 'inlineCode': (child: any, value: any) => { return `\`${child}\`` }, @@ -125,39 +157,29 @@ const TEXT_WRAPPERS: IJsonToMarkdownTextTags = { const getOLOrULStringFromJson = (value: any) => { let child = '' let nestedListFound = false - if(value.type === 'ol'){ + if(listTypes.includes(value.type)){ let start = parseInt(value?.attrs?.start || 1) + let symbol = value?.attrs?.listStyleType || '- ' Array.from(value.children).forEach((val: any, index) => { - if(val.hasOwnProperty('type') && val.type === 'li' && value.children[index + 1] && value.children[index + 1]?.type === 'ol'){ + if(val.hasOwnProperty('type') && val.type === 'li' && value.children[index + 1] && value.children[index + 1]?.type && listTypes.includes(value.children[index + 1].type)){ let liChildren = jsonToMarkdownSerializer(val) let nestedListChildren = getOLOrULStringFromJson(value.children[index + 1]) let indentedNestedListChildren = nestedListChildren.split('\n').filter((child) => child.length).map((child) => ` ${child}`).join('\n') - child += `${index + start}. ${liChildren}\n${indentedNestedListChildren}\n` - nestedListFound = true + if(value.type === 'ol') { + child += `${index + start}. ${liChildren}\n${indentedNestedListChildren}\n` + nestedListFound = true + } + if(value.type === 'ul') child += `${symbol}${liChildren}\n${indentedNestedListChildren}\n` } - else if(val.hasOwnProperty('type') && val.type !== 'ol'){ + else if(val.hasOwnProperty('type') && !listTypes.includes(val.type)){ let liChildren = jsonToMarkdownSerializer(val) - child += `${nestedListFound ? (index + start - 1): index + start}. ${liChildren}\n` + if(value.type === 'ol') child += `${nestedListFound ? (index + start - 1): index + start}. ${liChildren}\n` + if(value.type === 'ul') child += `${symbol}${liChildren}\n` } - }) } - if(value.type === 'ul') { - let symbol = value?.attrs?.listStyleType || '- ' - Array.from(value.children).forEach((val: any, index) => { - if(val.hasOwnProperty('type') && val.type === 'li' && value.children[index + 1] && value.children[index + 1]?.type === 'ol'){ - let liChildren = jsonToMarkdownSerializer(val) - let nestedListChildren = getOLOrULStringFromJson(value.children[index + 1]) - let indentedNestedListChildren = nestedListChildren.split('\n').filter((child) => child.length).map((child) => ` ${child}`).join('\n') - child += `${symbol}${liChildren}\n${indentedNestedListChildren}\n` - } - else if(val.hasOwnProperty('type') && val.type !== 'ol'){ - let liChildren = jsonToMarkdownSerializer(val) - child += `${symbol}${liChildren}\n` - } - }) - } return ` + ${child}` } @@ -178,6 +200,7 @@ export const jsonToMarkdownSerializer = (jsonValue: any): string => { return text } let children: any = '' + if(!jsonValue['type']) return children if (jsonValue.children) { children = Array.from(jsonValue.children).map((child) => jsonToMarkdownSerializer(child)) if (jsonValue['type'] === 'blockquote') { @@ -192,13 +215,25 @@ export const jsonToMarkdownSerializer = (jsonValue: any): string => { } if (ELEMENT_TYPES[jsonValue['type']]) { + let tableAttrs = {} if(jsonValue['type'] === 'ol' || jsonValue['type'] === 'ul') { //@ts-ignore return getOLOrULStringFromJson(jsonValue) } - + if(jsonValue['type'] === 'table') { + tableAttrs = cloneDeep(jsonValue['attrs']) + let thead = Array.from(jsonValue['children']).find((child: any) => child.type && child.type === 'thead') + if(!thead) { + tableAttrs['addEmptyThead'] = true + let emptyTableHead = ELEMENT_TYPES['thead'](tableAttrs, children) + if(emptyTableHead) children = emptyTableHead + children + } + } + if(jsonValue['type'] === 'td' || jsonValue['type'] === 'th') { + let NonAllowedTableChild = Array.from(jsonValue['children']).find((child: any) => elementsToAvoidWithinMarkdownTable.includes(child.type)) + if(NonAllowedTableChild) children = Node.string(jsonValue) + } return ELEMENT_TYPES[jsonValue['type']](jsonValue['attrs'], children) - } - + } return children } From 0f1409c0e07bb8820e82b431ab22058ad00101c0 Mon Sep 17 00:00:00 2001 From: Ashwini-Rathod Date: Thu, 6 Jun 2024 15:40:51 +0530 Subject: [PATCH 4/5] chore: updated readme and added tests --- README.md | 42 ++- test/expectedMarkdown.ts | 650 ++++++++++++++++++++++++++++++++++++ test/jsonToMarkdown.test.ts | 12 + 3 files changed, 703 insertions(+), 1 deletion(-) create mode 100644 test/expectedMarkdown.ts create mode 100644 test/jsonToMarkdown.test.ts diff --git a/README.md b/README.md index 7377114..5921e5d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ Contentstack is a headless CMS with an API-first approach. It is a CMS that developers can use to build powerful cross-platform applications in their favorite languages. Build your application frontend, and Contentstack will take care of the rest. [Read more](https://www.contentstack.com/docs/). -The JSON RTE Serializer package helps you convert the data inside your JSON Rich Text Editor field from JSON to HTML format and vice versa. +The JSON RTE Serializer package assists in converting the content within your JSON Rich Text Editor field between JSON and HTML formats. This means you can easily change your data from JSON format to HTML format for display purposes, and vice versa, for data storage or processing. +If you need to convert JSON to Markdown format, we offer the Markdown Serializer function. This function is specifically designed to transform your JSON data into Markdown, making it easier to handle text formatting for platforms that use Markdown. # Installation @@ -102,6 +103,45 @@ The resulting JSON-formatted data will look as follows: } ``` +### JSON to Markdown Conversion Code + +You can use the following JSON RTE Serializer code to convert your JSON RTE field data into Markdown format. + +```javascript +import { jsonToMarkdown } from "@contentstack/json-rte-serializer"; + +const markdownValue = jsonToHtml({ + type: "doc", + attrs: {}, + uid: "547a479c68824767ce1d9725852f042b", + children: [ + { + uid: "767a479c6882471d9725852f042b67ce", + type: "p", + attrs: {}, + children: [ + { text: "This is Markdown-formatted content which has some " }, + { text: "BOLD", bold: true }, + { text: " text and some "}, + { text: "Italic", italic: true }, + { text: " text."} + ] + }, + ], +}); + +console.log(markdownValue); +``` + +### Result of Conversion + +The resulting Markdown data will look as follows: + +```MARKDOWN + +This is Markdown-formatted content which has some **BOLD** text and some *Italic* text. +``` + ## Custom Conversion For customized conversion scenarios, you can customize your JSON RTE Serializer code to allow the support for additional tags or element types in the JSON Rich Text Editor field. Pass an `options` field (optional) within the `jsonToHtml` or `htmlToJson` method to manipulate the working of the JSON RTE Serializer package as per your requirements. diff --git a/test/expectedMarkdown.ts b/test/expectedMarkdown.ts new file mode 100644 index 0000000..9078c3c --- /dev/null +++ b/test/expectedMarkdown.ts @@ -0,0 +1,650 @@ +export default [ + { + "title": "Inline Element Conversion", + "json": [{ + "type": "doc", + "attrs": {}, + "uid": "08560d5073684e608977ef6d332cf02d", + "children": [{ + "type": "p", + "attrs": {}, + "uid": "c914a0768a1e4ade85f3a9a3aa74d962", + "children": [ + { + "text": "Hello world this is a good day. I have some " + }, + { + "text": "Bold", + "bold": true + }, + { + "text": " text, some " + }, + { + "text": "Italic", + "italic": true + }, + { + "text": " text, some " + }, + { + "text": "underlined", + "underline": true + }, + { + "text": " text." + } + ] + }, + { + "uid": "1fc3e96a71a94d56824997623d078835", + "type": "p", + "children": [ + { + "text": "Also, I have " + }, + { + "text": "sup", + "superscript": true + }, + { + "text": "superscript, " + }, + { + "text": "sub", + "subscript": true + }, + { + "text": "subscript and a line break\nand after a linebreak, we have a " + }, + { + "text": "strikethrough", + "strikethrough": true + }, + { + "text": "." + } + ], + "attrs": {} + }, + { + "uid": "5ead7fa9afe6497098827eda3a9f3667", + "type": "p", + "children": [ + { + "text": "This is a paragraph with " + }, + { + "text": "inline code", + "inlineCode": true + }, + { + "text": "." + } + ], + "attrs": {} + }] + }], + "markdown": ` + +Hello world this is a good day. I have some **Bold** text, some *Italic* text, some underlined text. + +Also, I have supsuperscript, subsubscript and a line break
and after a linebreak, we have a ~~strikethrough~~. + +This is a paragraph with \`inline code\`.` + + }, + { + "title": "Heading Conversion", + "json": [{ + "type": "doc", + "attrs": {}, + "uid": "08560d5073684e608977ef6d332cf02d", + "children": [ { + "uid": "cdfd3344c6314ee3835a7e6c5617de50", + "type": "h1", + "children": [ + { + "text": "Heading 1" + } + ], + "attrs": {} + }, + { + "uid": "32e6564f1b1d41328698cb57b1bf47ed", + "type": "h2", + "children": [ + { + "text": "Heading 2" + } + ], + "attrs": {} + }, + { + "uid": "984f55b404254e5d9ee08b67a9b63cc4", + "type": "h3", + "children": [ + { + "text": "Heading 3" + } + ], + "attrs": {} + }, + { + "uid": "53d56e38f5e04bcf894b6544e82d1df2", + "type": "h4", + "children": [ + { + "text": "Heading 4" + } + ], + "attrs": {} + }, + { + "uid": "31264ba132284ae189b70aa18db7a6d4", + "type": "h5", + "children": [ + { + "text": "Heading 5" + } + ], + "attrs": {} + }, + { + "uid": "c0a6defec7c14978b6f9da2400a5beba", + "type": "h6", + "children": [ + { + "text": "Heading 6" + } + ], + "attrs": {} + }] + }], + "markdown": ` + +#Heading 1# + +##Heading 2## + +###Heading 3### + +####Heading 4#### + +#####Heading 5##### + +######Heading 6######` +}, +{ + "title": "Block Quote Conversion", + "json": [{ + "type": "doc", + "attrs": {}, + "uid": "08560d5073684e608977ef6d332cf02d", + "children": [{ + "uid": "ed1bb8de67eb437da903bae0aa9242b5", + "type": "blockquote", + "children": [ + { + "text": "Block Quote" + } + ], + "attrs": {} + }] + }], + "markdown": ` + +> Block Quote` +}, +{ + "title": "Code Block Conversion", + "json": [{ + "type": "doc", + "attrs": {}, + "uid": "08560d5073684e608977ef6d332cf02d", + "children": [{ + "uid": "303de6b72d994df99e396abc1f87d3c0", + "type": "code", + "children": [ + { + "text": "Code Block" + } + ], + "attrs": { + "language": "html" + } + }] + }], + "markdown": ` + + Code Block ` +}, +{ + "title": "Divider Conversion", + "json": [{ + "type": "doc", + "attrs": {}, + "uid": "08560d5073684e608977ef6d332cf02d", + "children": [ + { + "uid": "f4b1d34cdd3a46519844232052f3aad6", + "type": "hr", + "children": [ + { + "text": "" + } + ], + "attrs": {} + }] + }], + "markdown": ` + +----------` +}, +{ + "title": "Reference Conversion", + "json": [ { + "uid": "6e24d3bb8d5f43e7ba1aef4faba22986", + "type": "p", + "attrs": {}, + "children": [ + { + "text": "" + }, + { + "uid": "9d496b31bfa64f0fbf4beb9859bef8de", + "type": "reference", + "attrs": { + "display-type": "display", + "asset-uid": "blt3108327519cf8663", + "content-type-uid": "sys_assets", + "asset-link": "https://images.contentstack.io/v3/assets/blt2cf669e5016d5e07/blt3108327519cf8663/65a8be2d14eace6980a5abd6/test-base-64.jpeg", + "asset-name": "test-base-64.jpeg", + "asset-type": "image/jpeg", + "type": "asset", + "class-name": "embedded-asset", + "alt": "test-base-64.jpeg", + "asset-alt": "test-base-64.jpeg", + "inline": true, + "redactor-attributes": { + "alt": "test-base-64.jpeg", + "position": "none", + "inline": true + }, + "style": {}, + "position": "none" + }, + "children": [ + { + "text": "" + } + ] + }, + { + "text": "" + } + ] + }], + "markdown": ` + + + +![test-base-64.jpeg] +(https://images.contentstack.io/v3/assets/blt2cf669e5016d5e07/blt3108327519cf8663/65a8be2d14eace6980a5abd6/test-base-64.jpeg)` +}, +{ + "title": "Anchor Link Conversion", + "json": [{ + "type": "doc", + "attrs": {}, + "uid": "08560d5073684e608977ef6d332cf02d", + "children": [ + { + "uid": "00ee249466604fe3bc1e9bc9c360c0c9", + "type": "p", + "children": [ + { + "text": "Some Paragraph and adding a " + }, + { + "uid": "2aef414d0d5d49038d472c2e59660941", + "type": "a", + "attrs": { + "url": "www.google.com", + "target": "_self" + }, + "children": [ + { + "text": "link" + } + ] + }, + { + "text": " in the middle." + } + ], + "attrs": {} + }] + }], + "markdown": ` + +Some Paragraph and adding a [link](www.google.com) in the middle.` +}, +{ + "title": "Asset as Link Conversion", + "json": [{ + "type": "doc", + "attrs": {}, + "uid": "08560d5073684e608977ef6d332cf02d", + "children": [ + { + "uid": "67a215fc034f4daf867ebc393fe7fd45", + "type": "p", + "children": [ + { + "text": "Also adding an asset as a link here. Here are the " + }, + { + "uid": "edaaa544ae9842d9b2ecd6d773b1a815", + "type": "reference", + "attrs": { + "display-type": "link", + "type": "asset", + "class-name": "embedded-entry redactor-component undefined-entry", + "asset-uid": "blt6a5e908abbd88573", + "content-type-uid": "sys_assets", + "target": "_self", + "href": "https://images.contentstack.io/v3/assets/blt2cf669e5016d5e07/blt6a5e908abbd88573/65a8bd5e14eace2183a5abcc/Phillip-Island-Penguin-Parade-10.jpeg" + }, + "children": [ + { + "text": "Penguins." + } + ] + }, + { + "text": " Enjoy!" + } + ], + "attrs": {} + }] + }], + "markdown": ` + +Also adding an asset as a link here. Here are the [Penguins.](https://images.contentstack.io/v3/assets/blt2cf669e5016d5e07/blt6a5e908abbd88573/65a8bd5e14eace2183a5abcc/Phillip-Island-Penguin-Parade-10.jpeg) Enjoy!` +}, +{ + "title": "Ordered List Conversion", + "json": [{ + "type": "doc", + "attrs": {}, + "uid": "08560d5073684e608977ef6d332cf02d", + "children": [ + { + "uid": "de365962da4a4c9eb2121d7face56b1d", + "type": "ol", + "children": [ + { + "uid": "1edc6c9df32747ca84aa16a6258d9b94", + "type": "li", + "children": [ + { + "text": "One" + } + ], + "attrs": {} + }, + { + "uid": "fa667acc02734b24bb4fb0eb4391e5f7", + "type": "ol", + "attrs": {}, + "children": [ + { + "uid": "d25d22e6485742b3a28816b84015522d", + "type": "li", + "children": [ + { + "text": "Nested One" + } + ], + "attrs": {} + }, + { + "uid": "d2ae3bacc14440c5b58656d6eebd1d29", + "type": "li", + "children": [ + { + "text": "Nested Two" + } + ], + "attrs": {} + } + ] + }, + { + "uid": "1daeae84b7504a9bb0ae3649d53e4731", + "type": "li", + "children": [ + { + "text": "Two" + } + ], + "attrs": {} + }, + { + "uid": "13d19bda8d6c4d889ad185b38961d5aa", + "type": "li", + "children": [ + { + "text": "Three" + } + ], + "attrs": {} + } + ], + "id": "e28ec771d8114561a3b7816370340166", + "attrs": {} + }] + }], + "markdown": ` + +1. One + 1. Nested One + 2. Nested Two +2. Two +3. Three +` +}, +{ + "title": "Unordered List Conversion", + "json": [{ + "type": "doc", + "attrs": {}, + "uid": "08560d5073684e608977ef6d332cf02d", + "children": [ + { + "uid": "aff3137e07be4d9b9684b5afb59d6a91", + "type": "ul", + "children": [ + { + "uid": "f2d239cc5bf043ecaf3e24cf68f894e9", + "type": "li", + "children": [ + { + "text": "UL One" + } + ], + "attrs": {} + }, + { + "uid": "98619c145a5145c6b5400a6dd38da02e", + "type": "li", + "children": [ + { + "text": "UL Two" + } + ], + "attrs": {} + }, + { + "uid": "7116006cfa0348eb93925bb3f8734631", + "type": "ul", + "attrs": {}, + "children": [ + { + "uid": "2856e1550cfb448380ac4fe1199e2d7c", + "type": "li", + "children": [ + { + "text": "nested ul one" + } + ], + "attrs": {} + } + ] + }, + { + "uid": "d1ebffb3bead4a18bc909b7217c335d6", + "type": "li", + "children": [ + { + "text": "UL Three" + } + ], + "attrs": {} + } + ], + "id": "e38afd21166441b984173ffd1b7d0b89", + "attrs": {} + }] + }], + "markdown": ` + +- UL One +- UL Two + - nested ul one +- UL Three +` +}, +{ + "title": "Table Conversion", + "json": [{ + "type": "doc", + "attrs": {}, + "uid": "08560d5073684e608977ef6d332cf02d", + "children": [ + { + "uid": "ae13dae85fd04e4584e1f677dcc03828", + "type": "table", + "attrs": { + "rows": 2, + "cols": 2, + "colWidths": [ + 250, + 250 + ], + "disabledCols": [ + 0, + 1 + ] + }, + "children": [ + { + "type": "tbody", + "attrs": {}, + "children": [ + { + "type": "tr", + "attrs": {}, + "children": [ + { + "type": "td", + "attrs": {}, + "children": [ + { + "type": "p", + "attrs": {}, + "children": [ + { + "text": "Cell 1" + } + ], + "uid": "9f2a19d96b1e4ce4b7fa6e65a04b5809" + } + ], + "uid": "acf20185ccba45729315fa6c2ebedf89" + }, + { + "type": "td", + "attrs": {}, + "children": [ + { + "type": "p", + "attrs": {}, + "children": [ + { + "text": "Cell 2" + } + ], + "uid": "058aba304d084b76a048d2b3b4591029" + } + ], + "uid": "3b99a891f8ae408199b73ad1fd0e2326" + } + ], + "uid": "597ccff75f4d4587882cdbdfaa43d2db" + }, + { + "type": "tr", + "attrs": {}, + "children": [ + { + "type": "td", + "attrs": { + "colSpan": 2, + "redactor-attributes": { + "colspan": 2 + } + }, + "children": [ + { + "type": "p", + "attrs": {}, + "children": [ + { + "text": "Cell 3" + } + ], + "uid": "d3a7b3cc7419402bac83adc91f3bddaa" + } + ], + "uid": "0504ac4bc6db4b7e848912e76aa04d6c" + }, + { + "uid": "17e95a5eb8e740f58e3034530b1227ef", + "type": "td", + "attrs": { + "void": true + }, + "children": [ + { + "text": "" + } + ] + } + ], + "uid": "eb9515fa140f484b92a61b1ed9b06063" + } + ], + "uid": "a3e2a861a2e1486e9c3a7c2a3882c449" + } + ] + }] + }], + "markdown": `| | | +| ----- | ----- | +| Cell 1 | Cell 2 | +| Cell 3 | | +` +}, +] \ No newline at end of file diff --git a/test/jsonToMarkdown.test.ts b/test/jsonToMarkdown.test.ts new file mode 100644 index 0000000..b759426 --- /dev/null +++ b/test/jsonToMarkdown.test.ts @@ -0,0 +1,12 @@ +import { jsonToMarkdownSerializer } from "../src/jsonToMarkdown" +import expectedMarkdown from "./expectedMarkdown" + +describe("Testing json to markdown conversion", () => { + for (const testCase of [...expectedMarkdown]) { + it(testCase.title, () => { + let jsonValue = testCase.json + let markdownValue = jsonToMarkdownSerializer(jsonValue[0]) + expect(markdownValue).toEqual(testCase.markdown) + }) + } +}) \ No newline at end of file From 933e2f5599d4fcc9e770e47490ead0c797066e30 Mon Sep 17 00:00:00 2001 From: Ashwini-Rathod Date: Fri, 7 Jun 2024 14:27:07 +0530 Subject: [PATCH 5/5] chore: updated package with new 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 34daa2b..f88724b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/json-rte-serializer", - "version": "2.0.6", + "version": "2.0.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@contentstack/json-rte-serializer", - "version": "2.0.6", + "version": "2.0.7", "license": "MIT", "dependencies": { "array-flat-polyfill": "^1.0.1", diff --git a/package.json b/package.json index 9d54d47..1fe2e40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/json-rte-serializer", - "version": "2.0.6", + "version": "2.0.7", "description": "This Package converts Html Document to Json and vice-versa.", "main": "lib/index.js", "module": "lib/index.mjs",