├── .gitattributes ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGES.md ├── LICENSE ├── README.md ├── icon.png ├── media ├── Preview.svg ├── PreviewOnRightPane_16x.svg ├── PreviewOnRightPane_16x_dark.svg ├── Preview_inverse.svg ├── ViewSource.svg ├── ViewSource_inverse.svg └── basestyle.css ├── package-lock.json ├── package.json ├── package.nls.json ├── preview-src ├── activeLineMarker.ts ├── csp.ts ├── events.ts ├── index.ts ├── messaging.ts ├── pre.ts ├── scroll-sync.ts ├── settings.ts ├── strings.ts └── tsconfig.json ├── src ├── commandManager.ts ├── commands │ ├── index.ts │ ├── moveCursorToPosition.ts │ ├── openBaseStyle.ts │ ├── openDocumentLink.ts │ ├── refreshPreview.ts │ ├── showInBrowser.ts │ ├── showPreview.ts │ ├── showPreviewSecuritySelector.ts │ ├── showSource.ts │ └── toggleLock.ts ├── extension.ts ├── features │ ├── preview.ts │ ├── previewConfig.ts │ ├── previewContentProvider.ts │ └── previewManager.ts ├── logger.ts ├── security.ts ├── test │ ├── inMemoryDocument.ts │ └── index.ts └── util │ ├── dispose.ts │ ├── file.ts │ ├── lazy.ts │ └── topmostLineMonitor.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | *.vsix 4 | .vscode/symbols.json 5 | npm-debug.log 6 | .vscode-test/ 7 | /media/index.js 8 | /media/pre.js 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "eg2.tslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "compile" 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "runtimeExecutable": "${execPath}", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/test/**/*.js" 32 | ], 33 | "preLaunchTask": "npm: watch" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "compile", 8 | "type": "npm", 9 | "script": "compile", 10 | "problemMatcher": "$tsc", 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | typings/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | .gitignore 8 | tsconfig.json 9 | vsc-extension-quickstart.md 10 | *.vsix 11 | tslint.json 12 | webpack.config.js 13 | .git/** 14 | .vscode-test/** 15 | preview-src/** 16 | .gitattributes 17 | package-lock.json 18 | node_modules/@types/** 19 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 0.2.5 4 | 5 | - Initial changes (George Oliveira) 6 | - Update version 7 | - Fixed content doubling in the preview 8 | - Added "basestyles.css" to define a default preview style 9 | - Added "Edit Preview Style" command to easily edit "basestyles.css" 10 | - Added "Show in Browser" command 11 | 12 | ## 0.2.4 13 | 14 | - Update version 15 | - Clean up and reduce published package size 16 | 17 | ## 0.2.3 18 | 19 | - Update version 20 | - Fix publisher and icon 21 | 22 | ## 0.2.2 23 | 24 | - Update version 25 | - Update icon and readme 26 | 27 | ## 0.2.1 28 | 29 | - Update package version 30 | - Clean up and make tests run 31 | 32 | ## 0.2.0 33 | 34 | - Upgrade package version 35 | - Upgrade nls 36 | - Clean up 37 | - Remove old markdown files 38 | - Fix auto scrolling 39 | - Upgrade to vscode-markdown impl of preview 40 | - Update extension 41 | - Update general code and fix tsc errors 42 | - Fix inline image regex error 43 | - Disable custom css override 44 | - Update configurations 45 | - Upgrade packages 46 | - Fix inline data:image/pnd;base64 images Closes #3 47 | - Fix tslint issues 48 | - Fix `when` state for commands. Avoid key cord issue on MacOS for terminal. (Hooky) 49 | 50 | ## 0.1.1 51 | 52 | - Update readme for MacOS keybindings (Thomas Townsend) 53 | - Bring command text inline with markdown (Thomas Townsend) 54 | 55 | ## 0.1.0 56 | 57 | - Hide tests for now (Thomas Townsend) 58 | - Update code to reflect how markdown is handled (Thomas Townsend) 59 | - Update packages and scripts (Thomas Townsend) 60 | - Update registered commands (Thomas Townsend) 61 | - Add media icons (Thomas Townsend) 62 | - Update config files (Thomas Haakon Townsend) 63 | - Revert "Use registerTextEditorCommand" (Thomas Haakon Townsend) 64 | 65 | This reverts commit 45720cc060123ac96fd16fd25d898401a8dcc3c9. 66 | - Fix preview to side command (Thomas Haakon Townsend) 67 | - fix idMap key iteration bug (Thomas Haakon Townsend) 68 | - dispose of events when deactivating (Thomas Haakon Townsend) 69 | - Don't extend map just have property in IDMap (Thomas Haakon Townsend) 70 | - Optimise HtmlDoc fetching and force toggle if active doc is html doc (Thomas Haakon Townsend) 71 | - Implement idMap (Thomas Haakon Townsend) 72 | - Add IDMap (Thomas Haakon Townsend) 73 | - Add getter for html Uri (Thomas Haakon Townsend) 74 | - Import fileUrl (Thomas Haakon Townsend) 75 | - Install typings and node-uuid (Thomas Haakon Townsend) 76 | - statically type registerCommand callback (Thomas Haakon Townsend) 77 | - Fix vscode version (Thomas Haakon Townsend) 78 | - Use registerTextEditorCommand (Thomas Haakon Townsend) 79 | - Update packages (Thomas Haakon Townsend) 80 | - Move activate function into a viewManager class (Thomas Haakon Townsend) 81 | - Move document classes to separate file (Thomas Haakon Townsend) 82 | - Add class to hold document previewer (Thomas Haakon Townsend) 83 | - Version Bump (Thomas Townsend) 84 | - Update README (Thomas Townsend) 85 | - Enable previewing of Jade documents (Thomas Townsend) 86 | - Version Bump (Thomas Haakon Townsend) 87 | - Update README (Thomas Haakon Townsend) 88 | - Update package manifest (Thomas Haakon Townsend) 89 | - Run on content change rather than save (Thomas Haakon Townsend) 90 | - Update README (Thomas Haakon Townsend) 91 | - Working extension (Thomas Haakon Townsend) 92 | - Initial commit (Thomas Townsend) 93 | - Initial Commit (Thomas Haakon Townsend) 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Thomas Townsend 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | An extension to preview HTML files while editing them in VSCode and open them in the default browser 4 | 5 | ## Keybindings 6 | 7 | * Toggle Preview - `ctrl+shift+v` or `cmd+shift+v` 8 | * Open Preview to the Side - `ctrl+k v` or `cmd+k v` 9 | * Show in Browser - `ctrl+k w` or `cmd+k w` 10 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/george-alisson/html-preview-vscode/cc9574eef89077d3bd83f32b66156974067797bb/icon.png -------------------------------------------------------------------------------- /media/Preview.svg: -------------------------------------------------------------------------------- 1 | SwitchToPreview_16x -------------------------------------------------------------------------------- /media/PreviewOnRightPane_16x.svg: -------------------------------------------------------------------------------- 1 | PreviewInRightPanel_16x -------------------------------------------------------------------------------- /media/PreviewOnRightPane_16x_dark.svg: -------------------------------------------------------------------------------- 1 | PreviewInRightPanel_16x -------------------------------------------------------------------------------- /media/Preview_inverse.svg: -------------------------------------------------------------------------------- 1 | SwitchToPreview_16x -------------------------------------------------------------------------------- /media/ViewSource.svg: -------------------------------------------------------------------------------- 1 | 3 | ]> -------------------------------------------------------------------------------- /media/ViewSource_inverse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media/basestyle.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #fff; 3 | color: #000; 4 | } 5 | 6 | body.showEditorSelection .code-line { 7 | position: relative; 8 | } 9 | 10 | body.showEditorSelection .code-active-line:before, 11 | body.showEditorSelection .code-line:hover:before { 12 | content: ""; 13 | display: block; 14 | position: absolute; 15 | top: 0; 16 | left: -11px; 17 | height: 100%; 18 | } 19 | 20 | body.showEditorSelection li.code-active-line:before, 21 | body.showEditorSelection li.code-line:hover:before { 22 | left: -30px; 23 | } 24 | 25 | body.showEditorSelection td.code-active-line:before, 26 | body.showEditorSelection td.code-line:hover:before, 27 | body.showEditorSelection th.code-active-line:before, 28 | body.showEditorSelection th.code-line:hover:before { 29 | left: -4px; 30 | } 31 | 32 | .showEditorSelection .code-active-line:before { 33 | border-left: 3px solid rgba(13, 56, 148, 0.5); 34 | } 35 | 36 | .showEditorSelection .code-line:hover:before { 37 | border-left: 3px solid rgba(11, 45, 110, 0.6); 38 | } 39 | 40 | .showEditorSelection .code-line .code-line:hover:before { 41 | border-left: none; 42 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-preview-vscode", 3 | "displayName": "%displayName%", 4 | "description": "%description%", 5 | "version": "0.2.5", 6 | "publisher": "george-alisson", 7 | "author": "George Oliveira, Thomas Haakon Townsend", 8 | "license": "MIT", 9 | "readme": "README.md", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/george-alisson/html-preview-vscode" 13 | }, 14 | "galleryBanner": { 15 | "color": "#78d6f0", 16 | "theme": "light" 17 | }, 18 | "bugs": "https://github.com/george-alisson/html-preview-vscode/issues", 19 | "homepage": "https://github.com/george-alisson/html-preview-vscode/blob/master/README.md", 20 | "icon": "icon.png", 21 | "main": "./out/extension.js", 22 | "engines": { 23 | "vscode": "^1.26.0" 24 | }, 25 | "categories": [ 26 | "Programming Languages" 27 | ], 28 | "activationEvents": [ 29 | "onLanguage:html", 30 | "onCommand:html.preview.toggleLock", 31 | "onCommand:html.preview.refresh", 32 | "onCommand:html.showPreview", 33 | "onCommand:html.showPreviewToSide", 34 | "onCommand:html.showLockedPreviewToSide", 35 | "onCommand:html.showSource", 36 | "onCommand:html.showPreviewSecuritySelector", 37 | "onWebviewPanel:html.preview", 38 | "onCommand:html.openBaseStyle", 39 | "onCommand:html.showInBrowser" 40 | ], 41 | "contributes": { 42 | "commands": [ 43 | { 44 | "command": "html.showPreview", 45 | "title": "%html.preview.title%", 46 | "category": "HTML", 47 | "icon": { 48 | "light": "./media/Preview.svg", 49 | "dark": "./media/Preview_inverse.svg" 50 | } 51 | }, 52 | { 53 | "command": "html.showPreviewToSide", 54 | "title": "%html.previewSide.title%", 55 | "category": "HTML", 56 | "icon": { 57 | "light": "./media/PreviewOnRightPane_16x.svg", 58 | "dark": "./media/PreviewOnRightPane_16x_dark.svg" 59 | } 60 | }, 61 | { 62 | "command": "html.showLockedPreviewToSide", 63 | "title": "%html.showLockedPreviewToSide.title%", 64 | "category": "HTML", 65 | "icon": { 66 | "light": "./media/PreviewOnRightPane_16x.svg", 67 | "dark": "./media/PreviewOnRightPane_16x_dark.svg" 68 | } 69 | }, 70 | { 71 | "command": "html.showSource", 72 | "title": "%html.showSource.title%", 73 | "category": "HTML", 74 | "icon": { 75 | "light": "./media/ViewSource.svg", 76 | "dark": "./media/ViewSource_inverse.svg" 77 | } 78 | }, 79 | { 80 | "command": "html.showPreviewSecuritySelector", 81 | "title": "%html.showPreviewSecuritySelector.title%", 82 | "category": "HTML" 83 | }, 84 | { 85 | "command": "html.preview.refresh", 86 | "title": "%html.preview.refresh.title%", 87 | "category": "HTML" 88 | }, 89 | { 90 | "command": "html.preview.toggleLock", 91 | "title": "%html.preview.toggleLock.title%", 92 | "category": "HTML" 93 | }, 94 | { 95 | "command": "html.openBaseStyle", 96 | "title": "%html.openBaseStyle.title%", 97 | "category": "HTML" 98 | }, 99 | { 100 | "command": "html.showInBrowser", 101 | "title": "%html.showInBrowser.title%", 102 | "category": "HTML" 103 | } 104 | ], 105 | "menus": { 106 | "editor/title": [ 107 | { 108 | "command": "html.showPreviewToSide", 109 | "when": "editorLangId == html", 110 | "alt": "html.showPreview", 111 | "group": "navigation" 112 | }, 113 | { 114 | "command": "html.showSource", 115 | "when": "htmlPreviewFocus", 116 | "group": "navigation" 117 | }, 118 | { 119 | "command": "html.preview.refresh", 120 | "when": "htmlPreviewFocus", 121 | "group": "1_html" 122 | }, 123 | { 124 | "command": "html.preview.toggleLock", 125 | "when": "htmlPreviewFocus", 126 | "group": "1_html" 127 | }, 128 | { 129 | "command": "html.showPreviewSecuritySelector", 130 | "when": "htmlPreviewFocus", 131 | "group": "1_html" 132 | }, 133 | { 134 | "command": "html.showInBrowser", 135 | "when": "htmlPreviewFocus", 136 | "group": "1_html" 137 | } 138 | ], 139 | "explorer/context": [ 140 | { 141 | "command": "html.showPreview", 142 | "when": "resourceLangId == html", 143 | "group": "navigation" 144 | }, 145 | { 146 | "command": "html.showInBrowser", 147 | "when": "resourceLangId == html", 148 | "group": "navigation" 149 | } 150 | ], 151 | "editor/context": [ 152 | { 153 | "command": "html.showInBrowser", 154 | "when": "resourceLangId == html", 155 | "group": "navigation" 156 | } 157 | ], 158 | "editor/title/context": [ 159 | { 160 | "command": "html.showPreview", 161 | "when": "resourceLangId == html", 162 | "group": "navigation" 163 | }, 164 | { 165 | "command": "html.showInBrowser", 166 | "when": "resourceLangId == html", 167 | "group": "navigation" 168 | } 169 | ], 170 | "commandPalette": [ 171 | { 172 | "command": "html.showPreview", 173 | "when": "editorLangId == html", 174 | "group": "navigation" 175 | }, 176 | { 177 | "command": "html.showPreviewToSide", 178 | "when": "editorLangId == html", 179 | "group": "navigation" 180 | }, 181 | { 182 | "command": "html.showLockedPreviewToSide", 183 | "when": "editorLangId == html", 184 | "group": "navigation" 185 | }, 186 | { 187 | "command": "html.showSource", 188 | "when": "htmlPreviewFocus", 189 | "group": "navigation" 190 | }, 191 | { 192 | "command": "html.showPreviewSecuritySelector", 193 | "when": "editorLangId == html" 194 | }, 195 | { 196 | "command": "html.showPreviewSecuritySelector", 197 | "when": "htmlPreviewFocus" 198 | }, 199 | { 200 | "command": "html.preview.toggleLock", 201 | "when": "htmlPreviewFocus" 202 | }, 203 | { 204 | "command": "html.showInBrowser", 205 | "when": "resourceLangId == html", 206 | "group": "navigation" 207 | }, 208 | { 209 | "command": "html.preview.refresh", 210 | "when": "resourceLangId == html", 211 | "group": "navigation" 212 | }, 213 | { 214 | "command": "html.openBaseStyle", 215 | "when": "resourceLangId == html", 216 | "group": "navigation" 217 | } 218 | ] 219 | }, 220 | "keybindings": [ 221 | { 222 | "command": "html.showPreview", 223 | "key": "shift+ctrl+v", 224 | "mac": "shift+cmd+v", 225 | "when": "editorLangId == html" 226 | }, 227 | { 228 | "command": "html.showPreviewToSide", 229 | "key": "ctrl+k v", 230 | "mac": "cmd+k v", 231 | "when": "editorLangId == html" 232 | }, 233 | { 234 | "command": "html.showInBrowser", 235 | "key": "ctrl+k w", 236 | "mac": "cmd+k w", 237 | "when": "editorLangId == html" 238 | } 239 | ], 240 | "configuration": { 241 | "type": "object", 242 | "title": "HTML", 243 | "order": 20, 244 | "properties": { 245 | "html.preview.scrollPreviewWithEditor": { 246 | "type": "boolean", 247 | "default": true, 248 | "description": "%html.preview.scrollPreviewWithEditor.desc%", 249 | "scope": "resource" 250 | }, 251 | "html.preview.markEditorSelection": { 252 | "type": "boolean", 253 | "default": true, 254 | "description": "%html.preview.markEditorSelection.desc%", 255 | "scope": "resource" 256 | }, 257 | "html.preview.scrollEditorWithPreview": { 258 | "type": "boolean", 259 | "default": true, 260 | "description": "%html.preview.scrollEditorWithPreview.desc%", 261 | "scope": "resource" 262 | }, 263 | "html.preview.doubleClickToSwitchToEditor": { 264 | "type": "boolean", 265 | "default": true, 266 | "description": "%html.preview.doubleClickToSwitchToEditor.desc%", 267 | "scope": "resource" 268 | }, 269 | "html.trace": { 270 | "type": "string", 271 | "enum": [ 272 | "off", 273 | "verbose" 274 | ], 275 | "default": "off", 276 | "description": "%html.trace.desc%", 277 | "scope": "window" 278 | } 279 | } 280 | } 281 | }, 282 | "scripts": { 283 | "vscode:prepublish": "npm run compile", 284 | "compile": "npm run build-ext && npm run build-preview", 285 | "build-ext": "npx tsc -p ./tsconfig.json", 286 | "build-preview": "./node_modules/.bin/webpack-cli", 287 | "watch": "npx tsc -watch -p ./tsconfig.json", 288 | "postinstall": "node ./node_modules/vscode/bin/install", 289 | "test": "npm run compile && node ./node_modules/vscode/bin/test", 290 | "preversion": "npm run compile", 291 | "version": "./node_modules/.bin/changes", 292 | "postversion": "git push --follow-tags" 293 | }, 294 | "dependencies": { 295 | "cheerio": "^1.0.0-rc.2", 296 | "lodash.throttle": "4.1.1", 297 | "opn": "^5.4.0", 298 | "vscode-nls": "3.2.4" 299 | }, 300 | "devDependencies": { 301 | "@studio/changes": "1.7.0", 302 | "@types/opn": "^5.1.0", 303 | "@types/cheerio": "0.22.9", 304 | "@types/lodash.throttle": "4.1.4", 305 | "@types/mocha": "2.2.42", 306 | "@types/node": "10.9.4", 307 | "ts-loader": "4.0.1", 308 | "css-select": "^2.0.2", 309 | "dom-serializer": "^0.1.0", 310 | "domelementtype": "^1.3.0", 311 | "htmlparser2": "^3.10.0", 312 | "tslib": "^1.9.3", 313 | "tslint": "5.8.0", 314 | "typescript": "3.0.3", 315 | "vscode": "1.1.21", 316 | "webpack": "4.19.0", 317 | "webpack-cli": "3.1.0" 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /package.nls.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "HTML Preview", 3 | "description": "Provides ability to preview HTML documents.", 4 | "html.preview.doubleClickToSwitchToEditor.desc": "Double click in the html preview to switch to the editor.", 5 | "html.preview.markEditorSelection.desc": "Mark the current editor selection in the html preview.", 6 | "html.preview.scrollEditorWithPreview.desc": "When a html preview is scrolled, update the view of the editor.", 7 | "html.preview.scrollPreviewWithEditor.desc": "When a html editor is scrolled, update the view of the preview.", 8 | "html.preview.title" : "Open Preview", 9 | "html.previewSide.title" : "Open Preview to the Side", 10 | "html.showLockedPreviewToSide.title": "Open Locked Preview to the Side", 11 | "html.showSource.title" : "Show Source", 12 | "html.showPreviewSecuritySelector.title": "Change Preview Security Settings", 13 | "html.trace.desc": "Enable debug logging for the html extension.", 14 | "html.preview.refresh.title": "Refresh Preview", 15 | "html.preview.toggleLock.title": "Toggle Preview Locking", 16 | "html.openBaseStyle.title": "Edit Preview Style", 17 | "html.showInBrowser.title": "Show in Browser" 18 | } 19 | -------------------------------------------------------------------------------- /preview-src/activeLineMarker.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import { getElementsForSourceLine } from './scroll-sync'; 6 | 7 | export class ActiveLineMarker { 8 | private _current: any; 9 | 10 | onDidChangeTextEditorSelection(line: number) { 11 | const { previous } = getElementsForSourceLine(line); 12 | this._update(previous && previous.element); 13 | } 14 | 15 | _update(before: HTMLElement | undefined) { 16 | this._unmarkActiveElement(this._current); 17 | this._markActiveElement(before); 18 | this._current = before; 19 | } 20 | 21 | _unmarkActiveElement(element: HTMLElement | undefined) { 22 | if (!element) { 23 | return; 24 | } 25 | element.className = element.className.replace(/\bcode-active-line\b/g, ''); 26 | } 27 | 28 | _markActiveElement(element: HTMLElement | undefined) { 29 | if (!element) { 30 | return; 31 | } 32 | element.className += ' code-active-line'; 33 | } 34 | } -------------------------------------------------------------------------------- /preview-src/csp.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { MessagePoster } from './messaging'; 7 | import { getSettings } from './settings'; 8 | import { getStrings } from './strings'; 9 | 10 | /** 11 | * Shows an alert when there is a content security policy violation. 12 | */ 13 | export class CspAlerter { 14 | private didShow = false; 15 | private didHaveCspWarning = false; 16 | 17 | private messaging?: MessagePoster; 18 | 19 | constructor() { 20 | document.addEventListener('securitypolicyviolation', () => { 21 | this.onCspWarning(); 22 | }); 23 | 24 | window.addEventListener('message', (event) => { 25 | if (event && event.data && event.data.name === 'vscode-did-block-svg') { 26 | this.onCspWarning(); 27 | } 28 | }); 29 | } 30 | 31 | public setPoster(poster: MessagePoster) { 32 | this.messaging = poster; 33 | if (this.didHaveCspWarning) { 34 | this.showCspWarning(); 35 | } 36 | } 37 | 38 | private onCspWarning() { 39 | this.didHaveCspWarning = true; 40 | this.showCspWarning(); 41 | } 42 | 43 | private showCspWarning() { 44 | const strings = getStrings(); 45 | const settings = getSettings(); 46 | 47 | if (this.didShow || settings.disableSecurityWarnings || !this.messaging) { 48 | return; 49 | } 50 | this.didShow = true; 51 | 52 | const notification = document.createElement('a'); 53 | notification.innerText = strings.cspAlertMessageText; 54 | notification.setAttribute('id', 'code-csp-warning'); 55 | notification.setAttribute('title', strings.cspAlertMessageTitle); 56 | 57 | notification.setAttribute('role', 'button'); 58 | notification.setAttribute('aria-label', strings.cspAlertMessageLabel); 59 | notification.onclick = () => { 60 | this.messaging!.postCommand('html.showPreviewSecuritySelector', [settings.source]); 61 | }; 62 | document.body.appendChild(notification); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /preview-src/events.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export function onceDocumentLoaded(f: () => void) { 7 | if (document.readyState === 'loading') { 8 | document.addEventListener('DOMContentLoaded', f); 9 | } else { 10 | f(); 11 | } 12 | } -------------------------------------------------------------------------------- /preview-src/index.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ActiveLineMarker } from './activeLineMarker'; 7 | import { onceDocumentLoaded } from './events'; 8 | import { createPosterForVsCode } from './messaging'; 9 | import { getEditorLineNumberForPageOffset, scrollToRevealSourceLine } from './scroll-sync'; 10 | import { getSettings, getData } from './settings'; 11 | import throttle = require('lodash.throttle'); 12 | 13 | declare var acquireVsCodeApi: any; 14 | 15 | var scrollDisabled = true; 16 | const marker = new ActiveLineMarker(); 17 | const settings = getSettings(); 18 | 19 | const vscode = acquireVsCodeApi(); 20 | 21 | // Set VS Code state 22 | const state = getData('data-state'); 23 | vscode.setState(state); 24 | 25 | const messaging = createPosterForVsCode(vscode); 26 | 27 | window.cspAlerter.setPoster(messaging); 28 | 29 | onceDocumentLoaded(() => { 30 | if (settings.scrollPreviewWithEditor) { 31 | setTimeout(() => { 32 | const initialLine = +settings.line; 33 | if (!isNaN(initialLine)) { 34 | scrollDisabled = true; 35 | scrollToRevealSourceLine(initialLine); 36 | } 37 | }, 0); 38 | } 39 | }); 40 | 41 | const onUpdateView = (() => { 42 | const doScroll = throttle((line: number) => { 43 | scrollDisabled = true; 44 | scrollToRevealSourceLine(line); 45 | }, 50); 46 | 47 | return (line: number, settings: any) => { 48 | if (!isNaN(line)) { 49 | settings.line = line; 50 | doScroll(line); 51 | } 52 | }; 53 | })(); 54 | 55 | window.addEventListener('resize', () => { 56 | scrollDisabled = true; 57 | }, true); 58 | 59 | window.addEventListener('message', event => { 60 | if (event.data.source !== settings.source) { 61 | return; 62 | } 63 | 64 | switch (event.data.type) { 65 | case 'onDidChangeTextEditorSelection': 66 | marker.onDidChangeTextEditorSelection(event.data.line); 67 | break; 68 | 69 | case 'updateView': 70 | onUpdateView(event.data.line, settings); 71 | break; 72 | } 73 | }, false); 74 | 75 | document.addEventListener('dblclick', event => { 76 | if (!settings.doubleClickToSwitchToEditor) { 77 | return; 78 | } 79 | 80 | // Ignore clicks on links 81 | for (let node = event.target as HTMLElement; node; node = node.parentNode as HTMLElement) { 82 | if (node.tagName === 'A') { 83 | return; 84 | } 85 | } 86 | 87 | const offset = event.pageY; 88 | const line = getEditorLineNumberForPageOffset(offset); 89 | if (typeof line === 'number' && !isNaN(line)) { 90 | messaging.postMessage('didClick', { line: Math.floor(line) }); 91 | } 92 | }); 93 | 94 | document.addEventListener('click', event => { 95 | if (!event) { 96 | return; 97 | } 98 | 99 | let node: any = event.target; 100 | while (node) { 101 | if (node.tagName && node.tagName === 'A' && node.href) { 102 | if (node.getAttribute('href').startsWith('#')) { 103 | break; 104 | } 105 | if (node.href.startsWith('file://') || node.href.startsWith('vscode-resource:')) { 106 | const [path, fragment] = node.href.replace(/^(file:\/\/|vscode-resource:)/i, '').split('#'); 107 | messaging.postCommand('_html.openDocumentLink', [{ path, fragment }]); 108 | event.preventDefault(); 109 | event.stopPropagation(); 110 | break; 111 | } 112 | break; 113 | } 114 | node = node.parentNode; 115 | } 116 | }, true); 117 | 118 | if (settings.scrollEditorWithPreview) { 119 | window.addEventListener('scroll', throttle(() => { 120 | if (scrollDisabled) { 121 | scrollDisabled = false; 122 | } else { 123 | const line = getEditorLineNumberForPageOffset(window.scrollY); 124 | if (typeof line === 'number' && !isNaN(line)) { 125 | messaging.postMessage('revealLine', { line }); 126 | } 127 | } 128 | }, 50)); 129 | } -------------------------------------------------------------------------------- /preview-src/messaging.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { getSettings } from './settings'; 7 | 8 | export interface MessagePoster { 9 | /** 10 | * Post a message to the html extension 11 | */ 12 | postMessage(type: string, body: object): void; 13 | 14 | 15 | /** 16 | * Post a command to be executed to the html extension 17 | */ 18 | postCommand(command: string, args: any[]): void; 19 | } 20 | 21 | export const createPosterForVsCode = (vscode: any) => { 22 | return new class implements MessagePoster { 23 | postMessage(type: string, body: object): void { 24 | vscode.postMessage({ 25 | type, 26 | source: getSettings().source, 27 | body 28 | }); 29 | } 30 | postCommand(command: string, args: any[]) { 31 | this.postMessage('command', { command, args }); 32 | } 33 | }; 34 | }; 35 | 36 | -------------------------------------------------------------------------------- /preview-src/pre.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { CspAlerter } from './csp'; 7 | 8 | declare global { 9 | interface Window { 10 | cspAlerter: CspAlerter; 11 | } 12 | } 13 | 14 | window.cspAlerter = new CspAlerter(); -------------------------------------------------------------------------------- /preview-src/scroll-sync.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { getSettings } from './settings'; 7 | 8 | 9 | function clamp(min: number, max: number, value: number) { 10 | return Math.min(max, Math.max(min, value)); 11 | } 12 | 13 | function clampLine(line: number) { 14 | return clamp(0, getSettings().lineCount - 1, line); 15 | } 16 | 17 | 18 | export interface CodeLineElement { 19 | element: HTMLElement; 20 | line: number; 21 | } 22 | 23 | const getCodeLineElements = (() => { 24 | let elements: CodeLineElement[]; 25 | return () => { 26 | if (!elements) { 27 | elements = Array.prototype.map.call( 28 | document.getElementsByClassName('code-line'), 29 | (element: any) => { 30 | const line = +element.getAttribute('data-line'); 31 | return { element, line }; 32 | }) 33 | .filter((x: any) => !isNaN(x.line)); 34 | } 35 | return elements; 36 | }; 37 | })(); 38 | 39 | /** 40 | * Find the html elements that map to a specific target line in the editor. 41 | * 42 | * If an exact match, returns a single element. If the line is between elements, 43 | * returns the element prior to and the element after the given line. 44 | */ 45 | export function getElementsForSourceLine(targetLine: number): { previous: CodeLineElement; next?: CodeLineElement; } { 46 | const lineNumber = Math.floor(targetLine); 47 | const lines = getCodeLineElements(); 48 | let previous = lines[0] || null; 49 | for (const entry of lines) { 50 | if (entry.line === lineNumber) { 51 | return { previous: entry, next: undefined }; 52 | } 53 | else if (entry.line > lineNumber) { 54 | return { previous, next: entry }; 55 | } 56 | previous = entry; 57 | } 58 | return { previous }; 59 | } 60 | 61 | /** 62 | * Find the html elements that are at a specific pixel offset on the page. 63 | */ 64 | export function getLineElementsAtPageOffset(offset: number): { previous: CodeLineElement; next?: CodeLineElement; } { 65 | const lines = getCodeLineElements(); 66 | const position = offset - window.scrollY; 67 | let lo = -1; 68 | let hi = lines.length - 1; 69 | while (lo + 1 < hi) { 70 | const mid = Math.floor((lo + hi) / 2); 71 | const bounds = lines[mid].element.getBoundingClientRect(); 72 | if (bounds.top + bounds.height >= position) { 73 | hi = mid; 74 | } 75 | else { 76 | lo = mid; 77 | } 78 | } 79 | const hiElement = lines[hi]; 80 | const hiBounds = hiElement.element.getBoundingClientRect(); 81 | if (hi >= 1 && hiBounds.top > position) { 82 | const loElement = lines[lo]; 83 | return { previous: loElement, next: hiElement }; 84 | } 85 | return { previous: hiElement }; 86 | } 87 | 88 | /** 89 | * Attempt to reveal the element for a source line in the editor. 90 | */ 91 | export function scrollToRevealSourceLine(line: number) { 92 | const { previous, next } = getElementsForSourceLine(line); 93 | if (previous && getSettings().scrollPreviewWithEditor) { 94 | let scrollTo = 0; 95 | const rect = previous.element.getBoundingClientRect(); 96 | const previousTop = rect.top; 97 | if (next && next.line !== previous.line) { 98 | // Between two elements. Go to percentage offset between them. 99 | const betweenProgress = (line - previous.line) / (next.line - previous.line); 100 | const elementOffset = next.element.getBoundingClientRect().top - previousTop; 101 | scrollTo = previousTop + betweenProgress * elementOffset; 102 | } 103 | else { 104 | scrollTo = previousTop; 105 | } 106 | window.scroll(0, Math.max(1, window.scrollY + scrollTo)); 107 | } 108 | } 109 | 110 | export function getEditorLineNumberForPageOffset(offset: number) { 111 | const { previous, next } = getLineElementsAtPageOffset(offset); 112 | if (previous) { 113 | const previousBounds = previous.element.getBoundingClientRect(); 114 | const offsetFromPrevious = (offset - window.scrollY - previousBounds.top); 115 | if (next) { 116 | const progressBetweenElements = offsetFromPrevious / (next.element.getBoundingClientRect().top - previousBounds.top); 117 | const line = previous.line + progressBetweenElements * (next.line - previous.line); 118 | return clampLine(line); 119 | } 120 | else { 121 | const progressWithinElement = offsetFromPrevious / (previousBounds.height); 122 | const line = previous.line + progressWithinElement; 123 | return clampLine(line); 124 | } 125 | } 126 | return null; 127 | } 128 | -------------------------------------------------------------------------------- /preview-src/settings.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export interface PreviewSettings { 7 | source: string; 8 | line: number; 9 | lineCount: number; 10 | scrollPreviewWithEditor?: boolean; 11 | scrollEditorWithPreview: boolean; 12 | disableSecurityWarnings: boolean; 13 | doubleClickToSwitchToEditor: boolean; 14 | } 15 | 16 | let cachedSettings: PreviewSettings | undefined = undefined; 17 | 18 | export function getData(key: string): PreviewSettings { 19 | const element = document.getElementById('vscode-html-preview-data'); 20 | if (element) { 21 | const data = element.getAttribute(key); 22 | if (data) { 23 | return JSON.parse(data); 24 | } 25 | } 26 | 27 | throw new Error(`Could not load data for ${key}`); 28 | } 29 | 30 | export function getSettings(): PreviewSettings { 31 | if (cachedSettings) { 32 | return cachedSettings; 33 | } 34 | 35 | cachedSettings = getData('data-settings'); 36 | if (cachedSettings) { 37 | return cachedSettings; 38 | } 39 | 40 | throw new Error('Could not load settings'); 41 | } 42 | -------------------------------------------------------------------------------- /preview-src/strings.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export function getStrings(): { [key: string]: string } { 7 | const store = document.getElementById('vscode-html-preview-data'); 8 | if (store) { 9 | const data = store.getAttribute('data-strings'); 10 | if (data) { 11 | return JSON.parse(data); 12 | } 13 | } 14 | throw new Error('Could not load strings'); 15 | } 16 | -------------------------------------------------------------------------------- /preview-src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "module": "commonjs", 5 | "target": "es6", 6 | "jsx": "react", 7 | "sourceMap": true, 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "noUnusedLocals": true 11 | } 12 | } -------------------------------------------------------------------------------- /src/commandManager.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | export interface Command { 9 | readonly id: string; 10 | 11 | execute(...args: any[]): void; 12 | } 13 | 14 | export class CommandManager { 15 | private readonly commands = new Map(); 16 | 17 | public dispose() { 18 | for (const registration of this.commands.values()) { 19 | registration.dispose(); 20 | } 21 | this.commands.clear(); 22 | } 23 | 24 | public register(command: T): T { 25 | this.registerCommand(command.id, command.execute, command); 26 | return command; 27 | } 28 | 29 | private registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any) { 30 | if (this.commands.has(id)) { 31 | return; 32 | } 33 | 34 | this.commands.set(id, vscode.commands.registerCommand(id, impl, thisArg)); 35 | } 36 | } -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export { OpenDocumentLinkCommand } from './openDocumentLink'; 7 | export { ShowPreviewCommand, ShowPreviewToSideCommand, ShowLockedPreviewToSideCommand } from './showPreview'; 8 | export { ShowSourceCommand } from './showSource'; 9 | export { RefreshPreviewCommand } from './refreshPreview'; 10 | export { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector'; 11 | export { MoveCursorToPositionCommand } from './moveCursorToPosition'; 12 | export { ToggleLockCommand } from './toggleLock'; 13 | export { OpenBaseStyleCommand } from './openBaseStyle'; 14 | export { ShowInBrowserCommand } from './showInBrowser'; 15 | -------------------------------------------------------------------------------- /src/commands/moveCursorToPosition.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | import { Command } from '../commandManager'; 9 | 10 | export class MoveCursorToPositionCommand implements Command { 11 | public readonly id = '_html.moveCursorToPosition'; 12 | 13 | public execute(line: number, character: number) { 14 | if (!vscode.window.activeTextEditor) { 15 | return; 16 | } 17 | const position = new vscode.Position(line, character); 18 | const selection = new vscode.Selection(position, position); 19 | vscode.window.activeTextEditor.revealRange(selection); 20 | vscode.window.activeTextEditor.selection = selection; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/openBaseStyle.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as path from 'path'; 8 | import { Command } from '../commandManager'; 9 | 10 | export class OpenBaseStyleCommand implements Command { 11 | public readonly id = 'html.openBaseStyle'; 12 | 13 | public constructor( 14 | private readonly context: vscode.ExtensionContext 15 | ) { } 16 | 17 | public execute() { 18 | const resource = vscode.Uri.file(this.context.asAbsolutePath(path.join('media', 'basestyle.css'))); 19 | return vscode.commands.executeCommand('vscode.open', resource); 20 | } 21 | } -------------------------------------------------------------------------------- /src/commands/openDocumentLink.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as path from 'path'; 8 | 9 | import { Command } from '../commandManager'; 10 | import { isHTMLFile } from '../util/file'; 11 | 12 | 13 | export interface OpenDocumentLinkArgs { 14 | path: string; 15 | fragment: string; 16 | } 17 | 18 | export class OpenDocumentLinkCommand implements Command { 19 | private static readonly id = '_html.openDocumentLink'; 20 | public readonly id = OpenDocumentLinkCommand.id; 21 | 22 | public static createCommandUri( 23 | path: string, 24 | fragment: string 25 | ): vscode.Uri { 26 | return vscode.Uri.parse(`command:${OpenDocumentLinkCommand.id}?${encodeURIComponent(JSON.stringify({ path, fragment }))}`); 27 | } 28 | 29 | public constructor( 30 | ) { } 31 | 32 | public execute(args: OpenDocumentLinkArgs) { 33 | const p = decodeURIComponent(args.path); 34 | return this.tryOpen(p, args).catch(() => { 35 | if (path.extname(p) === '') { 36 | return this.tryOpen(p + '.html', args); 37 | } 38 | const resource = vscode.Uri.file(p); 39 | return Promise.resolve(void 0) 40 | .then(() => vscode.commands.executeCommand('vscode.open', resource)) 41 | .then(() => void 0); 42 | }); 43 | } 44 | 45 | private async tryOpen(path: string, args: OpenDocumentLinkArgs) { 46 | const resource = vscode.Uri.file(path); 47 | if (vscode.window.activeTextEditor && isHTMLFile(vscode.window.activeTextEditor.document) && vscode.window.activeTextEditor.document.uri.fsPath === resource.fsPath) { 48 | return this.tryRevealLine(vscode.window.activeTextEditor, args.fragment); 49 | } else { 50 | return vscode.workspace.openTextDocument(resource) 51 | .then(vscode.window.showTextDocument) 52 | .then(editor => this.tryRevealLine(editor, args.fragment)); 53 | } 54 | } 55 | 56 | private async tryRevealLine(editor: vscode.TextEditor, fragment?: string) { 57 | if (editor && fragment) { 58 | const lineNumberFragment = fragment.match(/^L(\d+)$/i); 59 | if (lineNumberFragment) { 60 | const line = +lineNumberFragment[1] - 1; 61 | if (!isNaN(line)) { 62 | return editor.revealRange(new vscode.Range(line, 0, line, 0), vscode.TextEditorRevealType.AtTop); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/commands/refreshPreview.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Command } from '../commandManager'; 7 | import { HTMLPreviewManager } from '../features/previewManager'; 8 | 9 | export class RefreshPreviewCommand implements Command { 10 | public readonly id = 'html.preview.refresh'; 11 | 12 | public constructor( 13 | private readonly webviewManager: HTMLPreviewManager 14 | ) { } 15 | 16 | public execute() { 17 | this.webviewManager.refresh(); 18 | } 19 | } -------------------------------------------------------------------------------- /src/commands/showInBrowser.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | import opn = require('opn'); 8 | import { Command } from '../commandManager'; 9 | import { isHTMLFile } from '../util/file'; 10 | 11 | export class ShowInBrowserCommand implements Command { 12 | public readonly id = 'html.showInBrowser'; 13 | 14 | public execute(mainUri?: vscode.Uri) { 15 | if (mainUri && mainUri.fsPath) { 16 | return opn(mainUri.fsPath); 17 | } 18 | if (vscode.window.activeTextEditor && isHTMLFile(vscode.window.activeTextEditor.document)) { 19 | return opn(vscode.window.activeTextEditor.document.fileName); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/commands/showPreview.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | import { Command } from '../commandManager'; 9 | import { HTMLPreviewManager } from '../features/previewManager'; 10 | import { PreviewSettings } from '../features/preview'; 11 | 12 | interface ShowPreviewSettings { 13 | readonly sideBySide?: boolean; 14 | readonly locked?: boolean; 15 | } 16 | 17 | async function showPreview( 18 | webviewManager: HTMLPreviewManager, 19 | uri: vscode.Uri | undefined, 20 | previewSettings: ShowPreviewSettings, 21 | ): Promise { 22 | let resource = uri; 23 | if (!(resource instanceof vscode.Uri)) { 24 | if (vscode.window.activeTextEditor) { 25 | // we are relaxed and don't check for html files 26 | resource = vscode.window.activeTextEditor.document.uri; 27 | } 28 | } 29 | 30 | if (!(resource instanceof vscode.Uri)) { 31 | if (!vscode.window.activeTextEditor) { 32 | // this is most likely toggling the preview 33 | return vscode.commands.executeCommand('html.showSource'); 34 | } 35 | // nothing found that could be shown or toggled 36 | return; 37 | } 38 | 39 | const resourceColumn = (vscode.window.activeTextEditor && vscode.window.activeTextEditor.viewColumn) || vscode.ViewColumn.One; 40 | webviewManager.preview(resource, { 41 | resourceColumn: resourceColumn, 42 | previewColumn: previewSettings.sideBySide ? resourceColumn + 1 : resourceColumn, 43 | locked: !!previewSettings.locked 44 | }); 45 | 46 | } 47 | 48 | export class ShowPreviewCommand implements Command { 49 | public readonly id = 'html.showPreview'; 50 | 51 | public constructor( 52 | private readonly webviewManager: HTMLPreviewManager, 53 | ) { } 54 | 55 | public execute(mainUri?: vscode.Uri, allUris?: vscode.Uri[], previewSettings?: PreviewSettings) { 56 | for (const uri of Array.isArray(allUris) ? allUris : [mainUri]) { 57 | showPreview(this.webviewManager, uri, { 58 | sideBySide: false, 59 | locked: previewSettings && previewSettings.locked 60 | }); 61 | } 62 | } 63 | } 64 | 65 | export class ShowPreviewToSideCommand implements Command { 66 | public readonly id = 'html.showPreviewToSide'; 67 | 68 | public constructor( 69 | private readonly webviewManager: HTMLPreviewManager, 70 | ) { } 71 | 72 | public execute(uri?: vscode.Uri, previewSettings?: PreviewSettings) { 73 | showPreview(this.webviewManager, uri, { 74 | sideBySide: true, 75 | locked: previewSettings && previewSettings.locked 76 | }); 77 | } 78 | } 79 | 80 | 81 | export class ShowLockedPreviewToSideCommand implements Command { 82 | public readonly id = 'html.showLockedPreviewToSide'; 83 | 84 | public constructor( 85 | private readonly webviewManager: HTMLPreviewManager 86 | ) { } 87 | 88 | public execute(uri?: vscode.Uri) { 89 | showPreview(this.webviewManager, uri, { 90 | sideBySide: true, 91 | locked: true 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/commands/showPreviewSecuritySelector.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | import { Command } from '../commandManager'; 8 | import { PreviewSecuritySelector } from '../security'; 9 | import { isHTMLFile } from '../util/file'; 10 | import { HTMLPreviewManager } from '../features/previewManager'; 11 | 12 | export class ShowPreviewSecuritySelectorCommand implements Command { 13 | public readonly id = 'html.showPreviewSecuritySelector'; 14 | 15 | public constructor( 16 | private readonly previewSecuritySelector: PreviewSecuritySelector, 17 | private readonly previewManager: HTMLPreviewManager 18 | ) { } 19 | 20 | public execute(resource: string | undefined) { 21 | if (this.previewManager.activePreviewResource) { 22 | this.previewSecuritySelector.showSecuritySelectorForResource(this.previewManager.activePreviewResource); 23 | } else if (resource) { 24 | const source = vscode.Uri.parse(resource); 25 | this.previewSecuritySelector.showSecuritySelectorForResource(source.query ? vscode.Uri.parse(source.query) : source); 26 | } else if (vscode.window.activeTextEditor && isHTMLFile(vscode.window.activeTextEditor.document)) { 27 | this.previewSecuritySelector.showSecuritySelectorForResource(vscode.window.activeTextEditor.document.uri); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/commands/showSource.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | import { Command } from '../commandManager'; 8 | import { HTMLPreviewManager } from '../features/previewManager'; 9 | 10 | export class ShowSourceCommand implements Command { 11 | public readonly id = 'html.showSource'; 12 | 13 | public constructor( 14 | private readonly previewManager: HTMLPreviewManager 15 | ) { } 16 | 17 | public execute() { 18 | if (this.previewManager.activePreviewResource) { 19 | return vscode.workspace.openTextDocument(this.previewManager.activePreviewResource) 20 | .then(document => vscode.window.showTextDocument(document)); 21 | } 22 | return undefined; 23 | } 24 | } -------------------------------------------------------------------------------- /src/commands/toggleLock.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { Command } from '../commandManager'; 7 | import { HTMLPreviewManager } from '../features/previewManager'; 8 | 9 | export class ToggleLockCommand implements Command { 10 | public readonly id = 'html.preview.toggleLock'; 11 | 12 | public constructor( 13 | private readonly previewManager: HTMLPreviewManager 14 | ) { } 15 | 16 | public execute() { 17 | this.previewManager.toggleLock(); 18 | } 19 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | import { CommandManager } from './commandManager'; 8 | import * as commands from './commands/index'; 9 | import { HTMLContentProvider } from './features/previewContentProvider'; 10 | import { HTMLPreviewManager } from './features/previewManager'; 11 | import { Logger } from './logger'; 12 | import { ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './security'; 13 | 14 | let extensionPath = ""; 15 | 16 | export function getExtensionPath(): string { 17 | return extensionPath; 18 | } 19 | 20 | export function activate(context: vscode.ExtensionContext) { 21 | extensionPath = context.extensionPath; 22 | 23 | const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState, context.workspaceState); 24 | const logger = new Logger(); 25 | 26 | const contentProvider = new HTMLContentProvider(context, cspArbiter, logger); 27 | const previewManager = new HTMLPreviewManager(contentProvider, logger); 28 | context.subscriptions.push(previewManager); 29 | 30 | 31 | const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager); 32 | 33 | const commandManager = new CommandManager(); 34 | context.subscriptions.push(commandManager); 35 | commandManager.register(new commands.ShowPreviewCommand(previewManager)); 36 | commandManager.register(new commands.ShowPreviewToSideCommand(previewManager)); 37 | commandManager.register(new commands.ShowLockedPreviewToSideCommand(previewManager)); 38 | commandManager.register(new commands.ShowSourceCommand(previewManager)); 39 | commandManager.register(new commands.RefreshPreviewCommand(previewManager)); 40 | commandManager.register(new commands.MoveCursorToPositionCommand()); 41 | commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager)); 42 | commandManager.register(new commands.OpenDocumentLinkCommand()); 43 | commandManager.register(new commands.ToggleLockCommand(previewManager)); 44 | commandManager.register(new commands.OpenBaseStyleCommand(context)); 45 | commandManager.register(new commands.ShowInBrowserCommand()); 46 | 47 | context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => { 48 | logger.updateConfiguration(); 49 | previewManager.updateConfiguration(); 50 | })); 51 | } 52 | -------------------------------------------------------------------------------- /src/features/preview.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as path from 'path'; 8 | 9 | import { Logger } from '../logger'; 10 | import { HTMLContentProvider } from './previewContentProvider'; 11 | import { disposeAll } from '../util/dispose'; 12 | 13 | import * as nls from 'vscode-nls'; 14 | import { getVisibleLine, HTMLFileTopmostLineMonitor } from '../util/topmostLineMonitor'; 15 | import { HTMLPreviewConfigurationManager } from './previewConfig'; 16 | import { isHTMLFile } from '../util/file'; 17 | import { getExtensionPath } from '../extension'; 18 | const localize = nls.loadMessageBundle(); 19 | 20 | export class HTMLPreview { 21 | 22 | public static viewType = 'html.preview'; 23 | 24 | private _resource: vscode.Uri; 25 | private _locked: boolean; 26 | 27 | private readonly editor: vscode.WebviewPanel; 28 | private throttleTimer: any; 29 | private line: number | undefined = undefined; 30 | private readonly disposables: vscode.Disposable[] = []; 31 | private firstUpdate = true; 32 | private currentVersion?: { resource: vscode.Uri, version: number }; 33 | private forceUpdate = false; 34 | private isScrolling = false; 35 | private _disposed: boolean = false; 36 | 37 | public static async revive( 38 | webview: vscode.WebviewPanel, 39 | state: any, 40 | contentProvider: HTMLContentProvider, 41 | previewConfigurations: HTMLPreviewConfigurationManager, 42 | logger: Logger, 43 | topmostLineMonitor: HTMLFileTopmostLineMonitor, 44 | ): Promise { 45 | const resource = vscode.Uri.parse(state.resource); 46 | const locked = state.locked; 47 | const line = state.line; 48 | 49 | const preview = new HTMLPreview( 50 | webview, 51 | resource, 52 | locked, 53 | contentProvider, 54 | previewConfigurations, 55 | logger, 56 | topmostLineMonitor,); 57 | 58 | preview.editor.webview.options = HTMLPreview.getWebviewOptions(resource); 59 | 60 | if (!isNaN(line)) { 61 | preview.line = line; 62 | } 63 | await preview.doUpdate(); 64 | return preview; 65 | } 66 | 67 | public static create( 68 | resource: vscode.Uri, 69 | previewColumn: vscode.ViewColumn, 70 | locked: boolean, 71 | contentProvider: HTMLContentProvider, 72 | previewConfigurations: HTMLPreviewConfigurationManager, 73 | logger: Logger, 74 | topmostLineMonitor: HTMLFileTopmostLineMonitor, 75 | ): HTMLPreview { 76 | const webview = vscode.window.createWebviewPanel( 77 | HTMLPreview.viewType, 78 | HTMLPreview.getPreviewTitle(resource, locked), 79 | previewColumn, { 80 | enableFindWidget: true, 81 | ...HTMLPreview.getWebviewOptions(resource) 82 | }); 83 | 84 | return new HTMLPreview( 85 | webview, 86 | resource, 87 | locked, 88 | contentProvider, 89 | previewConfigurations, 90 | logger, 91 | topmostLineMonitor,); 92 | } 93 | 94 | private constructor( 95 | webview: vscode.WebviewPanel, 96 | resource: vscode.Uri, 97 | locked: boolean, 98 | private readonly _contentProvider: HTMLContentProvider, 99 | private readonly _previewConfigurations: HTMLPreviewConfigurationManager, 100 | private readonly _logger: Logger, 101 | topmostLineMonitor: HTMLFileTopmostLineMonitor, 102 | ) { 103 | this._resource = resource; 104 | this._locked = locked; 105 | this.editor = webview; 106 | 107 | this.editor.onDidDispose(() => { 108 | this.dispose(); 109 | }, null, this.disposables); 110 | 111 | this.editor.onDidChangeViewState(e => { 112 | this._onDidChangeViewStateEmitter.fire(e); 113 | }, null, this.disposables); 114 | 115 | this.editor.webview.onDidReceiveMessage(e => { 116 | if (e.source !== this._resource.toString()) { 117 | return; 118 | } 119 | 120 | switch (e.type) { 121 | case 'command': 122 | vscode.commands.executeCommand(e.body.command, ...e.body.args); 123 | break; 124 | 125 | case 'revealLine': 126 | this.onDidScrollPreview(e.body.line); 127 | break; 128 | 129 | case 'didClick': 130 | this.onDidClickPreview(e.body.line); 131 | break; 132 | 133 | } 134 | }, null, this.disposables); 135 | 136 | vscode.workspace.onDidChangeTextDocument(event => { 137 | if (this.isPreviewOf(event.document.uri)) { 138 | this.refresh(); 139 | } 140 | }, null, this.disposables); 141 | 142 | topmostLineMonitor.onDidChangeTopmostLine(event => { 143 | if (this.isPreviewOf(event.resource)) { 144 | this.updateForView(event.resource, event.line); 145 | } 146 | }, null, this.disposables); 147 | 148 | vscode.window.onDidChangeTextEditorSelection(event => { 149 | if (this.isPreviewOf(event.textEditor.document.uri)) { 150 | this.postMessage({ 151 | type: 'onDidChangeTextEditorSelection', 152 | line: event.selections[0].active.line, 153 | source: this.resource.toString() 154 | }); 155 | } 156 | }, null, this.disposables); 157 | 158 | vscode.window.onDidChangeActiveTextEditor(editor => { 159 | if (editor && isHTMLFile(editor.document) && !this._locked) { 160 | this.update(editor.document.uri); 161 | } 162 | }, null, this.disposables); 163 | } 164 | 165 | private readonly _onDisposeEmitter = new vscode.EventEmitter(); 166 | public readonly onDispose = this._onDisposeEmitter.event; 167 | 168 | private readonly _onDidChangeViewStateEmitter = new vscode.EventEmitter(); 169 | public readonly onDidChangeViewState = this._onDidChangeViewStateEmitter.event; 170 | 171 | public get resource(): vscode.Uri { 172 | return this._resource; 173 | } 174 | 175 | public get state() { 176 | return { 177 | resource: this.resource.toString(), 178 | locked: this._locked, 179 | line: this.line 180 | }; 181 | } 182 | 183 | public dispose() { 184 | if (this._disposed) { 185 | return; 186 | } 187 | 188 | this._disposed = true; 189 | this._onDisposeEmitter.fire(); 190 | 191 | this._onDisposeEmitter.dispose(); 192 | this._onDidChangeViewStateEmitter.dispose(); 193 | this.editor.dispose(); 194 | 195 | disposeAll(this.disposables); 196 | } 197 | 198 | public update(resource: vscode.Uri) { 199 | const editor = vscode.window.activeTextEditor; 200 | if (editor && editor.document.uri.fsPath === resource.fsPath) { 201 | this.line = getVisibleLine(editor); 202 | } 203 | 204 | // If we have changed resources, cancel any pending updates 205 | const isResourceChange = resource.fsPath !== this._resource.fsPath; 206 | if (isResourceChange) { 207 | clearTimeout(this.throttleTimer); 208 | this.throttleTimer = undefined; 209 | } 210 | 211 | this._resource = resource; 212 | 213 | // Schedule update if none is pending 214 | if (!this.throttleTimer) { 215 | if (isResourceChange || this.firstUpdate) { 216 | this.doUpdate(); 217 | } else { 218 | this.throttleTimer = setTimeout(() => this.doUpdate(), 300); 219 | } 220 | } 221 | 222 | this.firstUpdate = false; 223 | } 224 | 225 | public refresh() { 226 | this.forceUpdate = true; 227 | this.update(this._resource); 228 | } 229 | 230 | public updateConfiguration() { 231 | if (this._previewConfigurations.hasConfigurationChanged(this._resource)) { 232 | this.refresh(); 233 | } 234 | } 235 | 236 | public get position(): vscode.ViewColumn | undefined { 237 | return this.editor.viewColumn; 238 | } 239 | 240 | public matchesResource( 241 | otherResource: vscode.Uri, 242 | otherPosition: vscode.ViewColumn | undefined, 243 | otherLocked: boolean 244 | ): boolean { 245 | if (this.position !== otherPosition) { 246 | return false; 247 | } 248 | 249 | if (this._locked) { 250 | return otherLocked && this.isPreviewOf(otherResource); 251 | } else { 252 | return !otherLocked; 253 | } 254 | } 255 | 256 | public matches(otherPreview: HTMLPreview): boolean { 257 | return this.matchesResource(otherPreview._resource, otherPreview.position, otherPreview._locked); 258 | } 259 | 260 | public reveal(viewColumn: vscode.ViewColumn) { 261 | this.editor.reveal(viewColumn); 262 | } 263 | 264 | public toggleLock() { 265 | this._locked = !this._locked; 266 | this.editor.title = HTMLPreview.getPreviewTitle(this._resource, this._locked); 267 | } 268 | 269 | private get iconPath() { 270 | const root = path.join(getExtensionPath(), 'media'); 271 | return { 272 | light: vscode.Uri.file(path.join(root, 'Preview.svg')), 273 | dark: vscode.Uri.file(path.join(root, 'Preview_inverse.svg')) 274 | }; 275 | } 276 | 277 | private isPreviewOf(resource: vscode.Uri): boolean { 278 | return this._resource.fsPath === resource.fsPath; 279 | } 280 | 281 | private static getPreviewTitle(resource: vscode.Uri, locked: boolean): string { 282 | return locked 283 | ? localize('lockedPreviewTitle', '[Preview] {0}', path.basename(resource.fsPath)) 284 | : localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath)); 285 | } 286 | 287 | private updateForView(resource: vscode.Uri, topLine: number | undefined) { 288 | if (!this.isPreviewOf(resource)) { 289 | return; 290 | } 291 | 292 | if (this.isScrolling) { 293 | this.isScrolling = false; 294 | return; 295 | } 296 | 297 | if (typeof topLine === 'number') { 298 | this._logger.log('updateForView', { htmlFile: resource }); 299 | this.line = topLine; 300 | this.postMessage({ 301 | type: 'updateView', 302 | line: topLine, 303 | source: resource.toString() 304 | }); 305 | } 306 | } 307 | 308 | private postMessage(msg: any) { 309 | if (!this._disposed) { 310 | this.editor.webview.postMessage(msg); 311 | } 312 | } 313 | 314 | private async doUpdate(): Promise { 315 | const resource = this._resource; 316 | 317 | clearTimeout(this.throttleTimer); 318 | this.throttleTimer = undefined; 319 | 320 | const document = await vscode.workspace.openTextDocument(resource); 321 | if (!this.forceUpdate && this.currentVersion && this.currentVersion.resource.fsPath === resource.fsPath && this.currentVersion.version === document.version) { 322 | if (this.line) { 323 | this.updateForView(resource, this.line); 324 | } 325 | return; 326 | } 327 | this.forceUpdate = false; 328 | 329 | this.currentVersion = { resource, version: document.version }; 330 | const content: string = this._contentProvider.provideTextDocumentContent(document, this._previewConfigurations, this.line, this.state); 331 | if (this._resource === resource) { 332 | this.editor.title = HTMLPreview.getPreviewTitle(this._resource, this._locked); 333 | this.editor.iconPath = this.iconPath; 334 | this.editor.webview.options = HTMLPreview.getWebviewOptions(resource); 335 | this.editor.webview.html = content; 336 | } 337 | } 338 | 339 | private static getWebviewOptions( 340 | resource: vscode.Uri, 341 | ): vscode.WebviewOptions { 342 | return { 343 | enableScripts: true, 344 | enableCommandUris: true, 345 | localResourceRoots: HTMLPreview.getLocalResourceRoots(resource) 346 | }; 347 | } 348 | 349 | private static getLocalResourceRoots( 350 | resource: vscode.Uri, 351 | ): vscode.Uri[] { 352 | const baseRoots: vscode.Uri[] = [vscode.Uri.file(getExtensionPath() + "/media")]; 353 | 354 | const folder = vscode.workspace.getWorkspaceFolder(resource); 355 | if (folder) { 356 | return baseRoots.concat(folder.uri); 357 | } 358 | 359 | if (!resource.scheme || resource.scheme === 'file') { 360 | return baseRoots.concat(vscode.Uri.file(path.dirname(resource.fsPath))); 361 | } 362 | 363 | return baseRoots; 364 | } 365 | 366 | private onDidScrollPreview(line: number) { 367 | this.line = line; 368 | for (const editor of vscode.window.visibleTextEditors) { 369 | if (!this.isPreviewOf(editor.document.uri)) { 370 | continue; 371 | } 372 | 373 | this.isScrolling = true; 374 | const sourceLine = Math.floor(line); 375 | const fraction = line - sourceLine; 376 | const text = editor.document.lineAt(sourceLine).text; 377 | const start = Math.floor(fraction * text.length); 378 | editor.revealRange( 379 | new vscode.Range(sourceLine, start, sourceLine + 1, 0), 380 | vscode.TextEditorRevealType.AtTop); 381 | } 382 | } 383 | 384 | private async onDidClickPreview(line: number): Promise { 385 | for (const visibleEditor of vscode.window.visibleTextEditors) { 386 | if (this.isPreviewOf(visibleEditor.document.uri)) { 387 | const editor = await vscode.window.showTextDocument(visibleEditor.document, visibleEditor.viewColumn); 388 | const position = new vscode.Position(line, 0); 389 | editor.selection = new vscode.Selection(position, position); 390 | return; 391 | } 392 | } 393 | 394 | vscode.workspace.openTextDocument(this._resource).then(vscode.window.showTextDocument); 395 | } 396 | } 397 | 398 | export interface PreviewSettings { 399 | readonly resourceColumn: vscode.ViewColumn; 400 | readonly previewColumn: vscode.ViewColumn; 401 | readonly locked: boolean; 402 | } 403 | -------------------------------------------------------------------------------- /src/features/previewConfig.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | export class HTMLPreviewConfiguration { 9 | public static getForResource(resource: vscode.Uri) { 10 | return new HTMLPreviewConfiguration(resource); 11 | } 12 | 13 | public readonly scrollBeyondLastLine: boolean; 14 | public readonly wordWrap: boolean; 15 | public readonly previewFrontMatter: string; 16 | public readonly lineBreaks: boolean; 17 | public readonly doubleClickToSwitchToEditor: boolean; 18 | public readonly scrollEditorWithPreview: boolean; 19 | public readonly scrollPreviewWithEditor: boolean; 20 | public readonly markEditorSelection: boolean; 21 | 22 | public readonly styles: string[]; 23 | 24 | private constructor(resource: vscode.Uri) { 25 | const editorConfig = vscode.workspace.getConfiguration('editor', resource); 26 | const htmlConfig = vscode.workspace.getConfiguration('html', resource); 27 | const htmlEditorConfig = vscode.workspace.getConfiguration('[html]', resource); 28 | 29 | this.scrollBeyondLastLine = editorConfig.get('scrollBeyondLastLine', false); 30 | 31 | this.wordWrap = editorConfig.get('wordWrap', 'off') !== 'off'; 32 | if (htmlEditorConfig && htmlEditorConfig['editor.wordWrap']) { 33 | this.wordWrap = htmlEditorConfig['editor.wordWrap'] !== 'off'; 34 | } 35 | 36 | this.previewFrontMatter = htmlConfig.get('previewFrontMatter', 'hide'); 37 | this.scrollPreviewWithEditor = !!htmlConfig.get('preview.scrollPreviewWithEditor', true); 38 | this.scrollEditorWithPreview = !!htmlConfig.get('preview.scrollEditorWithPreview', true); 39 | this.lineBreaks = !!htmlConfig.get('preview.breaks', false); 40 | this.doubleClickToSwitchToEditor = !!htmlConfig.get('preview.doubleClickToSwitchToEditor', true); 41 | this.markEditorSelection = !!htmlConfig.get('preview.markEditorSelection', true); 42 | 43 | this.styles = htmlConfig.get('styles', []); 44 | } 45 | 46 | public isEqualTo(otherConfig: HTMLPreviewConfiguration) { 47 | for (let key in this) { 48 | if (this.hasOwnProperty(key) && key !== 'styles') { 49 | if (this[key] !== otherConfig[key]) { 50 | return false; 51 | } 52 | } 53 | } 54 | 55 | // Check styles 56 | if (this.styles.length !== otherConfig.styles.length) { 57 | return false; 58 | } 59 | for (let i = 0; i < this.styles.length; ++i) { 60 | if (this.styles[i] !== otherConfig.styles[i]) { 61 | return false; 62 | } 63 | } 64 | 65 | return true; 66 | } 67 | 68 | [key: string]: any; 69 | } 70 | 71 | export class HTMLPreviewConfigurationManager { 72 | private readonly previewConfigurationsForWorkspaces = new Map(); 73 | 74 | public loadAndCacheConfiguration( 75 | resource: vscode.Uri 76 | ): HTMLPreviewConfiguration { 77 | const config = HTMLPreviewConfiguration.getForResource(resource); 78 | this.previewConfigurationsForWorkspaces.set(this.getKey(resource), config); 79 | return config; 80 | } 81 | 82 | public hasConfigurationChanged( 83 | resource: vscode.Uri 84 | ): boolean { 85 | const key = this.getKey(resource); 86 | const currentConfig = this.previewConfigurationsForWorkspaces.get(key); 87 | const newConfig = HTMLPreviewConfiguration.getForResource(resource); 88 | return (!currentConfig || !currentConfig.isEqualTo(newConfig)); 89 | } 90 | 91 | private getKey( 92 | resource: vscode.Uri 93 | ): string { 94 | const folder = vscode.workspace.getWorkspaceFolder(resource); 95 | return folder ? folder.uri.toString() : ''; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/features/previewContentProvider.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as path from 'path'; 8 | 9 | import * as nls from 'vscode-nls'; 10 | const localize = nls.loadMessageBundle(); 11 | 12 | import { Logger } from '../logger'; 13 | import { ContentSecurityPolicyArbiter, HTMLPreviewSecurityLevel } from '../security'; 14 | import { HTMLPreviewConfigurationManager, HTMLPreviewConfiguration } from './previewConfig'; 15 | import * as cheerio from "cheerio"; 16 | 17 | /** 18 | * Strings used inside the html preview. 19 | * 20 | * Stored here and then injected in the preview so that they 21 | * can be localized using our normal localization process. 22 | */ 23 | const previewStrings = { 24 | cspAlertMessageText: localize( 25 | 'preview.securityMessage.text', 26 | 'Some content has been disabled in this document'), 27 | 28 | cspAlertMessageTitle: localize( 29 | 'preview.securityMessage.title', 30 | 'Potentially unsafe or insecure content has been disabled in the html preview. Change the HTML preview security setting to allow insecure content or enable scripts'), 31 | 32 | cspAlertMessageLabel: localize( 33 | 'preview.securityMessage.label', 34 | 'Content Disabled Security Warning') 35 | }; 36 | 37 | export class HTMLContentProvider { 38 | constructor( 39 | private readonly context: vscode.ExtensionContext, 40 | private readonly cspArbiter: ContentSecurityPolicyArbiter, 41 | private readonly logger: Logger 42 | ) { } 43 | 44 | private readonly TAG_RegEx = /^\s*?\<(p|h[1-6]|img|code|pre|blockquote|li|dt|dd|td|th)((\s+.*?)(class="(.*?)")(.*?\>)|\>|\>|\/\>|\s+.*?\>)/; 45 | 46 | public provideTextDocumentContent( 47 | htmlDocument: vscode.TextDocument, 48 | previewConfigurations: HTMLPreviewConfigurationManager, 49 | initialLine: number | undefined = undefined, 50 | state?: any 51 | ): string { 52 | const sourceUri = htmlDocument.uri; 53 | const config = previewConfigurations.loadAndCacheConfiguration(sourceUri); 54 | const initialData = { 55 | source: sourceUri.toString(), 56 | line: initialLine, 57 | lineCount: htmlDocument.lineCount, 58 | scrollPreviewWithEditor: config.scrollPreviewWithEditor, 59 | scrollEditorWithPreview: config.scrollEditorWithPreview, 60 | doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor, 61 | disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings() 62 | }; 63 | 64 | this.logger.log('provideTextDocumentContent', initialData); 65 | 66 | // Content Security Policy 67 | const nonce = new Date().getTime() + '' + new Date().getMilliseconds(); 68 | const csp = this.getCspForResource(sourceUri, nonce); 69 | 70 | const parsedDoc = htmlDocument.getText().split("\n").map((l,i) => 71 | l.replace(this.TAG_RegEx, ( 72 | match: string, p1: string, p2: string, p3: string, 73 | p4: string, p5: string, p6: string, offset: number) => 74 | match.replace(match, typeof p5 !== "string" ? 75 | `<${p1} class="code-line" data-line="${i}" ${p2}` : 76 | `<${p1} ${p3} class="${p5} code-line" data-line="${i}" ${p6}`)) 77 | ).join("\n"); 78 | const $ = cheerio.load(parsedDoc); 79 | $("head").prepend(` 80 | ${csp} 81 | 85 | 86 | 87 | 88 | ${this.getStyles(sourceUri, config)} 89 | `); 90 | $("body").addClass(`vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}`); 91 | return $.html(); 92 | } 93 | 94 | private extensionResourcePath(mediaFile: string): string { 95 | return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))) 96 | .with({ scheme: 'vscode-resource' }) 97 | .toString(); 98 | } 99 | 100 | private fixHref(resource: vscode.Uri, href: string): string { 101 | if (!href) { 102 | return href; 103 | } 104 | 105 | // Use href if it is already an URL 106 | const hrefUri = vscode.Uri.parse(href); 107 | if (['http', 'https'].indexOf(hrefUri.scheme) >= 0) { 108 | return hrefUri.toString(); 109 | } 110 | 111 | // Use href as file URI if it is absolute 112 | if (path.isAbsolute(href) || hrefUri.scheme === 'file') { 113 | return vscode.Uri.file(href) 114 | .with({ scheme: 'vscode-resource' }) 115 | .toString(); 116 | } 117 | 118 | // Use a workspace relative path if there is a workspace 119 | let root = vscode.workspace.getWorkspaceFolder(resource); 120 | if (root) { 121 | return vscode.Uri.file(path.join(root.uri.fsPath, href)) 122 | .with({ scheme: 'vscode-resource' }) 123 | .toString(); 124 | } 125 | 126 | // Otherwise look relative to the html file 127 | return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href)) 128 | .with({ scheme: 'vscode-resource' }) 129 | .toString(); 130 | } 131 | 132 | private getStyles(resource: vscode.Uri, config: HTMLPreviewConfiguration): string { 133 | if (Array.isArray(config.styles)) { 134 | return config.styles.map(style => { 135 | return ``; 136 | }).join('\n'); 137 | } 138 | return ''; 139 | } 140 | 141 | private getCspForResource(resource: vscode.Uri, nonce: string): string { 142 | switch (this.cspArbiter.getSecurityLevelForResource(resource)) { 143 | case HTMLPreviewSecurityLevel.AllowInsecureContent: 144 | return ``; 145 | 146 | case HTMLPreviewSecurityLevel.AllowInsecureLocalContent: 147 | return ``; 148 | 149 | case HTMLPreviewSecurityLevel.AllowScriptsAndAllContent: 150 | return ''; 151 | 152 | case HTMLPreviewSecurityLevel.Strict: 153 | default: 154 | return ``; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/features/previewManager.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | import { Logger } from '../logger'; 8 | import { disposeAll } from '../util/dispose'; 9 | import { HTMLFileTopmostLineMonitor } from '../util/topmostLineMonitor'; 10 | import { HTMLPreview, PreviewSettings } from './preview'; 11 | import { HTMLPreviewConfigurationManager } from './previewConfig'; 12 | import { HTMLContentProvider } from './previewContentProvider'; 13 | 14 | 15 | export class HTMLPreviewManager implements vscode.WebviewPanelSerializer { 16 | private static readonly htmlPreviewActiveContextKey = 'htmlPreviewFocus'; 17 | 18 | private readonly _topmostLineMonitor = new HTMLFileTopmostLineMonitor(); 19 | private readonly _previewConfigurations = new HTMLPreviewConfigurationManager(); 20 | private readonly _previews: HTMLPreview[] = []; 21 | private _activePreview: HTMLPreview | undefined = undefined; 22 | private readonly _disposables: vscode.Disposable[] = []; 23 | 24 | public constructor( 25 | private readonly _contentProvider: HTMLContentProvider, 26 | private readonly _logger: Logger, 27 | ) { 28 | this._disposables.push(vscode.window.registerWebviewPanelSerializer(HTMLPreview.viewType, this)); 29 | } 30 | 31 | public dispose(): void { 32 | disposeAll(this._disposables); 33 | disposeAll(this._previews); 34 | } 35 | 36 | public refresh() { 37 | for (const preview of this._previews) { 38 | preview.refresh(); 39 | } 40 | } 41 | 42 | public updateConfiguration() { 43 | for (const preview of this._previews) { 44 | preview.updateConfiguration(); 45 | } 46 | } 47 | 48 | public preview( 49 | resource: vscode.Uri, 50 | previewSettings: PreviewSettings 51 | ): void { 52 | let preview = this.getExistingPreview(resource, previewSettings); 53 | if (preview) { 54 | preview.reveal(previewSettings.previewColumn); 55 | } else { 56 | preview = this.createNewPreview(resource, previewSettings); 57 | } 58 | 59 | preview.update(resource); 60 | } 61 | 62 | public get activePreviewResource() { 63 | return this._activePreview && this._activePreview.resource; 64 | } 65 | 66 | public toggleLock() { 67 | const preview = this._activePreview; 68 | if (preview) { 69 | preview.toggleLock(); 70 | 71 | // Close any previews that are now redundant, such as having two dynamic previews in the same editor group 72 | for (const otherPreview of this._previews) { 73 | if (otherPreview !== preview && preview.matches(otherPreview)) { 74 | otherPreview.dispose(); 75 | } 76 | } 77 | } 78 | } 79 | 80 | public async deserializeWebviewPanel( 81 | webview: vscode.WebviewPanel, 82 | state: any 83 | ): Promise { 84 | const preview = await HTMLPreview.revive( 85 | webview, 86 | state, 87 | this._contentProvider, 88 | this._previewConfigurations, 89 | this._logger, 90 | this._topmostLineMonitor); 91 | 92 | this.registerPreview(preview); 93 | } 94 | 95 | private getExistingPreview( 96 | resource: vscode.Uri, 97 | previewSettings: PreviewSettings 98 | ): HTMLPreview | undefined { 99 | return this._previews.find(preview => 100 | preview.matchesResource(resource, previewSettings.previewColumn, previewSettings.locked)); 101 | } 102 | 103 | private createNewPreview( 104 | resource: vscode.Uri, 105 | previewSettings: PreviewSettings 106 | ): HTMLPreview { 107 | const preview = HTMLPreview.create( 108 | resource, 109 | previewSettings.previewColumn, 110 | previewSettings.locked, 111 | this._contentProvider, 112 | this._previewConfigurations, 113 | this._logger, 114 | this._topmostLineMonitor); 115 | 116 | this.setPreviewActiveContext(true); 117 | this._activePreview = preview; 118 | return this.registerPreview(preview); 119 | } 120 | 121 | private registerPreview( 122 | preview: HTMLPreview 123 | ): HTMLPreview { 124 | this._previews.push(preview); 125 | 126 | preview.onDispose(() => { 127 | const existing = this._previews.indexOf(preview); 128 | if (existing === -1) { 129 | return; 130 | } 131 | 132 | this._previews.splice(existing, 1); 133 | if (this._activePreview === preview) { 134 | this.setPreviewActiveContext(false); 135 | this._activePreview = undefined; 136 | } 137 | }); 138 | 139 | preview.onDidChangeViewState(({ webviewPanel }) => { 140 | disposeAll(this._previews.filter(otherPreview => preview !== otherPreview && preview!.matches(otherPreview))); 141 | this.setPreviewActiveContext(webviewPanel.active); 142 | this._activePreview = webviewPanel.active ? preview : undefined; 143 | }); 144 | 145 | return preview; 146 | } 147 | 148 | private setPreviewActiveContext(value: boolean) { 149 | vscode.commands.executeCommand('setContext', HTMLPreviewManager.htmlPreviewActiveContextKey, value); 150 | } 151 | } 152 | 153 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | import { lazy } from './util/lazy'; 8 | 9 | enum Trace { 10 | Off, 11 | Verbose 12 | } 13 | 14 | namespace Trace { 15 | export function fromString(value: string): Trace { 16 | value = value.toLowerCase(); 17 | switch (value) { 18 | case 'off': 19 | return Trace.Off; 20 | case 'verbose': 21 | return Trace.Verbose; 22 | default: 23 | return Trace.Off; 24 | } 25 | } 26 | } 27 | 28 | 29 | function isString(value: any): value is string { 30 | return Object.prototype.toString.call(value) === '[object String]'; 31 | } 32 | 33 | export class Logger { 34 | private trace?: Trace; 35 | 36 | private readonly outputChannel = lazy(() => vscode.window.createOutputChannel('HTML')); 37 | 38 | constructor() { 39 | this.updateConfiguration(); 40 | } 41 | 42 | public log(message: string, data?: any): void { 43 | if (this.trace === Trace.Verbose) { 44 | this.appendLine(`[Log - ${(new Date().toLocaleTimeString())}] ${message}`); 45 | if (data) { 46 | this.appendLine(Logger.data2String(data)); 47 | } 48 | } 49 | } 50 | 51 | public updateConfiguration() { 52 | this.trace = this.readTrace(); 53 | } 54 | 55 | private appendLine(value: string) { 56 | return this.outputChannel.value.appendLine(value); 57 | } 58 | 59 | private readTrace(): Trace { 60 | return Trace.fromString(vscode.workspace.getConfiguration().get('html.trace', 'off')); 61 | } 62 | 63 | private static data2String(data: any): string { 64 | if (data instanceof Error) { 65 | if (isString(data.stack)) { 66 | return data.stack; 67 | } 68 | return (data as Error).message; 69 | } 70 | if (isString(data)) { 71 | return data; 72 | } 73 | return JSON.stringify(data, undefined, 2); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/security.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | import { HTMLPreviewManager } from './features/previewManager'; 9 | 10 | import * as nls from 'vscode-nls'; 11 | 12 | const localize = nls.loadMessageBundle(); 13 | 14 | export enum HTMLPreviewSecurityLevel { 15 | Strict = 0, 16 | AllowInsecureContent = 1, 17 | AllowScriptsAndAllContent = 2, 18 | AllowInsecureLocalContent = 3 19 | } 20 | 21 | export interface ContentSecurityPolicyArbiter { 22 | getSecurityLevelForResource(resource: vscode.Uri): HTMLPreviewSecurityLevel; 23 | 24 | setSecurityLevelForResource(resource: vscode.Uri, level: HTMLPreviewSecurityLevel): Thenable; 25 | 26 | shouldAllowSvgsForResource(resource: vscode.Uri): void; 27 | 28 | shouldDisableSecurityWarnings(): boolean; 29 | 30 | setShouldDisableSecurityWarning(shouldShow: boolean): Thenable; 31 | } 32 | 33 | export class ExtensionContentSecurityPolicyArbiter implements ContentSecurityPolicyArbiter { 34 | private readonly old_trusted_workspace_key = 'trusted_preview_workspace:'; 35 | private readonly security_level_key = 'preview_security_level:'; 36 | private readonly should_disable_security_warning_key = 'preview_should_show_security_warning:'; 37 | 38 | constructor( 39 | private readonly globalState: vscode.Memento, 40 | private readonly workspaceState: vscode.Memento 41 | ) { } 42 | 43 | public getSecurityLevelForResource(resource: vscode.Uri): HTMLPreviewSecurityLevel { 44 | // Use new security level setting first 45 | const level = this.globalState.get(this.security_level_key + this.getRoot(resource), undefined); 46 | if (typeof level !== 'undefined') { 47 | return level; 48 | } 49 | 50 | // Fallback to old trusted workspace setting 51 | if (this.globalState.get(this.old_trusted_workspace_key + this.getRoot(resource), false)) { 52 | return HTMLPreviewSecurityLevel.AllowScriptsAndAllContent; 53 | } 54 | return HTMLPreviewSecurityLevel.Strict; 55 | } 56 | 57 | public setSecurityLevelForResource(resource: vscode.Uri, level: HTMLPreviewSecurityLevel): Thenable { 58 | return this.globalState.update(this.security_level_key + this.getRoot(resource), level); 59 | } 60 | 61 | public shouldAllowSvgsForResource(resource: vscode.Uri) { 62 | const securityLevel = this.getSecurityLevelForResource(resource); 63 | return securityLevel === HTMLPreviewSecurityLevel.AllowInsecureContent || securityLevel === HTMLPreviewSecurityLevel.AllowScriptsAndAllContent; 64 | } 65 | 66 | public shouldDisableSecurityWarnings(): boolean { 67 | return this.workspaceState.get(this.should_disable_security_warning_key, false); 68 | } 69 | 70 | public setShouldDisableSecurityWarning(disabled: boolean): Thenable { 71 | return this.workspaceState.update(this.should_disable_security_warning_key, disabled); 72 | } 73 | 74 | private getRoot(resource: vscode.Uri): vscode.Uri { 75 | if (vscode.workspace.workspaceFolders) { 76 | const folderForResource = vscode.workspace.getWorkspaceFolder(resource); 77 | if (folderForResource) { 78 | return folderForResource.uri; 79 | } 80 | 81 | if (vscode.workspace.workspaceFolders.length) { 82 | return vscode.workspace.workspaceFolders[0].uri; 83 | } 84 | } 85 | 86 | return resource; 87 | } 88 | } 89 | 90 | export class PreviewSecuritySelector { 91 | 92 | public constructor( 93 | private readonly cspArbiter: ContentSecurityPolicyArbiter, 94 | private readonly webviewManager: HTMLPreviewManager 95 | ) { } 96 | 97 | public async showSecuritySelectorForResource(resource: vscode.Uri): Promise { 98 | interface PreviewSecurityPickItem extends vscode.QuickPickItem { 99 | readonly type: 'moreinfo' | 'toggle' | HTMLPreviewSecurityLevel; 100 | } 101 | 102 | function markActiveWhen(when: boolean): string { 103 | return when ? '• ' : ''; 104 | } 105 | 106 | const currentSecurityLevel = this.cspArbiter.getSecurityLevelForResource(resource); 107 | const selection = await vscode.window.showQuickPick( 108 | [ 109 | { 110 | type: HTMLPreviewSecurityLevel.Strict, 111 | label: markActiveWhen(currentSecurityLevel === HTMLPreviewSecurityLevel.Strict) + localize('strict.title', 'Strict'), 112 | description: localize('strict.description', 'Only load secure content'), 113 | }, { 114 | type: HTMLPreviewSecurityLevel.AllowInsecureLocalContent, 115 | label: markActiveWhen(currentSecurityLevel === HTMLPreviewSecurityLevel.AllowInsecureLocalContent) + localize('insecureLocalContent.title', 'Allow insecure local content'), 116 | description: localize('insecureLocalContent.description', 'Enable loading content over http served from localhost'), 117 | }, { 118 | type: HTMLPreviewSecurityLevel.AllowInsecureContent, 119 | label: markActiveWhen(currentSecurityLevel === HTMLPreviewSecurityLevel.AllowInsecureContent) + localize('insecureContent.title', 'Allow insecure content'), 120 | description: localize('insecureContent.description', 'Enable loading content over http'), 121 | }, { 122 | type: HTMLPreviewSecurityLevel.AllowScriptsAndAllContent, 123 | label: markActiveWhen(currentSecurityLevel === HTMLPreviewSecurityLevel.AllowScriptsAndAllContent) + localize('disable.title', 'Disable'), 124 | description: localize('disable.description', 'Allow all content and script execution. Not recommended'), 125 | }, { 126 | type: 'moreinfo', 127 | label: localize('moreInfo.title', 'More Information'), 128 | description: '' 129 | }, { 130 | type: 'toggle', 131 | label: this.cspArbiter.shouldDisableSecurityWarnings() 132 | ? localize('enableSecurityWarning.title', "Enable preview security warnings in this workspace") 133 | : localize('disableSecurityWarning.title', "Disable preview security warning in this workspace"), 134 | description: localize('toggleSecurityWarning.description', 'Does not affect the content security level') 135 | }, 136 | ], { 137 | placeHolder: localize( 138 | 'preview.showPreviewSecuritySelector.title', 139 | 'Select security settings for HTML previews in this workspace'), 140 | }); 141 | if (!selection) { 142 | return; 143 | } 144 | 145 | if (selection.type === 'moreinfo') { 146 | vscode.commands.executeCommand('vscode.open', vscode.Uri.parse('https://go.microsoft.com/fwlink/?linkid=854414')); 147 | return; 148 | } 149 | 150 | if (selection.type === 'toggle') { 151 | this.cspArbiter.setShouldDisableSecurityWarning(!this.cspArbiter.shouldDisableSecurityWarnings()); 152 | return; 153 | } else { 154 | await this.cspArbiter.setSecurityLevelForResource(resource, selection.type); 155 | } 156 | this.webviewManager.refresh(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/test/inMemoryDocument.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | export class InMemoryDocument implements vscode.TextDocument { 9 | private readonly _lines: string[]; 10 | 11 | constructor( 12 | public readonly uri: vscode.Uri, 13 | private readonly _contents: string 14 | ) { 15 | this._lines = this._contents.split(/\n/g); 16 | } 17 | 18 | 19 | isUntitled: boolean = false; 20 | languageId: string = ''; 21 | version: number = 1; 22 | isDirty: boolean = false; 23 | isClosed: boolean = false; 24 | eol: vscode.EndOfLine = vscode.EndOfLine.LF; 25 | 26 | get fileName(): string { 27 | return this.uri.fsPath; 28 | } 29 | 30 | get lineCount(): number { 31 | return this._lines.length; 32 | } 33 | 34 | lineAt(line: any): vscode.TextLine { 35 | return { 36 | lineNumber: line, 37 | text: this._lines[line], 38 | range: new vscode.Range(0, 0, 0, 0), 39 | firstNonWhitespaceCharacterIndex: 0, 40 | rangeIncludingLineBreak: new vscode.Range(0, 0, 0, 0), 41 | isEmptyOrWhitespace: false 42 | }; 43 | } 44 | offsetAt(_position: vscode.Position): never { 45 | throw new Error('Method not implemented.'); 46 | } 47 | positionAt(offset: number): vscode.Position { 48 | const before = this._contents.slice(0, offset); 49 | const newLines = before.match(/\n/g); 50 | const line = newLines ? newLines.length : 0; 51 | const preCharacters = before.match(/(\n|^).*$/g); 52 | return new vscode.Position(line, preCharacters ? preCharacters[0].length : 0); 53 | } 54 | getText(_range?: vscode.Range | undefined): string { 55 | return this._contents; 56 | } 57 | getWordRangeAtPosition(_position: vscode.Position, _regex?: RegExp | undefined): never { 58 | throw new Error('Method not implemented.'); 59 | } 60 | validateRange(_range: vscode.Range): never { 61 | throw new Error('Method not implemented.'); 62 | } 63 | validatePosition(_position: vscode.Position): never { 64 | throw new Error('Method not implemented.'); 65 | } 66 | save(): never { 67 | throw new Error('Method not implemented.'); 68 | } 69 | } -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as path from"path"; 7 | import * as testRunner from"vscode/lib/testrunner"; 8 | 9 | const suite = "Integration Markdown Tests"; 10 | 11 | const options: any = { 12 | ui: "tdd", 13 | useColors: true, 14 | timeout: 60000 15 | }; 16 | 17 | // options.reporter = "mocha-multi-reporters"; 18 | // options.reporterOptions = { 19 | // reporterEnabled: "spec, mocha-junit-reporter", 20 | // mochaJunitReporterReporterOptions: { 21 | // testsuitesTitle: `${suite} ${process.platform}`, 22 | // mochaFile: path.join( 23 | // "test-results", 24 | // `test-results/${process.platform}-${suite 25 | // .toLowerCase() 26 | // .replace(/[^\w]/g, "-")}-results.xml` 27 | // ) 28 | // } 29 | // }; 30 | 31 | testRunner.configure(options); 32 | 33 | export = testRunner; 34 | -------------------------------------------------------------------------------- /src/util/dispose.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | export function disposeAll(disposables: vscode.Disposable[]) { 9 | while (disposables.length) { 10 | const item = disposables.pop(); 11 | if (item) { 12 | item.dispose(); 13 | } 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/util/file.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | 8 | export function isHTMLFile(document: vscode.TextDocument) { 9 | return document.languageId === 'html'; 10 | } -------------------------------------------------------------------------------- /src/util/lazy.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | export interface Lazy { 7 | readonly value: T; 8 | readonly hasValue: boolean; 9 | map(f: (x: T) => R): Lazy; 10 | } 11 | 12 | class LazyValue implements Lazy { 13 | private _hasValue: boolean = false; 14 | private _value?: T; 15 | 16 | constructor( 17 | private readonly _getValue: () => T 18 | ) { } 19 | 20 | get value(): T { 21 | if (!this._hasValue) { 22 | this._hasValue = true; 23 | this._value = this._getValue(); 24 | } 25 | return this._value!; 26 | } 27 | 28 | get hasValue(): boolean { 29 | return this._hasValue; 30 | } 31 | 32 | public map(f: (x: T) => R): Lazy { 33 | return new LazyValue(() => f(this.value)); 34 | } 35 | } 36 | 37 | export function lazy(getValue: () => T): Lazy { 38 | return new LazyValue(getValue); 39 | } -------------------------------------------------------------------------------- /src/util/topmostLineMonitor.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as vscode from 'vscode'; 7 | import { disposeAll } from '../util/dispose'; 8 | import { isHTMLFile } from './file'; 9 | 10 | export class HTMLFileTopmostLineMonitor { 11 | private readonly disposables: vscode.Disposable[] = []; 12 | 13 | private readonly pendingUpdates = new Map(); 14 | 15 | private readonly throttle = 50; 16 | 17 | constructor() { 18 | vscode.window.onDidChangeTextEditorVisibleRanges(event => { 19 | if (isHTMLFile(event.textEditor.document)) { 20 | const line = getVisibleLine(event.textEditor); 21 | if (typeof line === 'number') { 22 | this.updateLine(event.textEditor.document.uri, line); 23 | } 24 | } 25 | }, null, this.disposables); 26 | } 27 | 28 | dispose() { 29 | disposeAll(this.disposables); 30 | } 31 | 32 | private readonly _onDidChangeTopmostLineEmitter = new vscode.EventEmitter<{ resource: vscode.Uri, line: number }>(); 33 | public readonly onDidChangeTopmostLine = this._onDidChangeTopmostLineEmitter.event; 34 | 35 | private updateLine( 36 | resource: vscode.Uri, 37 | line: number 38 | ) { 39 | const key = resource.toString(); 40 | if (!this.pendingUpdates.has(key)) { 41 | // schedule update 42 | setTimeout(() => { 43 | if (this.pendingUpdates.has(key)) { 44 | this._onDidChangeTopmostLineEmitter.fire({ 45 | resource, 46 | line: this.pendingUpdates.get(key) as number 47 | }); 48 | this.pendingUpdates.delete(key); 49 | } 50 | }, this.throttle); 51 | } 52 | 53 | this.pendingUpdates.set(key, line); 54 | } 55 | } 56 | 57 | /** 58 | * Get the top-most visible range of `editor`. 59 | * 60 | * Returns a fractional line number based the visible character within the line. 61 | * Floor to get real line number 62 | */ 63 | export function getVisibleLine( 64 | editor: vscode.TextEditor 65 | ): number | undefined { 66 | if (!editor.visibleRanges.length) { 67 | return undefined; 68 | } 69 | 70 | const firstVisiblePosition = editor.visibleRanges[0].start; 71 | const lineNumber = firstVisiblePosition.line; 72 | const line = editor.document.lineAt(lineNumber); 73 | const progress = firstVisiblePosition.character / (line.text.length + 2); 74 | return lineNumber + progress; 75 | } 76 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2016", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation: */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | "outDir": "./out", /* Redirect output structure to the directory. */ 14 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | "strict": true /* Enable all strict type-checking options. */ 23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | // "strictNullChecks": true, /* Enable strict null checks. */ 25 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 26 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 27 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 28 | 29 | /* Additional Checks */ 30 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 31 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 32 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 33 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 34 | 35 | /* Module Resolution Options */ 36 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 37 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 38 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 39 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 40 | // "typeRoots": [], /* List of folders to include type definitions from. */ 41 | // "types": [], /* Type declaration files to be included in compilation. */ 42 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 43 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 44 | 45 | /* Source Map Options */ 46 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 47 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 48 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 49 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 50 | 51 | /* Experimental Options */ 52 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 53 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 54 | }, 55 | "include": [ 56 | "src/**/*" 57 | ] 58 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | "no-duplicate-variable": true, 6 | "curly": true, 7 | "class-name": true, 8 | "semicolon": [ 9 | true, 10 | "always" 11 | ], 12 | "triple-equals": true 13 | }, 14 | "defaultSeverity": "warning" 15 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | const path = require('path'); 6 | 7 | module.exports = { 8 | mode: "development", 9 | entry: { 10 | index: './preview-src/index.ts', 11 | pre: './preview-src/pre.ts' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.tsx?$/, 17 | use: 'ts-loader', 18 | exclude: /node_modules/ 19 | } 20 | ] 21 | }, 22 | resolve: { 23 | extensions: ['.tsx', '.ts', '.js'] 24 | }, 25 | devtool: 'inline-source-map', 26 | output: { 27 | filename: '[name].js', 28 | path: path.resolve(__dirname, 'media') 29 | } 30 | }; --------------------------------------------------------------------------------