├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── main.ts ├── manifest.json ├── package.json ├── rollup.config.js ├── tsconfig.json └── versions.json /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian Plugin 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/tags 5 | tags: 6 | - "*" # Push events to matching any tag format, i.e. 1.0, 20.15.10 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo 14 | - name: Use Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: "14.x" # You might need to adjust this value to your own version 18 | # Get the version number and put it in a variable 19 | - name: Get Version 20 | id: version 21 | run: | 22 | echo "::set-output name=tag::$(git describe --abbrev=0)" 23 | # Build the plugin 24 | - name: Build 25 | id: build 26 | run: | 27 | npm install 28 | npm run build --if-present 29 | # Package the required files into a zip 30 | - name: Package 31 | run: | 32 | mkdir ${{ github.event.repository.name }} 33 | cp main.js manifest.json README.md ${{ github.event.repository.name }} 34 | zip -r ${{ github.event.repository.name }}.zip ${{ github.event.repository.name }} 35 | # Create the release on github 36 | - name: Create Release 37 | id: create_release 38 | uses: actions/create-release@v1 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | VERSION: ${{ github.ref }} 42 | with: 43 | tag_name: ${{ github.ref }} 44 | release_name: ${{ github.ref }} 45 | draft: false 46 | prerelease: false 47 | # Upload the packaged release file 48 | - name: Upload zip file 49 | id: upload-zip 50 | uses: actions/upload-release-asset@v1 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | with: 54 | upload_url: ${{ steps.create_release.outputs.upload_url }} 55 | asset_path: ./${{ github.event.repository.name }}.zip 56 | asset_name: ${{ github.event.repository.name }}-${{ steps.version.outputs.tag }}.zip 57 | asset_content_type: application/zip 58 | # Upload the main.js 59 | - name: Upload main.js 60 | id: upload-main 61 | uses: actions/upload-release-asset@v1 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | with: 65 | upload_url: ${{ steps.create_release.outputs.upload_url }} 66 | asset_path: ./main.js 67 | asset_name: main.js 68 | asset_content_type: text/javascript 69 | # Upload the manifest.json 70 | - name: Upload manifest.json 71 | id: upload-manifest 72 | uses: actions/upload-release-asset@v1 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | with: 76 | upload_url: ${{ steps.create_release.outputs.upload_url }} 77 | asset_path: ./manifest.json 78 | asset_name: manifest.json 79 | asset_content_type: application/json 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build 10 | main.js 11 | *.js.map 12 | 13 | # obsidian 14 | data.json 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2021 Jonathan Miller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Wikipedia 2 | 3 | This is a plugin for Obsidian (https://obsidian.md). 4 | 5 | This plugin gets the first section of Wikipedia and pastes it into your active note. 6 | 7 | ## Usage 8 | 9 | This plugin has two commands: 10 | 11 | - `Get Wikipedia for Active Note`, which gets the first section of Wikipedia using the active note's title as search term. 12 | - `Get Wikipedia for Search Term`, which gets the first section of Wikipedia for a search term. 13 | 14 | ## Settings 15 | 16 | Settings for this plugin include: 17 | 18 | - **Language Prefix**: The prefix before `wikipedia.org` used to access the language of Wikipedia you want. (Default: 'en') 19 | - **Extract Template**: The template to use to paste your extract. Available variables are {{text}}, {{searchTerm}}, and {{url}} 20 | - **Bold Search Term?**: If set to True, bolds the first instance of the search term in the extract 21 | - **Use Paragraph Template?**: If set to true, the paragraph template will be inserted for each paragraph of text for {{text}} in main template. 22 | - **Paragraph Template**: If *Use Paragraph Template* is set to true, this template will be inserted for each paragraph in the text extract. Available variable: {{paragraphText}}. -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | Modal, 4 | Notice, 5 | Plugin, 6 | PluginSettingTab, 7 | Setting, 8 | Editor, 9 | MarkdownView, 10 | TextComponent, 11 | RequestParam, 12 | request, 13 | } from "obsidian"; 14 | 15 | interface WikipediaExtract { 16 | title: string; 17 | text: string; 18 | url: string; 19 | } 20 | 21 | interface WikipediaPluginSettings { 22 | template: string; 23 | shouldUseParagraphTemplate: boolean; 24 | shouldBoldSearchTerm: boolean; 25 | paragraphTemplate: string; 26 | language: string; 27 | } 28 | 29 | const DEFAULT_SETTINGS: WikipediaPluginSettings = { 30 | template: `{{text}}\n> [Wikipedia]({{url}})`, 31 | shouldUseParagraphTemplate: true, 32 | shouldBoldSearchTerm: true, 33 | paragraphTemplate: `> {{paragraphText}}\n>\n`, 34 | language: "en", 35 | }; 36 | 37 | const extractApiUrl = 38 | "wikipedia.org/w/api.php?format=json&action=query&prop=extracts&explaintext=1&redirects&origin=*&titles="; 39 | 40 | const disambiguationIdentifier = "may refer to:"; 41 | export default class WikipediaPlugin extends Plugin { 42 | settings: WikipediaPluginSettings; 43 | 44 | getLanguage(): string { 45 | return this.settings.language ? this.settings.language : "en"; 46 | } 47 | 48 | getUrl(title: string): string { 49 | return `https://${this.getLanguage()}.wikipedia.org/wiki/${encodeURI( 50 | title 51 | )}`; 52 | } 53 | 54 | getApiUrl(): string { 55 | return `https://${this.getLanguage()}.` + extractApiUrl; 56 | } 57 | 58 | formatExtractText(extract: WikipediaExtract, searchTerm: string): string { 59 | const text = extract.text; 60 | let formattedText: string = ""; 61 | if (this.settings.shouldUseParagraphTemplate) { 62 | const split = text.split("==")[0].trim().split("\n"); 63 | formattedText = split 64 | .map((paragraph) => 65 | this.settings.paragraphTemplate.replace( 66 | "{{paragraphText}}", 67 | paragraph 68 | ) 69 | ) 70 | .join("") 71 | .trim(); 72 | } else { 73 | formattedText = text.split("==")[0].trim(); 74 | } 75 | if (this.settings.shouldBoldSearchTerm) { 76 | const pattern = new RegExp(searchTerm, "i"); 77 | formattedText = formattedText.replace(pattern, `**${searchTerm}**`); 78 | } 79 | return formattedText; 80 | } 81 | 82 | handleNotFound(searchTerm: string) { 83 | new Notice(`${searchTerm} not found on Wikipedia.`); 84 | } 85 | 86 | handleCouldntResolveDisambiguation() { 87 | new Notice(`Could not automatically resolve disambiguation.`); 88 | } 89 | 90 | hasDisambiguation(extract: WikipediaExtract) { 91 | if (extract.text.includes(disambiguationIdentifier)) { 92 | return true; 93 | } 94 | return false; 95 | } 96 | 97 | parseResponse(json: any): WikipediaExtract | undefined { 98 | const pages = json.query.pages; 99 | const pageKeys = Object.keys(pages); 100 | if (pageKeys.includes("-1")) { 101 | return undefined; 102 | } 103 | const extracts: WikipediaExtract[] = pageKeys.map((key) => { 104 | const page = pages[key]; 105 | const extract: WikipediaExtract = { 106 | title: page.title, 107 | text: page.extract, 108 | url: this.getUrl(page.title), 109 | }; 110 | return extract; 111 | }); 112 | return extracts[0]; 113 | } 114 | 115 | formatExtractInsert(extract: WikipediaExtract, searchTerm: string): string { 116 | const formattedText = this.formatExtractText(extract, searchTerm); 117 | const template = this.settings.template; 118 | const formattedTemplate = template 119 | .replace("{{text}}", formattedText) 120 | .replace("{{searchTerm}}", searchTerm) 121 | .replace("{{url}}", extract.url); 122 | return formattedTemplate; 123 | } 124 | 125 | async getWikipediaText(title: string): Promise { 126 | const url = this.getApiUrl() + encodeURIComponent(title); 127 | const requestParam: RequestParam = { 128 | url: url, 129 | }; 130 | const resp = await request(requestParam) 131 | .then((r) => JSON.parse(r)) 132 | .catch( 133 | () => 134 | new Notice( 135 | "Failed to get Wikipedia. Check your internet connection or language prefix." 136 | ) 137 | ); 138 | const extract = this.parseResponse(resp); 139 | return extract; 140 | } 141 | 142 | async pasteIntoEditor(editor: Editor, searchTerm: string) { 143 | let extract: WikipediaExtract = await this.getWikipediaText(searchTerm); 144 | if (!extract) { 145 | this.handleNotFound(searchTerm); 146 | return; 147 | } 148 | if (this.hasDisambiguation(extract)) { 149 | new Notice( 150 | `Disambiguation found for ${searchTerm}. Choosing first result.` 151 | ); 152 | const newSearchTerm = extract.text 153 | .split(disambiguationIdentifier)[1] 154 | .trim() 155 | .split(",")[0] 156 | .split("==") 157 | .pop() 158 | .trim(); 159 | extract = await this.getWikipediaText(newSearchTerm); 160 | if (!extract) { 161 | this.handleCouldntResolveDisambiguation(); 162 | return; 163 | } 164 | } 165 | editor.replaceSelection(this.formatExtractInsert(extract, searchTerm)); 166 | } 167 | 168 | async getWikipediaTextForActiveFile(editor: Editor) { 169 | const activeFile = await this.app.workspace.getActiveFile(); 170 | if (activeFile) { 171 | const searchTerm = activeFile.basename; 172 | if (searchTerm) { 173 | await this.pasteIntoEditor(editor, searchTerm); 174 | } 175 | } 176 | } 177 | 178 | async getWikipediaTextForSearchTerm(editor: Editor) { 179 | new WikipediaSearchModal(this.app, this, editor).open(); 180 | } 181 | 182 | async onload() { 183 | await this.loadSettings(); 184 | 185 | this.addCommand({ 186 | id: "wikipedia-get-active-note-title", 187 | name: "Get Wikipedia for Active Note Title", 188 | editorCallback: (editor: Editor) => 189 | this.getWikipediaTextForActiveFile(editor), 190 | }); 191 | 192 | this.addCommand({ 193 | id: "wikipedia-get-search-term", 194 | name: "Get Wikipedia for Search Term", 195 | editorCallback: (editor: Editor) => 196 | this.getWikipediaTextForSearchTerm(editor), 197 | }); 198 | 199 | this.addSettingTab(new WikipediaSettingTab(this.app, this)); 200 | } 201 | 202 | async loadSettings() { 203 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 204 | } 205 | 206 | async saveSettings() { 207 | await this.saveData(this.settings); 208 | } 209 | } 210 | 211 | class WikipediaSearchModal extends Modal { 212 | searchTerm: string; 213 | plugin: WikipediaPlugin; 214 | editor: Editor; 215 | 216 | constructor(app: App, plugin: WikipediaPlugin, editor: Editor) { 217 | super(app); 218 | this.plugin = plugin; 219 | this.editor = editor; 220 | } 221 | 222 | onOpen() { 223 | let { contentEl } = this; 224 | 225 | contentEl.createEl("h2", { text: "Enter Search Term:" }); 226 | 227 | const inputs = contentEl.createDiv("inputs"); 228 | const searchInput = new TextComponent(inputs).onChange((searchTerm) => { 229 | this.searchTerm = searchTerm; 230 | }); 231 | searchInput.inputEl.focus(); 232 | searchInput.inputEl.addEventListener("keydown", (event) => { 233 | if (event.key === "Enter") { 234 | this.close(); 235 | } 236 | }); 237 | 238 | const controls = contentEl.createDiv("controls"); 239 | const searchButton = controls.createEl("button", { 240 | text: "Search", 241 | cls: "mod-cta", 242 | attr: { 243 | autofocus: true, 244 | }, 245 | }); 246 | searchButton.addEventListener("click", this.close.bind(this)); 247 | const cancelButton = controls.createEl("button", { text: "Cancel" }); 248 | cancelButton.addEventListener("click", this.close.bind(this)); 249 | } 250 | 251 | async onClose() { 252 | let { contentEl } = this; 253 | 254 | contentEl.empty(); 255 | if (this.searchTerm) { 256 | await this.plugin.pasteIntoEditor(this.editor, this.searchTerm); 257 | } 258 | } 259 | } 260 | 261 | class WikipediaSettingTab extends PluginSettingTab { 262 | plugin: WikipediaPlugin; 263 | 264 | constructor(app: App, plugin: WikipediaPlugin) { 265 | super(app, plugin); 266 | this.plugin = plugin; 267 | } 268 | 269 | display(): void { 270 | let { containerEl } = this; 271 | 272 | containerEl.empty(); 273 | 274 | containerEl.createEl("h2", { text: "Obsidian Wikipedia" }); 275 | 276 | new Setting(containerEl) 277 | .setName("Wikipedia Language Prefix") 278 | .setDesc(`Choose Wikipedia language prefix to use (ex. en for English)`) 279 | .addText((textField) => { 280 | textField 281 | .setValue(this.plugin.settings.language) 282 | .onChange(async (value) => { 283 | this.plugin.settings.language = value; 284 | await this.plugin.saveSettings(); 285 | }); 286 | }); 287 | 288 | new Setting(containerEl) 289 | .setName("Wikipedia Extract Template") 290 | .setDesc( 291 | `Set markdown template for extract to be inserted.\n 292 | Available template variables are {{text}}, {{searchTerm}} and {{url}}. 293 | ` 294 | ) 295 | .addTextArea((textarea) => 296 | textarea 297 | .setValue(this.plugin.settings.template) 298 | .onChange(async (value) => { 299 | this.plugin.settings.template = value; 300 | await this.plugin.saveSettings(); 301 | }) 302 | ); 303 | 304 | new Setting(containerEl) 305 | .setName("Bold Search Term?") 306 | .setDesc( 307 | "If set to true, the first instance of the search term will be **bolded**" 308 | ) 309 | .addToggle((toggle) => 310 | toggle 311 | .setValue(this.plugin.settings.shouldBoldSearchTerm) 312 | .onChange(async (value) => { 313 | this.plugin.settings.shouldBoldSearchTerm = value; 314 | await this.plugin.saveSettings(); 315 | }) 316 | ); 317 | 318 | new Setting(containerEl) 319 | .setName("Use paragraph template?") 320 | .setDesc( 321 | "If set to true, the paragraph template will be inserted for each paragraph of text for {{text}} in main template." 322 | ) 323 | .addToggle((toggle) => 324 | toggle 325 | .setValue(this.plugin.settings.shouldUseParagraphTemplate) 326 | .onChange(async (value) => { 327 | this.plugin.settings.shouldUseParagraphTemplate = value; 328 | await this.plugin.saveSettings(); 329 | }) 330 | ); 331 | 332 | new Setting(containerEl) 333 | .setName("Paragraph Template") 334 | .setDesc( 335 | `Set markdown template for extract paragraphs to be inserted.\n 336 | Available template variables are: {{paragraphText}} 337 | ` 338 | ) 339 | .addTextArea((textarea) => 340 | textarea 341 | .setValue(this.plugin.settings.paragraphTemplate) 342 | .onChange(async (value) => { 343 | this.plugin.settings.paragraphTemplate = value; 344 | await this.plugin.saveSettings(); 345 | }) 346 | ); 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-wikipedia", 3 | "name": "Wikipedia", 4 | "version": "1.0.3", 5 | "minAppVersion": "0.9.12", 6 | "description": "Grabs information from Wikipedia for a topic and brings it into Obsidian notes", 7 | "author": "Jonathan Miller", 8 | "authorUrl": "https://jmill.dev", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-wikipedia", 3 | "version": "1.0.3", 4 | "description": "Grabs information from Wikipedia for a topic and brings it into Obsidian notes", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "rollup --config rollup.config.js -w", 8 | "build": "rollup --config rollup.config.js --environment BUILD:production" 9 | }, 10 | "keywords": [ 11 | "obsidian-plugin" 12 | ], 13 | "author": "Jonathan Miller", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@rollup/plugin-commonjs": "^18.0.0", 17 | "@rollup/plugin-node-resolve": "^11.2.1", 18 | "@rollup/plugin-typescript": "^8.2.1", 19 | "@types/node": "^14.14.37", 20 | "obsidian": "^0.12.0", 21 | "rollup": "^2.32.1", 22 | "tslib": "^2.2.0", 23 | "typescript": "^4.2.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | 5 | const isProd = (process.env.BUILD === 'production'); 6 | 7 | const banner = 8 | `/* 9 | THIS IS A GENERATED/BUNDLED FILE BY ROLLUP 10 | if you want to view the source visit the plugins github repository 11 | */ 12 | `; 13 | 14 | export default { 15 | input: 'main.ts', 16 | output: { 17 | dir: '.', 18 | sourcemap: 'inline', 19 | sourcemapExcludeSources: isProd, 20 | format: 'cjs', 21 | exports: 'default', 22 | banner, 23 | }, 24 | external: ['obsidian'], 25 | plugins: [ 26 | typescript(), 27 | nodeResolve({browser: true}), 28 | commonjs(), 29 | ] 30 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "es6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "lib": [ 13 | "dom", 14 | "es5", 15 | "scripthost", 16 | "es2015" 17 | ] 18 | }, 19 | "include": [ 20 | "**/*.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.1": "0.9.12", 3 | "1.0.0": "0.9.7" 4 | } 5 | --------------------------------------------------------------------------------