793 lines
28 KiB
JavaScript
793 lines
28 KiB
JavaScript
/*
|
|
* Requires sudo dnf install wl-clipboard
|
|
TODOs:
|
|
- Replace - limit occurrences
|
|
- FIX - escape chars when building Regs
|
|
- Case coverters
|
|
- TODO - ability to undo `over $x ...` commands
|
|
*/
|
|
|
|
|
|
import readline from 'node:readline'
|
|
import { promisify } from 'node:util'
|
|
import { homedir } from 'node:os'
|
|
import fs from 'node:fs'
|
|
import crypto from 'node:crypto'
|
|
import { dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const PRESERVE_SUBJECT = Symbol('preserve-subject')
|
|
const PRESERVE_SUBJECT_NO_PRINT = Symbol('preserve-subject-no-print')
|
|
const EXIT = Symbol('should-exit')
|
|
const TERM = Symbol('terminator')
|
|
const tempFile = () => `/tmp/str-${crypto.randomBytes(4).readUInt32LE(0)}.txt`
|
|
|
|
const makeState = () => ({
|
|
quote: "'",
|
|
escape: "\\",
|
|
terminator: '\n',
|
|
session: `${homedir()}/.str.json`,
|
|
rc: `${homedir()}/.str.rc`,
|
|
commonQuotes: ["'", '"', '`'],
|
|
debug: 0,
|
|
encloses: {
|
|
'(': ')',
|
|
')': '(',
|
|
'[': ']',
|
|
']': '[',
|
|
'{': '}',
|
|
'}': '{',
|
|
'<': '>',
|
|
'>': '<',
|
|
},
|
|
})
|
|
|
|
let state = makeState()
|
|
|
|
const logIfDebug = (...out) => {
|
|
if ( parseInt(state.debug) ) console.log(...out)
|
|
}
|
|
|
|
class Lexer {
|
|
input = ''
|
|
token = ''
|
|
tokens = []
|
|
isEscape = false
|
|
isQuote = false
|
|
isLiteralEmpty = false
|
|
isAsLiteral = false
|
|
|
|
constructor(input) {
|
|
this.input = input
|
|
}
|
|
|
|
advanceToken() {
|
|
if ( this.token || this.isLiteralEmpty ) {
|
|
this.tokens.push({ token: this.token, asLiteral: this.isAsLiteral })
|
|
this.token = ''
|
|
this.isAsLiteral = false
|
|
this.isLiteralEmpty = false
|
|
}
|
|
}
|
|
|
|
run() {
|
|
while ( this.input ) this.step()
|
|
this.advanceToken()
|
|
return this.tokens
|
|
}
|
|
|
|
step() {
|
|
const c = this.input[0]
|
|
this.input = this.input.substring(1)
|
|
|
|
if ( this.isEscape ) {
|
|
if ( !this.token && c === '$' ) this.isAsLiteral = true // escaping var names
|
|
this.token += c
|
|
this.isEscape = false
|
|
return
|
|
}
|
|
|
|
if ( c === state.escape ) {
|
|
this.isEscape = true
|
|
return
|
|
}
|
|
|
|
if ( c === state.terminator ) {
|
|
this.advanceToken()
|
|
this.token = TERM
|
|
this.advanceToken()
|
|
return
|
|
}
|
|
|
|
if ( (c === ' ' || c === '\n' || c === '\t') && !this.isQuote ) {
|
|
this.advanceToken()
|
|
return
|
|
}
|
|
|
|
if ( c === state.quote ) {
|
|
if ( this.isQuote ) {
|
|
this.isQuote = false
|
|
if ( !this.token ) this.isLiteralEmpty = true
|
|
this.advanceToken()
|
|
return
|
|
} else {
|
|
this.isAsLiteral = true
|
|
this.isQuote = true
|
|
return
|
|
}
|
|
}
|
|
|
|
this.token += c
|
|
}
|
|
}
|
|
|
|
|
|
class Parser {
|
|
tokens = []
|
|
ast = []
|
|
|
|
constructor(tokens) {
|
|
this.tokens = tokens
|
|
}
|
|
|
|
parse() {
|
|
while ( this.tokens.length ) this.parseOnce()
|
|
return this.ast
|
|
}
|
|
|
|
parseOnce() {
|
|
const cmd = this.parseCommand()
|
|
this.popTerm()
|
|
this.ast.push(cmd)
|
|
}
|
|
|
|
parseCommand() {
|
|
const token = this.popToken()
|
|
|
|
const commandParsers = {
|
|
copy: () => ({ command: 'copy' }),
|
|
paste: () => ({ command: 'paste' }),
|
|
infile: () => ({ command: 'infile', file: this.popToken() }),
|
|
outfile: () => ({ command: 'outfile', file: this.popToken() }),
|
|
save: () => ({ command: 'save', file: this.popOptionalToken() }),
|
|
load: () => ({ command: 'load', file: this.popOptionalToken() }),
|
|
edit: () => ({ command: 'edit' }),
|
|
history: () => ({ command: 'history' }),
|
|
exit: () => ({ command: 'exit' }),
|
|
runfile: () => ({ command: 'runfile', file: this.popToken() }),
|
|
to: () => ({ command: 'to', lval: this.popLValToken() }),
|
|
from: () => ({ command: 'from', lval: this.popLValToken() }),
|
|
|
|
indent: () => ({
|
|
command: 'indent',
|
|
with: this.popTokenInSet(['spaces', 'tabs']),
|
|
level: this.popOptionalToken(),
|
|
}),
|
|
trim: () => ({
|
|
command: 'trim',
|
|
type: this.popOptionalTokenInSet(['start', 'end', 'both', 'left', 'right', 'lines'], 'both'),
|
|
char: this.popOptionalToken(),
|
|
}),
|
|
quote: () => ({ command: 'quote', with: this.popOptionalToken(state.quote) }),
|
|
unquote: () => ({ command: 'unquote', mark: this.popOptionalToken() }),
|
|
enclose: () => ({ command: 'enclose', with: this.popOptionalToken('('), rwith: this.popOptionalToken() }),
|
|
prefix: () => ({ command: 'prefix', with: this.popToken() }),
|
|
suffix: () => ({ command: 'suffix', with: this.popToken() }),
|
|
split: () => ({ command: 'split', on: this.popToken(), with: this.popOptionalToken('\n') }),
|
|
lines: () => ({ command: 'lines', on: this.popOptionalToken(), with: this.popOptionalToken('\n') }),
|
|
join: () => ({ command: 'join', with: this.popOptionalToken(',') }),
|
|
replace: () => ({ command: 'replace', find: this.popToken(), with: this.popToken() }),
|
|
lsub: () => ({ command: 'lsub', offset: this.popToken(), len: this.popOptionalToken() }),
|
|
rsub: () => ({ command: 'rsub', offset: this.popToken(), len: this.popOptionalToken() }),
|
|
reparse: () => ({
|
|
command: 'reparse',
|
|
fromLang: this.popTokenInSet(['json', 'php']),
|
|
toLang: this.popTokenInSet(['json', 'php']),
|
|
}),
|
|
contains: () => ({ command: 'contains', find: this.popToken() }),
|
|
missing: () => ({ command: 'missing', find: this.popToken() }),
|
|
upper: () => ({ command: 'upper' }),
|
|
lower: () => ({ command: 'lower' }),
|
|
|
|
help: () => ({ command: 'help' }),
|
|
show: () => ({ command: 'show' }),
|
|
clear: () => ({ command: 'clear' }),
|
|
undo: () => ({ command: 'undo', steps: this.popOptionalToken('1') }),
|
|
redo: () => ({ command: 'redo', steps: this.popOptionalToken('1') }),
|
|
set: () => ({
|
|
command: 'set',
|
|
setting: this.popTokenInSet(['quote', 'escape', 'terminator', 'session', 'debug']),
|
|
to: this.popOptionalToken(),
|
|
}),
|
|
|
|
over: () => ({ command: 'over', subject: this.popToken(), sub: this.parseCommand() }),
|
|
line: () => ({ command: 'line', sub: this.parseCommand() }),
|
|
word: () => ({ command: 'word', sub: this.parseCommand() }),
|
|
on: () => ({
|
|
command: 'on',
|
|
type: this.popTokenInSet(['line', 'word']),
|
|
specific: this.popToken(),
|
|
sub: this.parseCommand(),
|
|
}),
|
|
map: () => {
|
|
const cmd = {
|
|
command: 'map',
|
|
type: this.popTokenInSet(['line', 'word']),
|
|
start: this.popToken(),
|
|
}
|
|
|
|
let next = this.peekToken()
|
|
if ( next.token === 'to' ) {
|
|
this.popToken()
|
|
cmd.to = this.popToken()
|
|
next = this.peekToken()
|
|
}
|
|
|
|
if ( next.token === 'by' ) {
|
|
this.popToken()
|
|
cmd.by = this.popToken()
|
|
} else {
|
|
cmd.by = { token: '1', asLiteral: true }
|
|
}
|
|
|
|
cmd.sub = this.parseCommand()
|
|
|
|
return cmd
|
|
},
|
|
|
|
varSet: () => {
|
|
this.popTokenInSet('=')
|
|
const rval = this.popToken()
|
|
return { command: 'varSet', lval: token, rval }
|
|
},
|
|
varUnset: () => ({ command: 'varUnset', lval: this.popLValToken() }),
|
|
vars: () => ({ command: 'vars' }),
|
|
|
|
function: () => {
|
|
const cmd = {
|
|
command: 'function',
|
|
name: this.popTokenAsLiteral(),
|
|
subs: [],
|
|
}
|
|
|
|
this.popTerm()
|
|
|
|
// Parse commands one at a time until we hit an `end function`
|
|
while ( true ) {
|
|
const sub = this.parseCommand()
|
|
this.popTerm()
|
|
|
|
if ( state.debug ) console.log({ function: cmd.name, sub })
|
|
|
|
if ( sub.command === 'function' ) throw new Error('Nested functions are not supported.')
|
|
else if ( sub.command === 'end' && sub.type.token === 'function' ) break
|
|
else cmd.subs.push(sub)
|
|
}
|
|
|
|
return cmd
|
|
},
|
|
end: () => ({ command: 'end', type: this.popTokenInSetAsLiteral(['function']) }),
|
|
call: () => ({ command: 'call', name: this.popTokenAsLiteral() }), // todo: args
|
|
}
|
|
|
|
const parser = (!token.asLiteral && token.token.startsWith('$'))
|
|
? commandParsers.varSet
|
|
: commandParsers[token.token]
|
|
|
|
if ( !parser ) {
|
|
throw new Error('Cannot find parser for command: ' + token.token)
|
|
}
|
|
|
|
return parser()
|
|
}
|
|
|
|
popTerm() {
|
|
if ( this.tokens.length && this.tokens[0].token !== TERM ) throw new Error('Expected TERM; instead found token: ' + this.tokens[0].token)
|
|
if ( this.tokens.length ) this.tokens.shift()
|
|
}
|
|
|
|
popToken() {
|
|
if ( !this.tokens.length || this.tokens[0].token === TERM ) throw new Error('Unexpected end of token stream!')
|
|
return this.tokens.shift()
|
|
}
|
|
|
|
peekToken() {
|
|
if ( !this.tokens.length || this.tokens[0].token === TERM ) return undefined
|
|
return this.tokens[0]
|
|
}
|
|
|
|
popTokenAsLiteral() {
|
|
return {...this.popToken(), asLiteral: true}
|
|
}
|
|
|
|
popLValToken() {
|
|
const tok = this.popToken()
|
|
if ( !tok.token.startsWith('$') ) throw new Error('Expected literal variable name. Found: ' + tok.token)
|
|
return {...tok, asLiteral: false }
|
|
}
|
|
|
|
popTokenInSet(set) {
|
|
const tok = this.popToken()
|
|
if ( !set.includes(tok.token) ) throw new Error(`Invalid token "${tok}" (expected one of: ${set.join(',')})`)
|
|
return tok
|
|
}
|
|
|
|
popTokenInSetAsLiteral(set) {
|
|
return {...this.popTokenInSet(set), asLiteral: true}
|
|
}
|
|
|
|
popOptionalToken(fallback = undefined) {
|
|
if ( !this.tokens.length || this.tokens[0].token === TERM ) return { token: fallback, asLiteral: true }
|
|
return this.popToken()
|
|
}
|
|
|
|
popOptionalTokenInSet(set, fallback=undefined) {
|
|
if ( !this.tokens.length || this.tokens[0].token === TERM ) return { token: fallback, asLiteral: true }
|
|
const tok = this.popToken()
|
|
if ( !set.includes(tok.token) ) throw new Error(`Invalid token "${tok.token}" (expected one of: ${set.join(',')})`)
|
|
return tok
|
|
}
|
|
}
|
|
|
|
const lexParseAndRunOnVM = async (vm, input) => {
|
|
try {
|
|
const tokens = (new Lexer(input)).run()
|
|
logIfDebug('lexed tokens:', tokens)
|
|
try {
|
|
const cmds = (new Parser(tokens)).parse()
|
|
logIfDebug('parsed commands:', cmds)
|
|
await vm.runCommands(cmds)
|
|
} catch (parseErr) {
|
|
if ( state.debug ) console.error(parseErr)
|
|
else console.log('ERROR: ' + parseErr.message)
|
|
}
|
|
} catch (lexErr) {
|
|
if ( state.debug ) console.error(lexErr)
|
|
else console.log('ERROR: ' + lexErr.message)
|
|
}
|
|
}
|
|
|
|
class VM {
|
|
subject = ''
|
|
subjectBackHistory = []
|
|
subjectForwardHistory = []
|
|
vars = {}
|
|
functions = {}
|
|
|
|
rl = undefined
|
|
question = undefined
|
|
shouldExit = false
|
|
|
|
getState() {
|
|
return {
|
|
subject: this.subject,
|
|
subjectBackHistory: this.subjectBackHistory,
|
|
subjectForwardHistory: this.subjectForwardHistory,
|
|
globalState: state,
|
|
vars: this.vars,
|
|
functions: this.functions,
|
|
}
|
|
}
|
|
|
|
loadState(saved) {
|
|
this.subject = saved.subject
|
|
this.subjectBackHistory = saved.subjectBackHistory
|
|
this.subjectForwardHistory = saved.subjectForwardHistory
|
|
this.vars = {...this.vars, ...saved.vars}
|
|
this.functions = {...this.functions, ...saved.functions}
|
|
state = saved.globalState
|
|
}
|
|
|
|
welcome() {
|
|
console.log('str : An interactive string manipulation environment')
|
|
console.log(' (Type `help` for more info, or `exit` to close.)')
|
|
console.log('')
|
|
}
|
|
|
|
help() {
|
|
console.log(fs.readFileSync(`${dirname(fileURLToPath(import.meta.url))}/HELP.txt`).toString('utf-8'))
|
|
}
|
|
|
|
replacePrompt() {
|
|
this.closePrompt()
|
|
|
|
this.rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
})
|
|
|
|
this.question = promisify(this.rl.question).bind(this.rl)
|
|
}
|
|
|
|
getQuestionPrompt() {
|
|
this.replacePrompt()
|
|
return this.question
|
|
}
|
|
|
|
closePrompt() {
|
|
this.rl?.close?.()
|
|
this.rl = undefined
|
|
this.question = undefined
|
|
}
|
|
|
|
replaceSubject(subject) {
|
|
this.subjectBackHistory.push(this.subject)
|
|
this.subjectForwardHistory = []
|
|
this.subject = subject
|
|
}
|
|
|
|
undoSubject() {
|
|
if ( this.subjectBackHistory.length ) {
|
|
this.subjectForwardHistory.push(this.subject)
|
|
this.subject = this.subjectBackHistory.pop()
|
|
}
|
|
}
|
|
|
|
redoSubject() {
|
|
if ( this.subjectForwardHistory.length ) {
|
|
this.subjectBackHistory.push(this.subject)
|
|
this.subject = this.subjectForwardHistory.pop() || ''
|
|
}
|
|
}
|
|
|
|
resolveImmediate(token) {
|
|
if ( !token.token || token.asLiteral || !token.token.startsWith('$') ) {
|
|
return token.token
|
|
}
|
|
|
|
const varName = token.token.substring(1)
|
|
if ( !Object.prototype.hasOwnProperty.call(this.vars, varName) ) {
|
|
throw new Error(`Undefined variable: $${varName}`)
|
|
}
|
|
|
|
return this.vars[varName]
|
|
}
|
|
|
|
async runCommands(cmds) {
|
|
for ( const cmd of cmds ) {
|
|
try {
|
|
await this.runCommand(cmd)
|
|
if ( this.shouldExit ) break
|
|
} catch (e) {
|
|
console.log('ERROR: ' + e.message)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
async runCommand(cmd) {
|
|
const result = await this.runCommandOnSubject(cmd, this.subject)
|
|
if ( result === EXIT ) {
|
|
this.shouldExit = true
|
|
return this
|
|
}
|
|
|
|
if ( result !== PRESERVE_SUBJECT && result !== PRESERVE_SUBJECT_NO_PRINT ) this.replaceSubject(result)
|
|
if ( result !== PRESERVE_SUBJECT_NO_PRINT ) this.printValue(this.subject)
|
|
return this
|
|
}
|
|
|
|
printValue(val, prefix = '') {
|
|
console.log(`${prefix}\n---------------\n${val}\n---------------\n`)
|
|
}
|
|
|
|
async runCommandOnSubject(cmd, subject) {
|
|
const runners = {
|
|
copy: async () => {
|
|
const childProcess = await import('node:child_process')
|
|
const tmp = tempFile()
|
|
await fs.writeFileSync(tmp, subject)
|
|
const proc = childProcess.spawn('sh', ['-c', `wl-copy < "${tmp}"`], { stdio: 'inherit' })
|
|
await new Promise(res => {
|
|
proc.on('close', () => res())
|
|
})
|
|
return PRESERVE_SUBJECT
|
|
},
|
|
paste: async () => {
|
|
const childProcess = await import('node:child_process')
|
|
const tmp = tempFile()
|
|
await fs.writeFileSync(tmp, subject)
|
|
const proc = childProcess.spawn('sh', ['-c', `wl-paste > "${tmp}"`])
|
|
await new Promise(res => {
|
|
proc.on('close', () => res())
|
|
})
|
|
return fs.readFileSync(tmp).toString('utf-8')
|
|
},
|
|
edit: async () => {
|
|
this.closePrompt()
|
|
const childProcess = await import('node:child_process')
|
|
const tmp = tempFile()
|
|
await fs.writeFileSync(tmp, this.subject)
|
|
const proc = childProcess.spawn(process.env.EDITOR || 'vim', [tmp], { stdio: 'inherit' })
|
|
await new Promise(res => {
|
|
proc.on('close', () => res())
|
|
})
|
|
return fs.readFileSync(tmp).toString('utf-8')
|
|
},
|
|
infile: () => fs.readFileSync(this.resolveImmediate(cmd.file)).toString('utf-8'),
|
|
outfile: () => {
|
|
fs.writeFileSync(this.resolveImmediate(cmd.file), subject)
|
|
return subject
|
|
},
|
|
save: () => {
|
|
fs.writeFileSync(this.resolveImmediate(cmd.file) || state.session, JSON.stringify(this.getState()))
|
|
return PRESERVE_SUBJECT
|
|
},
|
|
load: () => {
|
|
this.loadState(JSON.parse(fs.readFileSync(this.resolveImmediate(cmd.file) || state.session)))
|
|
return PRESERVE_SUBJECT
|
|
},
|
|
history: () => {
|
|
for ( let i = 0; i < this.subjectBackHistory.length; i += 1 ) {
|
|
console.log(`--------------- UNDO ${(this.subjectBackHistory.length - i).toString().padStart(2)} ---------------`)
|
|
console.log(this.subjectBackHistory[i])
|
|
console.log('---------------------------------------')
|
|
}
|
|
console.log('')
|
|
console.log('--------------- CURRENT ---------------')
|
|
console.log(this.subject)
|
|
console.log('---------------------------------------')
|
|
console.log('')
|
|
for ( let i = 0; i < this.subjectForwardHistory.length; i += 1 ) {
|
|
console.log(`--------------- REDO ${i.toString().padStart(2)} ---------------`)
|
|
console.log(this.subjectForwardHistory[i])
|
|
console.log('---------------------------------------')
|
|
}
|
|
return PRESERVE_SUBJECT_NO_PRINT
|
|
},
|
|
exit: () => EXIT,
|
|
runfile: async () => {
|
|
const input = fs.readFileSync(this.resolveImmediate(cmd.file)).toString('utf-8')
|
|
await lexParseAndRunOnVM(this, input)
|
|
return PRESERVE_SUBJECT_NO_PRINT
|
|
},
|
|
to: () => {
|
|
this.vars[cmd.lval.token.substring(1)] = subject
|
|
return PRESERVE_SUBJECT
|
|
},
|
|
from: () => this.resolveImmediate(cmd.lval),
|
|
|
|
indent: () => {
|
|
const dent = this.resolveImmediate(cmd.with) === 'spaces'
|
|
? ''.padStart(parseInt(String(this.resolveImmediate(cmd.level) || '4')), ' ')
|
|
: ''.padStart(parseInt(String(this.resolveImmediate(cmd.level) || '1')), '\t')
|
|
|
|
return `${dent}${subject.replace(/^\s*/, '')}`
|
|
},
|
|
trim: () => {
|
|
const type = this.resolveImmediate(cmd.type)
|
|
const char = this.resolveImmediate(cmd.char)
|
|
if ( type === 'start' || type === 'left' || type === 'both' ) {
|
|
const leftRex = new RegExp(`^${char || '\\s'}*`, 's')
|
|
subject = subject.replace(leftRex, '')
|
|
}
|
|
|
|
if ( type === 'end' || type === 'right' || type === 'both' ) {
|
|
const rightRex = new RegExp(`${char || '\\s'}*$`, 's')
|
|
subject = subject.replace(rightRex, '')
|
|
}
|
|
|
|
if ( type === 'lines' ) {
|
|
subject = subject.split('\n')
|
|
.filter(l => l.trim())
|
|
.join('\n')
|
|
}
|
|
|
|
return subject
|
|
},
|
|
quote: () => {
|
|
for ( const mark of state.commonQuotes ) {
|
|
if ( !subject.startsWith(mark) || !subject.endsWith(mark) ) continue
|
|
subject = subject.substring(1, subject.length - 1)
|
|
break
|
|
}
|
|
|
|
const withChar = this.resolveImmediate(cmd.with)
|
|
return `${withChar || state.quote}${subject}${withChar || state.quote}`
|
|
},
|
|
unquote: () => {
|
|
const marks = state.commonQuotes
|
|
if ( cmd.mark ) marks.unshift(this.resolveImmediate(cmd.mark))
|
|
for ( const mark of marks ) {
|
|
if ( !subject.startsWith(mark) || !subject.endsWith(mark) ) continue
|
|
subject = subject.substring(1, subject.length - 1)
|
|
break
|
|
}
|
|
|
|
return subject
|
|
},
|
|
enclose: () => {
|
|
const withChar = this.resolveImmediate(cmd.with)
|
|
const rwithChar = this.resolveImmediate(cmd.rwith)
|
|
return `${withChar}${subject}${rwithChar || state.encloses[withChar] || withChar}`
|
|
},
|
|
prefix: () => `${this.resolveImmediate(cmd.with)}${subject}`,
|
|
suffix: () => `${subject}${this.resolveImmediate(cmd.with)}`,
|
|
split: () => subject.split(this.resolveImmediate(cmd.on)).join(this.resolveImmediate(cmd.with)),
|
|
lines: () => subject.split(new RegExp(`${this.resolveImmediate(cmd.on) || '\\s'}+`, 's'))
|
|
.join(this.resolveImmediate(cmd.with)),
|
|
join: () => subject.split('\n').join(this.resolveImmediate(cmd.with)),
|
|
replace: () => subject.replaceAll(this.resolveImmediate(cmd.find), this.resolveImmediate(cmd.with)),
|
|
lsub: () => subject.slice(
|
|
this.resolveImmediate(cmd.offset),
|
|
this.resolveImmediate(cmd.offset) + (this.resolveImmediate(cmd.len) || subject.length)),
|
|
rsub: () => subject.split('')
|
|
.reverse()
|
|
.slice(
|
|
this.resolveImmediate(cmd.offset),
|
|
this.resolveImmediate(cmd.offset) + (this.resolveImmediate(cmd.len) || subject.length))
|
|
.reverse()
|
|
.join(''),
|
|
upper: () => subject.toUpperCase(),
|
|
lower: () => subject.toLowerCase(),
|
|
// reparse,
|
|
|
|
help: () => {
|
|
this.help()
|
|
return PRESERVE_SUBJECT_NO_PRINT
|
|
},
|
|
show: () => PRESERVE_SUBJECT,
|
|
clear: () => '',
|
|
undo: () => {
|
|
for ( let i = 0; i < parseInt(this.resolveImmediate(cmd.steps)); i += 1 ) {
|
|
this.undoSubject()
|
|
}
|
|
return PRESERVE_SUBJECT
|
|
},
|
|
redo: () => {
|
|
for ( let i = 0; i < parseInt(this.resolveImmediate(cmd.steps)); i += 1 ) {
|
|
this.redoSubject()
|
|
}
|
|
return PRESERVE_SUBJECT
|
|
},
|
|
set: () => {
|
|
const setting = this.resolveImmediate(cmd.setting)
|
|
state[setting] = this.resolveImmediate(cmd.to) || (makeState()[setting])
|
|
return subject
|
|
},
|
|
|
|
over: async () => {
|
|
const explicitSubject = this.resolveImmediate(cmd.subject)
|
|
const result = await this.runCommandOnSubject(cmd.sub, explicitSubject)
|
|
if ( cmd.subject.token.startsWith('$') && !cmd.subject.asLiteral ) {
|
|
this.vars[cmd.subject.token.substring(1)] = result
|
|
}
|
|
this.printValue(result)
|
|
return PRESERVE_SUBJECT_NO_PRINT
|
|
},
|
|
line: async () => this.mapSubjectLines(
|
|
subject,
|
|
line => this.runCommandOnSubject(cmd.sub, line)),
|
|
word: async () => this.mapSubjectWords(
|
|
subject,
|
|
word => this.runCommandOnSubject(cmd.sub, word)),
|
|
contains: () => subject.includes(this.resolveImmediate(cmd.find)) ? subject : '',
|
|
missing: () => subject.includes(this.resolveImmediate(cmd.find)) ? '' : subject,
|
|
on: async () => {
|
|
const specific = this.resolveImmediate(cmd.specific)
|
|
const type = this.resolveImmediate(cmd.type)
|
|
if ( type === 'line' ) {
|
|
return this.mapSubjectLines(
|
|
subject,
|
|
(line, idx) => (idx + 1) == specific
|
|
? this.runCommandOnSubject(cmd.sub, line)
|
|
: line)
|
|
} else if ( type === 'word' ) {
|
|
return this.mapSubjectWords(
|
|
subject,
|
|
(word, idx) => (idx + 1) == specific
|
|
? this.runCommandOnSubject(cmd.sub, word)
|
|
: word)
|
|
}
|
|
return subject
|
|
},
|
|
map: async () => {
|
|
const type = this.resolveImmediate(cmd.type)
|
|
const loopForSubject = async (sub, idx) => {
|
|
if (
|
|
(!to || idx < to)
|
|
&& (idx >= start)
|
|
&& !((idx - start) % by)
|
|
) {
|
|
return this.runCommandOnSubject(cmd.sub, sub)
|
|
}
|
|
return sub
|
|
}
|
|
|
|
const start = this.resolveImmediate(cmd.start)
|
|
const to = cmd.to ? this.resolveImmediate(cmd.to) : undefined
|
|
const by = this.resolveImmediate(cmd.by)
|
|
|
|
if ( type === 'line' ) {
|
|
return this.mapSubjectLines(subject, loopForSubject)
|
|
} else if ( type === 'word' ) {
|
|
return this.mapSubjectWords(subject, loopForSubject)
|
|
}
|
|
|
|
},
|
|
|
|
varSet: () => {
|
|
const varName = cmd.lval.token.substring(1)
|
|
this.vars[varName] = this.resolveImmediate(cmd.rval)
|
|
this.printValue(this.vars[varName], `$${varName} =`)
|
|
return PRESERVE_SUBJECT_NO_PRINT
|
|
},
|
|
varUnset: () => {
|
|
const varName = cmd.lval.token.substring(1)
|
|
this.resolveImmediate(cmd.lval.token) // to weed out undefined vars
|
|
delete this.vars[varName]
|
|
return PRESERVE_SUBJECT_NO_PRINT
|
|
},
|
|
vars: () => {
|
|
for ( const varName in this.vars ) this.printValue(this.vars[varName], `$${varName} =`)
|
|
return PRESERVE_SUBJECT_NO_PRINT
|
|
},
|
|
function: () => {
|
|
this.functions[this.resolveImmediate(cmd.name)] = cmd
|
|
return PRESERVE_SUBJECT_NO_PRINT
|
|
},
|
|
call: async () => {
|
|
const fnName = this.resolveImmediate(cmd.name)
|
|
const fn = this.functions[fnName]
|
|
if ( !fn ) throw new Error('Could not find function: ' + fnName)
|
|
|
|
await this.runCommands(fn.subs)
|
|
return PRESERVE_SUBJECT
|
|
},
|
|
}
|
|
|
|
const runner = runners[cmd.command]
|
|
if ( !runner ) throw new Error('Invalid command: ' + cmd.command)
|
|
return runner()
|
|
}
|
|
|
|
async mapSubjectLines(subject, closure) {
|
|
return (await Promise.all(subject.split('\n')
|
|
.map(closure)))
|
|
.join('\n')
|
|
}
|
|
|
|
async mapSubjectWords(subject, closure) {
|
|
const separators = [...subject.matchAll(/\s+/sg)]
|
|
const words = await Promise.all(subject.split(/\s+/sg)
|
|
.map(closure))
|
|
|
|
const parts = []
|
|
for ( let i = 0; i < words.length; i += 1 ) {
|
|
parts.push(words[i])
|
|
if ( separators[i] ) parts.push(separators[i][0])
|
|
}
|
|
return parts.join('')
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
|
|
|
|
const vm = new VM()
|
|
vm.welcome()
|
|
|
|
// Execute the user's ~/.strrc file if it exists
|
|
if ( fs.existsSync(state.rc) ) {
|
|
await lexParseAndRunOnVM(vm, `runfile ${state.quote}${state.rc}${state.quote}`)
|
|
}
|
|
|
|
while ( true ) {
|
|
const question = vm.getQuestionPrompt()
|
|
let ans = (await question('str %> '))
|
|
while ( state.terminator !== '\n' && !ans.trim().endsWith(state.terminator) ) {
|
|
ans += '\n' + (await question('str |> '))
|
|
}
|
|
logIfDebug('raw input:', ans)
|
|
|
|
await lexParseAndRunOnVM(vm, ans)
|
|
if ( vm.shouldExit ) break
|
|
}
|
|
|
|
vm.closePrompt()
|
|
|
|
|
|
|
|
|
|
})();
|