shelving 1.29.0 → 1.30.3

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/db/Database.d.ts CHANGED
@@ -56,7 +56,9 @@ export declare class DataQuery<T extends Data = Data> extends Query<T> implement
56
56
  get count(): number | PromiseLike<number>;
57
57
  /**
58
58
  * Get an entry for the first document matching this query.
59
- * @return Entry in `[id, data]` format for the first document, or `undefined` if there are no matching documents (possibly promised).
59
+ *
60
+ * @return Entry in `[id, data]` format for the first document.
61
+ * @throws RequiredError if there were no results for this query.
60
62
  */
61
63
  get first(): Entry<T> | undefined | PromiseLike<Entry<T> | undefined>;
62
64
  /**
@@ -100,6 +102,8 @@ export declare class DataQuery<T extends Data = Data> extends Query<T> implement
100
102
  validate(unsafeEntries: Results): Results<T>;
101
103
  toString(): string;
102
104
  }
105
+ /** Get the data for a document from a result for that document. */
106
+ export declare function getQueryFirst<T extends Data>(results: Results<T>, ref: DataQuery<T>): Entry<T>;
103
107
  /** A document reference within a specific database. */
104
108
  export declare class DataDocument<T extends Data = Data> implements Observable<Result<T>>, Validatable<T> {
105
109
  readonly provider: Provider;
package/db/Database.js CHANGED
@@ -2,7 +2,7 @@ import { callAsync, getFirstItem, throwAsync, validate, toMap, countItems, Trans
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";
5
- import { DocumentRequiredError, DocumentValidationError, QueryValidationError } from "./errors.js";
5
+ import { DocumentRequiredError, DocumentValidationError, QueryRequiredError, QueryValidationError } from "./errors.js";
6
6
  import { DocumentDelete, DocumentSet, DocumentUpdate, Writes } from "./Write.js";
7
7
  /**
8
8
  * Combines a database model and a provider.
@@ -81,10 +81,12 @@ export class DataQuery extends Query {
81
81
  }
82
82
  /**
83
83
  * Get an entry for the first document matching this query.
84
- * @return Entry in `[id, data]` format for the first document, or `undefined` if there are no matching documents (possibly promised).
84
+ *
85
+ * @return Entry in `[id, data]` format for the first document.
86
+ * @throws RequiredError if there were no results for this query.
85
87
  */
86
88
  get first() {
87
- return callAsync(getFirstItem, this.max(1).results);
89
+ return callAsync(getQueryFirst, this.max(1).results, this);
88
90
  }
89
91
  /**
90
92
  * Subscribe to all matching documents.
@@ -158,6 +160,13 @@ export class DataQuery extends Query {
158
160
  return `${this.collection}?${super.toString()}`;
159
161
  }
160
162
  }
163
+ /** Get the data for a document from a result for that document. */
164
+ export function getQueryFirst(results, ref) {
165
+ const first = getFirstItem(results);
166
+ if (first)
167
+ return first;
168
+ throw new QueryRequiredError(ref);
169
+ }
161
170
  /** A document reference within a specific database. */
162
171
  export class DataDocument {
163
172
  constructor(provider, validator, collection, id) {
package/db/errors.d.ts CHANGED
@@ -7,6 +7,11 @@ export declare class DocumentRequiredError<T extends Data> extends RequiredError
7
7
  ref: DataDocument<T>;
8
8
  constructor(ref: DataDocument<T>);
9
9
  }
10
+ /** Thrown if a query doesn't exist. */
11
+ export declare class QueryRequiredError<T extends Data> extends RequiredError {
12
+ ref: DataQuery<T>;
13
+ constructor(ref: DataQuery<T>);
14
+ }
10
15
  /** Thrown if a document can't validate. */
11
16
  export declare class DocumentValidationError<T extends Data> extends ValidationError {
12
17
  ref: DataDocument<T>;
package/db/errors.js CHANGED
@@ -7,6 +7,14 @@ export class DocumentRequiredError extends RequiredError {
7
7
  }
8
8
  }
9
9
  DocumentRequiredError.prototype.name = "DocumentRequiredError";
10
+ /** Thrown if a query doesn't exist. */
11
+ export class QueryRequiredError extends RequiredError {
12
+ constructor(ref) {
13
+ super(`Query "${ref.toString()}" has no results`);
14
+ this.ref = ref;
15
+ }
16
+ }
17
+ QueryRequiredError.prototype.name = "QueryRequiredError";
10
18
  /** Thrown if a document can't validate. */
11
19
  export class DocumentValidationError extends ValidationError {
12
20
  constructor(ref, feedback) {
@@ -7,7 +7,7 @@ import type { MarkupElement, MarkupNode } from "./types.js";
7
7
  *
8
8
  * @example `- Item with *strong*\n- Item with _em_` becomes `Item with strong Item with em`
9
9
  */
10
- export declare const nodeToText: (node: MarkupNode) => string;
10
+ export declare function nodeToText(node: MarkupNode): string;
11
11
  /**
12
12
  * Take a Markup JSX node and convert it to an HTML string.
13
13
  *
@@ -17,13 +17,7 @@ export declare const nodeToText: (node: MarkupNode) => string;
17
17
  *
18
18
  * @example `- Item with *strong*\n- Item with _em_` becomes `<ul><li>Item with <strong>strong</strong></li><li>Item with <em>em</em></ul>`
19
19
  */
20
- export declare const nodeToHtml: (node: MarkupNode) => string;
21
- /**
22
- * Clean an input string.
23
- * - Allows our future RegExps to be less fussy (e.g. allowing for whitespace at the end of lines).
24
- * - Converts tabs and any other rogue whitespace (line feeds, obscure Unicode things) to whitespaces.
25
- */
26
- export declare const cleanMarkup: (content: string) => string;
20
+ export declare function nodeToHtml(node: MarkupNode): string;
27
21
  /**
28
22
  * Iterate through all elements in a node.
29
23
  * - This is useful if you, e.g. want to apply a `className` to all `<h1>` elements, or make a list of all URLs found in a Node.
package/markup/helpers.js CHANGED
@@ -7,7 +7,7 @@ import { serialise } from "../util/index.js";
7
7
  *
8
8
  * @example `- Item with *strong*\n- Item with _em_` becomes `Item with strong Item with em`
9
9
  */
10
- export const nodeToText = (node) => {
10
+ export function nodeToText(node) {
11
11
  if (typeof node === "string")
12
12
  return node;
13
13
  if (node instanceof Array)
@@ -15,7 +15,7 @@ export const nodeToText = (node) => {
15
15
  if (typeof node === "object" && node)
16
16
  return nodeToText(node.props.children);
17
17
  return "";
18
- };
18
+ }
19
19
  /**
20
20
  * Take a Markup JSX node and convert it to an HTML string.
21
21
  *
@@ -25,7 +25,7 @@ export const nodeToText = (node) => {
25
25
  *
26
26
  * @example `- Item with *strong*\n- Item with _em_` becomes `<ul><li>Item with <strong>strong</strong></li><li>Item with <em>em</em></ul>`
27
27
  */
28
- export const nodeToHtml = (node) => {
28
+ export function nodeToHtml(node) {
29
29
  if (typeof node === "string")
30
30
  return node;
31
31
  if (node instanceof Array)
@@ -36,7 +36,7 @@ export const nodeToHtml = (node) => {
36
36
  return `<${type}${strings.length ? ` ${strings.join(" ")}` : ""}>${nodeToHtml(children)}</${type}>`;
37
37
  }
38
38
  return "";
39
- };
39
+ }
40
40
  const propToString = ([key, value]) => value === true
41
41
  ? key
42
42
  : typeof value === "number" && Number.isFinite(value)
@@ -44,25 +44,6 @@ const propToString = ([key, value]) => value === true
44
44
  : typeof value === "string"
45
45
  ? `${key}=${serialise(value)}`
46
46
  : "";
47
- // Regular expressions used for preprocessing.
48
- const ROGUE_WHITESPACE = /[^\S\n ]/g; // Match whitespace that isn't "\n" line feed or ` ` space (includes tabs).
49
- const ROGUE_PARAGRAPHS = /\u2029/g; // Match newlines that use "\r".
50
- const ROGUE_NEWLINES = /\r\n?|\u2028/g; // Match newlines that use "\r".
51
- const TRAILING_SPACES = / +$/gm; // Match trailing spaces on a line.
52
- // eslint-disable-next-line no-control-regex
53
- const CONTROL_CHARS = /[\x00-\x09\x0B-\x1F\x7F-\x9F]/g; // Match all control characters (00-1F, 7F-9F) except `\x0A` `\n` line feed (security).
54
- /**
55
- * Clean an input string.
56
- * - Allows our future RegExps to be less fussy (e.g. allowing for whitespace at the end of lines).
57
- * - Converts tabs and any other rogue whitespace (line feeds, obscure Unicode things) to whitespaces.
58
- */
59
- export const cleanMarkup = (content) => content
60
- .replace(ROGUE_PARAGRAPHS, "\n\n") // Change weird new paragraph separators to standard "\n\n"
61
- .replace(ROGUE_NEWLINES, "\n") // Change weird newlines to standard "\n"
62
- .replace(ROGUE_WHITESPACE, " ") // Change obscure whitespace characters (e.g. tab and line-feed) to whitespace.
63
- .replace(TRAILING_SPACES, "") // Strip trailing whitespaces on any lines.
64
- .replace(CONTROL_CHARS, "") // Strip trailing spaces on any lines.
65
- .trimStart(); // Trim the start (including leading newlines).
66
47
  /**
67
48
  * Iterate through all elements in a node.
68
49
  * - This is useful if you, e.g. want to apply a `className` to all `<h1>` elements, or make a list of all URLs found in a Node.
@@ -32,9 +32,9 @@ import type { MarkupOptions, MarkupNode } from "./types.js";
32
32
  *
33
33
  * @returns ReactNode, i.e. either a complete `ReactElement`, `null`, `undefined`, `string`, or an array of zero or more of those.
34
34
  */
35
- export declare const renderMarkup: (content: string, options?: Partial<MarkupOptions> | undefined) => MarkupNode;
35
+ export declare function renderMarkup(content: string, options?: Partial<MarkupOptions>): MarkupNode;
36
36
  /**
37
37
  * Parse a text string as user-generated markup.
38
38
  * - Like `renderMarkup()` but only enables a subset of rules and applies `rel="nofollow ugc"` to all links.
39
39
  */
40
- export declare const renderUgcMarkup: (content: string, options?: Partial<MarkupOptions> | undefined) => MarkupNode;
40
+ export declare function renderUgcMarkup(content: string, options?: Partial<MarkupOptions>): MarkupNode;
package/markup/render.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /* eslint-disable no-param-reassign */
2
- import { cleanMarkup } from "./helpers.js";
2
+ import { sanitizeLines } from "../index.js";
3
3
  import { MARKUP_RULES, MARKUP_RULES_UGC } from "./rules.js";
4
4
  /** Convert a string into an array of React nodes using a set of rules. */
5
- const renderString = (content, options) => {
5
+ function renderString(content, options) {
6
6
  // If there's no context return the unmodified string.
7
7
  if (!options.context)
8
8
  return content;
@@ -60,14 +60,14 @@ const renderString = (content, options) => {
60
60
  }
61
61
  // If there's only one node return the single node (otherwise return the entire array).
62
62
  return !nodes.length ? null : nodes.length === 1 ? nodes[0] : nodes;
63
- };
63
+ }
64
64
  /**
65
65
  * Append a JSX node to a list of JSX nodes.
66
66
  * - Sets a generated `element.key` for JSX elements (based on `nodes.length`)
67
67
  * - JSX nodes can be arrays of nodes — these will be flattened directly into the nodes list.
68
68
  * - Nodes array is modified in place (not returned).
69
69
  */
70
- const appendNode = (nodes, node) => {
70
+ function appendNode(nodes, node) {
71
71
  if (!node) {
72
72
  // No need to append null, undefined, or empty string.
73
73
  return;
@@ -92,12 +92,12 @@ const appendNode = (nodes, node) => {
92
92
  // Append the node.
93
93
  nodes.push(node);
94
94
  }
95
- };
95
+ }
96
96
  /**
97
97
  * Render a JSX node
98
98
  * - Recursively renders the children of the node using the current options.
99
99
  */
100
- const renderNode = (node, options) => {
100
+ function renderNode(node, options) {
101
101
  if (typeof node === "string")
102
102
  return renderString(node, options);
103
103
  if (node instanceof Array)
@@ -109,7 +109,7 @@ const renderNode = (node, options) => {
109
109
  return node;
110
110
  }
111
111
  return node;
112
- };
112
+ }
113
113
  const REACT_SECURITY_SYMBOL = Symbol.for("react.element");
114
114
  /**
115
115
  * Parse a text string as Markdownish syntax and render it as a JSX node.
@@ -144,7 +144,9 @@ const REACT_SECURITY_SYMBOL = Symbol.for("react.element");
144
144
  *
145
145
  * @returns ReactNode, i.e. either a complete `ReactElement`, `null`, `undefined`, `string`, or an array of zero or more of those.
146
146
  */
147
- export const renderMarkup = (content, options) => renderString(cleanMarkup(content), { ...defaults, ...options });
147
+ export function renderMarkup(content, options) {
148
+ return renderString(sanitizeLines(content), { ...defaults, ...options });
149
+ }
148
150
  const defaults = {
149
151
  rules: MARKUP_RULES,
150
152
  context: "block",
@@ -156,7 +158,9 @@ const defaults = {
156
158
  * Parse a text string as user-generated markup.
157
159
  * - Like `renderMarkup()` but only enables a subset of rules and applies `rel="nofollow ugc"` to all links.
158
160
  */
159
- export const renderUgcMarkup = (content, options) => renderString(cleanMarkup(content), { ...defaultsUgc, ...options });
161
+ export function renderUgcMarkup(content, options) {
162
+ return renderString(sanitizeLines(content), { ...defaultsUgc, ...options });
163
+ }
160
164
  const defaultsUgc = {
161
165
  ...defaults,
162
166
  rules: MARKUP_RULES_UGC,
package/markup/rules.js CHANGED
@@ -7,16 +7,14 @@ const BLOCK = "[\\s\\S]*?"; // Match block of content (including newlines so don
7
7
  const BLOCK_START = "^\\n*|\\n+"; // Starts at start of a block (one or more linebreak or start of string).
8
8
  const BLOCK_END = "\\n*$|\\n\\n+"; // End of a block (two or more linebreaks or end of string).
9
9
  const BULLETS = "-*•+"; // Anything that can be a bullet (used for unordered lists and horizontal rules).
10
- const UNORDERED = `[${BULLETS}] +`; // Anything that can be a bullet (used for unordered lists and horizontal rules).
11
- const ORDERED = "[0-9]+[.):] +"; // Number for a numbered list (e.g. `1.` or `2)` or `3:`)
12
10
  const WORDS = `\\S(?:[\\s\\S]*?\\S)?`; // Run of text that starts and ends with non-space characters (possibly multi-line).
13
11
  // Regular expressions.
14
12
  const REPLACE_INDENT = /^ {1,2}/gm;
15
13
  // Regular expression makers.
16
- const createMatcher = (regexp) => content => content.match(regexp);
17
- const createBlockMatcher = (middle = BLOCK, end = BLOCK_END, start = BLOCK_START) => createMatcher(new RegExp(`(?:${start})${middle}(?:${end})`));
18
- const createLineMatcher = (middle = LINE, end = LINE_END, start = LINE_START) => createMatcher(new RegExp(`(?:${start})${middle}(?:${end})`));
19
- const createWrapMatcher = (chars, middle = WORDS) => {
14
+ const getMatcher = regexp => content => content.match(regexp);
15
+ const getBlockMatcher = (middle = BLOCK, end = BLOCK_END, start = BLOCK_START) => getMatcher(new RegExp(`(?:${start})${middle}(?:${end})`));
16
+ const getLineMatcher = (middle = LINE, end = LINE_END, start = LINE_START) => getMatcher(new RegExp(`(?:${start})${middle}(?:${end})`));
17
+ const getWrapMatcher = (chars, middle = WORDS) => {
20
18
  const regexp = new RegExp(`(${chars})(${middle})\\1`);
21
19
  return content => content.match(regexp);
22
20
  };
@@ -27,7 +25,7 @@ const createWrapMatcher = (chars, middle = WORDS) => {
27
25
  * - Markdown's underline syntax is not supported (for simplification).
28
26
  */
29
27
  const HEADING = {
30
- match: createLineMatcher(`(#{1,6}) +(${LINE})`),
28
+ match: getLineMatcher(`(#{1,6}) +(${LINE})`),
31
29
  render: ([, prefix = "", children = ""]) => ({ type: `h${prefix.length}`, key: null, props: { children } }),
32
30
  contexts: ["block"],
33
31
  childContext: "inline",
@@ -40,7 +38,7 @@ const HEADING = {
40
38
  * - Might have infinite number of spaces between the characters.
41
39
  */
42
40
  const HR = {
43
- match: createLineMatcher(`([${BULLETS}])(?: *\\1){2,}`),
41
+ match: getLineMatcher(`([${BULLETS}])(?: *\\1){2,}`),
44
42
  render: () => ({ type: "hr", key: null, props: {} }),
45
43
  contexts: ["block"],
46
44
  };
@@ -51,8 +49,9 @@ const HR = {
51
49
  * - Lists can be created with `•` bullet characters (in addition to `-` dash, `+` plus, and `*` asterisk).
52
50
  * - Second-level list can be indented with 1-2 spaces.
53
51
  */
52
+ const UNORDERED = `[${BULLETS}] +`; // Anything that can be a bullet (used for unordered lists and horizontal rules).
54
53
  const UL = {
55
- match: createBlockMatcher(`${UNORDERED}(${BLOCK})`),
54
+ match: getBlockMatcher(`${UNORDERED}(${BLOCK})`),
56
55
  render: ([, list = ""]) => {
57
56
  const children = list.split(SPLIT_UL_ITEMS).map(mapUnorderedItem);
58
57
  return { type: "ul", key: null, props: { children } };
@@ -70,8 +69,9 @@ const mapUnorderedItem = (item, key) => {
70
69
  * - No leading spaces are allowed for the top-level list.
71
70
  * - Second-level list can be indented with 1-3 spaces.
72
71
  */
72
+ const ORDERED = "[0-9]+[.):] +"; // Number for a numbered list (e.g. `1.` or `2)` or `3:`)
73
73
  const OL = {
74
- match: createBlockMatcher(`(${ORDERED}${BLOCK})`),
74
+ match: getBlockMatcher(`(${ORDERED}${BLOCK})`),
75
75
  render: ([, list = ""]) => {
76
76
  const children = list.split(SPLIT_OL_ITEMS).map(mapOrderedItem);
77
77
  return { type: "ol", key: null, props: { children } };
@@ -96,7 +96,7 @@ const mapOrderedItem = (item, key) => {
96
96
  * - Quote indent symbol can be followed by zero or more spaces.
97
97
  */
98
98
  const BLOCKQUOTE = {
99
- match: createLineMatcher(`(>${LINE}(?:\\n>${LINE})*)`),
99
+ match: getLineMatcher(`(>${LINE}(?:\\n>${LINE})*)`),
100
100
  render: ([, quote = ""]) => ({
101
101
  type: "blockquote",
102
102
  key: null,
@@ -115,7 +115,7 @@ const BLOCKQUOTE_LINES = /^>/gm;
115
115
  */
116
116
  const FENCED = {
117
117
  // Matcher has its own end that only stops when it reaches a matching closing fence or the end of the string.
118
- match: createBlockMatcher(`(\`{3,}|~{3,}) *(${LINE})\\n(${BLOCK})`, `\\n\\1\\n+|\\n\\1$|$`),
118
+ match: getBlockMatcher(`(\`{3,}|~{3,}) *(${LINE})\\n(${BLOCK})`, `\\n\\1\\n+|\\n\\1$|$`),
119
119
  render: ([, , file, children]) => ({
120
120
  type: "pre",
121
121
  key: null,
@@ -134,7 +134,7 @@ const FENCED = {
134
134
  * - When ordering rules, paragraph should go after other "block" context elements (because it has a very generous capture).
135
135
  */
136
136
  const PARAGRAPH = {
137
- match: createBlockMatcher(` *(${BLOCK})`),
137
+ match: getBlockMatcher(` *(${BLOCK})`),
138
138
  render: ([, children]) => ({ type: `p`, key: null, props: { children } }),
139
139
  contexts: ["block"],
140
140
  childContext: "inline",
@@ -205,7 +205,7 @@ const MATCH_AUTOLINK = /([a-z][a-z0-9-]*[a-z0-9]:\S+)(?: +(?:\(([^)]*?)\)|\[([^\
205
205
  * - Same as Markdown syntax.
206
206
  */
207
207
  const CODE = {
208
- match: createWrapMatcher("`+", BLOCK),
208
+ match: getWrapMatcher("`+", BLOCK),
209
209
  render: ([, , children]) => ({ type: "code", key: null, props: { children } }),
210
210
  contexts: ["inline", "list"],
211
211
  priority: 10, // Higher priority than other inlines so it matches first before e.g. `strong` or `em` (from CommonMark spec: "Code span backticks have higher precedence than any other inline constructs except HTML tags and autolinks.")
@@ -219,7 +219,7 @@ const CODE = {
219
219
  * - Different to Markdown: strong is always surrounded by `*asterisks*` and emphasis is always surrounded by `_underscores_` (strong isn't 'double emphasis').
220
220
  */
221
221
  const STRONG = {
222
- match: createWrapMatcher("\\*+"),
222
+ match: getWrapMatcher("\\*+"),
223
223
  render: ([, , children]) => ({ type: "strong", key: null, props: { children } }),
224
224
  contexts: ["inline", "list", "link"],
225
225
  childContext: "inline",
@@ -233,7 +233,7 @@ const STRONG = {
233
233
  * - Different to Markdown: strong is always surrounded by `*asterisks*` and emphasis is always surrounded by `_underscores_` (strong isn't 'double emphasis').
234
234
  */
235
235
  const EM = {
236
- match: createWrapMatcher("_+"),
236
+ match: getWrapMatcher("_+"),
237
237
  render: ([, , children]) => ({ type: "em", key: null, props: { children } }),
238
238
  contexts: ["inline", "list", "link"],
239
239
  childContext: "inline",
@@ -247,7 +247,7 @@ const EM = {
247
247
  * - Markdown doesn't have this.
248
248
  */
249
249
  const INS = {
250
- match: createWrapMatcher("\\++"),
250
+ match: getWrapMatcher("\\++"),
251
251
  render: ([, , children]) => ({ type: "ins", key: null, props: { children } }),
252
252
  contexts: ["inline", "list", "link"],
253
253
  childContext: "inline",
@@ -261,7 +261,7 @@ const INS = {
261
261
  * - Markdown doesn't have this.
262
262
  */
263
263
  const DEL = {
264
- match: createWrapMatcher("~+"),
264
+ match: getWrapMatcher("~+"),
265
265
  render: ([, , children]) => ({ type: "del", key: null, props: { children } }),
266
266
  contexts: ["inline", "list", "link"],
267
267
  childContext: "inline",
@@ -275,7 +275,7 @@ const DEL = {
275
275
  * - This works better with textareas that wrap text (since manually breaking up long lines is no longer necessary).
276
276
  */
277
277
  const BR = {
278
- match: createMatcher(/\n/),
278
+ match: getMatcher(/\n/),
279
279
  render: () => ({ type: "br", key: null, props: {} }),
280
280
  contexts: ["inline", "list", "link"],
281
281
  childContext: "inline",
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.29.0",
14
+ "version": "1.30.3",
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",
package/react/index.d.ts CHANGED
@@ -7,4 +7,5 @@ export * from "./useFetch.js";
7
7
  export * from "./useDocument.js";
8
8
  export * from "./useDocumentData.js";
9
9
  export * from "./useQuery.js";
10
+ export * from "./useQueryFirst.js";
10
11
  export * from "./usePagination.js";
package/react/index.js CHANGED
@@ -7,4 +7,5 @@ export * from "./useFetch.js";
7
7
  export * from "./useDocument.js";
8
8
  export * from "./useDocumentData.js";
9
9
  export * from "./useQuery.js";
10
+ export * from "./useQueryFirst.js";
10
11
  export * from "./usePagination.js";
@@ -14,7 +14,7 @@ import { DataDocument, Data } from "../index.js";
14
14
  * @trhows `Error` if a `CacheProvider` is not part of the database's provider chain.
15
15
  * @throws `Error` if there was a problem retrieving the data.
16
16
  */
17
- export declare function useAsyncDocumentData<T extends Data>(ref: DataDocument<T>, maxAge?: number | true): T | Promise<T>;
17
+ export declare function useAsyncDocumentData<T extends Data>(ref: DataDocument<T>, maxAge?: number | true): T | PromiseLike<T>;
18
18
  export declare function useAsyncDocumentData<T extends Data>(ref: DataDocument<T> | undefined, maxAge?: number | true): T | PromiseLike<T> | undefined;
19
19
  /**
20
20
  * Use the cached data of a document in a React component.
@@ -0,0 +1,36 @@
1
+ import { DataQuery, Data, Entry } from "../index.js";
2
+ /**
3
+ * Use the cached data of a document in a React component (or a `Promise` to indicate the data is still loading).
4
+ * - Requires database to use `CacheProvider` and will error if this does not exist.
5
+ *
6
+ * @param ref Query reference or `undefined`.
7
+ * - If `undefined` is set this function will always return `undefined` (this simplifies scenarios where no document is needed, as hooks must always be called in the same order).
8
+ * @param maxAge How 'out of date' data is allowed to be before it'll be refetched.
9
+ * - If `maxAge` is true, a realtime subscription to the data will be created for the lifetime of the component.
10
+ *
11
+ * @returns The data of the document, or `Promise` that resolves when the data has loaded.
12
+ *
13
+ * @throws `RequiredError` if the query had no results.
14
+ * @trhows `Error` if a `CacheProvider` is not part of the database's provider chain.
15
+ * @throws `Error` if there was a problem retrieving the data.
16
+ */
17
+ export declare function useAsyncQueryFirst<T extends Data>(ref: DataQuery<T>, maxAge?: number | true): Entry<T> | PromiseLike<Entry<T>>;
18
+ export declare function useAsyncQueryFirst<T extends Data>(ref: DataQuery<T> | undefined, maxAge?: number | true): Entry<T> | PromiseLike<Entry<T>> | undefined;
19
+ /**
20
+ * Use the cached data of a document in a React component.
21
+ * - Requires database to use `CacheProvider` and will error if this does not exist.
22
+ *
23
+ * @param ref Query reference or `undefined`.
24
+ * - If `undefined` is set this function will always return `undefined` (this simplifies scenarios where no document is needed, as hooks must always be called in the same order).
25
+ * @param maxAge How 'out of date' data is allowed to be before it'll be refetched.
26
+ * - If `maxAge` is true, a realtime subscription to the data will be created for the lifetime of the component.
27
+ *
28
+ * @returns The data of the document.
29
+ *
30
+ * @throws `Promise` that resolves when the data has loaded.
31
+ * @throws `RequiredError` if the query had no results.
32
+ * @trhows `Error` if a `CacheProvider` is not part of the database's provider chain.
33
+ * @throws `Error` if there was a problem retrieving the data.
34
+ */
35
+ export declare function useQueryFirst<T extends Data>(ref: DataQuery<T>, maxAge?: number | true): Entry<T>;
36
+ export declare function useQueryFirst<T extends Data>(ref: DataQuery<T> | undefined, maxAge?: number | true): Entry<T> | undefined;
@@ -0,0 +1,9 @@
1
+ import { callAsync, getQueryFirst, throwAsync } from "../index.js";
2
+ import { useAsyncQuery } from "./useQuery.js";
3
+ export function useAsyncQueryFirst(ref, maxAge) {
4
+ const results = useAsyncQuery(ref, maxAge);
5
+ return ref && results ? callAsync(getQueryFirst, results, ref) : undefined;
6
+ }
7
+ export function useQueryFirst(ref, maxAge) {
8
+ return throwAsync(useAsyncQueryFirst(ref, maxAge));
9
+ }
@@ -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!` */
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;
package/util/data.d.ts CHANGED
@@ -134,3 +134,11 @@ export declare type DeepMutable<T extends Data> = {
134
134
  export declare type DeepReadonly<T extends Data> = {
135
135
  +readonly [K in keyof T]: T[K] extends Data ? DeepReadonly<T[K]> : T[K];
136
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/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
@@ -29,51 +29,53 @@ export declare function toTitle(value: unknown): string;
29
29
  * - Stripping control characters.
30
30
  * - Normalising all space characters to " " space.
31
31
  *
32
- * @param dirty The dirty input string.
32
+ * @param str The dirty input 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: (str: 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: (str: 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-]`
58
58
  * - Change all spaces/separators/hyphens/dashes/underscores to `-` single hyphen.
59
59
  */
60
- export declare const toSlug: (value: string) => string;
60
+ export declare const toSlug: (str: string) => string;
61
61
  /**
62
62
  * Split a string into its separate words.
63
63
  * - Words enclosed "in quotes" are a single word.
64
64
  * - Performs no processing on the words, so control chars, punctuation, symbols, and case are all preserved.
65
65
  *
66
- * @param value The input string, e.g. `yellow dog "Golden Retriever"`
66
+ * @param str The input string, e.g. `yellow dog "Golden Retriever"`
67
67
  * @returns Array of the found words, e.g. `["yellow", "dog", "Golden Retriever"
68
68
  */
69
- export declare const toWords: (value: string) => ImmutableArray<string>;
69
+ export declare const toWords: (str: 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
  *
73
- * @param value The input string.
75
+ * @param str The input string.
74
76
  * @param flags RegExp flags that are passed into the created RegExp.
75
77
  */
76
- export declare const toRegExp: (value: string, flags?: string) => RegExp;
78
+ export declare const toRegExp: (str: string, flags?: string) => RegExp;
77
79
  /** Escape special characters in a string regular expression. */
78
80
  export declare const escapeRegExp: (str: string) => string;
79
81
  /** Is the first character of a string an uppercase letter? */
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
  /**
@@ -66,69 +65,70 @@ export function toTitle(value) {
66
65
  * - Stripping control characters.
67
66
  * - Normalising all space characters to " " space.
68
67
  *
69
- * @param dirty The dirty input string.
68
+ * @param str The dirty input string.
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 = (str) => str.replace(SANE_SPACES, " ").replace(SANE_STRIP, "").trim();
73
+ const SANE_SPACES = /\s+/g; // Runs of spaces.
74
+ const SANE_STRIP = /[\x00-\x1F\x7F-\x9F]+/g; // Control characters.
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 = (str) => str.replace("\u2029", "\n\n").replace(LINE_SPACES, " ").replace(LINE_STRIP, "").replace(LINE_ENDS, "").replace(LINE_START, "").replace(LINE_BREAKS, "\n\n");
80
+ const LINE_SPACES = /[^\S \t\n]/g; // Spaces except tab and newline.
81
+ const LINE_STRIP = /[\x00-\x08\x0B-\x1F\x7F-\x9F]+/g; // Control characters (except tab and newline).
82
+ const LINE_ENDS = /[ \t]+$/gm; // Spaces and tabs at ends of lines.
83
+ const LINE_START = /^\n+|\n+$/g; // Newlines at start and end of string.
84
+ const LINE_BREAKS = /\n\n\n+/g; // Three or more linebreaks in a row.
90
85
  /**
91
86
  * Normalize a string so it can be compared to another string (free from upper/lower cases, symbols, punctuation).
92
87
  *
93
88
  * Does the following:
94
- * - Santize the string to remove control characters.
89
+ * - Removes control characters.
95
90
  * - Remove symbols (e.g. `$` dollar) and punctuation (e.g. `"` double quote).
96
91
  * - Remove marks (e.g. the umlout dots above `ö`).
97
92
  * - Convert spaces/separators to " " single space (e.g. line breaks, non-breaking space).
98
93
  * - Convert to lowercase and trim excess whitespace.
99
94
  *
100
- * @example normalizeString("Däve-is REALLY éxcitable—apparęntly!!! 😂"); // Returns "dave is really excitable apparently"
95
+ * @example normalizeString("Däve-is\nREALLY éxcitable—apparęntly!!! 😂"); // Returns "dave is really excitable apparently"
101
96
  */
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;
97
+ export const normalizeString = (str) => sanitizeString(str.normalize("NFD").replace(NORMAL_STRIP, "").toLowerCase());
98
+ const NORMAL_STRIP = /[^\p{L}\p{N}\s]+/gu; // Anything except letters, numbers, spaces.
105
99
  /**
106
100
  * Convert a string to a `kebab-case` URL slug.
107
101
  * - Remove any characters not in the range `[a-z0-9-]`
108
102
  * - Change all spaces/separators/hyphens/dashes/underscores to `-` single hyphen.
109
103
  */
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.
104
+ export const toSlug = (str) => str.toLowerCase().normalize("NFD").replace(SLUG_HYPHENS, "-").replace(SLUG_STRIP, "");
105
+ const SLUG_HYPHENS = /[\s\-–—_]+/gu; // Runs of spaces and hyphens.
106
+ const SLUG_STRIP = /^[^a-z0-9]+|[^a-z0-9]+$|[^a-z0-9-]+/g; // Non-alphanumeric or hyphen anywhere, or non-alphanumeric at start and end.
114
107
  /**
115
108
  * Split a string into its separate words.
116
109
  * - Words enclosed "in quotes" are a single word.
117
110
  * - Performs no processing on the words, so control chars, punctuation, symbols, and case are all preserved.
118
111
  *
119
- * @param value The input string, e.g. `yellow dog "Golden Retriever"`
112
+ * @param str The input string, e.g. `yellow dog "Golden Retriever"`
120
113
  * @returns Array of the found words, e.g. `["yellow", "dog", "Golden Retriever"
121
114
  */
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;
124
- const MATCH_WORD = /[^\s"]+|"([^"]*)"/g;
115
+ export const toWords = (str) => Array.from(yieldWords(str));
116
+ /** Find and iterate over the words in a string. */
117
+ export function* yieldWords(value) {
118
+ for (const matches of value.matchAll(MATCH_WORD)) {
119
+ const str = matches[1] || matches[0];
120
+ if (str)
121
+ yield str;
122
+ }
123
+ }
124
+ const MATCH_WORD = /[^\s"]+|"([^"]*)"/g; // Runs of characters without spaces, or "quoted phrases"
125
125
  /**
126
126
  * Convert a string to a regular expression that matches that string.
127
127
  *
128
- * @param value The input string.
128
+ * @param str The input string.
129
129
  * @param flags RegExp flags that are passed into the created RegExp.
130
130
  */
131
- export const toRegExp = (value, flags = "") => new RegExp(escapeRegExp(value), flags);
131
+ export const toRegExp = (str, flags = "") => new RegExp(escapeRegExp(str), flags);
132
132
  /** Escape special characters in a string regular expression. */
133
133
  export const escapeRegExp = (str) => str.replace(REPLACE_ESCAPED, "\\$&");
134
134
  const REPLACE_ESCAPED = /[-[\]/{}()*+?.\\^$|]/g;