Commit 325d8e8b authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '350454-stub-hljs' into 'master'

Fix copy as GFM in the refactored blob viewer

See merge request gitlab-org/gitlab!81396
parents eb847f28 804d5ea7
...@@ -35,16 +35,20 @@ export default { ...@@ -35,16 +35,20 @@ export default {
}, },
highlightedContent() { highlightedContent() {
let highlightedContent; let highlightedContent;
let { language } = this;
if (this.hljs) { if (this.hljs) {
if (!this.language) { if (!language) {
highlightedContent = this.hljs.highlightAuto(this.content).value; const hljsHighlightAuto = this.hljs.highlightAuto(this.content);
highlightedContent = hljsHighlightAuto.value;
language = hljsHighlightAuto.language;
} else if (this.languageDefinition) { } else if (this.languageDefinition) {
highlightedContent = this.hljs.highlight(this.content, { language: this.language }).value; highlightedContent = this.hljs.highlight(this.content, { language: this.language }).value;
} }
} }
return wrapLines(highlightedContent); return wrapLines(highlightedContent, language);
}, },
}, },
watch: { watch: {
...@@ -110,7 +114,7 @@ export default { ...@@ -110,7 +114,7 @@ export default {
data-qa-selector="blob_viewer_file_content" data-qa-selector="blob_viewer_file_content"
> >
<line-numbers :lines="lineNumbers" /> <line-numbers :lines="lineNumbers" />
<pre class="code gl-pb-0!"><code v-safe-html="highlightedContent"></code> <pre class="code highlight gl-pb-0!"><code v-safe-html="highlightedContent"></code>
</pre> </pre>
</div> </div>
</template> </template>
export const wrapLines = (content) => { export const wrapLines = (content, language) => {
const isValidLanguage = /^[a-z\d\-_]+$/.test(language); // To prevent the possibility of a vulnerability we only allow languages that contain alphanumeric characters ([a-z\d), dashes (-) or underscores (_).
return ( return (
content && content &&
content content
.split('\n') .split('\n')
.map((line, i) => { .map((line, i) => {
let formattedLine; let formattedLine;
const idAttribute = `id="LC${i + 1}"`; const attributes = `id="LC${i + 1}" lang="${isValidLanguage ? language : ''}"`;
if (line.includes('<span class="hljs') && !line.includes('</span>')) { if (line.includes('<span class="hljs') && !line.includes('</span>')) {
/** /**
...@@ -14,9 +16,9 @@ export const wrapLines = (content) => { ...@@ -14,9 +16,9 @@ export const wrapLines = (content) => {
* example (before): <span class="hljs-code">```bash * example (before): <span class="hljs-code">```bash
* example (after): <span id="LC67" class="hljs-code">```bash * example (after): <span id="LC67" class="hljs-code">```bash
*/ */
formattedLine = line.replace(/(?=class="hljs)/, `${idAttribute} `); formattedLine = line.replace(/(?=class="hljs)/, `${attributes} `);
} else { } else {
formattedLine = `<span ${idAttribute} class="line">${line}</span>`; formattedLine = `<span ${attributes} class="line">${line}</span>`;
} }
return formattedLine; return formattedLine;
......
...@@ -7,10 +7,6 @@ RSpec.describe 'Copy as GFM', :js do ...@@ -7,10 +7,6 @@ RSpec.describe 'Copy as GFM', :js do
include RepoHelpers include RepoHelpers
include ActionView::Helpers::JavaScriptHelper include ActionView::Helpers::JavaScriptHelper
before do
stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/350454
end
describe 'Copying rendered GFM' do describe 'Copying rendered GFM' do
before do before do
@feat = MarkdownFeature.new @feat = MarkdownFeature.new
...@@ -764,8 +760,8 @@ RSpec.describe 'Copy as GFM', :js do ...@@ -764,8 +760,8 @@ RSpec.describe 'Copy as GFM', :js do
context 'selecting one word of text' do context 'selecting one word of text' do
it 'copies as inline code' do it 'copies as inline code' do
verify( verify(
'.line[id="LC9"] .no', '.line[id="LC10"]',
'`RuntimeError`' '`end`'
) )
end end
end end
...@@ -834,6 +830,7 @@ RSpec.describe 'Copy as GFM', :js do ...@@ -834,6 +830,7 @@ RSpec.describe 'Copy as GFM', :js do
end end
def verify(selector, gfm, target: nil) def verify(selector, gfm, target: nil)
expect(page).to have_selector('.js-syntax-highlight')
html = html_for_selector(selector) html = html_for_selector(selector)
output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target) output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target)
wait_for_requests wait_for_requests
......
...@@ -7,6 +7,7 @@ import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vu ...@@ -7,6 +7,7 @@ import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vu
import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants'; import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants';
import LineNumbers from '~/vue_shared/components/line_numbers.vue'; import LineNumbers from '~/vue_shared/components/line_numbers.vue';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import * as sourceViewerUtils from '~/vue_shared/components/source_viewer/utils';
jest.mock('highlight.js/lib/core'); jest.mock('highlight.js/lib/core');
Vue.use(VueRouter); Vue.use(VueRouter);
...@@ -36,6 +37,7 @@ describe('Source Viewer component', () => { ...@@ -36,6 +37,7 @@ describe('Source Viewer component', () => {
beforeEach(() => { beforeEach(() => {
hljs.highlight.mockImplementation(() => ({ value: highlightedContent })); hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent })); hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
jest.spyOn(sourceViewerUtils, 'wrapLines');
return createComponent(); return createComponent();
}); });
...@@ -73,6 +75,10 @@ describe('Source Viewer component', () => { ...@@ -73,6 +75,10 @@ describe('Source Viewer component', () => {
expect(findLoadingIcon().exists()).toBe(true); expect(findLoadingIcon().exists()).toBe(true);
}); });
it('calls the wrapLines helper method with highlightedContent and mappedLanguage', () => {
expect(sourceViewerUtils.wrapLines).toHaveBeenCalledWith(highlightedContent, mappedLanguage);
});
it('renders Line Numbers', () => { it('renders Line Numbers', () => {
expect(findLineNumbers().props('lines')).toBe(1); expect(findLineNumbers().props('lines')).toBe(1);
}); });
......
...@@ -2,12 +2,25 @@ import { wrapLines } from '~/vue_shared/components/source_viewer/utils'; ...@@ -2,12 +2,25 @@ import { wrapLines } from '~/vue_shared/components/source_viewer/utils';
describe('Wrap lines', () => { describe('Wrap lines', () => {
it.each` it.each`
input | output content | language | output
${'line 1'} | ${'<span id="LC1" class="line">line 1</span>'} ${'line 1'} | ${'javascript'} | ${'<span id="LC1" lang="javascript" class="line">line 1</span>'}
${'line 1\nline 2'} | ${`<span id="LC1" class="line">line 1</span>\n<span id="LC2" class="line">line 2</span>`} ${'line 1\nline 2'} | ${'html'} | ${`<span id="LC1" lang="html" class="line">line 1</span>\n<span id="LC2" lang="html" class="line">line 2</span>`}
${'<span class="hljs-code">line 1\nline 2</span>'} | ${`<span id="LC1" class="hljs-code">line 1\n<span id="LC2" class="line">line 2</span></span>`} ${'<span class="hljs-code">line 1\nline 2</span>'} | ${'html'} | ${`<span id="LC1" lang="html" class="hljs-code">line 1\n<span id="LC2" lang="html" class="line">line 2</span></span>`}
${'<span class="hljs-code">```bash'} | ${'<span id="LC1" class="hljs-code">```bash'} ${'<span class="hljs-code">```bash'} | ${'bash'} | ${'<span id="LC1" lang="bash" class="hljs-code">```bash'}
`('returns lines wrapped in spans containing line numbers', ({ input, output }) => { ${'<span class="hljs-code">```bash'} | ${'valid-language1'} | ${'<span id="LC1" lang="valid-language1" class="hljs-code">```bash'}
expect(wrapLines(input)).toBe(output); ${'<span class="hljs-code">```bash'} | ${'valid_language2'} | ${'<span id="LC1" lang="valid_language2" class="hljs-code">```bash'}
`('returns lines wrapped in spans containing line numbers', ({ content, language, output }) => {
expect(wrapLines(content, language)).toBe(output);
});
it.each`
language
${'invalidLanguage>'}
${'"invalidLanguage"'}
${'<invalidLanguage'}
`('returns lines safely without XSS language is not valid', ({ language }) => {
expect(wrapLines('<span class="hljs-code">```bash', language)).toBe(
'<span id="LC1" lang="" class="hljs-code">```bash',
);
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment