EsoML stands for Esoteric Markup Language and it is an esoteric language that allows you to write code as if you were writing Vue.js rendering functions, but with the feel of assembly. It's a language that lets you create awesome websites that feel like they're from the 2000s, without any attributes to elements and styling. Raw HTML rendered using JavaScript under the hood.
The language is based on a similar concept as modern compiled languages such as Vue.js with it's openBlock
etc.
functions, but reversed. It also resembles the feel of true assembly, although it has some big differences.
Adding attributes to elements and styling them is a TODO, it's not yet implemented.
This is a simple guide on how to use EsoML.
Line numbers are counted starting with 1
. There are no explicit comments. However, you can implicitly comment
instructions. That is because very instruction has the syntax/format as described
below <type> <argument1> <argument2>...<argumentN>
, you can use the arguments unused by the instruction to pass in
pseudo-comments (which will be treated as unused arguments for the instruction by the compiler). An example
is the following render
section:
.render main
cont h1 This will create a h1 container tag/element
text 0s Show the text from the top of the stack
econ Don't forget to close it
Sections are started with a so-called section header: .<section> <argument>
Format: .strings <locale>
This is the section where your localized strings will be stored. The <locale>
parameter is the locale you're defining
strings for.
You write strings there in the following format: Let <id> be translated to <text>.
. The <id>
parameter is
the ID of the <text>
(not the final number/key you reference in code) base-8 encoded for better readability.
See references to values for more info. The <text>
parameter is the text you
want to store.
Example:
.strings en_US
Let 1 be translated to EsoML string example.
Format: .rom <locale>
This is the section where your localized read-only numbers will be stored. The <locale>
parameter is the locale you're
defining numbers for.
You write numbers there in the following format: Remember that <id> will always be <number>.
. The <id>
parameter is
the ID of the <number>
(not the final number/key you reference in code) base-8 encoded for better readability.
See references to values for more info. The <number>
parameter is base-11
encoded to make
your code cleaner.
Example:
.rom en_US
Remember that 1 will always be 3.
Format:
- Code -
.code <label>
- Render -
.render <label>
These are the sections where your main code will be stored. The <label>
parameter is the label you call to control the
execution flow. The difference between .code <label>
and .render <label>
is the fact that .code <label>
is used to
handle events and .code main
is used to initialize the stack, for example, whereas the .render <label>
section is
used as a component that can simply be reused and that will be able to output elements (thus it can use
the text
, cont
and other render-only instructions.)
Example:
.render main
cont h1 Creates a container (div by default)
text 78t Adds text to the content (equivalent to innerText)
econ Closes the container
All instructions are exactly four characters long for better readability.
Format: <type> <argument1> <argument2>...<argumentN>
The cont <type?|div>
instruction is used to start/open a container.
The econ
instruction is used to end/close a container.
The elem <type>
instruction is used to create an element without the ability to set its contents.
It's an equivalent of doing this:
cont <type>
econ
The text <text>
instruction is used to inject/add/render text. Can be thought of as elem.innerText += <text>
.
The show <html>
instruction is used to inject/add/render raw HTML. Can be thought of as elem.innerHTML += <html>
.
Note that only the first HTML element the raw HTML results to when parsed is added/rendered.
The call
instruction is used to either execute another code
section if called from a code
section, or inject
another render
section if called from a render
section (can be thought of as using a custom UI component). The
executed section is run from its beginning to its end, there's no conditional return instruction. The only thing you can
do is conditionally execute instructions (and potentially perform nested calls) from within the ifis
-endi
statement.
The rend
instruction is used to schedule a re-render. Can be used in both code
and render
sections, as it is
essential to create a Truth Machine. Scheduled re-renders are executed every 100ms, though that can be changed
in lib.js --> RERENDER_TIMEOUT
The hear <event> <callback>
instruction is used to add an event listener to listen to any kind of event. This is the
bond of code
and render
sections, because it can only be used in a render
section, but the <callback>
must be
a code
section.
The push <value>
instruction is used to push a value to the top of the stack.
The copy
instruction is used to duplicate the value at the top of the stack. Technically, it is an equivalent of
doing push 0s
.
The pops
instruction is used to pop a value from the stack. The value is simply discarded.
The swap <offA?|0> <offB?|1>
instruction is used to swap the desired values on the stack. By default, it swaps the two
values at the top of the stack. To allow for complex programs, you can overwrite the offsets to your desired values.
The comp
instruction is used to compare the top two values on the stack. The compared values are deleted. If they are
equal (JavaScript comparison ===
) then 1
is pushed onto the stack, otherwise 0
is pushed. The process/calculation
is the following:
- Pop A from the top of the stack
- Pop B from the top of the stack
- Push
A === B ? 1 : 0
to the top of the stack
The read
instruction is used to read the contents of an element. If the element is an input, it will read its value.
Otherwise, it will fall back to reading innerHTML
. Which element are the contents read from? In the code
section, it
is the element the event listener was bound to using the hear
instruction. In the render
section, the element is the
current container you're writing the instruction in. If the target element cannot be determined, an error is thrown at
runtime. See the console for the currentTarget
variable to see the current target element (when in debug mode).
The madd
instruction is used to add two numbers together. The process/calculation is the following:
- Pop A from the top of the stack
- Pop B from the top of the stack
- Push
A + B
to the top of the stack
The msub
instruction is used to subtract two numbers from each other. The process/calculation is the following:
- Pop A from the top of the stack
- Pop B from the top of the stack
- Push
A - B
to the top of the stack
The mdiv
instruction is used to multiply two numbers. The process/calculation is the following:
- Pop A from the top of the stack
- Pop B from the top of the stack
- Push
A * B
to the top of the stack
The mdiv
instruction is used to integer divide two numbers. The process/calculation is the following:
- Pop A from the top of the stack
- Pop B from the top of the stack
- Push
A // B
to the top of the stack (//
is integer division, can be represented as JavaScriptMath.floor(A / B)
)
The ifis
instruction is used to start an if block. The top value is popped from the stack and if, and only if, the
value is exactly equal to 1
as an integer, the contents of the if statement are executed. You can compare two values
with the comp
instruction before using the comparison result in an if statement.
The endi
instruction is used to end an if block.
There are three types/ways of referencing a value:
<key>t
- reference to a key of a text in thestrings
section, for example69t
is a ref to a text in the strings with its key being 69<key>c
- reference to a key of a constant in therom
section, for example69c
is a ref to a constant in the rom with its key being 69<off>s
- reference to an offset from the end of the stack, for example1s
is a ref to the second item on the stack counted from the top of the stack
The keys are derived from ids, using a simple mathematical formula. It depends on the offset from the start of the
section (line number offset), so you can create the same offsets for multiple localizations. The source code for the
function can be found in the esoml.lexer
file, around line 65 of the file, it's a method of a class EsoMLLexer
defined as the following:
def convertIDToKey(id_str: str, line: int) -> int:
id_: int = int(id_str, 8)
line %= len(id_str)
line %= 3
if line == 0:
return ((id_ * 3) << 2) + 0x42
elif line == 1:
return ((id_ ^ 1337) << 3) - 5
elif line == 2:
return ((id_ // 7) % 0x69) + 666
else:
raise NotImplementedError("Huh? Math is failing?")
To describe the algorithm, we'll use simpler notation:
ID = parseInt(ID_STR, base=8)
OP_ID = LINE_NUM % LEN_ID % 3
RESULT = ((ID * 3) << 2) + 0x42 for OP_ID == 0
((ID ^ 1337) << 3) - 5 for OP_ID == 1
((ID // 7) % 0x69) + 666 for OP_ID == 2
where:
LINE_NUM
is the line number offset starting with0
from the section header (lineNumber - section.startsAtLine
)ID_STR
is the string version of the id provided in the source code, encoded in base 8 for convenienceID_LEN
is the length of the string version ofID_STR
in characters
Absolutely do not look at the compiler output in the Python console because it would help you to figure out which id at which line corresponds to which key.
You can find example code together with some comments in the examples/
folder.