├── __init__.py ├── test.slisp ├── parser.py ├── test.html.slisp ├── __main__.py ├── README.md └── slisp.py /__init__.py: -------------------------------------------------------------------------------- 1 | # hello 2 | #import slisp 3 | #import parser 4 | -------------------------------------------------------------------------------- /test.slisp: -------------------------------------------------------------------------------- 1 | (func atag "{2}") 2 | (func page (atag {0} "pages" {1})) 3 | (func li "\t
  • {0}
  • ") 4 | (func ul "") 5 | (push "a") 6 | (push "b") 7 | (push "c") 8 | (push "d") 9 | -------------------------------------------------------------------------------- /parser.py: -------------------------------------------------------------------------------- 1 | from ply.lex import lex 2 | from ply.yacc import yacc 3 | 4 | tokens = ('ID', # var name 5 | 'RPAREN', #) 6 | 'LPAREN', #( 7 | 'STRING', #" 8 | 'COMMENT' 9 | ) 10 | 11 | schar = r'a-zA-Z_0-9\$\*\{\}\',' 12 | t_STRING = r'"([^"]*?)"' 13 | t_ID = '[%s][%s]*' % (schar, schar) 14 | t_LPAREN = r'\(' 15 | t_RPAREN = r'\)' 16 | t_ignore_COMMENT = r'\;.*' 17 | t_ignore = ' \t\n' 18 | 19 | def t_error(t): 20 | print("Illegal character '%s'" % t.value[0]) 21 | t.lexer.skip(1) 22 | 23 | lexer = lex() 24 | 25 | def p_lisperal(p): 26 | ''' 27 | lisperal : LPAREN lisperal arguments RPAREN 28 | | LPAREN lisperal RPAREN 29 | | ID 30 | | STRING 31 | ''' 32 | if len(p) == 5: 33 | p[0] = [p[2], p[3]] 34 | if len(p) == 4: 35 | p[0] = [p[2]] 36 | if len(p) == 2: 37 | p[0] = p[1] 38 | 39 | def p_arguments(p): 40 | 'arguments : argument arguments' 41 | p[0] = [p[1]] + p[2] 42 | 43 | def p_argument(p): 44 | ''' 45 | argument : lisperal 46 | 47 | ''' 48 | if type(p[1]) == str: 49 | p[0] = p[1].replace('"', '') 50 | if type(p[1]) == list: 51 | p[0] = p[1] 52 | 53 | def p_arguments_empty(p): 54 | 'arguments : ' 55 | p[0] = [] 56 | 57 | def p_error(p): 58 | pass 59 | 60 | parser = yacc() 61 | -------------------------------------------------------------------------------- /test.html.slisp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(headerlink stylesheet text/css src/css/index.css) 5 | $(headerlink icon image/jpg src/imgs/icon.jpg) 6 | 7 | 8 | $name 9 | 10 | 11 | 12 |
    13 |        $(img src/imgs/squid.png "" "120px" "auto") 
    14 |     
    15 |

    16 | Hello! My name is Leonora and I wanted to make a personal site. 17 |

    18 | 19 |

    I wouldn't expect things to 20 | show up on here regularly, but I wanted a small corner of the internet to aggregate things 21 | I make and like. 22 |

    23 |
    24 |

    25 | Pages : 26 |

    27 | 33 |
    34 |

    35 | Social : 36 |

    37 | 41 |
    42 |

    Acknowledgements

    43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | import readline 2 | from os import path 3 | import importlib 4 | import sys 5 | import re 6 | from pprint import pprint 7 | from pathlib import Path 8 | from . import parser 9 | from . import slisp 10 | 11 | def execute(text): 12 | parsed = parser.parser.parse(text) 13 | evald = slisp.evaluate(parsed) 14 | if type(evald) == str and evald != 'None': 15 | return evald 16 | else: 17 | return '' 18 | 19 | def repl(): 20 | while True: 21 | try: 22 | repl_input = input("⊃ ") 23 | if repl_input == "reload": 24 | importlib.reload(slisp) 25 | continue 26 | print(execute(repl_input)) 27 | 28 | except KeyboardInterrupt: 29 | exit(1) 30 | 31 | def runFile(filePath): 32 | with open(filePath) as f: 33 | for line in f.readlines(): 34 | a = execute(line) 35 | print(a, end='') 36 | 37 | def template(file, outputDir='.'): 38 | path = Path(file) 39 | with open(path) as f: 40 | source = f.read() 41 | replacements = ( 42 | (i, execute(i[2:-2])) for i in re.findall(r'\$<.*>\$', source) 43 | ) 44 | for i in replacements: 45 | source = source.replace(i[0], i[1]) 46 | with open(Path(outputDir) / path.stem, 'w') as outfile: 47 | outfile.write(source) 48 | 49 | 50 | 51 | if __name__ == "__main__": 52 | if not not len(files := sys.argv[1:]): 53 | for i in files: 54 | file = Path(i) 55 | if file.suffixes.__len__() > 1 and file.suffix == '.slisp': 56 | template(file, outputDir='docs') 57 | elif file.suffix == '.slisp': 58 | runFile(file) 59 | else: 60 | repl() 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Story 2 | 3 | I was displeased with the way our culture pushes us towards large, complex templating engines that offer little to no arbitrary code execution, too many `%`'s and a frustrating amount of overhead. [Jinja](https://jinja.palletsprojects.com/en/3.1.x/) has loops, ints, native python types, "functions", and (possibly, idk I didn't check) recursion (it probably does). All I want to do is replace one string with another string, and it shouldn't be that hard. [Bash seemed like the perfect tool](https://github.com/turquoise-hexagon/catgirl.sh), until you realise all the hoops one must jump through to have [recursive evaluation in a heredoc](https://github.com/0x3444ac53/Not-Important/commit/cd2a9e3eef115ce5ff0fee3c651e24daa83ada1b#diff-fd3dae0e0ef5235e3f967bcbfa3a3833b22f0fcf8ff0c6c86e1d15c3208d718b). Not to mention the difficulty of trying to [execute a python script](https://github.com/0x3444ac53/Not-Important/blob/cd2a9e3eef115ce5ff0fee3c651e24daa83ada1b/main#L22) without having to create another file. 4 | 5 | I have taken it upon myself to solve this problem for all programmers that fell victim to the computer-science-drop-out-to-finance-bitch pipeline, and created a [Hand Coded Stringy Lisp](https://github.com/0x3444ac53/HCSL). 6 | 7 | # The Last Language you'll ever ~~want~~ need 8 | 9 | All functions evaluate to a lisperal™, there are no variables, only functions that return lisperal™s. Functions are defined using the `func` builtin, which takes two arguments, a id and a lisperal™ 10 | 11 | ```lisp 12 | (func foo "{0} {1}") 13 | (foo "hello" "world"); -> "hello world" 14 | (func bar "hello") 15 | (foo (bar) "world"); -> "hello world" 16 | (func baz (foo (bar) "world")); -> "hello world" 17 | ``` 18 | 19 | There are no `ints`, `floats`, or any of those mathy things that made you hate linear algebra, just strings. I did add a stack though 20 | 21 | I recognise that this may be a difficult transition for some (and we know all about those), and that edge cases may arise that require something other than strings. So, I have graciously included some built-in functions to aid those that have not yet ascended to a higher plane: 22 | 23 | ```lisp 24 | (exit "1") ; -> exits with exitcode 1 25 | ; not very useful in scripts, 26 | ; but handy in the repl 27 | 28 | (execute "bash code") ; will execute a file on your disk 29 | (debug) ; prints functions definitions 30 | (map foo) ; maps a function 'foo' onto all values on the stack 31 | (push "hello") ; pushes "hello" onto the stack 32 | (pop) ; pops value from stack 33 | (read "filePath") ; will read each line of a file onto the stack 34 | (eval "(bar)") ; evaluates code 35 | (source "filePath") ; will load another slisp file into the current namespace 36 | (concat " ") ; will concatinate all values on the stack with first 37 | ; as a seperater 38 | (concat " " "1" "2") ; will return "1 2" and leave the stack untouched 39 | ``` 40 | 41 | # Acknowledgments 42 | 43 | - [catgirl.sh](https://catgirl.sh) by [Camille](https://github.com/turquoise-hexagon) for inspiration 44 | - Samie, who can create a pull request to link to herself if she so desires 45 | - [Crafting Interpreters](https://craftinginterpreters.com/) by [Bob Nystrom](https://github.com/munificent) for introducing me to little languages 46 | - My cat, [Ajax](https://www.instagram.com/p/CvT5ztQgaPs/), for reminding me to stretch 47 | - My faithful companion, [Chester](https://www.instagram.com/p/CusjwW4AKO6/) for 13 wonderful years (R.I.P. 10/07/2023) 48 | - Erik Decker, for answering my calls more often than he should, and because he wanted to be acknowledged 49 | - [evrimoztamur](https://news.ycombinator.com/user?id=evrimoztamur), for [an orange site comment](https://news.ycombinator.com/item?id=37223889) explaining that looping might actually be a good idea 50 | -------------------------------------------------------------------------------- /slisp.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from pprint import pprint, pformat 3 | from . import parser 4 | from pathlib import Path 5 | import shlex 6 | from shutil import copy 7 | import re 8 | 9 | def define_function(function_def): 10 | functions[function_def[0]] = function_def[1:] 11 | return f"{function_def=}" 12 | 13 | def run_file(args): 14 | script = evaluate(*args) 15 | process = subprocess.run(script, 16 | shell=True, 17 | text=True, 18 | capture_output=True) 19 | return process.stdout 20 | 21 | def print_functions(x): 22 | return f"Stack = {pformat(slisp_stack)}\n\n\nFunctions = {pformat(functions)}\n Thank you SB" 23 | 24 | def process_slisp_file(inFile): 25 | print(f"Processing {inFile}") 26 | with open(inFile) as f: 27 | source = f.read() 28 | try: 29 | replacements = ( 30 | (i, evaluate(parser.parser.parse(i[2:-2]))) for i in re.findall(r'\$<.*>\$', source)) 31 | for i in replacements: 32 | source = re.sub(re.escape(i[0]), i[1], source, count=1) 33 | except Exception as e: 34 | print(e) 35 | return source 36 | 37 | def template_single(inFile, outFile): 38 | outFile.parents[0].mkdir(parents=True, exist_ok=True) 39 | if inFile.suffix == '.slisp': 40 | processed = process_slisp_file(inFile) 41 | outFile = outFile.with_name(outFile.stem) 42 | with outFile.open('w') as f: 43 | f.write(processed) 44 | returnString = f"SlispFile {inFile} to {outFile}" 45 | else: 46 | try: 47 | copy(inFile, outFile) 48 | return f"{inFile} to {outFile}" 49 | except IsADirectoryError: 50 | returnString = f"skipping {outFile}: is a directory" 51 | return returnString 52 | 53 | 54 | def template(args): 55 | args = [Path(evaluate(i)) 56 | for i in args] 57 | inFile, outFile = args 58 | if inFile.is_dir(): 59 | return "\n".join([template_single(file, outFile / Path("/".join(file.parts[1:]))) for file in inFile.rglob("**/*")]) 60 | 61 | 62 | def slisp_map(args): 63 | global slisp_stack 64 | for i in args: 65 | try: 66 | function_def = functions[i][0] 67 | slisp_stack = list(map(function_def.format, slisp_stack)) 68 | except TypeError: 69 | function_def = functions[i] 70 | slisp_stack = list(map(function_def, slisp_stack)) 71 | return f"mapped {function_def}" 72 | 73 | def concat(join_on): 74 | global slisp_stack 75 | returnval = "" 76 | join_on = [evaluate(i) if type(i) == list else i for i in join_on] 77 | if len(join_on) > 1: 78 | for i in join_on[1:]: 79 | returnval = returnval + join_on[0] + i 80 | else: 81 | for i in range(len(slisp_stack)): 82 | if i > 0: 83 | returnval = returnval + join_on[0] + slisp_stack.pop() 84 | else: 85 | returnval = returnval + slisp_stack.pop() 86 | return returnval 87 | 88 | def load_file(x): 89 | for file in x: 90 | with open(file) as f: 91 | for i in f.readlines(): 92 | slisp_stack.append(i) 93 | return "" 94 | 95 | def source_file(x): 96 | for file in x: 97 | try: 98 | with open(file) as f: 99 | a = [evaluate(parser.parser.parse(i)) for i in f.readlines()] 100 | return "\n".join(a) 101 | except FileNotFoundError as e: 102 | print(e) 103 | return "" 104 | 105 | def shell_quote(x): 106 | return shlex.quote(evaluate(*x)) 107 | 108 | def slisp_push(x): 109 | slisp_stack.append(evaluate(*x)) 110 | return f"pushed {slisp_stack[-1]}" 111 | 112 | def slisp_stack_dup(x): 113 | slisp_stack.append(slisp_stack[-1]) 114 | return f"Duped {slisp_stack=}" 115 | 116 | def evaluate(args): 117 | if not args: 118 | return "" 119 | if type(args) == str: 120 | return args 121 | 122 | try: 123 | func_name, args = args 124 | except ValueError: 125 | func_name = args[0] 126 | 127 | try: 128 | return functions[func_name](args) 129 | except TypeError: 130 | pass 131 | except KeyError: 132 | pass 133 | 134 | try: 135 | function_def = functions[func_name][0] 136 | except TypeError: 137 | function_def = functions[func_name] 138 | except KeyError: 139 | return " ".join([func_name] + args) 140 | 141 | args = [evaluate(i) for i in args] 142 | 143 | if type(function_def) == str: 144 | return function_def.format(*args, dq='"').replace("\\n", "\n") 145 | if type(function_def) == list: 146 | return evaluate(function_def).format(*args) 147 | 148 | slisp_stack = [] 149 | 150 | functions = { 151 | "func" : define_function, # function_def 152 | "exit" : lambda x: exit(int(*x)), # repl exit 153 | "source" : source_file, 154 | "execute" : lambda x: run_file(x), 155 | "debug" : print_functions, 156 | "map" : lambda x: slisp_map(x), 157 | "concat" : concat, 158 | "shell_quote" : shell_quote, 159 | "template" : template, 160 | "push" : slisp_push, 161 | "pop" : lambda x: slisp_stack.pop(), 162 | "read" : load_file, 163 | "eval" : lambda x: evaluate(parser.parser.parse(x)), 164 | "dup" : slisp_stack_dup 165 | } 166 | --------------------------------------------------------------------------------