Gemini2Markdown

Exports Google Gemini chats to Markdown. Captures "Thinking steps", auto-detects model names, loads full history, and supports dual/parallel responses.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         Gemini2Markdown
// @namespace    https://greasyfork.org/en/users/1552401-chipfin
// @version      2.0.1
// @description  Exports Google Gemini chats to Markdown. Captures "Thinking steps", auto-detects model names, loads full history, and supports dual/parallel responses.
// @icon64       https://upload.wikimedia.org/wikipedia/commons/archive/1/1d/20251003211919%21Google_Gemini_icon_2025.svg
// @match        https://gemini.google.com/*
// @grant        GM_registerMenuCommand
// @license      MIT
// @author       Gemini 3 Pro, Claude Sonnet 4.6 Thinking
// ==/UserScript==

(() => {
    'use strict';

    let isExporting = false;
    let toastTimeoutId = null;
    let toastFadeTimeoutId = null;

    /* ---------------- Utilities ---------------- */

    const sleep = (ms) => new Promise(r => setTimeout(r, ms));
    const trim = s => (s || '').toString().replace(/\r/g, '').trim();

    function getFormattedTimestamp() {
        const now = new Date();
        const pad = (n) => n.toString().padStart(2, '0');
        const tzo = -now.getTimezoneOffset();
        const dif = tzo >= 0 ? '+' : '-';
        const offHour = pad(Math.floor(Math.abs(tzo) / 60));
        const offMin = pad(Math.abs(tzo) % 60);
        return `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}T${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}${dif}${offHour}${offMin}`;
    }

    function waitForNonEmptyElement(selector, timeout = 6000) {
        return new Promise(resolve => {
            const check = () => {
                const el = document.querySelector(selector);
                if (el && el.textContent && el.textContent.trim().length > 10) return el;
                return null;
            };
            const existing = check();
            if (existing) return resolve(existing);
            const observer = new MutationObserver(() => {
                const el = check();
                if (el) { observer.disconnect(); resolve(el); }
            });
            observer.observe(document.body, { childList: true, subtree: true, characterData: true });
            setTimeout(() => { observer.disconnect(); resolve(null); }, timeout);
        });
    }

    function waitForElement(selector, timeout = 3000) {
        return new Promise(resolve => {
            const existing = document.querySelector(selector);
            if (existing) return resolve(existing);
            const observer = new MutationObserver(() => {
                const el = document.querySelector(selector);
                if (el) { observer.disconnect(); resolve(el); }
            });
            observer.observe(document.body, { childList: true, subtree: true });
            setTimeout(() => { observer.disconnect(); resolve(null); }, timeout);
        });
    }

    function cleanMarkdown(text) {
        if (!text) return '';

        text = text.replace(/https:\/\/[^ \n]+filename=([^& \n]+)[^ \n]*/g, (match, filename) => {
            try { return `[Uploaded File: ${decodeURIComponent(filename.replace(/\+/g, ' '))}]`; } catch (e) { return '[Uploaded File]'; }
        });

        text = text
            .replace(/https:\/\/drive\.google\.com\/viewerng\/thumb[^ \n]*/g, '')
            .replace(/https:\/\/contribution\.usercontent\.google\.com\/download[^ \n]*/g, '')
            .replace(/https:\/\/lh3\.googleusercontent\.com\/[^ \n]+/g, '[Image]')
            .replace(/\\(?![\\*_`])/g, '\\\\')
            .replace(/&lt;/g, '<')
            .replace(/&gt;/g, '>')
            .replace(/&amp;/g, '&');

        return text.replace(/\n\s*\n/g, '\n\n').trim();
    }

    function createSvgIcon(width = '24', height = '15') {
        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('width', width);
        svg.setAttribute('height', height);
        svg.setAttribute('viewBox', '0 0 208 128');

        const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
        rect.setAttribute('width', '198');
        rect.setAttribute('height', '118');
        rect.setAttribute('x', '5');
        rect.setAttribute('y', '5');
        rect.setAttribute('ry', '10');
        rect.setAttribute('stroke', 'currentColor');
        rect.setAttribute('stroke-width', '10');
        rect.setAttribute('fill', 'none');

        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path.setAttribute('d', 'M30 98V30h20l20 25 20-25h20v68H90V59L70 84 50 59v39zm125 0l-30-33h20V30h20v35h20z');
        path.setAttribute('fill', 'currentColor');

        svg.appendChild(rect);
        svg.appendChild(path);
        return svg;
    }

    /* ---------------- Actions ---------------- */

    function getChatScroller() {
        return document.querySelector('#chat-history.chat-history-scroll-container infinite-scroller.chat-history') ||
               document.querySelector('infinite-scroller.chat-history');
    }

    async function scrollChatToTop(statusCallback, cancelState) {
        const scroller = getChatScroller();
        if (!scroller) return;

        let stableCount = 0;
        for (let i = 0; i < 55; i++) {
            if (cancelState && cancelState.cancel) {
                if (statusCallback) statusCallback(`⏹️ Scrolling stopped.`);
                await sleep(400);
                break;
            }

            scroller.scrollTop = 0;
            if (statusCallback) statusCallback(`⬆️ Scrolling... ${i}`);
            await sleep(1300);

            if (scroller.scrollTop !== 0) {
                stableCount = 0;
            } else {
                stableCount++;
            }
            if (stableCount >= 4) break;
        }
    }

    async function extractDetailsFromSidebar(conversationContainer) {
        let details = { model: 'Gemini', thoughts: '' };

        const menuBtn = conversationContainer.querySelector(
            'gem-icon-button[data-test-id="more-menu-button"] button, ' +
            'button[aria-label="Show more options"], ' +
            '.more-menu-button-container button, ' +
            'button[data-test-id="more-actions-button"]'
        );
        if (!menuBtn) return details;

        menuBtn.click();
        await sleep(400);

        const overlayLabels = [...document.querySelectorAll('.cdk-overlay-pane gem-menu-item .label, .cdk-overlay-pane .mat-mdc-menu-item-text')];
        const oldModelItem = overlayLabels.find(el => el.textContent && el.textContent.trim().startsWith('Model:'));
        if (oldModelItem) {
            details.model = oldModelItem.textContent.trim().replace(/^Model:\s*/i, '');
        }

        const thinkingBtn = document.querySelector('.cdk-overlay-pane gem-menu-item[data-test-id="thinking-steps-button"]');
        const responseDetailsBtn = document.querySelector('.cdk-overlay-pane gem-menu-item[data-test-id="response-details-button"]');
        const genericBtn = document.querySelector('.cdk-overlay-pane gem-menu-item[value="thoughts"]')
            || [...document.querySelectorAll('.cdk-overlay-pane gem-menu-item')].find(el => {
                const t = (el.textContent || '').toLowerCase();
                return t.includes('thinking') || t.includes('steps');
            });

        const hasThoughts = !!thinkingBtn || (!responseDetailsBtn && !!genericBtn &&
            (genericBtn.textContent || '').toLowerCase().includes('thinking'));
        const detailsBtn = thinkingBtn || responseDetailsBtn || genericBtn;

        if (!detailsBtn) {
            document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, bubbles: true, cancelable: true }));
            await sleep(150);
            return details;
        }

        detailsBtn.click();

        if (hasThoughts) {
            const markdownEl = await waitForNonEmptyElement(
                'context-sidebar:not([style*="opacity: 0"]) [data-test-id="thought-steps"] .markdown.markdown-main-panel',
                6000
            );

            if (markdownEl) {
                const activeSidebar = document.querySelector('context-sidebar:not([style*="opacity: 0"])');
                if (activeSidebar) {
                    const modelSpan = activeSidebar.querySelector('[data-test-id="model-line"] span.ng-star-inserted');
                    if (modelSpan && modelSpan.textContent) {
                        details.model = modelSpan.textContent.trim()
                            .replace(/^Used\s+/i, '')
                            .replace(/\s*model$/i, '')
                            .trim();
                    }

                    const thoughtSteps = activeSidebar.querySelectorAll('[data-test-id="thought-steps"] .thought-step-container');
                    if (thoughtSteps.length > 0) {
                        const allThoughts = [];
                        thoughtSteps.forEach(step => {
                            const mdDiv = step.querySelector('.markdown.markdown-main-panel') || step;
                            allThoughts.push(processThoughtMarkdown(mdDiv));
                        });
                        details.thoughts = allThoughts.filter(Boolean).join('\n\n');
                    } else {
                        details.thoughts = processThoughtMarkdown(markdownEl);
                    }
                }
            }
        } else {
            await waitForElement('context-sidebar:not([style*="opacity: 0"]) [data-test-id="model-line"]', 2000);
            await sleep(200);
            const activeSidebar = document.querySelector('context-sidebar:not([style*="opacity: 0"])');
            if (activeSidebar) {
                const modelSpan = activeSidebar.querySelector('[data-test-id="model-line"] span.ng-star-inserted');
                if (modelSpan && modelSpan.textContent) {
                    details.model = modelSpan.textContent.trim()
                        .replace(/^Used\s+/i, '')
                        .replace(/\s*model$/i, '')
                        .trim();
                }
            }
        }

        const closeBtn = document.querySelector(
            'context-sidebar:not([style*="opacity: 0"]) button[data-test-id="close-button"], ' +
            'context-sidebar:not([style*="opacity: 0"]) button[aria-label="Close sidebar"]'
        );
        if (closeBtn) closeBtn.click();
        await sleep(400);

        return details;
    }

    /* ---------------- Extraction ---------------- */

    function processThoughtMarkdown(el) {
        if (!el) return '';
        const clone = el.cloneNode(true);

        clone.querySelectorAll('b, strong').forEach(b => {
            b.replaceWith(`**${b.textContent}**`);
        });
        clone.querySelectorAll('i, em').forEach(i => {
            i.replaceWith(`*${i.textContent}*`);
        });
        clone.querySelectorAll('code:not(pre code)').forEach(code => {
            code.replaceWith(`\`${code.textContent}\``);
        });
        clone.querySelectorAll('pre').forEach(pre => {
            const code = pre.innerText;
            const lang = pre.getAttribute('data-language') || '';
            pre.replaceWith(`\n\`\`\`${lang}\n${code}\n\`\`\`\n`);
        });

        clone.querySelectorAll('p').forEach(p => {
            p.after(document.createTextNode('\n\n'));
        });
        clone.querySelectorAll('br').forEach(br => br.replaceWith('\n'));

        let text = (clone.textContent || '').replace(/\n{3,}/g, '\n\n').trim();
        return cleanMarkdown(text);
    }

    function processElement(el) {
        if (!el) return '';
        const clone = el.cloneNode(true);

        clone.querySelectorAll('button, mat-icon, .action-bar, .feedback_buttons, .thoughts-header, .select-button').forEach(e => e.remove());

        clone.querySelectorAll('b, strong').forEach(b => b.textContent = `**${b.textContent}**`);
        clone.querySelectorAll('i, em').forEach(i => i.textContent = `*${i.textContent}*`);

        clone.querySelectorAll('code:not(pre code)').forEach(code => {
            code.textContent = `\`${code.textContent}\``;
        });

        clone.querySelectorAll('a').forEach(a => {
            const href = a.href;
            const text = a.innerText;
            if (href && text) a.textContent = `[${text}](${href})`;
        });

        clone.querySelectorAll('pre').forEach(pre => {
            const code = pre.innerText;
            const lang = pre.getAttribute('data-language') || '';
            pre.textContent = `\n\`\`\`${lang}\n${code}\n\`\`\`\n`;
        });

        const blockTags = ['p', 'div', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'tr'];
        blockTags.forEach(tag => {
            clone.querySelectorAll(tag).forEach(block => block.after('\n'));
        });

        clone.querySelectorAll('br').forEach(br => br.replaceWith('\n'));

        return cleanMarkdown(clone.textContent);
    }

    /* ---------------- Main Logic ---------------- */

    async function exportToMarkdown() {
        if (isExporting) {
            console.log("Gemini2Markdown: Export already in progress.");
            return;
        }

        isExporting = true;
        let cancelState = { cancel: false };

        const setStatus = (text, showStopBtn = false) => {
            // Clear any pending cleanup timeouts if a new status update happens
            if (toastTimeoutId) clearTimeout(toastTimeoutId);
            if (toastFadeTimeoutId) clearTimeout(toastFadeTimeoutId);

            let toast = document.getElementById('gemini2md-toast');
            if (!toast) {
                toast = document.createElement('div');
                toast.id = 'gemini2md-toast';
                toast.style.cssText = `
                    position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
                    background-color: var(--gem-sys-color--inverse-surface, #303030);
                    color: var(--gem-sys-color--inverse-on-surface, #f2f2f2);
                    padding: 12px 24px; border-radius: 8px;
                    font-family: var(--gem-sys-typography-type-scale--body-m-font-name, "Google Sans", sans-serif);
                    font-size: 14px; z-index: 10000;
                    box-shadow: 0 4px 6px rgba(0,0,0,0.3);
                    transition: opacity 0.3s;
                    display: flex; align-items: center; justify-content: center; gap: 16px;
                `;

                const textSpan = document.createElement('span');
                textSpan.id = 'gemini2md-toast-text';
                toast.appendChild(textSpan);

                const stopBtn = document.createElement('button');
                stopBtn.id = 'gemini2md-toast-stop';
                stopBtn.textContent = 'Stop Scroll';
                stopBtn.style.cssText = `
                    background-color: rgba(255, 255, 255, 0.2);
                    color: #fff;
                    border: none;
                    border-radius: 4px;
                    padding: 4px 8px;
                    font-size: 12px;
                    font-weight: 500;
                    cursor: pointer;
                    transition: background-color 0.2s;
                `;
                stopBtn.onmouseover = () => stopBtn.style.backgroundColor = 'rgba(255, 255, 255, 0.3)';
                stopBtn.onmouseout = () => stopBtn.style.backgroundColor = 'rgba(255, 255, 255, 0.2)';

                stopBtn.addEventListener('click', () => {
                    cancelState.cancel = true;
                    stopBtn.style.display = 'none';
                });

                toast.appendChild(stopBtn);
                document.body.appendChild(toast);
            }

            const textSpan = document.getElementById('gemini2md-toast-text');
            if (textSpan) textSpan.textContent = text;

            const stopBtn = document.getElementById('gemini2md-toast-stop');
            if (stopBtn) {
                stopBtn.style.display = (showStopBtn && !cancelState.cancel) ? 'block' : 'none';
            }

            toast.style.opacity = '1';
            console.log(`Gemini2Markdown: ${text}`);
        };

        try {
            await scrollChatToTop((text) => setStatus(text, true), cancelState);
            setStatus('🔍 Extracting chat data...');

            const containers = document.querySelectorAll('.conversation-container');
            if (containers.length === 0) throw new Error("No chat found. Please ensure the page is fully loaded.");

            const conversationId = location.pathname.match(/\/app\/([a-zA-Z0-9]+)/)?.[1];

            const titleEl =
                document.querySelector('.conversation-title-container .gds-title-m') ||
                (conversationId ? document.querySelector(`a.conversation[href*="/app/${conversationId}"] .conversation-title`) : null) ||
                document.querySelector('a.conversation.selected .conversation-title');

            let cleanTitle = '';
            if (titleEl && titleEl.innerText && titleEl.innerText.trim().length > 0) {
                cleanTitle = trim(titleEl.innerText);
            } else {
                cleanTitle = trim(document.title)
                    .replace(/ - Google Gemini$/i, '')
                    .replace(/ - Gemini$/i, '')
                    .replace(/ [-–|].*$/i, '');
            }

            if (/^(google\s*)?gemini$/i.test(cleanTitle) || cleanTitle === '' || cleanTitle.toLowerCase() === 'chats') {
                const firstQuery = document.querySelector('user-query p.query-text-line, user-query .query-text, user-query .query-content, .user-query');
                if (firstQuery && firstQuery.textContent) {
                    let fallbackText = trim(firstQuery.textContent).replace(/^You said\s*/i, '');
                    cleanTitle = fallbackText.split(/[\r\n]+/)[0].substring(0, 64).trim();
                } else {
                    cleanTitle = conversationId ? `chat-${conversationId}` : 'Gemini_Conversation';
                }
            }

            cleanTitle = cleanTitle.replace(/[<>:"/\\|?*]/g, '').replace(/\s+/g, ' ');

            let displayTitle = cleanTitle.substring(0, 64).trim();
            const timestamp = getFormattedTimestamp();

            const toc = [];
            const turnBuffer = [];
            let globalModel = 'Gemini';
            let chatIndex = 1;

            for (let i = 0; i < containers.length; i++) {
                const container = containers[i];
                const userQuery = container.querySelector('user-query .query-content, .user-query');

                const hasDual = !!container.querySelector('dual-model-response');
                const modelResponses = hasDual
                    ? [...container.querySelectorAll('response-selection-panel')]
                    : [...container.querySelectorAll('model-response')];

                const isDual = hasDual && modelResponses.length > 1;

                if (!userQuery && modelResponses.length === 0) continue;

                setStatus(`🔍 ${i+1}/${containers.length}`);

                let turnText = `### chat-${chatIndex}\n\n`;

                if (userQuery) {
                    const text = processElement(userQuery);
                    toc.push(`- [${chatIndex}: ${text.substring(0, 50).replace(/\n/g, ' ')}...](#chat-${chatIndex})`);
                    turnText += `####### User writes:\n\n${text}\n\n`;
                }

                if (modelResponses.length > 0) {
                    const details = await extractDetailsFromSidebar(container);

                    if (i === 0 && details.model !== 'Gemini') {
                        globalModel = details.model;
                    }

                    for (let rIndex = 0; rIndex < modelResponses.length; rIndex++) {
                        const responseNode = modelResponses[rIndex];

                        const draftLabel = isDual ? (rIndex === 0 ? ' (Choice A)' : ' (Choice B)') : '';
                        turnText += `####### Gemini (${details.model})${draftLabel} writes:\n\n`;

                        let legacyThoughtsText = '';
                        if (!details.thoughts) {
                            const legacyThoughtNode = responseNode.querySelector('model-thoughts');
                            if (legacyThoughtNode) {
                                const expandBtn = legacyThoughtNode.querySelector('.thoughts-header-button');
                                if (expandBtn && expandBtn.textContent && expandBtn.textContent.includes('Show thinking')) {
                                    expandBtn.click();
                                    await sleep(100);
                                }
                                legacyThoughtsText = processElement(legacyThoughtNode.querySelector('.thoughts-content'));
                            }
                        }

                        const finalThoughts = (rIndex === 0) ? (details.thoughts || legacyThoughtsText) : '';

                        if (finalThoughts) {
                            turnText += `**Thinking steps:**\n---\n\n${finalThoughts}\n\n`;
                        }

                        const responseClone = responseNode.cloneNode(true);
                        responseClone.querySelectorAll('model-thoughts, .thoughts-container').forEach(e => e.remove());

                        if (finalThoughts) turnText += `**Response (Gemini):**\n---\n\n`;

                        const contentNode = responseClone.querySelector('message-content') ||
                                            responseClone.querySelector('structured-content-container') ||
                                            responseClone;
                        turnText += `${processElement(contentNode)}\n\n`;

                        if (isDual && rIndex < modelResponses.length - 1) {
                            turnText += `---\n\n`;
                        }
                    }
                }

                turnText += `___\n###### [top](#table-of-contents)\n\n`;
                turnBuffer.push(turnText);
                chatIndex++;
            }

            const header = `---\ntitle: ${cleanTitle}\ndate: ${timestamp}\nurl: ${location.href}\nmodel: ${globalModel}\n---\n\n# ${cleanTitle}\n\n`;
            const finalContent = [header, `## Table of Contents\n${toc.join('\n')}\n\n---\n\n`, ...turnBuffer].join('');

            const blob = new Blob([finalContent], { type: 'text/markdown' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `GEMINI_${displayTitle}_${timestamp}.md`;
            a.click();
            URL.revokeObjectURL(url);

            setStatus(`✅ Done!`);

        } catch (e) {
            console.error(e);
            setStatus(`❌ Error: ${e.message}`);
            alert("Export failed: " + e.message);
        } finally {
            // Unset the lock and reset cancel state
            isExporting = false;
            cancelState.cancel = false;

            // Immediately hide stop button
            const stopBtn = document.getElementById('gemini2md-toast-stop');
            if (stopBtn) stopBtn.style.display = 'none';

            // Cleanly fade and entirely remove the toast element from the DOM
            toastTimeoutId = setTimeout(() => {
                const toast = document.getElementById('gemini2md-toast');
                if (toast) {
                    toast.style.opacity = '0';
                    toastFadeTimeoutId = setTimeout(() => {
                        if (toast.parentNode) toast.remove();
                    }, 300); // 300ms accounts for the CSS opacity transition
                }
            }, 2500);
        }
    }

    /* ---------------- UI Integration ---------------- */

    function addExportMenuOption() {
        const overlays = document.querySelectorAll('.cdk-overlay-pane gem-menu');

        for (const menu of overlays) {
            if (menu.querySelector('gem-menu-item[data-test-id="delete-button"]') && !menu.querySelector('#gemini-export-md-menu-item')) {

                const templateItem = menu.querySelector('gem-menu-item');
                if (!templateItem) continue;

                const mdItem = templateItem.cloneNode(true);
                mdItem.id = 'gemini-export-md-menu-item';
                mdItem.setAttribute('data-test-id', 'gemini-export-md-menu-item');
                mdItem.setAttribute('aria-label', 'Export to Markdown');
                mdItem.setAttribute('value', 'export-markdown');

                mdItem.classList.remove('active');
                const content = mdItem.querySelector('gem-menu-item-content');
                if (content) content.classList.remove('active');

                const labelSpan = mdItem.querySelector('.label span') || mdItem.querySelector('.label');
                if (labelSpan) {
                    labelSpan.textContent = 'Export to Markdown';
                }

                const iconContainer = mdItem.querySelector('.leading-container mat-icon');
                if (iconContainer) {
                    const svgIcon = createSvgIcon('20', '13');
                    svgIcon.style.display = 'block';
                    svgIcon.style.fill = 'currentColor';
                    iconContainer.parentNode.replaceChild(svgIcon, iconContainer);
                }

                mdItem.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    document.body.dispatchEvent(new KeyboardEvent('keydown', {
                        key: 'Escape', code: 'Escape', keyCode: 27, bubbles: true, cancelable: true
                    }));
                    exportToMarkdown();
                });

                mdItem.addEventListener('mouseenter', () => {
                    mdItem.style.backgroundColor = 'var(--mat-menu-item-hover-state-layer-color, rgba(0,0,0,0.04))';
                });
                mdItem.addEventListener('mouseleave', () => {
                    mdItem.style.backgroundColor = 'transparent';
                });

                const deleteBtn = menu.querySelector('gem-menu-item[data-test-id="delete-button"]');
                if (deleteBtn) {
                    menu.insertBefore(mdItem, deleteBtn);
                } else {
                    menu.appendChild(mdItem);
                }
            }
        }
    }

    if (typeof GM_registerMenuCommand !== 'undefined') {
        GM_registerMenuCommand('Export to Markdown', exportToMarkdown);
    }

    new MutationObserver(addExportMenuOption).observe(document.body, { childList: true, subtree: true });

})();