您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一键复制 Notion 页面内容为标准 Markdown 格式。
- // ==UserScript==
- // @name copy-notion-page-content-as-markdown
- // @name:en Copy Notion Page Content AS Markdown
- // @name:zh-CN 一键复制 Notion 页面内容为标准 Markdown 格式
- // @namespace https://github.com/Seven-Steven/tampermonkey-scripts/tree/main/copy-notion-page-content-as-markdown
- // @supportURL https://github.com/Seven-Steven/tampermonkey-scripts/issues
- // @description 一键复制 Notion 页面内容为标准 Markdown 格式。
- // @description:zh-CN 一键复制 Notion 页面内容为标准 Markdown 格式。
- // @description:en Copy Notion Page Content AS Markdown.
- // @version 2.2
- // @license MIT
- // @author Seven
- // @homepage https://blog.diqigan.cn
- // @match *://www.notion.so/*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=notion.so
- // ==/UserScript==
- (function () {
- 'use strict';
- /**
- * 复制按钮 ID
- */
- const DOM_ID_OF_COPY_BUTTON = 'tamper-monkey-plugin-copy-notion-content-as-markdown-copy-button';
- /**
- * Notion 页面祖先节点 Selector
- */
- const DOM_SELECTOR_NOTION_PAGE_ANCESTOR = '#notion-app';
- /**
- * 公共的 Notion Page Content Selector
- */
- const DOM_SELECTOR_NOTION_PAGE_CONTENT_COMMON = `${DOM_SELECTOR_NOTION_PAGE_ANCESTOR} .notion-page-content`;
- /**
- * 普通页面的 Notion Page Content Selector
- */
- const DOM_SELECTOR_NOTION_PAGE_CONTENT_NORMAL = `${DOM_SELECTOR_NOTION_PAGE_ANCESTOR} main.notion-frame .notion-page-content`;
- /**
- * 插件挂载状态
- */
- let PLUGIN_MOUNT_STATUS = false;
- init();
- /**
- * 初始化动作
- */
- function init() {
- console.log('init TamperMonkey plugin: Copy Notion Content AS Markdown.');
- const mountPlugin = () => {
- console.log('find Notion Page, mount Plugin directly.');
- onMount();
- };
- waitForElements(10000, 1000, DOM_SELECTOR_NOTION_PAGE_CONTENT_COMMON)
- // 对于 Notion Page 页面,直接初始化插件就好
- .then(mountPlugin).catch(() => { });
- waitForElements(10000, 1000, DOM_SELECTOR_NOTION_PAGE_CONTENT_NORMAL)
- .then(mountPlugin)
- // 对于 DataBase / View 等其他页面,需要监听 DOM 节点变化判断当前页面有没有 Notion Page Content DOM,进而装载 / 卸载插件
- .catch(() => {
- console.log('can not find notion page, add observe for ancestor.');
- autoMountOrUmountPluginByObserverFor(DOM_SELECTOR_NOTION_PAGE_ANCESTOR)
- });
- }
- /**
- * 监听指定 DOM 的子节点变化,并根据子节点变化动态装载 / 卸载插件
- * @param {string} selector 节点选择器
- */
- const autoMountOrUmountPluginDebounce = debounce(autoMountOrUmountPlugin, 500);
- function autoMountOrUmountPluginByObserverFor(selector) {
- const ancestorDOM = document.querySelector(selector);
- if (!ancestorDOM) {
- console.error('Ancestor DOM of Notion Page does not exist!');
- return;
- }
- // 创建 MutationObserver 实例,监听页面节点变化
- const observer = new MutationObserver(mutations => {
- for (let mutation of mutations) {
- if (mutation.type === 'childList') {
- // 在页面节点子元素发生变化时,根据条件挂载/卸载插件
- autoMountOrUmountPluginDebounce();
- break;
- }
- }
- });
- // 配置 MutationObserver 监听选项
- const observerConfig = {
- childList: true,
- subtree: true,
- characterData: false,
- attributes: false,
- };
- // 开始监听页面节点变化
- observer.observe(ancestorDOM, observerConfig);
- }
- /**
- * 装载/卸载插件
- */
- function autoMountOrUmountPlugin() {
- console.log('auto solve plugin...');
- waitForElements(500, 100, DOM_SELECTOR_NOTION_PAGE_CONTENT_COMMON).then(() => {
- console.log('Find Notion Page Content, begin to mount plugin......');
- onMount();
- }).catch(() => {
- console.log('Can not find Notion Page Content, begin to umount plugin......');
- onUmount();
- })
- }
- /**
- * 装载插件
- */
- function onMount() {
- if (PLUGIN_MOUNT_STATUS) {
- console.log('Plugin already mounted, return.');
- return;
- }
- initCopyButton();
- window.addEventListener('copy', fixNotionMarkdownInClipboard);
- PLUGIN_MOUNT_STATUS = true;
- console.log('Plugin Mounted.');
- }
- /**
- * 卸载插件
- */
- function onUmount() {
- if (!PLUGIN_MOUNT_STATUS) {
- console.log('Plugin not mounted, return.');
- return;
- }
- removeCopyButton();
- window.removeEventListener('copy', fixNotionMarkdownInClipboard);
- PLUGIN_MOUNT_STATUS = false;
- console.log('Plugin UnMounted.');
- }
- /**
- * 修正剪切板中的 Notion Markdown 文本格式
- */
- function fixNotionMarkdownInClipboard() {
- navigator.clipboard.readText().then(text => {
- const markdown = fixMarkdownFormat(text);
- navigator.clipboard.writeText(markdown).then(() => {
- showMessage('复制成功');
- }, () => {
- console.log('failed.');
- })
- })
- }
- /**
- * 修正 markdown 格式
- */
- function fixMarkdownFormat(markdown) {
- if (!markdown) {
- return;
- }
- // 给没有 Caption 的图片添加默认 ALT 文字
- markdown = markdown.replaceAll(/^!(http\S+)$/gm, (match, imgUrl) => {
- return ``;
- });
- // 给有 Caption 的图片去除多余文字
- const captionRegex = /(\!\[(?<title>.+?)\]\(.*?\)\s*)\k<title>\s*/g;
- return markdown.replaceAll(captionRegex, '$1');
- }
- /**
- * 初始化复制按钮
- */
- function initCopyButton() {
- const copyButton = document.createElement('div');
- copyButton.style.position = 'fixed';
- copyButton.style.width = '80px';
- copyButton.style.height = '22px';
- copyButton.style.lineHeight = '22px';
- copyButton.style.top = '14%';
- copyButton.style.right = '1%';
- copyButton.style.background = '#0084ff';
- copyButton.style.fontSize = '14px';
- copyButton.style.color = '#fff';
- copyButton.style.textAlign = 'center';
- copyButton.style.borderRadius = '6px';
- copyButton.style.zIndex = 10000;
- copyButton.style.cursor = 'pointer';
- copyButton.style.opacity = 0.7;
- copyButton.innerHTML = '复制内容';
- copyButton.id = DOM_ID_OF_COPY_BUTTON;
- copyButton.addEventListener('click', copyNotionPageContent);
- document.body.prepend(copyButton);
- }
- /**
- * 移除复制按钮
- */
- function removeCopyButton() {
- const copyButton = document.getElementById(DOM_ID_OF_COPY_BUTTON);
- if (!copyButton) {
- return;
- }
- copyButton.remove();
- }
- /**
- * 复制 Notion Page 内容
- */
- function copyNotionPageContent() {
- const selection = window.getSelection();
- selection.removeAllRanges();
- const pageContent = document.querySelector(DOM_SELECTOR_NOTION_PAGE_CONTENT_COMMON);
- if (!pageContent) {
- console.error("No Notion Page Content on Current Page.");
- return;
- }
- const range = new Range();
- const contentNextUncle = findNextElement(pageContent);
- range.setStart(pageContent, 0);
- if (contentNextUncle) {
- range.setEnd(contentNextUncle, 0);
- } else {
- range.setEndAfter(pageContent.lastChild);
- }
- selection.addRange(range);
- // console.log('childrenNodeCount', pageContent.childElementCount, pageContent.childNodes.length);
- // Array.from(pageContent.childNodes).forEach(e => console.log(selection.containsNode(e)));
- setTimeout(() => {
- document.execCommand('copy');
- selection.removeAllRanges();
- }, 500);
- }
- /**
- * 查找指定 DOM 的下一个元素
- * @param {Node} node DOM
- * @returns 指定 DOM 的下一个元素
- */
- function findNextElement(node) {
- while (node.nextSibling === null) {
- node = node.parentNode;
- }
- return node.nextSibling;
- }
- /**
- * 在页面显示提示信息
- */
- function showMessage(message) {
- const toast = document.createElement('div');
- toast.style.position = 'fixed';
- toast.style.bottom = '20px';
- toast.style.left = '50%';
- toast.style.transform = 'translateX(-50%)';
- toast.style.padding = '10px 20px';
- toast.style.background = 'rgba(0, 0, 0, 0.8)';
- toast.style.color = 'white';
- toast.style.borderRadius = '5px';
- toast.style.zIndex = '9999';
- toast.innerText = message;
- document.body.appendChild(toast);
- setTimeout(function () {
- toast.remove();
- }, 3000);
- }
- /**
- * 延迟执行
- **/
- function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
- /**
- * 防抖方法,连续触发场景下只执行一次
- * 触发高频事件后一段时间(wait)只会执行一次函数,如果指定时间(wait)内高频事件再次被触发,则重新计算时间。
- * @param {function} func 待执行的方法
- * @param {number} wait 执行方法前要等待的毫秒数
- * @returns
- */
- function debounce(func, wait) {
- let timeout = null;
- return function () {
- let context = this;
- let args = arguments;
- if (timeout) clearTimeout(timeout);
- timeout = setTimeout(() => {
- func.apply(context, args);
- }, wait);
- };
- }
- /**
- * 节流方法,连续触发场景下每 wait 时间区间执行一次
- * 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效
- * @param {function} func 待执行的方法
- * @param {number} wait 执行方法前要等待的毫秒数
- * @returns
- */
- function throttle(func, wait) {
- let timeout = null;
- return function () {
- let context = this;
- let args = arguments;
- if (!timeout) {
- timeout = setTimeout(() => {
- timeout = null;
- func.apply(context, args);
- }, wait);
- }
- };
- }
- /**
- * 在 maxWait 时间内等待所有 selectors 对应的 DOM 全部加载完成,每隔 interval 毫秒检查一次
- * @param {number} maxWait 最大等待毫秒数
- * @param {number} interval 检查间隔毫秒数
- * @param {...string} selectors DOM 选择器
- * @returns Promise
- */
- function waitForElements(maxWait, interval, ...selectors) {
- return new Promise((resolve, reject) => {
- const checkElements = () => {
- const elements = selectors.map(selector => document.querySelector(selector));
- if (elements.every(element => element != null)) {
- resolve(elements);
- } else if (maxWait <= 0) {
- reject(new Error('Timeout'));
- } else {
- setTimeout(checkElements, interval);
- maxWait -= interval;
- }
- };
- setTimeout(() => {
- reject(new Error('Timeout'));
- }, maxWait);
- checkElements();
- });
- }
- })();