DEV Community

Cover image for Building a PDF-to-Markdown Converter Using Cardinal API
Frank Joseph
Frank Joseph

Posted on

Building a PDF-to-Markdown Converter Using Cardinal API

Document processing involves the processes or methods and technologies used to capture, digitize, extract, and transform information from both physical and digital documents into structured, machine-readable data.

Document processing is an everyday activity for startups. Depending on the workflow and the volume of the document, processing documents could be tedious and time-consuming tasks. To successfully convert a document from one form to another while preserving its structure requires thoughtful planning and the right document-processing solution. In this article, we'll explore a state of the art AI powered document processing software; Cardinal API, look at some of the processing tasks that can be performed with Cardinal API and show a demo solution using the Cardinal API to build a PDF to Markdown converter.

What is Cardinal API?

According to Y Combinator Cardinal is the most accurate document processing API for Healthcare.

Y Combinator LinkedIn Post

Cardinal API in simple terms gives team the ability to transform any document into structured data. At the time of writing this, here are the list of accepted files types Cardinal API currently process:

  • .pdf
  • .jpeg/jpg
  • .png

πŸ’‘ While Cardinal does not natively support Excel (.xls / .xlsx) or CSV (.csv) files, you can convert them to PDF on your own and process them through Cardinal.

The following formats are not yet supported but are planned for future releases:

  • .doc / .docx β€” Microsoft Word
  • .tiff β€” Tagged Image File Format
  • .odt / .ott β€” OpenDocument Text & Template
  • .rtf β€” Rich Text Format
  • .txt β€” Plain Text
  • .html / .htm β€” HTML Document
  • .xml β€” XML Document
  • .wps β€” Microsoft Works Word Processor
  • .wpd β€” WordPerfect Document
  • .ods / .ots β€” OpenDocument Spreadsheet & Template
  • .ppt / .pptx β€” Microsoft PowerPoint
  • .odp / .otp β€” OpenDocument Presentation & Template

Use cases for Cardinal API

Here are some use cases for Cardinal API:

How to build A PDF to Markdown Converter using Cardinal API

To use the Cardinal API, you must:

  • Create an account
  • Have an API key

To do the above, visit this link
Login with your Gmail or with your company email

Login Page
After you done are one with the steps above, you'll be rerouted to your Cardinal dashboard

Cardinal Dashboard

At this point, you can request a free Cardinal API key. The free API key gives you access to 50 free requests. That's a lot to get started.
To request a free API key, type your preferred API key name in the text area as shown in your dashboard above and click on the Add Key button.

However, if you want to take things a step further you can subscribe to a plan. Click here to see the pricing.

