diff --git a/HELP.md b/HELP.md index 6cfd00a..5e727e7 100644 --- a/HELP.md +++ b/HELP.md @@ -62,6 +62,18 @@ str %> replace oo OO; replace ba BA str %> ``` +Comments start with `--` and run through the end of the line. Example: + +```text +str %> = abc -- my comment +┌─────────────── +│ 0 │abc +├─────────────── +│ :: string +└─────────────── +str %> +``` + ## Data Types @@ -231,6 +243,12 @@ Delete the specified word/line/index from the current subject. `index` is applied for destructured subjects. For destructured subjects you may omit the type (e.g. `drop 4`). Example: `foo bar baz` -> `drop word 1` -> `foo baz` +#### `take ` +Keep only the specified word/line/index from the current subject. +`word` and `line` apply to strings that have not been destructured. +`index` is applied for destructured subjects. For destructured subjects you may omit the type (e.g. `take 4`). +Example: `foo bar baz` -> `take word 1` -> `bar` + #### `contains ` If the subject contains the given substring, keep it. Otherwise, replace it with an empty string. Most often used in conjunction with `line`, `word`, or `each` for filtering. diff --git a/src/index.ts b/src/index.ts index 88e14cf..b5e0bb6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,10 @@ import * as fs from "node:fs"; const exec = new Executor(output, parser, input) exec.adoptLifecycle(lifecycle) - exec.subscribe(state => state.outputSubject()) + + console.log('`str` : An interactive string manipulation environment') + console.log('Copyright (C) 2026 Garrett Mills ') + console.log('') const rcFile = processPath('~/.str.rc') if ( fs.existsSync(rcFile) ) { @@ -36,8 +39,11 @@ import * as fs from "node:fs"; const rcFileContent = fs.readFileSync(rcFile).toString() await input.pushLines('\n' + rcFileContent) + + console.log('Successfully loaded ~/.str.rc\n') } + exec.subscribe(state => state.outputSubject()) input.setupPrompt() process.on('SIGINT', () => lifecycle.close()) diff --git a/src/log.ts b/src/log.ts index caf693a..48a2789 100644 --- a/src/log.ts +++ b/src/log.ts @@ -1,5 +1,5 @@ import {ConsoleLogger, Logger, LogLevel} from './util/log.js' -export const log: Logger = new ConsoleLogger(LogLevel.VERBOSE) +export const log: Logger = new ConsoleLogger(LogLevel.WARN) // log.setStreamLevel('lexer', LogLevel.INFO) // log.setStreamLevel('token', LogLevel.INFO) diff --git a/src/vm/commands/command.ts b/src/vm/commands/command.ts index fae5206..e56a8a5 100644 --- a/src/vm/commands/command.ts +++ b/src/vm/commands/command.ts @@ -382,7 +382,6 @@ export class ParseContext { // Now, the remainder of the subcontext inputs should be a series of executables // separated by `terminator` tokens -- e.g. (split _; join |), so parse executables // from the subcontext until it is empty: - console.log(sc.inputs) while ( sc.inputs.length > 0 ) { const [exec, remainingInputs] = await this.childParser(sc.inputs) lambda.body.push(exec) diff --git a/src/vm/commands/index.ts b/src/vm/commands/index.ts index 68fe300..0227e29 100644 --- a/src/vm/commands/index.ts +++ b/src/vm/commands/index.ts @@ -51,6 +51,7 @@ import {Concat} from "./concat.js"; import {Call} from "./call.js"; import {Chunk} from "./chunk.js"; import {Script} from "./script.js"; +import {Take} from "./take.js"; export type Commands = Command[] export const commands: Commands = [ @@ -97,6 +98,7 @@ export const commands: Commands = [ new Sort, new Split, new Suffix, + new Take, new To, new Trim, new Undo, diff --git a/src/vm/commands/take.ts b/src/vm/commands/take.ts new file mode 100644 index 0000000..6ab7377 --- /dev/null +++ b/src/vm/commands/take.ts @@ -0,0 +1,76 @@ +import {Command, ParseContext, StrTerm, TypeError} from "./command.js"; +import {LexInput} from "../lexer.js"; +import {StrVM} from "../vm.js"; +import {Lines} from "./lines.js"; +import {Words} from "./words.js"; + +export type TakeData = { + type: 'line'|'word'|'index', + specific: StrTerm, +} + +/** + * This command has a few forms: + * + * take line 3 + * Assume the subject is a string and keep only line 3 + * + * take word 3 + * Assume the subject is a string and keep only word 3 + * + * take index 3 + * take 3 + * Assume the subject is a destructured and keep only the item at index 3. + */ +export class Take extends Command { + async attemptParse(context: ParseContext): Promise { + // Check if the next term we received is an int or a variable. + // If so, we got the "on 3 " form of the command. + const next = await context.peekTerm() + if ( next?.term === 'int' || next?.term === 'variable' ) { + return { + type: 'index', + specific: await context.popTerm(), + } + } + + // Otherwise, assume we got the "on " form: + return { + type: context.popKeywordInSet(['line', 'word', 'index']).value, + specific: await context.popTerm(), + } + } + + getDisplayName(): string { + return 'take' + } + + isParseCandidate(token: LexInput): boolean { + return this.isKeyword(token, 'take') + } + + async execute(vm: StrVM, data: TakeData): Promise { + // If the type is line|word, first destructure the subject accordingly: + if ( data.type === 'line' ) { + vm = await (new Lines).execute(vm) + } else if ( data.type === 'word' ) { + vm = await (new Words).execute(vm) + } + + return vm.replaceContextMatchingTerm(ctx => ({ + override: async sub => { + if ( sub.term !== 'destructured' ) { + throw new TypeError('Cannot `take`: invalid type') + } + + // Retrieve the specific item in the destructured we're operating over: + const idx = ctx.resolveInt(data.specific) + const operand = sub.value[idx] + if ( !operand ) { + throw new Error(`Invalid ${data.type} ${idx}`) + } + return operand.value + }, + })) + } +} diff --git a/src/vm/lexer.ts b/src/vm/lexer.ts index 56b8275..3b1c033 100644 --- a/src/vm/lexer.ts +++ b/src/vm/lexer.ts @@ -22,6 +22,7 @@ export const tokenIsLVal = (input: LexInput): boolean => export class Lexer extends BehaviorSubject { private isEscape: boolean = false + private inComment: boolean = false private inQuote?: '"'|"'" private tokenAccumulator: string = '' @@ -57,6 +58,11 @@ export class Lexer extends BehaviorSubject { const c = inputChars.shift()! this.logState(c) + // We're in a comment. Ignore everything except newlines. + if ( this.inComment && c !== '\n' ) { + continue + } + // We got the 2nd character after an escape if ( this.isEscape ) { this.tokenAccumulator += LITERAL_MAP[c] || c @@ -75,6 +81,7 @@ export class Lexer extends BehaviorSubject { if ( this.tokenAccumulator ) { await this.emitToken('terminator') } + this.inComment = false await this.next({ type: 'terminator' }) continue } @@ -87,6 +94,13 @@ export class Lexer extends BehaviorSubject { continue } + // Comments start with -- + if ( this.tokenAccumulator === '-' && c === '-' && !this.inQuote ) { + this.tokenAccumulator = '' + this.inComment = true + continue + } + // We are either starting or ending an unescaped matching quote. // For now, only parse single quotes. Makes it nicer to type " in commands. if ( c === `'` ) { diff --git a/src/vm/vm.ts b/src/vm/vm.ts index 0ad0abd..3de4ff0 100644 --- a/src/vm/vm.ts +++ b/src/vm/vm.ts @@ -125,7 +125,6 @@ export const getTermOperatorInputDisplayList = (op: TermOperator): string[] => { } } - console.log({ vals }) return Object.keys(vals) }