非ASCIIなドメインへのメンションの修正 (#2903)

* punycodeでされたmentionのラベルをunicodeとして表示する

* post-form mentionはpunycodeにする

* mentionの表示はURLもAPI向けもunicodeにする
This commit is contained in:
MeiMei 2018-10-14 16:56:19 +09:00 committed by syuilo
parent 7aa6ef2c25
commit 3bdc97b242
6 changed files with 27 additions and 13 deletions

View file

@ -116,16 +116,16 @@ export default Vue.component('misskey-flavored-markdown', {
case 'mention': { case 'mention': {
return (createElement as any)('a', { return (createElement as any)('a', {
attrs: { attrs: {
href: `${url}/@${getAcct(token)}`, href: `${url}/${token.canonical}`,
target: '_blank', target: '_blank',
dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token), dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token),
style: 'color:var(--mfmMention);' style: 'color:var(--mfmMention);'
}, },
directives: [{ directives: [{
name: 'user-preview', name: 'user-preview',
value: token.content value: token.canonical
}] }]
}, token.content); }, token.canonical);
} }
case 'hashtag': { case 'hashtag': {

View file

@ -1,6 +1,6 @@
import * as getCaretCoordinates from 'textarea-caret'; import * as getCaretCoordinates from 'textarea-caret';
import MkAutocomplete from '../components/autocomplete.vue'; import MkAutocomplete from '../components/autocomplete.vue';
import renderAcct from '../../../../../misc/acct/render'; import { toASCII } from 'punycode';
export default { export default {
bind(el, binding, vn) { bind(el, binding, vn) {
@ -188,7 +188,7 @@ class Autocomplete {
const trimmedBefore = before.substring(0, before.lastIndexOf('@')); const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
const after = source.substr(caret); const after = source.substr(caret);
const acct = renderAcct(value); const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
// 挿入 // 挿入
this.text = `${trimmedBefore}@${acct} ${after}`; this.text = `${trimmedBefore}@${acct} ${after}`;

View file

@ -65,6 +65,7 @@ import { host } from '../../../config';
import { erase, unique } from '../../../../../prelude/array'; import { erase, unique } from '../../../../../prelude/array';
import { length } from 'stringz'; import { length } from 'stringz';
import parseAcct from '../../../../../misc/acct/parse'; import parseAcct from '../../../../../misc/acct/parse';
import { toASCII } from 'punycode';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -158,14 +159,14 @@ export default Vue.extend({
} }
if (this.reply && this.reply.user.host != null) { if (this.reply && this.reply.user.host != null) {
this.text = `@${this.reply.user.username}@${this.reply.user.host} `; this.text = `@${this.reply.user.username}@${toASCII(this.reply.user.host)} `;
} }
if (this.reply && this.reply.text != null) { if (this.reply && this.reply.text != null) {
const ast = parse(this.reply.text); const ast = parse(this.reply.text);
ast.filter(t => t.type == 'mention').forEach(x => { ast.filter(t => t.type == 'mention').forEach(x => {
const mention = x.host ? `@${x.username}@${x.host}` : `@${x.username}`; const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
// //
if (this.$store.state.i.username == x.username && x.host == null) return; if (this.$store.state.i.username == x.username && x.host == null) return;

View file

@ -62,6 +62,7 @@ import { host } from '../../../config';
import { erase, unique } from '../../../../../prelude/array'; import { erase, unique } from '../../../../../prelude/array';
import { length } from 'stringz'; import { length } from 'stringz';
import parseAcct from '../../../../../misc/acct/parse'; import parseAcct from '../../../../../misc/acct/parse';
import { toASCII } from 'punycode';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -153,14 +154,14 @@ export default Vue.extend({
} }
if (this.reply && this.reply.user.host != null) { if (this.reply && this.reply.user.host != null) {
this.text = `@${this.reply.user.username}@${this.reply.user.host} `; this.text = `@${this.reply.user.username}@${toASCII(this.reply.user.host)} `;
} }
if (this.reply && this.reply.text != null) { if (this.reply && this.reply.text != null) {
const ast = parse(this.reply.text); const ast = parse(this.reply.text);
ast.filter(t => t.type == 'mention').forEach(x => { ast.filter(t => t.type == 'mention').forEach(x => {
const mention = x.host ? `@${x.username}@${x.host}` : `@${x.username}`; const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
// //
if (this.$store.state.i.username == x.username && x.host == null) return; if (this.$store.state.i.username == x.username && x.host == null) return;

View file

@ -2,10 +2,12 @@
* Mention * Mention
*/ */
import parseAcct from '../../../misc/acct/parse'; import parseAcct from '../../../misc/acct/parse';
import { toUnicode } from 'punycode';
export type TextElementMention = { export type TextElementMention = {
type: 'mention' type: 'mention'
content: string content: string
canonical: string
username: string username: string
host: string host: string
}; };
@ -15,9 +17,11 @@ export default function(text: string) {
if (!match) return null; if (!match) return null;
const mention = match[0]; const mention = match[0];
const { username, host } = parseAcct(mention.substr(1)); const { username, host } = parseAcct(mention.substr(1));
const canonical = host != null ? `@${username}@${toUnicode(host)}` : mention;
return { return {
type: 'mention', type: 'mention',
content: mention, content: mention,
canonical,
username, username,
host host
} as TextElementMention; } as TextElementMention;

View file

@ -8,9 +8,9 @@ describe('Text', () => {
it('can be analyzed', () => { it('can be analyzed', () => {
const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr'); const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');
assert.deepEqual([ assert.deepEqual([
{ type: 'mention', content: '@himawari', username: 'himawari', host: null }, { type: 'mention', content: '@himawari', canonical: '@himawari', username: 'himawari', host: null },
{ type: 'text', content: ' '}, { type: 'text', content: ' '},
{ type: 'mention', content: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }, { type: 'mention', content: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
{ type: 'text', content: ' お腹ペコい ' }, { type: 'text', content: ' お腹ペコい ' },
{ type: 'emoji', content: ':cat:', emoji: 'cat'}, { type: 'emoji', content: ':cat:', emoji: 'cat'},
{ type: 'text', content: ' '}, { type: 'text', content: ' '},
@ -58,7 +58,7 @@ describe('Text', () => {
it('local', () => { it('local', () => {
const tokens = analyze('@himawari お腹ペコい'); const tokens = analyze('@himawari お腹ペコい');
assert.deepEqual([ assert.deepEqual([
{ type: 'mention', content: '@himawari', username: 'himawari', host: null }, { type: 'mention', content: '@himawari', canonical: '@himawari', username: 'himawari', host: null },
{ type: 'text', content: ' お腹ペコい' } { type: 'text', content: ' お腹ペコい' }
], tokens); ], tokens);
}); });
@ -66,7 +66,15 @@ describe('Text', () => {
it('remote', () => { it('remote', () => {
const tokens = analyze('@hima_sub@namori.net お腹ペコい'); const tokens = analyze('@hima_sub@namori.net お腹ペコい');
assert.deepEqual([ assert.deepEqual([
{ type: 'mention', content: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }, { type: 'mention', content: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
{ type: 'text', content: ' お腹ペコい' }
], tokens);
});
it('remote punycode', () => {
const tokens = analyze('@hima_sub@xn--q9j5bya.xn--zckzah お腹ペコい');
assert.deepEqual([
{ type: 'mention', content: '@hima_sub@xn--q9j5bya.xn--zckzah', canonical: '@hima_sub@なもり.テスト', username: 'hima_sub', host: 'xn--q9j5bya.xn--zckzah' },
{ type: 'text', content: ' お腹ペコい' } { type: 'text', content: ' お腹ペコい' }
], tokens); ], tokens);
}); });