Markdown Viewer

Automatically formats and displays .md files with a pleasant, readable theme and font settings. Turn your browser into the only Markdown viewer you need by giving your Tampermonkey access to local files.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Markdown Viewer
// @namespace    http://tampermonkey.net/
// @version      2.0.2
// @description  Automatically formats and displays .md files with a pleasant, readable theme and font settings. Turn your browser into the only Markdown viewer you need by giving your Tampermonkey access to local files.
// @description:en Automatically formats and displays .md files with a pleasant, readable theme and font settings. Turn your browser into the only Markdown viewer you need by giving your Tampermonkey access to local files.
// @description:de Automatisch .md-Dateien formatieren und anzeigen mit einem angenehmen, lesbaren Thema und Schriftarten. Machen Sie Ihren Browser zum einzigen Markdown-Viewer, den Sie benötigen, indem Sie Tampermonkey Zugriff auf lokale Dateien gewähren.
// @author       https://github.com/anga83
// @match        *://*/*.md
// @include      file://*/*.md
// @exclude      https://github.com/*
// @exclude      http://github.com/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js
// @resource     css_darkdown https://raw.githubusercontent.com/yrgoldteeth/darkdowncss/master/darkdown.css
// @grant        GM_getResourceText
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    // --- SETTINGS IDENTIFIERS ---
    const FONT_STYLE_KEY = 'markdownViewer_fontStyle';
    const THEME_KEY = 'markdownViewer_theme';
    const STYLE_ELEMENT_ID_FONT = 'userscript-markdown-font-style';
    const STYLE_ELEMENT_ID_THEME = 'userscript-markdown-theme-style';
    const STYLE_ELEMENT_ID_BASE = 'userscript-markdown-base-style';

    // --- FONT SETTINGS ---
    const FONT_SETTINGS = {
        'serif': `Iowan Old Style, Apple Garamond, Baskerville, Georgia, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol`,
        'sans-serif': `"Segoe UI", "SF Pro Text", "Helvetica Neue", "Ubuntu", "Arial", sans-serif`
    };

    function removeExistingStyleElement(id) {
        const existingStyle = document.getElementById(id);
        if (existingStyle) {
            existingStyle.remove();
        }
    }

    function addStyleElement(id, css) {
        removeExistingStyleElement(id);
        const style = document.createElement('style');
        style.id = id;
        style.textContent = css;
        (document.head || document.documentElement).appendChild(style);
    }

    function applyFontStyle() {
        const chosenFont = GM_getValue(FONT_STYLE_KEY, 'serif'); // Standard 'serif'
        const fontFamily = FONT_SETTINGS[chosenFont] || FONT_SETTINGS.serif;
        addStyleElement(STYLE_ELEMENT_ID_FONT, `.markdown-body { font-family: ${fontFamily} !important; }`);
    }

    // --- THEME SETTINGS ---
    function applyThemeStyle() {
        const chosenTheme = GM_getValue(THEME_KEY, 'system'); // 'system', 'light', 'dark'
        const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches;

        let useDarkTheme = false;
        if (chosenTheme === 'dark') {
            useDarkTheme = true;
        } else if (chosenTheme === 'system' && prefersDarkScheme) {
            useDarkTheme = true;
        }

        let themeCss = '';
        if (useDarkTheme) {
            const darkdownCss = GM_getResourceText("css_darkdown");
            if (darkdownCss) {
                 themeCss += darkdownCss;
            }
            // Dark Theme
            themeCss += `
                body {
                    background-color: rgb(27, 28, 29) !important;
                    color: rgb(220, 220, 220) !important;
                }
                .markdown-body {
                    color: rgb(220, 220, 220) !important;
                }
                .markdown-body a { 
                    color: #79b8ff !important; /* Dezenter blau-türkis Farbton statt kräftiges Blau */
                    text-decoration: none !important; /* Keine Unterstreichung standardmäßig */
                }
                .markdown-body a:hover {
                    text-decoration: underline !important; /* Nur beim Hovern unterstreichen */
                }
                .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
                    border-bottom-color: #30363d !important;
                    color: rgb(220, 220, 220) !important;
                }
                .markdown-body hr { background-color: #30363d !important; }
                .markdown-body blockquote {
                    color: #a0a0a0 !important;
                    border-left-color: #30363d !important;
                }
                .markdown-body table th, .markdown-body table td { border-color: #484f58 !important; }
                .markdown-body code:not(pre code) { /* Inline code */
                    background-color: rgb(50, 50, 50) !important;
                    border: 1px solid rgb(70, 70, 70) !important;
                    color: rgb(220, 220, 220) !important;
                }
                .markdown-body pre { /* Code block */
                    background-color: rgb(40, 42, 44) !important;
                    border: 1px solid rgb(60, 62, 64) !important;
                }
                .markdown-body pre code {
                     color: rgb(220, 220, 220) !important;
                }
                .markdown-body kbd {
                    background-color: rgb(50,50,50) !important;
                    border: 1px solid rgb(70,70,70) !important;
                    color: rgb(220,220,220) !important;
                    border-bottom-color: rgb(80,80,80) !important;
                }
                .markdown-body img { filter: brightness(.8) contrast(1.2); }
                
                /* Dark Mode Button Styling */
                .custom-play-button {
                    background-color: #444d56 !important; /* Dunklerer, weniger aufdringlicher Grauton */
                    color: #e1e4e8 !important; /* Helle Schrift für dunklen Hintergrund */
                    border: 1px solid #586069 !important; /* Dezenter Rand */
                }
                .custom-play-button:hover, .custom-play-button:focus {
                    background-color: #586069 !important; /* Etwas heller beim Hover */
                    color: #e1e4e8 !important;
                    border-color: #6a737d !important;
                }
                .custom-play-button a {
                    color: #e1e4e8 !important; /* Links erben die Button-Textfarbe */
                    text-decoration: none !important;
                }
                .custom-play-button a:hover {
                    color: #e1e4e8 !important; /* Auch beim Hover Button-Farbe beibehalten */
                    text-decoration: none !important;
                }
            `;
        } else { // Light Theme
            themeCss += `
                body {
                    background-color: #ffffff !important;
                    color: #24292e !important;
                }
                .markdown-body {
                    color: #24292e !important;
                }
                .markdown-body a { 
                    color: #0366d6 !important; 
                    text-decoration: none !important; /* Keine Unterstreichung standardmäßig */
                }
                .markdown-body a:hover {
                    text-decoration: underline !important; /* Nur beim Hovern unterstreichen */
                }
                .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
                    border-bottom-color: #eaecef !important;
                    color: #24292e !important;
                }
                .markdown-body hr { background-color: #e1e4e8 !important; }
                .markdown-body blockquote {
                    color: #6a737d !important;
                    border-left-color: #dfe2e5 !important;
                }
                .markdown-body table th, .markdown-body table td { border: 1px solid #dfe2e5 !important; }
                .markdown-body code:not(pre code) { /* Inline code */
                    background-color: rgba(27,31,35,.07) !important;
                    border: 1px solid rgba(27,31,35,.1) !important;
                    color: #24292e !important;
                }
                .markdown-body pre { /* Code block */
                    background-color: #f6f8fa !important;
                    border: 1px solid #eaecef !important;
                }
                 .markdown-body pre code {
                    color: #24292e !important;
                }
                .markdown-body kbd {
                    background-color: #fafbfc !important;
                    border: 1px solid #d1d5da !important;
                    border-bottom-color: #c6cbd1 !important;
                    color: #444d56 !important;
                }
                .markdown-body img { filter: none; }
                
                /* Light Mode Button Styling */
                .custom-play-button {
                    background-color: #f6f8fa !important; /* Heller, subtiler Grauton */
                    color: #24292e !important; /* Dunkle Schrift für hellen Hintergrund */
                    border: 1px solid #d1d5da !important; /* Dezenter Rand */
                }
                .custom-play-button:hover, .custom-play-button:focus {
                    background-color: #e1e4e8 !important; /* Etwas dunkler beim Hover */
                    color: #24292e !important;
                    border-color: #c6cbd1 !important;
                }
                .custom-play-button a {
                    color: #24292e !important; /* Links erben die Button-Textfarbe */
                    text-decoration: none !important;
                }
                .custom-play-button a:hover {
                    color: #24292e !important; /* Auch beim Hover Button-Farbe beibehalten */
                    text-decoration: none !important;
                }
            `;
        }
        addStyleElement(STYLE_ELEMENT_ID_THEME, themeCss);
    }

    // --- MENU COMMANDS ---
    GM_registerMenuCommand('Font: Serif', () => {
        GM_setValue(FONT_STYLE_KEY, 'serif');
        applyFontStyle();
    });

    GM_registerMenuCommand('Font: Sans-serif', () => {
        GM_setValue(FONT_STYLE_KEY, 'sans-serif');
        applyFontStyle();
    });

    GM_registerMenuCommand('Theme: System', () => {
        GM_setValue(THEME_KEY, 'system');
        applyThemeStyle();
    });

    GM_registerMenuCommand('Theme: Light', () => {
        GM_setValue(THEME_KEY, 'light');
        applyThemeStyle();
    });

    GM_registerMenuCommand('Theme: Dark', () => {
        GM_setValue(THEME_KEY, 'dark');
        applyThemeStyle();
    });

    // --- BASE STYLES ---
    function applyBaseStyles() {
        addStyleElement(STYLE_ELEMENT_ID_BASE, `
            body {
                margin: 0;
            }
            .markdown-body {
                box-sizing: border-box;
                min-width: 200px;
                max-width: 980px;
                margin: 0 auto;
                padding: 15px 30px 30px;
            }
            .markdown-body img {
                max-width: 100%; /* Korrigiert von 150% auf 100% */
                height: auto;
                display: block;
                margin-left: auto;
                margin-right: auto;
                border-radius: 3px;
            }
            .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
                margin-top: 1.8em;
                margin-bottom: 0.7em;
                padding-bottom: 0.3em; /* Für die untere Linie */
            }
            .markdown-body h1:hover, .markdown-body h2:hover, .markdown-body h3:hover, .markdown-body h4:hover, .markdown-body h5:hover, .markdown-body h6:hover {
                text-decoration: underline; /* Direkte Unterstreichung der Überschriften beim Hovern */
            }
            .markdown-body h1 { font-size: 2.1em; }
            .markdown-body h2 { font-size: 1.7em; }
            .markdown-body h3 { font-size: 1.4em; }
            .markdown-body h4 { font-size: 1.2em; }
            .markdown-body h5 { font-size: 1.05em; }
            .markdown-body h6 { font-size: 0.9em; }

            /* Code font family (colors/backgrounds are in theme) */
            .markdown-body code, .markdown-body kbd, .markdown-body samp, .markdown-body pre {
                font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
                font-size: 0.9em; /* Etwas kleiner für bessere Lesbarkeit im Fließtext */
            }
            .markdown-body pre {
                padding: 16px;
                overflow: auto;
                border-radius: 6px;
                line-height: 1.45;
            }
            .markdown-body code:not(pre code) { /* Inline code */
                padding: .2em .4em;
                margin: 0 .2em; /* Kleiner horizontaler Abstand */
                font-size: 88%; /* Relativ zur umgebenden Schriftgröße */
                border-radius: 3px;
            }
            .markdown-body kbd {
                padding: .2em .4em;
                margin: 0 .2em;
                font-size: 88%;
                border-radius: 3px;
            }
            .markdown-body ul, .markdown-body ol {
                padding-left: 2em; /* Standardeinzug für Listen */
            }
            .markdown-body table {
                display: block; /* Für Responsivität und Overflow */
                width: max-content; /* Passt sich dem Inhalt an, aber nicht breiter als Container */
                max-width: 100%;
                overflow: auto; /* Scrollbar bei Bedarf */
                border-spacing: 0;
                border-collapse: collapse;
                margin-top: 1em;
                margin-bottom: 1em;
            }
            .markdown-body table th, .markdown-body table td {
                padding: 6px 13px;
            }
            .markdown-body blockquote {
                margin-left: 0; /* Standard-Blockquote-Styling */
                margin-right: 0;
                padding: 0 1em; /* Innenabstand */
            }
            @media (max-width: 767px) {
                .markdown-body {
                    padding: 20px 15px 15px;
                }
                .markdown-body h1 { font-size: 1.8em; }
                .markdown-body h2 { font-size: 1.5em; }
                .markdown-body h3 { font-size: 1.3em; }
            }

            /* Custom Button Base Style */
            .custom-play-button {
                display: inline-block;
                padding: 10px 18px;
                text-decoration: none !important;
                border-radius: 5px;
                border: none;
                font-family: "Segoe UI", "SF Pro Text", "Helvetica Neue", "Ubuntu", "Arial", sans-serif;
                font-size: 0.95em;
                font-weight: 500;
                text-align: center;
                cursor: pointer;
                margin: 8px 0;
                transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
            }
            .custom-play-button a {
                color: inherit !important;
                text-decoration: none !important;
            }
        `);
    }

    // --- MAIN SCRIPT EXECUTION ---
    function initializeViewer() {
        applyBaseStyles();
        applyThemeStyle();
        applyFontStyle();

        // Listener für System-Theme-Änderungen
        if (GM_getValue(THEME_KEY, 'system') === 'system') {
            const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
            mediaQuery.removeEventListener('change', applyThemeStyle); // Vorsichtshalber entfernen
            mediaQuery.addEventListener('change', applyThemeStyle);
        }

        const markdownBodyDiv = document.createElement('div');
        markdownBodyDiv.className = 'markdown-body';

        // Überprüfen, ob marked durch @require geladen wurde
        if (typeof marked === 'undefined' || typeof marked.parse !== 'function') {
            console.error("Markdown Viewer: Marked.js library not loaded correctly via @require or 'parse' function is missing.");
            console.error("Markdown Viewer: typeof marked:", typeof marked);
            if (typeof marked !== 'undefined') {
                console.error("Markdown Viewer: marked properties:", Object.keys(marked));
                console.error("Markdown Viewer: typeof marked.parse:", typeof marked.parse);
            }
            markdownBodyDiv.innerHTML = `<p style="color:red; font-family:sans-serif;">Error: Marked.js library could not be loaded. Check console for details.</p>`;
            document.body.innerHTML = ''; // Vorhandenen Inhalt löschen
            document.body.appendChild(markdownBodyDiv);
            return;
        }

        try {
            let markdownContentToParse = "";
            if (document.contentType === 'text/markdown' ||
                (location.protocol === 'file:' && document.body && document.body.children.length === 1 && document.body.firstChild.tagName === 'PRE')) {
                markdownContentToParse = document.body.firstChild.innerText;
            } else if (document.body && document.body.innerText) {
                markdownContentToParse = document.body.innerText;
            } else if (document.body && document.body.textContent) {
                 markdownContentToParse = document.body.textContent;
            }

            const renderer = new marked.Renderer();
            const originalLinkRenderer = renderer.link;

            renderer.link = (href, title, text) => {
                const buttonPattern = /^\s*<kbd>\s*<br>\s*➡️ Play it right now in your browser\s*<br>\s*<\/kbd>\s*$/i;

                if (buttonPattern.test(text)) {
                    const buttonText = "➡️ Play it right now in your browser";
                    return `<a href="${href}" ${title ? `title="${title}"` : ''} class="custom-play-button" target="_blank" rel="noopener noreferrer">${buttonText}</a>`;
                }
                return originalLinkRenderer.call(renderer, href, title, text);
            };

            marked.use({ renderer });

            const htmlContent = marked.parse(markdownContentToParse);

            document.body.innerHTML = '';
            document.body.appendChild(markdownBodyDiv);
            markdownBodyDiv.innerHTML = htmlContent;

        } catch (e) {
            console.error("Markdown Viewer: Error during Markdown parsing:", e);
            markdownBodyDiv.innerHTML = `<p style="color:red; font-family:sans-serif;">Error rendering Markdown: ${e.message}. Check console for details.</p>`;
            if (!document.body.contains(markdownBodyDiv)) {
                 document.body.innerHTML = '';
                 document.body.appendChild(markdownBodyDiv);
            }
        }
    }

    // Stelle sicher, dass das DOM bereit ist, bevor es manipuliert wird
    if (document.readyState === "complete" || document.readyState === "interactive") {
        initializeViewer();
    } else {
        document.addEventListener("DOMContentLoaded", initializeViewer);
    }

})();