@automattic/jetpack-components 0.57.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ### This is a list detailing changes for the Jetpack RNA Components package releases.
4
4
 
5
+ ## [0.58.0] - 2024-10-15
6
+ ### Added
7
+ - Add DiffViewer component [#39672]
8
+ - Add ThreatSeverityBadge component [#39758]
9
+
5
10
  ## [0.57.0] - 2024-10-14
6
11
  ### Added
7
12
  - Add JetpackProtectLogo component. [#39703]
@@ -1190,6 +1195,7 @@
1190
1195
  ### Changed
1191
1196
  - Update node version requirement to 14.16.1
1192
1197
 
1198
+ [0.58.0]: https://github.com/Automattic/jetpack-components/compare/0.57.0...0.58.0
1193
1199
  [0.57.0]: https://github.com/Automattic/jetpack-components/compare/0.56.3...0.57.0
1194
1200
  [0.56.3]: https://github.com/Automattic/jetpack-components/compare/0.56.2...0.56.3
1195
1201
  [0.56.2]: https://github.com/Automattic/jetpack-components/compare/0.56.1...0.56.2
@@ -0,0 +1,49 @@
1
+ # Unified Diff Viewer
2
+
3
+ Originally forked over from [Calypso](https://github.com/Automattic/wp-calypso/tree/b7a4a07/client/components/diff-viewer).
4
+
5
+ This component renders the output of a unified diff (`git diff` or `diff -u`) in a
6
+ visual format recognizable by someone who works with `diff` and comparing files.
7
+
8
+ ## Usage
9
+
10
+ ```jsx
11
+ import DiffViewer from 'components/diff-viewer';
12
+ export const CommitView = ( { commitHash, description, diff } ) => (
13
+ <div>
14
+ <div>
15
+ <a href="https://wordpress.com">{ commitHash }</a>
16
+ </div>
17
+ <p>{ description }</p>
18
+ <DiffViewer diff={ diff } />
19
+ </div>
20
+ );
21
+ ```
22
+
23
+ ### Props
24
+
25
+ | Name | Type | Default | Description |
26
+ | -------- | -------- | ------- | ------------------------------ |
27
+ | `diff`\* | `string` | `''` | Actual text output of the diff |
28
+
29
+ ### Additional usage information
30
+
31
+ The diff output should be the full text produced by the diff command (including newlines).
32
+ Internally this component parses the output (the patch) and produces the data structure
33
+ used to display files, hunks (sections of change in the files), and the actual lines of
34
+ change and context.
35
+
36
+ ```
37
+ diff --git a/circle.yml b/circle.yml
38
+ index 51455bdb14..bc0622d001 100644
39
+ --- a/circle.yml
40
+ +++ b/circle.yml
41
+ @@ -1,6 +1,6 @@
42
+ machine:
43
+ node:
44
+ - version: 8.9.4
45
+ + version: 8.11.0
46
+ test:
47
+ pre:
48
+ - ? |
49
+ ```
@@ -0,0 +1,97 @@
1
+ import { Fragment } from 'react';
2
+ import parseFilename from './parse-filename';
3
+ import parsePatch from './parse-patch';
4
+ import styles from './styles.module.scss';
5
+
6
+ const filename = ( {
7
+ oldFileName,
8
+ newFileName,
9
+ }: {
10
+ oldFileName: string;
11
+ newFileName: string;
12
+ } ): JSX.Element => {
13
+ const { prev, next } = parseFilename( oldFileName, newFileName );
14
+
15
+ if ( prev.prefix + prev.path === next.prefix + next.path ) {
16
+ return (
17
+ <Fragment>
18
+ { prev.prefix && (
19
+ <span className={ styles[ 'diff-viewer__path-prefix' ] }>{ prev.prefix }</span>
20
+ ) }
21
+ <span className={ styles[ 'diff-viewer__path' ] }>{ prev.path }</span>
22
+ </Fragment>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <Fragment>
28
+ { !! prev.prefix && (
29
+ <span className={ styles[ 'diff-viewer__path-prefix' ] }>{ prev.prefix }</span>
30
+ ) }
31
+ <span className={ styles[ 'diff-viewer__path' ] }>{ prev.path }</span>
32
+ { ' → ' }
33
+ { !! next.prefix && (
34
+ <span className={ styles[ 'diff-viewer__path-prefix' ] }>{ next.prefix }</span>
35
+ ) }
36
+ <span className={ styles[ 'diff-viewer__path' ] }>{ next.path }</span>
37
+ </Fragment>
38
+ );
39
+ };
40
+
41
+ export const DiffViewer = ( { diff } ) => (
42
+ <div className={ styles[ 'diff-viewer' ] }>
43
+ { parsePatch( diff ).map( ( file, fileIndex ) => (
44
+ <Fragment key={ fileIndex }>
45
+ <div key={ `file-${ fileIndex }` } className={ styles[ 'diff-viewer__filename' ] }>
46
+ { filename( file ) }
47
+ </div>
48
+ <div key={ `diff-${ fileIndex }` } className={ styles[ 'diff-viewer__file' ] }>
49
+ <div key="left-numbers" className={ styles[ 'diff-viewer__line-numbers' ] }>
50
+ { file.hunks.map( ( hunk, hunkIndex ) => {
51
+ let lineOffset = 0;
52
+ return hunk.lines.map( ( line, index ) => (
53
+ <div key={ `${ hunkIndex }-${ index }` }>
54
+ { line[ 0 ] === '+' ? '\u00a0' : hunk.oldStart + lineOffset++ }
55
+ </div>
56
+ ) );
57
+ } ) }
58
+ </div>
59
+ <div key="right-numbers" className={ styles[ 'diff-viewer__line-numbers' ] }>
60
+ { file.hunks.map( ( hunk, hunkIndex ) => {
61
+ let lineOffset = 0;
62
+ return hunk.lines.map( ( line, index ) => (
63
+ <div key={ `${ hunkIndex }-${ index }` }>
64
+ { line[ 0 ] === '-' ? '\u00a0' : hunk.newStart + lineOffset++ }
65
+ </div>
66
+ ) );
67
+ } ) }
68
+ </div>
69
+ <div className={ styles[ 'diff-viewer__lines' ] }>
70
+ { file.hunks.map( ( hunk, hunkIndex ) =>
71
+ hunk.lines.map( ( line, index ) => {
72
+ const output = line.slice( 1 ).replace( /^\s*$/, '\u00a0' );
73
+ const key = `${ hunkIndex }-${ index }`;
74
+
75
+ switch ( line[ 0 ] ) {
76
+ case ' ':
77
+ return <div key={ key }>{ output }</div>;
78
+
79
+ case '-':
80
+ return <del key={ key }>{ output }</del>;
81
+
82
+ case '+':
83
+ return <ins key={ key }>{ output }</ins>;
84
+
85
+ default:
86
+ return undefined;
87
+ }
88
+ } )
89
+ ) }
90
+ </div>
91
+ </div>
92
+ </Fragment>
93
+ ) ) }
94
+ </div>
95
+ );
96
+
97
+ export default DiffViewer;
@@ -0,0 +1,75 @@
1
+ type ParsedFilename = {
2
+ prefix: string;
3
+ path: string;
4
+ };
5
+
6
+ const decompose = ( path: string ): ParsedFilename => {
7
+ const lastSlash = path.lastIndexOf( '/' );
8
+
9
+ return lastSlash > -1
10
+ ? { prefix: path.slice( 0, lastSlash ), path: path.slice( lastSlash ) }
11
+ : { prefix: '', path };
12
+ };
13
+
14
+ /**
15
+ * Parse the filename from a diff
16
+ *
17
+ * Uses a heuristic to return proper file name indicators
18
+ *
19
+ * It searches for the longest shared prefix and returns
20
+ * whatever remains after that. If the paths are identical
21
+ * it only returns a single filename as we have detected
22
+ * that the diff compares changes to only one file.
23
+ *
24
+ * An exception is made for `a/` and `b/` prefixes often
25
+ * added by `git` and other utilities to separate the left
26
+ * from the right when looking at the contents of a single
27
+ * file over time.
28
+ *
29
+ * @param {string} prev - filename of left contents
30
+ * @param {string} next - filename of right contents
31
+ *
32
+ * @return {object} - parsed filename
33
+ */
34
+ export default function (
35
+ prev: string,
36
+ next: string
37
+ ): { prev: ParsedFilename; next: ParsedFilename } {
38
+ // Remove 'a/' and 'b/' prefixes if present
39
+ const isLikelyPrefixed = prev.startsWith( 'a/' ) && next.startsWith( 'b/' );
40
+ prev = isLikelyPrefixed ? prev.slice( 2 ) : prev;
41
+ next = isLikelyPrefixed ? next.slice( 2 ) : next;
42
+
43
+ if ( prev === next ) {
44
+ // Paths are identical
45
+ const { prefix, path } = decompose( prev );
46
+ return { prev: { prefix, path }, next: { prefix, path } };
47
+ }
48
+
49
+ // Find longest shared base path ending with a slash
50
+ const length = Math.max( prev.length, next.length );
51
+ for ( let i = 0, slash = 0; i < length; i++ ) {
52
+ if ( prev[ i ] === '/' && next[ i ] === '/' ) {
53
+ slash = i;
54
+ }
55
+
56
+ if ( prev[ i ] !== next[ i ] ) {
57
+ return {
58
+ prev: {
59
+ prefix: prev.slice( 0, slash ),
60
+ path: prev.slice( slash ),
61
+ },
62
+ next: {
63
+ prefix: next.slice( 0, slash ),
64
+ path: next.slice( slash ),
65
+ },
66
+ };
67
+ }
68
+ }
69
+
70
+ // No shared base path
71
+ return {
72
+ prev: decompose( prev ),
73
+ next: decompose( next ),
74
+ };
75
+ }
@@ -0,0 +1,212 @@
1
+ type Hunk = {
2
+ oldStart: number;
3
+ oldLines: number;
4
+ newStart: number;
5
+ newLines: number;
6
+ lines: string[];
7
+ };
8
+
9
+ type Index = {
10
+ index?: string;
11
+ hunks?: Hunk[];
12
+ oldFileName?: string;
13
+ newFileName?: string;
14
+ oldHeader?: string;
15
+ newHeader?: string;
16
+ };
17
+
18
+ /**
19
+ * Parse Patch
20
+ *
21
+ * Adapted from https://github.com/kpdecker/jsdiff/blob/master/src/patch/parse.js
22
+ *
23
+ * @param {string} uniDiff - diff string
24
+ * @return {Array} - array of parsed files
25
+ */
26
+ export default function parsePatch( uniDiff: string ) {
27
+ const diffstr = uniDiff.split( /\n/ );
28
+ const list = [];
29
+ let i = 0;
30
+
31
+ /**
32
+ * Parse Index
33
+ */
34
+ function parseIndex() {
35
+ const index: Index = {};
36
+
37
+ list.push( index );
38
+
39
+ // Parse diff metadata
40
+ while ( i < diffstr.length ) {
41
+ const line = diffstr[ i ];
42
+
43
+ // File header found, end parsing diff metadata
44
+ if ( /^(---|\+\+\+|@@)\s/.test( line ) ) {
45
+ break;
46
+ }
47
+
48
+ // Diff index
49
+ const header = /^(?:Index:|diff(?: -r \w+)+)\s+(.+?)\s*$/.exec( line );
50
+
51
+ if ( header ) {
52
+ index.index = header[ 1 ];
53
+ }
54
+
55
+ i++;
56
+ }
57
+
58
+ // Parse file headers if they are defined. Unified diff requires them, but
59
+ // there's no technical issues to have an isolated hunk without file header
60
+ parseFileHeader( index );
61
+ parseFileHeader( index );
62
+
63
+ // Parse hunks
64
+ index.hunks = [];
65
+
66
+ while ( i < diffstr.length ) {
67
+ const _line = diffstr[ i ];
68
+
69
+ if (
70
+ /^(Index:\s|diff\s|---\s|\+\+\+\s|===================================================================)/.test(
71
+ _line
72
+ )
73
+ ) {
74
+ break;
75
+ } else if ( /^@@/.test( _line ) ) {
76
+ index.hunks.push( parseHunk() );
77
+ } else if ( _line ) {
78
+ throw new Error( 'Unknown line ' + ( i + 1 ) + ' ' + JSON.stringify( _line ) );
79
+ } else {
80
+ i++;
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Parse File Header
87
+ *
88
+ * Parses the --- and +++ headers, if none are found, no lines
89
+ * are consumed.
90
+ *
91
+ * @param {Array} index - array of parsed files
92
+ * @param {unknown} index.index - index
93
+ * @param {object[]} index.hunks - hunks
94
+ */
95
+ function parseFileHeader( index: Index ) {
96
+ const fileHeader = /^(---|\+\+\+)\s+(.*)\r?$/.exec( diffstr[ i ] );
97
+
98
+ if ( fileHeader ) {
99
+ const keyPrefix = fileHeader[ 1 ] === '---' ? 'old' : 'new';
100
+
101
+ const data = fileHeader[ 2 ].split( '\t', 2 );
102
+
103
+ let fileName = data[ 0 ].replace( /\\\\/g, '\\' );
104
+
105
+ if ( /^".*"$/.test( fileName ) ) {
106
+ fileName = fileName.substr( 1, fileName.length - 2 );
107
+ }
108
+
109
+ index[ keyPrefix + 'FileName' ] = fileName;
110
+
111
+ index[ keyPrefix + 'Header' ] = ( data[ 1 ] || '' ).trim();
112
+
113
+ i++;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Parse Hunk
119
+ * This assumes that we are at the start of a hunk.
120
+ *
121
+ * @return {object} - The parsed hunk.
122
+ */
123
+ function parseHunk(): Hunk {
124
+ const chunkHeaderIndex = i,
125
+ chunkHeaderLine = diffstr[ i++ ],
126
+ chunkHeader = chunkHeaderLine.split( /@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/ );
127
+
128
+ const hunk = {
129
+ oldStart: +chunkHeader[ 1 ],
130
+ oldLines: typeof chunkHeader[ 2 ] === 'undefined' ? 1 : +chunkHeader[ 2 ],
131
+ newStart: +chunkHeader[ 3 ],
132
+ newLines: typeof chunkHeader[ 4 ] === 'undefined' ? 1 : +chunkHeader[ 4 ],
133
+ lines: [],
134
+ };
135
+
136
+ // Unified Diff Format quirk: If the chunk size is 0,
137
+ // the first number is one lower than one would expect.
138
+ // https://www.artima.com/weblogs/viewpost.jsp?thread=164293
139
+
140
+ if ( hunk.oldLines === 0 ) {
141
+ hunk.oldStart += 1;
142
+ }
143
+
144
+ if ( hunk.newLines === 0 ) {
145
+ hunk.newStart += 1;
146
+ }
147
+
148
+ let addCount = 0,
149
+ removeCount = 0,
150
+ _diffstr$i;
151
+ for (
152
+ ;
153
+ i < diffstr.length &&
154
+ ( removeCount < hunk.oldLines ||
155
+ addCount < hunk.newLines ||
156
+ ( ( _diffstr$i = diffstr[ i ] ) !== null &&
157
+ _diffstr$i !== void 0 &&
158
+ _diffstr$i.startsWith( '\\' ) ) );
159
+ i++
160
+ ) {
161
+ const operation =
162
+ diffstr[ i ].length === 0 && i !== diffstr.length - 1 ? ' ' : diffstr[ i ][ 0 ];
163
+
164
+ if ( operation === '+' || operation === '-' || operation === ' ' || operation === '\\' ) {
165
+ hunk.lines.push( diffstr[ i ] );
166
+
167
+ if ( operation === '+' ) {
168
+ addCount++;
169
+ } else if ( operation === '-' ) {
170
+ removeCount++;
171
+ } else if ( operation === ' ' ) {
172
+ addCount++;
173
+ removeCount++;
174
+ }
175
+ } else {
176
+ throw new Error(
177
+ `Hunk at line ${ chunkHeaderIndex + 1 } contained invalid line ${ diffstr[ i ] }`
178
+ );
179
+ }
180
+ }
181
+
182
+ // Handle the empty block count case
183
+ if ( ! addCount && hunk.newLines === 1 ) {
184
+ hunk.newLines = 0;
185
+ }
186
+
187
+ if ( ! removeCount && hunk.oldLines === 1 ) {
188
+ hunk.oldLines = 0;
189
+ }
190
+
191
+ // Perform sanity checking
192
+ if ( addCount !== hunk.newLines ) {
193
+ throw new Error(
194
+ 'Added line count did not match for hunk at line ' + ( chunkHeaderIndex + 1 )
195
+ );
196
+ }
197
+
198
+ if ( removeCount !== hunk.oldLines ) {
199
+ throw new Error(
200
+ 'Removed line count did not match for hunk at line ' + ( chunkHeaderIndex + 1 )
201
+ );
202
+ }
203
+
204
+ return hunk;
205
+ }
206
+
207
+ while ( i < diffstr.length ) {
208
+ parseIndex();
209
+ }
210
+
211
+ return list;
212
+ }
@@ -0,0 +1,52 @@
1
+ .diff-viewer {
2
+ font-size: var( --font-body );
3
+ line-height: 1.5;
4
+ }
5
+
6
+ .diff-viewer__filename {
7
+ padding: calc( var( --spacing-base ) / 2 ) var( --spacing-base ); // 4px | 8px
8
+ background-color: var( --jp-gray-10 );
9
+ font-weight: 600;
10
+ }
11
+
12
+ .diff-viewer__file {
13
+ background-color: var( --jp-gray-0 );
14
+ display: flex;
15
+ font-family: "Courier 10 Pitch", Courier, monospace;
16
+ flex-direction: row;
17
+ overflow-x: scroll;
18
+ white-space: pre;
19
+ }
20
+
21
+ .diff-viewer__line-numbers {
22
+ padding: 0 var( --spacing-base ); // 0px | 8px
23
+ display: flex;
24
+ flex-direction: column;
25
+ text-align: right;
26
+ background-color: var( --jp-gray-10 );
27
+ color: var( --jp-gray-50 );
28
+ }
29
+
30
+ .diff-viewer__lines {
31
+ display: flex;
32
+ flex-direction: column;
33
+ flex-grow: 1;
34
+ overflow-x: visible;
35
+
36
+ & div,
37
+ & del,
38
+ & ins {
39
+ padding: 0 var( --spacing-base ); // 0px | 8px
40
+ text-decoration: none;
41
+ }
42
+
43
+ & del {
44
+ background-color: var( --jp-red-0 );
45
+ color: var( --jp-red-60 );
46
+ }
47
+
48
+ & ins {
49
+ background-color: var( --jp-green-5 );
50
+ color: var( --jp-green-60 );
51
+ }
52
+ }
@@ -0,0 +1,34 @@
1
+ import { _x } from '@wordpress/i18n';
2
+ import styles from './styles.module.scss';
3
+
4
+ const severityClassNames = severity => {
5
+ if ( severity >= 5 ) {
6
+ return 'is-critical';
7
+ } else if ( severity >= 3 && severity < 5 ) {
8
+ return 'is-high';
9
+ }
10
+ return 'is-low';
11
+ };
12
+
13
+ const severityText = severity => {
14
+ if ( severity >= 5 ) {
15
+ return _x( 'Critical', 'Severity label for issues rated 5 or higher.', 'jetpack' );
16
+ } else if ( severity >= 3 && severity < 5 ) {
17
+ return _x( 'High', 'Severity label for issues rated between 3 and 5.', 'jetpack' );
18
+ }
19
+ return _x( 'Low', 'Severity label for issues rated below 3.', 'jetpack' );
20
+ };
21
+
22
+ const ThreatSeverityBadge = ( { severity } ) => {
23
+ return (
24
+ <div
25
+ className={ `${ styles[ 'threat-severity-badge' ] } ${
26
+ styles[ severityClassNames( severity ) ]
27
+ }` }
28
+ >
29
+ { severityText( severity ) }
30
+ </div>
31
+ );
32
+ };
33
+
34
+ export default ThreatSeverityBadge;
@@ -0,0 +1,27 @@
1
+ .threat-severity-badge {
2
+ border-radius: 32px;
3
+ flex-shrink: 0;
4
+ font-size: 12px;
5
+ font-style: normal;
6
+ font-weight: 600;
7
+ line-height: 16px;
8
+ padding: calc( var( --spacing-base ) / 2 ); // 4px
9
+ position: relative;
10
+ text-align: center;
11
+ width: 60px;
12
+ }
13
+
14
+ .is-critical {
15
+ background: var( --jp-red-5 );
16
+ color: var( --jp-red-60 );
17
+ }
18
+
19
+ .is-high {
20
+ background: var( --jp-yellow-5 );
21
+ color: var( --jp-yellow-60 );
22
+ }
23
+
24
+ .is-low {
25
+ background: var( --jp-gray-0 );
26
+ color: var( --jp-gray-50 );
27
+ }
package/index.ts CHANGED
@@ -44,6 +44,7 @@ export { default as CopyToClipboard } from './components/copy-to-clipboard';
44
44
  export * from './components/icons';
45
45
  export { default as SplitButton } from './components/split-button';
46
46
  export { default as ThemeProvider } from './components/theme-provider';
47
+ export { default as ThreatSeverityBadge } from './components/threat-severity-badge';
47
48
  export { default as Text, H2, H3, Title } from './components/text';
48
49
  export { default as ToggleControl } from './components/toggle-control';
49
50
  export { default as numberFormat } from './components/number-format';
@@ -79,5 +80,6 @@ export { default as UpsellBanner } from './components/upsell-banner';
79
80
  export { getUserLocale, cleanLocale } from './lib/locale';
80
81
  export { default as RadioControl } from './components/radio-control';
81
82
  export { default as StatCard } from './components/stat-card';
83
+ export { default as DiffViewer } from './components/diff-viewer';
82
84
  export { default as MarkedLines } from './components/marked-lines';
83
85
  export * from './components/global-notices';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/jetpack-components",
3
- "version": "0.57.0",
3
+ "version": "0.58.0",
4
4
  "description": "Jetpack Components Package",
5
5
  "author": "Automattic",
6
6
  "license": "GPL-2.0-or-later",