diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js index f382ded90d7a1ca8c20324212d9eaf021a028b29..15335ea6edc514f88d9a0cc89205737c7bfd158f 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js @@ -120,6 +120,8 @@ export const EVENT_LABEL_FALLBACK = 'legacy_fallback'; export const LINES_PER_CHUNK = 70; +export const NEWLINE = '\n'; + export const BIDI_CHARS = [ '\u202A', // Left-to-Right Embedding (Try treating following text as left-to-right) '\u202B', // Right-to-Left Embedding (Try treating following text as right-to-left) diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js index 0da57f9e6fa4175721a553574e367347345f9b1a..142c135e9c1f2a19c9e435fe49403fb3d96c4d66 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js @@ -1,15 +1,47 @@ -import hljs from 'highlight.js/lib/core'; -import languageLoader from '~/content_editor/services/highlight_js_language_loader'; +import hljs from 'highlight.js'; import { registerPlugins } from '../plugins/index'; +import { LINES_PER_CHUNK, NEWLINE, ROUGE_TO_HLJS_LANGUAGE_MAP } from '../constants'; -const initHighlightJs = async (fileType, content, language) => { - const languageDefinition = await languageLoader[language](); - +const initHighlightJs = (fileType, content) => { registerPlugins(hljs, fileType, content); - hljs.registerLanguage(language, languageDefinition.default); }; -export const highlight = (fileType, content, language) => { - initHighlightJs(fileType, content, language); - return hljs.highlight(content, { language }).value; +const splitByLineBreaks = (content = '') => content.split(/\r?\n/); + +const createChunk = (language, rawChunkLines, highlightedChunkLines = [], startingFrom = 0) => ({ + highlightedContent: highlightedChunkLines.join(NEWLINE), + rawContent: rawChunkLines.join(NEWLINE), + totalLines: rawChunkLines.length, + startingFrom, + language, +}); + +const splitIntoChunks = (language, rawContent, highlightedContent) => { + const result = []; + const splitRawContent = splitByLineBreaks(rawContent); + const splitHighlightedContent = splitByLineBreaks(highlightedContent); + + for (let i = 0; i < splitRawContent.length; i += LINES_PER_CHUNK) { + const chunkIndex = Math.floor(i / LINES_PER_CHUNK); + const highlightedChunk = splitHighlightedContent.slice(i, i + LINES_PER_CHUNK); + const rawChunk = splitRawContent.slice(i, i + LINES_PER_CHUNK); + result[chunkIndex] = createChunk(language, rawChunk, highlightedChunk, i); + } + + return result; +}; + +const highlight = (fileType, rawContent, lang) => { + const language = ROUGE_TO_HLJS_LANGUAGE_MAP[lang.toLowerCase()]; + let result; + + if (language) { + initHighlightJs(fileType, rawContent, language); + const highlightedContent = hljs.highlight(rawContent, { language }).value; + result = splitIntoChunks(language, rawContent, highlightedContent); + } + + return result; }; + +export { highlight, splitIntoChunks }; diff --git a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js index 4a995e2fde16bc86547d339bdd3b98839096a1ef..d2dd4afe09e2dfb8238a4048b899144b365ba744 100644 --- a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js @@ -1,15 +1,10 @@ -import hljs from 'highlight.js/lib/core'; -import languageLoader from '~/content_editor/services/highlight_js_language_loader'; +import hljs from 'highlight.js'; import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index'; import { highlight } from '~/vue_shared/components/source_viewer/workers/highlight_utils'; +import { LINES_PER_CHUNK, NEWLINE } from '~/vue_shared/components/source_viewer/constants'; -jest.mock('highlight.js/lib/core', () => ({ - highlight: jest.fn().mockReturnValue({}), - registerLanguage: jest.fn(), -})); - -jest.mock('~/content_editor/services/highlight_js_language_loader', () => ({ - javascript: jest.fn().mockReturnValue({ default: jest.fn() }), +jest.mock('highlight.js', () => ({ + highlight: jest.fn().mockReturnValue({ value: 'highlighted content' }), })); jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({ @@ -17,28 +12,61 @@ jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({ })); const fileType = 'text'; -const content = 'function test() { return true };'; +const rawContent = 'function test() { return true }; \n // newline'; +const highlightedContent = 'highlighted content'; const language = 'javascript'; describe('Highlight utility', () => { - beforeEach(() => highlight(fileType, content, language)); - - it('loads the language', () => { - expect(languageLoader.javascript).toHaveBeenCalled(); - }); + beforeEach(() => highlight(fileType, rawContent, language)); it('registers the plugins', () => { expect(registerPlugins).toHaveBeenCalled(); }); - it('registers the language', () => { - expect(hljs.registerLanguage).toHaveBeenCalledWith( - language, - languageLoader[language]().default, + it('highlights the content', () => { + expect(hljs.highlight).toHaveBeenCalledWith(rawContent, { language }); + }); + + it('splits the content into chunks', () => { + const contentArray = Array.from({ length: 140 }, () => 'newline'); // simulate 140 lines of code + + const chunks = [ + { + language, + highlightedContent, + rawContent: contentArray.slice(0, 70).join(NEWLINE), // first 70 lines + startingFrom: 0, + totalLines: LINES_PER_CHUNK, + }, + { + language, + highlightedContent: '', + rawContent: contentArray.slice(70, 140).join(NEWLINE), // last 70 lines + startingFrom: 70, + totalLines: LINES_PER_CHUNK, + }, + ]; + + expect(highlight(fileType, contentArray.join(NEWLINE), language)).toEqual( + expect.arrayContaining(chunks), ); }); +}); - it('highlights the content', () => { - expect(hljs.highlight).toHaveBeenCalledWith(content, { language }); +describe('unsupported languages', () => { + const unsupportedLanguage = 'some_unsupported_language'; + + beforeEach(() => highlight(fileType, rawContent, unsupportedLanguage)); + + it('does not register plugins', () => { + expect(registerPlugins).not.toHaveBeenCalled(); + }); + + it('does not attempt to highlight the content', () => { + expect(hljs.highlight).not.toHaveBeenCalled(); + }); + + it('does not return a result', () => { + expect(highlight(fileType, rawContent, unsupportedLanguage)).toBe(undefined); }); });