Skip to content

Commit deb1c07

Browse files
committed
Adding syntax highlighting mode override mechanism for source code view component.
1 parent f866b1c commit deb1c07

10 files changed

Lines changed: 536 additions & 40 deletions

File tree

src/components/Solutions/SourceCodeBox/SourceCodeBox.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer';
66
import Prism from 'prismjs';
77

88
import Box from '../../widgets/Box';
9-
import SourceCodeViewer from '../../helpers/SourceCodeViewer';
9+
import SourceCodeViewer, { SourceCodeHighlightingSelector } from '../../helpers/SourceCodeViewer';
1010
import ResourceRenderer from '../../helpers/ResourceRenderer';
1111
import Icon, { CopyIcon, CopySuccessIcon, CodeCompareIcon, DownloadIcon, LoadingIcon, WarningIcon } from '../../icons';
1212

1313
import { getPrismModeFromExtension } from '../../helpers/syntaxHighlighting.js';
14-
import { getFileExtensionLC, simpleScalarMemoize } from '../../../helpers/common.js';
14+
import { getFileExtensionLC, simpleScalarMemoize, EMPTY_OBJ } from '../../../helpers/common.js';
1515

1616
const normalizeLineEndings = simpleScalarMemoize(content => content.replaceAll('\r', ''));
1717

