import React from "react"; import { Readable, Writable } from "stream"; import { registerActionComponents } from "project-editor/flow/component"; import type { IDashboardComponentContext } from "eez-studio-types"; import { registerSystemStructure, ValueType } from "project-editor/features/variable/value-type"; //////////////////////////////////////////////////////////////////////////////// const regexpIcon: any = ( ); const componentHeaderColor = "#E0BBE4"; const REGEXP_RESULT_STRUCT_NAME = "$RegExpResult"; registerSystemStructure({ name: REGEXP_RESULT_STRUCT_NAME, fields: [ { name: "index", type: "integer" }, { name: "texts", type: "array:string" }, { name: "indices", type: "array:array:integer" } ] }); registerActionComponents("Dashboard Specific", [ { name: "Regexp", icon: regexpIcon, componentHeaderColor, inputs: [ { name: "next", type: "any" as ValueType, isSequenceInput: true, isOptionalInput: true }, { name: "stop", type: "any" as ValueType, isSequenceInput: true, isOptionalInput: true } ], outputs: [ { name: "match", type: `struct:${REGEXP_RESULT_STRUCT_NAME}`, isSequenceOutput: false, isOptionalOutput: false }, { name: "done", type: "string", isSequenceOutput: false, isOptionalOutput: true } ], properties: [ { name: "pattern", type: "expression", valueType: "string" }, { name: "text", type: "expression", valueType: "string" }, { name: "global", type: "expression", valueType: "boolean" }, { name: "caseInsensitive", type: "expression", valueType: "boolean" } ], bodyPropertyName: "pattern", defaults: { global: "true", caseInsensitive: "false" }, migrateProperties: component => { if (component.text == undefined) { component.text = component.data; } }, execute: (context: IDashboardComponentContext) => { let executionState: RegexpExecutionState | undefined; if (context.getInputValue("@seqin") !== undefined) { context.setComponentExecutionState(undefined); executionState = undefined; } else { executionState = context.getComponentExecutionState(); if (!executionState) { context.throwError("Never started"); return; } } if (!executionState) { const patternValue: any = context.evalProperty("pattern"); if (typeof patternValue != "string") { context.throwError("pattern is not a string"); return; } const global: any = !!context.evalProperty("global"); const caseInsensitive: any = !!context.evalProperty("caseInsensitive"); let re; try { re = new RegExp( patternValue, "md" + (global ? "g" : "") + (caseInsensitive ? "i" : "") ); } catch (err) { context.throwError( "Invalid regular expression" + err.toString() ); return; } const textValue: any = context.evalProperty("text"); if (typeof textValue == "string") { context.setComponentExecutionState( new RegexpExecutionStateForString( context, re, textValue ) ); } else if (textValue instanceof Readable) { context.setComponentExecutionState( new RegexpExecutionStateForStream( context, re, textValue ) ); } else { context.throwError( "text is not a string or readable stream" ); } } else if (context.getInputValue("next") !== undefined) { executionState.getNext(); } else if (context.getInputValue("stop") !== undefined) { executionState.stop(); } } } ]); //////////////////////////////////////////////////////////////////////////////// abstract class RegexpExecutionState { abstract getNext(): void; abstract stop(): void; } function getMatchStruct(m: RegExpExecArray) { return { index: m.index, texts: m.map(x => x), indices: (m as any).indices.map((a: any) => { try { return a.map((x: any) => x); } catch (err) { return []; } }) }; } class RegexpExecutionStateForString extends RegexpExecutionState { done: boolean = false; constructor( private context: IDashboardComponentContext, private re: RegExp, private text: string ) { super(); this.getNext(); } getNext() { let m: RegExpExecArray | null; if (!this.done && (m = this.re.exec(this.text)) !== null) { // This is necessary to avoid infinite loops with zero-width matches if (m.index === this.re.lastIndex) { this.re.lastIndex++; } this.context.propagateValue("match", getMatchStruct(m)); if (!this.re.global) { this.done = true; } } else { this.context.propagateValue("done", null); this.context.setComponentExecutionState(undefined); } } stop() { this.context.propagateValue("done", null); this.context.setComponentExecutionState(undefined); } } class RegexpExecutionStateForStream extends RegexpExecutionState { private propagate = true; private isDone = false; private matches: RegExpExecArray[] = []; constructor( private context: IDashboardComponentContext, re: RegExp, stream: Readable ) { super(); const streamSnitch = new StreamSnitch( re, (m: RegExpExecArray) => { if (this.propagate) { this.context.propagateValue("match", getMatchStruct(m)); this.propagate = false; } else { this.matches.push(m); } }, () => { if (this.propagate) { if (!this.isDone) { context.propagateValue("done", null); this.context.setComponentExecutionState(undefined); } this.propagate = false; } this.isDone = true; } ); stream.pipe(streamSnitch); stream.on("close", () => { streamSnitch.destroy(); }); } getNext() { if (this.matches.length > 0) { const m = this.matches.shift(); this.context.propagateValue("match", getMatchStruct(m!)); } else if (this.isDone) { this.context.propagateValue("done", null); this.context.setComponentExecutionState(undefined); } else { this.propagate = true; } } stop() { this.context.propagateValue("done", null); this.context.setComponentExecutionState(undefined); this.isDone = true; } } class StreamSnitch extends Writable { _buffer = ""; bufferCap = 1048576; constructor( public regex: RegExp, private onDataCallback: (m: RegExpMatchArray) => void, onCloseCallback: () => void ) { super({ decodeStrings: false }); this.on("close", onCloseCallback); } async _write(chunk: any, encoding: any, cb: any) { let match; let lastMatch; if (Buffer.byteLength(this._buffer) > this.bufferCap) this.clearBuffer(); if (typeof chunk == "string") { this._buffer += chunk; } else if (chunk instanceof Buffer) { this._buffer += chunk.toString(); } for ( let i = 0; i < 100 && (match = this.regex.exec(this._buffer)); i++ ) { this.onDataCallback(match); lastMatch = match; if (!this.regex.global) { break; } } if (lastMatch) { this._buffer = this._buffer.slice( lastMatch.index + lastMatch[0].length ); } // if (this.regex.multiline) { // this._buffer = this._buffer.slice(this._buffer.lastIndexOf("\n")); // } cb(); } clearBuffer() { this._buffer = ""; } }