|
|
#!/usr/bin/env node |
|
|
|
|
|
import { config } from 'dotenv'; |
|
|
import { Client } from '@notionhq/client'; |
|
|
import { NotionConverter } from 'notion-to-md'; |
|
|
import { DefaultExporter } from 'notion-to-md/plugins/exporter'; |
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; |
|
|
import { join, dirname, basename } from 'path'; |
|
|
import { fileURLToPath } from 'url'; |
|
|
import { postProcessMarkdown } from './post-processor.mjs'; |
|
|
import { createCustomCodeRenderer } from './custom-code-renderer.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'); |
|
|
|
|
|
function parseArgs() { |
|
|
const args = process.argv.slice(2); |
|
|
const config = { |
|
|
input: DEFAULT_INPUT, |
|
|
output: DEFAULT_OUTPUT, |
|
|
clean: 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; |
|
|
} |
|
|
} |
|
|
|
|
|
return config; |
|
|
} |
|
|
|
|
|
function ensureDirectory(dir) { |
|
|
if (!existsSync(dir)) { |
|
|
mkdirSync(dir, { recursive: true }); |
|
|
} |
|
|
} |
|
|
|
|
|
function loadPagesConfig(configFile) { |
|
|
if (!existsSync(configFile)) { |
|
|
console.error(`β Configuration file not found: ${configFile}`); |
|
|
console.log('π Create a pages.json file with your Notion page IDs:'); |
|
|
console.log(` |
|
|
{ |
|
|
"pages": [ |
|
|
{ |
|
|
"id": "your-notion-page-id-1", |
|
|
"title": "Page Title 1", |
|
|
"slug": "page-1" |
|
|
}, |
|
|
{ |
|
|
"id": "your-notion-page-id-2", |
|
|
"title": "Page Title 2", |
|
|
"slug": "page-2" |
|
|
} |
|
|
] |
|
|
} |
|
|
`); |
|
|
process.exit(1); |
|
|
} |
|
|
|
|
|
try { |
|
|
const config = JSON.parse(readFileSync(configFile, 'utf8')); |
|
|
return config.pages || []; |
|
|
} catch (error) { |
|
|
console.error(`β Error reading configuration: ${error.message}`); |
|
|
process.exit(1); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function convertNotionPage(notion, pageId, outputDir, pageTitle) { |
|
|
console.log(`π Converting Notion page: ${pageTitle} (${pageId})`); |
|
|
|
|
|
try { |
|
|
|
|
|
const mediaDir = join(outputDir, 'media', pageId); |
|
|
ensureDirectory(mediaDir); |
|
|
|
|
|
|
|
|
const outputFile = join(outputDir, `${pageTitle}.md`); |
|
|
const exporter = new DefaultExporter({ |
|
|
outputType: 'file', |
|
|
outputPath: outputFile, |
|
|
}); |
|
|
|
|
|
|
|
|
const n2m = new NotionConverter(notion) |
|
|
.withExporter(exporter) |
|
|
|
|
|
.downloadMediaTo({ |
|
|
outputDir: mediaDir, |
|
|
|
|
|
transformPath: (localPath) => `/media/${pageId}/${basename(localPath)}`, |
|
|
}); |
|
|
|
|
|
|
|
|
const result = await n2m.convert(pageId); |
|
|
|
|
|
console.log(` β
Converted to: ${outputFile}`); |
|
|
console.log(` π Content length: ${result.content.length} characters`); |
|
|
console.log(` πΌοΈ Media saved to: ${mediaDir}`); |
|
|
|
|
|
return outputFile; |
|
|
|
|
|
} catch (error) { |
|
|
console.error(` β Failed to convert page ${pageId}: ${error.message}`); |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function convertNotionToMarkdown(inputFile, outputDir, notionToken) { |
|
|
console.log('π Notion to Markdown Converter'); |
|
|
console.log(`π Input: ${inputFile}`); |
|
|
console.log(`π Output: ${outputDir}`); |
|
|
|
|
|
|
|
|
if (!notionToken) { |
|
|
console.error('β NOTION_TOKEN not found. Please set it as environment variable or use --token=YOUR_TOKEN'); |
|
|
process.exit(1); |
|
|
} |
|
|
|
|
|
|
|
|
ensureDirectory(outputDir); |
|
|
|
|
|
try { |
|
|
|
|
|
const notion = new Client({ |
|
|
auth: notionToken, |
|
|
}); |
|
|
|
|
|
|
|
|
const pages = loadPagesConfig(inputFile); |
|
|
console.log(`π Found ${pages.length} page(s) to convert`); |
|
|
|
|
|
const convertedFiles = []; |
|
|
|
|
|
|
|
|
for (const page of pages) { |
|
|
try { |
|
|
const outputFile = await convertNotionPage( |
|
|
notion, |
|
|
page.id, |
|
|
outputDir, |
|
|
page.slug || page.title?.toLowerCase().replace(/\s+/g, '-') || page.id |
|
|
); |
|
|
convertedFiles.push(outputFile); |
|
|
} catch (error) { |
|
|
console.error(`β Failed to convert page ${page.id}: ${error.message}`); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
console.log('π§ Post-processing converted files...'); |
|
|
for (const file of convertedFiles) { |
|
|
try { |
|
|
let content = readFileSync(file, 'utf8'); |
|
|
content = postProcessMarkdown(content); |
|
|
writeFileSync(file, content); |
|
|
console.log(` β
Post-processed: ${basename(file)}`); |
|
|
} catch (error) { |
|
|
console.error(` β Failed to post-process ${file}: ${error.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
console.log(`β
Conversion completed! ${convertedFiles.length} file(s) generated`); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('β Conversion failed:', error.message); |
|
|
process.exit(1); |
|
|
} |
|
|
} |
|
|
|
|
|
function main() { |
|
|
const config = parseArgs(); |
|
|
|
|
|
if (config.clean) { |
|
|
console.log('π§Ή Cleaning output directory...'); |
|
|
|
|
|
} |
|
|
|
|
|
convertNotionToMarkdown(config.input, config.output, config.token); |
|
|
console.log('π Notion conversion completed!'); |
|
|
} |
|
|
|
|
|
|
|
|
if (process.argv.includes('--help') || process.argv.includes('-h')) { |
|
|
console.log(` |
|
|
π Notion to Markdown Converter |
|
|
|
|
|
Usage: |
|
|
node notion-converter.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 conversion |
|
|
--help, -h Show this help |
|
|
|
|
|
Environment Variables: |
|
|
NOTION_TOKEN Your Notion integration token |
|
|
|
|
|
Examples: |
|
|
# Basic conversion with environment token |
|
|
NOTION_TOKEN=your_token node notion-converter.mjs |
|
|
|
|
|
# Custom paths and token |
|
|
node notion-converter.mjs --input=my-pages.json --output=converted/ --token=your_token |
|
|
|
|
|
# Clean output first |
|
|
node notion-converter.mjs --clean |
|
|
|
|
|
Configuration File Format (pages.json): |
|
|
{ |
|
|
"pages": [ |
|
|
{ |
|
|
"id": "your-notion-page-id", |
|
|
"title": "Page Title", |
|
|
"slug": "page-slug" |
|
|
} |
|
|
] |
|
|
} |
|
|
`); |
|
|
process.exit(0); |
|
|
} |
|
|
|
|
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) { |
|
|
main(); |
|
|
} |
|
|
|