massive project restructure

relocated everything in its own module, `cli`, and split the server code
such that it's more component based, if you will

also, added a working test suite, which is somewhat exciting.
This commit is contained in:
stupidcomputer 2024-06-16 18:18:28 -05:00
parent b7c983d3d6
commit 6a038fcdde
20 changed files with 685 additions and 380 deletions

4
cli/__main__.py Normal file
View File

@ -0,0 +1,4 @@
from .cli import main
if __name__ == "__main__":
main()

30
cli/cli.py Normal file
View File

@ -0,0 +1,30 @@
from .lib.server import DesmosGraphServer
from .lib.graphparser import DesmosGraph, DesmosGraphOverride
from .lib.clientside import payload as JSGraphPayload
from .tests.isa import test_entry_point
def main():
# graph = DesmosGraph.from_file("data/computer.desmos")
# override = DesmosGraphOverride.from_file("test.override")
#
# graph.include_override(override)
# server = DesmosGraphServer()
# server.append_inst({
# "type": "insert_graph",
# "graph": graph,
# })
# server.append_inst({
# "type": "test_graph",
# "graph": graph,
# "name": "test and assert addition",
# "expectedOutput": [1, 4, 6, 0, 0, 4],
# "expression": "B",
# })
# server.start()
# print(server.outputs)
#
test_entry_point()
if __name__ == "__main__":
main()

0
cli/data/__init__.py Normal file
View File

118
cli/data/computer.py Normal file
View File

@ -0,0 +1,118 @@
payload = """
ticker 1 : main
# Main memory structure
# This should be filled in with an override
id testing : B = []
# Operations on memory
: q(l, v, i)=ifval(l, i, [1...length(l)], v)
: setlistval(index, v) = B -> q(B, v, index)
: incx(index, v) = setlistval(index, B[index] + v)
: ifval(l, index, c, v) = {c = index : v, l[c]}
# Operations
: oadd(x, y) = x + y
: osub(x, y) = x - y
: odiv(x, y) = x / y
: omul(x, y) = x * y
# Instruction implementation
: ijmp(a, b, c) = ip -> a, jumped -> 1
: iadd(a, b, t_o) = setlistval(t_o, oadd(B[a], B[b]))
: isub(a, b, t_o) = setlistval(t_o, osub(B[a], B[b]))
: idiv(a, b, t_o) = setlistval(t_o, odiv(B[a], B[b]))
: imul(a, b, t_o) = setlistval(t_o, omul(B[a], B[b]))
: icmp(a, b, z) = { \
a = b : equals -> 1 , \
a > b : greater -> 1 , \
a < b : less -> 1 \
}
: irst(a, b, c) = equals -> 0, greater -> 0, less -> 0
: ield(addr, b) = setlistval(addr, equals)
: igld(addr, b) = setlistval(addr, greater)
: illd(addr, b) = setlistval(addr, less)
: ibe(addr, b, c) = {equals = 1 : ijmp(addr, 0, 0), jumped -> 1}
: ibne(addr, b, c) = {equals = 0 : ijmp(addr, 0, 0), jumped -> 1}
: ibg(addr, b, c) = {greater = 1 : ijmp(addr, 0, 0), jumped -> 1}
: ibl(addr, b, c) = {less = 1 : ijmp(addr, 0, 0), jumped -> 1}
: isto(v, addr) = setlistval(addr, v)
: imov(from, target) = setlistval(target, B[from])
: ipsto(value, ptr) = setlistval(B[ptr], value)
# registers
# instruction pointer
: ip = 1
# is the result of icmp equal?
: equals = 0
# ditto for greater than
: greater = 0
# ditto for less than
: less = 0
# instruction loading areas
: inst = 0
# next three values after the instruction
: paramone = 0
: paramtwo = 0
: paramthree = 0
# main execution flows
: load(addr) = jumped -> 0, inst -> B[addr], \
paramone -> B[addr + 1], \
paramtwo -> B[addr + 2], \
paramthree -> B[addr + 3]
: exec = { \
inst = sto : isto(paramone, paramtwo), \
inst = psto : ipsto(paramone, paramtwo), \
inst = mov : imov(paramone, paramtwo), \
inst = add : iadd(paramone, paramtwo, paramthree), \
inst = cmp : icmp(paramone, paramtwo, paramthree), \
inst = eld : ield(paramone, paramtwo), \
inst = gld : igld(paramone, paramtwo), \
inst = lld : illd(paramone, paramtwo), \
inst = jmp : ijmp(paramone, paramtwo, paramthree), \
inst = be : ibe(paramone, paramtwo, paramthree), \
inst = bne : ibne(paramone, paramtwo, paramthree), \
inst = bg : ibg(paramone, paramtwo, paramthree), \
inst = bl : ibl(paramone, paramtwo, paramthree), \
inst = sub : isub(paramone, paramtwo, paramthree), \
inst = mul : imul(paramone, paramtwo, paramthree), \
inst = div : idiv(paramone, paramtwo, paramthree) \
}
: incip = {jumped = 0 : ip -> ip + instwidth[inst] + 1}
# execution occurs here
: execution = 0
: jumped = 0
: loop = {execution = 0 : execution -> 1, execution = 1 : execution -> 2, execution = 2 : execution -> 0}
: loopaction = {execution = 0 : load(ip), execution = 1 : exec, execution = 2 : incip}
: main = loopaction, loop
: sto = 1
: psto = 16
: mov = 17
: add = 2
: cmp = 3
: eld = 4
: gld = 5
: lld = 6
: jmp = 7
: be = 8
: bne = 9
: bg = 10
: bl = 11
: sub = 12
: mul = 13
: div = 14
: rst = 15
: instwidth = [2,3,1,1,1,1,1,1,1,1,3,3,3,3,0,2,2]
"""

