Skip to content

Instantly share code, notes, and snippets.

@volkanunsal
Last active October 3, 2025 11:38
Show Gist options
  • Save volkanunsal/5d67c79c3c117bc81d1301a3a5e3d18e to your computer and use it in GitHub Desktop.
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

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! ✨
// @ts-nocheck
// ==UserScript==
// @name Copy Note to Markdown
// @namespace https://github.com/volkanunsal
// @version 2025-09-09
// @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 u=[this.prefix];return this.timestamp&&u.push(`[${this.getTimestamp()}]`),u.push(`[${e.toUpperCase()}]`),[u.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 v(r={}){return new P(r)}function O(r,e){function t(u){return u.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,"")}if(window.trustedTypes){let n=window.trustedTypes.createPolicy("myHTMLPolicy",{createHTML:t}).createHTML(e);r.innerHTML=n}else{let u=t(e);r.innerHTML=u}}async function B(r){let{tagName:e,id:t,attributes:u={},parentSelector:n="body",parentElement:m,textContent:d,innerHTML:b,innerElement:w,returnElement:T=!0,checkExisting:k=!0,replaceExisting:C=!1,insertionMethod:$="append",contextElement:h,waitTimeout:I=3e4,persistent:M=!1,autoRemove:E=!0,debounceDelay:_=500,observerConfig:y={childList:!0,subtree:!0,attributes:!1,attributeOldValue:!1,characterData:!1,characterDataOldValue:!1},namespace:R,onMount:S}=r,a=v({prefix:"[createElement]",namespace:R}),o={action:"create",tagName:e,id:t,parentMethod:n?"selector":"element",parentIdentifier:n||(m?m.tagName:"unknown"),waitTimeout:I,insertionMethod:$};if(a.info(`Starting element creation: <${e}> with ID="${t}"${h?" (conditional)":""} using ${$} insertion`),!e||typeof e!="string")return a.error("Element creation failed: Tag parameter is required and must be a string",o),null;if(!t||typeof t!="string")return a.error("Element creation failed: ID parameter is required and must be a non-empty string",o),null;if(!n&&!m)return a.error("Element creation failed: Either parentSelector or parentElement must be provided",o),null;if($!=="append"&&$!=="insertBeforeElement"&&$!=="prepend")return a.error("Element creation failed: insertionMethod must be 'append', 'insertBeforeElement', or 'prepend'",o),null;function q(i,l,s){try{let g=new DOMParser().parseFromString(l,"text/html"),f=g.querySelector("parsererror");if(f)throw new Error(`HTML parsing error: ${f.textContent}`);let D=Array.from(g.body.childNodes);return D.length===0?(a.warn(`No valid elements found in HTML string: "${l.substring(0,50)}${l.length>50?"...":""}"`,s),!1):(D.forEach(N=>{let A=i.ownerDocument.importNode(N,!0);i.appendChild(A)}),a.debug(`Created DOM elements programmatically: "${l.substring(0,50)}${l.length>50?"...":""}" (${D.length} elements)`,s),!0)}catch(p){a.warn(`DOMParser failed, attempting fallback method: ${p.message}`,s);try{return O(i,l),a.debug(`Successfully created DOM elements via fallback method: "${l.substring(0,50)}${l.length>50?"...":""}"`,s),!0}catch(g){return a.error(`DOM insertion failed: ${g.message}`,{...s,originalError:p.message,fallbackError:g.message}),!1}}}function L(){try{let i=null;if(k&&(i=document.getElementById(t),i))if(o.action="exists",C)a.info(`Found existing element with ID="${t}", replacing as requested`,o),i.remove(),o.action="replace";else return a.warn(`Element with ID="${t}" already exists, skipping creation. Use replaceExisting=true to replace.`,o),T?i:null;let l=m;if(!l&&n){let f=document.querySelector(n);if(!f)return a.error(`Element creation failed: Parent element not found with selector: ${n}`,o),null;l=f,a.debug(`Found parent element with selector: ${n}`,o)}if(!l)return a.error("Element creation failed: No valid parent element found",o),null;let s=document.createElement(e);a.debug(`Created DOM element: <${e}>`,o),s.setAttribute("id",t),a.debug(`Set required ID attribute: id="${t}"`,o);let p=Object.keys(u).length;if(p>0&&(Object.entries(u).forEach(([f,D])=>{D!=null&&(s.setAttribute(f,String(D)),a.debug(`Set attribute: ${f}="${D}"`,o))}),a.debug(`Applied ${p} additional attributes to element`,o)),w&&typeof w=="function")try{let f=w();f instanceof HTMLElement?(s.appendChild(f),a.debug("Inserted child element via innerElement callback",o)):(a.warn("innerElement callback did not return a valid HTMLElement, falling back to other content methods",o),d!==void 0?(s.textContent=d,a.debug(`Set textContent: "${d.substring(0,100)}${d.length>100?"...":""}"`,o)):b!==void 0&&q(s,b,o))}catch(f){a.error("Error executing innerElement callback, falling back to other content methods:",f,o),d!==void 0?(s.textContent=d,a.debug(`Set textContent: "${d.substring(0,100)}${d.length>100?"...":""}"`,o)):b!==void 0&&q(s,b,o)}else d!==void 0?(s.textContent=d,a.debug(`Set textContent: "${d.substring(0,100)}${d.length>100?"...":""}"`,o)):b!==void 0&&q(s,b,o);if($==="insertBeforeElement")if(l.parentElement)l.parentElement.insertBefore(s,l),a.debug("Inserted element before parent",o);else return a.error("Element insertion failed: Parent element has no parent to insert before",o),null;else $==="prepend"?(l.insertBefore(s,l.firstChild),a.debug("Prepended element as first child of parent",o)):(l.appendChild(s),a.debug("Appended element to end of parent",o));if(S&&typeof S=="function")try{S(s),a.debug(`Successfully called onMount callback for element ID="${t}"`,o)}catch(f){a.warn(`Error in onMount callback for element ID="${t}":`,f,o)}let g=`Element creation successful: <${e}> (ID="${t}") ${o.action==="replace"?"replaced and ":""}${$}ed to ${o.parentMethod==="selector"?`parent selected by "${n}"`:"provided parent element"}${h?" (after content element)":""}`;return a.info(g,{...o,success:!0,hasAttributes:p>0,hasContent:!!(d||b||w),parentTagName:l.tagName,elementPath:s.tagName+"#"+s.id+(s.className?`.${s.className.replace(/\s+/g,".")}`:"")}),T?s:null}catch(i){let l=`Element creation failed with exception: ${i.message||"Unknown error"}`;return a.error(l,{...o,success:!1,error:i.message||"Unknown error",stack:i.stack||"No stack trace available"}),null}}async function H(){try{if(typeof h=="function"){let i=await h();return i instanceof HTMLElement&&document.contains(i)}else if(typeof h=="string")return document.querySelector(h)!==null}catch(i){a.warn(`Error checking content element for element ID="${t}":`,i)}return!1}function V(){if(!M&&!E)return;a.info(`Setting up ${M?"persistent":""}${M&&E?" and ":""}${E?"auto-removal":""} monitoring for element ID="${t}"`);let i=null,l=new MutationObserver(async()=>{i&&clearTimeout(i),i=setTimeout(async()=>{a.debug(`Persistent monitoring check triggered for element ID="${t}"`);let p=await H(),g=document.getElementById(t);p?M&&!g?(a.info(`Context element found but managed element missing, recreating element ID="${t}"`),L()):a.debug(`Both context and managed elements exist for ID="${t}"`):E&&g?(a.info(`Context element no longer present, removing managed element ID="${t}"`),g.remove()):a.debug(`Context element no longer present for element ID="${t}"`)},_)}),s={childList:y.childList===!0,subtree:y.subtree===!0,attributes:y.attributes===!0,attributeOldValue:y.attributeOldValue===!0,characterData:y.characterData===!0,characterDataOldValue:y.characterDataOldValue===!0};l.observe(document.documentElement||document.body,s),a.debug(`Started persistent MutationObserver for element ID="${t}"`)}if(!h)return L();if(a.info(`Checking initial condition for element ID="${t}"`,{...o,waitCondition:typeof h=="function"?"callback":h,persistent:M}),await H()){a.info(`Initial content element already satisfied for element ID="${t}"`,o);let i=L();return(M||E)&&V(),i}return new Promise(i=>{let l=Date.now(),s=null,p=null,g=!1;function f(N){g||E&&(g=!0,s&&(clearTimeout(s),s=null),p&&(p.disconnect(),p=null,a.debug(`Disconnected MutationObserver for element ID="${t}"`,o)),i(N))}s=setTimeout(()=>{a.error(`Element creation failed: Wait condition timeout after ${I}ms for element ID="${t}"`,{...o,success:!1,timeoutReached:!0,waitCondition:typeof h=="function"?"callback":h}),f(null)},I),a.info(`Setting up MutationObserver for element ID="${t}"`,{...o,observerConfig:y,waitCondition:typeof h=="function"?"callback":h}),p=new MutationObserver(async N=>{if(!g&&(a.debug(`MutationObserver detected ${N.length} mutations for element ID="${t}"`,o),await H())){let A=Date.now()-l;a.info(`Wait condition satisfied via MutationObserver for element ID="${t}" after DOM changes`,{...o,elapsedTime:A});let W=L();(M||E)&&V(),f(W)}});let D={childList:y.childList===!0,subtree:y.subtree===!0,attributes:y.attributes===!0,attributeOldValue:y.attributeOldValue===!0,characterData:y.characterData===!0,characterDataOldValue:y.characterDataOldValue===!0};p.observe(document.documentElement||document.body,D),a.debug(`Started MutationObserver monitoring for element ID="${t}"`,{...o,target:"document.documentElement"})})}function F(r){if(!r||typeof r!="string")return"";let e=r.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 O(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(r,e={listDepth:0,inList:!1}){if(!r)return"";if(r.nodeType===Node.TEXT_NODE){let t=r.textContent||"";return z(t)}if(r.nodeType===Node.ELEMENT_NODE){let t=r.tagName.toLowerCase(),n=Array.from(r.childNodes);switch(t){case"h1":return`# ${x(r)}
`;case"h2":return`## ${x(r)}
`;case"h3":return`### ${x(r)}
`;case"h4":return`#### ${x(r)}
`;case"h5":return`##### ${x(r)}
`;case"h6":return`###### ${x(r)}
`;case"p":let m=c(n,e);return m?`${m}
`:"";case"strong":case"b":return`**${c(n,e)}**`;case"em":case"i":return`*${c(n,e)}*`;case"code":return`\`${r.textContent||""}\``;case"pre":return`\`\`\`
${r.textContent||""}
\`\`\`
`;case"blockquote":return c(n,e).split(`
`).map(E=>`> ${E}`).join(`
`)+`
`;case"ul":return G(n,e);case"ol":return X(n,e);case"li":return j(n,e);case"a":let T=r.getAttribute("href"),k=c(n,e);return T?`[${k}](${T})`:k;case"img":let C=r.getAttribute("src"),$=r.getAttribute("alt")||"";return C?`![${$}](${C})`:"";case"br":return`
`;case"hr":return`
---
`;case"table":return K(r,e);case"div":case"span":case"section":case"article":if(r.className&&r.className.includes("ql-indent")){let E=J(r.className);return c(n,{...e,listDepth:E})}return c(n,e);case"kbd":return`\`${x(r)}\``;case"del":case"s":return`~~${c(n,e)}~~`;case"sup":return`^${c(n,e)}^`;case"sub":return`~${c(n,e)}~`;case"mark":return`**${c(n,e)}**`;case"u":return c(n,e);case"q":return`"${c(n,e)}"`;case"abbr":case"acronym":let I=r.getAttribute("title"),M=c(n,e);return I?`${M} (${I})`:M;case"time":return c(n,e);case"small":return c(n,e);case"cite":return`*${c(n,e)}*`;case"address":return c(n,e)+`
`;case"details":return c(n,e)+`
`;case"summary":return`**${c(n,e)}**
`;case"figure":return c(n,e)+`
`;case"figcaption":return`*${c(n,e)}*
`;default:return c(n,e)}}return""}function c(r,e){return r.map(t=>U(t,e)).join("")}function x(r){return(r.textContent||"").replace(/\s+/g," ").trim()}function z(r,e=!1){if(!r)return"";let t=r.replace(/\s+/g," ");return!e&&t&&(t=t.replace(/([\\`])/g,"\\$1")),t}function G(r,e){let t={...e,inList:!0,listDepth:e.listDepth+1},u=r.filter(n=>n.nodeType===Node.ELEMENT_NODE&&n.tagName.toLowerCase()==="li").map(n=>{let m=0;if(n.className){let C=n.className.match(/ql-indent-(\d+)/);C&&(m=parseInt(C[1],10))}let d=Math.max(0,e.listDepth+m),b=" ".repeat(d),T=Array.from(n.childNodes),k=j(T,t);return`${b}- ${k}`}).filter(n=>n.trim());return u.length>0?u.join(`
`)+`
`:""}function X(r,e){let t={...e,inList:!0,listDepth:e.listDepth+1},u=r.filter(n=>n.nodeType===Node.ELEMENT_NODE&&n.tagName.toLowerCase()==="li").map((n,m)=>{let d=0;if(n.className){let $=n.className.match(/ql-indent-(\d+)/);$&&(d=parseInt($[1],10))}let b=Math.max(0,e.listDepth+d),w=" ".repeat(b),k=Array.from(n.childNodes),C=j(k,t);return`${w}${m+1}. ${C}`}).filter(n=>n.trim());return u.length>0?u.join(`
`)+`
`:""}function j(r,e){return c(r,e).replace(/^\s+|\s+$/g,"").replace(/\n\n+/g,`
`)}function J(r){let e=r.match(/ql-indent-(\d+)/);return e?parseInt(e[1],10):0}function K(r,e){let t=Array.from(r.querySelectorAll("tr"));if(t.length===0)return"";let u=t.map(n=>"| "+Array.from(n.querySelectorAll("td, th")).map(d=>{let w=Array.from(d.childNodes);return c(w,e).trim()}).join(" | ")+" |");if(t[0]&&t[0].querySelector("th")){let n=t[0].querySelectorAll("th").length,m="| "+Array(n).fill("---").join(" | ")+" |";u.splice(1,0,m)}return u.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 r=v({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){r.error("Editor not found");return}let u=t.innerHTML,n=F(u);await navigator.clipboard.writeText(n),r.log("Note content copied as Markdown to clipboard")}catch(t){r.error("Failed to copy note content:",t)}})}})})();})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment