pl-fe: add nyaize

Signed-off-by: mkljczk <git@mkljczk.pl>
This commit is contained in:
mkljczk
2025-03-25 12:15:57 +01:00
parent de17e968b0
commit 251c0dfe9c
5 changed files with 50 additions and 12 deletions

View File

@ -29,6 +29,8 @@ const guessFqn = (account: Pick<Account, 'acct' | 'url'>): string => {
const filterBadges = (tags?: string[]) =>
tags?.filter(tag => tag.startsWith('badge:')).map(tag => v.parse(roleSchema, { id: tag, name: tag.replace(/^badge:/, '') }));
const MKLJCZK_ACCOUNTS = ['https://pl.fediverse.pl/users/mkljczk', 'https://gts.mkljczk.pl/users/mkljczk'];
const preprocessAccount = v.transform((account: any) => {
if (!account?.acct) return null;
@ -37,7 +39,7 @@ const preprocessAccount = v.transform((account: any) => {
const fqn = account.fqn || guessFqn(account);
const domain = fqn.split('@')[1] || '';
const isCat = (account.pleroma?.is_cat ?? account.is_cat) || account.uri === 'https://pl.fediverse.pl/users/mkljczk' || account.uri === 'https://gts.mkljczk.pl/users/mkljczk';
const isCat = (account.pleroma?.is_cat ?? account.is_cat) || MKLJCZK_ACCOUNTS.includes(account.uri ?? account.url);
const speakAsCat = account.pleroma?.speak_as_cat ?? account.speak_as_cat ?? isCat;
return {
@ -156,8 +158,8 @@ const baseAccountSchema = v.object({
pronouns: v.fallback(v.array(v.string()), []),
is_cat: v.fallback(v.optional(v.boolean()), false),
speak_as_cat: v.fallback(v.optional(v.boolean()), false),
is_cat: v.fallback(v.boolean(), false),
speak_as_cat: v.fallback(v.boolean(), false),
__meta: coerceObject({
pleroma: v.fallback(v.any(), undefined),

View File

@ -1,11 +1,11 @@
{
"name": "pl-api",
"version": "1.0.0-rc.32",
"version": "1.0.0-rc.33",
"type": "module",
"homepage": "https://github.com/mkljczk/pl-fe/tree/develop/packages/pl-api",
"repository": {
"type": "git",
"url": "https://github.com/mkljczk/pl-fe"
"url": "git+https://github.com/mkljczk/pl-fe.git"
},
"bugs": {
"url": "https://github.com/mkljczk/pl-fe/issues"

View File

@ -9,6 +9,7 @@ import { Link } from 'react-router-dom';
import Emojify from 'pl-fe/features/emoji/emojify';
import { useSettings } from 'pl-fe/hooks/use-settings';
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
import nyaize from 'pl-fe/utils/nyaize';
import Purify from 'pl-fe/utils/url-purify';
import HashtagLink from './hashtag-link';
@ -63,8 +64,8 @@ const uniqueHashtagsWithCaseHandling = (hashtags: string[]) => {
});
};
function parseContent(props: IParsedContent, extractHashtags?: false, cleanUrls?: boolean, greentext?: boolean): ReturnType<typeof domToReact>;
function parseContent(props: IParsedContent, extractHashtags: true, cleanUrls: boolean, greentext: boolean): {
function parseContent(props: IParsedContent, extractHashtags?: false, cleanUrls?: boolean, greentext?: boolean, speakAsCat?: boolean): ReturnType<typeof domToReact>;
function parseContent(props: IParsedContent, extractHashtags: true, cleanUrls: boolean, greentext: boolean, speakAsCat: boolean): {
hashtags: Array<string>;
content: ReturnType<typeof domToReact>;
};
@ -74,7 +75,7 @@ function parseContent({
mentions,
hasQuote,
emojis,
}: IParsedContent, extractHashtags = false, cleanUrls = false, greentext = false) {
}: IParsedContent, extractHashtags = false, cleanUrls = false, greentext = false, speakAsCat = false) {
if (html.length === 0) {
return extractHashtags ? { content: null, hashtags: [] } : null;
}
@ -94,9 +95,13 @@ function parseContent({
const options: HTMLReactParserOptions = {
replace(domNode) {
if (!(domNode instanceof Element)) {
if (greentext && domNode.data.startsWith('>')) {
return <span className='dark:text-accent-green text-lime-600'>{domNode.data}</span>;
const data = speakAsCat ? nyaize(domNode.data) : domNode.data;
if (greentext && data.startsWith('>')) {
return <span className='dark:text-accent-green text-lime-600'>{data}</span>;
}
if (speakAsCat) return <>{data}</>;
return;
}
@ -203,7 +208,7 @@ function parseContent({
const ParsedContent: React.FC<IParsedContent> = React.memo((props) => {
const settings = useSettings();
return parseContent(props, false, settings.urlPrivacy.clearLinksInContent, false);
return parseContent(props, false, settings.urlPrivacy.clearLinksInContent, false, false);
}, (prevProps, nextProps) => prevProps.html === nextProps.html);
export { ParsedContent, parseContent };

View File

@ -146,7 +146,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
mentions: status.mentions,
hasQuote: !!status.quote_id,
emojis: status.emojis,
}, true, urlPrivacy.clearLinksInContent, greentext), [content]);
}, true, urlPrivacy.clearLinksInContent, greentext, status.account.speak_as_cat), [content]);
useEffect(() => {
setLineClamp(!spoilerNode.current || spoilerNode.current.clientHeight >= 96);

View File

@ -0,0 +1,31 @@
// Adapted from Sharkey, licensed under AGPL-3.0-only
// https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/packages/misskey-js/src/nyaize.ts
const koRegex1 = /[나-낳]/g;
const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm;
const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm;
const ifAfter = (prefix: string, fn: (x: string) => string) => {
const preLen = prefix.length;
const regex = new RegExp(prefix, 'i');
return (x: string, pos: number, string: string) => {
return pos > 0 && string.substring(pos - preLen, pos).match(regex) ? fn(x) : x;
};
};
const nyaize = (text: string) =>
text
// ja-JP
.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
// en-US
.replace(/a/gi, ifAfter('n', x => x === 'A' ? 'YA' : 'ya'))
.replace(/ing/gi, ifAfter('morn', x => x === 'ING' ? 'YAN' : 'yan'))
.replace(/one/gi, ifAfter('every', x => x === 'ONE' ? 'NYAN' : 'nyan'))
// ko-KR
.replace(koRegex1, match => !isNaN(match.charCodeAt(0)) ? String.fromCharCode(
match.charCodeAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
) : match)
.replace(koRegex2, '다냥')
.replace(koRegex3, '냥');
export default nyaize;