shelving 1.27.0 → 1.30.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.
package/api/Resource.js CHANGED
@@ -1,4 +1,4 @@
1
- import { UNDEFINED, validate } from "../util/index.js";
1
+ import { GET_UNDEFINED, validate } from "../util/index.js";
2
2
  import { Feedback } from "../feedback/index.js";
3
3
  import { ResourceValidationError } from "./errors.js";
4
4
  /**
@@ -37,6 +37,6 @@ export class Resource {
37
37
  }
38
38
  }
39
39
  }
40
- export function RESOURCE(payload = UNDEFINED, result = UNDEFINED) {
40
+ export function RESOURCE(payload = GET_UNDEFINED, result = GET_UNDEFINED) {
41
41
  return new Resource(payload, result);
42
42
  }
package/db/Database.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { Entry, Observable, Observer, Result, Unsubscriber, ResultsMap, Validatable, Validator, Key, Data, Results, Datas, Validators, ValidatorType, Dispatcher } from "../util/index.js";
1
+ import { Entry, Observable, Observer, Result, Unsubscriber, ResultsMap, Validatable, Validator, Key, Data, Results, Datas, Validators, ValidatorType, Dispatcher, Nullish } from "../util/index.js";
2
2
  import { PropUpdates, Update } from "../update/index.js";
3
3
  import type { Provider } from "../provider/Provider.js";
4
4
  import { Filters, Sorts, Query } from "../query/index.js";
5
- import { DocumentDelete, DocumentSet, DocumentUpdate, Write, Writes } from "./Write.js";
5
+ import { DocumentDelete, DocumentSet, DocumentUpdate, Write } from "./Write.js";
6
6
  /**
7
7
  * Combines a database model and a provider.
8
8
  *
@@ -18,10 +18,10 @@ export declare class Database<V extends Validators<Datas> = Validators<Datas>> {
18
18
  query<K extends Key<V>>(collection: K, filters?: Filters<ValidatorType<V[K]>>, sorts?: Sorts<ValidatorType<V[K]>>, limit?: number | null): DataQuery<ValidatorType<V[K]>>;
19
19
  /** Reference a document in a collection in this model. */
20
20
  doc<K extends Key<V>>(collection: K, id: string): DataDocument<ValidatorType<V[K]>>;
21
- /** Perform a write on this database and return the `Write` instance representing the changes. */
22
- write(write: Write): Promise<Write>;
23
- /** Perform multiple writes on this database by combining them into a single `Writes` instance representing the changes. */
24
- writes(...writes: (Write | undefined)[]): Promise<Writes>;
21
+ /** Create a writer for this database from a set of separate writes. */
22
+ writer(...writes: Nullish<Write>[]): Write;
23
+ /** Perform one or more writes on this database and return the `Writes` instance representing the combined changes. */
24
+ write(...writes: Nullish<Write>[]): Promise<Write>;
25
25
  }
26
26
  /** A documents reference within a specific database. */