Now that we cleared all the prerequisites, let's build our PDF to Markdown Converter. Here is a live version of our application

  • Step 1: Create a HTML file and write the following markup in it.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cardinal API - Secure PDF/Image to Markdown Converter</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
 <div class="container">
        <div class="header">
            <h1>πŸ” Secure Cardinal API Converter</h1>
            <p>Convert PDFs and images to clean Markdown with secure API key handling</p>
        </div>

        <!-- Security Notice -->
        <div class="security-notice">
            <h4>πŸ›‘οΈ Security First</h4>
            <p>Your API key is stored securely in your browser and never transmitted to any third parties. It's only sent directly to Cardinal's API for conversions.</p>
        </div>

        <!-- API Key Setup -->
        <div class="api-key-setup">
            <h2 style="margin-bottom: 15px;">πŸ”‘ API Key Configuration</h2>
            <p style="color: #666; margin-bottom: 15px;">
                Enter your Cardinal API key to get started. You can get one from 
                <a href="https://dashboard.trycardinal.ai" target="_blank" style="color: #667eea;">Cardinal Dashboard</a>.
            </p>

            <div class="api-key-input">
                <input 
                    type="password" 
                    id="apiKeyInput" 
                    class="api-key-field" 
                    placeholder="Enter your Cardinal API key..."
                    autocomplete="off"
                >
                <button type="button" class="api-key-toggle" id="toggleApiKey">Show</button>
                <button type="button" class="api-key-save" id="saveApiKey">Validate & Save</button>
            </div>

            <div style="display: flex; align-items: center; gap: 10px; margin-top: 10px;">
                <input type="checkbox" id="rememberApiKey">
                <label for="rememberApiKey" style="font-size: 0.9rem; color: #666;">
                    Remember API key (stored locally in your browser)
                </label>
            </div>

            <div id="apiStatus" class="api-status" style="display: none;"></div>

            <div style="margin-top: 15px; font-size: 0.8rem; color: #888;">
                <p>πŸ’‘ <strong>Tips:</strong></p>
                <ul style="margin-left: 20px; margin-top: 5px;">
                    <li>API key is encrypted and stored only in your browser</li>
                    <li>You can clear stored keys anytime using the "Clear Keys" button</li>
                    <li>Keys are automatically validated before use</li>
                </ul>
            </div>

            <div style="margin-top: 15px;">
                <button type="button" id="clearApiKeys" style="background: #dc3545; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 0.9rem;">
                    πŸ—‘οΈ Clear Stored Keys
                </button>
            </div>
        </div>

        <!-- Main Content (Initially Disabled) -->
        <div id="mainContent" class="content-disabled">
            <!-- Options Section -->
            <div class="options-section">
                <h2 class="section-title">βš™οΈ Conversion Options</h2>
                <div class="options-grid">
                    <div class="option-item">
                        <input type="checkbox" id="markdown" checked>
                        <label for="markdown">Enable Markdown output</label>
                    </div>
                    <div class="option-item">
                        <input type="checkbox" id="denseTables" checked>
                        <label for="denseTables">Dense table formatting</label>
                    </div>
                    <div class="option-item">
                        <input type="checkbox" id="preserveLayout" checked>
                        <label for="preserveLayout">Preserve original layout</label>
                    </div>
                    <div class="option-item">
                        <input type="checkbox" id="cleanText" checked>
                        <label for="cleanText">Clean text output</label>
                    </div>
                </div>
            </div>

            <div class="main-content">
                <!-- File Upload Section -->
                <div class="upload-section">
                    <h2 class="section-title">πŸ“ Upload Files</h2>
                    <div id="upload-container"></div>
                </div>

                <!-- URL Conversion Section -->
                <div class="url-section">
                    <h2 class="section-title">πŸ”— Convert from URL</h2>
                    <div class="url-input-group">
                        <input type="url" id="fileUrl" placeholder="https://example.com/document.pdf" class="url-input">
                        <button id="convertUrl" class="convert-btn">Convert</button>
                    </div>
                    <p style="color: #666; font-size: 0.9rem;">
                        Enter the URL of a PDF or image file to convert it to Markdown
                    </p>
                    <div id="url-results"></div>
                </div>
            </div>
        </div>

        <!-- Results Container -->
        <div id="results-container"></div>
    </div>
   <script src="index.js"></script>
 </body>
</html>

