Skip to content

Instantly share code, notes, and snippets.

@volkanunsal
Last active November 9, 2025 07:07
Show Gist options
  • Select an option

  • Save volkanunsal/5d67c79c3c117bc81d1301a3a5e3d18e to your computer and use it in GitHub Desktop.

Select an option

Save volkanunsal/5d67c79c3c117bc81d1301a3a5e3d18e to your computer and use it in GitHub Desktop.
Copy Note to Markdown is a userscript that adds a button to copy the content of a note in markdown format in a Google NotebookLM note.

Copy Note to Markdown

Copy Note to Markdown is a userscript that adds a button to copy the content of a note in markdown format in a Google NotebookLM note.

Key Features

  • Adds a button to the top right corner of the note panel. Clicking this button copies the note content as markdown to the clipboard.
copy-button

Changelog

2025-11-07

  • Fix Editor not found errors (@heltonteixeira)
  • Disable lint checker for userscript file to avoid unnecessary warnings.

Getting Started

🔧 Installation

  1. Install Tampermonkey browser extension
  2. Click on the "Raw" button of the script below.
  3. Visit notebooklm.google.com to see the magic! ✨
/* eslint-disable */
// @ts-nocheck
// ==UserScript==
// @name Copy Note to Markdown
// @namespace https://github.com/volkanunsal
// @version 2025-11-07
// @description Copies the current note content as Markdown to clipboard
// @author Volkan Unsal
// @downloadURL https://gist.githubusercontent.com/volkanunsal/5d67c79c3c117bc81d1301a3a5e3d18e/raw/copy-note-to-markdown.user.js
// @updateURL https://gist.githubusercontent.com/volkanunsal/5d67c79c3c117bc81d1301a3a5e3d18e/raw/copy-note-to-markdown.user.js
// @match https://notebooklm.google.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant GM_addStyle
// ==/UserScript==
"use strict";(()=>{var P=class{constructor(e={}){this.namespace=e.namespace,this.prefix=[this.namespace,e.prefix].filter(Boolean).join(" "),this.enabled=e.enabled!==!1,this.timestamp=e.timestamp!==!1,this.timestampFormat=e.timestampFormat||"locale"}getTimestamp(){let e=new Date;return this.timestampFormat==="ISO"?e.toISOString():e.toLocaleString()}formatMessage(e,...t){let s=[this.prefix];return this.timestamp&&s.push(`[${this.getTimestamp()}]`),s.push(`[${e.toUpperCase()}]`),[s.join(" "),...t]}debug(...e){this.enabled&&console.debug(...this.formatMessage("debug",...e))}info(...e){this.enabled&&console.info(...this.formatMessage("info",...e))}warn(...e){this.enabled&&console.warn(...this.formatMessage("warn",...e))}error(...e){this.enabled&&console.error(...this.formatMessage("error",...e))}log(...e){this.enabled&&console.log(...this.formatMessage("log",...e))}custom(e,...t){this.enabled&&console.log(...this.formatMessage(e,...t))}group(e,t){if(!this.enabled)return t();console.group(`${this.prefix} ${e}`);try{t()}finally{console.groupEnd()}}groupCollapsed(e,t){if(!this.enabled)return t();console.groupCollapsed(`${this.prefix} ${e}`);try{t()}finally{console.groupEnd()}}table(e,t){this.enabled&&(this.info("Table data:"),console.table(e,t))}time(e){this.enabled&&console.time(`${this.prefix} ${e}`)}timeEnd(e){this.enabled&&console.timeEnd(`${this.prefix} ${e}`)}timeLog(e){this.enabled&&console.timeLog(`${this.prefix} ${e}`)}enable(){this.enabled=!0}disable(){this.enabled=!1}setPrefix(e){this.prefix=e}};function I(n={}){return new P(n)}function q(n,e){function t(s){return s.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,"")}if(window.trustedTypes){let r=window.trustedTypes.createPolicy("myHTMLPolicy",{createHTML:t}).createHTML(e);n.innerHTML=r}else{let s=t(e);n.innerHTML=s}}async function B(n){let{tagName:e,id:t,attributes:s={},parentSelector:r="body",parentElement:f,textContent:l,innerHTML:p,innerElement:m,returnElement:T=!0,checkExisting:N=!0,replaceExisting:D=!1,insertionMethod:y="append",contextElement:$,waitTimeout:k=3e4,persistent:M=!1,autoRemove:E=!0,debounceDelay:V=500,observerConfig:w={childList:!0,subtree:!0,attributes:!1,attributeOldValue:!1,characterData:!1,characterDataOldValue:!1},namespace:R,onMount:O}=n,o=I({prefix:"[createElement]",namespace:R}),a={action:"create",tagName:e,id:t,parentMethod:r?"selector":"element",parentIdentifier:r||(f?f.tagName:"unknown"),waitTimeout:k,insertionMethod:y};if(o.info(`Starting element creation: <${e}> with ID="${t}"${$?" (conditional)":""} using ${y} insertion`),!e||typeof e!="string")return o.error("Element creation failed: Tag parameter is required and must be a string",a),null;if(!t||typeof t!="string")return o.error("Element creation failed: ID parameter is required and must be a non-empty string",a),null;if(!r&&!f)return o.error("Element creation failed: Either parentSelector or parentElement must be provided",a),null;if(y!=="append"&&y!=="insertBeforeElement"&&y!=="prepend")return o.error("Element creation failed: insertionMethod must be 'append', 'insertBeforeElement', or 'prepend'",a),null;function S(c,u,i){try{let b=new DOMParser().parseFromString(u,"text/html"),g=b.querySelector("parsererror");if(g)throw new Error(`HTML parsing error: ${g.textContent}`);let C=Array.from(b.body.childNodes);return C.length===0?(o.warn(`No valid elements found in HTML string: "${u.substring(0,50)}${u.length>50?"...":""}"`,i),!1):(C.forEach(x=>{let H=c.ownerDocument.importNode(x,!0);c.appendChild(H)}),o.debug(`Created DOM elements programmatically: "${u.substring(0,50)}${u.length>50?"...":""}" (${C.length} elements)`,i),!0)}catch(h){o.warn(`DOMParser failed, attempting fallback method: ${h.message}`,i);try{return q(c,u),o.debug(`Successfully created DOM elements via fallback method: "${u.substring(0,50)}${u.length>50?"...":""}"`,i),!0}catch(b){return o.error(`DOM insertion failed: ${b.message}`,{...i,originalError:h.message,fallbackError:b.message}),!1}}}function L(){try{let c=null;if(N&&(c=document.getElementById(t),c))if(a.action="exists",D)o.info(`Found existing element with ID="${t}", replacing as requested`,a),c.remove(),a.action="replace";else return o.warn(`Element with ID="${t}" already exists, skipping creation. Use replaceExisting=true to replace.`,a),T?c:null;let u=f;if(!u&&r){let g=document.querySelector(r);if(!g)return o.error(`Element creation failed: Parent element not found with selector: ${r}`,a),null;u=g,o.debug(`Found parent element with selector: ${r}`,a)}if(!u)return o.error("Element creation failed: No valid parent element found",a),null;let i=document.createElement(e);o.debug(`Created DOM element: <${e}>`,a),i.setAttribute("id",t),o.debug(`Set required ID attribute: id="${t}"`,a);let h=Object.keys(s).length;if(h>0&&(Object.entries(s).forEach(([g,C])=>{C!=null&&(i.setAttribute(g,String(C)),o.debug(`Set attribute: ${g}="${C}"`,a))}),o.debug(`Applied ${h} additional attributes to element`,a)),m&&typeof m=="function")try{let g=m();g instanceof HTMLElement?(i.appendChild(g),o.debug("Inserted child element via innerElement callback",a)):(o.warn("innerElement callback did not return a valid HTMLElement, falling back to other content methods",a),l!==void 0?(i.textContent=l,o.debug(`Set textContent: "${l.substring(0,100)}${l.length>100?"...":""}"`,a)):p!==void 0&&S(i,p,a))}catch(g){o.error("Error executing innerElement callback, falling back to other content methods:",g,a),l!==void 0?(i.textContent=l,o.debug(`Set textContent: "${l.substring(0,100)}${l.length>100?"...":""}"`,a)):p!==void 0&&S(i,p,a)}else l!==void 0?(i.textContent=l,o.debug(`Set textContent: "${l.substring(0,100)}${l.length>100?"...":""}"`,a)):p!==void 0&&S(i,p,a);if(y==="insertBeforeElement")if(u.parentElement)u.parentElement.insertBefore(i,u),o.debug("Inserted element before parent",a);else return o.error("Element insertion failed: Parent element has no parent to insert before",a),null;else y==="prepend"?(u.insertBefore(i,u.firstChild),o.debug("Prepended element as first child of parent",a)):(u.appendChild(i),o.debug("Appended element to end of parent",a));if(O&&typeof O=="function")try{O(i),o.debug(`Successfully called onMount callback for element ID="${t}"`,a)}catch(g){o.warn(`Error in onMount callback for element ID="${t}":`,g,a)}let b=`Element creation successful: <${e}> (ID="${t}") ${a.action==="replace"?"replaced and ":""}${y}ed to ${a.parentMethod==="selector"?`parent selected by "${r}"`:"provided parent element"}${$?" (after content element)":""}`;return o.info(b,{...a,success:!0,hasAttributes:h>0,hasContent:!!(l||p||m),parentTagName:u.tagName,elementPath:i.tagName+"#"+i.id+(i.className?`.${i.className.replace(/\s+/g,".")}`:"")}),T?i:null}catch(c){let u=`Element creation failed with exception: ${c.message||"Unknown error"}`;return o.error(u,{...a,success:!1,error:c.message||"Unknown error",stack:c.stack||"No stack trace available"}),null}}async function A(){try{if(typeof $=="function"){let c=await $();return c instanceof HTMLElement&&document.contains(c)}else if(typeof $=="string")return document.querySelector($)!==null}catch(c){o.warn(`Error checking content element for element ID="${t}":`,c)}return!1}function _(){if(!M&&!E)return;o.info(`Setting up ${M?"persistent":""}${M&&E?" and ":""}${E?"auto-removal":""} monitoring for element ID="${t}"`);let c=null,u=new MutationObserver(async()=>{c&&clearTimeout(c),c=setTimeout(async()=>{o.debug(`Persistent monitoring check triggered for element ID="${t}"`);let h=await A(),b=document.getElementById(t);h?M&&!b?(o.info(`Context element found but managed element missing, recreating element ID="${t}"`),L()):o.debug(`Both context and managed elements exist for ID="${t}"`):E&&b?(o.info(`Context element no longer present, removing managed element ID="${t}"`),b.remove()):o.debug(`Context element no longer present for element ID="${t}"`)},V)}),i={childList:w.childList===!0,subtree:w.subtree===!0,attributes:w.attributes===!0,attributeOldValue:w.attributeOldValue===!0,characterData:w.characterData===!0,characterDataOldValue:w.characterDataOldValue===!0};u.observe(document.documentElement||document.body,i),o.debug(`Started persistent MutationObserver for element ID="${t}"`)}if(!$)return L();if(o.info(`Checking initial condition for element ID="${t}"`,{...a,waitCondition:typeof $=="function"?"callback":$,persistent:M}),await A()){o.info(`Initial content element already satisfied for element ID="${t}"`,a);let c=L();return(M||E)&&_(),c}return new Promise(c=>{let u=Date.now(),i=null,h=null,b=!1;function g(x){b||E&&(b=!0,i&&(clearTimeout(i),i=null),h&&(h.disconnect(),h=null,o.debug(`Disconnected MutationObserver for element ID="${t}"`,a)),c(x))}i=setTimeout(()=>{o.error(`Element creation failed: Wait condition timeout after ${k}ms for element ID="${t}"`,{...a,success:!1,timeoutReached:!0,waitCondition:typeof $=="function"?"callback":$}),g(null)},k),o.info(`Setting up MutationObserver for element ID="${t}"`,{...a,observerConfig:w,waitCondition:typeof $=="function"?"callback":$}),h=new MutationObserver(async x=>{if(!b&&(o.debug(`MutationObserver detected ${x.length} mutations for element ID="${t}"`,a),await A())){let H=Date.now()-u;o.info(`Wait condition satisfied via MutationObserver for element ID="${t}" after DOM changes`,{...a,elapsedTime:H});let W=L();(M||E)&&_(),g(W)}});let C={childList:w.childList===!0,subtree:w.subtree===!0,attributes:w.attributes===!0,attributeOldValue:w.attributeOldValue===!0,characterData:w.characterData===!0,characterDataOldValue:w.characterDataOldValue===!0};h.observe(document.documentElement||document.body,C),o.debug(`Started MutationObserver monitoring for element ID="${t}"`,{...a,target:"document.documentElement"})})}function j(n){if(!n||typeof n!="string")return"";let e=n.trim().replace(/\s+/g," ").replace(/<(\w+)[^>]*>\s*<\/\1>/g,"").replace(/(<\/(?:h[1-6]|p|div|ul|ol|li|blockquote)>)\s*(<(?:h[1-6]|p|div|ul|ol|li|blockquote))/g,`$1
$2`),t=document.createElement("div");return q(t,e),U(t).trim().replace(/\n{3,}/g,`
`).replace(/^\n+|\n+$/g,"").replace(/\*\*\s+\*\*/g,"").replace(/\*\s+\*/g,"").replace(/`\s*`/g,"").replace(/\[\s*\]\(\s*\)/g,"").replace(/[ \t]+$/gm,"").trim()}function U(n,e={listDepth:0,inList:!1}){if(!n)return"";if(n.nodeType===Node.TEXT_NODE){let t=n.textContent||"";return z(t)}if(n.nodeType===Node.ELEMENT_NODE){let t=n.tagName.toLowerCase(),r=Array.from(n.childNodes);switch(t){case"h1":return`# ${v(n)}
`;case"h2":return`## ${v(n)}
`;case"h3":return`### ${v(n)}
`;case"h4":return`#### ${v(n)}
`;case"h5":return`##### ${v(n)}
`;case"h6":return`###### ${v(n)}
`;case"p":let f=d(r,e);return f?`${f}
`:"";case"strong":case"b":return`**${d(r,e)}**`;case"em":case"i":return`*${d(r,e)}*`;case"code":return`\`${n.textContent||""}\``;case"pre":return`\`\`\`
${n.textContent||""}
\`\`\`
`;case"blockquote":return d(r,e).split(`
`).map(E=>`> ${E}`).join(`
`)+`
`;case"ul":return G(r,e);case"ol":return X(r,e);case"li":return F(r,e);case"a":let T=n.getAttribute("href"),N=d(r,e);return T?`[${N}](${T})`:N;case"img":let D=n.getAttribute("src"),y=n.getAttribute("alt")||"";return D?`![${y}](${D})`:"";case"br":return`
`;case"hr":return`
---
`;case"table":return K(n,e);case"div":case"span":case"section":case"article":if(n.className&&n.className.includes("ql-indent")){let E=J(n.className);return d(r,{...e,listDepth:E})}return d(r,e);case"kbd":return`\`${v(n)}\``;case"del":case"s":return`~~${d(r,e)}~~`;case"sup":return`^${d(r,e)}^`;case"sub":return`~${d(r,e)}~`;case"mark":return`**${d(r,e)}**`;case"u":return d(r,e);case"q":return`"${d(r,e)}"`;case"abbr":case"acronym":let k=n.getAttribute("title"),M=d(r,e);return k?`${M} (${k})`:M;case"time":return d(r,e);case"small":return d(r,e);case"cite":return`*${d(r,e)}*`;case"address":return d(r,e)+`
`;case"details":return d(r,e)+`
`;case"summary":return`**${d(r,e)}**
`;case"figure":return d(r,e)+`
`;case"figcaption":return`*${d(r,e)}*
`;default:return d(r,e)}}return""}function d(n,e){return n.map(t=>U(t,e)).join("")}function v(n){return(n.textContent||"").replace(/\s+/g," ").trim()}function z(n,e=!1){if(!n)return"";let t=n.replace(/\s+/g," ");return!e&&t&&(t=t.replace(/([\\`])/g,"\\$1")),t}function G(n,e){let t={...e,inList:!0,listDepth:e.listDepth+1},s=n.filter(r=>r.nodeType===Node.ELEMENT_NODE&&r.tagName.toLowerCase()==="li").map(r=>{let f=0;if(r.className){let D=r.className.match(/ql-indent-(\d+)/);D&&(f=parseInt(D[1],10))}let l=Math.max(0,e.listDepth+f),p=" ".repeat(l),T=Array.from(r.childNodes),N=F(T,t);return`${p}- ${N}`}).filter(r=>r.trim());return s.length>0?s.join(`
`)+`
`:""}function X(n,e){let t={...e,inList:!0,listDepth:e.listDepth+1},s=n.filter(r=>r.nodeType===Node.ELEMENT_NODE&&r.tagName.toLowerCase()==="li").map((r,f)=>{let l=0;if(r.className){let y=r.className.match(/ql-indent-(\d+)/);y&&(l=parseInt(y[1],10))}let p=Math.max(0,e.listDepth+l),m=" ".repeat(p),N=Array.from(r.childNodes),D=F(N,t);return`${m}${f+1}. ${D}`}).filter(r=>r.trim());return s.length>0?s.join(`
`)+`
`:""}function F(n,e){return d(n,e).replace(/^\s+|\s+$/g,"").replace(/\n\n+/g,`
`)}function J(n){let e=n.match(/ql-indent-(\d+)/);return e?parseInt(e[1],10):0}function K(n,e){let t=Array.from(n.querySelectorAll("tr"));if(t.length===0)return"";let s=t.map(r=>"| "+Array.from(r.querySelectorAll("td, th")).map(l=>{let m=Array.from(l.childNodes);return d(m,e).trim()}).join(" | ")+" |");if(t[0]&&t[0].querySelector("th")){let r=t[0].querySelectorAll("th").length,f="| "+Array(r).fill("---").join(" | ")+" |";s.splice(1,0,f)}return s.join(`
`)+`
`}(function(){"use strict";GM_addStyle(`
button#copy-note-to-markdown-btn {
background-color: #fff !important;
margin-right: 4px !important;
}
button#copy-note-to-markdown-btn:hover {
background-color: #f0f0f0 !important;
}
button#copy-note-to-markdown-btn:active {
background-color: #e0e0e0 !important;
}
.note-header__notices {
display: flex;
justify-content: space-between;
align-items: center;
}
`);let n=I({prefix:"UserScript",namespace:"[CopyNoteToMarkdown]"});B({tagName:"button",namespace:"[CopyNoteToMarkdown]",id:"copy-note-to-markdown-btn",insertionMethod:"append",autoRemove:!1,attributes:{title:"Copy Note as Markdown",style:"all:unset;cursor:pointer;display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:4px;padding:2px;"},parentSelector:".note-header__notices.note-header__notices-3panel.ng-star-inserted",contextElement:"note-editor",innerHTML:`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-copy" style="width:20px;height:20px;">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>`,onMount:e=>{e.addEventListener("click",async()=>{try{let t=document.querySelector(".editor.ql-container");if(!t){n.error("Editor not found");return}let s=t.innerHTML,r=j(s);await navigator.clipboard.writeText(r),n.log("Note content copied as Markdown to clipboard")}catch(t){n.error("Failed to copy note content:",t)}try{n.debug("Searching for editor element...");let t=null;if(t=document.querySelector(".editor.ql-container"),t)n.info("Found editor with original selector '.editor.ql-container'");else{if(n.error("Original selector '.editor.ql-container' not found"),t=document.querySelector("labs-tailwind-doc-viewer"),t)n.info("Found editor with selector 'labs-tailwind-doc-viewer'");else{let f=["labs-tailwind-doc-viewer","note-editor labs-tailwind-doc-viewer",".ql-editor",".editor","[contenteditable='true']",".note-content",".note-editor",".rich-text-editor",".prosemirror-editor","form labs-tailwind-doc-viewer","note-editor form labs-tailwind-doc-viewer"];for(let l of f){let p=document.querySelector(l);if(p){n.info(`Found editor with alternative selector: ${l}`),t=p;break}}}if(!t){let f=document.querySelectorAll('[class*="editor"]');n.debug(`Found ${f.length} elements with 'editor' in class:`,Array.from(f).map(m=>({tagName:m.tagName,className:m.className,id:m.id})));let l=document.querySelectorAll('[class*="ql-container"]');n.debug(`Found ${l.length} elements with 'ql-container' in class:`,Array.from(l).map(m=>({tagName:m.tagName,className:m.className,id:m.id})));let p=document.querySelectorAll("labs-tailwind-doc-viewer");n.debug(`Found ${p.length} labs-tailwind-doc-viewer elements:`,Array.from(p).map(m=>({tagName:m.tagName,className:m.className,id:m.id,innerHTML:m.innerHTML.substring(0,100)+"..."})))}if(!t){n.error("Editor not found with any selector");return}}let s=t.innerHTML;t.tagName.toLowerCase()==="labs-tailwind-doc-viewer"&&(s=(t.querySelector(".ql-editor")||t.querySelector('[contenteditable="true"]')||t.querySelector(".editor")||t).innerHTML);let r=j(s);await navigator.clipboard.writeText(r),n.log("Note content copied as Markdown to clipboard")}catch(t){n.error("Failed to copy note content:",t)}})}})})();})();
@heltonteixeira
Copy link

I think there may have some changes in the NotebookLM page that caused the script to not work anymore, it also had some lint errors.
I have fixed it, keep the credits: https://gist.github.com/heltonteixeira/116237abb54e2b55d519056a31d5d6b4

@volkanunsal
Copy link
Author

I think there may have some changes in the NotebookLM page that caused the script to not work anymore, it also had some lint errors. I have fixed it, keep the credits: heltonteixeira/116237abb54e2b55d519056a31d5d6b4

Many thanks. I wrote the original in Typescript, so I'm going to copy your changes to the onMount hook to the original and compile it again. The lint errors are probably happening because of the minification. I'll add a comment to disable lint checker in Tampermonkey output. Thanks again.

@heltonteixeira
Copy link

After some more testing, I noticed that there are still some issues, I was trying to work it out but didn't have much lucky.

@heltonteixeira
Copy link

I think I figured out some of the issues, I think the only selectors that were working for content were labs-tailwind-doc-viewer and ql-editor, I could not find any others.
I have updated my version, and also improved the markdown formatting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment