Compare commits
48 Commits
Author | SHA1 | Date | |
---|---|---|---|
ca8a218144 | |||
db9c2913cf | |||
286674c2bb | |||
7c259185bc | |||
79ffbf95db | |||
6e347e4221 | |||
28ccb14166 | |||
389315e000 | |||
168db6891f | |||
4a77548672 | |||
375b2bb284 | |||
b922277896 | |||
8f6f810dbd | |||
8f0c433e05 | |||
e332e3c248 | |||
2f90c38604 | |||
fa33d12bd7 | |||
86ab496fd6 | |||
ca0cb6fd42 | |||
be52779bbc | |||
23b64794a4 | |||
d5fed29df3 | |||
644bc985e7 | |||
5c1dc31131 | |||
31fae1caa6 | |||
9bc85ac511 | |||
69d6dc22b9 | |||
c4f81fc1a7 | |||
9a866766e0 | |||
eb7153acee | |||
f3d98da329 | |||
aac1c50a77 | |||
0619a27916 | |||
49ba56493f | |||
bf0dae8cc3 | |||
fbf77afde1 | |||
5159caa9ff | |||
5aef35f0b7 | |||
4db2843e7b | |||
a390e57dff | |||
8d70587814 | |||
6f3775de9d | |||
5a0a3050ae | |||
cbbf141846 | |||
43c2b00cf8 | |||
046ccd49ca | |||
e7df6f5c0e | |||
c67110835c |
22
docs/backup.fr.md
Normal file
22
docs/backup.fr.md
Normal file
@ -0,0 +1,22 @@
|
||||
Comment faire une sauvegarde de votre Misskey ?
|
||||
==========================
|
||||
|
||||
Assurez-vous d'avoir installé **mongodb-tools**.
|
||||
|
||||
---
|
||||
|
||||
Dans votre terminal :
|
||||
``` shell
|
||||
$ mongodump --archive=db-backup -u <VotreNomdUtilisateur> -p <VotreMotDePasse>
|
||||
```
|
||||
|
||||
Pour plus de détails, merci de consulter [la documentation de mongodump](https://docs.mongodb.com/manual/reference/program/mongodump/).
|
||||
|
||||
Restauration
|
||||
-------
|
||||
|
||||
``` shell
|
||||
$ mongorestore --archive=db-backup
|
||||
```
|
||||
|
||||
Pour plus de détails, merci de consulter [la documentation de mongorestore](https://docs.mongodb.com/manual/reference/program/mongorestore/).
|
@ -10,7 +10,7 @@ In your shell:
|
||||
$ mongodump --archive=db-backup -u <YourUserName> -p <YourPassword>
|
||||
```
|
||||
|
||||
For details, plese see [mongodump docs](https://docs.mongodb.com/manual/reference/program/mongodump/).
|
||||
For details, please see [mongodump docs](https://docs.mongodb.com/manual/reference/program/mongodump/).
|
||||
|
||||
Restore
|
||||
-------
|
||||
|
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "このユーザー情報はコピーです。"
|
||||
is-remote-post: "この投稿情報はコピーです。"
|
||||
view-on-remote: "正確な情報を見る"
|
||||
renoted-by: "{user}がRenote"
|
||||
error:
|
||||
title: '問題が発生しました'
|
||||
retry: 'やり直す'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
reposted-by: "{}がRenote"
|
||||
location: "位置情報"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
reply: "返信"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "もっと見る"
|
||||
close: "閉じる"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "返信"
|
||||
reaction: "リアクション"
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
|
@ -43,13 +43,13 @@ common:
|
||||
unknown: "Unbekannt"
|
||||
future: "Zukunft"
|
||||
just_now: "Gerade eben"
|
||||
seconds_ago: "vor {0} Sekunde{0:n}"
|
||||
minutes_ago: "vor {0} Minuten"
|
||||
hours_ago: "vor {0} Stunden"
|
||||
days_ago: "vor {0} Tag{0:en}"
|
||||
weeks_ago: "vor {0} Woche{0:n}"
|
||||
months_ago: "vor {0} Monat{0:en}"
|
||||
years_ago: "vor {} Jahr{0:en}"
|
||||
seconds_ago: "vor {} Sekunde(n)"
|
||||
minutes_ago: "vor {} Minute(n)"
|
||||
hours_ago: "vor {} Stunde(n)"
|
||||
days_ago: "vor {} Tag(en)"
|
||||
weeks_ago: "vor {} Woche(n)"
|
||||
months_ago: "vor {} Monat(en)"
|
||||
years_ago: "vor {} Jahr(en)"
|
||||
month-and-day: "{day}/{month}"
|
||||
trash: "Papierkorb"
|
||||
drive: "ドライブ"
|
||||
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "このユーザー情報はコピーです。"
|
||||
is-remote-post: "この投稿情報はコピーです。"
|
||||
view-on-remote: "正確な情報を見る"
|
||||
renoted-by: "{user}がRenote"
|
||||
error:
|
||||
title: '問題が発生しました'
|
||||
retry: 'Erneut versuchen'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "Dieser Post ist privat"
|
||||
deleted: "Dieser Beitrag wurde entfernt"
|
||||
reposted-by: "Repostet von {}"
|
||||
location: "Ort"
|
||||
renote: "Anmerkung"
|
||||
add-reaction: "Reaktion hinzufügen"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
reply: "返信"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "もっと見る"
|
||||
close: "閉じる"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "返信"
|
||||
reaction: "リアクション"
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
|
@ -29,7 +29,7 @@ common:
|
||||
got-it: "Got it!"
|
||||
customization-tips:
|
||||
title: "Customization tips"
|
||||
paragraph: "<p>Home customization allows you to add/delete, drag and drop and rearrange widgets.</p><p>You can change the display by <strong><strong>right</strong> clicking</strong> on some widgets.</p><p>To delete a widget, drag and drop the widget onto <strong>the area labeled \"Trash\"</strong> in the header.</p><p>To finish the customization, click \"Finish\" on the upper right.</p>"
|
||||
paragraph: "<p>Home customization allows you to add/delete, drag and drop and rearrange widgets.</p><p>You can change the display by <strong><strong>right</strong> clicking</strong> on some widgets.</p><p>To delete a widget, drag and drop the widget onto <strong>the area labeled \"Trash\"</strong> in the header.</p><p>To finish the customization, click \"Done\" on the upper right.</p>"
|
||||
gotit: "Got it!"
|
||||
notification:
|
||||
file-uploaded: "File uploaded!"
|
||||
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "This user information is copied."
|
||||
is-remote-post: "This post information is a copy."
|
||||
view-on-remote: "View it on remote"
|
||||
renoted-by: "Renoted by {user}"
|
||||
error:
|
||||
title: 'Something happened :('
|
||||
retry: 'Retry'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "Post is private"
|
||||
deleted: "Post has been removed"
|
||||
reposted-by: "Reposted by {}"
|
||||
location: "Location"
|
||||
renote: "Repost"
|
||||
add-reaction: "Add a reaction"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "Reposted by {}"
|
||||
reply: "Reply"
|
||||
renote: "Renote"
|
||||
add-reaction: "Add a reaction"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "See more"
|
||||
close: "Close"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "Reposted by {}"
|
||||
private: "This post is private"
|
||||
deleted: "This post has been deleted"
|
||||
location: "Location"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "Reply"
|
||||
reaction: "Reaction"
|
||||
reposted-by: "Reposted by {}"
|
||||
private: "This post is private"
|
||||
deleted: "This post has been deleted"
|
||||
location: "Location"
|
||||
|
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "このユーザー情報はコピーです。"
|
||||
is-remote-post: "この投稿情報はコピーです。"
|
||||
view-on-remote: "正確な情報を見る"
|
||||
renoted-by: "{user}がRenote"
|
||||
error:
|
||||
title: '問題が発生しました'
|
||||
retry: 'やり直す'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "Esta publicación es privada"
|
||||
deleted: "Esta publicación ha sido removida"
|
||||
reposted-by: "Republicado por {}"
|
||||
location: "Localización"
|
||||
renote: "Republicar"
|
||||
add-reaction: "Agregar una reacción"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
reply: "返信"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "もっと見る"
|
||||
close: "閉じる"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "返信"
|
||||
reaction: "リアクション"
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
|
@ -8,12 +8,12 @@ common:
|
||||
about: "Merci d’avoir choisis Misskey. Misskey est une <b>plateforme de microblogage distribuée</b> née sur Terre et fait partie du Fédiverse (un univers composé de diverses plateformes de réseaux sociaux organisées), elle est connectée mutuellement avec d’autres plateformes de réseaux sociaux. Désirez-vous prendre une pause, un court instant, loin de l’agitation de la ville et plonger dans un Internet d’un nouveau genre ?"
|
||||
intro:
|
||||
title: "C’est quoi Misskey ?"
|
||||
about: "Misskeyはオープンソースの<b>分散型マイクロブログSNS</b>です。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。"
|
||||
about: "Misskey est un <b>réseau social de Microblogage</b> open source. Il offre une interface utilisateur riche et hautement personnalisable, une variété de réactions aux publications et un lecteur pour la gestion centralisée de fichiers. De plus, comme il est possible de se connecter au reste du du Fédiverse, vous pouvez interagir avec d'autres plateformes fédérées. Par exemple, si vous publiez quelque chose, la note sera transmise non seulement aux utilisateurs de Misskey, mais aussi à d'autres plateformes de réseaux sociaux dans le Fédiverse. Imaginez que vous puissiez transmettre des ondes radio d'une planète vers l'autre."
|
||||
features: "Options"
|
||||
rich-contents: "Notes"
|
||||
rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。"
|
||||
rich-contents-desc: "Partagez vos idées, les événements et les sujets qui vous tiennent à cœur ainsi que tout autre chose que vous souhaitez partager avec les autres. Si vous le désirez, vous pouvez décorer vos messages en utilisant une syntaxe différente ou en y joignant des sondages et des fichiers, tels que les photos ou les vidéos que vous aimez."
|
||||
reaction: "Réactions"
|
||||
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
|
||||
reaction-desc: "Une manière simple d'exprimer vos émotions. Misskey peut attacher diverses réactions aux publications des autres utilisateurs. Si vous goûtez aux réactions sur Misskey une fois, vous ne pourrez plus être en mesure de retourner vers une autre plateforme de réseaux sociaux n'offrant que des \"J'aime\"."
|
||||
ui: "Interface utilisateur"
|
||||
ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。"
|
||||
drive: "Drive"
|
||||
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "Ces informations appartiennent à un·e utilisateur·rice distant·e."
|
||||
is-remote-post: "Ceci est une publication distante."
|
||||
view-on-remote: " Consulter le profil complet"
|
||||
renoted-by: "{user}がRenote"
|
||||
error:
|
||||
title: 'Une erreur est survenue'
|
||||
retry: 'Réessayer'
|
||||
@ -201,7 +202,7 @@ common/views/components/games/reversi/reversi.index.vue:
|
||||
sub-title: "Jouer à Reversi avec vos ami·e·s !"
|
||||
invite: "Inviter"
|
||||
rule: "Comment jouer ?"
|
||||
rule-desc: "リバーシは、相手と交互に石をボードに置いて、相手の石を挟んで自分の色に変えてゆき、最終的に残った石が多い方が勝ちというボードゲームです。"
|
||||
rule-desc: "Reversi est un jeu qui se joue sur un tablier et dans lequel les joueurs placent des pions sur ce dernier, à tour de rôle avec l'adversaire. Le but du jeu est d'avoir plus de pions de sa couleur que l'adversaire à la fin de la partie, celle-ci s'achevant lorsque aucun des deux joueurs ne peut plus jouer de coup légal, généralement lorsque les 64 cases sont occupées."
|
||||
mode-invite: "Inviter"
|
||||
mode-invite-desc: "Inviter un·e joueur·se."
|
||||
invitations: "Vous avez reçu une invitation !"
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "cette publication est privée"
|
||||
deleted: "cette publication a été supprimée"
|
||||
reposted-by: "Republié par {}"
|
||||
location: "Géolocalisation"
|
||||
renote: "Republier"
|
||||
add-reaction: "Ajouter votre reaction"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "Partagé par {}"
|
||||
reply: "Répondre"
|
||||
renote: "Partager"
|
||||
add-reaction: "Ajouter votre réaction"
|
||||
@ -736,7 +735,7 @@ desktop/views/components/settings.vue:
|
||||
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
|
||||
advanced: "Paramètres avancés"
|
||||
api-via-stream: "Requête API via le flux"
|
||||
api-via-stream-desc: "この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。"
|
||||
api-via-stream-desc: "Lorsque ce paramètre est activé, une demande d'API est effectuée via une connexion WebSocket (pour une meilleure performance). Lorsqu'il est désactivé, l'API de récupération native est utilisée. Ce paramètre n'est valide que sur cet appareil."
|
||||
deck-nav: "デッキ内ナビゲーション"
|
||||
deck-nav-desc: "デッキを使用しているとき、ナビゲーションが発生する際にページ遷移を行わずに一時的なカラムで受けるようにします。"
|
||||
deck-default: "Utiliser le Deck comme IU par défaut"
|
||||
@ -759,7 +758,7 @@ desktop/views/components/settings.vue:
|
||||
show-renoted-my-notes: "Afficher mes republications dans les fils"
|
||||
show-local-renotes: "Afficher les partages locaux sur les fils"
|
||||
show-maps: "Afficher la carte"
|
||||
remain-deleted-note: "削除された投稿を表示し続ける"
|
||||
remain-deleted-note: "Continuer à afficher les messages supprimés"
|
||||
deck-column-align: "Alignement des colonnes du Deck"
|
||||
deck-column-align-center: "Centrer"
|
||||
deck-column-align-left: "À gauche"
|
||||
@ -776,7 +775,7 @@ desktop/views/components/settings.vue:
|
||||
language-desc: "Le rechargement de la page est requis afin d'appliquer les modifications."
|
||||
cache: "Cache"
|
||||
clean-cache: "Nettoyage"
|
||||
cache-warn: "クリーンアップを行うと、ブラウザに記憶されたアカウント情報のキャッシュ、書きかけの投稿・返信・メッセージ、およびその他のデータ(設定情報含む)が削除されます。クリーンアップを行った後はページを再度読み込みする必要があります。"
|
||||
cache-warn: "Le nettoyage du cache du compte supprime les informations stockées dans le navigateur comme les messages, les réponses ainsi que d’autres données (y compris les paramètres de configuration). Après le nettoyage, vous devez recharger la page."
|
||||
cache-cleared: "Cache nettoyé"
|
||||
cache-cleared-desc: "Veuillez recharger la page."
|
||||
auto-watch: "Montre automatique"
|
||||
@ -790,7 +789,7 @@ desktop/views/components/settings.vue:
|
||||
do-update: "Rechercher des mises à jour"
|
||||
update-settings: "Paramètres avancés"
|
||||
prevent-update: "Reporter les mises à jour (non recommandé)"
|
||||
prevent-update-desc: "この設定をオンにしてもアップデートが反映される場合があります。この設定はこのデバイスのみ有効です。"
|
||||
prevent-update-desc: "Même si ce paramètre est activé, la mise à jour pourrait être appliquée. Ce paramètre n'est valide que sur cet appareil."
|
||||
no-updates: "Aucune mise à jour disponible"
|
||||
no-updates-desc: "Votre client Misskey est à jour."
|
||||
update-available: "Nouvelle version disponible !"
|
||||
@ -826,7 +825,7 @@ desktop/views/components/settings.2fa.vue:
|
||||
failed: "L’opération a échoué. Veuillez vous assurer que le jeton a été saisi correctement."
|
||||
info: "À partir de maintenant, à chaque fois que vous vous connectez entrez votre mot de passe ainsi que le jeton généré sur votre appareil."
|
||||
common/views/components/api-settings.vue:
|
||||
intro: "APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。"
|
||||
intro: "Pour accéder à l'API, définissez ce jeton comme la clé de \"i\" dans les paramètres de requête."
|
||||
caution: "Merci de ne pas introduire ce jeton dans aucune application ou le divulguer à quiconque. Ceci risque de compromettre votre compte."
|
||||
regeneration-of-token: "Si votre jeton est compromis, vous pouvez le régénérer."
|
||||
regenerate-token: "Régénérer le jeton"
|
||||
@ -1010,9 +1009,9 @@ admin/views/charts.vue:
|
||||
notes-total: "Total des publications"
|
||||
users: "Nombre d’utilisateur·rice·s : augmentation/diminution"
|
||||
users-total: "Nombre total des utilisateur·rice·s"
|
||||
drive: "ドライブ使用量の増減"
|
||||
drive: "Capacité utilisée comme stockage : augmentation/diminution"
|
||||
drive-total: "Utilisation totale du lecteur"
|
||||
drive-files: "ドライブのファイル数の増減"
|
||||
drive-files: "Le nombre de fichiers sur l'espace de stockage : augmentation/diminution"
|
||||
drive-files-total: "Nombre total de fichiers sur le lecteur"
|
||||
network-requests: "Requêtes"
|
||||
network-time: "Temps de réponse"
|
||||
@ -1027,7 +1026,7 @@ admin/views/users.vue:
|
||||
verify-user: "Paramètres de vérification du compte utilisateur"
|
||||
verify: "Vérification du compte"
|
||||
verified: "Le compte a été vérifié"
|
||||
unverify-user: "ユーザーの公式アカウント解除"
|
||||
unverify-user: "Paramètres de non-vérification du compte utilisateur"
|
||||
unverify: "Ôter la vérification du compte"
|
||||
unverified: "Ce compte n'est plus vérifié"
|
||||
admin/views/moderators.vue:
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "Voir plus"
|
||||
close: "Fermer"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "Republié par {}"
|
||||
private: "cette publication est privée"
|
||||
deleted: "cette publication a été supprimée"
|
||||
location: "Géolocalisation"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "Répondre"
|
||||
reaction: "Réaction"
|
||||
reposted-by: "Republié par {}"
|
||||
private: "cette publication est privée"
|
||||
deleted: "cette publication a été supprimée"
|
||||
location: "Lieu"
|
||||
@ -1459,7 +1456,7 @@ docs:
|
||||
require-permission: "Ce point de communication nécessite la permission {permission}."
|
||||
has-limit: "Il y’a un taux limite."
|
||||
duration-limit: "直近{duration}ミリ秒の間のこのエンドポイントへのリクエスト数の合計が{max}を超える場合はリクエストできません。"
|
||||
min-interval-limit: "前回のリクエストから{interval}ミリ秒経っていない場合はリクエストできません。"
|
||||
min-interval-limit: "Vous ne pourrez pas effectuer une nouvelle requête si {interval} millisecondes ne se sont pas écoulées depuis la dernière demande."
|
||||
show-src: "Vous pouvez voir le code source ce point de communication."
|
||||
show-src-link: "Consulter le code sur GitHub"
|
||||
generated: "Ce document est généré à partir de la définition de l’API."
|
||||
@ -1485,7 +1482,7 @@ dev/views/new-app.vue:
|
||||
callback-url-desc: "Vous pouvez définir l’URL de redirection lorsque l’utilisateur s’est authentifié via formulaire d’authentification."
|
||||
authority: "Autorisations "
|
||||
authority-desc: "Sont accessibles via l’API, uniquement les fonctionnalités demandées ici."
|
||||
authority-warning: "アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。"
|
||||
authority-warning: "Vous pouvez le changer même après avoir créé l'application, mais si vous attribuez une nouvelle permission, toutes les clés utilisateur associées seront dès lors invalides."
|
||||
account-read: "Afficher les informations du compte"
|
||||
account-write: "Modifications des informations du compte"
|
||||
note-write: "Publications."
|
||||
|
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "このユーザー情報はコピーです。"
|
||||
is-remote-post: "この投稿情報はコピーです。"
|
||||
view-on-remote: "正確な情報を見る"
|
||||
renoted-by: "{user}がRenote"
|
||||
error:
|
||||
title: '問題が発生しました'
|
||||
retry: 'やり直す'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
reposted-by: "{}がRenote"
|
||||
location: "位置情報"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
reply: "返信"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "もっと見る"
|
||||
close: "閉じる"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "返信"
|
||||
reaction: "リアクション"
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
|
@ -133,6 +133,7 @@ common:
|
||||
is-remote-user: "このユーザー情報はコピーです。"
|
||||
is-remote-post: "この投稿情報はコピーです。"
|
||||
view-on-remote: "正確な情報を見る"
|
||||
renoted-by: "{user}がRenote"
|
||||
|
||||
error:
|
||||
title: '問題が発生しました'
|
||||
@ -724,13 +725,11 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
reposted-by: "{}がRenote"
|
||||
location: "位置情報"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
reply: "返信"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
@ -1376,7 +1375,6 @@ mobile/views/components/friends-maker.vue:
|
||||
close: "閉じる"
|
||||
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
@ -1384,7 +1382,6 @@ mobile/views/components/note.vue:
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "返信"
|
||||
reaction: "リアクション"
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
|
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "このユーザー情報はコピーです。"
|
||||
is-remote-post: "この投稿情報はコピーです。"
|
||||
view-on-remote: "ちゃんとした情報見せてや!"
|
||||
renoted-by: "{user}がRenote"
|
||||
error:
|
||||
title: '問題が起こったわ'
|
||||
retry: 'もっぺん'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "この投稿は見せられへんわ"
|
||||
deleted: "この投稿なんか無くなってもうたわ"
|
||||
reposted-by: "{}がRenote"
|
||||
location: "ここおるで:"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
reply: "返す"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "もっとあるやろ!"
|
||||
close: "さいなら"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は見せられへんわ"
|
||||
deleted: "この投稿なんか無くなってもうたわ"
|
||||
location: "ここおるで:"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "返す"
|
||||
reaction: "リアクション"
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は見せられへんわ"
|
||||
deleted: "この投稿なんか無くなってもうたわ"
|
||||
location: "ここおるで:"
|
||||
|
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "このユーザー情報はコピーです。"
|
||||
is-remote-post: "この投稿情報はコピーです。"
|
||||
view-on-remote: "正確な情報を見る"
|
||||
renoted-by: "{user}がRenote"
|
||||
error:
|
||||
title: '問題が発生しました'
|
||||
retry: 'やり直す'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
reposted-by: "{}がRenote"
|
||||
location: "位置情報"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
reply: "返信"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "もっと見る"
|
||||
close: "閉じる"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "返信"
|
||||
reaction: "リアクション"
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
|
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "このユーザー情報はコピーです。"
|
||||
is-remote-post: "この投稿情報はコピーです。"
|
||||
view-on-remote: "正確な情報を見る"
|
||||
renoted-by: "{user}がRenote"
|
||||
error:
|
||||
title: '問題が発生しました'
|
||||
retry: 'やり直す'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "(dit bericht is privé)"
|
||||
deleted: "この投稿は削除されました"
|
||||
reposted-by: "{}がRenote"
|
||||
location: "Locatie"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
reply: "返信"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "もっと見る"
|
||||
close: "閉じる"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "Renote door {}"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "Beantwoorden"
|
||||
reaction: "Reactie"
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
|
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "このユーザー情報はコピーです。"
|
||||
is-remote-post: "この投稿情報はコピーです。"
|
||||
view-on-remote: "正確な情報を見る"
|
||||
renoted-by: "{user}がRenote"
|
||||
error:
|
||||
title: '問題が発生しました'
|
||||
retry: 'やり直す'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
reposted-by: "{}がRenote"
|
||||
location: "Lokasjon"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
reply: "返信"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "Se mer"
|
||||
close: "Lukk"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "Lokasjon"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "Svar"
|
||||
reaction: "リアクション"
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "Lokasjon"
|
||||
|
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "このユーザー情報はコピーです。"
|
||||
is-remote-post: "この投稿情報はコピーです。"
|
||||
view-on-remote: "正確な情報を見る"
|
||||
renoted-by: "{user}がRenote"
|
||||
error:
|
||||
title: '問題が発生しました'
|
||||
retry: 'やり直す'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "ten wpis jest prywatny"
|
||||
deleted: "ten wpis został usunięty"
|
||||
reposted-by: "Udostępniono przez {}"
|
||||
location: "Informacje o lokalizacji"
|
||||
renote: "Udostępnienie"
|
||||
add-reaction: "Dodaj reakcję"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
reply: "返信"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "Więcej"
|
||||
close: "Zamknij"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "Udostępniono przez {}"
|
||||
private: "ten wpis jest prywatny"
|
||||
deleted: "ten wpis został usunięty"
|
||||
location: "Informacje o lokalizacji"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "Odpowiedz"
|
||||
reaction: "Reakcja"
|
||||
reposted-by: "Udostępniono przez {}"
|
||||
private: "ten wpis jest prywatny"
|
||||
deleted: "ten wpis został usunięty"
|
||||
location: "Informacje o lokalizacji"
|
||||
|
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "このユーザー情報はコピーです。"
|
||||
is-remote-post: "この投稿情報はコピーです。"
|
||||
view-on-remote: "正確な情報を見る"
|
||||
renoted-by: "{user}がRenote"
|
||||
error:
|
||||
title: '問題が発生しました'
|
||||
retry: 'やり直す'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
reposted-by: "{}がRenote"
|
||||
location: "位置情報"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
reply: "返信"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "もっと見る"
|
||||
close: "閉じる"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "返信"
|
||||
reaction: "リアクション"
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
|
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "このユーザー情報はコピーです。"
|
||||
is-remote-post: "この投稿情報はコピーです。"
|
||||
view-on-remote: "正確な情報を見る"
|
||||
renoted-by: "{user}がRenote"
|
||||
error:
|
||||
title: '問題が発生しました'
|
||||
retry: 'やり直す'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
reposted-by: "{}がRenote"
|
||||
location: "位置情報"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
reply: "返信"
|
||||
renote: "Renote"
|
||||
add-reaction: "リアクション"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "もっと見る"
|
||||
close: "閉じる"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "返信"
|
||||
reaction: "リアクション"
|
||||
reposted-by: "{}がRenote"
|
||||
private: "この投稿は非公開です"
|
||||
deleted: "この投稿は削除されました"
|
||||
location: "位置情報"
|
||||
|
@ -115,7 +115,7 @@ common:
|
||||
always-show-nsfw: "总是显示 NSFW 的内容"
|
||||
always-mark-nsfw: "总是用 NSFW 来标记附件"
|
||||
show-full-acct: "不要从用户名中忽略主机名"
|
||||
show-via: "显示..."
|
||||
show-via: "显示 via"
|
||||
reduce-motion: "减弱UI中的动画效果"
|
||||
this-setting-is-this-device-only: "设置仅在本设备中生效"
|
||||
use-os-default-emojis: "使用设备系统默认的 emojis"
|
||||
@ -123,6 +123,7 @@ common:
|
||||
is-remote-user: "该用户的信息已被复制."
|
||||
is-remote-post: "该投稿已被复制."
|
||||
view-on-remote: "查看准确的信息"
|
||||
renoted-by: "由 {user} Renote"
|
||||
error:
|
||||
title: '哦不, 发生了一些问题! :('
|
||||
retry: '重试'
|
||||
@ -643,12 +644,10 @@ desktop/views/components/messaging-window.vue:
|
||||
desktop/views/components/note-detail.vue:
|
||||
private: "私密投稿"
|
||||
deleted: "投稿已删除"
|
||||
reposted-by: "由{}转发"
|
||||
location: "位置信息"
|
||||
renote: "转发"
|
||||
add-reaction: "添加一个反应"
|
||||
desktop/views/components/note.vue:
|
||||
reposted-by: "由{}转发"
|
||||
reply: "回复"
|
||||
renote: "Renote"
|
||||
add-reaction: "添加一个反应"
|
||||
@ -1212,14 +1211,12 @@ mobile/views/components/friends-maker.vue:
|
||||
refresh: "浏览更多"
|
||||
close: "关闭"
|
||||
mobile/views/components/note.vue:
|
||||
reposted-by: "由{}转发"
|
||||
private: "私密帖子"
|
||||
deleted: "帖子已删除"
|
||||
location: "位置信息"
|
||||
mobile/views/components/note-detail.vue:
|
||||
reply: "回复"
|
||||
reaction: "反应"
|
||||
reposted-by: "由{}Renote"
|
||||
private: "这个帖子是私密的"
|
||||
deleted: "帖子已删除"
|
||||
location: "位置信息"
|
||||
|
10
package.json
10
package.json
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "10.55.0",
|
||||
"clientVersion": "2.0.11909",
|
||||
"version": "10.56.0",
|
||||
"clientVersion": "2.0.11957",
|
||||
"codename": "nighthike",
|
||||
"main": "./built/index.js",
|
||||
"private": true,
|
||||
@ -65,6 +65,7 @@
|
||||
"@types/ms": "0.7.30",
|
||||
"@types/node": "10.12.2",
|
||||
"@types/oauth": "0.9.1",
|
||||
"@types/parsimmon": "1.10.0",
|
||||
"@types/portscanner": "2.1.0",
|
||||
"@types/pug": "2.0.4",
|
||||
"@types/qrcode": "1.3.0",
|
||||
@ -86,7 +87,7 @@
|
||||
"@types/websocket": "0.0.40",
|
||||
"@types/ws": "6.0.1",
|
||||
"animejs": "2.2.0",
|
||||
"apexcharts": "2.2.0",
|
||||
"apexcharts": "2.2.2",
|
||||
"autobind-decorator": "2.2.1",
|
||||
"autosize": "4.0.2",
|
||||
"autwh": "0.1.0",
|
||||
@ -170,6 +171,7 @@
|
||||
"on-build-webpack": "0.1.0",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "5.1.0",
|
||||
"parsimmon": "1.12.0",
|
||||
"portscanner": "2.2.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"progress-bar-webpack-plugin": "1.11.0",
|
||||
@ -209,7 +211,7 @@
|
||||
"ts-node": "7.0.1",
|
||||
"tslint": "5.10.0",
|
||||
"typescript": "3.1.6",
|
||||
"typescript-eslint-parser": "21.0.0",
|
||||
"typescript-eslint-parser": "21.0.1",
|
||||
"uglify-es": "3.3.9",
|
||||
"url-loader": "1.1.2",
|
||||
"uuid": "3.3.2",
|
||||
|
@ -41,6 +41,7 @@
|
||||
if (`${url.pathname}/`.startsWith('/dev/')) app = 'dev';
|
||||
if (`${url.pathname}/`.startsWith('/auth/')) app = 'auth';
|
||||
if (`${url.pathname}/`.startsWith('/admin/')) app = 'admin';
|
||||
if (`${url.pathname}/`.startsWith('/test/')) app = 'test';
|
||||
//#endregion
|
||||
|
||||
// Script version
|
||||
|
@ -65,5 +65,6 @@ export default Vue.extend({
|
||||
max-height 100%
|
||||
margin auto
|
||||
cursor zoom-out
|
||||
image-orientation from-image
|
||||
|
||||
</style>
|
||||
|
@ -10,13 +10,14 @@ import trends from './trends.vue';
|
||||
import analogClock from './analog-clock.vue';
|
||||
import menu from './menu.vue';
|
||||
import noteHeader from './note-header.vue';
|
||||
import renote from './renote.vue';
|
||||
import signin from './signin.vue';
|
||||
import signup from './signup.vue';
|
||||
import forkit from './forkit.vue';
|
||||
import acct from './acct.vue';
|
||||
import avatar from './avatar.vue';
|
||||
import nav from './nav.vue';
|
||||
import misskeyFlavoredMarkdown from './misskey-flavored-markdown';
|
||||
import misskeyFlavoredMarkdown from './misskey-flavored-markdown.vue';
|
||||
import poll from './poll.vue';
|
||||
import pollEditor from './poll-editor.vue';
|
||||
import reactionIcon from './reaction-icon.vue';
|
||||
@ -53,6 +54,7 @@ Vue.component('mk-trends', trends);
|
||||
Vue.component('mk-analog-clock', analogClock);
|
||||
Vue.component('mk-menu', menu);
|
||||
Vue.component('mk-note-header', noteHeader);
|
||||
Vue.component('mk-renote', renote);
|
||||
Vue.component('mk-signin', signin);
|
||||
Vue.component('mk-signup', signup);
|
||||
Vue.component('mk-forkit', forkit);
|
||||
|
@ -1,11 +1,39 @@
|
||||
import Vue, { VNode } from 'vue';
|
||||
import { length } from 'stringz';
|
||||
import { Node } from '../../../../../mfm/parser';
|
||||
import parse from '../../../../../mfm/parse';
|
||||
import getAcct from '../../../../../misc/acct/render';
|
||||
import MkUrl from './url.vue';
|
||||
import { concat } from '../../../../../prelude/array';
|
||||
import MkFormula from './formula.vue';
|
||||
import MkGoogle from './google.vue';
|
||||
import { toUnicode } from 'punycode';
|
||||
import syntaxHighlight from '../../../../../mfm/syntax-highlight';
|
||||
|
||||
function getText(tokens: Node[]): string {
|
||||
let text = '';
|
||||
const extract = (tokens: Node[]) => {
|
||||
tokens.filter(x => x.name === 'text').forEach(x => {
|
||||
text += x.props.text;
|
||||
});
|
||||
tokens.filter(x => x.children).forEach(x => {
|
||||
extract(x.children);
|
||||
});
|
||||
};
|
||||
extract(tokens);
|
||||
return text;
|
||||
}
|
||||
|
||||
function getChildrenCount(tokens: Node[]): number {
|
||||
let count = 0;
|
||||
const extract = (tokens: Node[]) => {
|
||||
tokens.filter(x => x.children).forEach(x => {
|
||||
count++;
|
||||
extract(x.children);
|
||||
});
|
||||
};
|
||||
extract(tokens);
|
||||
return count;
|
||||
}
|
||||
|
||||
export default Vue.component('misskey-flavored-markdown', {
|
||||
props: {
|
||||
@ -21,6 +49,10 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
author: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
i: {
|
||||
type: Object,
|
||||
default: null
|
||||
@ -31,23 +63,24 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
},
|
||||
|
||||
render(createElement) {
|
||||
let ast: any[];
|
||||
if (this.text == null || this.text == '') return;
|
||||
|
||||
let ast: Node[];
|
||||
|
||||
if (this.ast == null) {
|
||||
// Parse text to ast
|
||||
ast = parse(this.text);
|
||||
} else {
|
||||
ast = this.ast as any[];
|
||||
ast = this.ast as Node[];
|
||||
}
|
||||
|
||||
let bigCount = 0;
|
||||
let motionCount = 0;
|
||||
|
||||
// Parse ast to DOM
|
||||
const els = concat(ast.map((token): VNode[] => {
|
||||
switch (token.type) {
|
||||
const genEl = (ast: Node[]) => concat(ast.map((token): VNode[] => {
|
||||
switch (token.name) {
|
||||
case 'text': {
|
||||
const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
|
||||
const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
|
||||
|
||||
if (this.shouldBreak) {
|
||||
const x = text.split('\n')
|
||||
@ -60,12 +93,12 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
}
|
||||
|
||||
case 'bold': {
|
||||
return [createElement('b', token.bold)];
|
||||
return [createElement('b', genEl(token.children))];
|
||||
}
|
||||
|
||||
case 'big': {
|
||||
bigCount++;
|
||||
const isLong = length(token.big) > 10;
|
||||
const isLong = length(getText(token.children)) > 10 || getChildrenCount(token.children) > 5;
|
||||
const isMany = bigCount > 3;
|
||||
return (createElement as any)('strong', {
|
||||
attrs: {
|
||||
@ -75,12 +108,12 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
name: 'animate-css',
|
||||
value: { classes: 'tada', iteration: 'infinite' }
|
||||
}]
|
||||
}, token.big);
|
||||
}, genEl(token.children));
|
||||
}
|
||||
|
||||
case 'motion': {
|
||||
motionCount++;
|
||||
const isLong = length(token.motion) > 10;
|
||||
const isLong = length(getText(token.children)) > 10 || getChildrenCount(token.children) > 5;
|
||||
const isMany = motionCount > 3;
|
||||
return (createElement as any)('span', {
|
||||
attrs: {
|
||||
@ -90,13 +123,14 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
name: 'animate-css',
|
||||
value: { classes: 'rubberBand', iteration: 'infinite' }
|
||||
}]
|
||||
}, token.motion);
|
||||
}, genEl(token.children));
|
||||
}
|
||||
|
||||
case 'url': {
|
||||
return [createElement(MkUrl, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
url: token.content,
|
||||
url: token.props.url,
|
||||
target: '_blank',
|
||||
style: 'color:var(--mfmLink);'
|
||||
}
|
||||
@ -107,75 +141,75 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
return [createElement('a', {
|
||||
attrs: {
|
||||
class: 'link',
|
||||
href: token.url,
|
||||
href: token.props.url,
|
||||
target: '_blank',
|
||||
title: token.url,
|
||||
title: token.props.url,
|
||||
style: 'color:var(--mfmLink);'
|
||||
}
|
||||
}, token.title)];
|
||||
}, genEl(token.children))];
|
||||
}
|
||||
|
||||
case 'mention': {
|
||||
const host = token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host;
|
||||
const canonical = host != null ? `@${token.props.username}@${toUnicode(host)}` : `@${token.props.username}`;
|
||||
return (createElement as any)('router-link', {
|
||||
key: Math.random(),
|
||||
attrs: {
|
||||
to: `/${token.canonical}`,
|
||||
dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token),
|
||||
to: `/${canonical}`,
|
||||
// TODO
|
||||
//dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token),
|
||||
style: 'color:var(--mfmMention);'
|
||||
},
|
||||
directives: [{
|
||||
name: 'user-preview',
|
||||
value: token.canonical
|
||||
value: canonical
|
||||
}]
|
||||
}, token.canonical);
|
||||
}, canonical);
|
||||
}
|
||||
|
||||
case 'hashtag': {
|
||||
return [createElement('router-link', {
|
||||
key: Math.random(),
|
||||
attrs: {
|
||||
to: `/tags/${encodeURIComponent(token.hashtag)}`,
|
||||
to: `/tags/${encodeURIComponent(token.props.hashtag)}`,
|
||||
style: 'color:var(--mfmHashtag);'
|
||||
}
|
||||
}, token.content)];
|
||||
}, `#${token.props.hashtag}`)];
|
||||
}
|
||||
|
||||
case 'code': {
|
||||
case 'blockCode': {
|
||||
return [createElement('pre', {
|
||||
class: 'code'
|
||||
}, [
|
||||
createElement('code', {
|
||||
domProps: {
|
||||
innerHTML: token.html
|
||||
innerHTML: syntaxHighlight(token.props.code)
|
||||
}
|
||||
})
|
||||
])];
|
||||
}
|
||||
|
||||
case 'inline-code': {
|
||||
case 'inlineCode': {
|
||||
return [createElement('code', {
|
||||
domProps: {
|
||||
innerHTML: token.html
|
||||
innerHTML: syntaxHighlight(token.props.code)
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
case 'quote': {
|
||||
const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n');
|
||||
|
||||
if (this.shouldBreak) {
|
||||
const x = text2.split('\n')
|
||||
.map(t => [createElement('span', t), createElement('br')]);
|
||||
x[x.length - 1].pop();
|
||||
return [createElement('div', {
|
||||
attrs: {
|
||||
class: 'quote'
|
||||
}
|
||||
}, x)];
|
||||
}, genEl(token.children))];
|
||||
} else {
|
||||
return [createElement('span', {
|
||||
attrs: {
|
||||
class: 'quote'
|
||||
}
|
||||
}, text2.replace(/\n/g, ' '))];
|
||||
}, genEl(token.children))];
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,15 +218,16 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
attrs: {
|
||||
class: 'title'
|
||||
}
|
||||
}, token.title)];
|
||||
}, genEl(token.children))];
|
||||
}
|
||||
|
||||
case 'emoji': {
|
||||
const customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
|
||||
return [createElement('mk-emoji', {
|
||||
key: Math.random(),
|
||||
attrs: {
|
||||
emoji: token.emoji,
|
||||
name: token.name
|
||||
emoji: token.props.emoji,
|
||||
name: token.props.name
|
||||
},
|
||||
props: {
|
||||
customEmojis: this.customEmojis || customEmojis
|
||||
@ -203,8 +238,9 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
case 'math': {
|
||||
//const MkFormula = () => import('./formula.vue').then(m => m.default);
|
||||
return [createElement(MkFormula, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
formula: token.formula
|
||||
formula: token.props.formula
|
||||
}
|
||||
})];
|
||||
}
|
||||
@ -212,22 +248,22 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
case 'search': {
|
||||
//const MkGoogle = () => import('./google.vue').then(m => m.default);
|
||||
return [createElement(MkGoogle, {
|
||||
key: Math.random(),
|
||||
props: {
|
||||
q: token.query
|
||||
q: token.props.query
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
default: {
|
||||
console.log('unknown ast type:', token.type);
|
||||
console.log('unknown ast type:', token.name);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// el.tag === 'br' のとき i !== 0 が保証されるため、短絡評価により els[i - 1] は配列外参照しない
|
||||
const _els = els.filter((el, i) => !(el.tag === 'br' && ['div', 'pre'].includes(els[i - 1].tag)));
|
||||
return createElement('span', _els);
|
||||
// Parse ast to DOM
|
||||
return createElement('span', genEl(ast));
|
||||
}
|
||||
});
|
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<mfm v-bind="$attrs" class="havbbuyv"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import Mfm from './mfm';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
Mfm
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.havbbuyv
|
||||
>>> .title
|
||||
display block
|
||||
margin-bottom 4px
|
||||
padding 4px
|
||||
font-size 90%
|
||||
text-align center
|
||||
background var(--mfmTitleBg)
|
||||
border-radius 4px
|
||||
|
||||
>>> .code
|
||||
margin 8px 0
|
||||
|
||||
>>> .quote
|
||||
margin 8px
|
||||
padding 6px 12px
|
||||
color var(--mfmQuote)
|
||||
border-left solid 3px var(--mfmQuoteLine)
|
||||
|
||||
>>> code
|
||||
padding 4px 8px
|
||||
margin 0 0.5em
|
||||
font-size 80%
|
||||
color #525252
|
||||
background #f8f8f8
|
||||
border-radius 2px
|
||||
|
||||
>>> pre > code
|
||||
padding 16px
|
||||
margin 0
|
||||
|
||||
>>> [data-is-me]:after
|
||||
content "you"
|
||||
padding 0 4px
|
||||
margin-left 4px
|
||||
font-size 80%
|
||||
color var(--primaryForeground)
|
||||
background var(--primary)
|
||||
border-radius 4px
|
||||
|
||||
</style>
|
108
src/client/app/common/views/components/renote.vue
Normal file
108
src/client/app/common/views/components/renote.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="puqkfets" :class="{ mini }">
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
<fa icon="retweet"/>
|
||||
<i18n path="@.renoted-by" tag="span">
|
||||
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId" place="user">{{ note.user | userName }}</router-link>
|
||||
</i18n>
|
||||
<div class="info">
|
||||
<span class="mobile" v-if="note.viaMobile"><fa icon="mobile-alt"/></span>
|
||||
<mk-time :time="note.createdAt"/>
|
||||
<span class="visibility" v-if="note.visibility != 'public'">
|
||||
<fa v-if="note.visibility == 'home'" icon="home"/>
|
||||
<fa v-if="note.visibility == 'followers'" icon="unlock"/>
|
||||
<fa v-if="note.visibility == 'specified'" icon="envelope"/>
|
||||
<fa v-if="note.visibility == 'private'" icon="lock"/>
|
||||
</span>
|
||||
<span class="localOnly" v-if="note.localOnly == true"><fa icon="heart"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
mini: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.puqkfets
|
||||
display flex
|
||||
align-items center
|
||||
padding 16px 32px 8px 32px
|
||||
line-height 28px
|
||||
white-space pre
|
||||
color var(--renoteText)
|
||||
background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%)
|
||||
|
||||
&.mini
|
||||
padding 8px 16px
|
||||
|
||||
@media (min-width 500px)
|
||||
padding 16px
|
||||
|
||||
@media (min-width 600px)
|
||||
padding 16px 32px
|
||||
|
||||
> .avatar
|
||||
@media (min-width 500px)
|
||||
width 28px
|
||||
height 28px
|
||||
|
||||
> .avatar
|
||||
flex-shrink 0
|
||||
display inline-block
|
||||
width 28px
|
||||
height 28px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
> [data-icon]
|
||||
margin-right 4px
|
||||
|
||||
> span
|
||||
overflow hidden
|
||||
flex-shrink 1
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
|
||||
> .name
|
||||
font-weight bold
|
||||
|
||||
> .info
|
||||
margin-left auto
|
||||
font-size 0.9em
|
||||
|
||||
> .mobile
|
||||
margin-right 8px
|
||||
|
||||
> .mk-time
|
||||
flex-shrink 0
|
||||
|
||||
> .visibility
|
||||
margin-left 8px
|
||||
|
||||
[data-icon]
|
||||
margin-right 0
|
||||
|
||||
> .localOnly
|
||||
margin-left 4px
|
||||
|
||||
[data-icon]
|
||||
margin-right 0
|
||||
|
||||
</style>
|
@ -14,7 +14,7 @@
|
||||
</div>
|
||||
</header>
|
||||
<div class="text">
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :customEmojis="note.emojis"/>
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :author="note.user" :custom-emojis="note.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<router-link :to="user | userPage" class="name">{{ user | userName }}</router-link>
|
||||
<span class="username">@{{ user | acct }}</span>
|
||||
<div class="description">
|
||||
<misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
|
||||
<misskey-flavored-markdown v-if="user.description" :text="user.description" :author="user" :i="$store.state.i"/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<x-pie class="pie" :value="usage"/>
|
||||
<div>
|
||||
<p><fa icon="microchip"/>CPU</p>
|
||||
<p>{{ meta.cpu.cores }} Cores</p>
|
||||
<p>{{ meta.cpu.cores }} Logical cores</p>
|
||||
<p>{{ meta.cpu.model }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="mk-note-detail" :title="title">
|
||||
<button
|
||||
class="read-more"
|
||||
v-if="p.reply && p.reply.replyId && conversation.length == 0"
|
||||
v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0"
|
||||
:title="$t('title')"
|
||||
@click="fetchConversation"
|
||||
:disabled="conversationFetching"
|
||||
@ -13,65 +13,66 @@
|
||||
<div class="conversation">
|
||||
<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
|
||||
</div>
|
||||
<div class="reply-to" v-if="p.reply">
|
||||
<x-sub :note="p.reply"/>
|
||||
</div>
|
||||
<div class="renote" v-if="isRenote">
|
||||
<p>
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
<fa icon="retweet"/>
|
||||
<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
|
||||
<span>{{ this.$t('reposted-by').substr(0, this.$t('reposted-by').indexOf('{')) }}</span>
|
||||
<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
|
||||
<span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span>
|
||||
<mk-time :time="note.createdAt"/>
|
||||
</p>
|
||||
<div class="reply-to" v-if="appearNote.reply">
|
||||
<x-sub :note="appearNote.reply"/>
|
||||
</div>
|
||||
<mk-renote class="renote" v-if="isRenote" :note="note"/>
|
||||
<article>
|
||||
<mk-avatar class="avatar" :user="p.user"/>
|
||||
<mk-avatar class="avatar" :user="appearNote.user"/>
|
||||
<header>
|
||||
<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
|
||||
<span class="username"><mk-acct :user="p.user"/></span>
|
||||
<router-link class="time" :to="p | notePage">
|
||||
<mk-time :time="p.createdAt"/>
|
||||
</router-link>
|
||||
<router-link class="name" :to="appearNote.user | userPage" v-user-preview="appearNote.user.id">{{ appearNote.user | userName }}</router-link>
|
||||
<span class="username"><mk-acct :user="appearNote.user"/></span>
|
||||
<div class="info">
|
||||
<router-link class="time" :to="appearNote | notePage">
|
||||
<mk-time :time="appearNote.createdAt"/>
|
||||
</router-link>
|
||||
<div class="visibility-info">
|
||||
<span class="visibility" v-if="appearNote.visibility != 'public'">
|
||||
<fa v-if="appearNote.visibility == 'home'" icon="home"/>
|
||||
<fa v-if="appearNote.visibility == 'followers'" icon="unlock"/>
|
||||
<fa v-if="appearNote.visibility == 'specified'" icon="envelope"/>
|
||||
<fa v-if="appearNote.visibility == 'private'" icon="lock"/>
|
||||
</span>
|
||||
<span class="localOnly" v-if="appearNote.localOnly == true"><fa icon="heart"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p v-if="p.cw != null" class="cw">
|
||||
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span>
|
||||
<mk-cw-button v-model="showContent"/>
|
||||
</p>
|
||||
<div class="content" v-show="p.cw == null || showContent">
|
||||
<div class="content" v-show="appearNote.cw == null || showContent">
|
||||
<div class="text">
|
||||
<span v-if="p.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
|
||||
<span v-if="p.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span>
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :customEmojis="p.emojis" />
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
|
||||
<span v-if="appearNote.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
|
||||
</div>
|
||||
<div class="files" v-if="p.files.length > 0">
|
||||
<mk-media-list :media-list="p.files" :raw="true"/>
|
||||
<div class="files" v-if="appearNote.files.length > 0">
|
||||
<mk-media-list :media-list="appearNote.files" :raw="true"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p"/>
|
||||
<mk-poll v-if="appearNote.poll" :note="appearNote"/>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
|
||||
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
|
||||
<div class="map" v-if="p.geo" ref="map"></div>
|
||||
<div class="renote" v-if="p.renote">
|
||||
<mk-note-preview :note="p.renote"/>
|
||||
<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
|
||||
<div class="map" v-if="appearNote.geo" ref="map"></div>
|
||||
<div class="renote" v-if="appearNote.renote">
|
||||
<mk-note-preview :note="appearNote.renote"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<span class="app" v-if="note.app && $store.state.settings.showVia">via <b>{{ note.app.name }}</b></span>
|
||||
<mk-reactions-viewer :note="p"/>
|
||||
<mk-reactions-viewer :note="appearNote"/>
|
||||
<button class="replyButton" @click="reply" :title="$t('reply')">
|
||||
<template v-if="p.reply"><fa icon="reply-all"/></template>
|
||||
<template v-if="appearNote.reply"><fa icon="reply-all"/></template>
|
||||
<template v-else><fa icon="reply"/></template>
|
||||
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
||||
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<button class="renoteButton" @click="renote" :title="$t('renote')">
|
||||
<fa icon="retweet"/><p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
|
||||
<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
|
||||
</button>
|
||||
<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" :title="$t('add-reaction')">
|
||||
<fa icon="plus"/><p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
||||
<button class="reactionButton" :class="{ reacted: appearNote.myReaction != null }" @click="react" ref="reactButton" :title="$t('add-reaction')">
|
||||
<fa icon="plus"/><p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
|
||||
</button>
|
||||
<button @click="menu" ref="menuButton">
|
||||
<fa icon="ellipsis-h"/>
|
||||
@ -132,23 +133,23 @@ export default Vue.extend({
|
||||
this.note.poll == null);
|
||||
},
|
||||
|
||||
p(): any {
|
||||
appearNote(): any {
|
||||
return this.isRenote ? this.note.renote : this.note;
|
||||
},
|
||||
|
||||
reactionsCount(): number {
|
||||
return this.p.reactionCounts
|
||||
? sum(Object.values(this.p.reactionCounts))
|
||||
return this.appearNote.reactionCounts
|
||||
? sum(Object.values(this.appearNote.reactionCounts))
|
||||
: 0;
|
||||
},
|
||||
|
||||
title(): string {
|
||||
return new Date(this.p.createdAt).toLocaleString();
|
||||
return new Date(this.appearNote.createdAt).toLocaleString();
|
||||
},
|
||||
|
||||
urls(): string[] {
|
||||
if (this.p.text) {
|
||||
const ast = parse(this.p.text);
|
||||
if (this.appearNote.text) {
|
||||
const ast = parse(this.appearNote.text);
|
||||
return unique(ast
|
||||
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
|
||||
.map(t => t.url));
|
||||
@ -162,7 +163,7 @@ export default Vue.extend({
|
||||
// Get replies
|
||||
if (!this.compact) {
|
||||
this.$root.api('notes/replies', {
|
||||
noteId: this.p.id,
|
||||
noteId: this.appearNote.id,
|
||||
limit: 8
|
||||
}).then(replies => {
|
||||
this.replies = replies;
|
||||
@ -170,11 +171,11 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
// Draw map
|
||||
if (this.p.geo) {
|
||||
if (this.appearNote.geo) {
|
||||
const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true;
|
||||
if (shouldShowMap) {
|
||||
this.$root.os.getGoogleMaps().then(maps => {
|
||||
const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
|
||||
const uluru = new maps.LatLng(this.appearNote.geo.coordinates[1], this.appearNote.geo.coordinates[0]);
|
||||
const map = new maps.Map(this.$refs.map, {
|
||||
center: uluru,
|
||||
zoom: 15
|
||||
@ -194,7 +195,7 @@ export default Vue.extend({
|
||||
|
||||
// Fetch conversation
|
||||
this.$root.api('notes/conversation', {
|
||||
noteId: this.p.replyId
|
||||
noteId: this.appearNote.replyId
|
||||
}).then(conversation => {
|
||||
this.conversationFetching = false;
|
||||
this.conversation = conversation.reverse();
|
||||
@ -203,27 +204,27 @@ export default Vue.extend({
|
||||
|
||||
reply() {
|
||||
this.$root.new(MkPostFormWindow, {
|
||||
reply: this.p
|
||||
reply: this.appearNote
|
||||
});
|
||||
},
|
||||
|
||||
renote() {
|
||||
this.$root.new(MkRenoteFormWindow, {
|
||||
note: this.p
|
||||
note: this.appearNote
|
||||
});
|
||||
},
|
||||
|
||||
react() {
|
||||
this.$root.new(MkReactionPicker, {
|
||||
source: this.$refs.reactButton,
|
||||
note: this.p
|
||||
note: this.appearNote
|
||||
});
|
||||
},
|
||||
|
||||
menu() {
|
||||
this.$root.new(MkNoteMenu, {
|
||||
source: this.$refs.menuButton,
|
||||
note: this.p
|
||||
note: this.appearNote
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -266,29 +267,8 @@ export default Vue.extend({
|
||||
> *
|
||||
border-bottom 1px solid var(--faceDivider)
|
||||
|
||||
> .renote
|
||||
color var(--renoteText)
|
||||
background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%)
|
||||
|
||||
> p
|
||||
margin 0
|
||||
padding 16px 32px
|
||||
|
||||
.avatar
|
||||
display inline-block
|
||||
width 28px
|
||||
height 28px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
[data-icon]
|
||||
margin-right 4px
|
||||
|
||||
.name
|
||||
font-weight bold
|
||||
|
||||
& + article
|
||||
padding-top 8px
|
||||
> .renote + article
|
||||
padding-top 8px
|
||||
|
||||
> .reply-to
|
||||
border-bottom 1px solid var(--faceDivider)
|
||||
@ -335,12 +315,21 @@ export default Vue.extend({
|
||||
margin 0
|
||||
color var(--noteHeaderAcct)
|
||||
|
||||
> .time
|
||||
> .info
|
||||
position absolute
|
||||
top 0
|
||||
right 32px
|
||||
font-size 1em
|
||||
color var(--noteHeaderInfo)
|
||||
|
||||
> .time
|
||||
color var(--noteHeaderInfo)
|
||||
|
||||
> .visibility-info
|
||||
text-align: right
|
||||
color var(--noteHeaderInfo)
|
||||
|
||||
> .localOnly
|
||||
margin-left 4px
|
||||
|
||||
> .body
|
||||
padding 8px 0
|
||||
|
@ -13,21 +13,7 @@
|
||||
<div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
|
||||
<x-sub :note="appearNote.reply" :mini="mini"/>
|
||||
</div>
|
||||
<div class="renote" v-if="isRenote">
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
<fa icon="retweet"/>
|
||||
<span>{{ this.$t('reposted-by').substr(0, this.$t('reposted-by').indexOf('{')) }}</span>
|
||||
<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
|
||||
<span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span>
|
||||
<mk-time :time="note.createdAt"/>
|
||||
<span class="visibility" v-if="note.visibility != 'public'">
|
||||
<fa v-if="note.visibility == 'home'" icon="home"/>
|
||||
<fa v-if="note.visibility == 'followers'" icon="unlock"/>
|
||||
<fa v-if="note.visibility == 'specified'" icon="envelope"/>
|
||||
<fa v-if="note.visibility == 'private'" icon="lock"/>
|
||||
</span>
|
||||
<span class="localOnly" v-if="note.localOnly == true"><fa icon="heart"/></span>
|
||||
</div>
|
||||
<mk-renote class="renote" v-if="isRenote" :note="note"/>
|
||||
<article>
|
||||
<mk-avatar class="avatar" :user="appearNote.user"/>
|
||||
<div class="main">
|
||||
@ -41,7 +27,7 @@
|
||||
<div class="text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
|
||||
<a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text" :customEmojis="appearNote.emojis"/>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
|
||||
<a class="rp" v-if="appearNote.renote">RN:</a>
|
||||
</div>
|
||||
<div class="files" v-if="appearNote.files.length > 0">
|
||||
@ -185,56 +171,8 @@ export default Vue.extend({
|
||||
border 2px solid var(--primaryAlpha03)
|
||||
border-radius 4px
|
||||
|
||||
> .renote
|
||||
display flex
|
||||
align-items center
|
||||
padding 16px 32px 8px 32px
|
||||
line-height 28px
|
||||
white-space pre
|
||||
color var(--renoteText)
|
||||
background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%)
|
||||
|
||||
.avatar
|
||||
flex-shrink 0
|
||||
display inline-block
|
||||
width 28px
|
||||
height 28px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
[data-icon]
|
||||
margin-right 4px
|
||||
|
||||
> span
|
||||
flex-shrink 0
|
||||
|
||||
.name
|
||||
overflow hidden
|
||||
flex-shrink 1
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
font-weight bold
|
||||
|
||||
> .mk-time
|
||||
display block
|
||||
margin-left auto
|
||||
flex-shrink 0
|
||||
font-size 0.9em
|
||||
|
||||
> .visibility
|
||||
margin-left 8px
|
||||
|
||||
[data-icon]
|
||||
margin-right 0
|
||||
|
||||
> .localOnly
|
||||
margin-left 4px
|
||||
|
||||
[data-icon]
|
||||
margin-right 0
|
||||
|
||||
& + article
|
||||
padding-top 8px
|
||||
> .renote + article
|
||||
padding-top 8px
|
||||
|
||||
> article
|
||||
display flex
|
||||
@ -285,24 +223,6 @@ export default Vue.extend({
|
||||
overflow-wrap break-word
|
||||
color var(--noteText)
|
||||
|
||||
>>> .title
|
||||
display block
|
||||
margin-bottom 4px
|
||||
padding 4px
|
||||
font-size 90%
|
||||
text-align center
|
||||
background var(--mfmTitleBg)
|
||||
border-radius 4px
|
||||
|
||||
>>> .code
|
||||
margin 8px 0
|
||||
|
||||
>>> .quote
|
||||
margin 8px
|
||||
padding 6px 12px
|
||||
color var(--mfmQuote)
|
||||
border-left solid 3px var(--mfmQuoteLine)
|
||||
|
||||
> .reply
|
||||
margin-right 8px
|
||||
color var(--text)
|
||||
@ -384,28 +304,3 @@ export default Vue.extend({
|
||||
opacity 0.7
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="stylus" module>
|
||||
.text
|
||||
|
||||
code
|
||||
padding 4px 8px
|
||||
margin 0 0.5em
|
||||
font-size 80%
|
||||
color #525252
|
||||
background #f8f8f8
|
||||
border-radius 2px
|
||||
|
||||
pre > code
|
||||
padding 16px
|
||||
margin 0
|
||||
|
||||
[data-is-me]:after
|
||||
content "you"
|
||||
padding 0 4px
|
||||
margin-left 4px
|
||||
font-size 80%
|
||||
color var(--primaryForeground)
|
||||
background var(--primary)
|
||||
border-radius 4px
|
||||
</style>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<span v-if="note.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span>
|
||||
<a class="reply" v-if="note.replyId"><fa icon="reply"/></a>
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i" :custom-emojis="note.emojis"/>
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/>
|
||||
<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RN: ...</a>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
|
@ -7,7 +7,7 @@
|
||||
<router-link :to="user | userPage" class="name">{{ user | userName }}</router-link>
|
||||
<span class="username">@{{ user | acct }}</span>
|
||||
<div class="description">
|
||||
<misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
|
||||
<misskey-flavored-markdown v-if="user.description" :text="user.description" :author="user" :i="$store.state.i"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -22,7 +22,7 @@
|
||||
</header>
|
||||
<div class="info">
|
||||
<div class="description">
|
||||
<misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
|
||||
<misskey-flavored-markdown v-if="user.description" :text="user.description" :author="user" :i="$store.state.i"/>
|
||||
</div>
|
||||
<div class="counts">
|
||||
<div>
|
||||
|
@ -14,7 +14,7 @@
|
||||
<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
|
||||
<div class="body">
|
||||
<div class="description">
|
||||
<misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
|
||||
<misskey-flavored-markdown v-if="user.description" :text="user.description" :author="user" :i="$store.state.i"/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="location" v-if="user.host === null && user.profile.location"><fa icon="map-marker"/> {{ user.profile.location }}</span>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="mk-note-detail">
|
||||
<button
|
||||
class="more"
|
||||
v-if="p.reply && p.reply.replyId && conversation.length == 0"
|
||||
v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0"
|
||||
@click="fetchConversation"
|
||||
:disabled="conversationFetching"
|
||||
>
|
||||
@ -12,66 +12,65 @@
|
||||
<div class="conversation">
|
||||
<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
|
||||
</div>
|
||||
<div class="reply-to" v-if="p.reply">
|
||||
<x-sub :note="p.reply"/>
|
||||
</div>
|
||||
<div class="renote" v-if="isRenote">
|
||||
<p>
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
<fa icon="retweet"/>
|
||||
<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
|
||||
<span>{{ this.$t('reposted-by').substr(0, this.$t('reposted-by').indexOf('{')) }}</span>
|
||||
<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
|
||||
<span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span>
|
||||
<mk-time :time="note.createdAt"/>
|
||||
</p>
|
||||
<div class="reply-to" v-if="appearNote.reply">
|
||||
<x-sub :note="appearNote.reply"/>
|
||||
</div>
|
||||
<mk-renote class="renote" v-if="isRenote" :note="note" mini/>
|
||||
<article>
|
||||
<header>
|
||||
<mk-avatar class="avatar" :user="p.user"/>
|
||||
<mk-avatar class="avatar" :user="appearNote.user"/>
|
||||
<div>
|
||||
<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
|
||||
<span class="username"><mk-acct :user="p.user"/></span>
|
||||
<router-link class="name" :to="appearNote.user | userPage">{{ appearNote.user | userName }}</router-link>
|
||||
<span class="username"><mk-acct :user="appearNote.user"/></span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p v-if="p.cw != null" class="cw">
|
||||
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span>
|
||||
<mk-cw-button v-model="showContent"/>
|
||||
</p>
|
||||
<div class="content" v-show="p.cw == null || showContent">
|
||||
<div class="content" v-show="appearNote.cw == null || showContent">
|
||||
<div class="text">
|
||||
<span v-if="p.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
|
||||
<span v-if="p.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span>
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :customEmojis="p.emojis"/>
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
|
||||
<span v-if="appearNote.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
|
||||
</div>
|
||||
<div class="files" v-if="p.files.length > 0">
|
||||
<mk-media-list :media-list="p.files" :raw="true"/>
|
||||
<div class="files" v-if="appearNote.files.length > 0">
|
||||
<mk-media-list :media-list="appearNote.files" :raw="true"/>
|
||||
</div>
|
||||
<mk-poll v-if="p.poll" :note="p"/>
|
||||
<mk-poll v-if="appearNote.poll" :note="appearNote"/>
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
|
||||
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
|
||||
<div class="map" v-if="p.geo" ref="map"></div>
|
||||
<div class="renote" v-if="p.renote">
|
||||
<mk-note-preview :note="p.renote"/>
|
||||
<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
|
||||
<div class="map" v-if="appearNote.geo" ref="map"></div>
|
||||
<div class="renote" v-if="appearNote.renote">
|
||||
<mk-note-preview :note="appearNote.renote"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<router-link class="time" :to="p | notePage">
|
||||
<mk-time :time="p.createdAt" mode="detail"/>
|
||||
<router-link class="time" :to="appearNote | notePage">
|
||||
<mk-time :time="appearNote.createdAt" mode="detail"/>
|
||||
</router-link>
|
||||
<div class="visibility-info">
|
||||
<span class="visibility" v-if="appearNote.visibility != 'public'">
|
||||
<fa v-if="appearNote.visibility == 'home'" icon="home"/>
|
||||
<fa v-if="appearNote.visibility == 'followers'" icon="unlock"/>
|
||||
<fa v-if="appearNote.visibility == 'specified'" icon="envelope"/>
|
||||
<fa v-if="appearNote.visibility == 'private'" icon="lock"/>
|
||||
</span>
|
||||
<span class="localOnly" v-if="appearNote.localOnly == true"><fa icon="heart"/></span>
|
||||
</div>
|
||||
<footer>
|
||||
<mk-reactions-viewer :note="p"/>
|
||||
<mk-reactions-viewer :note="appearNote"/>
|
||||
<button @click="reply" :title="$t('title')">
|
||||
<template v-if="p.reply"><fa icon="reply-all"/></template>
|
||||
<template v-if="appearNote.reply"><fa icon="reply-all"/></template>
|
||||
<template v-else><fa icon="reply"/></template>
|
||||
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
|
||||
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<button @click="renote" title="Renote">
|
||||
<fa icon="retweet"/><p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
|
||||
<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
|
||||
</button>
|
||||
<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" :title="$t('title')">
|
||||
<fa icon="plus"/><p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
|
||||
<button :class="{ reacted: appearNote.myReaction != null }" @click="react" ref="reactButton" :title="$t('title')">
|
||||
<fa icon="plus"/><p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
|
||||
</button>
|
||||
<button @click="menu" ref="menuButton">
|
||||
<fa icon="ellipsis-h"/>
|
||||
@ -130,19 +129,19 @@ export default Vue.extend({
|
||||
this.note.poll == null);
|
||||
},
|
||||
|
||||
p(): any {
|
||||
appearNote(): any {
|
||||
return this.isRenote ? this.note.renote : this.note;
|
||||
},
|
||||
|
||||
reactionsCount(): number {
|
||||
return this.p.reactionCounts
|
||||
? sum(Object.values(this.p.reactionCounts))
|
||||
return this.appearNote.reactionCounts
|
||||
? sum(Object.values(this.appearNote.reactionCounts))
|
||||
: 0;
|
||||
},
|
||||
|
||||
urls(): string[] {
|
||||
if (this.p.text) {
|
||||
const ast = parse(this.p.text);
|
||||
if (this.appearNote.text) {
|
||||
const ast = parse(this.appearNote.text);
|
||||
return unique(ast
|
||||
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
|
||||
.map(t => t.url));
|
||||
@ -156,7 +155,7 @@ export default Vue.extend({
|
||||
// Get replies
|
||||
if (!this.compact) {
|
||||
this.$root.api('notes/replies', {
|
||||
noteId: this.p.id,
|
||||
noteId: this.appearNote.id,
|
||||
limit: 8
|
||||
}).then(replies => {
|
||||
this.replies = replies;
|
||||
@ -164,11 +163,11 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
// Draw map
|
||||
if (this.p.geo) {
|
||||
if (this.appearNote.geo) {
|
||||
const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true;
|
||||
if (shouldShowMap) {
|
||||
this.$root.os.getGoogleMaps().then(maps => {
|
||||
const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
|
||||
const uluru = new maps.LatLng(this.appearNote.geo.coordinates[1], this.appearNote.geo.coordinates[0]);
|
||||
const map = new maps.Map(this.$refs.map, {
|
||||
center: uluru,
|
||||
zoom: 15
|
||||
@ -188,7 +187,7 @@ export default Vue.extend({
|
||||
|
||||
// Fetch conversation
|
||||
this.$root.api('notes/conversation', {
|
||||
noteId: this.p.replyId
|
||||
noteId: this.appearNote.replyId
|
||||
}).then(conversation => {
|
||||
this.conversationFetching = false;
|
||||
this.conversation = conversation.reverse();
|
||||
@ -197,20 +196,20 @@ export default Vue.extend({
|
||||
|
||||
reply() {
|
||||
this.$post({
|
||||
reply: this.p
|
||||
reply: this.appearNote
|
||||
});
|
||||
},
|
||||
|
||||
renote() {
|
||||
this.$post({
|
||||
renote: this.p
|
||||
renote: this.appearNote
|
||||
});
|
||||
},
|
||||
|
||||
react() {
|
||||
this.$root.new(MkReactionPicker, {
|
||||
source: this.$refs.reactButton,
|
||||
note: this.p,
|
||||
note: this.appearNote,
|
||||
compact: true,
|
||||
big: true
|
||||
});
|
||||
@ -219,7 +218,7 @@ export default Vue.extend({
|
||||
menu() {
|
||||
this.$root.new(MkNoteMenu, {
|
||||
source: this.$refs.menuButton,
|
||||
note: this.p,
|
||||
note: this.appearNote,
|
||||
compact: true
|
||||
});
|
||||
}
|
||||
@ -268,29 +267,8 @@ export default Vue.extend({
|
||||
> *
|
||||
border-bottom 1px solid var(--faceDivider)
|
||||
|
||||
> .renote
|
||||
color var(--renoteText)
|
||||
background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%)
|
||||
|
||||
> p
|
||||
margin 0
|
||||
padding 16px 32px
|
||||
|
||||
.avatar
|
||||
display inline-block
|
||||
width 28px
|
||||
height 28px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
[data-icon]
|
||||
margin-right 4px
|
||||
|
||||
.name
|
||||
font-weight bold
|
||||
|
||||
& + article
|
||||
padding-top 8px
|
||||
> .renote + article
|
||||
padding-top 8px
|
||||
|
||||
> .reply-to
|
||||
border-bottom 1px solid var(--faceDivider)
|
||||
@ -400,6 +378,12 @@ export default Vue.extend({
|
||||
font-size 16px
|
||||
color var(--noteHeaderInfo)
|
||||
|
||||
> .visibility-info
|
||||
color var(--noteHeaderInfo)
|
||||
|
||||
> .localOnly
|
||||
margin-left 4px
|
||||
|
||||
> footer
|
||||
font-size 1.2em
|
||||
|
||||
|
@ -9,21 +9,7 @@
|
||||
<div class="reply-to" v-if="appearNote.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
|
||||
<x-sub :note="appearNote.reply"/>
|
||||
</div>
|
||||
<div class="renote" v-if="isRenote">
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
<fa icon="retweet"/>
|
||||
<span>{{ this.$t('reposted-by').substr(0, this.$t('reposted-by').indexOf('{')) }}</span>
|
||||
<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
|
||||
<span>{{ this.$t('reposted-by').substr(this.$t('reposted-by').indexOf('}') + 1) }}</span>
|
||||
<mk-time :time="note.createdAt"/>
|
||||
<span class="visibility" v-if="note.visibility != 'public'">
|
||||
<fa v-if="note.visibility == 'home'" icon="home"/>
|
||||
<fa v-if="note.visibility == 'followers'" icon="unlock"/>
|
||||
<fa v-if="note.visibility == 'specified'" icon="envelope"/>
|
||||
<fa v-if="note.visibility == 'private'" icon="lock"/>
|
||||
</span>
|
||||
<span class="localOnly" v-if="note.localOnly == true"><fa icon="heart"/></span>
|
||||
</div>
|
||||
<mk-renote class="renote" v-if="isRenote" :note="note" mini/>
|
||||
<article>
|
||||
<mk-avatar class="avatar" :user="appearNote.user" v-if="$store.state.device.postStyle != 'smart'"/>
|
||||
<div class="main">
|
||||
@ -37,7 +23,7 @@
|
||||
<div class="text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
|
||||
<a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text" :custom-emojis="appearNote.emojis"/>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
|
||||
<a class="rp" v-if="appearNote.renote != null">RN:</a>
|
||||
</div>
|
||||
<div class="files" v-if="appearNote.files.length > 0">
|
||||
@ -138,66 +124,8 @@ export default Vue.extend({
|
||||
align-items center
|
||||
margin-bottom 4px
|
||||
|
||||
> .renote
|
||||
display flex
|
||||
align-items center
|
||||
padding 8px 16px
|
||||
line-height 28px
|
||||
white-space pre
|
||||
color var(--renoteText)
|
||||
background linear-gradient(to bottom, var(--renoteGradient) 0%, var(--face) 100%)
|
||||
|
||||
@media (min-width 500px)
|
||||
padding 16px
|
||||
|
||||
@media (min-width 600px)
|
||||
padding 16px 32px
|
||||
|
||||
.avatar
|
||||
flex-shrink 0
|
||||
display inline-block
|
||||
width 20px
|
||||
height 20px
|
||||
margin 0 8px 0 0
|
||||
border-radius 6px
|
||||
|
||||
@media (min-width 500px)
|
||||
width 28px
|
||||
height 28px
|
||||
|
||||
[data-icon]
|
||||
margin-right 4px
|
||||
|
||||
> span
|
||||
flex-shrink 0
|
||||
|
||||
.name
|
||||
overflow hidden
|
||||
flex-shrink 1
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
font-weight bold
|
||||
|
||||
> .mk-time
|
||||
display block
|
||||
margin-left auto
|
||||
flex-shrink 0
|
||||
font-size 0.9em
|
||||
|
||||
> .visibility
|
||||
margin-left 8px
|
||||
|
||||
[data-icon]
|
||||
margin-right 0
|
||||
|
||||
> .localOnly
|
||||
margin-left 4px
|
||||
|
||||
[data-icon]
|
||||
margin-right 0
|
||||
|
||||
& + article
|
||||
padding-top 8px
|
||||
> .renote + article
|
||||
padding-top 8px
|
||||
|
||||
> article
|
||||
display flex
|
||||
@ -260,24 +188,6 @@ export default Vue.extend({
|
||||
overflow-wrap break-word
|
||||
color var(--noteText)
|
||||
|
||||
>>> .title
|
||||
display block
|
||||
margin-bottom 4px
|
||||
padding 4px
|
||||
font-size 90%
|
||||
text-align center
|
||||
background var(--mfmTitleBg)
|
||||
border-radius 4px
|
||||
|
||||
>>> .code
|
||||
margin 8px 0
|
||||
|
||||
>>> .quote
|
||||
margin 8px
|
||||
padding 6px 12px
|
||||
color var(--mfmQuote)
|
||||
border-left solid 3px var(--mfmQuoteLine)
|
||||
|
||||
> .reply
|
||||
margin-right 8px
|
||||
color var(--noteText)
|
||||
@ -287,15 +197,6 @@ export default Vue.extend({
|
||||
font-style oblique
|
||||
color var(--renoteText)
|
||||
|
||||
[data-is-me]:after
|
||||
content "you"
|
||||
padding 0 4px
|
||||
margin-left 4px
|
||||
font-size 80%
|
||||
color var(--primaryForeground)
|
||||
background var(--primary)
|
||||
border-radius 4px
|
||||
|
||||
.mk-url-preview
|
||||
margin-top 8px
|
||||
|
||||
@ -361,18 +262,3 @@ export default Vue.extend({
|
||||
opacity 0.7
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="stylus" module>
|
||||
.text
|
||||
code
|
||||
padding 4px 8px
|
||||
margin 0 0.5em
|
||||
font-size 80%
|
||||
color #525252
|
||||
background #f8f8f8
|
||||
border-radius 2px
|
||||
|
||||
pre > code
|
||||
padding 16px
|
||||
margin 0
|
||||
</style>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<span v-if="note.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span>
|
||||
<a class="reply" v-if="note.replyId"><fa icon="reply"/></a>
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i" :custom-emojis="note.emojis"/>
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/>
|
||||
<a class="rp" v-if="note.renoteId">RN: ...</a>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
|
@ -20,7 +20,7 @@
|
||||
<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
|
||||
<misskey-flavored-markdown v-if="user.description" :text="user.description" :author="user" :i="$store.state.i"/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<p class="location" v-if="user.host === null && user.profile.location">
|
||||
|
23
src/client/app/test/script.ts
Normal file
23
src/client/app/test/script.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
// Style
|
||||
import './style.styl';
|
||||
|
||||
import init from '../init';
|
||||
import Index from './views/index.vue';
|
||||
|
||||
init(launch => {
|
||||
document.title = 'Misskey';
|
||||
|
||||
// Init router
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: '/test/',
|
||||
routes: [
|
||||
{ path: '/', component: Index },
|
||||
]
|
||||
});
|
||||
|
||||
// Launch the app
|
||||
launch(router);
|
||||
});
|
6
src/client/app/test/style.styl
Normal file
6
src/client/app/test/style.styl
Normal file
@ -0,0 +1,6 @@
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
html
|
||||
height 100%
|
||||
background var(--bg)
|
68
src/client/app/test/views/index.vue
Normal file
68
src/client/app/test/views/index.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<main>
|
||||
<ui-card>
|
||||
<div slot="title">MFM Playground</div>
|
||||
<section class="fit-top">
|
||||
<ui-textarea v-model="mfm">
|
||||
<span>MFM</span>
|
||||
</ui-textarea>
|
||||
<div>
|
||||
<misskey-flavored-markdown :text="mfm" :i="$store.state.i"/>
|
||||
</div>
|
||||
</section>
|
||||
</ui-card>
|
||||
|
||||
<ui-card>
|
||||
<div slot="title">Dialog Generator</div>
|
||||
<section class="fit-top">
|
||||
<ui-select v-model="dialogType" placeholder="">
|
||||
<option value="info">Info</option>
|
||||
<option value="success">Success</option>
|
||||
<option value="error">Error</option>
|
||||
</ui-select>
|
||||
<ui-input v-model="dialogTitle">
|
||||
<span>Title</span>
|
||||
</ui-input>
|
||||
<ui-input v-model="dialogText">
|
||||
<span>Text</span>
|
||||
</ui-input>
|
||||
<ui-switch v-model="dialogShowCancelButton">With cancel button</ui-switch>
|
||||
<ui-button @click="showDialog">Show</ui-button>
|
||||
</section>
|
||||
</ui-card>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
mfm: '',
|
||||
dialogType: 'success',
|
||||
dialogTitle: '',
|
||||
dialogText: 'Hello World!',
|
||||
dialogShowCancelButton: false
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
showDialog() {
|
||||
this.$root.alert({
|
||||
type: this.dialogType,
|
||||
title: this.dialogTitle,
|
||||
text: this.dialogText,
|
||||
showCancelButton: this.dialogShowCancelButton
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
max-width 700px
|
||||
margin 0 auto
|
||||
|
||||
</style>
|
132
src/index.ts
132
src/index.ts
@ -14,16 +14,16 @@ import * as portscanner from 'portscanner';
|
||||
import isRoot = require('is-root');
|
||||
import Xev from 'xev';
|
||||
import * as program from 'commander';
|
||||
import * as sysUtils from 'systeminformation';
|
||||
import mongo, { nativeDbConn } from './db/mongodb';
|
||||
|
||||
import Logger from './misc/logger';
|
||||
import EnvironmentInfo from './misc/environmentInfo';
|
||||
import MachineInfo from './misc/machineInfo';
|
||||
import serverStats from './daemons/server-stats';
|
||||
import notesStats from './daemons/notes-stats';
|
||||
import loadConfig from './config/load';
|
||||
import { Config } from './config/types';
|
||||
import { lessThan } from './prelude/array';
|
||||
import { Db } from 'mongodb';
|
||||
|
||||
const clusterLog = debug('misskey:cluster');
|
||||
const ev = new Xev();
|
||||
@ -42,8 +42,6 @@ program
|
||||
.parse(process.argv);
|
||||
//#endregion
|
||||
|
||||
main();
|
||||
|
||||
/**
|
||||
* Init process
|
||||
*/
|
||||
@ -105,6 +103,43 @@ async function workerMain() {
|
||||
}
|
||||
}
|
||||
|
||||
const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10));
|
||||
const requiredNodejsVersion = [10, 0, 0];
|
||||
const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion);
|
||||
|
||||
function isWellKnownPort(port: number): boolean {
|
||||
return port < 1024;
|
||||
}
|
||||
|
||||
async function isPortAvailable(port: number): Promise<boolean> {
|
||||
return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed';
|
||||
}
|
||||
|
||||
async function showMachine() {
|
||||
const logger = new Logger('Machine');
|
||||
logger.info(`Hostname: ${os.hostname()}`);
|
||||
logger.info(`Platform: ${process.platform}`);
|
||||
logger.info(`Architecture: ${process.arch}`);
|
||||
logger.info(`CPU: ${os.cpus().length} core`);
|
||||
const mem = await sysUtils.mem();
|
||||
const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1);
|
||||
const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1);
|
||||
logger.info(`MEM: ${totalmem}GB (available: ${availmem}GB)`);
|
||||
}
|
||||
|
||||
function showEnvironment(): void {
|
||||
const env = process.env.NODE_ENV;
|
||||
const logger = new Logger('Env');
|
||||
logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
|
||||
|
||||
if (env !== 'production') {
|
||||
logger.warn('The environment is not in production mode');
|
||||
logger.warn('Do not use for production purpose');
|
||||
}
|
||||
|
||||
logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init app
|
||||
*/
|
||||
@ -112,14 +147,15 @@ async function init(): Promise<Config> {
|
||||
Logger.info('Welcome to Misskey!');
|
||||
Logger.info(`<<< Misskey v${pkg.version} >>>`);
|
||||
|
||||
new Logger('Nodejs').info(`Version ${process.version}`);
|
||||
if (lessThan(process.version.slice(1).split('.').map(x => parseInt(x, 10)), [10, 0, 0])) {
|
||||
new Logger('Nodejs').error(`Node.js version is less than 10.0.0. Please upgrade it.`);
|
||||
new Logger('Nodejs').info(`Version ${runningNodejsVersion.join('.')}`);
|
||||
|
||||
if (!satisfyNodejsVersion) {
|
||||
new Logger('Nodejs').error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await MachineInfo.show();
|
||||
EnvironmentInfo.show();
|
||||
await showMachine();
|
||||
showEnvironment();
|
||||
|
||||
const configLogger = new Logger('Config');
|
||||
let config;
|
||||
@ -145,70 +181,64 @@ async function init(): Promise<Config> {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (process.platform === 'linux' && !isRoot() && config.port < 1024) {
|
||||
Logger.error('You need root privileges to listen on port below 1024 on Linux');
|
||||
if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
|
||||
Logger.error('You need root privileges to listen on well-known port on Linux');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (await portscanner.checkPortStatus(config.port, '127.0.0.1') === 'open') {
|
||||
if (!await isPortAvailable(config.port)) {
|
||||
Logger.error(`Port ${config.port} is already in use`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Try to connect to MongoDB
|
||||
checkMongoDb(config);
|
||||
//await checkMongoDB(config);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function checkMongoDb(config: Config) {
|
||||
const requiredMongoDBVersion = [3, 6];
|
||||
|
||||
function checkMongoDB(config: Config): Promise<void> {
|
||||
const mongoDBLogger = new Logger('MongoDB');
|
||||
const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
|
||||
const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
|
||||
const uri = `mongodb://${u && p ? `${u}:****@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
|
||||
mongoDBLogger.info(`Connecting to ${uri}`);
|
||||
|
||||
mongo.then(() => {
|
||||
nativeDbConn().then(db => db.admin().serverInfo()).then(x => x.version).then((version: string) => {
|
||||
mongoDBLogger.info(`Version: ${version}`);
|
||||
if (lessThan(version.split('.').map(x => parseInt(x, 10)), [3, 6])) {
|
||||
mongoDBLogger.error(`MongoDB version is less than 3.6. Please upgrade it.`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
return mongo.then(async () => {
|
||||
mongoDBLogger.succ('Connectivity confirmed');
|
||||
})
|
||||
.catch(err => {
|
||||
mongoDBLogger.error(err.message);
|
||||
});
|
||||
|
||||
const runningMongoDBVersion = (await nativeDbConn().then(getMongoDBVersion)).split('.').map(x => parseInt(x, 10));
|
||||
mongoDBLogger.info(`Version: ${runningMongoDBVersion.join('.')}`);
|
||||
if (lessThan(runningMongoDBVersion, requiredMongoDBVersion)) {
|
||||
mongoDBLogger.error(`MongoDB version is less than ${requiredMongoDBVersion.join('.')}. Please upgrade it.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}).catch(err => {
|
||||
mongoDBLogger.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
function spawnWorkers(limit: number) {
|
||||
Logger.info('Starting workers...');
|
||||
async function getMongoDBVersion(db: Db): Promise<string> {
|
||||
return (await db.admin().serverInfo()).version;
|
||||
}
|
||||
|
||||
async function spawnWorkers(limit: number = Infinity) {
|
||||
const workers = Math.min(limit, os.cpus().length);
|
||||
Logger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
|
||||
await Promise.all([...Array(workers)].map(spawnWorker));
|
||||
Logger.succ('All workers started');
|
||||
}
|
||||
|
||||
function spawnWorker(): Promise<void> {
|
||||
return new Promise(res => {
|
||||
// Count the machine's CPUs
|
||||
const cpuCount = os.cpus().length;
|
||||
|
||||
const count = limit || cpuCount;
|
||||
let started = 0;
|
||||
|
||||
// Create a worker for each CPU
|
||||
for (let i = 0; i < count; i++) {
|
||||
const worker = cluster.fork();
|
||||
|
||||
worker.on('message', message => {
|
||||
if (message !== 'ready') return;
|
||||
started++;
|
||||
|
||||
// When all workers started
|
||||
if (started == count) {
|
||||
Logger.succ('All workers started');
|
||||
res();
|
||||
}
|
||||
});
|
||||
}
|
||||
const worker = cluster.fork();
|
||||
worker.on('message', message => {
|
||||
if (message !== 'ready') return;
|
||||
Logger.succ('A worker started');
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -246,3 +276,5 @@ process.on('exit', code => {
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
main();
|
||||
|
232
src/mfm/html.ts
232
src/mfm/html.ts
@ -1,127 +1,135 @@
|
||||
const { lib: emojilib } = require('emojilib');
|
||||
const jsdom = require('jsdom');
|
||||
const { JSDOM } = jsdom;
|
||||
import config from '../config';
|
||||
import { INote } from '../models/note';
|
||||
import { TextElement } from './parse';
|
||||
import { Node } from './parser';
|
||||
import { intersperse } from '../prelude/array';
|
||||
|
||||
const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers: INote['mentionedRemoteUsers']) => void } = {
|
||||
bold({ document }, { bold }) {
|
||||
const b = document.createElement('b');
|
||||
b.textContent = bold;
|
||||
document.body.appendChild(b);
|
||||
},
|
||||
|
||||
big({ document }, { big }) {
|
||||
const b = document.createElement('strong');
|
||||
b.textContent = big;
|
||||
document.body.appendChild(b);
|
||||
},
|
||||
|
||||
motion({ document }, { big }) {
|
||||
const b = document.createElement('strong');
|
||||
b.textContent = big;
|
||||
document.body.appendChild(b);
|
||||
},
|
||||
|
||||
code({ document }, { code }) {
|
||||
const pre = document.createElement('pre');
|
||||
const inner = document.createElement('code');
|
||||
inner.innerHTML = code;
|
||||
pre.appendChild(inner);
|
||||
document.body.appendChild(pre);
|
||||
},
|
||||
|
||||
emoji({ document }, { content, emoji }) {
|
||||
const found = emojilib[emoji];
|
||||
const node = document.createTextNode(found ? found.char : content);
|
||||
document.body.appendChild(node);
|
||||
},
|
||||
|
||||
hashtag({ document }, { hashtag }) {
|
||||
const a = document.createElement('a');
|
||||
a.href = `${config.url}/tags/${hashtag}`;
|
||||
a.textContent = `#${hashtag}`;
|
||||
a.setAttribute('rel', 'tag');
|
||||
document.body.appendChild(a);
|
||||
},
|
||||
|
||||
'inline-code'({ document }, { code }) {
|
||||
const element = document.createElement('code');
|
||||
element.textContent = code;
|
||||
document.body.appendChild(element);
|
||||
},
|
||||
|
||||
math({ document }, { formula }) {
|
||||
const element = document.createElement('code');
|
||||
element.textContent = formula;
|
||||
document.body.appendChild(element);
|
||||
},
|
||||
|
||||
link({ document }, { url, title }) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.textContent = title;
|
||||
document.body.appendChild(a);
|
||||
},
|
||||
|
||||
mention({ document }, { content, username, host }, mentionedRemoteUsers) {
|
||||
const a = document.createElement('a');
|
||||
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
|
||||
a.href = remoteUserInfo ? remoteUserInfo.uri : `${config.url}/${content}`;
|
||||
a.textContent = content;
|
||||
document.body.appendChild(a);
|
||||
},
|
||||
|
||||
quote({ document }, { quote }) {
|
||||
const blockquote = document.createElement('blockquote');
|
||||
blockquote.textContent = quote;
|
||||
document.body.appendChild(blockquote);
|
||||
},
|
||||
|
||||
title({ document }, { content }) {
|
||||
const h1 = document.createElement('h1');
|
||||
h1.textContent = content;
|
||||
document.body.appendChild(h1);
|
||||
},
|
||||
|
||||
text({ document }, { content }) {
|
||||
const nodes = (content as string).split('\n').map(x => document.createTextNode(x));
|
||||
for (const x of intersperse('br', nodes)) {
|
||||
if (x === 'br') {
|
||||
document.body.appendChild(document.createElement('br'));
|
||||
} else {
|
||||
document.body.appendChild(x);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
url({ document }, { url }) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.textContent = url;
|
||||
document.body.appendChild(a);
|
||||
},
|
||||
|
||||
search({ document }, { content, query }) {
|
||||
const a = document.createElement('a');
|
||||
a.href = `https://www.google.com/?#q=${query}`;
|
||||
a.textContent = content;
|
||||
document.body.appendChild(a);
|
||||
}
|
||||
};
|
||||
|
||||
export default (tokens: TextElement[], mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) => {
|
||||
export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) => {
|
||||
if (tokens == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { window } = new JSDOM('');
|
||||
|
||||
for (const token of tokens) {
|
||||
handlers[token.type](window, token, mentionedRemoteUsers);
|
||||
const doc = window.document;
|
||||
|
||||
function dive(nodes: Node[]): any[] {
|
||||
return nodes.map(n => handlers[n.name](n));
|
||||
}
|
||||
|
||||
return `<p>${window.document.body.innerHTML}</p>`;
|
||||
const handlers: { [key: string]: (token: Node) => any } = {
|
||||
bold(token) {
|
||||
const el = doc.createElement('b');
|
||||
dive(token.children).forEach(child => el.appendChild(child));
|
||||
return el;
|
||||
},
|
||||
|
||||
big(token) {
|
||||
const el = doc.createElement('strong');
|
||||
dive(token.children).forEach(child => el.appendChild(child));
|
||||
return el;
|
||||
},
|
||||
|
||||
motion(token) {
|
||||
const el = doc.createElement('i');
|
||||
dive(token.children).forEach(child => el.appendChild(child));
|
||||
return el;
|
||||
},
|
||||
|
||||
blockCode(token) {
|
||||
const pre = doc.createElement('pre');
|
||||
const inner = doc.createElement('code');
|
||||
inner.innerHTML = token.props.code;
|
||||
pre.appendChild(inner);
|
||||
return pre;
|
||||
},
|
||||
|
||||
emoji(token) {
|
||||
return doc.createTextNode(token.props.emoji ? token.props.emoji : `:${token.props.name}:`);
|
||||
},
|
||||
|
||||
hashtag(token) {
|
||||
const a = doc.createElement('a');
|
||||
a.href = `${config.url}/tags/${token.props.hashtag}`;
|
||||
a.textContent = `#${token.props.hashtag}`;
|
||||
a.setAttribute('rel', 'tag');
|
||||
return a;
|
||||
},
|
||||
|
||||
inlineCode(token) {
|
||||
const el = doc.createElement('code');
|
||||
el.textContent = token.props.code;
|
||||
return el;
|
||||
},
|
||||
|
||||
math(token) {
|
||||
const el = doc.createElement('code');
|
||||
el.textContent = token.props.formula;
|
||||
return el;
|
||||
},
|
||||
|
||||
link(token) {
|
||||
const a = doc.createElement('a');
|
||||
a.href = token.props.url;
|
||||
dive(token.children).forEach(child => a.appendChild(child));
|
||||
return a;
|
||||
},
|
||||
|
||||
mention(token) {
|
||||
const a = doc.createElement('a');
|
||||
const { username, host, acct } = token.props;
|
||||
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
|
||||
a.href = remoteUserInfo ? remoteUserInfo.uri : `${config.url}/${acct}`;
|
||||
a.textContent = acct;
|
||||
return a;
|
||||
},
|
||||
|
||||
quote(token) {
|
||||
const el = doc.createElement('blockquote');
|
||||
dive(token.children).forEach(child => el.appendChild(child));
|
||||
return el;
|
||||
},
|
||||
|
||||
title(token) {
|
||||
const el = doc.createElement('h1');
|
||||
dive(token.children).forEach(child => el.appendChild(child));
|
||||
return el;
|
||||
},
|
||||
|
||||
text(token) {
|
||||
const el = doc.createElement('span');
|
||||
const nodes = (token.props.text as string).split('\n').map(x => doc.createTextNode(x));
|
||||
|
||||
for (const x of intersperse('br', nodes)) {
|
||||
if (x === 'br') {
|
||||
el.appendChild(doc.createElement('br'));
|
||||
} else {
|
||||
el.appendChild(x);
|
||||
}
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
url(token) {
|
||||
const a = doc.createElement('a');
|
||||
a.href = token.props.url;
|
||||
a.textContent = token.props.url;
|
||||
return a;
|
||||
},
|
||||
|
||||
search(token) {
|
||||
const a = doc.createElement('a');
|
||||
a.href = `https://www.google.com/?#q=${token.props.query}`;
|
||||
a.textContent = token.props.content;
|
||||
return a;
|
||||
}
|
||||
};
|
||||
|
||||
dive(tokens).forEach(x => {
|
||||
doc.body.appendChild(x);
|
||||
});
|
||||
|
||||
return `<p>${doc.body.innerHTML}</p>`;
|
||||
};
|
||||
|
81
src/mfm/parse.ts
Normal file
81
src/mfm/parse.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import parser, { Node } from './parser';
|
||||
import * as A from '../prelude/array';
|
||||
import * as S from '../prelude/string';
|
||||
|
||||
export default (source: string): Node[] => {
|
||||
if (source == null || source == '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nodes: Node[] = parser.root.tryParse(source);
|
||||
|
||||
const combineText = (es: Node[]): Node =>
|
||||
({ name: 'text', props: { text: S.concat(es.map(e => e.props.text)) } });
|
||||
|
||||
const concatText = (nodes: Node[]): Node[] =>
|
||||
A.concat(A.groupOn(x => x.name, nodes).map(es =>
|
||||
es[0].name === 'text' ? [combineText(es)] : es
|
||||
));
|
||||
|
||||
const concatTextRecursive = (es: Node[]): void =>
|
||||
es.filter(x => x.children).forEach(x => {
|
||||
x.children = concatText(x.children);
|
||||
concatTextRecursive(x.children);
|
||||
});
|
||||
|
||||
nodes = concatText(nodes);
|
||||
concatTextRecursive(nodes);
|
||||
|
||||
function getBeforeTextNode(node: Node): Node {
|
||||
if (node == null) return null;
|
||||
if (node.name == 'text') return node;
|
||||
if (node.children) return getBeforeTextNode(node.children[node.children.length - 1]);
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAfterTextNode(node: Node): Node {
|
||||
if (node == null) return null;
|
||||
if (node.name == 'text') return node;
|
||||
if (node.children) return getBeforeTextNode(node.children[0]);
|
||||
return null;
|
||||
}
|
||||
|
||||
function isBlockNode(node: Node): boolean {
|
||||
return ['blockCode', 'quote', 'title'].includes(node.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* ブロック要素の前後にある改行を削除します(ブロック要素自体が改行の役割も果たすため、余計に改行されてしまうため)
|
||||
* @param nodes
|
||||
*/
|
||||
const removeNeedlessLineBreaks = (nodes: Node[]) => {
|
||||
nodes.forEach((node, i) => {
|
||||
if (node.children) removeNeedlessLineBreaks(node.children);
|
||||
if (isBlockNode(node)) {
|
||||
const before = getBeforeTextNode(nodes[i - 1]);
|
||||
const after = getAfterTextNode(nodes[i + 1]);
|
||||
if (before && before.props.text.endsWith('\n')) {
|
||||
before.props.text = before.props.text.substring(0, before.props.text.length - 1);
|
||||
}
|
||||
if (after && after.props.text.startsWith('\n')) {
|
||||
after.props.text = after.props.text.substring(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const removeEmptyTextNodes = (nodes: Node[]) => {
|
||||
nodes.forEach(n => {
|
||||
if (n.children) {
|
||||
n.children = removeEmptyTextNodes(n.children);
|
||||
}
|
||||
});
|
||||
return nodes.filter(n => !(n.name == 'text' && n.props.text == ''));
|
||||
};
|
||||
|
||||
removeNeedlessLineBreaks(nodes);
|
||||
|
||||
nodes = removeEmptyTextNodes(nodes);
|
||||
|
||||
return nodes;
|
||||
};
|
@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Big
|
||||
*/
|
||||
|
||||
export type TextElementBig = {
|
||||
type: 'big';
|
||||
content: string;
|
||||
big: string;
|
||||
};
|
||||
|
||||
export default function(text: string) {
|
||||
const match = text.match(/^\*\*\*(.+?)\*\*\*/);
|
||||
if (!match) return null;
|
||||
const big = match[0];
|
||||
return {
|
||||
type: 'big',
|
||||
content: big,
|
||||
big: match[1]
|
||||
} as TextElementBig;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Bold
|
||||
*/
|
||||
|
||||
export type TextElementBold = {
|
||||
type: 'bold';
|
||||
content: string;
|
||||
bold: string;
|
||||
};
|
||||
|
||||
export default function(text: string) {
|
||||
const match = text.match(/^\*\*(.+?)\*\*/);
|
||||
if (!match) return null;
|
||||
const bold = match[0];
|
||||
return {
|
||||
type: 'bold',
|
||||
content: bold,
|
||||
bold: match[1]
|
||||
} as TextElementBold;
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Code (block)
|
||||
*/
|
||||
|
||||
import genHtml from '../core/syntax-highlighter';
|
||||
|
||||
export type TextElementCode = {
|
||||
type: 'code';
|
||||
content: string;
|
||||
code: string;
|
||||
html: string;
|
||||
};
|
||||
|
||||
export default function(text: string) {
|
||||
const match = text.match(/^```([\s\S]+?)```/);
|
||||
if (!match) return null;
|
||||
const code = match[0];
|
||||
return {
|
||||
type: 'code',
|
||||
content: code,
|
||||
code: match[1],
|
||||
html: genHtml(match[1].trim())
|
||||
} as TextElementCode;
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Emoji
|
||||
*/
|
||||
|
||||
import { emojiRegex } from "./emoji.regex";
|
||||
|
||||
export type TextElementEmoji = {
|
||||
type: 'emoji';
|
||||
content: string;
|
||||
emoji?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export default function(text: string) {
|
||||
const name = text.match(/^:([a-zA-Z0-9+_-]+):/);
|
||||
if (name) {
|
||||
return {
|
||||
type: 'emoji',
|
||||
content: name[0],
|
||||
name: name[1]
|
||||
} as TextElementEmoji;
|
||||
}
|
||||
const unicode = text.match(emojiRegex);
|
||||
if (unicode) {
|
||||
const [content] = unicode;
|
||||
return {
|
||||
type: 'emoji',
|
||||
content,
|
||||
emoji: content
|
||||
} as TextElementEmoji;
|
||||
}
|
||||
return null;
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Hashtag
|
||||
*/
|
||||
|
||||
export type TextElementHashtag = {
|
||||
type: 'hashtag';
|
||||
content: string;
|
||||
hashtag: string;
|
||||
};
|
||||
|
||||
export default function(text: string, before: string) {
|
||||
const isBegin = before == '';
|
||||
|
||||
if (!(/^\s#[^\s\.,!\?#]+/.test(text) || (isBegin && /^#[^\s\.,!\?#]+/.test(text)))) return null;
|
||||
const isHead = text.startsWith('#');
|
||||
const hashtag = text.match(/^\s?#[^\s\.,!\?#]+/)[0];
|
||||
const res: any[] = !isHead ? [{
|
||||
type: 'text',
|
||||
content: text[0]
|
||||
}] : [];
|
||||
res.push({
|
||||
type: 'hashtag',
|
||||
content: isHead ? hashtag : hashtag.substr(1),
|
||||
hashtag: isHead ? hashtag.substr(1) : hashtag.substr(2)
|
||||
});
|
||||
return res as TextElementHashtag[];
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Code (inline)
|
||||
*/
|
||||
|
||||
import genHtml from '../core/syntax-highlighter';
|
||||
|
||||
export type TextElementInlineCode = {
|
||||
type: 'inline-code';
|
||||
content: string;
|
||||
code: string;
|
||||
html: string;
|
||||
};
|
||||
|
||||
export default function(text: string) {
|
||||
const match = text.match(/^`(.+?)`/);
|
||||
if (!match) return null;
|
||||
if (match[1].includes('´')) return null;
|
||||
const code = match[0];
|
||||
return {
|
||||
type: 'inline-code',
|
||||
content: code,
|
||||
code: match[1],
|
||||
html: genHtml(match[1])
|
||||
} as TextElementInlineCode;
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Link
|
||||
*/
|
||||
|
||||
export type TextElementLink = {
|
||||
type: 'link';
|
||||
content: string;
|
||||
title: string;
|
||||
url: string;
|
||||
silent: boolean;
|
||||
};
|
||||
|
||||
export default function(text: string) {
|
||||
const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/);
|
||||
if (!match) return null;
|
||||
const silent = text.startsWith('?');
|
||||
const link = match[0];
|
||||
const title = match[1];
|
||||
const url = match[2];
|
||||
return {
|
||||
type: 'link',
|
||||
content: link,
|
||||
title: title,
|
||||
url: url,
|
||||
silent: silent
|
||||
} as TextElementLink;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Math
|
||||
*/
|
||||
|
||||
export type TextElementMath = {
|
||||
type: 'math';
|
||||
content: string;
|
||||
formula: string;
|
||||
};
|
||||
|
||||
export default function(text: string) {
|
||||
const match = text.match(/^\\\((.+?)\\\)/);
|
||||
if (!match) return null;
|
||||
const math = match[0];
|
||||
return {
|
||||
type: 'math',
|
||||
content: math,
|
||||
formula: match[1]
|
||||
} as TextElementMath;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Mention
|
||||
*/
|
||||
import parseAcct from '../../../misc/acct/parse';
|
||||
import { toUnicode } from 'punycode';
|
||||
|
||||
export type TextElementMention = {
|
||||
type: 'mention';
|
||||
content: string;
|
||||
canonical: string;
|
||||
username: string;
|
||||
host: string;
|
||||
};
|
||||
|
||||
export default function(text: string, before: string) {
|
||||
const match = text.match(/^@[a-z0-9_]+(?:@[a-z0-9\.\-]+[a-z0-9])?/i);
|
||||
if (!match) return null;
|
||||
if (/[a-zA-Z0-9]$/.test(before)) return null;
|
||||
const mention = match[0];
|
||||
const { username, host } = parseAcct(mention.substr(1));
|
||||
const canonical = host != null ? `@${username}@${toUnicode(host)}` : mention;
|
||||
return {
|
||||
type: 'mention',
|
||||
content: mention,
|
||||
canonical,
|
||||
username,
|
||||
host
|
||||
} as TextElementMention;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Motion
|
||||
*/
|
||||
|
||||
export type TextElementMotion = {
|
||||
type: 'motion';
|
||||
content: string;
|
||||
motion: string;
|
||||
};
|
||||
|
||||
export default function(text: string) {
|
||||
const match = text.match(/^\(\(\((.+?)\)\)\)/) || text.match(/^<motion>(.+?)<\/motion>/);
|
||||
if (!match) return null;
|
||||
const motion = match[0];
|
||||
return {
|
||||
type: 'motion',
|
||||
content: motion,
|
||||
motion: match[1]
|
||||
} as TextElementMotion;
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
/**
|
||||
* Quoted text
|
||||
*/
|
||||
|
||||
export type TextElementQuote = {
|
||||
type: 'quote';
|
||||
content: string;
|
||||
quote: string;
|
||||
};
|
||||
|
||||
export default function(text: string, before: string) {
|
||||
const isBegin = before == '';
|
||||
|
||||
const match = text.match(/^"([\s\S]+?)\n"/) || text.match(/^\n>([\s\S]+?)(\n\n|$)/) ||
|
||||
(isBegin ? text.match(/^>([\s\S]+?)(\n\n|$)/) : null);
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
const quote = match[1]
|
||||
.split('\n')
|
||||
.map(line => line.replace(/^>+/g, '').trim())
|
||||
.join('\n')
|
||||
.trim();
|
||||
|
||||
return {
|
||||
type: 'quote',
|
||||
content: match[0],
|
||||
quote: quote,
|
||||
} as TextElementQuote;
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Search
|
||||
*/
|
||||
|
||||
export type TextElementSearch = {
|
||||
type: 'search';
|
||||
content: string;
|
||||
query: string;
|
||||
};
|
||||
|
||||
export default function(text: string) {
|
||||
const match = text.match(/^(.+?)( | )(検索|\[検索\]|Search|\[Search\])(\n|$)/i);
|
||||
if (!match) return null;
|
||||
return {
|
||||
type: 'search',
|
||||
content: match[0],
|
||||
query: match[1]
|
||||
};
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Title
|
||||
*/
|
||||
|
||||
export type TextElementTitle = {
|
||||
type: 'title';
|
||||
content: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export default function(text: string, before: string) {
|
||||
const isBegin = before == '';
|
||||
|
||||
const match = isBegin ? text.match(/^(【|\[)(.+?)(】|])\n/) : text.match(/^\n(【|\[)(.+?)(】|])\n/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
type: 'title',
|
||||
content: match[0],
|
||||
title: match[2]
|
||||
} as TextElementTitle;
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
/**
|
||||
* URL
|
||||
*/
|
||||
|
||||
export type TextElementUrl = {
|
||||
type: 'url';
|
||||
content: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function(text: string, before: string) {
|
||||
const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/);
|
||||
if (!match) return null;
|
||||
let url = match[0];
|
||||
if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.'));
|
||||
if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(','));
|
||||
if (url.endsWith(')') && before.endsWith('(')) url = url.substr(0, url.lastIndexOf(')'));
|
||||
return {
|
||||
type: 'url',
|
||||
content: url,
|
||||
url: url
|
||||
} as TextElementUrl;
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
/**
|
||||
* Misskey Text Analyzer
|
||||
*/
|
||||
|
||||
import { TextElementBold } from './elements/bold';
|
||||
import { TextElementBig } from './elements/big';
|
||||
import { TextElementCode } from './elements/code';
|
||||
import { TextElementEmoji } from './elements/emoji';
|
||||
import { TextElementHashtag } from './elements/hashtag';
|
||||
import { TextElementInlineCode } from './elements/inline-code';
|
||||
import { TextElementMath } from './elements/math';
|
||||
import { TextElementLink } from './elements/link';
|
||||
import { TextElementMention } from './elements/mention';
|
||||
import { TextElementQuote } from './elements/quote';
|
||||
import { TextElementSearch } from './elements/search';
|
||||
import { TextElementTitle } from './elements/title';
|
||||
import { TextElementUrl } from './elements/url';
|
||||
import { TextElementMotion } from './elements/motion';
|
||||
import { groupOn } from '../../prelude/array';
|
||||
import * as A from '../../prelude/array';
|
||||
import * as S from '../../prelude/string';
|
||||
|
||||
const elements = [
|
||||
require('./elements/big'),
|
||||
require('./elements/bold'),
|
||||
require('./elements/title'),
|
||||
require('./elements/url'),
|
||||
require('./elements/link'),
|
||||
require('./elements/mention'),
|
||||
require('./elements/hashtag'),
|
||||
require('./elements/code'),
|
||||
require('./elements/inline-code'),
|
||||
require('./elements/math'),
|
||||
require('./elements/quote'),
|
||||
require('./elements/emoji'),
|
||||
require('./elements/search'),
|
||||
require('./elements/motion')
|
||||
].map(element => element.default as TextElementProcessor);
|
||||
|
||||
export type TextElement = { type: 'text', content: string }
|
||||
| TextElementBold
|
||||
| TextElementBig
|
||||
| TextElementCode
|
||||
| TextElementEmoji
|
||||
| TextElementHashtag
|
||||
| TextElementInlineCode
|
||||
| TextElementMath
|
||||
| TextElementLink
|
||||
| TextElementMention
|
||||
| TextElementQuote
|
||||
| TextElementSearch
|
||||
| TextElementTitle
|
||||
| TextElementUrl
|
||||
| TextElementMotion;
|
||||
export type TextElementProcessor = (text: string, before: string) => TextElement | TextElement[];
|
||||
|
||||
export default (source: string): TextElement[] => {
|
||||
if (source == null || source == '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokens: TextElement[] = [];
|
||||
|
||||
function push(token: TextElement) {
|
||||
if (token != null) {
|
||||
tokens.push(token);
|
||||
source = source.substr(token.content.length);
|
||||
}
|
||||
}
|
||||
|
||||
// パース
|
||||
while (source != '') {
|
||||
const parsed = elements.some(el => {
|
||||
let _tokens = el(source, tokens.map(token => token.content).join(''));
|
||||
if (_tokens) {
|
||||
if (!Array.isArray(_tokens)) {
|
||||
_tokens = [_tokens];
|
||||
}
|
||||
_tokens.forEach(push);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!parsed) {
|
||||
push({
|
||||
type: 'text',
|
||||
content: source[0]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const combineText = (es: TextElement[]): TextElement =>
|
||||
({ type: 'text', content: S.concat(es.map(e => e.content)) });
|
||||
|
||||
return A.concat(groupOn(x => x.type, tokens).map(es =>
|
||||
es[0].type === 'text' ? [combineText(es)] : es
|
||||
));
|
||||
};
|
256
src/mfm/parser.ts
Normal file
256
src/mfm/parser.ts
Normal file
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
||||
import { capitalize, toUpperCase } from "../../../prelude/string";
|
||||
import { capitalize, toUpperCase } from "../prelude/string";
|
||||
|
||||
function escape(text: string) {
|
||||
return text
|
||||
@ -308,7 +308,7 @@ const elements: Element[] = [
|
||||
];
|
||||
|
||||
// specify lang is todo
|
||||
export default (source: string, lang?: string) => {
|
||||
export default (source: string, lang?: string): string => {
|
||||
let code = source;
|
||||
let html = '';
|
||||
|
@ -1,17 +0,0 @@
|
||||
import Logger from './logger';
|
||||
import isRoot = require('is-root');
|
||||
|
||||
export default class {
|
||||
public static show(): void {
|
||||
const env = process.env.NODE_ENV;
|
||||
const logger = new Logger('Env');
|
||||
logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
|
||||
|
||||
if (env !== 'production') {
|
||||
logger.warn('The environment is not in production mode');
|
||||
logger.warn('Do not use for production purpose');
|
||||
}
|
||||
|
||||
logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import * as os from 'os';
|
||||
import Logger from './logger';
|
||||
import * as sysUtils from 'systeminformation';
|
||||
|
||||
export default class {
|
||||
public static async show() {
|
||||
const logger = new Logger('Machine');
|
||||
logger.info(`Hostname: ${os.hostname()}`);
|
||||
logger.info(`Platform: ${process.platform}`);
|
||||
logger.info(`Architecture: ${process.arch}`);
|
||||
logger.info(`CPU: ${os.cpus().length} core`);
|
||||
const mem = await sysUtils.mem();
|
||||
const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1);
|
||||
const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1);
|
||||
logger.info(`MEM: ${totalmem}GB (available: ${availmem}GB)`);
|
||||
}
|
||||
}
|
@ -7,7 +7,6 @@ import DriveFile, { IDriveFile } from '../../../models/drive-file';
|
||||
import Note, { INote } from '../../../models/note';
|
||||
import User from '../../../models/user';
|
||||
import toHtml from '../misc/get-note-html';
|
||||
import parseMfm from '../../../mfm/parse';
|
||||
import Emoji, { IEmoji } from '../../../models/emoji';
|
||||
|
||||
export default async function renderNote(note: INote, dive = true): Promise<any> {
|
||||
@ -95,17 +94,6 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
|
||||
text += `\n\nRE: ${url}`;
|
||||
}
|
||||
|
||||
// 省略されたメンションのホストを復元する
|
||||
if (text != null && text != '') {
|
||||
text = parseMfm(text).map(x => {
|
||||
if (x.type == 'mention' && x.host == null) {
|
||||
return `${x.content}@${config.host}`;
|
||||
} else {
|
||||
return x.content;
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const content = toHtml(Object.assign({}, note, { text }));
|
||||
|
||||
const emojis = await getEmojis(note.emojis);
|
||||
|
@ -80,7 +80,7 @@ async function fetchAny(uri: string) {
|
||||
const user = await createPerson(object.id);
|
||||
return {
|
||||
type: 'User',
|
||||
object: user
|
||||
object: await packUser(user, null, { detail: true })
|
||||
};
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ async function fetchAny(uri: string) {
|
||||
const note = await createNote(object.id);
|
||||
return {
|
||||
type: 'Note',
|
||||
object: note
|
||||
object: await packNote(note, null, { detail: true })
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -189,6 +189,8 @@ router.get('/*/api/endpoints/*', async ctx => {
|
||||
};
|
||||
|
||||
await ctx.render('../../../../src/docs/api/endpoints/view', Object.assign(await genVars(lang), vars));
|
||||
|
||||
ctx.set('Cache-Control', 'public, max-age=300');
|
||||
});
|
||||
|
||||
router.get('/*/api/entities/*', async ctx => {
|
||||
@ -204,6 +206,8 @@ router.get('/*/api/entities/*', async ctx => {
|
||||
props: sortParams(Object.entries(x.props).map(([k, v]) => parsePropDefinition(k, v))),
|
||||
propDefs: extractPropDefRef(x.props)
|
||||
}));
|
||||
|
||||
ctx.set('Cache-Control', 'public, max-age=300');
|
||||
});
|
||||
|
||||
router.get('/*/*', async ctx => {
|
||||
@ -240,6 +244,8 @@ router.get('/*/*', async ctx => {
|
||||
title: md.match(/^# (.+?)\r?\n/)[1],
|
||||
src: `https://github.com/syuilo/misskey/tree/master/src/docs/${doc}.${lang}.md`
|
||||
}, await genVars(lang)));
|
||||
|
||||
ctx.set('Cache-Control', 'public, max-age=300');
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
@ -94,6 +94,7 @@ router.get('/@:user', async (ctx, next) => {
|
||||
|
||||
if (user != null) {
|
||||
await ctx.render('user', { user });
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
} else {
|
||||
// リモートユーザーなので
|
||||
await next();
|
||||
@ -110,6 +111,7 @@ router.get('/notes/:note', async ctx => {
|
||||
note: _note,
|
||||
summary: getNoteSummary(_note)
|
||||
});
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
} else {
|
||||
ctx.status = 404;
|
||||
}
|
||||
|
@ -68,6 +68,8 @@ router.get('/.well-known/webfinger', async ctx => {
|
||||
template: `${config.url}/authorize-follow?acct={uri}`
|
||||
}]
|
||||
};
|
||||
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
@ -25,15 +25,33 @@ const log = debug('misskey:drive:add-file');
|
||||
|
||||
async function save(path: string, name: string, type: string, hash: string, size: number, metadata: any): Promise<IDriveFile> {
|
||||
let thumbnail: Buffer;
|
||||
let thumbnailExt = 'jpg';
|
||||
let thumbnailType = 'image/jpeg';
|
||||
|
||||
if (['image/jpeg', 'image/png', 'image/webp'].includes(type)) {
|
||||
if (['image/jpeg', 'image/webp'].includes(type)) {
|
||||
thumbnail = await sharp(path)
|
||||
.resize(300)
|
||||
.resize(498, 280, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.rotate()
|
||||
.jpeg({
|
||||
quality: 50,
|
||||
quality: 85,
|
||||
progressive: true
|
||||
})
|
||||
.toBuffer();
|
||||
} else if (['image/png'].includes(type)) {
|
||||
thumbnail = await sharp(path)
|
||||
.resize(498, 280, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.rotate()
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
thumbnailExt = 'png';
|
||||
thumbnailType = 'image/png';
|
||||
}
|
||||
|
||||
if (config.drive && config.drive.storage == 'minio') {
|
||||
@ -48,7 +66,7 @@ async function save(path: string, name: string, type: string, hash: string, size
|
||||
}
|
||||
|
||||
const key = `${config.drive.prefix}/${uuid.v4()}${ext}`;
|
||||
const thumbnailKey = `${config.drive.prefix}/${uuid.v4()}.jpg`;
|
||||
const thumbnailKey = `${config.drive.prefix}/${uuid.v4()}.${thumbnailExt}`;
|
||||
|
||||
const baseUrl = config.drive.baseUrl
|
||||
|| `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`;
|
||||
@ -60,7 +78,7 @@ async function save(path: string, name: string, type: string, hash: string, size
|
||||
|
||||
if (thumbnail) {
|
||||
await minio.putObject(config.drive.bucket, thumbnailKey, thumbnail, size, {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'Content-Type': thumbnailType,
|
||||
'Cache-Control': 'max-age=31536000, immutable'
|
||||
});
|
||||
}
|
||||
@ -107,7 +125,7 @@ async function save(path: string, name: string, type: string, hash: string, size
|
||||
|
||||
await new Promise<IDriveFile>((resolve, reject) => {
|
||||
const writeStream = thumbnailBucket.openUploadStream(name, {
|
||||
contentType: 'image/jpeg',
|
||||
contentType: thumbnailType,
|
||||
metadata: {
|
||||
originalId: file._id
|
||||
}
|
||||
@ -149,6 +167,10 @@ async function deleteOldFile(user: IRemoteUser) {
|
||||
* @param comment Comment
|
||||
* @param folderId Folder ID
|
||||
* @param force If set to true, forcibly upload the file even if there is a file with the same hash.
|
||||
* @param isLink Do not save file to local
|
||||
* @param url URL of source (URLからアップロードされた場合(ローカル/リモート)の元URL)
|
||||
* @param uri URL of source (リモートインスタンスのURLからアップロードされた場合の元URL)
|
||||
* @param sensitive Mark file as sensitive
|
||||
* @return Created drive file
|
||||
*/
|
||||
export default async function(
|
||||
|
@ -21,8 +21,6 @@ import Meta from '../../models/meta';
|
||||
import config from '../../config';
|
||||
import registerHashtag from '../register-hashtag';
|
||||
import isQuote from '../../misc/is-quote';
|
||||
import { TextElementMention } from '../../mfm/parse/elements/mention';
|
||||
import { TextElementHashtag } from '../../mfm/parse/elements/hashtag';
|
||||
import notesChart from '../../chart/notes';
|
||||
import perUserNotesChart from '../../chart/per-user-notes';
|
||||
|
||||
@ -30,7 +28,7 @@ import { erase, unique } from '../../prelude/array';
|
||||
import insertNoteUnread from './unread';
|
||||
import registerInstance from '../register-instance';
|
||||
import Instance from '../../models/instance';
|
||||
import { TextElementEmoji } from '../../mfm/parse/elements/emoji';
|
||||
import { Node } from '../../mfm/parser';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
@ -162,7 +160,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
|
||||
|
||||
const emojis = extractEmojis(tokens);
|
||||
|
||||
const mentionedUsers = data.apMentions || await extractMentionedUsers(tokens);
|
||||
const mentionedUsers = data.apMentions || await extractMentionedUsers(user, tokens);
|
||||
|
||||
if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) {
|
||||
mentionedUsers.push(await User.findOne({ _id: data.reply.userId }));
|
||||
@ -460,21 +458,41 @@ async function insertNote(user: IUser, data: Option, tags: string[], emojis: str
|
||||
}
|
||||
|
||||
function extractHashtags(tokens: ReturnType<typeof parse>): string[] {
|
||||
const hashtags: string[] = [];
|
||||
|
||||
const extract = (tokens: Node[]) => {
|
||||
tokens.filter(x => x.name === 'hashtag').forEach(x => {
|
||||
if (x.props.hashtag.length <= 100) {
|
||||
hashtags.push(x.props.hashtag);
|
||||
}
|
||||
});
|
||||
tokens.filter(x => x.children).forEach(x => {
|
||||
extract(x.children);
|
||||
});
|
||||
};
|
||||
|
||||
// Extract hashtags
|
||||
const hashtags = tokens
|
||||
.filter(t => t.type == 'hashtag')
|
||||
.map(t => (t as TextElementHashtag).hashtag)
|
||||
.filter(tag => tag.length <= 100);
|
||||
extract(tokens);
|
||||
|
||||
return unique(hashtags);
|
||||
}
|
||||
|
||||
function extractEmojis(tokens: ReturnType<typeof parse>): string[] {
|
||||
const emojis: string[] = [];
|
||||
|
||||
const extract = (tokens: Node[]) => {
|
||||
tokens.filter(x => x.name === 'emoji').forEach(x => {
|
||||
if (x.props.name && x.props.name.length <= 100) {
|
||||
emojis.push(x.props.name);
|
||||
}
|
||||
});
|
||||
tokens.filter(x => x.children).forEach(x => {
|
||||
extract(x.children);
|
||||
});
|
||||
};
|
||||
|
||||
// Extract emojis
|
||||
const emojis = tokens
|
||||
.filter(t => t.type == 'emoji' && t.name)
|
||||
.map(t => (t as TextElementEmoji).name)
|
||||
.filter(emoji => emoji.length <= 100);
|
||||
extract(tokens);
|
||||
|
||||
return unique(emojis);
|
||||
}
|
||||
@ -638,16 +656,27 @@ function incNotesCount(user: IUser) {
|
||||
}
|
||||
}
|
||||
|
||||
async function extractMentionedUsers(tokens: ReturnType<typeof parse>): Promise<IUser[]> {
|
||||
async function extractMentionedUsers(user: IUser, tokens: ReturnType<typeof parse>): Promise<IUser[]> {
|
||||
if (tokens == null) return [];
|
||||
|
||||
const mentionTokens = tokens
|
||||
.filter(t => t.type == 'mention') as TextElementMention[];
|
||||
const mentions: any[] = [];
|
||||
|
||||
const extract = (tokens: Node[]) => {
|
||||
tokens.filter(x => x.name === 'mention').forEach(x => {
|
||||
mentions.push(x.props);
|
||||
});
|
||||
tokens.filter(x => x.children).forEach(x => {
|
||||
extract(x.children);
|
||||
});
|
||||
};
|
||||
|
||||
// Extract hashtags
|
||||
extract(tokens);
|
||||
|
||||
let mentionedUsers =
|
||||
erase(null, await Promise.all(mentionTokens.map(async m => {
|
||||
erase(null, await Promise.all(mentions.map(async m => {
|
||||
try {
|
||||
return await resolveUser(m.username, m.host);
|
||||
return await resolveUser(m.username, m.host ? m.host : user.host);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
570
test/mfm.ts
570
test/mfm.ts
@ -6,102 +6,158 @@ import * as assert from 'assert';
|
||||
|
||||
import analyze from '../src/mfm/parse';
|
||||
import toHtml from '../src/mfm/html';
|
||||
import syntaxhighlighter from '../src/mfm/parse/core/syntax-highlighter';
|
||||
|
||||
function _node(name: string, children: any[], props: any) {
|
||||
return children ? { name, children, props } : { name, props };
|
||||
}
|
||||
|
||||
function node(name: string, props?: any) {
|
||||
return _node(name, null, props);
|
||||
}
|
||||
|
||||
function nodeWithChildren(name: string, children: any[], props?: any) {
|
||||
return _node(name, children, props);
|
||||
}
|
||||
|
||||
function text(text: string) {
|
||||
return node('text', { text });
|
||||
}
|
||||
|
||||
describe('Text', () => {
|
||||
it('can be analyzed', () => {
|
||||
const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');
|
||||
assert.deepEqual([
|
||||
{ type: 'mention', content: '@himawari', canonical: '@himawari', username: 'himawari', host: null },
|
||||
{ type: 'text', content: ' ' },
|
||||
{ type: 'mention', content: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
|
||||
{ type: 'text', content: ' お腹ペコい ' },
|
||||
{ type: 'emoji', content: ':cat:', name: 'cat' },
|
||||
{ type: 'text', content: ' ' },
|
||||
{ type: 'hashtag', content: '#yryr', hashtag: 'yryr' }
|
||||
node('mention', { acct: '@himawari', canonical: '@himawari', username: 'himawari', host: null }),
|
||||
text(' '),
|
||||
node('mention', { acct: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }),
|
||||
text(' お腹ペコい '),
|
||||
node('emoji', { name: 'cat' }),
|
||||
text(' '),
|
||||
node('hashtag', { hashtag: 'yryr' }),
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('can be inverted', () => {
|
||||
const text = '@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr';
|
||||
assert.equal(analyze(text).map(x => x.content).join(''), text);
|
||||
});
|
||||
|
||||
describe('elements', () => {
|
||||
it('bold', () => {
|
||||
const tokens = analyze('**Strawberry** Pasta');
|
||||
assert.deepEqual([
|
||||
{ type: 'bold', content: '**Strawberry**', bold: 'Strawberry' },
|
||||
{ type: 'text', content: ' Pasta' }
|
||||
], tokens);
|
||||
describe('bold', () => {
|
||||
it('simple', () => {
|
||||
const tokens = analyze('**foo**');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('bold', [
|
||||
text('foo')
|
||||
]),
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('with other texts', () => {
|
||||
const tokens = analyze('bar**foo**bar');
|
||||
assert.deepEqual([
|
||||
text('bar'),
|
||||
nodeWithChildren('bold', [
|
||||
text('foo')
|
||||
]),
|
||||
text('bar'),
|
||||
], tokens);
|
||||
});
|
||||
});
|
||||
|
||||
it('big', () => {
|
||||
const tokens = analyze('***Strawberry*** Pasta');
|
||||
assert.deepEqual([
|
||||
{ type: 'big', content: '***Strawberry***', big: 'Strawberry' },
|
||||
{ type: 'text', content: ' Pasta' }
|
||||
nodeWithChildren('big', [
|
||||
text('Strawberry')
|
||||
]),
|
||||
text(' Pasta'),
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('motion', () => {
|
||||
const tokens1 = analyze('(((Strawberry))) Pasta');
|
||||
assert.deepEqual([
|
||||
{ type: 'motion', content: '(((Strawberry)))', motion: 'Strawberry' },
|
||||
{ type: 'text', content: ' Pasta' }
|
||||
], tokens1);
|
||||
describe('motion', () => {
|
||||
it('by triple brackets', () => {
|
||||
const tokens = analyze('(((foo)))');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('motion', [
|
||||
text('foo')
|
||||
]),
|
||||
], tokens);
|
||||
});
|
||||
|
||||
const tokens2 = analyze('<motion>Strawberry</motion> Pasta');
|
||||
assert.deepEqual([
|
||||
{ type: 'motion', content: '<motion>Strawberry</motion>', motion: 'Strawberry' },
|
||||
{ type: 'text', content: ' Pasta' }
|
||||
], tokens2);
|
||||
it('by triple brackets (with other texts)', () => {
|
||||
const tokens = analyze('bar(((foo)))bar');
|
||||
assert.deepEqual([
|
||||
text('bar'),
|
||||
nodeWithChildren('motion', [
|
||||
text('foo')
|
||||
]),
|
||||
text('bar'),
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('by <motion> tag', () => {
|
||||
const tokens = analyze('<motion>foo</motion>');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('motion', [
|
||||
text('foo')
|
||||
]),
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('by <motion> tag (with other texts)', () => {
|
||||
const tokens = analyze('bar<motion>foo</motion>bar');
|
||||
assert.deepEqual([
|
||||
text('bar'),
|
||||
nodeWithChildren('motion', [
|
||||
text('foo')
|
||||
]),
|
||||
text('bar'),
|
||||
], tokens);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mention', () => {
|
||||
it('local', () => {
|
||||
const tokens = analyze('@himawari お腹ペコい');
|
||||
const tokens = analyze('@himawari foo');
|
||||
assert.deepEqual([
|
||||
{ type: 'mention', content: '@himawari', canonical: '@himawari', username: 'himawari', host: null },
|
||||
{ type: 'text', content: ' お腹ペコい' }
|
||||
node('mention', { acct: '@himawari', canonical: '@himawari', username: 'himawari', host: null }),
|
||||
text(' foo')
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('remote', () => {
|
||||
const tokens = analyze('@hima_sub@namori.net お腹ペコい');
|
||||
const tokens = analyze('@hima_sub@namori.net foo');
|
||||
assert.deepEqual([
|
||||
{ type: 'mention', content: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
|
||||
{ type: 'text', content: ' お腹ペコい' }
|
||||
node('mention', { acct: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }),
|
||||
text(' foo')
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('remote punycode', () => {
|
||||
const tokens = analyze('@hima_sub@xn--q9j5bya.xn--zckzah お腹ペコい');
|
||||
const tokens = analyze('@hima_sub@xn--q9j5bya.xn--zckzah foo');
|
||||
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: ' お腹ペコい' }
|
||||
node('mention', { acct: '@hima_sub@xn--q9j5bya.xn--zckzah', canonical: '@hima_sub@なもり.テスト', username: 'hima_sub', host: 'xn--q9j5bya.xn--zckzah' }),
|
||||
text(' foo')
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('ignore', () => {
|
||||
const tokens = analyze('idolm@ster');
|
||||
assert.deepEqual([
|
||||
{ type: 'text', content: 'idolm@ster' }
|
||||
text('idolm@ster')
|
||||
], tokens);
|
||||
|
||||
const tokens2 = analyze('@a\n@b\n@c');
|
||||
assert.deepEqual([
|
||||
{ type: 'mention', content: '@a', canonical: '@a', username: 'a', host: null },
|
||||
{ type: 'text', content: '\n' },
|
||||
{ type: 'mention', content: '@b', canonical: '@b', username: 'b', host: null },
|
||||
{ type: 'text', content: '\n' },
|
||||
{ type: 'mention', content: '@c', canonical: '@c', username: 'c', host: null }
|
||||
node('mention', { acct: '@a', canonical: '@a', username: 'a', host: null }),
|
||||
text('\n'),
|
||||
node('mention', { acct: '@b', canonical: '@b', username: 'b', host: null }),
|
||||
text('\n'),
|
||||
node('mention', { acct: '@c', canonical: '@c', username: 'c', host: null })
|
||||
], tokens2);
|
||||
|
||||
const tokens3 = analyze('**x**@a');
|
||||
assert.deepEqual([
|
||||
{ type: 'bold', content: '**x**', bold: 'x' },
|
||||
{ type: 'mention', content: '@a', canonical: '@a', username: 'a', host: null }
|
||||
nodeWithChildren('bold', [
|
||||
text('x')
|
||||
]),
|
||||
node('mention', { acct: '@a', canonical: '@a', username: 'a', host: null })
|
||||
], tokens3);
|
||||
});
|
||||
});
|
||||
@ -109,172 +165,294 @@ describe('Text', () => {
|
||||
it('hashtag', () => {
|
||||
const tokens1 = analyze('Strawberry Pasta #alice');
|
||||
assert.deepEqual([
|
||||
{ type: 'text', content: 'Strawberry Pasta ' },
|
||||
{ type: 'hashtag', content: '#alice', hashtag: 'alice' }
|
||||
text('Strawberry Pasta '),
|
||||
node('hashtag', { hashtag: 'alice' })
|
||||
], tokens1);
|
||||
|
||||
const tokens2 = analyze('Foo #bar, baz #piyo.');
|
||||
assert.deepEqual([
|
||||
{ type: 'text', content: 'Foo ' },
|
||||
{ type: 'hashtag', content: '#bar', hashtag: 'bar' },
|
||||
{ type: 'text', content: ', baz ' },
|
||||
{ type: 'hashtag', content: '#piyo', hashtag: 'piyo' },
|
||||
{ type: 'text', content: '.' }
|
||||
text('Foo '),
|
||||
node('hashtag', { hashtag: 'bar' }),
|
||||
text(', baz '),
|
||||
node('hashtag', { hashtag: 'piyo' }),
|
||||
text('.'),
|
||||
], tokens2);
|
||||
|
||||
const tokens3 = analyze('#Foo!');
|
||||
assert.deepEqual([
|
||||
{ type: 'hashtag', content: '#Foo', hashtag: 'Foo' },
|
||||
{ type: 'text', content: '!' },
|
||||
node('hashtag', { hashtag: 'Foo' }),
|
||||
text('!'),
|
||||
], tokens3);
|
||||
});
|
||||
|
||||
it('quote', () => {
|
||||
const tokens1 = analyze('> foo\nbar\nbaz');
|
||||
assert.deepEqual([
|
||||
{ type: 'quote', content: '> foo\nbar\nbaz', quote: 'foo\nbar\nbaz' }
|
||||
], tokens1);
|
||||
describe('quote', () => {
|
||||
it('basic', () => {
|
||||
const tokens1 = analyze('> foo');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('quote', [
|
||||
text('foo')
|
||||
])
|
||||
], tokens1);
|
||||
|
||||
const tokens2 = analyze('before\n> foo\nbar\nbaz\n\nafter');
|
||||
assert.deepEqual([
|
||||
{ type: 'text', content: 'before' },
|
||||
{ type: 'quote', content: '\n> foo\nbar\nbaz\n\n', quote: 'foo\nbar\nbaz' },
|
||||
{ type: 'text', content: 'after' }
|
||||
], tokens2);
|
||||
const tokens2 = analyze('>foo');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('quote', [
|
||||
text('foo')
|
||||
])
|
||||
], tokens2);
|
||||
});
|
||||
|
||||
const tokens3 = analyze('piyo> foo\nbar\nbaz');
|
||||
assert.deepEqual([
|
||||
{ type: 'text', content: 'piyo> foo\nbar\nbaz' }
|
||||
], tokens3);
|
||||
it('series', () => {
|
||||
const tokens = analyze('> foo\n\n> bar');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('quote', [
|
||||
text('foo')
|
||||
]),
|
||||
nodeWithChildren('quote', [
|
||||
text('bar')
|
||||
]),
|
||||
], tokens);
|
||||
});
|
||||
|
||||
const tokens4 = analyze('> foo\n> bar\n> baz');
|
||||
assert.deepEqual([
|
||||
{ type: 'quote', content: '> foo\n> bar\n> baz', quote: 'foo\nbar\nbaz' }
|
||||
], tokens4);
|
||||
it('trailing line break', () => {
|
||||
const tokens1 = analyze('> foo\n');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('quote', [
|
||||
text('foo')
|
||||
]),
|
||||
], tokens1);
|
||||
|
||||
const tokens5 = analyze('"\nfoo\nbar\nbaz\n"');
|
||||
assert.deepEqual([
|
||||
{ type: 'quote', content: '"\nfoo\nbar\nbaz\n"', quote: 'foo\nbar\nbaz' }
|
||||
], tokens5);
|
||||
const tokens2 = analyze('> foo\n\n');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('quote', [
|
||||
text('foo')
|
||||
]),
|
||||
text('\n')
|
||||
], tokens2);
|
||||
});
|
||||
|
||||
it('multiline', () => {
|
||||
const tokens1 = analyze('>foo\n>bar');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('quote', [
|
||||
text('foo\nbar')
|
||||
])
|
||||
], tokens1);
|
||||
|
||||
const tokens2 = analyze('> foo\n> bar');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('quote', [
|
||||
text('foo\nbar')
|
||||
])
|
||||
], tokens2);
|
||||
});
|
||||
|
||||
it('multiline with trailing line break', () => {
|
||||
const tokens1 = analyze('> foo\n> bar\n');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('quote', [
|
||||
text('foo\nbar')
|
||||
]),
|
||||
], tokens1);
|
||||
|
||||
const tokens2 = analyze('> foo\n> bar\n\n');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('quote', [
|
||||
text('foo\nbar')
|
||||
]),
|
||||
text('\n')
|
||||
], tokens2);
|
||||
});
|
||||
|
||||
it('with before and after texts', () => {
|
||||
const tokens = analyze('before\n> foo\nafter');
|
||||
assert.deepEqual([
|
||||
text('before'),
|
||||
nodeWithChildren('quote', [
|
||||
text('foo')
|
||||
]),
|
||||
text('after'),
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('require line break before ">"', () => {
|
||||
const tokens = analyze('foo>bar');
|
||||
assert.deepEqual([
|
||||
text('foo>bar'),
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('nested', () => {
|
||||
const tokens = analyze('>> foo\n> bar');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('quote', [
|
||||
nodeWithChildren('quote', [
|
||||
text('foo')
|
||||
]),
|
||||
text('bar')
|
||||
])
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('trim line breaks', () => {
|
||||
const tokens = analyze('foo\n\n>a\n>>b\n>>\n>>>\n>>>c\n>>>\n>d\n\n');
|
||||
assert.deepEqual([
|
||||
text('foo\n'),
|
||||
nodeWithChildren('quote', [
|
||||
text('a'),
|
||||
nodeWithChildren('quote', [
|
||||
text('b\n'),
|
||||
nodeWithChildren('quote', [
|
||||
text('\nc\n')
|
||||
])
|
||||
]),
|
||||
text('d')
|
||||
]),
|
||||
text('\n'),
|
||||
], tokens);
|
||||
});
|
||||
});
|
||||
|
||||
describe('url', () => {
|
||||
it('simple', () => {
|
||||
const tokens = analyze('https://example.com');
|
||||
assert.deepEqual([{
|
||||
type: 'url',
|
||||
content: 'https://example.com',
|
||||
url: 'https://example.com'
|
||||
}], tokens);
|
||||
assert.deepEqual([
|
||||
node('url', { url: 'https://example.com' })
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('ignore trailing period', () => {
|
||||
const tokens = analyze('https://example.com.');
|
||||
assert.deepEqual([{
|
||||
type: 'url',
|
||||
content: 'https://example.com',
|
||||
url: 'https://example.com'
|
||||
}, {
|
||||
type: 'text', content: '.'
|
||||
}], tokens);
|
||||
assert.deepEqual([
|
||||
node('url', { url: 'https://example.com' }),
|
||||
text('.')
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('with comma', () => {
|
||||
const tokens = analyze('https://example.com/foo?bar=a,b');
|
||||
assert.deepEqual([{
|
||||
type: 'url',
|
||||
content: 'https://example.com/foo?bar=a,b',
|
||||
url: 'https://example.com/foo?bar=a,b'
|
||||
}], tokens);
|
||||
assert.deepEqual([
|
||||
node('url', { url: 'https://example.com/foo?bar=a,b' })
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('ignore trailing comma', () => {
|
||||
const tokens = analyze('https://example.com/foo, bar');
|
||||
assert.deepEqual([{
|
||||
type: 'url',
|
||||
content: 'https://example.com/foo',
|
||||
url: 'https://example.com/foo'
|
||||
}, {
|
||||
type: 'text', content: ', bar'
|
||||
}], tokens);
|
||||
assert.deepEqual([
|
||||
node('url', { url: 'https://example.com/foo' }),
|
||||
text(', bar')
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('with brackets', () => {
|
||||
const tokens = analyze('https://example.com/foo(bar)');
|
||||
assert.deepEqual([{
|
||||
type: 'url',
|
||||
content: 'https://example.com/foo(bar)',
|
||||
url: 'https://example.com/foo(bar)'
|
||||
}], tokens);
|
||||
assert.deepEqual([
|
||||
node('url', { url: 'https://example.com/foo(bar)' })
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('ignore parent brackets', () => {
|
||||
const tokens = analyze('(https://example.com/foo)');
|
||||
assert.deepEqual([{
|
||||
type: 'text', content: '('
|
||||
}, {
|
||||
type: 'url',
|
||||
content: 'https://example.com/foo',
|
||||
url: 'https://example.com/foo'
|
||||
}, {
|
||||
type: 'text', content: ')'
|
||||
}], tokens);
|
||||
assert.deepEqual([
|
||||
text('('),
|
||||
node('url', { url: 'https://example.com/foo' }),
|
||||
text(')')
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('ignore parent brackets with internal brackets', () => {
|
||||
const tokens = analyze('(https://example.com/foo(bar))');
|
||||
assert.deepEqual([{
|
||||
type: 'text', content: '('
|
||||
}, {
|
||||
type: 'url',
|
||||
content: 'https://example.com/foo(bar)',
|
||||
url: 'https://example.com/foo(bar)'
|
||||
}, {
|
||||
type: 'text', content: ')'
|
||||
}], tokens);
|
||||
assert.deepEqual([
|
||||
text('('),
|
||||
node('url', { url: 'https://example.com/foo(bar)' }),
|
||||
text(')')
|
||||
], tokens);
|
||||
});
|
||||
});
|
||||
|
||||
it('link', () => {
|
||||
const tokens = analyze('[ひまさく](https://himasaku.net)');
|
||||
assert.deepEqual([{
|
||||
type: 'link',
|
||||
content: '[ひまさく](https://himasaku.net)',
|
||||
title: 'ひまさく',
|
||||
url: 'https://himasaku.net',
|
||||
silent: false
|
||||
}], tokens);
|
||||
const tokens = analyze('[foo](https://example.com)');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('link', [
|
||||
text('foo')
|
||||
], { url: 'https://example.com', silent: false })
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('emoji', () => {
|
||||
const tokens1 = analyze(':cat:');
|
||||
assert.deepEqual([
|
||||
{ type: 'emoji', content: ':cat:', name: 'cat' }
|
||||
node('emoji', { name: 'cat' })
|
||||
], tokens1);
|
||||
|
||||
const tokens2 = analyze(':cat::cat::cat:');
|
||||
assert.deepEqual([
|
||||
{ type: 'emoji', content: ':cat:', name: 'cat' },
|
||||
{ type: 'emoji', content: ':cat:', name: 'cat' },
|
||||
{ type: 'emoji', content: ':cat:', name: 'cat' }
|
||||
node('emoji', { name: 'cat' }),
|
||||
node('emoji', { name: 'cat' }),
|
||||
node('emoji', { name: 'cat' })
|
||||
], tokens2);
|
||||
|
||||
const tokens3 = analyze('🍎');
|
||||
assert.deepEqual([
|
||||
{ type: 'emoji', content: '🍎', emoji: '🍎' }
|
||||
node('emoji', { emoji: '🍎' })
|
||||
], tokens3);
|
||||
});
|
||||
|
||||
it('block code', () => {
|
||||
const tokens = analyze('```\nvar x = "Strawberry Pasta";\n```');
|
||||
assert.equal(tokens[0].type, 'code');
|
||||
assert.equal(tokens[0].content, '```\nvar x = "Strawberry Pasta";\n```');
|
||||
describe('block code', () => {
|
||||
it('simple', () => {
|
||||
const tokens = analyze('```\nvar x = "Strawberry Pasta";\n```');
|
||||
assert.deepEqual([
|
||||
node('blockCode', { code: 'var x = "Strawberry Pasta";', lang: null })
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('can specify language', () => {
|
||||
const tokens = analyze('``` json\n{ "x": 42 }\n```');
|
||||
assert.deepEqual([
|
||||
node('blockCode', { code: '{ "x": 42 }', lang: 'json' })
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('require line break before "```"', () => {
|
||||
const tokens = analyze('before```\nfoo\n```');
|
||||
assert.deepEqual([
|
||||
text('before'),
|
||||
node('inlineCode', { code: '`' }),
|
||||
text('\nfoo\n'),
|
||||
node('inlineCode', { code: '`' })
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('series', () => {
|
||||
const tokens = analyze('```\nfoo\n```\n```\nbar\n```\n```\nbaz\n```');
|
||||
assert.deepEqual([
|
||||
node('blockCode', { code: 'foo', lang: null }),
|
||||
node('blockCode', { code: 'bar', lang: null }),
|
||||
node('blockCode', { code: 'baz', lang: null }),
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('ignore internal marker', () => {
|
||||
const tokens = analyze('```\naaa```bbb\n```');
|
||||
assert.deepEqual([
|
||||
node('blockCode', { code: 'aaa```bbb', lang: null })
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('trim after line break', () => {
|
||||
const tokens = analyze('```\nfoo\n```\nbar');
|
||||
assert.deepEqual([
|
||||
node('blockCode', { code: 'foo', lang: null }),
|
||||
text('bar')
|
||||
], tokens);
|
||||
});
|
||||
});
|
||||
|
||||
it('inline code', () => {
|
||||
const tokens = analyze('`var x = "Strawberry Pasta";`');
|
||||
assert.equal(tokens[0].type, 'inline-code');
|
||||
assert.equal(tokens[0].content, '`var x = "Strawberry Pasta";`');
|
||||
assert.deepEqual([
|
||||
node('inlineCode', { code: 'var x = "Strawberry Pasta";' })
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('math', () => {
|
||||
@ -282,82 +460,88 @@ describe('Text', () => {
|
||||
const text = `\\(${fomula}\\)`;
|
||||
const tokens = analyze(text);
|
||||
assert.deepEqual([
|
||||
{ type: 'math', content: text, formula: fomula }
|
||||
node('math', { formula: fomula })
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('search', () => {
|
||||
const tokens1 = analyze('a b c 検索');
|
||||
assert.deepEqual([
|
||||
{ type: 'search', content: 'a b c 検索', query: 'a b c' }
|
||||
node('search', { content: 'a b c 検索', query: 'a b c' })
|
||||
], tokens1);
|
||||
|
||||
const tokens2 = analyze('a b c Search');
|
||||
assert.deepEqual([
|
||||
{ type: 'search', content: 'a b c Search', query: 'a b c' }
|
||||
node('search', { content: 'a b c Search', query: 'a b c' })
|
||||
], tokens2);
|
||||
|
||||
const tokens3 = analyze('a b c search');
|
||||
assert.deepEqual([
|
||||
{ type: 'search', content: 'a b c search', query: 'a b c' }
|
||||
node('search', { content: 'a b c search', query: 'a b c' })
|
||||
], tokens3);
|
||||
|
||||
const tokens4 = analyze('a b c SEARCH');
|
||||
assert.deepEqual([
|
||||
{ type: 'search', content: 'a b c SEARCH', query: 'a b c' }
|
||||
node('search', { content: 'a b c SEARCH', query: 'a b c' })
|
||||
], tokens4);
|
||||
});
|
||||
|
||||
it('title', () => {
|
||||
const tokens1 = analyze('【yee】\nhaw');
|
||||
assert.deepEqual(
|
||||
{ type: 'title', content: '【yee】\n', title: 'yee' }
|
||||
, tokens1[0]);
|
||||
describe('title', () => {
|
||||
it('simple', () => {
|
||||
const tokens = analyze('【foo】');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('title', [
|
||||
text('foo')
|
||||
])
|
||||
], tokens);
|
||||
});
|
||||
|
||||
const tokens2 = analyze('[yee]\nhaw');
|
||||
assert.deepEqual(
|
||||
{ type: 'title', content: '[yee]\n', title: 'yee' }
|
||||
, tokens2[0]);
|
||||
it('require line break', () => {
|
||||
const tokens = analyze('a【foo】');
|
||||
assert.deepEqual([
|
||||
text('a【foo】')
|
||||
], tokens);
|
||||
});
|
||||
|
||||
const tokens3 = analyze('a [a]\nb [b]\nc [c]');
|
||||
assert.deepEqual(
|
||||
{ type: 'text', content: 'a [a]\nb [b]\nc [c]' }
|
||||
, tokens3[0]);
|
||||
|
||||
const tokens4 = analyze('foo\n【bar】\nbuzz');
|
||||
assert.deepEqual([
|
||||
{ type: 'text', content: 'foo' },
|
||||
{ type: 'title', content: '\n【bar】\n', title: 'bar' },
|
||||
{ type: 'text', content: 'buzz' },
|
||||
], tokens4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('syntax highlighting', () => {
|
||||
it('comment', () => {
|
||||
const html1 = syntaxhighlighter('// Strawberry pasta');
|
||||
assert.equal(html1, '<span class="comment">// Strawberry pasta</span>');
|
||||
|
||||
const html2 = syntaxhighlighter('x // x\ny // y');
|
||||
assert.equal(html2, 'x <span class="comment">// x\n</span>y <span class="comment">// y</span>');
|
||||
});
|
||||
|
||||
it('regexp', () => {
|
||||
const html = syntaxhighlighter('/.*/');
|
||||
assert.equal(html, '<span class="regexp">/.*/</span>');
|
||||
});
|
||||
|
||||
it('slash', () => {
|
||||
const html = syntaxhighlighter('/');
|
||||
assert.equal(html, '<span class="symbol">/</span>');
|
||||
it('with before and after texts', () => {
|
||||
const tokens = analyze('before\n【foo】\nafter');
|
||||
assert.deepEqual([
|
||||
text('before'),
|
||||
nodeWithChildren('title', [
|
||||
text('foo')
|
||||
]),
|
||||
text('after')
|
||||
], tokens);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toHtml', () => {
|
||||
it('br', () => {
|
||||
const input = 'foo\nbar\nbaz';
|
||||
const output = '<p>foo<br>bar<br>baz</p>';
|
||||
const output = '<p><span>foo<br>bar<br>baz</span></p>';
|
||||
assert.equal(toHtml(analyze(input)), output);
|
||||
});
|
||||
});
|
||||
|
||||
it('code block with quote', () => {
|
||||
const tokens = analyze('> foo\n```\nbar\n```');
|
||||
assert.deepEqual([
|
||||
nodeWithChildren('quote', [
|
||||
text('foo')
|
||||
]),
|
||||
node('blockCode', { code: 'bar', lang: null })
|
||||
], tokens);
|
||||
});
|
||||
|
||||
it('quote between two code blocks', () => {
|
||||
const tokens = analyze('```\nbefore\n```\n> foo\n```\nafter\n```');
|
||||
assert.deepEqual([
|
||||
node('blockCode', { code: 'before', lang: null }),
|
||||
nodeWithChildren('quote', [
|
||||
text('foo')
|
||||
]),
|
||||
node('blockCode', { code: 'after', lang: null })
|
||||
], tokens);
|
||||
});
|
||||
});
|
||||
|
@ -38,6 +38,7 @@ module.exports = {
|
||||
dev: './src/client/app/dev/script.ts',
|
||||
auth: './src/client/app/auth/script.ts',
|
||||
admin: './src/client/app/admin/script.ts',
|
||||
test: './src/client/app/test/script.ts',
|
||||
sw: './src/client/app/sw.js'
|
||||
},
|
||||
module: {
|
||||
|
Reference in New Issue
Block a user