Merge pull request '[BUG] Render emojis in labels in issue info popup' (#2888) from gusted/forgejo-emoji-popup into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2888
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
Gusted 2024-03-30 11:20:57 +00:00
commit ec459b23c8
6 changed files with 221 additions and 13 deletions

171
package-lock.json generated
View file

@ -70,6 +70,7 @@
"@stylistic/eslint-plugin-js": "1.7.0", "@stylistic/eslint-plugin-js": "1.7.0",
"@stylistic/stylelint-plugin": "2.1.0", "@stylistic/stylelint-plugin": "2.1.0",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"@vue/test-utils": "2.4.5",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-plugin-array-func": "4.0.0", "eslint-plugin-array-func": "4.0.0",
"eslint-plugin-github": "4.10.2", "eslint-plugin-github": "4.10.2",
@ -1329,6 +1330,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@one-ini/wasm": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
"dev": true
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -2680,6 +2687,16 @@
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
"integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==" "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g=="
}, },
"node_modules/@vue/test-utils": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.5.tgz",
"integrity": "sha512-oo2u7vktOyKUked36R93NB7mg2B+N7Plr8lxp2JBGwr18ch6EggFjixSCdIVVLkT6Qr0z359Xvnafc9dcKyDUg==",
"dev": true,
"dependencies": {
"js-beautify": "^1.14.9",
"vue-component-type-helpers": "^2.0.0"
}
},
"node_modules/@webassemblyjs/ast": { "node_modules/@webassemblyjs/ast": {
"version": "1.12.1", "version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
@ -2862,6 +2879,15 @@
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
}, },
"node_modules/abbrev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
"integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
"dev": true,
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/abort-controller": { "node_modules/abort-controller": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@ -3799,6 +3825,22 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
}, },
"node_modules/config-chain": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
"dev": true,
"dependencies": {
"ini": "^1.3.4",
"proto-list": "~1.2.1"
}
},
"node_modules/config-chain/node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true
},
"node_modules/core-js-compat": { "node_modules/core-js-compat": {
"version": "3.36.1", "version": "3.36.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz",
@ -4775,6 +4817,48 @@
"marked": "^4.1.0" "marked": "^4.1.0"
} }
}, },
"node_modules/editorconfig": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
"integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
"dev": true,
"dependencies": {
"@one-ini/wasm": "0.1.1",
"commander": "^10.0.0",
"minimatch": "9.0.1",
"semver": "^7.5.3"
},
"bin": {
"editorconfig": "bin/editorconfig"
},
"engines": {
"node": ">=14"
}
},
"node_modules/editorconfig/node_modules/commander": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
"dev": true,
"engines": {
"node": ">=14"
}
},
"node_modules/editorconfig/node_modules/minimatch": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.716", "version": "1.4.716",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.716.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.716.tgz",
@ -7398,6 +7482,58 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
}, },
"node_modules/js-beautify": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
"integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==",
"dev": true,
"dependencies": {
"config-chain": "^1.1.13",
"editorconfig": "^1.0.4",
"glob": "^10.3.3",
"js-cookie": "^3.0.5",
"nopt": "^7.2.0"
},
"bin": {
"css-beautify": "js/bin/css-beautify.js",
"html-beautify": "js/bin/html-beautify.js",
"js-beautify": "js/bin/js-beautify.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/js-beautify/node_modules/glob": {
"version": "10.3.12",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
"integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.6",
"minimatch": "^9.0.1",
"minipass": "^7.0.4",
"path-scurry": "^1.10.2"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"dev": true,
"engines": {
"node": ">=14"
}
},
"node_modules/js-levenshtein-esm": { "node_modules/js-levenshtein-esm": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-1.2.0.tgz", "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-1.2.0.tgz",
@ -8801,6 +8937,21 @@
"resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz",
"integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==" "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw=="
}, },
"node_modules/nopt": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz",
"integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==",
"dev": true,
"dependencies": {
"abbrev": "^2.0.0"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/normalize-package-data": { "node_modules/normalize-package-data": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -9140,11 +9291,11 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
}, },
"node_modules/path-scurry": { "node_modules/path-scurry": {
"version": "1.10.1", "version": "1.10.2",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz",
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==",
"dependencies": { "dependencies": {
"lru-cache": "^9.1.1 || ^10.0.0", "lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
}, },
"engines": { "engines": {
@ -9727,6 +9878,12 @@
"integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==",
"dev": true "dev": true
}, },
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
"dev": true
},
"node_modules/proto-props": { "node_modules/proto-props": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/proto-props/-/proto-props-2.0.0.tgz", "resolved": "https://registry.npmjs.org/proto-props/-/proto-props-2.0.0.tgz",
@ -12089,6 +12246,12 @@
"vue": "^3.0.0-0 || ^2.7.0" "vue": "^3.0.0-0 || ^2.7.0"
} }
}, },
"node_modules/vue-component-type-helpers": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.7.tgz",
"integrity": "sha512-7e12Evdll7JcTIocojgnCgwocX4WzIYStGClBQ+QuWPinZo/vQolv2EMq4a3lg16TKfwWafLimG77bxb56UauA==",
"dev": true
},
"node_modules/vue-eslint-parser": { "node_modules/vue-eslint-parser": {
"version": "9.4.2", "version": "9.4.2",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz",

View file

@ -69,6 +69,7 @@
"@stylistic/eslint-plugin-js": "1.7.0", "@stylistic/eslint-plugin-js": "1.7.0",
"@stylistic/stylelint-plugin": "2.1.0", "@stylistic/stylelint-plugin": "2.1.0",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"@vue/test-utils": "2.4.5",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-plugin-array-func": "4.0.0", "eslint-plugin-array-func": "4.0.0",
"eslint-plugin-github": "4.10.2", "eslint-plugin-github": "4.10.2",

View file

@ -3010,3 +3010,7 @@ tbody.commit-list {
margin-top: -1px; margin-top: -1px;
border-top: 1px solid var(--color-secondary); border-top: 1px solid var(--color-secondary);
} }
#issue-info-popup .emoji {
font-size: inherit;
line-height: inherit;
}

