diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..696ad874 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +PATH_add bin \ No newline at end of file diff --git a/.gitignore b/.gitignore index e3efbf6f..cb17bb0d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,14 +11,13 @@ vendor/pcre *.dylib *.dll *.a -bin/ scratch/ tools/build_helpers tools/build_helpers.exe logs/ *.exe *.out - +*.log app/.import .DS_Store @@ -38,7 +37,6 @@ user_config.nims nimble.develop nimble.paths /t.nim -fonts/ .submodules.tmp *.previous diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d6409d16..f2a03acc 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,7 +5,7 @@ "tasks": [ { "label": "Build enu", - "command": "nimble", + "command": "nim", "args": [ "build", "--debugger:native" diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 00000000..681311eb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index e0b31125..c07c5ec4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,18 @@ Enu is a 3D sandbox environment for creating and exploring voxel worlds using a - `atlas install && atlas rep` - Install dependencies (first-time setup) - `nim prereqs` - Build Godot, download fonts, generate API bindings and stdlib +### Dependency Management (Atlas) + +- `atlas.lock` pins exact dependency versions +- `atlas pin ` - Update lock file for a specific package after changing its version in deps/ +- `atlas install` - Install dependencies from lock file +- `atlas rep` - Regenerate nim.cfg paths from lock file +- When changing dependencies in enu.nimble, run `atlas install && atlas pin` to update the lock file + +### Generated Artifacts + +**Never manually edit files that are meant to be produced by tools** (lock files, sentinel files, zip archives, etc.), even if you know how to make the required changes. We need to verify our tools work correctly and eliminate potential failure vectors. If a tool isn't producing the expected output, investigate why. Confirm with the user before making any exceptions. + ### Core Development Commands - `nim build` - Build the main application (required after code changes) - `nim start` - Run Enu in development mode @@ -84,16 +96,26 @@ Pass `amd64` or `arm64` to `nim prereqs` to set the target architecture. See `do - Controllers manage game logic and coordinate between models and UI - UI components handle presentation and user interaction -### Important Notes +### Casing Conventions -- **Always use `snake_case` for identifiers.** Nim's identifier normalization means `snake_case` and `camelCase` are equivalent, but this project strictly uses `snake_case`. Only use `lowerCamelCase` when `snake_case` doesn't work, which should only happen with third-party macros that don't properly normalize identifiers. +This project follows Ruby-like casing conventions. Nim is style-insensitive for identifiers (ignoring case and underscores), so these are style guidelines rather than compiler requirements: + +- **Types**: `UpperCamelCase` (e.g., `VoxelRenderer`, `GameState`, `Build`) +- **Enum values**: `SCREAMING_SNAKE_CASE` (e.g., `ERASER`, `BLUE`, `CODE_MODE`, `PLAYING`) +- **Constants**: `SCREAMING_SNAKE_CASE` (e.g., `CHUNK_DIM`, `ACTION_COLORS`, `IR_BLACK`) +- **Everything else**: `snake_case` (procs, variables, fields, parameters, etc.) +- **Third-party identifiers starting with lowercase**: use `snake_case` (e.g., `writeVu64` → `write_vu64`) + +Only use `lowerCamelCase` when `snake_case` doesn't work, which should only happen with third-party macros that don't properly normalize identifiers. + +### Important Notes - Use `nim build` to verify changes compile correctly - The project uses ZenContext for metrics and threading - Scripts are Logo-inspired but use Nim syntax - World data is stored as JSON with accompanying Nim scripts - If you're not on `main` or a `0.x` branch, try to keep a clean history. Prefer rebasing and ammending commits to keep work in logical chunks and --force-with-lease push them to origin. Confirm with the user before pulling. -- Never include "Generated by Claude", "Co-Authored-By: Claude", or similar attribution in commits. Keep commit messages concise. Try not to include details that are obvious by quickly looking at the diff. +- **Never include "Generated by Claude", "Co-Authored-By: Claude"**, or similar attribution in commits. Keep commit messages concise. Try not to include details that are obvious by quickly looking at the diff. - If rebasing or squashing commits, confirm with the user before merging 10 or more commits. - Avoid nil checks, unless there is a known, non-bug reason why something could be nil. Asserting something isn't nil is fine. - Things that are fairly obvious shouldn't have comments. \ No newline at end of file diff --git a/atlas.lock b/atlas.lock index 42a11b92..5016b685 100644 --- a/atlas.lock +++ b/atlas.lock @@ -12,10 +12,10 @@ "commit": "3aeb5a61f306a0ad4f5a7292cfdc69af2fa35e4d", "version": "" }, - "model_citizen.getenu.github.com": { - "dir": "$deps/model_citizen.getenu.github.com", - "url": "https://github.com/getenu/model_citizen", - "commit": "cbc2c9d6a645b993ec76650b7735a9e927b223e0", + "ed.getenu.github.com": { + "dir": "$deps/ed.getenu.github.com", + "url": "https://github.com/getenu/ed", + "commit": "c12f6d673c46d4d765e1cfb3ccfdf86a8306d946", "version": "" }, "nanoid.nim.getenu.github.com": { @@ -75,7 +75,7 @@ "zippy": { "dir": "$deps/zippy", "url": "https://github.com/guzba/zippy", - "commit": "a0057dcc689bae60adfc286b8e49dc10db4c98d3", + "commit": "9ab82da84edfd6483d273dfcf8a921309945861e", "version": "" }, "unittest2": { @@ -84,6 +84,24 @@ "commit": "1efa65bb037185f0983ac4fa65932e8d3450825d", "version": "" }, + "nph": { + "dir": "$deps/nph", + "url": "https://github.com/arnetheduck/nph", + "commit": "1c6d7813f9f6d6a4fbcd36dfd1489b18982d0d55", + "version": "" + }, + "regex": { + "dir": "$deps/regex", + "url": "https://github.com/nitely/nim-regex", + "commit": "4593305ed1e49731fc75af1dc572dd2559aad19c", + "version": "" + }, + "nim-libbacktrace.dsrw.github.com": { + "dir": "$deps/nim-libbacktrace.dsrw.github.com", + "url": "https://github.com/dsrw/nim-libbacktrace", + "commit": "d8bd4ce5c46bb6d2f984f6b3f3d7380897d95ecb", + "version": "" + }, "threading": { "dir": "$deps/threading", "url": "https://github.com/nim-lang/threading", @@ -105,7 +123,7 @@ "supersnappy": { "dir": "$deps/supersnappy", "url": "https://github.com/guzba/supersnappy", - "commit": "e4df8cb5468dd96fc5a4764028e20c8a3942f16a", + "commit": "4fed6553d539cbbfb17ab5fea16a58b4f1916e7d", "version": "" }, "faststreams": { @@ -150,6 +168,30 @@ "commit": "0646c444fce7c7ed08ef6f2c9a7abfd172ffe655", "version": "" }, + "hldiff": { + "dir": "$deps/hldiff", + "url": "https://github.com/c-blake/hldiff", + "commit": "cc1fc22a18c57e16d89c580cf78478c38c394c66", + "version": "" + }, + "glob": { + "dir": "$deps/glob", + "url": "https://github.com/haltcase/glob", + "commit": "dfb567fe1803d08b2e6272c56f30885c8a63f4d1", + "version": "" + }, + "toml_serialization": { + "dir": "$deps/toml_serialization", + "url": "https://github.com/status-im/nim-toml-serialization", + "commit": "b5b387e6fb2a7cc75d54a269b07cc6218361bd46", + "version": "" + }, + "unicodedb": { + "dir": "$deps/unicodedb", + "url": "https://github.com/nitely/nim-unicodedb", + "commit": "66f2458710dc641dd4640368f9483c8a0ec70561", + "version": "" + }, "stew": { "dir": "$deps/stew", "url": "https://github.com/status-im/nim-stew", @@ -191,6 +233,12 @@ "url": "https://github.com/status-im/nim-http-utils", "commit": "c53852d9e24205b6363bba517fa8ee7bde823691", "version": "" + }, + "adix": { + "dir": "$deps/adix", + "url": "https://github.com/c-blake/adix", + "commit": "38a95ee6bd675b31746fa79d318b546495374ffa", + "version": "" } }, "nimcfg": [ @@ -198,7 +246,7 @@ "--noNimblePath", "--path:\"deps/Nim.getenu.github.com\"", "--path:\"deps/godot-nim.getenu.github.com/godot\"", - "--path:\"deps/model_citizen.getenu.github.com/src\"", + "--path:\"deps/ed.getenu.github.com/src\"", "--path:\"deps/nanoid.nim.getenu.github.com/src\"", "--path:\"deps/pretty/src\"", "--path:\"deps/cligen\"", @@ -210,6 +258,9 @@ "--path:\"deps/metrics\"", "--path:\"deps/zippy/src\"", "--path:\"deps/unittest2\"", + "--path:\"deps/nph/src\"", + "--path:\"deps/regex/src\"", + "--path:\"deps/nim-libbacktrace.dsrw.github.com\"", "--path:\"deps/threading\"", "--path:\"deps/flatty/src\"", "--path:\"deps/netty/src\"", @@ -223,11 +274,16 @@ "--path:\"deps/chronos\"", "--path:\"deps/results\"", "--path:\"deps/stew\"", + "--path:\"deps/hldiff\"", + "--path:\"deps/glob/src\"", + "--path:\"deps/toml_serialization\"", + "--path:\"deps/unicodedb/src\"", "--path:\"deps/fusion/src\"", "--path:\"deps/mustache/src\"", "--path:\"deps/parsetoml/src\"", "--path:\"deps/bearssl\"", "--path:\"deps/httputils\"", + "--path:\"deps/adix\"", "############# end Atlas config section ##########", "" ], @@ -244,16 +300,17 @@ "", "requires \"https://github.com/getenu/Nim#77d820e1\",", " \"https://github.com/getenu/godot-nim 0.8.6\",", - " \"https://github.com/getenu/model_citizen 0.19.10\",", + " \"https://github.com/getenu/ed 0.20.0\",", " \"https://github.com/getenu/nanoid.nim >= 0.2.1\",", " \"https://github.com/treeform/pretty >= 0.2.0\", \"cligen\", \"chroma\", \"markdown\",", - " \"chronicles\", \"dotenv\", \"nimibook\", \"metrics#51f1227\", \"zippy\", \"unittest2\"", + " \"chronicles\", \"dotenv\", \"nimibook\", \"metrics#51f1227\", \"zippy\", \"unittest2\",", + " \"nph\", \"regex\", \"https://github.com/dsrw/nim-libbacktrace\"", "" ] }, "hostOS": "macosx", "hostCPU": "arm64", - "nimVersion": "2.2.6 ab00c56904e3126ad826bb520d243513a139436a", + "nimVersion": "2.2.6", "gccVersion": "", "clangVersion": "" } \ No newline at end of file diff --git a/bin/config.nims b/bin/config.nims new file mode 100644 index 00000000..499e3d51 --- /dev/null +++ b/bin/config.nims @@ -0,0 +1 @@ +patch_file "nph", "phrenderer", "patches/phrenderer" diff --git a/bin/docs.nim b/bin/docs.nim new file mode 100644 index 00000000..b9847e36 --- /dev/null +++ b/bin/docs.nim @@ -0,0 +1,76 @@ +import std/[asynchttpserver, asyncdispatch, os, strutils, mimetypes] + +const + project_path = current_source_path().parent_dir().parent_dir() + vendor_path = project_path / "vendor" + voxel_docs_path = vendor_path / "modules/voxel/doc/site" + godot_docs_path = vendor_path / "godot/doc/_build/html" + enu_docs_path = project_path.parent_dir() / "enu-site" + port = 9999 + +proc handle_request(req: Request) {.async, gcsafe.} = + var path = req.url.path + + # Determine which docs to serve based on path prefix + var docs_path: string + var relative_path: string + + if path.starts_with("/voxel"): + docs_path = voxel_docs_path + relative_path = path[6 ..^ 1] # Strip "/voxel" + elif path.starts_with("/godot"): + docs_path = godot_docs_path + relative_path = path[6 ..^ 1] # Strip "/godot" + elif path.starts_with("/enu"): + docs_path = enu_docs_path + relative_path = path[4 ..^ 1] # Strip "/enu" + elif path == "/" or path == "": + # Serve index page with links to all + let index_html = + """ +Docs + +

Local Documentation

+ +""" + await req.respond( + HTTP200, index_html, new_http_headers([("Content-Type", "text/html")]) + ) + return + else: + await req.respond(HTTP404, "Not found. Try / for index.") + return + + # Handle empty path or directory paths + if relative_path == "" or relative_path == "/": + relative_path = "/index.html" + elif not relative_path.contains('.'): + relative_path = relative_path / "index.html" + + let file_path = docs_path / relative_path + + if file_exists(file_path): + let content = read_file(file_path) + let ext = file_path.split_file().ext + let mimes = new_mimetypes() + let mime = + mimes.get_mimetype(ext.strip(chars = {'.'}), default = "text/html") + await req.respond( + Http200, content, new_http_headers([("Content-Type", mime)]) + ) + else: + await req.respond(Http404, "Not found: " & path) + +proc main() = + echo "Serving docs at http://localhost:", port + echo " /enu/ -> Enu docs" + echo " /voxel/ -> godot-voxel docs" + echo " /godot/ -> Godot 3.5 class reference" + let server = new_async_http_server() + wait_for server.serve(port.Port, handle_request) + +main() diff --git a/bin/nph.nim b/bin/nph.nim new file mode 100644 index 00000000..8be792a5 --- /dev/null +++ b/bin/nph.nim @@ -0,0 +1 @@ +include pkg/nph diff --git a/bin/patches/phrenderer.nim b/bin/patches/phrenderer.nim new file mode 100644 index 00000000..05d9ae42 --- /dev/null +++ b/bin/patches/phrenderer.nim @@ -0,0 +1,22 @@ +# stupider like a fox! + +import std/[strutils, macros] +import pkg/regex + +macro patch_line_length() = + # strip "." from imports, since they're not siblings to this patch file but + # are on the nim path + const import_regex = re2(r"import ""\.""\/\[(.*)\]") + + let + path = "../../deps/nph/src/phrenderer.nim" + src = static_read path + og_line_length = "44 else: 88" + new_line_length = "40 else: 80" + patched = src.replace(import_regex, "import $1").replace( + og_line_length, new_line_length + ) + + parse_stmt(patched, path) + +patch_line_length() diff --git a/docs/api_docs.nim b/docs/api_docs.nim new file mode 100644 index 00000000..ae2782bc --- /dev/null +++ b/docs/api_docs.nim @@ -0,0 +1,384 @@ +## API Documentation Generator Module +## Extracts and formats API documentation from jsondoc output for Mustache templates. + +import std/[json, tables, strutils, sequtils, algorithm, sets, re, sugar] + +type + SymbolKind* = enum + skConst, skEnum, skType, skProc, skIterator, skTemplate, skMacro + + Symbol* = object + name*: string + display_name*: string # For static methods: "TypeName.name" + kind*: SymbolKind + code*: string + description*: string + line*: int + module*: string + is_static*: bool # True for type-bound procs like Ed.init + + TypeDoc* = object + name*: string + type_symbol*: Symbol # The type definition itself + has_type*: bool # Whether we have a type definition + procs*: seq[Symbol] # Operations on this type + static_procs*: seq[Symbol] # Static methods (init, bootstrap, etc.) + + DocData* = object + constants*: seq[Symbol] + enums*: seq[Symbol] + types*: Table[string, TypeDoc] # Type name -> TypeDoc + + ModuleConfig* = tuple[name: string, json: string] + +proc parse_kind*(s: string): SymbolKind = + case s + of "skConst": skConst + of "skType": skType + of "skProc": skProc + of "skIterator": skIterator + of "skTemplate": skTemplate + of "skMacro": skMacro + else: skProc + +proc is_enum*(entry: JsonNode): bool = + if entry["type"].get_str != "skType": + return false + let code = entry["code"].get_str + code.contains(" = enum") + +proc is_static_method*(entry: JsonNode): bool = + ## Check if this is a static method (first arg is `type` or `typedesc`) + if "signature" notin entry: + return false + let sig = entry["signature"] + if "arguments" notin sig or sig["arguments"].len == 0: + return true # No arguments = static + let first_arg = sig["arguments"][0] + let type_name = first_arg["type"].get_str + type_name.starts_with("type") or type_name.starts_with("typedesc") + +proc strip_generic_params*(type_name: string): string = + ## Strip generic parameters from a type name: "EdSeq[string]" -> "EdSeq" + result = type_name.replace(re"\[.*\]", "") + +proc extract_operating_type*(entry: JsonNode): string = + ## Extract the base type name from the first argument of a proc + if "signature" notin entry: + return "" + let sig = entry["signature"] + if "arguments" notin sig or sig["arguments"].len == 0: + return "" + let first_arg = sig["arguments"][0] + var type_name = first_arg["type"].get_str + + # Strip prefixes: "var ", "type ", "typedesc" + type_name = type_name.replace(re"^(var|type|typedesc)\s*", "") + + # Strip generic parameters + type_name = type_name.strip_generic_params() + + # Skip single-letter generic type params like T + if type_name.len <= 2 and type_name.match(re"^[A-Z]$"): + return "" + + result = type_name + +proc parse_symbol*(entry: JsonNode, module: string): Symbol = + result.name = entry["name"].get_str + result.display_name = result.name + result.kind = parse_kind(entry["type"].get_str) + result.code = entry["code"].get_str + result.description = entry.get_or_default("description").get_str + result.line = entry.get_or_default("line").get_int + result.module = module + result.is_static = is_static_method(entry) + +proc collect_symbols*(modules: seq[ModuleConfig]): DocData = + ## Collect symbols from pre-loaded JSON module data + result.types = init_table[string, TypeDoc]() + var seen_names = init_hash_set[string]() + var exported_types = init_hash_set[string]() + + # First pass: collect all exported types + for (module_name, json_content) in modules: + if json_content.len == 0: + continue + let doc = parse_json(json_content) + if "entries" notin doc: + continue + for entry in doc["entries"]: + if entry["type"].get_str == "skType": + let name = entry["name"].get_str + if not name.starts_with("_") and not name.contains("gensym"): + exported_types.incl(name) + + # Second pass: collect all symbols + for (module_name, json_content) in modules: + if json_content.len == 0: + continue + + let doc = parse_json(json_content) + if "entries" notin doc: + continue + + for entry in doc["entries"]: + let name = entry["name"].get_str + let entry_type = entry["type"].get_str + + if name.starts_with("_") or name.contains("gensym"): + continue + + # Include code signature to distinguish overloads + let code = entry["code"].get_str + let unique_key = name & ":" & entry_type & ":" & module_name & ":" & code + if unique_key in seen_names: + continue + seen_names.incl(unique_key) + + var symbol = parse_symbol(entry, module_name) + + case entry_type + of "skConst": + result.constants.add(symbol) + of "skType": + if is_enum(entry): + result.enums.add(symbol) + else: + # Add as a type + if name notin result.types: + result.types[name] = TypeDoc(name: name) + result.types[name].type_symbol = symbol + result.types[name].has_type = true + of "skProc", "skIterator", "skTemplate", "skMacro": + let op_type = extract_operating_type(entry) + if op_type.len > 0 and op_type in exported_types: + if op_type notin result.types: + result.types[op_type] = TypeDoc(name: op_type) + + if symbol.is_static: + symbol.display_name = op_type & "." & name + result.types[op_type].static_procs.add(symbol) + else: + result.types[op_type].procs.add(symbol) + else: + discard + +proc escape_html*(s: string): string = + s.multi_replace([ + ("&", "&"), + ("<", "<"), + (">", ">"), + ("\"", """), + ]) + +proc decode_html_entities*(s: string): string = + ## Decode HTML entities from nimdoc JSON + s.multi_replace([ + ("&", "&"), + ("<", "<"), + (">", ">"), + (""", "\""), + ]) + +proc to_display_name*(s: string): string = + ## Remove backticks and decode HTML entities for display + s.replace("`", "").decode_html_entities + +proc strip_pragmas*(code: string): string = + ## Remove pragma annotations like {.gcsafe.} from code + result = code.replace(re"\s*\{\.[^}]*\.?\}", "") + +proc highlight_code*(code: string): string = + ## Strip pragmas and escape HTML - highlight.js will handle syntax highlighting + escape_html(strip_pragmas(code)) + +proc join_code_blocks*(codes: seq[string]): string = + ## Join code blocks, adding a blank line after multi-line blocks (except the last) + if codes.len == 0: + return "" + if codes.len == 1: + return highlight_code(codes[0]) + + var parts: seq[string] = @[] + for i, code in codes: + let highlighted = highlight_code(code) + if i < codes.len - 1 and code.contains("\n"): + # Multi-line block, not the last - add blank line after + parts.add(highlighted & "\n") + else: + parts.add(highlighted) + + result = parts.join("\n") + +proc generate_anchor*(name: string, suffix: string = ""): string = + var base = name + if suffix.len > 0: + base = name & "_" & suffix + base.to_lower_ascii.multi_replace([ + ("[", ""), ("]", ""), ("=", "eq"), (",", "_"), (" ", "_"), + ("(", ""), (")", ""), ("*", ""), ("+", "plus"), ("-", ""), + ("&", "amp"), ("?", "q"), ("$", "dollar"), ("`", ""), (".", "_") + ]) + +type + OverloadGroup* = object + description*: string + codes*: seq[string] + modules*: seq[string] + +proc group_overloads_by_comment*(overloads: seq[Symbol]): seq[OverloadGroup] = + ## Group overloads by doc comments. + ## Functions with a comment start a new group. + ## Functions without comments join the current group. + if overloads.len == 0: + return @[] + + var groups: seq[OverloadGroup] = @[] + var current_group = OverloadGroup(description: "", codes: @[], modules: @[]) + + for ovl in overloads: + if ovl.description.len > 0: + # This overload has a comment - start a new group + if current_group.codes.len > 0: + # Save the previous group first + groups.add(current_group) + current_group = OverloadGroup( + description: ovl.description, + codes: @[ovl.code], + modules: @[ovl.module] + ) + else: + # No comment - add to current group + current_group.codes.add(ovl.code) + current_group.modules.add(ovl.module) + + # Don't forget the last group + if current_group.codes.len > 0: + groups.add(current_group) + + result = groups + +# Mustache context generation - returns JsonNode for direct use with Mustache + +proc to_symbol_json*(sym: Symbol, suffix: string = ""): JsonNode = + result = %*{ + "name": sym.name.to_display_name, + "anchor": generate_anchor(sym.name, suffix), + "description": sym.description, + "code": highlight_code(sym.code), + "module": sym.module + } + +proc to_api_json*(data: DocData): JsonNode = + ## Convert DocData to JSON for Mustache template + result = %*{ + "hasConstants": data.constants.len > 0, + "constants": new_j_array(), + "hasEnums": data.enums.len > 0, + "enums": new_j_array(), + "types": new_j_array() + } + + # Constants + for sym in data.constants: + result["constants"].add(sym.to_symbol_json("const")) + + # Enums + for sym in data.enums: + result["enums"].add(sym.to_symbol_json("enum")) + + # Types - sorted by name + var type_names = to_seq(data.types.keys) + type_names.sort() + + for type_name in type_names: + let td = data.types[type_name] + var tc = %*{ + "name": type_name, + "anchor": generate_anchor(type_name, "type"), + "hasType": td.has_type, + "hasOps": td.procs.len > 0 or td.static_procs.len > 0, + "description": "", + "code": "", + "module": "", + "hasStaticProcs": td.static_procs.len > 0, + "staticProcs": new_j_array(), + "hasProcs": td.procs.len > 0, + "procs": new_j_array(), + "staticProcNames": new_j_array(), + "procNames": new_j_array() + } + + if td.has_type: + tc["description"] = %td.type_symbol.description + tc["code"] = %highlight_code(td.type_symbol.code) + tc["module"] = %td.type_symbol.module + + # Static procs - group by display name, then by doc comments + if td.static_procs.len > 0: + var static_by_name = init_table[string, seq[Symbol]]() + for p in td.static_procs: + if p.display_name notin static_by_name: + static_by_name[p.display_name] = @[] + static_by_name[p.display_name].add(p) + + var static_names = to_seq(static_by_name.keys) + static_names.sort() + + for disp_name in static_names: + let overloads = static_by_name[disp_name] + let proc_anchor = generate_anchor(disp_name.decode_html_entities) + let groups = group_overloads_by_comment(overloads) + + var pc = %*{ + "name": disp_name.to_display_name, + "anchor": proc_anchor, + "groups": new_j_array() + } + + for group in groups: + let combined_code = join_code_blocks(group.codes) + pc["groups"].add(%*{ + "description": group.description, + "hasDescription": group.description.len > 0, + "code": combined_code + }) + + tc["staticProcs"].add(pc) + tc["staticProcNames"].add(%*{"name": disp_name.to_display_name, "anchor": proc_anchor}) + + # Regular procs - group by name, then by doc comments + if td.procs.len > 0: + var procs_by_name = init_table[string, seq[Symbol]]() + for p in td.procs: + if p.name notin procs_by_name: + procs_by_name[p.name] = @[] + procs_by_name[p.name].add(p) + + var proc_names = to_seq(procs_by_name.keys) + proc_names.sort() + + for proc_name in proc_names: + let overloads = procs_by_name[proc_name] + let proc_anchor = generate_anchor(proc_name.decode_html_entities, type_name) + let groups = group_overloads_by_comment(overloads) + + var pc = %*{ + "name": proc_name.to_display_name, + "anchor": proc_anchor, + "groups": new_j_array() + } + + for group in groups: + let combined_code = join_code_blocks(group.codes) + pc["groups"].add(%*{ + "description": group.description, + "hasDescription": group.description.len > 0, + "code": combined_code + }) + + tc["procs"].add(pc) + tc["procNames"].add(%*{"name": proc_name.to_display_name, "anchor": proc_anchor}) + + result["types"].add(tc) diff --git a/docs/book/api/ed_api.nim b/docs/book/api/ed_api.nim new file mode 100644 index 00000000..252cd46e --- /dev/null +++ b/docs/book/api/ed_api.nim @@ -0,0 +1,35 @@ +## Ed API Reference +## Generates API documentation page for Ed reactive data framework. + +import nimib, nimibook +import ../../enuib +import ../../api_docs + +# Load JSON documentation files at compile time +const + types_json = static_read("../../../../model_citizen/docs/json/ed/types.json") + initializers_json = static_read("../../../../model_citizen/docs/json/ed/zens/initializers.json") + operations_json = static_read("../../../../model_citizen/docs/json/ed/zens/operations.json") + contexts_json = static_read("../../../../model_citizen/docs/json/ed/zens/contexts.json") + validations_json = static_read("../../../../model_citizen/docs/json/ed/zens/validations.json") + +# Configure modules with their JSON content +const modules: seq[ModuleConfig] = @[ + ("ed/types", types_json), + ("ed/zens/initializers", initializers_json), + ("ed/zens/operations", operations_json), + ("ed/zens/contexts", contexts_json), + ("ed/zens/validations", validations_json), +] + +# Initialize nimib with API docs theme +nb_init(theme = use_api_docs) + +# Collect symbols and convert to JSON for Mustache +let data = collect_symbols(modules) +let api_json = data.to_api_json() + +# Set API context for template +nb.context["api"] = api_json + +nb_save diff --git a/docs/book/api/ed_readme.nim b/docs/book/api/ed_readme.nim new file mode 100644 index 00000000..d15532c6 --- /dev/null +++ b/docs/book/api/ed_readme.nim @@ -0,0 +1,34 @@ +## Ed README +## Ed README page with Ed-specific sidebar. + +import nimib, nimibook +import ../../enuib +import ../../api_docs + +# Load README at compile time +const readme_md = static_read("../../../../model_citizen/README.md") + +# Load JSON documentation files for sidebar +const + types_json = static_read("../../../../model_citizen/docs/json/ed/types.json") + initializers_json = static_read("../../../../model_citizen/docs/json/ed/zens/initializers.json") + operations_json = static_read("../../../../model_citizen/docs/json/ed/zens/operations.json") + contexts_json = static_read("../../../../model_citizen/docs/json/ed/zens/contexts.json") + validations_json = static_read("../../../../model_citizen/docs/json/ed/zens/validations.json") + +const modules: seq[ModuleConfig] = @[ + ("ed/types", types_json), + ("ed/zens/initializers", initializers_json), + ("ed/zens/operations", operations_json), + ("ed/zens/contexts", contexts_json), + ("ed/zens/validations", validations_json), +] + +nb_init(theme = use_ed_readme) + +# Set API context for sidebar +let data = collect_symbols(modules) +nb.context["api"] = data.to_api_json() + +nb_text(readme_md) +nb_save diff --git a/docs/book/assets/css/api.css b/docs/book/assets/css/api.css new file mode 100644 index 00000000..3bfaf42f --- /dev/null +++ b/docs/book/assets/css/api.css @@ -0,0 +1,264 @@ +/* Ed API Documentation - IR_BLACK Theme */ +/* Integrated with nimibook theme system */ + +/* Fix sidebar layout: chapter items with nested sections should stack vertically */ +.sidebar .chapter>li.chapter-item { + display: block; +} + +/* Nested section inside API Reference */ +.sidebar .api-nav-content { + padding-left: 20px; +} + +/* API Nav Section Headers */ +.api-nav-header { + font-size: 0.8em; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--sidebar-non-existant); + padding: 5px 0; + margin: 0; +} + +/* Type items with details/summary for collapsing operations */ +.api-type-details { + margin: 0; +} + +.api-type-details>summary { + cursor: pointer; + list-style: none; + display: block; +} + +.api-type-details>summary::-webkit-details-marker { + display: none; +} + +.api-type-details>summary>a { + color: #96CBFE; + text-decoration: none; +} + +.api-type-details>summary>a:hover { + color: var(--sidebar-active); +} + +/* Chevron after the type name */ +.api-type-details>summary>a::after { + content: " \25B6"; + font-size: 0.6em; + display: inline-block; + transition: transform 0.15s; + margin-left: 6px; + vertical-align: middle; +} + +.api-type-details[open]>summary>a::after { + transform: rotate(90deg); +} + +/* Types without operations - light blue */ +.chapter-item>a[href*="_type"] { + color: #96CBFE; +} + +/* Main Content Styling */ +.readme-section { + margin-bottom: 3rem; + padding-bottom: 2rem; + border-bottom: 1px solid var(--quote-border); +} + +.page-header { + margin-bottom: 2rem; +} + +.page-header h1 { + margin-top: 0; +} + +.warning { + background: #1a1a0a; + border: 1px solid #444400; + border-radius: 6px; + padding: 1rem; + margin: 1rem 0; + font-size: 0.9em; +} + +.intro { + margin-bottom: 2rem; +} + +/* Code blocks - IR_BLACK theme */ +.code-block { + background: #000000 !important; + border: 1px solid var(--quote-border); + border-radius: 6px; + padding: 1rem; + overflow-x: auto; + margin: 0.5em 0; +} + +.code-block code { + color: #F6F3E8 !important; + font-family: "IBM Plex Mono", "Source Code Pro", "Fira Code", "Monaco", monospace !important; + font-size: 0.85em; + background-color: transparent !important; +} + +/* Syntax highlighting - IR_BLACK colors */ +.code-block .kw { + color: #96CBFE; +} + +.code-block .str { + color: #A8FF60; +} + +.code-block .num { + color: #FF73FD; +} + +.code-block .cmt { + color: #7C7C7C; +} + +.code-block .op { + color: #FFFFB6; +} + +/* Inline code in descriptions */ +.description code { + color: #FF73FD; + font-family: "Source Code Pro", "Fira Code", "Monaco", monospace; + font-size: 0.9em; +} + +/* Symbols (constants, enums) */ +.symbol { + margin: 2rem 0; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--quote-border); +} + +.symbol:last-child { + border-bottom: none; +} + +.symbol h3, +.symbol h4 { + margin-top: 0; +} + +.symbol h3 a, +.symbol h4 a { + color: var(--links); +} + +.symbol h3 a:hover, +.symbol h4 a:hover { + text-decoration: none; +} + +.description { + margin: 0.5em 0; + color: var(--fg); +} + +.source { + font-size: 0.8em; + color: #7C7C7C; + margin-top: 0.5em; +} + +.source code { + color: #7C7C7C; +} + +.overload { + margin: 1em 0; + padding-left: 1em; + border-left: 2px solid var(--quote-border); +} + +.overload:first-child { + margin-top: 0; +} + +/* Type sections */ +.type-section { + margin: 2.5rem 0; + padding-bottom: 2rem; + border-bottom: 1px solid var(--quote-border); +} + +.type-section:last-child { + border-bottom: none; +} + +.type-heading { + font-size: 1.4em; + margin-top: 0; + margin-bottom: 1rem; +} + +.type-heading a { + color: var(--links); +} + +.operation { + margin: 1.5rem 0; + padding-left: 1rem; + border-left: 3px solid var(--quote-border); +} + +.operation h4, +.operation h5 { + margin-top: 0; + margin-bottom: 0.5rem; + font-size: 1.1em; +} + +.operation h4 a, +.operation h5 a { + color: var(--links); +} + +/* Section headers */ +section>h2, +section>h3 { + border-bottom: 1px solid var(--quote-border); + padding-bottom: 0.3em; +} + +/* Footer */ +main>footer { + margin-top: 3rem; + padding-top: 1.5rem; + border-top: 1px solid var(--quote-border); + text-align: center; + color: #7C7C7C; + font-size: 0.9em; +} + +/* Light theme adjustments */ +.light .warning { + background: #fffff0; + border-color: #e6e6cc; +} + +.light .code-block { + background: #1a1a1a; + border-color: #333; +} + +.light .api-type-details>summary>a { + color: #0066cc; +} + +.light .chapter-item>a[href*="_type"] { + color: #0066cc; +} \ No newline at end of file diff --git a/docs/book/assets/css/enu.css b/docs/book/assets/css/enu.css index c7174eb8..17b2b07a 100644 --- a/docs/book/assets/css/enu.css +++ b/docs/book/assets/css/enu.css @@ -1,28 +1,194 @@ -.dark code:not(.hljs){ - color: #FF73FD +@font-face { + font-family: 'Jost'; + src: url('../fonts/jost/Jost-400-Book.otf') format('opentype'); + font-weight: 400; + font-style: normal; } -.dark h1, h2, h3, h4, h5 { +@font-face { + font-family: 'Jost'; + src: url('../fonts/jost/Jost-400-BookItalic.otf') format('opentype'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'Jost'; + src: url('../fonts/jost/Jost-700-Bold.otf') format('opentype'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Jost'; + src: url('../fonts/jost/Jost-700-BoldItalic.otf') format('opentype'); + font-weight: 700; + font-style: italic; +} + +@font-face { + font-family: 'IBM Plex Mono'; + src: url('../fonts/ibm/IBMPlexMono-Regular.otf') format('opentype'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'IBM Plex Mono'; + src: url('../fonts/ibm/IBMPlexMono-Italic.otf') format('opentype'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'IBM Plex Mono'; + src: url('../fonts/ibm/IBMPlexMono-Bold.otf') format('opentype'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'IBM Plex Mono'; + src: url('../fonts/ibm/IBMPlexMono-BoldItalic.otf') format('opentype'); + font-weight: 700; + font-style: italic; +} + +:root { + --font-sans: 'Jost', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + --font-mono: 'IBM Plex Mono', "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; +} + +code, +pre, +.hljs, +.code-block code, +.api-nav-item>a { + font-family: var(--font-mono) !important; +} + +.dark code:not(.hljs) { + color: #96CBFE; +} + +/* H1 stays blue, H2-H5 become hot pink */ +.dark h1 { color: #96CBFE; } -.dark h2, h3 { +.dark h2, +h3, +h4, +h5 { + color: #FF73FD; +} + +.dark h2, +h3 { margin-top: 1em; } +/* Force dark background for all code blocks (API and non-API) */ +pre, +.code-block { + background-color: #000000 !important; + border: 1px solid var(--quote-border); + border-radius: 6px; + padding: 1rem; +} + +/* Remove !important from color to allow syntax highlighting to work */ +/* Inline code (not in pre) should be blue */ +code:not(pre code) { + color: #96CBFE !important; + background-color: transparent !important; +} + +/* Code block container text should be white (default for unhighlighted text) */ +pre code, +.code-block code, +.hljs { + color: #F6F3E8 !important; + background-color: transparent !important; +} + +/* Syntax highlighting - Match API Theme (IR_BLACK) for general docs */ +/* Usage of !important here ensures these colors override the default white above */ + +/* Keywords - Blue */ +.hljs-keyword, +.hljs-built_in, +.hljs-variable.language_ { + color: #96CBFE !important; +} + +.hljs-type { + color: #C6C5FE !important; +} + +/* Constants and Enums - Blue (applied via JS heuristic) */ +.hljs-constant { + color: #96CBFE !important; +} + +/* Strings - Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #A8FF60 !important; +} + +/* Numbers/Literals - Pink */ +.hljs-number, +.hljs-literal, +.hljs-link { + color: #FF73FD !important; +} + +/* Comments - Gray */ +.hljs-comment, +.hljs-quote, +.hljs-meta { + color: #7C7C7C !important; +} + +/* Operators/Punctuation - Yellow/White */ +.hljs-operator, +.hljs-punctuation { + color: #FFFFB6 !important; +} + +/* Functions/Methods - Blue or White */ +.hljs-title.function_, +.hljs-title.class_ { + color: #96CBFE !important; +} + +/* Params - White */ +.hljs-params { + color: #F6F3E8 !important; +} + +/* Ensure pre also has the correct font */ +pre { + font-family: var(--font-mono) !important; +} + body { - font-family: -apple-system, BlinkMacSystemFont, sans-serif; + font-family: var(--font-sans); font-size: 16pt; } .dark { + color-scheme: dark; --bg: #0b0517; --fg: #F6F3E8; --sidebar-bg: #000000; --sidebar-fg: #F6F3E8; --sidebar-non-existant: #505274; - --sidebar-active: #C6C5FE; + --sidebar-active: #FF73FD; --sidebar-spacer: #2d334f; --scrollbar: #F6F3E8; @@ -32,7 +198,8 @@ body { --links: #C6C5FE; - --inline-code-color: #c5c8c6;; + --inline-code-color: #c5c8c6; + ; --theme-popup-bg: #161923; --theme-popup-border: #737480; @@ -58,7 +225,225 @@ body { details.note { font-style: italic; } -details.note > summary { + +details.note>summary { font-weight: bold; cursor: pointer; } + +/* ========================================= */ +/* API Documentation Styles (Merged from api.css) */ +/* ========================================= */ + +/* Fix sidebar layout: chapter items with nested sections should stack vertically */ +.sidebar .chapter>li.chapter-item { + display: block; +} + +/* Nested section inside API Reference */ +.sidebar .api-nav-content { + padding-left: 20px; +} + +/* API Nav Section Headers */ +.api-nav-header { + font-size: 0.8em; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--sidebar-non-existant); + padding: 5px 0; + margin: 0; +} + +/* Type items with details/summary for collapsing operations */ +.api-type-details { + margin: 0; +} + +.api-type-details>summary { + cursor: pointer; + list-style: none; + display: block; +} + +.api-type-details>summary::-webkit-details-marker { + display: none; +} + +.api-type-details>summary>a { + color: #C6C5FE; + text-decoration: none; + font-family: var(--font-mono); +} + +.api-type-details>summary>a:hover { + color: var(--sidebar-active); +} + +/* Chevron after the type name */ +.api-type-details>summary>a::after { + content: " \25B6"; + font-size: 0.6em; + display: inline-block; + transition: transform 0.15s; + margin-left: 6px; + vertical-align: middle; +} + +.api-type-details[open]>summary>a::after { + transform: rotate(90deg); +} + +/* Types without operations - lavender */ +.chapter-item>a[href*="_type"] { + color: #C6C5FE; + font-family: var(--font-mono); +} + +/* Functions/Templates in sidebar - White */ +.chapter-item>a:not([href*="_type"]) { + color: #F6F3E8; + font-family: var(--font-mono); +} + +/* Main Content Styling */ +.readme-section { + margin-bottom: 3rem; + padding-bottom: 2rem; + border-bottom: 1px solid var(--quote-border); +} + +.page-header { + margin-bottom: 2rem; +} + +.page-header h1 { + margin-top: 0; +} + +.warning { + background: #1a1a0a; + border: 1px solid #444400; + border-radius: 6px; + padding: 1rem; + margin: 1rem 0; + font-size: 0.9em; +} + +.intro { + margin-bottom: 2rem; +} + +/* Symbols (constants, enums) */ +.symbol { + margin: 2rem 0; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--quote-border); +} + +.symbol:last-child { + border-bottom: none; +} + +.symbol h3, +.symbol h4 { + margin-top: 0; +} + +.symbol h3 a, +.symbol h4 a { + color: var(--links); + font-family: var(--font-mono); +} + +.symbol h3 a:hover, +.symbol h4 a:hover { + text-decoration: none; +} + +.description { + margin: 0.5em 0; + color: var(--fg); +} + +.source { + font-size: 0.8em; + color: #7C7C7C; + margin-top: 0.5em; +} + +.source code { + color: #7C7C7C; +} + +.overload { + margin: 1em 0; + padding-left: 1em; + border-left: 2px solid var(--quote-border); +} + +.overload:first-child { + margin-top: 0; +} + +/* Type sections */ +.type-section { + margin: 2.5rem 0; + padding-bottom: 2rem; + border-bottom: 1px solid var(--quote-border); +} + +.type-section:last-child { + border-bottom: none; +} + +.type-heading { + font-size: 1.4em; + margin-top: 0; + margin-bottom: 1rem; +} + +.type-heading a { + color: var(--links); + font-family: var(--font-mono); +} + +.operation { + margin: 1.5rem 0; + padding-left: 1rem; + border-left: 3px solid var(--quote-border); +} + +.operation h4, +.operation h5 { + margin-top: 0; + margin-bottom: 0.5rem; + font-size: 1.1em; +} + +.operation h4 a, +.operation h5 a { + color: #F6F3E8 !important; + font-family: var(--font-mono); +} + +/* Section headers */ +section>h2, +section>h3 { + border-bottom: 1px solid var(--quote-border); + padding-bottom: 0.3em; +} + +/* Light theme adjustments */ +.light .warning { + background: #fffff0; + border-color: #e6e6cc; +} + +.light .api-type-details>summary>a { + color: #0066cc; +} + +.light .chapter-item>a[href*="_type"] { + color: #0066cc; +} \ No newline at end of file diff --git a/docs/book/template.html.mustache b/docs/book/template.html.mustache index 71de1fec..3a66bd42 100644 --- a/docs/book/template.html.mustache +++ b/docs/book/template.html.mustache @@ -45,10 +45,6 @@ - - {{#additional_css}} @@ -236,9 +232,110 @@
+ {{#is_readme}} + +
+ {{#blocks}} + {{&.}} + {{/blocks}} +
+ {{/is_readme}} + + {{#is_api_docs}} + +
+

Ed API Reference

+ + {{#api.hasConstants}} +
+

Constants

+ {{#api.constants}} +
+

{{ name }}

+ {{#description}} +

{{{ description }}}

+ {{/description}} +
{{{ code }}}
+

Source: {{ module }}

+
+ {{/api.constants}} +
+ {{/api.hasConstants}} + + {{#api.hasEnums}} +
+

Enums

+ {{#api.enums}} +
+

{{ name }}

+ {{#description}} +

{{{ description }}}

+ {{/description}} +
{{{ code }}}
+

Source: {{ module }}

+
+ {{/api.enums}} +
+ {{/api.hasEnums}} + +
+

Types

+ {{#api.types}} +
+

{{ name }}

+ + {{#hasType}} + {{#description}} +

{{{ description }}}

+ {{/description}} +
{{{ code }}}
+

Source: {{ module }}

+ {{/hasType}} + + {{#hasStaticProcs}} + {{#staticProcs}} +
+
{{ name }}
+ {{#groups}} +
+ {{#hasDescription}} +

{{{ description }}}

+ {{/hasDescription}} +
{{{ code }}}
+
+ {{/groups}} +
+ {{/staticProcs}} + {{/hasStaticProcs}} + + {{#hasProcs}} + {{#procs}} +
+
{{ name }}
+ {{#groups}} +
+ {{#hasDescription}} +

{{{ description }}}

+ {{/hasDescription}} +
{{{ code }}}
+
+ {{/groups}} +
+ {{/procs}} + {{/hasProcs}} +
+ {{/api.types}} +
+
+ {{/is_api_docs}} + + {{^is_readme}} + {{^is_api_docs}} {{#blocks}} {{&.}} {{/blocks}} + {{/is_api_docs}} + {{/is_readme}}