├── .npmrc ├── bun.lockb ├── stuff ├── img.png └── img_3.png ├── docs ├── assets │ ├── img.png │ ├── img_1.png │ └── img_2.png ├── public │ ├── logo.webp │ └── logo-small.webp ├── .vitepress │ ├── theme │ │ ├── style.css │ │ └── index.ts │ └── config.mts ├── custom-javascript.md ├── quick-switch.md ├── inline-embedded.md ├── getting-started.md ├── gate-link.md ├── add-gate.md ├── gate-options.md ├── custom-css.md ├── introduction.md ├── index.md └── release.md ├── .prettierrc ├── vitest.config.ts ├── src ├── fns │ ├── getDefaultUserAgent.ts │ ├── getSvgIcon.ts │ ├── fetchTitle.ts │ ├── unloadView.ts │ ├── createEmptyGateOption.ts │ ├── registerGate.ts │ ├── setupInsertLinkMenu.ts │ ├── normalizeGateOption.ts │ ├── createIframe.ts │ ├── openView.ts │ ├── createWebviewTag.ts │ ├── setupLinkConvertMenu.ts │ ├── registerCodeBlockProcessor.ts │ └── createFormEditGate.ts ├── types.d.ts ├── MCPlugin.ts ├── GateOptions.d.ts ├── ModalEditGate.ts ├── ModalOnboarding.ts ├── ModalListGates.ts ├── ModalInsertLink.ts ├── SetingTab.ts ├── GateView.ts └── main.ts ├── .discourse-config ├── .gitignore ├── tsconfig.json ├── manifest.json ├── scripts └── post-to-discourse.sh ├── version-bump.mjs ├── CONTRIBUTING.md ├── LICENSE ├── .all-contributorsrc ├── esbuild.config.mjs ├── package.json ├── tests └── normalizeGateOption.test.ts ├── .specify ├── templates │ ├── commands │ │ ├── clarify.md │ │ ├── checklist.md │ │ └── analyze.md │ ├── plan-template.md │ ├── spec-template.md │ └── tasks-template.md └── memory │ └── constitution.md ├── versions.json ├── .github └── workflows │ ├── claude.yml │ ├── claude-code-review.yml │ └── release.yml ├── styles.css ├── README.md ├── CLAUDE.md └── .claude └── commands ├── release.md └── fix-issue.md /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/HEAD/bun.lockb -------------------------------------------------------------------------------- /stuff/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/HEAD/stuff/img.png -------------------------------------------------------------------------------- /stuff/img_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/HEAD/stuff/img_3.png -------------------------------------------------------------------------------- /docs/assets/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/HEAD/docs/assets/img.png -------------------------------------------------------------------------------- /docs/assets/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/HEAD/docs/assets/img_1.png -------------------------------------------------------------------------------- /docs/assets/img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/HEAD/docs/assets/img_2.png -------------------------------------------------------------------------------- /docs/public/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/HEAD/docs/public/logo.webp -------------------------------------------------------------------------------- /docs/public/logo-small.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nguyenvanduocit/obsidian-open-gate/HEAD/docs/public/logo-small.webp -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "printWidth": 180, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'node', 6 | globals: true, 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/fns/getDefaultUserAgent.ts: -------------------------------------------------------------------------------- 1 | export default function getDefaultUserAgent() { 2 | return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' 3 | } 4 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/style.css: -------------------------------------------------------------------------------- 1 | .aspect-ratio-16-9 { 2 | position: relative; 3 | padding-bottom: 56.25%; 4 | } 5 | 6 | .aspect-ratio-16-9 iframe { 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /src/fns/getSvgIcon.ts: -------------------------------------------------------------------------------- 1 | export const getSvgIcon = (siteUrl: string): string => { 2 | const domain = new URL(siteUrl).hostname 3 | return `` 4 | } -------------------------------------------------------------------------------- /src/fns/fetchTitle.ts: -------------------------------------------------------------------------------- 1 | export const fetchTitle = async (url: string): Promise => { 2 | let response = await fetch(url) 3 | let text = await response.text() 4 | let doc = new DOMParser().parseFromString(text, 'text/html') 5 | return doc.title 6 | } 7 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import { GateFrameOption } from './GateOptions' 2 | 3 | export interface PluginSetting { 4 | uuid: string 5 | gates: Record 6 | } 7 | 8 | export interface MarkdownLink { 9 | title: string 10 | url: string 11 | } 12 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | // @ts-ignore 3 | if (!import.meta.env.SSR) { 4 | import('quicklink').then((module) => { 5 | module.listen() 6 | }) 7 | } 8 | import './style.css' 9 | 10 | export default DefaultTheme 11 | -------------------------------------------------------------------------------- /.discourse-config: -------------------------------------------------------------------------------- 1 | # Discourse Forum Configuration 2 | # This file stores your plugin's forum thread URL 3 | 4 | # Your plugin's main forum topic URL 5 | # Set this to your existing announcement thread to post updates as replies 6 | DISCOURSE_TOPIC_URL="https://forum.obsidian.md/t/opengate-embed-any-website-to-obsidian/49522" 7 | 8 | # Leave empty to create new topics instead 9 | # DISCOURSE_TOPIC_URL="" 10 | -------------------------------------------------------------------------------- /src/fns/unloadView.ts: -------------------------------------------------------------------------------- 1 | import { removeIcon, Workspace } from 'obsidian' 2 | import { GateFrameOption } from '../GateOptions' 3 | 4 | export const unloadView = async (workspace: Workspace, gate: GateFrameOption): Promise => { 5 | workspace.detachLeavesOfType(gate.id) 6 | const ribbonIcons = workspace.containerEl.querySelector(`div[aria-label="${gate.title}"]`) 7 | if (ribbonIcons) { 8 | ribbonIcons.remove() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | 24 | docs/.vitepress/cache/ 25 | .env 26 | .discourse-key.pem 27 | -------------------------------------------------------------------------------- /src/fns/createEmptyGateOption.ts: -------------------------------------------------------------------------------- 1 | import getDefaultUserAgent from './getDefaultUserAgent' 2 | import { GateFrameOption } from '../GateOptions' 3 | 4 | export const createEmptyGateOption = (): GateFrameOption => { 5 | return { 6 | id: '', 7 | title: '', 8 | icon: '', 9 | hasRibbon: true, 10 | position: 'right', 11 | profileKey: 'open-gate', 12 | url: '', 13 | zoomFactor: 1.0, 14 | userAgent: getDefaultUserAgent() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MCPlugin.ts: -------------------------------------------------------------------------------- 1 | import { ViewUpdate, PluginValue, EditorView, ViewPlugin } from '@codemirror/view' 2 | 3 | class ExamplePlugin implements PluginValue { 4 | private dom: HTMLDivElement | null = null 5 | constructor(view: EditorView) { 6 | console.log(view.dom.getElementsByTagName('img')) 7 | } 8 | 9 | update(update: ViewUpdate) {} 10 | 11 | destroy() { 12 | this.dom?.remove() 13 | } 14 | } 15 | 16 | export const examplePlugin = ViewPlugin.fromClass(ExamplePlugin) 17 | -------------------------------------------------------------------------------- /docs/custom-javascript.md: -------------------------------------------------------------------------------- 1 | # Custom JavaScript 2 | 3 | The same with custom CSS, not much to say here, if you want to custom JS, it's mean you know what you are doing. 4 | 5 | But be careful, custom JS is very powerful, it can allow you to change the behavior of the website, or even leak your data. So only do it if you know what you are doing. 6 | 7 | In some use cases, I use custom JS to remove the annoying cookie banner, ads, popup, .... 8 | 9 | The best case is allows another plugin to interact with the embedded website. 10 | -------------------------------------------------------------------------------- /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 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": ["DOM", "ES5", "ES6", "ES7"] 15 | }, 16 | "include": ["src", "env.d.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "open-gate", 3 | "name": "Open Gate", 4 | "version": "1.11.14", 5 | "minAppVersion": "0.15.0", 6 | "description": "Embed any website to Obsidian, you have anything you need in one place. You can browse website and take notes at the same time. e.g. Ask ChatGPT and copy the answer directly to your note.", 7 | "author": "duocnv", 8 | "authorUrl": "https://twitter.com/duocdev", 9 | "fundingUrl": { 10 | "Buy Me a Coffee": "https://paypal.me/duocnguyen", 11 | "Follow me": "https://twitter.com/duocdev" 12 | } 13 | } -------------------------------------------------------------------------------- /scripts/post-to-discourse.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Wrapper script that calls the release skill's Discourse posting script 4 | # This keeps the Discourse functionality in the release skill folder 5 | 6 | SKILL_DIR="$HOME/.claude/skills/release" 7 | SCRIPT="$SKILL_DIR/open-discourse-post.sh" 8 | 9 | if [ ! -f "$SCRIPT" ]; then 10 | echo "❌ Error: Discourse posting script not found at: $SCRIPT" 11 | echo "Make sure the release skill is installed." 12 | exit 1 13 | fi 14 | 15 | # Pass all arguments to the skill script 16 | exec "$SCRIPT" "$@" 17 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'fs' 2 | 3 | const targetVersion = process.env.npm_package_version 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync('manifest.json', 'utf8')) 7 | const { minAppVersion } = manifest 8 | manifest.version = targetVersion 9 | writeFileSync('manifest.json', JSON.stringify(manifest, null, '\t')) 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync('versions.json', 'utf8')) 13 | versions[targetVersion] = minAppVersion 14 | writeFileSync('versions.json', JSON.stringify(versions, null, '\t')) 15 | -------------------------------------------------------------------------------- /src/GateOptions.d.ts: -------------------------------------------------------------------------------- 1 | export type GateFrameOptionType = 'left' | 'center' | 'right' 2 | 3 | export type GateFrameOption = { 4 | id: string 5 | icon: string // SVG code or icon id from lucide.dev 6 | title: string // Gate frame title 7 | url: string // Gate frame URL 8 | profileKey?: string // Similar to a Chrome profile 9 | hasRibbon?: boolean // If true, icon appears in the left sidebar 10 | position?: GateFrameOptionType // 'left', 'center', or 'right' 11 | userAgent?: string // User agent for the gate frame 12 | zoomFactor?: number // Zoom factor (0.5 = 50%, 1.0 = 100%, 2.0 = 200%, etc.) 13 | css?: string // Custom CSS for the gate frame 14 | js?: string // Custom JavaScript for the gate frame 15 | } 16 | -------------------------------------------------------------------------------- /src/ModalEditGate.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal } from 'obsidian' 2 | import { createFormEditGate } from './fns/createFormEditGate' 3 | import { GateFrameOption } from './GateOptions' 4 | 5 | export class ModalEditGate extends Modal { 6 | gateOptions: GateFrameOption 7 | onSubmit: (result: GateFrameOption) => void 8 | constructor(app: App, gateOptions: GateFrameOption, onSubmit: (result: GateFrameOption) => void) { 9 | super(app) 10 | this.onSubmit = onSubmit 11 | this.gateOptions = gateOptions 12 | } 13 | 14 | onOpen() { 15 | const { contentEl } = this 16 | contentEl.createEl('h3', { text: 'Open Gate' }) 17 | createFormEditGate(contentEl, this.gateOptions, (result) => { 18 | this.onSubmit(result) 19 | this.close() 20 | }) 21 | } 22 | 23 | onClose() { 24 | const { contentEl } = this 25 | contentEl.empty() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Functional programming 2 | 3 | Many times, I found myself trying to reuse some of the functions that I wrote in other projects. I found that the best way to do this is to write pure functions. Pure functions are functions that do not have any side effects and always return the same output for the same input. This makes them very easy to test and reuse. 4 | 5 | A lot of function in this project are copied from another project. 6 | 7 | Even thought JavaScript is not a functional programming language, I tried to write as much pure functions as possible. So, please keep this in mind when you are contributing to this project. 8 | 9 | Thanks for your contribution! 10 | 11 | ## Git commit message 12 | 13 | I prefer to use the AngularJS Git Commit Message Conventions, but it's optional. Nowerdays, we use AI to generate the changelog, message, so it's not that important anymore. But do not get me wrong, I still think that it's a good practice to use it. 14 | -------------------------------------------------------------------------------- /src/fns/registerGate.ts: -------------------------------------------------------------------------------- 1 | import { GateView } from '../GateView' 2 | import { openView } from './openView' 3 | import { addIcon, Plugin } from 'obsidian' 4 | import { GateFrameOption } from '../GateOptions' 5 | 6 | export const registerGate = (plugin: Plugin, options: GateFrameOption) => { 7 | plugin.registerView(options.id, (leaf) => { 8 | return new GateView(leaf, options) 9 | }) 10 | 11 | let iconName = options.icon 12 | 13 | if (options.icon.startsWith(' openView(plugin.app.workspace, options.id, options.position)) 20 | } 21 | 22 | plugin.addCommand({ 23 | id: `open-gate-${btoa(options.url)}`, 24 | name: `Open gate ${options.title}`, 25 | callback: async () => await openView(plugin.app.workspace, options.id, options.position) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/fns/setupInsertLinkMenu.ts: -------------------------------------------------------------------------------- 1 | import { App, Editor, Menu, Notice, Plugin } from 'obsidian' 2 | import { ModalInsertLink } from '../ModalInsertLink' 3 | import { GateFrameOption } from '../GateOptions' 4 | 5 | export const setupInsertLinkMenu = (plugin: Plugin) => { 6 | plugin.registerEvent(plugin.app.workspace.on('editor-menu', (menu, editor) => createMenu(plugin.app, menu, editor))) 7 | } 8 | 9 | const createMenu = (app: App, menu: Menu, editor: Editor) => { 10 | menu.addItem((item) => { 11 | item.setTitle('Insert Gate Link').onClick(async () => { 12 | const modal = new ModalInsertLink(app, async (gate: GateFrameOption) => { 13 | const gateLink = `[${gate.title}](obsidian://opengate?title=${encodeURIComponent(gate.title)}&url=${encodeURIComponent(gate.url)})` 14 | editor.replaceSelection(gateLink) 15 | modal.close() 16 | }) 17 | modal.open() 18 | }) 19 | }) 20 | } 21 | 22 | const getDialog = (plugin: Plugin) => {} 23 | -------------------------------------------------------------------------------- /docs/quick-switch.md: -------------------------------------------------------------------------------- 1 | # Quick switch 2 | 3 | You don’t want every gate to show up in the left sidebar. 4 | 5 | You will turn off the "Ribbon" options of the gates. Then how to access them quickly? We have two ways to do that. 6 | 7 | ## Command Palette 8 | 9 | 1. Open the command palette (`Ctrl+P` on Windows/Linux, `Cmd+P` on macOS). 10 | 2. Type `Open gate` and you will see the list of gates. 11 | 3. Select the gate you want to open. 12 | 4. Press `Enter`. 13 | 14 | ## Hotkey 15 | 16 | By default, the hotkey to open the last gate is `Ctrl+Shift+G` on Windows/Linux, `Cmd+Shift+G` on macOS (You can change it in the settings.), a popup will show up with the list of gates. Click on the gate you want to open. 17 | 18 | 19 |
20 | 21 |
22 | -------------------------------------------------------------------------------- /docs/inline-embedded.md: -------------------------------------------------------------------------------- 1 | # Inline Embedded 2 | 3 | You can embed a view directly in a note: 4 | 5 | ~~~md 6 | ```gate 7 | url: https://12bit.vn 8 | height: 300 9 | zoomFactor: 1 10 | css: | 11 | html { filter: invert(90%) hue-rotate(180deg)!important; } 12 | ``` 13 | ~~~ 14 | 15 | Here is the result: 16 | 17 | ![img.png](assets/img.png) 18 | 19 | You can use any property from the [Gate Options](gate-options.md) documentation. 20 | 21 | ## Reuse gates 22 | 23 | To simplify the reuse of gates configured in the settings, the plugin automatically matches the title or URL with existing gates. If a match is found, the options from the gate configured in the settings are merged with the options specified in the note. 24 | 25 | For instance, if there is a gate titled `12bit` configured in the settings, you can easily reuse it by only specifying the `title` and any additional option like `height` in your note as shown below: 26 | 27 | ~~~md 28 | ```gate 29 | title: 12bit 30 | height: 300 31 | ``` 32 | ~~~ 33 | 34 | This will open the `12bit` gate with a height of `300px`. 35 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # Getting Started 6 | 7 | To begin using the Obsidian Open Gate plugin, follow these simple steps to install it: 8 | 9 | 1. Direct Install: Navigate to the [Direct Install Link](https://obsidian.md/plugins?id=open-gate). This will take you to the plugin's page in the Obsidian app. Follow the instructions to install. 10 | 2. Once installed, access the plugin settings in Obsidian to configure it according to your preferences. 11 | 3. To start embedding websites, open the command palette (`Ctrl+P` on Windows/Linux, `Cmd+P` on macOS) and type `Create new gate`. Enter the desired URL and title for the website you wish to embed. 12 | 4. Your new gate will now appear in the left sidebar of Obsidian. Click on its icon to view the embedded website within Obsidian. 13 | 14 | You may notice that there are several options available for customizing your gates. We will deep dive into these features in the next section. For now, feel free to explore the plugin and experiment with embedding different websites to enhance your note-taking experience. 15 | 16 | ![img_1.png](assets/img_1.png) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nguyen Van Duoc 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 | -------------------------------------------------------------------------------- /src/fns/normalizeGateOption.ts: -------------------------------------------------------------------------------- 1 | import { getSvgIcon } from './getSvgIcon' 2 | import { GateFrameOption } from '../GateOptions' 3 | 4 | export const normalizeGateOption = (gate: Partial): GateFrameOption => { 5 | if (gate.url === '' || gate.url === undefined) { 6 | throw new Error('URL is required') 7 | } 8 | 9 | if (gate.id === '' || gate.id === undefined) { 10 | let seedString = gate.url! 11 | if (gate.profileKey != undefined && gate.profileKey !== 'open-gate' && gate.profileKey !== '') { 12 | seedString += gate.profileKey 13 | } 14 | gate.id = btoa(seedString) 15 | } 16 | 17 | if (gate.profileKey === '' || gate.profileKey === undefined) { 18 | gate.profileKey = 'open-gate' 19 | } 20 | 21 | if (gate.zoomFactor === 0 || gate.zoomFactor === undefined) { 22 | gate.zoomFactor = 1 23 | } 24 | 25 | if (gate.icon === '' || gate.icon === undefined) { 26 | gate.icon = gate.url?.startsWith('http') ? getSvgIcon(gate.url) : 'globe' 27 | } 28 | 29 | if (gate.title === '' || gate.title === undefined) { 30 | gate.title = gate.url 31 | } 32 | 33 | return gate 34 | } 35 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributors": [ 10 | { 11 | "login": "andrewmcgivery", 12 | "name": "andrewmcgivery", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/4482878?v=4", 14 | "profile": "https://github.com/andrewmcgivery", 15 | "contributions": [ 16 | "code" 17 | ] 18 | }, 19 | { 20 | "login": "miztizm", 21 | "name": "Digital Alchemist", 22 | "avatar_url": "https://avatars.githubusercontent.com/u/617020?v=4", 23 | "profile": "https://github.com/miztizm", 24 | "contributions": [ 25 | "code" 26 | ] 27 | }, 28 | { 29 | "login": "LiamSwayne", 30 | "name": "Liam Swayne", 31 | "avatar_url": "https://avatars.githubusercontent.com/u/108629034?v=4", 32 | "profile": "https://github.com/LiamSwayne", 33 | "contributions": [ 34 | "code" 35 | ] 36 | } 37 | ], 38 | "contributorsPerLine": 7, 39 | "skipCi": true, 40 | "repoType": "github", 41 | "repoHost": "https://github.com", 42 | "projectName": "obsidian-open-gate", 43 | "projectOwner": "nguyenvanduocit" 44 | } 45 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | import process from 'process' 3 | import builtins from 'builtin-modules' 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | ` 10 | 11 | const prod = process.argv[2] === 'production' 12 | 13 | esbuild 14 | .build({ 15 | banner: { 16 | js: banner 17 | }, 18 | entryPoints: ['src/main.ts'], 19 | bundle: true, 20 | external: [ 21 | 'obsidian', 22 | 'electron', 23 | '@codemirror/autocomplete', 24 | '@codemirror/collab', 25 | '@codemirror/commands', 26 | '@codemirror/language', 27 | '@codemirror/lint', 28 | '@codemirror/search', 29 | '@codemirror/state', 30 | '@codemirror/view', 31 | '@lezer/common', 32 | '@lezer/highlight', 33 | '@lezer/lr', 34 | ...builtins 35 | ], 36 | format: 'cjs', 37 | watch: !prod, 38 | target: 'es2018', 39 | logLevel: 'info', 40 | sourcemap: prod ? false : 'inline', 41 | treeShaking: true, 42 | outfile: 'main.js' 43 | }) 44 | .catch(() => process.exit(1)) 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-open-gate", 3 | "version": "1.11.14", 4 | "description": "Embedding any website to Obsidian, from now all, you have anything you need in one place.", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json", 10 | "format": "prettier --write .", 11 | "docs:dev": "vitepress dev docs", 12 | "docs:build": "vitepress build docs", 13 | "docs:preview": "vitepress preview docs", 14 | "test": "vitest" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@codemirror/view": "^6.35.0", 21 | "@types/chrome": "^0.0.203", 22 | "@types/node": "^16.18.121", 23 | "@typescript-eslint/eslint-plugin": "5.29.0", 24 | "@typescript-eslint/parser": "5.29.0", 25 | "builtin-modules": "3.3.0", 26 | "electron": "^23.3.13", 27 | "esbuild": "0.14.47", 28 | "obsidian": "latest", 29 | "prettier": "^2.8.8", 30 | "quicklink": "^2.3.0", 31 | "tslib": "2.4.0", 32 | "typescript": "4.7.4", 33 | "vitepress": "^1.5.0", 34 | "vitest": "^1.0.0" 35 | }, 36 | "dependencies": { 37 | "yaml": "^2.6.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/normalizeGateOption.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { normalizeGateOption } from '../src/fns/normalizeGateOption' 3 | import { getSvgIcon } from '../src/fns/getSvgIcon' 4 | 5 | // Helper to clone object 6 | const clone = (obj: T): T => JSON.parse(JSON.stringify(obj)) 7 | 8 | describe('normalizeGateOption', () => { 9 | it('generates id when none is supplied', () => { 10 | const gate = { url: 'https://example.com' } 11 | const normalized = normalizeGateOption(clone(gate)) 12 | expect(normalized.id).toBe(btoa('https://example.com')) 13 | }) 14 | 15 | it('applies default profile key and zoom factor', () => { 16 | const gate = { url: 'https://example.com' } 17 | const normalized = normalizeGateOption(clone(gate)) 18 | expect(normalized.profileKey).toBe('open-gate') 19 | expect(normalized.zoomFactor).toBe(1) 20 | }) 21 | 22 | it('defaults icon and title correctly', () => { 23 | const url = 'https://example.com' 24 | const normalized = normalizeGateOption({ url }) 25 | expect(normalized.icon).toBe(getSvgIcon(url)) 26 | expect(normalized.title).toBe(url) 27 | }) 28 | 29 | it('throws error when url is missing', () => { 30 | // @ts-expect-error testing missing url 31 | expect(() => normalizeGateOption({})).toThrow('URL is required') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /docs/gate-link.md: -------------------------------------------------------------------------------- 1 | # Gate Link 2 | 3 | This feature allows you to create a link that opens a URL within the gate view of Obsidian, rather than navigating away in an external browser 4 | 5 | ## Usage 6 | 7 | ```md 8 | [Open Google Gate](obsidian://opengate?title=google&url=https%3A%2F%2Fdocs.google.com%2Fdocument%2Fd%2Fabc123%2Fedit) 9 | ``` 10 | 11 | Please notice that the URL must be encoded. You can use [this tool](https://www.urlencoder.org/) to encode your URL. 12 | 13 | You may want to read [Gate Options](gate-options.md) to learn more about the options you can use. Of course, there is no space for custom CSS or JavaScript in the gate view. 14 | 15 | ## Editor context menu 16 | 17 | We know that encoding URLs can be a hassle, so we've added a context menu option to make it easier. Just right-click on the link, and select `Insert Gate Link`. 18 | 19 | A popup will appear, asking for the title and URL of the gate. The URL will be automatically encoded for you. 20 | 21 | ![img.png](assets/img_2.png) 22 | 23 | ## Convert to gate link 24 | 25 | If you right-click on a normal link, you will see an option to convert it to a gate link. The URL will be automatically encoded for you. 26 | 27 | ## Reuse gates 28 | 29 | Once again, to simplify the reuse of gates configured in the settings, the plugin automatically matches the title or URL with existing gates. If a match is found, the options from the gate configured in the settings are merged with the options specified in the note. 30 | -------------------------------------------------------------------------------- /src/fns/createIframe.ts: -------------------------------------------------------------------------------- 1 | import { GateFrameOption } from '../GateOptions' 2 | 3 | export const createIframe = (params: Partial, onReady?: () => void): HTMLIFrameElement => { 4 | const iframe = document.createElement('iframe') 5 | 6 | iframe.setAttribute('allowpopups', '') 7 | 8 | // Only set credentialless if supported (experimental feature, not on older Android WebView) 9 | if ('credentialless' in iframe) { 10 | iframe.setAttribute('credentialless', 'true') 11 | } 12 | 13 | iframe.setAttribute('src', params.url ?? 'about:blank') 14 | iframe.setAttribute('sandbox', 'allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts allow-top-navigation-by-user-activation') 15 | iframe.setAttribute('allow', 'encrypted-media; fullscreen; oversized-images; picture-in-picture; sync-xhr; geolocation') 16 | iframe.addClass('open-gate-iframe') 17 | 18 | iframe.addEventListener('load', () => { 19 | onReady?.call(null) 20 | 21 | if (params?.css) { 22 | const style = document.createElement('style') 23 | style.textContent = params.css 24 | iframe.contentDocument?.head.appendChild(style) 25 | } 26 | 27 | if (params?.js) { 28 | const script = document.createElement('script') 29 | script.textContent = params.js 30 | iframe.contentDocument?.head.appendChild(script) 31 | } 32 | }) 33 | 34 | return iframe 35 | } 36 | -------------------------------------------------------------------------------- /.specify/templates/commands/clarify.md: -------------------------------------------------------------------------------- 1 | # Clarify Feature Specification 2 | 3 | You are helping clarify underspecified areas in the current feature specification (`spec.md`). 4 | 5 | ## Process 6 | 7 | 1. Read the current `spec.md` file 8 | 2. Identify areas that are vague, ambiguous, or underspecified 9 | 3. Reference the project constitution at `.specify/memory/constitution.md` to ensure questions align with project principles 10 | 4. Generate 3-5 highly targeted clarification questions 11 | 5. Present questions to the user using AskUserQuestion tool 12 | 6. Encode answers back into `spec.md` 13 | 14 | ## Question Guidelines 15 | 16 | - **First-Principles Thinking**: Ask "why" questions that get to root requirements 17 | - **YAGNI Validation**: Challenge features that might not be needed 18 | - **KISS Enforcement**: If something seems complex, ask if there's a simpler approach 19 | - **Separation of Concerns**: Clarify responsibility boundaries 20 | - **Iteration-First**: Ask about change scenarios - "what if X changes?" 21 | 22 | ## Example Questions 23 | 24 | - "What is the root problem this feature solves for users?" 25 | - "Is [complex feature X] actually needed, or can we defer it?" 26 | - "What's the simplest version that delivers core value?" 27 | - "Which Obsidian API should handle [responsibility X]?" 28 | - "If this requirement changes, which code would need to update?" 29 | 30 | ## Output 31 | 32 | Update `spec.md` with clarified information, removing ambiguity and adding concrete details based on user responses. 33 | -------------------------------------------------------------------------------- /src/fns/openView.ts: -------------------------------------------------------------------------------- 1 | import { Workspace, WorkspaceLeaf } from 'obsidian' 2 | import { GateFrameOptionType } from '../GateOptions' 3 | 4 | export const openView = async (workspace: Workspace, id: string, position?: GateFrameOptionType): Promise => { 5 | let leafs = workspace.getLeavesOfType(id) 6 | if (leafs.length > 0) { 7 | workspace.revealLeaf(leafs[0]) 8 | return leafs[0] 9 | } 10 | 11 | const leaf = await createView(workspace, id, position) 12 | if (!leaf) { 13 | throw new Error(`Failed to create view with id: ${id}`) 14 | } 15 | workspace.revealLeaf(leaf) 16 | 17 | return leaf 18 | } 19 | 20 | export const isViewExist = (workspace: Workspace, id: string): boolean => { 21 | let leafs = workspace.getLeavesOfType(id) 22 | return leafs.length > 0 23 | } 24 | 25 | const createView = async (workspace: Workspace, id: string, position?: GateFrameOptionType): Promise => { 26 | let leaf: WorkspaceLeaf | null = null 27 | switch (position) { 28 | case 'left': 29 | leaf = workspace.getLeftLeaf(false) 30 | break 31 | case 'center': 32 | leaf = workspace.getLeaf(true) 33 | break 34 | case 'right': 35 | default: 36 | leaf = workspace.getRightLeaf(false) 37 | break 38 | } 39 | 40 | if (leaf) { 41 | await leaf.setViewState({ type: id, active: true }) 42 | return leaf 43 | } 44 | return undefined 45 | } 46 | -------------------------------------------------------------------------------- /src/ModalOnboarding.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal } from 'obsidian' 2 | import { createFormEditGate } from './fns/createFormEditGate' 3 | import { GateFrameOption } from './GateOptions' 4 | 5 | export class ModalOnBoarding extends Modal { 6 | gateOptions: GateFrameOption 7 | onSubmit: (result: GateFrameOption) => void 8 | constructor(app: App, gateOptions: GateFrameOption, onSubmit: (result: GateFrameOption) => void) { 9 | super(app) 10 | this.onSubmit = onSubmit 11 | this.gateOptions = gateOptions 12 | } 13 | 14 | onOpen() { 15 | const { contentEl } = this 16 | contentEl.createEl('h3', { text: 'Welcome to OpenGate' }) 17 | contentEl.createEl('p', { 18 | text: 'OpenGate is a plugin that allows you to embed any website in Obsidian. You will never have to leave Obsidian again!' 19 | }) 20 | 21 | contentEl.createEl('p', { 22 | text: 'If you need help, please join our community.' 23 | }) 24 | 25 | contentEl.createEl('a', { 26 | cls: 'community-link', 27 | text: 'Community', 28 | attr: { href: 'https://community.aiocean.io/' } 29 | }) 30 | 31 | contentEl.createEl('p', { 32 | text: 'But now you have to create your first gate.' 33 | }) 34 | 35 | createFormEditGate(contentEl, this.gateOptions, (result) => { 36 | this.onSubmit(result) 37 | this.close() 38 | }) 39 | } 40 | 41 | onClose() { 42 | const { contentEl } = this 43 | contentEl.empty() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ModalListGates.ts: -------------------------------------------------------------------------------- 1 | import { App, getIcon, Modal } from 'obsidian' 2 | import { createFormEditGate } from './fns/createFormEditGate' 3 | import { openView } from './fns/openView' 4 | import { GateFrameOption } from './GateOptions' 5 | 6 | export class ModalListGates extends Modal { 7 | gates: Record 8 | onSubmit: (result: GateFrameOption) => void 9 | constructor(app: App, gates: Record, onSubmit: (result: GateFrameOption) => void) { 10 | super(app) 11 | this.onSubmit = onSubmit 12 | this.gates = gates 13 | } 14 | 15 | onOpen() { 16 | const { contentEl } = this 17 | 18 | for (const gateId in this.gates) { 19 | const gate = this.gates[gateId] 20 | // create svg icon 21 | const container = contentEl.createEl('div', { 22 | cls: 'open-gate--quick-list-item' 23 | }) 24 | 25 | if (!gate.icon.startsWith(' { 37 | await openView(this.app.workspace, gate.id, gate.position) 38 | this.close() 39 | }) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/fns/createWebviewTag.ts: -------------------------------------------------------------------------------- 1 | import WebviewTag = Electron.WebviewTag 2 | import { GateFrameOption } from '../GateOptions' 3 | import getDefaultUserAgent from './getDefaultUserAgent' 4 | 5 | // Constants for repeated strings 6 | const DEFAULT_URL = 'about:blank' 7 | const GOOGLE_URL = 'https://google.com' 8 | const OPEN_GATE_WEBVIEW_CLASS = 'open-gate-webview' 9 | 10 | export const createWebviewTag = (params: Partial, onReady?: () => void, parentDoc?: Document): WebviewTag => { 11 | // Create a new webview tag using the parent document context 12 | const webviewTag = (parentDoc || document).createElement('webview') as unknown as WebviewTag 13 | 14 | // Set attributes for the webview tag 15 | webviewTag.setAttribute('partition', 'persist:' + params.profileKey) 16 | webviewTag.setAttribute('src', params.url ?? DEFAULT_URL) 17 | webviewTag.setAttribute('httpreferrer', params.url ?? GOOGLE_URL) 18 | webviewTag.setAttribute('allowpopups', 'true') 19 | webviewTag.addClass(OPEN_GATE_WEBVIEW_CLASS) 20 | 21 | // Set user agent (use default Chrome UA if not provided to avoid bot detection) 22 | webviewTag.setAttribute('useragent', params.userAgent || getDefaultUserAgent()) 23 | 24 | webviewTag.addEventListener('dom-ready', async () => { 25 | // Set zoom factor if provided 26 | if (params.zoomFactor) { 27 | webviewTag.setZoomFactor(params.zoomFactor) 28 | } 29 | 30 | if (params?.css) { 31 | await webviewTag.insertCSS(params.css) 32 | } 33 | 34 | if (params?.js) { 35 | await webviewTag.executeJavaScript(params.js) 36 | } 37 | 38 | onReady?.call(null) 39 | }) 40 | 41 | return webviewTag 42 | } 43 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0", 3 | "1.0.1": "0.15.0", 4 | "1.0.2": "0.15.0", 5 | "1.0.4": "0.15.0", 6 | "1.0.5": "0.15.0", 7 | "1.0.6": "0.15.0", 8 | "1.0.7": "0.15.0", 9 | "1.0.8": "0.15.0", 10 | "1.1.1": "0.15.0", 11 | "1.1.2": "0.15.0", 12 | "1.1.3": "0.15.0", 13 | "1.1.4": "0.15.0", 14 | "1.1.5": "0.15.0", 15 | "1.1.6": "0.15.0", 16 | "1.1.7": "0.15.0", 17 | "1.2.1": "0.15.0", 18 | "1.2.3": "0.15.0", 19 | "1.2.4": "0.15.0", 20 | "1.2.5": "0.15.0", 21 | "1.2.6": "0.15.0", 22 | "1.2.7": "0.15.0", 23 | "1.2.8": "0.15.0", 24 | "1.2.9": "0.15.0", 25 | "1.2.20": "0.15.0", 26 | "1.2.21": "0.15.0", 27 | "1.3.0": "0.15.0", 28 | "1.3.1": "0.15.0", 29 | "1.3.2": "0.15.0", 30 | "1.3.3": "0.15.0", 31 | "1.3.4": "0.15.0", 32 | "1.3.5": "0.15.0", 33 | "1.4.5": "0.15.0", 34 | "1.7.1": "0.15.0", 35 | "1.8.0": "0.15.0", 36 | "1.8.1": "0.15.0", 37 | "1.8.4": "0.15.0", 38 | "1.8.5": "0.15.0", 39 | "1.9.0": "0.15.0", 40 | "1.9.1": "0.15.0", 41 | "1.9.2": "0.15.0", 42 | "1.9.3": "0.15.0", 43 | "1.9.4": "0.15.0", 44 | "1.9.5": "0.15.0", 45 | "1.9.6": "0.15.0", 46 | "1.9.7": "0.15.0", 47 | "1.9.8": "0.15.0", 48 | "1.10.0": "0.15.0", 49 | "1.10.1": "0.15.0", 50 | "1.10.2": "0.15.0", 51 | "1.10.3": "0.15.0", 52 | "1.10.4": "0.15.0", 53 | "1.10.5": "0.15.0", 54 | "1.10.6": "0.15.0", 55 | "1.10.7": "0.15.0", 56 | "1.10.8": "0.15.0", 57 | "1.11.0": "0.15.0", 58 | "1.11.1": "0.15.0", 59 | "1.11.2": "0.15.0", 60 | "1.11.3": "0.15.0", 61 | "1.11.4": "0.15.0", 62 | "1.11.5": "0.15.0", 63 | "1.11.6": "0.15.0", 64 | "1.11.7": "0.15.0", 65 | "1.11.8": "0.15.0", 66 | "1.11.9": "0.15.0", 67 | "1.11.10": "0.15.0", 68 | "1.11.11": "0.15.0", 69 | "1.11.12": "0.15.0", 70 | "1.11.13": "0.15.0", 71 | "1.11.14": "0.15.0" 72 | } -------------------------------------------------------------------------------- /src/ModalInsertLink.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal, Setting } from 'obsidian' 2 | import { createEmptyGateOption } from './fns/createEmptyGateOption' 3 | import { normalizeGateOption } from './fns/normalizeGateOption' 4 | import { GateFrameOption } from './GateOptions' 5 | 6 | export class ModalInsertLink extends Modal { 7 | onSubmit: (result: GateFrameOption) => void 8 | constructor(app: App, onSubmit: (result: GateFrameOption) => void) { 9 | super(app) 10 | this.onSubmit = onSubmit 11 | } 12 | 13 | onOpen() { 14 | this.titleEl.setText('Insert Link') 15 | this.createFormInsertLink() 16 | } 17 | 18 | onClose() { 19 | const { contentEl } = this 20 | contentEl.empty() 21 | } 22 | 23 | createFormInsertLink() { 24 | let gateOptions = createEmptyGateOption() 25 | new Setting(this.contentEl) 26 | .setName('URL') 27 | .setClass('open-gate--form-field') 28 | .addText((text) => 29 | text.setPlaceholder('https://example.com').onChange(async (value) => { 30 | gateOptions.url = value 31 | }) 32 | ) 33 | 34 | new Setting(this.contentEl) 35 | .setName('Title') 36 | .setClass('open-gate--form-field') 37 | .addText((text) => 38 | text.onChange(async (value) => { 39 | gateOptions.title = value 40 | }) 41 | ) 42 | 43 | new Setting(this.contentEl).addButton((btn) => 44 | btn 45 | .setButtonText('Insert Link') 46 | .setCta() 47 | .onClick(async () => { 48 | gateOptions = normalizeGateOption(gateOptions) 49 | this.onSubmit(gateOptions) 50 | }) 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.specify/templates/commands/checklist.md: -------------------------------------------------------------------------------- 1 | # Generate Feature Checklist 2 | 3 | Generate a custom, actionable checklist for the current feature based on user requirements and constitutional principles. 4 | 5 | ## Process 6 | 7 | 1. Read the feature specification (`spec.md`) 8 | 2. Read the project constitution (`.specify/memory/constitution.md`) 9 | 3. Generate a checklist tailored to this specific feature 10 | 4. Ensure checklist items validate constitutional compliance 11 | 12 | ## Checklist Structure 13 | 14 | ### Constitutional Compliance 15 | - [ ] Iteration-first: Code changes are easy to modify 16 | - [ ] KISS: Solution is as simple as possible 17 | - [ ] YAGNI: No speculative features included 18 | - [ ] DRY: Abstractions only where truly needed 19 | - [ ] First-principles: Root cause/requirement identified 20 | - [ ] Separation of concerns: Single responsibility per module 21 | - [ ] Greenfield: No deprecated code patterns 22 | - [ ] No workarounds: Works with framework, not against it 23 | 24 | ### Feature-Specific Items 25 | [Generated based on spec.md content] 26 | 27 | ### Code Quality 28 | - [ ] TypeScript strict mode compliance 29 | - [ ] No ESLint errors 30 | - [ ] Prettier formatting applied 31 | - [ ] Functions are single-purpose 32 | - [ ] No util/helper proliferation 33 | 34 | ### Obsidian Integration 35 | - [ ] Follows Obsidian API conventions 36 | - [ ] Uses appropriate Obsidian UI components 37 | - [ ] Respects plugin lifecycle (onload/onunload) 38 | - [ ] Tested on desktop 39 | - [ ] Tested on mobile 40 | 41 | ### Documentation 42 | - [ ] README.md updated (if user-facing) 43 | - [ ] Code comments only where necessary 44 | - [ ] CHANGELOG.md updated 45 | - [ ] Docs site updated (if applicable) 46 | 47 | ### Release Readiness 48 | - [ ] Version bumped (manifest.json, package.json) 49 | - [ ] Breaking changes documented 50 | - [ ] Manual testing completed 51 | - [ ] No deprecated code remains 52 | 53 | ## Output 54 | 55 | Save the generated checklist as `checklist.md` in the feature directory. 56 | -------------------------------------------------------------------------------- /.specify/templates/commands/analyze.md: -------------------------------------------------------------------------------- 1 | # Analyze Cross-Artifact Consistency 2 | 3 | You are performing a non-destructive consistency and quality analysis across specification artifacts. 4 | 5 | ## Artifacts to Analyze 6 | 7 | 1. `.specify/memory/constitution.md` - Project principles 8 | 2. `spec.md` - Feature specification 9 | 3. `plan.md` - Implementation plan (if exists) 10 | 4. `tasks.md` - Task breakdown (if exists) 11 | 12 | ## Analysis Dimensions 13 | 14 | ### Constitutional Compliance 15 | 16 | For each artifact, verify: 17 | - [ ] References appropriate constitutional principles 18 | - [ ] Demonstrates iteration-first thinking 19 | - [ ] Follows KISS - no unnecessary complexity 20 | - [ ] Applies YAGNI - no speculative features 21 | - [ ] Uses first-principles reasoning 22 | - [ ] Maintains separation of concerns 23 | - [ ] Adheres to greenfield mindset (no backwards-compat cruft) 24 | 25 | ### Cross-Artifact Consistency 26 | 27 | Check for: 28 | - [ ] Spec goals align with plan approach 29 | - [ ] Plan steps match task breakdown 30 | - [ ] Task dependencies are logical and constitutional 31 | - [ ] File modifications are consistent across artifacts 32 | - [ ] No contradictions between spec, plan, and tasks 33 | 34 | ### Quality Checks 35 | 36 | - [ ] Spec is concrete, not vague 37 | - [ ] Plan justifies architectural decisions 38 | - [ ] Tasks are small, testable, and independent 39 | - [ ] Dependencies are explicitly stated 40 | - [ ] Obsidian API usage is correct 41 | 42 | ## Output Format 43 | 44 | Generate a report with: 45 | 46 | ### ✅ Strengths 47 | [What's well-aligned and constitutional] 48 | 49 | ### ⚠️ Warnings 50 | [Potential issues, inconsistencies, or principle violations] 51 | 52 | ### 🔍 Recommendations 53 | [Suggested improvements based on constitution] 54 | 55 | ### 📊 Metrics 56 | - Constitutional principle coverage: X/8 57 | - Artifact consistency score: [High/Medium/Low] 58 | - YAGNI violations detected: [Count] 59 | - Complexity red flags: [Count] 60 | 61 | Do NOT modify any files. This is a read-only analysis. 62 | -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | claude: 15 | if: | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | pull-requests: read 24 | issues: read 25 | id-token: write 26 | actions: read # Required for Claude to read CI results on PRs 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 1 32 | 33 | - name: Run Claude Code 34 | id: claude 35 | uses: anthropics/claude-code-action@v1 36 | with: 37 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} 38 | 39 | # This is an optional setting that allows Claude to read CI results on PRs 40 | additional_permissions: | 41 | actions: read 42 | 43 | # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. 44 | # prompt: 'Update the pull request description to include a summary of changes.' 45 | 46 | # Optional: Add claude_args to customize behavior and configuration 47 | # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md 48 | # or https://code.claude.com/docs/en/cli-reference for available options 49 | # claude_args: '--allowed-tools Bash(gh pr:*)' 50 | 51 | -------------------------------------------------------------------------------- /src/fns/setupLinkConvertMenu.ts: -------------------------------------------------------------------------------- 1 | import { Editor, Menu, Notice, Plugin } from 'obsidian' 2 | import { MarkdownLink } from '../types' 3 | 4 | export const setupLinkConvertMenu = (plugin: Plugin) => { 5 | plugin.registerEvent(plugin.app.workspace.on('editor-menu', createMenu)) 6 | } 7 | 8 | const parseLink = (text: string): MarkdownLink | undefined => { 9 | const markdownLinkMatch = text.match(/\[([^\]]+)\]\(([^)]+)\)/) 10 | if (markdownLinkMatch) { 11 | return { 12 | title: markdownLinkMatch[1], 13 | url: markdownLinkMatch[2] 14 | } 15 | } 16 | 17 | const urlMatch = text.match(/https?:\/\/[^ ]+/) 18 | if (urlMatch) { 19 | return { 20 | title: urlMatch[0], 21 | url: urlMatch[0] 22 | } 23 | } 24 | } 25 | 26 | const createMenu = (menu: Menu, editor: Editor) => { 27 | const selection = editor.getSelection() 28 | if (selection.length === 0) return 29 | 30 | const parsedLink = parseLink(selection) 31 | if (!parsedLink) return 32 | 33 | if (parsedLink.url.startsWith('obsidian://opengate')) { 34 | menu.addItem((item) => { 35 | item.setTitle('Convert to normal link').onClick(async () => { 36 | // get the url parameter from the link 37 | const urlMatch = parsedLink.url.match(/url=([^&]+)/) 38 | if (!urlMatch) { 39 | new Notice('Can not convert the pre-configured gate link to normal link.') 40 | return 41 | } 42 | 43 | const url = decodeURIComponent(urlMatch[1]) 44 | const normalLink = `[${parsedLink.title}](${url})` 45 | editor.replaceSelection(normalLink) 46 | }) 47 | }) 48 | } else { 49 | menu.addItem((item) => { 50 | item.setTitle('Convert to Gate Link').onClick(async () => { 51 | const gateLink = `[${parsedLink.title}](obsidian://opengate?title=${encodeURIComponent(parsedLink.title)}&url=${encodeURIComponent(parsedLink.url)})` 52 | editor.replaceSelection(gateLink) 53 | }) 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/claude-code-review.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code Review 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | # Optional: Only run on specific file changes 7 | # paths: 8 | # - "src/**/*.ts" 9 | # - "src/**/*.tsx" 10 | # - "src/**/*.js" 11 | # - "src/**/*.jsx" 12 | 13 | jobs: 14 | claude-review: 15 | # Optional: Filter by PR author 16 | # if: | 17 | # github.event.pull_request.user.login == 'external-contributor' || 18 | # github.event.pull_request.user.login == 'new-developer' || 19 | # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' 20 | 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | pull-requests: read 25 | issues: read 26 | id-token: write 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 1 33 | 34 | - name: Run Claude Code Review 35 | id: claude-review 36 | uses: anthropics/claude-code-action@v1 37 | with: 38 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} 39 | prompt: | 40 | REPO: ${{ github.repository }} 41 | PR NUMBER: ${{ github.event.pull_request.number }} 42 | 43 | Please review this pull request and provide feedback on: 44 | - Code quality and best practices 45 | - Potential bugs or issues 46 | - Performance considerations 47 | - Security concerns 48 | - Test coverage 49 | 50 | Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. 51 | 52 | Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. 53 | 54 | # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md 55 | # or https://code.claude.com/docs/en/cli-reference for available options 56 | claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' 57 | 58 | -------------------------------------------------------------------------------- /docs/add-gate.md: -------------------------------------------------------------------------------- 1 | # Add Gate 2 | 3 | There are two ways to add a new gate to your Obsidian vault: using the command palette or setting screen. This guide will walk you through both methods. 4 | 5 | ## Command Palette 6 | 7 | To add a new gate to your Obsidian vault, follow these simple steps: 8 | 9 | 1. Open the command palette (`Ctrl+P` on Windows/Linux, `Cmd+P` on macOS). 10 | 2. Type `Create new gate` and press `Enter`. 11 | 3. Enter the desired URL and title for the website you wish to embed. 12 | 4. Your new gate will now appear in the left sidebar of Obsidian. Click on its icon to view the embedded website within Obsidian. 13 | 14 |
15 | 16 |
17 | 18 | ## Setting Screen 19 | 20 | To add a new gate using the setting screen, follow these simple steps: 21 | 22 | 1. Open the settings 23 | 2. Navigate to the `Open Gate` tab. 24 | 3. Click on the `Add Gate` button. 25 | 4. Enter the desired URL and title for the website you wish to embed. 26 | 5. Your new gate will now appear in the left sidebar of Obsidian. Click on its icon to view the embedded website within Obsidian. 27 | 28 |
29 | 30 |
31 | 32 | 33 | ## Gate Options 34 | 35 | Please refer to the [Gate Options](gate-options.md) documentation for more information on customizing your gates. 36 | 37 | ## Custom icon 38 | 39 | The app will automatically generate an icon for your gate based on the site's favicon. However, you can also set a custom icon for your gate. You can fill svg code in the icon field to set a custom icon. Remember to remove the `width` and `height` attributes from the svg code. 40 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .open-gate-view { 2 | padding: 0 !important; 3 | overflow: hidden !important; 4 | } 5 | 6 | .open-gate-view .open-gate-iframe, 7 | .open-gate-view .open-gate-webview { 8 | width: 100%; 9 | height: 100%; 10 | border: none; 11 | background-color: #fff; 12 | } 13 | 14 | .gate-header { 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | } 19 | 20 | .open-gate-mobile-warning { 21 | background: #b38c1d; 22 | padding: 5px 10px; 23 | border-radius: 5px; 24 | margin-bottom: 20px; 25 | font-size: 14px; 26 | color: #fff; 27 | } 28 | 29 | .open-gate-mobile-link { 30 | color: #fff; 31 | text-decoration: underline; 32 | } 33 | 34 | .open-gate--form-field--column .setting-item-control, 35 | .open-gate--form-field--column .setting-item-control textarea, 36 | .open-gate--form-field .setting-item-control input[type='text'] { 37 | width: 100%; 38 | } 39 | 40 | .open-gate--form-field--column .setting-item-control textarea{ 41 | min-height: 80px; 42 | } 43 | 44 | .open-gate--form-field--column { 45 | display: flex; 46 | flex-direction: column; 47 | gap: 10px; 48 | align-items: flex-start; 49 | } 50 | 51 | .open-gate--advanced-options { 52 | border-top: 1px solid var(--background-modifier-border); 53 | padding: 0.75em 0; 54 | display: none; 55 | } 56 | 57 | .open-gate--advanced-options--show { 58 | display: block; 59 | } 60 | 61 | .open-gate--quick-list-item { 62 | padding: 10px; 63 | display: flex; 64 | justify-content: flex-start; 65 | align-items: center; 66 | gap: 10px; 67 | cursor: pointer; 68 | } 69 | 70 | .open-gate--quick-list-item.active, 71 | .open-gate--quick-list-item:hover { 72 | background: var(--background-modifier-hover); 73 | } 74 | 75 | div[data-gate-id] .setting-item { 76 | overflow: hidden; 77 | } 78 | 79 | div[data-gate-id] .setting-item-name { 80 | white-space: nowrap; 81 | overflow: hidden; 82 | text-overflow: ellipsis; 83 | } 84 | 85 | .open-gate--setting--gate .setting-item-info { 86 | width: 100%; 87 | overflow: hidden; 88 | } 89 | .open-gate--setting--gate .setting-item-description{ 90 | white-space: nowrap; 91 | overflow: hidden; 92 | text-overflow: ellipsis; 93 | } 94 | 95 | -------------------------------------------------------------------------------- /docs/gate-options.md: -------------------------------------------------------------------------------- 1 | # Gate Options 2 | 3 | If you can read code, here is what we have under the hood. The `GateFrameOption` type is used to define the options for a gate frame. 4 | 5 | <<< @/../src/GateOptions.d.ts 6 | 7 | ## User Agent 8 | 9 | Usually, you won't need to change the user agent. However, in some case, the website you are trying to embed may require a specific user agent to work correctly. For example, some websites may block requests from bots or crawlers. In this case, you can set the user agent to a common browser user agent to bypass this restriction. 10 | 11 | Currently, default value is: 12 | 13 | <<< @/../src/fns/getDefaultUserAgent.ts {2} 14 | 15 | ## profileKey 16 | 17 | This property is intriguing as it allows you to embed multiple emails in your vault using the `profileKey` for differentiation. The `profileKey` acts as a namespace, enabling gates with the same `profileKey` to share storage space. This facilitates using a single sign-on (SSO) to log into a website and maintaining the same session across all gates sharing that `profileKey`. 18 | 19 | In other words, `profileKey` is like profile on Chrome. 20 | 21 | ## zoomFactor 22 | 23 | The `zoomFactor` in the `GateFrameOption` determines the magnification level of the content within a "Gate" frame in Obsidian. A value of 1 means 100% zoom (normal size), 0.5 means the content is reduced to 50% of its normal size, and a value of 2 means the content is enlarged to 200% of its normal size. 24 | 25 | ## Css & Js Injection 26 | 27 | `css` and `js` allows you to customize the appearance and functionality of embedded websites. 28 | 29 | ### Example 30 | 31 | - Use CSS to modify styles for a consistent look with Obsidian. 32 | - Example: `html { font-family: 'Arial', sans-serif; }` changes the font. 33 | - Use JS to add interactivity or modify web content. 34 | - Example: `document.body.style.backgroundColor = "lightblue";` changes the background color. 35 | 36 | ### Warning 37 | 38 | - Incorrect CSS/JS may break the appearance/functionality of the gate. 39 | - Be cautious with JS that interacts with external websites to avoid security risks. 40 | 41 | You may want to read [Gate Options](gate-options.md) to learn more about the options you can use. Of course, there is no space for custom CSS or JavaScript in the gate view. 42 | -------------------------------------------------------------------------------- /docs/custom-css.md: -------------------------------------------------------------------------------- 1 | # Custom CSS 2 | 3 | Every website has its own style, and you can customize the appearance of embedded websites in Open Gate by adding custom CSS. This can help match the embedded content with your Obsidian theme or make it more readable. 4 | 5 | ## How to Add Custom CSS 6 | 7 | You can add custom CSS in two ways: 8 | 9 | 1. Through the Gate Settings: 10 | - Open Gate Settings 11 | - Edit a gate 12 | - Enable Advanced Options 13 | - Add your CSS in the CSS field 14 | 15 | 2. In Markdown Code Blocks: 16 | 17 | If you use [inline embedded](inline-embedded.md), you can add custom CSS in the code block like this: 18 | 19 | ~~~ 20 | ```gate 21 | url: https://example.com 22 | css: | 23 | html { font-size: 20px; } 24 | ``` 25 | ~~~ 26 | 27 | ## Useful Snippets 28 | 29 | ### Dark Mode 30 | Convert light websites to dark mode: 31 | ```css 32 | html { 33 | filter: invert(90%) hue-rotate(180deg)!important; 34 | } 35 | ``` 36 | 37 | ### Readability Improvements 38 | Make text more readable: 39 | ```css 40 | html { 41 | font-size: 20px; 42 | } 43 | 44 | body { 45 | line-height: 1.6; 46 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 47 | } 48 | ``` 49 | 50 | ### Hide Elements 51 | Remove unwanted elements: 52 | ```css 53 | .advertisement, 54 | .cookie-banner, 55 | .popup-overlay { 56 | display: none !important; 57 | } 58 | ``` 59 | 60 | ### Custom Scrollbar 61 | Style the scrollbar: 62 | ```css 63 | ::-webkit-scrollbar { 64 | width: 8px; 65 | } 66 | 67 | ::-webkit-scrollbar-track { 68 | background: var(--background-primary); 69 | } 70 | 71 | ::-webkit-scrollbar-thumb { 72 | background: var(--text-muted); 73 | border-radius: 4px; 74 | } 75 | ``` 76 | 77 | ## Tips 78 | - Use `!important` when your styles aren't being applied due to specificity conflicts 79 | - Test CSS changes incrementally to avoid breaking the layout 80 | - Use browser dev tools to inspect elements and find the right selectors 81 | - Consider using Obsidian CSS variables for better theme compatibility 82 | 83 | ## Resources 84 | - [MDN CSS Reference](https://developer.mozilla.org/en-US/docs/Web/CSS) 85 | - [CSS Tricks](https://css-tricks.com/) 86 | - Share your snippets in our [GitHub Discussions](https://github.com/nguyenvanduocit/obsidian-open-gate/discussions/categories/snippets) -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # What is Open Gate? 2 | 3 | **Obsidian Open Gate** seamlessly integrates web browsing into your Obsidian note-taking experience. Embed any website as a "Gate" within the Obsidian interface or directly within notes. Customize the layout, inject custom CSS, and access Gates quickly via hotkeys or a command palette. Whether you're researching, studying, or just browsing, Obsidian Open Gate keeps everything you need in one place, enhancing your productivity and streamlining your workflow. 4 | 5 | ## How does it work? 6 | 7 | Under the hood, Obsidian Open Gate uses webview (mobile) and iframe (desktop) technologies to display websites within the app. 8 | 9 | ## Why use Open Gate? 10 | 11 | - **Improved productivity:** Reduces context switching, allowing you to focus on your tasks. 12 | - **Interactive notes:** Embeds websites directly into notes, making them more engaging and interactive. 13 | - **Real-time updates:** Provides live updates from embedded websites. 14 | - **Enhanced aesthetics:** Brings life to websites by incorporating images, audio, and other elements. 15 | 16 | ## Usage cases 17 | 18 | Obsidian Open Gate transforms Obsidian into a more versatile workspace by embedding various web applications directly within. Here are 10 use cases: 19 | 20 | 1. **Calendar Management**: Embed your Google Calendar or other web-based calendars to stay organized and manage appointments directly in Obsidian. 21 | 2. **Collaborative Workspace**: Integrate Notion or other collaborative tools to work on shared projects and documents with colleagues or friends. 22 | 3. **Real-Time Document Editing**: Embed Google Docs or Microsoft Word Online to seamlessly edit documents and collaborate with others in real time. 23 | 4. **Interactive Learning**: Insert YouTube videos or other interactive web content to enhance your notes or learning materials. 24 | 5. **Project Management**: Embed Trello or Asana boards to manage tasks, track progress, and collaborate on projects. 25 | 6. **Financial Tracking**: Integrate Google Sheets or Excel Online to monitor financial data, create budgets, and track expenses. 26 | 7. **Interactive Whiteboarding**: Embed Miro or other online whiteboards for brainstorming, mind mapping, and collaborative idea generation. 27 | 8. **Content Curation**: Insert Pinterest or Pocket to collect and organize your favorite articles, images, or other online content. 28 | 9. **Research and Analysis**: Embed Google Scholar or JSTOR to access academic databases and research materials. 29 | 10. **Social Media Integration**: Connect with Twitter or LinkedIn to stay updated on industry trends or engage with professional networks. 30 | 31 | ## Why I created Open Gate? 32 | 33 | I created Open Gate because other plugins I tried were either too complicated or lacked features I needed. My goal was to simplify the user experience and include only essential functionalities. I use Open Gate daily and continuously update it to ensure it meets user needs effectively. 34 | -------------------------------------------------------------------------------- /.specify/templates/plan-template.md: -------------------------------------------------------------------------------- 1 | # Implementation Plan: [FEATURE_NAME] 2 | 3 | **Created:** [DATE] 4 | **Status:** [DRAFT | APPROVED | IN_PROGRESS | COMPLETED] 5 | **Owner:** [DEVELOPER_NAME] 6 | 7 | ## Constitution Compliance Check 8 | 9 | Before proceeding, verify alignment with these constitutional principles: 10 | 11 | - [ ] **Iteration-First**: Can this be changed easily tomorrow? 12 | - [ ] **KISS**: Is this the simplest solution that could work? 13 | - [ ] **YAGNI**: Are we building only what's needed now? 14 | - [ ] **DRY**: Have we avoided premature abstraction? 15 | - [ ] **First-Principles**: Have we identified the root cause/requirement? 16 | - [ ] **Separation of Concerns**: Is each module single-purpose? 17 | - [ ] **Greenfield**: Have we removed any deprecated patterns? 18 | - [ ] **No Workarounds**: Are we working with the framework, not against it? 19 | 20 | ## Problem Statement 21 | 22 | [Describe the problem or requirement from first principles. What is the root issue we're solving?] 23 | 24 | ## Proposed Solution 25 | 26 | [Describe the solution approach. Explain why this is the simplest solution that addresses the core problem.] 27 | 28 | ### Architecture Impact 29 | 30 | [How does this change affect the existing architecture? Which files/modules will be touched?] 31 | 32 | ### Obsidian API Integration 33 | 34 | [Which Obsidian APIs will be used? How does this align with Obsidian conventions?] 35 | 36 | ## Alternative Approaches Considered 37 | 38 | [List alternative approaches and explain why they were rejected. This demonstrates YAGNI thinking.] 39 | 40 | 1. **Alternative 1**: [Description] 41 | - **Rejected because**: [Reason] 42 | 43 | 2. **Alternative 2**: [Description] 44 | - **Rejected because**: [Reason] 45 | 46 | ## Implementation Steps 47 | 48 | [Break down into small, iterable steps. Each step should be independently testable.] 49 | 50 | 1. [Step 1 - smallest possible change] 51 | 2. [Step 2 - build on step 1] 52 | 3. [Step 3 - iterate further] 53 | ... 54 | 55 | ## Files to Modify/Create 56 | 57 | [List specific files. Prefer modification over creation (YAGNI).] 58 | 59 | - **Modify**: `[file path]` - [Why] 60 | - **Create**: `[file path]` - [Justification for new file] 61 | 62 | ## Dependencies 63 | 64 | [List any new dependencies. Justify each one - are they absolutely necessary?] 65 | 66 | - `[dependency name]` - [Concrete justification] 67 | 68 | ## Testing Strategy 69 | 70 | [How will we verify this works? Keep it simple.] 71 | 72 | - Manual test: [Steps] 73 | - Edge cases: [List] 74 | 75 | ## Rollout Plan 76 | 77 | [How will users experience this change?] 78 | 79 | - [ ] Update manifest version 80 | - [ ] Update CHANGELOG.md 81 | - [ ] Document in README if user-facing 82 | - [ ] Test on desktop 83 | - [ ] Test on mobile 84 | 85 | ## Open Questions 86 | 87 | [List any uncertainties or decisions needed before implementation] 88 | 89 | 1. [Question 1] 90 | 2. [Question 2] 91 | 92 | --- 93 | 94 | **Approval Required Before Implementation** 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./stuff/img.png) 2 | 3 | # CUSTOMER SUPPORT 4 | 5 | For the fastest and most reliable support, please use our customer support portal. This way, I can easily track your issue and make sure nothing gets overlooked. You can still reach me directly on Discord if needed. 6 | 7 | - [Discord](https://discord.gg/rxCdQ2K8M5) 8 | - [Support Portal](https://aiocean.atlassian.net/servicedesk/customer/portal/4) 9 | - [Tutorial](https://open-gate.aiocean.io/) 10 | 11 | # What is Open Gate? 12 | 13 | Obsidian Open Gate is a plugin for Obsidian. Allows you to embed any website into Obsidian, providing a seamless browsing and note-taking experience. Whether you're researching, studying, or just browsing the web, Obsidian Open Gate keeps everything you need in one place. 14 | 15 | ## Installation 16 | 17 | Click here to install the plugin: [Direct Install](https://obsidian.md/plugins?id=open-gate) 18 | 19 | ## Features 20 | 21 | - Embed any website in your Obsidian UI as a "Gate" 22 | - Open a Gate on the left, center, or right of the Obsidian UI 23 | - Embed a Gate directly within a note 24 | - Auto generate icon based on the site's favicon 25 | - Embed any site that can not be embedded by iframe 26 | - Support for mobile 27 | - Inject custom CSS to match the look and feel of Obsidian 28 | - Link to Gates from within your notes 29 | 30 | ## Tutorial 31 | 32 | We prepared a very detailed tutorial for you, don't forget to check it out: [Tutorial](https://open-gate.aiocean.io/) 33 | 34 | ## Contributors ✨ 35 | 36 | Thanks goes to these wonderful people. 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
andrewmcgivery
andrewmcgivery

💻
Digital Alchemist
Digital Alchemist

💻
Liam Swayne
Liam Swayne

💻
50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: "Open Gate" 6 | text: "Embed any website in to Obsidian" 7 | tagline: "Anything you need, right where you need it" 8 | actions: 9 | - theme: brand 10 | text: Direct Install 11 | link: https://obsidian.md/plugins?id=open-gate 12 | - theme: alt 13 | text: Tutorial 14 | link: /introduction 15 | 16 | image: 17 | src: /logo.webp 18 | alt: VitePress 19 | 20 | features: 21 | - title: Embed Any Website 22 | icon: 🖼️ 23 | details: Create "Gates" that display websites directly in Obsidian's interface 24 | - title: Flexible Options 25 | icon: 📄 26 | details: Open websites in dedicated views or embed them inline within your notes 27 | - title: Profile Management 28 | icon: 🔗 29 | details: Utilize profile keys to share storage between different gates, similar to Chrome profiles 30 | - title: Customizable Experience 31 | icon: 🎨 32 | details: Inject custom CSS and JavaScript to tailor the appearance and functionality of embedded websites 33 | --- 34 | 35 | 78 | 79 | ## Contributers 80 | 81 | Thanks to all the people who have contributed! 82 | 83 | 84 | 85 | 86 | 87 | 88 | 112 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build obsidian plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | env: 8 | PLUGIN_NAME: obsidian-open-gate 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Use bun 15 | uses: oven-sh/setup-bun@v1 16 | - name: Build 17 | id: build 18 | run: | 19 | bun install 20 | bun run build 21 | mkdir ${{ env.PLUGIN_NAME }} 22 | cp main.js manifest.json ${{ env.PLUGIN_NAME }} 23 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 24 | ls 25 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 26 | - name: Create Release 27 | id: create_release 28 | uses: actions/create-release@v1 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | VERSION: ${{ github.ref }} 32 | with: 33 | tag_name: ${{ github.ref }} 34 | release_name: ${{ github.ref }} 35 | draft: false 36 | prerelease: false 37 | - name: Upload zip file 38 | id: upload-zip 39 | uses: actions/upload-release-asset@v1 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | upload_url: ${{ steps.create_release.outputs.upload_url }} 44 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 45 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 46 | asset_content_type: application/zip 47 | - name: Upload main.js 48 | id: upload-main 49 | uses: actions/upload-release-asset@v1 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | with: 53 | upload_url: ${{ steps.create_release.outputs.upload_url }} 54 | asset_path: ./main.js 55 | asset_name: main.js 56 | asset_content_type: text/javascript 57 | - name: Upload manifest.json 58 | id: upload-manifest 59 | uses: actions/upload-release-asset@v1 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | with: 63 | upload_url: ${{ steps.create_release.outputs.upload_url }} 64 | asset_path: ./manifest.json 65 | asset_name: manifest.json 66 | asset_content_type: application/json 67 | 68 | - name: Upload css 69 | id: upload-css 70 | uses: actions/upload-release-asset@v1 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | with: 74 | upload_url: ${{ steps.create_release.outputs.upload_url }} 75 | asset_path: ./styles.css 76 | asset_name: styles.css 77 | asset_content_type: text/css 78 | -------------------------------------------------------------------------------- /docs/release.md: -------------------------------------------------------------------------------- 1 | # Hướng dẫn Release Phiên Bản Mới 2 | 3 | Tài liệu này hướng dẫn chi tiết quy trình release một phiên bản mới của plugin Open Gate. Vui lòng tuân thủ từng bước để đảm bảo process release diễn ra suôn sẻ. 4 | 5 | ## Các bước release 6 | 7 | ### 1. Cập nhật code và kiểm tra trạng thái 8 | 9 | ```bash 10 | # Fetch tất cả các thay đổi mới nhất từ repository 11 | git fetch --all 12 | 13 | # Kiểm tra trạng thái hiện tại của repository 14 | git status 15 | ``` 16 | 17 | Đảm bảo working tree của bạn sạch sẽ (clean) trước khi bắt đầu quy trình release. 18 | 19 | ### 2. Kiểm tra phiên bản hiện tại 20 | 21 | ```bash 22 | # Xem các tag gần đây để xác định phiên bản hiện tại 23 | git tag -l --sort=-v:refname | head -5 24 | ``` 25 | 26 | ### 3. Xem các commit gần đây 27 | 28 | ```bash 29 | # Xem 5 commit gần nhất để hiểu những thay đổi sẽ được bao gồm trong phiên bản mới 30 | git log -5 --pretty=format:"%h - %s (%cr)" | cat 31 | ``` 32 | 33 | ### 4. Cập nhật phiên bản trong các file 34 | 35 | Cần cập nhật số phiên bản trong 3 file: 36 | 37 | 1. `manifest.json` 38 | 2. `package.json` 39 | 3. `versions.json` 40 | 41 | Ví dụ, để cập nhật từ phiên bản 1.11.8 lên 1.11.9: 42 | 43 | #### Cập nhật manifest.json 44 | ```json 45 | { 46 | "id": "open-gate", 47 | "name": "Open Gate", 48 | "version": "1.11.9", // Cập nhật phiên bản tại đây 49 | "minAppVersion": "0.15.0", 50 | // ... 51 | } 52 | ``` 53 | 54 | #### Cập nhật package.json 55 | ```json 56 | { 57 | "name": "obsidian-open-gate", 58 | "version": "1.11.9", // Cập nhật phiên bản tại đây 59 | // ... 60 | } 61 | ``` 62 | 63 | #### Cập nhật versions.json 64 | ```json 65 | { 66 | // ... các phiên bản trước ... 67 | "1.11.8": "0.15.0", 68 | "1.11.9": "0.15.0" // Thêm phiên bản mới và minAppVersion 69 | } 70 | ``` 71 | 72 | ### 5. Commit các thay đổi 73 | 74 | ```bash 75 | # Add các file đã thay đổi 76 | git add manifest.json package.json versions.json 77 | 78 | # Commit với thông điệp mô tả rõ ràng 79 | git commit -m "chore: release version 1.11.9" 80 | ``` 81 | 82 | ### 6. Tạo tag cho phiên bản mới 83 | 84 | ```bash 85 | # Tạo annotated tag với message 86 | git tag -a "1.11.9" -m "Release version 1.11.9" 87 | 88 | # Kiểm tra lại tag vừa tạo 89 | git tag -l --sort=-v:refname | head -3 90 | ``` 91 | 92 | ### 7. Push commit và tag lên repository 93 | 94 | ```bash 95 | # Push commit lên nhánh chính (thường là main) 96 | git push 97 | 98 | # Push tag lên repository 99 | git push --tags 100 | ``` 101 | 102 | ## Lưu ý 103 | 104 | - Luôn đảm bảo bạn đã test kỹ tất cả các tính năng trước khi thực hiện release 105 | - Tuân thủ quy tắc semantic versioning (SemVer): 106 | - MAJOR: thay đổi không tương thích với API cũ 107 | - MINOR: thêm tính năng mới nhưng vẫn tương thích ngược 108 | - PATCH: sửa lỗi, cải thiện hiệu suất mà không thay đổi API 109 | - Nếu có nhiều thay đổi lớn, cân nhắc cập nhật changelog trong README hoặc tạo file CHANGELOG.md 110 | 111 | ## Quy trình sau khi release 112 | 113 | Sau khi release thành công, bạn nên: 114 | 115 | 1. Kiểm tra repository trên GitHub để xác nhận tag mới đã xuất hiện 116 | 2. Xác minh rằng phiên bản mới được hiển thị trong Obsidian Community Plugins 117 | 3. Nếu có bất kỳ vấn đề nào, phản hồi nhanh chóng và cân nhắc việc release hotfix nếu cần thiết -------------------------------------------------------------------------------- /src/fns/registerCodeBlockProcessor.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from 'obsidian' 2 | import { parse } from 'yaml' 3 | import { createIframe } from './createIframe' 4 | import { createWebviewTag } from './createWebviewTag' 5 | import WebviewTag = Electron.WebviewTag 6 | import { createEmptyGateOption } from './createEmptyGateOption' 7 | import OpenGatePlugin from '../main' 8 | import { normalizeGateOption } from './normalizeGateOption' 9 | import { GateFrameOption } from '../GateOptions' 10 | 11 | type CodeBlockOption = GateFrameOption & { 12 | height?: string | number 13 | } 14 | 15 | function processNewSyntax(plugin: OpenGatePlugin, sourceCode: string): Node { 16 | // we want user follow the yaml format, but sometime, it's easier for user to just type the url in the first line, so we support that 17 | const firstLineUrl = sourceCode.split('\n')[0] 18 | if (firstLineUrl.startsWith('http')) { 19 | sourceCode = sourceCode.replace(firstLineUrl, '').trim() 20 | } 21 | // Replace tabs with spaces at the start of each line, because YAML doesn't support tabs 22 | sourceCode = sourceCode.replace(/^\t+/gm, (match) => ' '.repeat(match.length)) 23 | 24 | if (sourceCode.length === 0) { 25 | return createFrame(createEmptyGateOption(), '800px') 26 | } 27 | 28 | let data: Partial = {} 29 | 30 | if (firstLineUrl.startsWith('http')) { 31 | data.url = firstLineUrl 32 | } 33 | 34 | try { 35 | data = Object.assign(data, parse(sourceCode)) 36 | } catch (error) { 37 | return createErrorMessage(error) 38 | } 39 | 40 | if (typeof data !== 'object' || data === null || Object.keys(data).length === 0) { 41 | return createErrorMessage() 42 | } 43 | 44 | let height = '800px' 45 | if (data.height) { 46 | height = typeof data.height === 'number' ? `${data.height}px` : data.height 47 | delete data.height 48 | } 49 | 50 | let prefill: GateFrameOption | undefined 51 | 52 | if (data.title) { 53 | prefill = plugin.findGateBy('title', data.title) 54 | } else if (data.url) { 55 | prefill = plugin.findGateBy('url', data.url) 56 | } 57 | 58 | if (prefill) { 59 | data = Object.assign(prefill, data) 60 | } 61 | 62 | return createFrame(normalizeGateOption(data), height) 63 | } 64 | 65 | function createErrorMessage(error?: Error): Node { 66 | const div = document.createElement('div') 67 | 68 | const messageText = 'The syntax has been updated. Please use the YAML format.' 69 | const messageTextNode = document.createTextNode(messageText) 70 | div.appendChild(messageTextNode) 71 | 72 | if (error) { 73 | const errorDetailsText = `\nError details: ${error.message}` 74 | const errorDetailsTextNode = document.createTextNode(errorDetailsText) 75 | div.appendChild(errorDetailsTextNode) 76 | } 77 | 78 | const linkText = '\nRead more about YAML here.' 79 | const linkTextNode = document.createTextNode(linkText) 80 | const linkNode = document.createElement('a') 81 | linkNode.href = 'https://yaml.org/spec/1.2/spec.html' 82 | linkNode.textContent = 'YAML Syntax' 83 | div.appendChild(linkTextNode) 84 | div.appendChild(linkNode) 85 | 86 | return div 87 | } 88 | 89 | function createFrame(options: GateFrameOption, height: string): HTMLIFrameElement | WebviewTag { 90 | let frame: HTMLIFrameElement | WebviewTag 91 | 92 | if (Platform.isMobileApp) { 93 | frame = createIframe(options) 94 | } else { 95 | frame = createWebviewTag(options) 96 | } 97 | 98 | frame.style.height = height 99 | 100 | return frame 101 | } 102 | 103 | export function registerCodeBlockProcessor(plugin: OpenGatePlugin) { 104 | plugin.registerMarkdownCodeBlockProcessor('gate', (sourceCode, el, _ctx) => { 105 | el.addClass('open-gate-view') 106 | const frame = processNewSyntax(plugin, sourceCode) 107 | el.appendChild(frame) 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /.specify/templates/spec-template.md: -------------------------------------------------------------------------------- 1 | # Feature Specification: [FEATURE_NAME] 2 | 3 | **Version:** 1.0.0 4 | **Created:** [DATE] 5 | **Last Updated:** [DATE] 6 | **Status:** [DRAFT | APPROVED | IMPLEMENTED] 7 | 8 | ## Overview 9 | 10 | [One-paragraph summary of what this feature does and why it exists.] 11 | 12 | ## User Problem 13 | 14 | [Describe the user problem from first principles. What pain point are we solving?] 15 | 16 | ## Goals 17 | 18 | [What are we trying to achieve? Keep focused - YAGNI applies here.] 19 | 20 | 1. [Primary goal] 21 | 2. [Secondary goal] 22 | ... 23 | 24 | ## Non-Goals 25 | 26 | [Explicitly state what this feature will NOT do. This prevents scope creep.] 27 | 28 | 1. [Non-goal 1] 29 | 2. [Non-goal 2] 30 | ... 31 | 32 | ## User Experience 33 | 34 | ### User Flow 35 | 36 | [Describe how users will interact with this feature. Keep it simple.] 37 | 38 | 1. User does [action] 39 | 2. System responds with [response] 40 | 3. User sees [result] 41 | 42 | ### UI/UX Design 43 | 44 | [If applicable, describe UI changes. Follow Obsidian's design patterns.] 45 | 46 | - **Location**: [Where in Obsidian UI] 47 | - **Appearance**: [Visual description] 48 | - **Interaction**: [How users interact] 49 | 50 | ## Technical Specification 51 | 52 | ### Architecture 53 | 54 | [High-level technical approach. Reference constitution principles.] 55 | 56 | **Principle Alignment:** 57 | - **Iteration-First**: [How this design supports easy changes] 58 | - **KISS**: [Why this is the simplest approach] 59 | - **Separation of Concerns**: [How responsibilities are split] 60 | 61 | ### Data Model 62 | 63 | [If applicable, describe data structures. Keep minimal - YAGNI.] 64 | 65 | ```typescript 66 | // Example structure 67 | interface [InterfaceName] { 68 | [field]: [type]; // Purpose 69 | } 70 | ``` 71 | 72 | ### API Surface 73 | 74 | [What functions/methods will be exposed? Only what's needed now.] 75 | 76 | ```typescript 77 | // Example API 78 | function [functionName]([params]): [returnType] { 79 | // Single responsibility 80 | } 81 | ``` 82 | 83 | ### Obsidian Integration Points 84 | 85 | [Which Obsidian APIs will be used?] 86 | 87 | - `[API name]` - [Purpose] 88 | 89 | ## Requirements 90 | 91 | ### Must Have 92 | 93 | [Core requirements - the minimum viable feature] 94 | 95 | 1. [Requirement 1] 96 | 2. [Requirement 2] 97 | ... 98 | 99 | ### Should Have 100 | 101 | [Nice-to-have features that can be deferred] 102 | 103 | 1. [Requirement 1] 104 | 2. [Requirement 2] 105 | ... 106 | 107 | ### Won't Have (This Version) 108 | 109 | [Explicitly deferred - YAGNI in action] 110 | 111 | 1. [Deferred feature 1] 112 | 2. [Deferred feature 2] 113 | ... 114 | 115 | ## Edge Cases 116 | 117 | [List known edge cases and how we'll handle them] 118 | 119 | 1. **[Edge case]**: [Handling approach] 120 | 2. **[Edge case]**: [Handling approach] 121 | 122 | ## Dependencies 123 | 124 | [New dependencies required. Each must be justified.] 125 | 126 | - `[dependency]` - [Why it's absolutely necessary] 127 | 128 | ## Testing Criteria 129 | 130 | [How do we know this works?] 131 | 132 | ### Desktop Testing 133 | - [ ] [Test case 1] 134 | - [ ] [Test case 2] 135 | 136 | ### Mobile Testing 137 | - [ ] [Test case 1] 138 | - [ ] [Test case 2] 139 | 140 | ## Documentation Needs 141 | 142 | [What documentation must be updated?] 143 | 144 | - [ ] README.md - [Section] 145 | - [ ] docs/ - [Page] 146 | - [ ] Code comments - [Where] 147 | 148 | ## Migration/Breaking Changes 149 | 150 | [Will this break existing functionality? How will users migrate?] 151 | 152 | - **Breaking**: [Yes/No] 153 | - **Migration Path**: [If applicable] 154 | - **Version Bump**: [Major/Minor/Patch] 155 | 156 | ## Open Questions 157 | 158 | [Unresolved questions that need answers] 159 | 160 | 1. [Question 1] 161 | 2. [Question 2] 162 | 163 | ## Approval 164 | 165 | - [ ] Specification reviewed 166 | - [ ] Constitution compliance verified 167 | - [ ] Open questions resolved 168 | - [ ] Ready for implementation planning 169 | 170 | --- 171 | 172 | **Next Step:** Create implementation plan using `/speckit.plan` 173 | -------------------------------------------------------------------------------- /src/SetingTab.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab, Setting, Platform } from 'obsidian' 2 | import OpenGatePlugin from './main' 3 | import { ModalEditGate } from './ModalEditGate' 4 | import { createEmptyGateOption } from './fns/createEmptyGateOption' 5 | import { GateFrameOption } from './GateOptions' 6 | 7 | export class SettingTab extends PluginSettingTab { 8 | plugin: OpenGatePlugin 9 | shouldNotify: boolean 10 | 11 | constructor(app: App, plugin: OpenGatePlugin) { 12 | super(app, plugin) 13 | this.plugin = plugin 14 | } 15 | 16 | async updateGate(gate: GateFrameOption) { 17 | await this.plugin.addGate(gate) 18 | this.display() 19 | } 20 | 21 | display(): void { 22 | this.shouldNotify = false 23 | const { containerEl } = this 24 | containerEl.empty() 25 | 26 | if (Platform.isMobileApp) { 27 | containerEl 28 | .createEl('div', { 29 | text: 'On mobile, some websites may not work. it is a limitation of Obsidian Mobile. Please use Obsidian Desktop instead. Follow me on Twitter to get the latest updates about the issue: ', 30 | cls: 'open-gate-mobile-warning' 31 | }) 32 | .createEl('a', { 33 | text: '@duocdev', 34 | cls: 'open-gate-mobile-link', 35 | href: 'https://twitter.com/duocdev' 36 | }) 37 | } 38 | 39 | containerEl.createEl('button', { text: 'New gate', cls: 'mod-cta' }).addEventListener('click', () => { 40 | new ModalEditGate(this.app, createEmptyGateOption(), this.updateGate.bind(this)).open() 41 | }) 42 | 43 | containerEl.createEl('hr') 44 | 45 | const settingContainerEl = containerEl.createDiv('setting-container') 46 | 47 | for (const gateId in this.plugin.settings.gates) { 48 | const gate = this.plugin.settings.gates[gateId] 49 | const gateEl = settingContainerEl.createEl('div', { 50 | attr: { 51 | 'data-gate-id': gate.id, 52 | class: 'open-gate--setting--gate' 53 | } 54 | }) 55 | 56 | new Setting(gateEl) 57 | .setName(gate.title) 58 | .setDesc(gate.url) 59 | .addButton((button) => { 60 | button.setButtonText('Delete').onClick(async () => { 61 | await this.plugin.removeGate(gateId) 62 | gateEl.remove() 63 | }) 64 | }) 65 | .addButton((button) => { 66 | button.setButtonText('Edit').onClick(() => { 67 | new ModalEditGate(this.app, gate, this.updateGate.bind(this)).open() 68 | }) 69 | }) 70 | } 71 | 72 | containerEl.createEl('h3', { text: 'Help' }) 73 | 74 | containerEl.createEl('small', { 75 | attr: { 76 | style: 'display: block; margin-bottom: 5px' 77 | }, 78 | text: 'When you delete or edit a gate, you need to reload Obsidian to see the changes.' 79 | }) 80 | 81 | containerEl.createEl('small', { 82 | attr: { 83 | style: 'display: block; margin-bottom: 1em;' 84 | }, 85 | text: `To reload Obsidian, you can use the menu "view -> Force reload" or "Reload App" in the command palette.` 86 | }) 87 | 88 | new Setting(containerEl) 89 | .setName('Follow me on Twitter') 90 | .setDesc('@duocdev') 91 | .addButton((button) => { 92 | button.setCta() 93 | button.setButtonText('Join Community').onClick(() => { 94 | window.open('https://community.aiocean.io/') 95 | }) 96 | }) 97 | .addButton((button) => { 98 | button.setCta() 99 | button.setButtonText('Follow for update').onClick(() => { 100 | window.open('https://twitter.com/duocdev') 101 | }) 102 | }) 103 | .addButton((button) => { 104 | button.buttonEl.outerHTML = 105 | "" 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | export default defineConfig({ 4 | title: 'OpenGate', 5 | description: 'Embed any website into Obsidian - The ultimate plugin for seamless web integration in your notes', 6 | lastUpdated: true, 7 | cleanUrls: true, 8 | metaChunk: true, 9 | locales: { 10 | root: { 11 | label: 'English', 12 | lang: 'en' 13 | }, 14 | fr: { 15 | label: 'Vietnamese', 16 | lang: 'vi', 17 | link: '/vi' 18 | } 19 | }, 20 | head: [ 21 | // Basic meta tags 22 | ['meta', { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }], 23 | ['meta', { name: 'robots', content: 'index, follow' }], 24 | ['meta', { name: 'keywords', content: 'obsidian, plugin, web embed, productivity, note-taking, open gate, iframe' }], 25 | ['meta', { name: 'author', content: 'Duoc NV' }], 26 | 27 | // Open Graph meta tags 28 | ['meta', { property: 'og:type', content: 'website' }], 29 | ['meta', { property: 'og:locale', content: 'en' }], 30 | ['meta', { property: 'og:title', content: 'OpenGate - Embed any website into Obsidian' }], 31 | [ 32 | 'meta', 33 | { 34 | property: 'og:description', 35 | content: 'The ultimate Obsidian plugin for seamless web integration in your notes. Embed any website, customize appearance, and boost your productivity.' 36 | } 37 | ], 38 | ['meta', { property: 'og:site_name', content: 'OpenGate' }], 39 | ['meta', { property: 'og:image', content: '/logo.webp' }], 40 | 41 | // Twitter Card meta tags 42 | ['meta', { name: 'twitter:card', content: 'summary_large_image' }], 43 | ['meta', { name: 'twitter:site', content: '@duocdev' }], 44 | ['meta', { name: 'twitter:creator', content: '@duocdev' }], 45 | ['meta', { name: 'twitter:title', content: 'OpenGate - Embed any website into Obsidian' }], 46 | [ 47 | 'meta', 48 | { 49 | name: 'twitter:description', 50 | content: 'The ultimate Obsidian plugin for seamless web integration in your notes. Embed any website, customize appearance, and boost your productivity.' 51 | } 52 | ], 53 | ['meta', { name: 'twitter:image', content: '/logo.webp' }], 54 | 55 | // Favicon 56 | ['link', { rel: 'icon', type: 'image/webp', href: '/logo-small.webp' }] 57 | ], 58 | themeConfig: { 59 | logo: { src: '/logo-small.webp', width: 24, height: 24 }, 60 | nav: [ 61 | { text: 'Install', link: 'https://obsidian.md/plugins?id=open-gate' }, 62 | { text: 'Tutorial', link: '/introduction' }, 63 | { text: 'Community', link: 'https://community.aiocean.io/' } 64 | ], 65 | editLink: { 66 | pattern: 'https://github.com/nguyenvanduocit/obsidian-open-gate/edit/main/docs/:path' 67 | }, 68 | search: { 69 | provider: 'local' 70 | }, 71 | footer: { 72 | message: 'Released under the MIT License.', 73 | copyright: 'Copyright © 2019-present Duoc NV' 74 | }, 75 | sidebar: [ 76 | { 77 | text: 'Introduction', 78 | collapsed: false, 79 | items: [ 80 | { text: 'What is Open Gate?', link: '/introduction' }, 81 | { text: 'Getting Started', link: '/getting-started' } 82 | ] 83 | }, 84 | { 85 | text: 'Tutorial', 86 | collapsed: false, 87 | items: [ 88 | { text: 'Add Gate', link: '/add-gate' }, 89 | { text: 'Quick Switch', link: '/quick-switch' }, 90 | { text: 'Inline Embedded', link: '/inline-embedded' }, 91 | { text: 'Gate Link', link: '/gate-link' }, 92 | { text: 'Custom CSS', link: '/custom-css' }, 93 | { text: 'Custom JavaScript', link: '/custom-javascript' } 94 | ] 95 | }, 96 | { 97 | text: 'Preferences', 98 | collapsed: false, 99 | items: [{ text: 'Gate Options', link: '/gate-options' }] 100 | } 101 | ], 102 | 103 | socialLinks: [{ icon: 'github', link: 'https://github.com/nguyenvanduocit/obsidian-open-gate' }] 104 | } 105 | }) 106 | -------------------------------------------------------------------------------- /.specify/templates/tasks-template.md: -------------------------------------------------------------------------------- 1 | # Tasks: [FEATURE_NAME] 2 | 3 | **Generated:** [DATE] 4 | **Status:** [NOT_STARTED | IN_PROGRESS | COMPLETED] 5 | **Related Spec:** [spec.md link] 6 | **Related Plan:** [plan.md link] 7 | 8 | ## Task Categories 9 | 10 | Tasks are organized by constitutional principle and dependency order. 11 | 12 | ### Setup & Foundation 13 | 14 | Tasks that establish the groundwork. Must be completed first. 15 | 16 | - [ ] **[TASK-001]** [Task description] 17 | - **Principle**: [Which constitution principle applies] 18 | - **Files**: [File paths] 19 | - **Estimated Complexity**: [Low/Medium/High] 20 | - **Dependencies**: None 21 | - **Validation**: [How to verify completion] 22 | 23 | ### Core Implementation 24 | 25 | Main feature implementation tasks. Follow dependency order. 26 | 27 | - [ ] **[TASK-002]** [Task description] 28 | - **Principle**: [Constitution principle] 29 | - **Files**: [File paths] 30 | - **Estimated Complexity**: [Low/Medium/High] 31 | - **Dependencies**: TASK-001 32 | - **Validation**: [How to verify completion] 33 | 34 | - [ ] **[TASK-003]** [Task description] 35 | - **Principle**: [Constitution principle] 36 | - **Files**: [File paths] 37 | - **Estimated Complexity**: [Low/Medium/High] 38 | - **Dependencies**: TASK-002 39 | - **Validation**: [How to verify completion] 40 | 41 | ### Integration 42 | 43 | Obsidian-specific integration tasks. 44 | 45 | - [ ] **[TASK-004]** [Task description] 46 | - **Principle**: [Constitution principle] 47 | - **Files**: [File paths] 48 | - **Estimated Complexity**: [Low/Medium/High] 49 | - **Dependencies**: TASK-003 50 | - **Validation**: [How to verify completion] 51 | 52 | ### Testing & Validation 53 | 54 | Verification tasks. Can run parallel after core implementation. 55 | 56 | - [ ] **[TASK-005]** Test on Obsidian desktop 57 | - **Principle**: First-Principles (verify root solution works) 58 | - **Files**: N/A 59 | - **Estimated Complexity**: Low 60 | - **Dependencies**: TASK-004 61 | - **Validation**: [Specific test cases pass] 62 | 63 | - [ ] **[TASK-006]** Test on Obsidian mobile 64 | - **Principle**: First-Principles 65 | - **Files**: N/A 66 | - **Estimated Complexity**: Low 67 | - **Dependencies**: TASK-004 68 | - **Validation**: [Specific test cases pass] 69 | 70 | ### Documentation 71 | 72 | Documentation tasks. Can run parallel with testing. 73 | 74 | - [ ] **[TASK-007]** Update README.md 75 | - **Principle**: Human Guidance (clear communication) 76 | - **Files**: README.md 77 | - **Estimated Complexity**: Low 78 | - **Dependencies**: TASK-004 79 | - **Validation**: [Documentation is clear and accurate] 80 | 81 | - [ ] **[TASK-008]** Update docs site 82 | - **Principle**: Human Guidance 83 | - **Files**: docs/**/* 84 | - **Estimated Complexity**: Low 85 | - **Dependencies**: TASK-004 86 | - **Validation**: [Tutorial is accurate] 87 | 88 | ### Cleanup & Finalization 89 | 90 | Final tasks before release. 91 | 92 | - [ ] **[TASK-009]** Remove deprecated code (if any) 93 | - **Principle**: Greenfield Development 94 | - **Files**: [Specific files] 95 | - **Estimated Complexity**: Low 96 | - **Dependencies**: TASK-004 97 | - **Validation**: No deprecated code remains 98 | 99 | - [ ] **[TASK-010]** Update CHANGELOG.md 100 | - **Principle**: Human Guidance 101 | - **Files**: CHANGELOG.md 102 | - **Estimated Complexity**: Low 103 | - **Dependencies**: All prior tasks 104 | - **Validation**: Changelog accurately reflects changes 105 | 106 | - [ ] **[TASK-011]** Bump version in manifest.json 107 | - **Principle**: Greenfield Development 108 | - **Files**: manifest.json, package.json 109 | - **Estimated Complexity**: Low 110 | - **Dependencies**: TASK-010 111 | - **Validation**: Version follows semver 112 | 113 | ## Task Execution Rules 114 | 115 | 1. **Dependency Order**: Complete dependencies before dependent tasks 116 | 2. **Iteration First**: Each task should be small enough to complete and test independently 117 | 3. **KISS**: If a task feels complex, break it down further 118 | 4. **YAGNI**: If a task builds something not immediately needed, defer it 119 | 5. **No Workarounds**: If you need a workaround, the task is wrong - redesign it 120 | 121 | ## Progress Tracking 122 | 123 | - **Total Tasks**: [COUNT] 124 | - **Completed**: [COUNT] 125 | - **In Progress**: [COUNT] 126 | - **Blocked**: [COUNT] 127 | 128 | ## Blockers 129 | 130 | [List any tasks that are blocked and why] 131 | 132 | - **[TASK-ID]**: [Blocking reason] 133 | 134 | ## Notes 135 | 136 | [Any implementation notes, discoveries, or context that emerged during execution] 137 | 138 | --- 139 | 140 | **To begin implementation:** `/speckit.implement` 141 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | Obsidian Open Gate is an Obsidian plugin that allows users to embed any website into Obsidian as a "Gate". The plugin creates custom views that can display web content either as sidebar panels or inline within notes. 8 | 9 | ## Build & Development Commands 10 | 11 | **Build:** 12 | ```bash 13 | bun run build 14 | ``` 15 | This runs TypeScript type checking and builds the production bundle via esbuild. 16 | 17 | **Development:** 18 | ```bash 19 | bun run dev 20 | ``` 21 | Runs esbuild in watch mode for development. Note: This plugin does NOT require running a dev server - it builds the plugin bundle that Obsidian loads directly. 22 | 23 | **Format:** 24 | ```bash 25 | bun run format 26 | ``` 27 | Formats code using Prettier. 28 | 29 | **Documentation:** 30 | ```bash 31 | bun run docs:dev # Start VitePress docs dev server 32 | bun run docs:build # Build documentation 33 | bun run docs:preview # Preview built docs 34 | ``` 35 | 36 | ## Architecture 37 | 38 | ### Core Plugin Architecture 39 | 40 | **Entry Point:** `src/main.ts` exports `OpenGatePlugin` class 41 | - Extends Obsidian's `Plugin` class 42 | - Manages plugin lifecycle (`onload`, settings, commands, protocol handlers) 43 | - Settings stored in `PluginSetting` interface containing `uuid` and `gates` (Record) 44 | - Each gate is registered with a unique ID and creates a custom Obsidian view 45 | 46 | ### Key Concepts 47 | 48 | **Gate:** A configured website embed with properties defined in `GateFrameOption`: 49 | - `id`: Unique identifier 50 | - `title`: Display name 51 | - `url`: Target URL 52 | - `icon`: SVG code or Lucide icon ID 53 | - `profileKey`: Electron partition key (similar to Chrome profiles) 54 | - `hasRibbon`: Whether to show in left sidebar 55 | - `position`: Where to open ('left', 'center', 'right') 56 | - `userAgent`: Custom user agent string 57 | - `zoomFactor`: Zoom level (0.5 to 2.0) 58 | - `css`: Custom CSS to inject into the page 59 | - `js`: Custom JavaScript to execute in the page 60 | 61 | **Gate View:** `GateView` class (extends `ItemView`) renders the actual web content 62 | - Uses Electron `` tag on desktop for full browser capabilities 63 | - Falls back to `