diff --git a/app/assets/javascripts/blob/notebook/notebook_viewer.vue b/app/assets/javascripts/blob/notebook/notebook_viewer.vue index d2a841c88f139dbca8547d64cc2b6b90dbaba89d..79b8d2738838770c55cf56c6dadff2bb366d1170 100644 --- a/app/assets/javascripts/blob/notebook/notebook_viewer.vue +++ b/app/assets/javascripts/blob/notebook/notebook_viewer.vue @@ -66,7 +66,7 @@ export default { <div v-if="loading && !error" class="text-center loading"> <gl-loading-icon class="mt-5" size="lg" /> </div> - <notebook-lab v-if="!loading && !error" :notebook="json" code-css-class="code white" /> + <notebook-lab v-if="!loading && !error" :notebook="json" /> <p v-if="error" class="text-center"> <span v-if="loadError" ref="loadErrorMessage">{{ __('An error occurred while loading the file. Please try again later.') diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue index f5a6f3a98174597c7b26237db5b92aefaa475b61..bc1bab625533f849c91823f64fa52fb4776c142d 100644 --- a/app/assets/javascripts/notebook/cells/code.vue +++ b/app/assets/javascripts/notebook/cells/code.vue @@ -13,11 +13,6 @@ export default { type: Object, required: true, }, - codeCssClass: { - type: String, - required: false, - default: '', - }, }, computed: { rawInputCode() { @@ -39,18 +34,12 @@ export default { <template> <div class="cell"> - <code-output - :raw-code="rawInputCode" - :count="cell.execution_count" - :code-css-class="codeCssClass" - type="input" - /> + <code-output :raw-code="rawInputCode" :count="cell.execution_count" type="input" /> <output-cell v-if="hasOutput" :count="cell.execution_count" :outputs="outputs" :metadata="cell.metadata" - :code-css-class="codeCssClass" /> </div> </template> diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue index e1ef9aa6d7968c7378de6e78605334dfbe7734cc..64e801a751667bf546309ebb6c30545a56888226 100644 --- a/app/assets/javascripts/notebook/cells/code/index.vue +++ b/app/assets/javascripts/notebook/cells/code/index.vue @@ -1,10 +1,11 @@ <script> -import Prism from '../../lib/highlight'; +import CodeBlockHighlighted from '~/vue_shared/components/code_block_highlighted.vue'; import Prompt from '../prompt.vue'; export default { name: 'CodeOutput', components: { + CodeBlockHighlighted, Prompt, }, props: { @@ -13,11 +14,6 @@ export default { required: false, default: 0, }, - codeCssClass: { - type: String, - required: false, - default: '', - }, type: { type: String, required: true, @@ -41,22 +37,21 @@ export default { return type.charAt(0).toUpperCase() + type.slice(1); }, - cellCssClass() { - return { - [this.codeCssClass]: true, - 'jupyter-notebook-scrolled': this.metadata.scrolled, - }; + maxHeight() { + return this.metadata.scrolled ? '20rem' : 'initial'; }, }, - mounted() { - Prism.highlightElement(this.$refs.code); - }, }; </script> <template> <div :class="type"> <prompt :type="promptType" :count="count" /> - <pre ref="code" :class="cellCssClass" class="language-python" v-text="code"></pre> + <code-block-highlighted + language="python" + :code="code" + :max-height="maxHeight" + class="gl-border" + /> </div> </template> diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index 5f7ef4a4377718ebdfd19dac1c5722fddb034967..88d01ffa6599fe5ff381a1cc6c712aaf8d55cf23 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -6,11 +6,6 @@ import LatexOutput from './latex.vue'; export default { props: { - codeCssClass: { - type: String, - required: false, - default: '', - }, count: { type: Number, required: false, @@ -96,7 +91,6 @@ export default { :index="index" :raw-code="rawCode(output)" :metadata="metadata" - :code-css-class="codeCssClass" /> </div> </template> diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue index 44dc1856e49b3f0a9176b377358cfdfddd949b1a..df9694b7cd82581c1bcd6ff1abef5892f84716f6 100644 --- a/app/assets/javascripts/notebook/index.vue +++ b/app/assets/javascripts/notebook/index.vue @@ -11,11 +11,6 @@ export default { type: Object, required: true, }, - codeCssClass: { - type: String, - required: false, - default: '', - }, }, computed: { cells() { @@ -52,7 +47,6 @@ export default { v-for="(cell, index) in cells" :key="index" :cell="cell" - :code-css-class="codeCssClass" /> </div> </template> diff --git a/app/assets/javascripts/notebook/lib/highlight.js b/app/assets/javascripts/notebook/lib/highlight.js deleted file mode 100644 index 313aeecbd51e23d1b07e01d2a0fbecbc7b79e46b..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/notebook/lib/highlight.js +++ /dev/null @@ -1,5 +0,0 @@ -import Prism from 'prismjs'; -import 'prismjs/components/prism-python'; -import 'prismjs/themes/prism.css'; - -export default Prism; diff --git a/app/assets/javascripts/vue_shared/components/code_block.stories.js b/app/assets/javascripts/vue_shared/components/code_block.stories.js new file mode 100644 index 0000000000000000000000000000000000000000..ad53afe3676882a5a6ee139df9248861ed003eb2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/code_block.stories.js @@ -0,0 +1,18 @@ +import CodeBlock from './code_block.vue'; + +export default { + component: CodeBlock, + title: 'vue_shared/components/code_block', +}; + +const Template = (args, { argTypes }) => ({ + components: { CodeBlock }, + props: Object.keys(argTypes), + template: '<code-block v-bind="$props" />', +}); + +export const Default = Template.bind({}); +Default.args = { + // eslint-disable-next-line @gitlab/require-i18n-strings + code: `git commit -a "Message"\ngit push`, +}; diff --git a/app/assets/javascripts/vue_shared/components/code_block.vue b/app/assets/javascripts/vue_shared/components/code_block.vue index 9856f35c7f618e5102b09f91ef2c226b0d289688..4a69845d3a42979aa17ccab85ff4ed4a84f51ed7 100644 --- a/app/assets/javascripts/vue_shared/components/code_block.vue +++ b/app/assets/javascripts/vue_shared/components/code_block.vue @@ -4,7 +4,8 @@ export default { props: { code: { type: String, - required: true, + required: false, + default: '', }, maxHeight: { type: String, @@ -32,5 +33,5 @@ export default { class="code-block rounded code" :class="$options.userColorScheme" :style="styleObject" - ><code class="d-block">{{ code }}</code></pre> + ><slot><code class="d-block">{{ code }}</code></slot></pre> </template> diff --git a/app/assets/javascripts/vue_shared/components/code_block_highlighted.stories.js b/app/assets/javascripts/vue_shared/components/code_block_highlighted.stories.js new file mode 100644 index 0000000000000000000000000000000000000000..1939575ae4055f3362d57436c1e3bbbe4174805f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/code_block_highlighted.stories.js @@ -0,0 +1,18 @@ +import CodeBlockHighlighted from './code_block_highlighted.vue'; + +export default { + component: CodeBlockHighlighted, + title: 'vue_shared/components/code_block_highlighted', +}; + +const Template = (args, { argTypes }) => ({ + components: { CodeBlockHighlighted }, + props: Object.keys(argTypes), + template: '<code-block-highlighted v-bind="$props" />', +}); + +export const Default = Template.bind({}); +Default.args = { + code: `const foo = 1;\nconsole.log(foo + ' yay')`, + language: 'javascript', +}; diff --git a/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue b/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue new file mode 100644 index 0000000000000000000000000000000000000000..65b08b608e85dd76ed3c2d05225e0c32d452ccca --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue @@ -0,0 +1,72 @@ +<script> +import { GlSafeHtmlDirective } from '@gitlab/ui'; + +import languageLoader from '~/content_editor/services/highlight_js_language_loader'; +import CodeBlock from './code_block.vue'; + +export default { + name: 'CodeBlockHighlighted', + directives: { + SafeHtml: GlSafeHtmlDirective, + }, + components: { + CodeBlock, + }, + props: { + code: { + type: String, + required: true, + }, + language: { + type: String, + required: true, + }, + maxHeight: { + type: String, + required: false, + default: 'initial', + }, + }, + data() { + return { + hljs: null, + languageLoaded: false, + }; + }, + computed: { + highlighted() { + if (this.hljs && this.languageLoaded) { + return this.hljs.highlight(this.code, { language: this.language }).value; + } + + return this.code; + }, + }, + async mounted() { + this.hljs = await this.loadHighlightJS(); + if (this.language) { + await this.loadLanguage(); + } + }, + methods: { + async loadLanguage() { + try { + const { default: languageDefinition } = await languageLoader[this.language](); + + this.hljs.registerLanguage(this.language, languageDefinition); + this.languageLoaded = true; + } catch (e) { + this.$emit('error', e); + } + }, + loadHighlightJS() { + return import('highlight.js/lib/core'); + }, + }, +}; +</script> +<template> + <code-block :max-height="maxHeight" class="highlight"> + <span v-safe-html="highlighted"></span> + </code-block> +</template> diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index b980d7fdaa7cba9b8b35a38af84ed1e15acf1d61..cba8f48071bb1a73c354c82adf0032d849fbc6a6 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -471,11 +471,6 @@ span.idiff { } } -.jupyter-notebook-scrolled { - overflow-y: auto; - max-height: 20rem; -} - #js-openapi-viewer { pre.version, code { diff --git a/package.json b/package.json index 580174d472098adac39eb277067caf9b03427c57..ed78775ed3c76a0368208dd3e0b688c04315e2c9 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,6 @@ "popper.js": "^1.16.1", "portal-vue": "^2.1.7", "postcss": "8.4.14", - "prismjs": "^1.21.0", "prosemirror-markdown": "1.9.1", "prosemirror-model": "^1.18.1", "prosemirror-state": "^1.4.1", diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js index 475c41a72f62ddcea7645dcafdb3e7c2d61f970d..b79000a3505b72a1f6fc146c32d998d98416d5ff 100644 --- a/spec/frontend/notebook/index_spec.js +++ b/spec/frontend/notebook/index_spec.js @@ -11,7 +11,7 @@ describe('Notebook component', () => { function buildComponent(notebook) { return mount(Component, { - propsData: { notebook, codeCssClass: 'js-code-class' }, + propsData: { notebook }, provide: { relativeRawPath: '' }, }).vm; } @@ -46,10 +46,6 @@ describe('Notebook component', () => { it('renders code cell', () => { expect(vm.$el.querySelector('pre')).not.toBeNull(); }); - - it('add code class to code blocks', () => { - expect(vm.$el.querySelector('.js-code-class')).not.toBeNull(); - }); }); describe('with worksheets', () => { @@ -72,9 +68,5 @@ describe('Notebook component', () => { it('renders code cell', () => { expect(vm.$el.querySelector('pre')).not.toBeNull(); }); - - it('add code class to code blocks', () => { - expect(vm.$el.querySelector('.js-code-class')).not.toBeNull(); - }); }); }); diff --git a/spec/frontend/notebook/lib/highlight_spec.js b/spec/frontend/notebook/lib/highlight_spec.js deleted file mode 100644 index 944ccd6aa9f085fa811f943e2486de6f8066141e..0000000000000000000000000000000000000000 --- a/spec/frontend/notebook/lib/highlight_spec.js +++ /dev/null @@ -1,15 +0,0 @@ -import Prism from '~/notebook/lib/highlight'; - -describe('Highlight library', () => { - it('imports python language', () => { - expect(Prism.languages.python).toBeDefined(); - }); - - it('uses custom CSS classes', () => { - const el = document.createElement('div'); - el.innerHTML = Prism.highlight('console.log("a");', Prism.languages.javascript); - - expect(el.querySelector('.string')).not.toBeNull(); - expect(el.querySelector('.function')).not.toBeNull(); - }); -}); diff --git a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap deleted file mode 100644 index 7f655d67ae81a3afe42388703d5437082ef6075d..0000000000000000000000000000000000000000 --- a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Code Block with default props renders correctly 1`] = ` -<pre - class="code-block rounded code" -> - <code - class="d-block" - > - test-code - </code> -</pre> -`; - -exports[`Code Block with maxHeight set to "200px" renders correctly 1`] = ` -<pre - class="code-block rounded code" - style="max-height: 200px; overflow-y: auto;" -> - <code - class="d-block" - > - test-code - </code> -</pre> -`; diff --git a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..181692e61b5b92758841764bbcd90750f927fa9b --- /dev/null +++ b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js @@ -0,0 +1,65 @@ +import { shallowMount } from '@vue/test-utils'; +import CodeBlock from '~/vue_shared/components/code_block_highlighted.vue'; +import waitForPromises from 'helpers/wait_for_promises'; + +describe('Code Block Highlighted', () => { + let wrapper; + + const code = 'const foo = 1;'; + + const createComponent = (propsData = {}) => { + wrapper = shallowMount(CodeBlock, { propsData }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders highlighted code if language is supported', async () => { + createComponent({ code, language: 'javascript' }); + + await waitForPromises(); + + expect(wrapper.element).toMatchInlineSnapshot(` + <code-block-stub + class="highlight" + code="" + maxheight="initial" + > + <span> + <span + class="hljs-keyword" + > + const + </span> + foo = + <span + class="hljs-number" + > + 1 + </span> + ; + </span> + </code-block-stub> + `); + }); + + it("renders plain text if language isn't supported", async () => { + createComponent({ code, language: 'foobar' }); + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[expect.any(TypeError)]]); + + expect(wrapper.element).toMatchInlineSnapshot(` + <code-block-stub + class="highlight" + code="" + maxheight="initial" + > + <span> + const foo = 1; + </span> + </code-block-stub> + `); + }); +}); diff --git a/spec/frontend/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js index 60b0b0b566b90e7ab95b85ac843d8e1a89b48f38..9a4dbcc47ff77d87b421aaaedb83ec7960c176a3 100644 --- a/spec/frontend/vue_shared/components/code_block_spec.js +++ b/spec/frontend/vue_shared/components/code_block_spec.js @@ -4,41 +4,77 @@ import CodeBlock from '~/vue_shared/components/code_block.vue'; describe('Code Block', () => { let wrapper; - const defaultProps = { - code: 'test-code', - }; + const code = 'test-code'; - const createComponent = (props = {}) => { + const createComponent = (propsData, slots = {}) => { wrapper = shallowMount(CodeBlock, { - propsData: { - ...defaultProps, - ...props, - }, + slots, + propsData, }); }; afterEach(() => { wrapper.destroy(); - wrapper = null; }); - describe('with default props', () => { - beforeEach(() => { - createComponent(); - }); + it('overwrites the default slot', () => { + createComponent({}, { default: 'DEFAULT SLOT' }); - it('renders correctly', () => { - expect(wrapper.element).toMatchSnapshot(); - }); + expect(wrapper.element).toMatchInlineSnapshot(` + <pre + class="code-block rounded code" + > + DEFAULT SLOT + </pre> + `); }); - describe('with maxHeight set to "200px"', () => { - beforeEach(() => { - createComponent({ maxHeight: '200px' }); - }); + it('renders with empty code prop', () => { + createComponent({}); - it('renders correctly', () => { - expect(wrapper.element).toMatchSnapshot(); - }); + expect(wrapper.element).toMatchInlineSnapshot(` + <pre + class="code-block rounded code" + > + <code + class="d-block" + > + + </code> + </pre> + `); + }); + + it('renders code prop when provided', () => { + createComponent({ code }); + + expect(wrapper.element).toMatchInlineSnapshot(` + <pre + class="code-block rounded code" + > + <code + class="d-block" + > + test-code + </code> + </pre> + `); + }); + + it('sets maxHeight properly when provided', () => { + createComponent({ code, maxHeight: '200px' }); + + expect(wrapper.element).toMatchInlineSnapshot(` + <pre + class="code-block rounded code" + style="max-height: 200px; overflow-y: auto;" + > + <code + class="d-block" + > + test-code + </code> + </pre> + `); }); }); diff --git a/storybook/config/preview.js b/storybook/config/preview.js index a55d0d52a0c3bd502028d989fa2bf23a90b3e372..6f3b81907422c2e215579ddcb5312a7f141c5f4b 100644 --- a/storybook/config/preview.js +++ b/storybook/config/preview.js @@ -6,13 +6,16 @@ import translateMixin from '~/vue_shared/translate'; const stylesheetsRequireCtx = require.context( '../../app/assets/stylesheets', true, - /(application|application_utilities)\.scss$/, + /(application|application_utilities|highlight\/themes\/white)\.scss$/, ); -window.gon = {}; +window.gon = { + user_color_scheme: 'white', +}; translateMixin(Vue); stylesheetsRequireCtx('./application.scss'); stylesheetsRequireCtx('./application_utilities.scss'); +stylesheetsRequireCtx('./highlight/themes/white.scss'); export const decorators = [withServer(createMockServer)]; diff --git a/yarn.lock b/yarn.lock index 335451c2ab585f22cd4cd4e12a4537860a269f28..868ae38c64f474b8d606931487b704b8bb668a47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3429,7 +3429,7 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -clipboard@^2.0.0, clipboard@^2.0.8: +clipboard@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.8.tgz#ffc6c103dd2967a83005f3f61976aa4655a4cdba" integrity sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ== @@ -9731,13 +9731,6 @@ pretty@^2.0.0: extend-shallow "^2.0.1" js-beautify "^1.6.12" -prismjs@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3" - integrity sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw== - optionalDependencies: - clipboard "^2.0.0" - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"