NotebookLM Robust KaTeX Renderer v2.0

Render LaTeX in NotebookLM using KaTeX, with support for multi-node math and TrustedHTML policy safety.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         NotebookLM Robust KaTeX Renderer v2.0
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Render LaTeX in NotebookLM using KaTeX, with support for multi-node math and TrustedHTML policy safety.
// @match        https://notebooklm.google.com/*
// @grant        none
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js
// @resource     KATEX_CSS https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    // Load KaTeX CSS
    const katexCSS = document.createElement('link');
    katexCSS.rel = 'stylesheet';
    katexCSS.href = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css';
    document.head.appendChild(katexCSS);

    const mathPattern = /\$\$(.+?)\$\$|\$(.+?)\$/gs;

    function extractMathExpressions(text) {
        return Array.from(text.matchAll(mathPattern)).map(m => m[0]);
    }

    function safeRenderMath(text) {
        const container = document.createDocumentFragment();
        let lastIndex = 0;

        for (const match of text.matchAll(mathPattern)) {
            const [full, displayExpr, inlineExpr] = match;
            const index = match.index;

            if (index > lastIndex) {
                container.appendChild(document.createTextNode(text.slice(lastIndex, index)));
            }

            const expr = (displayExpr || inlineExpr).trim();
            const isDisplay = !!displayExpr;
            const el = document.createElement(isDisplay ? 'div' : 'span');

            try {
                katex.render(expr, el, {
                    displayMode: isDisplay,
                    throwOnError: false
                });
            } catch (err) {
                console.error("KaTeX render error:", expr, err);
                el.textContent = full;
            }

            container.appendChild(el);
            lastIndex = index + full.length;
        }

        if (lastIndex < text.length) {
            container.appendChild(document.createTextNode(text.slice(lastIndex)));
        }

        return container;
    }

    function combineAndRender(container) {
        // 再帰的にテキストを結合して、$$ ... $$ の範囲を検出
        const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
        const textNodes = [];
        let buffer = '';
        let node;

        while ((node = walker.nextNode())) {
            textNodes.push(node);
            buffer += node.textContent;
        }

        const matches = [...buffer.matchAll(mathPattern)];
        if (matches.length === 0) return false;

        // 1つずつ置換していく
        let offset = 0;
        for (const match of matches) {
            const [full, displayExpr, inlineExpr] = match;
            const expr = (displayExpr || inlineExpr).trim();
            const isDisplay = !!displayExpr;
            const start = match.index;
            const end = start + full.length;

            // 開始・終了位置に該当する textNode を特定
            let startNodeIndex = 0, endNodeIndex = 0, pos = 0;
            for (let i = 0; i < textNodes.length; i++) {
                const len = textNodes[i].textContent.length;
                if (pos <= start) startNodeIndex = i;
                if (pos + len >= end) { endNodeIndex = i; break; }
                pos += len;
            }

            // 対象ノード群をまとめて置き換える
            const el = document.createElement(isDisplay ? 'div' : 'span');
            try {
                katex.render(expr, el, { displayMode: isDisplay, throwOnError: false });
            } catch (err) {
                console.error("KaTeX render error:", expr, err);
                el.textContent = full;
            }

            const firstNode = textNodes[startNodeIndex];
            const lastNode = textNodes[endNodeIndex];
            const range = document.createRange();
            range.setStartBefore(firstNode);
            range.setEndAfter(lastNode);
            range.deleteContents();
            range.insertNode(el);
        }

        return true;
    }

    function scanAndRender() {
        const cardContents = document.querySelectorAll('mat-card-content');

        cardContents.forEach(card => {
            const blocks = card.querySelectorAll('div.paragraph');

            blocks.forEach(block => {
                if (block.dataset.katexRendered === 'true') return;

                const success = combineAndRender(block);
                if (success) {
                    block.dataset.katexRendered = 'true';
                }
            });
        });
    }

    setInterval(scanAndRender, 1000);
})();