Commit af820bbb94

Loris Cro <kappaloris@gmail.com>
2023-01-24 18:56:35
autodoc: init support for guides
1 parent 5d46add
Changed files (3)
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>
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) {
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);
+}