jc/_generate_docs.jai
2025-09-03 20:27:41 -06:00

1288 lines
31 KiB
Text

// *** Here be dragons! ***
// This code is mostly a proof of concept that ended up working pretty well.
// I'm sure there's a ton of edgecases we'll run into as the base library grows,
// but it works for now.
UseLocalLinks :: false; // set to false when deploying the docs
#scope_file #run {
set_build_options_dc(.{ do_output = false });
ws := compiler_create_workspace();
opts := get_build_options(ws);
opts.output_type = .NO_OUTPUT;
set_build_options(opts, ws);
compiler_begin_intercept(ws);
add_build_string(#string END
// Add freestanding modules here.
// Other modules will be automatically generated if they're imported by someone else.
jc :: #import "jc";
hmm :: #import "jc/ext/hmm";
luajit :: #import "jc/ext/luajit";
raylib :: #import "jc/ext/raylib";
remotery :: #import "jc/ext/remotery";
// darwin :: #import "jc/ext/darwin";
// objc :: #import "jc/ext/objc";
END, ws);
CheckImport :: (import: *Code_Directive_Import) -> bool {
return (BelongsToJc(import) || contains(import.name, "jc")) && !(import.flags & .UNSHARED);
}
BuildImport :: (import: *Code_Directive_Import) -> Import {
return .{
node = import,
name = import.name,
external = !contains(import.name, "jc"),
};
}
while true {
msg := compiler_wait_for_message();
if msg.workspace != ws continue;
if msg.kind == .COMPLETE || msg.kind == .ERROR break;
if msg.kind == {
case .TYPECHECKED;
tc := msg.(*Message_Typechecked);
for tc.others if BelongsToJc(it.expression) {
if it.expression.kind == {
case .DIRECTIVE_IMPORT;
import := it.expression.(*Code_Directive_Import);
if CheckImport(import) {
name := ModuleName(import);
if name.count == 0 continue;
mod := GetModule(name);
array_add_if_unique(*mod.imports, BuildImport(import));
}
case .DIRECTIVE_MODULE_PARAMETERS;
}
}
for tc.declarations if BelongsToJc(it.expression) {
decl := it.expression;
if !(decl.flags & .IS_GLOBAL) continue;
if decl.flags & .SCOPE_FILE continue;
ignored := false;
for decl.notes if it.text == "jc.nodocs" {
ignored = true;
break;
}
// Ignore this decl if explicit instructed or
// if the expression isn't something we should document.
if ignored || !(ValidNode(decl.expression) && BelongsToJc(decl.expression))
{ continue; }
name := ModuleName(decl);
if name.count == 0 continue;
mod := GetModule(name);
expr := decl.expression;
if expr.kind == {
case .STRUCT; #through;
case .ENUM;
array_add(*mod.types, .{
decl = decl,
node = expr,
source = CleanJaiSource(decl),
docs = DocComments(decl),
});
case .PROCEDURE_HEADER;
array := *mod.procs;
header := expr.(*Code_Procedure_Header);
if header.procedure_flags & .MACRO {
array = *mod.macros;
}
array_add(array, .{
decl = decl,
node = header,
source = CleanJaiSource(decl),
docs = DocComments(decl),
});
case;
if !(decl.flags & .IS_CONSTANT)
{ continue; }
array := *mod.consts;
// Things we don't want to be constant decls
if expr.kind == {
case .TYPE_INSTANTIATION;
array = *mod.types;
case .IDENT;
ident := decl.expression.(*Code_Ident);
expr := ident.resolved_declaration.expression;
if expr.kind == .TYPE_DEFINITION {
array = *mod.types;
}
case .DIRECTIVE_IMPORT;
import := expr.(*Code_Directive_Import);
if CheckImport(import) {
array_add_if_unique(*mod.imports, BuildImport(import));
}
continue;
}
array_add(array, .{
decl = decl,
node = expr,
source = CleanJaiSource(decl),
docs = DocComments(decl),
});
}
}
}
}
compiler_end_intercept(ws);
print("generating documentation...\n");
// Render each module page
for * mod, mod_name: modules {
print("processing module '%'...\n", mod_name);
ok := write_entire_file(tprint("docs/%", LinkableName(mod_name)), ModuleToHtml(mod));
assert(ok, "failed to write documentation for module: %", mod_name);
}
// Render index.html
b: String_Builder;
StartPage(*b, tprint("Jc v%.% Documentation", JcMajor, JcMinor));
print_to_builder(*b, "<h1>Jc v%.% Documentation</h1>", JcMajor, JcMinor);
names: [..]string;
for _, mod_name: modules {
array_add(*names, mod_name);
}
contributors :: string.[
"Judah Caruso",
"Jesse Coyle",
];
print_to_builder(*b, #string END
<p>
License: <a class='link' href='%1' target='_blank'>Public Domain</a>
<br/>
Repository: <a class='link' href='%2' target='_blank'>%2</a>
<br/>
Contributors: %3
</p>
END,
"https://git.brut.systems/judah/jc/src/branch/master/LICENSE",
"https://git.brut.systems/judah/jc",
join(..contributors, ", "),
);
append(*b, #string END
<details id='index' class='index' open='true'>
<summary><h3>Modules</h3></summary>
<ul>
END);
for quick_sort(names, SortAlphabeticallyDescendingLength) {
print_to_builder(*b, "<li><a href='%1'>%2</a></li>", LinkableName(it), it);
}
append(*b, #string END
</ul>
</details>
END);
append(*b, #string END
<p>
<strong>What:</strong><br/>
A set of modules for things I usually want in Jai, namely, utilities, bindings, and experiments.
</p>
<p>
<strong>Why:</strong><br/>
Because Jai is still in closed beta (as of September, 2025),
updates to the compiler and included modules break
projects of mine; sometimes in very annoying ways.
<br/>
<br/>
Jc was made to 1) give myself an escape hatch/skin-suit to
cause fewer breaking changes when updating the compiler,
and 2) put all of my non-project code in a single place
that's easier to manage.
<br/>
<br/>
While I do use many of the modules shipped with the
compiler, my goal is to eventually replace them.
</p>
<p>
<strong>How:</strong><br/>
<pre>
# Direct installation
cd [jai install dir]/modules
git clone https://git.brut.systems/judah/jc.git
# Indirect installation
git clone https://git.brut.systems/judah/jc.git
ln -s "/path/to/jc" [jai install dir]/modules/jc # POSIX install
mklink /D "C:\path\to\jc" [jai install dir]\jc # Windows install
# Usage:
#import "jc";
#import "jc/[module]";
</pre>
</p>
END);
EndPage(*b);
ok := write_entire_file("docs/index.html", builder_to_string(*b));
assert(ok, "failed to write docs/index.html");
print("done!\n");
}
source_files: Table(string, []string);
GetSourceLines :: (filename: string) -> []string, bool {
// old_logger := context.logger;
// context.logger = (message: string, data: *void, info: Log_Info) {}; // do nothing logger
// defer context.logger = old_logger;
fok, src := table_find_new(*source_files, filename);
if fok {
return src, true;
}
data, rok := read_entire_file(filename);
lines: []string;
if rok {
lines = split(data, "\n");
table_add(*source_files, filename, lines);
}
return lines, rok;
}
modules: Table(string, Module);
GetModule :: (name: string) -> *Module {
mod := find_or_add(*modules, name);
mod.name = name;
if mod.main_docs.count != 0
{ return mod; }
lines, _ := GetSourceLines(tprint(".%/module.jai", string.{ data = name.data + 2, count = name.count - 2 }));
if lines.count == 0 {
mod.main_docs = " "; // non-empty so we don't try again
return mod;
}
// @todo(judah): this should handle preformatted text
cleaned_lines: [..]string;
blockbased := starts_with(lines[0], "/*");
linebased := starts_with(lines[0], "//") ;
for lines {
if blockbased {
if trim(it) == "*/"
{ break; }
}
else if linebased {
if !starts_with(it, "/")
{ break; }
}
else {
break;
}
line := trim_right(trim_left(it, "/*"));
if line.count > 0 {
array_add(*cleaned_lines, line);
}
}
mod.main_docs = join(..cleaned_lines, "\n");
return mod;
}
Decl :: struct(T: Type) {
using decl: *Code_Declaration;
node: *T;
source: string;
docs: string;
}
Import :: struct {
node: *Code_Directive_Import;
name: string;
external: bool;
}
operator== :: (a: Import, b: Import) -> bool {
return a.name == b.name;
}
Module :: struct {
name: string;
main_docs: string;
imports: [..]Import; // module import names
types: [..]Decl(Code_Node);
consts: [..]Decl(Code_Node);
procs: [..]Decl(Code_Procedure_Header);
macros: [..]Decl(Code_Procedure_Header);
}
LinkableName :: (mod_name: string) -> string {
return tprint("%.html", to_lower_copy(replace(mod_name, "/", "_")));
}
ModuleToHtml :: (mod: *Module) -> string {
Section :: (b: *String_Builder, name: string, decls: []Decl) -> bool {
print_to_builder(b, "<h2 id='%1'><a href='#%1'>%2</a></h2>", to_lower_copy(name), name);
if decls.count != 0 {
return true;
}
append(b, "<p>This section is empty.</p>");
return false;
}
sorted_consts := quick_sort(mod.consts, SortDeclsByName);
sorted_procs := quick_sort(mod.procs, SortDeclsByName);
sorted_macros := quick_sort(mod.macros, SortDeclsByName);
sorted_types := quick_sort(mod.types, SortDeclsByName);
sorted_imports := quick_sort(mod.imports, SortImportsByName);
b: String_Builder;
import_name := mod.name;
short_name := path_basename(import_name);
StartPage(*b, tprint("Module % &ndash; Jc v%.% Documentation", import_name, JcMajor, JcMinor));
print_to_builder(*b, "<h1><a href='index.html'>[..]</a> Module &ndash; %</h1>", short_name);
print_to_builder(*b, "<p>%</p>", mod.main_docs);
print_to_builder(*b, "<pre class='code'>#import \"%\";</pre>", import_name);
PrintIndexFor :: (b: *String_Builder, name: string, decls: []Decl) {
append(b, "<details>");
print_to_builder(b, "<summary><h3>% (%)</h3></summary>", name, decls.count);
append(b, "<ul>");
for decls {
print_to_builder(b, "<li><a href='#%1'>%1</a></li>", it.name);
}
append(b, "</ul>");
append(b, "</details>");
}
append(*b, "<div class='index'>");
append(*b, "<h2 id='index'><a href='#index'>Index</a></h2>");
if sorted_consts.count PrintIndexFor(*b, "Constants", sorted_consts);
if sorted_procs.count PrintIndexFor(*b, "Procedures", sorted_procs);
if sorted_macros.count PrintIndexFor(*b, "Macros", sorted_macros);
if sorted_types.count PrintIndexFor(*b, "Types", sorted_types);
if sorted_imports.count {
append(*b, "<details>");
print_to_builder(*b, "<summary><h3>Imports (%)</h3></summary>", sorted_imports.count);
append(*b, "<ul>");
for sorted_imports {
append(*b, "<li><a href='");
if it.external {
append(*b, NodeUrl(it.node));
}
else {
append(*b, LinkableName(it.name));
}
print_to_builder(*b, "'>%</a></li>", it.name);
}
append(*b, "</ul>");
append(*b, "</details>");
}
append(*b, "</div>");
append(*b, "<div class='all-constants'>");
if Section(*b, "Constants", sorted_consts) {
for sorted_consts {
append(*b, "<div class='constant'>");
ConstantToHtml(*b, it);
append(*b, "</div>");
}
}
append(*b, "</div>");
append(*b, "<div class='all-procedures'>");
if Section(*b, "Procedures", sorted_procs) {
for sorted_procs {
append(*b, "<div class='procedure'>");
ProcedureOrMacroToHtml(*b, it);
append(*b, "</div>");
}
}
append(*b, "</div>");
append(*b, "<div class='all-macros'>");
if Section(*b, "Macros", sorted_macros) {
for sorted_macros {
append(*b, "<div class='procedure'>");
ProcedureOrMacroToHtml(*b, it);
append(*b, "</div>");
}
}
append(*b, "</div>");
append(*b, "<div class='all-types'>");
if Section(*b, "Types", sorted_consts) {
for sorted_types {
append(*b, "<div class='type'>");
TypeToHtml(*b, it);
append(*b, "</div>");
}
}
append(*b, "</div>");
EndPage(*b);
return builder_to_string(*b);
}
#if UseLocalLinks {
BaseUrl :: "docs";
SrcPath :: "..";
}
else {
BaseUrl :: "https://judahcaruso.com/jc";
SrcPath :: "https://git.brut.systems/judah/jc/src";
}
NodeUrl :: (node: *Code_Node) -> string {
path := node.enclosing_load.fully_pathed_filename;
idx := find_index_from_right(path, "jc/");
assert(idx != -1);
idx += 3;
return tprint("%1/%2#L%3",
SrcPath,
string.{ data = path.data + idx, count = path.count - idx },
node.l0,
);
}
DeclTitle :: (b: *String_Builder, decl: Decl) {
print_to_builder(b, #string END
<h4 id='%1'>
<a href='%2' target='_blank'>%1</a>
<a href='#%1' class='short-link'>¶</a>
</h4>
END, decl.name, NodeUrl(decl));
print_to_builder(b, "<pre class='code'>% :: %</pre>", decl.name, decl.source);
}
WriteDocsAndCode :: (b: *String_Builder, docs: string, example: string) {
if docs.count != 0 {
append(b, "<p>");
append(b, docs);
append(b, "</p>");
}
if example.count != 0 {
append(b, "<details class='example'>");
append(b, "<summary><p>Example</p></summary>");
append(b, "<pre class='code'>");
append(b, example);
append(b, "</pre>");
append(b, "</details>");
}
}
ConstantToHtml :: (b: *String_Builder, decl: Decl) {
DeclTitle(b, decl);
doc_lines, example_lines := PrepareDocsAndCode(decl.docs);
docs := join(..doc_lines, "\n");
example := join(..example_lines, "\n");
WriteDocsAndCode(b, docs, example);
}
ProcedureOrMacroToHtml :: (b: *String_Builder, decl: Decl(Code_Procedure_Header)) {
DeclTitle(b, decl);
doc_lines, example_lines := PrepareDocsAndCode(decl.docs);
for line, li: doc_lines {
if starts_with(line, "Note:") {
doc_lines[li] = tprint("<br/>%", line);
}
if starts_with(line, "See:") {
doc_lines[li] = tprint("<br/>%", line);
}
}
// horrible
docs := join(..doc_lines, "\n");
for decl.node.arguments {
strs := [2]string.[
.[ tprint(" % ", it.name), tprint(" <span class='ident'>%</span> ", it.name) ],
.[ tprint("%.", it.name), tprint("<span class='ident'>%</span>.", it.name) ],
.[ tprint(",%", it.name), tprint(",<span class='ident'>%</span>", it.name) ],
.[ tprint("%,", it.name), tprint("<span class='ident'>%</span>,", it.name) ],
];
for strs {
docs = replace(docs, it[0], it[1]);
}
}
example := join(..example_lines, "\n");
WriteDocsAndCode(b, docs, example);
}
TypeToHtml :: (b: *String_Builder, decl: Decl) {
DeclTitle(b, decl);
doc_lines, example_lines := PrepareDocsAndCode(decl.docs);
docs := join(..doc_lines, "\n");
example := join(..example_lines, "\n");
WriteDocsAndCode(b, docs, example);
}
PrepareDocsAndCode :: (raw_docs: string) -> (doc_lines: []string, example_lines: []string) {
if raw_docs.count == 0 {
return .[], .[];
}
lines := split(raw_docs, "\n");
doc_lines: [..]string;
example_lines: [..]string;
example_start: int = -1;
leading := 2;
for lines {
line := string.{ data = it.data + 3, count = it.count - 3 };
if line.count <= 0 {
array_add(*doc_lines, "");
continue;
}
if line[0] == " " {
line.data += 1;
line.count -= 1;
}
l := 0;
while l < line.count if line[l] == " " {
l += 1;
} else {
break;
}
if l > leading {
leading = l;
example_start = it_index;
break;
}
array_add(*doc_lines, trim(line));
}
if example_start != -1 {
for example_start..lines.count - 1 {
line := trim_right(lines[it]);
line.data += 3 + leading;
line.count -= 3 + leading;
if line.count <= 0 {
array_add(*example_lines, "");
continue;
}
if line[0] == " " {
line.data += 1;
line.count -= 1;
}
array_add(*example_lines, line);
}
}
return doc_lines, example_lines;
}
ModuleName :: (node: *Code_Node) -> string {
assert(node.enclosing_load != null);
assert(node.enclosing_load.enclosing_import != null);
import := node.enclosing_load.enclosing_import;
return import.module_name;
}
SortDeclsByName :: (a: Decl($T), b: Decl(T)) -> int {
return SortAlphabeticallyDescendingLength(a.name, b.name);
}
SortImportsByName :: (a: Import, b: Import) -> int {
if a.external && !b.external
{ return 1; }
if b.external && !a.external
{ return -1; }
return SortAlphabeticallyDescendingLength(a.name, b.name);
}
SortAlphabeticallyDescendingLength :: (a: string, b: string) -> int {
for 0..a.count-1 {
if it >= b.count
{ return 1; }
delta := a[it].(int) - b[it].(int);
if delta
{ return delta; }
}
if b.count > a.count
{ return -1; }
return 0;
}
CleanJaiSource :: (node: *Code_Node) -> string {
assert(node.enclosing_load != null);
assert(node.kind == .DECLARATION);
decl := node.(*Code_Declaration);
lines, ok := GetSourceLines(node.enclosing_load.fully_pathed_filename);
assert(ok, "failed to get source: %", node.enclosing_load.fully_pathed_filename);
// for procedures, strip out anything that doesn't help documentation.
if decl.expression.kind == .PROCEDURE_HEADER {
line := trim(lines[node.l0-1]);
proc := decl.expression.(*Code_Procedure_Header);
has_ret := proc.returns.count != 0;
if has_ret {
arrow_idx := find_index_from_right(line, "->");
assert(arrow_idx != -1, "how?");
i := arrow_idx;
while i < line.count {
if line[i] == "#" || line[i] == "{" {
break;
}
i += 1;
}
line.count -= line.count - i;
}
else {
while line.count > 0 {
if line.data[line.count - 1] == ")" {
break;
}
line.count -= 1;
}
}
idx := find_index_from_left(line, "::");
assert(idx != -1);
head := string.{ data = line.data, count = idx };
tail := string.{ data = line.data + head.count, count = line.count - head.count };
while tail.count > 0 {
if tail[0] == "(" {
break;
}
tail.data += 1;
tail.count -= 1;
}
postfix: string;
header := decl.expression.(*Code_Procedure_Header);
if header.procedure_flags & .MACRO {
postfix = " #expand";
}
if header.procedure_flags & .COMPILE_TIME_ONLY {
postfix = tprint("% #compile_time", postfix);
}
return tprint("%1%2", trim(tail), postfix);
}
// for everything else, get the source range for the node and only strip out
// comments that would've been caught by the doc comment finder.
else {
GetRangeEnd :: (node: *Code_Node, prev_l1: int) -> int, *Code_Node {
if node.kind == {
case .STRUCT;
s := node.(*Code_Struct);
return s.l1, s.block;
case .ENUM;
e := node.(*Code_Enum);
return e.l1, e.block;
case .BLOCK;
return node.l1, null;
case;
// print("didn't handle: %\n", node.kind);
return node.l1, null;
}
return -1, null;
}
start := node.l0-1;
end := -1;
{
node := decl.expression;
while node != null {
e, n := GetRangeEnd(node, node.l1);
if n == null {
end = e;
break;
}
end = e;
node = n;
}
}
assert(end != -1);
cleaned_lines: [..]string;
for start..end - 1 {
line := lines[it];
inline_comment_idx := find_index_from_right(line, "///");
if inline_comment_idx != - 1 {
if !starts_with(trim(line), "///") {
line.count = inline_comment_idx;
}
}
array_add(*cleaned_lines, line);
}
source := join(..cleaned_lines, "\n");
index := find_index_from_left(source, "::");
// This is like 95% a module parameter
if index == -1 {
#import "Program_Print";
b: String_Builder;
if decl.expression != null {
print_expression(*b, decl.expression);
}
else if decl.type_inst {
print_expression(*b, decl.type_inst);
}
append(*b, "; // #module_parameter");
return builder_to_string(*b);
}
return trim(.{ data = source.data + (index + 2), count = source.count - (index + 2) });
}
}
DocComments :: (node: *Code_Node) -> string {
assert(node.enclosing_load != null);
all_lines, ok := GetSourceLines(node.enclosing_load.fully_pathed_filename);
assert(ok, "failed to get source: %", node.enclosing_load.fully_pathed_filename);
main_line := all_lines[node.l0-1];
inline_comment_idx := find_index_from_right(main_line, "///");
if inline_comment_idx != -1 {
return trim(.{
data = main_line.data + inline_comment_idx,
count = main_line.count - inline_comment_idx,
});
}
start := -1;
end := (node.l0-2).(int);
i := end;
while i >= 0 {
line := trim(all_lines[i]);
defer i -= 1;
if line.count == 0 || !starts_with(line, "///") {
start = i + 1;
break;
}
}
if start == -1 {
start = 0;
}
comment_lines: [..]string;
for start..end {
array_add(*comment_lines, trim(all_lines[it]));
}
return join(..comment_lines, "\n");
}
BelongsToJc :: (node: *Code_Node) -> bool {
if node.enclosing_load == null || node.enclosing_load.enclosing_import == null {
return false;
}
import := node.enclosing_load.enclosing_import;
return contains(import.module_name, "jc") && ValidNode(node);
}
ValidNode :: (node: *Code_Node) -> bool {
if node.kind == {
case .DECLARATION; #through;
case .DIRECTIVE_IMPORT; #through;
case .DIRECTIVE_RUN; #through;
case .PROCEDURE_HEADER; #through;
case .STRUCT; #through;
case .TYPE_DEFINITION; #through;
case .TYPE_INSTANTIATION; #through;
case .ENUM; #through;
case .LITERAL; #through;
case .IDENT; #through;
case .UNARY_OPERATOR; #through;
case .DIRECTIVE_MODULE_PARAMETERS; #through;
case .BINARY_OPERATOR;
return true;
case;
return false;
}
}
StartPage :: (b: *String_Builder, title: string) {
print_to_builder(b, #string END
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>%1</title>
<style>%2</style>
<style>%3</style>
</head>
<body>
<main>
END, title, ResetCss, MainCss);
}
EndPage :: (b: *String_Builder) {
append(b, #string END
<main>
<a href="#index" class="back-to-index" title="Back to top">↑</a>
<body>
<html>
END);
}
MainCss :: #string END
:root {
--bg-primary: #ffffff;
--text-primary: #333333;
--text-secondary: #666666;
--border-color: #e1e4e8;
--accent-color: #ec4899;
--code-bg: #f6f8fa;
--link-color: #ec4899;
--link-hover: #db2777;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #0d1117;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--border-color: #30363d;
--accent-color: #f472b6;
--code-bg: #161b22;
--link-color: #f472b6;
--link-hover: #f9a8d4;
}
}
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
line-height: 1.6;
color: var(--text-primary);
background-color: var(--bg-primary);
margin: 0;
padding: 0;
}
h1 {
font-size: 2.5rem;
font-weight: 600;
margin: 0 0 2rem 0;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.5rem;
}
h2 {
font-size: 1.8rem;
font-weight: 600;
margin: 2rem 0 1rem 0;
padding-top: 1rem;
}
h4 {
font-size: 1.1rem;
font-weight: 600;
margin: 1.5rem 0 1rem 0;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
}
a {
color: var(--link-color);
text-decoration: none;
}
a:hover {
color: var(--link-hover);
text-decoration: underline;
}
h4 a {
color: var(--text-primary);
}
h4 a:hover {
color: var(--accent-color);
}
.procedure h4 a,
.type h4 a,
.constant h4 a {
color: var(--accent-color);
}
.procedure h4 a:hover,
.type h4 a:hover,
.constant h4 a:hover {
opacity: 0.8;
}
.procedure h4 a[href^="#"],
.type h4 a[href^="#"],
.constant h4 a[href^="#"] {
color: var(--text-secondary);
font-size: 0.9em;
}
.procedure h4 a[href^="#"]:hover,
.type h4 a[href^="#"]:hover,
.constant h4 a[href^="#"]:hover {
color: var(--text-primary);
}
.index {
margin: 2rem 0;
}
.index h2 {
margin: 0 0 1rem 0;
padding-top: 0;
}
.index h2 a {
color: var(--text-primary);
}
.index h3 {
font-size: 1.1rem;
font-weight: 600;
margin: 1rem 0 0.5rem 0;
display: inline;
cursor: pointer;
}
.index ul {
margin: 0.25rem 0 0.5rem 1rem;
padding: 0;
list-style: none;
}
.index li {
margin: 0;
}
.index a {
color: var(--link-color);
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
}
.index a:hover {
color: var(--link-hover);
}
.all-constants,
.all-procedures,
.all-macros,
.all-types {
margin: 2rem 0;
}
.constant,
.procedure,
.macro,
.type {
margin: 1.5rem 0;
}
pre.code {
background-color: var(--code-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 1rem;
margin: 1rem 0;
overflow-x: auto;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.4;
}
span.ident {
color: var(--accent-color);
}
.example {
margin: 1rem 0;
}
.example pre.code {
margin: 0.5rem 0 0 0;
}
hr {
border: none;
height: 1px;
background-color: var(--border-color);
margin: 2rem 0;
}
details {
margin: 0.5rem 0;
}
summary {
cursor: pointer;
font-size: 1.1rem;
font-weight: 600;
margin: 1rem 0 0.5rem 0;
}
summary::-webkit-details-marker,
summary::marker {
display: none;
}
.example summary {
font-size: 1rem;
font-weight: 500;
margin: 0.5rem 0;
}
.example summary p {
margin: 0;
display: inline;
}
.index details {
margin: 0.25rem 0;
}
.index summary {
margin: 0.5rem 0 0.25rem 0;
}
main {
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
@media (max-width: 768px) {
main {
padding: 1rem;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.6rem;
margin: 1.5rem 0 0.75rem 0;
}
}
a:focus,
button:focus {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
}
html {
scroll-behavior: smooth;
}
.back-to-index {
position: fixed;
bottom: 2rem;
right: 2rem;
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 50%;
width: 3rem;
height: 3rem;
font-size: 1rem;
cursor: pointer;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
opacity: 0.5;
transition: all 0.3s ease;
}
.back-to-index:hover {
opacity: 1;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
color: var(--bg-primary);
text-decoration: none;
}
.back-to-index:focus {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
}
END;
ResetCss :: #string END
/* Box sizing rules */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Prevent font size inflation */
html {
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
/* Remove default margin in favour of better control in authored CSS */
body, h1, h2, h3, h4, p,
figure, blockquote, dl, dd {
margin-block-end: 0;
}
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
ul[role='list'],
ol[role='list'] {
list-style: none;
}
/* Set core body defaults */
body {
min-height: 100vh;
line-height: 1.5;
}
/* Set shorter line heights on headings and interactive elements */
h1, h2, h3, h4,
button, input, label {
line-height: 1.1;
}
/* Balance text wrapping on headings */
h1, h2,
h3, h4 {
text-wrap: balance;
}
/* A elements that don't have a class get default styles */
a:not([class]) {
text-decoration-skip-ink: auto;
color: currentColor;
}
/* Make images easier to work with */
img,
picture {
max-width: 100%;
display: block;
}
/* Inherit fonts for inputs and buttons */
input, button,
textarea, select {
font-family: inherit;
font-size: inherit;
}
/* Make sure textareas without a rows attribute are not tiny */
textarea:not([rows]) {
min-height: 10em;
}
/* Anything that has been anchored to should have extra scroll margin */
:target {
scroll-margin-block: 5ex;
}
END;
#load "./module.jai";
#import "String";
#import "Compiler";
using basic :: #import "Basic";
#poke_name basic operator==;
#import "File";
#import "Hash_Table";
#import "Sort";