|
|
#!/usr/bin/env node |
|
|
|
|
|
import { config } from 'dotenv'; |
|
|
import { join, dirname, basename } from 'path'; |
|
|
import { fileURLToPath } from 'url'; |
|
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs'; |
|
|
import { convertNotionToMarkdown } from './notion-converter.mjs'; |
|
|
import { convertToMdx } from './mdx-converter.mjs'; |
|
|
|
|
|
|
|
|
config(); |
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url); |
|
|
const __dirname = dirname(__filename); |
|
|
|
|
|
|
|
|
const DEFAULT_INPUT = join(__dirname, 'input', 'pages.json'); |
|
|
const DEFAULT_OUTPUT = join(__dirname, 'output'); |
|
|
const ASTRO_CONTENT_PATH = join(__dirname, '..', '..', 'src', 'content', 'article.mdx'); |
|
|
const ASTRO_ASSETS_PATH = join(__dirname, '..', '..', 'src', 'content', 'assets', 'image'); |
|
|
const ASTRO_BIB_PATH = join(__dirname, '..', '..', 'src', 'content', 'bibliography.bib'); |
|
|
|
|
|
function parseArgs() { |
|
|
const args = process.argv.slice(2); |
|
|
const config = { |
|
|
input: DEFAULT_INPUT, |
|
|
output: DEFAULT_OUTPUT, |
|
|
clean: false, |
|
|
notionOnly: false, |
|
|
mdxOnly: false, |
|
|
token: process.env.NOTION_TOKEN |
|
|
}; |
|
|
|
|
|
for (const arg of args) { |
|
|
if (arg.startsWith('--input=')) { |
|
|
config.input = arg.split('=')[1]; |
|
|
} else if (arg.startsWith('--output=')) { |
|
|
config.output = arg.split('=')[1]; |
|
|
} else if (arg.startsWith('--token=')) { |
|
|
config.token = arg.split('=')[1]; |
|
|
} else if (arg === '--clean') { |
|
|
config.clean = true; |
|
|
} else if (arg === '--notion-only') { |
|
|
config.notionOnly = true; |
|
|
} else if (arg === '--mdx-only') { |
|
|
config.mdxOnly = true; |
|
|
} |
|
|
} |
|
|
|
|
|
return config; |
|
|
} |
|
|
|
|
|
function showHelp() { |
|
|
console.log(` |
|
|
π Notion to MDX Toolkit |
|
|
|
|
|
Usage: |
|
|
node index.mjs [options] |
|
|
|
|
|
Options: |
|
|
--input=PATH Input pages configuration file (default: input/pages.json) |
|
|
--output=PATH Output directory (default: output/) |
|
|
--token=TOKEN Notion API token (or set NOTION_TOKEN env var) |
|
|
--clean Clean output directory before processing |
|
|
--notion-only Only convert Notion to Markdown (skip MDX conversion) |
|
|
--mdx-only Only convert existing Markdown to MDX |
|
|
--help, -h Show this help |
|
|
|
|
|
Environment Variables: |
|
|
NOTION_TOKEN Your Notion integration token |
|
|
|
|
|
Examples: |
|
|
# Full conversion workflow |
|
|
NOTION_TOKEN=your_token node index.mjs --clean |
|
|
|
|
|
# Only convert Notion pages to Markdown |
|
|
node index.mjs --notion-only --token=your_token |
|
|
|
|
|
# Only convert existing Markdown to MDX |
|
|
node index.mjs --mdx-only |
|
|
|
|
|
# Custom paths |
|
|
node index.mjs --input=my-pages.json --output=converted/ --token=your_token |
|
|
|
|
|
Configuration File Format (pages.json): |
|
|
{ |
|
|
"pages": [ |
|
|
{ |
|
|
"id": "your-notion-page-id", |
|
|
"title": "Page Title", |
|
|
"slug": "page-slug" |
|
|
} |
|
|
] |
|
|
} |
|
|
|
|
|
Workflow: |
|
|
1. Notion β Markdown (with media download) |
|
|
2. Markdown β MDX (with Astro components) |
|
|
3. Copy to Astro content directory |
|
|
`); |
|
|
} |
|
|
|
|
|
function ensureDirectory(dir) { |
|
|
if (!existsSync(dir)) { |
|
|
mkdirSync(dir, { recursive: true }); |
|
|
} |
|
|
} |
|
|
|
|
|
async function cleanDirectory(dir) { |
|
|
if (existsSync(dir)) { |
|
|
const { execSync } = await import('child_process'); |
|
|
execSync(`rm -rf "${dir}"/*`, { stdio: 'inherit' }); |
|
|
} |
|
|
} |
|
|
|
|
|
function readPagesConfig(inputFile) { |
|
|
try { |
|
|
const content = readFileSync(inputFile, 'utf8'); |
|
|
return JSON.parse(content); |
|
|
} catch (error) { |
|
|
console.error(`β Error reading pages config: ${error.message}`); |
|
|
return { pages: [] }; |
|
|
} |
|
|
} |
|
|
|
|
|
function copyToAstroContent(outputDir) { |
|
|
console.log('π Copying MDX files to Astro content directory...'); |
|
|
|
|
|
try { |
|
|
|
|
|
mkdirSync(dirname(ASTRO_CONTENT_PATH), { recursive: true }); |
|
|
mkdirSync(ASTRO_ASSETS_PATH, { recursive: true }); |
|
|
|
|
|
|
|
|
const files = readdirSync(outputDir); |
|
|
const mdxFiles = files.filter(file => file.endsWith('.mdx')); |
|
|
if (mdxFiles.length > 0) { |
|
|
const mdxFile = join(outputDir, mdxFiles[0]); |
|
|
copyFileSync(mdxFile, ASTRO_CONTENT_PATH); |
|
|
console.log(` β
Copied MDX to ${ASTRO_CONTENT_PATH}`); |
|
|
} |
|
|
|
|
|
|
|
|
const mediaDir = join(outputDir, 'media'); |
|
|
if (existsSync(mediaDir)) { |
|
|
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg']; |
|
|
let imageCount = 0; |
|
|
|
|
|
function copyImagesRecursively(dir) { |
|
|
const files = readdirSync(dir); |
|
|
for (const file of files) { |
|
|
const filePath = join(dir, file); |
|
|
const stat = statSync(filePath); |
|
|
|
|
|
if (stat.isDirectory()) { |
|
|
copyImagesRecursively(filePath); |
|
|
} else if (imageExtensions.some(ext => file.toLowerCase().endsWith(ext))) { |
|
|
const filename = basename(filePath); |
|
|
const destPath = join(ASTRO_ASSETS_PATH, filename); |
|
|
copyFileSync(filePath, destPath); |
|
|
imageCount++; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
copyImagesRecursively(mediaDir); |
|
|
console.log(` β
Copied ${imageCount} image(s) to ${ASTRO_ASSETS_PATH}`); |
|
|
|
|
|
|
|
|
const mdxContent = readFileSync(ASTRO_CONTENT_PATH, 'utf8'); |
|
|
let updatedContent = mdxContent.replace(/\.\/media\//g, './assets/image/'); |
|
|
|
|
|
updatedContent = updatedContent.replace(/\.\/assets\/image\/[^\/]+\//g, './assets/image/'); |
|
|
writeFileSync(ASTRO_CONTENT_PATH, updatedContent); |
|
|
console.log(` β
Updated image paths in MDX file`); |
|
|
} |
|
|
|
|
|
|
|
|
writeFileSync(ASTRO_BIB_PATH, ''); |
|
|
console.log(` β
Created empty bibliography at ${ASTRO_BIB_PATH}`); |
|
|
|
|
|
} catch (error) { |
|
|
console.warn(` β οΈ Failed to copy to Astro: ${error.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function main() { |
|
|
const args = process.argv.slice(2); |
|
|
|
|
|
if (args.includes('--help') || args.includes('-h')) { |
|
|
showHelp(); |
|
|
process.exit(0); |
|
|
} |
|
|
|
|
|
const config = parseArgs(); |
|
|
|
|
|
console.log('π Notion to MDX Toolkit'); |
|
|
console.log('========================'); |
|
|
|
|
|
try { |
|
|
if (config.clean) { |
|
|
console.log('π§Ή Cleaning output directory...'); |
|
|
await cleanDirectory(config.output); |
|
|
} |
|
|
|
|
|
if (config.mdxOnly) { |
|
|
|
|
|
console.log('π MDX conversion only mode'); |
|
|
await convertToMdx(config.output, config.output); |
|
|
copyToAstroContent(config.output); |
|
|
|
|
|
} else if (config.notionOnly) { |
|
|
|
|
|
console.log('π Notion conversion only mode'); |
|
|
await convertNotionToMarkdown(config.input, config.output, config.token); |
|
|
|
|
|
} else { |
|
|
|
|
|
console.log('π Full conversion workflow'); |
|
|
|
|
|
|
|
|
console.log('\nπ Step 1: Converting Notion pages to Markdown...'); |
|
|
await convertNotionToMarkdown(config.input, config.output, config.token); |
|
|
|
|
|
|
|
|
console.log('\nπ Step 2: Converting Markdown to MDX...'); |
|
|
const pagesConfig = readPagesConfig(config.input); |
|
|
const firstPage = pagesConfig.pages && pagesConfig.pages.length > 0 ? pagesConfig.pages[0] : null; |
|
|
const pageId = firstPage ? firstPage.id : null; |
|
|
await convertToMdx(config.output, config.output, pageId, config.token); |
|
|
|
|
|
|
|
|
console.log('\nπ Step 3: Copying to Astro content directory...'); |
|
|
copyToAstroContent(config.output); |
|
|
} |
|
|
|
|
|
console.log('\nπ Conversion completed successfully!'); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('β Error:', error.message); |
|
|
process.exit(1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export { convertNotionToMarkdown, convertToMdx }; |
|
|
|
|
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) { |
|
|
main(); |
|
|
} |
|
|
|