View file

@ -0,0 +1,39 @@
import {mount, flushPromises} from '@vue/test-utils';
import ContextPopup from './ContextPopup.vue';
test('renders a issue info popup', async () => {
const owner = 'user2';
const repo = 'repo1';
const index = 1;
vi.spyOn(global, 'fetch').mockResolvedValue({
json: vi.fn().mockResolvedValue({
ok: true,
created_at: '2023-09-30T19:00:00Z',
repository: {full_name: owner},
pull_request: null,
state: 'open',
title: 'Normal issue',
body: 'Lorem ipsum...',
number: index,
labels: [{color: 'ee0701', name: "Bug :+1: <script class='evil'>alert('Oh no!');</script>"}],
}),
ok: true,
});
const wrapper = mount(ContextPopup);
wrapper.vm.$el.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}}));
await flushPromises();
// Header
expect(wrapper.get('p:nth-of-type(1)').text()).toEqual('user2 on Sep 30, 2023');
// Title
expect(wrapper.get('p:nth-of-type(2)').text()).toEqual('Normal issue #1');
// Body
expect(wrapper.get('p:nth-of-type(3)').text()).toEqual('Lorem ipsum...');
// Check that the state is correct.
expect(wrapper.get('svg').classes()).toContain('octicon-issue-opened');
// Ensure that script is not an element.
expect(() => wrapper.get('.evil')).toThrowError();
// Check content of label
expect(wrapper.get('.ui.label').text()).toContain("Bug 👍 <script class='evil'>alert('Oh no!');</script>");
});

View file

@ -3,6 +3,8 @@ import {SvgIcon} from '../svg.js';
import {useLightTextOnBackground} from '../utils/color.js'; import {useLightTextOnBackground} from '../utils/color.js';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import {GET} from '../modules/fetch.js'; import {GET} from '../modules/fetch.js';
import {emojiHTML} from '../features/emoji.js';
import {htmlEscape} from 'escape-goat';
const {appSubUrl, i18n} = window.config; const {appSubUrl, i18n} = window.config;
@ -67,6 +69,10 @@ export default {
} else { } else {
textColor = '#111111'; textColor = '#111111';
} }
label.name = htmlEscape(label.name);
label.name = label.name.replaceAll(/:[-+\w]+:/g, (emoji) => {
return emojiHTML(emoji.substring(1, emoji.length - 1));
});
return {name: label.name, color: `#${label.color}`, textColor}; return {name: label.name, color: `#${label.color}`, textColor};
}); });
}, },
@ -104,19 +110,13 @@ export default {
<template> <template>
<div ref="root"> <div ref="root">
<div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/> <div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/>
<div v-if="!loading && issue !== null"> <div v-if="!loading && issue !== null" id="issue-info-popup">
<p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p> <p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p>
<p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p> <p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p>
<p>{{ body }}</p> <p>{{ body }}</p>
<div> <div>
<div <!-- eslint-disable-next-line vue/no-v-html -->
v-for="label in labels" <div v-for="label in labels" :key="label.name" class="ui label" :style="{ color: label.textColor, backgroundColor: label.color }" v-html="label.name"/>
:key="label.name"
class="ui label"
:style="{ color: label.textColor, backgroundColor: label.color }"
>
{{ label.name }}
</div>
</div> </div>
</div> </div>
<div v-if="!loading && issue === null"> <div v-if="!loading && issue === null">

View file

@ -4,6 +4,7 @@ window.config = {
csrfToken: 'test-csrf-token-123456', csrfToken: 'test-csrf-token-123456',
pageData: {}, pageData: {},
i18n: {}, i18n: {},
customEmojis: {},
appSubUrl: '', appSubUrl: '',
mentionValues: [ mentionValues: [
{key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'}, {key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'},