├── .editorconfig ├── .gitignore ├── .prettierrc.js ├── .vscode ├── launch.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── azure-pipeline.stable.yml ├── icon.png ├── images ├── modify-slide-type.png └── slide-types.png ├── package-lock.json ├── package.json ├── src ├── extension.ts ├── json.ts └── slideshow.ts ├── tsconfig.json ├── vscode.proposed.notebookWorkspaceEdit.d.ts └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Tab indentation 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | # The indent size used in the `package.json` file cannot be changed 14 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 15 | [{.travis.yml,npm-shrinkwrap.json,package.json}] 16 | indent_style = space 17 | indent_size = 4 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | out/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | printWidth: 120, 4 | tabWidth: 4, 5 | endOfLine: 'auto', 6 | trailingComma: 'none', 7 | }; 8 | -------------------------------------------------------------------------------- /.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": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}", 19 | }, 20 | { 21 | "name": "Run Extension (Web)", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionDevelopmentKind=web" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}", 32 | }, 33 | { 34 | "name": "Extension Tests", 35 | "type": "extensionHost", 36 | "request": "launch", 37 | "args": [ 38 | "--extensionDevelopmentPath=${workspaceFolder}", 39 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 40 | ], 41 | "outFiles": [ 42 | "${workspaceFolder}/out/test/**/*.js" 43 | ], 44 | "preLaunchTask": "${defaultBuildTask}" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /.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 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 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 | # Jupyter Slide Show support in VS Code 2 | 3 | This extension provides support for adding slide types to notebook cells for working with tools like [`nbconvert`](https://github.com/jupyter/nbconvert) to help easily convert your `.ipynb` file into a slide show. Supported slide types are: 4 | ![Slide types](images/slide-types.png) 5 | 6 | Support for additional and more flexible cell tagging is provided by the [Jupyter Cell Tags](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.vscode-jupyter-cell-tags) extension. 7 | 8 | ### Features: 9 | - Add a slide type to the cell you're on by opening the Command Palette (`Cmd+Shift+P`) and selecting **Switch Slide Type** 10 | - Modify slide types for notebook cells by selecting the slide type on the cell ![Modify slide type](images/modify-slide-type.png) 11 | - Add or modify slide type for multiple cells by editing the notebook's metadata (JSON format) by opening the Command Palette (`Cmd+Shift+P`) and selecting **Edit Slide Type (JSON)** 12 | 13 | ### Usage: 14 | After assigning slide types to your cells, create an HTML slideshow presentation by opening the integrated terminal and running the command, `jupyter nbconvert '.ipynb' --to slides --post serve`. 15 | 16 | This extension comes with the [Jupyter extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) and can be disabled or uninstalled. 17 | 18 | ## Contributing 19 | 20 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 21 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 22 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 23 | 24 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 25 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 26 | provided by the bot. You will only need to do this once across all repos using our CLA. 27 | 28 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 29 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 30 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 31 | 32 | ## Trademarks 33 | 34 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 35 | trademarks or logos is subject to and must follow 36 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 37 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 38 | Any use of third-party trademarks or logos are subject to those third-party's policies. 39 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. 7 | - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /azure-pipeline.stable.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - main 5 | pr: none 6 | 7 | resources: 8 | repositories: 9 | - repository: templates 10 | type: github 11 | name: microsoft/vscode-engineering 12 | ref: main 13 | endpoint: Monaco 14 | 15 | parameters: 16 | - name: publishExtension 17 | displayName: 🚀 Publish Extension 18 | type: boolean 19 | default: false 20 | 21 | extends: 22 | template: azure-pipelines/extension/stable.yml@templates 23 | parameters: 24 | publishExtension: ${{ parameters.publishExtension }} 25 | buildSteps: 26 | - script: npm i -g npm@8.15.1 27 | displayName: npm install npm@8.15.1 28 | 29 | - script: npm ci 30 | displayName: Install dependencies 31 | 32 | - script: npm run compile 33 | displayName: Compile 34 | 35 | tsa: 36 | config: 37 | areaPath: 'Visual Studio Code Jupyter Extensions' 38 | serviceTreeID: '14f24efd-b502-422a-9f40-09ea7ce9cf14' 39 | enabled: true 40 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-jupyter-slideshow/dd2de712e89ca65f2fbe912f71c1133b52e3f252/icon.png -------------------------------------------------------------------------------- /images/modify-slide-type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-jupyter-slideshow/dd2de712e89ca65f2fbe912f71c1133b52e3f252/images/modify-slide-type.png -------------------------------------------------------------------------------- /images/slide-types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/vscode-jupyter-slideshow/dd2de712e89ca65f2fbe912f71c1133b52e3f252/images/slide-types.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-jupyter-slideshow", 3 | "displayName": "Jupyter Slide Show", 4 | "description": "Jupyter Slide Show support for VS Code", 5 | "version": "0.1.6", 6 | "publisher": "ms-toolsai", 7 | "preview": true, 8 | "icon": "icon.png", 9 | "galleryBanner": { 10 | "color": "#ffffff", 11 | "theme": "light" 12 | }, 13 | "author": { 14 | "name": "Microsoft Corporation" 15 | }, 16 | "engines": { 17 | "vscode": "^1.88.0" 18 | }, 19 | "categories": [ 20 | "Notebooks" 21 | ], 22 | "activationEvents": [ 23 | "onNotebook:jupyter-notebook", 24 | "onCommand:jupyter-slideshow.switchSlideType", 25 | "onCommand:jupyter-slideshow.editSlideShowInJSON" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/Microsoft/vscode-jupyter-slideshow" 30 | }, 31 | "main": "./out/extension-node.js", 32 | "browser": "./out/extension-web.js", 33 | "contributes": { 34 | "commands": [ 35 | { 36 | "command": "jupyter-slideshow.switchSlideType", 37 | "title": "Switch Slide Type" 38 | }, 39 | { 40 | "command": "jupyter-slideshow.editSlideShowInJSON", 41 | "title": "Edit Slide Type (JSON)", 42 | "icon": "$(go-to-file)" 43 | } 44 | ], 45 | "menus": { 46 | "notebook/cell/title": [ 47 | { 48 | "command": "jupyter-slideshow.switchSlideType", 49 | "group": "jupyter-slideshow@1" 50 | }, 51 | { 52 | "command": "jupyter-slideshow.editSlideShowInJSON", 53 | "group": "jupyter-slideshow@2" 54 | } 55 | ] 56 | } 57 | }, 58 | "scripts": { 59 | "vscode:prepublish": "npm run compile", 60 | "compile": "webpack --mode none", 61 | "watch": "webpack --mode none --watch", 62 | "pretest": "npm run compile && npm run lint", 63 | "lint": "eslint src --ext ts", 64 | "test": "node ./out/test/runTest.js" 65 | }, 66 | "devDependencies": { 67 | "@types/vscode": "^1.71.0", 68 | "@types/glob": "^7.1.3", 69 | "@types/mocha": "^8.2.2", 70 | "@types/node": "14.x", 71 | "eslint": "^7.27.0", 72 | "@typescript-eslint/eslint-plugin": "^4.26.0", 73 | "@typescript-eslint/parser": "^4.26.0", 74 | "glob": "^7.1.7", 75 | "mocha": "^10.0.0", 76 | "typescript": "^4.3.2", 77 | "vscode-test": "^1.5.2", 78 | "ts-loader": "^9.1.1", 79 | "webpack": "^5.36.2", 80 | "webpack-cli": "^4.6.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from 'vscode'; 5 | import { register as registerSlideShow } from './slideshow'; 6 | 7 | export function activate(context: vscode.ExtensionContext) { 8 | registerSlideShow(context); 9 | } 10 | 11 | export function deactivate() {} 12 | -------------------------------------------------------------------------------- /src/json.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 const enum ScanError { 7 | None = 0, 8 | UnexpectedEndOfComment = 1, 9 | UnexpectedEndOfString = 2, 10 | UnexpectedEndOfNumber = 3, 11 | InvalidUnicode = 4, 12 | InvalidEscapeCharacter = 5, 13 | InvalidCharacter = 6 14 | } 15 | 16 | export const enum SyntaxKind { 17 | OpenBraceToken = 1, 18 | CloseBraceToken = 2, 19 | OpenBracketToken = 3, 20 | CloseBracketToken = 4, 21 | CommaToken = 5, 22 | ColonToken = 6, 23 | NullKeyword = 7, 24 | TrueKeyword = 8, 25 | FalseKeyword = 9, 26 | StringLiteral = 10, 27 | NumericLiteral = 11, 28 | LineCommentTrivia = 12, 29 | BlockCommentTrivia = 13, 30 | LineBreakTrivia = 14, 31 | Trivia = 15, 32 | Unknown = 16, 33 | EOF = 17 34 | } 35 | 36 | /** 37 | * The scanner object, representing a JSON scanner at a position in the input string. 38 | */ 39 | export interface JSONScanner { 40 | /** 41 | * Sets the scan position to a new offset. A call to 'scan' is needed to get the first token. 42 | */ 43 | setPosition(pos: number): void; 44 | /** 45 | * Read the next token. Returns the token code. 46 | */ 47 | scan(): SyntaxKind; 48 | /** 49 | * Returns the current scan position, which is after the last read token. 50 | */ 51 | getPosition(): number; 52 | /** 53 | * Returns the last read token. 54 | */ 55 | getToken(): SyntaxKind; 56 | /** 57 | * Returns the last read token value. The value for strings is the decoded string content. For numbers its of type number, for boolean it's true or false. 58 | */ 59 | getTokenValue(): string; 60 | /** 61 | * The start offset of the last read token. 62 | */ 63 | getTokenOffset(): number; 64 | /** 65 | * The length of the last read token. 66 | */ 67 | getTokenLength(): number; 68 | /** 69 | * An error code of the last scan. 70 | */ 71 | getTokenError(): ScanError; 72 | } 73 | 74 | 75 | 76 | export interface ParseError { 77 | error: ParseErrorCode; 78 | offset: number; 79 | length: number; 80 | } 81 | 82 | export const enum ParseErrorCode { 83 | InvalidSymbol = 1, 84 | InvalidNumberFormat = 2, 85 | PropertyNameExpected = 3, 86 | ValueExpected = 4, 87 | ColonExpected = 5, 88 | CommaExpected = 6, 89 | CloseBraceExpected = 7, 90 | CloseBracketExpected = 8, 91 | EndOfFileExpected = 9, 92 | InvalidCommentToken = 10, 93 | UnexpectedEndOfComment = 11, 94 | UnexpectedEndOfString = 12, 95 | UnexpectedEndOfNumber = 13, 96 | InvalidUnicode = 14, 97 | InvalidEscapeCharacter = 15, 98 | InvalidCharacter = 16 99 | } 100 | 101 | export type NodeType = 'object' | 'array' | 'property' | 'string' | 'number' | 'boolean' | 'null'; 102 | 103 | export interface Node { 104 | readonly type: NodeType; 105 | readonly value?: any; 106 | readonly offset: number; 107 | readonly length: number; 108 | readonly colonOffset?: number; 109 | readonly parent?: Node; 110 | readonly children?: Node[]; 111 | } 112 | 113 | export type Segment = string | number; 114 | export type JSONPath = Segment[]; 115 | 116 | export interface Location { 117 | /** 118 | * The previous property key or literal value (string, number, boolean or null) or undefined. 119 | */ 120 | previousNode?: Node; 121 | /** 122 | * The path describing the location in the JSON document. The path consists of a sequence strings 123 | * representing an object property or numbers for array indices. 124 | */ 125 | path: JSONPath; 126 | /** 127 | * Matches the locations path against a pattern consisting of strings (for properties) and numbers (for array indices). 128 | * '*' will match a single segment, of any property name or index. 129 | * '**' will match a sequence of segments or no segment, of any property name or index. 130 | */ 131 | matches: (patterns: JSONPath) => boolean; 132 | /** 133 | * If set, the location's offset is at a property key. 134 | */ 135 | isAtPropertyKey: boolean; 136 | } 137 | 138 | export interface ParseOptions { 139 | disallowComments?: boolean; 140 | allowTrailingComma?: boolean; 141 | allowEmptyContent?: boolean; 142 | } 143 | 144 | export namespace ParseOptions { 145 | export const DEFAULT = { 146 | allowTrailingComma: true 147 | }; 148 | } 149 | 150 | export interface JSONVisitor { 151 | /** 152 | * Invoked when an open brace is encountered and an object is started. The offset and length represent the location of the open brace. 153 | */ 154 | onObjectBegin?: (offset: number, length: number) => void; 155 | 156 | /** 157 | * Invoked when a property is encountered. The offset and length represent the location of the property name. 158 | */ 159 | onObjectProperty?: (property: string, offset: number, length: number) => void; 160 | 161 | /** 162 | * Invoked when a closing brace is encountered and an object is completed. The offset and length represent the location of the closing brace. 163 | */ 164 | onObjectEnd?: (offset: number, length: number) => void; 165 | 166 | /** 167 | * Invoked when an open bracket is encountered. The offset and length represent the location of the open bracket. 168 | */ 169 | onArrayBegin?: (offset: number, length: number) => void; 170 | 171 | /** 172 | * Invoked when a closing bracket is encountered. The offset and length represent the location of the closing bracket. 173 | */ 174 | onArrayEnd?: (offset: number, length: number) => void; 175 | 176 | /** 177 | * Invoked when a literal value is encountered. The offset and length represent the location of the literal value. 178 | */ 179 | onLiteralValue?: (value: any, offset: number, length: number) => void; 180 | 181 | /** 182 | * Invoked when a comma or colon separator is encountered. The offset and length represent the location of the separator. 183 | */ 184 | onSeparator?: (character: string, offset: number, length: number) => void; 185 | 186 | /** 187 | * When comments are allowed, invoked when a line or block comment is encountered. The offset and length represent the location of the comment. 188 | */ 189 | onComment?: (offset: number, length: number) => void; 190 | 191 | /** 192 | * Invoked on an error. 193 | */ 194 | onError?: (error: ParseErrorCode, offset: number, length: number) => void; 195 | } 196 | 197 | /** 198 | * Creates a JSON scanner on the given text. 199 | * If ignoreTrivia is set, whitespaces or comments are ignored. 200 | */ 201 | export function createScanner(text: string, ignoreTrivia: boolean = false): JSONScanner { 202 | 203 | let pos = 0; 204 | const len = text.length; 205 | let value: string = ''; 206 | let tokenOffset = 0; 207 | let token: SyntaxKind = SyntaxKind.Unknown; 208 | let scanError: ScanError = ScanError.None; 209 | 210 | function scanHexDigits(count: number): number { 211 | let digits = 0; 212 | let hexValue = 0; 213 | while (digits < count) { 214 | const ch = text.charCodeAt(pos); 215 | if (ch >= CharacterCodes._0 && ch <= CharacterCodes._9) { 216 | hexValue = hexValue * 16 + ch - CharacterCodes._0; 217 | } 218 | else if (ch >= CharacterCodes.A && ch <= CharacterCodes.F) { 219 | hexValue = hexValue * 16 + ch - CharacterCodes.A + 10; 220 | } 221 | else if (ch >= CharacterCodes.a && ch <= CharacterCodes.f) { 222 | hexValue = hexValue * 16 + ch - CharacterCodes.a + 10; 223 | } 224 | else { 225 | break; 226 | } 227 | pos++; 228 | digits++; 229 | } 230 | if (digits < count) { 231 | hexValue = -1; 232 | } 233 | return hexValue; 234 | } 235 | 236 | function setPosition(newPosition: number) { 237 | pos = newPosition; 238 | value = ''; 239 | tokenOffset = 0; 240 | token = SyntaxKind.Unknown; 241 | scanError = ScanError.None; 242 | } 243 | 244 | function scanNumber(): string { 245 | const start = pos; 246 | if (text.charCodeAt(pos) === CharacterCodes._0) { 247 | pos++; 248 | } else { 249 | pos++; 250 | while (pos < text.length && isDigit(text.charCodeAt(pos))) { 251 | pos++; 252 | } 253 | } 254 | if (pos < text.length && text.charCodeAt(pos) === CharacterCodes.dot) { 255 | pos++; 256 | if (pos < text.length && isDigit(text.charCodeAt(pos))) { 257 | pos++; 258 | while (pos < text.length && isDigit(text.charCodeAt(pos))) { 259 | pos++; 260 | } 261 | } else { 262 | scanError = ScanError.UnexpectedEndOfNumber; 263 | return text.substring(start, pos); 264 | } 265 | } 266 | let end = pos; 267 | if (pos < text.length && (text.charCodeAt(pos) === CharacterCodes.E || text.charCodeAt(pos) === CharacterCodes.e)) { 268 | pos++; 269 | if (pos < text.length && text.charCodeAt(pos) === CharacterCodes.plus || text.charCodeAt(pos) === CharacterCodes.minus) { 270 | pos++; 271 | } 272 | if (pos < text.length && isDigit(text.charCodeAt(pos))) { 273 | pos++; 274 | while (pos < text.length && isDigit(text.charCodeAt(pos))) { 275 | pos++; 276 | } 277 | end = pos; 278 | } else { 279 | scanError = ScanError.UnexpectedEndOfNumber; 280 | } 281 | } 282 | return text.substring(start, end); 283 | } 284 | 285 | function scanString(): string { 286 | 287 | let result = '', 288 | start = pos; 289 | 290 | while (true) { 291 | if (pos >= len) { 292 | result += text.substring(start, pos); 293 | scanError = ScanError.UnexpectedEndOfString; 294 | break; 295 | } 296 | const ch = text.charCodeAt(pos); 297 | if (ch === CharacterCodes.doubleQuote) { 298 | result += text.substring(start, pos); 299 | pos++; 300 | break; 301 | } 302 | if (ch === CharacterCodes.backslash) { 303 | result += text.substring(start, pos); 304 | pos++; 305 | if (pos >= len) { 306 | scanError = ScanError.UnexpectedEndOfString; 307 | break; 308 | } 309 | const ch2 = text.charCodeAt(pos++); 310 | switch (ch2) { 311 | case CharacterCodes.doubleQuote: 312 | result += '\"'; 313 | break; 314 | case CharacterCodes.backslash: 315 | result += '\\'; 316 | break; 317 | case CharacterCodes.slash: 318 | result += '/'; 319 | break; 320 | case CharacterCodes.b: 321 | result += '\b'; 322 | break; 323 | case CharacterCodes.f: 324 | result += '\f'; 325 | break; 326 | case CharacterCodes.n: 327 | result += '\n'; 328 | break; 329 | case CharacterCodes.r: 330 | result += '\r'; 331 | break; 332 | case CharacterCodes.t: 333 | result += '\t'; 334 | break; 335 | case CharacterCodes.u: { 336 | const ch3 = scanHexDigits(4); 337 | if (ch3 >= 0) { 338 | result += String.fromCharCode(ch3); 339 | } else { 340 | scanError = ScanError.InvalidUnicode; 341 | } 342 | break; 343 | } 344 | default: 345 | scanError = ScanError.InvalidEscapeCharacter; 346 | } 347 | start = pos; 348 | continue; 349 | } 350 | if (ch >= 0 && ch <= 0x1F) { 351 | if (isLineBreak(ch)) { 352 | result += text.substring(start, pos); 353 | scanError = ScanError.UnexpectedEndOfString; 354 | break; 355 | } else { 356 | scanError = ScanError.InvalidCharacter; 357 | // mark as error but continue with string 358 | } 359 | } 360 | pos++; 361 | } 362 | return result; 363 | } 364 | 365 | function scanNext(): SyntaxKind { 366 | 367 | value = ''; 368 | scanError = ScanError.None; 369 | 370 | tokenOffset = pos; 371 | 372 | if (pos >= len) { 373 | // at the end 374 | tokenOffset = len; 375 | return token = SyntaxKind.EOF; 376 | } 377 | 378 | let code = text.charCodeAt(pos); 379 | // trivia: whitespace 380 | if (isWhitespace(code)) { 381 | do { 382 | pos++; 383 | value += String.fromCharCode(code); 384 | code = text.charCodeAt(pos); 385 | } while (isWhitespace(code)); 386 | 387 | return token = SyntaxKind.Trivia; 388 | } 389 | 390 | // trivia: newlines 391 | if (isLineBreak(code)) { 392 | pos++; 393 | value += String.fromCharCode(code); 394 | if (code === CharacterCodes.carriageReturn && text.charCodeAt(pos) === CharacterCodes.lineFeed) { 395 | pos++; 396 | value += '\n'; 397 | } 398 | return token = SyntaxKind.LineBreakTrivia; 399 | } 400 | 401 | switch (code) { 402 | // tokens: []{}:, 403 | case CharacterCodes.openBrace: 404 | pos++; 405 | return token = SyntaxKind.OpenBraceToken; 406 | case CharacterCodes.closeBrace: 407 | pos++; 408 | return token = SyntaxKind.CloseBraceToken; 409 | case CharacterCodes.openBracket: 410 | pos++; 411 | return token = SyntaxKind.OpenBracketToken; 412 | case CharacterCodes.closeBracket: 413 | pos++; 414 | return token = SyntaxKind.CloseBracketToken; 415 | case CharacterCodes.colon: 416 | pos++; 417 | return token = SyntaxKind.ColonToken; 418 | case CharacterCodes.comma: 419 | pos++; 420 | return token = SyntaxKind.CommaToken; 421 | 422 | // strings 423 | case CharacterCodes.doubleQuote: 424 | pos++; 425 | value = scanString(); 426 | return token = SyntaxKind.StringLiteral; 427 | 428 | // comments 429 | case CharacterCodes.slash: { 430 | const start = pos - 1; 431 | // Single-line comment 432 | if (text.charCodeAt(pos + 1) === CharacterCodes.slash) { 433 | pos += 2; 434 | 435 | while (pos < len) { 436 | if (isLineBreak(text.charCodeAt(pos))) { 437 | break; 438 | } 439 | pos++; 440 | 441 | } 442 | value = text.substring(start, pos); 443 | return token = SyntaxKind.LineCommentTrivia; 444 | } 445 | 446 | // Multi-line comment 447 | if (text.charCodeAt(pos + 1) === CharacterCodes.asterisk) { 448 | pos += 2; 449 | 450 | const safeLength = len - 1; // For lookahead. 451 | let commentClosed = false; 452 | while (pos < safeLength) { 453 | const ch = text.charCodeAt(pos); 454 | 455 | if (ch === CharacterCodes.asterisk && text.charCodeAt(pos + 1) === CharacterCodes.slash) { 456 | pos += 2; 457 | commentClosed = true; 458 | break; 459 | } 460 | pos++; 461 | } 462 | 463 | if (!commentClosed) { 464 | pos++; 465 | scanError = ScanError.UnexpectedEndOfComment; 466 | } 467 | 468 | value = text.substring(start, pos); 469 | return token = SyntaxKind.BlockCommentTrivia; 470 | } 471 | // just a single slash 472 | value += String.fromCharCode(code); 473 | pos++; 474 | return token = SyntaxKind.Unknown; 475 | } 476 | // numbers 477 | case CharacterCodes.minus: 478 | value += String.fromCharCode(code); 479 | pos++; 480 | if (pos === len || !isDigit(text.charCodeAt(pos))) { 481 | return token = SyntaxKind.Unknown; 482 | } 483 | // found a minus, followed by a number so 484 | // we fall through to proceed with scanning 485 | // numbers 486 | case CharacterCodes._0: 487 | case CharacterCodes._1: 488 | case CharacterCodes._2: 489 | case CharacterCodes._3: 490 | case CharacterCodes._4: 491 | case CharacterCodes._5: 492 | case CharacterCodes._6: 493 | case CharacterCodes._7: 494 | case CharacterCodes._8: 495 | case CharacterCodes._9: 496 | value += scanNumber(); 497 | return token = SyntaxKind.NumericLiteral; 498 | // literals and unknown symbols 499 | default: 500 | // is a literal? Read the full word. 501 | while (pos < len && isUnknownContentCharacter(code)) { 502 | pos++; 503 | code = text.charCodeAt(pos); 504 | } 505 | if (tokenOffset !== pos) { 506 | value = text.substring(tokenOffset, pos); 507 | // keywords: true, false, null 508 | switch (value) { 509 | case 'true': return token = SyntaxKind.TrueKeyword; 510 | case 'false': return token = SyntaxKind.FalseKeyword; 511 | case 'null': return token = SyntaxKind.NullKeyword; 512 | } 513 | return token = SyntaxKind.Unknown; 514 | } 515 | // some 516 | value += String.fromCharCode(code); 517 | pos++; 518 | return token = SyntaxKind.Unknown; 519 | } 520 | } 521 | 522 | function isUnknownContentCharacter(code: CharacterCodes) { 523 | if (isWhitespace(code) || isLineBreak(code)) { 524 | return false; 525 | } 526 | switch (code) { 527 | case CharacterCodes.closeBrace: 528 | case CharacterCodes.closeBracket: 529 | case CharacterCodes.openBrace: 530 | case CharacterCodes.openBracket: 531 | case CharacterCodes.doubleQuote: 532 | case CharacterCodes.colon: 533 | case CharacterCodes.comma: 534 | case CharacterCodes.slash: 535 | return false; 536 | } 537 | return true; 538 | } 539 | 540 | 541 | function scanNextNonTrivia(): SyntaxKind { 542 | let result: SyntaxKind; 543 | do { 544 | result = scanNext(); 545 | } while (result >= SyntaxKind.LineCommentTrivia && result <= SyntaxKind.Trivia); 546 | return result; 547 | } 548 | 549 | return { 550 | setPosition: setPosition, 551 | getPosition: () => pos, 552 | scan: ignoreTrivia ? scanNextNonTrivia : scanNext, 553 | getToken: () => token, 554 | getTokenValue: () => value, 555 | getTokenOffset: () => tokenOffset, 556 | getTokenLength: () => pos - tokenOffset, 557 | getTokenError: () => scanError 558 | }; 559 | } 560 | 561 | function isWhitespace(ch: number): boolean { 562 | return ch === CharacterCodes.space || ch === CharacterCodes.tab || ch === CharacterCodes.verticalTab || ch === CharacterCodes.formFeed || 563 | ch === CharacterCodes.nonBreakingSpace || ch === CharacterCodes.ogham || ch >= CharacterCodes.enQuad && ch <= CharacterCodes.zeroWidthSpace || 564 | ch === CharacterCodes.narrowNoBreakSpace || ch === CharacterCodes.mathematicalSpace || ch === CharacterCodes.ideographicSpace || ch === CharacterCodes.byteOrderMark; 565 | } 566 | 567 | function isLineBreak(ch: number): boolean { 568 | return ch === CharacterCodes.lineFeed || ch === CharacterCodes.carriageReturn || ch === CharacterCodes.lineSeparator || ch === CharacterCodes.paragraphSeparator; 569 | } 570 | 571 | function isDigit(ch: number): boolean { 572 | return ch >= CharacterCodes._0 && ch <= CharacterCodes._9; 573 | } 574 | 575 | const enum CharacterCodes { 576 | nullCharacter = 0, 577 | maxAsciiCharacter = 0x7F, 578 | 579 | lineFeed = 0x0A, // \n 580 | carriageReturn = 0x0D, // \r 581 | lineSeparator = 0x2028, 582 | paragraphSeparator = 0x2029, 583 | 584 | // REVIEW: do we need to support this? The scanner doesn't, but our IText does. This seems 585 | // like an odd disparity? (Or maybe it's completely fine for them to be different). 586 | nextLine = 0x0085, 587 | 588 | // Unicode 3.0 space characters 589 | space = 0x0020, // " " 590 | nonBreakingSpace = 0x00A0, // 591 | enQuad = 0x2000, 592 | emQuad = 0x2001, 593 | enSpace = 0x2002, 594 | emSpace = 0x2003, 595 | threePerEmSpace = 0x2004, 596 | fourPerEmSpace = 0x2005, 597 | sixPerEmSpace = 0x2006, 598 | figureSpace = 0x2007, 599 | punctuationSpace = 0x2008, 600 | thinSpace = 0x2009, 601 | hairSpace = 0x200A, 602 | zeroWidthSpace = 0x200B, 603 | narrowNoBreakSpace = 0x202F, 604 | ideographicSpace = 0x3000, 605 | mathematicalSpace = 0x205F, 606 | ogham = 0x1680, 607 | 608 | _ = 0x5F, 609 | $ = 0x24, 610 | 611 | _0 = 0x30, 612 | _1 = 0x31, 613 | _2 = 0x32, 614 | _3 = 0x33, 615 | _4 = 0x34, 616 | _5 = 0x35, 617 | _6 = 0x36, 618 | _7 = 0x37, 619 | _8 = 0x38, 620 | _9 = 0x39, 621 | 622 | a = 0x61, 623 | b = 0x62, 624 | c = 0x63, 625 | d = 0x64, 626 | e = 0x65, 627 | f = 0x66, 628 | g = 0x67, 629 | h = 0x68, 630 | i = 0x69, 631 | j = 0x6A, 632 | k = 0x6B, 633 | l = 0x6C, 634 | m = 0x6D, 635 | n = 0x6E, 636 | o = 0x6F, 637 | p = 0x70, 638 | q = 0x71, 639 | r = 0x72, 640 | s = 0x73, 641 | t = 0x74, 642 | u = 0x75, 643 | v = 0x76, 644 | w = 0x77, 645 | x = 0x78, 646 | y = 0x79, 647 | z = 0x7A, 648 | 649 | A = 0x41, 650 | B = 0x42, 651 | C = 0x43, 652 | D = 0x44, 653 | E = 0x45, 654 | F = 0x46, 655 | G = 0x47, 656 | H = 0x48, 657 | I = 0x49, 658 | J = 0x4A, 659 | K = 0x4B, 660 | L = 0x4C, 661 | M = 0x4D, 662 | N = 0x4E, 663 | O = 0x4F, 664 | P = 0x50, 665 | Q = 0x51, 666 | R = 0x52, 667 | S = 0x53, 668 | T = 0x54, 669 | U = 0x55, 670 | V = 0x56, 671 | W = 0x57, 672 | X = 0x58, 673 | Y = 0x59, 674 | Z = 0x5A, 675 | 676 | ampersand = 0x26, // & 677 | asterisk = 0x2A, // * 678 | at = 0x40, // @ 679 | backslash = 0x5C, // \ 680 | bar = 0x7C, // | 681 | caret = 0x5E, // ^ 682 | closeBrace = 0x7D, // } 683 | closeBracket = 0x5D, // ] 684 | closeParen = 0x29, // ) 685 | colon = 0x3A, // : 686 | comma = 0x2C, // , 687 | dot = 0x2E, // . 688 | doubleQuote = 0x22, // " 689 | equals = 0x3D, // = 690 | exclamation = 0x21, // ! 691 | greaterThan = 0x3E, // > 692 | lessThan = 0x3C, // < 693 | minus = 0x2D, // - 694 | openBrace = 0x7B, // { 695 | openBracket = 0x5B, // [ 696 | openParen = 0x28, // ( 697 | percent = 0x25, // % 698 | plus = 0x2B, // + 699 | question = 0x3F, // ? 700 | semicolon = 0x3B, // ; 701 | singleQuote = 0x27, // ' 702 | slash = 0x2F, // / 703 | tilde = 0x7E, // ~ 704 | 705 | backspace = 0x08, // \b 706 | formFeed = 0x0C, // \f 707 | byteOrderMark = 0xFEFF, 708 | tab = 0x09, // \t 709 | verticalTab = 0x0B, // \v 710 | } 711 | 712 | interface NodeImpl extends Node { 713 | type: NodeType; 714 | value?: any; 715 | offset: number; 716 | length: number; 717 | colonOffset?: number; 718 | parent?: NodeImpl; 719 | children?: NodeImpl[]; 720 | } 721 | 722 | /** 723 | * For a given offset, evaluate the location in the JSON document. Each segment in the location path is either a property name or an array index. 724 | */ 725 | export function getLocation(text: string, position: number): Location { 726 | const segments: Segment[] = []; // strings or numbers 727 | const earlyReturnException = new Object(); 728 | let previousNode: NodeImpl | undefined = undefined; 729 | const previousNodeInst: NodeImpl = { 730 | value: {}, 731 | offset: 0, 732 | length: 0, 733 | type: 'object', 734 | parent: undefined 735 | }; 736 | let isAtPropertyKey = false; 737 | function setPreviousNode(value: string, offset: number, length: number, type: NodeType) { 738 | previousNodeInst.value = value; 739 | previousNodeInst.offset = offset; 740 | previousNodeInst.length = length; 741 | previousNodeInst.type = type; 742 | previousNodeInst.colonOffset = undefined; 743 | previousNode = previousNodeInst; 744 | } 745 | try { 746 | 747 | visit(text, { 748 | onObjectBegin: (offset: number, length: number) => { 749 | if (position <= offset) { 750 | throw earlyReturnException; 751 | } 752 | previousNode = undefined; 753 | isAtPropertyKey = position > offset; 754 | segments.push(''); // push a placeholder (will be replaced) 755 | }, 756 | onObjectProperty: (name: string, offset: number, length: number) => { 757 | if (position < offset) { 758 | throw earlyReturnException; 759 | } 760 | setPreviousNode(name, offset, length, 'property'); 761 | segments[segments.length - 1] = name; 762 | if (position <= offset + length) { 763 | throw earlyReturnException; 764 | } 765 | }, 766 | onObjectEnd: (offset: number, length: number) => { 767 | if (position <= offset) { 768 | throw earlyReturnException; 769 | } 770 | previousNode = undefined; 771 | segments.pop(); 772 | }, 773 | onArrayBegin: (offset: number, length: number) => { 774 | if (position <= offset) { 775 | throw earlyReturnException; 776 | } 777 | previousNode = undefined; 778 | segments.push(0); 779 | }, 780 | onArrayEnd: (offset: number, length: number) => { 781 | if (position <= offset) { 782 | throw earlyReturnException; 783 | } 784 | previousNode = undefined; 785 | segments.pop(); 786 | }, 787 | onLiteralValue: (value: any, offset: number, length: number) => { 788 | if (position < offset) { 789 | throw earlyReturnException; 790 | } 791 | setPreviousNode(value, offset, length, getNodeType(value)); 792 | 793 | if (position <= offset + length) { 794 | throw earlyReturnException; 795 | } 796 | }, 797 | onSeparator: (sep: string, offset: number, length: number) => { 798 | if (position <= offset) { 799 | throw earlyReturnException; 800 | } 801 | if (sep === ':' && previousNode && previousNode.type === 'property') { 802 | previousNode.colonOffset = offset; 803 | isAtPropertyKey = false; 804 | previousNode = undefined; 805 | } else if (sep === ',') { 806 | const last = segments[segments.length - 1]; 807 | if (typeof last === 'number') { 808 | segments[segments.length - 1] = last + 1; 809 | } else { 810 | isAtPropertyKey = true; 811 | segments[segments.length - 1] = ''; 812 | } 813 | previousNode = undefined; 814 | } 815 | } 816 | }); 817 | } catch (e) { 818 | if (e !== earlyReturnException) { 819 | throw e; 820 | } 821 | } 822 | 823 | return { 824 | path: segments, 825 | previousNode, 826 | isAtPropertyKey, 827 | matches: (pattern: Segment[]) => { 828 | let k = 0; 829 | for (let i = 0; k < pattern.length && i < segments.length; i++) { 830 | if (pattern[k] === segments[i] || pattern[k] === '*') { 831 | k++; 832 | } else if (pattern[k] !== '**') { 833 | return false; 834 | } 835 | } 836 | return k === pattern.length; 837 | } 838 | }; 839 | } 840 | 841 | 842 | /** 843 | * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result. 844 | * Therefore always check the errors list to find out if the input was valid. 845 | */ 846 | export function parse(text: string, errors: ParseError[] = [], options: ParseOptions = ParseOptions.DEFAULT): any { 847 | let currentProperty: string | null = null; 848 | let currentParent: any = []; 849 | const previousParents: any[] = []; 850 | 851 | function onValue(value: any) { 852 | if (Array.isArray(currentParent)) { 853 | (currentParent).push(value); 854 | } else if (currentProperty !== null) { 855 | currentParent[currentProperty] = value; 856 | } 857 | } 858 | 859 | const visitor: JSONVisitor = { 860 | onObjectBegin: () => { 861 | const object = {}; 862 | onValue(object); 863 | previousParents.push(currentParent); 864 | currentParent = object; 865 | currentProperty = null; 866 | }, 867 | onObjectProperty: (name: string) => { 868 | currentProperty = name; 869 | }, 870 | onObjectEnd: () => { 871 | currentParent = previousParents.pop(); 872 | }, 873 | onArrayBegin: () => { 874 | const array: any[] = []; 875 | onValue(array); 876 | previousParents.push(currentParent); 877 | currentParent = array; 878 | currentProperty = null; 879 | }, 880 | onArrayEnd: () => { 881 | currentParent = previousParents.pop(); 882 | }, 883 | onLiteralValue: onValue, 884 | onError: (error: ParseErrorCode, offset: number, length: number) => { 885 | errors.push({ error, offset, length }); 886 | } 887 | }; 888 | visit(text, visitor, options); 889 | return currentParent[0]; 890 | } 891 | 892 | 893 | /** 894 | * Parses the given text and returns a tree representation the JSON content. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result. 895 | */ 896 | export function parseTree(text: string, errors: ParseError[] = [], options: ParseOptions = ParseOptions.DEFAULT): Node { 897 | let currentParent: NodeImpl = { type: 'array', offset: -1, length: -1, children: [], parent: undefined }; // artificial root 898 | 899 | function ensurePropertyComplete(endOffset: number) { 900 | if (currentParent.type === 'property') { 901 | currentParent.length = endOffset - currentParent.offset; 902 | currentParent = currentParent.parent!; 903 | } 904 | } 905 | 906 | function onValue(valueNode: Node): Node { 907 | currentParent.children!.push(valueNode); 908 | return valueNode; 909 | } 910 | 911 | const visitor: JSONVisitor = { 912 | onObjectBegin: (offset: number) => { 913 | currentParent = onValue({ type: 'object', offset, length: -1, parent: currentParent, children: [] }); 914 | }, 915 | onObjectProperty: (name: string, offset: number, length: number) => { 916 | currentParent = onValue({ type: 'property', offset, length: -1, parent: currentParent, children: [] }); 917 | currentParent.children!.push({ type: 'string', value: name, offset, length, parent: currentParent }); 918 | }, 919 | onObjectEnd: (offset: number, length: number) => { 920 | currentParent.length = offset + length - currentParent.offset; 921 | currentParent = currentParent.parent!; 922 | ensurePropertyComplete(offset + length); 923 | }, 924 | onArrayBegin: (offset: number, length: number) => { 925 | currentParent = onValue({ type: 'array', offset, length: -1, parent: currentParent, children: [] }); 926 | }, 927 | onArrayEnd: (offset: number, length: number) => { 928 | currentParent.length = offset + length - currentParent.offset; 929 | currentParent = currentParent.parent!; 930 | ensurePropertyComplete(offset + length); 931 | }, 932 | onLiteralValue: (value: any, offset: number, length: number) => { 933 | onValue({ type: getNodeType(value), offset, length, parent: currentParent, value }); 934 | ensurePropertyComplete(offset + length); 935 | }, 936 | onSeparator: (sep: string, offset: number, length: number) => { 937 | if (currentParent.type === 'property') { 938 | if (sep === ':') { 939 | currentParent.colonOffset = offset; 940 | } else if (sep === ',') { 941 | ensurePropertyComplete(offset); 942 | } 943 | } 944 | }, 945 | onError: (error: ParseErrorCode, offset: number, length: number) => { 946 | errors.push({ error, offset, length }); 947 | } 948 | }; 949 | visit(text, visitor, options); 950 | 951 | const result = currentParent.children![0]; 952 | if (result) { 953 | delete result.parent; 954 | } 955 | return result; 956 | } 957 | 958 | /** 959 | * Finds the node at the given path in a JSON DOM. 960 | */ 961 | export function findNodeAtLocation(root: Node, path: JSONPath): Node | undefined { 962 | if (!root) { 963 | return undefined; 964 | } 965 | let node = root; 966 | for (const segment of path) { 967 | if (typeof segment === 'string') { 968 | if (node.type !== 'object' || !Array.isArray(node.children)) { 969 | return undefined; 970 | } 971 | let found = false; 972 | for (const propertyNode of node.children) { 973 | if (Array.isArray(propertyNode.children) && propertyNode.children[0].value === segment) { 974 | node = propertyNode.children[1]; 975 | found = true; 976 | break; 977 | } 978 | } 979 | if (!found) { 980 | return undefined; 981 | } 982 | } else { 983 | const index = segment; 984 | if (node.type !== 'array' || index < 0 || !Array.isArray(node.children) || index >= node.children.length) { 985 | return undefined; 986 | } 987 | node = node.children[index]; 988 | } 989 | } 990 | return node; 991 | } 992 | 993 | /** 994 | * Gets the JSON path of the given JSON DOM node 995 | */ 996 | export function getNodePath(node: Node): JSONPath { 997 | if (!node.parent || !node.parent.children) { 998 | return []; 999 | } 1000 | const path = getNodePath(node.parent); 1001 | if (node.parent.type === 'property') { 1002 | const key = node.parent.children[0].value; 1003 | path.push(key); 1004 | } else if (node.parent.type === 'array') { 1005 | const index = node.parent.children.indexOf(node); 1006 | if (index !== -1) { 1007 | path.push(index); 1008 | } 1009 | } 1010 | return path; 1011 | } 1012 | 1013 | /** 1014 | * Evaluates the JavaScript object of the given JSON DOM node 1015 | */ 1016 | export function getNodeValue(node: Node): any { 1017 | switch (node.type) { 1018 | case 'array': 1019 | return node.children!.map(getNodeValue); 1020 | case 'object': { 1021 | const obj = Object.create(null); 1022 | for (const prop of node.children!) { 1023 | const valueNode = prop.children![1]; 1024 | if (valueNode) { 1025 | obj[prop.children![0].value] = getNodeValue(valueNode); 1026 | } 1027 | } 1028 | return obj; 1029 | } 1030 | case 'null': 1031 | case 'string': 1032 | case 'number': 1033 | case 'boolean': 1034 | return node.value; 1035 | default: 1036 | return undefined; 1037 | } 1038 | 1039 | } 1040 | 1041 | export function contains(node: Node, offset: number, includeRightBound = false): boolean { 1042 | return (offset >= node.offset && offset < (node.offset + node.length)) || includeRightBound && (offset === (node.offset + node.length)); 1043 | } 1044 | 1045 | /** 1046 | * Finds the most inner node at the given offset. If includeRightBound is set, also finds nodes that end at the given offset. 1047 | */ 1048 | export function findNodeAtOffset(node: Node, offset: number, includeRightBound = false): Node | undefined { 1049 | if (contains(node, offset, includeRightBound)) { 1050 | const children = node.children; 1051 | if (Array.isArray(children)) { 1052 | for (let i = 0; i < children.length && children[i].offset <= offset; i++) { 1053 | const item = findNodeAtOffset(children[i], offset, includeRightBound); 1054 | if (item) { 1055 | return item; 1056 | } 1057 | } 1058 | 1059 | } 1060 | return node; 1061 | } 1062 | return undefined; 1063 | } 1064 | 1065 | 1066 | /** 1067 | * Parses the given text and invokes the visitor functions for each object, array and literal reached. 1068 | */ 1069 | export function visit(text: string, visitor: JSONVisitor, options: ParseOptions = ParseOptions.DEFAULT): any { 1070 | 1071 | const _scanner = createScanner(text, false); 1072 | 1073 | function toNoArgVisit(visitFunction?: (offset: number, length: number) => void): () => void { 1074 | return visitFunction ? () => visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength()) : () => true; 1075 | } 1076 | function toOneArgVisit(visitFunction?: (arg: T, offset: number, length: number) => void): (arg: T) => void { 1077 | return visitFunction ? (arg: T) => visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength()) : () => true; 1078 | } 1079 | 1080 | const onObjectBegin = toNoArgVisit(visitor.onObjectBegin), 1081 | onObjectProperty = toOneArgVisit(visitor.onObjectProperty), 1082 | onObjectEnd = toNoArgVisit(visitor.onObjectEnd), 1083 | onArrayBegin = toNoArgVisit(visitor.onArrayBegin), 1084 | onArrayEnd = toNoArgVisit(visitor.onArrayEnd), 1085 | onLiteralValue = toOneArgVisit(visitor.onLiteralValue), 1086 | onSeparator = toOneArgVisit(visitor.onSeparator), 1087 | onComment = toNoArgVisit(visitor.onComment), 1088 | onError = toOneArgVisit(visitor.onError); 1089 | 1090 | const disallowComments = options && options.disallowComments; 1091 | const allowTrailingComma = options && options.allowTrailingComma; 1092 | function scanNext(): SyntaxKind { 1093 | while (true) { 1094 | const token = _scanner.scan(); 1095 | switch (_scanner.getTokenError()) { 1096 | case ScanError.InvalidUnicode: 1097 | handleError(ParseErrorCode.InvalidUnicode); 1098 | break; 1099 | case ScanError.InvalidEscapeCharacter: 1100 | handleError(ParseErrorCode.InvalidEscapeCharacter); 1101 | break; 1102 | case ScanError.UnexpectedEndOfNumber: 1103 | handleError(ParseErrorCode.UnexpectedEndOfNumber); 1104 | break; 1105 | case ScanError.UnexpectedEndOfComment: 1106 | if (!disallowComments) { 1107 | handleError(ParseErrorCode.UnexpectedEndOfComment); 1108 | } 1109 | break; 1110 | case ScanError.UnexpectedEndOfString: 1111 | handleError(ParseErrorCode.UnexpectedEndOfString); 1112 | break; 1113 | case ScanError.InvalidCharacter: 1114 | handleError(ParseErrorCode.InvalidCharacter); 1115 | break; 1116 | } 1117 | switch (token) { 1118 | case SyntaxKind.LineCommentTrivia: 1119 | case SyntaxKind.BlockCommentTrivia: 1120 | if (disallowComments) { 1121 | handleError(ParseErrorCode.InvalidCommentToken); 1122 | } else { 1123 | onComment(); 1124 | } 1125 | break; 1126 | case SyntaxKind.Unknown: 1127 | handleError(ParseErrorCode.InvalidSymbol); 1128 | break; 1129 | case SyntaxKind.Trivia: 1130 | case SyntaxKind.LineBreakTrivia: 1131 | break; 1132 | default: 1133 | return token; 1134 | } 1135 | } 1136 | } 1137 | 1138 | function handleError(error: ParseErrorCode, skipUntilAfter: SyntaxKind[] = [], skipUntil: SyntaxKind[] = []): void { 1139 | onError(error); 1140 | if (skipUntilAfter.length + skipUntil.length > 0) { 1141 | let token = _scanner.getToken(); 1142 | while (token !== SyntaxKind.EOF) { 1143 | if (skipUntilAfter.indexOf(token) !== -1) { 1144 | scanNext(); 1145 | break; 1146 | } else if (skipUntil.indexOf(token) !== -1) { 1147 | break; 1148 | } 1149 | token = scanNext(); 1150 | } 1151 | } 1152 | } 1153 | 1154 | function parseString(isValue: boolean): boolean { 1155 | const value = _scanner.getTokenValue(); 1156 | if (isValue) { 1157 | onLiteralValue(value); 1158 | } else { 1159 | onObjectProperty(value); 1160 | } 1161 | scanNext(); 1162 | return true; 1163 | } 1164 | 1165 | function parseLiteral(): boolean { 1166 | switch (_scanner.getToken()) { 1167 | case SyntaxKind.NumericLiteral: { 1168 | let value = 0; 1169 | try { 1170 | value = JSON.parse(_scanner.getTokenValue()); 1171 | if (typeof value !== 'number') { 1172 | handleError(ParseErrorCode.InvalidNumberFormat); 1173 | value = 0; 1174 | } 1175 | } catch (e) { 1176 | handleError(ParseErrorCode.InvalidNumberFormat); 1177 | } 1178 | onLiteralValue(value); 1179 | break; 1180 | } 1181 | case SyntaxKind.NullKeyword: 1182 | onLiteralValue(null); 1183 | break; 1184 | case SyntaxKind.TrueKeyword: 1185 | onLiteralValue(true); 1186 | break; 1187 | case SyntaxKind.FalseKeyword: 1188 | onLiteralValue(false); 1189 | break; 1190 | default: 1191 | return false; 1192 | } 1193 | scanNext(); 1194 | return true; 1195 | } 1196 | 1197 | function parseProperty(): boolean { 1198 | if (_scanner.getToken() !== SyntaxKind.StringLiteral) { 1199 | handleError(ParseErrorCode.PropertyNameExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]); 1200 | return false; 1201 | } 1202 | parseString(false); 1203 | if (_scanner.getToken() === SyntaxKind.ColonToken) { 1204 | onSeparator(':'); 1205 | scanNext(); // consume colon 1206 | 1207 | if (!parseValue()) { 1208 | handleError(ParseErrorCode.ValueExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]); 1209 | } 1210 | } else { 1211 | handleError(ParseErrorCode.ColonExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]); 1212 | } 1213 | return true; 1214 | } 1215 | 1216 | function parseObject(): boolean { 1217 | onObjectBegin(); 1218 | scanNext(); // consume open brace 1219 | 1220 | let needsComma = false; 1221 | while (_scanner.getToken() !== SyntaxKind.CloseBraceToken && _scanner.getToken() !== SyntaxKind.EOF) { 1222 | if (_scanner.getToken() === SyntaxKind.CommaToken) { 1223 | if (!needsComma) { 1224 | handleError(ParseErrorCode.ValueExpected, [], []); 1225 | } 1226 | onSeparator(','); 1227 | scanNext(); // consume comma 1228 | if (_scanner.getToken() === SyntaxKind.CloseBraceToken && allowTrailingComma) { 1229 | break; 1230 | } 1231 | } else if (needsComma) { 1232 | handleError(ParseErrorCode.CommaExpected, [], []); 1233 | } 1234 | if (!parseProperty()) { 1235 | handleError(ParseErrorCode.ValueExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]); 1236 | } 1237 | needsComma = true; 1238 | } 1239 | onObjectEnd(); 1240 | if (_scanner.getToken() !== SyntaxKind.CloseBraceToken) { 1241 | handleError(ParseErrorCode.CloseBraceExpected, [SyntaxKind.CloseBraceToken], []); 1242 | } else { 1243 | scanNext(); // consume close brace 1244 | } 1245 | return true; 1246 | } 1247 | 1248 | function parseArray(): boolean { 1249 | onArrayBegin(); 1250 | scanNext(); // consume open bracket 1251 | 1252 | let needsComma = false; 1253 | while (_scanner.getToken() !== SyntaxKind.CloseBracketToken && _scanner.getToken() !== SyntaxKind.EOF) { 1254 | if (_scanner.getToken() === SyntaxKind.CommaToken) { 1255 | if (!needsComma) { 1256 | handleError(ParseErrorCode.ValueExpected, [], []); 1257 | } 1258 | onSeparator(','); 1259 | scanNext(); // consume comma 1260 | if (_scanner.getToken() === SyntaxKind.CloseBracketToken && allowTrailingComma) { 1261 | break; 1262 | } 1263 | } else if (needsComma) { 1264 | handleError(ParseErrorCode.CommaExpected, [], []); 1265 | } 1266 | if (!parseValue()) { 1267 | handleError(ParseErrorCode.ValueExpected, [], [SyntaxKind.CloseBracketToken, SyntaxKind.CommaToken]); 1268 | } 1269 | needsComma = true; 1270 | } 1271 | onArrayEnd(); 1272 | if (_scanner.getToken() !== SyntaxKind.CloseBracketToken) { 1273 | handleError(ParseErrorCode.CloseBracketExpected, [SyntaxKind.CloseBracketToken], []); 1274 | } else { 1275 | scanNext(); // consume close bracket 1276 | } 1277 | return true; 1278 | } 1279 | 1280 | function parseValue(): boolean { 1281 | switch (_scanner.getToken()) { 1282 | case SyntaxKind.OpenBracketToken: 1283 | return parseArray(); 1284 | case SyntaxKind.OpenBraceToken: 1285 | return parseObject(); 1286 | case SyntaxKind.StringLiteral: 1287 | return parseString(true); 1288 | default: 1289 | return parseLiteral(); 1290 | } 1291 | } 1292 | 1293 | scanNext(); 1294 | if (_scanner.getToken() === SyntaxKind.EOF) { 1295 | if (options.allowEmptyContent) { 1296 | return true; 1297 | } 1298 | handleError(ParseErrorCode.ValueExpected, [], []); 1299 | return false; 1300 | } 1301 | if (!parseValue()) { 1302 | handleError(ParseErrorCode.ValueExpected, [], []); 1303 | return false; 1304 | } 1305 | if (_scanner.getToken() !== SyntaxKind.EOF) { 1306 | handleError(ParseErrorCode.EndOfFileExpected, [], []); 1307 | } 1308 | return true; 1309 | } 1310 | 1311 | /** 1312 | * Takes JSON with JavaScript-style comments and remove 1313 | * them. Optionally replaces every none-newline character 1314 | * of comments with a replaceCharacter 1315 | */ 1316 | export function stripComments(text: string, replaceCh?: string): string { 1317 | 1318 | const _scanner = createScanner(text); 1319 | const parts: string[] = []; 1320 | let kind: SyntaxKind; 1321 | let offset = 0; 1322 | let pos: number; 1323 | 1324 | do { 1325 | pos = _scanner.getPosition(); 1326 | kind = _scanner.scan(); 1327 | switch (kind) { 1328 | case SyntaxKind.LineCommentTrivia: 1329 | case SyntaxKind.BlockCommentTrivia: 1330 | case SyntaxKind.EOF: 1331 | if (offset !== pos) { 1332 | parts.push(text.substring(offset, pos)); 1333 | } 1334 | if (replaceCh !== undefined) { 1335 | parts.push(_scanner.getTokenValue().replace(/[^\r\n]/g, replaceCh)); 1336 | } 1337 | offset = _scanner.getPosition(); 1338 | break; 1339 | } 1340 | } while (kind !== SyntaxKind.EOF); 1341 | 1342 | return parts.join(''); 1343 | } 1344 | 1345 | export function getNodeType(value: any): NodeType { 1346 | switch (typeof value) { 1347 | case 'boolean': return 'boolean'; 1348 | case 'number': return 'number'; 1349 | case 'string': return 'string'; 1350 | case 'object': { 1351 | if (!value) { 1352 | return 'null'; 1353 | } else if (Array.isArray(value)) { 1354 | return 'array'; 1355 | } 1356 | return 'object'; 1357 | } 1358 | default: return 'null'; 1359 | } 1360 | } 1361 | -------------------------------------------------------------------------------- /src/slideshow.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from 'vscode'; 5 | import * as json from './json'; 6 | 7 | enum SlideShowType { 8 | slide = 'slide', 9 | subslide = 'subslide', 10 | fragment = 'fragment', 11 | skip = 'skip', 12 | notes = 'notes', 13 | none = 'none' 14 | } 15 | 16 | export class CellSlideShowStatusBarProvider implements vscode.NotebookCellStatusBarItemProvider { 17 | provideCellStatusBarItems( 18 | cell: vscode.NotebookCell, 19 | token: vscode.CancellationToken 20 | ): vscode.ProviderResult { 21 | const items: vscode.NotebookCellStatusBarItem[] = []; 22 | const slideType = getSlideType(cell); 23 | 24 | if (slideType) { 25 | items.push({ 26 | text: `Slide Type: ${slideType}`, 27 | tooltip: `Slide Type: ${slideType}`, 28 | command: 'jupyter-slideshow.switchSlideType', 29 | alignment: vscode.NotebookCellStatusBarAlignment.Right 30 | }); 31 | } 32 | 33 | return items; 34 | } 35 | } 36 | 37 | export function getActiveCell() { 38 | // find active cell 39 | const editor = vscode.window.activeNotebookEditor; 40 | if (!editor) { 41 | return; 42 | } 43 | 44 | return editor.notebook.cellAt(editor.selections[0].start); 45 | } 46 | 47 | export function reviveCell(args: vscode.NotebookCell | vscode.Uri | undefined): vscode.NotebookCell | undefined { 48 | if (!args) { 49 | return getActiveCell(); 50 | } 51 | 52 | if (args && 'index' in args && 'kind' in args && 'notebook' in args && 'document' in args) { 53 | return args as vscode.NotebookCell; 54 | } 55 | 56 | if (args && 'scheme' in args && 'path' in args) { 57 | const cellUri = vscode.Uri.from(args); 58 | const cellUriStr = cellUri.toString(); 59 | let activeCell: vscode.NotebookCell | undefined = undefined; 60 | 61 | for (const document of vscode.workspace.notebookDocuments) { 62 | for (const cell of document.getCells()) { 63 | if (cell.document.uri.toString() === cellUriStr) { 64 | activeCell = cell; 65 | break; 66 | } 67 | } 68 | 69 | if (activeCell) { 70 | break; 71 | } 72 | } 73 | 74 | return activeCell; 75 | } 76 | 77 | return undefined; 78 | } 79 | 80 | export function register(context: vscode.ExtensionContext) { 81 | context.subscriptions.push( 82 | vscode.notebooks.registerNotebookCellStatusBarItemProvider( 83 | 'jupyter-notebook', 84 | new CellSlideShowStatusBarProvider() 85 | ) 86 | ); 87 | 88 | context.subscriptions.push( 89 | vscode.commands.registerCommand( 90 | 'jupyter-slideshow.switchSlideType', 91 | async (cell: vscode.NotebookCell | vscode.Uri | undefined) => { 92 | cell = reviveCell(cell); 93 | if (!cell) { 94 | return; 95 | } 96 | 97 | // create quick pick items for each slide type 98 | const items: vscode.QuickPickItem[] = []; 99 | for (const type in SlideShowType) { 100 | items.push({ 101 | label: type 102 | }); 103 | } 104 | 105 | // show quick pick 106 | const selected = await vscode.window.showQuickPick(items); 107 | // update cell metadata with this slide type 108 | if (selected) { 109 | const selectedType = selected.label === SlideShowType.none ? undefined : selected.label; 110 | await updateSlideType(cell, selectedType); 111 | } 112 | } 113 | ) 114 | ); 115 | 116 | context.subscriptions.push( 117 | vscode.commands.registerCommand( 118 | 'jupyter-slideshow.editSlideShowInJSON', 119 | async (cell: vscode.NotebookCell | vscode.Uri | undefined) => { 120 | cell = reviveCell(cell); 121 | if (!cell) { 122 | return; 123 | } 124 | 125 | const resourceUri = cell.notebook.uri; 126 | const document = await vscode.workspace.openTextDocument(resourceUri); 127 | const tree = json.parseTree(document.getText()); 128 | const cells = json.findNodeAtLocation(tree, ['cells']); 129 | if (cells && cells.children && cells.children[cell.index]) { 130 | const cellNode = cells.children[cell.index]; 131 | const metadata = json.findNodeAtLocation(cellNode, ['metadata']); 132 | if (metadata) { 133 | const slideshow = json.findNodeAtLocation(metadata, ['slideshow']); 134 | if (slideshow) { 135 | const range = new vscode.Range( 136 | document.positionAt(slideshow.offset), 137 | document.positionAt(slideshow.offset + slideshow.length) 138 | ); 139 | await vscode.window.showTextDocument(document, { 140 | selection: range, 141 | viewColumn: vscode.ViewColumn.Beside 142 | }); 143 | } else { 144 | const range = new vscode.Range( 145 | document.positionAt(metadata.offset), 146 | document.positionAt(metadata.offset + metadata.length) 147 | ); 148 | await vscode.window.showTextDocument(document, { 149 | selection: range, 150 | viewColumn: vscode.ViewColumn.Beside 151 | }); 152 | } 153 | } else { 154 | const range = new vscode.Range( 155 | document.positionAt(cellNode.offset), 156 | document.positionAt(cellNode.offset + cellNode.length) 157 | ); 158 | await vscode.window.showTextDocument(document, { 159 | selection: range, 160 | viewColumn: vscode.ViewColumn.Beside 161 | }); 162 | } 163 | } 164 | } 165 | ) 166 | ); 167 | } 168 | 169 | export function getSlideType(cell: vscode.NotebookCell): SlideShowType | undefined { 170 | const slideshow: { slide_type: SlideShowType } | undefined = 171 | (useCustomMetadata() ? cell.metadata.custom?.metadata?.slideshow : cell.metadata.metadata?.slideshow) ?? 172 | undefined; 173 | return slideshow?.slide_type; 174 | } 175 | export async function updateSlideType(cell: vscode.NotebookCell, slideType?: string) { 176 | if (!slideType && !getSlideType(cell)) { 177 | return; 178 | } 179 | 180 | const metadata = JSON.parse(JSON.stringify(cell.metadata)); 181 | if (useCustomMetadata()) { 182 | metadata.custom = metadata.custom || {}; 183 | metadata.custom.metadata = metadata.custom.metadata || {}; 184 | if (!slideType) { 185 | if (metadata.custom.metadata.slideshow) { 186 | delete metadata.custom.metadata.slideshow; 187 | } 188 | } else { 189 | metadata.custom.metadata.slideshow = metadata.custom.metadata.slideshow || {}; 190 | metadata.custom.metadata.slideshow.slide_type = slideType; 191 | } 192 | } else { 193 | metadata.metadata = metadata.metadata || {}; 194 | if (!slideType) { 195 | if (metadata.metadata.slideshow) { 196 | delete metadata.metadata.slideshow; 197 | } 198 | } else { 199 | metadata.metadata.slideshow = metadata.metadata.slideshow || {}; 200 | metadata.metadata.slideshow.slide_type = slideType; 201 | } 202 | } 203 | const edit = new vscode.WorkspaceEdit(); 204 | const nbEdit = vscode.NotebookEdit.updateCellMetadata(cell.index, sortObjectPropertiesRecursively(metadata)); 205 | edit.set(cell.notebook.uri, [nbEdit]); 206 | await vscode.workspace.applyEdit(edit); 207 | } 208 | 209 | function useCustomMetadata() { 210 | if (vscode.extensions.getExtension('vscode.ipynb')?.exports.dropCustomMetadata) { 211 | return false; 212 | } 213 | return true; 214 | } 215 | 216 | 217 | /** 218 | * Sort the JSON to minimize unnecessary SCM changes. 219 | * Jupyter notbeooks/labs sorts the JSON keys in alphabetical order. 220 | * https://github.com/microsoft/vscode/issues/208137 221 | */ 222 | function sortObjectPropertiesRecursively(obj: any): any { 223 | if (Array.isArray(obj)) { 224 | return obj.map(sortObjectPropertiesRecursively); 225 | } 226 | if (obj !== undefined && obj !== null && typeof obj === 'object' && Object.keys(obj).length > 0) { 227 | return ( 228 | Object.keys(obj) 229 | .sort() 230 | .reduce>((sortedObj, prop) => { 231 | sortedObj[prop] = sortObjectPropertiesRecursively(obj[prop]); 232 | return sortedObj; 233 | }, {}) as any 234 | ); 235 | } 236 | return obj; 237 | } 238 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /vscode.proposed.notebookWorkspaceEdit.d.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 | declare module 'vscode' { 7 | 8 | // https://github.com/microsoft/vscode/issues/106744 9 | 10 | /** 11 | * A notebook edit represents edits that should be applied to the contents of a notebook. 12 | */ 13 | export class NotebookEdit { 14 | 15 | /** 16 | * Utility to create a edit that replaces cells in a notebook. 17 | * 18 | * @param range The range of cells to replace 19 | * @param newCells The new notebook cells. 20 | */ 21 | static replaceCells(range: NotebookRange, newCells: NotebookCellData[]): NotebookEdit; 22 | 23 | /** 24 | * Utility to create an edit that replaces cells in a notebook. 25 | * 26 | * @param index The index to insert cells at. 27 | * @param newCells The new notebook cells. 28 | */ 29 | static insertCells(index: number, newCells: NotebookCellData[]): NotebookEdit; 30 | 31 | /** 32 | * Utility to create an edit that deletes cells in a notebook. 33 | * 34 | * @param range The range of cells to delete. 35 | */ 36 | static deleteCells(range: NotebookRange): NotebookEdit; 37 | 38 | /** 39 | * Utility to create an edit that update a cell's metadata. 40 | * 41 | * @param index The index of the cell to update. 42 | * @param newCellMetadata The new metadata for the cell. 43 | */ 44 | static updateCellMetadata(index: number, newCellMetadata: { [key: string]: any }): NotebookEdit; 45 | 46 | /** 47 | * Utility to create an edit that updates the notebook's metadata. 48 | * 49 | * @param newNotebookMetadata The new metadata for the notebook. 50 | */ 51 | static updateNotebookMetadata(newNotebookMetadata: { [key: string]: any }): NotebookEdit; 52 | 53 | /** 54 | * Range of the cells being edited. May be empty. 55 | */ 56 | range: NotebookRange; 57 | 58 | /** 59 | * New cells being inserted. May be empty. 60 | */ 61 | newCells: NotebookCellData[]; 62 | 63 | /** 64 | * Optional new metadata for the cells. 65 | */ 66 | newCellMetadata?: { [key: string]: any }; 67 | 68 | /** 69 | * Optional new metadata for the notebook. 70 | */ 71 | newNotebookMetadata?: { [key: string]: any }; 72 | 73 | constructor(range: NotebookRange, newCells: NotebookCellData[]); 74 | } 75 | 76 | export interface WorkspaceEdit { 77 | /** 78 | * Set (and replace) edits for a resource. 79 | * 80 | * @param uri A resource identifier. 81 | * @param edits An array of text or notebook edits. 82 | */ 83 | set(uri: Uri, edits: TextEdit[] | NotebookEdit[]): void; 84 | } 85 | } -------------------------------------------------------------------------------- /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 | //@ts-check 6 | 'use strict'; 7 | const path = require('path'); 8 | /**@type {import('webpack').Configuration}*/ 9 | const config = { 10 | entry: './src/extension.ts', 11 | devtool: 'source-map', 12 | externals: { 13 | vscode: 'commonjs vscode', 14 | }, 15 | resolve: { 16 | extensions: ['.ts', '.js'], 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.ts$/, 22 | exclude: /node_modules/, 23 | use: [ 24 | { 25 | loader: 'ts-loader', 26 | } 27 | ] 28 | }, 29 | ], 30 | }, 31 | }; 32 | const nodeConfig = { 33 | ...config, 34 | target: 'node', 35 | output: { // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 36 | path: path.resolve(__dirname, 'out'), 37 | filename: 'extension-node.js', 38 | libraryTarget: "commonjs2", 39 | devtoolModuleFilenameTemplate: "../[resource-path]", 40 | } 41 | }; 42 | const webConfig = { 43 | ...config, 44 | target: 'webworker', 45 | output: { // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 46 | path: path.resolve(__dirname, 'out'), 47 | filename: 'extension-web.js', 48 | libraryTarget: "commonjs2", 49 | devtoolModuleFilenameTemplate: "../[resource-path]", 50 | } 51 | }; 52 | module.exports = [nodeConfig, webConfig]; --------------------------------------------------------------------------------