Work in Progress
You are viewing unfinished draft material right now.
Setup
Macromania is an embedded language that requires a TypeScript host environment. We will explain how to set things up for the Deno TypeScript runtime. Before you proceed, ensure that running deno --version produces output similar to the following (the exact version numbers do not matter):
$ deno --version
deno 2.5.3 (stable, release, x86_64-pc-windows-msvc)
v8 14.0.365.5-rusty
typescript 5.9.2
Macromania uses the jsx dialect of typescript, which adds syntax similar to HTML tags. We will explain this syntax later in this document; for now we will configure Deno to correctly handle jsx in the first place.
To do so, ensure that in the deno.json configuration file
- the
importsmap includes thejsr:@macromania/macromaniapackage, and - the
compilerOptionsspecify bothjsxImportSource": "<your-import-of-macromania>"and"jsx": "react-jsxdev".
A complete, working example of a deno.json file:
{
"name": "getting-started-with-macromania",
"version": "1.0.0",
"imports": {
"macromania": "jsr:@macromania/macromania@^0.2.0",
},
"compilerOptions": {
"jsx": "react-jsxdev",
"jsxImportSource": "macromania",
"lib": ["deno.ns", "dom"],
"strict": true
},
}
We provide a full demo setup here.
To test whether things are set up correctly, create a file macromaniaYay.tsx with the following content:
import { evaluate } from "macromania";
console.log(await evaluate(<>Hi from macromania</>)); Executing this file (deno run macromaniaYay.tsx) should log Hi from macromania and should not yield any warnings or errors, despite <>something</> not being valid typescript — it is valid jsx syntax.
If running the file works well but your text editor complains about syntax errors, you will need to ensure that your text editor picks up the configuration from the deno.json file. If you are using macromania from within a workspace, you might need to configure the jsx setup on a workspace-level deno.json file, for example.
Basics and Syntax
The preceding sanity test already highlighted the basic workings of Macromania: import and call evaluate method to turn some input into a string (or null if evaluation failed for some reason).
The input to evaluate can be either a string or a jsx element. In the remainder of this document, we describe the syntax of these jsx elements. As a quick teaser, jsx lets you write and evaluate snippets such as the following:
<Definition id="tree" title="Tree">
A <Def>tree</Def> is a connected graph on <Tex>n</Tex>
vertices with <Tex>n - 1</Tex> edges<Cite id="Diestel2016" />.
</Definition>
The jsx “tags” serve as macro invocations. Let us define a basic macro, and then see how to invoke it.
import type { Children, Expression } from "macromania";
function MyFavouriteAnimal(): Expression {
return "cat";
}
console.log(await evaluate(<MyFavouriteAnimal />)); The preceding snippet defines a macro named MyFavouriteAnimal which takes no arguments and always expands to the string "cat". We can invoke it using a syntax similar to an HTML void element.
A macro is any function with a capitalised name which returns an Expression — i.e., a string or a jsx element. Passing any expression to evaluate turns it into a string. The capitalised name is important, jsx does not allow macro calls with lower-case identifiers.
Fragments
The next handy kind of expression is the jsx fragment, which lets you concatenate multiple expressions (including plain text) into a single one:
console.log(
await evaluate(
<>
A cuddly <MyFavouriteAnimal />.
</>,
),
);We can use fragments in the definition of macros, of course:
function MyFavouriteSong(): Expression {
return (
<>
<MyFavouriteAnimal /> food
</>
);
}Note that fragments remove leading and trailing whitespace, and collapse all inner whitespace down to a single space character.
Within fragments (and any jsx elements, really), you can use curly braces to evaluate arbitrary typescript:
console.log(
await evaluate(
<>A cuddly {7 > 3 ? <MyFavouriteAnimal /> : "dog"}.</>,
),
);
/* Logs "A cuddly cat." */ The typescript in the curly braces must always result in a valid Expression though. Trying to evaluate <>best number: {12 + 5}</> will log an error and cause evaluation to fail immediately.
Children
Macros become a lot more interesting once you can pass contents to them. Here is an example macro which wraps its contents in * characters:
function Emphasise(
{ children }: { children?: Children },
): Expression {
return (
<>
*<xs x={children} />*
</>
);
}
console.log(
await evaluate(
<>
A <Emphasise>cuddly</Emphasise> cat.
</>,
),
);
/* Logs "A *cuddly* cat." */ This example demonstrates several new concepts. When a macro takes exactly one argument, and that argument is an object with a children?: Children field, then it can be invoked with tag-like syntax. Everything in the tags is passed to the macro as the children field.
To use the children as part of the output of the macro, you must use the special <xs x={children} /> syntax. This inconvenient syntax is required to work around some limitations of jsx — if you simply used {children} in the return value, evaluation might fail under certain circumstances.
You can arbitrarily nest macro invocations:
console.log(
await evaluate(
<>
A{" "}
<Emphasise>
cuddly (<Emphasise>very</Emphasise>)
</Emphasise>{" "}
cat.
</>,
),
);
/* Logs "A *cuddly (*very*)* cat." */ Because the children field was marked as optional (with the ? syntax), you can even invoke the macro without any children:
console.log(
await evaluate(<Emphasise></Emphasise>),
/* equivalent: await evaluate(<Emphasise />) */
);
/* Logs "**" */Props
In addition to children, macros can take arbitrary further arguments, called props (short for properties).
/**
* Greet the contents with a configuarable number of `!`s.
*/
function Greet(
{ hype, children }: { hype: number; children?: Children },
): Expression {
return (
<>
Hello, <xs x={children} />
{"!".repeat(hype)}
</>
);
}
console.log(
await evaluate(
<Greet hype={1 + 2}>world</Greet>,
),
);
/* Logs "Hello, world!!!" */In macro invocations, props are supplied with syntax analogous to HTML attributes, except the value is supplied in curly braces. These curly braces can contain arbitrary typescript.
Macro invocations are statically typed; the following two examples do not even compile:
<Greet hype={"huh"}>world</Greet>
<Greet>world</Greet>
Props can be omitted from invocations if they are defined as optional fields in the function declaration:
{ hype, children }: { hype?: number; children?: Children }
This declaration (note the ? in the definition of the hype field) would admit <Greet>world</Greet> as an invocation.
Jsx offers convenient syntactic sugar for props of types string and boolean. When invoking a macro, string props can be passed as literals without surrounding curly braces:
// plain syntax:
<Link href={"example.org"}/>
// nicer syntax:
<Link href="example.org"/>
Optional boolean props can be passed as simple flags:
function Greeting({ fancy }: { fancy?: boolean }): Expression {
if (fancy) {
return "Salutations";
} else {
return "Hi";
}
}
console.log(await evaluate(<Greeting fancy />));
/* logs "Salutations" */
console.log(await evaluate(<Greeting />));
/* logs "Hi" */Intrinsics
Macromania has some intrinsic (i.e., built-in) macros. These start with lowercase names. We have already briefly encountered the <xs x={children}/> intrinsic for emitting children. More precisely, this intrinsic takes a value of type Children as its x prop, and converts it into a single expression.
Another useful intrinsic is the <omnomnom> intrinsic, which evaluates its children but yields the empty string as the result. It can be used to run macros solely for their side-effects.
console.log(
await evaluate(
<>
I like cats<omnomnom>and dogs</omnomnom>.
</>,
),
);
/* logs "I like cats." */ Other useful intrinsics include the sequence macro for linearising the evaluation of an array of expressions (don’t worry about it for now), and a collection of macros for logging and for configuring logging levels. You probably do not need to care about these yet, just remember they exist for when you might end up needing them.
Wrapping Up
This covers the basics of Macromania and its jsx syntax.
As a next step, we recommend either following the tutorial for using Macromania as a simple static site generator, or learning how to define stateful macros.