// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
// SPDX-License-Identifier: Apache-2.0
/* global console */
/* global process */
import { copyFileSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { basename, join } from 'node:path';
const allowList = [
'index.md',
// Top pages from Google Analytics
'core.md',
'core.medplumclient.md',
'core.medplumrequestoptions.md',
'core.medplumclient.executebatch.md',
'core.medplumclient.search.md',
'core.medplumclient.searchresources.md',
'core.hl7message.md',
'core.medplumclient.readpatienteverything.md',
'core.botevent.md',
'core.clientstorage.md',
// Sidebar links
'core.createpdffunction.md',
'core.mailaddress.md',
'core.mailattachment.md',
'core.mailoptions.md',
// Other linked pages
'core.medplumclient.createresourceifnoneexist.md',
'core.medplumclient.readhistory.md',
'core.medplumclient.sendemail.md',
'core.medplumclient.startclientlogin.md',
'core.medplumclient.signinwithexternalauth.md',
'core.medplumclient.processcode.md',
'core.medplumclient.startgooglelogin.md',
'core.medplumclient.startlogin.md',
'core.medplumclient.signinwithredirect.md',
'core.medplumclient.exchangeexternalaccesstoken.md',
'core.getquestionnaireanswers.md',
'core.medplumclient.createresource.md',
'core.botevent.secrets.md',
'core.medplumclient.createpdf.md',
'core.findobservationinterval.md',
'core.findobservationreferencerange.md',
'core.matchesrange.md',
'core.validateresource.md',
'core.medplumclient.setbasicauth.md',
'core.getreferencestring.md',
'core.medplumclient.searchresourcepages.md',
'core.medplumclient.getprofile.md',
];
function copyDir(sourceDir, targetDir) {
const files = readdirSync(sourceDir, { withFileTypes: true });
mkdirSync(targetDir, { recursive: true });
for (const file of files) {
const sourceFilePath = join(sourceDir, file.name);
const targetFilePath = join(targetDir, file.name);
if (file.isDirectory()) {
copyDir(sourceFilePath, targetFilePath);
} else {
copyFile(sourceFilePath, targetFilePath);
}
}
}
function copyFile(sourceFile, targetFile) {
if (!allowList.includes(basename(sourceFile))) {
return;
}
if (sourceFile.endsWith('.md')) {
writeFileSync(targetFile.replace('.md', '.mdx'), escapeMdx(sourceFile, readFileSync(sourceFile, 'utf8')));
} else {
copyFileSync(sourceFile, targetFile);
}
}
function escapeMdx(fileName, text) {
text = text
.replace('<!-- Do not edit this file. It is automatically generated by API Documenter. -->', '')
.trimStart()
.replaceAll('.md)', ')');
if (basename(fileName) === 'index.md') {
// In Docusaurus, the index.mdx file is used as the landing page for the folder.
// Relative links are relative to the parent, not the index.mdx file.
text = text.replaceAll('](./index)', '](../)').replaceAll('](./', '](./sdk/');
} else {
text = text.replaceAll('[Home](./index)', '[Home](./)');
}
// Remove links to any relative .md files not in the allow list
// Example: [TypedEventTarget](./core.typedeventtarget)
text = text.replaceAll(/\[([^\]]+)\]\(\.\/([^)]+)\)/g, (match, p1, p2) => {
if (allowList.includes(p2 + '.md')) {
return `[${p1}](./${p2})`;
} else {
return p1;
}
});
// Escape { and } characters outside of code blocks
const specialChars = ['{', '}'];
let inSingleBacktick = false;
let inTripleBacktick = false;
let backtickCount = 0;
let result = '';
for (let i = 0; i < text.length; i++) {
const char = text[i];
// Check for backtick
if (char === '`') {
result += char;
backtickCount++;
if (backtickCount === 3) {
inTripleBacktick = !inTripleBacktick;
backtickCount = 0;
} else if (backtickCount === 1 && i < text.length - 1 && text[i + 1] !== '`') {
inSingleBacktick = !inSingleBacktick;
backtickCount = 0;
}
} else {
if (backtickCount > 0 && backtickCount < 3) {
// Add missed backticks to result
result += '`'.repeat(backtickCount);
backtickCount = 0;
}
if (!inSingleBacktick && !inTripleBacktick && specialChars.includes(char)) {
// Escape character if not in backtick block
result += '\\' + char;
} else {
// Add character as is
result += char;
}
}
}
// Append any remaining backticks
if (backtickCount > 0) {
result += '`'.repeat(backtickCount);
}
return result;
}
if (import.meta.main) {
if (process.argv.length < 4) {
console.log('Usage: node markdown-to-mdx.mjs <sourceDir> <targetDir>');
process.exit(1);
}
const [_node, _script, source, target] = process.argv;
try {
copyDir(source, target);
} catch (error) {
console.error('Error processing files:', error);
}
}