├── .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 |
--------------------------------------------------------------------------------
/media/PreviewOnRightPane_16x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/media/PreviewOnRightPane_16x_dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/media/Preview_inverse.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------