@ryuzaki13/react-foundation-lib 1.4.1 → 1.4.2
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.
- package/README.md +154 -100
- package/dist/chunks/{formatters-CBgrBeSA.js → formatters-D33Uh3si.js} +7 -8
- package/dist/chunks/{formatters-CBgrBeSA.js.map → formatters-D33Uh3si.js.map} +1 -1
- package/dist/chunks/odata-service-CWkHASdw.js +2211 -0
- package/dist/chunks/odata-service-CWkHASdw.js.map +1 -0
- package/dist/chunks/{validate-BW-7BZy_.js → validate-DyzojM0w.js} +2 -2
- package/dist/chunks/{validate-BW-7BZy_.js.map → validate-DyzojM0w.js.map} +1 -1
- package/dist/error-report/index.js +2 -2
- package/dist/excel/index.js +20 -21
- package/dist/excel/index.js.map +1 -1
- package/dist/formatters/index.js +4 -5
- package/dist/{odata → odata-service}/buildSeparatedArrays.d.ts +1 -1
- package/dist/odata-service/buildSeparatedArrays.d.ts.map +1 -0
- package/dist/{odata/types.d.ts → odata-service/collectionTypes.d.ts} +1 -1
- package/dist/odata-service/collectionTypes.d.ts.map +1 -0
- package/dist/{odata → odata-service}/dateUtils.d.ts +1 -1
- package/dist/odata-service/dateUtils.d.ts.map +1 -0
- package/dist/{odata → odata-service}/findCollectionPairs.d.ts +1 -1
- package/dist/odata-service/findCollectionPairs.d.ts.map +1 -0
- package/dist/odata-service/index.d.ts +5 -0
- package/dist/odata-service/index.d.ts.map +1 -1
- package/dist/odata-service/index.js +2 -2
- package/dist/{odata/store.d.ts → odata-service/odataCollectionStore.d.ts} +1 -1
- package/dist/odata-service/odataCollectionStore.d.ts.map +1 -0
- package/dist/query-client/index.js.map +1 -1
- package/dist/query-client/queryClient.d.ts +1 -3
- package/dist/query-client/queryClient.d.ts.map +1 -1
- package/package.json +1 -5
- package/src/bounded-copy-stack/README.mdx +257 -0
- package/src/context-menu/README.mdx +326 -0
- package/src/currency/README.mdx +155 -0
- package/src/date-segments/README.mdx +319 -0
- package/src/error/README.mdx +95 -0
- package/src/error-report/README.mdx +612 -0
- package/src/excel/README.mdx +449 -0
- package/src/fingerprint/README.mdx +100 -0
- package/src/form/README.mdx +101 -0
- package/src/foundationLib.mdx +70 -54
- package/src/hooks/README.mdx +988 -0
- package/src/http/README.mdx +126 -0
- package/src/media/README.mdx +1853 -0
- package/src/notifications/README.mdx +323 -0
- package/src/number-scale/README.mdx +318 -0
- package/src/odata-service/README.mdx +2916 -0
- package/src/presets/README.mdx +136 -0
- package/src/pwa/README.mdx +394 -0
- package/src/query-client/README.mdx +2500 -0
- package/src/range-output/README.mdx +161 -0
- package/src/seo/README.mdx +279 -0
- package/src/session-storage/README.mdx +125 -0
- package/src/string-comparison/README.mdx +96 -0
- package/src/table/README.mdx +3106 -0
- package/src/three-scene/README.mdx +248 -0
- package/src/types/README.mdx +172 -0
- package/src/utils/README.mdx +1724 -0
- package/src/validators/README.mdx +239 -0
- package/src/virtualizer/README.mdx +138 -0
- package/src/xml/README.mdx +149 -0
- package/dist/chunks/dateUtils-B5V5aXB-.js +0 -847
- package/dist/chunks/dateUtils-B5V5aXB-.js.map +0 -1
- package/dist/chunks/odata-service-B6TxBSLm.js +0 -1277
- package/dist/chunks/odata-service-B6TxBSLm.js.map +0 -1
- package/dist/odata/buildSeparatedArrays.d.ts.map +0 -1
- package/dist/odata/dateUtils.d.ts.map +0 -1
- package/dist/odata/findCollectionPairs.d.ts.map +0 -1
- package/dist/odata/index.d.ts +0 -6
- package/dist/odata/index.d.ts.map +0 -1
- package/dist/odata/index.js +0 -102
- package/dist/odata/index.js.map +0 -1
- package/dist/odata/store.d.ts.map +0 -1
- package/dist/odata/types.d.ts.map +0 -1
- package/src/pwa/README.md +0 -638
- package/src/query-client/README.md +0 -565
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation Lib/Validation/Runtime Guards" />
|
|
4
|
+
|
|
5
|
+
# Runtime guards и validators через `@ryuzaki13/react-foundation-lib/validators`
|
|
6
|
+
|
|
7
|
+
Модуль содержит небольшие проверки для `unknown`, object-like значений, image extensions, positive integer, URL и Floating UI DOM reference.
|
|
8
|
+
|
|
9
|
+
> Название функции не расширяет её контракт. Например, `isSafe` проверяет только отсутствие `null`/`undefined`, а `isRecord` допускает `Date` и `Map`. Ниже указано точное поведение каждой проверки.
|
|
10
|
+
|
|
11
|
+
## Содержание
|
|
12
|
+
|
|
13
|
+
- [Установка и импорт](#установка-и-импорт)
|
|
14
|
+
- [Как работают type guards](#как-работают-type-guards)
|
|
15
|
+
- [`isSafe`](#issafe)
|
|
16
|
+
- [`isObject`, `isRecord`, `asRecord`](#isobject-isrecord-asrecord)
|
|
17
|
+
- [`isPlainObject`](#isplainobject)
|
|
18
|
+
- [`readPositiveInteger`](#readpositiveinteger)
|
|
19
|
+
- [Image extensions](#image-extensions)
|
|
20
|
+
- [`isDomReference`](#isdomreference)
|
|
21
|
+
- [`validateUrl`](#validateurl)
|
|
22
|
+
- [Security boundary](#security-boundary)
|
|
23
|
+
- [Полный API](#полный-api)
|
|
24
|
+
|
|
25
|
+
## Установка и импорт
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import {
|
|
29
|
+
asRecord,
|
|
30
|
+
IMAGE_EXTENSIONS,
|
|
31
|
+
isDomReference,
|
|
32
|
+
isImageExtension,
|
|
33
|
+
isObject,
|
|
34
|
+
isPlainObject,
|
|
35
|
+
isRecord,
|
|
36
|
+
isSafe,
|
|
37
|
+
readPositiveInteger,
|
|
38
|
+
validateUrl
|
|
39
|
+
} from "@ryuzaki13/react-foundation-lib/validators";
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`isDomReference` использует type из `@floating-ui/react`; host entrypoint должен разрешить совместимую peer dependency. Остальные функции не требуют React.
|
|
43
|
+
|
|
44
|
+
## Как работают type guards
|
|
45
|
+
|
|
46
|
+
Type guard возвращает boolean и помогает TypeScript сузить значение:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
function readCode(value: unknown): string | undefined {
|
|
50
|
+
if (!isRecord(value)) return undefined;
|
|
51
|
+
return typeof value.code === "string" ? value.code : undefined;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Guard не преобразует значение и не гарантирует поля, которые сам не проверил.
|
|
56
|
+
|
|
57
|
+
## `isSafe`
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
function isSafe<T>(value: T | null | undefined): value is T;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Возвращает `false` только для `null` и `undefined`:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
[0, "", false, null, undefined, "ok"].filter(isSafe);
|
|
67
|
+
// [0, "", false, "ok"].
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`0`, пустая строка и `false` являются безопасными в смысле этой функции. `isSafe` не проверяет XSS, SQL, URL, число, object schema или authorization.
|
|
71
|
+
|
|
72
|
+
## `isObject`, `isRecord`, `asRecord`
|
|
73
|
+
|
|
74
|
+
Три функции используют одинаковую базовую проверку: значение truthy, `typeof === "object"`, не array.
|
|
75
|
+
|
|
76
|
+
| Вход | `isObject` / `isRecord` | `asRecord` |
|
|
77
|
+
| ------------ | :---------------------: | -------------------------------------- |
|
|
78
|
+
| `{}` | `true` | тот же object |
|
|
79
|
+
| `new Date()` | `true` | тот же Date, типизированный как record |
|
|
80
|
+
| `new Map()` | `true` | тот же Map |
|
|
81
|
+
| DOM element | `true` | тот же element |
|
|
82
|
+
| `[]` | `false` | `null` |
|
|
83
|
+
| `null` | `false` | `null` |
|
|
84
|
+
| function | `false` | `null` |
|
|
85
|
+
|
|
86
|
+
`isObject` сужает до `object`, `isRecord` — до `Record<string, unknown>`, `asRecord` возвращает record или `null`.
|
|
87
|
+
|
|
88
|
+
Они не проверяют plain object и не гарантируют JSON-compatible данные.
|
|
89
|
+
|
|
90
|
+
## `isPlainObject`
|
|
91
|
+
|
|
92
|
+
Возвращает `true`, только если prototype равен `Object.prototype` или `null`:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
isPlainObject({}); // true.
|
|
96
|
+
isPlainObject(Object.create(null)); // true.
|
|
97
|
+
isPlainObject(new Date()); // false.
|
|
98
|
+
isPlainObject(new Map()); // false.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Сигнатура принимает `object`, поэтому сначала сузьте `unknown`:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
if (isObject(value) && isPlainObject(value)) {
|
|
105
|
+
// Plain record-like object.
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Прямой runtime-вызов `isPlainObject(null as never)` приведёт к ошибке `Object.getPrototypeOf`. TypeScript contract запрещает такой аргумент.
|
|
110
|
+
|
|
111
|
+
## `readPositiveInteger`
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
readPositiveInteger(12); // 12.
|
|
115
|
+
readPositiveInteger("12"); // 12.
|
|
116
|
+
readPositiveInteger(" 12 "); // 12.
|
|
117
|
+
readPositiveInteger(0); // undefined.
|
|
118
|
+
readPositiveInteger(-1); // undefined.
|
|
119
|
+
readPositiveInteger(1.5); // undefined.
|
|
120
|
+
readPositiveInteger("bad"); // undefined.
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
String преобразуется стандартным `Number(value)`, затем проверяются:
|
|
124
|
+
|
|
125
|
+
- runtime type number;
|
|
126
|
+
- `Number.isInteger`;
|
|
127
|
+
- значение строго больше `0`.
|
|
128
|
+
|
|
129
|
+
Следствия стандартного `Number`:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
readPositiveInteger("1e2"); // 100.
|
|
133
|
+
readPositiveInteger("0x10"); // 16.
|
|
134
|
+
readPositiveInteger(""); // undefined, потому что Number("") = 0.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Если формат input должен быть только decimal digits, дополнительно проверьте строковую грамматику на своей boundary.
|
|
138
|
+
|
|
139
|
+
## Image extensions
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
isImageExtension("PNG"); // true.
|
|
143
|
+
isImageExtension("svg"); // true.
|
|
144
|
+
isImageExtension("pdf"); // false.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Список:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
["png", "jpg", "jpeg", "gif", "webp", "svg"];
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Проверка регистронезависима через `toLowerCase`, но не делает trim и ожидает расширение без точки:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
isImageExtension(".png"); // false.
|
|
157
|
+
isImageExtension(" png "); // false.
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`IMAGE_EXTENSIONS` экспортирован как изменяемый array по текущему типу. Рассматривайте его как библиотечную константу и не вызывайте `push/splice`.
|
|
161
|
+
|
|
162
|
+
Расширение имени не доказывает MIME/type и безопасность содержимого. Для file/image boundary используйте `/file` и server-side validation.
|
|
163
|
+
|
|
164
|
+
## `isDomReference`
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
if (isDomReference(floatingReference)) {
|
|
168
|
+
floatingReference.focus();
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Возвращает `true`, только если reference является browser `Element`. Virtual element Floating UI и `null` возвращают `false`.
|
|
173
|
+
|
|
174
|
+
Функция выполняет `ref instanceof Element`. В server runtime без global `Element` её вызов может привести к `ReferenceError`; вызывайте только в browser boundary или после проверки DOM environment.
|
|
175
|
+
|
|
176
|
+
## `validateUrl`
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const result = validateUrl("https://example.com/path");
|
|
180
|
+
|
|
181
|
+
if (!result.isValid) {
|
|
182
|
+
showError(result.error);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Возможные результаты:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
{ isValid: true, error: "" }
|
|
190
|
+
{ isValid: false, error: "URL не может быть пустым" }
|
|
191
|
+
{ isValid: false, error: "URL слишком длинный" }
|
|
192
|
+
{ isValid: false, error: "URL содержит недопустимые символы" }
|
|
193
|
+
{ isValid: false, error: "Некорректный формат URL" }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Алгоритм:
|
|
197
|
+
|
|
198
|
+
1. Отклоняет пустую строку.
|
|
199
|
+
2. Если строка не начинается с lowercase `http`, временно добавляет `https://` для `new URL`.
|
|
200
|
+
3. Проверяет длину исходной строки не больше `2048`.
|
|
201
|
+
4. Применяет консервативное регулярное выражение.
|
|
202
|
+
|
|
203
|
+
Поддерживаемая грамматика намеренно узкая:
|
|
204
|
+
|
|
205
|
+
- optional `http://` или `https://`;
|
|
206
|
+
- domain с точкой;
|
|
207
|
+
- lowercase TLD длиной 2–6 по regex;
|
|
208
|
+
- простой path из ограниченного набора символов.
|
|
209
|
+
|
|
210
|
+
Query string, hash, port, localhost, IP-адреса, internationalized domains, многие валидные Unicode/path characters и длинные современные TLD могут быть отклонены.
|
|
211
|
+
|
|
212
|
+
## Security boundary
|
|
213
|
+
|
|
214
|
+
`validateUrl` — UI-проверка формата, не security policy. Она не:
|
|
215
|
+
|
|
216
|
+
- проверяет allowlist host/protocol;
|
|
217
|
+
- делает DNS resolution;
|
|
218
|
+
- защищает backend от SSRF;
|
|
219
|
+
- гарантирует отсутствие redirect на опасный host;
|
|
220
|
+
- проверяет доступность ресурса;
|
|
221
|
+
- санитизирует HTML;
|
|
222
|
+
- подтверждает, что URL ведёт на изображение.
|
|
223
|
+
|
|
224
|
+
Для внешней навигации/запроса применяйте отдельную security policy владельца boundary.
|
|
225
|
+
|
|
226
|
+
## Полный API
|
|
227
|
+
|
|
228
|
+
| Export | Точное назначение |
|
|
229
|
+
| --------------------- | --------------------------------------- |
|
|
230
|
+
| `isSafe` | Исключает `null`/`undefined` |
|
|
231
|
+
| `isObject` | Non-array truthy object |
|
|
232
|
+
| `isRecord` | Та же runtime-проверка с record type |
|
|
233
|
+
| `asRecord` | Record или `null` |
|
|
234
|
+
| `isPlainObject` | Проверяет prototype |
|
|
235
|
+
| `readPositiveInteger` | Positive integer number или `undefined` |
|
|
236
|
+
| `IMAGE_EXTENSIONS` | Список поддерживаемых extensions |
|
|
237
|
+
| `isImageExtension` | Регистронезависимая проверка extension |
|
|
238
|
+
| `isDomReference` | Floating reference является `Element` |
|
|
239
|
+
| `validateUrl` | Консервативный UI URL result |
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation Lib/React/Virtualizer Pagination" />
|
|
4
|
+
|
|
5
|
+
# Дозагрузка virtualized списка через `@ryuzaki13/react-foundation-lib/virtualizer`
|
|
6
|
+
|
|
7
|
+
`useFetchNextPageEffect` вызывает `fetchNextPage`, когда последняя отрендеренная virtual item достигает конца уже загруженного списка.
|
|
8
|
+
|
|
9
|
+
## Установка и импорт
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install react @tanstack/react-virtual @ryuzaki13/react-foundation-lib
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { useFetchNextPageEffect } from "@ryuzaki13/react-foundation-lib/virtualizer";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Host должен использовать совместимые peer dependencies React `>=19.2 <20` и `@tanstack/react-virtual >=3.14 <4`.
|
|
20
|
+
|
|
21
|
+
## Назначение и границы
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
virtualItems (видимый диапазон)
|
|
25
|
+
currentItemsCount (сколько уже загружено)
|
|
26
|
+
hasNextPage
|
|
27
|
+
│
|
|
28
|
+
последний virtual index >= currentItemsCount - 1
|
|
29
|
+
│
|
|
30
|
+
▼
|
|
31
|
+
fetchNextPage() один раз до завершения Promise
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Hook не создаёт virtualizer, не хранит backend data, не строит query key и не решает server pagination. TanStack Query должен оставаться владельцем server snapshot.
|
|
35
|
+
|
|
36
|
+
## Сигнатура
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
useFetchNextPageEffect({
|
|
40
|
+
virtualItems,
|
|
41
|
+
currentItemsCount,
|
|
42
|
+
fetchNextPage,
|
|
43
|
+
hasNextPage
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Поле | Значение |
|
|
48
|
+
| ------------------- | ------------------------------------ |
|
|
49
|
+
| `virtualItems` | Текущий массив `VirtualItem[]` |
|
|
50
|
+
| `currentItemsCount` | Число реально загруженных data items |
|
|
51
|
+
| `hasNextPage` | Есть ли следующая server page |
|
|
52
|
+
| `fetchNextPage` | Stable функция, возвращающая Promise |
|
|
53
|
+
|
|
54
|
+
Hook ничего не возвращает.
|
|
55
|
+
|
|
56
|
+
## Пример с TanStack Query/Virtual
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
const rows = query.data?.pages.flatMap((page) => page.rows) ?? [];
|
|
60
|
+
const virtualItems = virtualizer.getVirtualItems();
|
|
61
|
+
|
|
62
|
+
useFetchNextPageEffect({
|
|
63
|
+
virtualItems,
|
|
64
|
+
currentItemsCount: rows.length,
|
|
65
|
+
hasNextPage: query.hasNextPage,
|
|
66
|
+
fetchNextPage: async () => {
|
|
67
|
+
await query.fetchNextPage();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Передавайте количество загруженных строк, а не virtual total с loader row, group headers или estimated backend count.
|
|
73
|
+
|
|
74
|
+
## Точный trigger
|
|
75
|
+
|
|
76
|
+
Fetch не запускается, если:
|
|
77
|
+
|
|
78
|
+
- `virtualItems` пуст;
|
|
79
|
+
- `currentItemsCount` равен `0`;
|
|
80
|
+
- последняя virtual item ещё не достигла `currentItemsCount - 1`;
|
|
81
|
+
- `hasNextPage === false`;
|
|
82
|
+
- предыдущий вызов ещё не завершился.
|
|
83
|
+
|
|
84
|
+
Trigger происходит строго на последнем загруженном индексе. Preload threshold «за N строк до конца» в API отсутствует.
|
|
85
|
+
|
|
86
|
+
## Защита от параллельного fetch
|
|
87
|
+
|
|
88
|
+
Внутренний `useRef(false)` устанавливается до вызова и сбрасывается в `.finally()`:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
idle ── trigger ──► fetchingRef=true
|
|
92
|
+
│
|
|
93
|
+
Promise pending
|
|
94
|
+
│
|
|
95
|
+
resolve или reject
|
|
96
|
+
▼
|
|
97
|
+
fetchingRef=false
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Защита действует только внутри одного mounted hook instance. TanStack Query также должен владеть дедупликацией/network state.
|
|
101
|
+
|
|
102
|
+
Если `fetchNextPage` отклоняет Promise, hook не добавляет `.catch`; rejection может стать unhandled. Передавайте функцию с принятой error policy или wrapper, который корректно обрабатывает ожидаемую ошибку.
|
|
103
|
+
|
|
104
|
+
## Referential stability
|
|
105
|
+
|
|
106
|
+
Effect зависит от `virtualItems`, `currentItemsCount`, `hasNextPage`, `fetchNextPage`.
|
|
107
|
+
|
|
108
|
+
- Используйте значение virtualizer как предусмотрено его API.
|
|
109
|
+
- Не создавайте лишний async callback с новой identity без причины.
|
|
110
|
+
- React Compiler не отменяет необходимость stable external contract.
|
|
111
|
+
- После увеличения `currentItemsCount` новый fetch произойдёт только когда virtual range достигнет нового конца.
|
|
112
|
+
|
|
113
|
+
## Browser, SSR и lifecycle
|
|
114
|
+
|
|
115
|
+
Сам hook не читает `window`, но является React effect и не выполняет fetch во время server render. В browser effect запускается после commit.
|
|
116
|
+
|
|
117
|
+
При unmount Promise не отменяется. Abort/cancel должен быть реализован query/transport owner. `fetchingRef` больше не влияет после удаления hook instance.
|
|
118
|
+
|
|
119
|
+
## Типичные ошибки
|
|
120
|
+
|
|
121
|
+
- Передать total backend rows вместо количества загруженного массива.
|
|
122
|
+
- Включить client-side sorting/filtering одной страницы при server-side модели.
|
|
123
|
+
- Добавить фиктивный loader row в `currentItemsCount`.
|
|
124
|
+
- Ожидать preload заранее — trigger находится на самом конце.
|
|
125
|
+
- Передать rejecting Promise без error handling.
|
|
126
|
+
- Выполнять fetch прямо в row render вместо effect.
|
|
127
|
+
- Использовать изменяемый index как domain identity строки.
|
|
128
|
+
|
|
129
|
+
## API
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
function useFetchNextPageEffect(args: {
|
|
133
|
+
virtualItems: VirtualItem[];
|
|
134
|
+
currentItemsCount: number;
|
|
135
|
+
hasNextPage: boolean;
|
|
136
|
+
fetchNextPage: () => Promise<unknown>;
|
|
137
|
+
}): void;
|
|
138
|
+
```
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation Lib/Data/XML" />
|
|
4
|
+
|
|
5
|
+
# Простые XML helpers через `@ryuzaki13/react-foundation-lib/xml`
|
|
6
|
+
|
|
7
|
+
Модуль экранирует XML-текст, строит последовательность простых полей и читает текст первого тега через `DOMParser`.
|
|
8
|
+
|
|
9
|
+
## Содержание
|
|
10
|
+
|
|
11
|
+
- [Назначение и границы](#назначение-и-границы)
|
|
12
|
+
- [Установка и импорт](#установка-и-импорт)
|
|
13
|
+
- [`escapeXmlValue`](#escapexmlvalue)
|
|
14
|
+
- [`buildXmlFields`](#buildxmlfields)
|
|
15
|
+
- [`getXmlTagText`](#getxmltagtext)
|
|
16
|
+
- [Безопасность и ограничения](#безопасность-и-ограничения)
|
|
17
|
+
- [API](#api)
|
|
18
|
+
|
|
19
|
+
## Назначение и границы
|
|
20
|
+
|
|
21
|
+
Подходит для небольших flat XML fragments:
|
|
22
|
+
|
|
23
|
+
```xml
|
|
24
|
+
<ORDER_ID>42</ORDER_ID><COMMENT>A&B</COMMENT>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Модуль не является полноценным XML builder/parser:
|
|
28
|
+
|
|
29
|
+
- не строит document/envelope;
|
|
30
|
+
- не управляет namespaces и attributes;
|
|
31
|
+
- не валидирует XSD;
|
|
32
|
+
- не обрабатывает коллекции/вложенные объекты;
|
|
33
|
+
- не проверяет parser errors;
|
|
34
|
+
- не выполняет network request.
|
|
35
|
+
|
|
36
|
+
## Установка и импорт
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { buildXmlFields, escapeXmlValue, getXmlTagText } from "@ryuzaki13/react-foundation-lib/xml";
|
|
40
|
+
|
|
41
|
+
import type { XmlPrimitive } from "@ryuzaki13/react-foundation-lib/xml";
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Builder-функции не требуют DOM. `getXmlTagText` требует global `DOMParser`, доступный в браузере/jsdom, но обычно отсутствующий в чистом Node.js.
|
|
45
|
+
|
|
46
|
+
## `escapeXmlValue`
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
escapeXmlValue(`a&b<c>d"e'f`);
|
|
50
|
+
// "a&b<c>d"e'f"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Тип входа:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
type XmlPrimitive = string | number | null | undefined;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Правила:
|
|
60
|
+
|
|
61
|
+
| Символ | XML entity |
|
|
62
|
+
| ------ | ---------- |
|
|
63
|
+
| `&` | `&` |
|
|
64
|
+
| `<` | `<` |
|
|
65
|
+
| `>` | `>` |
|
|
66
|
+
| `"` | `"` |
|
|
67
|
+
| `'` | `'` |
|
|
68
|
+
|
|
69
|
+
`null` и `undefined` превращаются в пустую строку. Number сначала преобразуется через `String`.
|
|
70
|
+
|
|
71
|
+
Функция предназначена для текстового содержимого. Для attribute value она экранирует основные символы, но сам attribute builder модуль не предоставляет.
|
|
72
|
+
|
|
73
|
+
Не передавайте уже экранированную строку повторно:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
escapeXmlValue("A&B"); // "A&amp;B".
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## `buildXmlFields`
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const xml = buildXmlFields({
|
|
83
|
+
ORDER_ID: 42,
|
|
84
|
+
COMMENT: "A&B",
|
|
85
|
+
OPTIONAL: null
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// <ORDER_ID>42</ORDER_ID><COMMENT>A&B</COMMENT><OPTIONAL></OPTIONAL>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Функция:
|
|
92
|
+
|
|
93
|
+
- обходит `Object.entries` в стандартном порядке ключей;
|
|
94
|
+
- строит для каждого поля `<NAME>escaped value</NAME>`;
|
|
95
|
+
- не добавляет переносы строк;
|
|
96
|
+
- сохраняет поля с `null`/`undefined` как пустые теги;
|
|
97
|
+
- не изменяет исходный объект.
|
|
98
|
+
|
|
99
|
+
Имя тега вставляется без валидации и escaping. Ключи должны быть доверенными валидными XML names из программного contract:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// Нельзя строить key из пользовательского ввода.
|
|
103
|
+
buildXmlFields({ [userProvidedTagName]: value });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Для вложенной структуры передайте уже собранный fragment как часть внешнего builder-а; `buildXmlFields` сам воспринимает строку как текст и экранирует markup.
|
|
107
|
+
|
|
108
|
+
## `getXmlTagText`
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const message = getXmlTagText("<root><EX_MESSAGE> Ошибка </EX_MESSAGE></root>", "EX_MESSAGE");
|
|
112
|
+
|
|
113
|
+
console.log(message); // "Ошибка".
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Алгоритм:
|
|
117
|
+
|
|
118
|
+
1. Парсит всю строку как `application/xml`.
|
|
119
|
+
2. Вызывает `getElementsByTagName(tagName)`.
|
|
120
|
+
3. Берёт только первый найденный element.
|
|
121
|
+
4. Читает объединённый `textContent`.
|
|
122
|
+
5. Обрезает пробелы.
|
|
123
|
+
6. Возвращает `undefined`, если элемента/непустого текста нет.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
getXmlTagText("<root />", "MESSAGE"); // undefined.
|
|
127
|
+
getXmlTagText("<root><MESSAGE> </MESSAGE></root>", "MESSAGE"); // undefined.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Для namespace-sensitive XML точное поведение `getElementsByTagName` зависит от переданного имени. Модуль не предоставляет `getElementsByTagNameNS`.
|
|
131
|
+
|
|
132
|
+
## Безопасность и ограничения
|
|
133
|
+
|
|
134
|
+
- Не вставляйте результат XML в HTML DOM через `innerHTML`.
|
|
135
|
+
- Не используйте dynamic tag names из недоверенного ввода.
|
|
136
|
+
- Parser не проверяется на `<parsererror>`; malformed XML обычно даёт `undefined` или parser-specific tree, а не явное исключение этого helper.
|
|
137
|
+
- Читается только первое совпадение.
|
|
138
|
+
- Большие documents лучше разбирать специализированным parser/streaming boundary.
|
|
139
|
+
- Модуль не предотвращает XML signature wrapping и не предназначен для security validation.
|
|
140
|
+
- `XmlPrimitive` number допускает `NaN`/`Infinity` на уровне TypeScript; они станут строками `"NaN"`/`"Infinity"`.
|
|
141
|
+
|
|
142
|
+
## API
|
|
143
|
+
|
|
144
|
+
| Export | Результат |
|
|
145
|
+
| ---------------- | --------------------------------------- |
|
|
146
|
+
| `escapeXmlValue` | Экранированная строка |
|
|
147
|
+
| `buildXmlFields` | Компактный XML fragment |
|
|
148
|
+
| `getXmlTagText` | `string \| undefined` |
|
|
149
|
+
| `XmlPrimitive` | `string \| number \| null \| undefined` |
|