@@ -45,10 +45,15 @@ const SourceCodeBox = ({
4545
reviewClosed = false,
4646
collapsable = false,
4747
isOpen = true,
48+
highlightOverrides = EMPTY_OBJ,
49+
setHighlightOverride = null,
4850
}) => {
4951
const res = fileContentsSelector(parentId, entryName);
5052
const [clipboardCopied, setClipboardCopied] = useState(false);
5153
const [onlyComments, setOnlyComments] = useState(false);
54+
55+
const fileExtension = getFileExtensionLC(name);
56+
5257
return (
5358
<ResourceRenderer
5459
key={id}
@@ -111,7 +116,7 @@ const SourceCodeBox = ({
111116
/>
112117
)}
113118

114-
<code>{name}</code>
119+
<code className="me-2">{name}</code>
115120

116121
{download && (
117122
<DownloadIcon
@@ -163,6 +168,17 @@ const SourceCodeBox = ({
163168
</>
164169
)}
165170

171+
{setHighlightOverride && (
172+
<SourceCodeHighlightingSelector
173+
id={`${id}-highlighting`}
174+
extension={fileExtension}
175+
initialMode={highlightOverrides[fileExtension]}
176+
onChange={setHighlightOverride}
177+
timid
178+
gapLeft={2}
179+
/>
180+
)}
181+
166182
{diffMode && (
167183
<>
168184
<CodeCompareIcon
@@ -273,7 +289,7 @@ const SourceCodeBox = ({
273289
oldValue={normalizeLineEndings(content.content)}
274290
newValue={normalizeLineEndings(secondContent.content)}
275291
splitView={true}
276-
renderContent={diffViewHighlightSyntax(getPrismModeFromExtension(getFileExtensionLC(name)))}
292+
renderContent={diffViewHighlightSyntax(getPrismModeFromExtension(fileExtension))}
277293
compareMethod={DiffMethod.WORDS_WITH_SPACE}
278294
/>
279295
</div>
@@ -291,6 +307,7 @@ const SourceCodeBox = ({
291307
removeComment={removeComment}
292308
reviewClosed={reviewClosed}
293309
onlyComments={reviewClosed && onlyComments}
310+
highlightOverrides={highlightOverrides}
294311
/>
295312
)}
296313
</Box>
@@ -319,6 +336,8 @@ SourceCodeBox.propTypes = {
319336
reviewClosed: PropTypes.bool,
320337
collapsable: PropTypes.bool,
321338
isOpen: PropTypes.bool,
339+
highlightOverrides: PropTypes.object,
340+
setHighlightOverride: PropTypes.func,
322341
};
323342

324343
export default SourceCodeBox;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import React, { useState, useRef } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { FormattedMessage } from 'react-intl';
4+
import { Overlay, Popover, FormSelect, ButtonGroup } from 'react-bootstrap';
5+
6+
import Button from '../../widgets/TheButton';
7+
import Icon, { CloseIcon, RefreshIcon, SaveIcon } from '../../icons';
8+
9+
import { getPrismModeFromExtension, PRISM_SUPPORTED_LANGUAGES } from '../../helpers/syntaxHighlighting.js';
10+
11+
const SourceCodeHighlightingSelector = ({
12+
id,
13+
fullButton = false,
14+
extension,
15+
initialMode = null,
16+
onChange,
17+
...props
18+
}) => {
19+
const defaultMode = getPrismModeFromExtension(extension);
20+
const target = useRef(null);
21+
const [visible, setVisible] = useState(false);
22+
const [selectedMode, setSelectedMode] = useState(initialMode !== null ? initialMode : defaultMode);
23+
24+
const clickHandler = ev => {
25+
ev.stopPropagation();
26+
setVisible(!visible);
27+
};
28+
29+
return (
30+
<>
31+
{fullButton ? (
32+
<Button variant="secondary" size="xs" {...props} onClick={clickHandler} ref={target}>
33+
<Icon icon="highlighter" gapRight={2} />
34+
{(initialMode !== null ? initialMode : defaultMode) || (
35+
<FormattedMessage id="app.solutionSourceCodes.noHighlighting" defaultMessage="no highlighting" />
36+
)}
37+
</Button>
38+
) : (
39+
<Icon icon="highlighter" {...props} onClick={clickHandler} ref={target} />
40+
)}
41+
42+
<Overlay target={target.current} show={visible} placement="bottom">
43+
{props => (
44+
<Popover id={id} onClick={e => e.stopPropagation()} className="highlighting-selector" {...props}>
45+
<Popover.Header>
46+
{extension ? (
47+
<>
48+
<FormattedMessage
49+
id="app.solutionSourceCodes.highlightingTitle"
50+
defaultMessage="Highlighting for files with extension"
51+
/>{' '}
52+
<code>*.{extension}</code>
53+
</>
54+
) : (
55+
<FormattedMessage
56+
id="app.solutionSourceCodes.highlightingTitleNoExtension"
57+
defaultMessage="Highlighting for files without extension"
58+
/>
59+
)}
60+
</Popover.Header>
61+
<Popover.Body className="text-center">
62+
<FormSelect onChange={e => setSelectedMode(e.target.value)} value={selectedMode}>
63+
<option value="" className={selectedMode === '' ? 'fw-bold text-primary' : 'fw-italic text-muted'}>
64+
[<FormattedMessage id="app.solutionSourceCodes.noHighlighting" defaultMessage="no highlighting" />]
65+
{defaultMode === '' && (
66+
<>
67+
{' ('}
68+
<FormattedMessage id="generic.default" defaultMessage="default" />
69+
{')'}
70+
</>
71+
)}
72+
</option>
73+
{PRISM_SUPPORTED_LANGUAGES.map(lang => (
74+
<option
75+
value={lang}
76+
key={lang}
77+
className={lang === defaultMode || lang === selectedMode ? 'fw-bold text-primary' : ''}>
78+
{lang}
79+
{lang === defaultMode && (
80+
<>
81+
{' ('}
82+
<FormattedMessage id="generic.default" defaultMessage="default" />
83+
{')'}
84+
</>
85+
)}
86+
</option>
87+
))}
88+
</FormSelect>
89+
90+
<ButtonGroup className="mt-3">
91+
<Button
92+
variant="success"
93+
size="sm"
94+
onClick={() => {
95+
onChange(extension, selectedMode !== defaultMode ? selectedMode : null);
96+
setVisible(false);
97+
}}>
98+
<SaveIcon gapRight={2} />
99+
<FormattedMessage id="generic.save" defaultMessage="Save" />
100+
</Button>
101+
{selectedMode !== defaultMode && (
102+
<Button
103+
variant="danger"
104+
size="sm"
105+
onClick={() => {
106+
setSelectedMode(defaultMode);
107+
onChange(extension, null);
108+
}}>
109+
<RefreshIcon gapRight={2} />
110+
<FormattedMessage id="generic.reset" defaultMessage="Reset" />
111+
</Button>
112+
)}
113+
<Button
114+
variant="secondary"
115+
size="sm"
116+
onClick={() => {
117+
setVisible(false);
118+
}}>
119+
<CloseIcon gapRight={2} />
120+
<FormattedMessage id="generic.close" defaultMessage="Close" />
121+
</Button>
122+
</ButtonGroup>
123+
</Popover.Body>
124+
</Popover>
125+
)}
126+
</Overlay>
127+
</>
128+
);
129+
};
130+
131+
SourceCodeHighlightingSelector.propTypes = {
132+
id: PropTypes.string.isRequired,
133+
fullButton: PropTypes.bool,
134+
extension: PropTypes.string.isRequired,
135+
initialMode: PropTypes.string,
136+
onChange: PropTypes.func.isRequired,
137+
};
138+
139+
export default SourceCodeHighlightingSelector;

src/components/helpers/SourceCodeViewer/SourceCodeViewer.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,7 @@ pre .sourceCodeViewerComments, code .sourceCodeViewerComments {
156156
.sourceCodeViewer.addComment .scvAddButton:hover::before {
157157
opacity: 1;
158158
}
159+
160+
.popover.highlighting-selector {
161+
--bs-popover-max-width: 400px;
162+
}

src/components/helpers/SourceCodeViewer/SourceCodeViewer.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import 'prismjs/themes/prism.css';
77

88
import ReviewCommentForm, { newCommentFormInitialValues } from '../../forms/ReviewCommentForm';
99
import { getPrismModeFromExtension } from '../../helpers/syntaxHighlighting.js';
10-
import { getFileExtensionLC, canUseDOM } from '../../../helpers/common.js';
10+
import { getFileExtensionLC, canUseDOM, EMPTY_OBJ } from '../../../helpers/common.js';
1111

1212
import SourceCodeComment from './SourceCodeComment.js';
1313
import './SourceCodeViewer.css';
@@ -196,10 +196,17 @@ class SourceCodeViewer extends React.Component {
196196
});
197197

198198
render() {
199-
const { name, content = '', addComment } = this.props;
199+
const { name, content = '', addComment, highlightOverrides = EMPTY_OBJ } = this.props;
200+
201+
const extension = getFileExtensionLC(name);
202+
const language =
203+
highlightOverrides[extension] !== undefined
204+
? highlightOverrides[extension]
205+
: getPrismModeFromExtension(extension);
206+
200207
return canUseDOM ? (
201208
<SyntaxHighlighter
202-
language={getPrismModeFromExtension(getFileExtensionLC(name))}
209+
language={language}
203210
style={vs}
204211
className={
205212
addComment && !this.props.onlyComments && !this.state.activeLine
@@ -234,6 +241,7 @@ SourceCodeViewer.propTypes = {
234241
restrictCommentAuthor: PropTypes.string,
235242
reviewClosed: PropTypes.bool,
236243
onlyComments: PropTypes.bool,
244+
highlightOverrides: PropTypes.object,
237245
};
238246

239247
export default SourceCodeViewer;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { default } from './SourceCodeViewer.js';
22
export { default as SourceCodeComment } from './SourceCodeComment.js';
3+
export { default as SourceCodeHighlightingSelector } from './SourceCodeHighlightingSelector.js';

0 commit comments

Comments
 (0)