Repository: frencojobs/vscode-notion Branch: main Commit: d198d8c9db7a Files: 47 Total size: 58.0 KB Directory structure: gitextract_mc_ej1hd/ ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── .vscodeignore ├── .yarnrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build/ │ ├── node-extension.webpack.config.js │ └── react-webview.webpack.config.js ├── package.json ├── resources/ │ └── styles/ │ ├── notion.css │ ├── prism.css │ ├── reset.css │ └── vscode.css ├── src/ │ ├── CommandManager.ts │ ├── commands/ │ │ ├── BookmarkPage.ts │ │ ├── ClearRecents.ts │ │ ├── CopyLink.ts │ │ ├── OpenPage.ts │ │ ├── RefreshBookmarks.ts │ │ ├── RefreshPage.ts │ │ ├── RefreshRecents.ts │ │ ├── RemoveRecent.ts │ │ ├── UnBookmarkPage.ts │ │ └── index.ts │ ├── extension.ts │ ├── features/ │ │ ├── BookmarksProvider.ts │ │ ├── NotionConfig.ts │ │ ├── NotionItem.ts │ │ ├── NotionPanel.ts │ │ ├── NotionPanelManager.ts │ │ └── RecentsProvider.ts │ ├── sources.ts │ ├── types.d.ts │ ├── utils/ │ │ ├── escapeAttribute.ts │ │ ├── fetchData.ts │ │ ├── getNonce.ts │ │ ├── getTitle.ts │ │ └── parseId.ts │ └── webview/ │ ├── App.tsx │ ├── index.tsx │ └── tsconfig.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "root": true, "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 6, "sourceType": "module" }, "plugins": ["@typescript-eslint"], "rules": { "@typescript-eslint/naming-convention": "warn", "@typescript-eslint/semi": "off", "curly": "warn", "eqeqeq": "warn", "no-throw-literal": "warn", "semi": "off" }, "ignorePatterns": "resources" } ================================================ FILE: .gitignore ================================================ out dist node_modules .vscode-test/ *.vsix resources/webview ================================================ FILE: .prettierrc ================================================ { "plugins": ["./node_modules/prettier-plugin-import-sort"], "singleQuote": true, "semi": false } ================================================ FILE: .vscode/extensions.json ================================================ { // See http://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format "recommendations": ["dbaeumer.vscode-eslint", "eamodio.tsl-problem-matcher"] } ================================================ FILE: .vscode/launch.json ================================================ // A launch configuration that compiles the extension and then opens it inside a new window // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { "version": "0.2.0", "configurations": [ { "name": "Run Extension", "type": "extensionHost", "request": "launch", "args": ["--extensionDevelopmentPath=${workspaceFolder}"], "outFiles": ["${workspaceFolder}/dist/**/*.js"] }, { "name": "Extension Tests", "type": "extensionHost", "request": "launch", "args": [ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" ], "outFiles": ["${workspaceFolder}/out/test/**/*.js"] } ] } ================================================ FILE: .vscode/settings.json ================================================ // Place your settings in this file to overwrite default and user settings. { "files.exclude": { "out": false // set this to true to hide the "out" folder with the compiled JS files }, "search.exclude": { "out": true // set this to false to include "out" folder in search results }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off" } ================================================ FILE: .vscodeignore ================================================ .github/** .vscode/** .vscode-test/** out/** src/** .gitignore .yarnrc vsc-extension-quickstart.md **/tsconfig.json **/.eslintrc.json **/*.map **/*.ts ================================================ FILE: .yarnrc ================================================ --ignore-engines true ================================================ FILE: CHANGELOG.md ================================================ ## 1.1.0 - Support viewing private pages using `accessToken` setting - Allow certain embeddings by the `allowEmbeds` setting - Allow dynamic line height using `lineHeight` setting - Update extension icon and demo.gif - Add sidebar to view recent and bookmarked pages ## 1.0.1 - Parse URLs including usernames - Show error dialog ## 1.0.0 - Initial release - Add preview gif to README.md ## 0.0.4 - Exclude webview dependencies from package, reducing extension size ## 0.0.3 - Fix disappearing input box error ## 0.0.2 - Fix disposed webview left in cache error - Add fallback CSS properties - Use `--vscode-font-family` as monospace font - Align icon center ## 0.0.1 - Initial preview release ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Frenco Jobs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # VSCode Notion Browse Notion pages directly in Visual Studio Code. > Disclaimer: This is an unofficial extension made using an unofficial renderer with the data from an unofficial API. ## Features Here is a list of features that the extension currently supports. - 📄 View Notion pages while you're coding - 🔓 Supports both private + public pages - 🗓️ Browse recently opened pages - 📌 Bookmark important ones for next times Here is the checklist for features I'm planning to add to the extension. - [x] View notion pages - [x] Support embeddings for certain trusted sources - [ ] Native syntax highlight for code snippets - [ ] Authentication for viewing private pages - [ ] Sidebar for all of user's pages _Authentication is not currently available since the unofficial API doesn't support much._ But I'm planning to add it as soon as I get access to the official notion API that is coming very soon. ## Configuration ### API A URL to get the data for Notion pages. By default, it is a hosted version of [Notion Api Worker](https://github.com/splitbee/notion-api-worker) and feel free to host your own and use. ### Access Token The `Authorization` header to be used when getting the data from the API. Will be empty by default and replace it with your own to view private pages of yours. As of now, you can get the token from `token_v2` of Notion website's cookies in your web browser. ### Allow Embeds A boolean value to determine whether to allow iframe embeddings when viewing pages. It will be `false` by default. ### Font Family A comma separated string of font families to use in the pages. Will be `'Helvetica Neue', sans-serif` by default. ### Font Size The font size in pixels to use in the pages. It will be `14` by default. ### Line Height The unitless line height value to use in the pages. By default, `1.5` will be used. --- Here are the available settings with default values. ```json { "VSCodeNotion.api": "https://notion-api.frenco.dev", "VSCodeNotion.accessToken": "", "VSCodeNotion.allowEmbeds": false, "VSCodeNotion.fontFamily": "'Helvetica Neue', sans-serif", "VSCodeNotion.fontSize": 14, "VSCodeNotion.lineHeight": 1.5 } ``` ## Acknowledgement This project won't be possible without [React Notion](https://github.com/splitbee/react-notion) and [Notion API Worker](https://github.com/splitbee/notion-api-worker) libraries by Splitbee. ## License MIT © Frenco Jobs ================================================ FILE: build/node-extension.webpack.config.js ================================================ 'use strict' const path = require('path') /**@type {import('webpack').Configuration}*/ const config = { target: 'node', mode: 'none', entry: './src/extension.ts', output: { path: path.resolve(__dirname, '..', 'dist'), filename: 'extension.js', libraryTarget: 'commonjs2', }, devtool: 'nosources-source-map', externals: { vscode: 'commonjs vscode', }, resolve: { extensions: ['.ts', '.js'], }, module: { rules: [ { test: /\.ts$/, exclude: /node_modules/, use: [ { loader: 'ts-loader', }, ], }, ], }, } module.exports = config ================================================ FILE: build/react-webview.webpack.config.js ================================================ 'use strict' const path = require('path') /**@type {import('webpack').Configuration}*/ const config = { target: 'web', mode: 'production', entry: './src/webview/index.tsx', output: { path: path.resolve(__dirname, '..', 'resources/webview'), filename: 'index.js', libraryTarget: 'umd', }, devtool: false, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], }, module: { rules: [ { test: /\.tsx?$/, exclude: /node_modules/, use: [ { loader: 'ts-loader', }, ], }, ], }, } module.exports = config ================================================ FILE: package.json ================================================ { "name": "vscode-notion", "version": "1.1.0", "icon": "resources/icons/vscode-notion.png", "displayName": "VSCode Notion", "description": "Browse Notion pages directly in Visual Studio Code.", "publisher": "frenco", "author": "Frenco ", "homepage": "https://github.com/frencojobs/vscode-notion#readme", "repository": { "type": "git", "url": "https://github.com/frencojobs/vscode-notion" }, "bugs": { "url": "https://github.com/frencojobs/vscode-notion/issues" }, "engines": { "vscode": "^1.52.0" }, "categories": [ "Other" ], "keywords": [ "notion", "notion.so" ], "main": "./dist/extension.js", "activationEvents": [ "onCommand:vscode-notion.openPage", "onCommand:vscode-notion.refreshPage", "onCommand:vscode-notion.copyLink", "onCommand:vscode-notion.refreshRecents", "onCommand:vscode-notion.clearRecents", "onCommand:vscode-notion.removeRecent", "onCommand:vscode-notion.refreshBookmarks", "onCommand:vscode-notion.bookmarkPage", "onCommand:vscode-notion.unBookmarkPage", "onWebviewPanel:vscode-notion.pageView", "onView:vscode-notion-recents", "onView:vscode-notion-bookmarks" ], "contributes": { "configuration": { "title": "VSCode Notion", "properties": { "VSCodeNotion.api": { "type": "string", "default": "https://notion-api.frenco.dev", "description": "Specifies the API url to get the Notion data from." }, "VSCodeNotion.accessToken": { "type": "string", "default": "", "description": "Specifies the personal access token to use with API." }, "VSCodeNotion.allowEmbeds": { "type": "boolean", "default": false, "description": "Specifies whether to allow embeds or not." }, "VSCodeNotion.fontSize": { "type": "number", "default": 14, "description": "Specifies the font size in pixels." }, "VSCodeNotion.fontFamily": { "type": "string", "default": "'Helvetica Neue', sans-serif", "description": "Specifies the font family to use when rendering the Notion page." }, "VSCodeNotion.lineHeight": { "type": "number", "default": 1.5, "description": "Specifies the unitless line height." } } }, "commands": [ { "command": "vscode-notion.openPage", "category": "VSCode Notion", "title": "Open A Notion Page" }, { "command": "vscode-notion.refreshPage", "category": "VSCode Notion", "title": "Refresh Active Page", "icon": "$(extensions-refresh)" }, { "command": "vscode-notion.copyLink", "category": "VSCode Notion", "title": "Copy Link to Page", "icon": "$(link)" }, { "command": "vscode-notion.refreshRecents", "category": "VSCode Notion", "title": "Refresh Recently Opened Pages", "icon": "$(extensions-refresh)" }, { "command": "vscode-notion.clearRecents", "category": "VSCode Notion", "title": "Clear Recently Opened Pages", "icon": "$(trash)" }, { "command": "vscode-notion.removeRecent", "category": "VSCode Notion", "title": "Remove Item From Recents", "icon": "$(close)" }, { "command": "vscode-notion.refreshBookmarks", "category": "VSCode Notion", "title": "Refresh Bookmarked Pages", "icon": "$(extensions-refresh)" }, { "command": "vscode-notion.bookmarkPage", "category": "VSCode Notion", "title": "Bookmark A Page", "icon": "$(star-empty)" }, { "command": "vscode-notion.unBookmarkPage", "category": "VSCode Notion", "title": "Remove Page from Bookmarks", "icon": "$(star-full)" } ], "viewsContainers": { "activitybar": [ { "id": "vscode-notion-sidebar", "title": "VSCode Notion", "icon": "resources/icons/dark/notion.svg" } ] }, "viewsWelcome": [ { "view": "vscode-notion-recents", "contents": "You have not yet opened a page recently.\n[Open A Page](command:vscode-notion.openPage)" } ], "views": { "vscode-notion-sidebar": [ { "type": "tree", "id": "vscode-notion-recents", "name": "Recents", "contextualTitle": "Recently Opened Pages" }, { "type": "tree", "id": "vscode-notion-bookmarks", "name": "Bookmarks", "contextualTitle": "Bookmarked Pages" } ] }, "menus": { "commandPalette": [ { "command": "vscode-notion.refreshPage", "when": "false" }, { "command": "vscode-notion.copyLink", "when": "false" }, { "command": "vscode-notion.refreshRecents", "when": "false" }, { "command": "vscode-notion.clearRecents", "when": "false" }, { "command": "vscode-notion.removeRecent", "when": "false" }, { "command": "vscode-notion.refreshBookmarks", "when": "false" }, { "command": "vscode-notion.bookmarkPage", "when": "false" }, { "command": "vscode-notion.unBookmarkPage", "when": "false" } ], "editor/title": [ { "command": "vscode-notion.refreshPage", "when": "notionPageFocus", "group": "navigation@0" }, { "command": "vscode-notion.bookmarkPage", "when": "notionPageFocus && !notionPageBookmark", "group": "navigation@1" }, { "command": "vscode-notion.unBookmarkPage", "when": "notionPageFocus && notionPageBookmark", "group": "navigation@1" }, { "command": "vscode-notion.copyLink", "when": "notionPageFocus" } ], "view/title": [ { "command": "vscode-notion.refreshRecents", "when": "view == vscode-notion-recents", "group": "navigation@0" }, { "command": "vscode-notion.clearRecents", "when": "view == vscode-notion-recents", "group": "navigation@1" }, { "command": "vscode-notion.refreshBookmarks", "when": "view == vscode-notion-bookmarks", "group": "navigation@0" } ], "view/item/context": [ { "command": "vscode-notion.removeRecent", "when": "view == vscode-notion-recents", "group": "inline" } ] } }, "scripts": { "vscode:prepublish": "yarn run package:webview && yarn run package", "compile:webview": "webpack --config ./build/react-webview.webpack.config.js", "compile": "webpack --config ./build/node-extension.webpack.config.js", "watch:webview": "webpack --watch --config ./build/react-webview.webpack.config.js", "watch": "webpack --watch --config ./build/node-extension.webpack.config.js", "package:webview": "webpack --mode production --devtool hidden-source-map --config ./build/react-webview.webpack.config.js", "package": "webpack --mode production --devtool hidden-source-map --config ./build/node-extension.webpack.config.js", "test-compile": "tsc -p ./", "test-watch": "tsc -watch -p ./", "pretest": "yarn run test-compile && yarn run lint", "lint": "eslint src --ext ts", "test": "node ./out/test/runTest.js", "format": "prettier --write ." }, "devDependencies": { "@types/glob": "^7.1.3", "@types/mocha": "^8.0.4", "@types/node": "^12.11.7", "@types/node-fetch": "^2.5.7", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@types/vscode": "^1.52.0", "@typescript-eslint/eslint-plugin": "^4.9.0", "@typescript-eslint/parser": "^4.9.0", "eslint": "^7.15.0", "glob": "^7.1.6", "import-sort-style-absolute": "^1.0.1", "mocha": "^8.1.3", "prettier": "^2.2.1", "prettier-plugin-import-sort": "^0.0.6", "ts-loader": "^8.0.11", "typescript": "^4.1.2", "vscode-test": "^1.4.1", "webpack": "^5.10.0", "webpack-cli": "^4.2.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-notion": "^0.9.3" }, "dependencies": { "axios": "^0.21.1" }, "importSort": { ".js, .jsx, .ts, .tsx": { "style": "absolute", "parser": "typescript" } }, "license": "MIT" } ================================================ FILE: resources/styles/notion.css ================================================ body.vscode-light .notion-page-link, body.vscode-light .notion-image-caption, body.vscode-light .notion-bookmark > div:first-child, body.vscode-light .notion-bookmark-link > div, body.vscode-light .notion-th { color: rgb(55, 53, 47); caret-color: rgb(55, 53, 47); } body.vscode-dark .notion-page-link, body.vscode-dark .notion-image-caption, body.vscode-dark .notion-bookmark > div:first-child, body.vscode-dark .notion-bookmark-link > div, body.vscode-dark .notion-th { color: rgba(255, 255, 255, 0.9); caret-color: rgba(255, 255, 255, 0.9); } body.vscode-light .notion-bookmark, body.vscode-light .notion-table, body.vscode-light .notion-th, body.vscode-light .notion-td { border: 1px solid rgba(55, 53, 47, 0.16); } body.vscode-dark .notion-bookmark, body.vscode-dark .notion-table, body.vscode-dark .notion-th, body.vscode-dark .notion-td { border: 1px solid rgba(255, 255, 255, 0.9); } body.vscode-light .notion-gallery { border-top: 1px solid rgba(55, 53, 47, 0.16); } body.vscode-dark .notion-gallery { border-top: 1px solid rgba(255, 255, 255, 0.9); } .notion { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14; line-height: 1.5; font-family: var(--notion-font-family); font-size: var(--notion-font-size); line-height: var(--notion-line-height); } .notion > *, .notion-page > *, .notion-column > * { padding: 3px 0px; } .notion * { box-sizing: border-box; margin-block-start: 0px; margin-block-end: 0px; } .notion-red { color: rgb(224, 62, 62); } .notion-pink { color: rgb(173, 26, 114); } .notion-blue { color: rgb(11, 110, 153); } .notion-purple { color: rgb(105, 64, 165); } .notion-teal { color: rgb(15, 123, 108); } .notion-yellow { color: rgb(223, 171, 1); } .notion-orange { color: rgb(217, 115, 13); } .notion-brown { color: rgb(100, 71, 58); } .notion-gray { color: rgb(155, 154, 151); } .notion-red_background { background-color: rgb(251, 228, 228); } .notion-pink_background { background-color: rgb(244, 223, 235); } .notion-blue_background { background-color: rgb(221, 235, 241); } .notion-purple_background { background-color: rgb(234, 228, 242); } .notion-teal_background { background-color: rgb(221, 237, 234); } .notion-yellow_background { background-color: rgb(251, 243, 219); } .notion-orange_background { background-color: rgb(250, 235, 221); } .notion-brown_background { background-color: rgb(233, 229, 227); } .notion-gray_background { background-color: rgb(235, 236, 237); } .notion-red_background_co { background-color: rgb(251, 228, 228, 0.3); } .notion-pink_background_co { background-color: rgb(244, 223, 235, 0.3); } .notion-blue_background_co { background-color: rgb(221, 235, 241, 0.3); } .notion-purple_background_co { background-color: rgb(234, 228, 242, 0.3); } .notion-teal_background_co { background-color: rgb(221, 237, 234, 0.3); } .notion-yellow_background_co { background-color: rgb(251, 243, 219, 0.3); } .notion-orange_background_co { background-color: rgb(250, 235, 221, 0.3); } .notion-brown_background_co { background-color: rgb(233, 229, 227, 0.3); } .notion-gray_background_co { background-color: rgb(235, 236, 237, 0.3); } .notion b { font-weight: 600; } .notion-title { font-size: 2.5em; font-weight: 700; margin-top: 0.75em; margin-bottom: 0.25em; } .notion-h1, .notion-h2, .notion-h3 { font-weight: 600; line-height: 1.3; padding: 3px 2px; } .notion-h1 { font-size: 1.875em; margin-top: 1.4em; } .notion-h1:first-child { margin-top: 0; } .notion-h2 { font-size: 1.5em; margin-top: 1.1em; } .notion-h3 { font-size: 1.25em; margin-top: 1em; } .notion-emoji { font-family: 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'; } .notion-page-cover { display: block; object-fit: cover; width: 100%; height: 30vh; min-height: 30vh; padding: 0; } .notion-page { padding: 0; margin: 0 auto; max-width: 708px; width: 100%; } @media only screen and (max-width: 730px) { .notion-page { padding: 0 2vw; } } .notion-page-offset { margin-top: 96px; } span.notion-page-icon-cover { height: 78px; width: 78px; font-size: 78px; display: inline-block; line-height: 1.1; margin-left: 0px; } span.notion-page-icon-offset { margin-top: -42px; } img.notion-page-icon-cover { border-radius: 3px; width: 124px; height: 124px; margin: 8px; } img.notion-page-icon-offset { margin-top: -80px; } .notion-full-width { padding: 0 40px; max-width: 100%; } .notion-small-text { font-size: 14px; } .notion-quote { white-space: pre-wrap; word-break: break-word; border-left: 3px solid currentcolor; padding: 0.2em 0.9em; margin: 0; font-size: 1.2em; } .notion-hr { margin: 6px 0px; padding: 0; border-top-width: 1px; border-bottom-width: 0; border-color: rgba(55, 53, 47, 0.09); } .notion-link { color: inherit; word-break: break-word; text-decoration: underline; text-decoration-color: inherit; } .notion-blank { min-height: 1rem; padding: 3px 2px; margin-top: 1px; margin-bottom: 1px; } .notion-page-link { display: flex; /* color: rgb(55, 53, 47); */ text-decoration: none; height: 30px; margin: 1px 0px; transition: background 120ms ease-in 0s; } .notion-page-link:hover { background: rgba(55, 53, 47, 0.08); } .notion-page-icon { line-height: 1.4; margin-right: 4px; margin-left: 2px; } img.notion-page-icon { display: block; object-fit: cover; border-radius: 3px; width: 20px; height: 20px; } .notion-icon { display: block; width: 18px; height: 18px; color: rgba(55, 53, 47, 0.4); } .notion-page-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; line-height: 1.3; border-bottom: 1px solid rgba(55, 53, 47, 0.16); margin: 1px 0px; } .notion-inline-code { color: #eb5757; padding: 0.2em 0.4em; background: rgba(135, 131, 120, 0.15); border-radius: 3px; font-size: 85%; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-family: var(--vscode-editor-font-family); } .notion-list { margin: 0; margin-block-start: 0.6em; margin-block-end: 0.6em; } .notion-list-disc { list-style-type: disc; padding-inline-start: 1.7em; margin-top: 0px; margin-bottom: 0px; } .notion-list-numbered { list-style-type: decimal; padding-inline-start: 1.6em; margin-top: 0px; margin-bottom: 0px; } .notion-list-disc li { padding-left: 0.1em; } .notion-list-numbered li { padding-left: 0.2em; } .notion-list li { padding: 6px 0px; white-space: pre-wrap; } .notion-asset-wrapper { margin: 0.5rem auto 0.5rem; max-width: 100%; } .notion-asset-wrapper > img { max-width: 100%; } .notion-asset-wrapper iframe { border: none; } .notion-text { white-space: pre-wrap; caret-color: rgb(55, 53, 47); padding: 3px 2px; } .notion-block { padding: 3px 2px; } .notion .notion-code { font-size: 85%; } .notion-code { padding: 30px 16px 30px 20px; margin: 4px 0; border-radius: 3px; tab-size: 2; display: block; box-sizing: border-box; overflow-x: scroll; background: rgb(247, 246, 243); font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-family: var(--vscode-editor-font-family); } .notion-column { padding-top: 12px; padding-bottom: 12px; } .notion-column > *:first-child { margin-top: 0; margin-left: 0; margin-right: 0; } .notion-column > *:last-child { margin-left: 0; margin-right: 0; margin-bottom: 0; } .notion-row { display: flex; overflow: hidden; } .notion-bookmark { margin: 4px 0; width: 100%; box-sizing: border-box; text-decoration: none; /* border: 1px solid rgba(55, 53, 47, 0.16); */ border-radius: 3px; display: flex; overflow: hidden; user-select: none; } .notion-bookmark > div:first-child { flex: 4 1 180px; padding: 12px 14px 14px; overflow: hidden; text-align: left; /* color: rgb(55, 53, 47); */ } .notion-bookmark-title { font-size: 14px; line-height: 20px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-height: 24px; margin-bottom: 2px; } .notion-bookmark-description { font-size: 12px; line-height: 16px; opacity: 0.6; height: 32px; overflow: hidden; } .notion-bookmark-link { display: flex; margin-top: 6px; } .notion-bookmark-link > img { width: 16px; height: 16px; min-width: 16px; margin-right: 6px; } .notion-bookmark-link > div { font-size: 12px; line-height: 16px; /* color: rgb(55, 53, 47); */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .notion-bookmark-image { flex: 1 1 180px; position: relative; } .notion-bookmark-image img { object-fit: cover; width: 100%; height: 100%; position: absolute; } .notion-column .notion-bookmark-image { display: none; } @media (max-width: 640px) { .notion-bookmark-image { display: none; } .notion-row { flex-direction: column; } .notion-row > *, .notion-column > * { width: 100% !important; } } .notion-spacer:last-child { display: none; } .notion-image-inset { position: absolute; left: 0; top: 0; right: 0; bottom: 0; width: 100%; height: 100%; border-radius: 1px; } .notion-image-caption { padding: 6px 0px; white-space: pre-wrap; word-break: break-word; /* caret-color: rgb(55, 53, 47); */ font-size: 14px; line-height: 1.4; /* color: rgba(55, 53, 47, 0.6); */ } .notion-callout { padding: 16px 16px 16px 12px; display: inline-flex; width: 100%; border-radius: 3px; border-width: 1px; align-items: center; box-sizing: border-box; margin: 4px 0; } .notion-callout-text { margin-left: 8px; } .notion-toggle { padding: 3px 2px; } .notion-toggle > summary { cursor: pointer; outline: none; } .notion-toggle > div { margin-left: 1.1em; } .notion-table, .notion-th, .notion-td { /* border: 1px solid rgba(55, 53, 47, 0.09); */ border-collapse: collapse; } .notion-table { width: 100%; border-left: none; border-right: none; border-spacing: 0px; white-space: nowrap; } .notion-th, .notion-td { font-weight: normal; padding: 0.25em 0.5em; line-height: 1.5; min-height: 1.5em; text-align: left; font-size: 14px; } .notion-td.notion-bold { font-weight: 500; } .notion-th { /* color: rgba(55, 53, 47, 0.6); */ font-size: 14px; } .notion-td:first-child, .notion-th:first-child { border-left: 0; } .notion-td:last-child, .notion-th:last-child { border-right: 0; } .notion-gallery { display: grid; position: relative; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); grid-auto-rows: 1fr; gap: 16px; /* border-top: 1px solid rgba(55, 53, 47, 0.16); */ padding-top: 16px; padding-bottom: 4px; } .notion-gallery-card { display: block; color: inherit; text-decoration: none; box-shadow: rgba(15, 15, 15, 0.1) 0px 0px 0px 1px, rgba(15, 15, 15, 0.1) 0px 2px 4px; border-radius: 3px; background: white; overflow: hidden; transition: background 100ms ease-out 0s; position: static; height: 100%; } .notion-gallery-content { padding: 8px 10px 6px; font-size: 12px; white-space: nowrap; } .notion-gallery-data.is-first { white-space: nowrap; word-break: break-word; caret-color: rgb(55, 53, 47); font-size: 14px; line-height: 1.5; min-height: 21px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; } .notion-page-header { position: sticky; top: 0; width: 100%; max-width: 100vw; height: 45px; min-height: 45px; display: flex; background: #fff; flex-direction: row; box-sizing: border-box; justify-content: space-between; align-items: center; padding: 0 12px; text-size-adjust: 100%; line-height: 1.5; line-height: 1.2; font-size: 14px; } .notion-nav-breadcrumbs { display: flex; flex-direction: row; align-items: center; height: 100%; flex-grow: 0; min-width: 0; margin-right: 8px; } .notion-nav-breadcrumb { display: inline-flex; flex-direction: row; align-items: center; justify-content: center; white-space: nowrap; color: rgb(55, 53, 47); text-decoration: none; margin: 1px 0px; padding: 4px 6px; border-radius: 3px; transition: background 120ms ease-in 0s; user-select: none; background: transparent; cursor: pointer; } img.notion-nav-icon { width: 18px !important; height: 18px !important; } .notion-nav-icon { font-size: 18px; margin-right: 6px; line-height: 1.1; color: #000; } .notion-nav-breadcrumb:not(.notion-nav-breadcrumb-active):hover { background: rgba(55, 53, 47, 0.08); } .notion-nav-breadcrumb:not(.notion-nav-breadcrumb-active):active { background: rgba(55, 53, 47, 0.16); } .notion-nav-breadcrumb.notion-nav-breadcrumb-active { cursor: default; } .notion-nav-spacer { margin: 0 2px; color: rgba(55, 53, 47, 0.4); } ================================================ FILE: resources/styles/prism.css ================================================ /* PrismJS 1.23.0 https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+abap+abnf+actionscript+ada+agda+al+antlr4+apacheconf+apex+apl+applescript+aql+arduino+arff+asciidoc+aspnet+asm6502+autohotkey+autoit+bash+basic+batch+bbcode+birb+bison+bnf+brainfuck+brightscript+bro+bsl+c+csharp+cpp+cil+clojure+cmake+coffeescript+concurnas+csp+crystal+css-extras+cypher+d+dart+dataweave+dax+dhall+diff+django+dns-zone-file+docker+ebnf+editorconfig+eiffel+ejs+elixir+elm+etlua+erb+erlang+excel-formula+fsharp+factor+firestore-security-rules+flow+fortran+ftl+gml+gcode+gdscript+gedcom+gherkin+git+glsl+go+graphql+groovy+haml+handlebars+haskell+haxe+hcl+hlsl+http+hpkp+hsts+ichigojam+icon+ignore+inform7+ini+io+j+java+javadoc+javadoclike+javastacktrace+jolie+jq+jsdoc+js-extras+json+json5+jsonp+jsstacktrace+js-templates+julia+keyman+kotlin+latex+latte+less+lilypond+liquid+lisp+livescript+llvm+lolcode+lua+makefile+markdown+markup-templating+matlab+mel+mizar+mongodb+monkey+moonscript+n1ql+n4js+nand2tetris-hdl+naniscript+nasm+neon+nginx+nim+nix+nsis+objectivec+ocaml+opencl+oz+parigp+parser+pascal+pascaligo+pcaxis+peoplecode+perl+php+phpdoc+php-extras+plsql+powerquery+powershell+processing+prolog+promql+properties+protobuf+pug+puppet+pure+purebasic+purescript+python+q+qml+qore+r+racket+jsx+tsx+reason+regex+renpy+rest+rip+roboconf+robotframework+ruby+rust+sas+sass+scss+scala+scheme+shell-session+smali+smalltalk+smarty+sml+solidity+solution-file+soy+sparql+splunk-spl+sqf+sql+stan+iecst+stylus+swift+t4-templating+t4-cs+t4-vb+tap+tcl+tt2+textile+toml+turtle+twig+typescript+typoscript+unrealscript+v+vala+vbnet+velocity+verilog+vhdl+vim+visual-basic+warpscript+wasm+wiki+xeora+xml-doc+xojo+xquery+yaml+yang+zig */ /** * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML * Based on https://github.com/chriskempson/tomorrow-theme * @author Rose Pritchard */ code[class*='language-'], pre[class*='language-'] { color: #ccc; background: none; font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; font-family: var(--vscode-editor-font-family); font-size: 1em; text-align: left; white-space: pre; word-spacing: normal; word-break: normal; word-wrap: normal; line-height: 1.5; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } /* Code blocks */ pre[class*='language-'] { padding: 1em; margin: 0.5em 0; overflow: auto; } :not(pre) > code[class*='language-'], pre[class*='language-'] { background: #2d2d2d; } /* Inline code */ :not(pre) > code[class*='language-'] { padding: 0.1em; border-radius: 0.3em; white-space: normal; } .token.comment, .token.block-comment, .token.prolog, .token.doctype, .token.cdata { color: #999; } .token.punctuation { color: #ccc; } .token.tag, .token.attr-name, .token.namespace, .token.deleted { color: #e2777a; } .token.function-name { color: #6196cc; } .token.boolean, .token.number, .token.function { color: #f08d49; } .token.property, .token.class-name, .token.constant, .token.symbol { color: #f8c555; } .token.selector, .token.important, .token.atrule, .token.keyword, .token.builtin { color: #cc99cd; } .token.string, .token.char, .token.attr-value, .token.regex, .token.variable { color: #7ec699; } .token.operator, .token.entity, .token.url { color: #67cdcc; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } .token.entity { cursor: help; } .token.inserted { color: green; } ================================================ FILE: resources/styles/reset.css ================================================ html { box-sizing: border-box; font-size: 13px; } *, *:before, *:after { box-sizing: inherit; } body, h1, h2, h3, h4, h5, h6, p, ol, ul { margin: 0; padding: 0; font-weight: normal; } img { max-width: 100%; height: auto; } ================================================ FILE: resources/styles/vscode.css ================================================ :root { --container-paddding: 0px; --input-padding-vertical: 6px; --input-padding-horizontal: 4px; --input-margin-vertical: 4px; --input-margin-horizontal: 0; } body { padding: 0 var(--container-paddding) 20px var(--container-paddding); color: var(--vscode-foreground); font-size: var(--vscode-font-size); font-weight: var(--vscode-font-weight); font-family: var(--vscode-font-family); background-color: var(--vscode-editor-background); } ol, ul { padding-left: var(--container-paddding); } body > *, form > * { margin-block-start: var(--input-margin-vertical); margin-block-end: var(--input-margin-vertical); } *:focus { outline-color: var(--vscode-focusBorder) !important; } a { color: var(--vscode-textLink-foreground); } a:hover, a:active { color: var(--vscode-textLink-activeForeground); } code { font-size: var(--vscode-editor-font-size); font-family: var(--vscode-editor-font-family); } button { border: none; padding: var(--input-padding-vertical) var(--input-padding-horizontal); width: 100%; text-align: center; outline: 1px solid transparent; outline-offset: 2px !important; color: var(--vscode-button-foreground); background: var(--vscode-button-background); } button:hover { cursor: pointer; background: var(--vscode-button-hoverBackground); } button:focus { outline-color: var(--vscode-focusBorder); } button.secondary { color: var(--vscode-button-secondaryForeground); background: var(--vscode-button-secondaryBackground); } button.secondary:hover { background: var(--vscode-button-secondaryHoverBackground); } input:not([type='checkbox']), textarea { display: block; width: 100%; border: none; font-family: var(--vscode-font-family); padding: var(--input-padding-vertical) var(--input-padding-horizontal); color: var(--vscode-input-foreground); outline-color: var(--vscode-input-border); background-color: var(--vscode-input-background); } input::placeholder, textarea::placeholder { color: var(--vscode-input-placeholderForeground); } ================================================ FILE: src/CommandManager.ts ================================================ import * as vscode from 'vscode' export interface Command { readonly id: string execute(...args: any[]): void } export class CommandManager { private readonly commands = new Map() public dispose() { for (const registration of this.commands.values()) { registration.dispose() } this.commands.clear() } public register(command: T): T { this.registerCommand(command.id, command.execute, command) return command } private registerCommand( id: string, impl: (...args: any[]) => void, thisArg?: any ) { if (this.commands.has(id)) { return } this.commands.set(id, vscode.commands.registerCommand(id, impl, thisArg)) } } ================================================ FILE: src/commands/BookmarkPage.ts ================================================ import { Command } from '../CommandManager' import NotionPanelManager from '../features/NotionPanelManager' export class BookmarkPage implements Command { public readonly id = 'vscode-notion.bookmarkPage' constructor(private readonly manager: NotionPanelManager) {} execute() { const activeViews = Array.from(this.manager.cache.values()).filter( (x) => x.isActive ) if (activeViews.length > 0) { activeViews[0].bookmark() } } } ================================================ FILE: src/commands/ClearRecents.ts ================================================ import { Command } from '../CommandManager' import NotionPanelManager from '../features/NotionPanelManager' export class ClearRecents implements Command { public readonly id = 'vscode-notion.clearRecents' constructor(private readonly manager: NotionPanelManager) {} execute() { this.manager.clearRecents() } } ================================================ FILE: src/commands/CopyLink.ts ================================================ import * as vscode from 'vscode' import { Command } from '../CommandManager' import NotionPanelManager from '../features/NotionPanelManager' export class CopyLink implements Command { public readonly id = 'vscode-notion.copyLink' constructor(private readonly manager: NotionPanelManager) {} execute() { const activeViews = Array.from(this.manager.cache.values()).filter( (x) => x.isActive ) if (activeViews.length > 0) { vscode.env.clipboard.writeText(`https://notion.so/${activeViews[0].id}`) } } } ================================================ FILE: src/commands/OpenPage.ts ================================================ import * as vscode from 'vscode' import { Command } from '../CommandManager' import NotionPanelManager from '../features/NotionPanelManager' import parseId from '../utils/parseId' export class OpenPage implements Command { public readonly id = 'vscode-notion.openPage' public constructor(private readonly manager: NotionPanelManager) {} public async execute(urlOrId?: string) { let input = urlOrId ?? '' if (!urlOrId) { input = (await vscode.window.showInputBox({ prompt: 'Enter a full URL or just ID of the page.', })) ?? '' } if (!!input.trim()) { const id = parseId(input) this.manager.createOrShow(id) } } } ================================================ FILE: src/commands/RefreshBookmarks.ts ================================================ import { Command } from '../CommandManager' import NotionPanelManager from '../features/NotionPanelManager' export class RefreshBookmarks implements Command { public readonly id = 'vscode-notion.reloadBookmarks' constructor(private readonly manager: NotionPanelManager) {} execute() { this.manager.refreshBookmarks() } } ================================================ FILE: src/commands/RefreshPage.ts ================================================ import { Command } from '../CommandManager' import NotionPanelManager from '../features/NotionPanelManager' export class RefreshPage implements Command { public readonly id = 'vscode-notion.refreshPage' constructor(private readonly manager: NotionPanelManager) {} execute() { Array.from(this.manager.cache.values()) .filter((x) => x.isActive) .forEach((x) => x.refresh()) } } ================================================ FILE: src/commands/RefreshRecents.ts ================================================ import { Command } from '../CommandManager' import NotionPanelManager from '../features/NotionPanelManager' export class RefreshRecents implements Command { public readonly id = 'vscode-notion.refreshRecents' constructor(private readonly manager: NotionPanelManager) {} execute() { this.manager.refreshRecents() } } ================================================ FILE: src/commands/RemoveRecent.ts ================================================ import * as vscode from 'vscode' import { Command } from '../CommandManager' import NotionPanelManager from '../features/NotionPanelManager' export class RemoveRecent implements Command { public readonly id = 'vscode-notion.removeRecent' constructor(private readonly manager: NotionPanelManager) {} execute(item: vscode.TreeItem) { this.manager.removeRecent(item.id!) } } ================================================ FILE: src/commands/UnBookmarkPage.ts ================================================ import { Command } from '../CommandManager' import NotionPanelManager from '../features/NotionPanelManager' export class UnBookmarkPage implements Command { public readonly id = 'vscode-notion.unBookmarkPage' constructor(private readonly manager: NotionPanelManager) {} execute() { const activeViews = Array.from(this.manager.cache.values()).filter( (x) => x.isActive ) if (activeViews.length > 0) { activeViews[0].unBookmark() } } } ================================================ FILE: src/commands/index.ts ================================================ export { OpenPage } from './OpenPage' export { RefreshPage } from './RefreshPage' export { CopyLink } from './CopyLink' export { ClearRecents } from './ClearRecents' export { RefreshRecents } from './RefreshRecents' export { RemoveRecent } from './RemoveRecent' export { RefreshBookmarks } from './RefreshBookmarks' export { BookmarkPage } from './BookmarkPage' export { UnBookmarkPage } from './UnBookmarkPage' ================================================ FILE: src/extension.ts ================================================ import * as vscode from 'vscode' import * as commands from './commands' import { CommandManager } from './CommandManager' import BookmarksProvider from './features/BookmarksProvider' import NotionPanelManager from './features/NotionPanelManager' import RecentsProvider from './features/RecentsProvider' export async function activate(context: vscode.ExtensionContext) { const manager = new NotionPanelManager(context) const recents = new RecentsProvider(manager) const bookmarks = new BookmarksProvider(manager) context.subscriptions.push(registerCommands(manager)) context.subscriptions.push( vscode.window.registerWebviewPanelSerializer( 'vscode-notion.pageView', manager ) ) context.subscriptions.push( vscode.window.registerTreeDataProvider('vscode-notion-recents', recents) ) context.subscriptions.push( vscode.window.registerTreeDataProvider('vscode-notion-bookmarks', bookmarks) ) context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(() => manager.reloadConfig()) ) } function registerCommands(manager: NotionPanelManager): vscode.Disposable { const commandManager = new CommandManager() commandManager.register(new commands.OpenPage(manager)) commandManager.register(new commands.RefreshPage(manager)) commandManager.register(new commands.CopyLink(manager)) commandManager.register(new commands.ClearRecents(manager)) commandManager.register(new commands.RefreshRecents(manager)) commandManager.register(new commands.RemoveRecent(manager)) commandManager.register(new commands.BookmarkPage(manager)) commandManager.register(new commands.UnBookmarkPage(manager)) return commandManager } ================================================ FILE: src/features/BookmarksProvider.ts ================================================ import * as vscode from 'vscode' import NotionPageItem from './NotionItem' import NotionPanelManager from './NotionPanelManager' export default class BookmarksProvider implements vscode.TreeDataProvider { // --- yep, copied from StackOverflow private _onDidChangeTreeData: vscode.EventEmitter< NotionPageItem | undefined > = new vscode.EventEmitter() readonly onDidChangeTreeData: vscode.Event = this ._onDidChangeTreeData.event private refresh() { this._onDidChangeTreeData.fire(undefined) } /// --- constructor(private readonly manager: NotionPanelManager) { this.manager.onDidBookmarksUpdated = () => { this.refresh() } } getTreeItem(element: NotionPageItem): vscode.TreeItem { return element } getChildren(): Thenable> { return Promise.resolve( Object.entries(this.manager.bookmarks).map( ([id, title]) => new NotionPageItem(title, id) ) ) } } ================================================ FILE: src/features/NotionConfig.ts ================================================ import * as vscode from 'vscode' export default class NotionConfig { private static readonly key = 'VSCodeNotion' public readonly api: string public readonly accessToken: string public readonly allowEmbeds: boolean public readonly fontFamily: string public readonly fontSize: number public readonly lineHeight: number constructor() { const config = vscode.workspace.getConfiguration(NotionConfig.key) this.api = config.get('api')! this.accessToken = config.get('accessToken')! this.allowEmbeds = config.get('allowEmbeds')! this.fontSize = config.get('fontSize')! this.fontFamily = config.get('fontFamily')! this.lineHeight = config.get('lineHeight')! } } ================================================ FILE: src/features/NotionItem.ts ================================================ import * as vscode from 'vscode' export default class NotionPageItem extends vscode.TreeItem { iconPath = new vscode.ThemeIcon('symbol-file') command: vscode.Command = { title: 'Open Page', command: 'vscode-notion.openPage', arguments: [this.id], } constructor(public readonly label: string, public readonly id: string) { super(label) } } ================================================ FILE: src/features/NotionPanel.ts ================================================ import * as vscode from 'vscode' import NotionPanelManager from './NotionPanelManager' import fetchData from '../utils/fetchData' import getTitle from '../utils/getTitle' export default class NotionPanel { public static readonly viewType = 'vscode-notion.pageView' public static readonly viewActiveContextKey = 'notionPageFocus' public static readonly viewBookmarkContextKey = 'notionPageBookmark' private disposables: Array = [] constructor( public readonly id: string, private readonly panel: vscode.WebviewPanel, private readonly manager: NotionPanelManager, private data: NotionData ) { this.update() this.panel.onDidDispose(() => this.dispose(), null, this.disposables) this.panel.onDidChangeViewState( (_) => { if (this.panel.visible) { this.update() } }, null, this.disposables ) this.panel.webview.onDidReceiveMessage( async (message: Message) => { switch (message.command) { case 'open': await manager.createOrShow(message.text) break } }, null, this.disposables ) } public get isActive() { return this.panel.active } public revive(column: vscode.ViewColumn | undefined) { this.panel.reveal(column) } public async refresh() { this.data = await vscode.window.withProgress( { title: 'VSCode Notion', location: vscode.ProgressLocation.Notification, }, async (progress, _) => { progress.report({ message: 'Refreshing...' }) return fetchData({ id: this.id, api: this.manager.config.api, accessToken: this.manager.config.accessToken, }) } ) this.update() } public bookmark() { this.manager.updateBookmarkEntry({ id: this.id, title: getTitle(this.data), }) this.setBookmarkContext(true) } public unBookmark() { this.manager.removeBookmarkEntry(this.id) this.update() } private dispose() { this.setViewActiveContext(false) this.panel.dispose() this.manager.dispose(this.id) while (this.disposables.length) { const x = this.disposables.pop() if (x) { x.dispose() } } } private update() { const title = getTitle(this.data) this.panel.title = title this.setViewActiveContext(this.panel.active) if (Object.keys(this.manager.bookmarks).includes(this.id)) { this.manager.updateBookmarkEntry({ id: this.id, title, }) this.setBookmarkContext(true) } else { this.setBookmarkContext(false) } this.manager.updateRecentEntry({ id: this.id, title, }) this.panel.webview.html = this.manager.getHTML(this.panel.webview, { id: this.id, data: this.data, }) } private setViewActiveContext(value: boolean) { vscode.commands.executeCommand( 'setContext', NotionPanel.viewActiveContextKey, value ) } private setBookmarkContext(value: boolean) { vscode.commands.executeCommand( 'setContext', NotionPanel.viewBookmarkContextKey, value ) } } ================================================ FILE: src/features/NotionPanelManager.ts ================================================ import * as vscode from 'vscode' import NotionConfig from './NotionConfig' import NotionPanel from './NotionPanel' import escapeAttribute from '../utils/escapeAttribute' import fetchData from '../utils/fetchData' import getNonce from '../utils/getNonce' import sources from '../sources' export default class NotionPanelManager implements vscode.WebviewPanelSerializer { private readonly recentsKey = 'recents' private readonly bookmarksKey = 'bookmarks' private readonly uri: vscode.Uri public onDidRecentsUpdated: () => void = () => {} public onDidBookmarksUpdated: () => void = () => {} public config = new NotionConfig() public cache = new Map() constructor(private readonly context: vscode.ExtensionContext) { this.uri = this.context.extensionUri } public async createOrShow(id: string) { const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined try { if (this.cache.has(id)) { this.cache.get(id)?.revive(column) } else { const data = await vscode.window.withProgress( { title: 'VSCode Notion', location: vscode.ProgressLocation.Notification, }, async (progress, _) => { progress.report({ message: 'Loading...' }) return fetchData({ id, api: this.config.api, accessToken: this.config.accessToken, }) } ) const panel = vscode.window.createWebviewPanel( NotionPanel.viewType, 'VSCode Notion', column || vscode.ViewColumn.One, { enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [vscode.Uri.joinPath(this.uri, 'resources')], } ) panel.iconPath = this.iconPath this.cache.set(id, new NotionPanel(id, panel, this, data)) } } catch (e) { if (e instanceof Error) { vscode.window.showErrorMessage(e.message) } else { vscode.window.showErrorMessage(e) } } } public async deserializeWebviewPanel( webviewPanel: vscode.WebviewPanel, state: { id: string; data: NotionData } ) { this.cache.set( state.id, new NotionPanel(state.id, webviewPanel, this, state.data) ) } public dispose(id: string) { this.cache.delete(id) } public reloadConfig() { this.config = new NotionConfig() } public get recents() { return ( this.context.globalState.get>(this.recentsKey) ?? {} ) } public get bookmarks() { return ( this.context.globalState.get>(this.bookmarksKey) ?? {} ) } public async removeRecent(id: string) { const recents = this.context.globalState.get>( this.recentsKey ) if (recents) { delete recents[id] } this.onDidRecentsUpdated() } public async removeBookmarkEntry(id: string) { const bookmarks = this.context.globalState.get>( this.bookmarksKey ) if (bookmarks) { delete bookmarks[id] } this.onDidBookmarksUpdated() } public async updateRecentEntry({ id, title }: { id: string; title: string }) { const recents = this.context.globalState.get>( this.recentsKey ) await this.context.globalState.update(this.recentsKey, { ...(!!recents ? recents : {}), [id]: title, }) this.onDidRecentsUpdated() } public async updateBookmarkEntry({ id, title, }: { id: string title: string }) { const bookmarks = this.context.globalState.get>( this.bookmarksKey ) await this.context.globalState.update(this.bookmarksKey, { ...(!!bookmarks ? bookmarks : {}), [id]: title, }) this.onDidBookmarksUpdated() } public refreshRecents() { this.onDidRecentsUpdated() } public refreshBookmarks() { this.onDidBookmarksUpdated() } public clearRecents() { this.context.globalState.update(this.recentsKey, {}) this.onDidRecentsUpdated() } private getSettingsOverrideStyles(): string { return [ this.config.fontFamily ? `--notion-font-family: ${this.config.fontFamily};` : '', !isNaN(this.config.fontSize) ? `--notion-font-size: ${this.config.fontSize}px;` : '', !isNaN(this.config.lineHeight) ? `--notion-line-height: ${this.config.lineHeight};` : '', ].join('') } private getMetaTags(webview: vscode.Webview, nonce: string): string { const trustedSources = this.config.allowEmbeds ? sources.join(' ') : "'none'" return ` ` } private getStyles(webview: vscode.Webview, uri: vscode.Uri): string { return ['reset.css', 'vscode.css', 'notion.css', 'prism.css'] .map((x) => webview.asWebviewUri(vscode.Uri.joinPath(uri, 'resources', 'styles', x)) ) .map((x) => ``) .join('') } private getScripts( webview: vscode.Webview, uri: vscode.Uri, nonce: string, state: NotionState ): string { const reactWebviewUri = webview.asWebviewUri( vscode.Uri.joinPath(uri, 'resources', 'webview', 'index.js') ) return ` ` } public get iconPath() { const root = vscode.Uri.joinPath(this.uri, 'resources', 'icons') return { light: vscode.Uri.joinPath(root, 'light', 'notion.svg'), dark: vscode.Uri.joinPath(root, 'dark', 'notion.svg'), } } public getHTML(webview: vscode.Webview, state: NotionState) { const nonce = getNonce() return ` ${this.getMetaTags(webview, nonce)} ${this.getStyles(webview, this.uri)}
${this.getScripts(webview, this.uri, nonce, state)} ` } } ================================================ FILE: src/features/RecentsProvider.ts ================================================ import * as vscode from 'vscode' import NotionPageItem from './NotionItem' import NotionPanelManager from './NotionPanelManager' export default class RecentsProvider implements vscode.TreeDataProvider { // --- yep, copied from StackOverflow private _onDidChangeTreeData: vscode.EventEmitter< NotionPageItem | undefined > = new vscode.EventEmitter() readonly onDidChangeTreeData: vscode.Event = this ._onDidChangeTreeData.event private refresh() { this._onDidChangeTreeData.fire(undefined) } /// --- constructor(private readonly manager: NotionPanelManager) { this.manager.onDidRecentsUpdated = () => { this.refresh() } } getTreeItem(element: NotionPageItem): vscode.TreeItem { return element } getChildren(): Thenable> { return Promise.resolve( Object.entries(this.manager.recents) .reverse() .map(([id, title]) => new NotionPageItem(title, id)) ) } } ================================================ FILE: src/sources.ts ================================================ export default ['https://www.youtube.com', 'https://www.openstreetmap.org'] ================================================ FILE: src/types.d.ts ================================================ type NotionData = Record type NotionState = { id: string data: NotionData } type Message = { command: string text: string } ================================================ FILE: src/utils/escapeAttribute.ts ================================================ export default function escapeAttribute(value: string): string { return value.replace(/"/g, '"') } ================================================ FILE: src/utils/fetchData.ts ================================================ import axios from 'axios' export default async function fetchData({ api, id, accessToken, }: { api: string id: string accessToken: string }): Promise { if (!api.trim()) { throw new Error("API URL can't be empty.") } const res = await axios.get(`${api}/v1/page/${id}`, { headers: !!accessToken ? { // eslint-disable-next-line @typescript-eslint/naming-convention Authorization: `Bearer ${accessToken}`, } : {}, }) if (Object.keys(res.data).length < 1) { throw new Error("Couldn't load the data from API.") } else { return res.data } } ================================================ FILE: src/utils/getNonce.ts ================================================ export default function getNonce() { let text = '' const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' for (let i = 0; i < 32; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)) } return text } ================================================ FILE: src/utils/getTitle.ts ================================================ export default function getTitle(data: NotionData) { const firstKey = Object.keys(data)[0] const firstBlock = (data[firstKey] as { value: Record }).value as { properties: { title: Array> } } if (firstBlock?.properties?.title?.[0]) { return firstBlock.properties.title[0] .filter((x) => typeof x === 'string') .reduce((a, b) => a + b, '') } else { return firstKey.substr(0, 5) } } ================================================ FILE: src/utils/parseId.ts ================================================ export default function parseId(urlOrId: string): string { const pattern = /(?:https?:\/\/)?(?:www\.)?notion\.so\/([\w\.-]*\/?)*/ if (pattern.test(urlOrId)) { return urlOrId.match(pattern)?.[1]!.replace('/', '')! } return urlOrId } ================================================ FILE: src/webview/App.tsx ================================================ import { BlockMapType, NotionRenderer } from 'react-notion' import React from 'react' declare global { interface Window { vscode: { getState: () => any setState: (state: any) => void postMessage: (message: any) => void } } } // eslint-disable-next-line @typescript-eslint/naming-convention const App: React.FC = () => { return ( ( window.vscode.postMessage({ command: 'open', text: blockValue.id, }) } > {renderComponent()} ), }} /> ) } export default App ================================================ FILE: src/webview/index.tsx ================================================ import * as React from 'react' import * as ReactDOM from 'react-dom' import App from './App' ReactDOM.render(, document.getElementById('root')) ================================================ FILE: src/webview/tsconfig.json ================================================ { "compilerOptions": { "module": "esnext", "moduleResolution": "node", "target": "es6", "outDir": "webview-compiled", "lib": ["es6", "dom"], "jsx": "react", "sourceMap": true, "rootDir": "..", "noUnusedLocals": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "resolveJsonModule": true }, "exclude": ["node_modules"] } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "outDir": "out", "lib": ["es6"], "sourceMap": true, "rootDir": "src", "strict": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noUnusedParameters": true }, "exclude": ["node_modules", ".vscode-test", "**/webview/**"] }