diff --git a/.talismanrc b/.talismanrc index a1a6958..a156b3a 100644 --- a/.talismanrc +++ b/.talismanrc @@ -3,7 +3,7 @@ fileignoreconfig: ignore_detectors: - filecontent - filename: package-lock.json - checksum: 88174adc8b9dcedecf549defe09988aa4ca95940801e923d829123ba2f3ef6f4 + checksum: 46eef8560b6a5f38e7a3b55f74525cf47036763b239e33b2458e435651ef6ac0 - filename: src/entry-editable.ts checksum: 3ba7af9ed1c1adef2e2bd5610099716562bebb8ba750d4b41ddda99fc9eaf115 - filename: .husky/pre-commit diff --git a/__test__/json-to-html.test.ts b/__test__/json-to-html.test.ts index de20842..0bd5494 100644 --- a/__test__/json-to-html.test.ts +++ b/__test__/json-to-html.test.ts @@ -34,7 +34,11 @@ import { testJsonRte, testJsonAsset, embeddedAssetAsLinkJsonEntry, - escapeJsonHtml } from './mock/json-element-mock' + escapeJsonHtml, + breakTestEntry, + newlineBreakTestEntry, + multipleNewlinesBreakTestEntry, + plainNewlineTestEntry } from './mock/json-element-mock' import { blockquoteHtml, codeHtml, @@ -50,19 +54,23 @@ import { orderListHtml, paragraphHtml, paragraphHtmlWithNewLine, - plainTextHtml, - styleinPHtml, - tableHtml, + plainTextHtml, + styleinPHtml, + tableHtml, unorderListHtml, plainTextHtmlWithClass, plainTextHtmlWithId, htmlTextIdInAttrs, classAndIdAttrsHtml, - styleObjHtml, + styleObjHtml, referenceObjHtml, referenceObjHtmlBlock, imagetags, - escapeHtml } from './mock/json-element-mock-result' + escapeHtml, + breakTestHtml, + newlineBreakTestHtml, + multipleNewlinesBreakTestHtml, + plainNewlineTestHtml } from './mock/json-element-mock-result' describe('Node parser paragraph content', () => { it('Should accept proper values', done => { const entry = { uid: 'uid'} @@ -682,4 +690,59 @@ describe('Node parse json_rte Content', () => { expect(entry.json_rte).toEqual(imagetags) done() }) +}) + +describe('Break and Newline handling tests', () => { + it('Should handle break flag in text nodes correctly', done => { + const entry = {...breakTestEntry} + const paths = ['rich_text_editor'] + + jsonToHTML({ entry, paths }) + + expect(entry.rich_text_editor).toEqual(breakTestHtml) + done() + }) + + it('Should handle newline with break flag without duplication', done => { + const entry = {...newlineBreakTestEntry} + const paths = ['rich_text_editor'] + + jsonToHTML({ entry, paths }) + + expect(entry.rich_text_editor).toEqual(newlineBreakTestHtml) + done() + }) + + it('Should handle multiple newlines with break flag correctly', done => { + const entry = {...multipleNewlinesBreakTestEntry} + const paths = ['rich_text_editor'] + + jsonToHTML({ entry, paths }) + + expect(entry.rich_text_editor).toEqual(multipleNewlinesBreakTestHtml) + done() + }) + + it('Should handle plain newlines without break flag', done => { + const entry = {...plainNewlineTestEntry} + const paths = ['rich_text_editor'] + + jsonToHTML({ entry, paths }) + + expect(entry.rich_text_editor).toEqual(plainNewlineTestHtml) + done() + }) + + it('Should handle break flag in arrays', done => { + const entry = { + ...breakTestEntry, + rich_text_editor: [breakTestEntry.rich_text_editor] + } + const paths = ['rich_text_editor'] + + jsonToHTML({ entry, paths }) + + expect(entry.rich_text_editor).toEqual([breakTestHtml]) + done() + }) }) \ No newline at end of file diff --git a/__test__/mock/json-element-mock-result.ts b/__test__/mock/json-element-mock-result.ts index b8aec13..635c955 100644 --- a/__test__/mock/json-element-mock-result.ts +++ b/__test__/mock/json-element-mock-result.ts @@ -25,6 +25,10 @@ const referenceObjHtml = "

Embed entry as a link

Embed entry as a link open in new tab

" const imagetags = "
\"batman\"
The Batman
" const escapeHtml = "

<p>Welcome to Contentstack! <script>console.log(/"Hello from Contentstack!/");</script> Explore our platform to create, manage, and publish content seamlessly.</p>

" +const breakTestHtml = "

Normal text with
break tag after break.

" +const newlineBreakTestHtml = "

Text before newline break
Text after newline break

" +const multipleNewlinesBreakTestHtml = "

Text before


Text after

" +const plainNewlineTestHtml = "

Line 1
Line 2
Line 3

" export { h1Html, @@ -53,5 +57,9 @@ export { referenceObjHtmlBlock, imagetags, paragraphHtmlWithNewLine, - escapeHtml + escapeHtml, + breakTestHtml, + newlineBreakTestHtml, + multipleNewlinesBreakTestHtml, + plainNewlineTestHtml } \ No newline at end of file diff --git a/__test__/mock/json-element-mock.ts b/__test__/mock/json-element-mock.ts index 9a0511a..beb00b6 100644 --- a/__test__/mock/json-element-mock.ts +++ b/__test__/mock/json-element-mock.ts @@ -2421,6 +2421,139 @@ const escapeJsonHtml = { uid: 'asset_uid_10', } +const breakTestJson = { + uid: "break_test_uid", + _version: 1, + attrs: {}, + children: [ + { + type: "p", + attrs: {}, + uid: "break_paragraph_uid", + children: [ + { + text: "Normal text with ", + }, + { + text: "break tag", + break: true + }, + { + text: " after break." + } + ] + } + ], + type: "doc" +} + +const newlineBreakTestJson = { + uid: "newline_break_test_uid", + _version: 1, + attrs: {}, + children: [ + { + type: "p", + attrs: {}, + uid: "newline_break_paragraph_uid", + children: [ + { + text: "Text before newline break", + }, + { + text: "\n", + break: true + }, + { + text: "Text after newline break" + } + ] + } + ], + type: "doc" +} + +const multipleNewlinesBreakTestJson = { + uid: "multiple_newlines_break_test_uid", + _version: 1, + attrs: {}, + children: [ + { + type: "p", + attrs: {}, + uid: "multiple_newlines_paragraph_uid", + children: [ + { + text: "Text before", + }, + { + text: "\n\n\n", + break: true + }, + { + text: "Text after" + } + ] + } + ], + type: "doc" +} + +const plainNewlineTestJson = { + uid: "plain_newline_test_uid", + _version: 1, + attrs: {}, + children: [ + { + type: "p", + attrs: {}, + uid: "plain_newline_paragraph_uid", + children: [ + { + text: "Line 1\nLine 2\nLine 3" + } + ] + } + ], + type: "doc" +} + +const breakTestEntry = { + title: 'Break Test Entry', + url: '/break-test-entry', + rich_text_editor: {...breakTestJson}, + locale: 'en-us', + _in_progress: false, + uid: 'break_test_entry_uid', +} + +const newlineBreakTestEntry = { + title: 'Newline Break Test Entry', + url: '/newline-break-test-entry', + rich_text_editor: {...newlineBreakTestJson}, + locale: 'en-us', + _in_progress: false, + uid: 'newline_break_test_entry_uid', +} + +const multipleNewlinesBreakTestEntry = { + title: 'Multiple Newlines Break Test Entry', + url: '/multiple-newlines-break-test-entry', + rich_text_editor: {...multipleNewlinesBreakTestJson}, + locale: 'en-us', + _in_progress: false, + uid: 'multiple_newlines_break_test_entry_uid', +} + +const plainNewlineTestEntry = { + title: 'Plain Newline Test Entry', + url: '/plain-newline-test-entry', + rich_text_editor: {...plainNewlineTestJson}, + locale: 'en-us', + _in_progress: false, + uid: 'plain_newline_test_entry_uid', +} + export { h1Json, h2Json, @@ -2460,5 +2593,13 @@ export { testJsonRte, testJsonAsset, paragraphEntryWithNewline, - escapeJsonHtml + escapeJsonHtml, + breakTestJson, + newlineBreakTestJson, + multipleNewlinesBreakTestJson, + plainNewlineTestJson, + breakTestEntry, + newlineBreakTestEntry, + multipleNewlinesBreakTestEntry, + plainNewlineTestEntry } \ No newline at end of file diff --git a/__test__/text-node-to-html.test.ts b/__test__/text-node-to-html.test.ts index e7ac35d..876df5a 100644 --- a/__test__/text-node-to-html.test.ts +++ b/__test__/text-node-to-html.test.ts @@ -122,4 +122,94 @@ describe('Text Node To HTML', () => { expect(resultHtml).toEqual(`${textNode.text}`) done() }) + + it('Should return Break string text', done => { + const node = { + ...textNode, + break: true + } + + const resultHtml = textNodeToHTML(node, { + ...defaultNodeOption + }) + + expect(resultHtml).toEqual(`
${textNode.text}`) + done() + }) + + it('Should handle newline character without break flag', done => { + const node = { + ...textNode, + text: "line1\nline2" + } + + const resultHtml = textNodeToHTML(node, { + ...defaultNodeOption + }) + + expect(resultHtml).toEqual('line1
line2') + done() + }) + + it('Should handle single newline with break flag without duplication', done => { + const node = { + ...textNode, + text: "\n", + break: true + } + + const resultHtml = textNodeToHTML(node, { + ...defaultNodeOption + }) + + expect(resultHtml).toEqual('
') + done() + }) + + it('Should handle multiple newlines with break flag without duplication', done => { + const node = { + ...textNode, + text: "\n\n\n", + break: true + } + + const resultHtml = textNodeToHTML(node, { + ...defaultNodeOption + }) + + expect(resultHtml).toEqual('


') + done() + }) + + it('Should handle text with newline and break flag properly', done => { + const node = { + ...textNode, + text: "text with\nnewline", + break: true + } + + const resultHtml = textNodeToHTML(node, { + ...defaultNodeOption + }) + + expect(resultHtml).toEqual('
text with
newline') + done() + }) + + it('Should handle break with other marks', done => { + const node = { + ...textNode, + text: "break text", + break: true, + bold: true, + italic: true + } + + const resultHtml = textNodeToHTML(node, { + ...defaultNodeOption + }) + + expect(resultHtml).toEqual(`
${node.text}
`) + done() + }) }) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3200810..ac565c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/utils", - "version": "1.4.3", + "version": "1.4.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/utils", - "version": "1.4.3", + "version": "1.4.4", "license": "MIT", "devDependencies": { "@babel/preset-env": "^7.26.0", @@ -3937,7 +3937,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -5392,7 +5394,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5499,13 +5503,16 @@ "license": "ISC" }, "node_modules/form-data": { - "version": "4.0.2", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { diff --git a/package.json b/package.json index b54cbae..d9874da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/utils", - "version": "1.4.3", + "version": "1.4.4", "description": "Contentstack utilities for Javascript", "main": "dist/index.es.js", "types": "dist/types/index.d.ts", diff --git a/src/helper/enumerate-entries.ts b/src/helper/enumerate-entries.ts index ba688f9..122b135 100644 --- a/src/helper/enumerate-entries.ts +++ b/src/helper/enumerate-entries.ts @@ -43,33 +43,53 @@ export function enumerateContents( export function textNodeToHTML(node: TextNode, renderOption: RenderOption): string { let text = replaceHtmlEntities(node.text); + + // Convert newlines to
tags if there are no other marks + // This ensures newlines are always handled consistently + let hasMarks = false; + if (node.classname || node.id) { text = (renderOption[MarkType.CLASSNAME_OR_ID] as RenderMark)(text, node.classname, node.id); + hasMarks = true; } if (node.break) { text = (renderOption[MarkType.BREAK] as RenderMark)(text); + hasMarks = true; } if (node.superscript) { text = (renderOption[MarkType.SUPERSCRIPT] as RenderMark)(text); + hasMarks = true; } if (node.subscript) { text = (renderOption[MarkType.SUBSCRIPT] as RenderMark)(text); + hasMarks = true; } if (node.inlineCode) { text = (renderOption[MarkType.INLINE_CODE] as RenderMark)(text); + hasMarks = true; } if (node.strikethrough) { text = (renderOption[MarkType.STRIKE_THROUGH] as RenderMark)(text); + hasMarks = true; } if (node.underline) { text = (renderOption[MarkType.UNDERLINE] as RenderMark)(text); + hasMarks = true; } if (node.italic) { text = (renderOption[MarkType.ITALIC] as RenderMark)(text); + hasMarks = true; } if (node.bold) { text = (renderOption[MarkType.BOLD] as RenderMark)(text); + hasMarks = true; } + + // If no marks were applied, but text contains newlines, convert them to
+ if (!hasMarks && text.includes('\n')) { + text = text.replace(/\n/g, '
'); + } + return text; } export function referenceToHTML( diff --git a/src/options/default-node-options.ts b/src/options/default-node-options.ts index 27afbe5..3f19f37 100644 --- a/src/options/default-node-options.ts +++ b/src/options/default-node-options.ts @@ -179,6 +179,12 @@ export const defaultNodeOption: RenderOption = { return `${sanitizeHTML(text)}` }, [MarkType.BREAK]:(text: string) => { + // Check if text is only newlines (which will be converted to
by sanitizeHTML) + // If so, don't add an extra
to avoid duplication + const onlyNewlines = /^\n+$/.test(text); + if (onlyNewlines) { + return sanitizeHTML(text); + } return `
${sanitizeHTML(text)}` }, [MarkType.CLASSNAME_OR_ID]:(text: string, classname: string, id:string) => {