View File

@ -1,6 +1,8 @@
payload = """
: testing = 32
: b = testing
: m = 3
: y = m * x + b
: y = 2m * x + b
: \frac{x^{2+3}}{3}*4*testing
: \frac{x^{2+3}}{3}*4*testing
"""

0
cli/lib/__init__.py Normal file
View File

87
cli/lib/clientside.py Normal file
View File

@ -0,0 +1,87 @@
payload = """
// https://stackoverflow.com/questions/3115982/how-to-check-if-two-arrays-are-equal-with-javascript
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
function main() {
let socket = new WebSocket("ws://localhost:8764");
var toCompare = "";
socket.onopen = function(e) {
console.log("[LOG] sending client ping")
socket.send("client ping");
}
socket.onclose = function(e) {
setTimeout(function() {
main();
}, 1000);
}
socket.onmessage = function(e) {
var message = JSON.parse(e.data);
console.log(message.message)
if (message.message === "clear") {
console.log("[LOG] removing expressions from the graph");
Calc.getExpressions().map((i) => {
return i.id;
console.log(`[LOG] removing expression ${i.id}`);
}).map((i) => {
Calc.removeExpression({ id: i });
});
} else if (message.message === "expression") {
console.log(`[LOG] adding expression ${message.payload} as id ${message.id}`);
Calc.setExpression({
type: "expression",
latex: message.payload,
id: message.id,
})
} else if (message.message === "ticker") {
var state = Calc.getState();
state.expressions.ticker = {
handlerLatex: message.payload,
minStepLatex: message.rate,
open: true,
};
Calc.setState(JSON.stringify(state))
} else if (message.message === "eval") {
toCompare = eval(message.expectedOutput);
var exp = Calc.HelperExpression({
latex: message.expression
})
var timeoutid = setTimeout(function() {
console.log("[LOG] failing because timeout occured")
socket.send(JSON.stringify({
"name": message.expression,
"output": "false"
}))
exp.unobserve('listValue')
}, 5000);
exp.observe('listValue', function () {
console.log(exp.listValue);
if(arraysEqual(exp.listValue, toCompare)) {
socket.send(JSON.stringify({
"name": message.expression,
"output": "true"
}))
clearTimeout(timeoutid);
}
})
Calc.controller.listModel.ticker.playing = true;
} else {
console.log(`[LOG] couldn't parse message ${e.data}`)
}
}
} main();
"""

252
cli/lib/graphparser.py Normal file
View File

@ -0,0 +1,252 @@
from dataclasses import dataclass
from typing import Callable, ClassVar, Self, Any, IO, List, Dict
from json import loads
reserved_keywords = "sin cos tan csc sec cot mean median min max quertile quantile stdev stdevp var mad cov covp corr spearman stats count total join sort shuffle unique for histogram dotplot boxplot normaldist tdist poissondist binomialdist uniformdist pdf cdf inversecdf random ttest tscore ittest frac sinh cosh tanh csch sech coth polygon distance midpoint rgb hsv lcm gcd mod ceil floor round sign nPr nCr log with cdot to in length left right operatorname"
reserved_keywords = reserved_keywords.split(' ')
def continue_lines(string: str) -> str:
# if there's a '\' and then a newline, ignore the newline.
return string.replace('\\\n', '')
def replace_multiplication(string: str) -> str:
return string.replace('*', '\\cdot ')
def replace_actions(string: str) -> str:
return string.replace(' ->', '\\to ').replace('->', '\\to ')
def replace_tabs(string: str) -> str:
return string.replace('\t', ' ')
def merge_multiple_spaces(string: str) -> str:
return ' '.join(string.split())
def curly_brackets(string: str) -> str:
return string.replace('{', '\\left\\{').replace('}', '\\right\\}')
def parens(string: str) -> str:
return string.replace('(', '\\left(').replace(')', '\\right)')
def for_fix(string: str) -> str:
return string \
.replace('for\\ ', "\\operatorname{for}") \
.replace('for', "\\operatorname{for}") \
.replace('length\\ ', "\\operatorname{length}") \
.replace('length', "\\operatorname{length}")
def make_spaces_permanant(string: str) -> str:
return string.replace(' ', '\ ')
def change_square_brackets(string: str) -> str:
return string.replace('[', '\\left[').replace(']', '\\right]')
def subscriptize(string: str) -> str:
output = ""
buffer = ""
for char in (string + "\n"): # add a newline so there's always a non-alpha char at the end
if char.isalpha():
buffer += char
else:
if buffer:
# check if our buffer is a reserved word; if so, do
# not expand.
if not buffer in reserved_keywords:
if len(buffer) > 1:
output += "{}_{{{}}}".format(buffer[0], buffer[1:])
else:
output += buffer[0]
else:
output += buffer
buffer = ""
output += char
return output.rstrip()
@dataclass
class DesmosGraphOverride:
"""
Container for Desmos Overrides.
This is the method by which machine code compiled by assembler or otherwise, is inserted.
It's sort of like a fancy substitution macro system thing. It's still prone to stupidity.
"""
# XXX: If you use the Dict[str, str] type annotation, dataclass throws an error:
# ValueError: mutable default <class 'dict'> for field payload is not allowed: use default_factory
# Ask a question about this
payload: Any = None
@classmethod
def from_file(cls, filename: str) -> Self:
"""
Read a Override from the filename filename.
"""
fd = open(filename, "r")
return cls.from_file_object(fd)
@classmethod
def from_file_object(cls, io_obj: IO) -> Self:
"""
Read a Override from fileobj fileobj.
"""
text = io_obj.read()
return cls.from_text(text)
@classmethod
def from_text(cls, text: str) -> Self:
return cls(loads(text))
@dataclass
class DesmosGraphStatement:
"""
This class contains one Desmos graph statement -- that is, one line in the editor.
You probably shouldn't create this class manually; it's used in .DesmosGraph.
"""
commands: str
latex: str
def __getitem__(self, item) -> str:
try:
return self.commands[item]
except KeyError:
return None
def __repr__(self) -> str:
return "{} : {}".format(str(self.commands), self.latex)
@classmethod
def from_line(cls, line) -> Self:
if not line:
return None
if line[0] == "#": # ignore comments
return None
splitted = line.split(':')
commands = splitted[0]
latex = ':'.join(splitted[1:])
commands = commands.split(' ')
iterator = iter(commands)
commands = dict(zip(iterator, iterator))
try:
del commands['']
except KeyError:
pass
return cls(commands, latex)
@classmethod
def from_lines(cls, lines) -> List[str]:
output = []
for line in lines:
output.append(cls.from_line(line))
return list(
filter(
lambda item: item is not None,
output
)
)
@dataclass
class DesmosGraph:
"""
This class represents a Desmos Graph. That is, the thing that you interact with when you use Desmos. That thing.
You want to create one through a series of Desmos statements. This is a cobbled together DSL that uses .replace and .split regularly. Beware!
"""
text_preprocessors: ClassVar[List[Callable[[str], str]]] = [
continue_lines,
replace_multiplication,
replace_actions,
replace_tabs
]
line_preprocessors: ClassVar[List[Any]] = [
DesmosGraphStatement.from_lines
]
latex_preprocessors: ClassVar[List[Callable[[str], str]]] = [
merge_multiple_spaces,
curly_brackets,
parens,
subscriptize,
change_square_brackets,
for_fix,
make_spaces_permanant
]
text: str
def __post_init__(self):
self.ast: List[DesmosGraphStatement] = []
self._parse()
def _parse(self) -> None:
text: str = self.text
for preprocessor in self.text_preprocessors:
text = preprocessor(text)
lines = text.split('\n')
for preprocessor in self.line_preprocessors:
lines = preprocessor(lines)
for index, line in enumerate(lines):
if not line["comment"]:
for preprocessor in self.latex_preprocessors:
lines[index].latex = preprocessor(line.latex)
self.ast = lines
def to_file(self, filename: str) -> None:
"""
Write the Graph to the filename filename.
"""
fd = open(filename, "w")
self.to_fileobj(fd)
def to_fileobj(self, io_obj: IO) -> None:
"""
Write the Graph to the fileobj fileobj.
"""
io_obj.write(self.text)
@classmethod
def from_file(cls, filename: str) -> Self:
"""
Read a Graph from the filename filename.
"""
fd = open(filename, "r")
return cls.from_file_object(fd)
@classmethod
def from_file_object(cls, io_obj: IO) -> Self:
"""
Read a Graph from fileobj fileobj.
"""
text = io_obj.read()
return cls(text)
def include_override(self, override: DesmosGraphOverride) -> None:
"""
Overrides are the way to include changes in a Desmos Graph. This is changes in the instructions and stuff.
If you're writing an assembler, you can use an override for computer.desmos to include the compiled machine code.
"""
# XXX: This is O(n^2). Or somewhat inefficient. Not a computer science major.
for key in override.payload.keys():
for statement in self.ast:
try:
if statement.commands["id"] == key:
statement.latex = override.payload[key]
except KeyError:
pass

106
cli/lib/server.py Normal file
View File

@ -0,0 +1,106 @@
import asyncio
from websockets.server import serve
from websockets.sync.client import connect
import json
import random
class DesmosGraphServer:
instructions_to_run = []
outputs = []
async def _reset(self, websocket):
await websocket.send(
json.dumps(
{
"message": "clear",
"payload": "none",
}
)
)
async def _send_graph(self, websocket, graph):
for line in graph.ast:
if line["ticker"]:
await websocket.send(
json.dumps(
{
"message": "ticker",
"rate": line.commands["ticker"],
"payload": line.latex,
}
)
)
continue
# if the line has been assigned an id, make it so
if line["id"]:
ident = line.commands["id"]
else:
# else just choose a safe option
ident = "placeholder" + str(random.randint(1, 100100))
await websocket.send(
json.dumps(
{
"message": "expression",
"id": ident,
"payload": line.latex,
}
)
)
async def _check_for_thing(self, websocket, expectedOutput, expression):
await websocket.send(
json.dumps(
{
"message": "eval",
"expectedOutput": expectedOutput,
"expression": expression,
}
)
)
async def ws_main(self, websocket):
message = await websocket.recv() # eat the client ping
for instruction in self.instructions_to_run:
await self._reset(websocket)
if instruction["type"] == "insert_graph":
await self._send_graph(websocket, instruction["graph"])
elif instruction["type"] == "test_graph":
await self._send_graph(websocket, instruction["graph"])
await self._check_for_thing(
websocket,
instruction["expectedOutput"],
instruction["expression"]
)
message = await websocket.recv()
jsonified = json.loads(message)
result = jsonified["output"]
self.outputs.append(
{
"name": instruction["name"],
"output": result,
}
)
self.stop.set_result("sotp please!!!!")
async def main(self, stop):
async with serve(self.ws_main, "localhost", 8764):
self.stop = stop
await stop
def start(self):
loop = asyncio.get_event_loop()
stop = loop.create_future()
loop.run_until_complete(self.main(stop))
def append_inst(self, inst):
self.instructions_to_run.append(inst)

