From 251c0dfe9c9e78bcb2b3504e7d89a1518f5cd6c4 Mon Sep 17 00:00:00 2001 From: mkljczk Date: Tue, 25 Mar 2025 12:15:57 +0100 Subject: [PATCH] pl-fe: add nyaize Signed-off-by: mkljczk --- packages/pl-api/lib/entities/account.ts | 8 +++-- packages/pl-api/package.json | 4 +-- .../pl-fe/src/components/parsed-content.tsx | 17 ++++++---- .../pl-fe/src/components/status-content.tsx | 2 +- packages/pl-fe/src/utils/nyaize.ts | 31 +++++++++++++++++++ 5 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 packages/pl-fe/src/utils/nyaize.ts diff --git a/packages/pl-api/lib/entities/account.ts b/packages/pl-api/lib/entities/account.ts index 4c512a48d..76627a613 100644 --- a/packages/pl-api/lib/entities/account.ts +++ b/packages/pl-api/lib/entities/account.ts @@ -29,6 +29,8 @@ const guessFqn = (account: Pick): 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), diff --git a/packages/pl-api/package.json b/packages/pl-api/package.json index 3c2249ea1..76351e2c5 100644 --- a/packages/pl-api/package.json +++ b/packages/pl-api/package.json @@ -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" diff --git a/packages/pl-fe/src/components/parsed-content.tsx b/packages/pl-fe/src/components/parsed-content.tsx index 73c7cb794..708d69a1b 100644 --- a/packages/pl-fe/src/components/parsed-content.tsx +++ b/packages/pl-fe/src/components/parsed-content.tsx @@ -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; -function parseContent(props: IParsedContent, extractHashtags: true, cleanUrls: boolean, greentext: boolean): { +function parseContent(props: IParsedContent, extractHashtags?: false, cleanUrls?: boolean, greentext?: boolean, speakAsCat?: boolean): ReturnType; +function parseContent(props: IParsedContent, extractHashtags: true, cleanUrls: boolean, greentext: boolean, speakAsCat: boolean): { hashtags: Array; content: ReturnType; }; @@ -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 {domNode.data}; + const data = speakAsCat ? nyaize(domNode.data) : domNode.data; + if (greentext && data.startsWith('>')) { + return {data}; } + + if (speakAsCat) return <>{data}; + return; } @@ -203,7 +208,7 @@ function parseContent({ const ParsedContent: React.FC = 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 }; diff --git a/packages/pl-fe/src/components/status-content.tsx b/packages/pl-fe/src/components/status-content.tsx index a71af2c31..51e114275 100644 --- a/packages/pl-fe/src/components/status-content.tsx +++ b/packages/pl-fe/src/components/status-content.tsx @@ -146,7 +146,7 @@ const StatusContent: React.FC = 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); diff --git a/packages/pl-fe/src/utils/nyaize.ts b/packages/pl-fe/src/utils/nyaize.ts new file mode 100644 index 000000000..eefd88b3d --- /dev/null +++ b/packages/pl-fe/src/utils/nyaize.ts @@ -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;