@allior/wmake-utils 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +52 -0
  2. package/README_RU.md +52 -0
  3. package/dist/react/hooks/index.d.ts +4 -0
  4. package/dist/react/hooks/index.d.ts.map +1 -0
  5. package/dist/react/hooks/use-ignore-message.d.ts +9 -0
  6. package/dist/react/hooks/use-ignore-message.d.ts.map +1 -0
  7. package/dist/react/hooks/use-message-limit.d.ts +14 -0
  8. package/dist/react/hooks/use-message-limit.d.ts.map +1 -0
  9. package/dist/react/hooks/use-message-rows.d.ts +47 -0
  10. package/dist/react/hooks/use-message-rows.d.ts.map +1 -0
  11. package/dist/react/index.d.ts +3 -0
  12. package/dist/react/index.d.ts.map +1 -0
  13. package/dist/react/index.iife.js +2 -0
  14. package/dist/react/index.iife.js.map +1 -0
  15. package/dist/react/index.js +106 -0
  16. package/dist/react/index.js.map +1 -0
  17. package/dist/root/html-encode.d.ts +6 -0
  18. package/dist/root/html-encode.d.ts.map +1 -0
  19. package/dist/root/index.d.ts +5 -0
  20. package/dist/root/index.d.ts.map +1 -0
  21. package/dist/root/index.iife.js +2 -0
  22. package/dist/root/index.iife.js.map +1 -0
  23. package/dist/root/index.js +73 -0
  24. package/dist/root/index.js.map +1 -0
  25. package/dist/root/message-limit.d.ts +14 -0
  26. package/dist/root/message-limit.d.ts.map +1 -0
  27. package/dist/root/preload-images.d.ts +6 -0
  28. package/dist/root/preload-images.d.ts.map +1 -0
  29. package/dist/root/string-utils.d.ts +4 -0
  30. package/dist/root/string-utils.d.ts.map +1 -0
  31. package/package.json +44 -0
  32. package/src/react/hooks/index.ts +3 -0
  33. package/src/react/hooks/use-ignore-message.ts +25 -0
  34. package/src/react/hooks/use-message-limit.ts +42 -0
  35. package/src/react/hooks/use-message-rows.ts +154 -0
  36. package/src/react/index.ts +2 -0
  37. package/src/root/html-encode.ts +9 -0
  38. package/src/root/index.ts +4 -0
  39. package/src/root/message-limit.ts +79 -0
  40. package/src/root/preload-images.ts +34 -0
  41. package/src/root/string-utils.ts +20 -0
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @allior/wmake-utils
2
+
3
+ **Language / Язык:** English | [Русский](README_RU.md)
4
+
5
+ Utilities for widgets: message content parsers (emotes, mentions, HTML), image preloading, and more.
6
+
7
+ ## parseMessageContent
8
+
9
+ Converts a message object (`text` + `emotes`) into an HTML string: emotes become `<img>`, mentions `@nick` become `<span class="mentioned">`.
10
+
11
+ Logic ported from blyoka-chat (StreamElements): zero-width emote support, large/medium emote options when the message is “emotes only”.
12
+
13
+ ### Usage
14
+
15
+ ```ts
16
+ import { parseMessageContent } from "@allior/wmake-utils";
17
+
18
+ const html = parseMessageContent(
19
+ {
20
+ text: "Hello @user Kappa",
21
+ emotes: [{ name: "Kappa", urls: { "1": "https://..." } }],
22
+ },
23
+ {
24
+ zeroWidthEmotes: [],
25
+ largeEmotes: true,
26
+ largeEmotesAmount: 3,
27
+ mediumEmotes: false,
28
+ },
29
+ );
30
+ ```
31
+
32
+ ### Options
33
+
34
+ - **zeroWidthEmotes** — list of emotes treated as zero-width (glued to the previous container, class `zero`).
35
+ - **largeEmotes** / **largeEmotesAmount** — when the message contains only emotes and no more than `largeEmotesAmount`, use size `large` (urls["2"]).
36
+ - **mediumEmotes** — use size `medium` when there are many emotes.
37
+
38
+ ## preloadImagesThenShow
39
+
40
+ Waits for all `<img>` inside a container to load, then calls the callback.
41
+
42
+ ```ts
43
+ import { preloadImagesThenShow } from "@allior/wmake-utils";
44
+
45
+ preloadImagesThenShow(containerElement, (el) => {
46
+ // all images loaded, safe to show
47
+ });
48
+ ```
49
+
50
+ ## htmlEncode
51
+
52
+ Escapes a string for HTML: spaces normalized, characters `< > " ^` replaced with entities.
package/README_RU.md ADDED
@@ -0,0 +1,52 @@
1
+ # @allior/wmake-utils
2
+
3
+ Утилиты для виджетов: парсеры контента сообщений (эмоуты, упоминания, HTML), предзагрузка изображений и др.
4
+
5
+ ## parseMessageContent
6
+
7
+ Преобразует объект сообщения (`text` + `emotes`) в HTML-строку: эмоуты становятся `<img>`, упоминания `@nick` — `<span class="mentioned">`.
8
+
9
+ Логика перенесена из blyoka-chat (StreamElements): поддержка zero-width эмоутов, опции large/medium эмоутов при сообщении «только эмоуты».
10
+
11
+ ### Использование
12
+
13
+ ```ts
14
+ import { parseMessageContent } from "@allior/wmake-utils";
15
+
16
+ const html = parseMessageContent(
17
+ {
18
+ text: "Hello @user Kappa",
19
+ emotes: [{ name: "Kappa", urls: { "1": "https://..." } }],
20
+ },
21
+ {
22
+ zeroWidthEmotes: [],
23
+ largeEmotes: true,
24
+ largeEmotesAmount: 3,
25
+ mediumEmotes: false,
26
+ },
27
+ );
28
+ ```
29
+
30
+ ### Опции
31
+
32
+ - **zeroWidthEmotes** — список эмоутов, которые считаются zero-width (склейка с предыдущим контейнером, класс `zero`).
33
+ - **largeEmotes** / **largeEmotesAmount** — если в сообщении только эмоуты и их не больше `largeEmotesAmount`, использовать размер `large` (urls["2"]).
34
+ - **mediumEmotes** — при большом количестве эмоутов использовать размер `medium`.
35
+
36
+ ## preloadImagesThenShow
37
+
38
+ Ожидает загрузки всех `<img>` внутри контейнера, затем вызывает callback.
39
+
40
+ ```ts
41
+ import { preloadImagesThenShow } from "@allior/wmake-utils";
42
+
43
+ preloadImagesThenShow(containerElement, (el) => {
44
+ // все изображения загружены, можно показывать
45
+ });
46
+ ```
47
+
48
+ ## htmlEncode
49
+
50
+ Экранирование строки для HTML: пробелы нормализуются, символы `< > " ^` заменяются на сущности.
51
+
52
+ **[English](README.md)**
@@ -0,0 +1,4 @@
1
+ export * from "./use-ignore-message";
2
+ export * from "./use-message-limit";
3
+ export * from "./use-message-rows";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC"}
@@ -0,0 +1,9 @@
1
+ interface Props {
2
+ ignoredUsers?: string[];
3
+ ignoreCommands?: boolean;
4
+ }
5
+ export declare const useIgnoreMessage: ({ ignoredUsers, ignoreCommands, }: Props) => {
6
+ shouldIgnore: (username: string, text: string) => boolean;
7
+ };
8
+ export {};
9
+ //# sourceMappingURL=use-ignore-message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-ignore-message.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-ignore-message.ts"],"names":[],"mappings":"AAAA,UAAU,KAAK;IACb,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,eAAO,MAAM,gBAAgB,GAAI,mCAG9B,KAAK;6BAW0B,MAAM,QAAQ,MAAM;CAKrD,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { RefObject } from "react";
2
+ export interface UseMessageLimitOptions {
3
+ messageLimit?: number;
4
+ /** Селекторы строк для лимита (по умолчанию [".message-row", ".reply"]) */
5
+ limitSelectors?: string[];
6
+ /** Вызов при удалении элемента из DOM (для синхронизации state в React) */
7
+ onRemove?: (element: Element) => void;
8
+ }
9
+ /**
10
+ * Подключает message limit к корневому элементу и опциям из widget load.
11
+ * Возвращает handleMessageLimit — вызывать после добавления новой строки.
12
+ */
13
+ export declare function useMessageLimit(rootRef: RefObject<Element | null>, options?: UseMessageLimitOptions): () => void;
14
+ //# sourceMappingURL=use-message-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-message-limit.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-message-limit.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAOvC,MAAM,WAAW,sBAAsB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC,EAClC,OAAO,GAAE,sBAA2B,GACnC,MAAM,IAAI,CAkBZ"}
@@ -0,0 +1,47 @@
1
+ import type { Dispatch, RefObject, SetStateAction } from "react";
2
+ import type { UseMessageLimitOptions } from "./use-message-limit";
3
+ export interface UseMessageRowsOptions<T> extends Omit<UseMessageLimitOptions, "onRemove"> {
4
+ /**
5
+ * Функция получения id строки (для синхронизации state при удалении из DOM по data-row-id).
6
+ */
7
+ getRowId: (row: T) => number;
8
+ /** Имя атрибута с id на DOM-элементе строки (по умолчанию "data-row-id"). */
9
+ rowIdAttribute?: string;
10
+ /**
11
+ * Ref контейнера «подготовки»: при наличии addRow сначала добавляет в preparedRows;
12
+ * при пересечении строки с этим контейнером вызывается onPreparedEnter, затем строка переносится в rows.
13
+ */
14
+ preparingRef?: RefObject<Element | null>;
15
+ /**
16
+ * Вызывается, когда строка из preparedRows попадает в зону preparingRef (например, для processZeroWidthEmotes).
17
+ * После вызова строка переносится в основной список (main rows).
18
+ */
19
+ onPreparedEnter?: (rowId: number, element: Element) => void;
20
+ }
21
+ export interface UseMessageRowsReturn<T> {
22
+ /** Текущий список строк (основной список, рендер в main). */
23
+ rows: T[];
24
+ /** Строки в зоне подготовки (рендер внутри элемента с preparingRef). При отсутствии preparingRef — пустой массив. */
25
+ preparedRows: T[];
26
+ /** Установить список строк (полная замена). */
27
+ setRows: Dispatch<SetStateAction<T[]>>;
28
+ /** Добавить строку: при наличии preparingRef — в preparedRows, иначе в rows с применением лимита. */
29
+ addRow: (row: T) => void;
30
+ /** Перенести строку из preparedRows в rows по id. Вызывается автоматически после onPreparedEnter при использовании preparingRef. */
31
+ moveRow: (rowId: number) => void;
32
+ /**
33
+ * Зарегистрировать DOM-элемент подготовленной строки для наблюдения (вызывать при монтировании/размонтировании строки).
34
+ * Когда элемент попадает в зону preparingRef, вызывается onPreparedEnter и затем moveRow(rowId).
35
+ */
36
+ registerPreparedRow: (rowId: number, element: Element | null) => void;
37
+ /** Применить лимит сообщений (удалить лишние строки из DOM и state). */
38
+ applyLimit: () => void;
39
+ }
40
+ /**
41
+ * Хук для управления списком строк с учётом message limit:
42
+ * хранит rows в state, при applyLimit удаляет лишние из DOM и синхронизирует state по data-row-id.
43
+ * При передаче preparingRef новые строки сначала попадают в preparedRows; при пересечении с контейнером
44
+ * вызывается onPreparedEnter и строка переносится в rows.
45
+ */
46
+ export declare function useMessageRows<T>(rootRef: RefObject<Element | null>, options: UseMessageRowsOptions<T>): UseMessageRowsReturn<T>;
47
+ //# sourceMappingURL=use-message-rows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-message-rows.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-message-rows.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAEjE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAElE,MAAM,WAAW,qBAAqB,CAAC,CAAC,CAAE,SAAQ,IAAI,CACpD,sBAAsB,EACtB,UAAU,CACX;IACC;;OAEG;IACH,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC;IAC7B,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACzC;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CAC7D;AAED,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC,6DAA6D;IAC7D,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,qHAAqH;IACrH,YAAY,EAAE,CAAC,EAAE,CAAC;IAClB,+CAA+C;IAC/C,OAAO,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvC,qGAAqG;IACrG,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC;IACzB,oIAAoI;IACpI,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC;;;OAGG;IACH,mBAAmB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,KAAK,IAAI,CAAC;IACtE,wEAAwE;IACxE,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAC9B,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC,EAClC,OAAO,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAChC,oBAAoB,CAAC,CAAC,CAAC,CAiGzB"}
@@ -0,0 +1,3 @@
1
+ export * from "../root";
2
+ export * from "./hooks";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC"}
@@ -0,0 +1,2 @@
1
+ (function(l,g){"use strict";let M=15,w=[".message-row",".reply"],C=null;function R(e){C=e}function k(e){const t=C;if(!t)return;const o=w.join(", ");for(;;){const c=t.querySelectorAll(o),r=c.length;if(M<=0||r<=M)break;const s=c[0];if(!s)break;e==null||e(s),s.remove()}}function T(e,t){const o=C;if(!o)return;const c=w.map(s=>`${s}[data-msgid="${e}"]`).join(", ");o.querySelectorAll(c).forEach(s=>{t==null||t(e,s),s.remove()})}function _(e,t){const o=C;if(!o)return;const c=w.map(s=>`${s}[data-userid="${e}"]`).join(", ");o.querySelectorAll(c).forEach(s=>{t==null||t(e,s),s.remove()})}function A(e=[".message-row",".reply"],t){M=t??15,w=e}function $(e){return e.replace(/\s/g," ").replace(/[<>"^]/g,t=>"&#"+t.charCodeAt(0)+";")}function j(e,t){const o=e.querySelectorAll("img"),c=o.length;if(c===0){t(e);return}let r=0;function s(){r++,r===c&&t(e)}o.forEach(n=>{n.complete?s():(n.addEventListener("load",s),n.addEventListener("error",s))})}const O=e=>e.replace(/([A-Z])/g,"-$1").toLowerCase();function U(e){return e.toLowerCase().replace(/^vite_/,"").replace(/_([a-z])/g,(t,o)=>o.toUpperCase()).replace(/^[a-z]/,t=>t.toLowerCase())}function W(e){return"--"+e.toLowerCase().replace(/^vite_/,"").replace(/_/g,"-")}const z=({ignoredUsers:e=[],ignoreCommands:t=!0})=>{const o=e.map(n=>n.toLowerCase()),c=n=>o.includes(n.toLowerCase()),r=n=>t&&n.trim().startsWith("!");return{shouldIgnore:(n,f)=>c(n)||r(f)}};function I(e,t={}){const{messageLimit:o,limitSelectors:c,onRemove:r}=t,s=g.useRef(r);return s.current=r,g.useEffect(()=>{const n=e.current;return R(n),()=>R(null)},[e]),g.useEffect(()=>{A(c,o)},[o,c]),()=>{k(n=>{var f;return(f=s.current)==null?void 0:f.call(s,n)})}}function K(e,t){const{getRowId:o,rowIdAttribute:c="data-row-id",preparingRef:r,onPreparedEnter:s,...n}=t,[f,L]=g.useState([]),[N,P]=g.useState([]),b=g.useRef(s);b.current=s;const S=g.useRef(new Map),m=I(e,{...n,onRemove:a=>{const u=a.getAttribute(c);if(u!=null){const d=Number(u);L(i=>i.filter(h=>o(h)!==d))}}}),E=g.useCallback(a=>{P(u=>{const d=u.find(i=>o(i)===a);return d==null?u:(L(i=>i.some(h=>o(h)===a)?i:[...i,d]),queueMicrotask(m),u.filter(i=>o(i)!==a))})},[o,m]),Z=g.useCallback((a,u)=>{const d=S.current,i=d.get(a);if(i&&(i.disconnect(),d.delete(a)),u==null||!(r!=null&&r.current))return;const h=r.current,y=new IntersectionObserver(D=>{var q;const p=D[0];if(!(p!=null&&p.isIntersecting))return;const v=a;y.disconnect(),d.delete(v),(q=b.current)==null||q.call(b,v,p.target),E(v)},{root:h,threshold:0,rootMargin:"0px"});y.observe(u),d.set(a,y)},[r,E]);g.useEffect(()=>()=>{S.current.forEach(a=>a.disconnect()),S.current.clear()},[]);const B=g.useCallback(a=>{r?P(u=>[...u,a]):(L(u=>[...u,a]),m())},[r,m]);return{rows:f,preparedRows:r?N:[],setRows:L,addRow:B,moveRow:E,registerPreparedRow:r?Z:()=>{},applyLimit:m}}l.deleteMessage=T,l.deleteMessages=_,l.handleMessageLimit=k,l.htmlEncode=$,l.plugMessagesLimit=A,l.preloadImagesThenShow=j,l.setMessageLimitRoot=R,l.toCamelCase=U,l.toCssCustomProperty=W,l.toKebabCase=O,l.useIgnoreMessage=z,l.useMessageLimit=I,l.useMessageRows=K,Object.defineProperty(l,Symbol.toStringTag,{value:"Module"})})(this.WmakeTtsReact=this.WmakeTtsReact||{},React);
2
+ //# sourceMappingURL=index.iife.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.iife.js","sources":["../../src/root/message-limit.ts","../../src/root/html-encode.ts","../../src/root/preload-images.ts","../../src/root/string-utils.ts","../../src/react/hooks/use-ignore-message.ts","../../src/react/hooks/use-message-limit.ts","../../src/react/hooks/use-message-rows.ts"],"sourcesContent":["let messagesLimit = 15;\r\nlet limitSelectors: string[] = [\".message-row\", \".reply\"];\r\n\r\nlet rootRef: Element | null = null;\r\n\r\n/**\r\n * Set the container element for message limit and delete operations.\r\n * Pass the DOM element that wraps all message rows (e.g. document.querySelector(\"#main\")).\r\n */\r\nexport function setMessageLimitRoot(root: Element | null): void {\r\n rootRef = root;\r\n}\r\n\r\n/**\r\n * Enforces message limit by recalculating current count each time and removing\r\n * excess messages in a loop until totalMessages <= messagesLimit.\r\n */\r\nexport function handleMessageLimit(\r\n onRemoveMixin?: (element: Element) => void,\r\n): void {\r\n const root = rootRef;\r\n if (!root) return;\r\n\r\n const selector = limitSelectors.join(\", \");\r\n\r\n for (;;) {\r\n const children = root.querySelectorAll(selector);\r\n const totalMessages = children.length;\r\n\r\n if (messagesLimit <= 0 || totalMessages <= messagesLimit) {\r\n break;\r\n }\r\n\r\n const first = children[0];\r\n if (!first) break;\r\n onRemoveMixin?.(first);\r\n first.remove();\r\n }\r\n}\r\n\r\nexport function deleteMessage(\r\n msgId: string,\r\n mixin?: (msgId: string, element: Element) => void,\r\n): void {\r\n const root = rootRef;\r\n if (!root) return;\r\n const selectors = limitSelectors\r\n .map((s) => `${s}[data-msgid=\"${msgId}\"]`)\r\n .join(\", \");\r\n const elements = root.querySelectorAll(selectors);\r\n elements.forEach((el) => {\r\n mixin?.(msgId, el);\r\n el.remove();\r\n });\r\n}\r\n\r\nexport function deleteMessages(\r\n userId: string,\r\n mixin?: (userId: string, element: Element) => void,\r\n): void {\r\n const root = rootRef;\r\n if (!root) return;\r\n const selectors = limitSelectors\r\n .map((s) => `${s}[data-userid=\"${userId}\"]`)\r\n .join(\", \");\r\n const elements = root.querySelectorAll(selectors);\r\n elements.forEach((el) => {\r\n mixin?.(userId, el);\r\n el.remove();\r\n });\r\n}\r\n\r\nexport function plugMessagesLimit(\r\n _limitSelectors: string[] = [\".message-row\", \".reply\"],\r\n _messagesLimit?: number,\r\n): void {\r\n messagesLimit = _messagesLimit ?? 15;\r\n limitSelectors = _limitSelectors;\r\n}\r\n","/**\r\n * Экранирование строки для безопасной вставки в HTML:\r\n * нормализация пробелов и замена < > \" ^ на сущности.\r\n */\r\nexport function htmlEncode(html: string): string {\r\n return html\r\n .replace(/\\s/g, \" \")\r\n .replace(/[<>\"^]/g, (match) => \"&#\" + match.charCodeAt(0) + \";\");\r\n}\r\n","/**\r\n * Ожидает загрузки всех <img> внутри контейнера, затем вызывает callback.\r\n * Если изображений нет — callback вызывается сразу.\r\n */\r\nexport function preloadImagesThenShow<T extends Element>(\r\n element: T,\r\n showFunction: (element: T) => void,\r\n): void {\r\n const images = element.querySelectorAll<HTMLImageElement>(\"img\");\r\n const totalImages = images.length;\r\n\r\n if (totalImages === 0) {\r\n showFunction(element);\r\n return;\r\n }\r\n\r\n let loadedImages = 0;\r\n\r\n function imageLoaded() {\r\n loadedImages++;\r\n if (loadedImages === totalImages) {\r\n showFunction(element);\r\n }\r\n }\r\n\r\n images.forEach((img) => {\r\n if (img.complete) {\r\n imageLoaded();\r\n } else {\r\n img.addEventListener(\"load\", imageLoaded);\r\n img.addEventListener(\"error\", imageLoaded);\r\n }\r\n });\r\n}\r\n","export const toKebabCase = (str: string) =>\r\n str.replace(/([A-Z])/g, '-$1').toLowerCase();\r\n\r\nexport function toCamelCase(str: string): string {\r\n return str\r\n .toLowerCase()\r\n .replace(/^vite_/, \"\")\r\n .replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())\r\n .replace(/^[a-z]/, (match) => match.toLowerCase());\r\n}\r\n\r\nexport function toCssCustomProperty(str: string): string {\r\n return (\r\n \"--\" +\r\n str\r\n .toLowerCase()\r\n .replace(/^vite_/, \"\")\r\n .replace(/_/g, \"-\")\r\n );\r\n}\r\n","interface Props {\r\n ignoredUsers?: string[];\r\n ignoreCommands?: boolean;\r\n}\r\n\r\nexport const useIgnoreMessage = ({\r\n ignoredUsers = [],\r\n ignoreCommands = true,\r\n}: Props) => {\r\n const ignoredUsersLower = ignoredUsers.map((user) => user.toLowerCase());\r\n\r\n const shouldIgnoreUser = (username: string) => {\r\n return ignoredUsersLower.includes(username.toLowerCase());\r\n };\r\n\r\n const shouldIgnoreMessage = (text: string) => {\r\n return ignoreCommands && text.trim().startsWith(\"!\");\r\n };\r\n\r\n const shouldIgnore = (username: string, text: string) => {\r\n return shouldIgnoreUser(username) || shouldIgnoreMessage(text);\r\n };\r\n\r\n return { shouldIgnore };\r\n};\r\n","import { useEffect, useRef } from \"react\";\r\nimport type { RefObject } from \"react\";\r\nimport {\r\n setMessageLimitRoot,\r\n handleMessageLimit as handleMessageLimitRoot,\r\n plugMessagesLimit,\r\n} from \"@/root\";\r\n\r\nexport interface UseMessageLimitOptions {\r\n messageLimit?: number;\r\n /** Селекторы строк для лимита (по умолчанию [\".message-row\", \".reply\"]) */\r\n limitSelectors?: string[];\r\n /** Вызов при удалении элемента из DOM (для синхронизации state в React) */\r\n onRemove?: (element: Element) => void;\r\n}\r\n\r\n/**\r\n * Подключает message limit к корневому элементу и опциям из widget load.\r\n * Возвращает handleMessageLimit — вызывать после добавления новой строки.\r\n */\r\nexport function useMessageLimit(\r\n rootRef: RefObject<Element | null>,\r\n options: UseMessageLimitOptions = {},\r\n): () => void {\r\n const { messageLimit, limitSelectors, onRemove } = options;\r\n const onRemoveRef = useRef(onRemove);\r\n onRemoveRef.current = onRemove;\r\n\r\n useEffect(() => {\r\n const root = rootRef.current;\r\n setMessageLimitRoot(root);\r\n return () => setMessageLimitRoot(null);\r\n }, [rootRef]);\r\n\r\n useEffect(() => {\r\n plugMessagesLimit(limitSelectors, messageLimit);\r\n }, [messageLimit, limitSelectors]);\r\n\r\n return () => {\r\n handleMessageLimitRoot((el) => onRemoveRef.current?.(el));\r\n };\r\n}\r\n","import { useCallback, useEffect, useRef, useState } from \"react\";\r\nimport type { Dispatch, RefObject, SetStateAction } from \"react\";\r\nimport { useMessageLimit } from \"./use-message-limit\";\r\nimport type { UseMessageLimitOptions } from \"./use-message-limit\";\r\n\r\nexport interface UseMessageRowsOptions<T> extends Omit<\r\n UseMessageLimitOptions,\r\n \"onRemove\"\r\n> {\r\n /**\r\n * Функция получения id строки (для синхронизации state при удалении из DOM по data-row-id).\r\n */\r\n getRowId: (row: T) => number;\r\n /** Имя атрибута с id на DOM-элементе строки (по умолчанию \"data-row-id\"). */\r\n rowIdAttribute?: string;\r\n /**\r\n * Ref контейнера «подготовки»: при наличии addRow сначала добавляет в preparedRows;\r\n * при пересечении строки с этим контейнером вызывается onPreparedEnter, затем строка переносится в rows.\r\n */\r\n preparingRef?: RefObject<Element | null>;\r\n /**\r\n * Вызывается, когда строка из preparedRows попадает в зону preparingRef (например, для processZeroWidthEmotes).\r\n * После вызова строка переносится в основной список (main rows).\r\n */\r\n onPreparedEnter?: (rowId: number, element: Element) => void;\r\n}\r\n\r\nexport interface UseMessageRowsReturn<T> {\r\n /** Текущий список строк (основной список, рендер в main). */\r\n rows: T[];\r\n /** Строки в зоне подготовки (рендер внутри элемента с preparingRef). При отсутствии preparingRef — пустой массив. */\r\n preparedRows: T[];\r\n /** Установить список строк (полная замена). */\r\n setRows: Dispatch<SetStateAction<T[]>>;\r\n /** Добавить строку: при наличии preparingRef — в preparedRows, иначе в rows с применением лимита. */\r\n addRow: (row: T) => void;\r\n /** Перенести строку из preparedRows в rows по id. Вызывается автоматически после onPreparedEnter при использовании preparingRef. */\r\n moveRow: (rowId: number) => void;\r\n /**\r\n * Зарегистрировать DOM-элемент подготовленной строки для наблюдения (вызывать при монтировании/размонтировании строки).\r\n * Когда элемент попадает в зону preparingRef, вызывается onPreparedEnter и затем moveRow(rowId).\r\n */\r\n registerPreparedRow: (rowId: number, element: Element | null) => void;\r\n /** Применить лимит сообщений (удалить лишние строки из DOM и state). */\r\n applyLimit: () => void;\r\n}\r\n\r\n/**\r\n * Хук для управления списком строк с учётом message limit:\r\n * хранит rows в state, при applyLimit удаляет лишние из DOM и синхронизирует state по data-row-id.\r\n * При передаче preparingRef новые строки сначала попадают в preparedRows; при пересечении с контейнером\r\n * вызывается onPreparedEnter и строка переносится в rows.\r\n */\r\nexport function useMessageRows<T>(\r\n rootRef: RefObject<Element | null>,\r\n options: UseMessageRowsOptions<T>,\r\n): UseMessageRowsReturn<T> {\r\n const {\r\n getRowId,\r\n rowIdAttribute = \"data-row-id\",\r\n preparingRef,\r\n onPreparedEnter,\r\n ...limitOptions\r\n } = options;\r\n const [rows, setRows] = useState<T[]>([]);\r\n const [preparedRows, setPreparedRows] = useState<T[]>([]);\r\n const onPreparedEnterRef = useRef(onPreparedEnter);\r\n onPreparedEnterRef.current = onPreparedEnter;\r\n const observersRef = useRef<Map<number, IntersectionObserver>>(new Map());\r\n\r\n const applyLimit = useMessageLimit(rootRef, {\r\n ...limitOptions,\r\n onRemove: (el: Element) => {\r\n const idStr = el.getAttribute(rowIdAttribute);\r\n if (idStr != null) {\r\n const id = Number(idStr);\r\n setRows((prev) => prev.filter((r) => getRowId(r) !== id));\r\n }\r\n },\r\n });\r\n\r\n const moveRow = useCallback(\r\n (rowId: number) => {\r\n setPreparedRows((prev) => {\r\n const row = prev.find((r) => getRowId(r) === rowId);\r\n if (row == null) return prev;\r\n setRows((r) =>\r\n r.some((x) => getRowId(x) === rowId) ? r : [...r, row],\r\n );\r\n queueMicrotask(applyLimit);\r\n return prev.filter((r) => getRowId(r) !== rowId);\r\n });\r\n },\r\n [getRowId, applyLimit],\r\n );\r\n\r\n const registerPreparedRow = useCallback(\r\n (rowId: number, element: Element | null) => {\r\n const observers = observersRef.current;\r\n const existing = observers.get(rowId);\r\n if (existing) {\r\n existing.disconnect();\r\n observers.delete(rowId);\r\n }\r\n if (element == null || !preparingRef?.current) return;\r\n\r\n const root = preparingRef.current;\r\n const observer = new IntersectionObserver(\r\n (entries) => {\r\n const entry = entries[0];\r\n if (!entry?.isIntersecting) return;\r\n const id = rowId;\r\n observer.disconnect();\r\n observers.delete(id);\r\n onPreparedEnterRef.current?.(id, entry.target);\r\n moveRow(id);\r\n },\r\n { root, threshold: 0, rootMargin: \"0px\" },\r\n );\r\n observer.observe(element);\r\n observers.set(rowId, observer);\r\n },\r\n [preparingRef, moveRow],\r\n );\r\n\r\n useEffect(() => {\r\n return () => {\r\n observersRef.current.forEach((o) => o.disconnect());\r\n observersRef.current.clear();\r\n };\r\n }, []);\r\n\r\n const addRow = useCallback(\r\n (row: T) => {\r\n if (preparingRef) {\r\n setPreparedRows((prev) => [...prev, row]);\r\n } else {\r\n setRows((prev) => [...prev, row]);\r\n applyLimit();\r\n }\r\n },\r\n [preparingRef, applyLimit],\r\n );\r\n\r\n return {\r\n rows,\r\n preparedRows: preparingRef ? preparedRows : [],\r\n setRows,\r\n addRow,\r\n moveRow,\r\n registerPreparedRow: preparingRef ? registerPreparedRow : () => {},\r\n applyLimit,\r\n };\r\n}\r\n"],"names":["messagesLimit","limitSelectors","rootRef","setMessageLimitRoot","root","handleMessageLimit","onRemoveMixin","selector","children","totalMessages","first","deleteMessage","msgId","mixin","selectors","el","deleteMessages","userId","plugMessagesLimit","_limitSelectors","_messagesLimit","htmlEncode","html","match","preloadImagesThenShow","element","showFunction","images","totalImages","loadedImages","imageLoaded","img","toKebabCase","str","toCamelCase","_","letter","toCssCustomProperty","useIgnoreMessage","ignoredUsers","ignoreCommands","ignoredUsersLower","user","shouldIgnoreUser","username","shouldIgnoreMessage","text","useMessageLimit","options","messageLimit","onRemove","onRemoveRef","useRef","useEffect","handleMessageLimitRoot","_a","useMessageRows","getRowId","rowIdAttribute","preparingRef","onPreparedEnter","limitOptions","rows","setRows","useState","preparedRows","setPreparedRows","onPreparedEnterRef","observersRef","applyLimit","idStr","id","prev","r","moveRow","useCallback","rowId","row","x","registerPreparedRow","observers","existing","observer","entries","entry","o","addRow"],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ import { setMessageLimitRoot as P, plugMessagesLimit as U, handleMessageLimit as q } from "../root/index.js";
2
+ import { deleteMessage as B, deleteMessages as D, htmlEncode as F, preloadImagesThenShow as G, toCamelCase as H, toCssCustomProperty as J, toKebabCase as Q } from "../root/index.js";
3
+ import { useRef as C, useEffect as v, useState as S, useCallback as L } from "react";
4
+ const T = ({
5
+ ignoredUsers: u = [],
6
+ ignoreCommands: d = !0
7
+ }) => {
8
+ const n = u.map((s) => s.toLowerCase()), a = (s) => n.includes(s.toLowerCase()), t = (s) => d && s.trim().startsWith("!");
9
+ return { shouldIgnore: (s, l) => a(s) || t(l) };
10
+ };
11
+ function y(u, d = {}) {
12
+ const { messageLimit: n, limitSelectors: a, onRemove: t } = d, i = C(t);
13
+ return i.current = t, v(() => {
14
+ const s = u.current;
15
+ return P(s), () => P(null);
16
+ }, [u]), v(() => {
17
+ U(a, n);
18
+ }, [n, a]), () => {
19
+ q((s) => {
20
+ var l;
21
+ return (l = i.current) == null ? void 0 : l.call(i, s);
22
+ });
23
+ };
24
+ }
25
+ function W(u, d) {
26
+ const {
27
+ getRowId: n,
28
+ rowIdAttribute: a = "data-row-id",
29
+ preparingRef: t,
30
+ onPreparedEnter: i,
31
+ ...s
32
+ } = d, [l, p] = S([]), [E, I] = S([]), f = C(i);
33
+ f.current = i;
34
+ const h = C(/* @__PURE__ */ new Map()), g = y(u, {
35
+ ...s,
36
+ onRemove: (e) => {
37
+ const r = e.getAttribute(a);
38
+ if (r != null) {
39
+ const c = Number(r);
40
+ p((o) => o.filter((m) => n(m) !== c));
41
+ }
42
+ }
43
+ }), M = L(
44
+ (e) => {
45
+ I((r) => {
46
+ const c = r.find((o) => n(o) === e);
47
+ return c == null ? r : (p(
48
+ (o) => o.some((m) => n(m) === e) ? o : [...o, c]
49
+ ), queueMicrotask(g), r.filter((o) => n(o) !== e));
50
+ });
51
+ },
52
+ [n, g]
53
+ ), k = L(
54
+ (e, r) => {
55
+ const c = h.current, o = c.get(e);
56
+ if (o && (o.disconnect(), c.delete(e)), r == null || !(t != null && t.current)) return;
57
+ const m = t.current, b = new IntersectionObserver(
58
+ (O) => {
59
+ var x;
60
+ const w = O[0];
61
+ if (!(w != null && w.isIntersecting)) return;
62
+ const R = e;
63
+ b.disconnect(), c.delete(R), (x = f.current) == null || x.call(f, R, w.target), M(R);
64
+ },
65
+ { root: m, threshold: 0, rootMargin: "0px" }
66
+ );
67
+ b.observe(r), c.set(e, b);
68
+ },
69
+ [t, M]
70
+ );
71
+ v(() => () => {
72
+ h.current.forEach((e) => e.disconnect()), h.current.clear();
73
+ }, []);
74
+ const A = L(
75
+ (e) => {
76
+ t ? I((r) => [...r, e]) : (p((r) => [...r, e]), g());
77
+ },
78
+ [t, g]
79
+ );
80
+ return {
81
+ rows: l,
82
+ preparedRows: t ? E : [],
83
+ setRows: p,
84
+ addRow: A,
85
+ moveRow: M,
86
+ registerPreparedRow: t ? k : () => {
87
+ },
88
+ applyLimit: g
89
+ };
90
+ }
91
+ export {
92
+ B as deleteMessage,
93
+ D as deleteMessages,
94
+ q as handleMessageLimit,
95
+ F as htmlEncode,
96
+ U as plugMessagesLimit,
97
+ G as preloadImagesThenShow,
98
+ P as setMessageLimitRoot,
99
+ H as toCamelCase,
100
+ J as toCssCustomProperty,
101
+ Q as toKebabCase,
102
+ T as useIgnoreMessage,
103
+ y as useMessageLimit,
104
+ W as useMessageRows
105
+ };
106
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/react/hooks/use-ignore-message.ts","../../src/react/hooks/use-message-limit.ts","../../src/react/hooks/use-message-rows.ts"],"sourcesContent":["interface Props {\r\n ignoredUsers?: string[];\r\n ignoreCommands?: boolean;\r\n}\r\n\r\nexport const useIgnoreMessage = ({\r\n ignoredUsers = [],\r\n ignoreCommands = true,\r\n}: Props) => {\r\n const ignoredUsersLower = ignoredUsers.map((user) => user.toLowerCase());\r\n\r\n const shouldIgnoreUser = (username: string) => {\r\n return ignoredUsersLower.includes(username.toLowerCase());\r\n };\r\n\r\n const shouldIgnoreMessage = (text: string) => {\r\n return ignoreCommands && text.trim().startsWith(\"!\");\r\n };\r\n\r\n const shouldIgnore = (username: string, text: string) => {\r\n return shouldIgnoreUser(username) || shouldIgnoreMessage(text);\r\n };\r\n\r\n return { shouldIgnore };\r\n};\r\n","import { useEffect, useRef } from \"react\";\r\nimport type { RefObject } from \"react\";\r\nimport {\r\n setMessageLimitRoot,\r\n handleMessageLimit as handleMessageLimitRoot,\r\n plugMessagesLimit,\r\n} from \"@/root\";\r\n\r\nexport interface UseMessageLimitOptions {\r\n messageLimit?: number;\r\n /** Селекторы строк для лимита (по умолчанию [\".message-row\", \".reply\"]) */\r\n limitSelectors?: string[];\r\n /** Вызов при удалении элемента из DOM (для синхронизации state в React) */\r\n onRemove?: (element: Element) => void;\r\n}\r\n\r\n/**\r\n * Подключает message limit к корневому элементу и опциям из widget load.\r\n * Возвращает handleMessageLimit — вызывать после добавления новой строки.\r\n */\r\nexport function useMessageLimit(\r\n rootRef: RefObject<Element | null>,\r\n options: UseMessageLimitOptions = {},\r\n): () => void {\r\n const { messageLimit, limitSelectors, onRemove } = options;\r\n const onRemoveRef = useRef(onRemove);\r\n onRemoveRef.current = onRemove;\r\n\r\n useEffect(() => {\r\n const root = rootRef.current;\r\n setMessageLimitRoot(root);\r\n return () => setMessageLimitRoot(null);\r\n }, [rootRef]);\r\n\r\n useEffect(() => {\r\n plugMessagesLimit(limitSelectors, messageLimit);\r\n }, [messageLimit, limitSelectors]);\r\n\r\n return () => {\r\n handleMessageLimitRoot((el) => onRemoveRef.current?.(el));\r\n };\r\n}\r\n","import { useCallback, useEffect, useRef, useState } from \"react\";\r\nimport type { Dispatch, RefObject, SetStateAction } from \"react\";\r\nimport { useMessageLimit } from \"./use-message-limit\";\r\nimport type { UseMessageLimitOptions } from \"./use-message-limit\";\r\n\r\nexport interface UseMessageRowsOptions<T> extends Omit<\r\n UseMessageLimitOptions,\r\n \"onRemove\"\r\n> {\r\n /**\r\n * Функция получения id строки (для синхронизации state при удалении из DOM по data-row-id).\r\n */\r\n getRowId: (row: T) => number;\r\n /** Имя атрибута с id на DOM-элементе строки (по умолчанию \"data-row-id\"). */\r\n rowIdAttribute?: string;\r\n /**\r\n * Ref контейнера «подготовки»: при наличии addRow сначала добавляет в preparedRows;\r\n * при пересечении строки с этим контейнером вызывается onPreparedEnter, затем строка переносится в rows.\r\n */\r\n preparingRef?: RefObject<Element | null>;\r\n /**\r\n * Вызывается, когда строка из preparedRows попадает в зону preparingRef (например, для processZeroWidthEmotes).\r\n * После вызова строка переносится в основной список (main rows).\r\n */\r\n onPreparedEnter?: (rowId: number, element: Element) => void;\r\n}\r\n\r\nexport interface UseMessageRowsReturn<T> {\r\n /** Текущий список строк (основной список, рендер в main). */\r\n rows: T[];\r\n /** Строки в зоне подготовки (рендер внутри элемента с preparingRef). При отсутствии preparingRef — пустой массив. */\r\n preparedRows: T[];\r\n /** Установить список строк (полная замена). */\r\n setRows: Dispatch<SetStateAction<T[]>>;\r\n /** Добавить строку: при наличии preparingRef — в preparedRows, иначе в rows с применением лимита. */\r\n addRow: (row: T) => void;\r\n /** Перенести строку из preparedRows в rows по id. Вызывается автоматически после onPreparedEnter при использовании preparingRef. */\r\n moveRow: (rowId: number) => void;\r\n /**\r\n * Зарегистрировать DOM-элемент подготовленной строки для наблюдения (вызывать при монтировании/размонтировании строки).\r\n * Когда элемент попадает в зону preparingRef, вызывается onPreparedEnter и затем moveRow(rowId).\r\n */\r\n registerPreparedRow: (rowId: number, element: Element | null) => void;\r\n /** Применить лимит сообщений (удалить лишние строки из DOM и state). */\r\n applyLimit: () => void;\r\n}\r\n\r\n/**\r\n * Хук для управления списком строк с учётом message limit:\r\n * хранит rows в state, при applyLimit удаляет лишние из DOM и синхронизирует state по data-row-id.\r\n * При передаче preparingRef новые строки сначала попадают в preparedRows; при пересечении с контейнером\r\n * вызывается onPreparedEnter и строка переносится в rows.\r\n */\r\nexport function useMessageRows<T>(\r\n rootRef: RefObject<Element | null>,\r\n options: UseMessageRowsOptions<T>,\r\n): UseMessageRowsReturn<T> {\r\n const {\r\n getRowId,\r\n rowIdAttribute = \"data-row-id\",\r\n preparingRef,\r\n onPreparedEnter,\r\n ...limitOptions\r\n } = options;\r\n const [rows, setRows] = useState<T[]>([]);\r\n const [preparedRows, setPreparedRows] = useState<T[]>([]);\r\n const onPreparedEnterRef = useRef(onPreparedEnter);\r\n onPreparedEnterRef.current = onPreparedEnter;\r\n const observersRef = useRef<Map<number, IntersectionObserver>>(new Map());\r\n\r\n const applyLimit = useMessageLimit(rootRef, {\r\n ...limitOptions,\r\n onRemove: (el: Element) => {\r\n const idStr = el.getAttribute(rowIdAttribute);\r\n if (idStr != null) {\r\n const id = Number(idStr);\r\n setRows((prev) => prev.filter((r) => getRowId(r) !== id));\r\n }\r\n },\r\n });\r\n\r\n const moveRow = useCallback(\r\n (rowId: number) => {\r\n setPreparedRows((prev) => {\r\n const row = prev.find((r) => getRowId(r) === rowId);\r\n if (row == null) return prev;\r\n setRows((r) =>\r\n r.some((x) => getRowId(x) === rowId) ? r : [...r, row],\r\n );\r\n queueMicrotask(applyLimit);\r\n return prev.filter((r) => getRowId(r) !== rowId);\r\n });\r\n },\r\n [getRowId, applyLimit],\r\n );\r\n\r\n const registerPreparedRow = useCallback(\r\n (rowId: number, element: Element | null) => {\r\n const observers = observersRef.current;\r\n const existing = observers.get(rowId);\r\n if (existing) {\r\n existing.disconnect();\r\n observers.delete(rowId);\r\n }\r\n if (element == null || !preparingRef?.current) return;\r\n\r\n const root = preparingRef.current;\r\n const observer = new IntersectionObserver(\r\n (entries) => {\r\n const entry = entries[0];\r\n if (!entry?.isIntersecting) return;\r\n const id = rowId;\r\n observer.disconnect();\r\n observers.delete(id);\r\n onPreparedEnterRef.current?.(id, entry.target);\r\n moveRow(id);\r\n },\r\n { root, threshold: 0, rootMargin: \"0px\" },\r\n );\r\n observer.observe(element);\r\n observers.set(rowId, observer);\r\n },\r\n [preparingRef, moveRow],\r\n );\r\n\r\n useEffect(() => {\r\n return () => {\r\n observersRef.current.forEach((o) => o.disconnect());\r\n observersRef.current.clear();\r\n };\r\n }, []);\r\n\r\n const addRow = useCallback(\r\n (row: T) => {\r\n if (preparingRef) {\r\n setPreparedRows((prev) => [...prev, row]);\r\n } else {\r\n setRows((prev) => [...prev, row]);\r\n applyLimit();\r\n }\r\n },\r\n [preparingRef, applyLimit],\r\n );\r\n\r\n return {\r\n rows,\r\n preparedRows: preparingRef ? preparedRows : [],\r\n setRows,\r\n addRow,\r\n moveRow,\r\n registerPreparedRow: preparingRef ? registerPreparedRow : () => {},\r\n applyLimit,\r\n };\r\n}\r\n"],"names":["useIgnoreMessage","ignoredUsers","ignoreCommands","ignoredUsersLower","user","shouldIgnoreUser","username","shouldIgnoreMessage","text","useMessageLimit","rootRef","options","messageLimit","limitSelectors","onRemove","onRemoveRef","useRef","useEffect","root","setMessageLimitRoot","plugMessagesLimit","handleMessageLimitRoot","el","_a","useMessageRows","getRowId","rowIdAttribute","preparingRef","onPreparedEnter","limitOptions","rows","setRows","useState","preparedRows","setPreparedRows","onPreparedEnterRef","observersRef","applyLimit","idStr","id","prev","r","moveRow","useCallback","rowId","row","x","registerPreparedRow","element","observers","existing","observer","entries","entry","o","addRow"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Экранирование строки для безопасной вставки в HTML:
3
+ * нормализация пробелов и замена < > " ^ на сущности.
4
+ */
5
+ export declare function htmlEncode(html: string): string;
6
+ //# sourceMappingURL=html-encode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-encode.d.ts","sourceRoot":"","sources":["../../src/root/html-encode.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI/C"}
@@ -0,0 +1,5 @@
1
+ export * from "./message-limit";
2
+ export * from "./html-encode";
3
+ export * from "./preload-images";
4
+ export * from "./string-utils";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/root/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,2 @@
1
+ (function(a){"use strict";let g=15,c=[".message-row",".reply"],n=null;function i(e){n=e}function f(e){const t=n;if(!t)return;const o=c.join(", ");for(;;){const r=t.querySelectorAll(o),l=r.length;if(g<=0||l<=g)break;const s=r[0];if(!s)break;e==null||e(s),s.remove()}}function d(e,t){const o=n;if(!o)return;const r=c.map(s=>`${s}[data-msgid="${e}"]`).join(", ");o.querySelectorAll(r).forEach(s=>{t==null||t(e,s),s.remove()})}function m(e,t){const o=n;if(!o)return;const r=c.map(s=>`${s}[data-userid="${e}"]`).join(", ");o.querySelectorAll(r).forEach(s=>{t==null||t(e,s),s.remove()})}function h(e=[".message-row",".reply"],t){g=t??15,c=e}function C(e){return e.replace(/\s/g," ").replace(/[<>"^]/g,t=>"&#"+t.charCodeAt(0)+";")}function L(e,t){const o=e.querySelectorAll("img"),r=o.length;if(r===0){t(e);return}let l=0;function s(){l++,l===r&&t(e)}o.forEach(u=>{u.complete?s():(u.addEventListener("load",s),u.addEventListener("error",s))})}const p=e=>e.replace(/([A-Z])/g,"-$1").toLowerCase();function y(e){return e.toLowerCase().replace(/^vite_/,"").replace(/_([a-z])/g,(t,o)=>o.toUpperCase()).replace(/^[a-z]/,t=>t.toLowerCase())}function S(e){return"--"+e.toLowerCase().replace(/^vite_/,"").replace(/_/g,"-")}a.deleteMessage=d,a.deleteMessages=m,a.handleMessageLimit=f,a.htmlEncode=C,a.plugMessagesLimit=h,a.preloadImagesThenShow=L,a.setMessageLimitRoot=i,a.toCamelCase=y,a.toCssCustomProperty=S,a.toKebabCase=p,Object.defineProperty(a,Symbol.toStringTag,{value:"Module"})})(this.WmakeTts=this.WmakeTts||{});
2
+ //# sourceMappingURL=index.iife.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.iife.js","sources":["../../src/root/message-limit.ts","../../src/root/html-encode.ts","../../src/root/preload-images.ts","../../src/root/string-utils.ts"],"sourcesContent":["let messagesLimit = 15;\r\nlet limitSelectors: string[] = [\".message-row\", \".reply\"];\r\n\r\nlet rootRef: Element | null = null;\r\n\r\n/**\r\n * Set the container element for message limit and delete operations.\r\n * Pass the DOM element that wraps all message rows (e.g. document.querySelector(\"#main\")).\r\n */\r\nexport function setMessageLimitRoot(root: Element | null): void {\r\n rootRef = root;\r\n}\r\n\r\n/**\r\n * Enforces message limit by recalculating current count each time and removing\r\n * excess messages in a loop until totalMessages <= messagesLimit.\r\n */\r\nexport function handleMessageLimit(\r\n onRemoveMixin?: (element: Element) => void,\r\n): void {\r\n const root = rootRef;\r\n if (!root) return;\r\n\r\n const selector = limitSelectors.join(\", \");\r\n\r\n for (;;) {\r\n const children = root.querySelectorAll(selector);\r\n const totalMessages = children.length;\r\n\r\n if (messagesLimit <= 0 || totalMessages <= messagesLimit) {\r\n break;\r\n }\r\n\r\n const first = children[0];\r\n if (!first) break;\r\n onRemoveMixin?.(first);\r\n first.remove();\r\n }\r\n}\r\n\r\nexport function deleteMessage(\r\n msgId: string,\r\n mixin?: (msgId: string, element: Element) => void,\r\n): void {\r\n const root = rootRef;\r\n if (!root) return;\r\n const selectors = limitSelectors\r\n .map((s) => `${s}[data-msgid=\"${msgId}\"]`)\r\n .join(\", \");\r\n const elements = root.querySelectorAll(selectors);\r\n elements.forEach((el) => {\r\n mixin?.(msgId, el);\r\n el.remove();\r\n });\r\n}\r\n\r\nexport function deleteMessages(\r\n userId: string,\r\n mixin?: (userId: string, element: Element) => void,\r\n): void {\r\n const root = rootRef;\r\n if (!root) return;\r\n const selectors = limitSelectors\r\n .map((s) => `${s}[data-userid=\"${userId}\"]`)\r\n .join(\", \");\r\n const elements = root.querySelectorAll(selectors);\r\n elements.forEach((el) => {\r\n mixin?.(userId, el);\r\n el.remove();\r\n });\r\n}\r\n\r\nexport function plugMessagesLimit(\r\n _limitSelectors: string[] = [\".message-row\", \".reply\"],\r\n _messagesLimit?: number,\r\n): void {\r\n messagesLimit = _messagesLimit ?? 15;\r\n limitSelectors = _limitSelectors;\r\n}\r\n","/**\r\n * Экранирование строки для безопасной вставки в HTML:\r\n * нормализация пробелов и замена < > \" ^ на сущности.\r\n */\r\nexport function htmlEncode(html: string): string {\r\n return html\r\n .replace(/\\s/g, \" \")\r\n .replace(/[<>\"^]/g, (match) => \"&#\" + match.charCodeAt(0) + \";\");\r\n}\r\n","/**\r\n * Ожидает загрузки всех <img> внутри контейнера, затем вызывает callback.\r\n * Если изображений нет — callback вызывается сразу.\r\n */\r\nexport function preloadImagesThenShow<T extends Element>(\r\n element: T,\r\n showFunction: (element: T) => void,\r\n): void {\r\n const images = element.querySelectorAll<HTMLImageElement>(\"img\");\r\n const totalImages = images.length;\r\n\r\n if (totalImages === 0) {\r\n showFunction(element);\r\n return;\r\n }\r\n\r\n let loadedImages = 0;\r\n\r\n function imageLoaded() {\r\n loadedImages++;\r\n if (loadedImages === totalImages) {\r\n showFunction(element);\r\n }\r\n }\r\n\r\n images.forEach((img) => {\r\n if (img.complete) {\r\n imageLoaded();\r\n } else {\r\n img.addEventListener(\"load\", imageLoaded);\r\n img.addEventListener(\"error\", imageLoaded);\r\n }\r\n });\r\n}\r\n","export const toKebabCase = (str: string) =>\r\n str.replace(/([A-Z])/g, '-$1').toLowerCase();\r\n\r\nexport function toCamelCase(str: string): string {\r\n return str\r\n .toLowerCase()\r\n .replace(/^vite_/, \"\")\r\n .replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())\r\n .replace(/^[a-z]/, (match) => match.toLowerCase());\r\n}\r\n\r\nexport function toCssCustomProperty(str: string): string {\r\n return (\r\n \"--\" +\r\n str\r\n .toLowerCase()\r\n .replace(/^vite_/, \"\")\r\n .replace(/_/g, \"-\")\r\n );\r\n}\r\n"],"names":["messagesLimit","limitSelectors","rootRef","setMessageLimitRoot","root","handleMessageLimit","onRemoveMixin","selector","children","totalMessages","first","deleteMessage","msgId","mixin","selectors","el","deleteMessages","userId","plugMessagesLimit","_limitSelectors","_messagesLimit","htmlEncode","html","match","preloadImagesThenShow","element","showFunction","images","totalImages","loadedImages","imageLoaded","img","toKebabCase","str","toCamelCase","_","letter","toCssCustomProperty"],"mappings":"0BAAA,IAAIA,EAAgB,GAChBC,EAA2B,CAAC,eAAgB,QAAQ,EAEpDC,EAA0B,KAMvB,SAASC,EAAoBC,EAA4B,CAC9DF,EAAUE,CACZ,CAMO,SAASC,EACdC,EACM,CACN,MAAMF,EAAOF,EACb,GAAI,CAACE,EAAM,OAEX,MAAMG,EAAWN,EAAe,KAAK,IAAI,EAEzC,OAAS,CACP,MAAMO,EAAWJ,EAAK,iBAAiBG,CAAQ,EACzCE,EAAgBD,EAAS,OAE/B,GAAIR,GAAiB,GAAKS,GAAiBT,EACzC,MAGF,MAAMU,EAAQF,EAAS,CAAC,EACxB,GAAI,CAACE,EAAO,MACZJ,GAAA,MAAAA,EAAgBI,GAChBA,EAAM,OAAA,CACR,CACF,CAEO,SAASC,EACdC,EACAC,EACM,CACN,MAAMT,EAAOF,EACb,GAAI,CAACE,EAAM,OACX,MAAMU,EAAYb,EACf,IAAK,GAAM,GAAG,CAAC,gBAAgBW,CAAK,IAAI,EACxC,KAAK,IAAI,EACKR,EAAK,iBAAiBU,CAAS,EACvC,QAASC,GAAO,CACvBF,GAAA,MAAAA,EAAQD,EAAOG,GACfA,EAAG,OAAA,CACL,CAAC,CACH,CAEO,SAASC,EACdC,EACAJ,EACM,CACN,MAAMT,EAAOF,EACb,GAAI,CAACE,EAAM,OACX,MAAMU,EAAYb,EACf,IAAK,GAAM,GAAG,CAAC,iBAAiBgB,CAAM,IAAI,EAC1C,KAAK,IAAI,EACKb,EAAK,iBAAiBU,CAAS,EACvC,QAASC,GAAO,CACvBF,GAAA,MAAAA,EAAQI,EAAQF,GAChBA,EAAG,OAAA,CACL,CAAC,CACH,CAEO,SAASG,EACdC,EAA4B,CAAC,eAAgB,QAAQ,EACrDC,EACM,CACNpB,EAAgBoB,GAAkB,GAClCnB,EAAiBkB,CACnB,CC1EO,SAASE,EAAWC,EAAsB,CAC/C,OAAOA,EACJ,QAAQ,MAAO,GAAG,EAClB,QAAQ,UAAYC,GAAU,KAAOA,EAAM,WAAW,CAAC,EAAI,GAAG,CACnE,CCJO,SAASC,EACdC,EACAC,EACM,CACN,MAAMC,EAASF,EAAQ,iBAAmC,KAAK,EACzDG,EAAcD,EAAO,OAE3B,GAAIC,IAAgB,EAAG,CACrBF,EAAaD,CAAO,EACpB,MACF,CAEA,IAAII,EAAe,EAEnB,SAASC,GAAc,CACrBD,IACIA,IAAiBD,GACnBF,EAAaD,CAAO,CAExB,CAEAE,EAAO,QAASI,GAAQ,CAClBA,EAAI,SACND,EAAA,GAEAC,EAAI,iBAAiB,OAAQD,CAAW,EACxCC,EAAI,iBAAiB,QAASD,CAAW,EAE7C,CAAC,CACH,CCjCO,MAAME,EAAeC,GAC1BA,EAAI,QAAQ,WAAY,KAAK,EAAE,YAAA,EAE1B,SAASC,EAAYD,EAAqB,CAC/C,OAAOA,EACJ,cACA,QAAQ,SAAU,EAAE,EACpB,QAAQ,YAAa,CAACE,EAAGC,IAAWA,EAAO,YAAA,CAAa,EACxD,QAAQ,SAAWb,GAAUA,EAAM,aAAa,CACrD,CAEO,SAASc,EAAoBJ,EAAqB,CACvD,MACE,KACAA,EACG,YAAA,EACA,QAAQ,SAAU,EAAE,EACpB,QAAQ,KAAM,GAAG,CAExB"}
@@ -0,0 +1,73 @@
1
+ let f = 15, l = [".message-row", ".reply"], c = null;
2
+ function u(e) {
3
+ c = e;
4
+ }
5
+ function g(e) {
6
+ const t = c;
7
+ if (!t) return;
8
+ const r = l.join(", ");
9
+ for (; ; ) {
10
+ const s = t.querySelectorAll(r), a = s.length;
11
+ if (f <= 0 || a <= f)
12
+ break;
13
+ const o = s[0];
14
+ if (!o) break;
15
+ e == null || e(o), o.remove();
16
+ }
17
+ }
18
+ function p(e, t) {
19
+ const r = c;
20
+ if (!r) return;
21
+ const s = l.map((o) => `${o}[data-msgid="${e}"]`).join(", ");
22
+ r.querySelectorAll(s).forEach((o) => {
23
+ t == null || t(e, o), o.remove();
24
+ });
25
+ }
26
+ function d(e, t) {
27
+ const r = c;
28
+ if (!r) return;
29
+ const s = l.map((o) => `${o}[data-userid="${e}"]`).join(", ");
30
+ r.querySelectorAll(s).forEach((o) => {
31
+ t == null || t(e, o), o.remove();
32
+ });
33
+ }
34
+ function i(e = [".message-row", ".reply"], t) {
35
+ f = t ?? 15, l = e;
36
+ }
37
+ function m(e) {
38
+ return e.replace(/\s/g, " ").replace(/[<>"^]/g, (t) => "&#" + t.charCodeAt(0) + ";");
39
+ }
40
+ function C(e, t) {
41
+ const r = e.querySelectorAll("img"), s = r.length;
42
+ if (s === 0) {
43
+ t(e);
44
+ return;
45
+ }
46
+ let a = 0;
47
+ function o() {
48
+ a++, a === s && t(e);
49
+ }
50
+ r.forEach((n) => {
51
+ n.complete ? o() : (n.addEventListener("load", o), n.addEventListener("error", o));
52
+ });
53
+ }
54
+ const L = (e) => e.replace(/([A-Z])/g, "-$1").toLowerCase();
55
+ function h(e) {
56
+ return e.toLowerCase().replace(/^vite_/, "").replace(/_([a-z])/g, (t, r) => r.toUpperCase()).replace(/^[a-z]/, (t) => t.toLowerCase());
57
+ }
58
+ function y(e) {
59
+ return "--" + e.toLowerCase().replace(/^vite_/, "").replace(/_/g, "-");
60
+ }
61
+ export {
62
+ p as deleteMessage,
63
+ d as deleteMessages,
64
+ g as handleMessageLimit,
65
+ m as htmlEncode,
66
+ i as plugMessagesLimit,
67
+ C as preloadImagesThenShow,
68
+ u as setMessageLimitRoot,
69
+ h as toCamelCase,
70
+ y as toCssCustomProperty,
71
+ L as toKebabCase
72
+ };
73
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/root/message-limit.ts","../../src/root/html-encode.ts","../../src/root/preload-images.ts","../../src/root/string-utils.ts"],"sourcesContent":["let messagesLimit = 15;\r\nlet limitSelectors: string[] = [\".message-row\", \".reply\"];\r\n\r\nlet rootRef: Element | null = null;\r\n\r\n/**\r\n * Set the container element for message limit and delete operations.\r\n * Pass the DOM element that wraps all message rows (e.g. document.querySelector(\"#main\")).\r\n */\r\nexport function setMessageLimitRoot(root: Element | null): void {\r\n rootRef = root;\r\n}\r\n\r\n/**\r\n * Enforces message limit by recalculating current count each time and removing\r\n * excess messages in a loop until totalMessages <= messagesLimit.\r\n */\r\nexport function handleMessageLimit(\r\n onRemoveMixin?: (element: Element) => void,\r\n): void {\r\n const root = rootRef;\r\n if (!root) return;\r\n\r\n const selector = limitSelectors.join(\", \");\r\n\r\n for (;;) {\r\n const children = root.querySelectorAll(selector);\r\n const totalMessages = children.length;\r\n\r\n if (messagesLimit <= 0 || totalMessages <= messagesLimit) {\r\n break;\r\n }\r\n\r\n const first = children[0];\r\n if (!first) break;\r\n onRemoveMixin?.(first);\r\n first.remove();\r\n }\r\n}\r\n\r\nexport function deleteMessage(\r\n msgId: string,\r\n mixin?: (msgId: string, element: Element) => void,\r\n): void {\r\n const root = rootRef;\r\n if (!root) return;\r\n const selectors = limitSelectors\r\n .map((s) => `${s}[data-msgid=\"${msgId}\"]`)\r\n .join(\", \");\r\n const elements = root.querySelectorAll(selectors);\r\n elements.forEach((el) => {\r\n mixin?.(msgId, el);\r\n el.remove();\r\n });\r\n}\r\n\r\nexport function deleteMessages(\r\n userId: string,\r\n mixin?: (userId: string, element: Element) => void,\r\n): void {\r\n const root = rootRef;\r\n if (!root) return;\r\n const selectors = limitSelectors\r\n .map((s) => `${s}[data-userid=\"${userId}\"]`)\r\n .join(\", \");\r\n const elements = root.querySelectorAll(selectors);\r\n elements.forEach((el) => {\r\n mixin?.(userId, el);\r\n el.remove();\r\n });\r\n}\r\n\r\nexport function plugMessagesLimit(\r\n _limitSelectors: string[] = [\".message-row\", \".reply\"],\r\n _messagesLimit?: number,\r\n): void {\r\n messagesLimit = _messagesLimit ?? 15;\r\n limitSelectors = _limitSelectors;\r\n}\r\n","/**\r\n * Экранирование строки для безопасной вставки в HTML:\r\n * нормализация пробелов и замена < > \" ^ на сущности.\r\n */\r\nexport function htmlEncode(html: string): string {\r\n return html\r\n .replace(/\\s/g, \" \")\r\n .replace(/[<>\"^]/g, (match) => \"&#\" + match.charCodeAt(0) + \";\");\r\n}\r\n","/**\r\n * Ожидает загрузки всех <img> внутри контейнера, затем вызывает callback.\r\n * Если изображений нет — callback вызывается сразу.\r\n */\r\nexport function preloadImagesThenShow<T extends Element>(\r\n element: T,\r\n showFunction: (element: T) => void,\r\n): void {\r\n const images = element.querySelectorAll<HTMLImageElement>(\"img\");\r\n const totalImages = images.length;\r\n\r\n if (totalImages === 0) {\r\n showFunction(element);\r\n return;\r\n }\r\n\r\n let loadedImages = 0;\r\n\r\n function imageLoaded() {\r\n loadedImages++;\r\n if (loadedImages === totalImages) {\r\n showFunction(element);\r\n }\r\n }\r\n\r\n images.forEach((img) => {\r\n if (img.complete) {\r\n imageLoaded();\r\n } else {\r\n img.addEventListener(\"load\", imageLoaded);\r\n img.addEventListener(\"error\", imageLoaded);\r\n }\r\n });\r\n}\r\n","export const toKebabCase = (str: string) =>\r\n str.replace(/([A-Z])/g, '-$1').toLowerCase();\r\n\r\nexport function toCamelCase(str: string): string {\r\n return str\r\n .toLowerCase()\r\n .replace(/^vite_/, \"\")\r\n .replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())\r\n .replace(/^[a-z]/, (match) => match.toLowerCase());\r\n}\r\n\r\nexport function toCssCustomProperty(str: string): string {\r\n return (\r\n \"--\" +\r\n str\r\n .toLowerCase()\r\n .replace(/^vite_/, \"\")\r\n .replace(/_/g, \"-\")\r\n );\r\n}\r\n"],"names":["messagesLimit","limitSelectors","rootRef","setMessageLimitRoot","root","handleMessageLimit","onRemoveMixin","selector","children","totalMessages","first","deleteMessage","msgId","mixin","selectors","s","el","deleteMessages","userId","plugMessagesLimit","_limitSelectors","_messagesLimit","htmlEncode","html","match","preloadImagesThenShow","element","showFunction","images","totalImages","loadedImages","imageLoaded","img","toKebabCase","str","toCamelCase","_","letter","toCssCustomProperty"],"mappings":"AAAA,IAAIA,IAAgB,IAChBC,IAA2B,CAAC,gBAAgB,QAAQ,GAEpDC,IAA0B;AAMvB,SAASC,EAAoBC,GAA4B;AAC9D,EAAAF,IAAUE;AACZ;AAMO,SAASC,EACdC,GACM;AACN,QAAMF,IAAOF;AACb,MAAI,CAACE,EAAM;AAEX,QAAMG,IAAWN,EAAe,KAAK,IAAI;AAEzC,aAAS;AACP,UAAMO,IAAWJ,EAAK,iBAAiBG,CAAQ,GACzCE,IAAgBD,EAAS;AAE/B,QAAIR,KAAiB,KAAKS,KAAiBT;AACzC;AAGF,UAAMU,IAAQF,EAAS,CAAC;AACxB,QAAI,CAACE,EAAO;AACZ,IAAAJ,KAAA,QAAAA,EAAgBI,IAChBA,EAAM,OAAA;AAAA,EACR;AACF;AAEO,SAASC,EACdC,GACAC,GACM;AACN,QAAMT,IAAOF;AACb,MAAI,CAACE,EAAM;AACX,QAAMU,IAAYb,EACf,IAAI,CAACc,MAAM,GAAGA,CAAC,gBAAgBH,CAAK,IAAI,EACxC,KAAK,IAAI;AAEZ,EADiBR,EAAK,iBAAiBU,CAAS,EACvC,QAAQ,CAACE,MAAO;AACvB,IAAAH,KAAA,QAAAA,EAAQD,GAAOI,IACfA,EAAG,OAAA;AAAA,EACL,CAAC;AACH;AAEO,SAASC,EACdC,GACAL,GACM;AACN,QAAMT,IAAOF;AACb,MAAI,CAACE,EAAM;AACX,QAAMU,IAAYb,EACf,IAAI,CAACc,MAAM,GAAGA,CAAC,iBAAiBG,CAAM,IAAI,EAC1C,KAAK,IAAI;AAEZ,EADiBd,EAAK,iBAAiBU,CAAS,EACvC,QAAQ,CAACE,MAAO;AACvB,IAAAH,KAAA,QAAAA,EAAQK,GAAQF,IAChBA,EAAG,OAAA;AAAA,EACL,CAAC;AACH;AAEO,SAASG,EACdC,IAA4B,CAAC,gBAAgB,QAAQ,GACrDC,GACM;AACN,EAAArB,IAAgBqB,KAAkB,IAClCpB,IAAiBmB;AACnB;AC1EO,SAASE,EAAWC,GAAsB;AAC/C,SAAOA,EACJ,QAAQ,OAAO,GAAG,EAClB,QAAQ,WAAW,CAACC,MAAU,OAAOA,EAAM,WAAW,CAAC,IAAI,GAAG;AACnE;ACJO,SAASC,EACdC,GACAC,GACM;AACN,QAAMC,IAASF,EAAQ,iBAAmC,KAAK,GACzDG,IAAcD,EAAO;AAE3B,MAAIC,MAAgB,GAAG;AACrB,IAAAF,EAAaD,CAAO;AACpB;AAAA,EACF;AAEA,MAAII,IAAe;AAEnB,WAASC,IAAc;AACrB,IAAAD,KACIA,MAAiBD,KACnBF,EAAaD,CAAO;AAAA,EAExB;AAEA,EAAAE,EAAO,QAAQ,CAACI,MAAQ;AACtB,IAAIA,EAAI,WACND,EAAA,KAEAC,EAAI,iBAAiB,QAAQD,CAAW,GACxCC,EAAI,iBAAiB,SAASD,CAAW;AAAA,EAE7C,CAAC;AACH;ACjCO,MAAME,IAAc,CAACC,MAC1BA,EAAI,QAAQ,YAAY,KAAK,EAAE,YAAA;AAE1B,SAASC,EAAYD,GAAqB;AAC/C,SAAOA,EACJ,cACA,QAAQ,UAAU,EAAE,EACpB,QAAQ,aAAa,CAACE,GAAGC,MAAWA,EAAO,YAAA,CAAa,EACxD,QAAQ,UAAU,CAACb,MAAUA,EAAM,aAAa;AACrD;AAEO,SAASc,EAAoBJ,GAAqB;AACvD,SACE,OACAA,EACG,YAAA,EACA,QAAQ,UAAU,EAAE,EACpB,QAAQ,MAAM,GAAG;AAExB;"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Set the container element for message limit and delete operations.
3
+ * Pass the DOM element that wraps all message rows (e.g. document.querySelector("#main")).
4
+ */
5
+ export declare function setMessageLimitRoot(root: Element | null): void;
6
+ /**
7
+ * Enforces message limit by recalculating current count each time and removing
8
+ * excess messages in a loop until totalMessages <= messagesLimit.
9
+ */
10
+ export declare function handleMessageLimit(onRemoveMixin?: (element: Element) => void): void;
11
+ export declare function deleteMessage(msgId: string, mixin?: (msgId: string, element: Element) => void): void;
12
+ export declare function deleteMessages(userId: string, mixin?: (userId: string, element: Element) => void): void;
13
+ export declare function plugMessagesLimit(_limitSelectors?: string[], _messagesLimit?: number): void;
14
+ //# sourceMappingURL=message-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-limit.d.ts","sourceRoot":"","sources":["../../src/root/message-limit.ts"],"names":[],"mappings":"AAKA;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAE9D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GACzC,IAAI,CAmBN;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,GAChD,IAAI,CAWN;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,GACjD,IAAI,CAWN;AAED,wBAAgB,iBAAiB,CAC/B,eAAe,GAAE,MAAM,EAA+B,EACtD,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI,CAGN"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Ожидает загрузки всех <img> внутри контейнера, затем вызывает callback.
3
+ * Если изображений нет — callback вызывается сразу.
4
+ */
5
+ export declare function preloadImagesThenShow<T extends Element>(element: T, showFunction: (element: T) => void): void;
6
+ //# sourceMappingURL=preload-images.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preload-images.d.ts","sourceRoot":"","sources":["../../src/root/preload-images.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,OAAO,EACrD,OAAO,EAAE,CAAC,EACV,YAAY,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,GACjC,IAAI,CA0BN"}
@@ -0,0 +1,4 @@
1
+ export declare const toKebabCase: (str: string) => string;
2
+ export declare function toCamelCase(str: string): string;
3
+ export declare function toCssCustomProperty(str: string): string;
4
+ //# sourceMappingURL=string-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string-utils.d.ts","sourceRoot":"","sources":["../../src/root/string-utils.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,WACO,CAAC;AAE/C,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM/C;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQvD"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@allior/wmake-utils",
3
+ "version": "0.0.1",
4
+ "description": "Utilities: message content parsers, image preloading, and more.",
5
+ "type": "module",
6
+ "types": "./dist/root/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/root/index.d.ts",
10
+ "import": "./dist/root/index.js"
11
+ },
12
+ "./react": {
13
+ "types": "./dist/react/index.d.ts",
14
+ "import": "./dist/react/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src",
20
+ "README.md",
21
+ "README_RU.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "rimraf dist && vite build && cross-env VITE_IIFE_ENTRY=root vite build --config vite.iife.config.ts && cross-env VITE_IIFE_ENTRY=react vite build --config vite.iife.config.ts && tsc -p tsconfig.types.json",
25
+ "clean": "rimraf dist",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "keywords": [
29
+ "wmake",
30
+ "parser",
31
+ "chat",
32
+ "emotes",
33
+ "message"
34
+ ],
35
+ "author": "An1by",
36
+ "license": "ISC",
37
+ "peerDependencies": {
38
+ "react": ">=18.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "cross-env": "^10.1.0",
42
+ "react": "^19.0.0"
43
+ }
44
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./use-ignore-message";
2
+ export * from "./use-message-limit";
3
+ export * from "./use-message-rows";
@@ -0,0 +1,25 @@
1
+ interface Props {
2
+ ignoredUsers?: string[];
3
+ ignoreCommands?: boolean;
4
+ }
5
+
6
+ export const useIgnoreMessage = ({
7
+ ignoredUsers = [],
8
+ ignoreCommands = true,
9
+ }: Props) => {
10
+ const ignoredUsersLower = ignoredUsers.map((user) => user.toLowerCase());
11
+
12
+ const shouldIgnoreUser = (username: string) => {
13
+ return ignoredUsersLower.includes(username.toLowerCase());
14
+ };
15
+
16
+ const shouldIgnoreMessage = (text: string) => {
17
+ return ignoreCommands && text.trim().startsWith("!");
18
+ };
19
+
20
+ const shouldIgnore = (username: string, text: string) => {
21
+ return shouldIgnoreUser(username) || shouldIgnoreMessage(text);
22
+ };
23
+
24
+ return { shouldIgnore };
25
+ };
@@ -0,0 +1,42 @@
1
+ import { useEffect, useRef } from "react";
2
+ import type { RefObject } from "react";
3
+ import {
4
+ setMessageLimitRoot,
5
+ handleMessageLimit as handleMessageLimitRoot,
6
+ plugMessagesLimit,
7
+ } from "@/root";
8
+
9
+ export interface UseMessageLimitOptions {
10
+ messageLimit?: number;
11
+ /** Селекторы строк для лимита (по умолчанию [".message-row", ".reply"]) */
12
+ limitSelectors?: string[];
13
+ /** Вызов при удалении элемента из DOM (для синхронизации state в React) */
14
+ onRemove?: (element: Element) => void;
15
+ }
16
+
17
+ /**
18
+ * Подключает message limit к корневому элементу и опциям из widget load.
19
+ * Возвращает handleMessageLimit — вызывать после добавления новой строки.
20
+ */
21
+ export function useMessageLimit(
22
+ rootRef: RefObject<Element | null>,
23
+ options: UseMessageLimitOptions = {},
24
+ ): () => void {
25
+ const { messageLimit, limitSelectors, onRemove } = options;
26
+ const onRemoveRef = useRef(onRemove);
27
+ onRemoveRef.current = onRemove;
28
+
29
+ useEffect(() => {
30
+ const root = rootRef.current;
31
+ setMessageLimitRoot(root);
32
+ return () => setMessageLimitRoot(null);
33
+ }, [rootRef]);
34
+
35
+ useEffect(() => {
36
+ plugMessagesLimit(limitSelectors, messageLimit);
37
+ }, [messageLimit, limitSelectors]);
38
+
39
+ return () => {
40
+ handleMessageLimitRoot((el) => onRemoveRef.current?.(el));
41
+ };
42
+ }
@@ -0,0 +1,154 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import type { Dispatch, RefObject, SetStateAction } from "react";
3
+ import { useMessageLimit } from "./use-message-limit";
4
+ import type { UseMessageLimitOptions } from "./use-message-limit";
5
+
6
+ export interface UseMessageRowsOptions<T> extends Omit<
7
+ UseMessageLimitOptions,
8
+ "onRemove"
9
+ > {
10
+ /**
11
+ * Функция получения id строки (для синхронизации state при удалении из DOM по data-row-id).
12
+ */
13
+ getRowId: (row: T) => number;
14
+ /** Имя атрибута с id на DOM-элементе строки (по умолчанию "data-row-id"). */
15
+ rowIdAttribute?: string;
16
+ /**
17
+ * Ref контейнера «подготовки»: при наличии addRow сначала добавляет в preparedRows;
18
+ * при пересечении строки с этим контейнером вызывается onPreparedEnter, затем строка переносится в rows.
19
+ */
20
+ preparingRef?: RefObject<Element | null>;
21
+ /**
22
+ * Вызывается, когда строка из preparedRows попадает в зону preparingRef (например, для processZeroWidthEmotes).
23
+ * После вызова строка переносится в основной список (main rows).
24
+ */
25
+ onPreparedEnter?: (rowId: number, element: Element) => void;
26
+ }
27
+
28
+ export interface UseMessageRowsReturn<T> {
29
+ /** Текущий список строк (основной список, рендер в main). */
30
+ rows: T[];
31
+ /** Строки в зоне подготовки (рендер внутри элемента с preparingRef). При отсутствии preparingRef — пустой массив. */
32
+ preparedRows: T[];
33
+ /** Установить список строк (полная замена). */
34
+ setRows: Dispatch<SetStateAction<T[]>>;
35
+ /** Добавить строку: при наличии preparingRef — в preparedRows, иначе в rows с применением лимита. */
36
+ addRow: (row: T) => void;
37
+ /** Перенести строку из preparedRows в rows по id. Вызывается автоматически после onPreparedEnter при использовании preparingRef. */
38
+ moveRow: (rowId: number) => void;
39
+ /**
40
+ * Зарегистрировать DOM-элемент подготовленной строки для наблюдения (вызывать при монтировании/размонтировании строки).
41
+ * Когда элемент попадает в зону preparingRef, вызывается onPreparedEnter и затем moveRow(rowId).
42
+ */
43
+ registerPreparedRow: (rowId: number, element: Element | null) => void;
44
+ /** Применить лимит сообщений (удалить лишние строки из DOM и state). */
45
+ applyLimit: () => void;
46
+ }
47
+
48
+ /**
49
+ * Хук для управления списком строк с учётом message limit:
50
+ * хранит rows в state, при applyLimit удаляет лишние из DOM и синхронизирует state по data-row-id.
51
+ * При передаче preparingRef новые строки сначала попадают в preparedRows; при пересечении с контейнером
52
+ * вызывается onPreparedEnter и строка переносится в rows.
53
+ */
54
+ export function useMessageRows<T>(
55
+ rootRef: RefObject<Element | null>,
56
+ options: UseMessageRowsOptions<T>,
57
+ ): UseMessageRowsReturn<T> {
58
+ const {
59
+ getRowId,
60
+ rowIdAttribute = "data-row-id",
61
+ preparingRef,
62
+ onPreparedEnter,
63
+ ...limitOptions
64
+ } = options;
65
+ const [rows, setRows] = useState<T[]>([]);
66
+ const [preparedRows, setPreparedRows] = useState<T[]>([]);
67
+ const onPreparedEnterRef = useRef(onPreparedEnter);
68
+ onPreparedEnterRef.current = onPreparedEnter;
69
+ const observersRef = useRef<Map<number, IntersectionObserver>>(new Map());
70
+
71
+ const applyLimit = useMessageLimit(rootRef, {
72
+ ...limitOptions,
73
+ onRemove: (el: Element) => {
74
+ const idStr = el.getAttribute(rowIdAttribute);
75
+ if (idStr != null) {
76
+ const id = Number(idStr);
77
+ setRows((prev) => prev.filter((r) => getRowId(r) !== id));
78
+ }
79
+ },
80
+ });
81
+
82
+ const moveRow = useCallback(
83
+ (rowId: number) => {
84
+ setPreparedRows((prev) => {
85
+ const row = prev.find((r) => getRowId(r) === rowId);
86
+ if (row == null) return prev;
87
+ setRows((r) =>
88
+ r.some((x) => getRowId(x) === rowId) ? r : [...r, row],
89
+ );
90
+ queueMicrotask(applyLimit);
91
+ return prev.filter((r) => getRowId(r) !== rowId);
92
+ });
93
+ },
94
+ [getRowId, applyLimit],
95
+ );
96
+
97
+ const registerPreparedRow = useCallback(
98
+ (rowId: number, element: Element | null) => {
99
+ const observers = observersRef.current;
100
+ const existing = observers.get(rowId);
101
+ if (existing) {
102
+ existing.disconnect();
103
+ observers.delete(rowId);
104
+ }
105
+ if (element == null || !preparingRef?.current) return;
106
+
107
+ const root = preparingRef.current;
108
+ const observer = new IntersectionObserver(
109
+ (entries) => {
110
+ const entry = entries[0];
111
+ if (!entry?.isIntersecting) return;
112
+ const id = rowId;
113
+ observer.disconnect();
114
+ observers.delete(id);
115
+ onPreparedEnterRef.current?.(id, entry.target);
116
+ moveRow(id);
117
+ },
118
+ { root, threshold: 0, rootMargin: "0px" },
119
+ );
120
+ observer.observe(element);
121
+ observers.set(rowId, observer);
122
+ },
123
+ [preparingRef, moveRow],
124
+ );
125
+
126
+ useEffect(() => {
127
+ return () => {
128
+ observersRef.current.forEach((o) => o.disconnect());
129
+ observersRef.current.clear();
130
+ };
131
+ }, []);
132
+
133
+ const addRow = useCallback(
134
+ (row: T) => {
135
+ if (preparingRef) {
136
+ setPreparedRows((prev) => [...prev, row]);
137
+ } else {
138
+ setRows((prev) => [...prev, row]);
139
+ applyLimit();
140
+ }
141
+ },
142
+ [preparingRef, applyLimit],
143
+ );
144
+
145
+ return {
146
+ rows,
147
+ preparedRows: preparingRef ? preparedRows : [],
148
+ setRows,
149
+ addRow,
150
+ moveRow,
151
+ registerPreparedRow: preparingRef ? registerPreparedRow : () => {},
152
+ applyLimit,
153
+ };
154
+ }
@@ -0,0 +1,2 @@
1
+ export * from "../root";
2
+ export * from "./hooks";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Экранирование строки для безопасной вставки в HTML:
3
+ * нормализация пробелов и замена < > " ^ на сущности.
4
+ */
5
+ export function htmlEncode(html: string): string {
6
+ return html
7
+ .replace(/\s/g, " ")
8
+ .replace(/[<>"^]/g, (match) => "&#" + match.charCodeAt(0) + ";");
9
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./message-limit";
2
+ export * from "./html-encode";
3
+ export * from "./preload-images";
4
+ export * from "./string-utils";
@@ -0,0 +1,79 @@
1
+ let messagesLimit = 15;
2
+ let limitSelectors: string[] = [".message-row", ".reply"];
3
+
4
+ let rootRef: Element | null = null;
5
+
6
+ /**
7
+ * Set the container element for message limit and delete operations.
8
+ * Pass the DOM element that wraps all message rows (e.g. document.querySelector("#main")).
9
+ */
10
+ export function setMessageLimitRoot(root: Element | null): void {
11
+ rootRef = root;
12
+ }
13
+
14
+ /**
15
+ * Enforces message limit by recalculating current count each time and removing
16
+ * excess messages in a loop until totalMessages <= messagesLimit.
17
+ */
18
+ export function handleMessageLimit(
19
+ onRemoveMixin?: (element: Element) => void,
20
+ ): void {
21
+ const root = rootRef;
22
+ if (!root) return;
23
+
24
+ const selector = limitSelectors.join(", ");
25
+
26
+ for (;;) {
27
+ const children = root.querySelectorAll(selector);
28
+ const totalMessages = children.length;
29
+
30
+ if (messagesLimit <= 0 || totalMessages <= messagesLimit) {
31
+ break;
32
+ }
33
+
34
+ const first = children[0];
35
+ if (!first) break;
36
+ onRemoveMixin?.(first);
37
+ first.remove();
38
+ }
39
+ }
40
+
41
+ export function deleteMessage(
42
+ msgId: string,
43
+ mixin?: (msgId: string, element: Element) => void,
44
+ ): void {
45
+ const root = rootRef;
46
+ if (!root) return;
47
+ const selectors = limitSelectors
48
+ .map((s) => `${s}[data-msgid="${msgId}"]`)
49
+ .join(", ");
50
+ const elements = root.querySelectorAll(selectors);
51
+ elements.forEach((el) => {
52
+ mixin?.(msgId, el);
53
+ el.remove();
54
+ });
55
+ }
56
+
57
+ export function deleteMessages(
58
+ userId: string,
59
+ mixin?: (userId: string, element: Element) => void,
60
+ ): void {
61
+ const root = rootRef;
62
+ if (!root) return;
63
+ const selectors = limitSelectors
64
+ .map((s) => `${s}[data-userid="${userId}"]`)
65
+ .join(", ");
66
+ const elements = root.querySelectorAll(selectors);
67
+ elements.forEach((el) => {
68
+ mixin?.(userId, el);
69
+ el.remove();
70
+ });
71
+ }
72
+
73
+ export function plugMessagesLimit(
74
+ _limitSelectors: string[] = [".message-row", ".reply"],
75
+ _messagesLimit?: number,
76
+ ): void {
77
+ messagesLimit = _messagesLimit ?? 15;
78
+ limitSelectors = _limitSelectors;
79
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Ожидает загрузки всех <img> внутри контейнера, затем вызывает callback.
3
+ * Если изображений нет — callback вызывается сразу.
4
+ */
5
+ export function preloadImagesThenShow<T extends Element>(
6
+ element: T,
7
+ showFunction: (element: T) => void,
8
+ ): void {
9
+ const images = element.querySelectorAll<HTMLImageElement>("img");
10
+ const totalImages = images.length;
11
+
12
+ if (totalImages === 0) {
13
+ showFunction(element);
14
+ return;
15
+ }
16
+
17
+ let loadedImages = 0;
18
+
19
+ function imageLoaded() {
20
+ loadedImages++;
21
+ if (loadedImages === totalImages) {
22
+ showFunction(element);
23
+ }
24
+ }
25
+
26
+ images.forEach((img) => {
27
+ if (img.complete) {
28
+ imageLoaded();
29
+ } else {
30
+ img.addEventListener("load", imageLoaded);
31
+ img.addEventListener("error", imageLoaded);
32
+ }
33
+ });
34
+ }
@@ -0,0 +1,20 @@
1
+ export const toKebabCase = (str: string) =>
2
+ str.replace(/([A-Z])/g, '-$1').toLowerCase();
3
+
4
+ export function toCamelCase(str: string): string {
5
+ return str
6
+ .toLowerCase()
7
+ .replace(/^vite_/, "")
8
+ .replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
9
+ .replace(/^[a-z]/, (match) => match.toLowerCase());
10
+ }
11
+
12
+ export function toCssCustomProperty(str: string): string {
13
+ return (
14
+ "--" +
15
+ str
16
+ .toLowerCase()
17
+ .replace(/^vite_/, "")
18
+ .replace(/_/g, "-")
19
+ );
20
+ }