motiejus/zig

fork of https://codeberg.org/ziglang/zig
git clone https://git.jakstys.lt/motiejus/zig.git
Log | Tree | Refs | README | LICENSE

commit af820bbb94a06c5bae34a38bda0f964620413e92 (tree)
parent 5d46addd255e980ad86e7cc2ad077c6964a73b85
Author: Loris Cro <kappaloris@gmail.com>
Date:   Tue, 24 Jan 2023 18:56:35 +0100

autodoc: init support for guides

Diffstat:
Mlib/docs/index.html | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mlib/docs/main.js | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/Autodoc.zig | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
3 files changed, 314 insertions(+), 81 deletions(-)

diff --git a/lib/docs/index.html b/lib/docs/index.html @@ -117,6 +117,48 @@ overflow: visible; } + .sidebar ul.guides-api-switch { + display: flex; + flex-direction: row; + justify-content: center; + text-align: center; + list-style-type: none; + margin: 0; + padding: 0; + } + + .sidebar .guides-api-switch a { + display: block; + padding: 0.5rem 1rem; + color: var(--sidebar-pkglnk-tx-color); + background-color: var(--sidebar-pkglnk-bg-color); + border: 1px solid var(--tx-color); + } + + + #ApiSwitch { + border-radius: 10px 0 0 10px; + } + + #guideSwitch { + border-radius: 0 10px 10px 0; + } + + + #ApiSwitch:hover, #guideSwitch:hover { + text-decoration: none; + } + + #ApiSwitch:hover:not(.active), #guideSwitch:hover:not(.active) { + color: var(--sidebar-pkglnk-tx-color-hover); + background-color: var(--sidebar-pkglnk-bg-color-hover); + } + + .sidebar .guides-api-switch .active { + color: var(--sidebar-pkglnk-tx-color-active); + background-color: var(--sidebar-pkglnk-bg-color-active); + } + .sidebar h2 { margin: 0.5rem; padding: 0; @@ -157,6 +199,14 @@ font-family: var(--mono); } + #guides { + padding: 1rem 0.7rem 2.4rem 1.4rem; + box-sizing: border-box; + font-size: 1rem; + background-color: var(--bg-color); + overflow-wrap: break-word; + } + /* docs section */ .docs { padding: 1rem 0.7rem 2.4rem 1.4rem; @@ -643,28 +693,42 @@ </g> </svg> </div> - <div id="sectMainPkg" class="hidden"> - <h2><span>Main Package</span></h2> - <ul class="packages"> - <li><a id="mainPkg" class="" href=""></a></li> + <div id="sectGudeApiSwitch"> + <ul class="guides-api-switch"> + <li><a id="ApiSwitch" class="active" href="#A;">API</a></li> + <li><a id="guideSwitch" class="" href="#G;">Guides</a></li> </ul> </div> - <div id="sectPkgs" class="hidden"> - <h2><span>Dependencies</span></h2> - <ul id="listPkgs" class="packages"></ul> - </div> - <div id="sectInfo" class="hidden"> - <h2><span>Zig Version</span></h2> - <p class="str" id="tdZigVer"></p> + <div id="guidesMenu" class="hidden"> + <h2><span>Guide List</span></h2> + <ul id="guidesList" class="packages"></ul> </div> - <div> - <input id="privDeclsBox" type="checkbox"/> - <label for="privDeclsBox">Internal Doc Mode</label> + <div id="apiMenu" class="hidden"> + <div id="sectMainPkg" class="hidden"> + <h2><span>Main Package</span></h2> + <ul class="packages"> + <li><a id="mainPkg" class="" href=""></a></li> + </ul> + </div> + <div id="sectPkgs" class="hidden"> + <h2><span>Dependencies</span></h2> + <ul id="listPkgs" class="packages"></ul> + </div> + <div id="sectInfo" class="hidden"> + <h2><span>Zig Version</span></h2> + <p class="str" id="tdZigVer"></p> + </div> + <div> + <input id="privDeclsBox" type="checkbox"/> + <label for="privDeclsBox">Internal Doc Mode</label> + </div> </div> </nav> </div> - <div id="docs" class="flex-right"> - <div class="wrap"> + <div class="flex-right"> + <div id="guides" class="wrap hidden"> + </div> + <div id="docs" class="wrap hidden"> <section class="docs"> <div style="position: relative"> <span id="searchPlaceholder"><kbd>s</kbd> to search, <kbd>?</kbd> for more options</span> diff --git a/lib/docs/main.js b/lib/docs/main.js @@ -2,10 +2,21 @@ var zigAnalysis; +const NAV_MODES = { + API: "#A;", + API_INTERNAL: "#a;", + GUIDES: "#G;", +}; + (function () { const domStatus = document.getElementById("status"); const domSectNav = document.getElementById("sectNav"); const domListNav = document.getElementById("listNav"); + const domApiSwitch = document.getElementById("ApiSwitch"); + const domGuideSwitch = document.getElementById("guideSwitch"); + const domGuidesMenu = document.getElementById("guidesMenu"); + const domApiMenu = document.getElementById("apiMenu"); + const domGuidesList = document.getElementById("guidesList"); const domSectMainPkg = document.getElementById("sectMainPkg"); const domSectPkgs = document.getElementById("sectPkgs"); const domListPkgs = document.getElementById("listPkgs"); @@ -45,6 +56,7 @@ var zigAnalysis; const domSectSearchResults = document.getElementById("sectSearchResults"); const domSectSearchAllResultsLink = document.getElementById("sectSearchAllResultsLink"); const domDocs = document.getElementById("docs"); + const domGuides = document.getElementById("guides"); const domListSearchResults = document.getElementById("listSearchResults"); const domSectSearchNoResults = document.getElementById("sectSearchNoResults"); const domSectInfo = document.getElementById("sectInfo"); @@ -83,7 +95,8 @@ var zigAnalysis; let canonTypeDecls = null; // lazy; use getCanonTypeDecl let curNav = { - showPrivDecls: false, + mode: NAV_MODES.API, + activeGuide: "", // each element is a package name, e.g. @import("a") then within there @import("b") // starting implicitly from root package pkgNames: [], @@ -152,7 +165,7 @@ var zigAnalysis; ); if (location.hash == "") { - location.hash = "#root"; + location.hash = "#A;"; } // make the modal disappear if you click outside it @@ -173,17 +186,21 @@ var zigAnalysis; domLangRefLink.href = `https://ziglang.org/documentation/${langRefVersion}/`; function renderTitle() { - let list = curNav.pkgNames.concat(curNav.declNames); let suffix = " - Zig"; - if (list.length === 0) { - if (rootIsStd) { - document.title = "std" + suffix; - } else { - document.title = zigAnalysis.params.rootName + suffix; - } - } else { - document.title = list.join(".") + suffix; - } + switch (curNav.mode) { + case NAV_MODES.API: + case NAV_MODES.API_INTERNAL: + let list = curNav.pkgNames.concat(curNav.declNames); + if (list.length === 0) { + document.title = zigAnalysis.packages[zigAnalysis.rootPkg].name + suffix; + } else { + document.title = list.join(".") + suffix; + } + return; + case NAV_MODES.GUIDES: + document.title = "[G] " + curNav.activeGuide + suffix; + return; + } } function isDecl(x) { @@ -377,8 +394,80 @@ var zigAnalysis; // console.assert(false); // return ({}); // } + function renderGuides() { + renderTitle(); - function render() { + // set guide mode + domGuideSwitch.classList.add("active"); + domApiSwitch.classList.remove("active"); + domDocs.classList.add("hidden"); + domGuides.classList.remove("hidden"); + domApiMenu.classList.add("hidden"); + + // sidebar guides list + const list = Object.keys(zigAnalysis.guides); + resizeDomList(domGuidesList, list.length, '<li><a href="#"></a></li>'); + for (let i = 0; i < list.length; i += 1) { + let liDom = domGuidesList.children[i]; + let aDom = liDom.children[0]; + aDom.textContent = list[i]; + aDom.setAttribute("href", NAV_MODES.GUIDES + list[i]); + if (list[i] === curNav.activeGuide) { + aDom.classList.add("active"); + } else { + aDom.classList.remove("active"); + } + } + + if (list.length > 0) { + domGuidesMenu.classList.remove("hidden"); + } + + // main content + const activeGuide = zigAnalysis.guides[curNav.activeGuide]; + if (activeGuide == undefined) { + const root_file_idx = zigAnalysis.packages[zigAnalysis.rootPkg].file; + const root_file_name = zigAnalysis.files[root_file_idx]; + domGuides.innerHTML = markdown(` + # Zig Guides + These autodocs don't contain any guide. + + While the API section is a reference guide autogenerated from Zig source code, + guides are meant to be handwritten explanations that provide for example: + + - how-to explanations for common use-cases + - technical documentation + - information about advanced usage patterns + + You can add guides by specifying which markdown files to include + in the top level doc comment of your root file, like so: + + (At the top of \`${root_file_name}\`) + \`\`\` + //!zig-autodoc-guide: intro.md + //!zig-autodoc-guide: quickstart.md + //!zig-autodoc-guide: ../advanced-docs/advanced-stuff.md + \`\`\` + + **Note that this feature is still under heavy development so expect bugs** + **and missing features!** + + Happy writing! + `); + } else { + domGuides.innerHTML = markdown(activeGuide); + } + } + + function renderApi() { + // set Api mode + domApiSwitch.classList.add("active"); + domGuideSwitch.classList.remove("active"); + domGuides.classList.add("hidden"); + domDocs.classList.remove("hidden"); + domApiMenu.classList.remove("hidden"); + domGuidesMenu.classList.add("hidden"); + domStatus.classList.add("hidden"); domFnProto.classList.add("hidden"); domSectParams.classList.add("hidden"); @@ -411,17 +500,16 @@ var zigAnalysis; renderInfo(); renderPkgList(); - domPrivDeclsBox.checked = curNav.showPrivDecls; + domPrivDeclsBox.checked = curNav.mode == NAV_MODES.API_INTERNAL; if (curNavSearch !== "") { return renderSearch(); } - let rootPkg = zigAnalysis.packages[zigAnalysis.rootPkg]; let pkg = rootPkg; curNav.pkgObjs = [pkg]; - for (let i = 0; i < curNav.pkgNames.length; i += 1) { + for (let i = 1; i < curNav.pkgNames.length; i += 1) { let childPkg = zigAnalysis.packages[pkg.table[curNav.pkgNames[i]]]; if (childPkg == null) { return render404(); @@ -494,6 +582,19 @@ var zigAnalysis; } + function render() { + switch (curNav.mode) { + case NAV_MODES.API: + case NAV_MODES.API_INTERNAL: + return renderApi(); + case NAV_MODES.GUIDES: + return renderGuides(); + default: + throw "?"; + } + } + + function renderDocTest(decl) { if (!decl.decltest) return; const astNode = getAstNode(decl.decltest); @@ -705,7 +806,6 @@ var zigAnalysis; for (let i = 0; i < curNav.pkgNames.length; i += 1) { hrefPkgNames.push(curNav.pkgNames[i]); let name = curNav.pkgNames[i]; - if (name == "root") name = zigAnalysis.rootPkgName; list.push({ name: name, link: navLink(hrefPkgNames, hrefDeclNames), @@ -747,12 +847,12 @@ var zigAnalysis; } function renderPkgList() { - let rootPkg = zigAnalysis.packages[zigAnalysis.rootPkg]; + const rootPkg = zigAnalysis.packages[zigAnalysis.rootPkg]; let list = []; for (let key in rootPkg.table) { let pkgIndex = rootPkg.table[key]; if (zigAnalysis.packages[pkgIndex] == null) continue; - if (key == zigAnalysis.params.rootName) continue; + if (key == rootPkg.name) continue; list.push({ name: key, pkg: pkgIndex, @@ -761,9 +861,9 @@ var zigAnalysis; { let aDom = domSectMainPkg.children[1].children[0].children[0]; - aDom.textContent = zigAnalysis.rootPkgName; + aDom.textContent = rootPkg.name; aDom.setAttribute("href", navLinkPkg(zigAnalysis.rootPkg)); - if (zigAnalysis.params.rootName === curNav.pkgNames[0]) { + if (rootPkg.name === curNav.pkgNames[0]) { aDom.classList.add("active"); } else { aDom.classList.remove("active"); @@ -794,20 +894,17 @@ var zigAnalysis; } function navLink(pkgNames, declNames, callName) { - let base = "#"; - if (curNav.showPrivDecls) { - base += "*"; - } - + let base = curNav.mode; + if (pkgNames.length === 0 && declNames.length === 0) { return base; } else if (declNames.length === 0 && callName == null) { return base + pkgNames.join("."); } else if (callName == null) { - return base + pkgNames.join(".") + ";" + declNames.join("."); + return base + pkgNames.join(".") + ":" + declNames.join("."); } else { return ( - base + pkgNames.join(".") + ";" + declNames.join(".") + ";" + callName + base + pkgNames.join(".") + ":" + declNames.join(".") + ";" + callName ); } } @@ -2734,9 +2831,10 @@ var zigAnalysis; throw new Error("No type 'type' found"); } - function updateCurNav() { + + function updateCurNav() { curNav = { - showPrivDecls: false, + mode: NAV_MODES.API, pkgNames: [], pkgObjs: [], declNames: [], @@ -2745,30 +2843,54 @@ var zigAnalysis; }; curNavSearch = ""; - if (location.hash[0] === "#" && location.hash.length > 1) { - let query = location.hash.substring(1); - if (query[0] === "*") { - curNav.showPrivDecls = true; - query = query.substring(1); - } + const mode = location.hash.substring(0, 3); + let query = location.hash.substring(3); + + const DEFAULT_HASH = NAV_MODES.API + zigAnalysis.packages[zigAnalysis.rootPkg].name; + switch (mode) { + case NAV_MODES.API: + case NAV_MODES.API_INTERNAL: + // #A;PACKAGE:decl.decl.decl?search-term + curNav.mode = mode; + + let qpos = query.indexOf("?"); + let nonSearchPart; + if (qpos === -1) { + nonSearchPart = query; + } else { + nonSearchPart = query.substring(0, qpos); + curNavSearch = decodeURIComponent(query.substring(qpos + 1)); + } + + let parts = nonSearchPart.split(":"); + if (parts[0] == "") { + location.hash = DEFAULT_HASH; + } else { + curNav.pkgNames = decodeURIComponent(parts[0]).split("."); + } - let qpos = query.indexOf("?"); - let nonSearchPart; - if (qpos === -1) { - nonSearchPart = query; - } else { - nonSearchPart = query.substring(0, qpos); - curNavSearch = decodeURIComponent(query.substring(qpos + 1)); - } + if (parts[1] != null) { + curNav.declNames = decodeURIComponent(parts[1]).split("."); + } - let parts = nonSearchPart.split(";"); - curNav.pkgNames = decodeURIComponent(parts[0]).split("."); - if (parts[1] != null) { - curNav.declNames = decodeURIComponent(parts[1]).split("."); - } - } - } + return; + case NAV_MODES.GUIDES: + const guides = Object.keys(zigAnalysis.guides); + if (guides.length != 0 && query == "") { + location.hash = NAV_MODES.GUIDES + guides[0]; + return; + } + curNav.mode = mode; + curNav.activeGuide = query; + + return; + default: + location.hash = DEFAULT_HASH; + return; + } + } + function onHashChange() { updateCurNav(); if (domSearch.value !== curNavSearch) { diff --git a/src/Autodoc.zig b/src/Autodoc.zig @@ -28,6 +28,7 @@ decls: std.ArrayListUnmanaged(DocData.Decl) = .{}, exprs: std.ArrayListUnmanaged(DocData.Expr) = .{}, ast_nodes: std.ArrayListUnmanaged(DocData.AstNode) = .{}, comptime_exprs: std.ArrayListUnmanaged(DocData.ComptimeExpr) = .{}, +guides: std.StringHashMapUnmanaged([]const u8) = .{}, // These fields hold temporary state of the analysis process // and are mainly used by the decl path resolving algorithm. @@ -193,10 +194,15 @@ pub fn generateZirData(self: *Autodoc) !void { } } + const rootName = blk: { + const rootName = std.fs.path.basename(self.module.main_pkg.root_src_path); + break :blk rootName[0 .. rootName.len - 4]; + }; + const main_type_index = self.types.items.len; { try self.packages.put(self.arena, self.module.main_pkg, .{ - .name = "root", + .name = rootName, .main = main_type_index, .table = .{}, }); @@ -204,7 +210,7 @@ pub fn generateZirData(self: *Autodoc) !void { self.arena, self.module.main_pkg, .{ - .name = "root", + .name = rootName, .value = 0, }, ); @@ -215,12 +221,13 @@ pub fn generateZirData(self: *Autodoc) !void { .enclosing_type = main_type_index, }; - const maybe_tldoc_comment = try self.getTLDocComment(file); + const tldoc_comment = try self.getTLDocComment(file); try self.ast_nodes.append(self.arena, .{ .name = "(root)", - .docs = maybe_tldoc_comment, + .docs = tldoc_comment, }); try self.files.put(self.arena, file, main_type_index); + try self.findGuidePaths(file, tldoc_comment); _ = try self.walkInstruction(file, &root_scope, .{}, Zir.main_struct_inst, false); @@ -236,13 +243,8 @@ pub fn generateZirData(self: *Autodoc) !void { @panic("some decl paths were never fully analized"); } - const rootName = blk: { - const rootName = std.fs.path.basename(self.module.main_pkg.root_src_path); - break :blk rootName[0 .. rootName.len - 4]; - }; var data = DocData{ - .rootPkgName = rootName, - .params = .{ .rootName = "root" }, + .params = .{}, .packages = self.packages.values(), .files = self.files, .calls = self.calls.items, @@ -251,6 +253,7 @@ pub fn generateZirData(self: *Autodoc) !void { .exprs = self.exprs.items, .astNodes = self.ast_nodes.items, .comptimeExprs = self.comptime_exprs.items, + .guides = self.guides, }; const base_dir = self.doc_location.directory orelse @@ -370,12 +373,10 @@ const Scope = struct { const DocData = struct { typeKinds: []const []const u8 = std.meta.fieldNames(DocTypeKinds), rootPkg: u32 = 0, - rootPkgName: []const u8, params: struct { zigId: []const u8 = "arst", zigVersion: []const u8 = build_options.version, target: []const u8 = "arst", - rootName: []const u8, builds: []const struct { target: []const u8 } = &.{ .{ .target = "arst" }, }, @@ -391,6 +392,9 @@ const DocData = struct { decls: []Decl, exprs: []Expr, comptimeExprs: []ComptimeExpr, + + guides: std.StringHashMapUnmanaged([]const u8), + const Call = struct { func: Expr, args: []Expr, @@ -410,6 +414,7 @@ const DocData = struct { try jsw.objectField(f_name); switch (f) { .files => try writeFileTableToJson(self.files, &jsw), + .guides => try writeGuidesToJson(self.guides, &jsw), else => { try std.json.stringify(@field(self, f_name), opts, w); jsw.state_index -= 1; @@ -852,7 +857,7 @@ fn walkInstruction( const maybe_other_package: ?*Package = blk: { if (self.module.main_pkg_is_std and std.mem.eql(u8, path, "std")) { - path = "root"; + path = "std"; break :blk self.module.main_pkg; } else { break :blk file.pkg.table.get(path); @@ -4364,6 +4369,16 @@ fn writeFileTableToJson(map: std.AutoArrayHashMapUnmanaged(*File, usize), jsw: a try jsw.endArray(); } +fn writeGuidesToJson(map: std.StringHashMapUnmanaged([]const u8), jsw: anytype) !void { + try jsw.beginObject(); + var it = map.iterator(); + while (it.next()) |entry| { + try jsw.objectField(entry.key_ptr.*); + try jsw.emitString(entry.value_ptr.*); + } + try jsw.endObject(); +} + fn writePackageTableToJson( map: std.AutoHashMapUnmanaged(*Package, DocData.DocPackage.TableEntry), jsw: anytype, @@ -4422,8 +4437,40 @@ fn getTLDocComment(self: *Autodoc, file: *File) ![]const u8 { var tok = tokenizer.next(); var comment = std.ArrayList(u8).init(self.arena); while (tok.tag == .container_doc_comment) : (tok = tokenizer.next()) { - try comment.appendSlice(source[tok.loc.start + 3 .. tok.loc.end + 1]); + try comment.appendSlice(source[tok.loc.start + "//!".len .. tok.loc.end + 1]); } return comment.items; } + +fn findGuidePaths(self: *Autodoc, file: *File, str: []const u8) !void { + const prefix = "zig-autodoc-guide:"; + var it = std.mem.tokenize(u8, str, "\n"); + while (it.next()) |line| { + const trimmed_line = std.mem.trim(u8, line, " "); + if (std.mem.startsWith(u8, trimmed_line, prefix)) { + const path = trimmed_line[prefix.len..]; + const trimmed_path = std.mem.trim(u8, path, " "); + try self.addGuide(file, trimmed_path); + } + } +} + +fn addGuide(self: *Autodoc, file: *File, guide_path: []const u8) !void { + if (guide_path.len == 0) return error.MissingAutodocGuideName; + + const cur_pkg_dir_path = file.pkg.root_src_directory.path orelse "."; + const resolved_path = try std.fs.path.resolve(self.arena, &[_][]const u8{ + cur_pkg_dir_path, file.sub_file_path, "..", guide_path, + }); + + var guide_file = try file.pkg.root_src_directory.handle.openFile(resolved_path, .{}); + defer guide_file.close(); + + const guide = guide_file.reader().readAllAlloc(self.arena, 1 * 1024 * 1024) catch |err| switch (err) { + error.StreamTooLong => @panic("stream too long"), + else => |e| return e, + }; + + try self.guides.put(self.arena, resolved_path, guide); +}