Enter fullscreen mode Exit fullscreen mode
  • Step 2: Create a CSS (style.css) file and write the following styles in it:
 {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            line-height: 1.6;
            color: #333;
            background-color: #f8f9fa;
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
        }

        .header {
            text-align: center;
            margin-bottom: 40px;
            padding: 40px 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 12px;
            color: white;
        }

        .api-key-setup {
            background: white;
            padding: 30px;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
            margin-bottom: 30px;
            border-left: 4px solid #667eea;
        }

        .api-key-input {
            display: flex;
            gap: 10px;
            margin: 15px 0;
            align-items: center;
        }

        .api-key-field {
            flex: 1;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 6px;
            font-family: 'Courier New', monospace;
            font-size: 0.9rem;
        }

        .api-key-toggle {
            background: #6c757d;
            color: white;
            border: none;
            padding: 12px 16px;
            border-radius: 6px;
            cursor: pointer;
            min-width: 80px;
        }

        .api-key-save {
            background: #28a745;
            color: white;
            border: none;
            padding: 12px 20px;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 500;
        }

        .api-key-save:disabled {
            background: #ccc;
            cursor: not-allowed;
        }

        .api-status {
            margin-top: 15px;
            padding: 10px;
            border-radius: 6px;
            font-size: 0.9rem;
        }

        .status-success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }

        .status-error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }

        .status-info {
            background: #d1ecf1;
            color: #0c5460;
            border: 1px solid #bee5eb;
        }

        .main-content {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 40px;
            margin-bottom: 40px;
        }

        .content-disabled {
            opacity: 0.5;
            pointer-events: none;
        }

        .upload-section, .url-section {
            background: white;
            padding: 30px;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .section-title {
            font-size: 1.4rem;
            margin-bottom: 20px;
            color: #2c3e50;
            border-bottom: 2px solid #e9ecef;
            padding-bottom: 10px;
        }

        .drop-zone {
            border: 2px dashed #ddd;
            border-radius: 12px;
            padding: 40px 20px;
            text-align: center;
            cursor: pointer;
            transition: all 0.3s ease;
            background: #fafbfc;
        }

        .drop-zone:hover, .drop-zone.drag-over {
            border-color: #667eea;
            background-color: #f0f3ff;
            transform: translateY(-2px);
        }

        .browse-btn {
            background: #667eea;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 6px;
            cursor: pointer;
            font-size: 1rem;
        }

        .url-input-group {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
        }

        .url-input {
            flex: 1;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 6px;
            font-size: 1rem;
        }

        .convert-btn {
            background: #48bb78;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 6px;
            cursor: pointer;
            font-size: 1rem;
        }

        .options-section {
            background: white;
            padding: 30px;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
            margin-bottom: 40px;
        }

        .options-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 20px;
        }

        .option-item {
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .conversion-progress {
            margin-top: 20px;
            padding: 20px;
            border-radius: 8px;
            background: white;
            border-left: 4px solid #667eea;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }

        .progress-bar {
            width: 100%;
            height: 8px;
            background: #e9ecef;
            border-radius: 4px;
            overflow: hidden;
            margin: 15px 0;
        }

        .progress-bar-fill {
            height: 100%;
            background: linear-gradient(90deg, #48bb78, #38a169);
            width: 0%;
            transition: width 0.3s ease;
        }

        .security-notice {
            background: #fff3cd;
            border: 1px solid #ffeaa7;
            border-radius: 6px;
            padding: 15px;
            margin-bottom: 20px;
        }

        .security-notice h4 {
            color: #856404;
            margin-bottom: 5px;
        }

        .security-notice p {
            color: #856404;
            font-size: 0.9rem;
            margin: 0;
        }

        @media (max-width: 768px) {
            .main-content {
                grid-template-columns: 1fr;
            }

            .api-key-input {
                flex-direction: column;
                align-items: stretch;
            }
        }
Enter fullscreen mode Exit fullscreen mode
  • Step 3: Create a JavaScript (index.js) file and write the following code in it:
// Secure Cardinal API Converter Class
        class SecureCardinalConverter {
            constructor() {
                this.apiKey = null;
                this.baseUrl = 'https://api.trycardinal.ai';
                this.isValidated = false;
            }

            setApiKey(apiKey) {
                if (!apiKey || apiKey.trim() === '') {
                    throw new Error('API key is required');
                }
                this.apiKey = apiKey.trim();
                this.isValidated = false;
            }

            async validateApiKey() {
                if (!this.apiKey) {
                    throw new Error('API key not set');
                }

                try {
                    // Test with a lightweight request
                    const response = await fetch(`${this.baseUrl}/markdown`, {
                        method: 'POST',
                        headers: { 
                            'x-api-key': this.apiKey,
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({
                            fileUrl: 'https://httpbin.org/status/404', // This will fail but validates auth
                            markdown: true
                        })
                    });

                    // API key is valid if we don't get 401/403
                    this.isValidated = ![401, 403].includes(response.status);
                    return this.isValidated;
                } catch (error) {
                    console.warn('API validation error:', error);
                    return false;
                }
            }

            async convertFile(file, options = {}) {
                if (!this.apiKey || !this.isValidated) {
                    throw new Error('Please validate your API key first');
                }

                const formData = new FormData();
                formData.append('file', file);

                Object.keys(options).forEach(key => {
                    formData.append(key, options[key].toString());
                });

                const response = await fetch(`${this.baseUrl}/markdown`, {
                    method: 'POST',
                    headers: { 'x-api-key': this.apiKey },
                    body: formData
                });

                if (!response.ok) {
                    if (response.status === 401 || response.status === 403) {
                        this.isValidated = false;
                        throw new Error('API key is invalid or expired. Please check your key.');
                    }
                    throw new Error(`API request failed: ${response.status} ${response.statusText}`);
                }

                const result = await response.json();
                return result.pages?.[0]?.content || result.content || '';
            }

            async convertUrl(url, options = {}) {
                if (!this.apiKey || !this.isValidated) {
                    throw new Error('Please validate your API key first');
                }

                const response = await fetch(`${this.baseUrl}/markdown`, {
                    method: 'POST',
                    headers: {
                        'x-api-key': this.apiKey,
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ fileUrl: url, ...options })
                });

                if (!response.ok) {
                    if (response.status === 401 || response.status === 403) {
                        this.isValidated = false;
                        throw new Error('API key is invalid or expired. Please check your key.');
                    }
                    throw new Error(`API request failed: ${response.status} ${response.statusText}`);
                }

                const result = await response.json();
                return result.pages?.[0]?.content || result.content || '';
            }

            validateFile(file) {
                const maxSize = 10 * 1024 * 1024;
                const allowedTypes = [
                    'application/pdf', 'image/jpeg', 'image/jpg', 
                    'image/png', 'image/gif', 'image/bmp', 'image/tiff'
                ];

                if (file.size > maxSize) {
                    throw new Error('File size exceeds 10MB limit');
                }

                if (!allowedTypes.includes(file.type)) {
                    throw new Error('Unsupported file type. Please use PDF or image files.');
                }
            }
        }

        // Secure Storage Utilities
        const SecureStorage = {
            // Simple encryption for local storage (basic obfuscation)
            encrypt(text) {
                return btoa(encodeURIComponent(text));
            },

            decrypt(encoded) {
                try {
                    return decodeURIComponent(atob(encoded));
                } catch {
                    return null;
                }
            },

            store(key, value, remember = false) {
                const storage = remember ? localStorage : sessionStorage;
                storage.setItem(key, this.encrypt(value));
            },

            retrieve(key) {
                // Check session storage first, then local storage
                let value = sessionStorage.getItem(key) || localStorage.getItem(key);
                return value ? this.decrypt(value) : null;
            },

            clear(key) {
                sessionStorage.removeItem(key);
                localStorage.removeItem(key);
            }
        };

        // Initialize Application
        document.addEventListener('DOMContentLoaded', function() {
            const converter = new SecureCardinalConverter();

            // DOM Elements
            const apiKeyInput = document.getElementById('apiKeyInput');
            const toggleApiKey = document.getElementById('toggleApiKey');
            const saveApiKey = document.getElementById('saveApiKey');
            const rememberApiKey = document.getElementById('rememberApiKey');
            const apiStatus = document.getElementById('apiStatus');
            const clearApiKeys = document.getElementById('clearApiKeys');
            const mainContent = document.getElementById('mainContent');

            // Load stored API key
            const storedApiKey = SecureStorage.retrieve('cardinal_api_key');
            if (storedApiKey) {
                apiKeyInput.value = storedApiKey;
                rememberApiKey.checked = localStorage.getItem('cardinal_api_key') !== null;
                validateApiKey(storedApiKey, false);
            }

            // Toggle API key visibility
            toggleApiKey.addEventListener('click', function() {
                const isPassword = apiKeyInput.type === 'password';
                apiKeyInput.type = isPassword ? 'text' : 'password';
                toggleApiKey.textContent = isPassword ? 'Hide' : 'Show';
            });

            // Save and validate API key
            saveApiKey.addEventListener('click', function() {
                const apiKey = apiKeyInput.value.trim();
                if (!apiKey) {
                    showStatus('Please enter an API key', 'error');
                    return;
                }
                validateApiKey(apiKey, true);
            });

            // Clear stored keys
            clearApiKeys.addEventListener('click', function() {
                if (confirm('Are you sure you want to clear all stored API keys?')) {
                    SecureStorage.clear('cardinal_api_key');
                    apiKeyInput.value = '';
                    rememberApiKey.checked = false;
                    showStatus('All stored API keys have been cleared', 'info');
                    mainContent.classList.add('content-disabled');
                }
            });

            // Enter key support
            apiKeyInput.addEventListener('keypress', function(e) {
                if (e.key === 'Enter') {
                    saveApiKey.click();
                }
            });

            async function validateApiKey(apiKey, showFeedback = true) {
                if (showFeedback) {
                    saveApiKey.disabled = true;
                    saveApiKey.textContent = 'Validating...';
                    showStatus('Validating API key...', 'info');
                }

                try {
                    converter.setApiKey(apiKey);
                    const isValid = await converter.validateApiKey();

                    if (isValid) {
                        // Store the API key
                        SecureStorage.store('cardinal_api_key', apiKey, rememberApiKey.checked);

                        if (showFeedback) {
                            showStatus('βœ… API key validated successfully!', 'success');
                        }

                        // Enable main content
                        mainContent.classList.remove('content-disabled');

                        // Initialize the converter interface
                        initializeConverterInterface();
                    } else {
                        throw new Error('Invalid API key or service unavailable');
                    }
                } catch (error) {
                    if (showFeedback) {
                        showStatus(`❌ ${error.message}`, 'error');
                    }
                    mainContent.classList.add('content-disabled');
                } finally {
                    if (showFeedback) {
                        saveApiKey.disabled = false;
                        saveApiKey.textContent = 'Validate & Save';
                    }
                }
            }

            function showStatus(message, type) {
                apiStatus.textContent = message;
                apiStatus.className = `api-status status-${type}`;
                apiStatus.style.display = 'block';

                if (type === 'success' || type === 'info') {
                    setTimeout(() => {
                        apiStatus.style.display = 'none';
                    }, 5000);
                }
            }

            function initializeConverterInterface() {
                // Initialize file upload interface
                createDropZone();
                setupUrlConversion();
            }

            // Rest of the converter interface code (same as before but using the secure converter)
            function getConversionOptions() {
                return {
                    markdown: document.getElementById('markdown').checked,
                    denseTables: document.getElementById('denseTables').checked,
                    preserveLayout: document.getElementById('preserveLayout').checked,
                    cleanText: document.getElementById('cleanText').checked
                };
            }

            function createDropZone() {
                const container = document.getElementById('upload-container');
                container.innerHTML = ''; // Clear any existing content

                const fileInput = document.createElement('input');
                fileInput.type = 'file';
                fileInput.accept = '.pdf,image/*';
                fileInput.multiple = true;
                fileInput.style.display = 'none';

                const dropZone = document.createElement('div');
                dropZone.className = 'drop-zone';
                dropZone.innerHTML = `
                    <div class="drop-zone-content">
                        <p>πŸ“„ Drop PDF or image files here or <button type="button" class="browse-btn">browse</button></p>
                        <small>Supported formats: PDF, JPEG, PNG, GIF, BMP, TIFF (max 10MB each)</small>
                    </div>
                `;

                container.appendChild(dropZone);
                container.appendChild(fileInput);

                // Event listeners
                dropZone.addEventListener('click', () => fileInput.click());
                dropZone.querySelector('.browse-btn').addEventListener('click', (e) => {
                    e.stopPropagation();
                    fileInput.click();
                });

                // Drag and drop events
                ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
                    dropZone.addEventListener(eventName, preventDefaults);
                });

                function preventDefaults(e) {
                    e.preventDefault();
                    e.stopPropagation();
                }

                ['dragenter', 'dragover'].forEach(eventName => {
                    dropZone.addEventListener(eventName, () => dropZone.classList.add('drag-over'));
                });

                ['dragleave', 'drop'].forEach(eventName => {
                    dropZone.addEventListener(eventName, () => dropZone.classList.remove('drag-over'));
                });

                dropZone.addEventListener('drop', (e) => handleFiles(e.dataTransfer.files));
                fileInput.addEventListener('change', (e) => handleFiles(e.target.files));
            }

            async function handleFiles(files) {
                const fileList = Array.from(files);

                for (const file of fileList) {
                    try {
                        converter.validateFile(file);
                        await processFile(file);
                    } catch (error) {
                        showError(`Error with file "${file.name}": ${error.message}`);
                    }
                }
            }

            async function processFile(file) {
                const resultsContainer = document.getElementById('upload-container');

                const progressDiv = document.createElement('div');
                progressDiv.className = 'conversion-progress';
                progressDiv.innerHTML = `
                    <div style="display: flex; justify-content: space-between; align-items: center;">
                        <strong>πŸ”„ Converting: ${file.name}</strong>
                        <span class="file-size">${formatFileSize(file.size)}</span>
                    </div>
                    <div class="progress-bar">
                        <div class="progress-bar-fill"></div>
                    </div>
                    <div class="status">Initializing conversion...</div>
                `;
                resultsContainer.appendChild(progressDiv);

                const progressBar = progressDiv.querySelector('.progress-bar-fill');
                const statusText = progressDiv.querySelector('.status');

                try {
                    updateProgress(progressBar, statusText, 25, 'Uploading file...');

                    const options = getConversionOptions();
                    const markdown = await converter.convertFile(file, options);

                    updateProgress(progressBar, statusText, 100, 'Conversion completed!');

                    setTimeout(() => {
                        displayResult(file.name, markdown, progressDiv);
                    }, 500);

                } catch (error) {
                    progressDiv.innerHTML = `
                        <div style="background: #f8d7da; color: #721c24; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545;">
                            <strong>❌ Error converting ${file.name}</strong>
                            <p>${error.message}</p>
                            ${error.message.includes('API key') ? '<p><em>Please check your API key configuration above.</em></p>' : ''}
                        </div>
                    `;
                }
            }

            function setupUrlConversion() {
                const urlInput = document.getElementById('fileUrl');
                const convertBtn = document.getElementById('convertUrl');
                const resultsDiv = document.getElementById('url-results');

                convertBtn.addEventListener('click', async () => {
                    const url = urlInput.value.trim();

                    if (!url) {
                        showError('Please enter a valid URL');
                        return;
                    }

                    if (!isValidUrl(url)) {
                        showError('Please enter a valid URL starting with http:// or https://');
                        return;
                    }

                    convertBtn.disabled = true;
                    convertBtn.textContent = 'Converting...';
                    resultsDiv.innerHTML = '';

                    const progressDiv = document.createElement('div');
                    progressDiv.className = 'conversion-progress';
                    progressDiv.innerHTML = `
                        <div><strong>πŸ”„ Converting URL: ${url}</strong></div>
                        <div class="progress-bar">
                            <div class="progress-bar-fill" style="width: 50%"></div>
                        </div>
                        <div class="status">Processing URL...</div>
                    `;
                    resultsDiv.appendChild(progressDiv);

                    try {
                        const options = getConversionOptions();
                        const markdown = await converter.convertUrl(url, options);

                        const fileName = url.split('/').pop() || 'converted-document';
                        displayResult(fileName, markdown, progressDiv);

                    } catch (error) {
                        progressDiv.innerHTML = `
                            <div style="background: #f8d7da; color: #721c24; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545;">
                                <strong>❌ Error converting URL</strong>
                                <p>${error.message}</p>
                                ${error.message.includes('API key') ? '<p><em>Please check your API key configuration above.</em></p>' : ''}
                            </div>
                        `;
                    } finally {
                        convertBtn.disabled = false;
                        convertBtn.textContent = 'Convert';
                    }
                });

                urlInput.addEventListener('keypress', (e) => {
                    if (e.key === 'Enter') {
                        convertBtn.click();
                    }
                });
            }

            function updateProgress(progressBar, statusText, progress, status) {
                progressBar.style.width = `${progress}%`;
                statusText.textContent = status;
            }

            function displayResult(fileName, markdown, container) {
                container.innerHTML = `
                    <div style="margin-bottom: 15px;">
                        <strong>βœ… ${fileName} - Conversion completed!</strong>
                        <span style="color: #666; font-size: 0.9rem;">(${formatFileSize(markdown.length)} of markdown)</span>
                    </div>
                    <div style="display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap;">
                        <button class="copy-btn" style="background: #4299e1; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">πŸ“‹ Copy to Clipboard</button>
                        <button class="download-btn" style="background: #48bb78; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">πŸ’Ύ Download Markdown</button>
                        <button class="view-btn" style="background: #ed8936; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">πŸ‘οΈ View Content</button>
                    </div>
                    <div class="markdown-preview" style="display: none; background: #f8f9fa; padding: 20px; border-radius: 8px; max-height: 400px; overflow-y: auto; font-family: 'Courier New', monospace; font-size: 0.9rem; white-space: pre-wrap; border: 1px solid #e9ecef;">
                        <div style="margin-bottom: 10px; font-weight: bold; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">Markdown Content:</div>${escapeHtml(markdown)}
                    </div>
                `;

                const copyBtn = container.querySelector('.copy-btn');
                const downloadBtn = container.querySelector('.download-btn');
                const viewBtn = container.querySelector('.view-btn');
                const previewDiv = container.querySelector('.markdown-preview');

                copyBtn.addEventListener('click', async () => {
                    try {
                        await navigator.clipboard.writeText(markdown);
                        copyBtn.innerHTML = 'βœ… Copied!';
                        setTimeout(() => copyBtn.innerHTML = 'πŸ“‹ Copy to Clipboard', 2000);
                    } catch (err) {
                        showError('Failed to copy to clipboard');
                    }
                });

                downloadBtn.addEventListener('click', () => {
                    downloadMarkdown(markdown, fileName);
                });

                viewBtn.addEventListener('click', () => {
                    const isVisible = previewDiv.style.display !== 'none';
                    previewDiv.style.display = isVisible ? 'none' : 'block';
                    viewBtn.innerHTML = isVisible ? 'πŸ‘οΈ View Content' : 'πŸ™ˆ Hide Content';
                });
            }

            function formatFileSize(bytes) {
                if (bytes === 0) return '0 Bytes';
                const k = 1024;
                const sizes = ['Bytes', 'KB', 'MB', 'GB'];
                const i = Math.floor(Math.log(bytes) / Math.log(k));
                return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
            }

            function escapeHtml(text) {
                const div = document.createElement('div');
                div.textContent = text;
                return div.innerHTML;
            }

            function isValidUrl(string) {
                try {
                    const url = new URL(string);
                    return url.protocol === 'http:' || url.protocol === 'https:';
                } catch (_) {
                    return false;
                }
            }

            function downloadMarkdown(content, originalFileName) {
                const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `${originalFileName.replace(/\.[^/.]+$/, '')}.md`;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
            }

            function showError(message) {
                const errorDiv = document.createElement('div');
                errorDiv.style.cssText = `
                    background: #f8d7da; 
                    color: #721c24; 
                    padding: 15px; 
                    border-radius: 8px; 
                    margin-top: 15px; 
                    border-left: 4px solid #dc3545;
                `;
                errorDiv.innerHTML = `<strong>❌ Error:</strong> ${message}`;

                const container = document.getElementById('results-container');
                container.appendChild(errorDiv);

                setTimeout(() => {
                    if (errorDiv.parentNode) {
                        errorDiv.parentNode.removeChild(errorDiv);
                    }
                }, 5000);
            }
        });

Enter fullscreen mode Exit fullscreen mode

At this point, we have a fully functional web application you can view on your browser. You should see something like this:

Web page

Web page

To use this web app,

  • Copy and paste your API key in the API Key Configuration text area, - - Click the Validate and Save button
  • Go to the Upload file and upload the file you want to convert to Markdown or use the URL conversion option.

Top comments (0)