0
cli/tests/__init__.py Normal file
View File

84
cli/tests/isa.py Normal file
View File

@ -0,0 +1,84 @@
import unittest
import timeout_decorator
import time
from cli.lib.server import DesmosGraphServer
from cli.lib.graphparser import DesmosGraph, DesmosGraphOverride
from cli.data.computer import payload as computer_graph_payload
def arr_to_override_text(arr):
arr = [str(i) for i in arr]
return "B = \\left[{}\\right]".format(
", ".join(arr)
)
def instruction_test_helper(override_text, expected_output):
graph = DesmosGraph(computer_graph_payload)
override = DesmosGraphOverride({
"testing": arr_to_override_text(override_text)
})
graph.include_override(override)
server = DesmosGraphServer()
server.instructions_to_run = []
server.append_inst({
"type": "test_graph",
"graph": graph,
"name": "",
"expectedOutput": expected_output,
"expression": "B",
})
server.start()
time.sleep(1)
return server.outputs[-1]["output"] == "true"
class ISATest(unittest.TestCase):
def test_store_positive(self):
self.assertTrue(
instruction_test_helper(
[1, 4, 6, 0, 0, 0], # store lit. 4 to address 6
[1, 4, 6, 0, 0, 4]
)
)
def test_addition(self):
self.assertTrue(
instruction_test_helper(
[2, 1, 1, 5, 0], # add addresses 1 and 1 to cell 5
[2, 1, 1, 5, 4]
)
)
def test_subtraction(self):
self.assertTrue(
instruction_test_helper(
[12, 1, 2, 5, 0], # 12 - 1 into cell 5
[12, 1, 2, 5, 11]
)
)
def test_multiplication(self):
self.assertTrue(
instruction_test_helper(
[13, 1, 3, 5, 0], # 13 * 3
[13, 1, 3, 5, 39],
)
)
def test_division(self):
self.assertTrue(
instruction_test_helper(
[14, 1, 6, 5, 0, 7], # 14 / 7
[14, 1, 6, 5, 2, 7],
)
)
def test_division_with_decimal(self):
self.assertTrue(
instruction_test_helper(
[14, 1, 6, 5, 0, 4], # 14 / 4 = 3.5
[14, 1, 6, 5, 3.5, 4],
)
)

View File

@ -1,46 +0,0 @@
function main() {
let socket = new WebSocket("ws://localhost:8764");
var ids = [];
socket.onopen = function(e) {
console.log("[LOG] sending client ping")
socket.send("client ping");
}
socket.onmessage = function(e) {
var message = JSON.parse(e.data);
console.log(message.message)
if (message.message === "clear") {
console.log("[LOG] removing expressions from the graph");
for(i in ids) {
console.log(`[LOG] removing expression ${ids[i]}`)
Calc.removeExpression({
id: ids[i],
})
}
ids = [];
} else if (message.message === "expression") {
console.log(`[LOG] adding expression ${message.payload} as id ${message.id}`);
Calc.setExpression({
type: "expression",
latex: message.payload,
id: message.id,
})
ids.push(message.id)
} else if (message.message === "ticker") {
var state = Calc.getState();
state.expressions.ticker = {
handlerLatex: message.payload,
minStepLatex: message.rate,
open: true,
};
Calc.setState(JSON.stringify(state))
} else {
console.log(`[LOG] couldn't parse message ${e.data}`)
}
}
} main();

View File

