├── public ├── robots.txt ├── logo128.png ├── logo48.png ├── options.html └── index.html ├── tailwind.config.js ├── src ├── chromium │ ├── App.css │ ├── background.js │ ├── options.js │ ├── index.js │ ├── index.css │ ├── App.js │ └── OptionsApp.js └── firefox │ ├── App.css │ ├── background.js │ ├── options.js │ ├── index.js │ ├── index.css │ ├── App.js │ └── OptionsApp.js ├── .gitignore ├── manifests ├── manifest_chromium.json └── manifest_firefox.json ├── LICENSE ├── package.json ├── webpack.config.js ├── webpack.dev.js └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvavassori/obsidian-web-clipper/HEAD/public/logo128.png -------------------------------------------------------------------------------- /public/logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvavassori/obsidian-web-clipper/HEAD/public/logo48.png -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /src/chromium/App.css: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | 0% { 3 | transform: rotate(0deg); 4 | } 5 | 100% { 6 | transform: rotate(360deg); 7 | } 8 | } 9 | 10 | .my-spinner { 11 | animation: spin 1s linear infinite; 12 | } 13 | -------------------------------------------------------------------------------- /src/firefox/App.css: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | 0% { 3 | transform: rotate(0deg); 4 | } 5 | 100% { 6 | transform: rotate(360deg); 7 | } 8 | } 9 | 10 | .my-spinner { 11 | animation: spin 1s linear infinite; 12 | } 13 | -------------------------------------------------------------------------------- /src/firefox/background.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | browser.runtime.onInstalled.addListener((details) => { 3 | if (details.reason === "install") { 4 | // Redirect to the options page 5 | browser.runtime.openOptionsPage(); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /src/chromium/background.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | chrome.runtime.onInstalled.addListener(function (details) { 3 | if (details.reason === "install") { 4 | // Redirect to the options page 5 | chrome.runtime.openOptionsPage(); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /src/firefox/options.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import OptionsApp from "./OptionsApp"; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById("root")); 7 | root.render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /src/chromium/options.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import OptionsApp from "./OptionsApp"; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById("root")); 7 | root.render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /public/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Obsidian Web Clipper 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/chromium/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById("root")); 7 | root.render( 8 | 9 |
10 | 11 |
12 |
13 | ); 14 | -------------------------------------------------------------------------------- /src/firefox/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById("root")); 7 | root.render( 8 | 9 |
10 | 11 |
12 |
13 | ); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | *.zip 26 | *.crx 27 | *.pem 28 | 29 | dist/ 30 | dist-chromium/ 31 | dist-firefox/ 32 | firefox-addons-source/ 33 | firefox-addons-source-code/ 34 | firefox-source-code/ 35 | 36 | -------------------------------------------------------------------------------- /src/chromium/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 8 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 16 | monospace; 17 | } 18 | 19 | .textarea-title:focus, 20 | .textarea-content:focus { 21 | outline: none; 22 | } 23 | -------------------------------------------------------------------------------- /src/firefox/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 8 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 16 | monospace; 17 | } 18 | 19 | .textarea-title:focus, 20 | .textarea-content:focus { 21 | outline: none; 22 | } 23 | -------------------------------------------------------------------------------- /manifests/manifest_chromium.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Unofficial Obsidian Web Clipper", 4 | "version": "0.2.3", 5 | "description": "A clipping extension to create notes in Obsidian.", 6 | "action": { 7 | "default_icon": "logo48.png", 8 | "default_popup": "popup.html" 9 | }, 10 | "background": { 11 | "service_worker": "background.js" 12 | }, 13 | "options_page": "options.html", 14 | "permissions": ["activeTab", "storage"], 15 | "icons": { 16 | "48": "logo48.png", 17 | "128": "logo128.png" 18 | }, 19 | "content_security_policy": { 20 | "extension_pages": "script-src 'self'; object-src 'self'" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /manifests/manifest_firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Unofficial Obsidian Web Clipper", 4 | "version": "0.2.4", 5 | "description": "A clipping extension to create notes in Obsidian.", 6 | "icons": { 7 | "48": "logo48.png", 8 | "128": "logo128.png" 9 | }, 10 | "browser_action": { 11 | "default_icon": { 12 | "48": "logo48.png", 13 | "128": "logo128.png" 14 | }, 15 | "default_popup": "popup.html" 16 | }, 17 | "background": { 18 | "scripts": ["background.js"], 19 | "persistent": false 20 | }, 21 | "options_ui": { 22 | "page": "options.html", 23 | "open_in_tab": true 24 | }, 25 | "permissions": ["activeTab", "storage"], 26 | "content_security_policy": "script-src 'self'; object-src 'self'", 27 | "browser_specific_settings": { 28 | "gecko": { 29 | "id": "3728a58e-4fc4-47f0-8e78-02368b160b67@example.com" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Marco Vavassori 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-web-clipper", 3 | "version": "0.1.4", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^18.3.1", 7 | "react-dom": "^18.3.1", 8 | "react-scripts": "5.0.1", 9 | "react-textarea-autosize": "^8.5.3" 10 | }, 11 | "scripts": { 12 | "dev:chromium": "webpack --watch --env browser=chromium --config webpack.dev.js", 13 | "dev:firefox": "webpack --watch --env browser=firefox --config webpack.dev.js", 14 | "build:chromium": "webpack --env browser=chromium --config webpack.config.js", 15 | "build:firefox": "webpack --env browser=firefox --config webpack.config.js" 16 | }, 17 | "eslintConfig": { 18 | "extends": [ 19 | "react-app", 20 | "react-app/jest" 21 | ] 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.24.7", 37 | "@babel/preset-react": "^7.24.7", 38 | "babel-loader": "^9.1.3", 39 | "copy-webpack-plugin": "^12.0.2", 40 | "html-webpack-plugin": "^5.6.0", 41 | "mini-css-extract-plugin": "^2.9.0", 42 | "tailwindcss": "^3.4.4", 43 | "webpack": "^5.92.1", 44 | "webpack-cli": "^5.1.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const CopyPlugin = require("copy-webpack-plugin"); 5 | const tailwindcss = require("tailwindcss"); 6 | const autoprefixer = require("autoprefixer"); 7 | const webpack = require("webpack"); 8 | 9 | module.exports = (env) => { 10 | const browser = env.browser || "chromium"; // Default to chromium if not specified 11 | 12 | return { 13 | entry: { 14 | popup: `./src/${browser}/index.js`, 15 | options: `./src/${browser}/options.js`, 16 | background: `./src/${browser}/background.js`, 17 | }, 18 | output: { 19 | filename: "[name].js", 20 | path: path.resolve(__dirname, `dist-${browser}`), 21 | }, 22 | mode: "production", 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js|jsx$/, 27 | exclude: /node_modules/, 28 | use: { 29 | loader: "babel-loader", 30 | options: { 31 | presets: [ 32 | "@babel/preset-env", 33 | ["@babel/preset-react", { runtime: "automatic" }], 34 | ], 35 | }, 36 | }, 37 | }, 38 | { 39 | test: /\.css$/, 40 | use: [ 41 | MiniCssExtractPlugin.loader, 42 | "css-loader", 43 | { 44 | loader: "postcss-loader", 45 | options: { 46 | postcssOptions: { 47 | plugins: [tailwindcss, autoprefixer], 48 | }, 49 | }, 50 | }, 51 | ], 52 | }, 53 | ], 54 | }, 55 | plugins: [ 56 | new HtmlWebpackPlugin({ 57 | template: "./public/index.html", 58 | filename: "popup.html", 59 | chunks: ["popup"], 60 | }), 61 | new HtmlWebpackPlugin({ 62 | template: "./public/options.html", 63 | filename: "options.html", 64 | chunks: ["options"], 65 | }), 66 | new MiniCssExtractPlugin(), 67 | new CopyPlugin({ 68 | patterns: [ 69 | { from: "public", to: ".", globOptions: { ignore: ["**/*.html"] } }, 70 | { from: `manifests/manifest_${browser}.json`, to: "manifest.json" }, // Copy the appropriate manifest 71 | ], 72 | }), 73 | new webpack.DefinePlugin({ 74 | "process.env.BROWSER": JSON.stringify(browser), 75 | }), 76 | ], 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const CopyPlugin = require("copy-webpack-plugin"); 5 | const tailwindcss = require("tailwindcss"); 6 | const autoprefixer = require("autoprefixer"); 7 | 8 | module.exports = (env) => { 9 | const browser = env.browser || "chromium"; // Default to chromium if not specified 10 | 11 | return { 12 | entry: { 13 | popup: `./src/${browser}/index.js`, 14 | options: `./src/${browser}/options.js`, 15 | background: `./src/${browser}/background.js`, 16 | }, 17 | output: { 18 | filename: "[name].js", 19 | path: path.resolve(__dirname, `dist-${browser}`), 20 | }, 21 | mode: "development", 22 | devtool: "inline-source-map", 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js|jsx$/, 27 | exclude: /node_modules/, 28 | use: { 29 | loader: "babel-loader", 30 | options: { 31 | presets: [ 32 | "@babel/preset-env", 33 | ["@babel/preset-react", { runtime: "automatic" }], 34 | ], 35 | }, 36 | }, 37 | }, 38 | { 39 | test: /\.css$/, 40 | use: [ 41 | MiniCssExtractPlugin.loader, 42 | "css-loader", 43 | { 44 | loader: "postcss-loader", 45 | options: { 46 | postcssOptions: { 47 | plugins: [tailwindcss, autoprefixer], 48 | }, 49 | }, 50 | }, 51 | ], 52 | }, 53 | ], 54 | }, 55 | plugins: [ 56 | new HtmlWebpackPlugin({ 57 | template: "./public/index.html", 58 | filename: "popup.html", 59 | chunks: ["popup"], 60 | }), 61 | new HtmlWebpackPlugin({ 62 | template: "./public/options.html", 63 | filename: "options.html", 64 | chunks: ["options"], 65 | }), 66 | new MiniCssExtractPlugin(), 67 | new CopyPlugin({ 68 | patterns: [ 69 | { from: "public", to: ".", globOptions: { ignore: ["**/*.html"] } }, 70 | { from: `manifests/manifest_${browser}.json`, to: "manifest.json" }, // Copy the appropriate manifest 71 | ], 72 | }), 73 | ], 74 | devServer: { 75 | contentBase: path.join(__dirname, "dist"), 76 | compress: true, 77 | port: 9000, 78 | }, 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Web Clipper 2 | 3 | ## Description 4 | 5 | Obsidian Web Clipper is a simple Chrome extension for users of [Obsidian](https://obsidian.md/), a popular note-taking application. With this extension, you can quickly capture notes directly from your web browser and save them to your Obsidian vaults. 6 | 7 | ## Features 8 | 9 | - **Efficient Note-Taking:** Click the extension icon to open a popup where you can jot down notes related to the current webpage. 10 | - **Customizable Titles:** The title of the note defaults to the webpage title, but can be easily edited. 11 | - **Page Link Tracking:** The link to the current webpage is automatically added at the top of the note content for reference. You can choose to remove this link if you prefer. 12 | - **Direct Obsidian Integration:** Define the Obsidian vault where your clippings will be saved. Specify the folder structure using `Folder Name/{title}` format, or simply use `{title}` to save the note with the title used in the extension popup. 13 | 14 | - **Character Limits:** A maximum character limit of 50 characters for the note title and 1500 characters for the note content is enforced to ensure smooth handling with Obsidian URI. 15 | 16 | ## Installation 17 | 18 | Download the extension from the [Chrome Web Store](https://chrome.google.com/webstore/detail/obsidian-web-clipper/akiokmdijehkppdjnfdhdgcoeehpbfgd), the [Microsoft Edge Add-ons store](https://microsoftedge.microsoft.com/addons/detail/jgjacbgaegejdeiodlknbamdpmocmecg) or the [Firefox Add-ons store](https://addons.mozilla.org/it/firefox/addon/obsidian-web-clipper-add-on/) 19 | 20 | Once installed, right-click the extension icon in the toolbar and select **Options** to specify your Obsidian vault name and desired note-saving format. 21 | 22 | ## For Developers 23 | 24 | This extension is built with React (version 18.2.0) and uses Webpack for bundling. Other notable dependencies include react-textarea-autosize for the note-taking textarea, and tailwindcss for styling. 25 | 26 | ### Getting Started 27 | 28 | To get a local copy up and running follow these simple steps: 29 | 30 | 1. Clone the repository: `git clone https://github.com/mvavassori/obsidian-web-clipper.git` 31 | 32 | 2. Navigate to the project directory: `cd obsidian-web-clipper` 33 | 34 | 3. Install dependencies: `npm install` 35 | 36 | ### Available Scripts 37 | 38 | In the project directory, you can run the following scripts: 39 | 40 | - `npm run dev:chromium` (`npm run dev:firefox` if you're using firefox or one of its derivatives): Runs the webpack in the development mode. The bundle will be automatically rebuilt upon file changes. 41 | 42 | - `npm run build:chromium` (`npm run build:firefox` if you're using firefox or one of its derivatives): Runs the webpack in the production mode, creating a bundled output ready for distribution. 43 | 44 | Please note that you will need to have Node.js and npm installed on your machine to run these commands. 45 | 46 | ## Roadmap 47 | 48 | Here are some features and improvements that are planned for future updates of Obsidian Web Clipper: 49 | 50 | - [x] **Support for Firefox:** Extend the availability of the extension to Firefox users, allowing them to benefit from the same note-taking capabilities within their preferred browser. 51 | 52 | - [ ] **Markdown Preview in Popup:** Enable a markdown preview feature directly within the extension popup, allowing users to preview the rendered markdown of their notes before saving them to their Obsidian vaults. 53 | 54 | - [x] **Custom Standard Note Content** Add form to let users decide how and what to save to the note's content (timestamps, title, link, etc...). 55 | 56 | Please note that the roadmap is subject to change and these features are not yet implemented. However, they represent the direction and potential enhancements for future versions of Obsidian Web Clipper. 57 | 58 | ## Support 59 | 60 | If you find it helpful and wish to support its development, consider making a [donation through PayPal](https://www.paypal.com/donate/?hosted_button_id=M8RTMTXKV46EC). 61 | -------------------------------------------------------------------------------- /src/chromium/App.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | import "./App.css"; 3 | import React, { useState, useEffect, useRef } from "react"; 4 | import TextareaAutosize from "react-textarea-autosize"; 5 | 6 | function App() { 7 | const [pageInfo, setPageInfo] = useState({ title: "", url: "" }); 8 | const [headerVisible, setHeaderVisible] = useState(true); 9 | const [title, setTitle] = useState(""); 10 | const [content, setContent] = useState(""); 11 | const [saveButtonDisabled, setSaveButtonDisabled] = useState(true); 12 | const [showRemoveLinkTooltip, setShowRemoveLinkTooltip] = useState(false); 13 | const [showHamburgerMenu, setShowHamburgerMenu] = useState(false); 14 | const [showEditTitleIcon, setShowEditTitleIcon] = useState(false); 15 | const [isTitleInFocus, setIsTitleInFocus] = useState(false); 16 | 17 | const [obsidianVault, setObsidianVault] = useState(null); 18 | const [folderPath, setFolderPath] = useState(null); 19 | const [noteContentFormat, setNoteContentFormat] = useState(null); 20 | const [errorMsg, setErrorMsg] = useState(""); 21 | const [loading, setLoading] = useState(true); 22 | 23 | const titleInputRef = useRef(); 24 | const textAreaRef = useRef(); 25 | const containerRef = useRef(); 26 | const menuRef = useRef(); 27 | 28 | const removeLinkButtonRef = useRef(null); 29 | const cancelButtonRef = useRef(null); 30 | const saveButtonRef = useRef(null); 31 | const hamburgerMenuButtonRef = useRef(null); 32 | 33 | useEffect(() => { 34 | const handleClickOutside = (e) => { 35 | if ( 36 | menuRef.current && 37 | !menuRef.current.contains(e.target) && 38 | hamburgerMenuButtonRef.current && 39 | !hamburgerMenuButtonRef.current.contains(e.target) 40 | ) { 41 | setShowHamburgerMenu(false); 42 | } 43 | }; 44 | 45 | document.addEventListener("mousedown", handleClickOutside); 46 | return () => { 47 | document.removeEventListener("mousedown", handleClickOutside); 48 | }; 49 | }, []); 50 | 51 | useEffect(() => { 52 | if (title.trim() === "" && content.trim() === "" && !headerVisible) { 53 | setSaveButtonDisabled(true); 54 | } else if (errorMsg) { 55 | setSaveButtonDisabled(true); 56 | } else { 57 | setSaveButtonDisabled(false); 58 | } 59 | }, [title, content, headerVisible, errorMsg]); 60 | 61 | useEffect(() => { 62 | if (textAreaRef.current) { 63 | textAreaRef.current.focus(); 64 | } 65 | }, []); 66 | 67 | useEffect(() => { 68 | const getPageInfo = async () => { 69 | setLoading(true); 70 | try { 71 | const tabs = await new Promise((resolve) => { 72 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 73 | resolve(tabs); 74 | }); 75 | }); 76 | const tab = tabs[0]; 77 | setPageInfo({ title: tab.title, url: tab.url }); 78 | setTitle(sanitizeTitle(tab.title)); 79 | } catch (error) { 80 | console.error("Error getting page info: ", error); 81 | } finally { 82 | setLoading(false); 83 | } 84 | }; 85 | 86 | getPageInfo(); 87 | }, []); 88 | 89 | useEffect(() => { 90 | const loadSettings = async () => { 91 | setLoading(true); 92 | try { 93 | const result = await new Promise((resolve) => { 94 | chrome.storage.sync.get( 95 | ["obsidianVault", "folderPath", "noteContentFormat"], 96 | (result) => { 97 | resolve(result); 98 | } 99 | ); 100 | }); 101 | if (result.obsidianVault) { 102 | setObsidianVault(result.obsidianVault); 103 | } 104 | if (result.folderPath) { 105 | setFolderPath(result.folderPath); 106 | } 107 | if (result.noteContentFormat) { 108 | setNoteContentFormat(result.noteContentFormat); 109 | } else { 110 | // Set default note format if not found in storage 111 | setNoteContentFormat("{url}\n\n{content}"); 112 | } 113 | } catch (error) { 114 | console.error("Error loading settings: ", error); 115 | } finally { 116 | setLoading(false); 117 | } 118 | }; 119 | 120 | loadSettings(); 121 | }, []); 122 | 123 | if (loading) { 124 |
125 |
126 |
; 127 | } 128 | 129 | const saveNote = async () => { 130 | // Redirect to the options page if obsidianVault or folderPath is not set 131 | if (!obsidianVault || !folderPath) { 132 | chrome.runtime.openOptionsPage(); 133 | return; 134 | } 135 | 136 | if (title.length > 250) { 137 | setErrorMsg("Title is too long"); 138 | return; 139 | } 140 | 141 | // Format the note content using the custom note format 142 | const date = new Date().toLocaleDateString("en-CA"); 143 | let newContent = noteContentFormat 144 | .replace("{url}", headerVisible ? pageInfo.url : "") 145 | .replace("{title}", title) 146 | .replace("{content}", content) 147 | .replace("{date}", date); 148 | 149 | // Remove only the empty line that would have contained the URL if not visible 150 | if (!headerVisible) { 151 | const lines = newContent.split("\n"); 152 | const urlIndex = lines.findIndex((line) => line.trim() === ""); 153 | if (urlIndex !== -1) { 154 | lines.splice(urlIndex, 1); 155 | } 156 | newContent = lines.join("\n"); 157 | } 158 | 159 | // Remove the line for content if it's empty 160 | if (content.trim() === "") { 161 | const lines = newContent.split("\n"); 162 | const contentIndex = lines.findIndex((line) => line.trim() === ""); 163 | if (contentIndex !== -1) { 164 | lines.splice(contentIndex, 1); 165 | } 166 | newContent = lines.join("\n"); 167 | } 168 | 169 | // Replace {title} with the sanitized page title in the folderPath 170 | const sanitizedTitle = sanitizeTitle(title); 171 | const finalFolderPath = folderPath.replace("{title}", sanitizedTitle); 172 | 173 | try { 174 | // Generate the Obsidian URI 175 | const obsidianUri = `obsidian://new?vault=${encodeURIComponent( 176 | obsidianVault 177 | )}&file=${encodeURIComponent( 178 | finalFolderPath 179 | )}&content=${encodeURIComponent(newContent)}`; 180 | 181 | // Open the URI in a new tab 182 | window.open(obsidianUri, "_blank"); 183 | setTitle(""); 184 | setContent(""); 185 | } catch (error) { 186 | console.error("Error adding note: ", error); 187 | } 188 | }; 189 | 190 | const handleCancel = () => { 191 | // Clear title and content 192 | setTitle(""); 193 | setContent(""); 194 | window.close(); 195 | }; 196 | 197 | const selectAllInputText = () => { 198 | titleInputRef.current.select(); 199 | setShowEditTitleIcon(false); 200 | }; 201 | 202 | const sanitizeTitle = (title) => { 203 | const invalidCharacterPattern = /[\\:*?"<>|/]/g; 204 | return title.replace(invalidCharacterPattern, "-"); 205 | }; 206 | 207 | const handleTitleChange = (e) => { 208 | const sanitizedValue = sanitizeTitle(e.target.value); 209 | if (sanitizedValue !== e.target.value) { 210 | setErrorMsg( 211 | 'The title contains invalid characters. Please avoid using these characters in the title: \\ : * ? " < > | /' 212 | ); 213 | } else if (sanitizedValue.length > 250) { 214 | setErrorMsg("The title is too long"); 215 | } else { 216 | setErrorMsg(""); 217 | } 218 | setTitle(e.target.value); 219 | }; 220 | 221 | const donateRedirect = () => { 222 | chrome.tabs.create({ 223 | url: "https://www.paypal.com/donate/?hosted_button_id=M8RTMTXKV46EC", 224 | }); 225 | }; 226 | 227 | const optionsRedirect = () => { 228 | chrome.runtime.openOptionsPage(); 229 | }; 230 | 231 | return ( 232 |
236 | {headerVisible && ( 237 |
238 |
{pageInfo.url}
239 | 264 | {showRemoveLinkTooltip && ( 265 |
266 | Remove page link 267 |
268 | )} 269 |
270 | )} 271 |
!isTitleInFocus && setShowEditTitleIcon(true)} 274 | onMouseLeave={() => setShowEditTitleIcon(false)} 275 | > 276 | { 287 | setIsTitleInFocus(true); 288 | setShowEditTitleIcon(false); 289 | }} 290 | onBlur={() => { 291 | setIsTitleInFocus(false); 292 | setShowEditTitleIcon(false); 293 | }} 294 | /> 295 | {showEditTitleIcon && ( 296 |
297 | 313 |
314 | )} 315 |
316 | {errorMsg && ( 317 |
318 | {errorMsg} 319 |
320 | )} 321 | setContent(e.target.value)} 325 | className="w-full p-2 focus:border-none focus:ring-0 textarea-content resize-none bg-zinc-50 text-sm" 326 | placeholder="Take a brief note..." 327 | minRows={4} 328 | autoComplete="no-autocomplete-please" 329 | maxLength={1500} 330 | > 331 |
332 |
333 | 355 | {showHamburgerMenu && ( 356 |
360 | 366 | 372 |
373 | )} 374 |
375 |
376 | 383 | 395 |
396 |
397 |
398 | ); 399 | } 400 | 401 | export default App; 402 | -------------------------------------------------------------------------------- /src/firefox/App.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | import "./App.css"; 3 | import React, { useState, useEffect, useRef } from "react"; 4 | import TextareaAutosize from "react-textarea-autosize"; 5 | 6 | function App() { 7 | const [pageInfo, setPageInfo] = useState({ title: "", url: "" }); 8 | const [headerVisible, setHeaderVisible] = useState(true); 9 | const [title, setTitle] = useState(""); 10 | const [content, setContent] = useState(""); 11 | const [saveButtonDisabled, setSaveButtonDisabled] = useState(true); 12 | const [showRemoveLinkTooltip, setShowRemoveLinkTooltip] = useState(false); 13 | const [showHamburgerMenu, setShowHamburgerMenu] = useState(false); 14 | const [showEditTitleIcon, setShowEditTitleIcon] = useState(false); 15 | const [isTitleInFocus, setIsTitleInFocus] = useState(false); 16 | 17 | const [obsidianVault, setObsidianVault] = useState(null); 18 | const [folderPath, setFolderPath] = useState(null); 19 | const [noteContentFormat, setNoteContentFormat] = useState(null); 20 | const [errorMsg, setErrorMsg] = useState(""); 21 | const [loading, setLoading] = useState(true); 22 | 23 | const titleInputRef = useRef(); 24 | const textAreaRef = useRef(); 25 | const containerRef = useRef(); 26 | const menuRef = useRef(); 27 | 28 | const removeLinkButtonRef = useRef(null); 29 | const cancelButtonRef = useRef(null); 30 | const saveButtonRef = useRef(null); 31 | const hamburgerMenuButtonRef = useRef(null); 32 | 33 | useEffect(() => { 34 | const handleClickOutside = (e) => { 35 | if ( 36 | menuRef.current && 37 | !menuRef.current.contains(e.target) && 38 | hamburgerMenuButtonRef.current && 39 | !hamburgerMenuButtonRef.current.contains(e.target) 40 | ) { 41 | setShowHamburgerMenu(false); 42 | } 43 | }; 44 | 45 | document.addEventListener("mousedown", handleClickOutside); 46 | return () => { 47 | document.removeEventListener("mousedown", handleClickOutside); 48 | }; 49 | }, []); 50 | 51 | useEffect(() => { 52 | if (title.trim() === "" && content.trim() === "" && !headerVisible) { 53 | setSaveButtonDisabled(true); 54 | } else if (errorMsg) { 55 | setSaveButtonDisabled(true); 56 | } else { 57 | setSaveButtonDisabled(false); 58 | } 59 | }, [title, content, headerVisible, errorMsg]); 60 | 61 | useEffect(() => { 62 | if (textAreaRef.current) { 63 | textAreaRef.current.focus(); 64 | } 65 | }, []); 66 | 67 | useEffect(() => { 68 | const getPageInfo = async () => { 69 | setLoading(true); 70 | try { 71 | const tabs = await new Promise((resolve) => { 72 | browser.tabs.query({ active: true, currentWindow: true }, (tabs) => { 73 | resolve(tabs); 74 | }); 75 | }); 76 | const tab = tabs[0]; 77 | setPageInfo({ title: tab.title, url: tab.url }); 78 | setTitle(sanitizeTitle(tab.title)); 79 | } catch (error) { 80 | console.error("Error getting page info: ", error); 81 | } finally { 82 | setLoading(false); 83 | } 84 | }; 85 | 86 | getPageInfo(); 87 | }, []); 88 | 89 | useEffect(() => { 90 | const loadSettings = async () => { 91 | setLoading(true); 92 | try { 93 | const result = await new Promise((resolve) => { 94 | browser.storage.sync.get( 95 | ["obsidianVault", "folderPath", "noteContentFormat"], 96 | (result) => { 97 | resolve(result); 98 | } 99 | ); 100 | }); 101 | if (result.obsidianVault) { 102 | setObsidianVault(result.obsidianVault); 103 | } 104 | if (result.folderPath) { 105 | setFolderPath(result.folderPath); 106 | } 107 | if (result.noteContentFormat) { 108 | setNoteContentFormat(result.noteContentFormat); 109 | } else { 110 | // Set default note format if not found in storage 111 | setNoteContentFormat("{url}\n\n{content}"); 112 | } 113 | } catch (error) { 114 | console.error("Error loading settings: ", error); 115 | } finally { 116 | setLoading(false); 117 | } 118 | }; 119 | 120 | loadSettings(); 121 | }, []); 122 | 123 | if (loading) { 124 |
125 |
126 |
; 127 | } 128 | 129 | const saveNote = async () => { 130 | // Redirect to the options page if obsidianVault or folderPath is not set 131 | if (!obsidianVault || !folderPath) { 132 | browser.runtime.openOptionsPage(); 133 | return; 134 | } 135 | 136 | if (title.length > 250) { 137 | setErrorMsg("Title is too long"); 138 | return; 139 | } 140 | 141 | // Format the note content using the custom note format 142 | const date = new Date().toLocaleDateString("en-CA"); 143 | let newContent = noteContentFormat 144 | .replace("{url}", headerVisible ? pageInfo.url : "") 145 | .replace("{title}", title) 146 | .replace("{content}", content) 147 | .replace("{date}", date); 148 | 149 | // Remove only the empty line that would have contained the URL if not visible 150 | if (!headerVisible) { 151 | const lines = newContent.split("\n"); 152 | const urlIndex = lines.findIndex((line) => line.trim() === ""); 153 | if (urlIndex !== -1) { 154 | lines.splice(urlIndex, 1); 155 | } 156 | newContent = lines.join("\n"); 157 | } 158 | 159 | // Remove the line for content if it's empty 160 | if (content.trim() === "") { 161 | const lines = newContent.split("\n"); 162 | const contentIndex = lines.findIndex((line) => line.trim() === ""); 163 | if (contentIndex !== -1) { 164 | lines.splice(contentIndex, 1); 165 | } 166 | newContent = lines.join("\n"); 167 | } 168 | 169 | // Replace {title} with the sanitized page title in the folderPath 170 | const sanitizedTitle = sanitizeTitle(title); 171 | const finalFolderPath = folderPath.replace("{title}", sanitizedTitle); 172 | 173 | try { 174 | // Generate the Obsidian URI 175 | const obsidianUri = `obsidian://new?vault=${encodeURIComponent( 176 | obsidianVault 177 | )}&file=${encodeURIComponent( 178 | finalFolderPath 179 | )}&content=${encodeURIComponent(newContent)}`; 180 | 181 | // Open the URI in a new tab using the Firefox API 182 | browser.tabs.create({ url: obsidianUri, active: false }, (tab) => { 183 | // Close the blank page 184 | browser.tabs.remove(tab.id); 185 | }); 186 | 187 | setTitle(""); 188 | setContent(""); 189 | } catch (error) { 190 | console.error("Error adding note: ", error); 191 | } 192 | }; 193 | 194 | const handleCancel = () => { 195 | // Clear title and content 196 | setTitle(""); 197 | setContent(""); 198 | window.close(); 199 | }; 200 | 201 | const selectAllInputText = () => { 202 | titleInputRef.current.select(); 203 | setShowEditTitleIcon(false); 204 | }; 205 | 206 | const sanitizeTitle = (title) => { 207 | const invalidCharacterPattern = /[\\:*?"<>|/]/g; 208 | return title.replace(invalidCharacterPattern, "-"); 209 | }; 210 | 211 | const handleTitleChange = (e) => { 212 | const sanitizedValue = sanitizeTitle(e.target.value); 213 | if (sanitizedValue !== e.target.value) { 214 | setErrorMsg( 215 | 'The title contains invalid characters. Please avoid using these characters in the title: \\ : * ? " < > | /' 216 | ); 217 | } else if (sanitizedValue.length > 250) { 218 | setErrorMsg("The title is too long"); 219 | } else { 220 | setErrorMsg(""); 221 | } 222 | setTitle(e.target.value); 223 | }; 224 | 225 | const donateRedirect = () => { 226 | browser.tabs.create({ 227 | url: "https://www.paypal.com/donate/?hosted_button_id=M8RTMTXKV46EC", 228 | }); 229 | }; 230 | 231 | const optionsRedirect = () => { 232 | browser.runtime.openOptionsPage(); 233 | }; 234 | 235 | return ( 236 |
241 | {headerVisible && ( 242 |
246 |
{pageInfo.url}
247 | 273 | {showRemoveLinkTooltip && ( 274 |
275 | Remove page link 276 |
277 | )} 278 |
279 | )} 280 |
!isTitleInFocus && setShowEditTitleIcon(true)} 283 | onMouseLeave={() => setShowEditTitleIcon(false)} 284 | > 285 | { 296 | setIsTitleInFocus(true); 297 | setShowEditTitleIcon(false); 298 | }} 299 | onBlur={() => { 300 | setIsTitleInFocus(false); 301 | setShowEditTitleIcon(false); 302 | }} 303 | /> 304 | {showEditTitleIcon && ( 305 |
306 | 322 |
323 | )} 324 |
325 | {errorMsg && ( 326 |
327 | {errorMsg} 328 |
329 | )} 330 | setContent(e.target.value)} 334 | className="w-full p-2 focus:border-none focus:ring-0 textarea-content resize-none font-normal bg-zinc-50 text-sm" 335 | placeholder="Take a brief note..." 336 | minRows={4} 337 | autoComplete="no-autocomplete-please" 338 | maxLength={1500} 339 | > 340 |
341 |
342 | 364 | {showHamburgerMenu && ( 365 |
369 | 375 | 381 |
382 | )} 383 |
384 |
385 | 392 | 404 |
405 |
406 |
407 | ); 408 | } 409 | 410 | export default App; 411 | -------------------------------------------------------------------------------- /src/chromium/OptionsApp.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | import React, { useState, useEffect } from "react"; 3 | 4 | const OptionsApp = () => { 5 | const [vault, setVault] = useState(""); 6 | const [folder, setFolder] = useState(""); 7 | const [showAdvancedFeatures, setShowAdvancedFeatures] = useState(false); 8 | const [noteContentFormat, setNoteContentFormat] = useState(""); 9 | 10 | const defaultNoteContentFormat = "{url}\n\n{content}"; 11 | 12 | useEffect(() => { 13 | // Load the settings from browser storage 14 | chrome.storage.sync.get( 15 | [ 16 | "obsidianVault", 17 | "folderPath", 18 | "showAdvancedFeatures", 19 | "noteContentFormat", 20 | ], 21 | (result) => { 22 | if (result.obsidianVault) { 23 | setVault(result.obsidianVault); 24 | } 25 | if (result.folderPath) { 26 | setFolder(result.folderPath); 27 | } 28 | if (result.showAdvancedFeatures) { 29 | setShowAdvancedFeatures(result.showAdvancedFeatures); 30 | } 31 | if (result.noteContentFormat) { 32 | setNoteContentFormat(result.noteContentFormat); 33 | } else { 34 | // Set default note format if not found in storage 35 | setNoteContentFormat(defaultNoteContentFormat); 36 | } 37 | } 38 | ); 39 | }, []); 40 | 41 | useEffect(() => { 42 | if (!showAdvancedFeatures) { 43 | setNoteContentFormat(defaultNoteContentFormat); 44 | } 45 | }, [showAdvancedFeatures]); 46 | 47 | const handleSave = () => { 48 | // Check if the required fields are empty 49 | if (vault.trim() === "" || folder.trim() === "") { 50 | alert( 51 | "Please provide a value for both Obsidian Vault Name and Clip Notes to fields." 52 | ); 53 | return; 54 | } 55 | 56 | const invalidCharacterPattern = /[\\:*?"<>|]/; 57 | 58 | if ( 59 | invalidCharacterPattern.test(vault) || 60 | invalidCharacterPattern.test(folder) 61 | ) { 62 | alert( 63 | 'Invalid character detected. Please avoid using the following characters in the Vault Name or Folder Path: /, \\, :, *, ?, ", <, >, |' 64 | ); 65 | return; 66 | } 67 | 68 | // Check if the folder path matches the pattern 69 | // const folderPattern = /^(\{title\}|[\w\s]+\/\{title\})$/; 70 | // const folderPattern = /^([\w\s().]+\/)*\{title\}$/; 71 | // const folderPattern = /^([\p{L}\p{Script=Hani}\s().]+\/)*\{title\}$/u; 72 | // const folderPattern = /^([\p{L}\p{Script=Hani}\s().\-_!@#$%^&()+={}[\];',~]+\/)*\{title\}$/u; 73 | // const folderPattern = /^(([\p{L}\p{Script=Hani}\s()\-_!@#$%^&()+={}[\];',~][\p{L}\p{Script=Hani}\s().\-_!@#$%^&()+={}[\];',~]*\/)*\{title\})$/u; 74 | // const folderPattern = /^(([\p{L}\p{Script=Hani}\p{Emoji}\s()\-_!@#$%^&()+={}[\];',~][\p{L}\p{Script=Hani}\p{Emoji}\s().\-_!@#$%^&(`+={}[\];',~]*\/)*\{title\})$/u; 75 | const folderPattern = 76 | /^(([\p{L}\p{N}\p{Emoji}\p{Emoji_Component}\s()\-_!@#$%^&()+={}[\];',~][\p{L}\p{N}\p{Emoji}\p{Emoji_Component}\s().\-_!@#$%^&()+={}[\];',~]*\/)*\{title\})$/u; 77 | 78 | if (!folderPattern.test(folder.trim())) { 79 | alert( 80 | "Invalid folder format. Please use '{title}' or 'Folder Name/{title}' as the folder path." 81 | ); 82 | return; 83 | } 84 | 85 | // if the user has enabled advanced features and the noteContentFormat is empty, send an alert and reset it to the default format 86 | if (showAdvancedFeatures && noteContentFormat === "") { 87 | alert( 88 | "If you want to use advanced features, please provide a note content format." 89 | ); 90 | setNoteContentFormat(defaultNoteContentFormat); 91 | return; 92 | } 93 | 94 | // Determine the noteContentFormat to save 95 | const savedNoteContentFormat = showAdvancedFeatures 96 | ? noteContentFormat 97 | : defaultNoteContentFormat; 98 | 99 | // Save the settings to browser storage 100 | chrome.storage.sync.set( 101 | { 102 | obsidianVault: vault, 103 | folderPath: folder, 104 | showAdvancedFeatures: showAdvancedFeatures, 105 | noteContentFormat: savedNoteContentFormat, 106 | }, 107 | () => { 108 | if (chrome.runtime.lastError) { 109 | console.error(`Error: ${chrome.runtime.lastError}`); 110 | } else { 111 | alert( 112 | `Success!👌\n\nYour notes will be saved to the vault named "${vault}" using the format "${folder}".` 113 | ); 114 | } 115 | } 116 | ); 117 | }; 118 | 119 | const handleTest = () => { 120 | // Check if the required fields are empty 121 | if (vault.trim() === "" || folder.trim() === "") { 122 | alert( 123 | "Please provide a value for both Obsidian Vault Name and Clip Notes to fields." 124 | ); 125 | return; 126 | } 127 | const invalidCharacterPattern = /[\\:*?"<>|]/; 128 | 129 | if ( 130 | invalidCharacterPattern.test(vault) || 131 | invalidCharacterPattern.test(folder) 132 | ) { 133 | alert( 134 | 'Invalid character detected. Please avoid using the following characters in the Vault Name or Folder Path: /, \\, :, *, ?, ", <, >, |' 135 | ); 136 | return; 137 | } 138 | 139 | // if the user has enabled advanced features and the noteContentFormat is empty, set it to the default format 140 | if (showAdvancedFeatures && noteContentFormat === "") { 141 | alert( 142 | "If you want to use advanced features, please provide a note content format." 143 | ); 144 | setNoteContentFormat(defaultNoteContentFormat); 145 | return; 146 | } 147 | 148 | // Generate a test note based on the settings 149 | const title = "Obsidian Web Clipper Test Note"; 150 | const url = "https://example.com"; 151 | const content = 152 | "This is a test note generated by the Obsidian Web Clipper extension."; 153 | const date = new Date().toISOString().split("T")[0]; // Get current date in YYYY-MM-DD format 154 | 155 | let formattedContent; 156 | if (showAdvancedFeatures && noteContentFormat) { 157 | formattedContent = noteContentFormat 158 | .replace("{url}", url) 159 | .replace("{title}", title) 160 | .replace("{content}", content) 161 | .replace("{date}", date); 162 | } else { 163 | formattedContent = `${url}\n\n${content}`; 164 | } 165 | 166 | const folderPattern = 167 | /^(([\p{L}\p{N}\p{Emoji}\p{Emoji_Component}\s()\-_!@#$%^&()+={}[\];',~][\p{L}\p{N}\p{Emoji}\p{Emoji_Component}\s().\-_!@#$%^&()+={}[\];',~]*\/)*\{title\})$/u; 168 | 169 | if (!folderPattern.test(folder.trim())) { 170 | alert( 171 | "Invalid folder format. Please use '{title}' or 'Folder Name/{title}' as the folder path." 172 | ); 173 | return; 174 | } 175 | 176 | let folderPath = folder.trim().replace("{title}", ""); 177 | if (folderPath && !folderPath.endsWith("/")) { 178 | folderPath += "/"; 179 | } 180 | 181 | const obsidianUri = `obsidian://new?vault=${encodeURIComponent( 182 | vault 183 | )}&file=${encodeURIComponent( 184 | folderPath + title 185 | )}&content=${encodeURIComponent(formattedContent)}`; 186 | // Check if vault is not empty before opening the URI 187 | if (vault.trim() !== "") { 188 | window.open(obsidianUri, "_blank"); 189 | } else { 190 | alert("Please provide a valid Obsidian Vault Name."); 191 | } 192 | }; 193 | 194 | return ( 195 |
196 |

Obsidian Web Clipper

197 |

198 | This is an unofficial web clipper for Obsidian that allows you to easily 199 | create notes within a popup and save them directly to your Obsidian 200 | vault. By default, the extension designates the webpage title as your 201 | note title, and saves the webpage link at the top of your note's 202 | content. 203 |
204 |
205 | This extension is free, open-source, and available on{" "} 206 | 212 | GitHub 213 | 214 | . If you find this tool helpful and wish to support its development, 215 | you're welcome to make a donation through{" "} 216 | 222 | PayPal 223 | 224 | . 225 |

226 |

Extension Options

227 |
228 | 235 | setVault(e.target.value)} 242 | /> 243 |
244 |
245 | 261 | setFolder(e.target.value)} 268 | /> 269 |
270 |
271 | 280 | {/* `New` svg icon */} 281 | 287 | 291 | 292 |
293 | {showAdvancedFeatures && ( 294 |
295 |
299 |
300 |

Example with default format:

301 |
302 |
303 |                   {`{url}
304 | 
305 | {date}
306 | 
307 | {content}`}
308 |                 
309 |
310 |
311 |
312 |

Example with frontmatter format:

313 |
314 |
315 |                   {`---
316 | url: {url}
317 | date: {date}
318 | ---
319 | {content}`}
320 |                 
321 |
322 |
323 |
324 |
325 | 326 | If you're confused just copy either the default format or the 327 | frontmatter format and paste it in the text box below. Then click 328 | the "Test Settings" button to see how it looks. 329 | 330 |
331 | 355 |