diff --git a/data/onPostBuild/transpileMdxToMarkdown.test.ts b/data/onPostBuild/transpileMdxToMarkdown.test.ts index 906a1da7e6..8c29e8c1ed 100644 --- a/data/onPostBuild/transpileMdxToMarkdown.test.ts +++ b/data/onPostBuild/transpileMdxToMarkdown.test.ts @@ -172,6 +172,45 @@ describe('MDX to Markdown Transpilation', () => { expect(output).toContain(`${githubBase}/images/a.png`); expect(output).toContain(`${githubBase}/images/b.png`); }); + + it('should handle escaped brackets in alt text', () => { + const input = '![Alt \\[with brackets\\]](images/test.png)'; + const output = convertImagePathsToGitHub(input); + expect(output).toBe(`![Alt \\[with brackets\\]](${githubBase}/images/test.png)`); + }); + + it('should handle images with title attributes', () => { + const input = '![Alt text](images/test.png "Image title")'; + const output = convertImagePathsToGitHub(input); + expect(output).toBe(`![Alt text](${githubBase}/images/test.png)`); + }); + + it('should only convert paths with valid image extensions', () => { + const input = '![Link to folder](images/folder) should not convert'; + const output = convertImagePathsToGitHub(input); + expect(output).toBe(input); // Should remain unchanged + }); + + it('should handle all supported image formats', () => { + const formats = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'tiff', 'ico']; + formats.forEach((ext) => { + const input = `![Test](images/test.${ext})`; + const output = convertImagePathsToGitHub(input); + expect(output).toBe(`![Test](${githubBase}/images/test.${ext})`); + }); + }); + + it('should not convert non-image paths containing "images"', () => { + const input = '[Link to images folder](/images/readme.txt)'; + const output = convertImagePathsToGitHub(input); + expect(output).toBe(input); // Should remain unchanged (not an image tag) + }); + + it('should handle parentheses in image paths', () => { + const input = '![Alt](images/diagram(v2).png)'; + const output = convertImagePathsToGitHub(input); + expect(output).toBe(`![Alt](${githubBase}/images/diagram(v2).png)`); + }); }); describe('convertRelativeUrls', () => { diff --git a/data/onPostBuild/transpileMdxToMarkdown.ts b/data/onPostBuild/transpileMdxToMarkdown.ts index 9eefbd880d..7962c2c2eb 100644 --- a/data/onPostBuild/transpileMdxToMarkdown.ts +++ b/data/onPostBuild/transpileMdxToMarkdown.ts @@ -37,16 +37,18 @@ interface FrontMatterAttributes { * Handles both single-line and multi-line statements */ function removeImportExportStatements(content: string): string { - return content - // Remove import statements (single and multi-line) - .replace(/^import\s+[\s\S]*?from\s+['"][^'"]+['"];?\s*$/gm, '') - .replace(/^import\s+['"][^'"]+['"];?\s*$/gm, '') - // Remove export statements (single and multi-line) - .replace(/^export\s+\{[\s\S]*?\}\s*;?\s*$/gm, '') - .replace(/^export\s+\{[\s\S]*?\}\s+from\s+['"][^'"]+['"];?\s*$/gm, '') - .replace(/^export\s+(default|const|let|var|function|class)\s+.*$/gm, '') - // Clean up extra blank lines left behind - .replace(/\n\n\n+/g, '\n\n'); + return ( + content + // Remove import statements (single and multi-line) + .replace(/^import\s+[\s\S]*?from\s+['"][^'"]+['"];?\s*$/gm, '') + .replace(/^import\s+['"][^'"]+['"];?\s*$/gm, '') + // Remove export statements (single and multi-line) + .replace(/^export\s+\{[\s\S]*?\}\s*;?\s*$/gm, '') + .replace(/^export\s+\{[\s\S]*?\}\s+from\s+['"][^'"]+['"];?\s*$/gm, '') + .replace(/^export\s+(default|const|let|var|function|class)\s+.*$/gm, '') + // Clean up extra blank lines left behind + .replace(/\n\n\n+/g, '\n\n') + ); } /** @@ -200,28 +202,33 @@ function removeJsxComments(content: string): string { function convertImagePathsToGitHub(content: string): string { const githubBaseUrl = 'https://raw.githubusercontent.com/ably/docs/main/src'; - return content - // Handle relative paths: ../../../images/... - .replace( - /!\[([^\]]*)\]\(((?:\.\.\/)+)(images\/[^)]+)\)/g, - (match, altText, relativePath, imagePath) => { - return `![${altText}](${githubBaseUrl}/${imagePath})`; - } - ) - // Handle absolute paths: /images/... - .replace( - /!\[([^\]]*)\]\(\/(images\/[^)]+)\)/g, - (match, altText, imagePath) => { - return `![${altText}](${githubBaseUrl}/${imagePath})`; - } - ) - // Handle direct paths: images/... (no prefix) - .replace( - /!\[([^\]]*)\]\((images\/[^)]+)\)/g, - (match, altText, imagePath) => { - return `![${altText}](${githubBaseUrl}/${imagePath})`; - } - ); + // Supported image extensions + const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'tiff', 'ico']; + const imageExtPattern = imageExtensions.join('|'); + + // Regex to match Markdown images with images/ path and valid image extension + // Pattern breakdown: + // - !\[ - Image markdown syntax start + // - ((?:[^\]\\]|\\.)*) - Alt text: matches any char except unescaped ], allows escaped chars + // - \]\( - End alt text, start URL + // - ((?:\.\.\/)+)? - Optional: one or more ../ patterns (relative paths) + // - \/? - Optional: leading slash (absolute paths) + // - (images\/ - Required: images/ directory + // - (?:[^)\s]|\([^)]*\))+? - Path: allows any char except ) and whitespace, or matched parentheses + // - \.(?:${imageExtPattern}) - Required: file extension from allowed list + // - ) + // - (?:\s+"[^"]*")? - Optional: title attribute in quotes + // - \) - Close markdown image syntax + const imageRegex = new RegExp( + String.raw`!\[((?:[^\]\\]|\\.)*)\]\(((?:\.\.\/)+)?\/?(images\/(?:[^)\s]|\([^)]*\))+?\.(?:${imageExtPattern}))(?:\s+"[^"]*")?\)`, + 'g', + ); + + return content.replace(imageRegex, (match, altText, relativePath, imagePath) => { + // Remove any leading slash from imagePath (since githubBaseUrl already ends with /src) + const cleanImagePath = imagePath.replace(/^\/+/, ''); + return `![${altText}](${githubBaseUrl}/${cleanImagePath})`; + }); } /** @@ -234,37 +241,32 @@ function convertRelativeUrls(content: string, siteUrl: string): string { // Match markdown links: [text](url) // Only convert URLs that start with / (relative) and are not external URLs or hash-only - return content.replace( - /\[([^\]]+)\]\(([^)]+)\)/g, - (match, linkText, url) => { - // Don't convert external URLs - if (url.startsWith('http://') || url.startsWith('https://')) { - return match; - } - - // Don't convert hash-only anchors - if (url.startsWith('#')) { - return match; - } - - // Convert relative URLs (starting with /) - if (url.startsWith('/')) { - return `[${linkText}](${baseUrl}${url})`; - } + return content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => { + // Don't convert external URLs + if (url.startsWith('http://') || url.startsWith('https://')) { + return match; + } - // Keep other URLs as-is (relative paths without leading /) + // Don't convert hash-only anchors + if (url.startsWith('#')) { return match; } - ); + + // Convert relative URLs (starting with /) + if (url.startsWith('/')) { + return `[${linkText}](${baseUrl}${url})`; + } + + // Keep other URLs as-is (relative paths without leading /) + return match; + }); } /** * Replace template variables with readable placeholders */ function replaceTemplateVariables(content: string): string { - return content - .replace(/{{API_KEY}}/g, 'your-api-key') - .replace(/{{RANDOM_CHANNEL_NAME}}/g, 'your-channel-name'); + return content.replace(/{{API_KEY}}/g, 'your-api-key').replace(/{{RANDOM_CHANNEL_NAME}}/g, 'your-channel-name'); } /** @@ -390,9 +392,7 @@ export const onPostBuild: GatsbyNode['onPostBuild'] = async ({ graphql, reporter const { data, errors } = await graphql(query); if (errors) { - reporter.panicOnBuild( - `${REPORTER_PREFIX} Error running GraphQL query: ${JSON.stringify(errors)}` - ); + reporter.panicOnBuild(`${REPORTER_PREFIX} Error running GraphQL query: ${JSON.stringify(errors)}`); return; } @@ -421,22 +421,16 @@ export const onPostBuild: GatsbyNode['onPostBuild'] = async ({ graphql, reporter successCount++; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - reporter.warn( - `${REPORTER_PREFIX} Failed to transpile ${node.internal.contentFilePath}: ${errorMessage}` - ); + reporter.warn(`${REPORTER_PREFIX} Failed to transpile ${node.internal.contentFilePath}: ${errorMessage}`); failureCount++; } } // Report summary if (failureCount > 0) { - reporter.warn( - `${REPORTER_PREFIX} Transpiled ${successCount} files, ${failureCount} failed` - ); + reporter.warn(`${REPORTER_PREFIX} Transpiled ${successCount} files, ${failureCount} failed`); } else { - reporter.info( - `${REPORTER_PREFIX} Successfully transpiled ${successCount} MDX files to Markdown` - ); + reporter.info(`${REPORTER_PREFIX} Successfully transpiled ${successCount} MDX files to Markdown`); } };