Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions data/onPostBuild/transpileMdxToMarkdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
126 changes: 60 additions & 66 deletions data/onPostBuild/transpileMdxToMarkdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
);
}

/**
Expand Down Expand Up @@ -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})`;
});
}

/**
Expand All @@ -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');
}

/**
Expand Down Expand Up @@ -390,9 +392,7 @@ export const onPostBuild: GatsbyNode['onPostBuild'] = async ({ graphql, reporter
const { data, errors } = await graphql<MdxQueryResult>(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;
}

Expand Down Expand Up @@ -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`);
}
};

Expand Down