├── .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('