/**
* External dependencies
*/
import MarkdownIt from 'markdown-it';
/**
* Types
*/
import type { Options } from 'markdown-it';
export type Fix = 'list' | 'paragraph' | 'listItem' | 'table';
const addListComments = ( content: string ) => {
return (
content
// First remove any existing Gutenberg comments to avoid duplicates
.replaceAll( '', '' )
.replaceAll( '', '' )
.replaceAll( '', '' )
.replaceAll( '', '' )
// Add Gutenberg comments to
tags
.replaceAll( '', '' )
.replaceAll( '', '' )
// Add Gutenberg comments to tags
.replaceAll( '', '' )
.replaceAll( '
', '
' )
// Add Gutenberg comments to tags
.replaceAll( '' )
);
};
type Fixes = {
[ key in Fix ]: (
content: string,
extension?: boolean,
options?: { [ key: string ]: unknown }
) => string;
};
export const fixes: Fixes = {
list: ( content: string, extension = false ) => {
// Fix list indentation
const fixedIndentation = content
.replace( /- \s+
/g, '
- ' )
.replace( /<\/p>\s+<\/li>/g, '
' );
return extension ? addListComments( fixedIndentation ) : fixedIndentation;
},
listItem: ( content: string, extension = false ) => {
if ( ! extension ) {
return content;
}
return addListComments(
content
// Remove wrapping or tag
.replace( /^<[ou]l>\s*/g, '' )
.replace( /\s*<\/[ou]l>\s*$/g, '' )
);
},
paragraph: ( content: string, extension = false ) => {
if ( ! extension ) {
return content;
}
// Fix encoding of
tags and trim surrounding whitespace.
// Uses split/join instead of regex to avoid polynomial ReDoS on long whitespace runs.
return content
.split( '<br />' )
.map( ( s, i, arr ) => {
if ( i < arr.length - 1 ) s = s.trimEnd();
if ( i > 0 ) s = s.trimStart();
return s;
} )
.join( '
' );
},
table: ( content: string, extension = false, { hasFixedLayout = false } ) => {
if ( ! extension ) {
return content;
}
if ( content.startsWith( '${ content }`;
},
};
const defaultMarkdownItOptions: Options = {
breaks: true,
};
// The rules used by the AI Assistant block
const assistantBlockRules: Array< Fix > = [ 'list' ];
export default class MarkdownToHTML {
markdownConverter: MarkdownIt;
constructor( options: Options = defaultMarkdownItOptions ) {
this.markdownConverter = new MarkdownIt( options );
}
/**
* Renders HTML from Markdown content with specified processing rules.
* @param {object} options - The options to use when rendering the HTML content
* @param {string} options.content - The Markdown content to render
* @param {string} options.rules - The rules to apply to the rendered content
* @param {boolean} options.extension - Whether to apply the extension-specific rules
* @return {string} The rendered HTML content
*/
render( {
content,
rules = assistantBlockRules,
extension = false,
}: {
content: string;
rules: Array< Fix >;
extension?: boolean;
} ): string {
const rendered = this.markdownConverter.render( content );
return rules.reduce( ( renderedContent, rule ) => {
return fixes[ rule ]( renderedContent, extension );
}, rendered );
}
}