From f866b1ca6d9714faf13678b246590ee143a2c19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kruli=C5=A1?= Date: Sat, 31 Jan 2026 19:27:07 +0100 Subject: [PATCH 1/3] Adding copy-to-clipboard button for file preview dialog. --- config/webpack.config-dev.js | 4 ++-- .../Solutions/SolutionFiles/SolutionFiles.js | 2 +- .../SourceCodeViewerContainer.js | 21 ++++++++++++++++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/config/webpack.config-dev.js b/config/webpack.config-dev.js index 76461728e..4a0dd6661 100644 --- a/config/webpack.config-dev.js +++ b/config/webpack.config-dev.js @@ -28,8 +28,8 @@ export default { // switch the source map generation when debugging // note, we used 'eval-source-map' before, but since webpack 5.100, it breaks the build // (causes 'SyntaxError: redeclaration of function normalize') - devtool: 'inline-source-map', - //devtool: false, // turn it off completely + // devtool: 'inline-source-map', + devtool: false, // turn it off completely entry: path.join(__dirname, '..', 'src/client.js'), output: { diff --git a/src/components/Solutions/SolutionFiles/SolutionFiles.js b/src/components/Solutions/SolutionFiles/SolutionFiles.js index 13542e7eb..01cba57e4 100644 --- a/src/components/Solutions/SolutionFiles/SolutionFiles.js +++ b/src/components/Solutions/SolutionFiles/SolutionFiles.js @@ -145,7 +145,7 @@ const SolutionFiles = ({ + ); class SourceCodeViewerContainer extends Component { + state = { clipboardCopied: false }; + componentDidMount() { const { fileId, loadAsync } = this.props; if (fileId !== null) { @@ -99,7 +102,7 @@ class SourceCodeViewerContainer extends Component { <> {preprocessFiles(files).map(f => ( @@ -113,11 +116,23 @@ class SourceCodeViewerContainer extends Component { ))} - + { + this.setState({ clipboardCopied: true }); + window.setTimeout(() => this.setState({ clipboardCopied: false }), 2000); + }}> + + + {files.length > 0 && ( Date: Tue, 3 Feb 2026 00:52:46 +0100 Subject: [PATCH 2/3] Adding syntax highlighting mode override mechanism for source code view component. --- .../Solutions/SourceCodeBox/SourceCodeBox.js | 27 +- .../SourceCodeHighlightingSelector.js | 139 ++++++++ .../SourceCodeViewer/SourceCodeViewer.css | 4 + .../SourceCodeViewer/SourceCodeViewer.js | 14 +- .../helpers/SourceCodeViewer/index.js | 1 + src/components/helpers/syntaxHighlighting.js | 323 +++++++++++++++++- .../SourceCodeViewerContainer.js | 24 +- src/locales/cs.json | 4 + src/locales/en.json | 6 +- .../SolutionSourceCodes.js | 34 +- 10 files changed, 536 insertions(+), 40 deletions(-) create mode 100644 src/components/helpers/SourceCodeViewer/SourceCodeHighlightingSelector.js diff --git a/src/components/Solutions/SourceCodeBox/SourceCodeBox.js b/src/components/Solutions/SourceCodeBox/SourceCodeBox.js index 56454de06..4fa248331 100644 --- a/src/components/Solutions/SourceCodeBox/SourceCodeBox.js +++ b/src/components/Solutions/SourceCodeBox/SourceCodeBox.js @@ -6,12 +6,12 @@ import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer'; import Prism from 'prismjs'; import Box from '../../widgets/Box'; -import SourceCodeViewer from '../../helpers/SourceCodeViewer'; +import SourceCodeViewer, { SourceCodeHighlightingSelector } from '../../helpers/SourceCodeViewer'; import ResourceRenderer from '../../helpers/ResourceRenderer'; import Icon, { CopyIcon, CopySuccessIcon, CodeCompareIcon, DownloadIcon, LoadingIcon, WarningIcon } from '../../icons'; import { getPrismModeFromExtension } from '../../helpers/syntaxHighlighting.js'; -import { getFileExtensionLC, simpleScalarMemoize } from '../../../helpers/common.js'; +import { getFileExtensionLC, simpleScalarMemoize, EMPTY_OBJ } from '../../../helpers/common.js'; const normalizeLineEndings = simpleScalarMemoize(content => content.replaceAll('\r', '')); @@ -45,10 +45,15 @@ const SourceCodeBox = ({ reviewClosed = false, collapsable = false, isOpen = true, + highlightOverrides = EMPTY_OBJ, + setHighlightOverride = null, }) => { const res = fileContentsSelector(parentId, entryName); const [clipboardCopied, setClipboardCopied] = useState(false); const [onlyComments, setOnlyComments] = useState(false); + + const fileExtension = getFileExtensionLC(name); + return ( )} - {name} + {name} {download && ( )} + {setHighlightOverride && ( + + )} + {diffMode && ( <> @@ -291,6 +307,7 @@ const SourceCodeBox = ({ removeComment={removeComment} reviewClosed={reviewClosed} onlyComments={reviewClosed && onlyComments} + highlightOverrides={highlightOverrides} /> )} @@ -319,6 +336,8 @@ SourceCodeBox.propTypes = { reviewClosed: PropTypes.bool, collapsable: PropTypes.bool, isOpen: PropTypes.bool, + highlightOverrides: PropTypes.object, + setHighlightOverride: PropTypes.func, }; export default SourceCodeBox; diff --git a/src/components/helpers/SourceCodeViewer/SourceCodeHighlightingSelector.js b/src/components/helpers/SourceCodeViewer/SourceCodeHighlightingSelector.js new file mode 100644 index 000000000..b0de801f2 --- /dev/null +++ b/src/components/helpers/SourceCodeViewer/SourceCodeHighlightingSelector.js @@ -0,0 +1,139 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { Overlay, Popover, FormSelect, ButtonGroup } from 'react-bootstrap'; + +import Button from '../../widgets/TheButton'; +import Icon, { CloseIcon, RefreshIcon, SaveIcon } from '../../icons'; + +import { getPrismModeFromExtension, PRISM_SUPPORTED_LANGUAGES } from '../../helpers/syntaxHighlighting.js'; + +const SourceCodeHighlightingSelector = ({ + id, + fullButton = false, + extension, + initialMode = null, + onChange, + ...props +}) => { + const defaultMode = getPrismModeFromExtension(extension); + const target = useRef(null); + const [visible, setVisible] = useState(false); + const [selectedMode, setSelectedMode] = useState(initialMode !== null ? initialMode : defaultMode); + + const clickHandler = ev => { + ev.stopPropagation(); + setVisible(!visible); + }; + + return ( + <> + {fullButton ? ( + + ) : ( + + )} + + + {props => ( + e.stopPropagation()} className="highlighting-selector" {...props}> + + {extension ? ( + <> + {' '} + *.{extension} + + ) : ( + + )} + + + setSelectedMode(e.target.value)} value={selectedMode}> + + {PRISM_SUPPORTED_LANGUAGES.map(lang => ( + + ))} + + + + + {selectedMode !== defaultMode && ( + + )} + + + + + )} + + + ); +}; + +SourceCodeHighlightingSelector.propTypes = { + id: PropTypes.string.isRequired, + fullButton: PropTypes.bool, + extension: PropTypes.string.isRequired, + initialMode: PropTypes.string, + onChange: PropTypes.func.isRequired, +}; + +export default SourceCodeHighlightingSelector; diff --git a/src/components/helpers/SourceCodeViewer/SourceCodeViewer.css b/src/components/helpers/SourceCodeViewer/SourceCodeViewer.css index 3103baf8e..a16d4af82 100644 --- a/src/components/helpers/SourceCodeViewer/SourceCodeViewer.css +++ b/src/components/helpers/SourceCodeViewer/SourceCodeViewer.css @@ -156,3 +156,7 @@ pre .sourceCodeViewerComments, code .sourceCodeViewerComments { .sourceCodeViewer.addComment .scvAddButton:hover::before { opacity: 1; } + +.popover.highlighting-selector { + --bs-popover-max-width: 400px; +} diff --git a/src/components/helpers/SourceCodeViewer/SourceCodeViewer.js b/src/components/helpers/SourceCodeViewer/SourceCodeViewer.js index 1388f1c06..827b2f1c5 100644 --- a/src/components/helpers/SourceCodeViewer/SourceCodeViewer.js +++ b/src/components/helpers/SourceCodeViewer/SourceCodeViewer.js @@ -7,7 +7,7 @@ import 'prismjs/themes/prism.css'; import ReviewCommentForm, { newCommentFormInitialValues } from '../../forms/ReviewCommentForm'; import { getPrismModeFromExtension } from '../../helpers/syntaxHighlighting.js'; -import { getFileExtensionLC, canUseDOM } from '../../../helpers/common.js'; +import { getFileExtensionLC, canUseDOM, EMPTY_OBJ } from '../../../helpers/common.js'; import SourceCodeComment from './SourceCodeComment.js'; import './SourceCodeViewer.css'; @@ -196,10 +196,17 @@ class SourceCodeViewer extends React.Component { }); render() { - const { name, content = '', addComment } = this.props; + const { name, content = '', addComment, highlightOverrides = EMPTY_OBJ } = this.props; + + const extension = getFileExtensionLC(name); + const language = + highlightOverrides[extension] !== undefined + ? highlightOverrides[extension] + : getPrismModeFromExtension(extension); + return canUseDOM ? ( ext => { - ext = ext.trim().toLowerCase(); - if (ext === '') { - return mapping.makefile; // makefile has no extension - } else if (mapping[ext]) { - return mapping[ext]; // mapping found - } else { - return mapping.cpp; // C/C++ is default - } -}; +export const createExtensionTranslator = + (mapping, defaultDefault = null) => + (ext, defaultValue = defaultDefault) => { + ext = ext.trim().toLowerCase(); + if (ext === '') { + return mapping.makefile; // makefile has no extension + } else if (mapping[ext]) { + return mapping[ext]; // mapping found + } else { + return defaultValue; + } + }; const prismMapping = { ...commonMapping, @@ -61,5 +63,302 @@ const prismMapping = { xml: 'markup', }; -export const getAceModeFromExtension = createExtensionTranslator(aceMapping); -export const getPrismModeFromExtension = createExtensionTranslator(prismMapping); +export const getAceModeFromExtension = createExtensionTranslator(aceMapping, aceMapping.cpp); +export const getPrismModeFromExtension = createExtensionTranslator(prismMapping, null); + +export const PRISM_SUPPORTED_LANGUAGES = [ + 'abap', + 'abnf', + 'actionscript', + 'ada', + 'agda', + 'al', + 'antlr4', + 'apacheconf', + 'apex', + 'apl', + 'applescript', + 'aql', + 'arduino', + 'arff', + 'armasm', + 'arturo', + 'asciidoc', + 'asm6502', + 'asmatmel', + 'aspnet', + 'autohotkey', + 'autoit', + 'avisynth', + 'awk', + 'bash', + 'basic', + 'batch', + 'bbcode', + 'bbj', + 'bicep', + 'birb', + 'bison', + 'bnf', + 'bqn', + 'brainfuck', + 'brightscript', + 'bro', + 'bsl', + 'c', + 'cfscript', + 'chaiscript', + 'cil', + 'cilkc', + 'cilkcpp', + 'clike', + 'clojure', + 'cmake', + 'cobol', + 'coffeescript', + 'concurnas', + 'cooklang', + 'coq', + 'cpp', + 'crystal', + 'csharp', + 'cshtml', + 'csp', + 'cssExtras', + 'css', + 'csv', + 'cue', + 'cypher', + 'd', + 'dart', + 'dataweave', + 'dax', + 'dhall', + 'diff', + 'django', + 'dnsZoneFile', + 'docker', + 'dot', + 'ebnf', + 'editorconfig', + 'eiffel', + 'ejs', + 'elixir', + 'elm', + 'erb', + 'erlang', + 'etlua', + 'excelFormula', + 'factor', + 'falselang', + 'firestoreSecurityRules', + 'flow', + 'fortran', + 'fsharp', + 'ftl', + 'gap', + 'gcode', + 'gdscript', + 'gedcom', + 'gettext', + 'gherkin', + 'git', + 'glsl', + 'gml', + 'gn', + 'goModule', + 'go', + 'gradle', + 'graphql', + 'groovy', + 'haml', + 'handlebars', + 'haskell', + 'haxe', + 'hcl', + 'hlsl', + 'hoon', + 'hpkp', + 'hsts', + 'http', + 'ichigojam', + 'icon', + 'icuMessageFormat', + 'idris', + 'iecst', + 'ignore', + 'inform7', + 'ini', + 'io', + 'j', + 'java', + 'javadoc', + 'javadoclike', + 'javascript', + 'javastacktrace', + 'jexl', + 'jolie', + 'jq', + 'jsExtras', + 'jsTemplates', + 'jsdoc', + 'json', + 'json5', + 'jsonp', + 'jsstacktrace', + 'jsx', + 'julia', + 'keepalived', + 'keyman', + 'kotlin', + 'kumir', + 'kusto', + 'latex', + 'latte', + 'less', + 'lilypond', + 'linkerScript', + 'liquid', + 'lisp', + 'livescript', + 'llvm', + 'log', + 'lolcode', + 'lua', + 'magma', + 'makefile', + 'markdown', + 'markupTemplating', + 'markup', + 'mata', + 'matlab', + 'maxscript', + 'mel', + 'mermaid', + 'metafont', + 'mizar', + 'mongodb', + 'monkey', + 'moonscript', + 'n1ql', + 'n4js', + 'nand2tetrisHdl', + 'naniscript', + 'nasm', + 'neon', + 'nevod', + 'nginx', + 'nim', + 'nix', + 'nsis', + 'objectivec', + 'ocaml', + 'odin', + 'opencl', + 'openqasm', + 'oz', + 'parigp', + 'parser', + 'pascal', + 'pascaligo', + 'pcaxis', + 'peoplecode', + 'perl', + 'php', + 'phpdoc', + 'plantUml', + 'plsql', + 'powerquery', + 'powershell', + 'processing', + 'prolog', + 'promql', + 'properties', + 'protobuf', + 'psl', + 'pug', + 'puppet', + 'pure', + 'purebasic', + 'purescript', + 'python', + 'q', + 'qml', + 'qore', + 'qsharp', + 'r', + 'racket', + 'reason', + 'regex', + 'rego', + 'renpy', + 'rescript', + 'rest', + 'rip', + 'roboconf', + 'robotframework', + 'ruby', + 'rust', + 'sas', + 'sass', + 'scala', + 'scheme', + 'scss', + 'shellSession', + 'smali', + 'smalltalk', + 'smarty', + 'sml', + 'solidity', + 'soy', + 'sparql', + 'splunkSpl', + 'sqf', + 'sql', + 'squirrel', + 'stan', + 'stata', + 'stylus', + 'supercollider', + 'swift', + 'systemd', + 't4Cs', + 't4Templating', + 't4Vb', + 'tap', + 'tcl', + 'textile', + 'toml', + 'tremor', + 'tsx', + 'tt2', + 'turtle', + 'twig', + 'typescript', + 'typoscript', + 'unrealscript', + 'uorazor', + 'uri', + 'v', + 'vala', + 'vbnet', + 'velocity', + 'verilog', + 'vhdl', + 'vim', + 'visualBasic', + 'warpscript', + 'wasm', + 'webIdl', + 'wgsl', + 'wiki', + 'wolfram', + 'wren', + 'xeora', + 'xmlDoc', + 'xojo', + 'xquery', + 'yaml', + 'yang', + 'zig', +]; diff --git a/src/containers/SourceCodeViewerContainer/SourceCodeViewerContainer.js b/src/containers/SourceCodeViewerContainer/SourceCodeViewerContainer.js index 2a9882b5e..112f4f283 100644 --- a/src/containers/SourceCodeViewerContainer/SourceCodeViewerContainer.js +++ b/src/containers/SourceCodeViewerContainer/SourceCodeViewerContainer.js @@ -121,17 +121,19 @@ class SourceCodeViewerContainer extends Component { - { - this.setState({ clipboardCopied: true }); - window.setTimeout(() => this.setState({ clipboardCopied: false }), 2000); - }}> - - + {!content.tooLarge && !content.malformedCharacters && ( + { + this.setState({ clipboardCopied: true }); + window.setTimeout(() => this.setState({ clipboardCopied: false }), 2000); + }}> + + + )} {files.length > 0 && ( [file.parentId || file.id, file.entryName || null]; const wrapInArray = lruMemoize(entry => [entry]); const localStorageDiffMappingsKey = 'SolutionSourceCodes.diffMappings.'; +const localStorageHighlightOverridesKey = 'SolutionSourceCodes.highlightOverrides'; class SolutionSourceCodes extends Component { state = { @@ -78,6 +78,7 @@ class SolutionSourceCodes extends Component { mappingDialogDiffWith: null, diffMappings: {}, commentsOpen: false, + highlightOverrides: {}, }; static loadAsync = ({ solutionId, assignmentId, secondSolutionId }, dispatch) => @@ -122,10 +123,12 @@ class SolutionSourceCodes extends Component { const lsKey = this.getDiffMappingsLocalStorageKey(); if (lsKey) { - this.setState({ - diffMappings: storageGetItem(lsKey, {}), - }); + this.setState({ diffMappings: storageGetItem(lsKey, {}) }); } + + this.setState({ + highlightOverrides: storageGetItem(localStorageHighlightOverridesKey, {}), + }); } componentDidUpdate(prevProps) { @@ -137,10 +140,12 @@ class SolutionSourceCodes extends Component { const lsKey = this.getDiffMappingsLocalStorageKey(); if (lsKey) { - this.setState({ - diffMappings: storageGetItem(lsKey, {}), - }); + this.setState({ diffMappings: storageGetItem(lsKey, {}) }); } + + this.setState({ + highlightOverrides: storageGetItem(localStorageHighlightOverridesKey, {}), + }); } } @@ -213,6 +218,15 @@ class SolutionSourceCodes extends Component { } }; + setHighlightOverride = (extension, mode) => { + const { [extension]: _, ...newOverrides } = this.state.highlightOverrides; + if (mode || mode === '') { + newOverrides[extension] = mode; + } + this.setState({ highlightOverrides: newOverrides }); + storageSetItem(localStorageHighlightOverridesKey, newOverrides); + }; + render() { const { assignment, @@ -520,6 +534,8 @@ class SolutionSourceCodes extends Component { removeComment={ canUpdateComments && hasPermissions(solution, 'review') ? removeComment : null } + highlightOverrides={this.state.highlightOverrides} + setHighlightOverride={this.setHighlightOverride} /> ))} From a4ec9cfbe9ddbe252248997618c0373038f36406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kruli=C5=A1?= Date: Tue, 3 Feb 2026 23:59:16 +0100 Subject: [PATCH 3/3] Adding syntax highlighting selection to source code dialogs. --- .../SourceCodeHighlightingSelector.js | 18 +++++-- .../helpers/SourceCodeViewer/index.js | 5 +- .../SourceCodeViewerContainer.js | 54 +++++++++++++++++-- .../SolutionSourceCodes.js | 2 +- 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/components/helpers/SourceCodeViewer/SourceCodeHighlightingSelector.js b/src/components/helpers/SourceCodeViewer/SourceCodeHighlightingSelector.js index b0de801f2..015a128a9 100644 --- a/src/components/helpers/SourceCodeViewer/SourceCodeHighlightingSelector.js +++ b/src/components/helpers/SourceCodeViewer/SourceCodeHighlightingSelector.js @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import { Overlay, Popover, FormSelect, ButtonGroup } from 'react-bootstrap'; @@ -8,6 +8,8 @@ import Icon, { CloseIcon, RefreshIcon, SaveIcon } from '../../icons'; import { getPrismModeFromExtension, PRISM_SUPPORTED_LANGUAGES } from '../../helpers/syntaxHighlighting.js'; +export const localStorageHighlightOverridesKey = 'SourceCodeViewer.highlightOverrides'; + const SourceCodeHighlightingSelector = ({ id, fullButton = false, @@ -21,15 +23,23 @@ const SourceCodeHighlightingSelector = ({ const [visible, setVisible] = useState(false); const [selectedMode, setSelectedMode] = useState(initialMode !== null ? initialMode : defaultMode); + useEffect(() => { + setSelectedMode(initialMode !== null ? initialMode : defaultMode); + }, [initialMode, extension]); + const clickHandler = ev => { ev.stopPropagation(); setVisible(!visible); }; + const changeHandler = ev => { + setSelectedMode(ev.target.value); + }; + return ( <> {fullButton ? ( -