diff --git a/tools/odin-html-docs/odin_html_docs_main.odin b/tools/odin-html-docs/odin_html_docs_main.odin new file mode 100644 index 000000000..7c822e4a1 --- /dev/null +++ b/tools/odin-html-docs/odin_html_docs_main.odin @@ -0,0 +1,609 @@ +package odin_html_docs + +import doc "core:odin/doc-format" +import "core:fmt" +import "core:io" +import "core:os" +import "core:strings" +import "core:path/slashpath" +import "core:sort" +import "core:slice" + +header: ^doc.Header +files: []doc.File +pkgs: []doc.Pkg +entities: []doc.Entity +types: []doc.Type + +pkgs_to_use: map[string]^doc.Pkg // trimmed path +pkg_to_path: map[^doc.Pkg]string // trimmed path + +array :: proc(a: $A/doc.Array($T)) -> []T { + return doc.from_array(header, a) +} +str :: proc(s: $A/doc.String) -> string { + return doc.from_string(header, s) +} + +errorf :: proc(format: string, args: ..any) -> ! { + fmt.eprintf("%s ", os.args[0]) + fmt.eprintf(format, ..args) + fmt.eprintln() + os.exit(1) +} + +common_prefix :: proc(strs: []string) -> string { + if len(strs) == 0 { + return "" + } + n := max(int) + for str in strs { + n = min(n, len(str)) + } + + prefix := strs[0][:n] + for str in strs[1:] { + for len(prefix) != 0 && str[:len(prefix)] != prefix { + prefix = prefix[:len(prefix)-1] + } + if len(prefix) == 0 { + break + } + } + return prefix +} + + +write_html_header :: proc(w: io.Writer, title: string) { + fmt.wprintf(w, ` + + + + + %s + + + + + + +`, title) + fmt.wprintln(w, "\n
") + fmt.wprintln(w, "\nCore Directory") + +} + +write_html_footer :: proc(w: io.Writer) { + fmt.wprintf(w, "
\n\n") +} + +main :: proc() { + if len(os.args) != 2 { + errorf("expected 1 .odin-doc file") + } + data, ok := os.read_entire_file(os.args[1]) + if !ok { + errorf("unable to read file:", os.args[1]) + } + err: doc.Reader_Error + header, err = doc.read_from_bytes(data) + switch err { + case .None: + case .Header_Too_Small: + errorf("file is too small for the file format") + case .Invalid_Magic: + errorf("invalid magic for the file format") + case .Data_Too_Small: + errorf("data is too small for the file format") + case .Invalid_Version: + errorf("invalid file format version") + } + files = array(header.files) + pkgs = array(header.pkgs) + entities = array(header.entities) + types = array(header.types) + + fullpaths: [dynamic]string + defer delete(fullpaths) + + for pkg in pkgs[1:] { + append(&fullpaths, str(pkg.fullpath)) + } + path_prefix := common_prefix(fullpaths[:]) + + pkgs_to_use = make(map[string]^doc.Pkg) + for fullpath, i in fullpaths { + path := strings.trim_prefix(fullpath, path_prefix) + if strings.has_prefix(path, "core/") { + pkgs_to_use[strings.trim_prefix(path, "core/")] = &pkgs[i+1] + } + } + sort.map_entries_by_key(&pkgs_to_use) + for path, pkg in pkgs_to_use { + pkg_to_path[pkg] = path + } + + b := strings.make_builder() + w := strings.to_writer(&b) + { + strings.reset_builder(&b) + write_html_header(w, "core library - pkg.odin-lang.org") + write_core_directory(w) + write_html_footer(w) + os.make_directory("core", 0) + os.write_entire_file("core/index.html", b.buf[:]) + } + + for path, pkg in pkgs_to_use { + strings.reset_builder(&b) + write_html_header(w, fmt.tprintf("package %s - pkg.odin-lang.org", path)) + write_pkg(w, path, pkg) + write_html_footer(w) + os.make_directory(fmt.tprintf("core/%s", path), 0) + os.write_entire_file(fmt.tprintf("core/%s/index.html", path), b.buf[:]) + } +} + + +write_core_directory :: proc(w: io.Writer) { + Node :: struct { + dir: string, + path: string, + name: string, + pkg: ^doc.Pkg, + next: ^Node, + first_child: ^Node, + } + add_child :: proc(parent: ^Node, child: ^Node) -> ^Node { + assert(parent != nil) + end := &parent.first_child + for end^ != nil { + end = &end^.next + } + child.next = end^ + end^ = child + return child + } + + root: Node + for path, pkg in pkgs_to_use { + dir, _, inner := strings.partition(path, "/") + + node: ^Node = nil + for node = root.first_child; node != nil; node = node.next { + if node.dir == dir { + break + } + } + if inner == "" { + if node == nil { + add_child(&root, new_clone(Node{ + dir = dir, + name = dir, + path = path, + pkg = pkg, + })) + } else { + node.dir = dir + node.name = dir + node.path = path + node.pkg = pkg + } + } else { + if node == nil { + node = add_child(&root, new_clone(Node{ + dir = dir, + name = dir, + })) + } + assert(node != nil) + child := add_child(node, new_clone(Node{ + dir = dir, + name = inner, + path = path, + pkg = pkg, + })) + } + } + + + fmt.wprintln(w, "

