diff --git a/designer/client/src/views/preview-components/declarationfield.njk b/designer/client/src/views/preview-components/declarationfield.njk index 2ad3349c9e..faf4a1217d 100644 --- a/designer/client/src/views/preview-components/declarationfield.njk +++ b/designer/client/src/views/preview-components/declarationfield.njk @@ -13,7 +13,7 @@
- {{ model.declaration.text | markdown | safe }} + {{ model.declaration.text | markdown({ startingHeaderLevel: 2 }) | safe }}
diff --git a/model/src/form/form-editor/preview/markdown.js b/model/src/form/form-editor/preview/markdown.js index ed83d3b57a..e914b0c0d9 100644 --- a/model/src/form/form-editor/preview/markdown.js +++ b/model/src/form/form-editor/preview/markdown.js @@ -31,7 +31,7 @@ export class Markdown extends Content { constructor(htmlElements, questionRenderer) { super(htmlElements, questionRenderer) const { content } = htmlElements.values - this._content = markdownToHtml(content) + this._content = markdownToHtml(content, { startingHeaderLevel: 2 }) } /** @@ -39,7 +39,7 @@ export class Markdown extends Content { * @protected */ _setContent(value) { - super._setContent(markdownToHtml(value)) + super._setContent(markdownToHtml(value, { startingHeaderLevel: 2 })) } } diff --git a/model/src/form/form-editor/preview/markdown.test.js b/model/src/form/form-editor/preview/markdown.test.js index a73385b3b7..4204f7c3f6 100644 --- a/model/src/form/form-editor/preview/markdown.test.js +++ b/model/src/form/form-editor/preview/markdown.test.js @@ -8,18 +8,20 @@ describe('markdown', () => { const questionElements = new ContentElements( buildMarkdownComponent({ title: 'Which quest would you like to pick?', - content: '# This is a heading' + content: '# This is a heading demoted' }) ) describe('Markdown', () => { it('should create class', () => { - expect(questionElements.values.content).toBe('# This is a heading') + expect(questionElements.values.content).toBe( + '# This is a heading demoted' + ) const res = new Markdown(questionElements, renderer) expect(res.renderInput).toEqual({ id: 'markdown', name: 'markdown', classes: '', - content: '

This is a heading

\n' + content: '

This is a heading demoted

\n' }) expect(res.titleText).toBe('Which quest would you like to pick?') expect(res.question).toBe('Which quest would you like to pick?') @@ -40,7 +42,7 @@ describe('markdown', () => { res.optional = true expect(res.titleText).toBe('New question (optional)') res.content = '## This is a subheading' - expect(res.content).toBe('

This is a subheading

\n') + expect(res.content).toBe('

This is a subheading

\n') }) }) }) diff --git a/model/src/utils/markdown.test.js b/model/src/utils/markdown.test.js index 018a217b6c..e8649588fa 100644 --- a/model/src/utils/markdown.test.js +++ b/model/src/utils/markdown.test.js @@ -45,7 +45,7 @@ describe('Helpers', () => { html: '

link

\n' } ])("formats '$markdown' to '$html'", ({ markdown, html }) => { - expect(markdownToHtml(markdown, exampleBaseUrl)).toBe(html) + expect(markdownToHtml(markdown, { baseUrl: exampleBaseUrl })).toBe(html) }) }) @@ -63,4 +63,49 @@ describe('Helpers', () => { expect(markdownToHtml(markdown)).toBe(html) }) }) + + describe('markdown with demotion from speific levels', () => { + it.each([ + { + level: 1, + markdown: + '# This is H1\n## This is H2\n### This is H3\n#### This is H4\n##### This is H5\n###### This is H6\n', + html: '

This is H1

\n

This is H2

\n

This is H3

\n

This is H4

\n
This is H5
\n
This is H6
\n' + }, + { + level: 2, + markdown: + '# This is H1\n## This is H2\n### This is H3\n#### This is H4\n##### This is H5\n###### This is H6\n', + html: '

This is H1

\n

This is H2

\n

This is H3

\n
This is H4
\n
This is H5
\n
This is H6
\n' + }, + { + level: 3, + markdown: + '# This is H1\n## This is H2\n### This is H3\n#### This is H4\n##### This is H5\n###### This is H6\n', + html: '

This is H1

\n

This is H2

\n
This is H3
\n
This is H4
\n
This is H5
\n
This is H6
\n' + }, + { + level: 4, + markdown: + '# This is H1\n## This is H2\n### This is H3\n#### This is H4\n##### This is H5\n###### This is H6\n', + html: '

This is H1

\n
This is H2
\n
This is H3
\n
This is H4
\n
This is H5
\n
This is H6
\n' + }, + { + level: 5, + markdown: + '# This is H1\n## This is H2\n### This is H3\n#### This is H4\n##### This is H5\n###### This is H6\n', + html: '
This is H1
\n
This is H2
\n
This is H3
\n
This is H4
\n
This is H5
\n
This is H6
\n' + }, + { + level: 6, + markdown: + '# This is H1\n## This is H2\n### This is H3\n#### This is H4\n##### This is H5\n###### This is H6\n', + html: '
This is H1
\n
This is H2
\n
This is H3
\n
This is H4
\n
This is H5
\n
This is H6
\n' + } + ])("formats '$markdown' to '$html'", ({ markdown, html, level }) => { + expect(markdownToHtml(markdown, { startingHeaderLevel: level })).toBe( + html + ) + }) + }) }) diff --git a/model/src/utils/markdown.ts b/model/src/utils/markdown.ts index f3962fb53e..febe4cb5d8 100644 --- a/model/src/utils/markdown.ts +++ b/model/src/utils/markdown.ts @@ -26,12 +26,26 @@ function renderLink(href: string, text: string, baseUrl?: string) { return `${label}` } +function demoteHeading( + text: string, + depth: number, + startingHeaderLevel: number +) { + // Max heading is h6 so don't demote further than that + depth = Math.min(depth + startingHeaderLevel - 1, 6) + return `${text} +` +} + /** * Convert markdown to HTML, escaping any HTML tags first */ export function markdownToHtml( markdown: string | null | undefined, - baseUrl?: string // optional in some contexts, e.g. from the designer where it might not make sense + options?: { + baseUrl?: string // optional in some contexts, e.g. from the designer where it might not make sense, + startingHeaderLevel?: number + } ) { if (markdown === undefined || markdown === null) { return '' @@ -46,8 +60,12 @@ export function markdownToHtml( const renderer = new Renderer() renderer.link = ({ href, text }: Tokens.Link): string => { - return renderLink(href, text, baseUrl) + return renderLink(href, text, options?.baseUrl) + } + if (options?.startingHeaderLevel) { + renderer.heading = ({ text, depth }: Tokens.Heading): string => { + return demoteHeading(text, depth, options.startingHeaderLevel ?? 1) + } } - return marked.parse(escaped, { async: false, renderer }) }