@ -1,42 +0,0 @@
from .server import main
import argparse
import pyperclip
import json
def get_overrides(file):
fd = open(file, "r")
data = json.loads(fd.read())
fd.close()
return data
def define_args(parser):
parser.add_argument('--copy', action="store_true", help="copy the client side JS to clipboard")
parser.add_argument('--assemble', nargs=2, help="assemble the file INFILE and write the resultant overrides to OUTFILE")
parser.add_argument('--run', help="specify file to start the desmos server for")
parser.add_argument('--overrides', help="specify file that contains overrides for desmos expressions")
def entry():
parser = argparse.ArgumentParser(
prog="desmosisa",
description="a smörgåsbord of utilities for desmos, including some implementations of an desmos-based isa",
)
define_args(parser)
args = parser.parse_args()
if args.overrides:
args.overrides = get_overrides(args.overrides)
if args.run:
main(args.run, args.overrides if args.overrides else {})
elif args.copy:
fd = open("console.js", "r")
buffer = fd.read()
pyperclip.copy(buffer)
print("copied")
else:
parser.print_help()
if __name__ == "__main__":
entry()

View File

@ -1,4 +0,0 @@
from . import entry
if __name__ == "__main__":
entry()

View File

@ -1,176 +0,0 @@
# desmos reserved keywords. think function names, and other verbs
# like with, etc.
reserved_keywords = "sin cos tan csc sec cot mean median min max quertile quantile stdev stdevp var mad cov covp corr spearman stats count total join sort shuffle unique for histogram dotplot boxplot normaldist tdist poissondist binomialdist uniformdist pdf cdf inversecdf random ttest tscore ittest frac sinh cosh tanh csch sech coth polygon distance midpoint rgb hsv lcm gcd mod ceil floor round sign nPr nCr log with cdot to in length left right operatorname"
reserved_keywords = reserved_keywords.split(' ')
def continue_lines(string):
# if there's a '\' and then a newline, ignore the newline.
return string.replace('\\\n', '')
def replace_multiplication(string):
return string.replace('*', '\\cdot ')
def replace_actions(string):
return string.replace(' ->', '\\to ').replace('->', '\\to ')
def replace_tabs(string):
return string.replace('\t', ' ')
def split_linewise(string):
return string.split('\n')
def merge_multiple_spaces(string):
return ' '.join(string.split())
def curly_brackets(string):
return string.replace('{', '\\left\\{').replace('}', '\\right\\}')
def parens(string):
return string.replace('(', '\\left(').replace(')', '\\right)')
def for_fix(string):
return string \
.replace('for\\ ', "\\operatorname{for}") \
.replace('for', "\\operatorname{for}") \
.replace('length\\ ', "\\operatorname{length}") \
.replace('length', "\\operatorname{length}")
def make_spaces_permanant(string):
return string.replace(' ', '\ ')
def change_square_brackets(string):
return string.replace('[', '\\left[').replace(']', '\\right]')
def subscriptize(string):
output = ""
buffer = ""
for char in (string + "\n"): # add a newline so there's always a non-alpha char at the end
if char.isalpha():
buffer += char
else:
if buffer:
# check if our buffer is a reserved word; if so, do
# not expand.
if not buffer in reserved_keywords:
if len(buffer) > 1:
output += "{}_{{{}}}".format(buffer[0], buffer[1:])
else:
output += buffer[0]
else:
output += buffer
buffer = ""
output += char
return output.rstrip()
class Statement:
def __init__(self, commands, latex):
self.commands = commands
self.latex = latex
def __getitem__(self, item):
try:
return self.commands[item]
except KeyError:
return None
def __repr__(self):
return "{} : {}".format(str(self.commands), self.latex)
@classmethod
def from_line(cls, line):
if not line:
return None
if line[0] == "#": # ignore comments
return None
splitted = line.split(':')
commands = splitted[0]
latex = ':'.join(splitted[1:])
commands = commands.split(' ')
iterator = iter(commands)
commands = dict(zip(iterator, iterator))
try:
del commands['']
except KeyError:
pass
return cls(commands, latex)
@classmethod
def from_lines(cls, lines):
output = []
for line in lines:
output.append(cls.from_line(line))
return list(
filter(
lambda item: item is not None,
output
)
)
class Parser:
"""
Implements the parsing of the Desmos local DSL.
General syntax:
option1 value1 option2 value2 option3 value3 : latex
in latex statements, multiplication between variables is *not* implicit.
this is because
`testing`
becomes
`t_{esting}`.
In order to multiple two variables together, use
`a * b`
instead of
`ab`.
"""
def __init__(self, file):
self.file = file
self.ast = []
def parse(self):
with open(self.file, "r") as f:
text = f.read()
text = continue_lines(text)
text = replace_multiplication(text)
text = replace_actions(text)
text = replace_tabs(text)
lines = split_linewise(text)
lines = Statement.from_lines(lines)
for index, line in enumerate(lines):
if not line["comment"]:
# now, remove multiple spaces in lines, replacing them with one.
lines[index].latex = merge_multiple_spaces(line.latex)
# replace curly brackets and parens
lines[index].latex = curly_brackets(line.latex)
lines[index].latex = parens(line.latex)
# convert things like testing to t_{esting}
lines[index].latex = subscriptize(line.latex)
# change square brackets to \\left[ and \\right]
lines[index].latex = change_square_brackets(line.latex)
print(lines[index].latex)
# replace for with \\operatorname{for}
lines[index].latex = for_fix(line.latex)
print(lines[index].latex)
# make the spaces escaped and 'permanant'
lines[index].latex = make_spaces_permanant(line.latex)
self.ast = lines

