It's micro-optimization week

Includes experimental replacing dangerouslySetInnerHTML with setting actual DOM
This commit is contained in:
Lim Chee Aun 2024-09-25 17:18:20 +08:00
parent 4d35c9d2c3
commit 65bd6fb1ef
7 changed files with 316 additions and 242 deletions

View file

@ -23,10 +23,12 @@ const ctx = canvas.getContext('2d', {
});
ctx.imageSmoothingEnabled = false;
const MISSING_IMAGE_PATH_REGEX = /missing\.png$/;
function Avatar({ url, size, alt = '', squircle, ...props }) {
size = SIZES[size] || size || SIZES.m;
const avatarRef = useRef();
const isMissing = /missing\.png$/.test(url);
const isMissing = MISSING_IMAGE_PATH_REGEX.test(url);
return (
<span
ref={avatarRef}

View file

@ -1,29 +1,40 @@
import { memo } from 'preact/compat';
import mem from '../utils/mem';
import CustomEmoji from './custom-emoji';
const shortcodesRegexp = mem((shortcodes) => {
return new RegExp(`:(${shortcodes.join('|')}):`, 'g');
});
function EmojiText({ text, emojis }) {
if (!text) return '';
if (!emojis?.length) return text;
if (text.indexOf(':') === -1) return text;
const regex = new RegExp(
`:(${emojis.map((e) => e.shortcode).join('|')}):`,
'g',
);
// const regex = new RegExp(
// `:(${emojis.map((e) => e.shortcode).join('|')}):`,
// 'g',
// );
const regex = shortcodesRegexp(emojis.map((e) => e.shortcode));
const elements = text.split(regex).map((word) => {
const emoji = emojis.find((e) => e.shortcode === word);
if (emoji) {
const { url, staticUrl } = emoji;
return <CustomEmoji staticUrl={staticUrl} alt={word} url={url} />;
return (
<CustomEmoji staticUrl={staticUrl} alt={word} url={url} key={word} />
);
}
return word;
});
return elements;
}
export default memo(
EmojiText,
(oldProps, newProps) =>
oldProps.text === newProps.text &&
oldProps.emojis?.length === newProps.emojis?.length,
);
export default mem(EmojiText);
// export default memo(
// EmojiText,
// (oldProps, newProps) =>
// oldProps.text === newProps.text &&
// oldProps.emojis?.length === newProps.emojis?.length,
// );

View file

@ -21,6 +21,11 @@ const nameCollator = mem((locale) => {
}
});
const ACCT_REGEX = /([^@]+)(@.+)/i;
const SHORTCODES_REGEX = /(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g;
const SPACES_REGEX = /\s+/g;
const NON_ALPHA_NUMERIC_REGEX = /[^a-z0-9@\.]/gi;
function NameText({
account,
instance,
@ -42,17 +47,17 @@ function NameText({
bot,
username,
} = account;
const [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
const [_, acct1, acct2] = acct.match(ACCT_REGEX) || [, acct];
if (!instance) instance = api().instance;
const trimmedUsername = username.toLowerCase().trim();
const trimmedDisplayName = (displayName || '').toLowerCase().trim();
const shortenedDisplayName = trimmedDisplayName
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
.replace(/\s+/g, ''); // E.g. "My name" === "myname"
.replace(SHORTCODES_REGEX, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
.replace(SPACES_REGEX, ''); // E.g. "My name" === "myname"
const shortenedAlphaNumericDisplayName = shortenedDisplayName.replace(
/[^a-z0-9@\.]/gi,
NON_ALPHA_NUMERIC_REGEX,
'',
); // Remove non-alphanumeric characters
@ -130,9 +135,11 @@ function NameText({
);
}
export default memo(NameText, (oldProps, newProps) => {
// Only care about account.id, the other props usually don't change
const { account } = oldProps;
const { account: newAccount } = newProps;
return account?.acct === newAccount?.acct;
});
export default mem(NameText);
// export default memo(NameText, (oldProps, newProps) => {
// // Only care about account.id, the other props usually don't change
// const { account } = oldProps;
// const { account: newAccount } = newProps;
// return account?.acct === newAccount?.acct;
// });

View file

@ -124,11 +124,31 @@ function getPostText(status) {
);
}
const PostContent = memo(
const HTTP_REGEX = /^http/i;
const PostContent =
/*memo(*/
({ post, instance, previewMode }) => {
const { content, emojis, language, mentions, url } = post;
const divRef = useRef();
useEffect(() => {
if (!divRef.current) return;
const dom = enhanceContent(content, {
emojis,
returnDOM: true,
});
// Remove target="_blank" from links
for (const a of dom.querySelectorAll('a.u-url[target="_blank"]')) {
if (!HTTP_REGEX.test(a.innerText.trim())) {
a.removeAttribute('target');
}
}
divRef.current.replaceChildren(dom.cloneNode(true));
}, [content, emojis.length]);
return (
<div
ref={divRef}
lang={language}
dir="auto"
class="inner-content"
@ -138,28 +158,28 @@ const PostContent = memo(
previewMode,
statusURL: url,
})}
dangerouslySetInnerHTML={{
__html: enhanceContent(content, {
emojis,
postEnhanceDOM: (dom) => {
// Remove target="_blank" from links
dom.querySelectorAll('a.u-url[target="_blank"]').forEach((a) => {
if (!/http/i.test(a.innerText.trim())) {
a.removeAttribute('target');
}
});
},
}),
}}
// dangerouslySetInnerHTML={{
// __html: enhanceContent(content, {
// emojis,
// postEnhanceDOM: (dom) => {
// // Remove target="_blank" from links
// dom.querySelectorAll('a.u-url[target="_blank"]').forEach((a) => {
// if (!/http/i.test(a.innerText.trim())) {
// a.removeAttribute('target');
// }
// });
// },
// }),
// }}
/>
);
},
}; /*,
(oldProps, newProps) => {
const { post: oldPost } = oldProps;
const { post: newPost } = newProps;
return oldPost.content === newPost.content;
},
);
);*/
const SIZE_CLASS = {
s: 'small',

288
src/locales/en.po generated
View file

@ -13,40 +13,40 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
#: src/components/account-block.jsx:133
#: src/components/account-block.jsx:135
msgid "Locked"
msgstr ""
#: src/components/account-block.jsx:139
#: src/components/account-block.jsx:141
msgid "Posts: {0}"
msgstr ""
#: src/components/account-block.jsx:144
#: src/components/account-block.jsx:146
msgid "Last posted: {0}"
msgstr ""
#: src/components/account-block.jsx:159
#: src/components/account-block.jsx:161
#: src/components/account-info.jsx:634
msgid "Automated"
msgstr ""
#: src/components/account-block.jsx:166
#: src/components/account-block.jsx:168
#: src/components/account-info.jsx:639
#: src/components/status.jsx:440
#: src/components/status.jsx:460
#: src/pages/catchup.jsx:1471
msgid "Group"
msgstr ""
#: src/components/account-block.jsx:176
#: src/components/account-block.jsx:178
msgid "Mutual"
msgstr ""
#: src/components/account-block.jsx:180
#: src/components/account-block.jsx:182
#: src/components/account-info.jsx:1677
msgid "Requested"
msgstr ""
#: src/components/account-block.jsx:184
#: src/components/account-block.jsx:186
#: src/components/account-info.jsx:416
#: src/components/account-info.jsx:742
#: src/components/account-info.jsx:756
@ -58,21 +58,21 @@ msgstr ""
msgid "Following"
msgstr ""
#: src/components/account-block.jsx:188
#: src/components/account-block.jsx:190
#: src/components/account-info.jsx:1059
msgid "Follows you"
msgstr ""
#: src/components/account-block.jsx:196
#: src/components/account-block.jsx:198
msgid "{followersCount, plural, one {# follower} other {# followers}}"
msgstr ""
#: src/components/account-block.jsx:205
#: src/components/account-block.jsx:207
#: src/components/account-info.jsx:680
msgid "Verified"
msgstr ""
#: src/components/account-block.jsx:220
#: src/components/account-block.jsx:222
#: src/components/account-info.jsx:777
msgid "Joined <0>{0}</0>"
msgstr ""
@ -108,11 +108,11 @@ msgstr ""
#: src/components/compose.jsx:2463
#: src/components/media-alt-modal.jsx:45
#: src/components/media-modal.jsx:283
#: src/components/status.jsx:1636
#: src/components/status.jsx:1653
#: src/components/status.jsx:1777
#: src/components/status.jsx:2375
#: src/components/status.jsx:2378
#: src/components/status.jsx:1656
#: src/components/status.jsx:1673
#: src/components/status.jsx:1797
#: src/components/status.jsx:2395
#: src/components/status.jsx:2398
#: src/pages/account-statuses.jsx:528
#: src/pages/accounts.jsx:109
#: src/pages/hashtag.jsx:199
@ -181,7 +181,7 @@ msgid "Original"
msgstr ""
#: src/components/account-info.jsx:859
#: src/components/status.jsx:2166
#: src/components/status.jsx:2186
#: src/pages/catchup.jsx:71
#: src/pages/catchup.jsx:1445
#: src/pages/catchup.jsx:2056
@ -277,30 +277,30 @@ msgid "Add/Remove from Lists"
msgstr ""
#: src/components/account-info.jsx:1299
#: src/components/status.jsx:1079
#: src/components/status.jsx:1099
msgid "Link copied"
msgstr ""
#: src/components/account-info.jsx:1302
#: src/components/status.jsx:1082
#: src/components/status.jsx:1102
msgid "Unable to copy link"
msgstr ""
#: src/components/account-info.jsx:1308
#: src/components/shortcuts-settings.jsx:1056
#: src/components/status.jsx:1088
#: src/components/status.jsx:3150
#: src/components/status.jsx:1108
#: src/components/status.jsx:3170
msgid "Copy"
msgstr ""
#: src/components/account-info.jsx:1323
#: src/components/shortcuts-settings.jsx:1074
#: src/components/status.jsx:1104
#: src/components/status.jsx:1124
msgid "Sharing doesn't seem to work."
msgstr ""
#: src/components/account-info.jsx:1329
#: src/components/status.jsx:1110
#: src/components/status.jsx:1130
msgid "Share…"
msgstr ""
@ -417,9 +417,9 @@ msgstr ""
#: src/components/shortcuts-settings.jsx:227
#: src/components/shortcuts-settings.jsx:580
#: src/components/shortcuts-settings.jsx:780
#: src/components/status.jsx:2875
#: src/components/status.jsx:3114
#: src/components/status.jsx:3612
#: src/components/status.jsx:2895
#: src/components/status.jsx:3134
#: src/components/status.jsx:3632
#: src/pages/accounts.jsx:36
#: src/pages/catchup.jsx:1581
#: src/pages/filters.jsx:224
@ -605,7 +605,7 @@ msgid "Attachment #{i} failed"
msgstr ""
#: src/components/compose.jsx:1118
#: src/components/status.jsx:1962
#: src/components/status.jsx:1982
#: src/components/timeline.jsx:984
msgid "Content warning"
msgstr ""
@ -641,7 +641,7 @@ msgstr ""
#: src/components/compose.jsx:1185
#: src/components/status.jsx:96
#: src/components/status.jsx:1840
#: src/components/status.jsx:1860
msgid "Private mention"
msgstr ""
@ -671,10 +671,10 @@ msgstr ""
#: src/components/compose.jsx:1479
#: src/components/keyboard-shortcuts-help.jsx:143
#: src/components/status.jsx:831
#: src/components/status.jsx:1616
#: src/components/status.jsx:1617
#: src/components/status.jsx:2271
#: src/components/status.jsx:851
#: src/components/status.jsx:1636
#: src/components/status.jsx:1637
#: src/components/status.jsx:2291
msgid "Reply"
msgstr ""
@ -889,7 +889,7 @@ msgstr ""
#: src/components/drafts.jsx:127
#: src/components/list-add-edit.jsx:183
#: src/components/status.jsx:1251
#: src/components/status.jsx:1271
#: src/pages/filters.jsx:587
msgid "Delete…"
msgstr ""
@ -1089,10 +1089,10 @@ msgid "<0>l</0> or <1>f</1>"
msgstr ""
#: src/components/keyboard-shortcuts-help.jsx:164
#: src/components/status.jsx:839
#: src/components/status.jsx:2297
#: src/components/status.jsx:2329
#: src/components/status.jsx:2330
#: src/components/status.jsx:859
#: src/components/status.jsx:2317
#: src/components/status.jsx:2349
#: src/components/status.jsx:2350
msgid "Boost"
msgstr ""
@ -1101,9 +1101,9 @@ msgid "<0>Shift</0> + <1>b</1>"
msgstr ""
#: src/components/keyboard-shortcuts-help.jsx:172
#: src/components/status.jsx:924
#: src/components/status.jsx:2354
#: src/components/status.jsx:2355
#: src/components/status.jsx:944
#: src/components/status.jsx:2374
#: src/components/status.jsx:2375
msgid "Bookmark"
msgstr ""
@ -1162,15 +1162,15 @@ msgid "Media description"
msgstr ""
#: src/components/media-alt-modal.jsx:57
#: src/components/status.jsx:968
#: src/components/status.jsx:995
#: src/components/status.jsx:988
#: src/components/status.jsx:1015
#: src/components/translation-block.jsx:195
msgid "Translate"
msgstr ""
#: src/components/media-alt-modal.jsx:68
#: src/components/status.jsx:982
#: src/components/status.jsx:1009
#: src/components/status.jsx:1002
#: src/components/status.jsx:1029
msgid "Speak"
msgstr ""
@ -1207,9 +1207,9 @@ msgid "Filtered: {filterTitleStr}"
msgstr ""
#: src/components/media-post.jsx:133
#: src/components/status.jsx:3442
#: src/components/status.jsx:3538
#: src/components/status.jsx:3616
#: src/components/status.jsx:3462
#: src/components/status.jsx:3558
#: src/components/status.jsx:3636
#: src/components/timeline.jsx:973
#: src/pages/catchup.jsx:75
#: src/pages/catchup.jsx:1876
@ -1489,8 +1489,8 @@ msgid "[Unknown notification type: {type}]"
msgstr ""
#: src/components/notification.jsx:433
#: src/components/status.jsx:938
#: src/components/status.jsx:948
#: src/components/status.jsx:958
#: src/components/status.jsx:968
msgid "Boosted/Liked by…"
msgstr ""
@ -1512,7 +1512,7 @@ msgid "Learn more <0/>"
msgstr ""
#: src/components/notification.jsx:745
#: src/components/status.jsx:190
#: src/components/status.jsx:210
msgid "Read more →"
msgstr ""
@ -1805,7 +1805,7 @@ msgid "Move down"
msgstr ""
#: src/components/shortcuts-settings.jsx:376
#: src/components/status.jsx:1216
#: src/components/status.jsx:1236
#: src/pages/list.jsx:170
msgid "Edit"
msgstr ""
@ -2003,297 +2003,297 @@ msgstr ""
msgid "Import/export settings from/to instance server (Very experimental)"
msgstr ""
#: src/components/status.jsx:464
#: src/components/status.jsx:484
msgid "<0/> <1>boosted</1>"
msgstr ""
#: src/components/status.jsx:563
#: src/components/status.jsx:583
msgid "Sorry, your current logged-in instance can't interact with this post from another instance."
msgstr ""
#: src/components/status.jsx:716
#: src/components/status.jsx:736
msgid "Unliked @{0}'s post"
msgstr "Unliked @{0}'s post"
#: src/components/status.jsx:717
#: src/components/status.jsx:737
msgid "Liked @{0}'s post"
msgstr "Liked @{0}'s post"
#: src/components/status.jsx:756
#: src/components/status.jsx:776
msgid "Unbookmarked @{0}'s post"
msgstr ""
#: src/components/status.jsx:757
#: src/components/status.jsx:777
msgid "Bookmarked @{0}'s post"
msgstr ""
#: src/components/status.jsx:839
#: src/components/status.jsx:901
#: src/components/status.jsx:2297
#: src/components/status.jsx:2329
#: src/components/status.jsx:859
#: src/components/status.jsx:921
#: src/components/status.jsx:2317
#: src/components/status.jsx:2349
msgid "Unboost"
msgstr ""
#: src/components/status.jsx:855
#: src/components/status.jsx:2312
#: src/components/status.jsx:875
#: src/components/status.jsx:2332
msgid "Quote"
msgstr ""
#: src/components/status.jsx:863
#: src/components/status.jsx:2321
#: src/components/status.jsx:883
#: src/components/status.jsx:2341
msgid "Some media have no descriptions."
msgstr ""
#: src/components/status.jsx:870
#: src/components/status.jsx:890
msgid "Old post (<0>{0}</0>)"
msgstr ""
#: src/components/status.jsx:889
#: src/components/status.jsx:1341
#: src/components/status.jsx:909
#: src/components/status.jsx:1361
msgid "Unboosted @{0}'s post"
msgstr ""
#: src/components/status.jsx:890
#: src/components/status.jsx:1342
#: src/components/status.jsx:910
#: src/components/status.jsx:1362
msgid "Boosted @{0}'s post"
msgstr ""
#: src/components/status.jsx:902
#: src/components/status.jsx:922
msgid "Boost…"
msgstr ""
#: src/components/status.jsx:914
#: src/components/status.jsx:1626
#: src/components/status.jsx:2342
#: src/components/status.jsx:934
#: src/components/status.jsx:1646
#: src/components/status.jsx:2362
msgid "Unlike"
msgstr ""
#: src/components/status.jsx:915
#: src/components/status.jsx:1626
#: src/components/status.jsx:1627
#: src/components/status.jsx:2342
#: src/components/status.jsx:2343
#: src/components/status.jsx:935
#: src/components/status.jsx:1646
#: src/components/status.jsx:1647
#: src/components/status.jsx:2362
#: src/components/status.jsx:2363
msgid "Like"
msgstr ""
#: src/components/status.jsx:924
#: src/components/status.jsx:2354
#: src/components/status.jsx:944
#: src/components/status.jsx:2374
msgid "Unbookmark"
msgstr ""
#: src/components/status.jsx:1032
#: src/components/status.jsx:1052
msgid "View post by <0>@{0}</0>"
msgstr ""
#: src/components/status.jsx:1053
#: src/components/status.jsx:1073
msgid "Show Edit History"
msgstr ""
#: src/components/status.jsx:1056
#: src/components/status.jsx:1076
msgid "Edited: {editedDateText}"
msgstr ""
#: src/components/status.jsx:1123
#: src/components/status.jsx:3119
#: src/components/status.jsx:1143
#: src/components/status.jsx:3139
msgid "Embed post"
msgstr ""
#: src/components/status.jsx:1137
#: src/components/status.jsx:1157
msgid "Conversation unmuted"
msgstr ""
#: src/components/status.jsx:1137
#: src/components/status.jsx:1157
msgid "Conversation muted"
msgstr ""
#: src/components/status.jsx:1143
#: src/components/status.jsx:1163
msgid "Unable to unmute conversation"
msgstr ""
#: src/components/status.jsx:1144
#: src/components/status.jsx:1164
msgid "Unable to mute conversation"
msgstr ""
#: src/components/status.jsx:1153
#: src/components/status.jsx:1173
msgid "Unmute conversation"
msgstr ""
#: src/components/status.jsx:1160
#: src/components/status.jsx:1180
msgid "Mute conversation"
msgstr ""
#: src/components/status.jsx:1176
#: src/components/status.jsx:1196
msgid "Post unpinned from profile"
msgstr ""
#: src/components/status.jsx:1177
#: src/components/status.jsx:1197
msgid "Post pinned to profile"
msgstr ""
#: src/components/status.jsx:1182
#: src/components/status.jsx:1202
msgid "Unable to unpin post"
msgstr ""
#: src/components/status.jsx:1182
#: src/components/status.jsx:1202
msgid "Unable to pin post"
msgstr ""
#: src/components/status.jsx:1191
#: src/components/status.jsx:1211
msgid "Unpin from profile"
msgstr ""
#: src/components/status.jsx:1198
#: src/components/status.jsx:1218
msgid "Pin to profile"
msgstr ""
#: src/components/status.jsx:1227
#: src/components/status.jsx:1247
msgid "Delete this post?"
msgstr ""
#: src/components/status.jsx:1240
#: src/components/status.jsx:1260
msgid "Post deleted"
msgstr ""
#: src/components/status.jsx:1243
#: src/components/status.jsx:1263
msgid "Unable to delete post"
msgstr ""
#: src/components/status.jsx:1271
#: src/components/status.jsx:1291
msgid "Report post…"
msgstr ""
#: src/components/status.jsx:1627
#: src/components/status.jsx:1663
#: src/components/status.jsx:2343
#: src/components/status.jsx:1647
#: src/components/status.jsx:1683
#: src/components/status.jsx:2363
msgid "Liked"
msgstr ""
#: src/components/status.jsx:1660
#: src/components/status.jsx:2330
#: src/components/status.jsx:1680
#: src/components/status.jsx:2350
msgid "Boosted"
msgstr ""
#: src/components/status.jsx:1670
#: src/components/status.jsx:2355
#: src/components/status.jsx:1690
#: src/components/status.jsx:2375
msgid "Bookmarked"
msgstr ""
#: src/components/status.jsx:1674
#: src/components/status.jsx:1694
msgid "Pinned"
msgstr ""
#: src/components/status.jsx:1719
#: src/components/status.jsx:2174
#: src/components/status.jsx:1739
#: src/components/status.jsx:2194
msgid "Deleted"
msgstr ""
#: src/components/status.jsx:1760
#: src/components/status.jsx:1780
msgid "{repliesCount, plural, one {# reply} other {# replies}}"
msgstr ""
#: src/components/status.jsx:1849
#: src/components/status.jsx:1869
msgid "Thread{0}"
msgstr ""
#: src/components/status.jsx:1925
#: src/components/status.jsx:1987
#: src/components/status.jsx:2072
#: src/components/status.jsx:1945
#: src/components/status.jsx:2007
#: src/components/status.jsx:2092
msgid "Show less"
msgstr ""
#: src/components/status.jsx:1925
#: src/components/status.jsx:1987
#: src/components/status.jsx:1945
#: src/components/status.jsx:2007
msgid "Show content"
msgstr ""
#: src/components/status.jsx:2072
#: src/components/status.jsx:2092
msgid "Show media"
msgstr ""
#: src/components/status.jsx:2195
#: src/components/status.jsx:2215
msgid "Edited"
msgstr ""
#: src/components/status.jsx:2272
#: src/components/status.jsx:2292
msgid "Comments"
msgstr ""
#. More from [Author]
#: src/components/status.jsx:2581
#: src/components/status.jsx:2601
msgid "More from <0/>"
msgstr "More from <0/>"
#: src/components/status.jsx:2880
#: src/components/status.jsx:2900
msgid "Edit History"
msgstr ""
#: src/components/status.jsx:2884
#: src/components/status.jsx:2904
msgid "Failed to load history"
msgstr ""
#: src/components/status.jsx:2889
#: src/components/status.jsx:2909
msgid "Loading…"
msgstr ""
#: src/components/status.jsx:3124
#: src/components/status.jsx:3144
msgid "HTML Code"
msgstr ""
#: src/components/status.jsx:3141
#: src/components/status.jsx:3161
msgid "HTML code copied"
msgstr ""
#: src/components/status.jsx:3144
#: src/components/status.jsx:3164
msgid "Unable to copy HTML code"
msgstr ""
#: src/components/status.jsx:3156
#: src/components/status.jsx:3176
msgid "Media attachments:"
msgstr ""
#: src/components/status.jsx:3178
#: src/components/status.jsx:3198
msgid "Account Emojis:"
msgstr ""
#: src/components/status.jsx:3209
#: src/components/status.jsx:3254
#: src/components/status.jsx:3229
#: src/components/status.jsx:3274
msgid "static URL"
msgstr ""
#: src/components/status.jsx:3223
#: src/components/status.jsx:3243
msgid "Emojis:"
msgstr ""
#: src/components/status.jsx:3268
#: src/components/status.jsx:3288
msgid "Notes:"
msgstr ""
#: src/components/status.jsx:3272
#: src/components/status.jsx:3292
msgid "This is static, unstyled and scriptless. You may need to apply your own styles and edit as needed."
msgstr ""
#: src/components/status.jsx:3278
#: src/components/status.jsx:3298
msgid "Polls are not interactive, becomes a list with vote counts."
msgstr ""
#: src/components/status.jsx:3283
#: src/components/status.jsx:3303
msgid "Media attachments can be images, videos, audios or any file types."
msgstr ""
#: src/components/status.jsx:3289
#: src/components/status.jsx:3309
msgid "Post could be edited or deleted later."
msgstr ""
#: src/components/status.jsx:3295
#: src/components/status.jsx:3315
msgid "Preview"
msgstr ""
#: src/components/status.jsx:3304
#: src/components/status.jsx:3324
msgid "Note: This preview is lightly styled."
msgstr ""
#. [Name] [Visibility icon] boosted
#: src/components/status.jsx:3546
#: src/components/status.jsx:3566
msgid "<0/> <1/> boosted"
msgstr ""

View file

@ -4,53 +4,85 @@ import mem from './mem';
const fauxDiv = document.createElement('div');
const whitelistLinkClasses = ['u-url', 'mention', 'hashtag'];
const HTML_CHARS_REGEX = /[&<>]/g;
function escapeHTML(html) {
return html.replace(
HTML_CHARS_REGEX,
(c) =>
({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
}[c]),
);
}
const LINK_REGEX = /<a/i;
const HTTP_LINK_REGEX = /^https?:\/\//i;
const MENTION_REGEX = /^@[^@]+(@[^@]+)?$/g;
const HASHTAG_REGEX = /^#[^#]+$/g;
const CODE_BLOCK_REGEX = /^```[^]+```$/g;
const CODE_BLOCK_START_REGEX = /^```/g;
const CODE_BLOCK_END_REGEX = /```$/g;
const INLINE_CODE_REGEX = /`[^`]+`/g;
const TWITTER_DOMAIN_REGEX = /(twitter|x)\.com/i;
const TWITTER_MENTION_REGEX = /@[a-zA-Z0-9_]+@(twitter|x)\.com/g;
const TWITTER_MENTION_CAPTURE_REGEX = /(@([a-zA-Z0-9_]+)@(twitter|x)\.com)/g;
function createDOM(html, isDocumentFragment) {
const tpl = document.createElement('template');
tpl.innerHTML = html;
return isDocumentFragment ? tpl.content : tpl;
}
function _enhanceContent(content, opts = {}) {
const { emojis, postEnhanceDOM = () => {} } = opts;
const { emojis, returnDOM, postEnhanceDOM = () => {} } = opts;
let enhancedContent = content;
const dom = document.createElement('div');
dom.innerHTML = enhancedContent;
const hasLink = /<a/i.test(enhancedContent);
// const dom = document.createElement('div');
// dom.innerHTML = enhancedContent;
const dom = createDOM(enhancedContent, returnDOM);
const hasLink = LINK_REGEX.test(enhancedContent);
const hasCodeBlock = enhancedContent.includes('```');
if (hasLink) {
// Add target="_blank" to all links with no target="_blank"
// E.g. `note` in `account`
const noTargetBlankLinks = dom.querySelectorAll('a:not([target="_blank"])');
noTargetBlankLinks.forEach((link) => {
for (const link of noTargetBlankLinks) {
link.setAttribute('target', '_blank');
});
}
// Remove all classes except `u-url`, `mention`, `hashtag`
const links = dom.querySelectorAll('a[class]');
links.forEach((link) => {
link.classList.forEach((c) => {
for (const link of links) {
for (const c of link.classList) {
if (!whitelistLinkClasses.includes(c)) {
link.classList.remove(c);
}
});
});
}
}
}
// Add 'has-url-text' to all links that contains a url
if (hasLink) {
const links = dom.querySelectorAll('a[href]');
links.forEach((link) => {
if (/^https?:\/\//i.test(link.textContent.trim())) {
for (const link of links) {
if (HTTP_LINK_REGEX.test(link.textContent.trim())) {
link.classList.add('has-url-text');
shortenLink(link);
}
});
}
}
// Spanify un-spanned mentions
if (hasLink) {
const links = dom.querySelectorAll('a[href]');
const usernames = [];
links.forEach((link) => {
for (const link of links) {
const text = link.innerText.trim();
const hasChildren = link.querySelector('*');
// If text looks like @username@domain, then it's a mention
if (/^@[^@]+(@[^@]+)?$/g.test(text)) {
if (MENTION_REGEX.test(text)) {
// Only show @username
const [_, username, domain] = text.split('@');
if (!hasChildren) {
@ -67,11 +99,11 @@ function _enhanceContent(content, opts = {}) {
link.classList.add('mention');
}
// If text looks like #hashtag, then it's a hashtag
if (/^#[^#]+$/g.test(text)) {
if (HASHTAG_REGEX.test(text)) {
if (!hasChildren) link.innerHTML = `#<span>${text.slice(1)}</span>`;
link.classList.add('mention', 'hashtag');
}
});
}
}
// EMOJIS
@ -80,18 +112,14 @@ function _enhanceContent(content, opts = {}) {
let textNodes;
if (enhancedContent.includes(':')) {
textNodes = extractTextNodes(dom);
textNodes.forEach((node) => {
let html = node.nodeValue
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
for (const node of textNodes) {
let html = escapeHTML(node.nodeValue);
if (emojis) {
html = emojifyText(html, emojis);
}
fauxDiv.innerHTML = html;
// const nodes = [...fauxDiv.childNodes];
node.replaceWith(...fauxDiv.childNodes);
});
}
}
// CODE BLOCKS
@ -99,31 +127,35 @@ function _enhanceContent(content, opts = {}) {
// Convert ```code``` to <pre><code>code</code></pre>
if (hasCodeBlock) {
const blocks = [...dom.querySelectorAll('p')].filter((p) =>
/^```[^]+```$/g.test(p.innerText.trim()),
CODE_BLOCK_REGEX.test(p.innerText.trim()),
);
blocks.forEach((block) => {
for (const block of blocks) {
const pre = document.createElement('pre');
// Replace <br /> with newlines
block.querySelectorAll('br').forEach((br) => br.replaceWith('\n'));
for (const br of block.querySelectorAll('br')) {
br.replaceWith('\n');
}
pre.innerHTML = `<code>${block.innerHTML.trim()}</code>`;
block.replaceWith(pre);
});
}
}
// Convert multi-paragraph code blocks to <pre><code>code</code></pre>
if (hasCodeBlock) {
const paragraphs = [...dom.querySelectorAll('p')];
// Filter out paragraphs with ``` in beginning only
const codeBlocks = paragraphs.filter((p) => /^```/g.test(p.innerText));
const codeBlocks = paragraphs.filter((p) =>
CODE_BLOCK_START_REGEX.test(p.innerText),
);
// For each codeBlocks, get all paragraphs until the last paragraph with ``` at the end only
codeBlocks.forEach((block) => {
for (const block of codeBlocks) {
const nextParagraphs = [block];
let hasCodeBlock = false;
let currentBlock = block;
while (currentBlock.nextElementSibling) {
const next = currentBlock.nextElementSibling;
if (next && next.tagName === 'P') {
if (/```$/g.test(next.innerText)) {
if (CODE_BLOCK_END_REGEX.test(next.innerText)) {
nextParagraphs.push(next);
hasCodeBlock = true;
break;
@ -137,16 +169,20 @@ function _enhanceContent(content, opts = {}) {
}
if (hasCodeBlock) {
const pre = document.createElement('pre');
nextParagraphs.forEach((p) => {
for (const p of nextParagraphs) {
// Replace <br /> with newlines
p.querySelectorAll('br').forEach((br) => br.replaceWith('\n'));
});
for (const br of p.querySelectorAll('br')) {
br.replaceWith('\n');
}
}
const codeText = nextParagraphs.map((p) => p.innerHTML).join('\n\n');
pre.innerHTML = `<code tabindex="0">${codeText}</code>`;
block.replaceWith(pre);
nextParagraphs.forEach((p) => p.remove());
for (const p of nextParagraphs) {
p.remove();
}
}
}
});
}
// INLINE CODE
@ -154,42 +190,36 @@ function _enhanceContent(content, opts = {}) {
// Convert `code` to <code>code</code>
if (enhancedContent.includes('`')) {
textNodes = extractTextNodes(dom);
textNodes.forEach((node) => {
let html = node.nodeValue
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
if (/`[^`]+`/g.test(html)) {
for (const node of textNodes) {
let html = escapeHTML(node.nodeValue);
if (INLINE_CODE_REGEX.test(html)) {
html = html.replaceAll(/(`[^]+?`)/g, '<code>$1</code>');
}
fauxDiv.innerHTML = html;
// const nodes = [...fauxDiv.childNodes];
node.replaceWith(...fauxDiv.childNodes);
});
}
}
// TWITTER USERNAMES
// =================
// Convert @username@twitter.com to <a href="https://twitter.com/username">@username@twitter.com</a>
if (/twitter\.com/i.test(enhancedContent)) {
if (TWITTER_DOMAIN_REGEX.test(enhancedContent)) {
textNodes = extractTextNodes(dom, {
rejectFilter: ['A'],
});
textNodes.forEach((node) => {
let html = node.nodeValue
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
if (/@[a-zA-Z0-9_]+@twitter\.com/g.test(html)) {
for (const node of textNodes) {
let html = escapeHTML(node.nodeValue);
if (TWITTER_MENTION_REGEX.test(html)) {
html = html.replaceAll(
/(@([a-zA-Z0-9_]+)@twitter\.com)/g,
TWITTER_MENTION_CAPTURE_REGEX,
'<a href="https://twitter.com/$2" rel="nofollow noopener noreferrer" target="_blank">$1</a>',
);
}
fauxDiv.innerHTML = html;
// const nodes = [...fauxDiv.childNodes];
node.replaceWith(...fauxDiv.childNodes);
});
}
}
// HASHTAG STUFFING
@ -235,22 +265,24 @@ function _enhanceContent(content, opts = {}) {
},
);
if (hashtagStuffedParagraphs?.length) {
hashtagStuffedParagraphs.forEach((p) => {
for (const p of hashtagStuffedParagraphs) {
p.classList.add('hashtag-stuffing');
p.title = p.innerText;
});
}
}
}
// ADD ASPECT RATIO TO ALL IMAGES
if (enhancedContent.includes('<img')) {
dom.querySelectorAll('img').forEach((img) => {
const imgs = dom.querySelectorAll('img');
for (let i = 0; i < imgs.length; i++) {
const img = imgs[i];
const width = img.getAttribute('width') || img.naturalWidth;
const height = img.getAttribute('height') || img.naturalHeight;
if (width && height) {
img.style.setProperty('--original-aspect-ratio', `${width}/${height}`);
}
});
}
}
if (postEnhanceDOM) {
@ -258,9 +290,7 @@ function _enhanceContent(content, opts = {}) {
// postEnhanceDOM(dom); // mutate dom
}
enhancedContent = dom.innerHTML;
return enhancedContent;
return returnDOM ? dom : dom.innerHTML;
}
const enhanceContent = mem(_enhanceContent);

View file

@ -3,5 +3,9 @@ import moize from 'moize';
window._moize = moize;
export default function mem(fn, opts = {}) {
return moize(fn, { ...opts, maxSize: 50, isDeepEqual: true });
return moize(fn, {
...opts,
maxSize: 30,
isDeepEqual: true,
});
}