Big bang
This commit is contained in:
commit
dd0fdbff3b
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.idea
|
||||||
|
node_modules
|
||||||
4
README.md
Normal file
4
README.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# `str`: An interactive string manipulation environment
|
||||||
|
|
||||||
|
WIP
|
||||||
|
|
||||||
654
str.mjs
Normal file
654
str.mjs
Normal file
@ -0,0 +1,654 @@
|
|||||||
|
/*
|
||||||
|
* Requires sudo dnf install wl-clipboard
|
||||||
|
TODOs:
|
||||||
|
- Replace - limit occurrences
|
||||||
|
- FIX - escape chars when building Regs
|
||||||
|
- Case coverters
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
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`,
|
||||||
|
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
|
||||||
|
|
||||||
|
constructor(input) {
|
||||||
|
this.input = input
|
||||||
|
}
|
||||||
|
|
||||||
|
advanceToken() {
|
||||||
|
if ( this.token || this.isLiteralEmpty ) {
|
||||||
|
this.tokens.push(this.token)
|
||||||
|
this.token = ''
|
||||||
|
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 ) {
|
||||||
|
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.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' }),
|
||||||
|
|
||||||
|
indent: () => ({
|
||||||
|
command: 'indent',
|
||||||
|
with: this.popTokenInSet(['spaces', 'tabs']),
|
||||||
|
level: this.popOptionalToken(),
|
||||||
|
}),
|
||||||
|
trim: () => ({
|
||||||
|
command: 'trim',
|
||||||
|
type: this.popOptionalTokenInSet(['start', 'end', 'both', 'left', 'right'], 'both'),
|
||||||
|
char: this.popOptionalToken(),
|
||||||
|
}),
|
||||||
|
quote: () => ({ command: 'quote', with: this.popOptionalToken(state.quote) }),
|
||||||
|
unquote: () => ({ command: 'unquote', mark: this.popOptionalToken() }),
|
||||||
|
enclose: () => ({ command: 'enclose', with: 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() }),
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
line: () => ({ command: 'line', sub: this.parseCommand() }),
|
||||||
|
word: () => ({ command: 'word', sub: this.parseCommand() }),
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = commandParsers[token]
|
||||||
|
if ( !parser ) {
|
||||||
|
throw new Error('Cannot find parser for command: ' + token)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parser()
|
||||||
|
}
|
||||||
|
|
||||||
|
popTerm() {
|
||||||
|
if ( this.tokens.length && this.tokens[0] !== TERM ) throw new Error('Expected TERM; instead found token: ' + this.tokens[0])
|
||||||
|
if ( this.tokens.length ) this.tokens.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
popToken() {
|
||||||
|
if ( !this.tokens.length || this.tokens[0] === TERM ) throw new Error('Unexpected end of token stream!')
|
||||||
|
return this.tokens.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
popTokenInSet(set) {
|
||||||
|
const tok = this.popToken()
|
||||||
|
if ( !set.includes(tok) ) throw new Error(`Invalid token "${tok}" (expected one of: ${set.join(',')})`)
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
|
||||||
|
popOptionalToken(fallback=undefined) {
|
||||||
|
if ( !this.tokens.length || this.tokens[0] === TERM ) return fallback
|
||||||
|
return this.popToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
popOptionalTokenInSet(set, fallback=undefined) {
|
||||||
|
if ( !this.tokens.length || this.tokens[0] === TERM ) return fallback
|
||||||
|
const tok = this.popToken()
|
||||||
|
if ( !set.includes(tok) ) throw new Error(`Invalid token "${tok}" (expected one of: ${set.join(',')})`)
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VM {
|
||||||
|
subject = ''
|
||||||
|
subjectBackHistory = []
|
||||||
|
subjectForwardHistory = []
|
||||||
|
|
||||||
|
rl = undefined
|
||||||
|
question = undefined
|
||||||
|
shouldExit = false
|
||||||
|
|
||||||
|
getState() {
|
||||||
|
return {
|
||||||
|
subject: this.subject,
|
||||||
|
subjectBackHistory: this.subjectBackHistory,
|
||||||
|
subjectForwardHistory: this.subjectForwardHistory,
|
||||||
|
globalState: state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState(saved) {
|
||||||
|
this.subject = saved.subject
|
||||||
|
this.subjectBackHistory = saved.subjectBackHistory
|
||||||
|
this.subjectForwardHistory = saved.subjectForwardHistory
|
||||||
|
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('str : An interactive string manipulation environment')
|
||||||
|
console.log(' Copyright (C) 2025 Garrett Mills <shout@garrettmills.dev>')
|
||||||
|
console.log('')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
console.log('Input / Output')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
console.log('copy')
|
||||||
|
console.log(' Copy the current string to the clipboard. (Requires wl-clipboard.)')
|
||||||
|
console.log('')
|
||||||
|
console.log('paste')
|
||||||
|
console.log(' Paste the contents of the clipboard to replace the current string. (Requires wl-clipboard.)')
|
||||||
|
console.log('')
|
||||||
|
console.log('infile <file path>')
|
||||||
|
console.log(' Replace the current string with the contents of <file path>.')
|
||||||
|
console.log('')
|
||||||
|
console.log('outfile <file path>')
|
||||||
|
console.log(' Write the current string as the contents of <file path>.')
|
||||||
|
console.log('')
|
||||||
|
console.log('edit')
|
||||||
|
console.log(' Open the current string in EDITOR.')
|
||||||
|
console.log('')
|
||||||
|
console.log('history')
|
||||||
|
console.log(' Print the undo/redo history')
|
||||||
|
console.log('')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
console.log('String Manipulation')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
console.log('indent <spaces|tabs> [<level>]')
|
||||||
|
console.log(' Indent the string with the specified number of spaces or tabs.')
|
||||||
|
console.log(' Default is a single standard indentation level.')
|
||||||
|
console.log('')
|
||||||
|
console.log('trim [<start|end|both>] [<char>]')
|
||||||
|
console.log(' Remove instances of the given character from either the start/end or both sides of the string.')
|
||||||
|
console.log(' Default is to trim whitespace from both ends.')
|
||||||
|
console.log('')
|
||||||
|
console.log('quote [<char>]')
|
||||||
|
console.log(' Surround the current string in the given quote character.')
|
||||||
|
console.log(' Default is to use the current "quote" setting, which defaults to a single-quote.')
|
||||||
|
console.log('')
|
||||||
|
console.log('unquote [<char>]')
|
||||||
|
console.log(' Try to strip surrounding quotes of the given character from the string.')
|
||||||
|
console.log(' Will only proceed if the string has the quote mark on both ends.')
|
||||||
|
console.log(' Default is to try common quote schemes (single/double quotes, backtick).')
|
||||||
|
console.log('')
|
||||||
|
console.log('enclose [<char>]')
|
||||||
|
console.log(' Wrap the string in the given character. Tries to match pairs when possible.')
|
||||||
|
console.log(' Example: Using `(` will wrap with a closing `)`')
|
||||||
|
console.log(' Default is to wrap with parentheses.')
|
||||||
|
console.log('')
|
||||||
|
console.log('prefix <string>')
|
||||||
|
console.log(' Prepend <string> to the current string.')
|
||||||
|
console.log('')
|
||||||
|
console.log('suffix <string>')
|
||||||
|
console.log(' Append <string> to the current string.')
|
||||||
|
console.log('')
|
||||||
|
console.log('split <on> [<join>]')
|
||||||
|
console.log(' Split the current string using the given <on> separator, and rejoin it using the given <join> separator.')
|
||||||
|
console.log(' Default is to rejoin on newlines.')
|
||||||
|
console.log('')
|
||||||
|
console.log('lines [<on>] [<join>]')
|
||||||
|
console.log(' Like `split`, but defaults to splitting on chunks of whitespace.')
|
||||||
|
console.log('')
|
||||||
|
console.log('join [<with>]')
|
||||||
|
console.log(' Join separate lines in the string using the given <with> separator.')
|
||||||
|
console.log('')
|
||||||
|
console.log('replace <find> <replace>')
|
||||||
|
console.log(' Replace all instances of <find> with <replace>.')
|
||||||
|
console.log('')
|
||||||
|
console.log('lsub <offset> [<length>]')
|
||||||
|
console.log(' Replace the current string with a substring from the left, starting at <offset>.')
|
||||||
|
console.log(' Optionally, limit to <length> characters.')
|
||||||
|
console.log('')
|
||||||
|
console.log('rsub <offset> [<length>]')
|
||||||
|
console.log(' Like `lsub`, but works on the string from right-to-left.')
|
||||||
|
console.log('')
|
||||||
|
console.log('reparse <fromlang> <tolang>')
|
||||||
|
console.log(' Assuming the string is a valid <fromlang> expression, parse it and convert it to <tolang> encoding.')
|
||||||
|
console.log(' Example: reparse json php')
|
||||||
|
console.log('')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
console.log('Advanced Manipulation')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
console.log('line <command...>')
|
||||||
|
console.log(' Runs the given command for every line in the string (separated by \\n).')
|
||||||
|
console.log('')
|
||||||
|
console.log('word <command...>')
|
||||||
|
console.log(' Runs the given command for every word in the string (separated by whitespace).')
|
||||||
|
console.log('')
|
||||||
|
console.log('contains <search>')
|
||||||
|
console.log(' Check if the current string contains <search>. If not, replace it with an empty string.')
|
||||||
|
console.log('')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
console.log('State Management')
|
||||||
|
console.log('----------------------------------------')
|
||||||
|
console.log('show')
|
||||||
|
console.log(' Print out the current string.')
|
||||||
|
console.log('')
|
||||||
|
console.log('clear')
|
||||||
|
console.log(' Replace the current string with an empty string.')
|
||||||
|
console.log('')
|
||||||
|
console.log('undo [<steps>]')
|
||||||
|
console.log(' Undo the past <steps> operations on the string. Defaults to a single operation.')
|
||||||
|
console.log('')
|
||||||
|
console.log('redo')
|
||||||
|
console.log(' Redo the past <steps> operations that were undone. Defaults to a single operation.')
|
||||||
|
console.log('')
|
||||||
|
console.log('save [<file>]')
|
||||||
|
console.log(' Store the current state of the interpreter to the given file path.')
|
||||||
|
console.log(' Defaults to ~/.str.json')
|
||||||
|
console.log('')
|
||||||
|
console.log('load [<file>]')
|
||||||
|
console.log(' Restore a saved state from the given file path to the interpreter.')
|
||||||
|
console.log(' Defaults to ~/.str.json')
|
||||||
|
console.log('exit')
|
||||||
|
console.log(' Exit the interpreter.')
|
||||||
|
console.log('')
|
||||||
|
console.log('set <setting> [<value>]')
|
||||||
|
console.log(' Change the given interpreter <setting> to the given <value>.')
|
||||||
|
console.log(' If no value is given, will reset it back to the default.')
|
||||||
|
console.log(' Supported settings:')
|
||||||
|
console.log(' quote (default: \') - What character is used to parse quoted strings.')
|
||||||
|
console.log(' Also sets the default for the `quote` command.')
|
||||||
|
console.log(' escape (default: \\) - What character is used to parse escape sequences.')
|
||||||
|
console.log(' session (default: ~/.str.json) - Default file used by `save`/`load` commands.')
|
||||||
|
console.log(' debug (default: 0) - Set to 1 to enable debug mode')
|
||||||
|
console.log(' terminator (default: \\n) - What character is used to parse sequential commands.')
|
||||||
|
console.log(' Example: `set terminator ;` will begin parsing commands')
|
||||||
|
console.log(' separated by a ; instead of when enter is pressed.')
|
||||||
|
}
|
||||||
|
|
||||||
|
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() || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ) console.log(`\n---------------\n${this.subject}\n---------------\n`)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
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}"`])
|
||||||
|
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(cmd.file).toString('utf-8'),
|
||||||
|
outfile: () => {
|
||||||
|
fs.writeFileSync(cmd.file, subject)
|
||||||
|
return subject
|
||||||
|
},
|
||||||
|
save: () => {
|
||||||
|
fs.writeFileSync(cmd.file || state.session, JSON.stringify(this.getState()))
|
||||||
|
return PRESERVE_SUBJECT
|
||||||
|
},
|
||||||
|
load: () => {
|
||||||
|
this.loadState(JSON.parse(fs.readFileSync(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,
|
||||||
|
|
||||||
|
indent: () => {
|
||||||
|
const dent = cmd.with === 'spaces'
|
||||||
|
? ''.padStart(parseInt(String(cmd.level || '4')), ' ')
|
||||||
|
: ''.padStart(parseInt(String(cmd.level || '1')), '\t')
|
||||||
|
|
||||||
|
return `${dent}${subject.replace(/^\s*/, '')}`
|
||||||
|
},
|
||||||
|
trim: () => {
|
||||||
|
if ( cmd.type === 'start' || cmd.type === 'left' || cmd.type === 'both' ) {
|
||||||
|
const leftRex = new RegExp(`^${cmd.char || '\\s'}*`, 's')
|
||||||
|
subject = subject.replace(leftRex, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( cmd.type === 'end' || cmd.type === 'right' || cmd.type === 'both' ) {
|
||||||
|
const rightRex = new RegExp(`${cmd.char || '\\s'}*$`, 's')
|
||||||
|
subject = subject.replace(rightRex, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${cmd.with || state.quote}${subject}${cmd.with || state.quote}`
|
||||||
|
},
|
||||||
|
unquote: () => {
|
||||||
|
const marks = state.commonQuotes
|
||||||
|
if ( cmd.mark ) marks.unshift(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: () => `${cmd.with}${subject}${state.encloses[cmd.with] || cmd.with}`,
|
||||||
|
prefix: () => `${cmd.with}${subject}`,
|
||||||
|
suffix: () => `${subject}${cmd.with}`,
|
||||||
|
split: () => subject.split(cmd.on).join(cmd.with),
|
||||||
|
lines: () => subject.split(new RegExp(`${cmd.on || '\\s'}+`, 's')).join(cmd.with),
|
||||||
|
join: () => subject.split('\n').join(cmd.with),
|
||||||
|
replace: () => subject.replaceAll(cmd.find, cmd.with),
|
||||||
|
lsub: () => subject.slice(cmd.offset, cmd.offset + (cmd.len || subject.length)),
|
||||||
|
rsub: () => subject.split('').reverse().slice(cmd.offset, cmd.offset + (cmd.len || subject.length)).reverse().join(''),
|
||||||
|
// reparse,
|
||||||
|
|
||||||
|
help: () => {
|
||||||
|
this.help()
|
||||||
|
return PRESERVE_SUBJECT_NO_PRINT
|
||||||
|
},
|
||||||
|
show: () => PRESERVE_SUBJECT,
|
||||||
|
clear: () => '',
|
||||||
|
undo: () => {
|
||||||
|
for ( let i = 0; i < parseInt(cmd.steps); i += 1 ) {
|
||||||
|
this.undoSubject()
|
||||||
|
}
|
||||||
|
return PRESERVE_SUBJECT
|
||||||
|
},
|
||||||
|
redo: () => {
|
||||||
|
for ( let i = 0; i < parseInt(cmd.steps); i += 1 ) {
|
||||||
|
this.redoSubject()
|
||||||
|
}
|
||||||
|
return PRESERVE_SUBJECT
|
||||||
|
},
|
||||||
|
set: () => {
|
||||||
|
state[cmd.setting] = cmd.to || (makeState()[cmd.setting])
|
||||||
|
return subject
|
||||||
|
},
|
||||||
|
|
||||||
|
line: async () => (await Promise.all(subject
|
||||||
|
.split('\n')
|
||||||
|
.map(line => this.runCommandOnSubject(cmd.sub, line))))
|
||||||
|
.join('\n'),
|
||||||
|
word: async () => {
|
||||||
|
const separators = [...subject.matchAll(/\s+/sg)]
|
||||||
|
const words = await Promise.all(subject.split(/\s+/sg)
|
||||||
|
.map(word => this.runCommandOnSubject(cmd.sub, word)))
|
||||||
|
|
||||||
|
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('')
|
||||||
|
},
|
||||||
|
contains: () => subject.includes(cmd.find) ? subject : '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = runners[cmd.command]
|
||||||
|
if ( !runner ) throw new Error('Invalid command: ' + cmd.command)
|
||||||
|
return runner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const vm = new VM()
|
||||||
|
vm.welcome()
|
||||||
|
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)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokens = (new Lexer(ans)).run()
|
||||||
|
logIfDebug('lexed tokens:', tokens)
|
||||||
|
try {
|
||||||
|
const cmds = (new Parser(tokens)).parse()
|
||||||
|
logIfDebug('parsed commands:', cmds)
|
||||||
|
await vm.runCommands(cmds)
|
||||||
|
if ( vm.shouldExit ) break
|
||||||
|
} catch (parseErr) {
|
||||||
|
console.log('ERROR: ' + parseErr.message)
|
||||||
|
}
|
||||||
|
} catch (lexErr) {
|
||||||
|
console.log('ERROR: ' + lexErr.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.closePrompt()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
})();
|
||||||
Loading…
Reference in New Issue
Block a user