├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .idea
├── .gitignore
├── Memos-Extension.iml
├── modules.xml
└── vcs.xml
├── .nvmrc
├── .vscode
├── settings.json
└── tasks.json
├── LICENSE
├── Obsidian-Memos-Chrome-Extension 0.1.3.zip
├── README.md
├── assets
├── build_icons.dev.sh
├── build_icons.sh
├── icon.dev.png
├── icon.png
├── icon.xcf
├── popup.png
├── popup.xcf
├── preferences-simple.png
├── preferences-simple.xcf
├── preferences-template.png
├── preferences-template.xcf
├── screenshot.png
└── screenshot.xcf
├── img
└── img.png
├── jest.config.js
├── package-lock.json
├── package.json
├── public
├── MaterialIcons-Regular.ttf
├── Roboto-Regular.ttf
├── handlebars.html
├── icon128.png
├── icon16.png
├── icon256.png
├── icon48.png
├── manifest.json
├── options.html
├── popup.html
└── styles.css
├── scripts
├── build_icons.dev.sh
├── build_icons.sh
└── build_package.sh
├── src
├── components
│ ├── Alert.tsx
│ ├── HeaderControl.tsx
│ ├── MentionNotice.tsx
│ └── RequestParameters.tsx
├── constants.ts
├── handlebars.ts
├── options.tsx
├── popup.tsx
├── sendIt.ts
├── service_worker.ts
├── theme.ts
├── types.ts
└── utils.ts
├── tsconfig.json
├── webpack
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
└── yarn.lock
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: build
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [14.x, 15.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v2
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v1
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | - run: npm ci
29 | - run: npm run build --if-present
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | node_modules/
3 | dist/
4 | tmp/
5 |
6 | .vscode/
7 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # 默认忽略的文件
2 | /shelf/
3 | /workspace.xml
4 | # 基于编辑器的 HTTP 客户端请求
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/Memos-Extension.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v14.17.5
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "./node_modules/typescript/lib",
3 | "files.eol": "\n",
4 | "json.schemas": [
5 | {
6 | "fileMatch": ["/manifest.json"],
7 | "url": "http://json.schemastore.org/chrome-manifest"
8 | }
9 | ],
10 | "editor.formatOnSave": true,
11 | "html.autoClosingTags": false,
12 | "typescript.autoClosingTags": false,
13 | "files.exclude": {
14 | "dist/**": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "command": "npm",
6 | "tasks": [
7 | {
8 | "label": "install",
9 | "type": "shell",
10 | "command": "npm",
11 | "args": ["install"]
12 | },
13 | {
14 | "label": "update",
15 | "type": "shell",
16 | "command": "npm",
17 | "args": ["update"]
18 | },
19 | {
20 | "label": "test",
21 | "type": "shell",
22 | "command": "npm",
23 | "args": ["run", "test"]
24 | },
25 | {
26 | "label": "build",
27 | "type": "shell",
28 | "group": "build",
29 | "command": "npm",
30 | "args": ["run", "watch"]
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Tomofumi Chiba
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 |
--------------------------------------------------------------------------------
/Obsidian-Memos-Chrome-Extension 0.1.3.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/Obsidian-Memos-Chrome-Extension 0.1.3.zip
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Obsidian Memos Extension
2 |
3 | This is an official Chrome extension for [Obsidian Memos](https://github.com/Quorafind/Obsidian-Memos) that lets you send content from the web to your memos in Obsidian.
4 |
5 | You will just need to install the extension and right click your selection or page and select "Send to Obsidian Memos".
6 |
7 | > Thanks to [coddingtonbear](https://github.com/coddingtonbear/obsidian-web) for the original code.
8 |
9 | ## Prerequisites
10 |
11 | * [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api)
12 | * Note: Supports use only with the default port (27124).
13 |
14 | You can read the instruction in Chinese here:
15 |
16 | ## Quickstart
17 |
18 | 1. Install this extension from releases, **not available on Chrome Store yet**.
19 | 2. Install and enable [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) from the Obsidian Community Plugins settings in Obsidian.
20 | 3. Click on the "Obsidian Memos Extension" icon in your toolbar and follow the displayed instructions.
21 |
22 | Now you should be able to send content from the web to your memos in Obsidian.
23 |
24 | ## Options
25 |
26 | Options can be accessed by right-clicking on the icon in your toolbar or pressing "Options/选项".
27 |
28 | 
29 |
30 | ## Development
31 |
32 | ```
33 | npm i
34 | npm run dev
35 | ```
36 |
37 | OR
38 |
39 | ```
40 | yarn
41 | yarn dev
42 | ```
43 |
44 | Then: load your "unpacked extension" from [Chrome Extensions](chrome://extensions/) by pointing Chrome at the `dist` folder. Afterward, you will receive some instructions whenyou click on the "Obsidian Web" icon in your toolbar.
45 |
--------------------------------------------------------------------------------
/assets/build_icons.dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | convert icon.dev.png -resize 256x256 ../public/icon256.png
3 | convert icon.dev.png -resize 128x128 ../public/icon128.png
4 | convert icon.dev.png -resize 48x48 ../public/icon48.png
5 | convert icon.dev.png -resize 16x16 ../public/icon16.png
6 |
--------------------------------------------------------------------------------
/assets/build_icons.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | convert icon.png -resize 256x256 ../public/icon256.png
3 | convert icon.png -resize 128x128 ../public/icon128.png
4 | convert icon.png -resize 48x48 ../public/icon48.png
5 | convert icon.png -resize 16x16 ../public/icon16.png
6 |
--------------------------------------------------------------------------------
/assets/icon.dev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/assets/icon.dev.png
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/assets/icon.png
--------------------------------------------------------------------------------
/assets/icon.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/assets/icon.xcf
--------------------------------------------------------------------------------
/assets/popup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/assets/popup.png
--------------------------------------------------------------------------------
/assets/popup.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/assets/popup.xcf
--------------------------------------------------------------------------------
/assets/preferences-simple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/assets/preferences-simple.png
--------------------------------------------------------------------------------
/assets/preferences-simple.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/assets/preferences-simple.xcf
--------------------------------------------------------------------------------
/assets/preferences-template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/assets/preferences-template.png
--------------------------------------------------------------------------------
/assets/preferences-template.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/assets/preferences-template.xcf
--------------------------------------------------------------------------------
/assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/assets/screenshot.png
--------------------------------------------------------------------------------
/assets/screenshot.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/assets/screenshot.xcf
--------------------------------------------------------------------------------
/img/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/img/img.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "roots": [
3 | "src"
4 | ],
5 | "transform": {
6 | "^.+\\.ts$": "ts-jest"
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-memos-extension",
3 | "version": "0.0.1",
4 | "description": "Connect your browser with your memos in Obsidian",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "webpack --config webpack/webpack.dev.js --watch",
8 | "build": "webpack --config webpack/webpack.prod.js",
9 | "build-icons-dev": "./scripts/build_icons.dev.sh",
10 | "build-icons": "./scripts/build_icons.sh",
11 | "dist-chrome": "./scripts/build_package.sh",
12 | "clean": "rimraf dist",
13 | "test": "npx jest",
14 | "style": "prettier --write \"src/**/*.{ts,tsx}\""
15 | },
16 | "author": "",
17 | "license": "MIT",
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/Quorafind/Memos-Extension.git"
21 | },
22 | "dependencies": {
23 | "@emotion/react": "^11.7.1",
24 | "@emotion/styled": "^11.6.0",
25 | "@mozilla/readability": "^0.4.2",
26 | "@mui/icons-material": "^5.3.1",
27 | "@mui/material": "^5.4.0",
28 | "compare-versions": "^4.1.3",
29 | "date-fns": "^2.28.0",
30 | "escape-string-regexp": "^5.0.0",
31 | "handlebars": "^4.7.7",
32 | "react": "^17.0.1",
33 | "react-dom": "^17.0.1",
34 | "turndown": "^7.1.1",
35 | "uuid": "^8.3.2"
36 | },
37 | "devDependencies": {
38 | "@types/chrome": "0.0.158",
39 | "@types/jest": "^27.0.2",
40 | "@types/react": "^17.0.0",
41 | "@types/react-dom": "^17.0.0",
42 | "@types/turndown": "^5.0.1",
43 | "@types/uuid": "^8.3.4",
44 | "copy-webpack-plugin": "^9.0.1",
45 | "glob": "^7.1.6",
46 | "jest": "^27.2.1",
47 | "prettier": "^2.2.1",
48 | "rimraf": "^3.0.2 ",
49 | "ts-jest": "^27.0.5",
50 | "ts-loader": "^8.0.0",
51 | "typescript": "^4.4.3 ",
52 | "webpack": "^5.0.0",
53 | "webpack-cli": "^4.0.0",
54 | "webpack-merge": "^5.0.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/public/MaterialIcons-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/public/MaterialIcons-Regular.ttf
--------------------------------------------------------------------------------
/public/Roboto-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/public/Roboto-Regular.ttf
--------------------------------------------------------------------------------
/public/handlebars.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Obsidian Handlebars Sandbox
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/public/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/public/icon128.png
--------------------------------------------------------------------------------
/public/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/public/icon16.png
--------------------------------------------------------------------------------
/public/icon256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/public/icon256.png
--------------------------------------------------------------------------------
/public/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Quorafind/Memos-Extension/2a743f6109b0ea393474aacdaa5e1dfa8cff640a/public/icon48.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 |
4 | "name": "Obsidian Memos Extension",
5 | "description": "Connect your browser with your memos in Obsidian",
6 | "version": "0.1.3",
7 |
8 | "options_page": "options.html",
9 |
10 | "background": {
11 | "service_worker": "js/background.js"
12 | },
13 |
14 | "sandbox": {
15 | "pages": ["handlebars.html"]
16 | },
17 |
18 | "icons": {
19 | "16": "icon16.png",
20 | "48": "icon48.png",
21 | "128": "icon128.png",
22 | "256": "icon256.png"
23 | },
24 |
25 | "action": {
26 | "default_icon": {
27 | "16": "icon16.png",
28 | "48": "icon48.png",
29 | "128": "icon128.png",
30 | "256": "icon256.png"
31 | },
32 | "default_popup": "options.html"
33 | },
34 |
35 | "commands": {
36 | "memos-selection": {
37 | "suggested_key": "Ctrl+Shift+X",
38 | "description": "Send Selection to Memos"
39 | },
40 | "memos-page": {
41 | "suggested_key": "Ctrl+Shift+Z",
42 | "description": "Send Page Url to Memos"
43 | }
44 | },
45 |
46 | "permissions": ["tabs","contextMenus","storage", "activeTab", "scripting"],
47 |
48 | "host_permissions": ["https://127.0.0.1:27124/*", "http://127.0.0.1:27123/*"]
49 | }
50 |
--------------------------------------------------------------------------------
/public/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Obsidian Memos Extension
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/public/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Obsidian Memos Pop-up
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/styles.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Roboto";
3 | font-style: normal;
4 | font-weight: 400;
5 | font-display: swap;
6 | src: url(chrome-extension://__MSG_@@extension_id__/Roboto-Regular.ttf);
7 | }
8 |
9 | @font-face {
10 | font-family: "Material Icons";
11 | font-style: normal;
12 | font-weight: 400;
13 | font-display: swap;
14 | src: url(chrome-extension://__MSG_@@extension_id__/MaterialIcons-Regular.ttf);
15 | }
16 |
17 | body {
18 | font-family: "Roboto";
19 | }
20 |
21 | body.options {
22 | min-width: 800px;
23 | }
24 |
25 | body.options #root {
26 | margin: 20px;
27 | }
28 |
29 | body.popup {
30 | width: 600px;
31 | }
32 |
33 | div.option-value {
34 | display: flex;
35 | flex-direction: row;
36 | margin-bottom: 10px;
37 | justify-content: space-between;
38 | }
39 |
40 | div.option-value.api-key {
41 | justify-content: left;
42 | }
43 |
44 | div.api-key-valid-icon svg {
45 | margin-top: 30%;
46 | }
47 |
48 | div.submit {
49 | display: flex;
50 | flex-direction: row;
51 | justify-content: right;
52 | }
53 |
54 | div.modal {
55 | margin: 50px 100px;
56 | padding: 25px;
57 | overflow-y: scroll;
58 | }
59 |
60 | div.options-header {
61 | display: flex;
62 | flex-direction: row;
63 | justify-content: left;
64 | }
65 |
66 | div.options-container {
67 | max-width: 800px;
68 | margin: 0 auto;
69 | padding: 10px 50px;
70 | }
71 |
72 | div.loading {
73 | display: flex;
74 | flex-direction: column;
75 | }
76 |
77 | div.loading > p {
78 | text-align: center;
79 | }
80 |
81 | div.loading > span {
82 | margin: 0 auto;
83 | }
84 |
85 | .paper-option-panel {
86 | padding: 5px 20px 20px 20px;
87 | margin-top: 10px;
88 | }
89 |
90 | div.mentions {
91 | display: flex;
92 | flex-direction: column;
93 | }
94 |
95 | div.mentions div.mention-notice {
96 | margin-top: 5px;
97 | }
98 |
99 | div.mentions div.mention-notice div.MuiAlert-message {
100 | width: 100%;
101 | }
102 |
103 | div.mentions div.mention-notice a {
104 | cursor: pointer;
105 | }
106 |
107 | .mention-cta {
108 | padding: 0 !important;
109 | float: right;
110 | }
111 |
112 | div.protip {
113 | margin-top: 10px;
114 | padding: 20px;
115 | background-color: #ede8ed;
116 | }
117 |
118 | div.protip p {
119 | margin-bottom: 0px;
120 | }
121 |
122 | .send-to-obsidian {
123 | width: 56px;
124 | height: 56px;
125 | }
126 |
127 | .selection_bubble {
128 | visibility: hidden;
129 | position: absolute;
130 | top: 0;
131 | left: 0;
132 | background:-webkit-gradient(linear, left top, left bottom, from(#2e88c4), to(#075698));
133 | }
134 |
--------------------------------------------------------------------------------
/scripts/build_icons.dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | convert assets/icon.dev.png -resize 256x256 public/icon256.png
3 | convert assets/icon.dev.png -resize 128x128 public/icon128.png
4 | convert assets/icon.dev.png -resize 48x48 public/icon48.png
5 | convert assets/icon.dev.png -resize 16x16 public/icon16.png
6 |
--------------------------------------------------------------------------------
/scripts/build_icons.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | convert assets/icon.png -resize 256x256 public/icon256.png
3 | convert assets/icon.png -resize 128x128 public/icon128.png
4 | convert assets/icon.png -resize 48x48 public/icon48.png
5 | convert assets/icon.png -resize 16x16 public/icon16.png
6 |
--------------------------------------------------------------------------------
/scripts/build_package.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | export ARCHIVE="obsidian-$(jq -r '.version' package.json).chrome.zip"
3 | npm run build-icons
4 | npm run build
5 | cd dist/
6 | zip -r ../$ARCHIVE ./
7 |
--------------------------------------------------------------------------------
/src/components/Alert.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import MaterialAlert from "@mui/material/Alert";
4 | import AlertTitle from "@mui/material/AlertTitle";
5 |
6 | import { AlertStatus } from "../types";
7 |
8 | interface Props {
9 | value: AlertStatus;
10 | onClick?: () => {};
11 | }
12 |
13 | const Alert: React.FC = ({ value, onClick }) => {
14 | return (
15 |
16 | {value.title}
17 | {value.message}
18 |
19 | );
20 | };
21 |
22 | export default Alert;
23 |
--------------------------------------------------------------------------------
/src/components/HeaderControl.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import TextField from "@mui/material/TextField";
3 |
4 | interface Props {
5 | headers: Record;
6 | onChange: (headers: Record) => void;
7 | }
8 |
9 | const HeaderControl: React.FC = ({ headers, onChange }) => {
10 | const [headersAsText, setHeadersAsText] = React.useState("");
11 |
12 | React.useEffect(() => {
13 | const rows = [];
14 | for (const header in headers) {
15 | rows.push(`${header}: ${headers[header]}`);
16 | }
17 | setHeadersAsText(rows.join("\n"));
18 | }, [headers]);
19 |
20 | const onChangedHeadersAsText = (value: string) => {
21 | const generatedHeaders: Record = {};
22 |
23 | for (const line of value.split("\n")) {
24 | const delimiter = line.indexOf(":");
25 | if (delimiter > -1) {
26 | generatedHeaders[line.slice(0, delimiter).trim()] = line
27 | .slice(delimiter + 1)
28 | .trim();
29 | }
30 | }
31 |
32 | onChange(generatedHeaders);
33 | };
34 |
35 | return (
36 | <>
37 | setHeadersAsText(event.target.value)}
43 | onBlur={(event) => onChangedHeadersAsText(event.target.value)}
44 | />
45 | >
46 | );
47 | };
48 |
49 | export default HeaderControl;
50 |
--------------------------------------------------------------------------------
/src/components/MentionNotice.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import MaterialAlert from "@mui/material/Alert";
3 | import Link from "@mui/material/Link";
4 | import IconButton from "@mui/material/IconButton";
5 |
6 | import UpgradeIcon from "@mui/icons-material/Upgrade";
7 |
8 | import { OutputPreset, SearchJsonResponseItem } from "../types";
9 | import { openFileInObsidian } from "../utils";
10 |
11 | export interface Props {
12 | apiKey: string;
13 | insecureMode: boolean;
14 | type: "mention" | "direct";
15 | templateSuggestion: string | undefined;
16 | mention: SearchJsonResponseItem;
17 | presets: OutputPreset[];
18 | acceptSuggestion: (filename: string, template: string) => void;
19 | }
20 |
21 | const MentionNotice: React.FC = ({
22 | type,
23 | templateSuggestion,
24 | apiKey,
25 | insecureMode,
26 | presets,
27 | mention,
28 | acceptSuggestion,
29 | }) => {
30 | const preset = presets.find((val) => val.name === templateSuggestion);
31 |
32 | return (
33 |
38 | {preset && (
39 | acceptSuggestion(mention.filename, preset.name)}
41 | className="mention-cta"
42 | aria-label="Use existing note"
43 | title="Use existing note"
44 | >
45 |
46 |
47 | )}
48 | {type === "direct" && <>This URL has a dedicated note: >}
49 | {type === "mention" && <>This URL is mentioned in an existing note: >}
50 |
53 | openFileInObsidian(apiKey, insecureMode, mention.filename)
54 | }
55 | >
56 | {mention.filename}
57 |
58 | .
59 |
60 | );
61 | };
62 |
63 | export default MentionNotice;
64 |
--------------------------------------------------------------------------------
/src/components/RequestParameters.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import TextField from "@mui/material/TextField";
4 | import Select from "@mui/material/Select";
5 | import MenuItem from "@mui/material/MenuItem";
6 |
7 | import { OutputPreset } from "../types";
8 | import HeaderControl from "./HeaderControl";
9 |
10 | interface Props {
11 | method: OutputPreset["method"];
12 | url: string;
13 | headers: Record;
14 | content: string;
15 |
16 | onChangeMethod: (method: OutputPreset["method"]) => void;
17 | onChangeUrl: (url: string) => void;
18 | onChangeHeaders: (headers: Record) => void;
19 | onChangeContent: (content: string) => void;
20 | }
21 |
22 | const RequestParameters: React.FC = ({
23 | method,
24 | url,
25 | headers,
26 | content,
27 | onChangeMethod,
28 | onChangeUrl,
29 | onChangeHeaders,
30 | onChangeContent,
31 | }) => {
32 | return (
33 | <>
34 |
35 |
36 |
40 | onChangeMethod(event.target.value as OutputPreset["method"])
41 | }
42 | >
43 | POST
44 | PUT
45 | PATCH
46 |
47 | onChangeUrl(event.target.value)}
52 | />
53 |
54 |
55 |
60 |
61 |
62 | onChangeContent(event.target.value)}
68 | />
69 |
70 |
71 | >
72 | );
73 | };
74 |
75 | export default RequestParameters;
76 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import TurndownService from "turndown";
2 |
3 | import { ExtensionLocalSettings, ExtensionSyncSettings } from "./types";
4 |
5 | export const MinVersion = "1.3.1";
6 |
7 | export const DefaultContentTemplate =
8 | '---\npage-title: {{json page.title}}\nurl: {{page.url}}\ndate: "{{date}}"\n---\n{{#if page.selectedText}}\n\n{{quote page.selectedText}}\n{{/if}}';
9 | export const DefaultUrlTemplate = "/vault/{{filename page.title}}.md";
10 | export const DefaultSendPageTemplate = "[{{page.title}}]({{page.url}}) #project";
11 | export const DefaultSendSelectionTemplate = "[{{page.title}}]({{page.url}}) {{selection}} #idea";
12 | export const DefaultHeaders = {};
13 | export const DefaultMethod = "put";
14 | export const DefaultSendHeading = "Journal";
15 |
16 | export const DefaultLocalSettings: ExtensionLocalSettings = {
17 | version: "0.1",
18 | insecureMode: false,
19 | apiKey: "",
20 | pageTemplate: "",
21 | selectionTemplate: "",
22 | heading: "Journal",
23 | };
24 |
25 | export const DefaultSyncSettings: ExtensionSyncSettings = {
26 | version: "0.1",
27 | presets: [
28 | {
29 | name: "Append to current daily note",
30 | urlTemplate: "/periodic/daily/",
31 | contentTemplate:
32 | "## {{page.title}}\nURL: {{page.url}}\n{{#if page.selectedText}}\n\n{{quote page.selectedText}}\n{{/if}}",
33 | headers: {},
34 | method: "post",
35 | },
36 | {
37 | name: "Create new note",
38 | urlTemplate: DefaultUrlTemplate,
39 | contentTemplate: DefaultContentTemplate,
40 | headers: DefaultHeaders,
41 | method: DefaultMethod,
42 | },
43 | {
44 | name: "Capture page snapshot",
45 | urlTemplate: DefaultUrlTemplate,
46 | contentTemplate:
47 | '---\npage-title: {{json page.title}}\nurl: {{page.url}}\ndate: "{{date}}"\n---\n{{#if page.selectedText}}\n\n{{quote page.selectedText}}\n\n---\n\n{{/if}}{{page.content}}',
48 | headers: DefaultHeaders,
49 | method: DefaultMethod,
50 | },
51 | {
52 | name: "Append to existing note",
53 | urlTemplate: "",
54 | contentTemplate:
55 | "## {{date}}\n{{#if page.selectedText}}\n\n{{quote page.selectedText}}\n{{/if}}",
56 | headers: {},
57 | method: "post",
58 | },
59 | ],
60 | searchEnabled: false,
61 | searchBackgroundEnabled: false,
62 | searchMatchMentionTemplate: "",
63 | searchMatchDirectTemplate: "Append to existing note",
64 | };
65 |
66 | export const TurndownConfiguration: TurndownService.Options = {
67 | headingStyle: "atx",
68 | hr: "---",
69 | bulletListMarker: "-",
70 | codeBlockStyle: "fenced",
71 | emDelimiter: "*",
72 | };
73 |
--------------------------------------------------------------------------------
/src/handlebars.ts:
--------------------------------------------------------------------------------
1 | import Handlebars from "handlebars";
2 | import { v4 as uuid } from "uuid";
3 | import { format as formatDate } from "date-fns";
4 | import {
5 | SandboxRequest,
6 | SandboxRenderResponse,
7 | SandboxRenderRequest,
8 | } from "./types";
9 |
10 | Handlebars.registerHelper("quote", (value: string): string => {
11 | const lines: string[] = [];
12 | for (const rawLine of value.split("\n")) {
13 | lines.push(`> ${rawLine}`);
14 | }
15 | return lines.join("\n");
16 | });
17 |
18 | Handlebars.registerHelper(
19 | "date",
20 | (format: string | { [key: string]: string }): string => {
21 | const now = new Date();
22 | let formatStr: string = "yyyy-MM-dd HH:mm:ss";
23 | if (typeof format === "string") {
24 | formatStr = format;
25 | }
26 | return formatDate(now, formatStr);
27 | }
28 | );
29 |
30 | Handlebars.registerHelper("filename", (unsafe: string | undefined): string => {
31 | if (typeof unsafe === "string") {
32 | return unsafe.replace(/[/\\?%*:|"<>]/g, "");
33 | }
34 | return "";
35 | });
36 |
37 | Handlebars.registerHelper("json", (unsafe: string | undefined): string => {
38 | if (typeof unsafe === "string") {
39 | return JSON.stringify(unsafe);
40 | }
41 | return "";
42 | });
43 |
44 | Handlebars.registerHelper("uuid", (): string => {
45 | return uuid();
46 | });
47 |
48 | const render = (request: SandboxRenderRequest): SandboxRenderResponse => {
49 | const compiled = Handlebars.compile(request.template, { noEscape: true });
50 |
51 | return {
52 | success: true,
53 | request,
54 | rendered: compiled(request.context),
55 | };
56 | };
57 |
58 | function handleEvent(evt: MessageEvent): void {
59 | const command = evt.data.command;
60 |
61 | const debug = document.getElementById("debug");
62 | if (debug) {
63 | debug.innerHTML = JSON.stringify(evt.data);
64 | }
65 |
66 | try {
67 | switch (command) {
68 | case "render":
69 | (evt.source as WindowProxy).postMessage(render(evt.data), evt.origin);
70 | break;
71 | default:
72 | throw new Error(`Unexpected command: ${command}`);
73 | }
74 | } catch (e) {
75 | (evt.source as WindowProxy).postMessage(
76 | {
77 | success: false,
78 | request: evt.data,
79 | message: (e as Error).message,
80 | },
81 | evt.origin
82 | );
83 | }
84 | }
85 |
86 | window.addEventListener("message", handleEvent);
87 | window.parent.postMessage({ loaded: true }, "*");
88 |
--------------------------------------------------------------------------------
/src/options.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import compareVersions from "compare-versions";
5 |
6 | import ThemeProvider from "@mui/system/ThemeProvider";
7 | import Button from "@mui/material/Button";
8 | import TextField from "@mui/material/TextField";
9 | import Typography from "@mui/material/Typography";
10 | import MaterialAlert from "@mui/material/Alert";
11 | import Table from "@mui/material/Table";
12 | import TableBody from "@mui/material/TableBody";
13 | import TableCell from "@mui/material/TableCell";
14 | import TableContainer from "@mui/material/TableContainer";
15 | import TableHead from "@mui/material/TableHead";
16 | import TableRow from "@mui/material/TableRow";
17 | import Paper from "@mui/material/Paper";
18 | import IconButton from "@mui/material/IconButton";
19 | import Modal from "@mui/material/Modal";
20 | import Switch from "@mui/material/Switch";
21 | import FormControlLabel from "@mui/material/FormControlLabel";
22 | import FormGroup from "@mui/material/FormGroup";
23 | import MenuItem from "@mui/material/MenuItem";
24 | import Select from "@mui/material/Select";
25 | import Chip from "@mui/material/Chip";
26 | import Snackbar from "@mui/material/Snackbar";
27 |
28 | import SecureConnection from "@mui/icons-material/GppGood";
29 | import InsecureConnection from "@mui/icons-material/GppMaybe";
30 | import Error from "@mui/icons-material/Error";
31 | import Copy from "@mui/icons-material/ContentCopy";
32 | import Promote from "@mui/icons-material/ArrowCircleUp";
33 | import Star from "@mui/icons-material/StarRate";
34 | import DeleteIcon from "@mui/icons-material/Delete";
35 | import EditIcon from "@mui/icons-material/Edit";
36 | import CreateIcon from "@mui/icons-material/AddCircle";
37 | import RestoreIcon from "@mui/icons-material/SettingsBackupRestore";
38 |
39 | import {
40 | DefaultContentTemplate,
41 | DefaultHeaders,
42 | DefaultMethod, DefaultSendHeading, DefaultSendPageTemplate, DefaultSendSelectionTemplate,
43 | DefaultSyncSettings,
44 | DefaultUrlTemplate,
45 | MinVersion,
46 | } from "./constants";
47 | import {
48 | ExtensionSyncSettings,
49 | OutputPreset,
50 | AlertStatus,
51 | ExtensionLocalSettings,
52 | StatusResponse,
53 | } from "./types";
54 | import {
55 | getLocalSettings,
56 | getSyncSettings,
57 | obsidianRequest,
58 | compileTemplate,
59 | } from "./utils";
60 | import Alert from "./components/Alert";
61 | import RequestParameters from "./components/RequestParameters";
62 | import { PurpleTheme } from "./theme";
63 |
64 | const Options = () => {
65 | const minVersion = MinVersion;
66 |
67 | const [loaded, setLoaded] = useState(false);
68 | const [status, setStatus] = useState();
69 | const [pluginVersion, setPluginVersion] = useState();
70 | const [modalStatus, setModalStatus] = useState();
71 |
72 | const [apiKey, setApiKey] = useState("");
73 | const [sendPageTemplate, setSendPageTemplate] = useState(
74 | DefaultSendPageTemplate
75 | );
76 | const [sendSelectionTemplate, setSendSelectionTemplate] = useState(
77 | DefaultSendSelectionTemplate
78 | );
79 | const [sendHeading, setSendHeading] = useState(
80 | DefaultSendHeading
81 | );
82 | const [apiKeyOk, setApiKeyOk] = useState(false);
83 | const [apiKeyError, setApiKeyError] = useState();
84 |
85 | const [searchEnabled, setSearchEnabled] = useState(false);
86 | const [searchBackgroundEnabled, setSearchBackgroundEnabled] =
87 | useState(false);
88 | const [searchMatchMentionTemplate, setSearchMatchMentionTemplate] =
89 | useState("");
90 | const [searchMatchDirectTemplate, setSearchMatchDirectTemplate] =
91 | useState("");
92 |
93 | const [presetName, setPresetName] = useState("");
94 | const [insecureMode, setInsecureMode] = useState(false);
95 | const [contentTemplate, setContentTemplate] = useState("");
96 | const [urlTemplate, setUrlTemplate] = useState("");
97 | const [headers, setHeaders] = useState>({});
98 | const [method, setMethod] = useState("post");
99 |
100 | const [editingPreset, setEditingPreset] = useState();
101 |
102 | const [presets, setPresets] = useState([]);
103 |
104 | useEffect(() => {
105 | async function handle() {
106 | setApiKeyOk(false);
107 | if (apiKey === "") {
108 | setApiKeyError(undefined);
109 | } else {
110 | let usedInsecureMode = false;
111 | let result: Response;
112 | try {
113 | result = await obsidianRequest(apiKey, "/", { method: "get" }, false);
114 | } catch (e) {
115 | try {
116 | result = await obsidianRequest(
117 | apiKey,
118 | "/",
119 | { method: "get" },
120 | true
121 | );
122 | usedInsecureMode = true;
123 | } catch (e) {
124 | setApiKeyError(
125 | `Unable to connect to Obsidian: ${
126 | (e as Error).message
127 | }. Obsidian Local REST API is probably running in secure-only mode, and your browser probably does not trust its certificate. Either enable insecure mode from Obsidian Local REST API's settings panel, or see the settings panel for instructions regarding where to acquire the certificate you need to configure your browser to trust.`
128 | );
129 | return;
130 | }
131 | }
132 |
133 | const body: StatusResponse = await result.json();
134 | if (result.status !== 200) {
135 | setApiKeyError(
136 | `Unable to connect to Obsidian: (Status Code ${
137 | result.status
138 | }) ${JSON.stringify(body)}.`
139 | );
140 | return;
141 | }
142 |
143 | setPluginVersion(body.versions.self);
144 |
145 | if (!body.authenticated) {
146 | setApiKeyError(`Your API key was not accepted.`);
147 | return;
148 | }
149 |
150 | setInsecureMode(usedInsecureMode);
151 | setApiKeyError(undefined);
152 | setApiKeyOk(true);
153 | }
154 |
155 | if (loaded) {
156 | // If we are *not* loaded, it means we're just in the process
157 | // of populating the form from stored settings. If we are,
158 | // it means you've changed something.
159 | await chrome.storage.local.set({
160 | apiKey,
161 | insecureMode,
162 | } as ExtensionLocalSettings);
163 | showSaveNotice();
164 | }
165 | }
166 |
167 | handle();
168 | }, [apiKey]);
169 |
170 | useEffect(() => {
171 | async function handle() {
172 | if (loaded) {
173 | await chrome.storage.local.set({
174 | heading: sendHeading,
175 | } as ExtensionLocalSettings);
176 | showSaveNotice();
177 | }
178 | }
179 | handle();
180 | }, [sendHeading]);
181 |
182 | useEffect(() => {
183 | async function handle() {
184 | if (loaded) {
185 | await chrome.storage.local.set({
186 | pageTemplate: sendPageTemplate,
187 | } as ExtensionLocalSettings);
188 | showSaveNotice();
189 | }
190 | }
191 | handle();
192 | }, [sendPageTemplate]);
193 |
194 | useEffect(() => {
195 | async function handle() {
196 | if (loaded) {
197 | await chrome.storage.local.set({
198 | selectionTemplate: sendSelectionTemplate,
199 | } as ExtensionLocalSettings);
200 | const localSettings = await getLocalSettings(chrome.storage.local);
201 | showSaveNotice();
202 | }
203 | }
204 | handle();
205 | }, [sendSelectionTemplate]);
206 |
207 | useEffect(() => {
208 | if (!loaded) {
209 | return;
210 | }
211 | async function handle() {
212 | await chrome.storage.sync.set({
213 | presets,
214 | searchEnabled,
215 | searchBackgroundEnabled,
216 | searchMatchDirectTemplate,
217 | searchMatchMentionTemplate,
218 | } as ExtensionSyncSettings);
219 | showSaveNotice();
220 | }
221 |
222 | handle();
223 | }, [
224 | presets,
225 | searchEnabled,
226 | searchBackgroundEnabled,
227 | searchMatchDirectTemplate,
228 | searchMatchMentionTemplate,
229 | ]);
230 |
231 | useEffect(() => {
232 | // Restores select box and checkbox state using the preferences
233 | // stored in chrome.storage.
234 | async function handle() {
235 | const syncSettings = await getSyncSettings(chrome.storage.sync);
236 | const localSettings = await getLocalSettings(chrome.storage.local);
237 |
238 | setApiKey(localSettings.apiKey);
239 | setSendPageTemplate(localSettings.pageTemplate);
240 | setSendSelectionTemplate(localSettings.selectionTemplate);
241 | setSendHeading(localSettings.heading);
242 | setPresets(syncSettings.presets);
243 | setSearchEnabled(syncSettings.searchEnabled);
244 | setSearchBackgroundEnabled(syncSettings.searchBackgroundEnabled);
245 | setSearchMatchDirectTemplate(syncSettings.searchMatchDirectTemplate);
246 | setSearchMatchMentionTemplate(syncSettings.searchMatchMentionTemplate);
247 | setLoaded(true);
248 |
249 | // If we do not have "tabs" permission; we can't really use
250 | // background search; so let's un-toggle that so they can re-toggle
251 | // it to re-probe for permissions
252 | chrome.permissions.contains(
253 | {
254 | permissions: ["tabs"],
255 | },
256 | (result) => {
257 | if (!result) {
258 | setSearchBackgroundEnabled(false);
259 | }
260 | }
261 | );
262 | }
263 |
264 | handle();
265 | }, []);
266 |
267 | const closeEditingModal = () => {
268 | setEditingPreset(undefined);
269 | };
270 |
271 | const openEditingModal = (
272 | idx: number | null,
273 | template?: number | undefined
274 | ) => {
275 | prepareForm(template ?? idx ?? null, template !== undefined);
276 | setEditingPreset(idx ?? -1);
277 | };
278 |
279 | const prepareForm = (idx: number | null, fromTemplate?: boolean) => {
280 | if (idx !== null) {
281 | const preset = presets[idx];
282 |
283 | setPresetName(fromTemplate ? `Copy of ${preset.name}` : preset.name);
284 | setContentTemplate(preset.contentTemplate);
285 | setUrlTemplate(preset.urlTemplate);
286 | setMethod(preset.method);
287 | setHeaders(preset.headers);
288 | } else {
289 | setPresetName("Untitled Preset");
290 | setContentTemplate(DefaultContentTemplate);
291 | setUrlTemplate(DefaultUrlTemplate);
292 | setMethod(DefaultMethod);
293 | setHeaders(DefaultHeaders);
294 | }
295 | };
296 |
297 | const deletePreset = (idx: number) => {
298 | const newPresets = [...presets.slice(0, idx), ...presets.slice(idx + 1)];
299 | setPresets(newPresets);
300 | };
301 |
302 | const setAsDefault = (idx: number) => {
303 | const thisItem = presets[idx];
304 |
305 | const newPresets = [
306 | thisItem,
307 | ...presets.slice(0, idx),
308 | ...presets.slice(idx + 1),
309 | ];
310 | setPresets(newPresets);
311 | };
312 |
313 | const restoreDefaultTemplates = () => {
314 | setPresets([...presets, ...DefaultSyncSettings.presets]);
315 | };
316 |
317 | const savePreset = async () => {
318 | if (editingPreset === undefined) {
319 | return;
320 | }
321 |
322 | let errorMessage: string | undefined = undefined;
323 |
324 | try {
325 | await compileTemplate(contentTemplate, {});
326 | } catch (e) {
327 | errorMessage = "Could not compile content template.";
328 | }
329 |
330 | if (!errorMessage) {
331 | try {
332 | await compileTemplate(urlTemplate, {});
333 | } catch (e) {
334 | errorMessage = "Could not compile url template.";
335 | }
336 | }
337 |
338 | if (errorMessage) {
339 | setModalStatus({
340 | severity: "error",
341 | title: "Error",
342 | message: `Could not save preset: ${errorMessage}`,
343 | });
344 | } else {
345 | setModalStatus(undefined);
346 | const preset = {
347 | name: presetName,
348 | urlTemplate: urlTemplate,
349 | contentTemplate: contentTemplate,
350 | headers: headers,
351 | method: method,
352 | };
353 | if (editingPreset === -1) {
354 | setPresets([...presets, preset]);
355 | } else {
356 | const newPresets = presets.slice();
357 | newPresets[editingPreset] = preset;
358 | setPresets(newPresets);
359 | }
360 | closeEditingModal();
361 | }
362 | };
363 |
364 | const onToggleBackgroundSearch = (targetStateEnabled: boolean) => {
365 | if (targetStateEnabled) {
366 | chrome.permissions.request(
367 | {
368 | permissions: ["tabs"],
369 | },
370 | (granted) => {
371 | if (granted) {
372 | setSearchBackgroundEnabled(targetStateEnabled);
373 | }
374 | }
375 | );
376 | } else {
377 | chrome.permissions.remove(
378 | {
379 | permissions: ["tabs"],
380 | },
381 | (removed) => {
382 | if (removed) {
383 | setSearchBackgroundEnabled(targetStateEnabled);
384 | }
385 | }
386 | );
387 | }
388 | };
389 |
390 | const showSaveNotice = () => {
391 | setStatus({
392 | severity: "success",
393 | title: "Success",
394 | message: "Options saved",
395 | });
396 | };
397 |
398 | return (
399 |
400 |
401 |
402 |
403 |
404 |
405 |
Obsidian Memos Extension Settings
406 |
407 |
408 | {editingPreset === undefined && (
409 | <>
410 |
411 | You can configure the connection between Obsidian Memos Extension and your
412 | Memos here.
413 |
414 |
415 | Obsidian Memos Extension integrates with Obsidian via the interface provided
416 | by the{" "}
417 |
421 | Local REST API
422 | {" "}
423 | plugin. Before beginning to use this, you will want to install
424 | and enable that plugin from within Obsidian.
425 |
426 |
427 |
428 |
setApiKey(event.target.value)}
433 | />
434 |
435 | {apiKeyOk && (
436 | <>
437 | {insecureMode && (
438 |
443 | )}
444 | {!insecureMode && (
445 |
450 | )}
451 | >
452 | )}
453 | {apiKeyError && (
454 |
459 | )}
460 |
461 |
462 | {apiKeyError && (
463 |
464 |
465 | {apiKeyError}
466 |
467 |
468 | )}
469 | {pluginVersion &&
470 | compareVersions(pluginVersion, minVersion) < 0 && (
471 | <>
472 |
473 |
474 |
475 | Your install of Obsidian Local REST API is
476 | out-of-date and missing some important capabilities.
477 | {" "}
478 | Some features may not work correctly as a result.
479 | Please go to the "Community Plugins" section of your
480 | settings in Obsidian to update the "Obsidian Local
481 | REST API" plugin to the latest version.
482 |
483 |
484 | >
485 | )}
486 |
487 |
488 | {/*
*/}
489 | {/*
Note Recall */}
490 | {/*
*/}
491 | {/* Have you been to this page before? Maybe you already have*/}
492 | {/* notes about it. Enabling this feature will let this extension*/}
493 | {/* search your notes when you click on the extension icon and, if*/}
494 | {/* you enable background searches, show a badge on the extension*/}
495 | {/* icon while you are browsing the web to let you know that you*/}
496 | {/* have notes about the page you are currently visiting.*/}
497 | {/* */}
498 | {/*
*/}
499 | {/* {*/}
503 | {/* // If background search is enabled; disable it first.*/}
504 | {/* if (searchBackgroundEnabled) {*/}
505 | {/* onToggleBackgroundSearch(false);*/}
506 | {/* }*/}
507 | {/* setSearchEnabled(evt.target.checked);*/}
508 | {/* }}*/}
509 | {/* checked={searchEnabled}*/}
510 | {/* />*/}
511 | {/* }*/}
512 | {/* label={*/}
513 | {/* <>*/}
514 | {/* Search for previous notes about this page when you open*/}
515 | {/* the extension menu?*/}
516 | {/* >*/}
517 | {/* }*/}
518 | {/* />*/}
519 | {/* */}
520 | {/*
*/}
521 | {/* */}
525 | {/* onToggleBackgroundSearch(evt.target.checked)*/}
526 | {/* }*/}
527 | {/* disabled={!searchEnabled}*/}
528 | {/* checked={searchBackgroundEnabled}*/}
529 | {/* />*/}
530 | {/* }*/}
531 | {/* label={*/}
532 | {/* <>*/}
533 | {/* Search for previous notes about this page in the*/}
534 | {/* background?*/}
535 | {/* */}
536 | {/* >*/}
537 | {/* }*/}
538 | {/* />*/}
539 | {/* */}
540 | {/* {searchEnabled && (*/}
541 | {/*
*/}
542 | {/* Page Notes */}
543 | {/* */}
544 | {/* When the URL of the page you are visiting has been found*/}
545 | {/* to match the url
field in the frontmatter of*/}
546 | {/* an existing note in your vault, suggest this template for*/}
547 | {/* updating the existing note:*/}
548 | {/* */}
549 | {/* */}
555 | {/* setSearchMatchDirectTemplate(event.target.value)*/}
556 | {/* }*/}
557 | {/* >*/}
558 | {/* */}
559 | {/* None (Do not suggest updating the existing note)*/}
560 | {/* */}
561 | {/* {presets.map((preset) => (*/}
562 | {/* */}
563 | {/* {preset.name}*/}
564 | {/* */}
565 | {/* ))}*/}
566 | {/* */}
567 | {/* Mentions */}
568 | {/* */}
569 | {/* When the URL of the page you are visiting has been found*/}
570 | {/* in the content of a note in your vault, suggest this*/}
571 | {/* template for updating the existing note:*/}
572 | {/* */}
573 | {/* */}
579 | {/* setSearchMatchMentionTemplate(event.target.value)*/}
580 | {/* }*/}
581 | {/* >*/}
582 | {/* */}
583 | {/* None (Do not suggest updating the existing note)*/}
584 | {/* */}
585 | {/* {presets.map((preset) => (*/}
586 | {/* */}
587 | {/* {preset.name}*/}
588 | {/* */}
589 | {/* ))}*/}
590 | {/* */}
591 | {/* */}
592 | {/* )}*/}
593 | {/*
*/}
594 |
595 |
596 |
Heading
597 |
598 | setSendHeading(event.target.value)}
603 | />
604 |
605 |
606 |
607 |
Page URL Template
608 |
609 | setSendPageTemplate(event.target.value)}
614 | />
615 |
616 |
617 |
618 |
Page URL Template
619 |
620 | setSendSelectionTemplate(event.target.value)}
625 | />
626 |
627 |
628 |
629 | {/*
*/}
630 | {/*
Templates */}
631 | {/*
*/}
632 | {/* You can configure multiple templates for use when inserting*/}
633 | {/* content into Obsidian. Each template describes how to convert*/}
634 | {/* information about the current tab into content for insertion*/}
635 | {/* into your notes.*/}
636 | {/* */}
637 | {/*
*/}
638 | {/*
*/}
639 | {/* */}
640 | {/* */}
641 | {/* */}
642 | {/* */}
643 | {/* Name */}
644 | {/* Options */}
645 | {/* */}
646 | {/* */}
647 | {/* */}
648 | {/* {presets.map((preset, idx) => (*/}
649 | {/* */}
650 | {/* */}
651 | {/* {idx === 0 && (*/}
652 | {/* */}
653 | {/* )}*/}
654 | {/* */}
655 | {/* */}
656 | {/* {preset.name}*/}
657 | {/* */}
658 | {/* */}
659 | {/* {idx !== 0 && (*/}
660 | {/* {*/}
664 | {/* setAsDefault(idx);*/}
665 | {/* }}*/}
666 | {/* >*/}
667 | {/* */}
668 | {/* */}
669 | {/* )}*/}
670 | {/* {*/}
674 | {/* openEditingModal(idx);*/}
675 | {/* }}*/}
676 | {/* >*/}
677 | {/* */}
678 | {/* */}
679 | {/* {*/}
683 | {/* openEditingModal(null, idx);*/}
684 | {/* }}*/}
685 | {/* >*/}
686 | {/* */}
687 | {/* */}
688 | {/* {presets.length > 1 && (*/}
689 | {/* {*/}
693 | {/* deletePreset(idx);*/}
694 | {/* }}*/}
695 | {/* >*/}
696 | {/* */}
697 | {/* */}
698 | {/* )}*/}
699 | {/* */}
700 | {/* */}
701 | {/* ))}*/}
702 | {/* */}
703 | {/* */}
704 | {/* */}
705 | {/* */}
706 | {/* restoreDefaultTemplates()}*/}
708 | {/* >*/}
709 | {/* */}
710 | {/* */}
711 | {/* openEditingModal(null)}>*/}
712 | {/* */}
713 | {/* */}
714 | {/* */}
715 | {/* */}
716 | {/* */}
717 | {/*
*/}
718 | {/* */}
719 | {/*
*/}
720 | {/*
*/}
721 |
722 | {/*
*/}
723 | {/* */}
724 | {/* Protip: Looking for ideas about how you can*/}
725 | {/* use this plugin to improve your workflow; have a look at the{" "}*/}
726 | {/* */}
730 | {/* Wiki*/}
731 | {/* {" "}*/}
732 | {/* for tips.*/}
733 | {/* */}
734 | {/* */}
735 |
736 |
setStatus(undefined)}
740 | >
741 |
742 |
743 | >
744 | )}
745 |
746 |
747 |
748 | );
749 | };
750 |
751 | ReactDOM.render(
752 |
753 |
754 | ,
755 | document.getElementById("root")
756 | );
757 |
--------------------------------------------------------------------------------
/src/popup.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | import ReactDOM from "react-dom";
4 | import Turndown from "turndown";
5 | import { Readability } from "@mozilla/readability";
6 |
7 | import Button from "@mui/material/Button";
8 | import MenuItem from "@mui/material/MenuItem";
9 | import Select from "@mui/material/Select";
10 | import ThemeProvider from "@mui/system/ThemeProvider";
11 | import IconButton from "@mui/material/IconButton";
12 | import Accordion from "@mui/material/Accordion";
13 | import AccordionDetails from "@mui/material/AccordionDetails";
14 | import AccordionSummary from "@mui/material/AccordionSummary";
15 | import Typography from "@mui/material/Typography";
16 | import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
17 | import CircularProgress from "@mui/material/CircularProgress";
18 | import MaterialAlert from "@mui/material/Alert";
19 |
20 | import SendIcon from "@mui/icons-material/SaveAlt";
21 |
22 | import { PurpleTheme } from "./theme";
23 | import Alert from "./components/Alert";
24 | import {
25 | AlertStatus,
26 | ExtensionLocalSettings,
27 | ExtensionSyncSettings,
28 | OutputPreset,
29 | SearchJsonResponseItem,
30 | StatusResponse,
31 | } from "./types";
32 | import {
33 | getLocalSettings,
34 | getSyncSettings,
35 | obsidianRequest,
36 | compileTemplate,
37 | getUrlMentions,
38 | } from "./utils";
39 | import RequestParameters from "./components/RequestParameters";
40 | import { TurndownConfiguration } from "./constants";
41 | import MentionNotice from "./components/MentionNotice";
42 |
43 | const Popup = () => {
44 | const [status, setStatus] = useState();
45 |
46 | const [sandboxReady, setSandboxReady] = useState(false);
47 | const [obsidianUnavailable, setObsidianUnavailable] =
48 | useState(false);
49 | const [ready, setReady] = useState(false);
50 | const [apiKey, setApiKey] = useState("");
51 | const [insecureMode, setInsecureMode] = useState(false);
52 |
53 | const [url, setUrl] = useState("");
54 | const [title, setTitle] = useState("");
55 | const [selection, setSelection] = useState("");
56 | const [pageContent, setPageContent] = useState("");
57 |
58 | const [suggestionAccepted, setSuggestionAccepted] = useState(false);
59 | const [mentions, setMentions] = useState([]);
60 | const [directReferences, setDirectReferences] = useState<
61 | SearchJsonResponseItem[]
62 | >([]);
63 |
64 | const [searchEnabled, setSearchEnabled] = useState(false);
65 | const [searchMatchMentionTemplate, setSearchMatchMentionTemplate] =
66 | useState("");
67 | const [searchMatchDirectTemplate, setSearchMatchDirectTemplate] =
68 | useState("");
69 |
70 | const [method, setMethod] = useState("post");
71 | const [overrideUrl, setOverrideUrl] = useState();
72 | const [compiledUrl, setCompiledUrl] = useState("");
73 | const [headers, setHeaders] = useState>({});
74 | const [compiledContent, setCompiledContent] = useState("");
75 |
76 | const [presets, setPresets] = useState([]);
77 | const [selectedPreset, setSelectedPreset] = useState(0);
78 |
79 | const turndown = new Turndown(TurndownConfiguration);
80 |
81 | window.addEventListener(
82 | "message",
83 | () => {
84 | setSandboxReady(true);
85 | },
86 | {
87 | once: true,
88 | }
89 | );
90 |
91 | useEffect(() => {
92 | if (!apiKey) {
93 | return;
94 | }
95 |
96 | async function handle() {
97 | try {
98 | const request = await obsidianRequest(
99 | apiKey,
100 | "/",
101 | { method: "get" },
102 | insecureMode
103 | );
104 | const result: StatusResponse = await request.json();
105 | if (
106 | result.status === "OK" &&
107 | result.service.includes("Obsidian Local REST API")
108 | ) {
109 | setObsidianUnavailable(false);
110 | } else {
111 | setObsidianUnavailable(true);
112 | }
113 | } catch (e) {
114 | setObsidianUnavailable(true);
115 | }
116 | }
117 | handle();
118 | }, []);
119 |
120 | useEffect(() => {
121 | async function handle() {
122 | let syncSettings: ExtensionSyncSettings;
123 | let localSettings: ExtensionLocalSettings;
124 |
125 | try {
126 | localSettings = await getLocalSettings(chrome.storage.local);
127 | } catch (e) {
128 | setStatus({
129 | severity: "error",
130 | title: "Error",
131 | message: "Could not get local settings!",
132 | });
133 | return;
134 | }
135 |
136 | try {
137 | syncSettings = await getSyncSettings(chrome.storage.sync);
138 | setPresets(syncSettings.presets);
139 | } catch (e) {
140 | setStatus({
141 | severity: "error",
142 | title: "Error",
143 | message: "Could not get settings!",
144 | });
145 | return;
146 | }
147 |
148 | setApiKey(localSettings.apiKey);
149 | setSearchEnabled(syncSettings.searchEnabled);
150 | setSearchMatchMentionTemplate(syncSettings.searchMatchMentionTemplate);
151 | setSearchMatchDirectTemplate(syncSettings.searchMatchDirectTemplate);
152 | setInsecureMode(localSettings.insecureMode ?? false);
153 | }
154 | handle();
155 | }, []);
156 |
157 | useEffect(() => {
158 | async function handle() {
159 | let tab: chrome.tabs.Tab;
160 | try {
161 | const tabs = await chrome.tabs.query({
162 | active: true,
163 | currentWindow: true,
164 | });
165 | tab = tabs[0];
166 | } catch (e) {
167 | setStatus({
168 | severity: "error",
169 | title: "Error",
170 | message: "Could not get current tab!",
171 | });
172 | return;
173 | }
174 | if (!tab.id) {
175 | return;
176 | }
177 |
178 | let selectedText: string;
179 | try {
180 | const selectedTextInjected = await chrome.scripting.executeScript({
181 | target: { tabId: tab.id },
182 | func: () => window.getSelection()?.toString(),
183 | });
184 | selectedText = selectedTextInjected[0].result;
185 | } catch (e) {
186 | selectedText = "";
187 | }
188 |
189 | let pageContent: string = "";
190 | try {
191 | const pageContentInjected = await chrome.scripting.executeScript({
192 | target: { tabId: tab.id },
193 | func: () => window.document.body.innerHTML,
194 | });
195 | const tempDoc = document.implementation.createHTMLDocument();
196 | tempDoc.body.innerHTML = pageContentInjected[0].result;
197 | const reader = new Readability(tempDoc);
198 | const parsed = reader.parse();
199 | if (parsed) {
200 | pageContent = turndown.turndown(parsed.content);
201 | }
202 | } catch (e) {
203 | // Nothing -- we'll just have no pageContent
204 | }
205 |
206 | setUrl(tab.url ?? "");
207 | setTitle(tab.title ?? "");
208 | setSelection(selectedText);
209 | setPageContent(pageContent);
210 | }
211 | handle();
212 | }, []);
213 |
214 | useEffect(() => {
215 | if (!searchEnabled) {
216 | return;
217 | }
218 |
219 | async function handle() {
220 | const allMentions = await getUrlMentions(apiKey, insecureMode, url);
221 |
222 | setMentions(allMentions.mentions);
223 | setDirectReferences(allMentions.direct);
224 | }
225 |
226 | handle();
227 | }, [url]);
228 |
229 | useEffect(() => {
230 | if (!sandboxReady) {
231 | return;
232 | }
233 |
234 | async function handle() {
235 | const preset = presets[selectedPreset];
236 |
237 | const context = {
238 | page: {
239 | url: url,
240 | title: title,
241 | selectedText: selection,
242 | content: pageContent,
243 | },
244 | };
245 |
246 | if (overrideUrl) {
247 | setCompiledUrl(overrideUrl);
248 | setOverrideUrl(undefined);
249 | } else {
250 | const compiledUrl = await compileTemplate(preset.urlTemplate, context);
251 | setCompiledUrl(compiledUrl);
252 | }
253 | const compiledContent = await compileTemplate(
254 | preset.contentTemplate,
255 | context
256 | );
257 |
258 | setMethod(preset.method as OutputPreset["method"]);
259 | setHeaders(preset.headers);
260 | setCompiledContent(compiledContent);
261 | setReady(true);
262 | }
263 |
264 | handle();
265 | }, [
266 | sandboxReady,
267 | selectedPreset,
268 | presets,
269 | url,
270 | title,
271 | selection,
272 | pageContent,
273 | ]);
274 |
275 | const sendToObsidian = async () => {
276 | const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
277 | const tab = tabs[0];
278 |
279 | if (!tab.id) {
280 | return;
281 | }
282 |
283 | const requestHeaders = {
284 | ...headers,
285 | "Content-Type": "text/markdown",
286 | };
287 | const request: RequestInit = {
288 | method: method,
289 | body: compiledContent,
290 | headers: requestHeaders,
291 | };
292 | const result = await obsidianRequest(
293 | apiKey,
294 | compiledUrl,
295 | request,
296 | insecureMode
297 | );
298 | const text = await result.text();
299 |
300 | if (result.status < 300) {
301 | setStatus({
302 | severity: "success",
303 | title: "All done!",
304 | message: "Your content was sent to Obsidian successfully.",
305 | });
306 | setTimeout(() => window.close(), 2000);
307 | } else {
308 | try {
309 | const body = JSON.parse(text);
310 | setStatus({
311 | severity: "error",
312 | title: "Error",
313 | message: `Could not send content to Obsidian: (Error Code ${body.errorCode}) ${body.message}`,
314 | });
315 | } catch (e) {
316 | setStatus({
317 | severity: "error",
318 | title: "Error",
319 | message: `Could not send content to Obsidian!: (Status Code ${result.status}) ${text}`,
320 | });
321 | }
322 | }
323 | };
324 |
325 | const acceptSuggestion = (filename: string, template: string) => {
326 | const matchingPresetIdx = presets.findIndex(
327 | (preset) => preset.name === template
328 | );
329 | setOverrideUrl(`/vault/${filename}`);
330 | setSelectedPreset(matchingPresetIdx);
331 | setSuggestionAccepted(true);
332 | };
333 |
334 | return (
335 |
336 | {ready && !status && !obsidianUnavailable && (
337 | <>
338 | {apiKey.length === 0 && (
339 | <>
340 |
341 | Thanks for installing Obsidian Memos Extension! Obsidian Memos Extension needs some
342 | information from you before it can connect to your Obsidian
343 | instance.
344 | chrome.runtime.openOptionsPage()}>
345 | Go to settings
346 |
347 |
348 | >
349 | )}
350 | {apiKey && (
351 | <>
352 |
353 |
354 |
359 | setSelectedPreset(
360 | typeof event.target.value === "number"
361 | ? event.target.value
362 | : parseInt(event.target.value, 10)
363 | )
364 | }
365 | >
366 | {presets.map((preset, idx) => (
367 |
368 | {preset.name}
369 |
370 | ))}
371 |
372 |
380 |
381 |
382 |
383 |
384 |
385 | }>
386 | Entry Details
387 |
388 |
389 |
399 |
400 |
401 | {!suggestionAccepted && (
402 | <>
403 | {(mentions.length > 0 || directReferences.length > 0) && (
404 |
405 | {directReferences.map((ref) => (
406 |
416 | ))}
417 | {mentions
418 | .filter(
419 | (ref) =>
420 | !directReferences.find(
421 | (d) => d.filename === ref.filename
422 | )
423 | )
424 | .map((ref) => (
425 |
435 | ))}
436 |
437 | )}
438 | >
439 | )}
440 | >
441 | )}
442 | >
443 | )}
444 | {obsidianUnavailable && (
445 | <>
446 |
447 | Could not connect to Obsidian! Make sure Obsidian is running and
448 | that the Obsidian Local REST API plugin is enabled.
449 |
450 | >
451 | )}
452 | {!ready && !obsidianUnavailable && (
453 |
454 | {" "}
455 |
456 | Gathering page information...
457 |
458 |
459 |
460 | )}
461 | {status && }
462 |
463 | );
464 | };
465 |
466 | ReactDOM.render(
467 |
468 |
469 | ,
470 | document.getElementById("root")
471 | );
472 |
--------------------------------------------------------------------------------
/src/sendIt.ts:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { AlertStatus, ExtensionLocalSettings, ExtensionSyncSettings, OutputPreset, StatusResponse } from "./types";
3 | import { compileTemplate, compileToMemo, getLocalSettings, getSyncSettings, obsidianRequest } from "./utils";
4 | import OnClickData = chrome.contextMenus.OnClickData;
5 |
6 | //
7 | // const SendIt = (info:OnClickData) => {
8 | // const [status, setStatus] = useState();
9 | //
10 | // const [sandboxReady, setSandboxReady] = useState(false);
11 | // const [obsidianUnavailable, setObsidianUnavailable] =
12 | // useState(false);
13 | // const [ready, setReady] = useState(false);
14 | // const [apiKey, setApiKey] = useState("");
15 | // const [insecureMode, setInsecureMode] = useState(false);
16 | //
17 | // const [url, setUrl] = useState("");
18 | // const [title, setTitle] = useState("");
19 | // const [selection, setSelection] = useState("");
20 | // const [pageContent, setPageContent] = useState("");
21 | //
22 | // const [method, setMethod] = useState("patch");
23 | // const [overrideUrl, setOverrideUrl] = useState();
24 | // const [compiledUrl, setCompiledUrl] = useState("");
25 | // const [headers, setHeaders] = useState>({});
26 | // const [compiledContent, setCompiledContent] = useState("");
27 | //
28 | // const [presets, setPresets] = useState([]);
29 | // const [selectedPreset, setSelectedPreset] = useState(0);
30 | //
31 | // window.addEventListener(
32 | // "message",
33 | // () => {
34 | // setSandboxReady(true);
35 | // },
36 | // {
37 | // once: true,
38 | // }
39 | // );
40 | //
41 | // useEffect(() => {
42 | // if (!apiKey) {
43 | // return;
44 | // }
45 | //
46 | // async function handle() {
47 | // try {
48 | // const request = await obsidianRequest(
49 | // apiKey,
50 | // "/",
51 | // { method: "get" },
52 | // insecureMode
53 | // );
54 | // const result: StatusResponse = await request.json();
55 | // if (
56 | // result.status === "OK" &&
57 | // result.service.includes("Obsidian Local REST API")
58 | // ) {
59 | // setObsidianUnavailable(false);
60 | // } else {
61 | // setObsidianUnavailable(true);
62 | // }
63 | // } catch (e) {
64 | // setObsidianUnavailable(true);
65 | // }
66 | // }
67 | // handle();
68 | // }, []);
69 | //
70 | // useEffect(() => {
71 | // async function handle() {
72 | // let syncSettings: ExtensionSyncSettings;
73 | // let localSettings: ExtensionLocalSettings;
74 | //
75 | // try {
76 | // localSettings = await getLocalSettings(chrome.storage.local);
77 | // } catch (e) {
78 | // setStatus({
79 | // severity: "error",
80 | // title: "Error",
81 | // message: "Could not get local settings!",
82 | // });
83 | // return;
84 | // }
85 | //
86 | // try {
87 | // syncSettings = await getSyncSettings(chrome.storage.sync);
88 | // setPresets(syncSettings.presets);
89 | // } catch (e) {
90 | // setStatus({
91 | // severity: "error",
92 | // title: "Error",
93 | // message: "Could not get settings!",
94 | // });
95 | // return;
96 | // }
97 | //
98 | // setApiKey(localSettings.apiKey);
99 | // }
100 | // handle();
101 | // }, []);
102 | //
103 | // useEffect(() => {
104 | // async function handle() {
105 | // let tab: chrome.tabs.Tab;
106 | // try {
107 | // const tabs = await chrome.tabs.query({
108 | // active: true,
109 | // currentWindow: true,
110 | // });
111 | // tab = tabs[0];
112 | // } catch (e) {
113 | // setStatus({
114 | // severity: "error",
115 | // title: "Error",
116 | // message: "Could not get current tab!",
117 | // });
118 | // return;
119 | // }
120 | // if (!tab.id) {
121 | // return;
122 | // }
123 | //
124 | // let selectedText: string;
125 | // try {
126 | // const selectedTextInjected = await chrome.scripting.executeScript({
127 | // target: { tabId: tab.id },
128 | // func: () => window.getSelection()?.toString(),
129 | // });
130 | // selectedText = selectedTextInjected[0].result;
131 | // } catch (e) {
132 | // selectedText = "";
133 | // }
134 | //
135 | // let pageContent: string = "";
136 | // // try {
137 | // // const pageContentInjected = await chrome.scripting.executeScript({
138 | // // target: { tabId: tab.id },
139 | // // func: () => window.document.body.innerHTML,
140 | // // });
141 | // // const tempDoc = document.implementation.createHTMLDocument();
142 | // // tempDoc.body.innerHTML = pageContentInjected[0].result;
143 | // // const reader = new Readability(tempDoc);
144 | // // const parsed = reader.parse();
145 | // // if (parsed) {
146 | // // pageContent = turndown.turndown(parsed.content);
147 | // // }
148 | // // } catch (e) {
149 | // // // Nothing -- we'll just have no pageContent
150 | // // }
151 | //
152 | // setUrl(tab.url ?? "");
153 | // setTitle(tab.title ?? "");
154 | // setSelection(selectedText);
155 | // setPageContent(pageContent);
156 | // }
157 | // handle();
158 | // }, []);
159 | //
160 | //
161 | // useEffect(() => {
162 | // if (!sandboxReady) {
163 | // return;
164 | // }
165 | //
166 | // if(info.selectionText === '') {
167 | // setStatus({
168 | // severity: "error",
169 | // title: "Error",
170 | // message: "No text selected!",
171 | // });
172 | // return;
173 | // }
174 | //
175 | // async function handle() {
176 | // const preset = presets[selectedPreset];
177 | //
178 | // const context = {
179 | // page: {
180 | // url: info.pageUrl,
181 | // title: title,
182 | // selectedText: info.selectionText,
183 | // content: pageContent,
184 | // },
185 | // };
186 | //
187 | // if (overrideUrl) {
188 | // setCompiledUrl(overrideUrl);
189 | // setOverrideUrl(undefined);
190 | // } else {
191 | // // const compiledUrl = await compileTemplate(preset.urlTemplate, context);
192 | //
193 | // setCompiledUrl("/periodic/daily");
194 | // }
195 | // const compiledContent = compileToMemo(typeof info.selectionText === "string" ? info.selectionText : "");
196 | //
197 | // // setMethod(preset.method as OutputPreset["method"]);
198 | // // setMethod("PATCH");
199 | // setHeaders(preset.headers);
200 | // setCompiledContent(compiledContent);
201 | // setReady(true);
202 | // }
203 | //
204 | // handle();
205 | // }, [
206 | // sandboxReady,
207 | // selectedPreset,
208 | // presets,
209 | // url,
210 | // title,
211 | // selection,
212 | // pageContent,
213 | // ]);
214 | //
215 | // useEffect(() => {
216 | // if(!ready) {
217 | // return;
218 | // }
219 | //
220 | // if(info.selectionText === '') {
221 | // setStatus({
222 | // severity: "error",
223 | // title: "Error",
224 | // message: "No text selected!",
225 | // });
226 | // return;
227 | // }
228 | //
229 | // const sendToObsidian = async () => {
230 | // const requestHeaders = {
231 | // ...headers,
232 | // "Content-Type": "text/markdown",
233 | // };
234 | // const request: RequestInit = {
235 | // method: method,
236 | // body: compiledContent,
237 | // headers: requestHeaders,
238 | // };
239 | // const result = await obsidianRequest(
240 | // apiKey,
241 | // compiledUrl,
242 | // request,
243 | // insecureMode
244 | // );
245 | // const text = await result.text();
246 | //
247 | // if (result.status < 300) {
248 | // setStatus({
249 | // severity: "success",
250 | // title: "All done!",
251 | // message: "Your content was sent to Obsidian successfully.",
252 | // });
253 | // setTimeout(() => window.close(), 2000);
254 | // } else {
255 | // try {
256 | // const body = JSON.parse(text);
257 | // setStatus({
258 | // severity: "error",
259 | // title: "Error",
260 | // message: `Could not send content to Obsidian: (Error Code ${body.errorCode}) ${body.message}`,
261 | // });
262 | // } catch (e) {
263 | // setStatus({
264 | // severity: "error",
265 | // title: "Error",
266 | // message: `Could not send content to Obsidian!: (Status Code ${result.status}) ${text}`,
267 | // });
268 | // }
269 | // }
270 | // };
271 | // }, [ready, compiledUrl, compiledContent, method, headers, insecureMode, apiKey]);
272 | //
273 | // return (
274 | // <>
275 | // >
276 | //
277 | // )
278 | // }
279 |
280 |
281 | export const SendItCommand = async (commandId?: string) => {
282 |
283 | let syncSettings: ExtensionSyncSettings;
284 | let localSettings: ExtensionLocalSettings;
285 | let apiKey: string;
286 | let heading: string;
287 | let insecureMode: boolean = false;
288 | let obsidianUnavailable: boolean = false;
289 | let pageContent: string = "";
290 | let obsidianDailyNoteUnavailable: boolean = false;
291 | let title: string = "";
292 | let compiledUrl: string = "/periodic/daily";
293 | let method: string = "PATCH";
294 | let headers: { [key: string]: string } = {};
295 | let position: string = "end";
296 |
297 | localSettings = await getLocalSettings(chrome.storage.local);
298 |
299 | try {
300 | localSettings = await getLocalSettings(chrome.storage.local);
301 | apiKey = localSettings.apiKey;
302 | } catch (e) {
303 | console.error(e);
304 | return;
305 | }
306 |
307 | try {
308 | localSettings = await getLocalSettings(chrome.storage.local);
309 | heading = localSettings.heading;
310 | } catch (e) {
311 | console.error(e);
312 | return;
313 | }
314 |
315 | try {
316 | const request = await obsidianRequest(
317 | apiKey,
318 | "/",
319 | { method: "get" },
320 | insecureMode
321 | );
322 | const result: StatusResponse = await request.json();
323 | obsidianUnavailable = !(result.status === "OK" &&
324 | result.service.includes("Obsidian Local REST API"));
325 | } catch (e) {
326 | obsidianUnavailable = true;
327 | }
328 |
329 | try {
330 | const request = await obsidianRequest(
331 | apiKey,
332 | "/periodic/daily",
333 | { method: "get" },
334 | insecureMode
335 | );
336 | obsidianDailyNoteUnavailable = !(request.statusText === "OK");
337 | } catch (e) {
338 | console.error(e);
339 | obsidianDailyNoteUnavailable = true;
340 | }
341 |
342 | if(obsidianDailyNoteUnavailable && !obsidianUnavailable) {
343 | try {
344 | const request = await obsidianRequest(
345 | apiKey,
346 | "/commands/daily-notes",
347 | { method: "post" },
348 | insecureMode
349 | );
350 | obsidianDailyNoteUnavailable = !(request.statusText === "OK");
351 | } catch (e) {
352 | console.error(e);
353 | obsidianDailyNoteUnavailable = true;
354 | }
355 | }
356 |
357 | let tab: chrome.tabs.Tab;
358 | try {
359 | const tabs = await chrome.tabs.query({
360 | active: true,
361 | currentWindow: true,
362 | });
363 | tab = tabs[0];
364 | } catch (e) {
365 | console.error(e);
366 | return;
367 | }
368 | if (!tab?.id) {
369 | return;
370 | }
371 |
372 | let selectedText: string;
373 | try {
374 | const selectedTextInjected = await chrome.scripting.executeScript({
375 | target: { tabId: tab.id },
376 | func: () => window.getSelection()?.toString(),
377 | });
378 | selectedText = selectedTextInjected[0].result;
379 | } catch (e) {
380 | selectedText = "";
381 | }
382 |
383 | let compiledContent: string;
384 | if(commandId === "memos-page") {
385 | const replaceContent = localSettings.pageTemplate.replace(/{{page.title}}/g, tab?.title).replace(/{{page.url}}/g, tab?.url);
386 | compiledContent = compileToMemo(replaceContent);
387 | }else{
388 | const replaceContent = localSettings.selectionTemplate.replace(/{{selection}}/g, selectedText).replace(/{{page.title}}/g, tab?.title).replace(/{{page.url}}/g, tab?.url);
389 | compiledContent = compileToMemo(replaceContent);
390 | }
391 |
392 | if (compiledContent === "") {
393 | return;
394 | }
395 |
396 | const requestHeaders = {
397 | ...headers,
398 | "Content-Type": "text/markdown",
399 | "Content-Insertion-Position": position,
400 | Heading: heading,
401 | };
402 | const request: RequestInit = {
403 | method: method,
404 | body: compiledContent,
405 | headers: requestHeaders,
406 | };
407 | const result = await obsidianRequest(
408 | apiKey,
409 | compiledUrl,
410 | request,
411 | insecureMode
412 | );
413 | const text = await result.text();
414 |
415 | if (result.status < 300) {
416 | console.log("All done!");
417 | } else {
418 | try {
419 | const body = JSON.parse(text);
420 | console.log(body);
421 | } catch (e) {
422 | console.error(e);
423 | }
424 | }
425 | }
426 |
427 | const SendIt = async (info?:OnClickData) => {
428 |
429 | let syncSettings: ExtensionSyncSettings;
430 | let localSettings: ExtensionLocalSettings;
431 | let apiKey: string;
432 | let heading: string;
433 | let insecureMode: boolean = false;
434 | let obsidianUnavailable: boolean = false;
435 | let obsidianDailyNoteUnavailable: boolean = false;
436 | let pageContent: string = "";
437 | let title: string = "";
438 | let compiledUrl: string = "/periodic/daily";
439 | let obMethod: string = "PATCH";
440 | let headers: { [key: string]: string } = {};
441 | let position: string = "end";
442 |
443 | localSettings = await getLocalSettings(chrome.storage.local);
444 |
445 | try {
446 | localSettings = await getLocalSettings(chrome.storage.local);
447 | apiKey = localSettings.apiKey;
448 | } catch (e) {
449 | console.error(e);
450 | return;
451 | }
452 |
453 | try {
454 | localSettings = await getLocalSettings(chrome.storage.local);
455 | heading = localSettings.heading;
456 | } catch (e) {
457 | console.error(e);
458 | return;
459 | }
460 |
461 | try {
462 | const request = await obsidianRequest(
463 | apiKey,
464 | "/",
465 | { method: "get" },
466 | insecureMode
467 | );
468 | const result: StatusResponse = await request.json();
469 | obsidianUnavailable = !(result.status === "OK" &&
470 | result.service.includes("Obsidian Local REST API"));
471 | } catch (e) {
472 | obsidianUnavailable = true;
473 | }
474 |
475 | try {
476 | const request = await obsidianRequest(
477 | apiKey,
478 | "/periodic/daily",
479 | { method: "get" },
480 | insecureMode
481 | );
482 | obsidianDailyNoteUnavailable = !(request.statusText === "OK");
483 | } catch (e) {
484 | console.error(e);
485 | obsidianDailyNoteUnavailable = true;
486 | }
487 |
488 | if(obsidianDailyNoteUnavailable && !obsidianUnavailable) {
489 | try {
490 | const request = await obsidianRequest(
491 | apiKey,
492 | "/commands/daily-notes",
493 | { method: "post" },
494 | insecureMode
495 | );
496 | obsidianDailyNoteUnavailable = !(request.statusText === "OK");
497 | } catch (e) {
498 | console.error(e);
499 | obsidianDailyNoteUnavailable = true;
500 | }
501 | }
502 |
503 |
504 | let tab: chrome.tabs.Tab;
505 | try {
506 | const tabs = await chrome.tabs.query({
507 | active: true,
508 | currentWindow: true,
509 | });
510 | tab = tabs[0];
511 | } catch (e) {
512 | console.error(e);
513 | return;
514 | }
515 | if (!tab?.id) {
516 | return;
517 | }
518 |
519 | let selectedText: string;
520 | try {
521 | const selectedTextInjected = await chrome.scripting.executeScript({
522 | target: { tabId: tab?.id },
523 | func: () => window.getSelection()?.toString(),
524 | });
525 | selectedText = selectedTextInjected[0].result;
526 | } catch (e) {
527 | selectedText = "";
528 | }
529 |
530 | let compiledContent: string;
531 | try {
532 |
533 | if(info?.menuItemId === 'memos-selection-context-menu') {
534 | const replaceContent = localSettings.selectionTemplate.replace(/{{selection}}/g, selectedText).replace(/{{page.title}}/g, tab?.title).replace(/{{page.url}}/g, tab?.url);
535 | compiledContent = compileToMemo(replaceContent);
536 | } else{
537 | const replaceContent = localSettings.pageTemplate.replace(/{{page.title}}/g, tab?.title).replace(/{{page.url}}/g, tab?.url);
538 | compiledContent = compileToMemo(replaceContent);
539 | }
540 | } catch (e) {
541 | compiledContent = "";
542 | }
543 |
544 | if (compiledContent === "") {
545 | return;
546 | }
547 |
548 | const requestHeaders = {
549 | ...headers,
550 | "Content-Type": "text/markdown",
551 | "Content-Insertion-Position": position,
552 | Heading: heading,
553 | };
554 | const request: RequestInit = {
555 | method: obMethod,
556 | body: compiledContent,
557 | headers: requestHeaders,
558 | };
559 | const result = await obsidianRequest(
560 | apiKey,
561 | compiledUrl,
562 | request,
563 | insecureMode
564 | );
565 | const text = await result.text();
566 |
567 | if (result.status < 300) {
568 | console.log("All done!");
569 | } else {
570 | try {
571 | const body = JSON.parse(text);
572 | } catch (e) {
573 | console.error(e);
574 | }
575 | }
576 | }
577 |
578 | export default SendIt;
579 |
--------------------------------------------------------------------------------
/src/service_worker.ts:
--------------------------------------------------------------------------------
1 | import { getUrlMentions, getLocalSettings, obsidianRequest } from "./utils";
2 | import { ExtensionLocalSettings } from "./types";
3 | import SendIt, { SendItCommand } from "./sendIt";
4 |
5 | chrome.runtime.onInstalled.addListener(() => {
6 | chrome.contextMenus.create({
7 | title: "Send Selection to Memos",
8 | id: 'memos-selection-context-menu',
9 | contexts:["selection"],
10 | });
11 | chrome.contextMenus.create({
12 | title: "Send Page Url to Memos",
13 | id: 'memos-page-context-menu',
14 | contexts:["page"],
15 | });
16 | });
17 |
18 | chrome.commands.onCommand.addListener((command) => {
19 | if(command === "memos-selection" || command === "memos-page") {
20 | SendItCommand(command);
21 | }
22 | });
23 |
24 | chrome.contextMenus.onClicked.addListener(function(info, tab) {
25 | if (info.menuItemId == "memos-selection-context-menu") {
26 | SendIt(info);
27 | }
28 | if (info.menuItemId == "memos-page-context-menu") {
29 | SendIt(info);
30 | }
31 | });
32 |
33 |
34 |
35 | chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
36 | const localSettings: ExtensionLocalSettings = await getLocalSettings(
37 | chrome.storage.local
38 | );
39 | const url = tab.url;
40 |
41 | if (
42 | !localSettings ||
43 | !localSettings.apiKey ||
44 | !url ||
45 | changeInfo.status !== "loading"
46 | ) {
47 | return;
48 | }
49 |
50 | try {
51 | const mentions = await getUrlMentions(
52 | localSettings.apiKey,
53 | localSettings.insecureMode || false,
54 | url
55 | );
56 |
57 | if (mentions.direct.length > 0) {
58 | chrome.action.setBadgeBackgroundColor({
59 | color: "#A68B36",
60 | tabId,
61 | });
62 | chrome.action.setBadgeText({
63 | text: `${mentions.direct.length}`,
64 | tabId,
65 | });
66 | } else if (mentions.mentions.length > 0) {
67 | chrome.action.setBadgeBackgroundColor({
68 | color: "#3D7D98",
69 | tabId,
70 | });
71 | chrome.action.setBadgeText({
72 | text: `${mentions.mentions.length}`,
73 | tabId,
74 | });
75 | } else {
76 | chrome.action.setBadgeText({
77 | text: "",
78 | tabId,
79 | });
80 | }
81 |
82 | for (const mention of mentions.direct) {
83 | const mentionData = await obsidianRequest(
84 | localSettings.apiKey,
85 | `/vault/${mention.filename}`,
86 | {
87 | method: "get",
88 | headers: {
89 | Accept: "application/vnd.olrapi.note+json",
90 | },
91 | },
92 | localSettings.insecureMode || false
93 | );
94 | const result = await mentionData.json();
95 |
96 | if (result.frontmatter["web-badge-color"]) {
97 | chrome.action.setBadgeBackgroundColor({
98 | color: result.frontmatter["web-badge-color"],
99 | tabId,
100 | });
101 | }
102 | if (result.frontmatter["web-badge-message"]) {
103 | chrome.action.setBadgeText({
104 | text: result.frontmatter["web-badge-message"],
105 | tabId,
106 | });
107 | }
108 | }
109 | } catch (e) {
110 | chrome.action.setBadgeText({
111 | text: "ERR",
112 | });
113 | console.error(e);
114 | }
115 | });
116 | //
117 | // // Add bubble to the top of the page.
118 | // var bubbleDOM = document.createElement('div');
119 | // bubbleDOM.setAttribute('class', 'selection_bubble');
120 | // document.body.appendChild(bubbleDOM);
121 | //
122 | // // Lets listen to mouseup DOM events.
123 | // document.addEventListener('mouseup', function (e) {
124 | // var selection = window.getSelection()?.toString();
125 | // if(selection === undefined){
126 | // return;
127 | // }
128 | // if (selection.length > 0) {
129 | // renderBubble(e.clientX, e.clientY, selection);
130 | // }
131 | // }, false);
132 | //
133 | //
134 | // // Close the bubble when we click on the screen.
135 | // document.addEventListener('mousedown', function (e) {
136 | // bubbleDOM.style.visibility = 'hidden';
137 | // }, false);
138 | //
139 | // // Move that bubble to the appropriate location.
140 | // function renderBubble(mouseX: string | number, mouseY: string | number, selection: string) {
141 | // bubbleDOM.innerHTML = selection;
142 | // bubbleDOM.style.top = mouseY + 'px';
143 | // bubbleDOM.style.left = mouseX + 'px';
144 | // bubbleDOM.style.visibility = 'visible';
145 | // }
146 |
--------------------------------------------------------------------------------
/src/theme.ts:
--------------------------------------------------------------------------------
1 | import { createTheme } from "@mui/material/styles";
2 |
3 | export const PurpleTheme = createTheme({
4 | palette: {
5 | primary: {
6 | light: "#7a61cb",
7 | main: "#483699",
8 | dark: "#0c0e6a",
9 | contrastText: "#ffffff",
10 | },
11 | secondary: {
12 | light: "#525252",
13 | main: "#2a2a2a",
14 | dark: "#000000",
15 | contrastText: "#ffffff",
16 | },
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { AlertProps } from "@mui/material/Alert";
2 |
3 | export interface OutputPreset {
4 | name: string;
5 | urlTemplate: string;
6 | contentTemplate: string;
7 | headers: Record;
8 | method: "post" | "put" | "patch";
9 | }
10 |
11 | export interface ExtensionLocalSettings {
12 | version: string;
13 | apiKey: string;
14 | insecureMode?: boolean;
15 | pageTemplate: string;
16 | selectionTemplate: string;
17 | heading: string;
18 | }
19 |
20 | export interface ExtensionSyncSettings {
21 | version: string;
22 | presets: OutputPreset[];
23 | searchEnabled: boolean;
24 | searchBackgroundEnabled: boolean;
25 | searchMatchMentionTemplate: string;
26 | searchMatchDirectTemplate: string;
27 | }
28 |
29 | export interface AlertStatus {
30 | severity: AlertProps["severity"];
31 | title: string;
32 | message: string;
33 | }
34 |
35 | export interface SandboxRenderRequest {
36 | command: "render";
37 | template: string;
38 | context: Record;
39 | }
40 |
41 | export type SandboxRequest = SandboxRenderRequest;
42 |
43 | export interface SandboxResponseBase {
44 | request: SandboxRequest;
45 | success: boolean;
46 | }
47 |
48 | export interface SandboxExceptionResponse extends SandboxResponseBase {
49 | success: false;
50 | message: string;
51 | }
52 |
53 | export interface SandboxSuccessResponse extends SandboxResponseBase {
54 | success: true;
55 | }
56 |
57 | export interface SandboxRenderResponse extends SandboxSuccessResponse {
58 | request: SandboxRenderRequest;
59 | rendered: string;
60 | }
61 |
62 | export type SandboxResponse = SandboxRenderResponse | SandboxExceptionResponse;
63 |
64 | export interface SearchJsonResponseItem {
65 | filename: string;
66 | result: unknown;
67 | }
68 |
69 | export interface StatusResponse {
70 | status: string;
71 | versions: {
72 | obsidian: string;
73 | self: string;
74 | };
75 | service: string;
76 | authenticated: boolean;
77 | }
78 |
79 | export interface FileMetadataObject {
80 | tags: string[];
81 | frontmatter: Record;
82 | path: string;
83 | content: string;
84 | }
85 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import escapeStringRegexp from "escape-string-regexp";
2 |
3 | import {
4 | ExtensionLocalSettings,
5 | ExtensionSyncSettings,
6 | SandboxExceptionResponse,
7 | SandboxRenderRequest,
8 | SandboxRenderResponse,
9 | SearchJsonResponseItem,
10 | } from "./types";
11 | import { DefaultLocalSettings, DefaultSyncSettings } from "./constants";
12 |
13 | export async function getSyncSettings(
14 | sync: chrome.storage.SyncStorageArea
15 | ): Promise {
16 | const settings = await sync.get(DefaultSyncSettings);
17 | return settings as ExtensionSyncSettings;
18 | }
19 |
20 | export async function getLocalSettings(
21 | local: chrome.storage.LocalStorageArea
22 | ): Promise {
23 | const settings = await local.get(DefaultLocalSettings);
24 | return settings as ExtensionLocalSettings;
25 | }
26 |
27 | export async function openFileInObsidian(
28 | apiKey: string,
29 | insecureMode: boolean,
30 | filename: string
31 | ): ReturnType {
32 | return obsidianRequest(
33 | apiKey,
34 | `/open/${filename}`,
35 | { method: "post" },
36 | insecureMode
37 | );
38 | }
39 |
40 | export async function getUrlMentions(
41 | apiKey: string,
42 | insecureMode: boolean,
43 | url: string
44 | ): Promise<{
45 | mentions: SearchJsonResponseItem[];
46 | direct: SearchJsonResponseItem[];
47 | }> {
48 | async function handleMentions() {
49 | return await obsidianSearchRequest(apiKey, insecureMode, {
50 | regexp: [`${escapeStringRegexp(url)}(?=\\s|\\)|$)`, { var: "content" }],
51 | });
52 | }
53 |
54 | async function handleDirect() {
55 | return await obsidianSearchRequest(apiKey, insecureMode, {
56 | glob: [{ var: "frontmatter.url" }, url],
57 | });
58 | }
59 |
60 | return {
61 | mentions: await handleMentions(),
62 | direct: await handleDirect(),
63 | };
64 | }
65 |
66 | export async function obsidianSearchRequest(
67 | apiKey: string,
68 | insecureMode: boolean,
69 | query: Record
70 | ): Promise {
71 | const result = await obsidianRequest(
72 | apiKey,
73 | "/search/",
74 | {
75 | method: "post",
76 | body: JSON.stringify(query),
77 | headers: {
78 | "Content-type": "application/vnd.olrapi.jsonlogic+json",
79 | },
80 | },
81 | insecureMode
82 | );
83 |
84 | return await result.json();
85 | }
86 |
87 | export async function obsidianRequest(
88 | apiKey: string,
89 | path: string,
90 | options: RequestInit,
91 | insecureMode: boolean
92 | ): ReturnType {
93 | const requestOptions: RequestInit = {
94 | ...options,
95 | headers: {
96 | ...options.headers,
97 | Authorization: `Bearer ${apiKey}`,
98 | },
99 | method: options.method?.toUpperCase(),
100 | mode: "cors",
101 | };
102 |
103 | return fetch(
104 | `http${insecureMode ? "" : "s"}://127.0.0.1:${
105 | insecureMode ? "27123" : "27124"
106 | }${path}`,
107 | requestOptions
108 | );
109 | }
110 |
111 | export function compileToMemo (
112 | content: string,
113 | ): string {
114 | const compiledContent = content.replace(/\n/g, " ");
115 | const d = new Date();
116 | const hours = (d.getHours() < 10?'0':'') + d.getHours();
117 | const mins = (d.getMinutes() < 10?'0':'') + d.getMinutes();
118 | return "- " + d.getHours() + ":" + mins + " " + compiledContent;
119 | }
120 |
121 |
122 | export function compileTemplate(
123 | template: string,
124 | context: Record
125 | ): Promise {
126 | const result = new Promise((resolve, reject) => {
127 | const sandbox = document.getElementById(
128 | "handlebars-sandbox"
129 | ) as HTMLIFrameElement;
130 |
131 | const message: SandboxRenderRequest = {
132 | command: "render",
133 | template,
134 | context,
135 | };
136 |
137 | if (!sandbox.contentWindow) {
138 | throw new Error("No content window found for handlebars sandbox!");
139 | }
140 |
141 | const handler = (
142 | event: MessageEvent
143 | ) => {
144 | if (event.data.success) {
145 | resolve(event.data.rendered);
146 | } else {
147 | reject(event.data.message);
148 | }
149 | };
150 |
151 | window.addEventListener("message", handler, { once: true });
152 |
153 | sandbox.contentWindow.postMessage(message, "*");
154 | });
155 |
156 | return result;
157 | }
158 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "module": "commonjs",
5 | "target": "es6",
6 | "esModuleInterop": true,
7 | "sourceMap": true,
8 | "rootDir": "src",
9 | "outDir": "dist/js",
10 | "noEmitOnError": false,
11 | "jsx": "react",
12 | "typeRoots": [ "node_modules/@types" ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/webpack/webpack.common.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack");
2 | const path = require("path");
3 | const CopyPlugin = require("copy-webpack-plugin");
4 | const srcDir = path.join(__dirname, "..", "src");
5 |
6 | module.exports = {
7 | entry: {
8 | popup: path.join(srcDir, "popup.tsx"),
9 | options: path.join(srcDir, "options.tsx"),
10 | handlebars: path.join(srcDir, "handlebars.ts"),
11 | background: path.join(srcDir, "service_worker.ts"),
12 | },
13 | output: {
14 | path: path.join(__dirname, "../dist/js"),
15 | filename: "[name].js",
16 | },
17 | optimization: {
18 | splitChunks: {
19 | name: "vendor",
20 | chunks(chunk) {
21 | return chunk.name !== "background";
22 | },
23 | },
24 | },
25 | module: {
26 | rules: [
27 | {
28 | test: /\.tsx?$/,
29 | use: "ts-loader",
30 | exclude: /node_modules/,
31 | },
32 | ],
33 | },
34 | resolve: {
35 | extensions: [".ts", ".tsx", ".js"],
36 | alias: {
37 | handlebars: "handlebars/dist/handlebars.min.js",
38 | },
39 | },
40 | plugins: [
41 | new CopyPlugin({
42 | patterns: [{ from: ".", to: "../", context: "public" }],
43 | options: {},
44 | }),
45 | ],
46 | };
47 |
--------------------------------------------------------------------------------
/webpack/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | devtool: 'inline-source-map',
6 | mode: 'development'
7 | });
8 |
--------------------------------------------------------------------------------
/webpack/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | mode: 'production'
6 | });
--------------------------------------------------------------------------------