Directories

") + + fmt.wprintln(w, "\t") + fmt.wprintln(w, "\t\t") + + for dir := root.first_child; dir != nil; dir = dir.next { + if dir.first_child != nil { + fmt.wprint(w, `") + if dir.pkg != nil { + line_doc, _, _ := strings.partition(str(dir.pkg.docs), "\n") + line_doc = strings.trim_space(line_doc) + if line_doc != "" { + fmt.wprintf(w, ``, line_doc) + } + } + fmt.wprintf(w, "\n") + + for child := dir.first_child; child != nil; child = child.next { + assert(child.pkg != nil) + fmt.wprintf(w, `") + + line_doc, _, _ := strings.partition(str(child.pkg.docs), "\n") + line_doc = strings.trim_space(line_doc) + if line_doc != "" { + fmt.wprintf(w, ``, line_doc) + } + + fmt.wprintf(w, "\n") + } + } + + fmt.wprintln(w, "\t\t") + fmt.wprintln(w, "\t
`, dir.dir) + } else { + fmt.wprintf(w, `
`, dir.dir) + } + + if dir.pkg != nil { + fmt.wprintf(w, `%s`, dir.path, dir.name) + } else { + fmt.wprintf(w, "%s", dir.name) + } + fmt.wprintf(w, "%s
`, str(child.pkg.name)) + fmt.wprintf(w, `%s`, child.path, child.name) + fmt.wprintf(w, "%s
") +} + +is_entity_blank :: proc(e: doc.Entity_Index) -> bool { + name := str(entities[e].name) + return name == "" || name == "_" +} + +Write_Type_Flag :: enum { + Is_Results, + Variadic, +} +Write_Type_Flags :: distinct bit_set[Write_Type_Flag] + +write_type :: proc(w: io.Writer, pkg: doc.Pkg_Index, type: doc.Type, flags: Write_Type_Flags) { + type_entites := array(type.entities) + type_types := array(type.types) + switch type.kind { + case .Invalid: + // ignore + case .Basic: + type_flags := transmute(doc.Type_Flags_Basic)type.flags + if .Untyped in type_flags { + io.write_string(w, str(type.name)) + } else { + fmt.wprintf(w, `%s`, str(type.name)) + } + case .Named: + e := entities[type_entites[0]] + name := str(type.name) + fmt.wprintf(w, ``) + tn_pkg := files[e.pos.file].pkg + if tn_pkg != pkg { + fmt.wprintf(w, `%s.`, str(pkgs[pkg].name)) + } + fmt.wprintf(w, `{1:s}`, pkg_to_path[&pkgs[tn_pkg]], name) + case .Generic: + name := str(type.name) + io.write_byte(w, '$') + io.write_string(w, name) + if len(array(type.types)) == 1 { + io.write_byte(w, '/') + write_type(w, pkg, types[type_types[0]], flags) + } + case .Pointer: + io.write_byte(w, '^') + write_type(w, pkg, types[type_types[0]], flags) + case .Array: + assert(type.elem_count_len == 1) + io.write_byte(w, '[') + io.write_uint(w, uint(type.elem_counts[0])) + io.write_byte(w, ']') + write_type(w, pkg, types[type_types[0]], flags) + case .Enumerated_Array: + io.write_byte(w, '[') + write_type(w, pkg, types[type_types[0]], flags) + io.write_byte(w, ']') + write_type(w, pkg, types[type_types[1]], flags) + case .Slice: + if .Variadic in flags { + io.write_string(w, "..") + } else { + io.write_string(w, "[]") + } + write_type(w, pkg, types[type_types[0]], flags - {.Variadic}) + case .Dynamic_Array: + io.write_string(w, "[dynamic]") + write_type(w, pkg, types[type_types[0]], flags) + case .Map: + io.write_string(w, "map[") + write_type(w, pkg, types[type_types[0]], flags) + io.write_byte(w, ']') + write_type(w, pkg, types[type_types[1]], flags) + case .Struct: + type_flags := transmute(doc.Type_Flags_Struct)type.flags + io.write_string(w, "struct {}") + case .Union: + type_flags := transmute(doc.Type_Flags_Union)type.flags + io.write_string(w, "union {}") + case .Enum: + io.write_string(w, "enum {}") + case .Tuple: + entity_indices := type_entites + if len(entity_indices) == 0 { + return + } + require_parens := (.Is_Results in flags) && (len(entity_indices) > 1 || !is_entity_blank(entity_indices[0])) + if require_parens { io.write_byte(w, '(') } + for entity_index, i in entity_indices { + e := &entities[entity_index] + name := str(e.name) + + if i > 0 { + io.write_string(w, ", ") + } + if .Param_Using in e.flags { io.write_string(w, "using ") } + if .Param_Const in e.flags { io.write_string(w, "#const ") } + if .Param_Auto_Cast in e.flags { io.write_string(w, "#auto_cast ") } + if .Param_CVararg in e.flags { io.write_string(w, "#c_vararg ") } + if .Param_No_Alias in e.flags { io.write_string(w, "#no_alias ") } + if .Param_Any_Int in e.flags { io.write_string(w, "#any_int ") } + + if name != "" { + io.write_string(w, name) + io.write_string(w, ": ") + } + param_flags := flags - {.Is_Results} + if .Param_Ellipsis in e.flags { + param_flags += {.Variadic} + } + write_type(w, pkg, types[e.type], param_flags) + } + if require_parens { io.write_byte(w, ')') } + + case .Proc: + type_flags := transmute(doc.Type_Flags_Proc)type.flags + io.write_string(w, "proc") + cc := str(type.calling_convention) + if cc != "" { + io.write_byte(w, ' ') + io.write_quoted_string(w, cc) + io.write_byte(w, ' ') + } + params := array(type.types)[0] + results := array(type.types)[1] + io.write_byte(w, '(') + write_type(w, pkg, types[params], flags) + io.write_byte(w, ')') + if results != 0 { + assert(.Diverging not_in type_flags) + io.write_string(w, " -> ") + write_type(w, pkg, types[results], flags+{.Is_Results}) + } + if .Diverging in type_flags { + io.write_string(w, " -> !") + } + if .Optional_Ok in type_flags { + io.write_string(w, " #optional_ok") + } + + case .Bit_Set: + type_flags := transmute(doc.Type_Flags_Bit_Set)type.flags + case .Simd_Vector: + io.write_string(w, "#simd[") + io.write_uint(w, uint(type.elem_counts[0])) + io.write_byte(w, ']') + case .SOA_Struct_Fixed: + io.write_string(w, "#soa[") + io.write_uint(w, uint(type.elem_counts[0])) + io.write_byte(w, ']') + case .SOA_Struct_Slice: + io.write_string(w, "#soa[]") + case .SOA_Struct_Dynamic: + io.write_string(w, "#soa[dynamic]") + case .Relative_Pointer: + io.write_string(w, "#relative(") + write_type(w, pkg, types[type_types[1]], flags) + io.write_string(w, ") ") + write_type(w, pkg, types[type_types[0]], flags) + case .Relative_Slice: + io.write_string(w, "#relative(") + write_type(w, pkg, types[type_types[1]], flags) + io.write_string(w, ") ") + write_type(w, pkg, types[type_types[0]], flags) + case .Multi_Pointer: + io.write_string(w, "[^]") + write_type(w, pkg, types[type_types[0]], flags) + case .Matrix: + io.write_string(w, "matrix[") + io.write_uint(w, uint(type.elem_counts[0])) + io.write_string(w, ", ") + io.write_uint(w, uint(type.elem_counts[1])) + io.write_string(w, "]") + write_type(w, pkg, types[type_types[0]], flags) + } +} + +write_docs :: proc(w: io.Writer, pkg: ^doc.Pkg, docs: string) { + if docs == "" { + return + } + it := docs + was_code := true + was_paragraph := true + for line in strings.split_iterator(&it, "\n") { + if strings.has_prefix(line, "\t") { + if !was_code { + was_code = true; + fmt.wprint(w, `
`)
+			}
+			fmt.wprintf(w, "%s\n", strings.trim_prefix(line, "\t"))
+			continue
+		} else if was_code {
+			was_code = false
+			fmt.wprintln(w, "
") + } + text := strings.trim_space(line) + if text == "" { + if was_paragraph { + was_paragraph = false + fmt.wprintln(w, "

") + } + continue + } + if !was_paragraph { + fmt.wprintln(w, "

") + } + assert(!was_code) + was_paragraph = true + fmt.wprintln(w, text) + } + if was_code { + // assert(!was_paragraph, str(pkg.name)) + was_code = false + fmt.wprintln(w, "") + } else if was_paragraph { + fmt.wprintln(w, "

") + } +} + +write_pkg :: proc(w: io.Writer, path: string, pkg: ^doc.Pkg) { + fmt.wprintf(w, "

package core:%s

\n", path) + fmt.wprintln(w, "

Documentation

") + docs := strings.trim_space(str(pkg.docs)) + if docs != "" { + fmt.wprintln(w, "

Overview

") + fmt.wprintln(w, "
") + defer fmt.wprintln(w, "
") + + write_docs(w, pkg, docs) + } + + fmt.wprintln(w, "

Index

") + fmt.wprintln(w, `
`) + pkg_procs: [dynamic]^doc.Entity + pkg_proc_groups: [dynamic]^doc.Entity + pkg_types: [dynamic]^doc.Entity + pkg_vars: [dynamic]^doc.Entity + pkg_consts: [dynamic]^doc.Entity + + for entity_index in array(pkg.entities) { + e := &entities[entity_index] + name := str(e.name) + if name == "" || name[0] == '_' { + continue + } + switch e.kind { + case .Invalid, .Import_Name, .Library_Name: + // ignore + case .Constant: append(&pkg_consts, e) + case .Variable: append(&pkg_vars, e) + case .Type_Name: append(&pkg_types, e) + case .Procedure: append(&pkg_procs, e) + case .Proc_Group: append(&pkg_proc_groups, e) + } + } + + entity_key :: proc(e: ^doc.Entity) -> string { + return str(e.name) + } + + slice.sort_by_key(pkg_procs[:], entity_key) + slice.sort_by_key(pkg_proc_groups[:], entity_key) + slice.sort_by_key(pkg_types[:], entity_key) + slice.sort_by_key(pkg_vars[:], entity_key) + slice.sort_by_key(pkg_consts[:], entity_key) + + print_index :: proc(w: io.Writer, name: string, entities: []^doc.Entity) { + fmt.wprintf(w, "

%s

\n", name) + fmt.wprintln(w, `
`) + fmt.wprintln(w, "") + fmt.wprintln(w, "
") + } + + + print_index(w, "Procedures", pkg_procs[:]) + print_index(w, "Procedure Groups", pkg_proc_groups[:]) + print_index(w, "Types", pkg_types[:]) + print_index(w, "Variables", pkg_vars[:]) + print_index(w, "Constants", pkg_consts[:]) + + fmt.wprintln(w, "
") + + + print_entity :: proc(w: io.Writer, e: ^doc.Entity) { + pkg := &pkgs[files[e.pos.file].pkg] + name := str(e.name) + fmt.wprintf(w, "

{0:s}

\n", name) + switch e.kind { + case .Invalid, .Import_Name, .Library_Name: + // ignore + case .Constant: + case .Variable: + case .Type_Name: + case .Procedure: + fmt.wprint(w, "
")
+			fmt.wprintf(w, "%s :: ", name)
+			write_type(w, files[e.pos.file].pkg, types[e.type], nil)
+			where_clauses := array(e.where_clauses)
+			if len(where_clauses) != 0 {
+				io.write_string(w, " where ")
+				for clause, i in where_clauses {
+					if i > 0 {
+						io.write_string(w, ", ")
+					}
+					io.write_string(w, str(clause))
+				}
+			}
+
+			fmt.wprint(w, " {…}")
+			fmt.wprintln(w, "
") + case .Proc_Group: + } + + write_docs(w, pkg, strings.trim_space(str(e.docs))) + } + print_entities :: proc(w: io.Writer, title: string, entities: []^doc.Entity) { + fmt.wprintf(w, "

%s

\n", title) + fmt.wprintln(w, `
`) + for e in entities { + print_entity(w, e) + } + fmt.wprintln(w, "
") + } + + print_entities(w, "Procedures", pkg_procs[:]) + print_entities(w, "Procedure Groups", pkg_proc_groups[:]) + print_entities(w, "Types", pkg_types[:]) + print_entities(w, "Variables", pkg_vars[:]) + print_entities(w, "Constants", pkg_consts[:]) + + + fmt.wprintln(w, "

Source Files

") + fmt.wprintln(w, "") + +} \ No newline at end of file diff --git a/tools/odin-html-docs/style.css b/tools/odin-html-docs/style.css new file mode 100644 index 000000000..7c23d0bc7 --- /dev/null +++ b/tools/odin-html-docs/style.css @@ -0,0 +1,31 @@ +.container { + max-width: 60em; + margin: 0 auto; + padding-left: 0.01em 1em; +} + +.directory-pkg { + width: 20em; +} + +.directory-child .pkg-name { + position: relative; + left: 2em; + width: 18em; +} + +pre { + white-space: pre; + tab-size: 8; + background-color: #f8f8f8; + color: #202224; + border: 1px solid #c6c8ca; + border-radius: 0.25rem; + padding: 0.625rem; +} + +.documentation pre a { + text-decoration: none; + font-weight: bold; + color: #00bfd5; +} \ No newline at end of file