summaryrefslogtreecommitdiffstats
path: root/llparse-builder/src/node
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:56:19 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:56:19 +0000
commit0b6210cd37b68b94252cb798598b12974a20e1c1 (patch)
treee371686554a877842d95aa94f100bee552ff2a8e /llparse-builder/src/node
parentInitial commit. (diff)
downloadnode-undici-upstream.tar.xz
node-undici-upstream.zip
Adding upstream version 5.28.2+dfsg1+~cs23.11.12.3.upstream/5.28.2+dfsg1+_cs23.11.12.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'llparse-builder/src/node')
-rw-r--r--llparse-builder/src/node/base.ts96
-rw-r--r--llparse-builder/src/node/consume.ts19
-rw-r--r--llparse-builder/src/node/error.ts24
-rw-r--r--llparse-builder/src/node/index.ts8
-rw-r--r--llparse-builder/src/node/invoke.ts39
-rw-r--r--llparse-builder/src/node/match.ts162
-rw-r--r--llparse-builder/src/node/pause.ts25
-rw-r--r--llparse-builder/src/node/span-end.ts19
-rw-r--r--llparse-builder/src/node/span-start.ts16
9 files changed, 408 insertions, 0 deletions
diff --git a/llparse-builder/src/node/base.ts b/llparse-builder/src/node/base.ts
new file mode 100644
index 0000000..9840f16
--- /dev/null
+++ b/llparse-builder/src/node/base.ts
@@ -0,0 +1,96 @@
+import * as assert from 'assert';
+import binarySearch = require('binary-search');
+import { Edge } from '../edge';
+
+/**
+ * Base class for all graph nodes.
+ */
+export abstract class Node {
+ private otherwiseEdge: Edge | undefined;
+ private privEdges: Edge[] = [];
+
+ /**
+ * @param name Node name
+ */
+ constructor(public readonly name: string) {
+ // no-op
+ }
+
+ /**
+ * Create an otherwise edge to node `node`.
+ *
+ * This edge is executed when no other edges match current input. No
+ * characters are consumed upon transition.
+ *
+ * NOTE: At most one otherwise (skipping or not) edge can be set, most nodes
+ * except `Error` require it.
+ *
+ * @param node Target node
+ */
+ public otherwise(node: Node): this {
+ if (this.otherwiseEdge !== undefined) {
+ throw new Error('Node already has `otherwise` or `skipTo`');
+ }
+
+ this.otherwiseEdge = new Edge(node, true, undefined, undefined);
+ return this;
+ }
+
+ /**
+ * Create a skipping otherwise edge to node `node`.
+ *
+ * This edge is executed when no other edges match current input. Single
+ * character is consumed upon transition.
+ *
+ * NOTE: At most one otherwise (skipping or not) edge can be set, most nodes
+ * except `Error` require it.
+ *
+ * @param node Target node
+ */
+ public skipTo(node: Node): this {
+ if (this.otherwiseEdge !== undefined) {
+ throw new Error('Node already has `otherwise` or `skipTo`');
+ }
+
+ this.otherwiseEdge = new Edge(node, false, undefined, undefined);
+ return this;
+ }
+
+ // Limited public use
+
+ /** Get otherwise edge. */
+ public getOtherwiseEdge(): Edge | undefined {
+ return this.otherwiseEdge;
+ }
+
+ /** Get list of all non-otherwise edges. */
+ public getEdges(): ReadonlyArray<Edge> {
+ return this.privEdges;
+ }
+
+ /** Get list of all edges (including otherwise, if present). */
+ public getAllEdges(): ReadonlyArray<Edge> {
+ const res = this.privEdges;
+ if (this.otherwiseEdge === undefined) {
+ return res;
+ } else {
+ return res.concat(this.otherwiseEdge);
+ }
+ }
+
+ /** Get iterator through all non-otherwise edges. */
+ public *[Symbol.iterator](): Iterator<Edge> {
+ yield* this.privEdges;
+ }
+
+ // Internal
+
+ protected addEdge(edge: Edge): void {
+ assert.notStrictEqual(edge.key, undefined);
+
+ const index = binarySearch(this.privEdges, edge, Edge.compare);
+ assert(index < 0, 'Attempting to create duplicate edge');
+
+ this.privEdges.splice(-1 - index, 0, edge);
+ }
+}
diff --git a/llparse-builder/src/node/consume.ts b/llparse-builder/src/node/consume.ts
new file mode 100644
index 0000000..eff4037
--- /dev/null
+++ b/llparse-builder/src/node/consume.ts
@@ -0,0 +1,19 @@
+import * as assert from 'assert';
+import { Node } from './base';
+
+/**
+ * This node consumes number of characters specified by state's property with
+ * name `field` from the input, and forwards execution to `otherwise` node.
+ */
+export class Consume extends Node {
+ /**
+ * @param field State's property name
+ */
+ constructor(public readonly field: string) {
+ super('consume_' + field);
+
+ if (/^_/.test(field)) {
+ throw new Error(`Can't use internal field in \`consume()\`: "${field}"`);
+ }
+ }
+}
diff --git a/llparse-builder/src/node/error.ts b/llparse-builder/src/node/error.ts
new file mode 100644
index 0000000..393f566
--- /dev/null
+++ b/llparse-builder/src/node/error.ts
@@ -0,0 +1,24 @@
+import * as assert from 'assert';
+import { Node } from './base';
+
+/**
+ * This node terminates the execution with an error
+ */
+class NodeError extends Node {
+ /**
+ * @param code Error code to return to user
+ * @param reason Error description to store in parser's state
+ */
+ constructor(public readonly code: number, public readonly reason: string) {
+ super('error');
+ assert.strictEqual(code, code | 0, 'code must be integer');
+ }
+
+ /** `.otherwise()` is not supported on this type of node */
+ public otherwise(node: Node): this { throw new Error('Not supported'); }
+
+ /** `.skipTo()` is not supported on this type of node */
+ public skipTo(node: Node): this { throw new Error('Not supported'); }
+}
+
+export { NodeError as Error };
diff --git a/llparse-builder/src/node/index.ts b/llparse-builder/src/node/index.ts
new file mode 100644
index 0000000..e3d5fe5
--- /dev/null
+++ b/llparse-builder/src/node/index.ts
@@ -0,0 +1,8 @@
+export { Node } from './base';
+export { Consume } from './consume';
+export { Error } from './error';
+export { Invoke, IInvokeMap } from './invoke';
+export { Match } from './match';
+export { Pause } from './pause';
+export { SpanStart } from './span-start';
+export { SpanEnd } from './span-end';
diff --git a/llparse-builder/src/node/invoke.ts b/llparse-builder/src/node/invoke.ts
new file mode 100644
index 0000000..d6791a7
--- /dev/null
+++ b/llparse-builder/src/node/invoke.ts
@@ -0,0 +1,39 @@
+import * as assert from 'assert';
+
+import { Code } from '../code';
+import { Edge } from '../edge';
+import { Node } from './base';
+
+/**
+ * Map of return codes of the callback. Each key is a return code,
+ * value is the target node that must be executed upon getting such return code.
+ */
+export interface IInvokeMap {
+ readonly [key: number]: Node;
+}
+
+/**
+ * This node invokes either external callback or intrinsic code and passes the
+ * execution to either a target from a `map` (if the return code matches one of
+ * registered in it), or to `otherwise` node.
+ */
+export class Invoke extends Node {
+ /**
+ * @param code External callback or intrinsic code. Can be created with
+ * `builder.code.*()` methods.
+ * @param map Map from callback return codes to target nodes
+ */
+ constructor(public readonly code: Code, map: IInvokeMap) {
+ super('invoke_' + code.name);
+
+ Object.keys(map).forEach((mapKey) => {
+ const numKey: number = parseInt(mapKey, 10);
+ const targetNode = map[numKey]!;
+
+ assert.strictEqual(numKey, numKey | 0,
+ 'Invoke\'s map keys must be integers');
+
+ this.addEdge(new Edge(targetNode, true, numKey, undefined));
+ });
+ }
+}
diff --git a/llparse-builder/src/node/match.ts b/llparse-builder/src/node/match.ts
new file mode 100644
index 0000000..617a659
--- /dev/null
+++ b/llparse-builder/src/node/match.ts
@@ -0,0 +1,162 @@
+import * as assert from 'assert';
+import { Buffer } from 'buffer';
+
+import { Edge } from '../edge';
+import { Transform } from '../transform';
+import { toBuffer } from '../utils';
+import { Node } from './base';
+
+/**
+ * Character/sequence to match.
+ *
+ * May have following types:
+ *
+ * * `number` - for single character
+ * * `string` - for printable character sequence
+ * * `Buffer` - for raw byte sequence
+ */
+export type MatchSingleValue = string | number | Buffer;
+
+/**
+ * Convenience type for passing several characters/sequences to match methods.
+ */
+export type MatchValue = MatchSingleValue | ReadonlyArray<MatchSingleValue>;
+
+/**
+ * A map from characters/sequences to `.select()`'s values. Used for specifying
+ * the value to be passed to `.select()'`s targets.
+ */
+export interface IMatchSelect {
+ readonly [key: string]: number;
+}
+
+/**
+ * This node matches characters/sequences and forwards the execution according
+ * to matched character with optional attached value (See `.select()`).
+ */
+export class Match extends Node {
+ private transformFn: Transform | undefined;
+
+ /**
+ * Set character transformation function.
+ *
+ * @param transform Transformation to apply. Can be created with
+ * `builder.transform.*()` methods.
+ */
+ public transform(transformFn: Transform): this {
+ this.transformFn = transformFn;
+ return this;
+ }
+
+ /**
+ * Match sequence/character and forward execution to `next` on success,
+ * consuming matched bytes of the input.
+ *
+ * No value is attached on such execution forwarding, and the target node
+ * **must not** be an `Invoke` node with a callback expecting the value.
+ *
+ * @param value Sequence/character to be matched
+ * @param next Target node to be executed on success.
+ */
+ public match(value: MatchValue, next: Node): this {
+ if (Array.isArray(value)) {
+ for (const subvalue of value) {
+ this.match(subvalue, next);
+ }
+ return this;
+ }
+
+ const buffer = toBuffer(value as MatchSingleValue);
+ const edge = new Edge(next, false, buffer, undefined);
+ this.addEdge(edge);
+ return this;
+ }
+
+ /**
+ * Match character and forward execution to `next` on success
+ * without consuming one byte of the input.
+ *
+ * No value is attached on such execution forwarding, and the target node
+ * **must not** be an `Invoke` with a callback expecting the value.
+ *
+ * @param value Character to be matched
+ * @param next Target node to be executed on success.
+ */
+ public peek(value: MatchValue, next: Node): this {
+ if (Array.isArray(value)) {
+ for (const subvalue of value) {
+ this.peek(subvalue, next);
+ }
+ return this;
+ }
+
+ const buffer = toBuffer(value as MatchSingleValue);
+ assert.strictEqual(buffer.length, 1,
+ '`.peek()` accepts only single character keys');
+
+ const edge = new Edge(next, true, buffer, undefined);
+ this.addEdge(edge);
+ return this;
+ }
+
+ /**
+ * Match character/sequence and forward execution to `next` on success
+ * consumed matched bytes of the input.
+ *
+ * Value is attached on such execution forwarding, and the target node
+ * **must** be an `Invoke` with a callback expecting the value.
+ *
+ * Possible signatures:
+ *
+ * * `.select(key, value [, next ])`
+ * * `.select({ key: value } [, next])`
+ *
+ * @param keyOrMap Either a sequence to match, or a map from sequences to
+ * values
+ * @param valueOrNext Either an integer value to be forwarded to the target
+ * node, or an otherwise node
+ * @param next Convenience param. Same as calling `.otherwise(...)`
+ */
+ public select(keyOrMap: MatchSingleValue | IMatchSelect,
+ valueOrNext?: number | Node, next?: Node): this {
+ // .select({ key: value, ... }, next)
+ if (typeof keyOrMap === 'object') {
+ assert(valueOrNext instanceof Node,
+ 'Invalid `next` argument of `.select()`');
+ assert.strictEqual(next, undefined,
+ 'Invalid argument count of `.select()`');
+
+ const map: IMatchSelect = keyOrMap as IMatchSelect;
+ next = valueOrNext as Node | undefined;
+
+ Object.keys(map).forEach((mapKey) => {
+ const numKey: number = mapKey as any;
+
+ this.select(numKey, map[numKey]!, next);
+ });
+ return this;
+ }
+
+ // .select(key, value, next)
+ assert.strictEqual(typeof valueOrNext, 'number',
+ 'Invalid `value` argument of `.select()`');
+ assert.notStrictEqual(next, undefined,
+ 'Invalid `next` argument of `.select()`');
+
+ const key = toBuffer(keyOrMap as MatchSingleValue);
+ const value = valueOrNext as number;
+
+ const edge = new Edge(next!, false, key, value);
+ this.addEdge(edge);
+ return this;
+ }
+
+ // Limited public use
+
+ /**
+ * Get tranformation function
+ */
+ public getTransform(): Transform | undefined {
+ return this.transformFn;
+ }
+}
diff --git a/llparse-builder/src/node/pause.ts b/llparse-builder/src/node/pause.ts
new file mode 100644
index 0000000..2dcf5d1
--- /dev/null
+++ b/llparse-builder/src/node/pause.ts
@@ -0,0 +1,25 @@
+import * as assert from 'assert';
+import { Node } from './base';
+
+/**
+ * This returns the specified error code, but makes the resumption to
+ * `otherwise` target possible.
+ */
+export class Pause extends Node {
+ /**
+ * @param code Error code to return
+ * @param reason Error description
+ */
+ constructor(public readonly code: number, public readonly reason: string) {
+ super('pause');
+ assert.strictEqual(code, code | 0, 'code must be integer');
+ }
+
+ /**
+ * `.skipTo()` is not supported on this type of node, please use
+ * `.otherwise()`
+ */
+ public skipTo(node: Node): this {
+ throw new Error('Not supported, please use `pause.otherwise()`');
+ }
+}
diff --git a/llparse-builder/src/node/span-end.ts b/llparse-builder/src/node/span-end.ts
new file mode 100644
index 0000000..377cd73
--- /dev/null
+++ b/llparse-builder/src/node/span-end.ts
@@ -0,0 +1,19 @@
+import { Span } from '../span';
+import { Node } from './base';
+
+/**
+ * Indicates span end.
+ *
+ * A callback will be invoked with all input data since the most recent of:
+ *
+ * * Span start invocation
+ * * Parser execution
+ */
+export class SpanEnd extends Node {
+ /**
+ * @param span Span instance
+ */
+ constructor(public readonly span: Span) {
+ super(`span_end_${span.callback.name}`);
+ }
+}
diff --git a/llparse-builder/src/node/span-start.ts b/llparse-builder/src/node/span-start.ts
new file mode 100644
index 0000000..f81b432
--- /dev/null
+++ b/llparse-builder/src/node/span-start.ts
@@ -0,0 +1,16 @@
+import { Span } from '../span';
+import { Node } from './base';
+
+/**
+ * Indicates span start.
+ *
+ * See `SpanEnd` for details on callback invocation.
+ */
+export class SpanStart extends Node {
+ /**
+ * @param span Span instance
+ */
+ constructor(public readonly span: Span) {
+ super(`span_start_${span.callback.name}`);
+ }
+}