shelving 1.69.1 → 1.71.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 +2 -2
- package/error/ValidationError.js +2 -1
- package/feedback/Feedback.d.ts +4 -16
- package/feedback/Feedback.js +8 -31
- package/markup/index.d.ts +2 -1
- package/markup/index.js +2 -1
- package/markup/options.d.ts +16 -0
- package/markup/options.js +9 -0
- package/markup/regexp.d.ts +39 -0
- package/markup/regexp.js +50 -0
- package/markup/render.d.ts +4 -4
- package/markup/render.js +95 -123
- package/markup/rules.d.ts +117 -61
- package/markup/rules.js +178 -182
- package/package.json +2 -2
- package/query/Filter.d.ts +1 -1
- package/query/Filter.js +3 -2
- package/react/useDocument.js +3 -4
- package/react/useQuery.js +3 -4
- package/schema/AllowSchema.js +4 -4
- package/schema/ArraySchema.js +2 -2
- package/schema/LinkSchema.js +2 -2
- package/schema/index.d.ts +0 -1
- package/schema/index.js +0 -1
- package/test/util.d.ts +3 -2
- package/update/DataUpdate.js +4 -6
- package/util/array.d.ts +1 -2
- package/util/array.js +7 -5
- package/util/clone.js +2 -2
- package/util/color.js +3 -2
- package/util/data.d.ts +13 -2
- package/util/data.js +19 -0
- package/util/date.d.ts +17 -16
- package/util/date.js +4 -3
- package/util/debug.d.ts +15 -1
- package/util/debug.js +62 -22
- package/util/equal.d.ts +2 -2
- package/util/equal.js +12 -7
- package/util/filter.d.ts +0 -4
- package/util/filter.js +0 -6
- package/util/function.d.ts +2 -0
- package/util/hydrate.js +8 -7
- package/util/index.d.ts +1 -1
- package/util/index.js +1 -1
- package/util/iterate.d.ts +1 -1
- package/util/jsx.d.ts +7 -10
- package/util/jsx.js +15 -13
- package/util/map.d.ts +12 -21
- package/util/map.js +13 -9
- package/util/match.js +4 -3
- package/util/number.d.ts +2 -2
- package/util/number.js +5 -1
- package/util/object.d.ts +12 -8
- package/util/object.js +13 -12
- package/util/regexp.d.ts +39 -0
- package/util/regexp.js +42 -0
- package/util/set.d.ts +14 -0
- package/util/set.js +17 -0
- package/util/sort.d.ts +1 -1
- package/util/string.d.ts +43 -33
- package/util/string.js +72 -76
- package/util/template.d.ts +9 -4
- package/util/template.js +14 -20
- package/util/timeout.js +3 -3
- package/util/transform.d.ts +10 -6
- package/util/transform.js +5 -1
- package/util/units.d.ts +8 -8
- package/util/url.d.ts +7 -5
- package/util/url.js +17 -14
- package/util/validate.js +6 -8
- package/markup/types.d.ts +0 -44
- package/markup/types.js +0 -1
- package/schema/MapSchema.d.ts +0 -19
- package/schema/MapSchema.js +0 -29
- package/util/search.d.ts +0 -71
- package/util/search.js +0 -97
package/api/Resource.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { validate } from "../util/validate.js";
|
|
2
2
|
import { getUndefined } from "../util/undefined.js";
|
|
3
|
-
import {
|
|
3
|
+
import { isFeedback } from "../feedback/Feedback.js";
|
|
4
4
|
import { ValidationError } from "../error/ValidationError.js";
|
|
5
5
|
/**
|
|
6
6
|
* An abstract API resource definition, used to specify types for e.g. serverless functions..
|
|
@@ -34,7 +34,7 @@ export class Resource {
|
|
|
34
34
|
return validate(unsafeResult, this.result);
|
|
35
35
|
}
|
|
36
36
|
catch (thrown) {
|
|
37
|
-
throw thrown
|
|
37
|
+
throw isFeedback(thrown) ? new ValidationError(`Invalid result for resource`, thrown) : thrown;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
package/error/ValidationError.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { debug } from "../util/debug.js";
|
|
1
2
|
/** Thrown if a value isn't valid. */
|
|
2
3
|
export class ValidationError extends Error {
|
|
3
4
|
constructor(message, feedback) {
|
|
4
|
-
super(`${message}:\n${feedback.
|
|
5
|
+
super(`${message}:\n${feedback.message} (received ${debug(feedback.details)})`);
|
|
5
6
|
this.feedback = feedback;
|
|
6
7
|
}
|
|
7
8
|
}
|
package/feedback/Feedback.d.ts
CHANGED
|
@@ -12,28 +12,16 @@ import type { ImmutableObject } from "../util/object.js";
|
|
|
12
12
|
*/
|
|
13
13
|
export declare class Feedback {
|
|
14
14
|
/** String feedback message that is safe to show to a user. */
|
|
15
|
-
readonly
|
|
15
|
+
readonly message: string;
|
|
16
16
|
/** Nested details providing deeper feedback. */
|
|
17
17
|
readonly details: ImmutableObject;
|
|
18
18
|
constructor(feedback: string, details?: ImmutableObject);
|
|
19
19
|
/**
|
|
20
20
|
* Map details to a set of string messages.
|
|
21
|
-
* - If a detail is another `Feedback` instance, return its
|
|
21
|
+
* - If a detail is another `Feedback` instance, return its `.message` string.
|
|
22
22
|
* - If a detail is anything else, convert it to string using `toString()`
|
|
23
23
|
*/
|
|
24
24
|
get messages(): ImmutableObject<string>;
|
|
25
|
-
/**
|
|
26
|
-
* Convert to string (equivalent to `message.details`).
|
|
27
|
-
* Returns a string including the main message string and a deeply nested list of child message strings.
|
|
28
|
-
*
|
|
29
|
-
* > Invalid format
|
|
30
|
-
* > - name: Invalid format
|
|
31
|
-
* > - first: Must be string
|
|
32
|
-
* > - value: 123
|
|
33
|
-
* > - last: Must be string
|
|
34
|
-
* > - value: true
|
|
35
|
-
* > - age: Must be number
|
|
36
|
-
* > - value: "abc"
|
|
37
|
-
*/
|
|
38
|
-
toString(): string;
|
|
39
25
|
}
|
|
26
|
+
/** Is an unknown value a `Feedback` instance? */
|
|
27
|
+
export declare const isFeedback: <T extends Feedback>(v: unknown) => v is Feedback;
|
package/feedback/Feedback.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { getString } from "../util/string.js";
|
|
2
|
+
import { mapObject } from "../util/transform.js";
|
|
3
3
|
/**
|
|
4
4
|
* The `Feedback` class represents a feedback message that should be shown to the user.
|
|
5
5
|
* - Basic `Feedback` is neither good nor bad, `SuccessFeedback` indicates good news, and `ErrorFeedback` indicates bad news.
|
|
@@ -13,41 +13,18 @@ import { getTitle } from "../util/string.js";
|
|
|
13
13
|
*/
|
|
14
14
|
export class Feedback {
|
|
15
15
|
constructor(feedback, details = {}) {
|
|
16
|
-
this.
|
|
16
|
+
this.message = feedback;
|
|
17
17
|
this.details = details;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Map details to a set of string messages.
|
|
21
|
-
* - If a detail is another `Feedback` instance, return its
|
|
21
|
+
* - If a detail is another `Feedback` instance, return its `.message` string.
|
|
22
22
|
* - If a detail is anything else, convert it to string using `toString()`
|
|
23
23
|
*/
|
|
24
24
|
get messages() {
|
|
25
|
-
|
|
26
|
-
for (const [k, v] of Object.entries(this.details)) {
|
|
27
|
-
if (v instanceof Feedback)
|
|
28
|
-
messages[k] = v.feedback;
|
|
29
|
-
else
|
|
30
|
-
messages[k] = getTitle(v);
|
|
31
|
-
}
|
|
32
|
-
return messages;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Convert to string (equivalent to `message.details`).
|
|
36
|
-
* Returns a string including the main message string and a deeply nested list of child message strings.
|
|
37
|
-
*
|
|
38
|
-
* > Invalid format
|
|
39
|
-
* > - name: Invalid format
|
|
40
|
-
* > - first: Must be string
|
|
41
|
-
* > - value: 123
|
|
42
|
-
* > - last: Must be string
|
|
43
|
-
* > - value: true
|
|
44
|
-
* > - age: Must be number
|
|
45
|
-
* > - value: "abc"
|
|
46
|
-
*/
|
|
47
|
-
toString() {
|
|
48
|
-
let output = this.feedback;
|
|
49
|
-
for (const [k, v] of Object.entries(this.details))
|
|
50
|
-
output += `\n- ${k}: ${v instanceof Feedback ? v.toString().replace(/\n/g, "\n ") : debug(v)}`;
|
|
51
|
-
return output;
|
|
25
|
+
return mapObject(this.details, _getMessage);
|
|
52
26
|
}
|
|
53
27
|
}
|
|
28
|
+
const _getMessage = (v) => (isFeedback(v) ? v.message : getString(v));
|
|
29
|
+
/** Is an unknown value a `Feedback` instance? */
|
|
30
|
+
export const isFeedback = (v) => v instanceof Feedback;
|
package/markup/index.d.ts
CHANGED
package/markup/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MarkupRules } from "./rules.js";
|
|
2
|
+
/** The current parsing options (represents the current state of the parsing). */
|
|
3
|
+
export declare type MarkupOptions = {
|
|
4
|
+
/** The active list of parsing rules. */
|
|
5
|
+
readonly rules: MarkupRules;
|
|
6
|
+
/** The initial context to start parsing in (rules may render their children with a different context). */
|
|
7
|
+
readonly context: string;
|
|
8
|
+
/** Set the base URL that any relative links will be relative to (defaults to `window.location.href`, if undefined then relative links won't work). */
|
|
9
|
+
readonly url: string | undefined;
|
|
10
|
+
/** Set the `rel=""` property used for any links (e.g. `rel="nofollow ugc"`). */
|
|
11
|
+
readonly rel: string | undefined;
|
|
12
|
+
/** Valid URL schemes/protocols for links (including trailing commas), defaults to `[`http:`, `https:`]` */
|
|
13
|
+
readonly schemes: string[];
|
|
14
|
+
};
|
|
15
|
+
/** Default options */
|
|
16
|
+
export declare const MARKUP_OPTIONS: MarkupOptions;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Data } from "../util/data.js";
|
|
2
|
+
import { PossibleRegExp } from "../util/regexp.js";
|
|
3
|
+
import type { MarkupOptions } from "./options.js";
|
|
4
|
+
/** Subset of `NamedRegExpArray<T>` that are the only things we're required return from a `MarkupMatcher` function. */
|
|
5
|
+
export declare type MarkupMatch<T extends Data | undefined = Data | undefined> = {
|
|
6
|
+
0: string;
|
|
7
|
+
index: number;
|
|
8
|
+
groups: T;
|
|
9
|
+
};
|
|
10
|
+
/** Function that matches a string and returns a `MarkupMatch` or `null` or `void` */
|
|
11
|
+
export declare type MarkupMatcher<T extends Data | undefined = Data | undefined> = (input: string, options: MarkupOptions) => MarkupMatch<T> | null | void;
|
|
12
|
+
export declare const LINE_REGEXP: RegExp;
|
|
13
|
+
export declare const LINE_START_REGEXP: RegExp;
|
|
14
|
+
export declare const LINE_END_REGEXP: RegExp;
|
|
15
|
+
export declare const BLOCK_REGEXP: RegExp;
|
|
16
|
+
export declare const BLOCK_START_REGEXP: RegExp;
|
|
17
|
+
export declare const BLOCK_END_REGEXP: RegExp;
|
|
18
|
+
/** Create regular expression that matches a block of content. */
|
|
19
|
+
export declare function getBlockRegExp(content?: PossibleRegExp, end?: PossibleRegExp, start?: PossibleRegExp): RegExp;
|
|
20
|
+
/** Create regular expression that matches a line of content. */
|
|
21
|
+
export declare function getLineRegExp(content?: PossibleRegExp, end?: PossibleRegExp, start?: PossibleRegExp): RegExp;
|
|
22
|
+
/**
|
|
23
|
+
* Regular expression that only matches complete its pattern if it's a complete word.
|
|
24
|
+
* - Won't match if there are letters or numbers directly before/after the matched content.
|
|
25
|
+
* - Will match if there is punctuation before/after the matched content or it is at the start/end of the string.
|
|
26
|
+
* - e.g. `this` and `"this"` and `that this that` and `that (this) that` will match because `this` is a complete word.
|
|
27
|
+
* - e.g. `thatthis` and `thatthisthat` will not because `this` is only part of a complete word.
|
|
28
|
+
*
|
|
29
|
+
* @note This isn't guaranteed to work with `String.prototype.match()` and `String.prototype.replace()`
|
|
30
|
+
*
|
|
31
|
+
* @todo This can be much less complicated when Safari supports lookbehinds in regular expressions.
|
|
32
|
+
* - We use a negative lookahead for the end of the word and it works great.
|
|
33
|
+
* - If we could use a negative lookbehind for the start of the word we wouldn't need to create a function that offsets the start.
|
|
34
|
+
*/
|
|
35
|
+
export declare class WordRegExp extends RegExp {
|
|
36
|
+
constructor(pattern: string);
|
|
37
|
+
exec(input: string): RegExpExecArray | null;
|
|
38
|
+
test(input: string): boolean;
|
|
39
|
+
}
|
package/markup/regexp.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getRegExpSource } from "../util/regexp.js";
|
|
2
|
+
// Regular expressions.
|
|
3
|
+
export const LINE_REGEXP = /[^\n]*/; // Match line of content (anything that's not a newline).
|
|
4
|
+
export const LINE_START_REGEXP = /^\n*|\n+/; // Starts at start of line (one or more linebreak or start of string).
|
|
5
|
+
export const LINE_END_REGEXP = /\n+|$/; // Ends at end of line (one or more linebreak or end of string).
|
|
6
|
+
export const BLOCK_REGEXP = /[\s\S]*?/; // Match block of content (including newlines so don't be greedy).
|
|
7
|
+
export const BLOCK_START_REGEXP = /^\n*|\n+/; // Starts at start of a block (one or more linebreak or start of string).
|
|
8
|
+
export const BLOCK_END_REGEXP = /\n*$|\n\n+/; // End of a block (two or more linebreaks or end of string).
|
|
9
|
+
/** Create regular expression that matches a block of content. */
|
|
10
|
+
export function getBlockRegExp(content = BLOCK_REGEXP, end = BLOCK_END_REGEXP, start = BLOCK_START_REGEXP) {
|
|
11
|
+
return new RegExp(`(?:${getRegExpSource(start)})(?:${getRegExpSource(content)})(?:${getRegExpSource(end)})`);
|
|
12
|
+
}
|
|
13
|
+
/** Create regular expression that matches a line of content. */
|
|
14
|
+
export function getLineRegExp(content = LINE_REGEXP, end = LINE_END_REGEXP, start = LINE_START_REGEXP) {
|
|
15
|
+
return new RegExp(`(?:${getRegExpSource(start)})(?:${getRegExpSource(content)})(?:${getRegExpSource(end)})`);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Regular expression that only matches complete its pattern if it's a complete word.
|
|
19
|
+
* - Won't match if there are letters or numbers directly before/after the matched content.
|
|
20
|
+
* - Will match if there is punctuation before/after the matched content or it is at the start/end of the string.
|
|
21
|
+
* - e.g. `this` and `"this"` and `that this that` and `that (this) that` will match because `this` is a complete word.
|
|
22
|
+
* - e.g. `thatthis` and `thatthisthat` will not because `this` is only part of a complete word.
|
|
23
|
+
*
|
|
24
|
+
* @note This isn't guaranteed to work with `String.prototype.match()` and `String.prototype.replace()`
|
|
25
|
+
*
|
|
26
|
+
* @todo This can be much less complicated when Safari supports lookbehinds in regular expressions.
|
|
27
|
+
* - We use a negative lookahead for the end of the word and it works great.
|
|
28
|
+
* - If we could use a negative lookbehind for the start of the word we wouldn't need to create a function that offsets the start.
|
|
29
|
+
*/
|
|
30
|
+
export class WordRegExp extends RegExp {
|
|
31
|
+
constructor(pattern) {
|
|
32
|
+
super(`(?<lookbehind>^|[^\\p{L}\\p{N}])${pattern}(?![\\p{L}\\p{N}])`);
|
|
33
|
+
}
|
|
34
|
+
exec(input) {
|
|
35
|
+
var _a;
|
|
36
|
+
const match = super.exec(input);
|
|
37
|
+
if (match) {
|
|
38
|
+
const { 0: zero, groups } = match;
|
|
39
|
+
const offset = ((_a = groups === null || groups === void 0 ? void 0 : groups.lookbehind) === null || _a === void 0 ? void 0 : _a.length) || 0;
|
|
40
|
+
if (zero && offset) {
|
|
41
|
+
match[0] = zero.slice(offset); // Slice off the start of the match to remove the matched first character.
|
|
42
|
+
match.index += offset; // Increment the index to remove the matched first character.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return match;
|
|
46
|
+
}
|
|
47
|
+
test(input) {
|
|
48
|
+
return !!this.exec(input);
|
|
49
|
+
}
|
|
50
|
+
}
|
package/markup/render.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSXNode } from "../util/jsx.js";
|
|
2
|
-
import
|
|
2
|
+
import { MarkupOptions } from "./options.js";
|
|
3
3
|
/**
|
|
4
4
|
* Parse a text string as Markdownish syntax and render it as a JSX node.
|
|
5
5
|
* - Syntax is not defined by this code, but by the rules supplied to it.
|
|
@@ -28,9 +28,9 @@ import type { MarkupOptions } from "./types.js";
|
|
|
28
28
|
* - If the first thing in the definition is a URL, then it's recognised as a link reference (and produces an `<a href=""></a>`)
|
|
29
29
|
* - If the first thing in the definition isn't a URL, then it's recognised as a sidenote/footnote and tapping it will scroll you to that point (and popup the definition like Marco Arment's Bigfoot code).
|
|
30
30
|
*
|
|
31
|
-
* @param
|
|
31
|
+
* @param input The string content possibly containing markup syntax, e.g. "This is a *bold* string.
|
|
32
32
|
* @param options An options object for the render.
|
|
33
33
|
*
|
|
34
|
-
* @returns
|
|
34
|
+
* @returns JSXNode, i.e. either a complete `JSXElement`, `null`, `undefined`, `string`, or an array of zero or more of those.
|
|
35
35
|
*/
|
|
36
|
-
export declare function renderMarkup(
|
|
36
|
+
export declare function renderMarkup(input: string, options?: Partial<MarkupOptions>): JSXNode;
|
package/markup/render.js
CHANGED
|
@@ -1,116 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-param-reassign */
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
/** Convert a string into an array of React nodes using a set of rules. */
|
|
5
|
-
function renderString(content, options) {
|
|
6
|
-
// If there's no context return the unmodified string.
|
|
7
|
-
if (!options.context)
|
|
8
|
-
return content;
|
|
9
|
-
const nodes = [];
|
|
10
|
-
// Loop until we've parsed the entire string.
|
|
11
|
-
while (content.length) {
|
|
12
|
-
// Loop through all rules in the list and see if any match.
|
|
13
|
-
let matchedPriority = Number.MIN_SAFE_INTEGER;
|
|
14
|
-
let matchedIndex = Number.MAX_SAFE_INTEGER;
|
|
15
|
-
let matchedRule = undefined;
|
|
16
|
-
let matchedResult = undefined;
|
|
17
|
-
for (const rule of options.rules) {
|
|
18
|
-
const { priority = 0, match, regexp, contexts } = rule;
|
|
19
|
-
// Only apply this rule if both:
|
|
20
|
-
// 1. The priority is equal or higher to the current priority.
|
|
21
|
-
// 2. The rule is allowed in the current context.
|
|
22
|
-
if (priority >= matchedPriority && contexts.includes(options.context)) {
|
|
23
|
-
const result = match ? match(content, options) : regexp ? content.match(regexp) : null;
|
|
24
|
-
// If this matched and has an index (it might not if it's a `/g` global RegExp, which would be a mistake).
|
|
25
|
-
if (result && typeof result.index === "number") {
|
|
26
|
-
const index = result.index;
|
|
27
|
-
// Only match the rule if either:
|
|
28
|
-
// 1. The index is lower than the previous index then this rule takes priority.
|
|
29
|
-
// 2. The priority is higher than the previous match then this rule takes priority.
|
|
30
|
-
if (index < matchedIndex || priority > matchedPriority) {
|
|
31
|
-
matchedRule = rule;
|
|
32
|
-
matchedResult = result;
|
|
33
|
-
matchedIndex = index;
|
|
34
|
-
matchedPriority = priority;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
// Did at least one rule match?
|
|
40
|
-
if (matchedRule && matchedResult && matchedResult[0]) {
|
|
41
|
-
// If index is more than zero, then add a string node before this one.
|
|
42
|
-
if (matchedIndex) {
|
|
43
|
-
const prefix = content.slice(0, matchedIndex);
|
|
44
|
-
appendNode(nodes, renderString(prefix, options));
|
|
45
|
-
}
|
|
46
|
-
// Call the rule's `render()` function to generate the node.
|
|
47
|
-
const childOptions = { ...options, context: matchedRule.childContext };
|
|
48
|
-
const element = matchedRule.render(matchedResult, childOptions);
|
|
49
|
-
appendNode(nodes, renderNode(element, childOptions));
|
|
50
|
-
// Decrement the content.
|
|
51
|
-
content = content.slice(matchedIndex + matchedResult[0].length);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
// If nothing else matched add the rest of the string as a node.
|
|
55
|
-
// Don't need to push the string through `renderNode()` because we already know it doesn't match any rules in the current context.
|
|
56
|
-
nodes.push(content);
|
|
57
|
-
content = "";
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
// If there's only one node return the single node (otherwise return the entire array).
|
|
61
|
-
return !nodes.length ? null : nodes.length === 1 ? nodes[0] : nodes;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Append a JSX node to a list of JSX nodes.
|
|
65
|
-
* - Sets a generated `element.key` for JSX elements (based on `nodes.length`)
|
|
66
|
-
* - JSX nodes can be arrays of nodes — these will be flattened directly into the nodes list.
|
|
67
|
-
* - Nodes array is modified in place (not returned).
|
|
68
|
-
*/
|
|
69
|
-
function appendNode(nodes, node) {
|
|
70
|
-
if (!node) {
|
|
71
|
-
// No need to append null, undefined, or empty string.
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
else if (typeof node === "string") {
|
|
75
|
-
// String nodes should be merged into the last node (if there are several in a row).
|
|
76
|
-
const i = nodes.length - 1;
|
|
77
|
-
if (typeof nodes[i] === "string")
|
|
78
|
-
nodes[i] += node;
|
|
79
|
-
else
|
|
80
|
-
nodes.push(node);
|
|
81
|
-
}
|
|
82
|
-
else if (node instanceof Array) {
|
|
83
|
-
// Nested arrays of nodes are flattened.
|
|
84
|
-
for (const n of node)
|
|
85
|
-
appendNode(nodes, n);
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
// Generate `key` property using a numeric incrementor (if it's a string let it pass — there's probably a reason it's a string).
|
|
89
|
-
if (typeof node.key !== "string")
|
|
90
|
-
node.key = nodes.length;
|
|
91
|
-
// Append the node.
|
|
92
|
-
nodes.push(node);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Render a JSX node
|
|
97
|
-
* - Recursively renders the children of the node using the current options.
|
|
98
|
-
*/
|
|
99
|
-
function renderNode(node, options) {
|
|
100
|
-
if (typeof node === "string")
|
|
101
|
-
return renderString(node, options);
|
|
102
|
-
if (node instanceof Array)
|
|
103
|
-
return node.map(n => renderNode(n, options));
|
|
104
|
-
if (typeof node === "object" && node) {
|
|
105
|
-
return {
|
|
106
|
-
...node,
|
|
107
|
-
$$typeof: REACT_SECURITY_SYMBOL,
|
|
108
|
-
props: node.props.children ? { ...node.props, children: renderNode(node.props.children, options) } : node.props,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
return node;
|
|
112
|
-
}
|
|
113
|
-
const REACT_SECURITY_SYMBOL = Symbol.for("react.element");
|
|
2
|
+
import { isArray } from "../util/array.js";
|
|
3
|
+
import { MARKUP_OPTIONS } from "./options.js";
|
|
114
4
|
/**
|
|
115
5
|
* Parse a text string as Markdownish syntax and render it as a JSX node.
|
|
116
6
|
* - Syntax is not defined by this code, but by the rules supplied to it.
|
|
@@ -139,18 +29,100 @@ const REACT_SECURITY_SYMBOL = Symbol.for("react.element");
|
|
|
139
29
|
* - If the first thing in the definition is a URL, then it's recognised as a link reference (and produces an `<a href=""></a>`)
|
|
140
30
|
* - If the first thing in the definition isn't a URL, then it's recognised as a sidenote/footnote and tapping it will scroll you to that point (and popup the definition like Marco Arment's Bigfoot code).
|
|
141
31
|
*
|
|
142
|
-
* @param
|
|
32
|
+
* @param input The string content possibly containing markup syntax, e.g. "This is a *bold* string.
|
|
143
33
|
* @param options An options object for the render.
|
|
144
34
|
*
|
|
145
|
-
* @returns
|
|
35
|
+
* @returns JSXNode, i.e. either a complete `JSXElement`, `null`, `undefined`, `string`, or an array of zero or more of those.
|
|
146
36
|
*/
|
|
147
|
-
export function renderMarkup(
|
|
148
|
-
|
|
37
|
+
export function renderMarkup(input, options) {
|
|
38
|
+
if (!input)
|
|
39
|
+
return null;
|
|
40
|
+
const combined = options ? { ...MARKUP_OPTIONS, ...options } : MARKUP_OPTIONS;
|
|
41
|
+
return _renderString(input, combined, combined.context);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Render a string to its corresponding JSX node in a given context.
|
|
45
|
+
*/
|
|
46
|
+
function _renderString(input, options, context) {
|
|
47
|
+
const nodes = Array.from(_parseString(input, options, context));
|
|
48
|
+
return !nodes.length ? null : nodes.length === 1 ? nodes[0] : nodes;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Render a JSX node in a given context.
|
|
52
|
+
*/
|
|
53
|
+
function _renderNode(node, options, context) {
|
|
54
|
+
if (!node)
|
|
55
|
+
return node;
|
|
56
|
+
if (typeof node === "string")
|
|
57
|
+
return _renderString(node, options, context);
|
|
58
|
+
if (isArray(node)) {
|
|
59
|
+
for (let i = 0; i <= node.length - 1; i++)
|
|
60
|
+
node[i] = _renderNode(node[i], options, context);
|
|
61
|
+
return node;
|
|
62
|
+
}
|
|
63
|
+
return _renderElement(node, options, context);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Render a JSX element in a given context.
|
|
67
|
+
*/
|
|
68
|
+
function _renderElement(element, options, context) {
|
|
69
|
+
if (element.props.children)
|
|
70
|
+
element.props.children = _renderNode(element.props.children, options, context);
|
|
71
|
+
return element;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse a string to its corresponding JSX nodes in a given context.
|
|
75
|
+
*
|
|
76
|
+
* @param offset Keeps track of where we are within the wider string we're parsing when we're several calls deep.
|
|
77
|
+
*/
|
|
78
|
+
function* _parseString(input, options, context, offset = 0) {
|
|
79
|
+
let matchedRule = undefined;
|
|
80
|
+
let matchedPriority = Number.MIN_SAFE_INTEGER;
|
|
81
|
+
let matchedIndex = Number.MIN_SAFE_INTEGER;
|
|
82
|
+
let matchedLength = 0;
|
|
83
|
+
let matchedGroups = undefined;
|
|
84
|
+
// Loop through all rules in the list and see if any match.
|
|
85
|
+
for (const rule of options.rules) {
|
|
86
|
+
// Only apply this rule if both:
|
|
87
|
+
// 1. The priority is equal or higher to the current priority.
|
|
88
|
+
// 2. The rule is allowed in the current context.
|
|
89
|
+
const { priority = 0, match, contexts } = rule;
|
|
90
|
+
if (priority >= matchedPriority && contexts.includes(context)) {
|
|
91
|
+
const result = typeof match === "function" ? match(input, options) : match.exec(input);
|
|
92
|
+
if (result) {
|
|
93
|
+
// Use the match if it has length and is earlier in the string or is higher priority.
|
|
94
|
+
const { 0: { length } = "", index, groups } = result;
|
|
95
|
+
if (length && (index < matchedIndex || priority > matchedPriority)) {
|
|
96
|
+
matchedRule = rule;
|
|
97
|
+
matchedPriority = priority;
|
|
98
|
+
matchedIndex = index;
|
|
99
|
+
matchedLength = length;
|
|
100
|
+
matchedGroups = groups;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Did at least one rule match?
|
|
106
|
+
if (matchedRule && matchedLength) {
|
|
107
|
+
// If index is more than zero, then the string before the match may match another rule at lower priority.
|
|
108
|
+
const prefix = input.slice(0, matchedIndex);
|
|
109
|
+
if (prefix.length)
|
|
110
|
+
yield* _parseString(prefix, options, context, offset);
|
|
111
|
+
// Call the rule's `render()` function to generate the node.
|
|
112
|
+
// React gets annoyed if we don't set a `key:` property on lists of elements.
|
|
113
|
+
// We use the string offset as the `.key` property in the element because it's cheap to calculate and guaranteed to be unique within the string.
|
|
114
|
+
// Trying to generate an incrementing number would require tracking the number and passing it back and forth through `_parseString()`
|
|
115
|
+
const { render, subcontext } = matchedRule;
|
|
116
|
+
const element = render(matchedGroups, options);
|
|
117
|
+
element.key = offset + matchedIndex;
|
|
118
|
+
yield subcontext ? _renderElement(element, options, subcontext) : element;
|
|
119
|
+
// Decrement the content.
|
|
120
|
+
const suffix = input.slice(matchedIndex + matchedLength);
|
|
121
|
+
if (suffix.length)
|
|
122
|
+
yield* _parseString(suffix, options, context, offset + matchedIndex + matchedLength);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// If nothing matched return the entire string..
|
|
126
|
+
yield input;
|
|
127
|
+
}
|
|
149
128
|
}
|
|
150
|
-
const defaults = {
|
|
151
|
-
rules: MARKUP_RULES,
|
|
152
|
-
context: "block",
|
|
153
|
-
url: undefined,
|
|
154
|
-
rel: undefined,
|
|
155
|
-
schemes: ["http:", "https:"],
|
|
156
|
-
};
|