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 %>
|
||||
```
|
||||
|
||||
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 <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>`
|
||||
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.
|
||||
|
||||
@@ -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 <shout@garrettmills.dev>')
|
||||
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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<CommandData>[]
|
||||
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,
|
||||
|
||||
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> {
|
||||
private isEscape: boolean = false
|
||||
private inComment: boolean = false
|
||||
private inQuote?: '"'|"'"
|
||||
private tokenAccumulator: string = ''
|
||||
|
||||
@@ -57,6 +58,11 @@ export class Lexer extends BehaviorSubject<LexToken> {
|
||||
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<LexToken> {
|
||||
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<LexToken> {
|
||||
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 === `'` ) {
|
||||
|
||||
@@ -125,7 +125,6 @@ export const getTermOperatorInputDisplayList = (op: TermOperator): string[] => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log({ vals })
|
||||
return Object.keys(vals)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user