diff --git a/app/containers/codeArea/highlighting.js b/app/containers/codeArea/highlighting.js new file mode 100644 index 00000000..609ac230 --- /dev/null +++ b/app/containers/codeArea/highlighting.js @@ -0,0 +1,63 @@ +import HighlightJS from 'highlight.js' + +export function highlightContent (content, language) { + if (language && HighlightJS.getLanguage(language)) { + try { + return HighlightJS.highlight(content, { language, ignoreIllegals: true }).value + } catch (__) {} + } + + return HighlightJS.highlightAuto(content).value +} + +export function adaptedLanguage (filename, lang) { + let language = lang || 'Other' + + // Adjust the language based on file extensions. + const filenameExtension = filename && filename.split('.').pop().toLowerCase() + switch (filenameExtension) { + case 'c': + case 'h': + if (!lang) language = 'c' + break + case 'leptonrc': + language = 'json' + break + case 'bat': + case 'cmd': + language = 'dos' + break + case 'zshrc': + language = 'bash' + break + case 'sql': + language = 'sql' + break + case 'solidity': + case 'sol': + language = 'solidity' + break + case 'vue': + language = 'xml' + break + default: + // intentionally left blank + } + + // Adapt the language name for Highlight.js. For example, 'C#' should be + // expressed as 'cs' to be recognized by Highlight.js. + switch (language) { + case 'Shell': return 'bash' + case 'C': return 'c' + case 'C++': return 'cpp' + case 'C#': return 'cs' + case 'Objective-C': return 'objectivec' + case 'Objective-C++': return 'objectivec' + case 'Visual Basic': return 'vbscript' + case 'Batchfile': return 'dos' + case 'Vue': return 'xml' + default: + } + + return language +} diff --git a/app/containers/codeArea/index.js b/app/containers/codeArea/index.js index 3f6dd232..05b9233d 100644 --- a/app/containers/codeArea/index.js +++ b/app/containers/codeArea/index.js @@ -5,6 +5,7 @@ import Markdown from '../../utilities/markdown' import nb from '../../utilities/jupyterNotebook' import React, { Component } from 'react' import electronBridge from '../../utilities/electronBridge' +import { adaptedLanguage, highlightContent } from './highlighting' import '../../utilities/vendor/prism/prism.scss' import './jupyterNotebook.scss' @@ -41,7 +42,7 @@ export default class CodeArea extends Component { createHighlightedCodeBlock (content, language, kTabLength) { let lineNumber = 0 - const highlightedContent = HighlightJS.highlightAuto(content, [language]).value + const highlightedContent = highlightContent(content, language) /* Highlight.js wraps comment blocks inside . @@ -68,55 +69,8 @@ export default class CodeArea extends Component { return `
${contentTable}
` } - // Find the best language for code highlighting by best effort. - adaptedLanguage (filename, lang) { - let language = lang || 'Other' - - // Adjust the language based on file extensions. - const filenameExtension = filename.split('.').pop().toLowerCase() - switch (filenameExtension) { - case 'leptonrc': - language = 'json' - break - case 'bat': - case 'cmd': - language = 'dos' - break - case 'zshrc': - language = 'bash' - break - case 'sql': - language = 'sql' - break - case 'solidity': - case 'sol': - language = 'solidity' - break - case 'vue': - language = 'xml' - break - default: - // intentionally left blank - } - - // Adapt the language name for Highlight.js. For example, 'C#' should be - // expressed as 'cs' to be recognized by Highlight.js. - switch (language) { - case 'Shell': return 'bash' - case 'C#': return 'cs' - case 'Objective-C': return 'objectivec' - case 'Objective-C++': return 'objectivec' - case 'Visual Basic': return 'vbscript' - case 'Batchfile': return 'dos' - case 'Vue': return 'xml' - default: - } - - return language - } - renderCodeArea (filename, content, lang, kTabLength) { - const language = this.adaptedLanguage(filename, lang) + const language = adaptedLanguage(filename, lang) let htmlContent = '' switch (language) { case 'Jupyter Notebook': diff --git a/tests/containers/codeArea.test.js b/tests/containers/codeArea.test.js new file mode 100644 index 00000000..2f5413fd --- /dev/null +++ b/tests/containers/codeArea.test.js @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest' + +import { + adaptedLanguage, + highlightContent +} from '../../app/containers/codeArea/highlighting' + +const cSource = '#include\n\nint main(void) {\n printf("Hello World\\n");\n return 0;\n}' + +describe('CodeArea syntax highlighting', () => { + it('normalizes GitHub language names for Highlight.js', () => { + expect(adaptedLanguage('main.c', 'C')).toBe('c') + expect(adaptedLanguage('main.cpp', 'C++')).toBe('cpp') + expect(adaptedLanguage('Program.cs', 'C#')).toBe('cs') + expect(adaptedLanguage('App.m', 'Objective-C')).toBe('objectivec') + expect(adaptedLanguage('App.mm', 'Objective-C++')).toBe('objectivec') + expect(adaptedLanguage('script.bat', 'Batchfile')).toBe('dos') + expect(adaptedLanguage('App.vue', 'Vue')).toBe('xml') + expect(adaptedLanguage('shell.sh', 'Shell')).toBe('bash') + expect(adaptedLanguage('form.vb', 'Visual Basic')).toBe('vbscript') + }) + + it('falls back to C highlighting for C source and header file extensions', () => { + expect(adaptedLanguage('main.c')).toBe('c') + expect(adaptedLanguage('main.h')).toBe('c') + }) + + it('uses extension overrides for files without GitHub language metadata', () => { + expect(adaptedLanguage('.leptonrc')).toBe('json') + expect(adaptedLanguage('setup.CMD')).toBe('dos') + expect(adaptedLanguage('.zshrc')).toBe('bash') + expect(adaptedLanguage('query.SQL')).toBe('sql') + expect(adaptedLanguage('Contract.sol')).toBe('solidity') + expect(adaptedLanguage('Contract.solidity')).toBe('solidity') + expect(adaptedLanguage('Component.VUE')).toBe('xml') + }) + + it('does not let ambiguous C header fallback override provided metadata', () => { + expect(adaptedLanguage('types.h', 'C++')).toBe('cpp') + expect(adaptedLanguage('ObjCBridge.h', 'Objective-C')).toBe('objectivec') + }) + + it('renders C source with Highlight.js spans instead of escaped plain text', () => { + const html = highlightContent(cSource, adaptedLanguage('main.c', 'C')) + + expect(html).toContain('hljs-keyword') + expect(html).toContain('hljs-string') + expect(html).toContain('<stdio.h>') + }) + + it('falls back to automatic highlighting for unknown language names', () => { + const html = highlightContent('function answer() { return 42 }', 'Other') + + expect(html).toContain('hljs-keyword') + }) +})