diff --git a/src/util/types.ts b/src/util/types.ts index cccbce0..05946fc 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -3,6 +3,8 @@ export type Awaitable = T | Promise export type JSONScalar = string | boolean | number | undefined export type JSONData = JSONScalar | Array | { [key: string]: JSONScalar | JSONData } +export type ElementType = T extends (infer U)[] ? U : never; + /** A typescript-compatible version of Object.hasOwnProperty. */ export function hasOwnProperty(obj: X, prop: Y): obj is X & Record { // eslint-disable-line @typescript-eslint/ban-types return Object.hasOwnProperty.call(obj, prop) diff --git a/src/vm/commands/clear.ts b/src/vm/commands/clear.ts new file mode 100644 index 0000000..7249078 --- /dev/null +++ b/src/vm/commands/clear.ts @@ -0,0 +1,16 @@ +import {Command, ParseContext} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Clear extends Command<{}> { + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'clear') + } + + attemptParse(context: ParseContext): {} { + return {} + } + + getDisplayName(): string { + return 'clear' + } +} diff --git a/src/vm/commands/command.ts b/src/vm/commands/command.ts index 91ce675..6855e19 100644 --- a/src/vm/commands/command.ts +++ b/src/vm/commands/command.ts @@ -5,6 +5,7 @@ import { IsNotKeywordError, UnexpectedEndOfInputError } from "../parse.js"; +import {ElementType} from "../../util/types.js"; export type StrLVal = { term: 'variable', name: string } @@ -48,7 +49,12 @@ export class ParseContext { return { term: 'string', value: input.value, literal: input.literal } } - popKeywordInSet(options: T) { + popOptionalKeywordInSet(options: T): (StrTerm & { value: ElementType }) | undefined { + if ( this.inputs.length ) return this.popKeywordInSet(options) + return undefined + } + + popKeywordInSet(options: T): StrTerm & { value: ElementType } { if ( !this.inputs.length ) { throw new UnexpectedEndOfInputError('Unexpected end of input. Expected one of: ' + options.join(', ')) } @@ -58,6 +64,8 @@ export class ParseContext { if ( input.literal || !options.includes(input.value) ) { throw new IsNotKeywordError('Unexpected term: ' + input.value + ' (expected one of: ' + options.join(', ') + ')') } + + return { term: 'string', value: input.value as ElementType } } popLVal(): StrLVal { diff --git a/src/vm/commands/contains.ts b/src/vm/commands/contains.ts new file mode 100644 index 0000000..f89b7ee --- /dev/null +++ b/src/vm/commands/contains.ts @@ -0,0 +1,18 @@ +import { LexInput } from "../lexer.js"; +import {Command, ParseContext, StrTerm} from "./command.js"; + +export class Contains extends Command<{ find: StrTerm }> { + attemptParse(context: ParseContext): { find: StrTerm } { + return { + find: context.popTerm(), + } + } + + getDisplayName(): string { + return 'contains' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'contains') + } +} diff --git a/src/vm/commands/enclose.ts b/src/vm/commands/enclose.ts new file mode 100644 index 0000000..7dbfec6 --- /dev/null +++ b/src/vm/commands/enclose.ts @@ -0,0 +1,24 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export type EncloseData = { + left?: StrTerm, + right?: StrTerm, +} + +export class Enclose extends Command { + attemptParse(context: ParseContext): EncloseData { + return { + left: context.popOptionalTerm(), + right: context.popOptionalTerm(), + } + } + + getDisplayName(): string { + return 'enclose' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'enclose') + } +} diff --git a/src/vm/commands/help.ts b/src/vm/commands/help.ts new file mode 100644 index 0000000..7865d39 --- /dev/null +++ b/src/vm/commands/help.ts @@ -0,0 +1,16 @@ +import {Command, ParseContext} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Help extends Command<{}> { + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'help') + } + + attemptParse(context: ParseContext): {} { + return {} + } + + getDisplayName(): string { + return 'help' + } +} diff --git a/src/vm/commands/indent.ts b/src/vm/commands/indent.ts new file mode 100644 index 0000000..6d59413 --- /dev/null +++ b/src/vm/commands/indent.ts @@ -0,0 +1,24 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export type IndentData = { + type: 'space'|'tab', + level?: StrTerm, +} + +export class Indent extends Command { + attemptParse(context: ParseContext): IndentData { + return { + type: context.popKeywordInSet(['space', 'tab']).value, + level: context.popOptionalTerm(), + } + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'indent') + } + + getDisplayName(): string { + return 'indent' + } +} diff --git a/src/vm/commands/index.ts b/src/vm/commands/index.ts index a93d0b5..fb22e4e 100644 --- a/src/vm/commands/index.ts +++ b/src/vm/commands/index.ts @@ -11,19 +11,67 @@ import {Paste} from "./paste.js"; import {RunFile} from "./runfile.js"; import {Save} from "./save.js"; import {To} from "./to.js"; +import {Lipsum} from "./lipsum.js"; +import {Indent} from "./indent.js"; +import {Clear} from "./clear.js"; +import {Contains} from "./contains.js"; +import {Enclose} from "./enclose.js"; +import {Help} from "./help.js"; +import {Join} from "./join.js"; +import {Lines} from "./lines.js"; +import {Lower} from "./lower.js"; +import {LSub} from "./lsub.js"; +import {Missing} from "./missing.js"; +import {Prefix} from "./prefix.js"; +import {Quote} from "./quote.js"; +import {Redo} from "./redo.js"; +import {Replace} from "./replace.js"; +import {RSub} from "./rsub.js"; +import {Show} from "./show.js"; +import {Split} from "./split.js"; +import {Suffix} from "./suffix.js"; +import {Trim} from "./trim.js"; +import {Undo} from "./undo.js"; +import {Unique} from "./unique.js"; +import {Unquote} from "./unquote.js"; +import {Upper} from "./upper.js"; export type Commands = Command[] export const commands: Commands = [ + new Clear, + new Contains, new Copy, new Edit, + new Enclose, new Exit, new From, + new Help, new History, + new Indent, new InFile, + new Join, + new Lines, + new Lipsum, new Load, + new Lower, + new LSub, + new Missing, new OutFile, new Paste, + new Prefix, + new Quote, + new Redo, + new Replace, + new RSub, new RunFile, new Save, + new Show, + new Split, + new Suffix, new To, + new Trim, + new Undo, + new Unique, + new Unquote, + new Upper, ] diff --git a/src/vm/commands/join.ts b/src/vm/commands/join.ts new file mode 100644 index 0000000..01ece67 --- /dev/null +++ b/src/vm/commands/join.ts @@ -0,0 +1,18 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Join extends Command<{ with: StrTerm }> { + attemptParse(context: ParseContext): { with: StrTerm } { + return { + with: context.popTerm(), + } + } + + getDisplayName(): string { + return 'join' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'join') + } +} diff --git a/src/vm/commands/lines.ts b/src/vm/commands/lines.ts new file mode 100644 index 0000000..19e5880 --- /dev/null +++ b/src/vm/commands/lines.ts @@ -0,0 +1,24 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export type LinesData = { + on?: StrTerm, + with?: StrTerm, +} + +export class Lines extends Command { + attemptParse(context: ParseContext): LinesData { + return { + on: context.popOptionalTerm(), + with: context.popOptionalTerm(), + } + } + + getDisplayName(): string { + return 'lines' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'lines') + } +} diff --git a/src/vm/commands/lipsum.ts b/src/vm/commands/lipsum.ts index ec0456b..8f4253a 100644 --- a/src/vm/commands/lipsum.ts +++ b/src/vm/commands/lipsum.ts @@ -1,10 +1,13 @@ import {Command, ParseContext, StrTerm} from './command.js' import {LexInput} from '../lexer.js' -export class Lipsum extends Command<{ length: StrTerm }> { - attemptParse(context: ParseContext): { length: StrTerm } { +export type LipsumData = { length: StrTerm, type: 'word'|'line'|'para' } + +export class Lipsum extends Command { + attemptParse(context: ParseContext): LipsumData { return { length: context.popTerm(), + type: context.popKeywordInSet(['word', 'line', 'para']).value, } } diff --git a/src/vm/commands/lower.ts b/src/vm/commands/lower.ts new file mode 100644 index 0000000..5d79b4f --- /dev/null +++ b/src/vm/commands/lower.ts @@ -0,0 +1,16 @@ +import {Command, ParseContext} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Lower extends Command<{}> { + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'lower') + } + + attemptParse(context: ParseContext): {} { + return {} + } + + getDisplayName(): string { + return 'lower' + } +} diff --git a/src/vm/commands/lsub.ts b/src/vm/commands/lsub.ts new file mode 100644 index 0000000..43e1f84 --- /dev/null +++ b/src/vm/commands/lsub.ts @@ -0,0 +1,24 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export type LSubData = { + offset: StrTerm, + length?: StrTerm, +} + +export class LSub extends Command { + attemptParse(context: ParseContext): LSubData { + return { + offset: context.popTerm(), + length: context.popOptionalTerm(), + } + } + + getDisplayName(): string { + return 'lsub' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'lsub') + } +} diff --git a/src/vm/commands/missing.ts b/src/vm/commands/missing.ts new file mode 100644 index 0000000..7d6afa9 --- /dev/null +++ b/src/vm/commands/missing.ts @@ -0,0 +1,18 @@ +import { LexInput } from "../lexer.js"; +import {Command, ParseContext, StrTerm} from "./command.js"; + +export class Missing extends Command<{ find: StrTerm }> { + attemptParse(context: ParseContext): { find: StrTerm } { + return { + find: context.popTerm(), + } + } + + getDisplayName(): string { + return 'missing' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'missing') + } +} diff --git a/src/vm/commands/prefix.ts b/src/vm/commands/prefix.ts new file mode 100644 index 0000000..71fc078 --- /dev/null +++ b/src/vm/commands/prefix.ts @@ -0,0 +1,18 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Prefix extends Command<{ with: StrTerm }> { + attemptParse(context: ParseContext): { with: StrTerm } { + return { + with: context.popTerm(), + } + } + + getDisplayName(): string { + return 'prefix' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'prefix') + } +} diff --git a/src/vm/commands/quote.ts b/src/vm/commands/quote.ts new file mode 100644 index 0000000..dbf0006 --- /dev/null +++ b/src/vm/commands/quote.ts @@ -0,0 +1,18 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Quote extends Command<{ with?: StrTerm }> { + attemptParse(context: ParseContext): { with?: StrTerm } { + return { + with: context.popOptionalTerm(), + } + } + + getDisplayName(): string { + return 'quote' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'quote') + } +} diff --git a/src/vm/commands/redo.ts b/src/vm/commands/redo.ts new file mode 100644 index 0000000..805d74d --- /dev/null +++ b/src/vm/commands/redo.ts @@ -0,0 +1,18 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Redo extends Command<{ steps: StrTerm }> { + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'redo') + } + + attemptParse(context: ParseContext): { steps: StrTerm } { + return { + steps: context.popTerm(), + } + } + + getDisplayName(): string { + return 'redo' + } +} diff --git a/src/vm/commands/replace.ts b/src/vm/commands/replace.ts new file mode 100644 index 0000000..bcbca96 --- /dev/null +++ b/src/vm/commands/replace.ts @@ -0,0 +1,24 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export type ReplaceData = { + find: StrTerm, + with: StrTerm, +} + +export class Replace extends Command { + attemptParse(context: ParseContext): ReplaceData { + return { + find: context.popTerm(), + with: context.popTerm(), + } + } + + getDisplayName(): string { + return 'replace' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'replace') + } +} diff --git a/src/vm/commands/rsub.ts b/src/vm/commands/rsub.ts new file mode 100644 index 0000000..3ae388b --- /dev/null +++ b/src/vm/commands/rsub.ts @@ -0,0 +1,24 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export type RSubData = { + offset: StrTerm, + length?: StrTerm, +} + +export class RSub extends Command { + attemptParse(context: ParseContext): RSubData { + return { + offset: context.popTerm(), + length: context.popOptionalTerm(), + } + } + + getDisplayName(): string { + return 'rsub' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'rsub') + } +} diff --git a/src/vm/commands/show.ts b/src/vm/commands/show.ts new file mode 100644 index 0000000..3ed88c4 --- /dev/null +++ b/src/vm/commands/show.ts @@ -0,0 +1,16 @@ +import {Command, ParseContext} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Show extends Command<{}> { + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'show') + } + + attemptParse(context: ParseContext): {} { + return {} + } + + getDisplayName(): string { + return 'show' + } +} diff --git a/src/vm/commands/split.ts b/src/vm/commands/split.ts new file mode 100644 index 0000000..86ce27d --- /dev/null +++ b/src/vm/commands/split.ts @@ -0,0 +1,24 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export type SplitData = { + on: StrTerm, + with?: StrTerm, +} + +export class Split extends Command { + attemptParse(context: ParseContext): SplitData { + return { + on: context.popTerm(), + with: context.popOptionalTerm(), + } + } + + getDisplayName(): string { + return 'split' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'split') + } +} diff --git a/src/vm/commands/suffix.ts b/src/vm/commands/suffix.ts new file mode 100644 index 0000000..fea854d --- /dev/null +++ b/src/vm/commands/suffix.ts @@ -0,0 +1,18 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Suffix extends Command<{ with: StrTerm }> { + attemptParse(context: ParseContext): { with: StrTerm } { + return { + with: context.popTerm(), + } + } + + getDisplayName(): string { + return 'suffix' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'suffix') + } +} diff --git a/src/vm/commands/trim.ts b/src/vm/commands/trim.ts new file mode 100644 index 0000000..af77c9f --- /dev/null +++ b/src/vm/commands/trim.ts @@ -0,0 +1,24 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export type TrimData = { + type?: 'start'|'end'|'both'|'left'|'right'|'lines', + char?: StrTerm, +} + +export class Trim extends Command { + attemptParse(context: ParseContext): TrimData { + return { + type: context.popOptionalKeywordInSet(['start', 'end', 'both', 'left', 'right', 'lines'])?.value, + char: context.popOptionalTerm(), + } + } + + getDisplayName(): string { + return 'trim' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'trim') + } +} diff --git a/src/vm/commands/undo.ts b/src/vm/commands/undo.ts new file mode 100644 index 0000000..ab9b5c2 --- /dev/null +++ b/src/vm/commands/undo.ts @@ -0,0 +1,18 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Undo extends Command<{ steps: StrTerm }> { + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'undo') + } + + attemptParse(context: ParseContext): { steps: StrTerm } { + return { + steps: context.popTerm(), + } + } + + getDisplayName(): string { + return 'undo' + } +} diff --git a/src/vm/commands/unique.ts b/src/vm/commands/unique.ts new file mode 100644 index 0000000..0c31233 --- /dev/null +++ b/src/vm/commands/unique.ts @@ -0,0 +1,16 @@ +import {Command, ParseContext} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Unique extends Command<{}> { + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'unique') + } + + attemptParse(context: ParseContext): {} { + return {} + } + + getDisplayName(): string { + return 'unique' + } +} diff --git a/src/vm/commands/unquote.ts b/src/vm/commands/unquote.ts new file mode 100644 index 0000000..9b65001 --- /dev/null +++ b/src/vm/commands/unquote.ts @@ -0,0 +1,18 @@ +import {Command, ParseContext, StrTerm} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Unquote extends Command<{ with?: StrTerm }> { + attemptParse(context: ParseContext): { with?: StrTerm } { + return { + with: context.popOptionalTerm(), + } + } + + getDisplayName(): string { + return 'unquote' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'unquote') + } +} diff --git a/src/vm/commands/upper.ts b/src/vm/commands/upper.ts new file mode 100644 index 0000000..ac0fd6e --- /dev/null +++ b/src/vm/commands/upper.ts @@ -0,0 +1,16 @@ +import {Command, ParseContext} from "./command.js"; +import {LexInput} from "../lexer.js"; + +export class Upper extends Command<{}> { + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'upper') + } + + attemptParse(context: ParseContext): {} { + return {} + } + + getDisplayName(): string { + return 'upper' + } +}