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,