diff --git a/packages/text-markdown/src/__tests__/markdown.test.ts b/packages/text-markdown/src/__tests__/markdown.test.ts index 0e78d8b5e2..782f5da32a 100644 --- a/packages/text-markdown/src/__tests__/markdown.test.ts +++ b/packages/text-markdown/src/__tests__/markdown.test.ts @@ -36,8 +36,8 @@ expect.extend({ return { message: () => pass - ? `Expected markdown strings NOT to be equal:\n Received: ${received}\n Expected: ${expected}` - : `Expected markdown strings to be equal:\n Received: ${received}\n Expected: ${expected}`, + ? `Expected markdown strings NOT to be equal:\n Received:\n${received}\n Expected:\n${expected}` + : `Expected markdown strings to be equal:\n Received:\n${received}\n Expected:\n${expected}`, pass } } @@ -1033,6 +1033,15 @@ describe('markdownToMarkup -> markupToMarkdown', () => { name: 'Image', markdown: 'image' }, + { + name: 'Images', + markdown: ` +Screenshot 2025-09-11 at 15 42 40 + +Screenshot 2025-09-11 at 15 43 42 + +Screenshot 2025-09-11 at 15 43 50` + }, { name: 'Image with multiline alt', markdown: '![link0\\\n\\\nline1](http://example.com/image.png)' diff --git a/packages/text-markdown/src/compare.ts b/packages/text-markdown/src/compare.ts index 40c2438345..136da3ea13 100644 --- a/packages/text-markdown/src/compare.ts +++ b/packages/text-markdown/src/compare.ts @@ -49,20 +49,71 @@ export function calcSørensenDiceCoefficient (a: string, b: string): number { * Perform markdown diff/comparison to understand do we have a major differences. */ export function isMarkdownsEquals (source1: string, source2: string): boolean { - const normalizeLineEndings = (str: string): string => str.replace(/\r?\n/g, '\n') - - const excludeBlankLines = (str: string): string => - str - .split('\n') - .map((it) => it.trimEnd()) - .filter((it) => it.length > 0) - .join('\n') - - const norm1 = normalizeLineEndings(source1 ?? '') - const lines1 = excludeBlankLines(norm1) - - const norm2 = normalizeLineEndings(source2 ?? '') - const lines2 = excludeBlankLines(norm2) - - return lines1 === lines2 + const normalized1 = normalizeMarkdown(source1) + const normalized2 = normalizeMarkdown(source2) + return normalized1 === normalized2 +} + +export function normalizeMarkdown (source: string): string { + const tagRegex = /<(\w+)([^>]*?)(\/?)>/g + const attrRegex = /(\w+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g + + // Normalize line endings to LF + source = source.replace(/\r?\n/g, '\n') + + // Remove extra blank lines + source = source + .split('\n') + .map((it) => it.trimEnd()) + .filter((it) => it.length > 0) + .join('\n') + + // Normalize HTML tags + source = source.replace(tagRegex, (match, tagName, attributes) => { + const attrs: Record = {} + + let attrMatch = attrRegex.exec(attributes) + while (attrMatch !== null) { + const attrName = attrMatch[1] + const attrValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? '' + attrs[attrName] = attrValue + attrMatch = attrRegex.exec(attributes) + } + + // Sort attributes by name for consistent order + const sortedAttrs = Object.keys(attrs) + .sort() + .map((key) => { + const value = attrs[key] + return value !== '' ? `${key}="${value}"` : key + }) + .join(' ') + + // Normalize to self-closing format for void elements + const voidElements = [ + 'img', + 'br', + 'hr', + 'input', + 'meta', + 'area', + 'base', + 'col', + 'embed', + 'link', + 'param', + 'source', + 'track', + 'wbr' + ] + const isVoidElement = voidElements.includes(tagName.toLowerCase()) + + if (sortedAttrs !== '') { + return isVoidElement ? `<${tagName} ${sortedAttrs} />` : `<${tagName} ${sortedAttrs}>` + } else { + return isVoidElement ? `<${tagName} />` : `<${tagName}>` + } + }) + + return source } diff --git a/packages/text-markdown/src/serializer.ts b/packages/text-markdown/src/serializer.ts index 4d13a7222c..ff9e326ac6 100644 --- a/packages/text-markdown/src/serializer.ts +++ b/packages/text-markdown/src/serializer.ts @@ -204,7 +204,6 @@ export const storeNodes: Record = { ) } else { if (attrs.width != null || attrs.height != null) { - // state.write(`{alt}`) state.write( '