// *** 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, "

Jc v%.% Documentation

", 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

License: Public Domain
Repository: %2
Contributors: %3

END, "https://git.brut.systems/judah/jc/src/branch/master/LICENSE", "https://git.brut.systems/judah/jc", join(..contributors, ", "), ); append(*b, #string END

Modules

END); append(*b, #string END

What:
A set of modules for things I usually want in Jai, namely, utilities, bindings, and experiments.

Why:
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.

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.

While I do use many of the modules shipped with the compiler, my goal is to eventually replace them.

How:

# 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]";
   

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, "

%2

", to_lower_copy(name), name); if decls.count != 0 { return true; } append(b, "

This section is empty.

"); 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 % – Jc v%.% Documentation", import_name, JcMajor, JcMinor)); print_to_builder(*b, "

[..] Module – %

", short_name); print_to_builder(*b, "

%

", mod.main_docs); print_to_builder(*b, "
#import \"%\";
", import_name); PrintIndexFor :: (b: *String_Builder, name: string, decls: []Decl) { append(b, "
"); print_to_builder(b, "

% (%)

", name, decls.count); append(b, ""); append(b, "
"); } append(*b, "
"); append(*b, "

Index

"); 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, "
"); print_to_builder(*b, "

Imports (%)

", sorted_imports.count); append(*b, ""); append(*b, "
"); } append(*b, "
"); append(*b, "
"); if Section(*b, "Constants", sorted_consts) { for sorted_consts { append(*b, "
"); ConstantToHtml(*b, it); append(*b, "
"); } } append(*b, "
"); append(*b, "
"); if Section(*b, "Procedures", sorted_procs) { for sorted_procs { append(*b, "
"); ProcedureOrMacroToHtml(*b, it); append(*b, "
"); } } append(*b, "
"); append(*b, "
"); if Section(*b, "Macros", sorted_macros) { for sorted_macros { append(*b, "
"); ProcedureOrMacroToHtml(*b, it); append(*b, "
"); } } append(*b, "
"); append(*b, "
"); if Section(*b, "Types", sorted_consts) { for sorted_types { append(*b, "
"); TypeToHtml(*b, it); append(*b, "
"); } } append(*b, "
"); 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

%1

END, decl.name, NodeUrl(decl)); print_to_builder(b, "
% :: %
", decl.name, decl.source); } WriteDocsAndCode :: (b: *String_Builder, docs: string, example: string) { if docs.count != 0 { append(b, "

"); append(b, docs); append(b, "

"); } if example.count != 0 { append(b, "
"); append(b, "

Example

"); append(b, "
");
      append(b, example);
      append(b, "
"); append(b, "
"); } } 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("
%", line); } if starts_with(line, "See:") { doc_lines[li] = tprint("
%", line); } } // horrible docs := join(..doc_lines, "\n"); for decl.node.arguments { strs := [2]string.[ .[ tprint(" % ", it.name), tprint(" % ", it.name) ], .[ tprint("%.", it.name), tprint("%.", it.name) ], .[ tprint(",%", it.name), tprint(",%", it.name) ], .[ tprint("%,", it.name), tprint("%,", 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 %1
END, title, ResetCss, MainCss); } EndPage :: (b: *String_Builder) { append(b, #string END
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";