diff --git a/.babelrc b/.babelrc index 3c732dd1b..945211470 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - "presets": ["@babel/preset-env"], - "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-transform-vue-jsx"], + "presets": ["@babel/preset-env", "@vue/babel-preset-jsx"], + "plugins": ["@babel/plugin-transform-runtime", "lodash"], "comments": false } diff --git a/package.json b/package.json index 993012665..5134a8b15 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,8 @@ "@babel/preset-env": "^7.7.6", "@babel/register": "^7.7.4", "@ungap/event-target": "^0.1.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.1.2", + "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1", + "@vue/babel-preset-jsx": "^1.2.4", "@vue/test-utils": "^1.0.0-beta.26", "autoprefixer": "^6.4.0", "babel-eslint": "^7.0.0", diff --git a/src/components/chat_list_item/chat_list_item.js b/src/components/chat_list_item/chat_list_item.js index bee1ad535..e01b9bd4d 100644 --- a/src/components/chat_list_item/chat_list_item.js +++ b/src/components/chat_list_item/chat_list_item.js @@ -1,5 +1,5 @@ import { mapState } from 'vuex' -import StatusContent from '../status_content/status_content.vue' +import StatusBody from '../status_content/status_content.vue' import fileType from 'src/services/file_type/file_type.service' import UserAvatar from '../user_avatar/user_avatar.vue' import AvatarList from '../avatar_list/avatar_list.vue' @@ -16,7 +16,7 @@ const ChatListItem = { AvatarList, Timeago, ChatTitle, - StatusContent + StatusBody }, computed: { ...mapState({ @@ -38,12 +38,15 @@ const ChatListItem = { }, messageForStatusContent () { const message = this.chat.lastMessage + const messageEmojis = message ? message.emojis : [] const isYou = message && message.account_id === this.currentUser.id - const content = message ? (this.attachmentInfo || message.content) : '' + const content = message ? (this.attachmentInfo || message.content_raw) : '' const messagePreview = isYou ? `${this.$t('chats.you')} ${content}` : content return { summary: '', + emojis: messageEmojis, statusnet_html: messagePreview, + raw_html: messagePreview, text: messagePreview, attachments: [] } diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss index 9e97b28ea..57332bed4 100644 --- a/src/components/chat_list_item/chat_list_item.scss +++ b/src/components/chat_list_item/chat_list_item.scss @@ -77,18 +77,15 @@ border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); } - .StatusContent { - img.emoji { - width: 1.4em; - height: 1.4em; - } + .chat-preview-body { + --emoji-size: 1.4em; } .time-wrapper { line-height: 1.4em; } - .single-line { + .chat-preview-body { padding-right: 1em; } } diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue index cd3f436e2..c7c0e878f 100644 --- a/src/components/chat_list_item/chat_list_item.vue +++ b/src/components/chat_list_item/chat_list_item.vue @@ -29,7 +29,8 @@
for paragraphs, GS uses
between them)
+ // as well as approximate line count by counting characters and approximating ~80
+ // per line.
+ //
+ // Using max-height + overflow: auto for status components resulted in false positives
+ // very often with japanese characters, and it was very annoying.
+ tallStatus () {
+ const lengthScore = this.status.statusnet_html.split(/
20
+ },
+ longSubject () {
+ return this.status.summary.length > 240
+ },
+ // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
+ mightHideBecauseSubject () {
+ return !!this.status.summary && this.localCollapseSubjectDefault
+ },
+ mightHideBecauseTall () {
+ return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
+ },
+ hideSubjectStatus () {
+ return this.mightHideBecauseSubject && !this.expandingSubject
+ },
+ hideTallStatus () {
+ return this.mightHideBecauseTall && !this.showingTall
+ },
+ showingMore () {
+ return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
+ },
+ postBodyHtml () {
+ const html = this.status.raw_html
+
+ if (this.mergedConfig.greentext) {
+ try {
+ if (html.includes('>')) {
+ // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
+ return processHtml(html, (string) => {
+ if (string.includes('>') &&
+ string
+ .replace(/<[^>]+?>/gi, '') // remove all tags
+ .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+ .trim()
+ .startsWith('>')) {
+ return `${string}`
+ } else {
+ return string
+ }
+ })
+ } else {
+ return html
+ }
+ } catch (e) {
+ console.error('Failed to process status html', e)
+ return html
+ }
+ } else {
+ return html
+ }
+ },
+ attachmentTypes () {
+ return this.status.attachments.map(file => fileType.fileType(file.mimetype))
+ },
+ ...mapGetters(['mergedConfig'])
+ },
+ components: {
+ RichContent
+ },
+ mounted () {
+ this.status.attentions && this.status.attentions.forEach(attn => {
+ const { id } = attn
+ this.$store.dispatch('fetchUserIfMissing', id)
+ })
+ },
+ methods: {
+ linkClicked (event) {
+ const target = event.target.closest('.status-content a')
+ if (target) {
+ if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
+ // Extract tag name from dataset or link url
+ const tag = target.dataset.tag || extractTagFromUrl(target.href)
+ if (tag) {
+ const link = this.generateTagLink(tag)
+ this.$router.push(link)
+ return
+ }
+ }
+ window.open(target.href, '_blank')
+ }
+ },
+ toggleShowMore () {
+ if (this.mightHideBecauseTall) {
+ this.showingTall = !this.showingTall
+ } else if (this.mightHideBecauseSubject) {
+ this.expandingSubject = !this.expandingSubject
+ }
+ },
+ generateTagLink (tag) {
+ return `/tag/${tag}`
+ }
+ }
+}
+
+export default StatusContent
diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss
new file mode 100644
index 000000000..b65e3b278
--- /dev/null
+++ b/src/components/status_body/status_body.scss
@@ -0,0 +1,123 @@
+@import '../../_variables.scss';
+
+.StatusBody {
+
+ .emoji {
+ --_still_image-label-scale: 0.5;
+ }
+
+ & .text,
+ & .summary {
+ font-family: var(--postFont, sans-serif);
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+ line-height: 1.4em;
+ }
+
+ .summary {
+ display: block;
+ font-style: italic;
+ padding-bottom: 0.5em;
+ }
+
+ .text {
+ &.-single-line {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ height: 1.4em;
+ }
+ }
+
+ .summary-wrapper {
+ margin-bottom: 0.5em;
+ border-style: solid;
+ border-width: 0 0 1px 0;
+ border-color: var(--border, $fallback--border);
+ flex-grow: 0;
+
+ &.-tall {
+ position: relative;
+
+ .summary {
+ max-height: 2em;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+
+ .text-wrapper {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: nowrap;
+
+ &.-tall-status {
+ position: relative;
+ height: 220px;
+ overflow-x: hidden;
+ overflow-y: hidden;
+ z-index: 1;
+
+ .text {
+ min-height: 0;
+ mask:
+ linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
+ linear-gradient(to top, white, white);
+
+ /* Autoprefixed seem to ignore this one, and also syntax is different */
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ }
+ }
+ }
+
+ & .tall-status-hider,
+ & .tall-subject-hider,
+ & .status-unhider,
+ & .cw-status-hider {
+ display: inline-block;
+ word-break: break-all;
+ width: 100%;
+ text-align: center;
+ }
+
+ .tall-status-hider {
+ position: absolute;
+ height: 70px;
+ margin-top: 150px;
+ line-height: 110px;
+ z-index: 2;
+ }
+
+ .tall-subject-hider {
+ // position: absolute;
+ padding-bottom: 0.5em;
+ }
+
+ & .status-unhider,
+ & .cw-status-hider {
+ word-break: break-all;
+
+ svg {
+ color: inherit;
+ }
+ }
+
+ .greentext {
+ color: $fallback--cGreen;
+ color: var(--postGreentext, $fallback--cGreen);
+ }
+
+ /* Not sure if this is necessary */
+ video {
+ max-width: 100%;
+ max-height: 400px;
+ vertical-align: middle;
+ object-fit: contain;
+ }
+
+}
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
new file mode 100644
index 000000000..6f982f2e3
--- /dev/null
+++ b/src/components/status_body/status_body.vue
@@ -0,0 +1,95 @@
+
+
for paragraphs, GS uses
between them)
- // as well as approximate line count by counting characters and approximating ~80
- // per line.
- //
- // Using max-height + overflow: auto for status components resulted in false positives
- // very often with japanese characters, and it was very annoying.
- tallStatus () {
- const lengthScore = this.status.statusnet_html.split(/
20
- },
- longSubject () {
- return this.status.summary.length > 240
- },
- // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
- mightHideBecauseSubject () {
- return !!this.status.summary && this.localCollapseSubjectDefault
- },
- mightHideBecauseTall () {
- return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
- },
- hideSubjectStatus () {
- return this.mightHideBecauseSubject && !this.expandingSubject
- },
- hideTallStatus () {
- return this.mightHideBecauseTall && !this.showingTall
- },
- showingMore () {
- return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
- },
nsfwClickthrough () {
if (!this.status.nsfw) {
return false
@@ -118,45 +75,11 @@ const StatusContent = {
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
)
},
- attachmentTypes () {
- return this.status.attachments.map(file => fileType.fileType(file.mimetype))
- },
maxThumbnails () {
return this.mergedConfig.maxThumbnails
},
- postBodyHtml () {
- const html = this.status.statusnet_html
-
- if (this.mergedConfig.greentext) {
- try {
- if (html.includes('>')) {
- // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
- return processHtml(html, (string) => {
- if (string.includes('>') &&
- string
- .replace(/<[^>]+?>/gi, '') // remove all tags
- .replace(/@\w+/gi, '') // remove mentions (even failed ones)
- .trim()
- .startsWith('>')) {
- return `${string}`
- } else {
- return string
- }
- })
- } else {
- return html
- }
- } catch (e) {
- console.err('Failed to process status html', e)
- return html
- }
- } else {
- return html
- }
- },
...mapGetters(['mergedConfig']),
...mapState({
- betterShadow: state => state.interface.browserSupport.cssFilter,
currentUser: state => state.users.currentUser
})
},
@@ -164,48 +87,10 @@ const StatusContent = {
Attachment,
Poll,
Gallery,
- LinkPreview
+ LinkPreview,
+ StatusBody
},
methods: {
- linkClicked (event) {
- const target = event.target.closest('.status-content a')
- if (target) {
- if (target.className.match(/mention/)) {
- const href = target.href
- const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
- if (attn) {
- event.stopPropagation()
- event.preventDefault()
- const link = this.generateUserProfileLink(attn.id, attn.screen_name)
- this.$router.push(link)
- return
- }
- }
- if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
- // Extract tag name from dataset or link url
- const tag = target.dataset.tag || extractTagFromUrl(target.href)
- if (tag) {
- const link = this.generateTagLink(tag)
- this.$router.push(link)
- return
- }
- }
- window.open(target.href, '_blank')
- }
- },
- toggleShowMore () {
- if (this.mightHideBecauseTall) {
- this.showingTall = !this.showingTall
- } else if (this.mightHideBecauseSubject) {
- this.expandingSubject = !this.expandingSubject
- }
- },
- generateUserProfileLink (id, name) {
- return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
- },
- generateTagLink (tag) {
- return `/tag/${tag}`
- },
setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
return () => this.$store.dispatch('setMedia', attachments)
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 90bfaf40c..0283ce2dd 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -1,133 +1,51 @@
-
2 ',
+ ['2'],
+ ' 2 ',
+ ['2'],
+ ' 42'
+ expect(convertHtml(input)).to.eql([
+ '1 ',
+ [' ',
+ [' 42']
+ ]
+ ])
+ })
+ it('realistic case 1', () => {
+ const input = ' ',
+ [
+ [
+ '',
+ [
+ [
+ '',
+ [
+ '@',
+ [
+ '',
+ [
+ 'benis'
+ ],
+ ''
+ ]
+ ],
+ ''
+ ]
+ ],
+ ''
+ ],
+ ' ',
+ [
+ '',
+ [
+ [
+ '',
+ [
+ '@',
+ [
+ '',
+ [
+ 'hj'
+ ],
+ ''
+ ]
+ ],
+ ''
+ ]
+ ],
+ ''
+ ],
+ ' nice'
+ ],
+ '45'
+ expect(convertHtml(input)).to.eql([
+ '1 ',
+ [
+ '
'],
+ '4'
+ ],
+ ''
+ ],
+ '5'
+ ])
+ })
+ it('converts html to tree while preserving tag formatting', () => {
+ const input = '1
45'
+ expect(convertHtml(input)).to.eql([
+ '1 ',
+ [
+ '
'],
+ '4'
+ ],
+ ''
+ ],
+ '5'
+ ])
+ })
+ it('converts semi-broken html', () => {
+ const input = '1
2
'],
+ ' 2 ',
+ [
+ '
Audience: Memphis
Improv troupe: come on, a better one
Audience: el paso'
+ expect(convertHtml(inputOutput)).to.eql([
+ 'Country improv: give me a city',
+ [
+ '
'
+ ],
+ 'Audience: Memphis',
+ [
+ '
'
+ ],
+ 'Improv troupe: come on, a better one',
+ [
+ '
'
+ ],
+ 'Audience: el paso'
+ ])
+ })
+ })
+
+ describe('processTextForEmoji', () => {
+ it('processes all emoji in text', () => {
+ const input = 'Hello from finland! :lol: We have best water! :lmao:'
+ const emojis = [
+ { shortcode: 'lol', src: 'LOL' },
+ { shortcode: 'lmao', src: 'LMAO' }
+ ]
+ const processor = ({ shortcode, src }) => ({ shortcode, src })
+ expect(processTextForEmoji(input, emojis, processor)).to.eql([
+ 'Hello from finland! ',
+ { shortcode: 'lol', src: 'LOL' },
+ ' We have best water! ',
+ { shortcode: 'lmao', src: 'LMAO' }
+ ])
+ })
+ it('leaves text as is', () => {
+ const input = 'Number one: that\'s terror'
+ const emojis = []
+ const processor = ({ shortcode, src }) => ({ shortcode, src })
+ expect(processTextForEmoji(input, emojis, processor)).to.eql([
+ 'Number one: that\'s terror'
+ ])
+ })
+ })
+
+ describe('getAttrs', () => {
+ it('extracts arguments from tag', () => {
+ const input = ''
+ const output = { src: 'boop', cool: true, ebin: 'true' }
+
+ expect(getAttrs(input)).to.eql(output)
+ })
+ })
+})
diff --git a/yarn.lock b/yarn.lock
index 23cc895b8..9329cc3a2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1011,23 +1011,86 @@
resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b"
integrity sha512-W2oyj0Fe1w/XhPZjkI3oUcDUAmu5P4qsdT2/2S8aMhtAWM/CE/jYWtji0pKNPDfxLI75fa5gWSEmnynKMNP/oA==
-"@vue/babel-helper-vue-jsx-merge-props@^1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040"
- integrity sha512-6tyf5Cqm4m6v7buITuwS+jHzPlIPxbFzEhXR5JGZpbrvOcp1hiQKckd305/3C7C36wFekNTQSxAtgeM0j0yoUw==
+"@vue/babel-helper-vue-jsx-merge-props@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz#31624a7a505fb14da1d58023725a4c5f270e6a81"
+ integrity sha512-QOi5OW45e2R20VygMSNhyQHvpdUwQZqGPc748JLGCYEy+yp8fNFNdbNIGAgZmi9e+2JHPd6i6idRuqivyicIkA==
-"@vue/babel-plugin-transform-vue-jsx@^1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.1.2.tgz#c0a3e6efc022e75e4247b448a8fc6b86f03e91c0"
- integrity sha512-YfdaoSMvD1nj7+DsrwfTvTnhDXI7bsuh+Y5qWwvQXlD24uLgnsoww3qbiZvWf/EoviZMrvqkqN4CBw0W3BWUTQ==
+"@vue/babel-plugin-transform-vue-jsx@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.2.1.tgz#646046c652c2f0242727f34519d917b064041ed7"
+ integrity sha512-HJuqwACYehQwh1fNT8f4kyzqlNMpBuUK4rSiSES5D4QsYncv5fxFsLyrxFPG2ksO7t5WP+Vgix6tt6yKClwPzA==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/plugin-syntax-jsx" "^7.2.0"
- "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0"
+ "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1"
html-tags "^2.0.0"
lodash.kebabcase "^4.1.1"
svg-tags "^1.0.0"
+"@vue/babel-preset-jsx@^1.2.4":
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.2.4.tgz#92fea79db6f13b01e80d3a0099e2924bdcbe4e87"
+ integrity sha512-oRVnmN2a77bYDJzeGSt92AuHXbkIxbf/XXSE3klINnh9AXBmVS1DGa1f0d+dDYpLfsAKElMnqKTQfKn7obcL4w==
+ dependencies:
+ "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1"
+ "@vue/babel-plugin-transform-vue-jsx" "^1.2.1"
+ "@vue/babel-sugar-composition-api-inject-h" "^1.2.1"
+ "@vue/babel-sugar-composition-api-render-instance" "^1.2.4"
+ "@vue/babel-sugar-functional-vue" "^1.2.2"
+ "@vue/babel-sugar-inject-h" "^1.2.2"
+ "@vue/babel-sugar-v-model" "^1.2.3"
+ "@vue/babel-sugar-v-on" "^1.2.3"
+
+"@vue/babel-sugar-composition-api-inject-h@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.2.1.tgz#05d6e0c432710e37582b2be9a6049b689b6f03eb"
+ integrity sha512-4B3L5Z2G+7s+9Bwbf+zPIifkFNcKth7fQwekVbnOA3cr3Pq71q71goWr97sk4/yyzH8phfe5ODVzEjX7HU7ItQ==
+ dependencies:
+ "@babel/plugin-syntax-jsx" "^7.2.0"
+
+"@vue/babel-sugar-composition-api-render-instance@^1.2.4":
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.2.4.tgz#e4cbc6997c344fac271785ad7a29325c51d68d19"
+ integrity sha512-joha4PZznQMsxQYXtR3MnTgCASC9u3zt9KfBxIeuI5g2gscpTsSKRDzWQt4aqNIpx6cv8On7/m6zmmovlNsG7Q==
+ dependencies:
+ "@babel/plugin-syntax-jsx" "^7.2.0"
+
+"@vue/babel-sugar-functional-vue@^1.2.2":
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.2.2.tgz#267a9ac8d787c96edbf03ce3f392c49da9bd2658"
+ integrity sha512-JvbgGn1bjCLByIAU1VOoepHQ1vFsroSA/QkzdiSs657V79q6OwEWLCQtQnEXD/rLTA8rRit4rMOhFpbjRFm82w==
+ dependencies:
+ "@babel/plugin-syntax-jsx" "^7.2.0"
+
+"@vue/babel-sugar-inject-h@^1.2.2":
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.2.2.tgz#d738d3c893367ec8491dcbb669b000919293e3aa"
+ integrity sha512-y8vTo00oRkzQTgufeotjCLPAvlhnpSkcHFEp60+LJUwygGcd5Chrpn5480AQp/thrxVm8m2ifAk0LyFel9oCnw==
+ dependencies:
+ "@babel/plugin-syntax-jsx" "^7.2.0"
+
+"@vue/babel-sugar-v-model@^1.2.3":
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.2.3.tgz#fa1f29ba51ebf0aa1a6c35fa66d539bc459a18f2"
+ integrity sha512-A2jxx87mySr/ulAsSSyYE8un6SIH0NWHiLaCWpodPCVOlQVODCaSpiR4+IMsmBr73haG+oeCuSvMOM+ttWUqRQ==
+ dependencies:
+ "@babel/plugin-syntax-jsx" "^7.2.0"
+ "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1"
+ "@vue/babel-plugin-transform-vue-jsx" "^1.2.1"
+ camelcase "^5.0.0"
+ html-tags "^2.0.0"
+ svg-tags "^1.0.0"
+
+"@vue/babel-sugar-v-on@^1.2.3":
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.2.3.tgz#342367178586a69f392f04bfba32021d02913ada"
+ integrity sha512-kt12VJdz/37D3N3eglBywV8GStKNUhNrsxChXIV+o0MwVXORYuhDTHJRKPgLJRb/EY3vM2aRFQdxJBp9CLikjw==
+ dependencies:
+ "@babel/plugin-syntax-jsx" "^7.2.0"
+ "@vue/babel-plugin-transform-vue-jsx" "^1.2.1"
+ camelcase "^5.0.0"
+
"@vue/test-utils@^1.0.0-beta.26":
version "1.0.0-beta.28"
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.28.tgz#767c43413df8cde86128735e58923803e444b9a5"