1382 lines
33 KiB
Text
1382 lines
33 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.
|
|
|
|
_ :: #import "jc";
|
|
_ :: #import "jc/math";
|
|
_ :: #import "jc/fmt/base64";
|
|
|
|
_ :: #import "jc/x/json";
|
|
|
|
_ :: #import "jc/ext/hmm";
|
|
_ :: #import "jc/ext/luajit";
|
|
_ :: #import "jc/ext/raylib";
|
|
_ :: #import "jc/ext/remotery";
|
|
// _ :: #import "jc/ext/darwin";
|
|
// _ :: #import "jc/ext/objc";
|
|
// _ :: #import "jc/ext/win32";
|
|
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;
|
|
}
|
|
|
|
// there should really be a flag on the proc header...
|
|
first := to_lower(decl.name[0]);
|
|
if !(first >= #char "a" && first <= #char "z") {
|
|
decl.name = tprint("operator%", decl.name);
|
|
}
|
|
|
|
array_add(array, .{
|
|
decl = decl,
|
|
node = header,
|
|
source = CleanJaiSource(decl),
|
|
docs = DocComments(decl),
|
|
});
|
|
|
|
case;
|
|
if !(decl.flags & .IS_CONSTANT)
|
|
{ continue; }
|
|
|
|
if decl.name == "RunTests" || decl.name == "RUN_TESTS"
|
|
{ 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, ", "),
|
|
);
|
|
|
|
sorted_names := quick_sort(names, SortAlphabeticallyDescendingLength);
|
|
|
|
append(*b, #string END
|
|
<div class='index'>
|
|
END);
|
|
|
|
{
|
|
append(*b, #string END
|
|
<details open='true'>
|
|
<summary><h3>Modules</h3></summary>
|
|
<ul>
|
|
END);
|
|
|
|
|
|
for sorted_names if !contains(it, "jc/ext") {
|
|
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
|
|
<details>
|
|
<summary><h3>Bindings</h3></summary>
|
|
<ul>
|
|
END);
|
|
|
|
for sorted_names if contains(it, "jc/ext") {
|
|
print_to_builder(*b, "<li><a href='%1'>%2</a></li>", LinkableName(it), it);
|
|
}
|
|
|
|
append(*b, #string END
|
|
</ul>
|
|
</details>
|
|
END);
|
|
}
|
|
|
|
append(*b, "</div>");
|
|
|
|
|
|
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 "$(pwd)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;
|
|
}
|
|
|
|
|
|
lines: []string;
|
|
data, rok := read_entire_file(filename);
|
|
if rok {
|
|
lines = split(data, "\n");
|
|
table_add(*source_files, filename, lines);
|
|
}
|
|
else {
|
|
print("warn: file didn't exist '%'\n", filename);
|
|
}
|
|
|
|
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 % – Jc v%.% Documentation", import_name, JcMajor, JcMinor));
|
|
print_to_builder(*b, "<h1><a href='index.html'>[..]</a> Module – %</h1>", short_name);
|
|
print_to_builder(*b, "<p>%</p>", mod.main_docs);
|
|
|
|
if contains(import_name, "jc/x") {
|
|
append(*b, "<span class='module-warning'>");
|
|
print_to_builder(*b, "Warning: % is experimental and has no stability guarantees or versioning; use with caution.", short_name);
|
|
append(*b, "</span>");
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
if header.procedure_flags & .SYMMETRIC {
|
|
postfix = tprint("% #symmetric", 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, "::");
|
|
|
|
// @todo(judah): handle module parameters correctly
|
|
if index == -1 {
|
|
#import "Program_Print";
|
|
b: String_Builder;
|
|
|
|
is_module_param := true;
|
|
if decl.expression != null {
|
|
print_expression(*b, decl.expression);
|
|
}
|
|
else if decl.type_inst {
|
|
print_expression(*b, decl.type_inst);
|
|
is_module_param = false;
|
|
}
|
|
|
|
append(*b, ";");
|
|
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', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
|
line-height: 1.6;
|
|
color: var(--text-primary);
|
|
background-color: var(--bg-primary);
|
|
margin: 0;
|
|
padding: 0;
|
|
font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
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: 'Menlo', 'Monaco', 'SF Mono', '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: 'Menlo', 'Monaco', 'SF Mono', '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: 'Menlo', 'Monaco', 'SF Mono', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
|
font-size: 0.9rem;
|
|
line-height: 1.4;
|
|
font-feature-settings: 'liga' 1, 'calt' 1;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
h4 {
|
|
font-size: 1rem;
|
|
font-family: 'Menlo', 'Monaco', Consolas, 'Courier New', monospace;
|
|
}
|
|
|
|
.index a {
|
|
font-family: 'Menlo', 'Monaco', Consolas, 'Courier New', monospace;
|
|
}
|
|
|
|
pre.code {
|
|
font-family: 'Menlo', 'Monaco', Consolas, 'Courier New', monospace;
|
|
font-size: 0.85rem;
|
|
padding: 0.75rem;
|
|
-webkit-text-size-adjust: 100%;
|
|
text-rendering: optimizeLegibility;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
span.module-warning {
|
|
background-color: var(--bg-primary);
|
|
color: var(--accent-color);
|
|
display: block;
|
|
border-radius: 6px;
|
|
font-weight: bold;
|
|
border: 1px solid var(--accent-color);
|
|
border-radius: 6px;
|
|
padding: 0.5rem;
|
|
}
|
|
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;
|
|
|
|
#import "jc";
|
|
|
|
#import "String";
|
|
#import "Compiler";
|
|
|
|
using basic :: #import "Basic";
|
|
#poke_name basic operator==;
|
|
|
|
#import "File";
|
|
#import "Hash_Table";
|
|
#import "Sort";
|