diff --git a/.gitignore b/.gitignore index 177824c..8b654ef 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ node_modules/ out/ dist/ *.vsix -media/diff2html/ # IDE .vscode-test/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 6583ec2..964853e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Change Log +## [Unreleased] + +### Changed +- Refactored the webview so the diff renderer runs from standalone static assets (`media/patchViewer.js` and `media/patchViewer.css`) loaded via `webview.asWebviewUri(...)`, instead of embedding the ~350‑line client script and CSS inside a TypeScript template literal. The browser now receives the script verbatim, eliminating the class of escaping bugs (`\n`, ``, ...) that repeatedly broke the inline script. + +### Fixed +- Fixed a blank Visual view and an unresponsive Content tab caused by the inline client script failing to run when the surrounding template literal mangled its escape sequences. The diff now renders and the tabs respond regardless of the patch contents. +- The initial patch content is passed to the webview through a non-executable JSON data block (with every `<` escaped as `\u003c`), so patches that contain `` or HTML comments can no longer abort the viewer. +- Vendored the `diff2html` webview assets (`media/diff2html/`) into the repository so they are always present when the extension runs from source (Extension Development Host), during tests, and when packaged. Previously these files were git-ignored and only generated at publish time, which left the Visual view blank whenever the assets had not been copied. + +### Tests +- Replaced the inline-script validity test with checks that the shipped `media/patchViewer.js` parses as JavaScript and that the embedded initial content round-trips exactly (including `` payloads). +- Added an end-to-end test that loads the shipped `diff2html` bundle and `media/patchViewer.js` into a DOM and asserts that a patch actually renders into the Visual view and that the Content tab switches, guarding against the "blank Visual tab" class of regressions. + ## [1.0.2] - 2026-07-03 ### Fixed diff --git a/media/diff2html/css/diff2html.min.css b/media/diff2html/css/diff2html.min.css new file mode 100644 index 0000000..8d572e0 --- /dev/null +++ b/media/diff2html/css/diff2html.min.css @@ -0,0 +1 @@ +:host,:root{--d2h-bg-color:#fff;--d2h-border-color:#ddd;--d2h-dim-color:rgba(0,0,0,.3);--d2h-line-border-color:#eee;--d2h-file-header-bg-color:#f7f7f7;--d2h-file-header-border-color:#d8d8d8;--d2h-empty-placeholder-bg-color:#f1f1f1;--d2h-empty-placeholder-border-color:#e1e1e1;--d2h-selected-color:#c8e1ff;--d2h-ins-bg-color:#dfd;--d2h-ins-border-color:#b4e2b4;--d2h-ins-highlight-bg-color:#97f295;--d2h-ins-label-color:#399839;--d2h-del-bg-color:#fee8e9;--d2h-del-border-color:#e9aeae;--d2h-del-highlight-bg-color:#ffb6ba;--d2h-del-label-color:#c33;--d2h-change-del-color:#fdf2d0;--d2h-change-ins-color:#ded;--d2h-info-bg-color:#f8fafd;--d2h-info-border-color:#d5e4f2;--d2h-change-label-color:#d0b44c;--d2h-moved-label-color:#3572b0;--d2h-dark-color:#e6edf3;--d2h-dark-bg-color:#0d1117;--d2h-dark-border-color:#30363d;--d2h-dark-dim-color:#6e7681;--d2h-dark-line-border-color:#21262d;--d2h-dark-file-header-bg-color:#161b22;--d2h-dark-file-header-border-color:#30363d;--d2h-dark-empty-placeholder-bg-color:hsla(215,8%,47%,.1);--d2h-dark-empty-placeholder-border-color:#30363d;--d2h-dark-selected-color:rgba(56,139,253,.1);--d2h-dark-ins-bg-color:rgba(46,160,67,.15);--d2h-dark-ins-border-color:rgba(46,160,67,.4);--d2h-dark-ins-highlight-bg-color:rgba(46,160,67,.4);--d2h-dark-ins-label-color:#3fb950;--d2h-dark-del-bg-color:rgba(248,81,73,.1);--d2h-dark-del-border-color:rgba(248,81,73,.4);--d2h-dark-del-highlight-bg-color:rgba(248,81,73,.4);--d2h-dark-del-label-color:#f85149;--d2h-dark-change-del-color:rgba(210,153,34,.2);--d2h-dark-change-ins-color:rgba(46,160,67,.25);--d2h-dark-info-bg-color:rgba(56,139,253,.1);--d2h-dark-info-border-color:rgba(56,139,253,.4);--d2h-dark-change-label-color:#d29922;--d2h-dark-moved-label-color:#3572b0}.d2h-wrapper{text-align:left}.d2h-file-header{background-color:#f7f7f7;background-color:var(--d2h-file-header-bg-color);border-bottom:1px solid #d8d8d8;border-bottom:1px solid var(--d2h-file-header-border-color);display:-webkit-box;display:-ms-flexbox;display:flex;font-family:Source Sans Pro,Helvetica Neue,Helvetica,Arial,sans-serif;height:35px;padding:5px 10px}.d2h-file-header.d2h-sticky-header{position:sticky;top:0;z-index:1}.d2h-file-stats{display:-webkit-box;display:-ms-flexbox;display:flex;font-size:14px;margin-left:auto}.d2h-lines-added{border:1px solid #b4e2b4;border:1px solid var(--d2h-ins-border-color);border-radius:5px 0 0 5px;color:#399839;color:var(--d2h-ins-label-color);padding:2px;text-align:right;vertical-align:middle}.d2h-lines-deleted{border:1px solid #e9aeae;border:1px solid var(--d2h-del-border-color);border-radius:0 5px 5px 0;color:#c33;color:var(--d2h-del-label-color);margin-left:1px;padding:2px;text-align:left;vertical-align:middle}.d2h-file-name-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:15px;width:100%}.d2h-file-name{overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.d2h-file-wrapper{border:1px solid #ddd;border:1px solid var(--d2h-border-color);border-radius:3px;margin-bottom:1em}.d2h-file-collapse{-webkit-box-pack:end;-ms-flex-pack:end;cursor:pointer;display:none;font-size:12px;justify-content:flex-end;-webkit-box-align:center;-ms-flex-align:center;align-items:center;border:1px solid #ddd;border:1px solid var(--d2h-border-color);border-radius:3px;padding:4px 8px}.d2h-file-collapse.d2h-selected{background-color:#c8e1ff;background-color:var(--d2h-selected-color)}.d2h-file-collapse-input{margin:0 4px 0 0}.d2h-diff-table{border-collapse:collapse;font-family:Menlo,Consolas,monospace;font-size:13px;width:100%}.d2h-files-diff{display:-webkit-box;display:-ms-flexbox;display:flex;width:100%}.d2h-file-diff{overflow-y:hidden}.d2h-file-diff.d2h-d-none,.d2h-files-diff.d2h-d-none{display:none}.d2h-file-side-diff{display:inline-block;overflow-x:scroll;overflow-y:hidden;width:50%}.d2h-code-line{padding:0 8em;width:calc(100% - 16em)}.d2h-code-line,.d2h-code-side-line{display:inline-block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;white-space:nowrap}.d2h-code-side-line{padding:0 4.5em;width:calc(100% - 9em)}.d2h-code-line-ctn{background:none;display:inline-block;padding:0;word-wrap:normal;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;vertical-align:middle;white-space:pre;width:100%}.d2h-code-line del,.d2h-code-side-line del{background-color:#ffb6ba;background-color:var(--d2h-del-highlight-bg-color)}.d2h-code-line del,.d2h-code-line ins,.d2h-code-side-line del,.d2h-code-side-line ins{border-radius:.2em;display:inline-block;margin-top:-1px;-webkit-text-decoration:none;text-decoration:none}.d2h-code-line ins,.d2h-code-side-line ins{background-color:#97f295;background-color:var(--d2h-ins-highlight-bg-color);text-align:left}.d2h-code-line-prefix{background:none;display:inline;padding:0;word-wrap:normal;white-space:pre}.line-num1{float:left}.line-num1,.line-num2{-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;padding:0 .5em;text-overflow:ellipsis;width:3.5em}.line-num2{float:right}.d2h-code-linenumber{background-color:#fff;background-color:var(--d2h-bg-color);border:solid #eee;border:solid var(--d2h-line-border-color);border-width:0 1px;-webkit-box-sizing:border-box;box-sizing:border-box;color:rgba(0,0,0,.3);color:var(--d2h-dim-color);cursor:pointer;display:inline-block;position:absolute;text-align:right;width:7.5em}.d2h-code-linenumber:after{content:"\200b"}.d2h-code-side-linenumber{background-color:#fff;background-color:var(--d2h-bg-color);border:solid #eee;border:solid var(--d2h-line-border-color);border-width:0 1px;-webkit-box-sizing:border-box;box-sizing:border-box;color:rgba(0,0,0,.3);color:var(--d2h-dim-color);cursor:pointer;display:inline-block;overflow:hidden;padding:0 .5em;position:absolute;text-align:right;text-overflow:ellipsis;width:4em}.d2h-code-side-linenumber:after{content:"\200b"}.d2h-code-side-emptyplaceholder,.d2h-emptyplaceholder{background-color:#f1f1f1;background-color:var(--d2h-empty-placeholder-bg-color);border-color:#e1e1e1;border-color:var(--d2h-empty-placeholder-border-color)}.d2h-code-line-prefix,.d2h-code-linenumber,.d2h-code-side-linenumber,.d2h-emptyplaceholder{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.d2h-code-linenumber,.d2h-code-side-linenumber{direction:rtl}.d2h-del{background-color:#fee8e9;background-color:var(--d2h-del-bg-color);border-color:#e9aeae;border-color:var(--d2h-del-border-color)}.d2h-ins{background-color:#dfd;background-color:var(--d2h-ins-bg-color);border-color:#b4e2b4;border-color:var(--d2h-ins-border-color)}.d2h-info{background-color:#f8fafd;background-color:var(--d2h-info-bg-color);border-color:#d5e4f2;border-color:var(--d2h-info-border-color);color:rgba(0,0,0,.3);color:var(--d2h-dim-color)}.d2h-file-diff .d2h-del.d2h-change{background-color:#fdf2d0;background-color:var(--d2h-change-del-color)}.d2h-file-diff .d2h-ins.d2h-change{background-color:#ded;background-color:var(--d2h-change-ins-color)}.d2h-file-list-wrapper{margin-bottom:10px}.d2h-file-list-wrapper a{-webkit-text-decoration:none;text-decoration:none}.d2h-file-list-wrapper a,.d2h-file-list-wrapper a:visited{color:#3572b0;color:var(--d2h-moved-label-color)}.d2h-file-list-header{text-align:left}.d2h-file-list-title{font-weight:700}.d2h-file-list-line{display:-webkit-box;display:-ms-flexbox;display:flex;text-align:left}.d2h-file-list{display:block;list-style:none;margin:0;padding:0}.d2h-file-list>li{border-bottom:1px solid #ddd;border-bottom:1px solid var(--d2h-border-color);margin:0;padding:5px 10px}.d2h-file-list>li:last-child{border-bottom:none}.d2h-file-switch{cursor:pointer;display:none;font-size:10px}.d2h-icon{fill:currentColor;margin-right:10px;vertical-align:middle}.d2h-deleted{color:#c33;color:var(--d2h-del-label-color)}.d2h-added{color:#399839;color:var(--d2h-ins-label-color)}.d2h-changed{color:#d0b44c;color:var(--d2h-change-label-color)}.d2h-moved{color:#3572b0;color:var(--d2h-moved-label-color)}.d2h-tag{background-color:#fff;background-color:var(--d2h-bg-color);display:-webkit-box;display:-ms-flexbox;display:flex;font-size:10px;margin-left:5px;padding:0 2px}.d2h-deleted-tag{border:1px solid #c33;border:1px solid var(--d2h-del-label-color)}.d2h-added-tag{border:1px solid #399839;border:1px solid var(--d2h-ins-label-color)}.d2h-changed-tag{border:1px solid #d0b44c;border:1px solid var(--d2h-change-label-color)}.d2h-moved-tag{border:1px solid #3572b0;border:1px solid var(--d2h-moved-label-color)}.d2h-dark-color-scheme{background-color:#0d1117;background-color:var(--d2h-dark-bg-color);color:#e6edf3;color:var(--d2h-dark-color)}.d2h-dark-color-scheme .d2h-file-header{background-color:#161b22;background-color:var(--d2h-dark-file-header-bg-color);border-bottom:#30363d;border-bottom:var(--d2h-dark-file-header-border-color)}.d2h-dark-color-scheme .d2h-lines-added{border:1px solid rgba(46,160,67,.4);border:1px solid var(--d2h-dark-ins-border-color);color:#3fb950;color:var(--d2h-dark-ins-label-color)}.d2h-dark-color-scheme .d2h-lines-deleted{border:1px solid rgba(248,81,73,.4);border:1px solid var(--d2h-dark-del-border-color);color:#f85149;color:var(--d2h-dark-del-label-color)}.d2h-dark-color-scheme .d2h-code-line del,.d2h-dark-color-scheme .d2h-code-side-line del{background-color:rgba(248,81,73,.4);background-color:var(--d2h-dark-del-highlight-bg-color)}.d2h-dark-color-scheme .d2h-code-line ins,.d2h-dark-color-scheme .d2h-code-side-line ins{background-color:rgba(46,160,67,.4);background-color:var(--d2h-dark-ins-highlight-bg-color)}.d2h-dark-color-scheme .d2h-diff-tbody{border-color:#30363d;border-color:var(--d2h-dark-border-color)}.d2h-dark-color-scheme .d2h-code-side-linenumber{background-color:#0d1117;background-color:var(--d2h-dark-bg-color);border-color:#21262d;border-color:var(--d2h-dark-line-border-color);color:#6e7681;color:var(--d2h-dark-dim-color)}.d2h-dark-color-scheme .d2h-files-diff .d2h-code-side-emptyplaceholder,.d2h-dark-color-scheme .d2h-files-diff .d2h-emptyplaceholder{background-color:hsla(215,8%,47%,.1);background-color:var(--d2h-dark-empty-placeholder-bg-color);border-color:#30363d;border-color:var(--d2h-dark-empty-placeholder-border-color)}.d2h-dark-color-scheme .d2h-code-linenumber{background-color:#0d1117;background-color:var(--d2h-dark-bg-color);border-color:#21262d;border-color:var(--d2h-dark-line-border-color);color:#6e7681;color:var(--d2h-dark-dim-color)}.d2h-dark-color-scheme .d2h-del{background-color:rgba(248,81,73,.1);background-color:var(--d2h-dark-del-bg-color);border-color:rgba(248,81,73,.4);border-color:var(--d2h-dark-del-border-color)}.d2h-dark-color-scheme .d2h-ins{background-color:rgba(46,160,67,.15);background-color:var(--d2h-dark-ins-bg-color);border-color:rgba(46,160,67,.4);border-color:var(--d2h-dark-ins-border-color)}.d2h-dark-color-scheme .d2h-info{background-color:rgba(56,139,253,.1);background-color:var(--d2h-dark-info-bg-color);border-color:rgba(56,139,253,.4);border-color:var(--d2h-dark-info-border-color);color:#6e7681;color:var(--d2h-dark-dim-color)}.d2h-dark-color-scheme .d2h-file-diff .d2h-del.d2h-change{background-color:rgba(210,153,34,.2);background-color:var(--d2h-dark-change-del-color)}.d2h-dark-color-scheme .d2h-file-diff .d2h-ins.d2h-change{background-color:rgba(46,160,67,.25);background-color:var(--d2h-dark-change-ins-color)}.d2h-dark-color-scheme .d2h-file-wrapper{border:1px solid #30363d;border:1px solid var(--d2h-dark-border-color)}.d2h-dark-color-scheme .d2h-file-collapse{border:1px solid #0d1117;border:1px solid var(--d2h-dark-bg-color)}.d2h-dark-color-scheme .d2h-file-collapse.d2h-selected{background-color:rgba(56,139,253,.1);background-color:var(--d2h-dark-selected-color)}.d2h-dark-color-scheme .d2h-file-list-wrapper a,.d2h-dark-color-scheme .d2h-file-list-wrapper a:visited{color:#3572b0;color:var(--d2h-dark-moved-label-color)}.d2h-dark-color-scheme .d2h-file-list>li{border-bottom:1px solid #0d1117;border-bottom:1px solid var(--d2h-dark-bg-color)}.d2h-dark-color-scheme .d2h-deleted{color:#f85149;color:var(--d2h-dark-del-label-color)}.d2h-dark-color-scheme .d2h-added{color:#3fb950;color:var(--d2h-dark-ins-label-color)}.d2h-dark-color-scheme .d2h-changed{color:#d29922;color:var(--d2h-dark-change-label-color)}.d2h-dark-color-scheme .d2h-moved{color:#3572b0;color:var(--d2h-dark-moved-label-color)}.d2h-dark-color-scheme .d2h-tag{background-color:#0d1117;background-color:var(--d2h-dark-bg-color)}.d2h-dark-color-scheme .d2h-deleted-tag{border:1px solid #f85149;border:1px solid var(--d2h-dark-del-label-color)}.d2h-dark-color-scheme .d2h-added-tag{border:1px solid #3fb950;border:1px solid var(--d2h-dark-ins-label-color)}.d2h-dark-color-scheme .d2h-changed-tag{border:1px solid #d29922;border:1px solid var(--d2h-dark-change-label-color)}.d2h-dark-color-scheme .d2h-moved-tag{border:1px solid #3572b0;border:1px solid var(--d2h-dark-moved-label-color)}@media (prefers-color-scheme:dark){.d2h-auto-color-scheme{background-color:#0d1117;background-color:var(--d2h-dark-bg-color);color:#e6edf3;color:var(--d2h-dark-color)}.d2h-auto-color-scheme .d2h-file-header{background-color:#161b22;background-color:var(--d2h-dark-file-header-bg-color);border-bottom:#30363d;border-bottom:var(--d2h-dark-file-header-border-color)}.d2h-auto-color-scheme .d2h-lines-added{border:1px solid rgba(46,160,67,.4);border:1px solid var(--d2h-dark-ins-border-color);color:#3fb950;color:var(--d2h-dark-ins-label-color)}.d2h-auto-color-scheme .d2h-lines-deleted{border:1px solid rgba(248,81,73,.4);border:1px solid var(--d2h-dark-del-border-color);color:#f85149;color:var(--d2h-dark-del-label-color)}.d2h-auto-color-scheme .d2h-code-line del,.d2h-auto-color-scheme .d2h-code-side-line del{background-color:rgba(248,81,73,.4);background-color:var(--d2h-dark-del-highlight-bg-color)}.d2h-auto-color-scheme .d2h-code-line ins,.d2h-auto-color-scheme .d2h-code-side-line ins{background-color:rgba(46,160,67,.4);background-color:var(--d2h-dark-ins-highlight-bg-color)}.d2h-auto-color-scheme .d2h-diff-tbody{border-color:#30363d;border-color:var(--d2h-dark-border-color)}.d2h-auto-color-scheme .d2h-code-side-linenumber{background-color:#0d1117;background-color:var(--d2h-dark-bg-color);border-color:#21262d;border-color:var(--d2h-dark-line-border-color);color:#6e7681;color:var(--d2h-dark-dim-color)}.d2h-auto-color-scheme .d2h-files-diff .d2h-code-side-emptyplaceholder,.d2h-auto-color-scheme .d2h-files-diff .d2h-emptyplaceholder{background-color:hsla(215,8%,47%,.1);background-color:var(--d2h-dark-empty-placeholder-bg-color);border-color:#30363d;border-color:var(--d2h-dark-empty-placeholder-border-color)}.d2h-auto-color-scheme .d2h-code-linenumber{background-color:#0d1117;background-color:var(--d2h-dark-bg-color);border-color:#21262d;border-color:var(--d2h-dark-line-border-color);color:#6e7681;color:var(--d2h-dark-dim-color)}.d2h-auto-color-scheme .d2h-del{background-color:rgba(248,81,73,.1);background-color:var(--d2h-dark-del-bg-color);border-color:rgba(248,81,73,.4);border-color:var(--d2h-dark-del-border-color)}.d2h-auto-color-scheme .d2h-ins{background-color:rgba(46,160,67,.15);background-color:var(--d2h-dark-ins-bg-color);border-color:rgba(46,160,67,.4);border-color:var(--d2h-dark-ins-border-color)}.d2h-auto-color-scheme .d2h-info{background-color:rgba(56,139,253,.1);background-color:var(--d2h-dark-info-bg-color);border-color:rgba(56,139,253,.4);border-color:var(--d2h-dark-info-border-color);color:#6e7681;color:var(--d2h-dark-dim-color)}.d2h-auto-color-scheme .d2h-file-diff .d2h-del.d2h-change{background-color:rgba(210,153,34,.2);background-color:var(--d2h-dark-change-del-color)}.d2h-auto-color-scheme .d2h-file-diff .d2h-ins.d2h-change{background-color:rgba(46,160,67,.25);background-color:var(--d2h-dark-change-ins-color)}.d2h-auto-color-scheme .d2h-file-wrapper{border:1px solid #30363d;border:1px solid var(--d2h-dark-border-color)}.d2h-auto-color-scheme .d2h-file-collapse{border:1px solid #0d1117;border:1px solid var(--d2h-dark-bg-color)}.d2h-auto-color-scheme .d2h-file-collapse.d2h-selected{background-color:rgba(56,139,253,.1);background-color:var(--d2h-dark-selected-color)}.d2h-auto-color-scheme .d2h-file-list-wrapper a,.d2h-auto-color-scheme .d2h-file-list-wrapper a:visited{color:#3572b0;color:var(--d2h-dark-moved-label-color)}.d2h-auto-color-scheme .d2h-file-list>li{border-bottom:1px solid #0d1117;border-bottom:1px solid var(--d2h-dark-bg-color)}.d2h-dark-color-scheme .d2h-deleted{color:#f85149;color:var(--d2h-dark-del-label-color)}.d2h-auto-color-scheme .d2h-added{color:#3fb950;color:var(--d2h-dark-ins-label-color)}.d2h-auto-color-scheme .d2h-changed{color:#d29922;color:var(--d2h-dark-change-label-color)}.d2h-auto-color-scheme .d2h-moved{color:#3572b0;color:var(--d2h-dark-moved-label-color)}.d2h-auto-color-scheme .d2h-tag{background-color:#0d1117;background-color:var(--d2h-dark-bg-color)}.d2h-auto-color-scheme .d2h-deleted-tag{border:1px solid #f85149;border:1px solid var(--d2h-dark-del-label-color)}.d2h-auto-color-scheme .d2h-added-tag{border:1px solid #3fb950;border:1px solid var(--d2h-dark-ins-label-color)}.d2h-auto-color-scheme .d2h-changed-tag{border:1px solid #d29922;border:1px solid var(--d2h-dark-change-label-color)}.d2h-auto-color-scheme .d2h-moved-tag{border:1px solid #3572b0;border:1px solid var(--d2h-dark-moved-label-color)}} \ No newline at end of file diff --git a/media/diff2html/js/diff2html.min.js b/media/diff2html/js/diff2html.min.js new file mode 100644 index 0000000..1085451 --- /dev/null +++ b/media/diff2html/js/diff2html.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("Diff2Html",[],t):"object"==typeof exports?exports.Diff2Html=t():e.Diff2Html=t()}(this,(()=>{return e={25(e,t,n){"use strict";var r,i=this&&this.__extends||(r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},r(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)});Object.defineProperty(t,"__esModule",{value:!0}),t.wordsWithSpaceDiff=t.wordDiff=void 0,t.diffWords=function(e,n,r){return null==(null==r?void 0:r.ignoreWhitespace)||r.ignoreWhitespace?t.wordDiff.diff(e,n,r):d(e,n,r)},t.diffWordsWithSpace=d;var o=n(188),s=n(665),a="a-zA-Z0-9_\\u{AD}\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}",l=new RegExp("[".concat(a,"]+|\\s+|[^").concat(a,"]"),"ug"),c=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return i(t,e),t.prototype.equals=function(e,t,n){return n.ignoreCase&&(e=e.toLowerCase(),t=t.toLowerCase()),e.trim()===t.trim()},t.prototype.tokenize=function(e,t){var n;if(void 0===t&&(t={}),t.intlSegmenter){var r=t.intlSegmenter;if("word"!=r.resolvedOptions().granularity)throw new Error('The segmenter passed must have a granularity of "word"');n=[];for(var i=0,o=Array.from(r.segment(e));i{let t;return t=e.blocks.length?this.generateFileHtml(e):this.generateEmptyDiff(),this.makeFileDiffHtml(e,t)})).join("\n");return this.hoganUtils.render(f,"wrapper",{colorScheme:l.colorSchemeToCss(this.config.colorScheme),content:t})}makeFileDiffHtml(e,t){if(this.config.renderNothingWhenEmpty&&Array.isArray(e.blocks)&&0===e.blocks.length)return"";const n=this.hoganUtils.template("side-by-side","file-diff"),r=this.hoganUtils.template(f,"file-path"),i=this.hoganUtils.template("icon","file"),o=this.hoganUtils.template("tag",l.getFileIcon(e));return n.render({file:e,fileHtmlId:l.getHtmlId(e),diffs:t,filePath:r.render({fileDiffName:l.filenameDiff(e)},{fileIcon:i,fileTag:o})})}generateEmptyDiff(){return{right:"",left:this.hoganUtils.render(f,"empty-diff",{contentClass:"d2h-code-side-line",CSSLineClass:l.CSSLineClass})}}generateFileHtml(e){const t=a.newMatcherFn(a.newDistanceFn((t=>l.deconstructLine(t.content,e.isCombined).content)));return e.blocks.map((n=>{const r={left:this.makeHeaderHtml(n.header,e),right:this.makeHeaderHtml("")};return this.applyLineGroupping(n).forEach((([n,i,o])=>{if(i.length&&o.length&&!n.length)this.applyRematchMatching(i,o,t).map((([t,n])=>{const{left:i,right:o}=this.processChangedLines(e.isCombined,t,n);r.left+=i,r.right+=o}));else if(n.length)n.forEach((t=>{const{prefix:n,content:i}=l.deconstructLine(t.content,e.isCombined),{left:o,right:s}=this.generateLineHtml({type:l.CSSLineClass.CONTEXT,prefix:n,content:i,number:t.oldNumber},{type:l.CSSLineClass.CONTEXT,prefix:n,content:i,number:t.newNumber});r.left+=o,r.right+=s}));else if(i.length||o.length){const{left:t,right:n}=this.processChangedLines(e.isCombined,i,o);r.left+=t,r.right+=n}else console.error("Unknown state reached while processing groups of lines",n,i,o)})),r})).reduce(((e,t)=>({left:e.left+t.left,right:e.right+t.right})),{left:"",right:""})}applyLineGroupping(e){const t=[];let n=[],r=[];for(let i=0;i0)&&(t.push([[],n,r]),n=[],r=[]),o.type===c.LineType.CONTEXT?t.push([[o],[],[]]):o.type===c.LineType.INSERT&&0===n.length?t.push([[],[],[o]]):o.type===c.LineType.INSERT&&n.length>0?r.push(o):o.type===c.LineType.DELETE&&n.push(o)}return(n.length||r.length)&&(t.push([[],n,r]),n=[],r=[]),t}applyRematchMatching(e,t,n){const r=e.length*t.length,i=(0,u.max)(e.concat(t).map((e=>e.content.length)));return r{const r=a.compile(n,{asString:!1});return Object.assign(Object.assign({},e),{[t]:r})}),{});this.preCompiledTemplates=Object.assign(Object.assign(Object.assign({},l.defaultTemplates),e),n)}static compile(e){return a.compile(e,{asString:!1})}render(e,t,n,r,i){const o=this.templateKey(e,t);try{return this.preCompiledTemplates[o].render(n,r,i)}catch(e){throw new Error(`Could not find template to render '${o}'`)}}template(e,t){return this.preCompiledTemplates[this.templateKey(e,t)]}templateKey(e,t){return`${e}-${t}`}}},185(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.escapeForRegExp=function(e){return e.replace(n,"\\$&")},t.unifyPath=function(e){return e?e.replace(/\\/g,"/"):e},t.hashCode=function(e){let t,n,r,i=0;for(t=0,r=e.length;t=l&&p+1>=a)return s(this.buildValues(h[0].lastComponent,t,e));var b=-1/0,g=1/0,v=function(){for(var r=Math.max(b,-c);r<=Math.min(g,c);r+=2){var i=void 0,u=h[r-1],f=h[r+1];u&&(h[r-1]=void 0);var d=!1;if(f){var v=f.oldPos-r;d=f&&0<=v&&v=l&&p+1>=a)return s(o.buildValues(i.lastComponent,t,e))||!0;h[r]=i,i.oldPos+1>=l&&(g=Math.min(g,r-1)),p+1>=a&&(b=Math.max(b,r+1))}else h[r]=void 0}c++};if(r)!function e(){setTimeout((function(){if(c>u||Date.now()>d)return r(void 0);v()||e()}),0)}();else for(;c<=u&&Date.now()<=d;){var m=v();if(m)return m}},e.prototype.addToPath=function(e,t,n,r,i){var o=e.lastComponent;return o&&!i.oneChangePerToken&&o.added===t&&o.removed===n?{oldPos:e.oldPos+r,lastComponent:{count:o.count+1,added:t,removed:n,previousComponent:o.previousComponent}}:{oldPos:e.oldPos+r,lastComponent:{count:1,added:t,removed:n,previousComponent:o}}},e.prototype.extractCommon=function(e,t,n,r,i){for(var o=t.length,s=n.length,a=e.oldPos,l=a-r,c=0;l+1e.length?r:e})),c.value=this.join(u)}else c.value=this.join(t.slice(a,a+c.count));a+=c.count,c.added||(l+=c.count)}}return i},e}();t.default=n},302(e,t,n){"use strict";var r,i=this&&this.__createBinding||(Object.create?function(e,t,n,r){void 0===r&&(r=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,r,i)}:function(e,t,n,r){void 0===r&&(r=n),e[r]=t[n]}),o=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),s=this&&this.__importStar||(r=function(e){return r=Object.getOwnPropertyNames||function(e){var t=[];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[t.length]=n);return t},r(e)},function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n=r(e),s=0;ss||f.content.length>s)return{oldLine:{prefix:u.prefix,content:g(u.content)},newLine:{prefix:f.prefix,content:g(f.content)}};const p="char"===c?a.diffChars(u.content,f.content):a.diffWordsWithSpace(u.content,f.content),m=[];if("word"===c&&"words"===o){const e=p.filter((e=>e.removed)),t=p.filter((e=>e.added));h(t,e).forEach((e=>{1===e[0].length&&1===e[1].length&&d(e[0][0],e[1][0]){const n=t.added?"ins":t.removed?"del":null,r=m.indexOf(t)>-1?' class="d2h-change"':"",i=g(t.value);return null!==n?`${e}<${n}${r}>${i}`:`${e}${i}`}),"");return{oldLine:{prefix:u.prefix,content:(w=y,w.replace(/(]*>((.|\n)*?)<\/ins>)/g,""))},newLine:{prefix:f.prefix,content:b(y)}};var w};const a=s(n(801)),l=n(185),c=s(n(598)),u=n(613);t.CSSLineClass={INSERTS:"d2h-ins",DELETES:"d2h-del",CONTEXT:"d2h-cntx",INFO:"d2h-info",INSERT_CHANGES:"d2h-ins d2h-change",DELETE_CHANGES:"d2h-del d2h-change"},t.defaultRenderConfig={matching:u.LineMatchingType.NONE,matchWordsThreshold:.25,maxLineLengthHighlight:1e4,diffStyle:u.DiffStyleType.WORD,colorScheme:u.ColorSchemeType.LIGHT};const f="/",d=c.newDistanceFn((e=>e.value)),h=c.newMatcherFn(d);function p(e){return-1!==e.indexOf("dev/null")}function b(e){return e.replace(/(]*>((.|\n)*?)<\/del>)/g,"")}function g(e){return e.slice(0).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")}function v(e,t,n=!0){const r=function(e){return e?2:1}(t);return{prefix:e.substring(0,r),content:n?g(e.substring(r)):e.substring(r)}}function m(e){const t=(0,l.unifyPath)(e.oldName),n=(0,l.unifyPath)(e.newName);if(t===n||p(t)||p(n))return p(n)?t:n;{const e=[],r=[],i=t.split(f),o=n.split(f);let s=0,a=i.length-1,l=o.length-1;for(;ss&&l>s&&i[a]===o[l];)r.unshift(o[l]),a-=1,l-=1;const c=e.join(f),u=r.join(f),d=i.slice(s,a+1).join(f),h=o.slice(s,l+1).join(f);return c.length&&u.length?c+f+"{"+d+" → "+h+"}"+f+u:c.length?c+f+"{"+d+" → "+h+"}":u.length?"{"+d+" → "+h+"}"+f+u:t+" → "+n}}},363(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.parsePatch=function(e){var t=e.split(/\n/),n=[],r=0;function i(){var e={};for(n.push(e);r":7,"=":8,_v:9,"{":10,"&":11,_t:12},e.scan=function(n,r){var i,o=n.length,s=0,a=null,u=null,f="",d=[],h=!1,p=0,b=0,g="{{",v="}}";function m(){f.length>0&&(d.push({tag:"_t",text:new String(f)}),f="")}function y(n,r){if(m(),n&&function(){for(var n=!0,r=b;r"==i.tag&&(i.indent=d[o].text.toString()),d.splice(o,1));else r||d.push({tag:"\n"});h=!1,b=d.length}function w(e,t){var n="="+v,r=e.indexOf(n,t),i=l(e.substring(e.indexOf("=",t)+1,r)).split(" ");return g=i[0],v=i[i.length-1],r+n.length-1}for(r&&(r=r.split(" "),g=r[0],v=r[1]),p=0;p0;){if(l=t.shift(),o&&"<"==o.tag&&!(l.tag in u))throw new Error("Illegal content in < super tag.");if(e.tags[l.tag]<=e.tags.$||d(l,i))r.push(l),l.nodes=f(t,l.tag,r,i);else{if("/"==l.tag){if(0===r.length)throw new Error("Closing tag without opener: /"+l.n);if(a=r.pop(),l.n!=a.n&&!h(l.n,a.n,i))throw new Error("Nesting error: "+a.n+" vs. "+l.n);return a.end=l.i,s}"\n"==l.tag&&(l.last=0==t.length||"\n"==t[0].tag)}s.push(l)}if(r.length>0)throw new Error("missing closing tag: "+r.pop().n);return s}function d(e,t){for(var n=0,r=t.length;n":m,"<":function(t,n){var r={partials:{},code:"",subs:{},inPartial:!0};e.walk(t.nodes,r);var i=n.partials[m(t,n)];i.subs=r.subs,i.partials=r.partials},$:function(t,n){var r={subs:{},code:"",partials:n.partials,prefix:t.n};e.walk(t.nodes,r),n.subs[t.n]=r.code,n.inPartial||(n.code+='t.sub("'+g(t.n)+'",c,p,i);')},"\n":function(e,t){t.code+=w('"\\n"'+(e.last?"":" + i"))},_v:function(e,t){t.code+="t.b(t.v(t."+v(e.n)+'("'+g(e.n)+'",c,p,0)));'},_t:function(e,t){t.code+=w('"'+g(e.text)+'"')},"{":y,"&":y},e.walk=function(t,n){for(var r,i=0,o=t.length;i'),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b(r.rp("'),r.b(r.v(r.f("fileName",e,t,0))),r.b(""),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b(' '),r.b(r.v(r.f("addedLines",e,t,0))),r.b(""),r.b("\n"+n),r.b(' '),r.b(r.v(r.f("deletedLines",e,t,0))),r.b(""),r.b("\n"+n),r.b(" "),r.b("\n"+n),r.b(" "),r.b("\n"+n),r.b(""),r.fl()},partials:{"'),r.b("\n"+n),r.b('
'),r.b("\n"+n),r.b(' Files changed ('),r.b(r.v(r.f("filesNumber",e,t,0))),r.b(")"),r.b("\n"+n),r.b(' hide'),r.b("\n"+n),r.b(' show'),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b('
    '),r.b("\n"+n),r.b(" "),r.b(r.t(r.f("files",e,t,0))),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b(""),r.fl()},partials:{},subs:{}}),t.defaultTemplates["generic-block-header"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b(""),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b('
'),r.s(r.f("blockHeader",e,t,1),e,t,0,156,173,"{{ }}")&&(r.rs(e,t,(function(e,t,n){n.b(n.t(n.f("blockHeader",e,t,0)))})),e.pop()),r.s(r.f("blockHeader",e,t,1),e,t,1,0,0,"")||r.b(" "),r.b("
"),r.b("\n"+n),r.b(" "),r.b("\n"+n),r.b(""),r.fl()},partials:{},subs:{}}),t.defaultTemplates["generic-empty-diff"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b(""),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b('
'),r.b("\n"+n),r.b(" File without changes"),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b(" "),r.b("\n"+n),r.b(""),r.fl()},partials:{},subs:{}}),t.defaultTemplates["generic-file-path"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b(''),r.b("\n"+n),r.b(r.rp("'),r.b(r.v(r.f("fileDiffName",e,t,0))),r.b(""),r.b("\n"+n),r.b(r.rp(""),r.b("\n"+n),r.b('"),r.fl()},partials:{""),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b(" "),r.b(r.t(r.f("lineNumber",e,t,0))),r.b("\n"+n),r.b(" "),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b('
'),r.b("\n"+n),r.s(r.f("prefix",e,t,1),e,t,0,162,238,"{{ }}")&&(r.rs(e,t,(function(e,t,r){r.b(' '),r.b(r.t(r.f("prefix",e,t,0))),r.b(""),r.b("\n"+n)})),e.pop()),r.s(r.f("prefix",e,t,1),e,t,1,0,0,"")||(r.b('  '),r.b("\n"+n)),r.s(r.f("content",e,t,1),e,t,0,371,445,"{{ }}")&&(r.rs(e,t,(function(e,t,r){r.b(' '),r.b(r.t(r.f("content",e,t,0))),r.b(""),r.b("\n"+n)})),e.pop()),r.s(r.f("content",e,t,1),e,t,1,0,0,"")||(r.b('
'),r.b("\n"+n)),r.b("
"),r.b("\n"+n),r.b(" "),r.b("\n"+n),r.b(""),r.fl()},partials:{},subs:{}}),t.defaultTemplates["generic-wrapper"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('
'),r.b("\n"+n),r.b(" "),r.b(r.t(r.f("content",e,t,0))),r.b("\n"+n),r.b("
"),r.fl()},partials:{},subs:{}}),t.defaultTemplates["icon-file-added"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('"),r.fl()},partials:{},subs:{}}),t.defaultTemplates["icon-file-changed"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('"),r.fl()},partials:{},subs:{}}),t.defaultTemplates["icon-file-deleted"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('"),r.fl()},partials:{},subs:{}}),t.defaultTemplates["icon-file-renamed"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('"),r.fl()},partials:{},subs:{}}),t.defaultTemplates["icon-file"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('"),r.fl()},partials:{},subs:{}}),t.defaultTemplates["line-by-line-file-diff"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('
'),r.b("\n"+n),r.b('
'),r.b("\n"+n),r.b(" "),r.b(r.t(r.f("filePath",e,t,0))),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b('
'),r.b("\n"+n),r.b('
'),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b(" "),r.b(r.t(r.f("diffs",e,t,0))),r.b("\n"+n),r.b(" "),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b("
"),r.fl()},partials:{},subs:{}}),t.defaultTemplates["line-by-line-numbers"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('
'),r.b(r.v(r.f("oldNumber",e,t,0))),r.b("
"),r.b("\n"+n),r.b('
'),r.b(r.v(r.f("newNumber",e,t,0))),r.b("
"),r.fl()},partials:{},subs:{}}),t.defaultTemplates["side-by-side-file-diff"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('
'),r.b("\n"+n),r.b('
'),r.b("\n"+n),r.b(" "),r.b(r.t(r.f("filePath",e,t,0))),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b('
'),r.b("\n"+n),r.b('
'),r.b("\n"+n),r.b('
'),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b(" "),r.b(r.t(r.d("diffs.left",e,t,0))),r.b("\n"+n),r.b(" "),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b('
'),r.b("\n"+n),r.b('
'),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b(' '),r.b("\n"+n),r.b(" "),r.b(r.t(r.d("diffs.right",e,t,0))),r.b("\n"+n),r.b(" "),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b("
"),r.b("\n"+n),r.b("
"),r.fl()},partials:{},subs:{}}),t.defaultTemplates["tag-file-added"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('ADDED'),r.fl()},partials:{},subs:{}}),t.defaultTemplates["tag-file-changed"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('CHANGED'),r.fl()},partials:{},subs:{}}),t.defaultTemplates["tag-file-deleted"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('DELETED'),r.fl()},partials:{},subs:{}}),t.defaultTemplates["tag-file-renamed"]=new a.Template({code:function(e,t,n){var r=this;return r.b(n=n||""),r.b('RENAMED'),r.fl()},partials:{},subs:{}})},501(e,t,n){"use strict";var r,i=this&&this.__createBinding||(Object.create?function(e,t,n,r){void 0===r&&(r=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,r,i)}:function(e,t,n,r){void 0===r&&(r=n),e[r]=t[n]}),o=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),s=this&&this.__importStar||(r=function(e){return r=Object.getOwnPropertyNames||function(e){var t=[];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[t.length]=n);return t},r(e)},function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n=r(e),s=0;sthis.hoganUtils.render(l,"line",{fileHtmlId:a.getHtmlId(e),oldName:e.oldName,newName:e.newName,fileName:a.filenameDiff(e),deletedLines:"-"+e.deletedLines,addedLines:"+"+e.addedLines},{fileIcon:this.hoganUtils.template("icon",a.getFileIcon(e))}))).join("\n");return this.hoganUtils.render(l,"wrapper",{colorScheme:a.colorSchemeToCss(this.config.colorScheme),filesNumber:e.length,files:t})}}},598(e,t){"use strict";function n(e,t){if(0===e.length)return t.length;if(0===t.length)return e.length;const n=[];let r,i;for(r=0;r<=t.length;r++)n[r]=[r];for(i=0;i<=e.length;i++)n[0][i]=i;for(r=1;r<=t.length;r++)for(i=1;i<=e.length;i++)t.charAt(r-1)===e.charAt(i-1)?n[r][i]=n[r-1][i-1]:n[r][i]=Math.min(n[r-1][i-1]+1,Math.min(n[r][i-1]+1,n[r-1][i]+1));return n[t.length][e.length]}Object.defineProperty(t,"__esModule",{value:!0}),t.levenshtein=n,t.newDistanceFn=function(e){return(t,r)=>{const i=e(t).trim(),o=e(r).trim();return n(i,o)/(i.length+o.length)}},t.newMatcherFn=function(e){return function t(n,r,i=0,o=new Map){const s=function(t,n,r=new Map){let i,o=1/0;for(let s=0;s0||s.indexB>0)&&(m=b.concat(m)),(n.length>f||r.length>d)&&(m=m.concat(v)),m}}},613(e,t){"use strict";var n,r;Object.defineProperty(t,"__esModule",{value:!0}),t.ColorSchemeType=t.DiffStyleType=t.LineMatchingType=t.OutputFormatType=t.LineType=void 0,function(e){e.INSERT="insert",e.DELETE="delete",e.CONTEXT="context"}(n||(t.LineType=n={})),t.OutputFormatType={LINE_BY_LINE:"line-by-line",SIDE_BY_SIDE:"side-by-side"},t.LineMatchingType={LINES:"lines",WORDS:"words",NONE:"none"},t.DiffStyleType={WORD:"word",CHAR:"char"},function(e){e.AUTO="auto",e.DARK="dark",e.LIGHT="light"}(r||(t.ColorSchemeType=r={}))},622(e,t,n){"use strict";var r,i=this&&this.__extends||(r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},r(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)});Object.defineProperty(t,"__esModule",{value:!0}),t.characterDiff=void 0,t.diffChars=function(e,n,r){return t.characterDiff.diff(e,n,r)};var o=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return i(t,e),t}(n(188).default);t.characterDiff=new o},660(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.convertChangesToXML=function(e){for(var t=[],n=0;n"):r.removed&&t.push(""),t.push(r.value.replace(/&/g,"&").replace(//g,">").replace(/"/g,""")),r.added?t.push(""):r.removed&&t.push("")}return t.join("")}},665(e,t){"use strict";function n(e,t,n){if(e.slice(0,t.length)!=t)throw Error("string ".concat(JSON.stringify(e)," doesn't start with prefix ").concat(JSON.stringify(t),"; this is a bug"));return n+e.slice(t.length)}function r(e,t,n){if(!t)return e+n;if(e.slice(-t.length)!=t)throw Error("string ".concat(JSON.stringify(e)," doesn't end with suffix ").concat(JSON.stringify(t),"; this is a bug"));return e.slice(0,-t.length)+n}Object.defineProperty(t,"__esModule",{value:!0}),t.longestCommonPrefix=function(e,t){var n;for(n=0;nt.length&&(n=e.length-t.length);var r=t.length;e.length0&&t[s]!=t[o];)o=i[o];t[s]==t[o]&&o++}o=0;for(var a=n;a0&&e[a]!=t[o];)o=i[o];e[a]==t[o]&&o++}return o}(e,t))},t.hasOnlyWinLineEndings=function(e){return e.includes("\r\n")&&!e.startsWith("\n")&&!e.match(/[^\r]\n/)},t.hasOnlyUnixLineEndings=function(e){return!e.includes("\r\n")&&e.includes("\n")},t.trailingWs=function(e){var t;for(t=e.length-1;t>=0&&e[t].match(/\s/);t--);return e.substring(t+1)},t.leadingWs=function(e){var t=e.match(/^\s*/);return t?t[0]:""}},725(e,t){"use strict";var n=this&&this.__assign||function(){return n=Object.assign||function(e){for(var t,n=1,r=arguments.length;n=0;l--)if(void 0!==(o=t(e,n[l],a))){s=!0;break}return s?(i||"function"!=typeof o||(o=this.mv(o,n,r)),o):!i&&""},ls:function(e,t,n,r,i,o){var s=this.options.delimiters;return this.options.delimiters=o,this.b(this.ct(l(e.call(t,i,n)),t,r)),this.options.delimiters=s,!1},ct:function(e,t,n){if(this.options.disableLambda)throw new Error("Lambda features disabled.");return this.c.compile(e,this.options).render(t,n)},b:function(e){this.buf+=e},fl:function(){var e=this.buf;return this.buf="",e},ms:function(e,t,n,r,i,o,s){var a,l=t[t.length-1],c=e.call(l);return"function"==typeof c?!!r||(a=this.activeSub&&this.subsText&&this.subsText[this.activeSub]?this.subsText[this.activeSub]:this.text,this.ls(c,l,t,n,a.substring(i,o),s)):c},mv:function(e,t,n){var r=t[t.length-1],i=e.call(r);return"function"==typeof i?this.ct(l(i.call(r)),r,n):i},sub:function(e,t,n,r){var i=this.subs[e];i&&(this.activeSub=e,i(t,n,this,r),this.activeSub=!1)}};var n=/&/g,r=//g,o=/\'/g,s=/\"/g,a=/[&<>\"\']/;function l(e){return String(null==e?"":e)}var c=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)}}(t)},801(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.canonicalize=t.convertChangesToXML=t.convertChangesToDMP=t.reversePatch=t.parsePatch=t.applyPatches=t.applyPatch=t.OMIT_HEADERS=t.FILE_HEADERS_ONLY=t.INCLUDE_HEADERS=t.formatPatch=t.createPatch=t.createTwoFilesPatch=t.structuredPatch=t.arrayDiff=t.diffArrays=t.jsonDiff=t.diffJson=t.cssDiff=t.diffCss=t.sentenceDiff=t.diffSentences=t.diffTrimmedLines=t.lineDiff=t.diffLines=t.wordsWithSpaceDiff=t.diffWordsWithSpace=t.wordDiff=t.diffWords=t.characterDiff=t.diffChars=t.Diff=void 0;var r=n(188);t.Diff=r.default;var i=n(622);Object.defineProperty(t,"diffChars",{enumerable:!0,get:function(){return i.diffChars}}),Object.defineProperty(t,"characterDiff",{enumerable:!0,get:function(){return i.characterDiff}});var o=n(25);Object.defineProperty(t,"diffWords",{enumerable:!0,get:function(){return o.diffWords}}),Object.defineProperty(t,"diffWordsWithSpace",{enumerable:!0,get:function(){return o.diffWordsWithSpace}}),Object.defineProperty(t,"wordDiff",{enumerable:!0,get:function(){return o.wordDiff}}),Object.defineProperty(t,"wordsWithSpaceDiff",{enumerable:!0,get:function(){return o.wordsWithSpaceDiff}});var s=n(839);Object.defineProperty(t,"diffLines",{enumerable:!0,get:function(){return s.diffLines}}),Object.defineProperty(t,"diffTrimmedLines",{enumerable:!0,get:function(){return s.diffTrimmedLines}}),Object.defineProperty(t,"lineDiff",{enumerable:!0,get:function(){return s.lineDiff}});var a=n(484);Object.defineProperty(t,"diffSentences",{enumerable:!0,get:function(){return a.diffSentences}}),Object.defineProperty(t,"sentenceDiff",{enumerable:!0,get:function(){return a.sentenceDiff}});var l=n(42);Object.defineProperty(t,"diffCss",{enumerable:!0,get:function(){return l.diffCss}}),Object.defineProperty(t,"cssDiff",{enumerable:!0,get:function(){return l.cssDiff}});var c=n(843);Object.defineProperty(t,"diffJson",{enumerable:!0,get:function(){return c.diffJson}}),Object.defineProperty(t,"canonicalize",{enumerable:!0,get:function(){return c.canonicalize}}),Object.defineProperty(t,"jsonDiff",{enumerable:!0,get:function(){return c.jsonDiff}});var u=n(968);Object.defineProperty(t,"diffArrays",{enumerable:!0,get:function(){return u.diffArrays}}),Object.defineProperty(t,"arrayDiff",{enumerable:!0,get:function(){return u.arrayDiff}});var f=n(904);Object.defineProperty(t,"applyPatch",{enumerable:!0,get:function(){return f.applyPatch}}),Object.defineProperty(t,"applyPatches",{enumerable:!0,get:function(){return f.applyPatches}});var d=n(363);Object.defineProperty(t,"parsePatch",{enumerable:!0,get:function(){return d.parsePatch}});var h=n(406);Object.defineProperty(t,"reversePatch",{enumerable:!0,get:function(){return h.reversePatch}});var p=n(822);Object.defineProperty(t,"structuredPatch",{enumerable:!0,get:function(){return p.structuredPatch}}),Object.defineProperty(t,"createTwoFilesPatch",{enumerable:!0,get:function(){return p.createTwoFilesPatch}}),Object.defineProperty(t,"createPatch",{enumerable:!0,get:function(){return p.createPatch}}),Object.defineProperty(t,"formatPatch",{enumerable:!0,get:function(){return p.formatPatch}}),Object.defineProperty(t,"INCLUDE_HEADERS",{enumerable:!0,get:function(){return p.INCLUDE_HEADERS}}),Object.defineProperty(t,"FILE_HEADERS_ONLY",{enumerable:!0,get:function(){return p.FILE_HEADERS_ONLY}}),Object.defineProperty(t,"OMIT_HEADERS",{enumerable:!0,get:function(){return p.OMIT_HEADERS}});var b=n(868);Object.defineProperty(t,"convertChangesToDMP",{enumerable:!0,get:function(){return b.convertChangesToDMP}});var g=n(660);Object.defineProperty(t,"convertChangesToXML",{enumerable:!0,get:function(){return g.convertChangesToXML}})},822(e,t,n){"use strict";var r=this&&this.__assign||function(){return r=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0?E(b.lines.slice(-u)):[]).length,o-=l.length)}for(var g=0,v=p;g1&&!n.includeFileHeaders)throw new Error("Cannot omit file headers on a multi-file patch. (The result would be unparseable; how would a tool trying to apply the patch know which changes are to which file?)");return e.map((function(e){return s(e,n)})).join("\n")}var r=[];n.includeIndex&&e.oldFileName==e.newFileName&&r.push("Index: "+e.oldFileName),n.includeUnderline&&r.push("==================================================================="),n.includeFileHeaders&&(r.push("--- "+e.oldFileName+(void 0===e.oldHeader?"":"\t"+e.oldHeader)),r.push("+++ "+e.newFileName+(void 0===e.newHeader?"":"\t"+e.newHeader)));for(var i=0;i{let t;return t=e.blocks.length?this.generateFileHtml(e):this.generateEmptyDiff(),this.makeFileDiffHtml(e,t)})).join("\n");return this.hoganUtils.render(f,"wrapper",{colorScheme:l.colorSchemeToCss(this.config.colorScheme),content:t})}makeFileDiffHtml(e,t){if(this.config.renderNothingWhenEmpty&&Array.isArray(e.blocks)&&0===e.blocks.length)return"";const n=this.hoganUtils.template(d,"file-diff"),r=this.hoganUtils.template(f,"file-path"),i=this.hoganUtils.template("icon","file"),o=this.hoganUtils.template("tag",l.getFileIcon(e));return n.render({file:e,fileHtmlId:l.getHtmlId(e),diffs:t,filePath:r.render({fileDiffName:l.filenameDiff(e)},{fileIcon:i,fileTag:o})})}generateEmptyDiff(){return this.hoganUtils.render(f,"empty-diff",{contentClass:"d2h-code-line",CSSLineClass:l.CSSLineClass})}generateFileHtml(e){const t=a.newMatcherFn(a.newDistanceFn((t=>l.deconstructLine(t.content,e.isCombined).content)));return e.blocks.map((n=>{let r=this.hoganUtils.render(f,"block-header",{CSSLineClass:l.CSSLineClass,blockHeader:e.isTooBig?n.header:l.escapeForHtml(n.header),lineClass:"d2h-code-linenumber",contentClass:"d2h-code-line"});return this.applyLineGroupping(n).forEach((([n,i,o])=>{if(i.length&&o.length&&!n.length)this.applyRematchMatching(i,o,t).map((([t,n])=>{const{left:i,right:o}=this.processChangedLines(e,e.isCombined,t,n);r+=i,r+=o}));else if(n.length)n.forEach((t=>{const{prefix:n,content:i}=l.deconstructLine(t.content,e.isCombined);r+=this.generateSingleLineHtml(e,{type:l.CSSLineClass.CONTEXT,prefix:n,content:i,oldNumber:t.oldNumber,newNumber:t.newNumber})}));else if(i.length||o.length){const{left:t,right:n}=this.processChangedLines(e,e.isCombined,i,o);r+=t,r+=n}else console.error("Unknown state reached while processing groups of lines",n,i,o)})),r})).join("\n")}applyLineGroupping(e){const t=[];let n=[],r=[];for(let i=0;i0)&&(t.push([[],n,r]),n=[],r=[]),o.type===c.LineType.CONTEXT?t.push([[o],[],[]]):o.type===c.LineType.INSERT&&0===n.length?t.push([[],[],[o]]):o.type===c.LineType.INSERT&&n.length>0?r.push(o):o.type===c.LineType.DELETE&&n.push(o)}return(n.length||r.length)&&(t.push([[],n,r]),n=[],r=[]),t}applyRematchMatching(e,t,n){const r=e.length*t.length,i=(0,u.max)(e.concat(t).map((e=>e.content.length)));return r1)throw new Error("applyPatch only works with a single input.");return function(e,t,n){void 0===n&&(n={}),(n.autoConvertLineEndings||null==n.autoConvertLineEndings)&&((0,r.hasOnlyWinLineEndings)(e)&&(0,i.isUnix)(t)?t=(0,i.unixToWin)(t):(0,r.hasOnlyUnixLineEndings)(e)&&(0,i.isWin)(t)&&(t=(0,i.winToUnix)(t)));var o=e.split("\n"),a=t.hunks,l=n.compareLine||function(e,t,n,r){return t===r},c=n.fuzzFactor||0,u=0;if(c<0||!Number.isInteger(c))throw new Error("fuzzFactor must be a non-negative integer");if(!a.length)return e;for(var f="",d=!1,h=!1,p=0;p0?f[0]:" ",h=f.length>0?f.substr(1):f;if("-"===d){if(!l(t+1,o[t],d,h))return n&&null!=o[t]?(s[a]=o[t],b(e,t+1,n-1,r,!1,s,a+1)):null;t++,c=0}if("+"===d){if(!i)return null;s[a]=h,a++,c=0,u=!0}if(" "===d){if(c++,s[a]=o[t],!l(t+1,o[t],d,h))return u||!n?null:o[t]&&(b(e,t+1,n-1,r+1,!1,s,a+1)||b(e,t+1,n-1,r,!1,s,a+1))||b(e,t,n-1,r+1,!1,s,a);a++,i=!0,u=!1,t++}}return a-=c,t-=c,s.length=a,{patchedLines:s,oldLineLastI:t-1}}var g=[],v=0;for(p=0;p{if(!e||e.startsWith("*"))return;let M;const W=H[u-1],A=H[u+1],R=H[u+2];if(e.startsWith("diff --git")||e.startsWith("diff --combined")){if(I(),(M=/^diff --git "?([a-ciow]\/.+)"? "?([a-ciow]\/.+)"?/.exec(e))&&(d=l(M[1],void 0,t.dstPrefix),h=l(M[2],void 0,t.srcPrefix)),null===i)throw new Error("Where is my file !!!");return void(i.isGitDiff=!0)}if(e.startsWith("Binary files")&&!(null==i?void 0:i.isGitDiff)){if(I(),(M=/^Binary files "?([a-ciow]\/.+)"? and "?([a-ciow]\/.+)"? differ/.exec(e))&&(d=l(M[1],void 0,t.dstPrefix),h=l(M[2],void 0,t.srcPrefix)),null===i)throw new Error("Where is my file !!!");return void(i.isBinary=!0)}if((!i||!i.isGitDiff&&i&&e.startsWith(p)&&A.startsWith(b)&&R.startsWith(g))&&I(),null==i?void 0:i.isTooBig)return;if(i&&("number"==typeof t.diffMaxChanges&&i.addedLines+i.deletedLines>t.diffMaxChanges||"number"==typeof t.diffMaxLineLength&&e.length>t.diffMaxLineLength))return i.isTooBig=!0,i.addedLines=0,i.deletedLines=0,i.blocks=[],a=null,void F("function"==typeof t.diffTooBigMessage?t.diffTooBigMessage(n.length):"Diff too big to be displayed");if(e.startsWith(p)&&A.startsWith(b)||e.startsWith(b)&&W.startsWith(p)){if(i&&!i.oldName&&e.startsWith("--- ")&&(M=function(e,t){return l(e,"---",t)}(e,t.srcPrefix)))return i.oldName=M,void(i.language=o(i.oldName,i.language));if(i&&!i.newName&&e.startsWith("+++ ")&&(M=function(e,t){return l(e,"+++",t)}(e,t.dstPrefix)))return i.newName=M,void(i.language=o(i.newName,i.language))}if(i&&(e.startsWith(g)||i.isGitDiff&&i.oldName&&i.newName&&!a))return void F(e);if(a&&(e.startsWith("+")||e.startsWith("-")||e.startsWith(" ")))return void function(e){if(null===i||null===a||null===c||null===f)return;const t={content:e},n=i.isCombined?["+ "," +","++"]:["+"],o=i.isCombined?["- "," -","--"]:["-"];s(e,n)?(i.addedLines++,t.type=r.LineType.INSERT,t.oldNumber=void 0,t.newNumber=f++):s(e,o)?(i.deletedLines++,t.type=r.LineType.DELETE,t.oldNumber=c++,t.newNumber=void 0):(t.type=r.LineType.CONTEXT,t.oldNumber=c++,t.newNumber=f++),a.lines.push(t)}(e);const z=!function(e,t){let n=t;for(;n1?n[n.length-1]:t}function s(e,t){return t.reduce(((t,n)=>t||e.startsWith(n)),!1)}const a=["a/","b/","i/","w/","c/","o/"];function l(e,t,n){const r=void 0!==n?[...a,n]:a,o=t?new RegExp(`^${(0,i.escapeForRegExp)(t)} "?(.+?)"?$`):new RegExp('^"?(.+?)"?$'),[,s=""]=o.exec(e)||[],l=r.find((e=>0===s.indexOf(e)));return(l?s.slice(l.length):s).replace(/\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)? [+-]\d{4}.*$/,"")}},968(e,t,n){"use strict";var r,i=this&&this.__extends||(r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},r(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)});Object.defineProperty(t,"__esModule",{value:!0}),t.arrayDiff=void 0,t.diffArrays=function(e,n,r){return t.arrayDiff.diff(e,n,r)};var o=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return i(t,e),t.prototype.tokenize=function(e){return e.slice()},t.prototype.join=function(e){return e},t.prototype.removeEmpty=function(e){return e},t}(n(188).default);t.arrayDiff=new o}},t={},function n(r){var i=t[r];if(void 0!==i)return i.exports;var o=t[r]={exports:{}};return e[r].call(o.exports,o,o.exports,n),o.exports}(166);var e,t})); \ No newline at end of file diff --git a/media/patchViewer.css b/media/patchViewer.css new file mode 100644 index 0000000..06ea37f --- /dev/null +++ b/media/patchViewer.css @@ -0,0 +1,458 @@ +/* Patch Reader webview styles. + * Loaded into the custom editor webview via webview.asWebviewUri(). + * Kept as a standalone file so the browser receives it verbatim (no template-literal escaping). */ +:root { + --bg-primary: var(--vscode-editor-background); + --bg-secondary: var(--vscode-sideBar-background, var(--vscode-editor-background)); + --text-primary: var(--vscode-editor-foreground); + --text-muted: var(--vscode-descriptionForeground); + --border-color: var(--vscode-panel-border, var(--vscode-editorGroup-border)); + --btn-bg: var(--vscode-button-secondaryBackground); + --btn-bg-hover: var(--vscode-button-secondaryHoverBackground); + --btn-text: var(--vscode-button-secondaryForeground); + --btn-primary-bg: var(--vscode-button-background); + --btn-primary-bg-hover: var(--vscode-button-hoverBackground); + --btn-primary-text: var(--vscode-button-foreground); + --tab-active-bg: var(--vscode-tab-activeBackground); + --tab-inactive-bg: var(--vscode-tab-inactiveBackground); + --tab-active-fg: var(--vscode-tab-activeForeground); + --tab-inactive-fg: var(--vscode-tab-inactiveForeground); + --tab-border: var(--vscode-tab-border); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + background-color: var(--bg-primary); + color: var(--text-primary); + height: 100vh; + overflow: hidden; +} + +.container { + display: flex; + flex-direction: column; + height: 100vh; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; + background-color: var(--bg-secondary); + border-top: 1px solid var(--border-color); + order: 1; +} + +.tabs { + display: flex; + gap: 0; +} + +.tab { + padding: 8px 16px; + border: none; + background-color: var(--tab-inactive-bg); + color: var(--tab-inactive-fg); + cursor: pointer; + font-size: 13px; + border-top: 2px solid transparent; + transition: all 0.2s ease; +} + +.tab:hover { + background-color: var(--tab-active-bg); +} + +.tab.active { + background-color: var(--tab-active-bg); + color: var(--tab-active-fg); + border-top-color: var(--btn-primary-bg); +} + +.view-toggle { + display: flex; + gap: 4px; +} + +.view-btn { + padding: 6px 12px; + border: 1px solid var(--border-color); + background-color: var(--btn-bg); + color: var(--btn-text); + cursor: pointer; + font-size: 12px; + border-radius: 4px; + transition: all 0.2s ease; +} + +.view-btn:hover { + background-color: var(--btn-bg-hover); +} + +.view-btn.active { + background-color: var(--btn-primary-bg); + color: var(--btn-primary-text); + border-color: var(--btn-primary-bg); +} + +.content { + flex: 1; + overflow: auto; + padding: 16px; + min-height: 0; +} + +.tab-content { + display: none; + height: 100%; + overflow: hidden; +} + +.tab-content.active { + display: block; +} + +#diff-output { + overflow: auto; + height: 100%; +} + +#content-output { + width: 100%; + height: 100%; + overflow: auto; + padding: 8px 12px; + margin: 0; + font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace); + font-size: var(--vscode-editor-font-size, 14px); + line-height: var(--vscode-editor-line-height, 1.5); + background-color: var(--bg-primary); + color: var(--text-primary); + border: none; + white-space: pre; + tab-size: 4; + resize: none; + outline: none; +} + +/* Diff syntax highlighting - matching VS Code built-in editor */ +.diff-line-header { + color: var(--vscode-diffEditor-unchangedRegionForeground, #608b4e); +} +.diff-line-add { + color: var(--vscode-gitDecoration-addedResourceForeground, #89d185); + background-color: var(--vscode-diffEditor-insertedLineBackground, rgba(155, 185, 85, 0.2)); +} +.diff-line-delete { + color: var(--vscode-gitDecoration-deletedResourceForeground, #f14c4c); + background-color: var(--vscode-diffEditor-removedLineBackground, rgba(255, 0, 0, 0.2)); +} +.diff-line-meta { + color: var(--vscode-symbolIcon-functionForeground, #569cd6); +} +.diff-line-range { + color: var(--vscode-editorLineNumber-activeForeground, #c586c0); +} +.diff-line-index { + color: var(--vscode-textPreformat-foreground, #9cdcfe); +} + +/* Light theme overrides */ +body.vscode-light .diff-line-header { + color: #22863a; +} +body.vscode-light .diff-line-add { + color: #22863a; + background-color: rgba(46, 160, 67, 0.15); +} +body.vscode-light .diff-line-delete { + color: #cb2431; + background-color: rgba(248, 81, 73, 0.15); +} +body.vscode-light .diff-line-meta { + color: #0550ae; +} +body.vscode-light .diff-line-range { + color: #6f42c1; +} +body.vscode-light .diff-line-index { + color: #953800; +} + +.placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); +} + +/* diff2html theme overrides for VS Code integration */ +.d2h-wrapper { + font-family: var(--vscode-editor-font-family, monospace); +} + +.d2h-file-header { + background-color: var(--bg-secondary); + border-color: var(--border-color); +} + +.d2h-file-name { + color: var(--text-primary); +} + +.d2h-file-wrapper { + border-color: var(--border-color); +} + +/* Light theme diff colors */ +body.vscode-light .d2h-del { + background-color: #ffeef0; +} + +body.vscode-light .d2h-ins { + background-color: #e6ffec; +} + +body.vscode-light .d2h-info { + background-color: #f1f8ff; + color: #0366d6; +} + +body.vscode-light .d2h-code-line-ctn, +body.vscode-light .d2h-code-side-line { + background-color: var(--bg-primary); + color: var(--text-primary); +} + +body.vscode-light .d2h-code-linenumber, +body.vscode-light .d2h-code-side-linenumber { + background-color: var(--bg-secondary); + color: var(--text-muted); + border-color: var(--border-color); +} + +body.vscode-light .d2h-del .d2h-code-line-ctn, +body.vscode-light .d2h-del .d2h-code-side-line { + background-color: #ffeef0; +} + +body.vscode-light .d2h-ins .d2h-code-line-ctn, +body.vscode-light .d2h-ins .d2h-code-side-line { + background-color: #e6ffec; +} + +/* Dark theme diff colors */ +body.vscode-dark .d2h-del, +body.vscode-high-contrast .d2h-del { + background-color: #3d1d26; +} + +body.vscode-dark .d2h-ins, +body.vscode-high-contrast .d2h-ins { + background-color: #1f3d2a; +} + +body.vscode-dark .d2h-info, +body.vscode-high-contrast .d2h-info { + background-color: #1f2937; + color: #93c5fd; + border-color: var(--border-color); +} + +body.vscode-dark .d2h-code-line-ctn, +body.vscode-dark .d2h-code-side-line, +body.vscode-high-contrast .d2h-code-line-ctn, +body.vscode-high-contrast .d2h-code-side-line { + background-color: var(--bg-primary); + color: var(--text-primary); +} + +body.vscode-dark .d2h-code-linenumber, +body.vscode-dark .d2h-code-side-linenumber, +body.vscode-high-contrast .d2h-code-linenumber, +body.vscode-high-contrast .d2h-code-side-linenumber { + background-color: var(--bg-secondary); + color: var(--text-muted); + border-color: var(--border-color); +} + +body.vscode-dark .d2h-del .d2h-code-line-ctn, +body.vscode-dark .d2h-del .d2h-code-side-line, +body.vscode-high-contrast .d2h-del .d2h-code-line-ctn, +body.vscode-high-contrast .d2h-del .d2h-code-side-line { + background-color: #3d1d26; +} + +body.vscode-dark .d2h-ins .d2h-code-line-ctn, +body.vscode-dark .d2h-ins .d2h-code-side-line, +body.vscode-high-contrast .d2h-ins .d2h-code-line-ctn, +body.vscode-high-contrast .d2h-ins .d2h-code-side-line { + background-color: #1f3d2a; +} + +body.vscode-dark .d2h-emptyplaceholder, +body.vscode-dark .d2h-code-side-emptyplaceholder, +body.vscode-high-contrast .d2h-emptyplaceholder, +body.vscode-high-contrast .d2h-code-side-emptyplaceholder { + background-color: var(--bg-secondary); + border-color: var(--border-color); +} + +body.vscode-dark .d2h-file-list-wrapper, +body.vscode-high-contrast .d2h-file-list-wrapper { + background-color: var(--bg-secondary); + border-color: var(--border-color); +} + +body.vscode-dark .d2h-file-list-header, +body.vscode-high-contrast .d2h-file-list-header { + background-color: var(--bg-secondary); +} + +body.vscode-dark .d2h-file-list-title, +body.vscode-high-contrast .d2h-file-list-title { + color: var(--text-primary); +} + +body.vscode-dark .d2h-file-list li, +body.vscode-high-contrast .d2h-file-list li { + border-color: var(--border-color); +} + +/* Unified (line-by-line) view specific styles */ +body.vscode-dark .d2h-file-diff .d2h-del, +body.vscode-high-contrast .d2h-file-diff .d2h-del { + background-color: rgba(248, 81, 73, 0.25); +} + +body.vscode-dark .d2h-file-diff .d2h-ins, +body.vscode-high-contrast .d2h-file-diff .d2h-ins { + background-color: rgba(63, 185, 80, 0.25); +} + +body.vscode-dark .d2h-file-diff .d2h-del .d2h-code-line-ctn, +body.vscode-high-contrast .d2h-file-diff .d2h-del .d2h-code-line-ctn { + background-color: rgba(248, 81, 73, 0.25); +} + +body.vscode-dark .d2h-file-diff .d2h-ins .d2h-code-line-ctn, +body.vscode-high-contrast .d2h-file-diff .d2h-ins .d2h-code-line-ctn { + background-color: rgba(63, 185, 80, 0.25); +} + +body.vscode-light .d2h-file-diff .d2h-del { + background-color: #ffebe9; +} + +body.vscode-light .d2h-file-diff .d2h-ins { + background-color: #e6ffec; +} + +body.vscode-light .d2h-file-diff .d2h-del .d2h-code-line-ctn { + background-color: #ffebe9; +} + +body.vscode-light .d2h-file-diff .d2h-ins .d2h-code-line-ctn { + background-color: #e6ffec; +} + +/* Deletion marker color */ +body.vscode-dark .d2h-del .d2h-code-line-prefix, +body.vscode-high-contrast .d2h-del .d2h-code-line-prefix { + color: #f85149; +} + +/* Insertion marker color */ +body.vscode-dark .d2h-ins .d2h-code-line-prefix, +body.vscode-high-contrast .d2h-ins .d2h-code-line-prefix { + color: #3fb950; +} + +body.vscode-light .d2h-del .d2h-code-line-prefix { + color: #cf222e; +} + +body.vscode-light .d2h-ins .d2h-code-line-prefix { + color: #1a7f37; +} + +/* Viewed checkbox styles */ +.d2h-viewed-checkbox { + display: flex; + align-items: center; + gap: 4px; + margin-left: auto; + padding-right: 8px; + cursor: pointer; + font-size: 12px; + color: var(--text-muted); +} + +.d2h-viewed-checkbox input[type="checkbox"] { + cursor: pointer; +} + +.d2h-file-wrapper.viewed .d2h-file-diff, +.d2h-file-wrapper.viewed .d2h-files-diff { + display: none; +} + +.d2h-file-name-wrapper { + display: flex; + align-items: center; + width: 100%; +} + +/* Fix: Make line numbers sticky on the left when scrolling horizontally */ +/* Fix: Remove the gap between line numbers and content */ +.d2h-code-linenumber, +.d2h-code-side-linenumber { + position: sticky; + left: 0; + z-index: 1; + border-right: none; + padding-right: 0; + padding-left: 0; +} + +/* Fix: Adjust content padding to match line number width exactly */ +.d2h-code-linenumber { + width: 5em; +} + +.d2h-code-side-linenumber { + width: 2.5em; +} + +.d2h-code-line { + padding-left: 5em; + padding-right: 0; + width: calc(100% - 5em); +} + +.d2h-code-side-line { + padding-left: 2.5em; + padding-right: 0; + width: calc(100% - 2.5em); +} + +/* Fix: Remove padding from code line prefix to eliminate gap */ +.d2h-code-line-prefix { + padding: 0; + margin: 0; +} + +/* Fix: Ensure header tabs appear above sticky line numbers */ +.header { + position: relative; + z-index: 10; +} diff --git a/media/patchViewer.js b/media/patchViewer.js new file mode 100644 index 0000000..a5e140d --- /dev/null +++ b/media/patchViewer.js @@ -0,0 +1,381 @@ +// Patch Reader webview client script. +// +// Loaded into the custom editor webview via webview.asWebviewUri(). Keeping this +// as a standalone static file (rather than inlining it in a TypeScript template +// literal) means the browser receives it verbatim, so escape sequences such as +// "\n" and "" can never be mangled by the surrounding literal. The +// diff2html library is loaded before this script and exposes a global Diff2Html. +// +// The initial patch content is read from a non-executable JSON data block +// (#patch-initial-content) that the extension injects into the page. +(function() { + const vscode = acquireVsCodeApi(); + + // Logging helpers — forward messages to the VS Code output channel + function logInfo(message) { + vscode.postMessage({ type: 'log', message: message }); + } + function logWarn(message) { + vscode.postMessage({ type: 'warn', message: message }); + } + function logError(message, error) { + vscode.postMessage({ type: 'error', message: message, error: error instanceof Error ? error.message : String(error || '') }); + } + + // Read the initial patch content that the extension embedded in a + // non-executable JSON data block. Using a data block (instead of inlining + // the content into executable JS) means malformed or ""-containing + // patches can never break the script. + function getInitialContent() { + const dataEl = document.getElementById('patch-initial-content'); + if (!dataEl) { + return ''; + } + try { + return JSON.parse(dataEl.textContent || '""'); + } catch (error) { + logError('Failed to parse initial patch content', error); + return ''; + } + } + + // DOM elements + const diffOutput = document.getElementById('diff-output'); + const contentOutput = document.getElementById('content-output'); + const tabContents = document.querySelectorAll('.tab-content'); + const tabs = document.querySelectorAll('.tab'); + const viewBtns = document.querySelectorAll('.view-btn'); + + if (!diffOutput) { + logError('Failed to find #diff-output element in DOM'); + } + if (!contentOutput) { + logError('Failed to find #content-output element in DOM'); + } + + // State + let currentContent = getInitialContent(); + let currentViewMode = 'side-by-side'; + let renderDebounceTimer = null; + + // Debounce helper + function debounce(fn, delay) { + return function(...args) { + if (renderDebounceTimer) { + clearTimeout(renderDebounceTimer); + } + renderDebounceTimer = setTimeout(() => fn.apply(this, args), delay); + }; + } + + // Debounced render for content changes + const debouncedRenderDiff = debounce(renderDiff, 300); + + // Initialize + function init() { + logInfo('Initializing patch viewer'); + bindEvents(); + renderDiff(); + updateContentTab(); + } + + // Bind events + function bindEvents() { + // Tab switching + tabs.forEach(tab => { + tab.addEventListener('click', () => { + const targetTab = tab.dataset.tab; + switchTab(targetTab); + }); + }); + + // View mode switching + viewBtns.forEach(btn => { + btn.addEventListener('click', () => { + const viewMode = btn.dataset.view; + setViewMode(viewMode); + }); + }); + + // Content editing - send changes to VS Code + if (contentOutput) { + contentOutput.addEventListener('input', () => { + const newContent = contentOutput.value; + if (newContent !== currentContent) { + currentContent = newContent; + vscode.postMessage({ + type: 'contentChanged', + content: newContent + }); + debouncedRenderDiff(); + } + }); + } + + // Handle messages from extension + window.addEventListener('message', event => { + const trustedOrigin = window.location.origin; + if (event.origin !== trustedOrigin) { + logWarn('Ignored message from untrusted origin: ' + event.origin); + return; + } + + const message = event.data; + switch (message.type) { + case 'update': + logInfo('Document updated, re-rendering diff'); + currentContent = message.content; + renderDiff(); + updateContentTab(); + break; + case 'themeChanged': + logInfo('Theme changed to kind: ' + message.kind); + applyTheme(message.kind); + renderDiff(); + break; + case 'setViewMode': + logInfo('View mode set to: ' + message.viewMode); + setViewMode(message.viewMode, false); + break; + } + }); + } + + // Set view mode + function setViewMode(viewMode, notify = true) { + currentViewMode = viewMode; + viewBtns.forEach(btn => { + const isActive = btn.dataset.view === viewMode; + btn.classList.toggle('active', isActive); + btn.setAttribute('aria-pressed', isActive ? 'true' : 'false'); + }); + renderDiff(); + + if (notify) { + vscode.postMessage({ + type: 'viewModeChanged', + viewMode: viewMode + }); + } + } + + // Switch between tabs + function switchTab(targetTab) { + // Update tab buttons + tabs.forEach(tab => { + const isActive = tab.dataset.tab === targetTab; + tab.classList.toggle('active', isActive); + tab.setAttribute('aria-selected', isActive ? 'true' : 'false'); + }); + + // Update tab content visibility + tabContents.forEach(content => { + const isActive = content.id === targetTab + '-tab'; + content.classList.toggle('active', isActive); + }); + } + + // Update content tab with raw content + function updateContentTab() { + if (contentOutput) { + contentOutput.value = currentContent || ''; + } + } + + // Apply VS Code theme + function applyTheme(themeKind) { + // Remove existing theme classes + document.body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast'); + + // Add appropriate class based on theme kind + // ThemeKind: 1 = Light, 2 = Dark, 3 = HighContrast (Dark), 4 = HighContrast (Light) + switch (themeKind) { + case 1: + document.body.classList.add('vscode-light'); + break; + case 2: + document.body.classList.add('vscode-dark'); + break; + case 3: + document.body.classList.add('vscode-high-contrast'); + break; + case 4: + document.body.classList.add('vscode-light'); + break; + } + } + + // Setup synchronized horizontal scrolling for side-by-side view + function setupSynchronizedScroll() { + // Find all file wrappers with side-by-side diff + const fileWrappers = diffOutput.querySelectorAll('.d2h-file-wrapper'); + + fileWrappers.forEach(wrapper => { + const sidePanels = wrapper.querySelectorAll('.d2h-file-side-diff'); + if (sidePanels.length !== 2) return; + + const leftPanel = sidePanels[0]; + const rightPanel = sidePanels[1]; + let scrollSource = null; + + function syncScroll(source, target) { + if (scrollSource && scrollSource !== source) return; + scrollSource = source; + + requestAnimationFrame(() => { + target.scrollLeft = source.scrollLeft; + scrollSource = null; + }); + } + + leftPanel.addEventListener('scroll', function() { + syncScroll(this, rightPanel); + }); + + rightPanel.addEventListener('scroll', function() { + syncScroll(this, leftPanel); + }); + }); + } + + // Setup viewed checkbox functionality for each file + function setupViewedCheckboxes() { + const fileWrappers = diffOutput.querySelectorAll('.d2h-file-wrapper'); + fileWrappers.forEach((wrapper, index) => { + const header = wrapper.querySelector('.d2h-file-header'); + if (!header) return; + + // Check if checkbox already exists + if (header.querySelector('.d2h-viewed-checkbox')) return; + + // Get file name for accessibility + const fileNameEl = header.querySelector('.d2h-file-name'); + const fileName = fileNameEl ? fileNameEl.textContent : 'File ' + (index + 1); + const checkboxId = 'd2h-viewed-' + index; + + // Create viewed checkbox container + const checkboxContainer = document.createElement('label'); + checkboxContainer.className = 'd2h-viewed-checkbox'; + checkboxContainer.setAttribute('for', checkboxId); + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.id = checkboxId; + checkbox.setAttribute('aria-label', 'Mark ' + fileName + ' as viewed'); + checkbox.addEventListener('change', function() { + wrapper.classList.toggle('viewed', this.checked); + }); + + const labelText = document.createElement('span'); + labelText.textContent = 'Viewed'; + + checkboxContainer.appendChild(checkbox); + checkboxContainer.appendChild(labelText); + + // Insert checkbox into header + const fileNameWrapper = header.querySelector('.d2h-file-name-wrapper'); + if (fileNameWrapper) { + fileNameWrapper.appendChild(checkboxContainer); + } + }); + } + + // Strip git format-patch footer (a trailing "-- " line followed by a git version such as 2.43.0). + // This footer is added by git format-patch and should not be parsed as diff content. + function stripGitPatchFooter(content) { + if (!content) return content; + // Match the git email signature footer: a "-- " line on its own, + // immediately followed by a git version number (such as 2.43.0). + return content.replace(/\n-- \n\d+\.\d+[^\n]*\n?$/, '\n'); + } + + // Render diff using diff2html + function renderDiff() { + if (!diffOutput) { + logError('Cannot render diff: #diff-output element not found'); + return; + } + if (!currentContent || !currentContent.trim()) { + logInfo('No diff content to display'); + diffOutput.innerHTML = '
No diff content to display
'; + return; + } + + // Check if Diff2Html is available + const Diff2HtmlLib = typeof Diff2Html !== 'undefined' ? Diff2Html : window.Diff2Html; + if (!Diff2HtmlLib) { + const errorMsg = 'Diff2Html library is not loaded. Please reload the editor.'; + logError('Failed to render diff', 'Diff2Html is not defined — check that the extension assets are correctly installed'); + diffOutput.innerHTML = '
' + errorMsg + '
'; + return; + } + + try { + const outputFormat = currentViewMode === 'side-by-side' ? 'side-by-side' : 'line-by-line'; + + // Strip git format-patch footer before parsing + const contentToParse = stripGitPatchFooter(currentContent); + + // Parse the diff content + const diffJson = Diff2HtmlLib.parse(contentToParse, { + inputFormat: 'diff' + }); + + // Check if parsing produced any results + if (!diffJson || diffJson.length === 0) { + const errorMsg = 'Unable to parse diff content. Please check if the content is a valid diff/patch format.'; + logWarn('Diff2Html.parse returned an empty result for the provided content'); + diffOutput.innerHTML = '
' + errorMsg + '
'; + return; + } + + logInfo('Rendering diff: ' + diffJson.length + ' file(s), format=' + outputFormat); + + // Generate HTML from parsed diff + const html = Diff2HtmlLib.html(diffJson, { + inputFormat: 'json', + outputFormat: outputFormat, + showFiles: true, + matching: 'lines', + matchWordsThreshold: 0.25, + maxLineLengthHighlight: 10000, + renderNothingWhenEmpty: false, + fileListToggle: true, + fileListStartVisible: true, + fileContentToggle: true, + stickyFileHeaders: true + }); + + // Check if HTML output is empty + if (!html || html.trim() === '') { + logWarn('Diff2Html.html returned empty output for ' + diffJson.length + ' parsed file(s)'); + diffOutput.innerHTML = '
Failed to generate diff view. The content could not be rendered.
'; + return; + } + + diffOutput.innerHTML = html; + + // Setup synchronized scrolling for side-by-side view + if (currentViewMode === 'side-by-side') { + setupSynchronizedScroll(); + } + + // Add viewed checkbox functionality + setupViewedCheckboxes(); + } catch (error) { + const errorMessage = 'An error occurred while rendering the diff. Please check if the content is a valid diff/patch format.'; + logError('Failed to render diff', error); + diffOutput.innerHTML = '
' + errorMessage + '
'; + } + } + + // Initialize + try { + init(); + } catch (error) { + logError('Fatal error during initialization', error); + if (diffOutput) { + diffOutput.innerHTML = '
Failed to initialize the viewer. Please close and reopen the file.
'; + } + } +})(); diff --git a/package-lock.json b/package-lock.json index 0ded1fa..bfe61c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@babel/eslint-parser": "^8.0.1", "@babel/plugin-syntax-typescript": "^8.0.3", "@eslint/js": "^10.0.1", + "@types/jsdom": "^27.0.0", "@types/mocha": "^10.0.10", "@types/node": "26.x", "@types/vscode": "^1.125.0", @@ -23,12 +24,66 @@ "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", "eslint": "^10.8.0", + "jsdom": "^30.0.1", "typescript": "^7.0.2" }, "engines": { "vscode": "^1.125.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@azu/format-text": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", @@ -595,6 +650,159 @@ "node": ">=18" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -723,6 +931,24 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1258,6 +1484,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsdom": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-27.0.0.tgz", + "integrity": "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/jsesc": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", @@ -1303,6 +1541,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/vscode": { "version": "1.125.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", @@ -2079,6 +2324,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binaryextensions": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", @@ -2704,6 +2959,20 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-what": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", @@ -2717,6 +2986,45 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2748,6 +3056,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -3807,6 +4122,19 @@ "node": ">=10" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4087,6 +4415,13 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -4246,6 +4581,103 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/undici": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4612,6 +5044,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", @@ -5940,6 +6379,19 @@ "node": ">=11.0.0" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/secretlint": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", @@ -6184,6 +6636,16 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -6433,6 +6895,13 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/table": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", @@ -6590,6 +7059,26 @@ "url": "https://bevry.me/fund" } }, + "node_modules/tldts": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.10" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", @@ -6613,6 +7102,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6865,6 +7380,29 @@ "url": "https://bevry.me/fund" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -6889,6 +7427,21 @@ "node": ">=18" } }, + "node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7044,6 +7597,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -7068,6 +7631,13 @@ "node": ">=4.0" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index b460b58..3ff636e 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "compile": "tsc -p ./", "copy-assets": "node scripts/copy-assets.js", "watch": "tsc -watch -p ./", - "pretest": "npm run compile && npm run lint", + "pretest": "npm run compile && npm run copy-assets && npm run lint", "lint": "eslint src", "test": "vscode-test --config ./.vscode-test.json" }, @@ -86,6 +86,7 @@ "@babel/eslint-parser": "^8.0.1", "@babel/plugin-syntax-typescript": "^8.0.3", "@eslint/js": "^10.0.1", + "@types/jsdom": "^27.0.0", "@types/mocha": "^10.0.10", "@types/node": "26.x", "@types/vscode": "^1.125.0", @@ -93,6 +94,7 @@ "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", "eslint": "^10.8.0", + "jsdom": "^30.0.1", "typescript": "^7.0.2" }, "dependencies": { diff --git a/src/patchEditorProvider.ts b/src/patchEditorProvider.ts index f4cecf4..37f4b46 100644 --- a/src/patchEditorProvider.ts +++ b/src/patchEditorProvider.ts @@ -55,6 +55,24 @@ export class PatchEditorProvider implements vscode.CustomTextEditorProvider { } } + /** + * Log info message to output channel + */ + private logInfo(message: string): void { + const timestamp = new Date().toISOString(); + PatchEditorProvider.outputChannel?.appendLine(`[${timestamp}] INFO: ${message}`); + } + + /** + * Log warning message to output channel + */ + private logWarn(message: string, detail?: unknown): void { + const timestamp = new Date().toISOString(); + const detailMessage = detail instanceof Error ? detail.message : (detail ? String(detail) : ''); + const logMessage = detailMessage ? `[${timestamp}] WARN: ${message} - ${detailMessage}` : `[${timestamp}] WARN: ${message}`; + PatchEditorProvider.outputChannel?.appendLine(logMessage); + } + /** * Log error message to output channel */ @@ -90,18 +108,23 @@ export class PatchEditorProvider implements vscode.CustomTextEditorProvider { _token: vscode.CancellationToken ): Promise { this.activeWebviewPanel = webviewPanel; + this.logInfo(`Opening patch editor for: ${document.uri.fsPath}`); // Setup webview options const diff2htmlAssetDirectory = resolveDiff2HtmlAssetDirectory(this.context.extensionUri.fsPath); if (!hasDiff2HtmlAssets(diff2htmlAssetDirectory)) { this.logError(`Diff2Html assets were not found in ${diff2htmlAssetDirectory}`); + } else { + this.logInfo(`Using diff2html assets from: ${diff2htmlAssetDirectory}`); } const diff2htmlAssetRoot = vscode.Uri.file(diff2htmlAssetDirectory); + const mediaRoot = vscode.Uri.joinPath(this.context.extensionUri, 'media'); webviewPanel.webview.options = { enableScripts: true, localResourceRoots: [ + mediaRoot, diff2htmlAssetRoot ] }; @@ -114,12 +137,23 @@ export class PatchEditorProvider implements vscode.CustomTextEditorProvider { vscode.Uri.joinPath(diff2htmlAssetRoot, 'js', 'diff2html.min.js') ); + // Get URIs for the extension's own webview assets (kept as standalone + // static files so the browser receives them verbatim). + const patchViewerCssUri = webviewPanel.webview.asWebviewUri( + vscode.Uri.joinPath(mediaRoot, 'patchViewer.css') + ); + const patchViewerJsUri = webviewPanel.webview.asWebviewUri( + vscode.Uri.joinPath(mediaRoot, 'patchViewer.js') + ); + // Set initial HTML content webviewPanel.webview.html = this.getHtmlForWebview( webviewPanel.webview, document.getText(), diff2htmlCssUri, - diff2htmlJsUri + diff2htmlJsUri, + patchViewerCssUri, + patchViewerJsUri ); // Handle messages from the webview @@ -127,19 +161,30 @@ export class PatchEditorProvider implements vscode.CustomTextEditorProvider { switch (message.type) { case 'viewModeChanged': this.currentViewMode = message.viewMode; + this.logInfo(`View mode changed to: ${message.viewMode}`); + break; + case 'log': + this.logInfo(`[Webview] ${message.message}`); + break; + case 'warn': + this.logWarn(`[Webview] ${message.message}`); break; case 'error': - this.logError(message.message, message.error); + this.logError(`[Webview] ${message.message}`, message.error); break; case 'contentChanged': { // Apply edits from the webview to the document - const edit = new vscode.WorkspaceEdit(); - const fullRange = new vscode.Range( - document.positionAt(0), - document.positionAt(document.getText().length) - ); - edit.replace(document.uri, fullRange, message.content); - await vscode.workspace.applyEdit(edit); + try { + const edit = new vscode.WorkspaceEdit(); + const fullRange = new vscode.Range( + document.positionAt(0), + document.positionAt(document.getText().length) + ); + edit.replace(document.uri, fullRange, message.content); + await vscode.workspace.applyEdit(edit); + } catch (err) { + this.logError('Failed to apply content edit to document', err); + } break; } } @@ -192,10 +237,20 @@ export class PatchEditorProvider implements vscode.CustomTextEditorProvider { webview: vscode.Webview, content: string, diff2htmlCssUri: vscode.Uri, - diff2htmlJsUri: vscode.Uri + diff2htmlJsUri: vscode.Uri, + patchViewerCssUri: vscode.Uri, + patchViewerJsUri: vscode.Uri ): string { const nonce = getNonce(); + // Embed the initial patch content in a non-executable JSON data block. + // A `type="application/json"` block is never executed, so the CSP + // `script-src` directive does not apply to it and it needs no nonce. + // Escaping every "<" as "\u003c" keeps the JSON valid while guaranteeing + // the raw text can never contain "", so the HTML parser cannot + // close the data block early — regardless of what the patch contains. + const initialContentJson = JSON.stringify(content).replace(/ @@ -204,464 +259,8 @@ export class PatchEditorProvider implements vscode.CustomTextEditorProvider { + Patch Reader -
@@ -685,344 +284,9 @@ export class PatchEditorProvider implements vscode.CustomTextEditorProvider {
+ - + `; diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 46a3b41..5cd4861 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -3,7 +3,7 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import * as vscode from 'vscode'; -import { resolveDiff2HtmlAssetDirectory } from '../patchEditorProvider'; +import { PatchEditorProvider, resolveDiff2HtmlAssetDirectory } from '../patchEditorProvider'; suite('Extension Test Suite', () => { vscode.window.showInformationMessage('Start all tests.'); @@ -72,4 +72,186 @@ suite('Extension Test Suite', () => { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); + + test('Webview client script file should be valid JavaScript', () => { + // The webview client script is now a standalone static file loaded via + // asWebviewUri(), so it is delivered to the browser verbatim. Validate + // that the shipped file parses as JavaScript. + const extension = vscode.extensions.getExtension('unknowIfGuestInDream.tlcsdm-patch-reader'); + assert.ok(extension, 'Extension should be present'); + const scriptPath = path.join(extension.extensionPath, 'media', 'patchViewer.js'); + assert.ok(fs.existsSync(scriptPath), 'media/patchViewer.js should exist'); + const scriptBody = fs.readFileSync(scriptPath, 'utf8'); + + // new Function only parses (does not execute) the body, so browser globals + // are not required. A malformed script would throw a SyntaxError here. + assert.doesNotThrow( + () => new Function(scriptBody), + 'Webview client script must be syntactically valid JavaScript' + ); + }); + + test('Webview client script and diff2html should render a diff end-to-end', async () => { + // End-to-end validation of the diff2html usage: load the shipped + // diff2html bundle and the webview client script into a DOM and assert + // that a patch is actually rendered into #diff-output. This guards + // against the "blank Visual tab" class of regressions. + const { JSDOM } = await import('jsdom'); + + const extension = vscode.extensions.getExtension('unknowIfGuestInDream.tlcsdm-patch-reader'); + assert.ok(extension, 'Extension should be present'); + const assetDir = resolveDiff2HtmlAssetDirectory(extension.extensionPath); + const diff2htmlJsPath = path.join(assetDir, 'js', 'diff2html.min.js'); + assert.ok(fs.existsSync(diff2htmlJsPath), 'diff2html.min.js should be available for the webview'); + const diff2htmlJs = fs.readFileSync(diff2htmlJsPath, 'utf8'); + const patchViewerJs = fs.readFileSync( + path.join(extension.extensionPath, 'media', 'patchViewer.js'), + 'utf8' + ); + + const patch = [ + 'diff --git a/file.txt b/file.txt', + 'index 0000000..1111111 100644', + '--- a/file.txt', + '+++ b/file.txt', + '@@ -1 +1 @@', + '-old line', + '+new line', + '' + ].join('\n'); + const initialContentJson = JSON.stringify(patch).replace(/ +
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+ +`; + + const dom = new JSDOM(html, { runScripts: 'dangerously', pretendToBeVisual: true }); + const { window } = dom; + try { + (window as any).acquireVsCodeApi = () => ({ + postMessage: () => { /* no-op */ } + }); + + const bundleScript = window.document.createElement('script'); + bundleScript.textContent = diff2htmlJs; + window.document.body.appendChild(bundleScript); + assert.ok( + (window as any).Diff2Html, + 'diff2html bundle should expose a global Diff2Html' + ); + + const viewerScript = window.document.createElement('script'); + viewerScript.textContent = patchViewerJs; + window.document.body.appendChild(viewerScript); + + const diffOutput = window.document.getElementById('diff-output'); + assert.ok(diffOutput, '#diff-output should exist'); + assert.ok( + diffOutput!.querySelector('.d2h-file-wrapper'), + 'diff2html should render at least one file into #diff-output' + ); + + // Content tab must be switchable: clicking the Content tab activates it. + const contentTabBtn = window.document.querySelector('.tab[data-tab="content"]') as any; + assert.ok(contentTabBtn, 'Content tab button should exist'); + contentTabBtn!.click(); + assert.ok( + window.document.getElementById('content-tab')!.classList.contains('active'), + 'Clicking the Content tab should activate the content panel' + ); + assert.strictEqual( + (window.document.getElementById('content-output') as any).value, + patch, + 'Content tab should display the raw patch text' + ); + } finally { + window.close(); + } + }); + + test('Initial patch content should be embedded safely and round-trip', () => { + const provider = new PatchEditorProvider({ + extensionUri: vscode.Uri.file('/ext'), + subscriptions: [] + } as any); + const panel = vscode.window.createWebviewPanel( + 'tlcsdm.patchReader.test', + 'Patch Reader Test', + vscode.ViewColumn.One, + { enableScripts: true } + ); + try { + const cssUri = panel.webview.asWebviewUri(vscode.Uri.file('/assets/diff2html.min.css')); + const jsUri = panel.webview.asWebviewUri(vscode.Uri.file('/assets/diff2html.min.js')); + const viewerCssUri = panel.webview.asWebviewUri(vscode.Uri.file('/media/patchViewer.css')); + const viewerJsUri = panel.webview.asWebviewUri(vscode.Uri.file('/media/patchViewer.js')); + + // Content that would break naive inline embedding: a diffed ', + '+', + '-- ', + '2.43.0', + '' + ].join('\n'); + + const html = (provider as any).getHtmlForWebview( + panel.webview, + trickyContent, + cssUri, + jsUri, + viewerCssUri, + viewerJsUri + ); + + // The client script must be referenced as an external file, not inlined. + assert.ok( + /]*src="[^"]*patchViewer\.js"[^>]*><\/script>/.test(html), + 'Webview HTML should load the external patchViewer.js script' + ); + + // Because getHtmlForWebview escapes every "<" as "\u003c", the patch's + // own "" becomes "\u003c/script>" in the data block. The lazy + // regex terminator below is therefore only matched by the block's real + // closing tag, and the data block itself contains no raw "". + const dataMatch = html.match( + / sequence' + ); + + // The data block must be valid JSON that round-trips to the original + // content exactly, proving the escaping preserves the patch verbatim. + assert.strictEqual( + JSON.parse(dataBlock), + trickyContent, + 'Embedded content must round-trip back to the original patch text' + ); + } finally { + panel.dispose(); + } + }); });