View File

@ -1,109 +0,0 @@
from websockets.server import serve
import asyncio
import os
import time
import json
import random
import queue
import functools
from .parser import Parser, Statement
from watchdog.events import FileSystemEventHandler
from watchdog.events import FileModifiedEvent
from watchdog.observers import Observer
class FSEHandler(FileSystemEventHandler):
def __init__(self, queue, *args, **kwargs):
self.queue = queue
super().__init__(*args, **kwargs)
def on_modified(self, event):
self.queue.put("")
async def serv(websocket, file, overrides={}):
message = await websocket.recv()
lmtime = 0
epsilon = 0.25 # tweak this to what makes sense. 0.25 seconds makes sense to me.
q = queue.Queue()
# make it read the file initially
q.put("")
# setup the watchdog
observer = Observer()
event_handler = FSEHandler(q)
observer.schedule(event_handler, file)
observer.start()
print("client connected")
while True:
# there are sometimes multiple writes bundled close together -- so
# debounce them.
q.get()
if time.time() - lmtime < epsilon:
continue
else:
lmtime = time.time()
parser = Parser(file)
parser.parse()
await websocket.send(
json.dumps(
{
"message": "clear",
"payload": "none",
}
)
)
for line in parser.ast:
if line["ticker"]:
await websocket.send(
json.dumps(
{
"message": "ticker",
"rate": line.commands["ticker"],
"payload": line.latex,
}
)
)
continue
# if the line has been assigned an id, make it so
if line["id"]:
ident = line.commands["id"]
print("performing substitution")
else:
# else just choose a safe option
ident = "placeholder" + str(random.randint(1, 100100))
if ident in overrides.keys():
to_send = overrides[ident]
else:
to_send = line.latex
await websocket.send(
json.dumps(
{
"message": "expression",
"id": ident,
"payload": to_send,
}
)
)
async def start_server(file, overrides):
print("starting server")
wrapper = functools.partial(serv, file=file, overrides=overrides)
async with serve(wrapper, "localhost", 8764):
print("starting server for realz")
await asyncio.Future()
def main(file, overrides):
asyncio.run(start_server(file, overrides))
if __name__ == "__main__":
main("data/testing.desmos")

View File

@ -1,4 +1,4 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = with pkgs.python311Packages; [ websockets watchdog pyperclip ];
nativeBuildInputs = with pkgs.python311Packages; [ websockets watchdog pyperclip timeout-decorator ];
}

View File

@ -1 +0,0 @@
{"testing": "\\left[1,2,3,4,5,1\\right]"}