├── .browserslistrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .eslintrc.json
├── .gitattributes
├── .github
└── workflows
│ ├── add-issue-to-project.yml
│ ├── close-comment.yml
│ ├── notify-ready.yml
│ ├── release.yml
│ ├── semantic-pr.yml
│ ├── test.yml
│ ├── wbfy-merge.yml
│ └── wbfy.yml
├── .gitignore
├── .husky
├── post-merge
├── pre-commit
└── pre-push
├── .idea
├── codeStyles
│ └── codeStyleConfig.xml
├── misc.xml
├── modules.xml
├── plantuml-visualizer.iml
├── vcs.xml
└── watcherTasks.xml
├── .lintstagedrc.cjs
├── .prettierignore
├── .releaserc.json
├── .renovaterc.json
├── .tool-versions
├── .vscode
└── settings.json
├── .yarn
├── plugins
│ ├── plugin-auto-install.cjs
│ └── plugin-auto-install
│ │ └── .gitignore
└── releases
│ └── yarn-4.0.0-rc.47.cjs
├── .yarnrc.yml
├── LICENSE
├── NOTICE
├── README.md
├── allow-access.png
├── babel.config.json
├── example.png
├── icon
├── icon128.png
├── icon16.png
├── icon16gray.png
└── icon48.png
├── manifest.json
├── package.json
├── puml-sample
├── README.md
├── class.pu
├── included.pu
├── including.pu
├── japanese.pu
├── sequence.wsd
├── subincluded.pu
└── subincluding.pu
├── rollup.config.mjs
├── src
├── background.ts
├── components
│ ├── BodyContainer.svelte
│ ├── Button.svelte
│ ├── Footer.svelte
│ ├── Header.svelte
│ ├── ItemContainer.svelte
│ ├── ItemLabel.svelte
│ ├── Switch.svelte
│ └── TextField.svelte
├── config.ts
├── constants.ts
├── contentScripts.ts
├── directiveRegexes.ts
├── encoder
│ └── plantUmlEncoder.ts
├── finder
│ ├── codeBlockFinder.ts
│ ├── finder.ts
│ ├── finderUtil.ts
│ └── github
│ │ ├── gitHubFileViewFinder.ts
│ │ └── gitHubPullRequestDiffFinder.ts
├── mutator
│ ├── descriptionMutator.ts
│ ├── diffMutator.ts
│ └── mutatorUtil.ts
├── popup.html
├── popup.svelte
├── popup.ts
└── types
│ └── svelte.d.ts
├── tests
├── playwright.test.ts
└── playwrightFixtures.ts
├── tsconfig.json
└── yarn.lock
/.browserslistrc:
--------------------------------------------------------------------------------
1 | last 2 Chrome versions
2 | last 2 Firefox versions
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 |
9 | [*.{cjs,cpp,cts,dart,htm,html,js,json,json5,jsx,mjs,mts,pu,puml,rb,ts,tsx,vue,yaml,yml}]
10 | indent_size = 2
11 | indent_style = space
12 |
13 | [*.{go,gradle,py}]
14 | indent_size = 4
15 | indent_style = space
16 |
17 | [*.sh]
18 | indent_size = 8
19 | indent_style = space
20 |
21 | [*.md]
22 | max_line_length = off
23 | trim_trailing_whitespace = false
24 |
25 | [{Makefile,*.mk}]
26 | indent_style = tab
27 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Project-specific settings (head)
2 |
3 |
4 | # Generated by @willbooster/willboosterify
5 |
6 | 3rd-party/
7 | @types/
8 | __generated__/
9 | android/
10 | ios/
11 | no-format/
12 | test-fixtures/
13 | *.config.*js
14 | *.d.ts
15 | *.min.*js
16 | .yarn/
17 | .pnp.js
18 | dist.zip
19 |
20 | .devcontainer/
21 | dist/
22 | temp/
23 | Icon[
]
24 | !.keep
25 | test-results/
26 | # Created by https://www.toptal.com/developers/gitignore/api/windows
27 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows
28 |
29 | ### Windows ###
30 | # Windows thumbnail cache files
31 | Thumbs.db
32 | Thumbs.db:encryptable
33 | ehthumbs.db
34 | ehthumbs_vista.db
35 |
36 | # Dump file
37 | *.stackdump
38 |
39 | # Folder config file
40 | [Dd]esktop.ini
41 |
42 | # Recycle Bin used on file shares
43 | $RECYCLE.BIN/
44 |
45 | # Windows Installer files
46 | *.cab
47 | *.msi
48 | *.msix
49 | *.msm
50 | *.msp
51 |
52 | # Windows shortcuts
53 | *.lnk
54 |
55 | # End of https://www.toptal.com/developers/gitignore/api/windows
56 |
57 | # Created by https://www.toptal.com/developers/gitignore/api/macos
58 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos
59 |
60 | ### macOS ###
61 | # General
62 | .DS_Store
63 | .AppleDouble
64 | .LSOverride
65 |
66 | # Icon must end with two \r
67 | Icon
68 |
69 | # Thumbnails
70 | ._*
71 |
72 | # Files that might appear in the root of a volume
73 | .DocumentRevisions-V100
74 | .fseventsd
75 | .Spotlight-V100
76 | .TemporaryItems
77 | .Trashes
78 | .VolumeIcon.icns
79 | .com.apple.timemachine.donotpresent
80 |
81 | # Directories potentially created on remote AFP share
82 | .AppleDB
83 | .AppleDesktop
84 | Network Trash Folder
85 | Temporary Items
86 | .apdisk
87 |
88 | ### macOS Patch ###
89 | # iCloud generated files
90 | *.icloud
91 |
92 | # End of https://www.toptal.com/developers/gitignore/api/macos
93 |
94 | # Created by https://www.toptal.com/developers/gitignore/api/linux
95 | # Edit at https://www.toptal.com/developers/gitignore?templates=linux
96 |
97 | ### Linux ###
98 | *~
99 |
100 | # temporary files which can be created if a process still has a handle open of a deleted file
101 | .fuse_hidden*
102 |
103 | # KDE directory preferences
104 | .directory
105 |
106 | # Linux trash folder which might appear on any partition or disk
107 | .Trash-*
108 |
109 | # .nfs files are created when an open file is removed but is still being accessed
110 | .nfs*
111 |
112 | # End of https://www.toptal.com/developers/gitignore/api/linux
113 |
114 | # Created by https://www.toptal.com/developers/gitignore/api/jetbrains
115 | # Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains
116 |
117 | ### JetBrains ###
118 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
119 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
120 |
121 | # User-specific stuff
122 | .idea/**/workspace.xml
123 | .idea/**/tasks.xml
124 | .idea/**/usage.statistics.xml
125 | .idea/**/dictionaries
126 | .idea/**/shelf
127 |
128 | # AWS User-specific
129 | .idea/**/aws.xml
130 |
131 | # Generated files
132 | .idea/**/contentModel.xml
133 |
134 | # Sensitive or high-churn files
135 | .idea/**/dataSources/
136 | .idea/**/dataSources.ids
137 | .idea/**/dataSources.local.xml
138 | .idea/**/sqlDataSources.xml
139 | .idea/**/dynamic.xml
140 | .idea/**/uiDesigner.xml
141 | .idea/**/dbnavigator.xml
142 |
143 | # Gradle
144 | .idea/**/gradle.xml
145 | .idea/**/libraries
146 |
147 | # Gradle and Maven with auto-import
148 | # When using Gradle or Maven with auto-import, you should exclude module files,
149 | # since they will be recreated, and may cause churn. Uncomment if using
150 | # auto-import.
151 | # .idea/artifacts
152 | # .idea/compiler.xml
153 | # .idea/jarRepositories.xml
154 | # .idea/modules.xml
155 | # .idea/*.iml
156 | # .idea/modules
157 | # *.iml
158 | # *.ipr
159 |
160 | # CMake
161 | cmake-build-*/
162 |
163 | # Mongo Explorer plugin
164 | .idea/**/mongoSettings.xml
165 |
166 | # File-based project format
167 | *.iws
168 |
169 | # IntelliJ
170 | out/
171 |
172 | # mpeltonen/sbt-idea plugin
173 | .idea_modules/
174 |
175 | # JIRA plugin
176 | atlassian-ide-plugin.xml
177 |
178 | # Cursive Clojure plugin
179 | .idea/replstate.xml
180 |
181 | # SonarLint plugin
182 | .idea/sonarlint/
183 |
184 | # Crashlytics plugin (for Android Studio and IntelliJ)
185 | com_crashlytics_export_strings.xml
186 | crashlytics.properties
187 | crashlytics-build.properties
188 | fabric.properties
189 |
190 | # Editor-based Rest Client
191 | .idea/httpRequests
192 |
193 | # Android studio 3.1+ serialized cache file
194 | .idea/caches/build_file_checksums.ser
195 |
196 | ### JetBrains Patch ###
197 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
198 |
199 | # *.iml
200 | # modules.xml
201 | # .idea/misc.xml
202 | # *.ipr
203 |
204 | # Sonarlint plugin
205 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
206 | .idea/**/sonarlint/
207 |
208 | # SonarQube Plugin
209 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
210 | .idea/**/sonarIssues.xml
211 |
212 | # Markdown Navigator plugin
213 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
214 | .idea/**/markdown-navigator.xml
215 | .idea/**/markdown-navigator-enh.xml
216 | .idea/**/markdown-navigator/
217 |
218 | # Cache file creation bug
219 | # See https://youtrack.jetbrains.com/issue/JBR-2257
220 | .idea/$CACHE_FILE$
221 |
222 | # CodeStream plugin
223 | # https://plugins.jetbrains.com/plugin/12206-codestream
224 | .idea/codestream.xml
225 |
226 | # Azure Toolkit for IntelliJ plugin
227 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
228 | .idea/**/azureSettings.xml
229 |
230 | # End of https://www.toptal.com/developers/gitignore/api/jetbrains
231 |
232 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode
233 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode
234 |
235 | ### VisualStudioCode ###
236 | .vscode/*
237 | !.vscode/settings.json
238 | !.vscode/tasks.json
239 | !.vscode/launch.json
240 | !.vscode/extensions.json
241 | !.vscode/*.code-snippets
242 |
243 | # Local History for Visual Studio Code
244 | .history/
245 |
246 | # Built Visual Studio Code Extensions
247 | *.vsix
248 |
249 | ### VisualStudioCode Patch ###
250 | # Ignore all local history of files
251 | .history
252 | .ionide
253 |
254 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
255 |
256 | # Created by https://www.toptal.com/developers/gitignore/api/emacs
257 | # Edit at https://www.toptal.com/developers/gitignore?templates=emacs
258 |
259 | ### Emacs ###
260 | # -*- mode: gitignore; -*-
261 | *~
262 | \#*\#
263 | /.emacs.desktop
264 | /.emacs.desktop.lock
265 | *.elc
266 | auto-save-list
267 | tramp
268 | .\#*
269 |
270 | # Org-mode
271 | .org-id-locations
272 | *_archive
273 |
274 | # flymake-mode
275 | *_flymake.*
276 |
277 | # eshell files
278 | /eshell/history
279 | /eshell/lastdir
280 |
281 | # elpa packages
282 | /elpa/
283 |
284 | # reftex files
285 | *.rel
286 |
287 | # AUCTeX auto folder
288 | /auto/
289 |
290 | # cask packages
291 | .cask/
292 | dist/
293 |
294 | # Flycheck
295 | flycheck_*.el
296 |
297 | # server auth directory
298 | /server/
299 |
300 | # projectiles files
301 | .projectile
302 |
303 | # directory configuration
304 | .dir-locals.el
305 |
306 | # network security
307 | /network-security.data
308 |
309 | # End of https://www.toptal.com/developers/gitignore/api/emacs
310 |
311 | # Created by https://www.toptal.com/developers/gitignore/api/vim
312 | # Edit at https://www.toptal.com/developers/gitignore?templates=vim
313 |
314 | ### Vim ###
315 | # Swap
316 | [._]*.s[a-v][a-z]
317 | !*.svg # comment out if you don't need vector files
318 | [._]*.sw[a-p]
319 | [._]s[a-rt-v][a-z]
320 | [._]ss[a-gi-z]
321 | [._]sw[a-p]
322 |
323 | # Session
324 | Session.vim
325 | Sessionx.vim
326 |
327 | # Temporary
328 | .netrwhist
329 | *~
330 | # Auto-generated tag files
331 | tags
332 | # Persistent undo
333 | [._]*.un~
334 |
335 | # End of https://www.toptal.com/developers/gitignore/api/vim
336 |
337 | # Created by https://www.toptal.com/developers/gitignore/api/yarn
338 | # Edit at https://www.toptal.com/developers/gitignore?templates=yarn
339 |
340 | ### yarn ###
341 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
342 |
343 | .yarn/*
344 | !.yarn/releases
345 | !.yarn/patches
346 | !.yarn/plugins
347 | !.yarn/sdks
348 | !.yarn/versions
349 |
350 | # if you are NOT using Zero-installs, then:
351 | # comment the following lines
352 | # !.yarn/cache
353 |
354 | # and uncomment the following lines
355 | .pnp.*
356 |
357 | # End of https://www.toptal.com/developers/gitignore/api/yarn
358 |
359 | # Created by https://www.toptal.com/developers/gitignore/api/node
360 | # Edit at https://www.toptal.com/developers/gitignore?templates=node
361 |
362 | ### Node ###
363 | # Logs
364 | logs
365 | *.log
366 | npm-debug.log*
367 | yarn-debug.log*
368 | yarn-error.log*
369 | lerna-debug.log*
370 | .pnpm-debug.log*
371 |
372 | # Diagnostic reports (https://nodejs.org/api/report.html)
373 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
374 |
375 | # Runtime data
376 | pids
377 | *.pid
378 | *.seed
379 | *.pid.lock
380 |
381 | # Directory for instrumented libs generated by jscoverage/JSCover
382 | lib-cov
383 |
384 | # Coverage directory used by tools like istanbul
385 | coverage
386 | *.lcov
387 |
388 | # nyc test coverage
389 | .nyc_output
390 |
391 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
392 | .grunt
393 |
394 | # Bower dependency directory (https://bower.io/)
395 | bower_components
396 |
397 | # node-waf configuration
398 | .lock-wscript
399 |
400 | # Compiled binary addons (https://nodejs.org/api/addons.html)
401 | build/Release
402 |
403 | # Dependency directories
404 | node_modules/
405 | jspm_packages/
406 |
407 | # Snowpack dependency directory (https://snowpack.dev/)
408 | web_modules/
409 |
410 | # TypeScript cache
411 | *.tsbuildinfo
412 |
413 | # Optional npm cache directory
414 | .npm
415 |
416 | # Optional eslint cache
417 | .eslintcache
418 |
419 | # Optional stylelint cache
420 | .stylelintcache
421 |
422 | # Microbundle cache
423 | .rpt2_cache/
424 | .rts2_cache_cjs/
425 | .rts2_cache_es/
426 | .rts2_cache_umd/
427 |
428 | # Optional REPL history
429 | .node_repl_history
430 |
431 | # Output of 'npm pack'
432 | *.tgz
433 |
434 | # Yarn Integrity file
435 | .yarn-integrity
436 |
437 | # dotenv environment variable files
438 | .env
439 | .env.development.local
440 | .env.test.local
441 | .env.production.local
442 | .env.local
443 |
444 | # parcel-bundler cache (https://parceljs.org/)
445 | .cache
446 | .parcel-cache
447 |
448 | # Next.js build output
449 | .next
450 | out
451 |
452 | # Nuxt.js build / generate output
453 | .nuxt
454 | dist
455 |
456 | # Gatsby files
457 | .cache/
458 | # Comment in the public line in if your project uses Gatsby and not Next.js
459 | # https://nextjs.org/blog/next-9-1#public-directory-support
460 | # public
461 |
462 | # vuepress build output
463 | .vuepress/dist
464 |
465 | # vuepress v2.x temp and cache directory
466 | .temp
467 |
468 | # Docusaurus cache and generated files
469 | .docusaurus
470 |
471 | # Serverless directories
472 | .serverless/
473 |
474 | # FuseBox cache
475 | .fusebox/
476 |
477 | # DynamoDB Local files
478 | .dynamodb/
479 |
480 | # TernJS port file
481 | .tern-port
482 |
483 | # Stores VSCode versions used for testing VSCode extensions
484 | .vscode-test
485 |
486 | # yarn v2
487 | .yarn/cache
488 | .yarn/unplugged
489 | .yarn/build-state.yml
490 | .yarn/install-state.gz
491 | .pnp.*
492 |
493 | ### Node Patch ###
494 | # Serverless Webpack directories
495 | .webpack/
496 |
497 | # Optional stylelint cache
498 |
499 | # SvelteKit build / generate output
500 | .svelte-kit
501 |
502 | # End of https://www.toptal.com/developers/gitignore/api/node
503 |
504 | # Project-specific settings (tail)
505 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['@willbooster/eslint-config-ts'],
4 | plugins: ['svelte3'],
5 | overrides: [
6 | {
7 | files: ['*.svelte'],
8 | processor: 'svelte3/svelte3',
9 | },
10 | ],
11 | settings: {
12 | 'svelte3/ignore-styles': (attributes) => attributes.lang === 'scss',
13 | 'svelte3/typescript': require('typescript'),
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | { "root": true, "extends": ["@willbooster/eslint-config-ts"] }
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
3 | *.vcproj text eol=crlf
4 |
5 | *.cjs text eol=lf
6 | *.cpp text eol=lf
7 | *.cts text eol=lf
8 | *.dart text eol=lf
9 | *.htm text eol=lf
10 | *.html text eol=lf
11 | *.js text eol=lf
12 | *.json text eol=lf
13 | *.json5 text eol=lf
14 | *.jsx text eol=lf
15 | *.mjs text eol=lf
16 | *.mts text eol=lf
17 | *.pu text eol=lf
18 | *.puml text eol=lf
19 | *.rb text eol=lf
20 | *.ts text eol=lf
21 | *.tsx text eol=lf
22 | *.vue text eol=lf
23 | *.yaml text eol=lf
24 | *.yml text eol=lf
25 | *.go text eol=lf
26 | *.gradle text eol=lf
27 | *.py text eol=lf
28 | *.md text eol=lf
29 |
--------------------------------------------------------------------------------
/.github/workflows/add-issue-to-project.yml:
--------------------------------------------------------------------------------
1 | name: Add issue to github project
2 | on:
3 | issues:
4 | types:
5 | - labeled
6 | jobs:
7 | add-issue-to-project:
8 | uses: WillBooster/reusable-workflows/.github/workflows/add-issue-to-project.yml@main
9 | secrets:
10 | GH_PROJECT_URL: ${{ secrets.GH_PROJECT_URL }}
11 | GH_TOKEN: ${{ secrets.PUBLIC_GH_BOT_PAT }}
12 |
--------------------------------------------------------------------------------
/.github/workflows/close-comment.yml:
--------------------------------------------------------------------------------
1 | name: Add close comment
2 | on:
3 | pull_request:
4 | types:
5 | - opened
6 | jobs:
7 | close-comment:
8 | uses: WillBooster/reusable-workflows/.github/workflows/close-comment.yml@main
9 |
--------------------------------------------------------------------------------
/.github/workflows/notify-ready.yml:
--------------------------------------------------------------------------------
1 | name: Notify ready
2 | on:
3 | issues:
4 | types:
5 | - labeled
6 | jobs:
7 | notify-ready:
8 | uses: WillBooster/reusable-workflows/.github/workflows/notify-ready.yml@main
9 | secrets:
10 | DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL_FOR_READY }}
11 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | workflow_dispatch:
4 | schedule:
5 | - cron: 0 5 * * 0
6 | concurrency:
7 | group: ${{ github.workflow }}
8 | cancel-in-progress: false
9 | jobs:
10 | release:
11 | uses: WillBooster/reusable-workflows/.github/workflows/release.yml@main
12 | with:
13 | github_hosted_runner: true
14 | secrets:
15 | DOT_ENV: ${{ secrets.DOT_ENV }}
16 | GH_TOKEN: ${{ secrets.PUBLIC_GH_BOT_PAT }}
17 |
--------------------------------------------------------------------------------
/.github/workflows/semantic-pr.yml:
--------------------------------------------------------------------------------
1 | name: Lint PR title
2 | on:
3 | pull_request_target:
4 | types:
5 | - opened
6 | - edited
7 | - synchronize
8 | jobs:
9 | semantic-pr:
10 | uses: WillBooster/reusable-workflows/.github/workflows/semantic-pr.yml@main
11 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | pull_request:
4 | paths-ignore:
5 | - '**.md'
6 | - '**/docs/**'
7 | push:
8 | branches:
9 | - main
10 | - wbfy
11 | - renovate/**
12 | paths-ignore:
13 | - '**.md'
14 | - '**/docs/**'
15 | concurrency:
16 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
17 | cancel-in-progress: true
18 | jobs:
19 | test:
20 | uses: WillBooster/reusable-workflows/.github/workflows/test.yml@main
21 | with:
22 | github_hosted_runner: true
23 | secrets:
24 | GH_TOKEN: ${{ secrets.PUBLIC_GH_BOT_PAT }}
25 |
--------------------------------------------------------------------------------
/.github/workflows/wbfy-merge.yml:
--------------------------------------------------------------------------------
1 | name: Merge wbfy
2 | on:
3 | workflow_dispatch:
4 | schedule:
5 | - cron: 9 16 * * *
6 | jobs:
7 | wbfy-merge:
8 | uses: WillBooster/reusable-workflows/.github/workflows/wbfy-merge.yml@main
9 | with:
10 | github_hosted_runner: true
11 | secrets:
12 | GH_TOKEN: ${{ secrets.PUBLIC_GH_BOT_PAT }}
13 |
--------------------------------------------------------------------------------
/.github/workflows/wbfy.yml:
--------------------------------------------------------------------------------
1 | name: Willboosterify
2 | on:
3 | workflow_dispatch:
4 | schedule:
5 | - cron: 7 14 * * *
6 | jobs:
7 | wbfy:
8 | uses: WillBooster/reusable-workflows/.github/workflows/wbfy.yml@main
9 | with:
10 | github_hosted_runner: true
11 | secrets:
12 | GH_TOKEN: ${{ secrets.PUBLIC_GH_BOT_PAT }}
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project-specific settings (head)
2 |
3 | dist.zip
4 |
5 | # Generated by @willbooster/willboosterify
6 |
7 | .devcontainer/
8 | dist/
9 | temp/
10 | Icon[
]
11 | !.keep
12 | test-results/
13 | # Created by https://www.toptal.com/developers/gitignore/api/windows
14 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows
15 |
16 | ### Windows ###
17 | # Windows thumbnail cache files
18 | Thumbs.db
19 | Thumbs.db:encryptable
20 | ehthumbs.db
21 | ehthumbs_vista.db
22 |
23 | # Dump file
24 | *.stackdump
25 |
26 | # Folder config file
27 | [Dd]esktop.ini
28 |
29 | # Recycle Bin used on file shares
30 | $RECYCLE.BIN/
31 |
32 | # Windows Installer files
33 | *.cab
34 | *.msi
35 | *.msix
36 | *.msm
37 | *.msp
38 |
39 | # Windows shortcuts
40 | *.lnk
41 |
42 | # End of https://www.toptal.com/developers/gitignore/api/windows
43 |
44 | # Created by https://www.toptal.com/developers/gitignore/api/macos
45 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos
46 |
47 | ### macOS ###
48 | # General
49 | .DS_Store
50 | .AppleDouble
51 | .LSOverride
52 |
53 | # Icon must end with two \r
54 | Icon
55 |
56 | # Thumbnails
57 | ._*
58 |
59 | # Files that might appear in the root of a volume
60 | .DocumentRevisions-V100
61 | .fseventsd
62 | .Spotlight-V100
63 | .TemporaryItems
64 | .Trashes
65 | .VolumeIcon.icns
66 | .com.apple.timemachine.donotpresent
67 |
68 | # Directories potentially created on remote AFP share
69 | .AppleDB
70 | .AppleDesktop
71 | Network Trash Folder
72 | Temporary Items
73 | .apdisk
74 |
75 | ### macOS Patch ###
76 | # iCloud generated files
77 | *.icloud
78 |
79 | # End of https://www.toptal.com/developers/gitignore/api/macos
80 |
81 | # Created by https://www.toptal.com/developers/gitignore/api/linux
82 | # Edit at https://www.toptal.com/developers/gitignore?templates=linux
83 |
84 | ### Linux ###
85 | *~
86 |
87 | # temporary files which can be created if a process still has a handle open of a deleted file
88 | .fuse_hidden*
89 |
90 | # KDE directory preferences
91 | .directory
92 |
93 | # Linux trash folder which might appear on any partition or disk
94 | .Trash-*
95 |
96 | # .nfs files are created when an open file is removed but is still being accessed
97 | .nfs*
98 |
99 | # End of https://www.toptal.com/developers/gitignore/api/linux
100 |
101 | # Created by https://www.toptal.com/developers/gitignore/api/jetbrains
102 | # Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains
103 |
104 | ### JetBrains ###
105 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
106 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
107 |
108 | # User-specific stuff
109 | .idea/**/workspace.xml
110 | .idea/**/tasks.xml
111 | .idea/**/usage.statistics.xml
112 | .idea/**/dictionaries
113 | .idea/**/shelf
114 |
115 | # AWS User-specific
116 | .idea/**/aws.xml
117 |
118 | # Generated files
119 | .idea/**/contentModel.xml
120 |
121 | # Sensitive or high-churn files
122 | .idea/**/dataSources/
123 | .idea/**/dataSources.ids
124 | .idea/**/dataSources.local.xml
125 | .idea/**/sqlDataSources.xml
126 | .idea/**/dynamic.xml
127 | .idea/**/uiDesigner.xml
128 | .idea/**/dbnavigator.xml
129 |
130 | # Gradle
131 | .idea/**/gradle.xml
132 | .idea/**/libraries
133 |
134 | # Gradle and Maven with auto-import
135 | # When using Gradle or Maven with auto-import, you should exclude module files,
136 | # since they will be recreated, and may cause churn. Uncomment if using
137 | # auto-import.
138 | # .idea/artifacts
139 | # .idea/compiler.xml
140 | # .idea/jarRepositories.xml
141 | # .idea/modules.xml
142 | # .idea/*.iml
143 | # .idea/modules
144 | # *.iml
145 | # *.ipr
146 |
147 | # CMake
148 | cmake-build-*/
149 |
150 | # Mongo Explorer plugin
151 | .idea/**/mongoSettings.xml
152 |
153 | # File-based project format
154 | *.iws
155 |
156 | # IntelliJ
157 | out/
158 |
159 | # mpeltonen/sbt-idea plugin
160 | .idea_modules/
161 |
162 | # JIRA plugin
163 | atlassian-ide-plugin.xml
164 |
165 | # Cursive Clojure plugin
166 | .idea/replstate.xml
167 |
168 | # SonarLint plugin
169 | .idea/sonarlint/
170 |
171 | # Crashlytics plugin (for Android Studio and IntelliJ)
172 | com_crashlytics_export_strings.xml
173 | crashlytics.properties
174 | crashlytics-build.properties
175 | fabric.properties
176 |
177 | # Editor-based Rest Client
178 | .idea/httpRequests
179 |
180 | # Android studio 3.1+ serialized cache file
181 | .idea/caches/build_file_checksums.ser
182 |
183 | ### JetBrains Patch ###
184 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
185 |
186 | # *.iml
187 | # modules.xml
188 | # .idea/misc.xml
189 | # *.ipr
190 |
191 | # Sonarlint plugin
192 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
193 | .idea/**/sonarlint/
194 |
195 | # SonarQube Plugin
196 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
197 | .idea/**/sonarIssues.xml
198 |
199 | # Markdown Navigator plugin
200 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
201 | .idea/**/markdown-navigator.xml
202 | .idea/**/markdown-navigator-enh.xml
203 | .idea/**/markdown-navigator/
204 |
205 | # Cache file creation bug
206 | # See https://youtrack.jetbrains.com/issue/JBR-2257
207 | .idea/$CACHE_FILE$
208 |
209 | # CodeStream plugin
210 | # https://plugins.jetbrains.com/plugin/12206-codestream
211 | .idea/codestream.xml
212 |
213 | # Azure Toolkit for IntelliJ plugin
214 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
215 | .idea/**/azureSettings.xml
216 |
217 | # End of https://www.toptal.com/developers/gitignore/api/jetbrains
218 |
219 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode
220 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode
221 |
222 | ### VisualStudioCode ###
223 | .vscode/*
224 | !.vscode/settings.json
225 | !.vscode/tasks.json
226 | !.vscode/launch.json
227 | !.vscode/extensions.json
228 | !.vscode/*.code-snippets
229 |
230 | # Local History for Visual Studio Code
231 | .history/
232 |
233 | # Built Visual Studio Code Extensions
234 | *.vsix
235 |
236 | ### VisualStudioCode Patch ###
237 | # Ignore all local history of files
238 | .history
239 | .ionide
240 |
241 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
242 |
243 | # Created by https://www.toptal.com/developers/gitignore/api/emacs
244 | # Edit at https://www.toptal.com/developers/gitignore?templates=emacs
245 |
246 | ### Emacs ###
247 | # -*- mode: gitignore; -*-
248 | *~
249 | \#*\#
250 | /.emacs.desktop
251 | /.emacs.desktop.lock
252 | *.elc
253 | auto-save-list
254 | tramp
255 | .\#*
256 |
257 | # Org-mode
258 | .org-id-locations
259 | *_archive
260 |
261 | # flymake-mode
262 | *_flymake.*
263 |
264 | # eshell files
265 | /eshell/history
266 | /eshell/lastdir
267 |
268 | # elpa packages
269 | /elpa/
270 |
271 | # reftex files
272 | *.rel
273 |
274 | # AUCTeX auto folder
275 | /auto/
276 |
277 | # cask packages
278 | .cask/
279 | dist/
280 |
281 | # Flycheck
282 | flycheck_*.el
283 |
284 | # server auth directory
285 | /server/
286 |
287 | # projectiles files
288 | .projectile
289 |
290 | # directory configuration
291 | .dir-locals.el
292 |
293 | # network security
294 | /network-security.data
295 |
296 |
297 | # End of https://www.toptal.com/developers/gitignore/api/emacs
298 |
299 | # Created by https://www.toptal.com/developers/gitignore/api/vim
300 | # Edit at https://www.toptal.com/developers/gitignore?templates=vim
301 |
302 | ### Vim ###
303 | # Swap
304 | [._]*.s[a-v][a-z]
305 | !*.svg # comment out if you don't need vector files
306 | [._]*.sw[a-p]
307 | [._]s[a-rt-v][a-z]
308 | [._]ss[a-gi-z]
309 | [._]sw[a-p]
310 |
311 | # Session
312 | Session.vim
313 | Sessionx.vim
314 |
315 | # Temporary
316 | .netrwhist
317 | *~
318 | # Auto-generated tag files
319 | tags
320 | # Persistent undo
321 | [._]*.un~
322 |
323 | # End of https://www.toptal.com/developers/gitignore/api/vim
324 |
325 | # Created by https://www.toptal.com/developers/gitignore/api/yarn
326 | # Edit at https://www.toptal.com/developers/gitignore?templates=yarn
327 |
328 | ### yarn ###
329 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
330 |
331 | .yarn/*
332 | !.yarn/releases
333 | !.yarn/patches
334 | !.yarn/plugins
335 | !.yarn/sdks
336 | !.yarn/versions
337 |
338 | # if you are NOT using Zero-installs, then:
339 | # comment the following lines
340 | # !.yarn/cache
341 |
342 | # and uncomment the following lines
343 | .pnp.*
344 |
345 | # End of https://www.toptal.com/developers/gitignore/api/yarn
346 |
347 | # Created by https://www.toptal.com/developers/gitignore/api/node
348 | # Edit at https://www.toptal.com/developers/gitignore?templates=node
349 |
350 | ### Node ###
351 | # Logs
352 | logs
353 | *.log
354 | npm-debug.log*
355 | yarn-debug.log*
356 | yarn-error.log*
357 | lerna-debug.log*
358 | .pnpm-debug.log*
359 |
360 | # Diagnostic reports (https://nodejs.org/api/report.html)
361 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
362 |
363 | # Runtime data
364 | pids
365 | *.pid
366 | *.seed
367 | *.pid.lock
368 |
369 | # Directory for instrumented libs generated by jscoverage/JSCover
370 | lib-cov
371 |
372 | # Coverage directory used by tools like istanbul
373 | coverage
374 | *.lcov
375 |
376 | # nyc test coverage
377 | .nyc_output
378 |
379 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
380 | .grunt
381 |
382 | # Bower dependency directory (https://bower.io/)
383 | bower_components
384 |
385 | # node-waf configuration
386 | .lock-wscript
387 |
388 | # Compiled binary addons (https://nodejs.org/api/addons.html)
389 | build/Release
390 |
391 | # Dependency directories
392 | node_modules/
393 | jspm_packages/
394 |
395 | # Snowpack dependency directory (https://snowpack.dev/)
396 | web_modules/
397 |
398 | # TypeScript cache
399 | *.tsbuildinfo
400 |
401 | # Optional npm cache directory
402 | .npm
403 |
404 | # Optional eslint cache
405 | .eslintcache
406 |
407 | # Optional stylelint cache
408 | .stylelintcache
409 |
410 | # Microbundle cache
411 | .rpt2_cache/
412 | .rts2_cache_cjs/
413 | .rts2_cache_es/
414 | .rts2_cache_umd/
415 |
416 | # Optional REPL history
417 | .node_repl_history
418 |
419 | # Output of 'npm pack'
420 | *.tgz
421 |
422 | # Yarn Integrity file
423 | .yarn-integrity
424 |
425 | # dotenv environment variable files
426 | .env
427 | .env.development.local
428 | .env.test.local
429 | .env.production.local
430 | .env.local
431 |
432 | # parcel-bundler cache (https://parceljs.org/)
433 | .cache
434 | .parcel-cache
435 |
436 | # Next.js build output
437 | .next
438 | out
439 |
440 | # Nuxt.js build / generate output
441 | .nuxt
442 | dist
443 |
444 | # Gatsby files
445 | .cache/
446 | # Comment in the public line in if your project uses Gatsby and not Next.js
447 | # https://nextjs.org/blog/next-9-1#public-directory-support
448 | # public
449 |
450 | # vuepress build output
451 | .vuepress/dist
452 |
453 | # vuepress v2.x temp and cache directory
454 | .temp
455 |
456 | # Docusaurus cache and generated files
457 | .docusaurus
458 |
459 | # Serverless directories
460 | .serverless/
461 |
462 | # FuseBox cache
463 | .fusebox/
464 |
465 | # DynamoDB Local files
466 | .dynamodb/
467 |
468 | # TernJS port file
469 | .tern-port
470 |
471 | # Stores VSCode versions used for testing VSCode extensions
472 | .vscode-test
473 |
474 | # yarn v2
475 | .yarn/cache
476 | .yarn/unplugged
477 | .yarn/build-state.yml
478 | .yarn/install-state.gz
479 | .pnp.*
480 |
481 | ### Node Patch ###
482 | # Serverless Webpack directories
483 | .webpack/
484 |
485 | # Optional stylelint cache
486 |
487 | # SvelteKit build / generate output
488 | .svelte-kit
489 |
490 | # End of https://www.toptal.com/developers/gitignore/api/node
491 |
492 | # Project-specific settings (tail)
493 |
--------------------------------------------------------------------------------
/.husky/post-merge:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"
5 |
6 | run_if_changed() {
7 | if echo "$changed_files" | grep --quiet -E "$1"; then
8 | eval "$2"
9 | fi
10 | }
11 |
12 | run_if_changed "\..+-version" "asdf plugin update --all"
13 | run_if_changed "\..+-version" "asdf install"
14 | run_if_changed "package\.json" "yarn"
15 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | node node_modules/.bin/lint-staged
5 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | node node_modules/.bin/tsc --noEmit --Pretty
5 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
` tag with prefix `@startuml` and suffix `@enduml` 35 | 36 | (We will list GitLab pages with testing urls later) 37 | 38 | ### .pu / .puml / .plantuml / .wsd files 39 | 40 | - GitHub Raw Files (only Chrome) 41 | - https://raw.githubusercontent.com/WillBooster/plantuml-visualizer/master/puml-sample/class.pu 42 | - `!include` directive: https://raw.githubusercontent.com/WillBooster/plantuml-visualizer/master/puml-sample/including.pu 43 | - `!includesub` directive: https://raw.githubusercontent.com/WillBooster/plantuml-visualizer/master/puml-sample/subincluding.pu 44 | - IMPORTANT NOTE: any extension on Firefox cannot work on GitHub Raw Files due to https://bugzilla.mozilla.org/show_bug.cgi?id=1411641 45 | - Local Files 46 | - file:///C:/Users/XXX/Projects/plantuml-visualizer/puml-sample/class.pu 47 | - `!include` directive for local files will NOT be supported because of security problems 48 | - Please use another software for rich rendering of local files (e.g. the official PlantUML renderer: https://plantuml.com/en/starting) 49 | - IMPORTANT NOTE: if you use Google Chrome, you need to allow this extension to access file URLs 50 | 1. Open chrome://extensions/?id=ffaloebcmkogfdkemcekamlmfkkmgkcf in Chrome 51 | 2. Enable "Allow access to file URLs" 52 |  53 | 54 | ### Improve Default Allow/Deny Lists 55 | 56 | The default lists are defined at https://github.com/WillBooster/plantuml-visualizer/blob/main/src/constants.ts 57 | Please help us to improve the default lists for enabling/disabling visualization on specific web pages! 58 | 59 | ## Visualization Examples 60 | 61 | The visualization result of https://github.com/WillBooster/plantuml-visualizer/pull/24/files is as follows. 62 | 63 |  64 | 65 | ## Default Visualization Server 66 | 67 | The default server is https://plantuml-service-willbooster.fly.dev 68 | ([source code](https://github.com/WillBooster/plantuml-service)). 69 | You may check the PlantUML version via [this link](https://plantuml-service-willbooster.fly.dev/version). 70 | 71 | You may use another **HTTPS** PlantUML server by changing settings in the configuration window. 72 | 73 | ## Requirements for Development 74 | 75 | - [Node.js](https://nodejs.org/) 76 | - We define a recommended version on https://github.com/WillBooster/plantuml-visualizer/blob/main/.node-version 77 | - [Yarn v1](https://classic.yarnpkg.com/) 78 | 79 | ## Development Preparation 80 | 81 | 1. `yarn` to install the latest dependencies 82 | 2. `yarn build` 83 | 3. Open Chrome browser 84 | 4. Open [chrome://extensions](chrome://extensions) 85 | 5. Enable `Developer Mode` 86 | 6. Click `Load Unpacked` and open `dist` directory (`plantuml-visualizer/dist`) 87 | - `Load Unpacked` is `パッケージ化されていない拡張機能を読み込む` in Japanese 88 | 89 | ## Development 90 | 91 | 1. `yarn` to install the latest dependencies 92 | 2. `yarn start` 93 | 3. Open Chrome 94 | 4. Rewrite some code files 95 | 5. Close and Reopen Chrome browser (not only tabs) 96 | - or reload this extension in [chrome://extensions](chrome://extensions) and reload pages 97 | 6. Debug code 98 | 7. Go to `step 4` 99 | 100 | ## Deployment for Chrome 101 | 102 | 1. Bump version in `manifest.json` and `package.json` 103 | 2. `yarn package` 104 | 3. Open https://chrome.google.com/webstore/developer/dashboard 105 | 4. Upload `dist.zip` 106 | 107 | ## Deployment for Firefox 108 | 109 | 1. Bump version in `manifest.json` and `package.json` 110 | 2. `yarn package` 111 | 3. Open https://addons.mozilla.org/en-US/developers/addon/plantuml-visualizer/edit 112 | 4. Upload `dist.zip` file 113 | -------------------------------------------------------------------------------- /allow-access.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WillBooster/plantuml-visualizer/20a34e8cab6a894960407936480d63e367efc487/allow-access.png -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/typescript"], 3 | "plugins": ["@babel/proposal-class-properties", "@babel/proposal-numeric-separator"], 4 | "env": { 5 | "production": { 6 | "plugins": [ 7 | [ 8 | "transform-remove-console", 9 | { 10 | "exclude": ["error", "info", "warn"] 11 | } 12 | ] 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WillBooster/plantuml-visualizer/20a34e8cab6a894960407936480d63e367efc487/example.png -------------------------------------------------------------------------------- /icon/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WillBooster/plantuml-visualizer/20a34e8cab6a894960407936480d63e367efc487/icon/icon128.png -------------------------------------------------------------------------------- /icon/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WillBooster/plantuml-visualizer/20a34e8cab6a894960407936480d63e367efc487/icon/icon16.png -------------------------------------------------------------------------------- /icon/icon16gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WillBooster/plantuml-visualizer/20a34e8cab6a894960407936480d63e367efc487/icon/icon16gray.png -------------------------------------------------------------------------------- /icon/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WillBooster/plantuml-visualizer/20a34e8cab6a894960407936480d63e367efc487/icon/icon48.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "PlantUML Visualizer", 4 | "description": "A Chrome / Firefox extension for visualizing PlantUML descriptions.", 5 | "version": "0.0.0", 6 | "browser_action": { 7 | "default_icon": "icon/icon16.png", 8 | "default_popup": "popup.html" 9 | }, 10 | "icons": { 11 | "16": "icon/icon16.png", 12 | "48": "icon/icon48.png", 13 | "128": "icon/icon128.png" 14 | }, 15 | "permissions": ["https://*/*", "http://*/*", "storage"], 16 | "background": { 17 | "scripts": ["background.js"] 18 | }, 19 | "content_scripts": [ 20 | { 21 | "matches": ["https://*/*", "http://*/*", "file:///*/*"], 22 | "js": ["content_scripts.js"] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plantuml-visualizer", 3 | "version": "0.0.0-semantically-released", 4 | "private": true, 5 | "description": "A Chrome / Firefox extension for visualizing PlantUML code.", 6 | "homepage": "https://github.com/WillBooster/plantuml-visualizer", 7 | "bugs": { 8 | "url": "https://github.com/WillBooster/plantuml-visualizer/issues" 9 | }, 10 | "repository": "github:WillBooster/plantuml-visualizer", 11 | "license": "Apache-2.0", 12 | "author": "WillBooster Inc.", 13 | "scripts": { 14 | "build": "cross-env NODE_ENV=production yarn build-common", 15 | "build-common": "yarn recreate-dist && rollup -c", 16 | "build-dev": "cross-env NODE_ENV=development yarn build-common", 17 | "cleanup": "yarn format && yarn lint-fix", 18 | "format": "sort-package-json && yarn prettify", 19 | "postinstall": "husky install", 20 | "lint": "eslint --color \"./{scripts,src,tests}/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}\"", 21 | "lint-fix": "yarn lint --fix", 22 | "prepack": "pinst --disable", 23 | "postpack": "pinst --enable", 24 | "package": "rm -f \"*.tgz\" \"*.zip\" && yarn pack && yarn build && cd dist && bestzip ../dist.zip *", 25 | "prettify": "prettier --cache --color --write \"**/{.*/,}*.{cjs,css,cts,htm,html,js,json,json5,jsx,md,mjs,mts,scss,ts,tsx,vue,yaml,yml}\" \"!**/test-fixtures/**\"", 26 | "recreate-dist": "rm -Rf dist && mkdir dist && cp -r src/popup.html icon manifest.json dist/", 27 | "release": "yarn build && dotenv -- semantic-release", 28 | "release-test": "dotenv -- semantic-release --dry-run", 29 | "start": "cross-env NODE_ENV=development yarn build-common -w", 30 | "test": "playwright test", 31 | "test/ci-setup": "playwright install chromium", 32 | "typecheck": "tsc --noEmit --Pretty" 33 | }, 34 | "prettier": "@willbooster/prettier-config", 35 | "dependencies": { 36 | "jquery": "3.7.0", 37 | "pako": "2.1.0" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "7.22.5", 41 | "@babel/plugin-proposal-class-properties": "7.18.6", 42 | "@babel/plugin-proposal-numeric-separator": "7.18.6", 43 | "@babel/plugin-proposal-optional-chaining": "7.21.0", 44 | "@babel/preset-env": "7.22.5", 45 | "@babel/preset-react": "7.22.5", 46 | "@babel/preset-typescript": "7.22.5", 47 | "@playwright/test": "1.35.1", 48 | "@rollup/plugin-babel": "6.0.3", 49 | "@rollup/plugin-commonjs": "25.0.2", 50 | "@rollup/plugin-node-resolve": "15.1.0", 51 | "@tsconfig/svelte": "4.0.1", 52 | "@types/chrome": "0.0.239", 53 | "@types/eslint": "8.40.2", 54 | "@types/jquery": "3.5.16", 55 | "@types/micromatch": "4.0.2", 56 | "@types/pako": "2.0.0", 57 | "@types/sass": "1.43.1", 58 | "@typescript-eslint/eslint-plugin": "5.60.1", 59 | "@typescript-eslint/parser": "5.60.1", 60 | "@willbooster/eslint-config-ts": "10.2.0", 61 | "@willbooster/prettier-config": "9.1.1", 62 | "@willbooster/renovate-config": "9.5.0", 63 | "babel-plugin-transform-remove-console": "6.9.4", 64 | "bestzip": "2.2.1", 65 | "conventional-changelog-conventionalcommits": "6.1.0", 66 | "cross-env": "7.0.3", 67 | "dotenv-cli": "7.2.1", 68 | "eslint": "8.43.0", 69 | "eslint-config-prettier": "8.8.0", 70 | "eslint-import-resolver-typescript": "3.5.5", 71 | "eslint-plugin-import": "2.27.5", 72 | "eslint-plugin-sort-class-members": "1.18.0", 73 | "eslint-plugin-sort-destructure-keys": "1.5.0", 74 | "eslint-plugin-svelte3": "4.0.0", 75 | "eslint-plugin-unicorn": "47.0.0", 76 | "husky": "8.0.3", 77 | "lint-staged": "13.2.3", 78 | "micromatch": "4.0.5", 79 | "pinst": "3.0.0", 80 | "prettier": "2.8.8", 81 | "prettier-plugin-svelte": "2.10.1", 82 | "rollup": "3.26.0", 83 | "rollup-plugin-svelte": "7.1.6", 84 | "rollup-plugin-terser": "7.0.2", 85 | "sass": "1.63.6", 86 | "semantic-release": "20.1.3", 87 | "semantic-release-chrome": "3.2.0", 88 | "semantic-release-firefox-add-on": "0.2.8", 89 | "sort-package-json": "2.4.1", 90 | "svelte": "4.0.1", 91 | "svelte-check": "3.4.4", 92 | "svelte-preprocess": "5.0.4", 93 | "typescript": "5.1.6" 94 | }, 95 | "packageManager": "yarn@4.0.0-rc.47" 96 | } 97 | -------------------------------------------------------------------------------- /puml-sample/README.md: -------------------------------------------------------------------------------- 1 | # Sample README 2 | 3 | ## Sample code block 4 | 5 | The following code block should be visualized by our extension. 6 | 7 | ``` 8 | @startuml 9 | class A { 10 | -int privateField 11 | +String publicField 12 | -void privateMethod() 13 | +int publicMethod() 14 | } 15 | @enduml 16 | ``` 17 | 18 | ## Sample PlantUML files 19 | 20 | - [class.pu](./class.pu) 21 | - [sequence.wsd](./sequence.wsd) 22 | - [japanese.pu](./japanese.pu) 23 | - [including.pu](./including.pu) 24 | - [subincluding.pu](./subincluding.pu) 25 | -------------------------------------------------------------------------------- /puml-sample/class.pu: -------------------------------------------------------------------------------- 1 | @startuml 2 | class A { 3 | -int privateField 4 | +String publicField 5 | -void privateMethod() 6 | +int publicMethod() 7 | } 8 | 9 | class B { 10 | } 11 | 12 | class C { 13 | } 14 | 15 | A <|-- B 16 | B --> C 17 | @enduml 18 | -------------------------------------------------------------------------------- /puml-sample/included.pu: -------------------------------------------------------------------------------- 1 | @startuml 2 | state IncludedState { 3 | state Sub1 4 | state Sub2 5 | state Sub3 6 | 7 | [*] -> Sub1 8 | Sub1 -> Sub2 9 | Sub2 -> Sub3 10 | Sub1 -> Sub3 11 | Sub3 -> [*] 12 | } 13 | @enduml 14 | -------------------------------------------------------------------------------- /puml-sample/including.pu: -------------------------------------------------------------------------------- 1 | @startuml 2 | !include included.pu 3 | state State1 4 | state State2 5 | 6 | [*] -> State1 7 | State1 -> IncludedState 8 | IncludedState -> State2 9 | State2 -> [*] 10 | @enduml 11 | -------------------------------------------------------------------------------- /puml-sample/japanese.pu: -------------------------------------------------------------------------------- 1 | @startuml 2 | title シーケンス図 3 | 4 | 太郎->花子: 認証リクエスト 5 | note right of 花子: 花子が認証処理 6 | 花子->太郎: 認証応答 7 | 8 | @enduml 9 | -------------------------------------------------------------------------------- /puml-sample/sequence.wsd: -------------------------------------------------------------------------------- 1 | title Web Sequence Diagram 2 | 3 | Alice->Bob: Authentication Request 4 | note right of Bob: Bob thinks about it 5 | Bob->Alice: Authentication Response 6 | -------------------------------------------------------------------------------- /puml-sample/subincluded.pu: -------------------------------------------------------------------------------- 1 | @startuml 2 | !startsub sub 3 | state IncludedState1 { 4 | state Sub11 5 | state Sub12 6 | state Sub13 7 | 8 | [*] -> Sub11 9 | Sub11 -> Sub12 10 | Sub12 -> Sub13 11 | Sub11 -> Sub13 12 | Sub13 -> [*] 13 | } 14 | !endsub 15 | 16 | state IngoredIncludedState { 17 | state Sub1 18 | state Sub2 19 | state Sub3 20 | 21 | [*] -> Sub1 22 | Sub1 -> Sub2 23 | Sub2 -> Sub3 24 | Sub1 -> Sub3 25 | Sub3 -> [*] 26 | } 27 | 28 | !startsub sub 29 | state IncludedState2 { 30 | state Sub21 31 | state Sub22 32 | state Sub23 33 | 34 | [*] -> Sub21 35 | Sub21 -> Sub22 36 | Sub22 -> Sub23 37 | Sub21 -> Sub23 38 | Sub23 -> [*] 39 | } 40 | !endsub 41 | @enduml 42 | -------------------------------------------------------------------------------- /puml-sample/subincluding.pu: -------------------------------------------------------------------------------- 1 | @startuml 2 | !includesub subincluded.pu!sub 3 | state State1 4 | state State2 5 | 6 | [*] -> State1 7 | State1 -> IncludedState1 8 | IncludedState1 -> IngoredIncludedState 9 | IngoredIncludedState -> IncludedState2 10 | IncludedState2 -> State2 11 | State2 -> [*] 12 | @enduml 13 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import svelte from 'rollup-plugin-svelte'; 4 | import sveltePreprocess from 'svelte-preprocess'; 5 | import babel from '@rollup/plugin-babel'; 6 | import { terser } from 'rollup-plugin-terser'; 7 | 8 | const extensions = ['.mjs', '.js', '.json', '.ts']; 9 | 10 | const plugins = [ 11 | resolve({ extensions }), 12 | commonjs(), 13 | svelte({ include: 'src/**/*.svelte', preprocess: sveltePreprocess(), emitCss: false }), 14 | babel({ extensions, babelHelpers: 'bundled', exclude: 'node_modules/**' }), 15 | ]; 16 | if (process.env.NODE_ENV === 'production') plugins.push(terser()); 17 | 18 | export default [ 19 | { 20 | input: 'src/background.ts', 21 | output: [ 22 | { 23 | file: 'dist/background.js', 24 | format: 'es', 25 | sourcemap: true, 26 | }, 27 | ], 28 | plugins, 29 | }, 30 | { 31 | input: 'src/contentScripts.ts', 32 | output: [ 33 | { 34 | file: 'dist/content_scripts.js', 35 | format: 'es', 36 | sourcemap: true, 37 | }, 38 | ], 39 | plugins, 40 | }, 41 | { 42 | input: 'src/popup.ts', 43 | output: [ 44 | { 45 | file: 'dist/popup.js', 46 | format: 'es', 47 | sourcemap: true, 48 | }, 49 | ], 50 | plugins, 51 | }, 52 | ]; 53 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from './config'; 2 | import { Constants } from './constants'; 3 | 4 | let config = { ...Constants.defaultConfig }; 5 | 6 | chrome.storage.sync.get((storage: Partial) => { 7 | if (storage.extensionEnabled !== undefined) config.extensionEnabled = storage.extensionEnabled; 8 | if (storage.allowedUrls !== undefined) config.allowedUrls = storage.allowedUrls; 9 | if (storage.deniedUrls !== undefined) config.deniedUrls = storage.deniedUrls; 10 | if (storage.pumlServerUrl !== undefined) config.pumlServerUrl = storage.pumlServerUrl; 11 | setIcon(); 12 | }); 13 | 14 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 15 | switch (request.command) { 16 | case Constants.commands.getConfig: { 17 | sendResponse(config); 18 | break; 19 | } 20 | case Constants.commands.getExtensionEnabled: { 21 | sendResponse(config.extensionEnabled); 22 | break; 23 | } 24 | case Constants.commands.getAllowedUrls: { 25 | sendResponse(config.allowedUrls); 26 | break; 27 | } 28 | case Constants.commands.getDeniedUrls: { 29 | sendResponse(config.deniedUrls); 30 | break; 31 | } 32 | case Constants.commands.getPumlServerUrl: { 33 | sendResponse(config.pumlServerUrl); 34 | break; 35 | } 36 | case Constants.commands.setConfig: { 37 | config = request.config; 38 | sendResponse(config); 39 | chrome.storage.sync.set(config); 40 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 41 | if (tabs[0].id) chrome.tabs.reload(tabs[0].id); 42 | }); 43 | break; 44 | } 45 | case Constants.commands.toggleExtensionEnabled: { 46 | config.extensionEnabled = !config.extensionEnabled; 47 | sendResponse(config.extensionEnabled); 48 | void chrome.storage.sync.set({ extensionEnabled: config.extensionEnabled }); 49 | setIcon(); 50 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 51 | if (tabs[0].id) void chrome.tabs.reload(tabs[0].id); 52 | }); 53 | break; 54 | } 55 | case Constants.commands.setAllowedUrls: { 56 | config.allowedUrls = request.allowedUrls; 57 | sendResponse(config.allowedUrls); 58 | void chrome.storage.sync.set({ allowedUrls: config.allowedUrls }); 59 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 60 | if (tabs[0].id) void chrome.tabs.reload(tabs[0].id); 61 | }); 62 | break; 63 | } 64 | case Constants.commands.setDeniedUrls: { 65 | config.deniedUrls = request.deniedUrls; 66 | sendResponse(config.deniedUrls); 67 | void chrome.storage.sync.set({ deniedUrls: config.deniedUrls }); 68 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 69 | if (tabs[0].id) void chrome.tabs.reload(tabs[0].id); 70 | }); 71 | break; 72 | } 73 | case Constants.commands.setPumlServerUrl: { 74 | config.pumlServerUrl = request.pumlServerUrl; 75 | sendResponse(config.pumlServerUrl); 76 | void chrome.storage.sync.set({ pumlServerUrl: config.pumlServerUrl }); 77 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 78 | if (tabs[0].id) void chrome.tabs.reload(tabs[0].id); 79 | }); 80 | break; 81 | } 82 | } 83 | }); 84 | 85 | function setIcon(): void { 86 | chrome.browserAction.setIcon({ path: config.extensionEnabled ? 'icon/icon16.png' : 'icon/icon16gray.png' }, () => { 87 | // do nothing 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /src/components/BodyContainer.svelte: -------------------------------------------------------------------------------- 1 | 2 |4 | 5 | 10 | -------------------------------------------------------------------------------- /src/components/Button.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 29 | -------------------------------------------------------------------------------- /src/components/Footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /src/components/Header.svelte: -------------------------------------------------------------------------------- 1 |3 | PlantUML Visualizer2 | 3 | 13 | -------------------------------------------------------------------------------- /src/components/ItemContainer.svelte: -------------------------------------------------------------------------------- 1 |2 |4 | 5 | 17 | -------------------------------------------------------------------------------- /src/components/ItemLabel.svelte: -------------------------------------------------------------------------------- 1 |3 | 2 |4 | 5 | 11 | -------------------------------------------------------------------------------- /src/components/Switch.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 63 | -------------------------------------------------------------------------------- /src/components/TextField.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if multiline} 9 | 10 | {:else} 11 | 12 | {/if} 13 | 14 | 45 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | extensionEnabled: boolean; 3 | pumlServerUrl: string; 4 | allowedUrls: string[]; 5 | deniedUrls: string[]; 6 | } 7 | 8 | export function urlToRegExp(deniedUrl: string): RegExp { 9 | return new RegExp( 10 | '^' + 11 | deniedUrl 12 | .split('*') 13 | .map((str) => str.replaceAll(/([!$()*+./:=?[\\\]^{|}])/g, '\\$1')) 14 | .join('.*') + 15 | '$' 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from './config'; 2 | 3 | export const Constants = { 4 | defaultConfig: { 5 | extensionEnabled: true, 6 | pumlServerUrl: 'https://plantuml-service-willbooster.fly.dev', 7 | allowedUrls: [ 8 | 'https://github.com/*', 9 | 'https://raw.githubusercontent.com/*', 10 | 'https://gitlab.com/*', 11 | 'https://gist.github.com/*', 12 | 'file:///*/*', 13 | ], 14 | deniedUrls: ['https://github.com/*/edit/*'], 15 | } as Config, 16 | versionUmlText: ['@startuml', 'version', '@enduml'].join('\n'), 17 | ignoreAttribute: 'data-wb-ignore', 18 | imageTestIdAttribute: 'puml-vis-wb-img', 19 | textTestIdAttribute: 'puml-vis-wb-txt', 20 | commands: { 21 | getConfig: 'getConfig', 22 | getExtensionEnabled: 'getExtensionEnabled', 23 | getAllowedUrls: 'getAllowedUrls', 24 | getDeniedUrls: 'getDeniedUrls', 25 | getPumlServerUrl: 'getPumlServerUrl', 26 | setConfig: 'setConfig', 27 | toggleExtensionEnabled: 'toggleExtensionEnabled', 28 | setAllowedUrls: 'setAllowedUrls', 29 | setDeniedUrls: 'setDeniedUrls', 30 | setPumlServerUrl: 'setPumlServerUrl', 31 | }, 32 | } as const; 33 | -------------------------------------------------------------------------------- /src/contentScripts.ts: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | import type { Config } from './config'; 4 | import { urlToRegExp } from './config'; 5 | import { Constants } from './constants'; 6 | import { CodeBlockFinder } from './finder/codeBlockFinder'; 7 | import type { CodeFinder, DiffFinder } from './finder/finder'; 8 | import { GitHubFileViewFinder } from './finder/github/gitHubFileViewFinder'; 9 | import { GitHubPullRequestDiffFinder } from './finder/github/gitHubPullRequestDiffFinder'; 10 | import { descriptionMutator } from './mutator/descriptionMutator'; 11 | import { diffMutator } from './mutator/diffMutator'; 12 | 13 | const sleep = (msec: number): Promise3 | => new Promise((resolve) => setTimeout(resolve, msec)); 14 | 15 | const allCodeFinders = [new CodeBlockFinder(), new GitHubFileViewFinder()] as const; 16 | const allDiffFinders = [new GitHubPullRequestDiffFinder()] as const; 17 | let enabledFinders: CodeFinder[]; 18 | let enabledDiffFinders: DiffFinder[]; 19 | let lastUrl: string; 20 | let embedding = false; 21 | 22 | main(); 23 | 24 | function main(): void { 25 | chrome.runtime.sendMessage({ command: Constants.commands.getConfig }, (config: Config) => { 26 | if ( 27 | config.extensionEnabled && 28 | config.allowedUrls.some((url) => urlToRegExp(url).test(location.href)) && 29 | !config.deniedUrls.some((url) => urlToRegExp(url).test(location.href)) 30 | ) { 31 | apply(); 32 | } 33 | }); 34 | } 35 | 36 | function apply(): void { 37 | embedPlantUmlImages().finally(); 38 | 39 | const observer = new MutationObserver(async (mutations) => { 40 | const addedSomeNodes = mutations.some((mutation) => mutation.addedNodes.length > 0); 41 | if (addedSomeNodes) { 42 | await embedPlantUmlImages(); 43 | embedding = false; 44 | } 45 | }); 46 | observer.observe(document.body, { childList: true, subtree: true }); 47 | } 48 | 49 | async function embedPlantUmlImages(): Promise { 50 | if (lastUrl === location.href && embedding) return []; 51 | 52 | embedding = true; 53 | if (lastUrl === location.href) { 54 | // Deal with re-rendering multiple times (e.g. it occurs when updating a GitHub issue) 55 | await sleep(1000); 56 | } else { 57 | lastUrl = location.href; 58 | enabledFinders = allCodeFinders.filter((f) => f.canFind(location.href)); 59 | enabledDiffFinders = allDiffFinders.filter((f) => f.canFind(location.href)); 60 | } 61 | return Promise.all([ 62 | descriptionMutator.embedPlantUmlImages(enabledFinders, location.href, $(document.body)), 63 | diffMutator.embedPlantUmlImages(enabledDiffFinders, location.href, $(document.body)), 64 | ]); 65 | } 66 | -------------------------------------------------------------------------------- /src/directiveRegexes.ts: -------------------------------------------------------------------------------- 1 | export const INCLUDE_REGEX = /^\s*!include(?:url)?\s+(.*\.(plantuml|pu|puml|wsd))\s*$/i; 2 | export const INCLUDESUB_REGEX = /^\s*!includesub\s+(.*\.(plantuml|pu|puml|wsd))!(.*)\s*$/i; 3 | 4 | export const STARTSUB_REGEX = /^\s*!startsub\s+(.*)\s*$/i; 5 | export const ENDSUB_REGEX = /^\s*!endsub\s*$/i; 6 | -------------------------------------------------------------------------------- /src/encoder/plantUmlEncoder.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/prefer-code-point */ 2 | // fromCharCode is faster than codePointAt and enough for our use case. 3 | 4 | import { deflateRaw } from 'pako'; 5 | 6 | import { Constants } from '../constants'; 7 | 8 | let pumlServerUrl = Constants.defaultConfig.pumlServerUrl; 9 | 10 | chrome.runtime.sendMessage({ command: Constants.commands.getPumlServerUrl }, (url) => { 11 | pumlServerUrl = url; 12 | }); 13 | 14 | export const PlantUmlEncoder = { 15 | getImageUrl(pumlContent: string, serverUrl: string = pumlServerUrl): string { 16 | const textEncoder = new TextEncoder(); 17 | const encoded = encode64(deflateRaw(textEncoder.encode(pumlContent))); 18 | return `${serverUrl}/svg/${encoded}`; 19 | }, 20 | }; 21 | 22 | function encode64(array: Uint8Array): string { 23 | const length = array.length - 3; 24 | let r = ''; 25 | let i = 0; 26 | for (; i < length; i += 3) { 27 | r += append3bytes(array[i], array[i + 1], array[i + 2]); 28 | } 29 | if (length > -3) { 30 | r += append3bytes(array[i], array[i + 1] || 0, array[i + 2] || 0); 31 | } 32 | return r; 33 | } 34 | 35 | function append3bytes(code1: number, code2: number, code3: number): string { 36 | return ( 37 | encode6bit(code1 >> 2) + 38 | encode6bit((code1 << 4) | (code2 >> 4)) + 39 | encode6bit((code2 << 2) | (code3 >> 6)) + 40 | encode6bit(code3) 41 | ); 42 | } 43 | 44 | function encode6bit(code64: number): string { 45 | code64 &= 0x3f; 46 | if (code64 < 10) return String.fromCharCode(48 + code64); 47 | code64 -= 10; 48 | if (code64 < 26) return String.fromCharCode(65 + code64); 49 | code64 -= 26; 50 | if (code64 < 26) return String.fromCharCode(97 + code64); 51 | code64 -= 26; 52 | if (code64 === 0) return '-'; 53 | if (code64 === 1) return '_'; 54 | return '?'; 55 | } 56 | -------------------------------------------------------------------------------- /src/finder/codeBlockFinder.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from '../constants'; 2 | import { INCLUDE_REGEX, INCLUDESUB_REGEX } from '../directiveRegexes'; 3 | 4 | import type { CodeFinder, UmlCodeContent } from './finder'; 5 | import { extractSubIncludedText } from './finderUtil'; 6 | 7 | export class CodeBlockFinder implements CodeFinder { 8 | private readonly URL_REGEX = /^(file|https?):\/\/.+$/; 9 | 10 | canFind(webPageUrl: string): boolean { 11 | return this.URL_REGEX.test(webPageUrl); 12 | } 13 | 14 | async findContents(webPageUrl: string, $root: JQuery ): Promise { 15 | const $texts = $root.find(`pre:not([${Constants.ignoreAttribute}])`); 16 | const result = []; 17 | for (let i = 0; i < $texts.length; i++) { 18 | const $text = $texts.eq(i); 19 | let content = $text.text().trim(); 20 | if (!content.startsWith('@startuml') || !content.endsWith('@enduml')) continue; 21 | content = await this.preprocessIncludeDirective(webPageUrl, content); 22 | content = await this.preprocessIncludesubDirective(webPageUrl, content); 23 | result.push({ $text, text: content }); 24 | } 25 | return result; 26 | } 27 | 28 | private async preprocessIncludeDirective(webPageUrl: string, content: string): Promise { 29 | const contentLines = content.split('\n'); 30 | const dirUrl = webPageUrl.replace(/\/[^/]*\.(plantuml|pu|puml|wsd)(\?.*)?$/, ''); 31 | 32 | const preprocessedLines = []; 33 | for (const line of contentLines) { 34 | const match = INCLUDE_REGEX.exec(line); 35 | if (!match) { 36 | preprocessedLines.push(line); 37 | continue; 38 | } 39 | 40 | const includedFileUrl = `${dirUrl}/${match[1]}`; 41 | const response = await fetch(includedFileUrl); 42 | if (!response.ok) { 43 | continue; 44 | } 45 | let text = await response.text(); 46 | text = await this.preprocessIncludeDirective(includedFileUrl, text); 47 | text = await this.preprocessIncludesubDirective(includedFileUrl, text); 48 | const includedText = text.replaceAll('@startuml', '').replaceAll('@enduml', ''); 49 | preprocessedLines.push(includedText); 50 | } 51 | 52 | return preprocessedLines.join('\n'); 53 | } 54 | 55 | private async preprocessIncludesubDirective(webPageUrl: string, content: string): Promise { 56 | const contentLines = content.split('\n'); 57 | const dirUrl = webPageUrl.replace(/\/[^/]*\.(plantuml|pu|puml|wsd)(\?.*)?$/, ''); 58 | 59 | const preprocessedLines = []; 60 | for (const line of contentLines) { 61 | const match = INCLUDESUB_REGEX.exec(line); 62 | if (!match) { 63 | preprocessedLines.push(line); 64 | continue; 65 | } 66 | 67 | const includedFileUrl = `${dirUrl}/${match[1]}`; 68 | const response = await fetch(includedFileUrl); 69 | if (!response.ok) { 70 | continue; 71 | } 72 | let text = await response.text(); 73 | text = await this.preprocessIncludeDirective(includedFileUrl, text); 74 | text = await this.preprocessIncludesubDirective(includedFileUrl, text); 75 | const includedText = extractSubIncludedText(text, match[3]); 76 | preprocessedLines.push(includedText); 77 | } 78 | 79 | return preprocessedLines.join('\n'); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/finder/finder.ts: -------------------------------------------------------------------------------- 1 | export interface UmlCodeContent { 2 | $text: JQuery ; 3 | text: string; 4 | } 5 | 6 | export interface CodeFinder { 7 | canFind(webPageUrl: string): boolean; 8 | findContents(webPageUrl: string, $root: JQuery ): Promise ; 9 | } 10 | 11 | export interface UmlDiffContent { 12 | $diff: JQuery ; 13 | baseBranchName: string; 14 | headBranchName: string; 15 | baseTexts: string[]; 16 | headTexts: string[]; 17 | } 18 | 19 | export interface DiffFinder { 20 | canFind(webPageUrl: string): boolean; 21 | findContents(webPageUrl: string, $root: JQuery ): Promise ; 22 | } 23 | -------------------------------------------------------------------------------- /src/finder/finderUtil.ts: -------------------------------------------------------------------------------- 1 | import { ENDSUB_REGEX, STARTSUB_REGEX } from '../directiveRegexes'; 2 | 3 | export function extractSubIncludedText(content: string, tag: string): string { 4 | const contentLines = content.split('\n'); 5 | const subTextLines = []; 6 | let subDepth = 0; 7 | for (const line of contentLines) { 8 | if (subDepth === 0) { 9 | const match = STARTSUB_REGEX.exec(line); 10 | if (match && match[1] === tag) subDepth++; 11 | continue; 12 | } 13 | const startMatch = STARTSUB_REGEX.exec(line); 14 | const endMatch = ENDSUB_REGEX.exec(line); 15 | if (startMatch) { 16 | subDepth++; 17 | subTextLines.push(line); 18 | } else if (endMatch) { 19 | subDepth--; 20 | if (subDepth > 0) subTextLines.push(line); 21 | } else { 22 | subTextLines.push(line); 23 | } 24 | } 25 | 26 | return subTextLines.join('\n'); 27 | } 28 | -------------------------------------------------------------------------------- /src/finder/github/gitHubFileViewFinder.ts: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | import { Constants } from '../../constants'; 4 | import { INCLUDE_REGEX, INCLUDESUB_REGEX } from '../../directiveRegexes'; 5 | import type { CodeFinder, UmlCodeContent } from '../finder'; 6 | import { extractSubIncludedText } from '../finderUtil'; 7 | 8 | export class GitHubFileViewFinder implements CodeFinder { 9 | private readonly URL_REGEX = /^https:\/\/github\.com\/.*\/.*\.(plantuml|pu|puml|wsd)(\?.*)?$/; 10 | 11 | canFind(webPageUrl: string): boolean { 12 | return this.URL_REGEX.test(webPageUrl); 13 | } 14 | 15 | async findContents(webPageUrl: string, $root: JQuery ): Promise { 16 | const $texts = $root.find(`div[itemprop='text']:not([${Constants.ignoreAttribute}])`); 17 | const result = []; 18 | for (let i = 0; i < $texts.length; i++) { 19 | const $text = $texts.eq(i); 20 | const $fileLines = $text.find('tr'); 21 | let fileText = [...Array.from({ length: $fileLines.length }).keys()] 22 | .map((lineno) => $fileLines.eq(lineno).find("[id^='LC']").text() + '\n') 23 | .join(''); 24 | fileText = await this.preprocessIncludeDirective(webPageUrl, fileText); 25 | fileText = await this.preprocessIncludeSubDirective(webPageUrl, fileText); 26 | result.push({ $text, text: fileText }); 27 | } 28 | return result; 29 | } 30 | 31 | private async preprocessIncludeDirective(webPageUrl: string, fileText: string): Promise { 32 | const fileTextLines = fileText.split('\n'); 33 | const dirUrl = webPageUrl.replace(/\/[^/]*\.(plantuml|pu|puml|wsd)(\?.*)?$/, ''); 34 | 35 | const preprocessedLines = []; 36 | for (const line of fileTextLines) { 37 | const match = INCLUDE_REGEX.exec(line); 38 | if (!match) { 39 | preprocessedLines.push(line); 40 | continue; 41 | } 42 | 43 | const includedFileUrl = `${dirUrl}/${match[1]}`; 44 | const response = await fetch(includedFileUrl); 45 | if (!response.ok) { 46 | preprocessedLines.push(line); 47 | continue; 48 | } 49 | const htmlString = await response.text(); 50 | const $body = $(new DOMParser().parseFromString(htmlString, 'text/html')).find('body'); 51 | const fileTexts = await this.findContents(includedFileUrl, $body); 52 | const includedText = fileTexts 53 | .map((fileText) => fileText.text.replaceAll('@startuml', '').replaceAll('@enduml', '')) 54 | .join('\n'); 55 | preprocessedLines.push(includedText); 56 | } 57 | 58 | return preprocessedLines.join('\n'); 59 | } 60 | 61 | private async preprocessIncludeSubDirective(webPageUrl: string, content: string): Promise { 62 | const contentLines = content.split('\n'); 63 | const dirUrl = webPageUrl.replace(/\/[^/]*\.(plantuml|pu|puml|wsd)(\?.*)?$/, ''); 64 | 65 | const preprocessedLines = []; 66 | for (const line of contentLines) { 67 | const match = INCLUDESUB_REGEX.exec(line); 68 | if (!match) { 69 | preprocessedLines.push(line); 70 | continue; 71 | } 72 | 73 | const includedFileUrl = `${dirUrl}/${match[1]}`; 74 | const response = await fetch(includedFileUrl); 75 | if (!response.ok) { 76 | preprocessedLines.push(line); 77 | continue; 78 | } 79 | const htmlString = await response.text(); 80 | const $body = $(new DOMParser().parseFromString(htmlString, 'text/html')).find('body'); 81 | const fileTexts = await this.findContents(includedFileUrl, $body); 82 | const includedText = fileTexts.map((fileText) => extractSubIncludedText(fileText.text, match[3])).join('\n'); 83 | preprocessedLines.push(includedText); 84 | } 85 | 86 | return preprocessedLines.join('\n'); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/finder/github/gitHubPullRequestDiffFinder.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/no-array-method-this-argument */ 2 | 3 | import $ from 'jquery'; 4 | 5 | import { Constants } from '../../constants'; 6 | import type { DiffFinder, UmlDiffContent } from '../finder'; 7 | 8 | import { GitHubFileViewFinder } from './gitHubFileViewFinder'; 9 | 10 | export class GitHubPullRequestDiffFinder implements DiffFinder { 11 | private static getBaseHeadFilePaths($diff: JQuery ): [string, string] { 12 | const title = $diff.find('div.file-info a').attr('title'); 13 | if (!title) return ['', '']; 14 | const separator = ' → '; 15 | const fragments = title.split(separator); 16 | const filePaths = []; 17 | let filePath = ''; 18 | for (const fragment of fragments) { 19 | filePath += fragment; 20 | if (/^.*\.(plantuml|pu|puml|wsd)$/.test(filePath)) { 21 | filePaths.push(filePath); 22 | filePath = ''; 23 | } else { 24 | filePath += separator; 25 | } 26 | } 27 | if (filePaths.length === 1) return [filePaths[0], filePaths[0]]; 28 | if (filePaths.length === 2) return [filePaths[0], filePaths[1]]; 29 | return ['', '']; 30 | } 31 | 32 | private static async getTexts(fileUrl: string): Promise { 33 | const response = await fetch(fileUrl); 34 | if (!response.ok) return []; 35 | const htmlString = await response.text(); 36 | const $body = $(new DOMParser().parseFromString(htmlString, 'text/html')).find('body'); 37 | const fileBlockFinder = new GitHubFileViewFinder(); 38 | const contents = await fileBlockFinder.findContents(fileUrl, $body); 39 | return contents.map((content) => content.text); 40 | } 41 | 42 | private readonly URL_REGEX = /^https:\/\/github\.com\/.*\/pull\/\d+\/files/; 43 | 44 | canFind(webPageUrl: string): boolean { 45 | return this.URL_REGEX.test(webPageUrl); 46 | } 47 | 48 | async findContents(webPageUrl: string, $root: JQuery ): Promise { 49 | const blobRoot = webPageUrl.replace(/pull\/\d+\/files.*/, 'blob'); 50 | const [baseBranchName, headBranchName] = this.getBaseHeadBranchNames($root); 51 | const diffs = this.getDiffs($root); 52 | const result = await Promise.all( 53 | diffs.map(($diff) => this.getDiffContent(blobRoot, baseBranchName, headBranchName, $diff)) 54 | ); 55 | return result.filter((content) => content.$diff.length > 0); 56 | } 57 | 58 | private getBaseHeadBranchNames($root: JQuery ): [string, string] { 59 | const $baseRef = $root.find(this.getBranchNameSelector('base')); 60 | const $headRef = $root.find(this.getBranchNameSelector('head')); 61 | return [$baseRef.text(), $headRef.text()]; 62 | } 63 | 64 | private getBranchNameSelector(baseOrHead: 'base' | 'head'): string { 65 | return `span.commit-ref.css-truncate.user-select-contain.expandable.${baseOrHead}-ref span.css-truncate-target`; 66 | } 67 | 68 | private getDiffs($root: JQuery ): JQuery [] { 69 | const $diffs = $root.find(`div[id^='diff-']:not([${Constants.ignoreAttribute}])`); 70 | return [...Array.from({ length: $diffs.length }).keys()].map((i) => $diffs.eq(i)); 71 | } 72 | 73 | private async getDiffContent( 74 | blobRoot: string, 75 | baseBranchName: string, 76 | headBranchName: string, 77 | $diff: JQuery 78 | ): Promise { 79 | const [baseFilePath, headFilePath] = GitHubPullRequestDiffFinder.getBaseHeadFilePaths($diff); 80 | const $diffBlock = $diff.find('div.js-file-content.Details-content--hidden'); 81 | if ( 82 | (!baseFilePath && !headFilePath) || 83 | $diffBlock.length === 0 || 84 | $diffBlock.find('div.data.highlight.empty').length > 0 85 | ) { 86 | return { $diff: $(), baseBranchName, headBranchName, baseTexts: [], headTexts: [] }; 87 | } 88 | const fileUrls = [ 89 | blobRoot + '/' + baseBranchName + '/' + baseFilePath, 90 | blobRoot + '/' + headBranchName + '/' + headFilePath, 91 | ]; 92 | const [baseTexts, headTexts] = await Promise.all( 93 | fileUrls.map((fileUrl) => GitHubPullRequestDiffFinder.getTexts(fileUrl)) 94 | ); 95 | return { $diff: $diffBlock, baseBranchName, headBranchName, baseTexts, headTexts }; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/mutator/descriptionMutator.ts: -------------------------------------------------------------------------------- 1 | import type { CodeFinder } from '../finder/finder'; 2 | 3 | import { markAsIgnore, setDblclickHandlers, textToImage } from './mutatorUtil'; 4 | 5 | class DescriptionMutator { 6 | async embedPlantUmlImages(finders: CodeFinder[], webPageUrl: string, $root: JQuery ): Promise { 7 | await Promise.all( 8 | finders.map(async (finder) => { 9 | const contents = await finder.findContents(webPageUrl, $root); 10 | for (const content of contents) { 11 | // Skip if no PlantUML descriptions exist 12 | if (content.text.length === 0) continue; 13 | 14 | const $text = content.$text; 15 | 16 | // To avoid embedding an image multiple times 17 | let $image: JQuery ; 18 | if (markAsIgnore($text)) { 19 | $image = await textToImage(content.text); 20 | markAsIgnore($image); 21 | $image.insertAfter($text); 22 | } else { 23 | $image = $text.next(); 24 | } 25 | 26 | setDblclickHandlers($text, $image); 27 | } 28 | }) 29 | ); 30 | } 31 | } 32 | 33 | export const descriptionMutator = new DescriptionMutator(); 34 | -------------------------------------------------------------------------------- /src/mutator/diffMutator.ts: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | import type { DiffFinder } from '../finder/finder'; 4 | 5 | import { markAsIgnore, setDblclickHandlers, textsToImages } from './mutatorUtil'; 6 | 7 | class DiffMutator { 8 | async embedPlantUmlImages(diffFinders: DiffFinder[], webPageUrl: string, $root: JQuery ): Promise { 9 | await Promise.all( 10 | diffFinders.map(async (diffFinder) => { 11 | const contents = await diffFinder.findContents(webPageUrl, $root); 12 | for (const content of contents) { 13 | // Skip if no PlantUML descriptions exist 14 | if (content.baseTexts.length === 0 && content.headTexts.length === 0) continue; 15 | 16 | const $diffText = content.$diff; 17 | 18 | // To avoid embedding an image multiple times 19 | let $idffImage: JQuery ; 20 | if (markAsIgnore($diffText)) { 21 | $idffImage = $(``); 22 | 23 | const $baseBranchMark = $(` ${content.baseBranchName}`); 24 | const $headBranchMark = $(`${content.headBranchName}`); 25 | 26 | $baseBranchMark 27 | .css('padding', '4px 10px') 28 | .css('background-color', '#ffdce0') 29 | .css('color', 'rgba(27,31,35,.7)') 30 | .css('font-size', '12px'); 31 | 32 | $headBranchMark 33 | .css('padding', '4px 10px') 34 | .css('background-color', '#cdffd8') 35 | .css('color', 'rgba(27,31,35,.7)') 36 | .css('font-size', '12px'); 37 | 38 | const [baseImages, headImages] = await Promise.all([ 39 | textsToImages(content.baseTexts, 'Nothing'), 40 | textsToImages(content.headTexts, 'Deleted'), 41 | ]); 42 | 43 | for (const $image of baseImages) { 44 | $image.css('background-color', '#ffeef0'); 45 | } 46 | for (const $image of headImages) { 47 | $image.css('background-color', '#e6ffed'); 48 | } 49 | 50 | $idffImage.append($baseBranchMark); 51 | baseImages[0].insertAfter($baseBranchMark); 52 | for (let i = 1; i < baseImages.length; i++) { 53 | baseImages[i].insertAfter(baseImages[i - 1]); 54 | } 55 | $headBranchMark.insertAfter(baseImages.at(-1) as JQuery); 56 | headImages[0].insertAfter($headBranchMark); 57 | for (let i = 1; i < headImages.length; i++) { 58 | headImages[i].insertAfter(headImages[i - 1]); 59 | } 60 | markAsIgnore($idffImage); 61 | $idffImage.insertAfter($diffText); 62 | } else { 63 | $idffImage = $diffText.next(); 64 | } 65 | 66 | setDblclickHandlers($diffText, $idffImage); 67 | } 68 | }) 69 | ); 70 | } 71 | } 72 | 73 | export const diffMutator = new DiffMutator(); 74 | -------------------------------------------------------------------------------- /src/mutator/mutatorUtil.ts: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | import { Constants } from '../constants'; 4 | import { PlantUmlEncoder } from '../encoder/plantUmlEncoder'; 5 | 6 | export function markAsIgnore($content: JQuery ): boolean { 7 | if ($content.attr(Constants.ignoreAttribute) !== undefined) { 8 | return false; 9 | } 10 | $content.attr(Constants.ignoreAttribute, ''); 11 | return true; 12 | } 13 | 14 | export async function textToImage(text: string): Promise > { 15 | const $div = $(' ').css('overflow', 'auto').css('padding', '4px 10px'); 16 | const res = await fetch(PlantUmlEncoder.getImageUrl(text)); 17 | const encoded = `data:image/svg+xml,${encodeURIComponent(await res.text())}`; 18 | return $div.append($('').attr('src', encoded)); 19 | } 20 | 21 | export async function textsToImages(texts: string[], noContentsMessage: string): Promise
[]> { 22 | if (texts.length === 0) { 23 | return [$(` ${noContentsMessage}`)]; 24 | } 25 | const ret = []; 26 | for (const text of texts) { 27 | ret.push(await textToImage(text)); 28 | } 29 | return ret; 30 | } 31 | 32 | export function setDblclickHandlers($text: JQuery, $image: JQuery ): void { 33 | $text.attr('data-testid', Constants.textTestIdAttribute); 34 | $image.attr('data-testid', Constants.imageTestIdAttribute); 35 | 36 | $text.off('dblclick'); 37 | $text.on('dblclick', () => { 38 | $text.hide(); 39 | $image.show(); 40 | }); 41 | $image.off('dblclick'); 42 | $image.on('dblclick', () => { 43 | $image.hide(); 44 | $text.show(); 45 | }); 46 | $text.trigger('dblclick'); 47 | } 48 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/popup.svelte: -------------------------------------------------------------------------------- 1 | 117 | 118 | 119 | 120 | 121 | 124 | 125 |Visualize PlantUML code 122 |123 | 126 | 137 | 138 |Server URL 127 |{ 131 | if (event.key === 'Enter') handleChangeServerUrl(); 132 | }} 133 | disabled={loading} 134 | placeholder="https://* or http://*" 135 | /> 136 | 139 | 151 | 152 |140 | {#if loading} 141 |150 |Loading...142 | {:else if inputServerUrlErrorMessage} 143 |{inputServerUrlErrorMessage}144 | {:else if !versionPumlSrc} 145 | No server 146 | {:else} 147 |148 | {/if} 149 |
153 | 157 | 158 |Allowed URLs 154 |155 | 156 | 159 | 163 | 164 | 165 | 166 | 229 | -------------------------------------------------------------------------------- /src/popup.ts: -------------------------------------------------------------------------------- 1 | import Popup from './popup.svelte'; 2 | 3 | new Popup({ 4 | target: document.body, 5 | }); 6 | -------------------------------------------------------------------------------- /src/types/svelte.d.ts: -------------------------------------------------------------------------------- 1 | ///Denied URLs 160 |161 | 162 | 2 | -------------------------------------------------------------------------------- /tests/playwright.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from './playwrightFixtures'; 2 | 3 | const imageTestId = 'puml-vis-wb-img'; 4 | const textTestId = 'puml-vis-wb-txt'; 5 | 6 | test("code block in GitHub's README", async ({ page }) => { 7 | await page.goto('https://github.com/WillBooster/plantuml-visualizer/blob/master/puml-sample/README.md'); 8 | 9 | await expect(page.getByTestId(imageTestId)).toHaveCount(1); 10 | await expect(page.getByTestId(textTestId)).toHaveCount(1); 11 | 12 | const image = page.getByTestId(imageTestId).nth(0); 13 | const text = page.getByTestId(textTestId).nth(0); 14 | 15 | await expect(image).toBeVisible(); 16 | await expect(text).not.toBeVisible(); 17 | 18 | await image.dblclick(); 19 | 20 | await expect(image).not.toBeVisible(); 21 | await expect(text).toBeVisible(); 22 | 23 | await text.dblclick(); 24 | 25 | await expect(image).toBeVisible(); 26 | await expect(text).not.toBeVisible(); 27 | }); 28 | 29 | test("code block in GitHub's issue", async ({ page }) => { 30 | await page.goto('https://github.com/WillBooster/plantuml-visualizer/issues/54'); 31 | 32 | await expect(page.getByTestId(imageTestId)).toHaveCount(2); 33 | await expect(page.getByTestId(textTestId)).toHaveCount(2); 34 | 35 | const firstImage = page.getByTestId(imageTestId).nth(0); 36 | const firstText = page.getByTestId(textTestId).nth(0); 37 | 38 | await expect(firstImage).toBeVisible(); 39 | await expect(firstText).not.toBeVisible(); 40 | 41 | await firstImage.dblclick(); 42 | 43 | await expect(firstImage).not.toBeVisible(); 44 | await expect(firstText).toBeVisible(); 45 | 46 | await firstText.dblclick(); 47 | 48 | await expect(firstImage).toBeVisible(); 49 | await expect(firstText).not.toBeVisible(); 50 | 51 | const secondImage = page.getByTestId(imageTestId).nth(1); 52 | const secondText = page.getByTestId(textTestId).nth(1); 53 | 54 | await expect(secondImage).toBeVisible(); 55 | await expect(secondText).not.toBeVisible(); 56 | 57 | await secondImage.dblclick(); 58 | 59 | await expect(secondImage).not.toBeVisible(); 60 | await expect(secondText).toBeVisible(); 61 | 62 | await secondText.dblclick(); 63 | 64 | await expect(secondImage).toBeVisible(); 65 | await expect(secondText).not.toBeVisible(); 66 | }); 67 | 68 | test('file block without preprocessing directive in GitHub', async ({ page }) => { 69 | await page.goto('https://github.com/WillBooster/plantuml-visualizer/blob/master/puml-sample/class.pu'); 70 | 71 | await expect(page.getByTestId(imageTestId)).toHaveCount(1); 72 | await expect(page.getByTestId(textTestId)).toHaveCount(1); 73 | 74 | const image = page.getByTestId(imageTestId).nth(0); 75 | const text = page.getByTestId(textTestId).nth(0); 76 | 77 | await expect(image).toBeVisible(); 78 | await expect(text).not.toBeVisible(); 79 | 80 | await image.dblclick(); 81 | 82 | await expect(image).not.toBeVisible(); 83 | await expect(text).toBeVisible(); 84 | 85 | await text.dblclick(); 86 | 87 | await expect(image).toBeVisible(); 88 | await expect(text).not.toBeVisible(); 89 | }); 90 | 91 | test('file block with !include directive in GitHub', async ({ page }) => { 92 | await page.goto('https://github.com/WillBooster/plantuml-visualizer/blob/master/puml-sample/including.pu'); 93 | 94 | await expect(page.getByTestId(imageTestId)).toHaveCount(1); 95 | await expect(page.getByTestId(textTestId)).toHaveCount(1); 96 | 97 | const image = page.getByTestId(imageTestId).nth(0); 98 | const text = page.getByTestId(textTestId).nth(0); 99 | 100 | await expect(image).toBeVisible(); 101 | await expect(text).not.toBeVisible(); 102 | 103 | await image.dblclick(); 104 | 105 | await expect(image).not.toBeVisible(); 106 | await expect(text).toBeVisible(); 107 | 108 | await text.dblclick(); 109 | 110 | await expect(image).toBeVisible(); 111 | await expect(text).not.toBeVisible(); 112 | }); 113 | 114 | test('file block with !includesub directive in GitHub', async ({ page }) => { 115 | await page.goto('https://github.com/WillBooster/plantuml-visualizer/blob/master/puml-sample/subincluding.pu'); 116 | 117 | await expect(page.getByTestId(imageTestId)).toHaveCount(1); 118 | await expect(page.getByTestId(textTestId)).toHaveCount(1); 119 | 120 | const image = page.getByTestId(imageTestId).nth(0); 121 | const text = page.getByTestId(textTestId).nth(0); 122 | 123 | await expect(image).toBeVisible(); 124 | await expect(text).not.toBeVisible(); 125 | 126 | await image.dblclick(); 127 | 128 | await expect(image).not.toBeVisible(); 129 | await expect(text).toBeVisible(); 130 | 131 | await text.dblclick(); 132 | 133 | await expect(image).toBeVisible(); 134 | await expect(text).not.toBeVisible(); 135 | }); 136 | 137 | test('pull request adding a PlantUML file', async ({ page }) => { 138 | await page.goto('https://github.com/WillBooster/plantuml-visualizer/pull/49/files'); 139 | 140 | const fileDiff = page.locator('[data-details-container-group="file"]'); 141 | 142 | await expect(fileDiff.getByText('Nothing', { exact: true })).toHaveCount(1); 143 | await expect(fileDiff.getByText('Deleted', { exact: true })).toHaveCount(0); 144 | 145 | await expect(fileDiff.getByTestId(imageTestId)).toHaveCount(1); 146 | await expect(fileDiff.getByTestId(textTestId)).toHaveCount(1); 147 | 148 | const image = fileDiff.getByTestId(imageTestId).nth(0); 149 | const text = fileDiff.getByTestId(textTestId).nth(0); 150 | 151 | await expect(image).toBeVisible(); 152 | await expect(text).not.toBeVisible(); 153 | 154 | await image.dblclick(); 155 | 156 | await expect(image).not.toBeVisible(); 157 | await expect(text).toBeVisible(); 158 | 159 | await text.dblclick(); 160 | 161 | await expect(image).toBeVisible(); 162 | await expect(text).not.toBeVisible(); 163 | }); 164 | 165 | test('pull request deleting a PlantUML file', async ({ page }) => { 166 | await page.goto('https://github.com/WillBooster/plantuml-visualizer/pull/50/files'); 167 | 168 | const fileDiff = page.locator('[data-details-container-group="file"]'); 169 | 170 | await expect(fileDiff.getByText('Nothing', { exact: true })).toHaveCount(0); 171 | await expect(fileDiff.getByText('Deleted', { exact: true })).toHaveCount(1); 172 | 173 | await expect(fileDiff.getByTestId(imageTestId)).toHaveCount(1); 174 | await expect(fileDiff.getByTestId(textTestId)).toHaveCount(1); 175 | 176 | const image = fileDiff.getByTestId(imageTestId).nth(0); 177 | const text = fileDiff.getByTestId(textTestId).nth(0); 178 | 179 | await expect(image).toBeVisible(); 180 | await expect(text).not.toBeVisible(); 181 | 182 | await image.dblclick(); 183 | 184 | await expect(image).not.toBeVisible(); 185 | await expect(text).toBeVisible(); 186 | 187 | await text.dblclick(); 188 | 189 | await expect(image).toBeVisible(); 190 | await expect(text).not.toBeVisible(); 191 | }); 192 | 193 | test('pull request changing a PlantUML file', async ({ page }) => { 194 | await page.goto('https://github.com/WillBooster/plantuml-visualizer/pull/24/files'); 195 | 196 | const fileDiff = page.locator('[data-details-container-group="file"]'); 197 | 198 | await expect(fileDiff.getByText('Nothing', { exact: true })).toHaveCount(0); 199 | await expect(fileDiff.getByText('Deleted', { exact: true })).toHaveCount(0); 200 | 201 | await expect(fileDiff.getByTestId(imageTestId)).toHaveCount(1); 202 | await expect(fileDiff.getByTestId(textTestId)).toHaveCount(1); 203 | 204 | const image = fileDiff.getByTestId(imageTestId).nth(0); 205 | const text = fileDiff.getByTestId(textTestId).nth(0); 206 | 207 | await expect(image).toBeVisible(); 208 | await expect(text).not.toBeVisible(); 209 | 210 | await image.dblclick(); 211 | 212 | await expect(image).not.toBeVisible(); 213 | await expect(text).toBeVisible(); 214 | 215 | await text.dblclick(); 216 | 217 | await expect(image).toBeVisible(); 218 | await expect(text).not.toBeVisible(); 219 | }); 220 | 221 | test('pull request renaming a PlantUML file with a little changes', async ({ page }) => { 222 | await page.goto('https://github.com/WillBooster/plantuml-visualizer/pull/106/files'); 223 | 224 | const fileDiff = page.locator('[data-details-container-group="file"]'); 225 | 226 | await expect(fileDiff.getByText('Nothing', { exact: true })).toHaveCount(0); 227 | await expect(fileDiff.getByText('Deleted', { exact: true })).toHaveCount(0); 228 | 229 | await expect(fileDiff.getByTestId(imageTestId)).toHaveCount(1); 230 | await expect(fileDiff.getByTestId(textTestId)).toHaveCount(1); 231 | 232 | const image = fileDiff.getByTestId(imageTestId).nth(0); 233 | const text = fileDiff.getByTestId(textTestId).nth(0); 234 | 235 | await expect(image).toBeVisible(); 236 | await expect(text).not.toBeVisible(); 237 | 238 | await image.dblclick(); 239 | 240 | await expect(image).not.toBeVisible(); 241 | await expect(text).toBeVisible(); 242 | 243 | await text.dblclick(); 244 | 245 | await expect(image).toBeVisible(); 246 | await expect(text).not.toBeVisible(); 247 | }); 248 | -------------------------------------------------------------------------------- /tests/playwrightFixtures.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import { test as base, chromium, type BrowserContext } from '@playwright/test'; 4 | 5 | // cf. https://playwright.dev/docs/chrome-extensions 6 | export const test = base.extend<{ context: BrowserContext; extensionId: string }>({ 7 | // eslint-disable-next-line no-empty-pattern 8 | context: async ({}, use) => { 9 | // eslint-disable-next-line unicorn/prefer-module 10 | const pathToExtension = path.join(__dirname, '..', 'dist'); 11 | const context = await chromium.launchPersistentContext('', { 12 | headless: false, 13 | args: [`--headless=new`, `--disable-extensions-except=${pathToExtension}`, `--load-extension=${pathToExtension}`], 14 | }); 15 | await use(context); 16 | await context.close(); 17 | }, 18 | extensionId: async ({ context }, use) => { 19 | let [background] = context.serviceWorkers(); 20 | if (!background) background = await context.waitForEvent('serviceworker'); 21 | const extensionId = background.url().split('/')[2]; 22 | await use(extensionId); 23 | }, 24 | }); 25 | 26 | export const expect = test.expect; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "importHelpers": false, 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "outDir": "dist", 11 | "resolveJsonModule": true, 12 | "skipLibCheck": true, 13 | "sourceMap": true, 14 | "strict": true, 15 | "target": "esnext", 16 | "typeRoots": ["./node_modules/@types", "./@types"] 17 | }, 18 | "include": ["scripts/**/*", "src/**/*", "tests/**/*"], 19 | "extends": "@tsconfig/svelte/tsconfig.json" 20 | } 21 | --------------------------------------------------------------------------------