27
27
  export declare class DataQuery<T extends Data = Data> extends Query<T> implements Observable<Results<T>>, Validatable<Results<T>>, Iterable<Entry<T>> {
package/db/Database.js CHANGED
@@ -1,4 +1,4 @@
1
- import { callAsync, getFirstItem, throwAsync, validate, toMap, countItems, TransformObserver, } from "../util/index.js";
1
+ import { callAsync, getFirstItem, throwAsync, validate, toMap, countItems, TransformObserver, NOT_NULLISH, } from "../util/index.js";
2
2
  import { DataUpdate, Update } from "../update/index.js";
3
3
  import { Feedback, InvalidFeedback } from "../feedback/index.js";
4
4
  import { Filters, Query, EqualFilter } from "../query/index.js";
@@ -25,14 +25,13 @@ export class Database {
25
25
  doc(collection, id) {
26
26
  return new DataDocument(this.provider, this.validators[collection], collection, id);
27
27
  }
28
- /** Perform a write on this database and return the `Write` instance representing the changes. */
29
- async write(write) {
30
- await write.transform(this);
31
- return write;
28
+ /** Create a writer for this database from a set of separate writes. */
29
+ writer(...writes) {
30
+ return new Writes(...writes.filter(NOT_NULLISH));
32
31
  }
33
- /** Perform multiple writes on this database by combining them into a single `Writes` instance representing the changes. */
34
- async writes(...writes) {
35
- const write = new Writes(...writes);
32
+ /** Perform one or more writes on this database and return the `Writes` instance representing the combined changes. */
33
+ async write(...writes) {
34
+ const write = new Writes(...writes.filter(NOT_NULLISH));
36
35
  await write.transform(this);
37
36
  return write;
38
37
  }
package/db/Write.d.ts CHANGED
@@ -12,7 +12,7 @@ export declare abstract class Write implements Transformable<Database, void | Pr
12
12
  */
13
13
  export declare class Writes extends Write {
14
14
  readonly writes: ImmutableArray<Write>;
15
- constructor(...writes: (Write | undefined)[]);
15
+ constructor(...writes: Write[]);
16
16
  transform(db: Database): Promise<void>;
17
17
  }
18
18
  /** Represent a write made to a single document in a database. */
package/db/Write.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DataUpdate, Update } from "../update/index.js";
2
- import { transform, IS_DEFINED } from "../util/index.js";
2
+ import { transform } from "../util/index.js";
3
3
  /** Represent a write made to a database. */
4
4
  export class Write {
5
5
  }
@@ -11,7 +11,7 @@ export class Write {
11
11
  export class Writes extends Write {
12
12
  constructor(...writes) {
13
13
  super();
14
- this.writes = writes.filter(IS_DEFINED);
14
+ this.writes = writes;
15
15
  }
16
16
  async transform(db) {
17
17
  for (const writes of this.writes)
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.27.0",
14
+ "version": "1.30.1",
15
15
  "repository": "https://github.com/dhoulb/shelving",
16
16
  "author": "Dave Houlbrooke <dave@shax.com>",
17
17
  "license": "0BSD",
@@ -63,20 +63,20 @@
63
63
  "@types/jest": "^27.0.3",
64
64
  "@types/react": "^17.0.37",
65
65
  "@types/react-dom": "^17.0.11",
66
- "@typescript-eslint/eslint-plugin": "^5.5.0",
67
- "@typescript-eslint/parser": "^5.5.0",
68
- "eslint": "^8.4.0",
66
+ "@typescript-eslint/eslint-plugin": "^5.6.0",
67
+ "@typescript-eslint/parser": "^5.6.0",
68
+ "eslint": "^8.4.1",
69
69
  "eslint-config-prettier": "^8.3.0",
70
70
  "eslint-plugin-import": "^2.25.3",
71
71
  "eslint-plugin-prettier": "^4.0.0",
72
- "firebase": "^9.6.0",
73
- "jest": "^27.4.3",
72
+ "firebase": "^9.6.1",
73
+ "jest": "^27.4.4",
74
74
  "jest-ts-webcompat-resolver": "^1.0.0",
75
75
  "prettier": "^2.5.1",
76
76
  "react": "^17.0.2",
77
77
  "react-dom": "^17.0.2",
78
- "ts-jest": "^27.0.7",
79
- "typescript": "^4.5.2"
78
+ "ts-jest": "^27.1.1",
79
+ "typescript": "^4.5.3"
80
80
  },
81
81
  "peerDependencies": {
82
82
  "@google-cloud/firestore": ">=4.0.0",
@@ -1,6 +1,8 @@
1
1
  import { Schema } from "./Schema.js";
2
2
  /** `type=""` prop for HTML `<input />` tags that are relevant for strings. */
3
3
  export declare type HtmlInputType = "text" | "password" | "color" | "date" | "email" | "number" | "tel" | "search" | "url";
4
+ /** Function that sanitizes a string. */
5
+ export declare type Sanitizer = (str: string) => string;
4
6
  /**
5
7
  * Schema that defines a valid string.
6
8
  *
@@ -27,16 +29,16 @@ export declare class StringSchema extends Schema<string> {
27
29
  readonly min: number;
28
30
  readonly max: number | null;
29
31
  readonly match: RegExp | null;
32
+ readonly sanitizer: Sanitizer | null;
30
33
  readonly multiline: boolean;
31
- readonly trim: boolean;
32
- constructor({ value, type, min, max, match, multiline, trim, ...rest }: ConstructorParameters<typeof Schema>[0] & {
34
+ constructor({ value, type, min, max, match, sanitizer, multiline, ...rest }: ConstructorParameters<typeof Schema>[0] & {
33
35
  readonly value?: string;
34
36
  readonly type?: HtmlInputType;
35
37
  readonly min?: number;
36
38
  readonly max?: number | null;
37
39
  readonly match?: RegExp | null;
40
+ readonly sanitizer?: Sanitizer | null;
38
41
  readonly multiline?: boolean;
39
- readonly trim?: boolean;
40
42
  });
41
43
  validate(unsafeValue?: unknown): string;
42
44
  /**
@@ -22,15 +22,15 @@ import { Schema } from "./Schema.js";
22
22
  * schema.validate('j'); // Throws 'Minimum 3 chaacters'
23
23
  */
24
24
  export class StringSchema extends Schema {
25
- constructor({ value = "", type = "text", min = 0, max = null, match = null, multiline = false, trim = true, ...rest }) {
25
+ constructor({ value = "", type = "text", min = 0, max = null, match = null, sanitizer = null, multiline = false, ...rest }) {
26
26
  super(rest);
27
27
  this.type = type;
28
28
  this.value = value;
29
29
  this.min = min;
30
30
  this.max = max;
31
31
  this.match = match;
32
+ this.sanitizer = sanitizer;
32
33
  this.multiline = multiline;
33
- this.trim = trim;
34
34
  }
35
35
  validate(unsafeValue = this.value) {
36
36
  const unsafeString = typeof unsafeValue === "number" ? unsafeValue.toString() : unsafeValue;
@@ -51,7 +51,7 @@ export class StringSchema extends Schema {
51
51
  * - Applies `options.sanitizer` too (if it's set).
52
52
  */
53
53
  sanitize(uncleanString) {
54
- return this.multiline ? sanitizeLines(uncleanString, this.trim) : sanitizeString(uncleanString, this.trim);
54
+ return this.sanitizer ? this.sanitizer(uncleanString) : this.multiline ? sanitizeLines(uncleanString) : sanitizeString(uncleanString);
55
55
  }
56
56
  }
57
57
  /** Valid string, e.g. `Hello there!` */
@@ -1,9 +1,24 @@
1
- import { Key, Data, PropTransformers } from "../util/index.js";
1
+ import { Key, Data, PropTransformers, Result } from "../util/index.js";
2
2
  import { State } from "./State.js";
3
- /** State that stores an array and has additional methods to help with that. */
3
+ /** State that stores a data object and has additional methods to help with that. */
4
4
  export declare class DataState<T extends Data> extends State<T> {
5
+ /** Get the data value of this state. */
6
+ get data(): T;
5
7
  /** Set a prop in this object to a new value. */
6
8
  set<K extends Key<T>>(key: K, value: T[K]): void;
7
9
  /** Update several props in this object. */
8
10
  update(updates: PropTransformers<T>): void;
9
11
  }
12
+ /** State that stores an optional data object and has additional methods to help with that. */
13
+ export declare class ResultState<T extends Data> extends State<Result<T>> {
14
+ /** Get the result value of this state. */
15
+ get result(): Result<T>;
16
+ /** Get current data value of this state (or throw `Promise` that resolves to the next required value). */
17
+ get data(): T;
18
+ /** Set a prop in this object to a new value. */
19
+ set<K extends Key<T>>(key: K, value: T[K]): void;
20
+ /** Update several props in this object. */
21
+ update(updates: PropTransformers<T>): void;
22
+ /** Delete this result. */
23
+ delete(): void;
24
+ }
@@ -1,13 +1,44 @@
1
- import { withProp, transformData } from "../util/index.js";
1
+ import { withProp, transformData, NOERROR, LOADING, awaitNext, getData } from "../util/index.js";
2
2
  import { State } from "./State.js";
3
- /** State that stores an array and has additional methods to help with that. */
3
+ /** State that stores a data object and has additional methods to help with that. */
4
4
  export class DataState extends State {
5
+ /** Get the data value of this state. */
6
+ get data() {
7
+ return this.value;
8
+ }
5
9
  /** Set a prop in this object to a new value. */
6
10
  set(key, value) {
7
- this.next(withProp(this.value, key, value));
11
+ this.next(withProp(this.data, key, value));
8
12
  }
9
13
  /** Update several props in this object. */
10
14
  update(updates) {
11
- this.next(transformData(this.value, updates));
15
+ this.next(transformData(this.data, updates));
16
+ }
17
+ }
18
+ /** State that stores an optional data object and has additional methods to help with that. */
19
+ export class ResultState extends State {
20
+ /** Get the result value of this state. */
21
+ get result() {
22
+ return this.value;
23
+ }
24
+ /** Get current data value of this state (or throw `Promise` that resolves to the next required value). */
25
+ get data() {
26
+ if (this.reason !== NOERROR)
27
+ throw this.reason;
28
+ if (this._value === LOADING)
29
+ throw awaitNext(this).then(getData);
30
+ return getData(this._value);
31
+ }
32
+ /** Set a prop in this object to a new value. */
33
+ set(key, value) {
34
+ this.next(withProp(this.data, key, value));
35
+ }
36
+ /** Update several props in this object. */
37
+ update(updates) {
38
+ this.next(transformData(this.data, updates));
39
+ }
40
+ /** Delete this result. */
41
+ delete() {
42
+ this.next(undefined);
12
43
  }
13
44
  }
package/stream/State.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Transformer, LOADING, ObserverType, NOERROR, Observer } from "../util/index.js";
2
- import { Stream } from "./Stream.js";
2
+ import { AnyStream, Stream } from "./Stream.js";
3
3
  /** Any state (useful for `extends AnySubscribable` clauses). */
4
4
  export declare type AnyState = State<any>;
5
5
  /**
@@ -14,8 +14,11 @@ export declare type AnyState = State<any>;
14
14
  * */
15
15
  export interface State<T> {
16
16
  to(): State<T>;
17
+ to<O extends AnyStream>(target: O): O;
17
18
  derive<TT>(transformer: Transformer<T, TT>): State<TT>;
19
+ derive<O extends AnyStream>(transformer: Transformer<T, ObserverType<O>>, target: O): O;
18
20
  deriveAsync<TT>(transformer: Transformer<T, PromiseLike<TT>>): State<TT>;
21
+ deriveAsync<O extends AnyStream>(transformer: Transformer<T, Promise<ObserverType<O>>>, target: O): O;
19
22
  }
20
23
  export declare class State<T> extends Stream<T> {
21
24
  static [Symbol.species]: typeof State;
@@ -28,8 +31,6 @@ export declare class State<T> extends Stream<T> {
28
31
  /** Most recently dispatched value (or throw `Promise` that resolves to the next value). */
29
32
  get value(): T;
30
33
  protected _value: T | typeof LOADING;
31
- /** Get current required value (or throw `Promise` that resolves to the next required value). */
32
- get data(): Exclude<T, null | undefined>;
33
34
  /** Is there a current value, or is it still loading. */
34
35
  get loading(): boolean;
35
36
  /** Apply a transformer to this state. */
package/stream/State.js CHANGED
@@ -1,5 +1,5 @@
1
1
  var _a;
2
- import { LOADING, NOERROR, dispatchNext, dispatchError, dispatchComplete, transform, getRequired, awaitNext, } from "../util/index.js";
2
+ import { LOADING, NOERROR, dispatchNext, dispatchError, dispatchComplete, transform, awaitNext, } from "../util/index.js";
3
3
  import { Stream } from "./Stream.js";
4
4
  export class State extends Stream {
5
5
  constructor() {
@@ -22,14 +22,6 @@ export class State extends Stream {
22
22
  throw awaitNext(this);
23
23
  return this._value;
24
24
  }
25
- /** Get current required value (or throw `Promise` that resolves to the next required value). */
26
- get data() {
27
- if (this.reason !== NOERROR)
28
- throw this.reason;
29
- if (this._value === LOADING)
30
- throw awaitNext(this).then(getRequired);
31
- return getRequired(this._value);
32
- }
33
25
  /** Is there a current value, or is it still loading. */
34
26
  get loading() {
35
27
  return this._value === LOADING;
package/util/data.d.ts CHANGED
@@ -25,9 +25,6 @@ export declare const isData: <T extends Data>(value: unknown) => value is T;
25
25
  /** Turn a data object into an array of entries (if it isn't one already). */
26
26
  export declare function toProps<T extends Data>(input: T): ImmutableArray<Prop<T>>;
27
27
  export declare function toProps<T extends Data>(input: Partial<T>): ImmutableArray<Prop<T>>;
28
- /** Get a required value (returns value or throws `RequiredError` if value is `null` or `undefined`). */
29
- export declare function getRequired<T>(v: T): Exclude<T, null | undefined>;
30
- export declare function getRequired<T>(v: T | null | undefined): T;
31
28
  /** Get the data of a result (returns data or throws `RequiredError` if value is `null` or `undefined`). */
32
29
  export declare function getData<T extends Data>(result: Result<T>): T;
33
30
  /**
@@ -137,3 +134,11 @@ export declare type DeepMutable<T extends Data> = {
137
134
  export declare type DeepReadonly<T extends Data> = {
138
135
  +readonly [K in keyof T]: T[K] extends Data ? DeepReadonly<T[K]> : T[K];
139
136
  };
137
+ /** Pick only the properties of an object that match a type. */
138
+ export declare type PickProps<T, TT> = Pick<T, {
139
+ [K in keyof T]: T[K] extends TT ? K : never;
140
+ }[keyof T]>;
141
+ /** Omit the properties of an object that match a type. */
142
+ export declare type OmitProps<T, TT> = Omit<T, {
143
+ [K in keyof T]: T[K] extends TT ? K : never;
144
+ }[keyof T]>;
package/util/data.js CHANGED
@@ -4,11 +4,6 @@ export const isData = (value) => typeof value === "object" && value !== null;
4
4
  export function toProps(input) {
5
5
  return Object.entries(input);
6
6
  }
7
- export function getRequired(v) {
8
- if (v === undefined || v === null)
9
- throw new RequiredError(v === null ? "Required value is null" : "Required value is undefined");
10
- return v;
11
- }
12
7
  /** Get the data of a result (returns data or throws `RequiredError` if value is `null` or `undefined`). */
13
8
  export function getData(result) {
14
9
  if (!result)
package/util/null.d.ts CHANGED
@@ -3,4 +3,15 @@ export declare const IS_NULL: (v: unknown) => v is null;
3
3
  /** Is a value not null? */
4
4
  export declare const NOT_NULL: <T>(v: T | null) => v is T;
5
5
  /** Function that always returns null. */
6
- export declare const NULL: () => null;
6
+ export declare const GET_NULL: () => null;
7
+ /** Nullish is `null` or `undefined` */
8
+ export declare type Nullish<T> = T | null | undefined;
9
+ /** Nullish is `null` or `undefined` */
10
+ export declare type NotNullish<T> = Exclude<T, null | undefined>;
11
+ /** Is a value nullish? */
12
+ export declare const IS_NULLISH: <T>(v: Nullish<T>) => v is null | undefined;
13
+ /** Is a value nullish? */
14
+ export declare const NOT_NULLISH: <T>(v: Nullish<T>) => v is T;
15
+ /** Get a required value (returns value or throws `RequiredError` if value is `null` or `undefined`). */
16
+ export declare function getRequired<T>(v: T): NotNullish<T>;
17
+ export declare function getRequired<T>(v: Nullish<T>): T;
package/util/null.js CHANGED
@@ -1,6 +1,16 @@
1
+ import { RequiredError } from "../error/index.js";
1
2
  /** Is a value null? */
2
3
  export const IS_NULL = (v) => v === null;
3
4
  /** Is a value not null? */
4
5
  export const NOT_NULL = (v) => v !== null;
5
6
  /** Function that always returns null. */
6
- export const NULL = () => null;
7
+ export const GET_NULL = () => null;
8
+ /** Is a value nullish? */
9
+ export const IS_NULLISH = (v) => v === null || v === undefined;
10
+ /** Is a value nullish? */
11
+ export const NOT_NULLISH = (v) => v !== null && v !== undefined;
12
+ export function getRequired(v) {
13
+ if (v === undefined || v === null)
14
+ throw new RequiredError("Required value is missing");
15
+ return v;
16
+ }
package/util/object.d.ts CHANGED
@@ -87,3 +87,11 @@ export declare function deleteEntry<T>(obj: MutableObject<T>, key: string, value
87
87
  * @param entries Set of keys or entries to remove.
88
88
  */
89
89
  export declare function deleteEntries<T>(obj: MutableObject<T>, keys: ImmutableArray<string>): void;
90
+ /** Type that represents an empty object. */
91
+ export declare type EmptyObject = {
92
+ readonly [K in never]: never;
93
+ };
94
+ /** An empty object. */
95
+ export declare const EMPTY_OBJECT: EmptyObject;
96
+ /** Function that returns an an empty object. */
97
+ export declare const GET_EMPTY_OBJECT: () => EmptyObject;
package/util/object.js CHANGED
@@ -98,3 +98,7 @@ export function deleteEntries(obj, keys) {
98
98
  for (const key of keys)
99
99
  delete obj[key];
100
100
  }
101
+ /** An empty object. */
102
+ export const EMPTY_OBJECT = {};
103
+ /** Function that returns an an empty object. */
104
+ export const GET_EMPTY_OBJECT = () => EMPTY_OBJECT;
package/util/search.d.ts CHANGED
@@ -36,7 +36,7 @@ export declare function MATCHES_ANY(item: unknown, regexps: ImmutableArray<RegEx
36
36
  * - Quoted phrases match fully (starting and ending with a word boundary).
37
37
  */
38
38
  export declare const toWordRegExps: (query: string) => ImmutableArray<RegExp>;
39
- /** Convert a string word to the corresponding set of case-insensitive regular expressions. */
39
+ /** Convert a string to a regular expression matching the start of a word boundary. */
40
40
  export declare const toWordRegExp: (word: string) => RegExp;
41
41
  /** Matcher that matches any words in a string. */
42
42
  export declare class MatchAnyWord implements Matchable<unknown, void> {
@@ -51,7 +51,7 @@ export declare class MatchAllWords implements Matchable<unknown, void> {
51
51
  match(value: string): boolean;
52
52
  }
53
53
  /** Matcher that matches an exact phrase. */
54
- export declare class MatchPhrase implements Matchable<unknown, void> {
54
+ export declare class MatchWord implements Matchable<unknown, void> {
55
55
  private _regexp;
56
56
  constructor(phrase: string);
57
57
  match(value: string): boolean;
package/util/search.js CHANGED
@@ -50,9 +50,9 @@ export function MATCHES_ANY(item, regexps) {
50
50
  * - Unquoted words match partially (starting with a word boundary).
51
51
  * - Quoted phrases match fully (starting and ending with a word boundary).
52
52
  */
53
- export const toWordRegExps = (query) => toWords(query).map(normalizeString).map(toWordRegExp);
54
- /** Convert a string word to the corresponding set of case-insensitive regular expressions. */
55
- export const toWordRegExp = (word) => word.includes(" ") ? new RegExp(`\\b${escapeRegExp(word)}\\b`, "i") : new RegExp(`\\b${escapeRegExp(word)}`, "i");
53
+ export const toWordRegExps = (query) => toWords(query).map(toWordRegExp);
54
+ /** Convert a string to a regular expression matching the start of a word boundary. */
55
+ export const toWordRegExp = (word) => new RegExp(`\\b${escapeRegExp(normalizeString(word))}`, "i");
56
56
  /** Matcher that matches any words in a string. */
57
57
  export class MatchAnyWord {
58
58
  constructor(words) {
@@ -72,7 +72,7 @@ export class MatchAllWords {
72
72
  }
73
73
  }
74
74
  /** Matcher that matches an exact phrase. */
75
- export class MatchPhrase {
75
+ export class MatchWord {
76
76
  constructor(phrase) {
77
77
  this._regexp = toWordRegExp(phrase);
78
78
  }
package/util/string.d.ts CHANGED
@@ -33,25 +33,25 @@ export declare function toTitle(value: unknown): string;
33
33
  * @param multiline If `true`, `\t` horizontal tabs and `\r` newlines are allowed (defaults to `false` which strips these characters).
34
34
  * @returns The clean output string.
35
35
  */
36
- export declare function sanitizeString(dirty: string, trim?: boolean): string;
36
+ export declare const sanitizeString: (dirty: string) => string;
37
37
  /**
38
38
  * Sanitize a multiline string.
39
39
  * - Like `sanitizeString()` but allows `\t` horizontal tab and `\r` newline.
40
40
  */
41
- export declare function sanitizeLines(dirty: string, trim?: boolean): string;
41
+ export declare const sanitizeLines: (dirty: string) => string;
42
42
  /**
43
43
  * Normalize a string so it can be compared to another string (free from upper/lower cases, symbols, punctuation).
44
44
  *
45
45
  * Does the following:
46
- * - Santize the string to remove control characters.
46
+ * - Removes control characters.
47
47
  * - Remove symbols (e.g. `$` dollar) and punctuation (e.g. `"` double quote).
48
48
  * - Remove marks (e.g. the umlout dots above `ö`).
49
49
  * - Convert spaces/separators to " " single space (e.g. line breaks, non-breaking space).
50
50
  * - Convert to lowercase and trim excess whitespace.
51
51
  *
52
- * @example normalizeString("Däve-is REALLY éxcitable—apparęntly!!! 😂"); // Returns "dave is really excitable apparently"
52
+ * @example normalizeString("Däve-is\nREALLY éxcitable—apparęntly!!! 😂"); // Returns "dave is really excitable apparently"
53
53
  */
54
- export declare const normalizeString: (value: string) => string;
54
+ export declare const normalizeString: (str: string) => string;
55
55
  /**
56
56
  * Convert a string to a `kebab-case` URL slug.
57
57
  * - Remove any characters not in the range `[a-z0-9-]`
@@ -67,6 +67,8 @@ export declare const toSlug: (value: string) => string;
67
67
  * @returns Array of the found words, e.g. `["yellow", "dog", "Golden Retriever"
68
68
  */
69
69
  export declare const toWords: (value: string) => ImmutableArray<string>;
70
+ /** Find and iterate over the words in a string. */
71
+ export declare function yieldWords(value: string): Generator<string, void, void>;
70
72
  /**
71
73
  * Convert a string to a regular expression that matches that string.
72
74
  *
package/util/string.js CHANGED
@@ -3,7 +3,6 @@ import { formatDate } from "./date.js";
3
3
  import { isData } from "./data.js";
4
4
  import { isArray } from "./array.js";
5
5
  import { formatNumber, isBetween } from "./number.js";
6
- import { IS_DEFINED } from "./undefined.js";
7
6
  /** Is a value a string? */
8
7
  export const IS_STRING = (v) => typeof v === "string";
9
8
  /**
@@ -70,47 +69,40 @@ export function toTitle(value) {
70
69
  * @param multiline If `true`, `\t` horizontal tabs and `\r` newlines are allowed (defaults to `false` which strips these characters).
71
70
  * @returns The clean output string.
72
71
  */
73
- export function sanitizeString(dirty, trim = true) {
74
- const clean = dirty.replace(CONTROLS, "").replace(SPACES, " ");
75
- return trim ? clean.trim() : clean;
76
- }
77
- const CONTROLS = /[\x00-\x1F\x7F-\x9F]/g; // All control characters (`\x00`-`\x1F`, `\x7F`-`\x9F`)
78
- const SPACES = /\s/g; // Sanitize zero-width spacers etc.
72
+ export const sanitizeString = (dirty) => dirty.replace(SANE_DISALLOWED, "").replace(SANE_SPACES, " ").trim();
73
+ const SANE_DISALLOWED = /[\x00-\x1F\x7F-\x9F]/g; // All control characters (`\x00`-`\x1F`, `\x7F`-`\x9F`)
74
+ const SANE_SPACES = /\s/g; // Zero-width spacers etc.
79
75
  /**
80
76
  * Sanitize a multiline string.
81
77
  * - Like `sanitizeString()` but allows `\t` horizontal tab and `\r` newline.
82
78
  */
83
- export function sanitizeLines(dirty, trim = true) {
84
- const clean = dirty.replace(CONTROLS_MULTILINE, "").replace(SPACES_MULTILINE, " ");
85
- return trim ? clean.replace(TRIM_END_MULTILINE, "") : clean;
86
- }
87
- const CONTROLS_MULTILINE = /(?![\t\n])[\x00-\x1F\x7F-\x9F]/g; // Control characters except `\t` horizontal tab and `\n` new line.
88
- const SPACES_MULTILINE = /(?![\t\n])\s/g; // All spaces except `\t` horizontal tab and `\n` new line.
89
- const TRIM_END_MULTILINE = /\s+$/gm;
79
+ export const sanitizeLines = (dirty) => dirty.replace(LINES_DISALLOW, "").replace(LINES_SPACES, " ").replace(LINES_TRIM, "");
80
+ const LINES_DISALLOW = /(?![\t\n])[\x00-\x1F\x7F-\x9F]/g; // Control characters except `\t` horizontal tab and `\n` new line.
81
+ const LINES_TRIM = /\s+$/gm; // Spaces at the end of lines.
82
+ const LINES_SPACES = /(?![\t\n])\s/g; // All spaces except `\t` horizontal tab and `\n` new line.
90
83
  /**
91
84
  * Normalize a string so it can be compared to another string (free from upper/lower cases, symbols, punctuation).
92
85
  *
93
86
  * Does the following:
94
- * - Santize the string to remove control characters.
87
+ * - Removes control characters.
95
88
  * - Remove symbols (e.g. `$` dollar) and punctuation (e.g. `"` double quote).
96
89
  * - Remove marks (e.g. the umlout dots above `ö`).
97
90
  * - Convert spaces/separators to " " single space (e.g. line breaks, non-breaking space).
98
91
  * - Convert to lowercase and trim excess whitespace.
99
92
  *
100
- * @example normalizeString("Däve-is REALLY éxcitable—apparęntly!!! 😂"); // Returns "dave is really excitable apparently"
93
+ * @example normalizeString("Däve-is\nREALLY éxcitable—apparęntly!!! 😂"); // Returns "dave is really excitable apparently"
101
94
  */
102
- export const normalizeString = (value) => sanitizeString(value).normalize("NFD").replace(STRIP, "").replace(SEPARATORS, " ").trim().toLowerCase();
103
- const STRIP = /[\p{Symbol}\p{Mark}\p{Punctuation}]+/gu;
104
- const SEPARATORS = /\s+/g;
95
+ export const normalizeString = (str) => str.normalize("NFD").replace(NORMAL_DISALLOW, "").replace(NORMAL_SPACES, " ").trim().toLowerCase();
96
+ const NORMAL_DISALLOW = /[^\p{L}\p{N}\s]+/gu; // Keep letters, numbers, separators. Don't need to use `\p{Z}` because `\s` matches those already e.g. hair space and nbsp.
97
+ const NORMAL_SPACES = /\s+/gu; // Convert all possible separators to space.
105
98
  /**
106
99
  * Convert a string to a `kebab-case` URL slug.
107
100
  * - Remove any characters not in the range `[a-z0-9-]`
108
101
  * - Change all spaces/separators/hyphens/dashes/underscores to `-` single hyphen.
109
102
  */
110
- export const toSlug = (value) => value.toLowerCase().normalize("NFD").replace(TO_HYPHEN, "-").replace(NON_ALPHANUMERIC, "").replace(TRIM_HYPHENS, "");
111
- const TO_HYPHEN = /[\s-–—_]+/g; // Anything that is a space becomes a hyphen.
112
- const NON_ALPHANUMERIC = /[^a-z0-9-]+/gu; // Anything that isn't [a-z0-9-] gets removed.
113
- const TRIM_HYPHENS = /^-+|-+$/g; // Trim excess hyphens at start and end.
103
+ export const toSlug = (value) => value.toLowerCase().normalize("NFD").replace(SLUG_HYPHENS, "-").replace(SLUG_DISALLOWED, "");
104
+ const SLUG_DISALLOWED = /[^a-z0-9-]+|^-+|-+$/gu; // Remove anything outside a-z and hyphen (except at start/end).
105
+ const SLUG_HYPHENS = /[\s\-–—_]+/gu; // Anything that is a space becomes a hyphen.
114
106
  /**
115
107
  * Split a string into its separate words.
116
108
  * - Words enclosed "in quotes" are a single word.
@@ -119,8 +111,15 @@ const TRIM_HYPHENS = /^-+|-+$/g; // Trim excess hyphens at start and end.
119
111
  * @param value The input string, e.g. `yellow dog "Golden Retriever"`
120
112
  * @returns Array of the found words, e.g. `["yellow", "dog", "Golden Retriever"
121
113
  */
122
- export const toWords = (value) => Array.from(value.matchAll(MATCH_WORD)).map(toWord).filter(IS_DEFINED);
123
- const toWord = (matches) => matches[1] || matches[0] || undefined;
114
+ export const toWords = (value) => Array.from(yieldWords(value));
115
+ /** Find and iterate over the words in a string. */
116
+ export function* yieldWords(value) {
117
+ for (const matches of value.matchAll(MATCH_WORD)) {
118
+ const str = matches[1] || matches[0];
119
+ if (str)
120
+ yield str;
121
+ }
122
+ }
124
123
  const MATCH_WORD = /[^\s"]+|"([^"]*)"/g;
125
124
  /**
126
125
  * Convert a string to a regular expression that matches that string.
@@ -4,6 +4,4 @@ export declare const IS_UNDEFINED: (v: unknown) => v is undefined;
4
4
  export declare const IS_DEFINED: <T>(v: T | undefined) => v is T;
5
5
  export declare const NOT_UNDEFINED: <T>(v: T | undefined) => v is T;
6
6
  /** Function that always returns undefined. */
7
- export declare const UNDEFINED: () => undefined;
8
- /** Function that always returns void. */
9
- export declare const VOID: () => void;
7
+ export declare const GET_UNDEFINED: () => undefined;
package/util/undefined.js CHANGED
@@ -4,6 +4,4 @@ export const IS_UNDEFINED = (v) => v === undefined;
4
4
  export const IS_DEFINED = (v) => v !== undefined;
5
5
  export const NOT_UNDEFINED = IS_DEFINED;
6
6
  /** Function that always returns undefined. */
7
- export const UNDEFINED = () => undefined;
8
- /** Function that always returns void. */
9
- export const VOID = UNDEFINED;
7
+ export const GET_UNDEFINED = () => undefined;
package/util/url.d.ts CHANGED
@@ -1,10 +1,9 @@
1
+ /** Things that can be converted to a URL instance. */
2
+ export declare type PossibleURL = string | URL;
3
+ export declare type PossibleOptionalURL = PossibleURL | null;
4
+ /** Convert a possible URL to a URL or return `null` if conversion fails. */
5
+ export declare function toURL(url: PossibleURL, base?: PossibleOptionalURL): URL | null;
6
+ /** Convert a possible URL to a URL but throw `AssertionError` if conversion fails. */
7
+ export declare function getURL(input: PossibleURL, base?: PossibleOptionalURL): URL;
1
8
  /** Just get important part of a URL, e.g. `http://shax.com/test?uid=129483` → `shax.com/test` */
2
- export declare const formatUrl: (url: string | URL) => string;
3
- /**
4
- * Convert a string to a URL instance or return `null` if we can't.
5
- * - Automatically prepend `https://` if there's no `:` anywhere.
6
- *
7
- * @param url Base
8
- * @param
9
- */
10
- export declare function toURL(url: string | URL, base?: URL | string | undefined): URL | null;
9
+ export declare const formatUrl: (url: PossibleURL) => string;
package/util/url.js CHANGED
@@ -1,25 +1,26 @@
1
- import { getRequired } from "./data.js";
2
- /** Just get important part of a URL, e.g. `http://shax.com/test?uid=129483` `shax.com/test` */
3
- export const formatUrl = (url) => {
4
- const { host, pathname } = getRequired(toURL(url));
5
- return `${host}${pathname.length > 1 ? pathname : ""}`;
6
- };
7
- /**
8
- * Convert a string to a URL instance or return `null` if we can't.
9
- * - Automatically prepend `https://` if there's no `:` anywhere.
10
- *
11
- * @param url Base
12
- * @param
13
- */
14
- export function toURL(url, base = typeof window === "object" ? window.location.href : undefined) {
1
+ import { AssertionError } from "../error/index.js";
2
+ /** Convert a possible URL to a URL or return `null` if conversion fails. */
3
+ export function toURL(url, base = typeof window === "object" ? window.location.href : null) {
15
4
  if (url instanceof URL)
16
5
  return url;
17
6
  if (!url)
18
7
  return null;
19
8
  try {
20
- return new URL(url, base);
9
+ return new URL(url, base || undefined);
21
10
  }
22
11
  catch (e) {
23
12
  return null;
24
13
  }
25
14
  }
15
+ /** Convert a possible URL to a URL but throw `AssertionError` if conversion fails. */
16
+ export function getURL(input, base) {
17
+ const url = toURL(input, base);
18
+ if (!url)
19
+ throw new AssertionError("Invalid URL", input);
20
+ return url;
21
+ }
22
+ /** Just get important part of a URL, e.g. `http://shax.com/test?uid=129483` → `shax.com/test` */
23
+ export const formatUrl = (url) => {
24
+ const { host, pathname } = getURL(url);
25
+ return `${host}${pathname.length > 1 ? pathname : ""}`;
26
+ };