diff --git a/package-lock.json b/package-lock.json index 7deee9c..4047ef4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/json-rte-serializer", - "version": "2.0.13", + "version": "3.0.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@contentstack/json-rte-serializer", - "version": "2.0.13", + "version": "3.0.4", "license": "MIT", "dependencies": { "array-flat-polyfill": "^1.0.1", diff --git a/package.json b/package.json index 46f84fd..d2e3a7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/json-rte-serializer", - "version": "3.0.3", + "version": "3.0.4", "description": "This Package converts Html Document to Json and vice-versa.", "main": "lib/index.js", "module": "lib/index.mjs", diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..cd5f230 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,517 @@ +import isEmpty from 'lodash.isempty' +import { IJsonToMarkdownElementTags, IJsonToMarkdownTextTags, IHtmlToJsonElementTags, IHtmlToJsonTextTags, IJsonToHtmlElementTags, IJsonToHtmlTextTags, IJsonToHtmlAllowedEmptyAttributes } from '../types' + +export const listTypes = ['ol', 'ul']; + +export const elementsToAvoidWithinMarkdownTable = [ + 'ol', + 'ul', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'blockquote', + 'code', + 'reference', + 'img', + 'fragment' +]; + +export const ELEMENT_TYPES: IJsonToMarkdownElementTags = { + 'blockquote': (attrs: any, child: any) => { + return ` + +> ${child}` + }, + '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: (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})` + } + return '' + }, + 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: (attrsJson: any, child: any) => { + return `[${child}](${attrsJson.url})` + }, + hr: (attrs: any, child: any) => { + return ` + +----------` + }, + span: (attrs: any, child: any) => { + return `${child}` + }, + reference: (attrsJson: any, child: any): any => { + if(attrsJson?.['display-type'] === 'display') { + if(attrsJson) { + let assetName = attrsJson?.['asset-name'] ? attrsJson['asset-name'] : 'enter image description here' + let assetURL = attrsJson?.['asset-link'] ? attrsJson['asset-link'] : '' + return ` + +![${assetName}] +(${assetURL})` + } + } + else if(attrsJson?.['display-type'] === 'link') { + if(attrsJson) { + return `[${child}](${attrsJson?.['href'] ? attrsJson['href'] : "#"})` + } + } + }, + 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()} |` + } +}; + +export const TEXT_WRAPPERS: IJsonToMarkdownTextTags = { + 'bold': (child: any, value: any) => { + return `**${child}**`; + }, + 'italic': (child: any, value: any) => { + return `*${child}*`; + }, + 'strikethrough': (child: any, value: any) => { + return `~~${child}~~`; + }, + 'inlineCode': (child: any, value: any) => { + return `\`${child}\`` + }, +}; + +export const ELEMENT_TAGS: IHtmlToJsonElementTags = { + A: (el: HTMLElement) => { + const attrs: Record = {} + const target = el.getAttribute('target'); + const href = el.getAttribute('href'); + const title = el.getAttribute('title'); + + attrs.url = href ? href : '#'; + + if(target && target !== '') { + attrs.target = target; + } + if(title && title !== '') { + attrs.title = title; + } + + return { + type: "a", + attrs: attrs, + }; + }, + BLOCKQUOTE: () => ({ type: 'blockquote', attrs: {} }), + H1: () => ({ type: 'h1', attrs: {} }), + H2: () => ({ type: 'h2', attrs: {} }), + H3: () => ({ type: 'h3', attrs: {} }), + H4: () => ({ type: 'h4', attrs: {} }), + H5: () => ({ type: 'h5', attrs: {} }), + H6: () => ({ type: 'h6', attrs: {} }), + IMG: (el: HTMLElement) => { + let imageUrl = el.getAttribute('src')?.split(".") || ["png"] + let imageType = imageUrl[imageUrl?.length - 1] + const assetUid = el.getAttribute('asset_uid') + if(assetUid){ + + const splittedUrl = el.getAttribute('src')?.split('/')! || [null] + const assetName = splittedUrl[splittedUrl?.length - 1] + return { type: 'reference', attrs: { "asset-name": assetName,"content-type-uid" : "sys_assets", "asset-link": el.getAttribute('src'), "asset-type": `image/${imageType}`, "display-type": "display", "type": "asset", "asset-uid": assetUid } } + } + const imageAttrs : any = { type: 'img', attrs: { url: el.getAttribute('src') } } + if (el.getAttribute('width')) { + imageAttrs.attrs['width'] = el.getAttribute('width') + } + return imageAttrs + }, + LI: () => ({ type: 'li', attrs: {} }), + OL: () => ({ type: 'ol', attrs: {} }), + P: () => ({ type: 'p', attrs: {} }), + PRE: () => ({ type: 'code', attrs: {} }), + UL: () => ({ type: 'ul', attrs: {} }), + IFRAME: (el: HTMLElement) => { + if(el.getAttribute('data-type') === "social-embeds") { + const src = el.getAttribute('src') + el.removeAttribute('data-type') + el.removeAttribute('src') + return { type: 'social-embeds', attrs: { src } } + } + return { type: 'embed', attrs: { src: el.getAttribute('src') } } + }, + TABLE: (el: HTMLElement) => ({ type: 'table', attrs: {} }), + THEAD: (el: HTMLElement) => ({ type: 'thead', attrs: {} }), + TBODY: (el: HTMLElement) => ({ type: 'tbody', attrs: {} }), + TR: (el: HTMLElement) => ({ type: 'tr', attrs: {} }), + TD: (el: HTMLElement) => ({ type: 'td', attrs: { ...spanningAttrs(el) } }), + TH: (el: HTMLElement) => ({ type: 'th', attrs: { ...spanningAttrs(el) } }), + // FIGURE: (el: HTMLElement) => ({ type: 'reference', attrs: { default: true, "display-type": "display", "type": "asset" } }), + + FIGURE: (el: HTMLElement) => { + if (el.lastChild && el.lastChild.nodeName === 'P') { + return { type: 'figure', attrs: {} } + } + else { + return { type: 'img', attrs: {} } + } + + }, + SPAN: (el: HTMLElement) => { + return { type: 'span', attrs: {} } + }, + DIV: (el: HTMLElement) => { + return { type: 'div', attrs: {} } + }, + VIDEO: (el: HTMLElement) => { + const srcArray = Array.from(el.querySelectorAll("source")).map((source) => + source.getAttribute("src") + ); + + return { + type: 'embed', + attrs: { + src: srcArray.length > 0 ? srcArray[0] : null, + }, + } + }, + STYLE: (el: HTMLElement) => { + return { type: 'style', attrs: { "style-text": el.textContent } } + }, + SCRIPT: (el: HTMLElement) => { + return { type: 'script', attrs: {} } + }, + HR: () => ({ type: 'hr', attrs: {} }), + FIGCAPTION: () => ({ type: 'figcaption', attrs: {} }), +} + +const spanningAttrs = (el: HTMLElement) => { + const attrs = {} + const rowSpan = parseInt(el.getAttribute('rowspan') ?? '1') + const colSpan = parseInt(el.getAttribute('colspan') ?? '1') + if (rowSpan > 1) attrs['rowSpan'] = rowSpan + if (colSpan > 1) attrs['colSpan'] = colSpan + + return attrs +} + +export const TEXT_TAGS: IHtmlToJsonTextTags = { + CODE: () => ({ code: true }), + DEL: () => ({ strikethrough: true }), + EM: () => ({ italic: true }), + I: () => ({ italic: true }), + S: () => ({ strikethrough: true }), + STRONG: () => ({ bold: true }), + B: () => ({ bold: true }), + U: () => ({ underline: true }), + SUP: () => ({ superscript: true }), + SUB: () => ({ subscript: true }) +} + +export const HTML_ELEMENT_TYPES: IJsonToHtmlElementTags = { + 'blockquote': (attrs: string, child: string) => { + return `${child}` + }, + 'h1': (attrs, child) => { + 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,jsonBlock: 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}` + }, + + embed: (attrs: any, child: any) => { + return `` + }, + p: (attrs: any, child: any) => { + if(child.includes("${child}` + return `${child}

` + }, + ol: (attrs: any, child: any) => { + return `${child}` + }, + ul: (attrs: any, child: any) => { + return `${child}` + }, + code: (attrs: any, child: any) => { + return `${child.replace(//g, '\n')}` + }, + li: (attrs: any, child: any) => { + return `${child}` + }, + a: (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) => { + return `${child}` + }, + tr: (attrs: any, child: any) => { + return `${child}` + }, + trgrp: (attrs: any, child: any) => { + return child + }, + td: (attrs: any, child: any) => { + return `${child}` + }, + th: (attrs: any, child: any) => { + return `${child}` + }, + 'check-list': (attrs: any, child: any) => { + return `${child}

` + }, + row: (attrs: any, child: any) => { + return `${child}` + }, + column: (attrs: any, child: any) => { + return `${child}` + }, + 'grid-container': (attrs: any, child: any) => { + return `${child}` + }, + 'grid-child': (attrs: any, child: any) => { + return `${child}` + }, + 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, jsonBlock: any, extraAttrs: any) => { + if (extraAttrs?.displayType === 'inline') { + return `${child}` + } else if (extraAttrs?.displayType === 'block') { + return `${child}` + } else if (extraAttrs?.displayType === 'link') { + return `${child}` + } else if (extraAttrs?.displayType === 'asset') { + return `${child}` + } + + else if (extraAttrs?.displayType === "display") { + const anchor = jsonBlock?.["attrs"]?.["link"] ?? jsonBlock?.["attrs"]?.["anchorLink"]; + + const caption = jsonBlock?.["attrs"]?.["asset-caption"]; + const position = jsonBlock?.["attrs"]?.["position"]; + const inline = jsonBlock?.["attrs"]?.["inline"] + let figureAttrs = "" + const figureStyles: { [key: string]: string } = { + margin: "0", + }; + if(!attrs.includes(`src="${jsonBlock?.["attrs"]?.["asset-link"]}`)){ + attrs = ` src="${jsonBlock?.["attrs"]?.["asset-link"]}"` + attrs; + } + let img = ``; + + if (anchor) { + const target = jsonBlock?.["attrs"]?.["target"]; + let anchorAttrs = `href="${anchor}"`; + if (target) { + anchorAttrs = `${anchorAttrs} target="${target}"`; + } + img = `${img}`; + } + + if (caption || (position && position !== "none")) { + const figcaption = caption + ? `
${caption}
` + : ""; + + if (inline && position !== "right" && position !== "left") { + figureStyles["display"] = "inline-block"; + } + if (position && position !== "none") { + figureStyles[inline ? "float" : "text-align"] = position; + } + + if(figcaption){ + img = `
${img}${figcaption}
`; + } + } + if(!isEmpty(figureStyles)){ + figureAttrs = ` style="${Object.keys(figureStyles).map((key) => `${key}: ${figureStyles[key]}`).join("; ")}"` + } + if(inline && !caption && (!position ||position==='none')){ + return img + } + return `${img}`; + } + return `${child}` + }, + inlineCode: (attrs: any, child: any) => { + return "" + }, + fragment: (attrs: any, child: any) => { + return child + }, + style: (attrs: any, child: any) => { + return `` + }, + script: (attrs: any, child: any) => { + return `` + }, + "social-embeds": (attrs: any, child: any) => { + return `` + } +} + +export const HTML_TEXT_WRAPPERS: IJsonToHtmlTextTags = { + 'bold': (child: any, value:any) => { + return `${child}`; + }, + 'italic': (child: any, value:any) => { + return `${child}`; + }, + 'underline': (child: any, value:any) => { + return `${child}`; + }, + '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}` + }, +} + +export const ALLOWED_EMPTY_ATTRIBUTES: IJsonToHtmlAllowedEmptyAttributes = { + img: ['alt'], + reference: ['alt'] +} + +export const isInline = ['span', 'a', 'inlineCode', 'reference'] +export const isVoid = ['img', 'embed'] + +export const whiteCharPattern = /^[\s ]{2,}$/ diff --git a/src/fromRedactor.tsx b/src/fromRedactor.tsx index 07021ff..21c4808 100644 --- a/src/fromRedactor.tsx +++ b/src/fromRedactor.tsx @@ -7,129 +7,12 @@ import cloneDeep from "lodash/cloneDeep" import isUndefined from "lodash/isUndefined" import { jsx } from './utils/jsx' +import { ELEMENT_TAGS, TEXT_TAGS, isInline, isVoid, whiteCharPattern } from './constants' import {IHtmlToJsonElementTags,IHtmlToJsonOptions, IHtmlToJsonTextTags, IAnyObject} from './types' const generateId = () => v4().split('-').join('') -const isInline = ['span', 'a', 'inlineCode', 'reference'] -const isVoid = ['img', 'embed'] - -export const ELEMENT_TAGS: IHtmlToJsonElementTags = { - A: (el: HTMLElement) => { - const attrs: Record = {} - const target = el.getAttribute('target'); - const href = el.getAttribute('href'); - const title = el.getAttribute('title'); - - attrs.url = href ? href : '#'; - - if(target && target !== '') { - attrs.target = target; - } - if(title && title !== '') { - attrs.title = title; - } - - return { - type: "a", - attrs: attrs, - }; - }, - BLOCKQUOTE: () => ({ type: 'blockquote', attrs: {} }), - H1: () => ({ type: 'h1', attrs: {} }), - H2: () => ({ type: 'h2', attrs: {} }), - H3: () => ({ type: 'h3', attrs: {} }), - H4: () => ({ type: 'h4', attrs: {} }), - H5: () => ({ type: 'h5', attrs: {} }), - H6: () => ({ type: 'h6', attrs: {} }), - IMG: (el: HTMLElement) => { - let imageUrl = el.getAttribute('src')?.split(".") || ["png"] - let imageType = imageUrl[imageUrl?.length - 1] - const assetUid = el.getAttribute('asset_uid') - if(assetUid){ - - const splittedUrl = el.getAttribute('src')?.split('/')! || [null] - const assetName = splittedUrl[splittedUrl?.length - 1] - return { type: 'reference', attrs: { "asset-name": assetName,"content-type-uid" : "sys_assets", "asset-link": el.getAttribute('src'), "asset-type": `image/${imageType}`, "display-type": "display", "type": "asset", "asset-uid": assetUid } } - } - const imageAttrs : any = { type: 'img', attrs: { url: el.getAttribute('src') } } - if (el.getAttribute('width')) { - imageAttrs.attrs['width'] = el.getAttribute('width') - } - return imageAttrs - }, - LI: () => ({ type: 'li', attrs: {} }), - OL: () => ({ type: 'ol', attrs: {} }), - P: () => ({ type: 'p', attrs: {} }), - PRE: () => ({ type: 'code', attrs: {} }), - UL: () => ({ type: 'ul', attrs: {} }), - IFRAME: (el: HTMLElement) => { - if(el.getAttribute('data-type') === "social-embeds") { - const src = el.getAttribute('src') - el.removeAttribute('data-type') - el.removeAttribute('src') - return { type: 'social-embeds', attrs: { src } } - } - return { type: 'embed', attrs: { src: el.getAttribute('src') } } - }, - TABLE: (el: HTMLElement) => ({ type: 'table', attrs: {} }), - THEAD: (el: HTMLElement) => ({ type: 'thead', attrs: {} }), - TBODY: (el: HTMLElement) => ({ type: 'tbody', attrs: {} }), - TR: (el: HTMLElement) => ({ type: 'tr', attrs: {} }), - TD: (el: HTMLElement) => ({ type: 'td', attrs: { ...spanningAttrs(el) } }), - TH: (el: HTMLElement) => ({ type: 'th', attrs: { ...spanningAttrs(el) } }), - // FIGURE: (el: HTMLElement) => ({ type: 'reference', attrs: { default: true, "display-type": "display", "type": "asset" } }), - - FIGURE: (el: HTMLElement) => { - if (el.lastChild && el.lastChild.nodeName === 'P') { - return { type: 'figure', attrs: {} } - } - else { - return { type: 'img', attrs: {} } - } - - }, - SPAN: (el: HTMLElement) => { - return { type: 'span', attrs: {} } - }, - DIV: (el: HTMLElement) => { - return { type: 'div', attrs: {} } - }, - VIDEO: (el: HTMLElement) => { - const srcArray = Array.from(el.querySelectorAll("source")).map((source) => - source.getAttribute("src") - ); - - return { - type: 'embed', - attrs: { - src: srcArray.length > 0 ? srcArray[0] : null, - }, - } - }, - STYLE: (el: HTMLElement) => { - return { type: 'style', attrs: { "style-text": el.textContent } } - }, - SCRIPT: (el: HTMLElement) => { - return { type: 'script', attrs: {} } - }, - HR: () => ({ type: 'hr', attrs: {} }), - FIGCAPTION: () => ({ type: 'figcaption', attrs: {} }), -} - -const TEXT_TAGS: IHtmlToJsonTextTags = { - CODE: () => ({ code: true }), - DEL: () => ({ strikethrough: true }), - EM: () => ({ italic: true }), - I: () => ({ italic: true }), - S: () => ({ strikethrough: true }), - STRONG: () => ({ bold: true }), - B: () => ({ bold: true }), - U: () => ({ underline: true }), - SUP: () => ({ superscript: true }), - SUB: () => ({ subscript: true }) -} const trimChildString = (child: any) => { if (typeof child === 'string') { return child.trim() !== '' @@ -201,8 +84,17 @@ const traverseChildAndWarpChild = (children: Array, allowNonStandardTags return children } -const whiteCharPattern = /^[\s ]{2,}$/ export const fromRedactor = (el: any, options?:IHtmlToJsonOptions) : IAnyObject | null => { + let localElementTags: IHtmlToJsonElementTags = ELEMENT_TAGS; + let localTextTags: IHtmlToJsonTextTags = TEXT_TAGS; + + if (options?.customElementTags && !isEmpty(options.customElementTags)){ + localElementTags = { ...localElementTags, ...options.customElementTags }; + } + + if (options?.customTextTags && !isEmpty(options.customTextTags)) { + localTextTags = { ...localTextTags, ...options.customTextTags }; + } // If node is text node if (el.nodeType === 3) { if (whiteCharPattern.test(el.textContent)) return null @@ -293,14 +185,8 @@ export const fromRedactor = (el: any, options?:IHtmlToJsonOptions) : IAnyObject } const { nodeName } = el let parent = el - if(el.nodeName === "BODY"){ - if(options?.customElementTags && !isEmpty(options.customElementTags)){ - Object.assign(ELEMENT_TAGS, options.customElementTags) - } - if(options?.customTextTags && !isEmpty(options.customTextTags)) { - Object.assign(TEXT_TAGS, options.customTextTags) - } - } + + let children: any = flatten(Array.from(parent.childNodes).map((child) => fromRedactor(child, options))) children = children.filter((child: any) => child !== null) children = traverseChildAndWarpChild(children, options?.allowNonStandardTags) @@ -317,7 +203,7 @@ 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)) { + if (options?.allowNonStandardTags && !Object.keys(localElementTags).includes(nodeName) && !Object.keys(localTextTags).includes(nodeName)) { const attributes = (el as HTMLElement).attributes const attributeMap = {} Array.from(attributes).forEach((attribute) => { @@ -535,7 +421,7 @@ export const fromRedactor = (el: any, options?:IHtmlToJsonOptions) : IAnyObject } } - if (ELEMENT_TAGS[nodeName]) { + if (localElementTags[nodeName]) { if (el.nodeName === 'P') { children = children.map((child: any) => { if (typeof child === 'string') { @@ -553,7 +439,7 @@ export const fromRedactor = (el: any, options?:IHtmlToJsonOptions) : IAnyObject return null } - let elementAttrs = ELEMENT_TAGS[nodeName](el) + let elementAttrs = localElementTags[nodeName](el) const attributes = el.attributes if (attributes.length !== 0) { const attribute = Array.from(attributes).map(getDomAttributes) @@ -872,7 +758,7 @@ export const fromRedactor = (el: any, options?:IHtmlToJsonOptions) : IAnyObject } let noOfInlineElement = 0 Array.from(el.parentNode?.childNodes || []).forEach((child: any) => { - if (child.nodeType === 3 || child.nodeName === 'SPAN' || child.nodeName === 'A' || (options?.allowNonStandardTags && child.getAttribute('inline')) || child.nodeName in TEXT_TAGS) { + if (child.nodeType === 3 || child.nodeName === 'SPAN' || child.nodeName === 'A' || (options?.allowNonStandardTags && child.getAttribute('inline')) || child.nodeName in localTextTags) { noOfInlineElement += 1 } }) @@ -902,8 +788,8 @@ export const fromRedactor = (el: any, options?:IHtmlToJsonOptions) : IAnyObject return jsx('element', elementAttrs, children) } - if (TEXT_TAGS[nodeName]) { - const attrs = TEXT_TAGS[nodeName](el) + if (localTextTags[nodeName]) { + const attrs = localTextTags[nodeName](el) let attrsStyle = { attrs: { style: {} }, ...attrs } let newChildren = children.map((child: any) => { @@ -1014,16 +900,6 @@ export const getNestedValueIfAvailable = (value: string) => { } }; - -const spanningAttrs = (el: HTMLElement) => { - const attrs = {} - const rowSpan = parseInt(el.getAttribute('rowspan') ?? '1') - const colSpan = parseInt(el.getAttribute('colspan') ?? '1') - if (rowSpan > 1) attrs['rowSpan'] = rowSpan - if (colSpan > 1) attrs['colSpan'] = colSpan - - return attrs -} const emptyCell = (cellType: string, attrs = {}) => { return jsx('element', { type: cellType, attrs: { void: true, ...attrs } }, [{ text: '' }]) } diff --git a/src/jsonToMarkdown.tsx b/src/jsonToMarkdown.tsx index 3ec4f03..cc2d7fc 100644 --- a/src/jsonToMarkdown.tsx +++ b/src/jsonToMarkdown.tsx @@ -1,158 +1,7 @@ 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}` - }, - '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: (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})` - } - return '' - }, - 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: (attrsJson: any, child: any) => { - return `[${child}](${attrsJson.url})` - }, - hr: (attrs: any, child: any) => { - return ` - -----------` - }, - span: (attrs: any, child: any) => { - return `${child}` - }, - reference: (attrsJson: any, child: any): any => { - if(attrsJson?.['display-type'] === 'display') { - if(attrsJson) { - let assetName = attrsJson?.['asset-name'] ? attrsJson['asset-name'] : 'enter image description here' - let assetURL = attrsJson?.['asset-link'] ? attrsJson['asset-link'] : '' - return ` - -![${assetName}] -(${assetURL})` - } - } - else if(attrsJson?.['display-type'] === 'link') { - if(attrsJson) { - return `[${child}](${attrsJson?.['href'] ? attrsJson['href'] : "#"})` - } - } - }, - 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) => { - return `**${child}**`; - }, - 'italic': (child: any, value: any) => { - return `*${child}*`; - }, - 'strikethrough': (child: any, value: any) => { - return `~~${child}~~`; - }, - 'inlineCode': (child: any, value: any) => { - return `\`${child}\`` - }, -} +import { listTypes, elementsToAvoidWithinMarkdownTable, ELEMENT_TYPES, TEXT_WRAPPERS } from './constants' const getOLOrULStringFromJson = (value: any) => { let child = '' diff --git a/src/toRedactor.tsx b/src/toRedactor.tsx index 02ab067..6ff92a0 100644 --- a/src/toRedactor.tsx +++ b/src/toRedactor.tsx @@ -3,220 +3,7 @@ import isEmpty from 'lodash.isempty' import {IJsonToHtmlElementTags, IJsonToHtmlOptions, IJsonToHtmlTextTags, IJsonToHtmlAllowedEmptyAttributes} from './types' import isPlainObject from 'lodash.isplainobject' import {replaceHtmlEntities, forbiddenAttrChars } from './utils' - -const ELEMENT_TYPES: IJsonToHtmlElementTags = { - 'blockquote': (attrs: string, child: string) => { - return `${child}` - }, - 'h1': (attrs, child) => { - 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,jsonBlock: 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}` - }, - - embed: (attrs: any, child: any) => { - return `` - }, - p: (attrs: any, child: any) => { - if(child.includes("${child}` - return `${child}

` - }, - ol: (attrs: any, child: any) => { - return `${child}` - }, - ul: (attrs: any, child: any) => { - return `${child}` - }, - code: (attrs: any, child: any) => { - return `${child.replace(//g, '\n')}` - }, - li: (attrs: any, child: any) => { - return `${child}` - }, - a: (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) => { - return `${child}` - }, - tr: (attrs: any, child: any) => { - return `${child}` - }, - trgrp: (attrs: any, child: any) => { - return child - }, - td: (attrs: any, child: any) => { - return `${child}` - }, - th: (attrs: any, child: any) => { - return `${child}` - }, - 'check-list': (attrs: any, child: any) => { - return `${child}

` - }, - row: (attrs: any, child: any) => { - return `${child}` - }, - column: (attrs: any, child: any) => { - return `${child}` - }, - 'grid-container': (attrs: any, child: any) => { - return `${child}` - }, - 'grid-child': (attrs: any, child: any) => { - return `${child}` - }, - 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, jsonBlock: any, extraAttrs: any) => { - if (extraAttrs?.displayType === 'inline') { - return `${child}` - } else if (extraAttrs?.displayType === 'block') { - return `${child}` - } else if (extraAttrs?.displayType === 'link') { - return `${child}` - } else if (extraAttrs?.displayType === 'asset') { - return `${child}` - } - - else if (extraAttrs?.displayType === "display") { - const anchor = jsonBlock?.["attrs"]?.["link"] ?? jsonBlock?.["attrs"]?.["anchorLink"]; - - const caption = jsonBlock?.["attrs"]?.["asset-caption"]; - const position = jsonBlock?.["attrs"]?.["position"]; - const inline = jsonBlock?.["attrs"]?.["inline"] - let figureAttrs = "" - const figureStyles = { - margin: "0", - }; - if(!attrs.includes(`src="${jsonBlock?.["attrs"]?.["asset-link"]}`)){ - attrs = ` src="${jsonBlock?.["attrs"]?.["asset-link"]}"` + attrs; - } - let img = ``; - - if (anchor) { - const target = jsonBlock?.["attrs"]?.["target"]; - let anchorAttrs = `href="${anchor}"`; - if (target) { - anchorAttrs = `${anchorAttrs} target="${target}"`; - } - img = `${img}`; - } - - if (caption || (position && position !== "none")) { - const figcaption = caption - ? `
${caption}
` - : ""; - - if (inline && position !== "right" && position !== "left") { - figureStyles["display"] = "inline-block"; - } - if (position && position !== "none") { - figureStyles[inline ? "float" : "text-align"] = position; - } - - if(figcaption){ - img = `
${img}${figcaption}
`; - } - } - if(!isEmpty(figureStyles)){ - figureAttrs = ` style="${getStyleStringFromObject(figureStyles)}"` - } - if(inline && !caption && (!position ||position==='none')){ - return img - } - return `${img}`; - } - return `${child}` - }, - inlineCode: (attrs: any, child: any) => { - return "" - }, - fragment: (attrs: any, child: any) => { - return child - }, - style: (attrs: any, child: any) => { - return `` - }, - script: (attrs: any, child: any) => { - return `` - }, - "social-embeds": (attrs: any, child: any) => { - return `` - } -} -const TEXT_WRAPPERS: IJsonToHtmlTextTags = { - 'bold': (child: any, value:any) => { - return `${child}`; - }, - 'italic': (child: any, value:any) => { - return `${child}`; - }, - 'underline': (child: any, value:any) => { - return `${child}`; - }, - '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 ALLOWED_EMPTY_ATTRIBUTES: IJsonToHtmlAllowedEmptyAttributes = { - img: ['alt'], - reference: ['alt'] -} +import { HTML_ELEMENT_TYPES, HTML_TEXT_WRAPPERS, ALLOWED_EMPTY_ATTRIBUTES } from './constants' let ADD_NBSP_FOR_EMPTY_BLOCKS : boolean = false @@ -225,13 +12,18 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string if(options?.addNbspForEmptyBlocks){ ADD_NBSP_FOR_EMPTY_BLOCKS = options?.addNbspForEmptyBlocks } + let localTextWrappers: IJsonToHtmlTextTags = HTML_TEXT_WRAPPERS; + let localAllowedEmptyAttributes: IJsonToHtmlAllowedEmptyAttributes = ALLOWED_EMPTY_ATTRIBUTES; + let localElementTypes: IJsonToHtmlElementTags = HTML_ELEMENT_TYPES; + if(options?.customTextWrapper && !isEmpty(options.customTextWrapper)){ - Object.assign(TEXT_WRAPPERS,options.customTextWrapper) + localTextWrappers = { ...localTextWrappers, ...options.customTextWrapper }; } if (options?.allowedEmptyAttributes && !isEmpty(options.allowedEmptyAttributes)) { + localAllowedEmptyAttributes = { ...ALLOWED_EMPTY_ATTRIBUTES }; Object.keys(options.allowedEmptyAttributes).forEach(key => { - ALLOWED_EMPTY_ATTRIBUTES[key] = [ - ...(ALLOWED_EMPTY_ATTRIBUTES[key] ?? []), + localAllowedEmptyAttributes[key] = [ + ...(localAllowedEmptyAttributes[key] ?? []), ...(options.allowedEmptyAttributes?.[key] || []) ]; }); @@ -256,8 +48,8 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string text = text.replace(/\n/g, '
') } Object.entries(jsonValue).forEach(([key, value]) => { - if(TEXT_WRAPPERS.hasOwnProperty(key)){ - text = TEXT_WRAPPERS[key](text,value) + if(localTextWrappers.hasOwnProperty(key)){ + text = localTextWrappers[key](text,value) } }) if (jsonValue['attrs']) { @@ -282,7 +74,7 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string } let children: any = '' if (options?.customElementTypes && !isEmpty(options.customElementTypes)) { - Object.assign(ELEMENT_TYPES, options.customElementTypes) + localElementTypes = { ...localElementTypes, ...options.customElementTypes }; } if (jsonValue.children) { children = Array.from(jsonValue.children).map((child) => toRedactor(child,options)) @@ -296,7 +88,7 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string } children = children.join('') } - if (options?.allowNonStandardTypes && !Object.keys(ELEMENT_TYPES).includes(jsonValue['type']) && jsonValue['type'] !== 'doc') { + if (options?.allowNonStandardTypes && !Object.keys(localElementTypes).includes(jsonValue['type']) && jsonValue['type'] !== 'doc') { let attrs = '' Object.entries(jsonValue?.attrs|| {}).forEach(([key, val]) => { if(isPlainObject(val)){ @@ -311,7 +103,7 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string console.warn(`${jsonValue['type']} is not a valid element type.`) return `<${jsonValue['type'].toLowerCase()}${attrs}>${children}` } - if (ELEMENT_TYPES[jsonValue['type']]) { + if (localElementTypes[jsonValue['type']]) { let attrs = '' let orgType let figureStyles: any = { @@ -530,7 +322,7 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string return; } - if (ALLOWED_EMPTY_ATTRIBUTES.hasOwnProperty(jsonValue['type']) && ALLOWED_EMPTY_ATTRIBUTES[jsonValue['type']].includes(item[0])) { + if (localAllowedEmptyAttributes.hasOwnProperty(jsonValue['type']) && localAllowedEmptyAttributes[jsonValue['type']].includes(item[0])) { attrs += `${item[0]}="${replaceHtmlEntities(item[1])}" `; return; } @@ -595,7 +387,7 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string attrs = (attrs.trim() ? ' ' : '') + attrs.trim() - return ELEMENT_TYPES[orgType || jsonValue['type']]( + return localElementTypes[orgType || jsonValue['type']]( attrs, ADD_NBSP_FOR_EMPTY_BLOCKS && !children ? ' ' : children, jsonValue, diff --git a/test/fromRedactor.test.ts b/test/fromRedactor.test.ts index a145ad7..01b3f0e 100644 --- a/test/fromRedactor.test.ts +++ b/test/fromRedactor.test.ts @@ -1,11 +1,13 @@ // @ts-nocheck -import { ELEMENT_TAGS, fromRedactor, getNestedValueIfAvailable } from "../src/fromRedactor" +import { fromRedactor, getNestedValueIfAvailable } from "../src/fromRedactor" import { JSDOM } from "jsdom" import isEqual from "lodash.isequal" import omitdeep from "omit-deep-lodash" import expectedValue from "./expectedJson" import { IHtmlToJsonOptions } from "../src/types" +import { ELEMENT_TAGS } from "../src/constants" + const docWrapper = (children: any) => { return { "type": "doc",