@lucasbernardosn/ts-utils 0.3.0 → 0.5.0

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 CHANGED
@@ -24,6 +24,7 @@ import { getInitials, isDeepEqual, type DeepPartial } from '@lucasbernardosn/ts-
24
24
  | ------ | -------- |
25
25
  | `composeEventHandlers` | Junta vários handlers num só, chamados em sequência com os mesmos argumentos; os `undefined` são ignorados |
26
26
  | `createSplitProps` | Separa um objeto de props em dois pela lista de chaves, que precisa cobrir todas as chaves do tipo pedido |
27
+ | `getHighlightChunks` | Divide um texto em trechos que casam ou não com a busca (sem acento nem caixa), no formato `{ text, match }`, para destacar o que casou |
27
28
  | `getInitials` | As iniciais de um nome, em maiúsculas: a do primeiro e a do último nome |
28
29
  | `isDeepEqual` | Igualdade estrutural, inclusive de `Map`, `Set`, `Date`, `RegExp` e arrays tipados |
29
30
  | `isNotNullish` | Guard que tira `null` e `undefined` do tipo, para usar no `filter` |
package/dist/index.d.ts CHANGED
@@ -21,9 +21,10 @@ import { UnknownArray, UnknownFunction, UnknownLooseValue, UnknownObject, Unknow
21
21
  import { ValueOf } from "./types/value-of.js";
22
22
  import { composeEventHandlers } from "./utils/compose-event-handlers.js";
23
23
  import { createSplitProps } from "./utils/create-split-props.js";
24
+ import { GetHighlightChunksOptions, HighlightChunk, getHighlightChunks } from "./utils/get-highlight-chunks.js";
24
25
  import { getInitials } from "./utils/get-initials.js";
25
26
  import { isDeepEqual } from "./utils/is-deep-equal.js";
26
27
  import { isNotNullish } from "./utils/is-not-nullish.js";
27
28
  import { mergeBooleansAnd, mergeBooleansOr } from "./utils/merge-booleans.js";
28
29
  import { PascalCase, toPascalCase } from "./utils/to-pascal-case.js";
29
- export { AnyArray, AnyFunction, AnyLooseValue, AnyObject, AnyType, ArrayElement, DeepKeys, DeepKeysFlat, DeepPartial, DeepReadonly, DeepValue, EnsureString, KeysOfType, MergeOverride, NonEmptyArray, Nullish, OmitByValue, Optional, PascalCase, PickByValue, Recompose, RequiredExcept, SerializableValue, StrictEmptyObject, UnknownArray, UnknownFunction, UnknownLooseValue, UnknownObject, UnknownType, ValueOf, composeEventHandlers, createSplitProps, getInitials, isDeepEqual, isNotNullish, mergeBooleansAnd, mergeBooleansOr, sleep, toPascalCase };
30
+ export { AnyArray, AnyFunction, AnyLooseValue, AnyObject, AnyType, ArrayElement, DeepKeys, DeepKeysFlat, DeepPartial, DeepReadonly, DeepValue, EnsureString, GetHighlightChunksOptions, HighlightChunk, KeysOfType, MergeOverride, NonEmptyArray, Nullish, OmitByValue, Optional, PascalCase, PickByValue, Recompose, RequiredExcept, SerializableValue, StrictEmptyObject, UnknownArray, UnknownFunction, UnknownLooseValue, UnknownObject, UnknownType, ValueOf, composeEventHandlers, createSplitProps, getHighlightChunks, getInitials, isDeepEqual, isNotNullish, mergeBooleansAnd, mergeBooleansOr, sleep, toPascalCase };
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { sleep } from "./dev/sleep.js";
2
2
  import { composeEventHandlers } from "./utils/compose-event-handlers.js";
3
3
  import { createSplitProps } from "./utils/create-split-props.js";
4
+ import { getHighlightChunks } from "./utils/get-highlight-chunks.js";
4
5
  import { getInitials } from "./utils/get-initials.js";
5
6
  import { isDeepEqual } from "./utils/is-deep-equal.js";
6
7
  import { isNotNullish } from "./utils/is-not-nullish.js";
7
8
  import { mergeBooleansAnd, mergeBooleansOr } from "./utils/merge-booleans.js";
8
9
  import { toPascalCase } from "./utils/to-pascal-case.js";
9
- export { composeEventHandlers, createSplitProps, getInitials, isDeepEqual, isNotNullish, mergeBooleansAnd, mergeBooleansOr, sleep, toPascalCase };
10
+ export { composeEventHandlers, createSplitProps, getHighlightChunks, getInitials, isDeepEqual, isNotNullish, mergeBooleansAnd, mergeBooleansOr, sleep, toPascalCase };
@@ -0,0 +1,38 @@
1
+ //#region src/utils/get-highlight-chunks.d.ts
2
+ /**
3
+ * Trecho de um texto: `match` diz se ele casou com a busca.
4
+ */
5
+ export type HighlightChunk = {
6
+ text: string;
7
+ match: boolean;
8
+ };
9
+ export type GetHighlightChunksOptions = {
10
+ /** Texto original, que é fatiado em trechos. */
11
+ text: string;
12
+ /** Um termo ou uma lista de termos. Cada um é buscado inteiro, como um trecho contínuo. */
13
+ query: string | readonly string[];
14
+ };
15
+ /**
16
+ * Divide o texto em trechos que casam ou não com a busca, para destacar os que casam.
17
+ *
18
+ * A regra é a do filtro do `Command` e do `useFilter` do Base UI: a busca inteira é um trecho
19
+ * contínuo (não é quebrada em palavras), sem diferenciar acento nem caixa. Os trechos saem do texto
20
+ * original, com o acento e a caixa que ele tem: `comissao` em `Comissão de vendas` marca
21
+ * `Comissão`. Comparar o texto cru marcaria nada, e normalizar o texto antes de fatiar devolveria
22
+ * o texto sem acento.
23
+ *
24
+ * Com uma lista, as ocorrências de todos os termos são juntas, e as que se sobrepõem ou encostam
25
+ * viram um trecho só. Busca vazia (ou só espaços) devolve o texto inteiro, sem destaque. Não há
26
+ * trecho vazio, e trechos vizinhos nunca têm o mesmo `match`. O formato é o do
27
+ * `@zag-js/highlight-word`.
28
+ *
29
+ * @example
30
+ * getHighlightChunks({ text: 'Comissão de vendas', query: 'comissao' })
31
+ * // [{ text: 'Comissão', match: true }, { text: ' de vendas', match: false }]
32
+ */
33
+ export declare const getHighlightChunks: (options: GetHighlightChunksOptions) => {
34
+ text: string;
35
+ match: boolean;
36
+ }[];
37
+ //#endregion
38
+ //# sourceMappingURL=get-highlight-chunks.d.ts.map
@@ -0,0 +1,107 @@
1
+ //#region src/utils/get-highlight-chunks.ts
2
+ const COMBINING_MARKS = /\p{M}/gu;
3
+ const normalizeCharacter = (character) => character.normalize("NFD").replace(COMBINING_MARKS, "").toLocaleLowerCase("pt-BR");
4
+ /**
5
+ * Normaliza o texto caractere a caractere e guarda, para cada posição do texto normalizado, onde o
6
+ * caractere de origem começa e termina no texto original.
7
+ */
8
+ const normalizeWithMap = (text) => {
9
+ const starts = [];
10
+ const ends = [];
11
+ let normalized = "";
12
+ let index = 0;
13
+ for (const character of text) {
14
+ const end = index + character.length;
15
+ const normalizedCharacter = normalizeCharacter(character);
16
+ if (normalizedCharacter === "") {
17
+ const previousStart = starts.at(-1);
18
+ for (let position = ends.length - 1; position >= 0; position -= 1) {
19
+ if (starts[position] !== previousStart) break;
20
+ ends[position] = end;
21
+ }
22
+ } else {
23
+ /** Uma entrada por unidade UTF-16, porque o `indexOf` devolve posições assim. */
24
+ starts.push(...new Array(normalizedCharacter.length).fill(index));
25
+ ends.push(...new Array(normalizedCharacter.length).fill(end));
26
+ normalized += normalizedCharacter;
27
+ }
28
+ index = end;
29
+ }
30
+ return {
31
+ normalized,
32
+ starts,
33
+ ends
34
+ };
35
+ };
36
+ const normalizeQuery = (term) => Array.from(term.trim(), normalizeCharacter).join("");
37
+ /**
38
+ * Divide o texto em trechos que casam ou não com a busca, para destacar os que casam.
39
+ *
40
+ * A regra é a do filtro do `Command` e do `useFilter` do Base UI: a busca inteira é um trecho
41
+ * contínuo (não é quebrada em palavras), sem diferenciar acento nem caixa. Os trechos saem do texto
42
+ * original, com o acento e a caixa que ele tem: `comissao` em `Comissão de vendas` marca
43
+ * `Comissão`. Comparar o texto cru marcaria nada, e normalizar o texto antes de fatiar devolveria
44
+ * o texto sem acento.
45
+ *
46
+ * Com uma lista, as ocorrências de todos os termos são juntas, e as que se sobrepõem ou encostam
47
+ * viram um trecho só. Busca vazia (ou só espaços) devolve o texto inteiro, sem destaque. Não há
48
+ * trecho vazio, e trechos vizinhos nunca têm o mesmo `match`. O formato é o do
49
+ * `@zag-js/highlight-word`.
50
+ *
51
+ * @example
52
+ * getHighlightChunks({ text: 'Comissão de vendas', query: 'comissao' })
53
+ * // [{ text: 'Comissão', match: true }, { text: ' de vendas', match: false }]
54
+ */
55
+ const getHighlightChunks = (options) => {
56
+ const { text, query } = options;
57
+ const terms = (typeof query === "string" ? [query] : query).map(normalizeQuery).filter((term) => term !== "");
58
+ if (text === "" || terms.length === 0) return text === "" ? [] : [{
59
+ text,
60
+ match: false
61
+ }];
62
+ const { normalized, starts, ends } = normalizeWithMap(text);
63
+ const ranges = [];
64
+ for (const term of terms) {
65
+ let from = 0;
66
+ while (from <= normalized.length - term.length) {
67
+ const found = normalized.indexOf(term, from);
68
+ if (found === -1) break;
69
+ const start = starts[found];
70
+ const end = ends[found + term.length - 1];
71
+ if (start !== void 0 && end !== void 0) ranges.push({
72
+ start,
73
+ end
74
+ });
75
+ from = found + term.length;
76
+ }
77
+ }
78
+ ranges.sort((a, b) => a.start - b.start || a.end - b.end);
79
+ const merged = [];
80
+ for (const range of ranges) {
81
+ const last = merged.at(-1);
82
+ if (last !== void 0 && range.start <= last.end) last.end = Math.max(last.end, range.end);
83
+ else merged.push({ ...range });
84
+ }
85
+ const chunks = [];
86
+ let cursor = 0;
87
+ for (const range of merged) {
88
+ if (range.start > cursor) chunks.push({
89
+ text: text.slice(cursor, range.start),
90
+ match: false
91
+ });
92
+ chunks.push({
93
+ text: text.slice(range.start, range.end),
94
+ match: true
95
+ });
96
+ cursor = range.end;
97
+ }
98
+ if (cursor < text.length) chunks.push({
99
+ text: text.slice(cursor),
100
+ match: false
101
+ });
102
+ return chunks;
103
+ };
104
+ //#endregion
105
+ export { getHighlightChunks };
106
+
107
+ //# sourceMappingURL=get-highlight-chunks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-highlight-chunks.js","names":[],"sources":["../../src/utils/get-highlight-chunks.ts"],"sourcesContent":["/**\n * Trecho de um texto: `match` diz se ele casou com a busca.\n */\nexport type HighlightChunk = {\n text: string;\n match: boolean;\n};\n\nexport type GetHighlightChunksOptions = {\n /** Texto original, que é fatiado em trechos. */\n text: string;\n /** Um termo ou uma lista de termos. Cada um é buscado inteiro, como um trecho contínuo. */\n query: string | readonly string[];\n};\n\ntype Range = { start: number; end: number };\n\nconst COMBINING_MARKS = /\\p{M}/gu;\n\nconst normalizeCharacter = (character: string) =>\n character\n .normalize('NFD')\n .replace(COMBINING_MARKS, '')\n .toLocaleLowerCase('pt-BR');\n\n/**\n * Normaliza o texto caractere a caractere e guarda, para cada posição do texto normalizado, onde o\n * caractere de origem começa e termina no texto original.\n */\nconst normalizeWithMap = (text: string) => {\n const starts: number[] = [];\n const ends: number[] = [];\n let normalized = '';\n let index = 0;\n\n for (const character of text) {\n const end = index + character.length;\n const normalizedCharacter = normalizeCharacter(character);\n\n if (normalizedCharacter === '') {\n // Marca combinante solta (texto em NFD): gruda no caractere anterior.\n const previousStart = starts.at(-1);\n\n for (let position = ends.length - 1; position >= 0; position -= 1) {\n if (starts[position] !== previousStart) {\n break;\n }\n\n ends[position] = end;\n }\n } else {\n /** Uma entrada por unidade UTF-16, porque o `indexOf` devolve posições assim. */\n starts.push(...new Array<number>(normalizedCharacter.length).fill(index));\n ends.push(...new Array<number>(normalizedCharacter.length).fill(end));\n normalized += normalizedCharacter;\n }\n\n index = end;\n }\n\n return { normalized, starts, ends };\n};\n\nconst normalizeQuery = (term: string) =>\n Array.from(term.trim(), normalizeCharacter).join('');\n\n/**\n * Divide o texto em trechos que casam ou não com a busca, para destacar os que casam.\n *\n * A regra é a do filtro do `Command` e do `useFilter` do Base UI: a busca inteira é um trecho\n * contínuo (não é quebrada em palavras), sem diferenciar acento nem caixa. Os trechos saem do texto\n * original, com o acento e a caixa que ele tem: `comissao` em `Comissão de vendas` marca\n * `Comissão`. Comparar o texto cru marcaria nada, e normalizar o texto antes de fatiar devolveria\n * o texto sem acento.\n *\n * Com uma lista, as ocorrências de todos os termos são juntas, e as que se sobrepõem ou encostam\n * viram um trecho só. Busca vazia (ou só espaços) devolve o texto inteiro, sem destaque. Não há\n * trecho vazio, e trechos vizinhos nunca têm o mesmo `match`. O formato é o do\n * `@zag-js/highlight-word`.\n *\n * @example\n * getHighlightChunks({ text: 'Comissão de vendas', query: 'comissao' })\n * // [{ text: 'Comissão', match: true }, { text: ' de vendas', match: false }]\n */\nexport const getHighlightChunks = (options: GetHighlightChunksOptions) => {\n const { text, query } = options;\n\n const terms = (typeof query === 'string' ? [query] : query)\n .map(normalizeQuery)\n .filter((term) => term !== '');\n\n if (text === '' || terms.length === 0) {\n return text === '' ? [] : [{ text, match: false }];\n }\n\n const { normalized, starts, ends } = normalizeWithMap(text);\n\n const ranges: Range[] = [];\n\n for (const term of terms) {\n let from = 0;\n\n while (from <= normalized.length - term.length) {\n const found = normalized.indexOf(term, from);\n\n if (found === -1) {\n break;\n }\n\n const start = starts[found];\n const end = ends[found + term.length - 1];\n\n if (start !== undefined && end !== undefined) {\n ranges.push({ start, end });\n }\n\n from = found + term.length;\n }\n }\n\n ranges.sort((a, b) => a.start - b.start || a.end - b.end);\n\n const merged: Range[] = [];\n\n for (const range of ranges) {\n const last = merged.at(-1);\n\n if (last !== undefined && range.start <= last.end) {\n last.end = Math.max(last.end, range.end);\n } else {\n merged.push({ ...range });\n }\n }\n\n const chunks: HighlightChunk[] = [];\n let cursor = 0;\n\n for (const range of merged) {\n if (range.start > cursor) {\n chunks.push({ text: text.slice(cursor, range.start), match: false });\n }\n\n chunks.push({ text: text.slice(range.start, range.end), match: true });\n cursor = range.end;\n }\n\n if (cursor < text.length) {\n chunks.push({ text: text.slice(cursor), match: false });\n }\n\n return chunks;\n};\n"],"mappings":";AAiBA,MAAM,kBAAkB;AAExB,MAAM,sBAAsB,cAC1B,UACG,UAAU,KAAK,CAAC,CAChB,QAAQ,iBAAiB,EAAE,CAAC,CAC5B,kBAAkB,OAAO;;;;;AAM9B,MAAM,oBAAoB,SAAiB;CACzC,MAAM,SAAmB,CAAC;CAC1B,MAAM,OAAiB,CAAC;CACxB,IAAI,aAAa;CACjB,IAAI,QAAQ;CAEZ,KAAK,MAAM,aAAa,MAAM;EAC5B,MAAM,MAAM,QAAQ,UAAU;EAC9B,MAAM,sBAAsB,mBAAmB,SAAS;EAExD,IAAI,wBAAwB,IAAI;GAE9B,MAAM,gBAAgB,OAAO,GAAG,EAAE;GAElC,KAAK,IAAI,WAAW,KAAK,SAAS,GAAG,YAAY,GAAG,YAAY,GAAG;IACjE,IAAI,OAAO,cAAc,eACvB;IAGF,KAAK,YAAY;GACnB;EACF,OAAO;;GAEL,OAAO,KAAK,GAAG,IAAI,MAAc,oBAAoB,MAAM,CAAC,CAAC,KAAK,KAAK,CAAC;GACxE,KAAK,KAAK,GAAG,IAAI,MAAc,oBAAoB,MAAM,CAAC,CAAC,KAAK,GAAG,CAAC;GACpE,cAAc;EAChB;EAEA,QAAQ;CACV;CAEA,OAAO;EAAE;EAAY;EAAQ;CAAK;AACpC;AAEA,MAAM,kBAAkB,SACtB,MAAM,KAAK,KAAK,KAAK,GAAG,kBAAkB,CAAC,CAAC,KAAK,EAAE;;;;;;;;;;;;;;;;;;;AAoBrD,MAAa,sBAAsB,YAAuC;CACxE,MAAM,EAAE,MAAM,UAAU;CAExB,MAAM,SAAS,OAAO,UAAU,WAAW,CAAC,KAAK,IAAI,MAAA,CAClD,IAAI,cAAc,CAAC,CACnB,QAAQ,SAAS,SAAS,EAAE;CAE/B,IAAI,SAAS,MAAM,MAAM,WAAW,GAClC,OAAO,SAAS,KAAK,CAAC,IAAI,CAAC;EAAE;EAAM,OAAO;CAAM,CAAC;CAGnD,MAAM,EAAE,YAAY,QAAQ,SAAS,iBAAiB,IAAI;CAE1D,MAAM,SAAkB,CAAC;CAEzB,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,OAAO;EAEX,OAAO,QAAQ,WAAW,SAAS,KAAK,QAAQ;GAC9C,MAAM,QAAQ,WAAW,QAAQ,MAAM,IAAI;GAE3C,IAAI,UAAU,IACZ;GAGF,MAAM,QAAQ,OAAO;GACrB,MAAM,MAAM,KAAK,QAAQ,KAAK,SAAS;GAEvC,IAAI,UAAU,KAAA,KAAa,QAAQ,KAAA,GACjC,OAAO,KAAK;IAAE;IAAO;GAAI,CAAC;GAG5B,OAAO,QAAQ,KAAK;EACtB;CACF;CAEA,OAAO,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG;CAExD,MAAM,SAAkB,CAAC;CAEzB,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,OAAO,OAAO,GAAG,EAAE;EAEzB,IAAI,SAAS,KAAA,KAAa,MAAM,SAAS,KAAK,KAC5C,KAAK,MAAM,KAAK,IAAI,KAAK,KAAK,MAAM,GAAG;OAEvC,OAAO,KAAK,EAAE,GAAG,MAAM,CAAC;CAE5B;CAEA,MAAM,SAA2B,CAAC;CAClC,IAAI,SAAS;CAEb,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,MAAM,QAAQ,QAChB,OAAO,KAAK;GAAE,MAAM,KAAK,MAAM,QAAQ,MAAM,KAAK;GAAG,OAAO;EAAM,CAAC;EAGrE,OAAO,KAAK;GAAE,MAAM,KAAK,MAAM,MAAM,OAAO,MAAM,GAAG;GAAG,OAAO;EAAK,CAAC;EACrE,SAAS,MAAM;CACjB;CAEA,IAAI,SAAS,KAAK,QAChB,OAAO,KAAK;EAAE,MAAM,KAAK,MAAM,MAAM;EAAG,OAAO;CAAM,CAAC;CAGxD,OAAO;AACT"}
@@ -1,8 +1,9 @@
1
1
  import { composeEventHandlers } from "./compose-event-handlers.js";
2
2
  import { createSplitProps } from "./create-split-props.js";
3
+ import { GetHighlightChunksOptions, HighlightChunk, getHighlightChunks } from "./get-highlight-chunks.js";
3
4
  import { getInitials } from "./get-initials.js";
4
5
  import { isDeepEqual } from "./is-deep-equal.js";
5
6
  import { isNotNullish } from "./is-not-nullish.js";
6
7
  import { mergeBooleansAnd, mergeBooleansOr } from "./merge-booleans.js";
7
8
  import { PascalCase, toPascalCase } from "./to-pascal-case.js";
8
- export { PascalCase, composeEventHandlers, createSplitProps, getInitials, isDeepEqual, isNotNullish, mergeBooleansAnd, mergeBooleansOr, toPascalCase };
9
+ export { GetHighlightChunksOptions, HighlightChunk, PascalCase, composeEventHandlers, createSplitProps, getHighlightChunks, getInitials, isDeepEqual, isNotNullish, mergeBooleansAnd, mergeBooleansOr, toPascalCase };
@@ -1,8 +1,9 @@
1
1
  import { composeEventHandlers } from "./compose-event-handlers.js";
2
2
  import { createSplitProps } from "./create-split-props.js";
3
+ import { getHighlightChunks } from "./get-highlight-chunks.js";
3
4
  import { getInitials } from "./get-initials.js";
4
5
  import { isDeepEqual } from "./is-deep-equal.js";
5
6
  import { isNotNullish } from "./is-not-nullish.js";
6
7
  import { mergeBooleansAnd, mergeBooleansOr } from "./merge-booleans.js";
7
8
  import { toPascalCase } from "./to-pascal-case.js";
8
- export { composeEventHandlers, createSplitProps, getInitials, isDeepEqual, isNotNullish, mergeBooleansAnd, mergeBooleansOr, toPascalCase };
9
+ export { composeEventHandlers, createSplitProps, getHighlightChunks, getInitials, isDeepEqual, isNotNullish, mergeBooleansAnd, mergeBooleansOr, toPascalCase };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lucasbernardosn/ts-utils",
3
- "version": "0.3.0",
4
- "description": "TypeScript utilities and type helpers, framework-agnostic.",
3
+ "version": "0.5.0",
4
+ "description": "Funções e helpers de tipo em TypeScript puro, sem React e sem dependências.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
@@ -22,6 +22,11 @@
22
22
  "dist"
23
23
  ],
24
24
  "sideEffects": false,
25
+ "keywords": [
26
+ "typescript",
27
+ "utils",
28
+ "type-helpers"
29
+ ],
25
30
  "license": "MIT",
26
31
  "publishConfig": {
27
32
  "access": "public"