Implement take command and comments (--) + misc cleanup
This commit is contained in:
18
HELP.md
18
HELP.md
@@ -62,6 +62,18 @@ str %> replace oo OO; replace ba BA
|
|||||||
str %>
|
str %>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Comments start with `--` and run through the end of the line. Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
str %> = abc -- my comment
|
||||||
|
┌───────────────
|
||||||
|
│ 0 │abc
|
||||||
|
├───────────────
|
||||||
|
│ :: string
|
||||||
|
└───────────────
|
||||||
|
str %>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Data Types
|
## 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`).
|
`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`
|
Example: `foo bar baz` -> `drop word 1` -> `foo baz`
|
||||||
|
|
||||||
|
#### `take <word|line|index> <index>`
|
||||||
|
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 <find>`
|
#### `contains <find>`
|
||||||
If the subject contains the given substring, keep it. Otherwise, replace it with an empty string.
|
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.
|
Most often used in conjunction with `line`, `word`, or `each` for filtering.
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ import * as fs from "node:fs";
|
|||||||
|
|
||||||
const exec = new Executor(output, parser, input)
|
const exec = new Executor(output, parser, input)
|
||||||
exec.adoptLifecycle(lifecycle)
|
exec.adoptLifecycle(lifecycle)
|
||||||
exec.subscribe(state => state.outputSubject())
|
|
||||||
|
console.log('`str` : An interactive string manipulation environment')
|
||||||
|
console.log('Copyright (C) 2026 Garrett Mills <shout@garrettmills.dev>')
|
||||||
|
console.log('')
|
||||||
|
|
||||||
const rcFile = processPath('~/.str.rc')
|
const rcFile = processPath('~/.str.rc')
|
||||||
if ( fs.existsSync(rcFile) ) {
|
if ( fs.existsSync(rcFile) ) {
|
||||||
@@ -36,8 +39,11 @@ import * as fs from "node:fs";
|
|||||||
|
|
||||||
const rcFileContent = fs.readFileSync(rcFile).toString()
|
const rcFileContent = fs.readFileSync(rcFile).toString()
|
||||||
await input.pushLines('\n' + rcFileContent)
|
await input.pushLines('\n' + rcFileContent)
|
||||||
|
|
||||||
|
console.log('Successfully loaded ~/.str.rc\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exec.subscribe(state => state.outputSubject())
|
||||||
input.setupPrompt()
|
input.setupPrompt()
|
||||||
|
|
||||||
process.on('SIGINT', () => lifecycle.close())
|
process.on('SIGINT', () => lifecycle.close())
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {ConsoleLogger, Logger, LogLevel} from './util/log.js'
|
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('lexer', LogLevel.INFO)
|
||||||
// log.setStreamLevel('token', LogLevel.INFO)
|
// log.setStreamLevel('token', LogLevel.INFO)
|
||||||
|
|||||||
@@ -382,7 +382,6 @@ export class ParseContext {
|
|||||||
// Now, the remainder of the subcontext inputs should be a series of executables
|
// Now, the remainder of the subcontext inputs should be a series of executables
|
||||||
// separated by `terminator` tokens -- e.g. (split _; join |), so parse executables
|
// separated by `terminator` tokens -- e.g. (split _; join |), so parse executables
|
||||||
// from the subcontext until it is empty:
|
// from the subcontext until it is empty:
|
||||||
console.log(sc.inputs)
|
|
||||||
while ( sc.inputs.length > 0 ) {
|
while ( sc.inputs.length > 0 ) {
|
||||||
const [exec, remainingInputs] = await this.childParser(sc.inputs)
|
const [exec, remainingInputs] = await this.childParser(sc.inputs)
|
||||||
lambda.body.push(exec)
|
lambda.body.push(exec)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {Concat} from "./concat.js";
|
|||||||
import {Call} from "./call.js";
|
import {Call} from "./call.js";
|
||||||
import {Chunk} from "./chunk.js";
|
import {Chunk} from "./chunk.js";
|
||||||
import {Script} from "./script.js";
|
import {Script} from "./script.js";
|
||||||
|
import {Take} from "./take.js";
|
||||||
|
|
||||||
export type Commands = Command<CommandData>[]
|
export type Commands = Command<CommandData>[]
|
||||||
export const commands: Commands = [
|
export const commands: Commands = [
|
||||||
@@ -97,6 +98,7 @@ export const commands: Commands = [
|
|||||||
new Sort,
|
new Sort,
|
||||||
new Split,
|
new Split,
|
||||||
new Suffix,
|
new Suffix,
|
||||||
|
new Take,
|
||||||
new To,
|
new To,
|
||||||
new Trim,
|
new Trim,
|
||||||
new Undo,
|
new Undo,
|
||||||
|
|||||||
76
src/vm/commands/take.ts
Normal file
76
src/vm/commands/take.ts
Normal file
@@ -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<TakeData> {
|
||||||
|
async attemptParse(context: ParseContext): Promise<TakeData> {
|
||||||
|
// Check if the next term we received is an int or a variable.
|
||||||
|
// If so, we got the "on 3 <exec>" 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 <type> <index> <exec>" 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<StrVM> {
|
||||||
|
// 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
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ export const tokenIsLVal = (input: LexInput): boolean =>
|
|||||||
|
|
||||||
export class Lexer extends BehaviorSubject<LexToken> {
|
export class Lexer extends BehaviorSubject<LexToken> {
|
||||||
private isEscape: boolean = false
|
private isEscape: boolean = false
|
||||||
|
private inComment: boolean = false
|
||||||
private inQuote?: '"'|"'"
|
private inQuote?: '"'|"'"
|
||||||
private tokenAccumulator: string = ''
|
private tokenAccumulator: string = ''
|
||||||
|
|
||||||
@@ -57,6 +58,11 @@ export class Lexer extends BehaviorSubject<LexToken> {
|
|||||||
const c = inputChars.shift()!
|
const c = inputChars.shift()!
|
||||||
this.logState(c)
|
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
|
// We got the 2nd character after an escape
|
||||||
if ( this.isEscape ) {
|
if ( this.isEscape ) {
|
||||||
this.tokenAccumulator += LITERAL_MAP[c] || c
|
this.tokenAccumulator += LITERAL_MAP[c] || c
|
||||||
@@ -75,6 +81,7 @@ export class Lexer extends BehaviorSubject<LexToken> {
|
|||||||
if ( this.tokenAccumulator ) {
|
if ( this.tokenAccumulator ) {
|
||||||
await this.emitToken('terminator')
|
await this.emitToken('terminator')
|
||||||
}
|
}
|
||||||
|
this.inComment = false
|
||||||
await this.next({ type: 'terminator' })
|
await this.next({ type: 'terminator' })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -87,6 +94,13 @@ export class Lexer extends BehaviorSubject<LexToken> {
|
|||||||
continue
|
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.
|
// We are either starting or ending an unescaped matching quote.
|
||||||
// For now, only parse single quotes. Makes it nicer to type " in commands.
|
// For now, only parse single quotes. Makes it nicer to type " in commands.
|
||||||
if ( c === `'` ) {
|
if ( c === `'` ) {
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ export const getTermOperatorInputDisplayList = (op: TermOperator): string[] => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log({ vals })
|
|
||||||
return Object.keys(vals)
|
return Object.keys(vals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user