diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index 4d3d9fd91fef4b50f3897a75d14365a30ba0be3b..c02950534ff2bf606bc7b9bcdf28e66b7caf7ac8 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -42,7 +42,7 @@ export function renderGFM(element) { '.js-render-mermaid', '[data-canonical-lang="json"][data-lang-params~="table"]', 'table[data-table-fields]', - '[data-canonical-lang="glql"]', + '[data-canonical-lang="glql"], .language-glql', '.gfm-project_member', '.gfm-issue, .gfm-work_item, .gfm-merge_request, .gfm-epic, .gfm-milestone', '.task-list-item-checkbox', @@ -59,6 +59,6 @@ export function renderGFM(element) { highlightCurrentUser(userEls); initPopovers(popoverEls); addAriaLabels(taskListCheckboxEls); - renderGlql(glqlEls.map((e) => e.parentNode)); + renderGlql(glqlEls); renderImageLightbox(imageEls, element); } diff --git a/app/assets/javascripts/glql/index.js b/app/assets/javascripts/glql/index.js index cecc13f0252a1bef4bab88f82cbed88c5016a5ce..47b9a9b8d92b668bca083c6e3c6fa2690e02baf2 100644 --- a/app/assets/javascripts/glql/index.js +++ b/app/assets/javascripts/glql/index.js @@ -4,9 +4,14 @@ import Facade from './components/common/facade.vue'; const renderGlqlNode = (el) => { const container = document.createElement('div'); - const pre = el.querySelector('pre'); + const pre = el.closest('pre'); - el.parentNode.replaceChild(container, el); + // When wrapped in a js-markdown-code block we want to hide the copy-code button + const wrapper = pre.closest('.js-markdown-code') || pre; + + if (!wrapper.parentNode) return null; + + wrapper.parentNode.replaceChild(container, wrapper); return new Vue({ el: container, diff --git a/spec/frontend/behaviors/markdown/render_gfm_spec.js b/spec/frontend/behaviors/markdown/render_gfm_spec.js index e9c75cff1e789231e241e417a7da032049706c0f..8208e2cde4507986ccbbd2b045b43d69b806073f 100644 --- a/spec/frontend/behaviors/markdown/render_gfm_spec.js +++ b/spec/frontend/behaviors/markdown/render_gfm_spec.js @@ -28,14 +28,18 @@ describe('renderGFM', () => { beforeEach(() => { element = document.createElement('div'); - element.innerHTML = - '
labels = any
'; }); - it('calls renderGlql', () => { + it.each` + description | innerHTML | selector + ${'with data-canonical-lang data attr'} | ${'
labels = any
'} | ${'[data-canonical-lang="glql"]'} + ${'with language class on code tag'} | ${'
labels = any
'} | ${'.language-glql'} + `('calls renderGlql $description', ({ innerHTML, selector }) => { + element.innerHTML = innerHTML; + renderGFM(element); - expect(renderGlql).toHaveBeenCalledWith([element.firstElementChild]); + expect(renderGlql).toHaveBeenCalledWith([element.querySelector(selector)]); }); }); diff --git a/spec/frontend/glql/index_spec.js b/spec/frontend/glql/index_spec.js index f94c47cf5e8e4c46666ed929cae19bfa71985eb5..b07fe304f3a1627c796bb38f7f6b3dbaecef6c83 100644 --- a/spec/frontend/glql/index_spec.js +++ b/spec/frontend/glql/index_spec.js @@ -7,17 +7,36 @@ jest.mock('~/glql/core/parser'); describe('renderGlqlNodes', () => { stubCrypto(); - it('loops over all glql code blocks and renders them', async () => { - const container = document.createElement('div'); + let container; + + beforeEach(async () => { + container = document.createElement('div'); container.innerHTML = ` -
assignee = currentUser()
-
label = "bug"
+
assignee = currentUser()button
+
label = "bug"button
+
labels = any
`; - await renderGlqlNodes( - [...container.querySelectorAll('[data-canonical-lang="glql"]')].map((el) => el.parentNode), - ); + await renderGlqlNodes([ + ...container.querySelectorAll('[data-canonical-lang="glql"], .language-glql'), + ]); + }); + + it('loops over all glql code blocks and renders them', () => { + expect(container.querySelectorAll('[data-testid="glql-facade"]')).toHaveLength(3); + }); + + it('does not render the copy-code button', () => { + expect(container.querySelector('copy-code')).toBeNull(); + }); + + it('does not render glql nodes without a parent element', async () => { + const orphanedPre = document.createElement('pre'); + orphanedPre.dataset.canonicalLang = 'glql'; + orphanedPre.innerHTML = 'assignee = currentUser()'; + + await renderGlqlNodes([orphanedPre]); - expect(container.querySelectorAll('[data-testid="glql-facade"]')).toHaveLength(2); + expect(orphanedPre.querySelector('[data-testid="glql-facade"]')).toBeNull(); }); });