-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/lib/docs/main.js b/lib/docs/main.js
index 4991c31b57f99fe5e07309d3ce8aae60f7ca3349..95e0a445e87893e553b51c717b35a651d9cb0d75 100644
--- a/lib/docs/main.js
+++ b/lib/docs/main.js
@@ -1,5242 +1,851 @@
-"use strict";
-
-var zigAnalysis = {
- typeKinds,
- rootMod,
- modules,
- astNodes,
- calls,
- files,
- decls,
- exprs,
- types,
- comptimeExprs,
- guideSections
-};
-
-let skipNextHashChange = null;
-
-const NAV_MODES = {
- API: "#A;",
- GUIDES: "#G;",
-};
-
-
-var scrollHistory = {};
-
(function() {
- const domBanner = document.getElementById("banner");
- const domMain = document.getElementById("main");
- const domStatus = document.getElementById("status");
- const domSectNavAPI = document.getElementById("sectNavAPI");
- const domListNavAPI = document.getElementById("listNavAPI");
- const domSectNavGuides = document.getElementById("sectNavGuides");
- const domListNavGuides = document.getElementById("listNavGuides");
- const domApiSwitch = document.getElementById("ApiSwitch");
- const domGuideSwitch = document.getElementById("guideSwitch");
- const domGuidesMenu = document.getElementById("guidesMenu");
- const domGuidesMenuTitle = document.getElementById("guidesMenuTitle");
- const domGuideTocList = document.getElementById("guideTocList");
- const domGuideTocListEmtpy = document.getElementById("guideTocListEmpty");
- const domListMods = document.getElementById("listMods");
- const domSectTypes = document.getElementById("sectTypes");
- const domListTypesLeft = document.getElementById("listTypesLeft");
- const domListTypesRight = document.getElementById("listTypesRight");
- const domSectTests = document.getElementById("sectTests");
- const domListTests = document.getElementById("listTests");
- const domSectDocTests = document.getElementById("sectDocTests");
- const domDocTestsCode = document.getElementById("docTestsCode");
- const domSectNamespaces = document.getElementById("sectNamespaces");
- const domListNamespacesLeft = document.getElementById("listNamespacesLeft");
- const domListNamespacesRight = document.getElementById("listNamespacesRight");
- const domNoDocsNamespaces = document.getElementById("noDocsNamespaces");
- const domSectErrSets = document.getElementById("sectErrSets");
- const domListErrSets = document.getElementById("listErrSets");
- const domSectFns = document.getElementById("sectFns");
- const domListFns = document.getElementById("listFns");
- const domSectFields = document.getElementById("sectFields");
- const domListFields = document.getElementById("listFields");
- const domSectGlobalVars = document.getElementById("sectGlobalVars");
- const domListGlobalVars = document.getElementById("listGlobalVars");
- const domSectValues = document.getElementById("sectValues");
- const domListValues = document.getElementById("listValues");
- const domFnProto = document.getElementById("fnProto");
- const domFnProtoCode = document.getElementById("fnProtoCode");
- const domFnSourceLink = document.getElementById("fnSourceLink");
- const domSectParams = document.getElementById("sectParams");
- const domListParams = document.getElementById("listParams");
- const domTldDocs = document.getElementById("tldDocs");
- const domSectFnErrors = document.getElementById("sectFnErrors");
- const domListFnErrors = document.getElementById("listFnErrors");
- const domTableFnErrors = document.getElementById("tableFnErrors");
- const domFnErrorsAnyError = document.getElementById("fnErrorsAnyError");
- const domFnExamples = document.getElementById("fnExamples");
- // const domListFnExamples = (document.getElementById("listFnExamples"));
- const domFnNoExamples = document.getElementById("fnNoExamples");
- const domDeclNoRef = document.getElementById("declNoRef");
- const domSearch = document.getElementById("search");
- const domSearchHelp = document.getElementById("searchHelp");
- const domSearchHelpSummary = document.getElementById("searchHelpSummary");
- const domSectSearchResults = document.getElementById("sectSearchResults");
- const domSectSearchAllResultsLink = document.getElementById("sectSearchAllResultsLink");
- const domDocs = document.getElementById("docs");
- const domDocsScroll = document.getElementById("docs-scroll");
- const domGuidesSection = document.getElementById("guides");
- const domActiveGuide = document.getElementById("activeGuide");
-
- const domListSearchResults = document.getElementById("listSearchResults");
- const domSectSearchNoResults = document.getElementById("sectSearchNoResults");
- // const domTdTarget = (document.getElementById("tdTarget"));
- const domTdZigVer = document.getElementById("tdZigVer");
- const domHdrName = document.getElementById("hdrName");
- const domHelpModal = document.getElementById("helpModal");
- const domSearchKeys = document.getElementById("searchKeys");
- const domPrefsModal = document.getElementById("prefsModal");
- const domSearchPlaceholder = document.getElementById("searchPlaceholder");
- const domSearchPlaceholderText = document.getElementById("searchPlaceholderText");
- const sourceFileUrlTemplate = "src/{{mod}}/{{file}}.html#L{{line}}"
- const domLangRefLink = document.getElementById("langRefLink");
-
- const domPrefSlashSearch = document.getElementById("prefSlashSearch");
- const prefs = getLocalStorage();
- loadPrefs();
-
- domPrefSlashSearch.addEventListener("change", () => setPrefSlashSearch(domPrefSlashSearch.checked));
-
- const scrollMonitor = [
- domActiveGuide,
- domGuideTocList,
- domDocsScroll,
- domSectSearchResults,
- ];
-
- computeGuideHashes();
-
- let searchTimer = null;
- let searchTrimResults = true;
-
- let escapeHtmlReplacements = {
- "&": "&",
- '"': """,
- "<": "<",
- ">": ">",
- };
-
- let typeKinds = indexTypeKinds();
- let typeTypeId = findTypeTypeId();
- let pointerSizeEnum = { One: 0, Many: 1, Slice: 2, C: 3 };
-
- let declSearchIndex = new RadixTree();
- window.search = declSearchIndex;
-
- // for each module, is an array with modules to get to this one
- let canonModPaths = computeCanonicalModulePaths();
-
- // for each decl, is an array with {declNames, modNames} to get to this one
- let canonDeclPaths = null; // lazy; use getCanonDeclPath
-
- // for each type, is an array with {declNames, modNames} to get to this one
- let canonTypeDecls = null; // lazy; use getCanonTypeDecl
-
- let curNav = {
- hash: "",
- mode: NAV_MODES.API,
- activeGuide: "",
- activeGuideScrollTo: null,
- // each element is a module name, e.g. @import("a") then within there @import("b")
- // starting implicitly from root module
- modNames: [],
- // same as above except actual modules, not names
- modObjs: [],
- // Each element is a decl name, `a.b.c`, a is 0, b is 1, c is 2, etc.
- // empty array means refers to the module itself
- declNames: [],
- // these will be all types, except the last one may be a type or a decl
- declObjs: [],
- // (a, b, c, d) comptime call; result is the value the docs refer to
- callName: null,
- };
-
- let curNavSearch = "";
- let curSearchIndex = -1;
- let imFeelingLucky = false;
-
- let rootIsStd = detectRootIsStd();
-
- // map of decl index to list of non-generic fn indexes
- // let nodesToFnsMap = indexNodesToFns();
- // map of decl index to list of comptime fn calls
- // let nodesToCallsMap = indexNodesToCalls();
-
- let guidesSearchIndex = {};
- window.guideSearch = guidesSearchIndex;
- parseGuides();
-
- // identifiers can contain modal trigger characters so we want to allow typing
- // such characters when the search is focused instead of toggling the modal
- let canToggleModal = true;
-
- domSearch.disabled = false;
- domSearch.addEventListener("keydown", onSearchKeyDown, false);
- domSearch.addEventListener("input", onSearchInput, false);
- domSearch.addEventListener("focus", ev => {
- domSearchPlaceholder.classList.add("hidden");
- canToggleModal = false;
- });
- domSearch.addEventListener("blur", ev => {
- if (domSearch.value.length == 0)
- domSearchPlaceholder.classList.remove("hidden");
- canToggleModal = true;
- });
- domSectSearchAllResultsLink.addEventListener('click', onClickSearchShowAllResults, false);
- function onClickSearchShowAllResults(ev) {
- ev.preventDefault();
- ev.stopPropagation();
- searchTrimResults = false;
- onHashChange();
- }
-
- if (location.hash == "") {
- location.hash = "#A;";
- }
-
- // make the modal disappear if you click outside it
- function handleModalClick(ev) {
- if (ev.target.classList.contains("modal-container")) {
- hideModal(this);
- }
- }
- domHelpModal.addEventListener("click", handleModalClick);
- domPrefsModal.addEventListener("click", handleModalClick);
-
- window.addEventListener("hashchange", onHashChange, false);
- window.addEventListener("keydown", onWindowKeyDown, false);
- onHashChange();
-
- // TODO: fix this once langref becomes part of autodoc
- let langRefVersion = "master";
- domLangRefLink.href = `https://ziglang.org/documentation/${langRefVersion}/`;
-
- function renderTitle() {
- let suffix = " - Zig";
- switch (curNav.mode) {
- case NAV_MODES.API:
- let list = curNav.modNames.concat(curNav.declNames);
- if (list.length === 0) {
- document.title = zigAnalysis.modules[zigAnalysis.rootMod].name + suffix;
- } else {
- document.title = list.join(".") + suffix;
- }
- return;
- case NAV_MODES.GUIDES:
- document.title = "[G] " + curNav.activeGuide + suffix;
- return;
- }
- }
-
- function isDecl(x) {
- return "value" in x;
- }
-
- function isType(x) {
- return "kind" in x && !("value" in x);
- }
-
- function isContainerType(x) {
- return isType(x) && typeKindIsContainer(x.kind);
- }
-
- function typeShorthandName(expr) {
- let resolvedExpr = resolveValue({ expr: expr });
- if (!("type" in resolvedExpr)) {
- return null;
- }
- let type = getType(resolvedExpr.type);
-
- outer: for (let i = 0; i < 10000; i += 1) {
- switch (type.kind) {
- case typeKinds.Optional:
- case typeKinds.Pointer:
- let child = type.child;
- let resolvedChild = resolveValue(child);
- if ("type" in resolvedChild) {
- type = getType(resolvedChild.type);
- continue;
- } else {
- return null;
- }
- default:
- break outer;
- }
-
- if (i == 9999) throw "Exhausted typeShorthandName quota";
- }
-
- let name = undefined;
- if (type.kind === typeKinds.Struct) {
- name = "struct";
- } else if (type.kind === typeKinds.Enum) {
- name = "enum";
- } else if (type.kind === typeKinds.Union) {
- name = "union";
- } else {
- console.log("TODO: unhandled case in typeShortName");
- return null;
- }
-
- return escapeHtml(name);
- }
-
- function typeKindIsContainer(typeKind) {
- return (
- typeKind === typeKinds.Struct ||
- typeKind === typeKinds.Union ||
- typeKind === typeKinds.Enum ||
- typeKind === typeKinds.Opaque
- );
- }
-
- function declCanRepresentTypeKind(typeKind) {
- return typeKind === typeKinds.ErrorSet || typeKindIsContainer(typeKind);
- }
-
- //
- // function findCteInRefPath(path) {
- // for (let i = path.length - 1; i >= 0; i -= 1) {
- // const ref = path[i];
- // if ("string" in ref) continue;
- // if ("comptimeExpr" in ref) return ref;
- // if ("refPath" in ref) return findCteInRefPath(ref.refPath);
- // return null;
- // }
-
- // return null;
- // }
-
- function resolveValue(value, trackDecls) {
- let seenDecls = [];
- let i = 0;
- while (true) {
- i += 1;
- if (i >= 10000) {
- throw "resolveValue quota exceeded"
- }
-
- if ("refPath" in value.expr) {
- value = { expr: value.expr.refPath[value.expr.refPath.length - 1] };
- continue;
- }
-
- if ("declRef" in value.expr) {
- seenDecls.push(value.expr.declRef);
- value = getDecl(value.expr.declRef).value;
- continue;
- }
-
- if ("as" in value.expr) {
- value = {
- typeRef: zigAnalysis.exprs[value.expr.as.typeRefArg],
- expr: zigAnalysis.exprs[value.expr.as.exprArg],
- };
- continue;
- }
-
- if (trackDecls) return { value, seenDecls };
- return value;
- }
- }
-
- function resolveGenericRet(genericFunc) {
- if (genericFunc.generic_ret == null) return null;
- let result = resolveValue({ expr: genericFunc.generic_ret });
-
- let i = 0;
- while (true) {
- i += 1;
- if (i >= 10000) {
- throw "resolveGenericRet quota exceeded"
- }
-
- if ("call" in result.expr) {
- let call = zigAnalysis.calls[result.expr.call];
- let resolvedFunc = resolveValue({ expr: call.func });
- if (!("type" in resolvedFunc.expr)) return null;
- let callee = getType(resolvedFunc.expr.type);
- if (!callee.generic_ret) return null;
- result = resolveValue({ expr: callee.generic_ret });
- continue;
- }
-
- return result;
- }
- }
-
- // function typeOfDecl(decl){
- // return decl.value.typeRef;
- //
- // let i = 0;
- // while(i < 1000) {
- // i += 1;
- // console.assert(isDecl(decl));
- // if ("type" in decl.value) {
- // return ({ type: typeTypeId });
- // }
- //
- //// if ("string" in decl.value) {
- //// return ({ type: {
- //// kind: typeKinds.Pointer,
- //// size: pointerSizeEnum.One,
- //// child: });
- //// }
- //
- // if ("refPath" in decl.value) {
- // decl = ({
- // value: decl.value.refPath[decl.value.refPath.length -1]
- // });
- // continue;
- // }
- //
- // if ("declRef" in decl.value) {
- // decl = zigAnalysis.decls[decl.value.declRef];
- // continue;
- // }
- //
- // if ("int" in decl.value) {
- // return decl.value.int.typeRef;
- // }
- //
- // if ("float" in decl.value) {
- // return decl.value.float.typeRef;
- // }
- //
- // if ("array" in decl.value) {
- // return decl.value.array.typeRef;
- // }
- //
- // if ("struct" in decl.value) {
- // return decl.value.struct.typeRef;
- // }
- //
- // if ("comptimeExpr" in decl.value) {
- // const cte = zigAnalysis.comptimeExprs[decl.value.comptimeExpr];
- // return cte.typeRef;
- // }
- //
- // if ("call" in decl.value) {
- // const fn_call = zigAnalysis.calls[decl.value.call];
- // let fn_decl = undefined;
- // if ("declRef" in fn_call.func) {
- // fn_decl = zigAnalysis.decls[fn_call.func.declRef];
- // } else if ("refPath" in fn_call.func) {
- // console.assert("declRef" in fn_call.func.refPath[fn_call.func.refPath.length -1]);
- // fn_decl = zigAnalysis.decls[fn_call.func.refPath[fn_call.func.refPath.length -1].declRef];
- // } else throw {};
- //
- // const fn_decl_value = resolveValue(fn_decl.value);
- // console.assert("type" in fn_decl_value); //TODO handle comptimeExpr
- // const fn_type = (zigAnalysis.types[fn_decl_value.type]);
- // console.assert(fn_type.kind === typeKinds.Fn);
- // return fn_type.ret;
- // }
- //
- // if ("void" in decl.value) {
- // return ({ type: typeTypeId });
- // }
- //
- // if ("bool" in decl.value) {
- // return ({ type: typeKinds.Bool });
- // }
- //
- // console.log("TODO: handle in `typeOfDecl` more cases: ", decl);
- // console.assert(false);
- // throw {};
- // }
- // console.assert(false);
- // return ({});
- // }
- function detectDeclPath(text, context) {
- let result = "";
- let separator = ":";
- const components = text.split(".");
- let curDeclOrType = undefined;
-
- let curContext = context;
- let limit = 10000;
- while (curContext) {
- limit -= 1;
-
- if (limit == 0) {
- throw "too many iterations";
- }
-
- curDeclOrType = findSubDecl(curContext, components[0]);
-
- if (!curDeclOrType) {
- if (curContext.parent_container == null) break;
- curContext = getType(curContext.parent_container);
- continue;
- }
-
- if (curContext == context) {
- separator = '.';
- result = location.hash + separator + components[0];
- } else {
- // We had to go up, which means we need a new path!
- const canonPath = getCanonDeclPath(curDeclOrType.find_subdecl_idx);
- if (!canonPath) return;
-
- let lastModName = canonPath.modNames[canonPath.modNames.length - 1];
- let fullPath = lastModName + ":" + canonPath.declNames.join(".");
-
- separator = '.';
- result = "#A;" + fullPath;
- }
-
- break;
- }
-
- if (!curDeclOrType) {
- for (let i = 0; i < zigAnalysis.modules.length; i += 1){
- const p = zigAnalysis.modules[i];
- if (p.name == components[0]) {
- curDeclOrType = getType(p.main);
- result += "#A;" + components[0];
- break;
- }
- }
- }
-
- if (!curDeclOrType) return null;
-
- for (let i = 1; i < components.length; i += 1) {
- curDeclOrType = findSubDecl(curDeclOrType, components[i]);
- if (!curDeclOrType) return null;
- result += separator + components[i];
- separator = '.';
- }
-
- return result;
-
- }
-
- function renderGuides() {
- renderTitle();
-
- // set guide mode
- domGuideSwitch.classList.add("active");
- domApiSwitch.classList.remove("active");
- domDocs.classList.add("hidden");
- domSectNavAPI.classList.add("hidden");
- domSectNavGuides.classList.remove("hidden");
- domGuidesSection.classList.remove("hidden");
- domActiveGuide.classList.add("hidden");
- domSectSearchResults.classList.add("hidden");
- domSectSearchAllResultsLink.classList.add("hidden");
- domSectSearchNoResults.classList.add("hidden");
- if (curNavSearch !== "") {
- return renderSearchGuides();
- }
-
- let activeGuide = undefined;
- outer: for (let i = 0; i < zigAnalysis.guideSections.length; i += 1) {
- const section = zigAnalysis.guideSections[i];
- for (let j = 0; j < section.guides.length; j += 1) {
- const guide = section.guides[j];
- if (guide.name == curNav.activeGuide) {
- activeGuide = guide;
- break outer;
- }
- }
- }
-
-
- // navigation bar
-
- const guideIndexDom = domListNavGuides.children[0].children[0];
- const guideDom = domListNavGuides.children[1].children[0];
- if (activeGuide){
- guideDom.textContent = activeGuide.title;
- guideDom.setAttribute("href", location.hash);
- guideDom.classList.remove("hidden");
- guideIndexDom.classList.remove("active");
- } else {
- guideDom.classList.add("hidden");
- guideIndexDom.classList.add("active");
- }
-
- // main content
- domGuidesMenuTitle.textContent = "Table of Contents";
- if (activeGuide) {
- if (activeGuide.toc != "") {
- domGuideTocList.innerHTML = activeGuide.toc;
- // add js callbacks to all links
- function onLinkClick(ev) {
- const link = ev.target.getAttribute("href");
- skipNextHashChange = link;
- location.replace(link);
- scrollToHeading(":" + link.split(":")[1], true);
- ev.preventDefault();
- ev.stopPropagation();
- }
- for (let a of domGuideTocList.querySelectorAll("a")) {
- a.addEventListener('click', onLinkClick, false);
- }
- domGuideTocList.classList.remove("hidden");
- domGuideTocListEmtpy.classList.add("hidden");
- } else {
- domGuideTocListEmtpy.classList.remove("hidden");
- domGuideTocList.classList.add("hidden");
- }
-
- let reader = new commonmark.Parser({
- smart: true,
- autoDoc: {
- detectDeclPath: detectDeclPath,
- }
- });
- let ast = reader.parse(activeGuide.body);
- let writer = new commonmark.HtmlRenderer();
- let result = writer.render(ast);
- domActiveGuide.innerHTML = result;
- if (curNav.activeGuideScrollTo !== null) {
- scrollToHeading(curNav.activeGuideScrollTo, false);
- }
- } else {
- domGuideTocList.classList.add("hidden");
- domGuideTocListEmtpy.classList.remove("hidden");
-
- if (zigAnalysis.guideSections.length > 1 || (zigAnalysis.guideSections[0].guides.length > 0)) {
- renderGuidesIndex();
- } else {
- noGuidesAtAll();
- }
- }
-
- domGuidesMenu.classList.remove("hidden");
- domActiveGuide.classList.remove("hidden");
- }
-
- // TODO: ensure unique hashes
- // TODO: hash also guides and their headings
- function computeGuideHashes() {
- for (let i = 1; i < zigAnalysis.guideSections.length; i += 1) {
- const section = zigAnalysis.guideSections[i];
- section.hash = "section-" + slugify(section.name || i);
- }
- }
-
- function renderGuidesIndex() {
- // main content
- {
- let html = "";
- for (let i = 0; i < zigAnalysis.guideSections.length; i += 1) {
- const section = zigAnalysis.guideSections[i];
- if (i != 0) { // first section is the default section
- html += "
";
- }
- }
- domActiveGuide.innerHTML = html;
- }
-
- // sidebar / fast navigation
- {
- domGuidesMenuTitle.textContent = "Sections";
- if (zigAnalysis.guideSections.length > 1) {
- let html = "";
- for (let i = 1; i < zigAnalysis.guideSections.length; i += 1) {
- const section = zigAnalysis.guideSections[i];
- html += "
";
-
- function onLinkClick(ev) {
- const link = ev.target.getAttribute("href");
- skipNextHashChange = link;
- location.replace(link);
- scrollToHeading(link.split(":")[1], true);
- ev.preventDefault();
- ev.stopPropagation();
- }
- for (let a of domGuideTocList.querySelectorAll("a")) {
- a.addEventListener('click', onLinkClick, false);
- }
-
- domGuideTocList.classList.remove("hidden");
- domGuideTocListEmtpy.classList.add("hidden");
- } else {
- domGuideTocList.classList.add("hidden");
- domGuideTocListEmtpy.classList.remove("hidden");
- }
- }
- }
-
- function noGuidesAtAll() {
- const root_file_idx = zigAnalysis.modules[zigAnalysis.rootMod].file;
- const root_file_name = getFile(root_file_idx).name;
- let reader = new commonmark.Parser({smart: true});
- let ast = reader.parse(`
-# No 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
-\`\`\`
-
-You can also create sections to group guides together:
-
-\`\`\`
-//!zig-autodoc-section: CLI Usage
-//!zig-autodoc-guide: cli-basics.md
-//!zig-autodoc-guide: cli-advanced.md
-\`\`\`
-
-
-**Note that this feature is still under heavy development so expect bugs**
-**and missing features!**
-
-Happy writing!
-`);
-
- let writer = new commonmark.HtmlRenderer();
- let result = writer.render(ast);
- domActiveGuide.innerHTML = result;
-
- }
-
- function renderApi() {
- // set Api mode
- domApiSwitch.classList.add("active");
- domGuideSwitch.classList.remove("active");
- domGuidesSection.classList.add("hidden");
- domSectNavAPI.classList.remove("hidden");
- domSectNavGuides.classList.add("hidden");
- domDocs.classList.remove("hidden");
- domGuidesMenu.classList.add("hidden");
- domStatus.classList.add("hidden");
- domFnProto.classList.add("hidden");
- domSectParams.classList.add("hidden");
- domTldDocs.classList.add("hidden");
- domSectTypes.classList.add("hidden");
- domSectTests.classList.add("hidden");
- domSectDocTests.classList.add("hidden");
- domSectNamespaces.classList.add("hidden");
- domListNamespacesLeft.classList.add("hidden");
- domListNamespacesRight.classList.add("hidden");
- domNoDocsNamespaces.classList.add("hidden");
- domSectErrSets.classList.add("hidden");
- domSectFns.classList.add("hidden");
- domSectFields.classList.add("hidden");
- domSectSearchResults.classList.add("hidden");
- domSectSearchAllResultsLink.classList.add("hidden");
- domSectSearchNoResults.classList.add("hidden");
- domHdrName.classList.add("hidden");
- domSectFnErrors.classList.add("hidden");
- domFnExamples.classList.add("hidden");
- domFnNoExamples.classList.add("hidden");
- domFnSourceLink.classList.add("hidden");
- domDeclNoRef.classList.add("hidden");
- domFnErrorsAnyError.classList.add("hidden");
- domTableFnErrors.classList.add("hidden");
- domSectGlobalVars.classList.add("hidden");
- domSectValues.classList.add("hidden");
-
- renderTitle();
-
- if (curNavSearch !== "") {
- return renderSearchAPI();
- }
-
- let rootMod = zigAnalysis.modules[zigAnalysis.rootMod];
- let mod = rootMod;
- curNav.modObjs = [mod];
- for (let i = 0; i < curNav.modNames.length; i += 1) {
- let childMod = zigAnalysis.modules[mod.table[curNav.modNames[i]]];
- if (childMod == null) {
- return render404();
- }
- mod = childMod;
- curNav.modObjs.push(mod);
- }
-
- let currentType = getType(mod.main);
- curNav.declObjs = [currentType];
- let lastDecl = mod.main;
- for (let i = 0; i < curNav.declNames.length; i += 1) {
- let childDecl = findSubDecl(currentType, curNav.declNames[i]);
- window.last_decl = childDecl;
- if (childDecl == null || childDecl.is_private === true) {
- return render404();
- }
- lastDecl = childDecl;
-
- let childDeclValue = resolveValue(childDecl.value).expr;
- if ("type" in childDeclValue) {
- const t = getType(childDeclValue.type);
- if (t.kind != typeKinds.Fn) {
- childDecl = t;
- }
- }
-
- currentType = childDecl;
- curNav.declObjs.push(currentType);
- }
-
-
-
- window.x = currentType;
-
- renderNav();
-
- let last = curNav.declObjs[curNav.declObjs.length - 1];
- let lastIsDecl = isDecl(last);
- let lastIsType = isType(last);
- let lastIsContainerType = isContainerType(last);
-
- renderDocTest(lastDecl);
-
- if (lastIsContainerType) {
- return renderContainer(last);
- }
-
- if (!lastIsDecl && !lastIsType) {
- return renderUnknownDecl(last);
- }
-
- if (lastIsType) {
- return renderType(last);
- }
-
- if (lastIsDecl && last.kind === "var") {
- return renderVar(last);
- }
-
- if (lastIsDecl && last.kind === "const") {
- const value = resolveValue(last.value);
- if ("type" in value.expr) {
- let typeObj = getType(value.expr.type);
- if (typeObj.kind === typeKinds.Fn) {
- return renderFn(last);
- }
- }
- return renderValue(last);
- }
-
- }
-
- function render() {
- switch (curNav.mode) {
- case NAV_MODES.API:
- return renderApi();
- case NAV_MODES.GUIDES:
- return renderGuides();
- default:
- throw "?";
- }
- }
-
-
- function renderDocTest(decl) {
- if (!decl.decltest) return;
- const astNode = getAstNode(decl.decltest);
- domSectDocTests.classList.remove("hidden");
- domDocTestsCode.innerHTML = renderTokens(
- DecoratedTokenizer(astNode.code, decl));
- }
-
- function renderUnknownDecl(decl) {
- domDeclNoRef.classList.remove("hidden");
-
- let docs = getAstNode(decl.src).docs;
- if (docs != null) {
- domTldDocs.innerHTML = markdown(docs);
- } else {
- domTldDocs.innerHTML =
- "
There are no doc comments for this declaration.
";
- }
- domTldDocs.classList.remove("hidden");
- }
-
- function typeIsErrSet(typeIndex) {
- let typeObj = getType(typeIndex);
- return typeObj.kind === typeKinds.ErrorSet;
- }
-
- function typeIsStructWithNoFields(typeIndex) {
- let typeObj = getType(typeIndex);
- if (typeObj.kind !== typeKinds.Struct) return false;
- return typeObj.field_types.length == 0;
- }
-
- function typeIsGenericFn(typeIndex) {
- let typeObj = getType(typeIndex);
- if (typeObj.kind !== typeKinds.Fn) {
- return false;
- }
- return typeObj.generic_ret != null;
- }
-
- function renderFn(fnDecl) {
- if ("refPath" in fnDecl.value.expr) {
- let last = fnDecl.value.expr.refPath.length - 1;
- let lastExpr = fnDecl.value.expr.refPath[last];
- console.assert("declRef" in lastExpr);
- fnDecl = getDecl(lastExpr.declRef);
- }
-
- let value = resolveValue(fnDecl.value);
- console.assert("type" in value.expr);
- let typeObj = getType(value.expr.type);
-
- domFnProtoCode.innerHTML = renderTokens(ex(value.expr, { fnDecl: fnDecl }));
- domFnSourceLink.classList.remove("hidden");
- domFnSourceLink.innerHTML = "[
]";
-
- let docsSource = null;
- let srcNode = getAstNode(fnDecl.src);
- if (srcNode.docs != null) {
- docsSource = srcNode.docs;
- }
-
- renderFnParamDocs(fnDecl, typeObj);
-
- let retExpr = resolveValue({ expr: typeObj.ret }).expr;
- if ("type" in retExpr) {
- let retIndex = retExpr.type;
- let errSetTypeIndex = null;
- let retType = getType(retIndex);
- if (retType.kind === typeKinds.ErrorSet) {
- errSetTypeIndex = retIndex;
- } else if (retType.kind === typeKinds.ErrorUnion) {
- errSetTypeIndex = retType.err.type;
- }
- if (errSetTypeIndex != null) {
- let errSetType = getType(errSetTypeIndex);
- renderErrorSet(errSetType);
- }
- }
-
- let protoSrcIndex = fnDecl.src;
- if (typeIsGenericFn(value.expr.type)) {
- // does the generic_ret contain a container?
- var resolvedGenericRet = resolveValue({ expr: typeObj.generic_ret });
-
- if ("call" in resolvedGenericRet.expr) {
- let call = zigAnalysis.calls[resolvedGenericRet.expr.call];
- let resolvedFunc = resolveValue({ expr: call.func });
- if (!("type" in resolvedFunc.expr)) return;
- let callee = getType(resolvedFunc.expr.type);
- if (!callee.generic_ret) return;
- resolvedGenericRet = resolveValue({ expr: callee.generic_ret });
- }
-
- // TODO: see if unwrapping the `as` here is a good idea or not.
- if ("as" in resolvedGenericRet.expr) {
- resolvedGenericRet = {
- expr: zigAnalysis.exprs[resolvedGenericRet.expr.as.exprArg],
- };
- }
-
- if (!("type" in resolvedGenericRet.expr)) return;
- const genericType = getType(resolvedGenericRet.expr.type);
- if (isContainerType(genericType)) {
- renderContainer(genericType);
- }
-
- // old code
- // let instantiations = nodesToFnsMap[protoSrcIndex];
- // let calls = nodesToCallsMap[protoSrcIndex];
- // if (instantiations == null && calls == null) {
- // domFnNoExamples.classList.remove("hidden");
- // } else if (calls != null) {
- // // if (fnObj.combined === undefined) fnObj.combined = allCompTimeFnCallsResult(calls);
- // if (fnObj.combined != null) renderContainer(fnObj.combined);
-
- // resizeDomList(domListFnExamples, calls.length, '
');
-
- // for (let callI = 0; callI < calls.length; callI += 1) {
- // let liDom = domListFnExamples.children[callI];
- // liDom.innerHTML = getCallHtml(fnDecl, calls[callI]);
- // }
-
- // domFnExamples.classList.remove("hidden");
- // } else if (instantiations != null) {
- // // TODO
- // }
- } else {
- domFnExamples.classList.add("hidden");
- domFnNoExamples.classList.add("hidden");
- }
-
- let protoSrcNode = getAstNode(protoSrcIndex);
- if (
- docsSource == null &&
- protoSrcNode != null &&
- protoSrcNode.docs != null
- ) {
- docsSource = protoSrcNode.docs;
- }
- if (docsSource != null) {
- domTldDocs.innerHTML = markdown(docsSource, fnDecl);
- domTldDocs.classList.remove("hidden");
- }
- domFnProto.classList.remove("hidden");
- }
-
- function renderFnParamDocs(fnDecl, typeObj) {
- let docCount = 0;
-
- let fnNode = getAstNode(fnDecl.src);
- let fields = fnNode.fields;
- if (fields === null) {
- fields = getAstNode(typeObj.src).fields;
- }
- let isVarArgs = typeObj.is_var_args;
-
- for (let i = 0; i < fields.length; i += 1) {
- let field = fields[i];
- let fieldNode = getAstNode(field);
- if (fieldNode.docs != null) {
- docCount += 1;
- }
- }
- if (docCount == 0) {
- return;
- }
-
- resizeDomList(domListParams, docCount, "
");
- let domIndex = 0;
-
- for (let i = 0; i < fields.length; i += 1) {
- let field = fields[i];
- let fieldNode = getAstNode(field);
- let docs = fieldNode.docs;
- if (fieldNode.docs == null) {
- continue;
- }
- let docsNonEmpty = docs !== "";
- let divDom = domListParams.children[domIndex];
- domIndex += 1;
-
- let value = typeObj.params[i];
- let preClass = docsNonEmpty ? ' class="fieldHasDocs"' : "";
- let html = "
";
- }
- divDom.innerHTML = html;
- }
- domSectParams.classList.remove("hidden");
- }
-
- function renderNav() {
- let len = curNav.modNames.length + curNav.declNames.length;
- resizeDomList(domListNavAPI, len, '
');
- let list = [];
- let hrefModNames = [];
- let hrefDeclNames = [];
- for (let i = 0; i < curNav.modNames.length; i += 1) {
- hrefModNames.push(curNav.modNames[i]);
- let name = curNav.modNames[i];
- list.push({
- name: name,
- link: navLink(hrefModNames, hrefDeclNames),
- });
- }
- for (let i = 0; i < curNav.declNames.length; i += 1) {
- hrefDeclNames.push(curNav.declNames[i]);
- list.push({
- name: curNav.declNames[i],
- link: navLink(hrefModNames, hrefDeclNames),
- });
- }
-
- for (let i = 0; i < list.length; i += 1) {
- let liDom = domListNavAPI.children[i];
- let aDom = liDom.children[0];
- aDom.textContent = list[i].name;
- aDom.setAttribute("href", list[i].link);
- if (i + 1 == list.length) {
- aDom.classList.add("active");
- } else {
- aDom.classList.remove("active");
- }
- }
-
- }
-
-
- function render404() {
- domStatus.textContent = "404 Not Found";
- domStatus.classList.remove("hidden");
- }
-
- // function renderModList() {
- // const rootMod = zigAnalysis.modules[zigAnalysis.rootMod];
- // let list = [];
- // for (let key in rootMod.table) {
- // let modIndex = rootMod.table[key];
- // if (zigAnalysis.modules[modIndex] == null) continue;
- // if (key == rootMod.name) continue;
- // list.push({
- // name: key,
- // mod: modIndex,
- // });
- // }
-
- // {
- // let aDom = domSectMainMod.children[1].children[0].children[0];
- // aDom.textContent = rootMod.name;
- // aDom.setAttribute("href", navLinkMod(zigAnalysis.rootMod));
- // if (rootMod.name === curNav.modNames[0]) {
- // aDom.classList.add("active");
- // } else {
- // aDom.classList.remove("active");
- // }
- // domSectMainMod.classList.remove("hidden");
- // }
-
- // list.sort(function (a, b) {
- // return operatorCompare(a.name.toLowerCase(), b.name.toLowerCase());
- // });
-
- // if (list.length !== 0) {
- // resizeDomList(domListMods, list.length, '
');
- // for (let i = 0; i < list.length; i += 1) {
- // let liDom = domListMods.children[i];
- // let aDom = liDom.children[0];
- // aDom.textContent = list[i].name;
- // aDom.setAttribute("href", navLinkMod(list[i].mod));
- // if (list[i].name === curNav.modNames[0]) {
- // aDom.classList.add("active");
- // } else {
- // aDom.classList.remove("active");
- // }
- // }
-
- // domSectMods.classList.remove("hidden");
- // }
- // }
-
- function navLink(modNames, declNames, callName) {
- let base = curNav.mode;
-
- if (modNames.length === 0 && declNames.length === 0) {
- return base;
- } else if (declNames.length === 0 && callName == null) {
- return base + modNames.join(".");
- } else if (callName == null) {
- return base + modNames.join(".") + ":" + declNames.join(".");
- } else {
- return (
- base + modNames.join(".") + ":" + declNames.join(".") + ";" + callName
- );
- }
- }
-
- function navLinkMod(modIndex) {
- return navLink(canonModPaths[modIndex], []);
- }
-
- function navLinkDecl(childName) {
- return navLink(curNav.modNames, curNav.declNames.concat([childName]));
- }
-
- function findDeclNavLink(declName) {
- if (curNav.declObjs.length == 0) return null;
- const curFile = getAstNode(curNav.declObjs[curNav.declObjs.length - 1].src).file;
-
- for (let i = curNav.declObjs.length - 1; i >= 0; i--) {
- const curDecl = curNav.declObjs[i];
- const curDeclName = curNav.declNames[i - 1];
- if (curDeclName == declName) {
- const declPath = curNav.declNames.slice(0, i);
- return navLink(curNav.modNames, declPath);
- }
-
- const subDecl = findSubDecl(curDecl, declName);
-
- if (subDecl != null) {
- if (subDecl.is_private === true) {
- return sourceFileLink(subDecl);
- } else {
- const declPath = curNav.declNames.slice(0, i).concat([declName]);
- return navLink(curNav.modNames, declPath);
- }
- }
- }
-
- //throw("could not resolve links for '" + declName + "'");
- }
-
- //
- // function navLinkCall(callObj) {
- // let declNamesCopy = curNav.declNames.concat([]);
- // let callName = (declNamesCopy.pop());
-
- // callName += '(';
- // for (let arg_i = 0; arg_i < callObj.args.length; arg_i += 1) {
- // if (arg_i !== 0) callName += ',';
- // let argObj = callObj.args[arg_i];
- // callName += getValueText(argObj, argObj, false, false);
- // }
- // callName += ')';
-
- // declNamesCopy.push(callName);
- // return navLink(curNav.modNames, declNamesCopy);
- // }
-
- function resizeDomListDl(dlDom, desiredLen) {
- // add the missing dom entries
- for (let i = dlDom.childElementCount / 2; i < desiredLen; i += 1) {
- dlDom.insertAdjacentHTML("beforeend", "
");
- }
- // remove extra dom entries
- while (desiredLen < dlDom.childElementCount / 2) {
- dlDom.removeChild(dlDom.lastChild);
- dlDom.removeChild(dlDom.lastChild);
- }
- }
-
- function resizeDomList(listDom, desiredLen, templateHtml) {
- // add the missing dom entries
- for (let i = listDom.childElementCount; i < desiredLen; i += 1) {
- listDom.insertAdjacentHTML("beforeend", templateHtml);
- }
- // remove extra dom entries
- while (desiredLen < listDom.childElementCount) {
- listDom.removeChild(listDom.lastChild);
- }
- }
-
- function walkResultTypeRef(wr) {
- if (wr.typeRef) return wr.typeRef;
- let resolved = resolveValue(wr);
- if (wr === resolved) {
- return { "undefined": {} };
- }
- return walkResultTypeRef(resolved);
- }
-
- function* DecoratedTokenizer(src, context) {
- let tok_it = Tokenizer(src);
- for (let t of tok_it) {
- if (t.tag == Tag.identifier) {
- const link = detectDeclPath(t.src, context);
- if (link) {
- t.link = link;
- }
- }
-
- yield t;
- }
- }
-
-
- function renderSingleToken(t) {
-
- if (t.tag == Tag.whitespace) {
- return t.src;
- }
-
- let src = t.src;
- // if (t.tag == Tag.identifier) {
- // src = escapeHtml(src);
- // }
- let result = "";
- if (t.tag == Tag.identifier && isSimpleType(t.src)) {
- result = `
`;
- } else if (t.tag == Tag.identifier && isSpecialIndentifier(t.src)) {
- result = `
";
- }
-
- return result;
- }
-
- function renderTokens(tok_it) {
- var html = [];
-
- const max_iter = 100000;
- let i = 0;
- for (const t of tok_it) {
- i += 1;
- if (i > max_iter)
- throw "too many iterations";
-
- if (t.tag == Tag.eof)
- break;
-
- html.push(renderSingleToken(t));
- }
-
- return html.join("");
- }
-
- function* ex(expr, opts) {
- switch (Object.keys(expr)[0]) {
- default:
- throw "this expression is not implemented yet: " + Object.keys(expr)[0];
- case "comptimeExpr": {
- const src = zigAnalysis.comptimeExprs[expr.comptimeExpr].code;
- yield* DecoratedTokenizer(src);
- return;
- }
- case "declName": {
- yield { src: expr.declName, tag: Tag.identifier };
- return;
- }
- case "declRef": {
- const name = getDecl(expr.declRef).name;
- const link = declLinkOrSrcLink(expr.declRef);
- if (link) {
- yield { src: name, tag: Tag.identifier, isDecl: true, link };
- } else {
- yield { src: name, tag: Tag.identifier, isDecl: true };
- }
- return;
- }
- case "refPath": {
- for (let i = 0; i < expr.refPath.length; i += 1) {
- if (i > 0) yield Tok.period;
- yield* ex(expr.refPath[i], opts);
- }
- return;
- }
- case "fieldRef": {
- const field_idx = expr.fieldRef.index;
- const type = getType(expr.fieldRef.type);
- const field = getAstNode(type.src).fields[field_idx];
- const name = getAstNode(field).name;
- yield { src: name, tag: Tag.identifier };
- return;
- }
- case "bool": {
- if (expr.bool) {
- yield { src: "true", tag: Tag.identifier };
- return;
- }
- yield { src: "false", tag: Tag.identifier };
- return;
- }
-
- case "unreachable": {
- yield { src: "unreachable", tag: Tag.identifier };
- return;
- }
-
- case "&": {
- yield { src: "&", tag: Tag.ampersand };
- yield* ex(zigAnalysis.exprs[expr["&"]], opts);
- return;
- }
-
- case "load": {
- yield* ex(zigAnalysis.exprs[expr.load], opts);
- yield Tok.period;
- yield Tok.asterisk;
- return;
- }
-
- case "call": {
-
- let call = zigAnalysis.calls[expr.call];
-
- switch (Object.keys(call.func)[0]) {
- default:
- throw "TODO";
- case "declRef":
- case "refPath": {
- yield* ex(call.func, opts);
- break;
- }
- }
- yield Tok.l_paren;
-
- for (let i = 0; i < call.args.length; i++) {
- if (i != 0) {
- yield Tok.comma;
- yield Tok.space;
- }
- yield* ex(call.args[i], opts);
- }
-
- yield Tok.r_paren;
- return;
- }
- case "typeOf_peer": {
- yield { src: "@TypeOf", tag: Tag.builtin };
- yield { src: "(", tag: Tag.l_paren };
- for (let i = 0; i < expr.typeOf_peer.length; i+=1) {
- const elem = zigAnalysis.exprs[expr.typeOf_peer[i]];
- yield* ex(elem, opts);
- if (i != expr.typeOf_peer.length - 1) {
- yield Tok.comma;
- yield Tok.space;
- }
- }
- yield { src: ")", tag: Tag.r_paren };
- return;
- }
- case "sizeOf": {
- const sizeOf = zigAnalysis.exprs[expr.sizeOf];
- yield { src: "@sizeOf", tag: Tag.builtin };
- yield Tok.l_paren;
- yield* ex(sizeOf, opts);
- yield Tok.r_paren;
- return;
- }
- case "bitSizeOf": {
- const bitSizeOf = zigAnalysis.exprs[expr.bitSizeOf];
- yield { src: "@bitSizeOf", tag: Tag.builtin };
- yield Tok.l_paren;
- yield* ex(bitSizeOf, opts);
- yield Tok.r_paren;
- return;
- }
-
- case "as": {
- const exprArg = zigAnalysis.exprs[expr.as.exprArg];
- yield* ex(exprArg, opts);
- return;
- }
-
- case "int": {
- yield { src: expr.int, tag: Tag.number_literal };
- return;
- }
-
- case "int_big": {
- if (expr.int_big.negated) {
- yield { src: "-", tag: Tag.minus };
- }
- yield { src: expr.int_big.value, tag: Tag.number_literal };
- return;
- }
-
- case "float": {
- let float = expr.float;
- if (Number.isSafeInteger(float)) float = float.toFixed(1);
- yield { src: float, tag: Tag.number_literal };
- return;
- }
-
- case "float128": {
- yield { src: expr.float128, tag: Tag.number_literal };
- return;
- }
-
- case "array": {
- yield Tok.period;
- yield Tok.l_brace;
- for (let i = 0; i < expr.array.length; i++) {
- if (i != 0) {
- yield Tok.comma;
- yield Tok.space;
- }
- let elem = zigAnalysis.exprs[expr.array[i]];
- yield* ex(elem, opts);
- }
- yield Tok.r_brace;
- return;
- }
-
- case "compileError": {
- yield { src: "@compileError", tag: Tag.builtin };
- yield Tok.l_paren;
- yield* ex(zigAnalysis.exprs[expr.compileError], opts);
- yield Tok.r_paren;
- return;
- }
-
- case "optionalPayload": {
- const opt = zigAnalysis.exprs[expr.optionalPayload];
- yield* ex(opt, opts);
- yield Tok.period;
- yield Tok.question_mark;
- return;
- }
-
- case "elemVal": {
- const lhs = zigAnalysis.exprs[expr.elemVal.lhs];
- const rhs = zigAnalysis.exprs[expr.elemVal.rhs];
- yield* ex(lhs);
- yield Tok.l_bracket;
- yield* ex(rhs);
- yield Tok.r_bracket;
- return;
- }
-
- case "sliceIndex": {
- const slice = zigAnalysis.exprs[expr.sliceIndex];
- yield* ex(slice, opts);
- return;
- }
-
- case "slice": {
- const slice = expr.slice;
- const lhs = zigAnalysis.exprs[slice.lhs];
- const start = zigAnalysis.exprs[slice.start];
- yield* ex(lhs, opts);
- yield Tok.l_bracket;
- yield* ex(start, opts);
- yield Tok.period;
- yield Tok.period;
- if (slice.end !== null) {
- const end = zigAnalysis.exprs[slice.end];
- yield* ex(end, opts);
- }
- if (slice.sentinel !== null) {
- yield Tok.colon;
- const sent = zigAnalysis.exprs[slice.sentinel];
- yield* ex(sent, opts);
- }
- yield Tok.r_brace;
- return;
- }
-
- case "sliceLength": {
- const slice = expr.sliceLength;
- const lhs = zigAnalysis.exprs[slice.lhs];
- const start = zigAnalysis.exprs[slice.start];
- const len = zigAnalysis.exprs[slice.len];
- yield* ex(lhs, opts);
- yield Tok.l_bracket;
- yield* ex(start, opts);
- yield Tok.period;
- yield Tok.period;
- yield Tok.r_bracket;
- yield Tok.l_bracket;
- yield { src: "0", tag: Tag.number_literal };
- yield Tok.period;
- yield Tok.period;
- yield* ex(len, opts);
- if (slice.sentinel !== null) {
- yield Tok.colon;
- const sent = zigAnalysis.exprs[slice.sentinel];
- yield* ex(sent, opts);
- }
- yield Tok.r_brace;
- return;
- }
-
- case "string": {
- yield { src: '"' + expr.string + '"', tag: Tag.string_literal };
- return;
- }
-
- case "struct": {
- yield Tok.period;
- yield Tok.l_brace;
- if (expr.struct.length > 0) yield Tok.space;
-
- for (let i = 0; i < expr.struct.length; i++) {
- const fv = expr.struct[i];
- const field_name = fv.name;
- const field_expr = zigAnalysis.exprs[fv.val.expr];
- const field_value = ex(field_expr, opts);
- yield Tok.period;
- yield { src: field_name, tag: Tag.identifier };
- yield Tok.space;
- yield Tok.eql;
- yield Tok.space;
- yield* field_value;
- if (i !== expr.struct.length - 1) {
- yield Tok.comma;
- yield Tok.space;
- } else {
- yield Tok.space;
- }
- }
- yield Tok.r_brace;
- return;
- }
-
- case "unOpIndex": {
- const unOp = zigAnalysis.exprs[expr.unOpIndex];
- yield* ex(unOp, opts);
- return;
- }
-
- case "unOp": {
- const param = zigAnalysis.exprs[expr.unOp.param];
-
- switch (expr.unOp.name) {
- case "bit_not": {
- yield { src: "~", tag: Tag.tilde };
- break;
- }
- case "bool_not": {
- yield { src: "!", tag: Tag.bang };
- break;
- }
- case "negate_wrap": {
- yield { src: "-%", tag: Tag.minus_percent };
- break;
- }
- case "negate": {
- yield { src: "-", tag: Tag.minus };
- break;
- }
- default:
- throw "unOp: `" + expr.unOp.name + "` not implemented yet!"
- }
-
- if (param["binOpIndex"] !== undefined) {
- yield Tok.l_paren;
- yield* ex(param, opts);
- yield Tok.r_paren;
- } else {
- yield* ex(param, opts);
- }
- return;
- }
-
- case "fieldVal": {
- const fv = expr.fieldVal;
- const field_name = fv.name;
- yield { src: field_name, tag: Tag.identifier };
- return;
- }
-
- case "binOpIndex": {
- const binOp = zigAnalysis.exprs[expr.binOpIndex];
- yield* ex(binOp, opts);
- return;
- }
-
- case "binOp": {
- const lhsOp = zigAnalysis.exprs[expr.binOp.lhs];
- const rhsOp = zigAnalysis.exprs[expr.binOp.rhs];
-
- if (lhsOp["binOpIndex"] !== undefined) {
- yield Tok.l_paren;
- yield* ex(lhsOp, opts);
- yield Tok.r_paren;
- } else {
- yield* ex(lhsOp, opts);
- }
-
- yield Tok.space;
-
- switch (expr.binOp.name) {
- case "add": {
- yield { src: "+", tag: Tag.plus };
- break;
- }
- case "addwrap": {
- yield { src: "+%", tag: Tag.plus_percent };
- break;
- }
- case "add_sat": {
- yield { src: "+|", tag: Tag.plus_pipe };
- break;
- }
- case "sub": {
- yield { src: "-", tag: Tag.minus };
- break;
- }
- case "subwrap": {
- yield { src: "-%", tag: Tag.minus_percent };
- break;
- }
- case "sub_sat": {
- yield { src: "-|", tag: Tag.minus_pipe };
- break;
- }
- case "mul": {
- yield { src: "*", tag: Tag.asterisk };
- break;
- }
- case "mulwrap": {
- yield { src: "*%", tag: Tag.asterisk_percent };
- break;
- }
- case "mul_sat": {
- yield { src: "*|", tag: Tag.asterisk_pipe };
- break;
- }
- case "div": {
- yield { src: "/", tag: Tag.slash };
- break;
- }
- case "xor": {
- yield { src: "^", tag: Tag.caret };
- break;
- }
- case "shl": {
- yield { src: "<<", tag: Tag.angle_bracket_angle_bracket_left };
- break;
- }
- case "shl_sat": {
- yield { src: "<<|", tag: Tag.angle_bracket_angle_bracket_left_pipe };
- break;
- }
- case "shr": {
- yield { src: ">>", tag: Tag.angle_bracket_angle_bracket_right };
- break;
- }
- case "bit_or": {
- yield { src: "|", tag: Tag.pipe };
- break;
- }
- case "bit_and": {
- yield { src: "&", tag: Tag.ampersand };
- break;
- }
- case "array_cat": {
- yield { src: "++", tag: Tag.plus_plus };
- break;
- }
- case "array_mul": {
- yield { src: "**", tag: Tag.asterisk_asterisk };
- break;
- }
- case "cmp_eq": {
- yield { src: "==", tag: Tag.equal_equal };
- break;
- }
- case "cmp_neq": {
- yield { src: "!=", tag: Tag.bang_equal };
- break;
- }
- case "cmp_gt": {
- yield { src: ">", tag: Tag.angle_bracket_right };
- break;
- }
- case "cmp_gte": {
- yield { src: ">=", tag: Tag.angle_bracket_right_equal };
- break;
- }
- case "cmp_lt": {
- yield { src: "<", tag: Tag.angle_bracket_left };
- break;
- }
- case "cmp_lte": {
- yield { src: "<=", tag: Tag.angle_bracket_left_equal };
- break;
- }
- case "bool_br_and": {
- yield { src: "and", tag: Tag.keyword_and };
- break;
- }
- case "bool_br_or": {
- yield { src: "or", tag: Tag.keyword_or };
- break;
- }
- default:
- console.log("operator not handled yet or doesn't exist!");
- }
-
- yield Tok.space;
-
- if (rhsOp["binOpIndex"] !== undefined) {
- yield Tok.l_paren;
- yield* ex(rhsOp, opts);
- yield Tok.r_paren;
- } else {
- yield* ex(rhsOp, opts);
- }
- return;
- }
-
- case "builtinIndex": {
- const builtin = zigAnalysis.exprs[expr.builtinIndex];
- yield* ex(builtin, opts);
- return;
- }
-
- case "builtin": {
- const builtin = expr.builtin;
- let name = "@";
- const param = zigAnalysis.exprs[builtin.param];
- switch (builtin.name) {
- case "align_of": { name += "alignOf"; break; }
- case "int_from_bool": { name += "intFromBool"; break; }
- case "embed_file": { name += "embedFile"; break; }
- case "error_name": { name += "errorName"; break; }
- case "panic": { name += "panic"; break; }
- case "set_runtime_safety": { name += "setRuntimeSafety"; break; }
- case "sqrt": { name += "sqrt"; break; }
- case "sin": { name += "sin"; break; }
- case "cos": { name += "cos"; break; }
- case "tan": { name += "tan"; break; }
- case "exp": { name += "exp"; break; }
- case "exp2": { name += "exp2"; break; }
- case "log": { name += "log"; break; }
- case "log2": { name += "log2"; break; }
- case "log10": { name += "log10"; break; }
- case "fabs": { name += "fabs"; break; }
- case "floor": { name += "floor"; break; }
- case "ceil": { name += "ceil"; break; }
- case "trunc": { name += "trunc"; break; }
- case "round": { name += "round"; break; }
- case "tag_name": { name += "tagName"; break; }
- case "type_name": { name += "typeName"; break; }
- case "type_info": { name += "typeInfo"; break; }
- case "frame_type": { name += "Frame"; break; }
- case "frame_size": { name += "frameSize"; break; }
- case "int_from_ptr": { name += "intFromPtr"; break; }
- case "int_from_enum": { name += "intFromEnum"; break; }
- case "clz": { name += "clz"; break; }
- case "ctz": { name += "ctz"; break; }
- case "pop_count": { name += "popCount"; break; }
- case "byte_swap": { name += "byteSwap"; break; }
- case "bit_reverse": { name += "bitReverse"; break; }
- default: throw "builtin: `" + builtin.name + "` not implemented yet!";
- }
- yield { src: name, tag: Tag.builtin };
- yield Tok.l_paren;
- yield* ex(param, opts);
- yield Tok.r_paren;
- return;
- }
-
- case "builtinBinIndex": {
- const builtinBinIndex = zigAnalysis.exprs[expr.builtinBinIndex];
- yield* ex(builtinBinIndex, opts);
- return;
- }
-
- case "builtinBin": {
- const lhsOp = zigAnalysis.exprs[expr.builtinBin.lhs];
- const rhsOp = zigAnalysis.exprs[expr.builtinBin.rhs];
-
- let builtinName = "@";
- switch (expr.builtinBin.name) {
- case "int_from_float": {
- builtinName += "intFromFloat";
- break;
- }
- case "float_from_int": {
- builtinName += "floatFromInt";
- break;
- }
- case "ptr_from_int": {
- builtinName += "ptrFromInt";
- break;
- }
- case "enum_from_int": {
- builtinName += "enumFromInt";
- break;
- }
- case "float_cast": {
- builtinName += "floatCast";
- break;
- }
- case "int_cast": {
- builtinName += "intCast";
- break;
- }
- case "ptr_cast": {
- builtinName += "ptrCast";
- break;
- }
- case "const_cast": {
- builtinName += "constCast";
- break;
- }
- case "volatile_cast": {
- builtinName += "volatileCast";
- break;
- }
- case "truncate": {
- builtinName += "truncate";
- break;
- }
- case "has_decl": {
- builtinName += "hasDecl";
- break;
- }
- case "has_field": {
- builtinName += "hasField";
- break;
- }
- case "bit_reverse": {
- builtinName += "bitReverse";
- break;
- }
- case "div_exact": {
- builtinName += "divExact";
- break;
- }
- case "div_floor": {
- builtinName += "divFloor";
- break;
- }
- case "div_trunc": {
- builtinName += "divTrunc";
- break;
- }
- case "mod": {
- builtinName += "mod";
- break;
- }
- case "rem": {
- builtinName += "rem";
- break;
- }
- case "mod_rem": {
- builtinName += "rem";
- break;
- }
- case "shl_exact": {
- builtinName += "shlExact";
- break;
- }
- case "shr_exact": {
- builtinName += "shrExact";
- break;
- }
- case "bitcast": {
- builtinName += "bitCast";
- break;
- }
- case "align_cast": {
- builtinName += "alignCast";
- break;
- }
- case "vector_type": {
- builtinName += "Vector";
- break;
- }
- case "reduce": {
- builtinName += "reduce";
- break;
- }
- case "splat": {
- builtinName += "splat";
- break;
- }
- case "offset_of": {
- builtinName += "offsetOf";
- break;
- }
- case "bit_offset_of": {
- builtinName += "bitOffsetOf";
- break;
- }
- default:
- console.log("builtin function not handled yet or doesn't exist!");
- }
-
- yield { src: builtinName, tag: Tag.builtin };
- yield Tok.l_paren;
- yield* ex(lhsOp, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(rhsOp, opts);
- yield Tok.r_paren;
- return;
- }
-
- case "unionInit": {
- let ui = expr.unionInit;
- let type = zigAnalysis.exprs[ui.type];
- let field = zigAnalysis.exprs[ui.field];
- let init = zigAnalysis.exprs[ui.init];
- yield { src: "@unionInit", tag: Tag.builtin };
- yield Tok.l_paren;
- yield* ex(type, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(field, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(init, opts);
- yield Tok.r_paren;
- return;
- }
-
- case "builtinCall": {
- let bcall = expr.builtinCall;
- let mods = zigAnalysis.exprs[bcall.modifier];
- let calee = zigAnalysis.exprs[bcall.function];
- let args = zigAnalysis.exprs[bcall.args];
- yield { src: "@call", tag: Tag.builtin };
- yield Tok.l_paren;
- yield* ex(mods, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(calee, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(args, opts);
- yield Tok.r_paren;
- return;
- }
-
- case "mulAdd": {
- let muladd = expr.mulAdd;
- let mul1 = zigAnalysis.exprs[muladd.mulend1];
- let mul2 = zigAnalysis.exprs[muladd.mulend2];
- let add = zigAnalysis.exprs[muladd.addend];
- let type = zigAnalysis.exprs[muladd.type];
- yield { src: "@mulAdd", tag: Tag.builtin };
- yield Tok.l_paren;
- yield* ex(type, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(mul1, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(mul2, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(add, opts);
- yield Tok.r_paren;
- return;
- }
-
- case "cmpxchgIndex": {
- const cmpxchg = zigAnalysis.exprs[expr.cmpxchgIndex];
- yield* ex(cmpxchg, opts);
- return;
- }
-
- case "cmpxchg": {
- const type = zigAnalysis.exprs[expr.cmpxchg.type];
- const ptr = zigAnalysis.exprs[expr.cmpxchg.ptr];
- const expectedValue = zigAnalysis.exprs[expr.cmpxchg.expected_value];
- const newValue = zigAnalysis.exprs[expr.cmpxchg.new_value];
- const successOrder = zigAnalysis.exprs[expr.cmpxchg.success_order];
- const failureOrder = zigAnalysis.exprs[expr.cmpxchg.failure_order];
-
- let fnName = "@";
- switch (expr.cmpxchg.name) {
- case "cmpxchg_strong": {
- fnName += "cmpxchgStrong";
- break;
- }
- case "cmpxchg_weak": {
- fnName += "cmpxchgWeak";
- break;
- }
- default:
- throw "Unexpected cmpxchg name: `" + expr.cmpxchg.name + "`!";
- }
- yield { src: fnName, tag: Tag.builtin };
- yield Tok.l_paren;
- yield* ex(type, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(ptr, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(expectedValue, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(newValue, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(successOrder, opts);
- yield Tok.comma;
- yield Tok.space;
- yield* ex(failureOrder, opts);
- yield Tok.r_paren;
- return;
- }
-
- case "enumLiteral": {
- let literal = expr.enumLiteral;
- yield Tok.period;
- yield { src: literal, tag: Tag.identifier };
- return;
- }
-
- case "void": {
- yield { src: "void", tag: Tag.identifier };
- return;
- }
-
- case "null": {
- yield { src: "null", tag: Tag.identifier };
- return;
- }
-
- case "undefined": {
- yield { src: "undefined", tag: Tag.identifier };
- return;
- }
-
- case "anytype": {
- yield { src: "anytype", tag: Tag.keyword_anytype };
- return;
- }
-
- case "this": {
- yield { src: "@This", tag: Tag.builtin };
- yield Tok.l_paren;
- yield Tok.r_paren;
- return;
- }
-
- case "switchIndex": {
- const switchIndex = zigAnalysis.exprs[expr.switchIndex];
- yield* ex(switchIndex, opts);
- return;
- }
-
- case "errorSets": {
- const errSetsObj = getType(expr.errorSets);
- yield* ex(errSetsObj.lhs, opts);
- yield Tok.space;
- yield { src: "||", tag: Tag.pipe_pipe };
- yield Tok.space;
- yield* ex(errSetsObj.rhs, opts);
- return;
- }
-
- case "errorUnion": {
- const errUnionObj = getType(expr.errorUnion);
- yield* ex(errUnionObj.lhs, opts);
- yield { src: "!", tag: Tag.bang };
- yield* ex(errUnionObj.rhs, opts);
- return;
- }
-
- case "type": {
- let name = "";
-
- let typeObj = expr.type;
- if (typeof typeObj === "number") typeObj = getType(typeObj);
- switch (typeObj.kind) {
- default:
- throw "TODO: " + typeObj.kind;
- case typeKinds.Type: {
- yield { src: typeObj.name, tag: Tag.identifier };
- return;
- }
- case typeKinds.Void: {
- yield { src: "void", tag: Tag.identifier };
- return;
- }
- case typeKinds.NoReturn: {
- yield { src: "noreturn", tag: Tag.identifier };
- return;
- }
- case typeKinds.ComptimeExpr: {
- yield { src: "anyopaque", tag: Tag.identifier };
- return;
- }
- case typeKinds.Bool: {
- yield { src: "bool", tag: Tag.identifier };
- return;
- }
- case typeKinds.ComptimeInt: {
- yield { src: "comptime_int", tag: Tag.identifier };
- return;
- }
- case typeKinds.ComptimeFloat: {
- yield { src: "comptime_float", tag: Tag.identifier };
- return;
- }
- case typeKinds.Int: {
- yield { src: typeObj.name, tag: Tag.identifier };
- return;
- }
- case typeKinds.Float: {
- yield { src: typeObj.name, tag: Tag.identifier };
- return;
- }
- case typeKinds.Array: {
- yield Tok.l_bracket;
- yield* ex(typeObj.len, opts);
- if (typeObj.sentinel) {
- yield Tok.colon;
- yield* ex(typeObj.sentinel, opts);
- }
- yield Tok.r_bracket;
- yield* ex(typeObj.child, opts);
- return;
- }
- case typeKinds.Optional: {
- yield Tok.question_mark;
- yield* ex(typeObj.child, opts);
- return;
- }
- case typeKinds.Pointer: {
- let ptrObj = typeObj;
- switch (ptrObj.size) {
- default:
- console.log("TODO: implement unhandled pointer size case");
- case pointerSizeEnum.One:
- yield { src: "*", tag: Tag.asterisk };
- break;
- case pointerSizeEnum.Many:
- yield Tok.l_bracket;
- yield { src: "*", tag: Tag.asterisk };
- if (ptrObj.sentinel !== null) {
- yield Tok.colon;
- yield* ex(ptrObj.sentinel, opts);
- }
- yield Tok.r_bracket;
- break;
- case pointerSizeEnum.Slice:
- if (ptrObj.is_ref) {
- yield { src: "*", tag: Tag.asterisk };
- }
- yield Tok.l_bracket;
- if (ptrObj.sentinel !== null) {
- yield Tok.colon;
- yield* ex(ptrObj.sentinel, opts);
- }
- yield Tok.r_bracket;
- break;
- case pointerSizeEnum.C:
- yield Tok.l_bracket;
- yield { src: "*", tag: Tag.asterisk };
- yield { src: "c", tag: Tag.identifier };
- if (typeObj.sentinel !== null) {
- yield Tok.colon;
- yield* ex(ptrObj.sentinel, opts);
- }
- yield Tok.r_bracket;
- break;
- }
- if (!ptrObj.is_mutable) {
- yield Tok.const;
- yield Tok.space;
- }
- if (ptrObj.is_allowzero) {
- yield { src: "allowzero", tag: Tag.keyword_allowzero };
- yield Tok.space;
- }
- if (ptrObj.is_volatile) {
- yield { src: "volatile", tag: Tag.keyword_volatile };
- }
- if (ptrObj.has_addrspace) {
- yield { src: "addrspace", tag: Tag.keyword_addrspace };
- yield Tok.l_paren;
- yield Tok.period;
- yield Tok.r_paren;
- }
- if (ptrObj.has_align) {
- yield { src: "align", tag: Tag.keyword_align };
- yield Tok.l_paren;
- yield* ex(ptrObj.align, opts);
- if (ptrObj.hostIntBytes !== undefined && ptrObj.hostIntBytes !== null) {
- yield Tok.colon;
- yield* ex(ptrObj.bitOffsetInHost, opts);
- yield Tok.colon;
- yield* ex(ptrObj.hostIntBytes, opts);
- }
- yield Tok.r_paren;
- yield Tok.space;
- }
- yield* ex(ptrObj.child, opts);
- return;
- }
- case typeKinds.Struct: {
- let structObj = typeObj;
- if (structObj.layout !== null) {
- switch (structObj.layout.enumLiteral) {
- case "Packed": {
- yield { src: "packed", tag: Tag.keyword_packed };
- break;
- }
- case "Extern": {
- yield { src: "extern", tag: Tag.keyword_extern };
- break;
- }
- }
- yield Tok.space;
- }
- yield { src: "struct", tag: Tag.keyword_struct };
- if (structObj.backing_int !== null) {
- yield Tok.l_paren;
- yield* ex(structObj.backing_int, opts);
- yield Tok.r_paren;
- }
- yield Tok.space;
- yield Tok.l_brace;
-
- if (structObj.field_types.length > 1) {
- yield Tok.enter;
- } else {
- yield Tok.space;
- }
-
- let indent = 0;
- if (structObj.field_types.length > 1) {
- indent = 1;
- }
- if (opts.indent && structObj.field_types.length > 1) {
- indent += opts.ident;
- }
-
- let structNode = getAstNode(structObj.src);
- for (let i = 0; i < structObj.field_types.length; i += 1) {
- let fieldNode = getAstNode(structNode.fields[i]);
- let fieldName = fieldNode.name;
-
- for (let j = 0; j < indent; j += 1) {
- yield Tok.tab;
- }
-
- if (!typeObj.is_tuple) {
- yield { src: fieldName, tag: Tag.identifier };
- }
-
- let fieldTypeExpr = structObj.field_types[i];
- if (!typeObj.is_tuple) {
- yield Tok.colon;
- yield Tok.space;
- }
- yield* ex(fieldTypeExpr, { ...opts, indent: indent });
-
- if (structObj.field_defaults[i] !== null) {
- yield Tok.space;
- yield Tok.eql;
- yield Tok.space;
- yield* ex(structObj.field_defaults[i], opts);
- }
-
- if (structObj.field_types.length > 1) {
- yield Tok.comma;
- yield Tok.enter;
- } else {
- yield Tok.space;
- }
- }
- yield Tok.r_brace;
- return;
- }
- case typeKinds.Enum: {
- let enumObj = typeObj;
- yield { src: "enum", tag: Tag.keyword_enum };
- if (enumObj.tag) {
- yield Tok.l_paren;
- yield* ex(enumObj.tag, opts);
- yield Tok.r_paren;
- }
- yield Tok.space;
- yield Tok.l_brace;
-
- let enumNode = getAstNode(enumObj.src);
- let fields_len = enumNode.fields.length;
- if (enumObj.nonexhaustive) {
- fields_len += 1;
- }
-
- if (fields_len > 1) {
- yield Tok.enter;
- } else {
- yield Tok.space;
- }
-
- let indent = 0;
- if (fields_len > 1) {
- indent = 1;
- }
- if (opts.indent) {
- indent += opts.indent;
- }
-
- for (let i = 0; i < enumNode.fields.length; i += 1) {
- let fieldNode = getAstNode(enumNode.fields[i]);
- let fieldName = fieldNode.name;
-
- for (let j = 0; j < indent; j += 1) yield Tok.tab;
- yield { src: fieldName, tag: Tag.identifier };
-
- if (enumObj.values[i] !== null) {
- yield Tok.space;
- yield Tok.eql;
- yield Tok.space;
- yield* ex(enumObj.values[i], opts);
- }
-
- if (fields_len > 1) {
- yield Tok.comma;
- yield Tok.enter;
- }
- }
- if (enumObj.nonexhaustive) {
- for (let j = 0; j < indent; j += 1) yield Tok.tab;
-
- yield { src: "_", tag: Tag.identifier };
-
- if (fields_len > 1) {
- yield Tok.comma;
- yield Tok.enter;
- }
- }
- if (opts.indent) {
- for (let j = 0; j < opts.indent; j += 1) yield Tok.tab;
- }
- yield Tok.r_brace;
- return;
- }
- case typeKinds.Union: {
- let unionObj = typeObj;
- if (unionObj.layout !== null) {
- switch (unionObj.layout.enumLiteral) {
- case "Packed": {
- yield { src: "packed", tag: Tag.keyword_packed };
- break;
- }
- case "Extern": {
- yield { src: "extern", tag: Tag.keyword_extern };
- break;
- }
- }
- yield Tok.space;
- }
- yield { src: "union", tag: Tag.keyword_union };
- if (unionObj.auto_tag) {
- yield Tok.l_paren;
- yield { src: "enum", tag: Tag.keyword_enum };
- if (unionObj.tag) {
- yield Tok.l_paren;
- yield* ex(unionObj.tag, opts);
- yield Tok.r_paren;
- yield Tok.r_paren;
- } else {
- yield Tok.r_paren;
- }
- } else if (unionObj.tag) {
- yield Tok.l_paren;
- yield* ex(unionObj.tag, opts);
- yield Tok.r_paren;
- }
- yield Tok.space;
- yield Tok.l_brace;
- if (unionObj.field_types.length > 1) {
- yield Tok.enter;
- } else {
- yield Tok.space;
- }
- let indent = 0;
- if (unionObj.field_types.length > 1) {
- indent = 1;
- }
- if (opts.indent) {
- indent += opts.indent;
- }
- let unionNode = getAstNode(unionObj.src);
- for (let i = 0; i < unionObj.field_types.length; i += 1) {
- let fieldNode = getAstNode(unionNode.fields[i]);
- let fieldName = fieldNode.name;
- for (let j = 0; j < indent; j += 1) yield Tok.tab;
- yield { src: fieldName, tag: Tag.identifier };
-
- let fieldTypeExpr = unionObj.field_types[i];
- yield Tok.colon;
- yield Tok.space;
-
- yield* ex(fieldTypeExpr, { ...opts, indent: indent });
-
- if (unionObj.field_types.length > 1) {
- yield Tok.comma;
- yield Tok.enter;
- } else {
- yield Tok.space;
- }
- }
- if (opts.indent) {
- for (let j = 0; j < opts.indent; j += 1) yield Tok.tab;
- }
- yield Tok.r_brace;
- return;
- }
- case typeKinds.Opaque: {
- yield { src: "opaque", tag: Tag.keyword_opaque };
- yield Tok.space;
- yield Tok.l_brace;
- yield Tok.r_brace;
- return;
- }
- case typeKinds.EnumLiteral: {
- yield { src: "(enum literal)", tag: Tag.identifier };
- return;
- }
- case typeKinds.ErrorSet: {
- let errSetObj = typeObj;
- if (errSetObj.fields === null) {
- yield { src: "anyerror", tag: Tag.identifier };
- } else if (errSetObj.fields.length == 0) {
- yield { src: "error", tag: Tag.keyword_error };
- yield Tok.l_brace;
- yield Tok.r_brace;
- } else if (errSetObj.fields.length == 1) {
- yield { src: "error", tag: Tag.keyword_error };
- yield Tok.l_brace;
- yield { src: errSetObj.fields[0].name, tag: Tag.identifier };
- yield Tok.r_brace;
- } else {
- yield { src: "error", tag: Tag.keyword_error };
- yield Tok.l_brace;
- yield { src: errSetObj.fields[0].name, tag: Tag.identifier };
- for (let i = 1; i < errSetObj.fields.length; i++) {
- yield Tok.comma;
- yield Tok.space;
- yield { src: errSetObj.fields[i].name, tag: Tag.identifier };
- }
- yield Tok.r_brace;
- }
- return;
- }
- case typeKinds.ErrorUnion: {
- let errUnionObj = typeObj;
- yield* ex(errUnionObj.lhs, opts);
- yield { src: "!", tag: Tag.bang };
- yield* ex(errUnionObj.rhs, opts);
- return;
- }
- case typeKinds.InferredErrorUnion: {
- let errUnionObj = typeObj;
- yield { src: "!", tag: Tag.bang };
- yield* ex(errUnionObj.payload, opts);
- return;
- }
- case typeKinds.Fn: {
- let fnObj = typeObj;
- let fnDecl = opts.fnDecl;
- let linkFnNameDecl = opts.linkFnNameDecl;
- opts.fnDecl = null;
- opts.linkFnNameDecl = null;
- if (opts.addParensIfFnSignature && fnObj.src == 0) {
- yield Tok.l_paren;
- }
- if (fnObj.is_extern) {
- yield { src: "extern", tag: Tag.keyword_extern };
- yield Tok.space;
- } else if (fnObj.has_cc) {
- let cc_expr = zigAnalysis.exprs[fnObj.cc];
- if (cc_expr.enumLiteral === "Inline") {
- yield { src: "inline", tag: Tag.keyword_inline };
- yield Tok.space;
- }
- }
- if (fnObj.has_lib_name) {
- yield { src: '"' + fnObj.lib_name + '"', tag: Tag.string_literal };
- yield Tok.space;
- }
- yield { src: "fn", tag: Tag.keyword_fn };
- yield Tok.space;
- if (fnDecl) {
- if (linkFnNameDecl) {
- yield { src: fnDecl.name, tag: Tag.identifier, link: linkFnNameDecl, fnDecl: false };
- } else {
- yield { src: fnDecl.name, tag: Tag.identifier, fnDecl: true };
- }
- }
- yield Tok.l_paren;
- if (fnObj.params) {
- let fields = null;
- let isVarArgs = false;
- if (fnObj.src != 0) {
- let fnNode = getAstNode(fnObj.src);
- fields = fnNode.fields;
- isVarArgs = fnNode.varArgs;
- }
-
- for (let i = 0; i < fnObj.params.length; i += 1) {
- if (i != 0) {
- yield Tok.comma;
- yield Tok.space;
- }
-
- let value = fnObj.params[i];
- let paramValue = resolveValue({ expr: value });
-
- if (fields != null) {
- let paramNode = getAstNode(fields[i]);
-
- if (paramNode.varArgs) {
- yield Tok.period;
- yield Tok.period;
- yield Tok.period;
- continue;
- }
-
- if (paramNode.noalias) {
- yield { src: "noalias", tag: Tag.keyword_noalias };
- yield Tok.space;
- }
-
- if (paramNode.comptime) {
- yield { src: "comptime", tag: Tag.keyword_comptime };
- yield Tok.space;
- }
-
- let paramName = paramNode.name;
- if (paramName != null) {
- // skip if it matches the type name
- if (!shouldSkipParamName(paramValue, paramName)) {
- if (paramName === "") {
- paramName = "_";
- }
- yield { src: paramName, tag: Tag.identifier };
- yield Tok.colon;
- yield Tok.space;
- }
- }
- }
-
- // TODO: most of this seems redundant
- if (isVarArgs && i === fnObj.params.length - 1) {
- yield Tok.period;
- yield Tok.period;
- yield Tok.period;
- } else if ("alignOf" in value) {
- yield* ex(value, opts);
- } else if ("typeOf" in value) {
- yield* ex(value, opts);
- } else if ("typeOf_peer" in value) {
- yield* ex(value, opts);
- } else if ("declRef" in value) {
- yield* ex(value, opts);
- } else if ("call" in value) {
- yield* ex(value, opts);
- } else if ("refPath" in value) {
- yield* ex(value, opts);
- } else if ("type" in value) {
- yield* ex(value, opts);
- //payloadHtml += '
";
- } else if ("binOpIndex" in value) {
- yield* ex(value, opts);
- } else if ("comptimeExpr" in value) {
- let comptimeExpr =
- zigAnalysis.comptimeExprs[value.comptimeExpr].code;
- yield* Tokenizer(comptimeExpr);
- } else {
- yield { src: "anytype", tag: Tag.keyword_anytype };
- }
- }
- }
-
- yield Tok.r_paren;
- yield Tok.space;
-
- if (fnObj.has_align) {
- let align = zigAnalysis.exprs[fnObj.align];
- yield { src: "align", tag: Tag.keyword_align };
- yield Tok.l_paren;
- yield* ex(align, opts);
- yield Tok.r_paren;
- yield Tok.space;
- }
- if (fnObj.has_cc) {
- let cc = zigAnalysis.exprs[fnObj.cc];
- if (cc) {
- if (cc.enumLiteral !== "Inline") {
- yield { src: "callconv", tag: Tag.keyword_callconv };
- yield Tok.l_paren;
- yield* ex(cc, opts);
- yield Tok.r_paren;
- yield Tok.space;
- }
- }
- }
-
- if (fnObj.is_inferred_error) {
- yield { src: "!", tag: Tag.bang };
- }
- if (fnObj.ret != null) {
- yield* ex(fnObj.ret, {
- ...opts,
- addParensIfFnSignature: true,
- });
- } else {
- yield { src: "anytype", tag: Tag.keyword_anytype };
- }
-
- if (opts.addParensIfFnSignature && fnObj.src == 0) {
- yield Tok.r_paren;
- }
- return;
- }
- }
- }
-
- case "typeOf": {
- const typeRefArg = zigAnalysis.exprs[expr.typeOf];
- yield { src: "@TypeOf", tag: Tag.builtin };
- yield Tok.l_paren;
- yield* ex(typeRefArg, opts);
- yield Tok.r_paren;
- return;
- }
-
- case "builtinField": {
- yield { src: expr.builtinField, tag: Tag.identifier };
- return;
- }
- }
-
-
- }
-
-
-
- function shouldSkipParamName(typeRef, paramName) {
- let resolvedTypeRef = resolveValue({ expr: typeRef });
- if ("type" in resolvedTypeRef) {
- let typeObj = getType(resolvedTypeRef.type);
- if (typeObj.kind === typeKinds.Pointer) {
- let ptrObj = typeObj;
- if (getPtrSize(ptrObj) === pointerSizeEnum.One) {
- const value = resolveValue(ptrObj.child);
- return typeValueName(value, false, true).toLowerCase() === paramName;
- }
- }
- }
- return false;
- }
-
- function getPtrSize(typeObj) {
- return typeObj.size == null ? pointerSizeEnum.One : typeObj.size;
- }
-
- function renderType(typeObj) {
- let name;
- if (
- rootIsStd &&
- typeObj ===
- getType(zigAnalysis.modules[zigAnalysis.rootMod].main)
- ) {
- name = renderSingleToken(Tok.identifier("std"));
- } else {
- name = renderTokens(ex({ type: typeObj }));
- }
- if (name != null && name != "") {
- domHdrName.innerHTML = "
("
- + zigAnalysis.typeKinds[typeObj.kind] + ")";
- domHdrName.classList.remove("hidden");
- }
- if (typeObj.kind == typeKinds.ErrorSet) {
- renderErrorSet(typeObj);
- }
- }
-
- function renderErrorSet(errSetType) {
- if (errSetType.fields == null) {
- domFnErrorsAnyError.classList.remove("hidden");
- } else {
- let errorList = [];
- for (let i = 0; i < errSetType.fields.length; i += 1) {
- let errObj = errSetType.fields[i];
- //let srcObj = zigAnalysis.astNodes[errObj.src];
- errorList.push(errObj);
- }
- errorList.sort(function(a, b) {
- return operatorCompare(a.name.toLowerCase(), b.name.toLowerCase());
- });
-
- resizeDomListDl(domListFnErrors, errorList.length);
- for (let i = 0; i < errorList.length; i += 1) {
- let nameTdDom = domListFnErrors.children[i * 2 + 0];
- let descTdDom = domListFnErrors.children[i * 2 + 1];
- nameTdDom.textContent = errorList[i].name;
- let docs = errorList[i].docs;
- if (docs != null) {
- descTdDom.innerHTML = markdown(docs);
- } else {
- descTdDom.textContent = "";
- }
- }
- domTableFnErrors.classList.remove("hidden");
- }
- domSectFnErrors.classList.remove("hidden");
- }
-
- // function allCompTimeFnCallsHaveTypeResult(typeIndex, value) {
- // let srcIndex = zigAnalysis.fns[value].src;
- // let calls = nodesToCallsMap[srcIndex];
- // if (calls == null) return false;
- // for (let i = 0; i < calls.length; i += 1) {
- // let call = zigAnalysis.calls[calls[i]];
- // if (call.result.type !== typeTypeId) return false;
- // }
- // return true;
- // }
- //
- // function allCompTimeFnCallsResult(calls) {
- // let firstTypeObj = null;
- // let containerObj = {
- // privDecls: [],
- // };
- // for (let callI = 0; callI < calls.length; callI += 1) {
- // let call = zigAnalysis.calls[calls[callI]];
- // if (call.result.type !== typeTypeId) return null;
- // let typeObj = zigAnalysis.types[call.result.value];
- // if (!typeKindIsContainer(typeObj.kind)) return null;
- // if (firstTypeObj == null) {
- // firstTypeObj = typeObj;
- // containerObj.src = typeObj.src;
- // } else if (firstTypeObj.src !== typeObj.src) {
- // return null;
- // }
- //
- // if (containerObj.fields == null) {
- // containerObj.fields = (typeObj.fields || []).concat([]);
- // } else for (let fieldI = 0; fieldI < typeObj.fields.length; fieldI += 1) {
- // let prev = containerObj.fields[fieldI];
- // let next = typeObj.fields[fieldI];
- // if (prev === next) continue;
- // if (typeof(prev) === 'object') {
- // if (prev[next] == null) prev[next] = typeObj;
- // } else {
- // containerObj.fields[fieldI] = {};
- // containerObj.fields[fieldI][prev] = firstTypeObj;
- // containerObj.fields[fieldI][next] = typeObj;
- // }
- // }
- //
- // if (containerObj.pubDecls == null) {
- // containerObj.pubDecls = (typeObj.pubDecls || []).concat([]);
- // } else for (let declI = 0; declI < typeObj.pubDecls.length; declI += 1) {
- // let prev = containerObj.pubDecls[declI];
- // let next = typeObj.pubDecls[declI];
- // if (prev === next) continue;
- // // TODO instead of showing "examples" as the public declarations,
- // // do logic like this:
- // //if (typeof(prev) !== 'object') {
- // // let newDeclId = zigAnalysis.decls.length;
- // // prev = clone(zigAnalysis.decls[prev]);
- // // prev.id = newDeclId;
- // // zigAnalysis.decls.push(prev);
- // // containerObj.pubDecls[declI] = prev;
- // //}
- // //mergeDecls(prev, next, firstTypeObj, typeObj);
- // }
- // }
- // for (let declI = 0; declI < containerObj.pubDecls.length; declI += 1) {
- // let decl = containerObj.pubDecls[declI];
- // if (typeof(decl) === 'object') {
- // containerObj.pubDecls[declI] = containerObj.pubDecls[declI].id;
- // }
- // }
- // return containerObj;
- // }
-
- function renderValue(decl) {
- let resolvedValue = resolveValue(decl.value);
- if (resolvedValue.expr.fieldRef) {
- const declRef = decl.value.expr.refPath[0].declRef;
- const type = getDecl(declRef);
-
- domFnProtoCode.innerHTML = renderTokens(
- (function*() {
- yield Tok.const;
- yield Tok.space;
- yield Tok.identifier(decl.name);
- yield Tok.colon;
- yield Tok.space;
- yield Tok.identifier(type.name);
- yield Tok.space;
- yield Tok.eql;
- yield Tok.space;
- yield* ex(decl.value.expr, {});
- yield Tok.semi;
- })());
- } else if (
- resolvedValue.expr.string !== undefined ||
- resolvedValue.expr.call !== undefined ||
- resolvedValue.expr.comptimeExpr !== undefined
- ) {
- // TODO: we're using the resolved value but
- // not keeping track of how we got there
- // that's important context that should
- // be shown to the user!
- domFnProtoCode.innerHTML = renderTokens(
- (function*() {
- yield Tok.const;
- yield Tok.space;
- yield Tok.identifier(decl.name);
- if (decl.value.typeRef) {
- yield Tok.colon;
- yield Tok.space;
- yield* ex(decl.value.typeRef, {});
- }
- yield Tok.space;
- yield Tok.eql;
- yield Tok.space;
- yield* ex(resolvedValue.expr, {});
- yield Tok.semi;
- })());
- } else if (resolvedValue.expr.compileError) {
- domFnProtoCode.innerHTML = renderTokens(
- (function*() {
- yield Tok.const;
- yield Tok.space;
- yield Tok.identifier(decl.name);
- yield Tok.space;
- yield Tok.eql;
- yield Tok.space;
- yield* ex(decl.value.expr, {});
- yield Tok.semi;
- })());
- } else {
- const parent = getType(decl.parent_container);
- domFnProtoCode.innerHTML = renderTokens(
- (function*() {
- yield Tok.const;
- yield Tok.space;
- yield Tok.identifier(decl.name);
- if (decl.value.typeRef !== null) {
- yield Tok.colon;
- yield Tok.space;
- yield* ex(decl.value.typeRef, {});
- }
- yield Tok.space;
- yield Tok.eql;
- yield Tok.space;
- yield* ex(decl.value.expr, {});
- yield Tok.semi;
- })());
- }
-
- let docs = getAstNode(decl.src).docs;
- if (docs != null) {
- // TODO: it shouldn't just be decl.parent_container, but rather
- // the type that the decl holds (if the value is a type)
- domTldDocs.innerHTML = markdown(docs, decl);
-
- domTldDocs.classList.remove("hidden");
- }
-
- domFnProto.classList.remove("hidden");
- }
-
- function renderVar(decl) {
- let resolvedVar = resolveValue(decl.value);
-
- if (resolvedVar.expr.fieldRef) {
- const declRef = decl.value.expr.refPath[0].declRef;
- const type = getDecl(declRef);
- domFnProtoCode.innerHTML = renderTokens(
- (function*() {
- yield Tok.var;
- yield Tok.space;
- yield Tok.identifier(decl.name);
- yield Tok.colon;
- yield Tok.space;
- yield Tok.identifier(type.name);
- yield Tok.space;
- yield Tok.eql;
- yield Tok.space;
- yield* ex(decl.value.expr, {});
- yield Tok.semi;
- })());
- } else if (
- resolvedVar.expr.string !== undefined ||
- resolvedVar.expr.call !== undefined ||
- resolvedVar.expr.comptimeExpr !== undefined
- ) {
- domFnProtoCode.innerHTML = renderTokens(
- (function*() {
- yield Tok.var;
- yield Tok.space;
- yield Tok.identifier(decl.name);
- if (decl.value.typeRef) {
- yield Tok.colon;
- yield Tok.space;
- yield* ex(decl.value.typeRef, {});
- }
- yield Tok.space;
- yield Tok.eql;
- yield Tok.space;
- yield* ex(decl.value.expr, {});
- yield Tok.semi;
- })());
- } else if (resolvedVar.expr.compileError) {
- domFnProtoCode.innerHTML = renderTokens(
- (function*() {
- yield Tok.var;
- yield Tok.space;
- yield Tok.identifier(decl.name);
- yield Tok.space;
- yield Tok.eql;
- yield Tok.space;
- yield* ex(decl.value.expr, {});
- yield Tok.semi;
- })());
- } else {
- domFnProtoCode.innerHTML = renderTokens(
- (function*() {
- yield Tok.var;
- yield Tok.space;
- yield Tok.identifier(decl.name);
- yield Tok.colon;
- yield Tok.space;
- yield* ex(resolvedVar.typeRef, {});
- yield Tok.space;
- yield Tok.eql;
- yield Tok.space;
- yield* ex(decl.value.expr, {});
- yield Tok.semi;
- })());
- }
-
- let docs = getAstNode(decl.src).docs;
- if (docs != null) {
- domTldDocs.innerHTML = markdown(docs);
- domTldDocs.classList.remove("hidden");
- }
-
- domFnProto.classList.remove("hidden");
- }
-
- function categorizeDecls(
- decls,
- typesList,
- namespacesWithDocsList,
- namespacesNoDocsList,
- errSetsList,
- fnsList,
- varsList,
- valsList,
- testsList,
- unsList
- ) {
- for (let i = 0; i < decls.length; i += 1) {
- let decl = getDecl(decls[i]);
- let declValue = resolveValue(decl.value);
-
- // if (decl.isTest) {
- // testsList.push(decl);
- // continue;
- // }
-
- if (decl.kind === "var") {
- varsList.push(decl);
- continue;
- }
-
- if (decl.kind === "const") {
- if ("type" in declValue.expr) {
- // We have the actual type expression at hand.
- const typeExpr = getType(declValue.expr.type);
- if (typeExpr.kind == typeKinds.Fn) {
- const funcRetExpr = resolveValue({
- expr: typeExpr.ret,
- });
- if (
- "type" in funcRetExpr.expr &&
- funcRetExpr.expr.type == typeTypeId
- ) {
- if (typeIsErrSet(declValue.expr.type)) {
- errSetsList.push(decl);
- } else if (typeIsStructWithNoFields(declValue.expr.type)) {
-
- let docs = getAstNode(decl.src).docs;
- if (!docs) {
- // If this is a re-export, try to fetch docs from the actual definition
- const { value, seenDecls } = resolveValue(decl.value, true);
- if (seenDecls.length > 0) {
- const definitionDecl = getDecl(seenDecls[seenDecls.length - 1]);
- docs = getAstNode(definitionDecl.src).docs;
- } else {
- docs = getAstNode(getType(value.expr.type).src).docs;
- }
- }
-
- if (docs) {
- namespacesWithDocsList.push({decl, docs});
- } else {
- namespacesNoDocsList.push(decl);
- }
- } else {
- typesList.push(decl);
- }
- } else {
- fnsList.push(decl);
- }
- } else {
- if (typeIsErrSet(declValue.expr.type)) {
- errSetsList.push(decl);
- } else if (typeIsStructWithNoFields(declValue.expr.type)) {
- let docs = getAstNode(decl.src).docs;
- if (!docs) {
- // If this is a re-export, try to fetch docs from the actual definition
- const { value, seenDecls } = resolveValue(decl.value, true);
- if (seenDecls.length > 0) {
- const definitionDecl = getDecl(seenDecls[seenDecls.length - 1]);
- docs = getAstNode(definitionDecl.src).docs;
- } else {
- docs = getAstNode(getType(value.expr.type).src).docs;
- }
- }
- if (docs) {
- namespacesWithDocsList.push({decl, docs});
- } else {
- namespacesNoDocsList.push(decl);
- }
- } else {
- typesList.push(decl);
- }
- }
- } else if (declValue.typeRef) {
- if ("type" in declValue.typeRef && declValue.typeRef == typeTypeId) {
- // We don't know what the type expression is, but we know it's a type.
- typesList.push(decl);
- } else {
- valsList.push(decl);
- }
- } else {
- valsList.push(decl);
- }
- }
-
- if (decl.is_uns) {
- unsList.push(decl);
- }
- }
- }
-
- function sourceFileLink(decl) {
- const srcNode = getAstNode(decl.src);
- const srcFile = getFile(srcNode.file);
- return sourceFileUrlTemplate.
- replace("{{mod}}", zigAnalysis.modules[srcFile.modIndex].name).
- replace("{{file}}", srcFile.name).
- replace("{{line}}", srcNode.line + 1);
- }
-
- function renderContainer(container) {
- let typesList = [];
-
- let namespacesWithDocsList = [];
- let namespacesNoDocsList = [];
-
- let errSetsList = [];
-
- let fnsList = [];
-
- let varsList = [];
-
- let valsList = [];
-
- let testsList = [];
-
- let unsList = [];
-
- categorizeDecls(
- container.pubDecls,
- typesList,
- namespacesWithDocsList,
- namespacesNoDocsList,
- errSetsList,
- fnsList,
- varsList,
- valsList,
- testsList,
- unsList
- );
- if (curNav.showPrivDecls)
- categorizeDecls(
- container.privDecls,
- typesList,
- namespacesWithDocsList,
- namespacesNoDocsList,
- errSetsList,
- fnsList,
- varsList,
- valsList,
- testsList,
- unsList
- );
-
- while (unsList.length > 0) {
- let uns = unsList.shift();
- let declValue = resolveValue(uns.value);
- if (!("type" in declValue.expr)) continue;
- let uns_container = getType(declValue.expr.type);
- if (!isContainerType(uns_container)) continue;
- categorizeDecls(
- uns_container.pubDecls,
- typesList,
- namespacesWithDocsList,
- namespacesNoDocsList,
- errSetsList,
- fnsList,
- varsList,
- valsList,
- testsList,
- unsList
- );
- if (curNav.showPrivDecls)
- categorizeDecls(
- uns_container.privDecls,
- typesList,
- namespacesWithDocsList,
- namespacesNoDocsList,
- errSetsList,
- fnsList,
- varsList,
- valsList,
- testsList,
- unsList
- );
- }
-
- typesList.sort(byNameProperty);
- namespacesWithDocsList.sort(byNameProperty);
- namespacesNoDocsList.sort(byNameProperty);
- errSetsList.sort(byNameProperty);
- fnsList.sort(byNameProperty);
- varsList.sort(byNameProperty);
- valsList.sort(byNameProperty);
- testsList.sort(byNameProperty);
-
- if (container.src != null) {
- let docs = getAstNode(container.src).docs;
- if (docs != null) {
- domTldDocs.innerHTML = markdown(docs, container);
- domTldDocs.classList.remove("hidden");
- }
- }
-
- if (typesList.length !== 0) {
- const splitPoint = Math.ceil(typesList.length / 2);
- const template = '
';
- resizeDomList(domListTypesLeft, splitPoint, template);
- resizeDomList(domListTypesRight, typesList.length - splitPoint, template);
-
- let activeList = domListTypesLeft;
- let offset = 0;
- for (let i = 0; i < typesList.length; i += 1) {
- let liDom = activeList.children[i - offset];
- let aDom = liDom.children[0];
- let decl = typesList[i];
- aDom.textContent = decl.name;
- aDom.setAttribute("href", navLinkDecl(decl.name));
-
- let descDom = liDom.children[1];
- let docs = getAstNode(decl.src).docs;
- if (!docs) {
- // If this is a re-export, try to fetch docs from the actual definition
- const { value, seenDecls } = resolveValue(decl.value, true);
- if (seenDecls.length > 0) {
- const definitionDecl = getDecl(seenDecls[seenDecls.length - 1]);
- docs = getAstNode(definitionDecl.src).docs;
- } else {
- const type = getType(value.expr.type);
- if ("src" in type) {
- docs = getAstNode(type.src).docs;
- }
- }
- }
-
- if (docs) {
- descDom.innerHTML = markdown(shortDesc(docs));
- } else {
- descDom.innerHTML = "
";
- }
- if (i == splitPoint - 1) {
- activeList = domListTypesRight;
- offset = splitPoint;
- }
- }
- domSectTypes.classList.remove("hidden");
- }
-
- if (namespacesWithDocsList.length !== 0) {
- const splitPoint = Math.ceil(namespacesWithDocsList.length / 2);
- const template = '
';
- resizeDomList(domListNamespacesLeft, splitPoint, template);
- resizeDomList(domListNamespacesRight,
- namespacesWithDocsList.length - splitPoint,
- template);
-
- let activeList = domListNamespacesLeft;
- let offset = 0;
- for (let i = 0; i < namespacesWithDocsList.length; i += 1) {
- let liDom = activeList.children[i - offset];
- let aDom = liDom.children[0];
- let { decl, docs } = namespacesWithDocsList[i];
- aDom.textContent = decl.name;
- aDom.setAttribute("href", navLinkDecl(decl.name));
-
-
- let descDom = liDom.children[1];
- descDom.innerHTML = markdown(shortDesc(docs));
- if (i == splitPoint - 1) {
- activeList = domListNamespacesRight;
- offset = splitPoint;
- }
- }
-
- domListNamespacesLeft.classList.remove("hidden");
- domListNamespacesRight.classList.remove("hidden");
- domSectNamespaces.classList.remove("hidden");
- }
-
- if (namespacesNoDocsList.length !== 0) {
- resizeDomList(
- domNoDocsNamespaces,
- namespacesNoDocsList.length,
- '
'
- );
- for (let i = 0; i < namespacesNoDocsList.length; i += 1) {
- let aDom = domNoDocsNamespaces.children[i].children[0];
- let decl = namespacesNoDocsList[i];
- aDom.textContent = decl.name;
- aDom.setAttribute("href", navLinkDecl(decl.name));
- let comma = domNoDocsNamespaces.children[i].children[1];
- if (i == namespacesNoDocsList.length - 1) {
- comma.textContent = "";
- } else {
- comma.textContent = ", ";
- }
- }
-
- domNoDocsNamespaces.classList.remove("hidden");
- domSectNamespaces.classList.remove("hidden");
- }
-
-
-
-
- if (errSetsList.length !== 0) {
- resizeDomList(
- domListErrSets,
- errSetsList.length,
- '
'
- );
- for (let i = 0; i < errSetsList.length; i += 1) {
- let liDom = domListErrSets.children[i];
- let aDom = liDom.children[0];
- let decl = errSetsList[i];
- aDom.textContent = decl.name;
- aDom.setAttribute("href", navLinkDecl(decl.name));
- }
- domSectErrSets.classList.remove("hidden");
- }
-
- if (fnsList.length !== 0) {
- resizeDomList(
- domListFns,
- fnsList.length,
- '
'
- );
-
- for (let i = 0; i < fnsList.length; i += 1) {
- let decl = fnsList[i];
- let trDom = domListFns.children[i];
-
- let tdFnSignature = trDom.children[0].children[0];
- let tdFnSrc = trDom.children[0].children[1];
- let tdDesc = trDom.children[1];
-
- let declType = resolveValue(decl.value);
- console.assert("type" in declType.expr);
- tdFnSignature.innerHTML = renderTokens(ex(declType.expr, {
- fnDecl: decl,
- linkFnNameDecl: navLinkDecl(decl.name),
- }));
- tdFnSrc.innerHTML = "
";
-
- let docs = getAstNode(decl.src).docs;
- if (docs != null) {
- docs = docs.trim();
- var short = shortDesc(docs);
- if (short != docs) {
- short = markdown(short, container);
- var long = markdown(docs, container); // TODO: this needs to be the file top lvl struct
- tdDesc.innerHTML =
- "
" + short + "
" + "
" + long + "
";
- }
- else {
- tdDesc.innerHTML = markdown(short, container);
- }
- } else {
- tdDesc.innerHTML = "
No documentation provided.
";
- }
- }
- domSectFns.classList.remove("hidden");
- }
-
- let containerNode = getAstNode(container.src);
- if (containerNode.fields && containerNode.fields.length > 0) {
- resizeDomList(domListFields, containerNode.fields.length, "
");
-
- for (let i = 0; i < containerNode.fields.length; i += 1) {
- let fieldNode = getAstNode(containerNode.fields[i]);
- let divDom = domListFields.children[i];
- let fieldName = fieldNode.name;
- let docs = fieldNode.docs;
- let docsNonEmpty = docs != null && docs !== "";
- let extraPreClass = docsNonEmpty ? " fieldHasDocs" : "";
-
- let html =
- '
";
-
- if (docsNonEmpty) {
- html += '
' + markdown(docs) + "
";
- }
- divDom.innerHTML = html;
- }
- domSectFields.classList.remove("hidden");
- }
-
- if (varsList.length !== 0) {
- resizeDomList(
- domListGlobalVars,
- varsList.length,
- '
| | |
'
- );
- for (let i = 0; i < varsList.length; i += 1) {
- let decl = varsList[i];
- let trDom = domListGlobalVars.children[i];
-
- let tdName = trDom.children[0];
- let tdNameA = tdName.children[0];
- let tdType = trDom.children[1];
- let preType = tdType.children[0];
- let tdDesc = trDom.children[2];
-
- tdNameA.setAttribute("href", navLinkDecl(decl.name));
- tdNameA.textContent = decl.name;
-
- preType.innerHTML = renderTokens(ex(walkResultTypeRef(decl.value), {}));
-
- let docs = getAstNode(decl.src).docs;
- if (docs != null) {
- tdDesc.innerHTML = shortDescMarkdown(docs);
- } else {
- tdDesc.textContent = "";
- }
- }
- domSectGlobalVars.classList.remove("hidden");
- }
-
- if (valsList.length !== 0) {
- resizeDomList(
- domListValues,
- valsList.length,
- '
| | |
'
- );
- for (let i = 0; i < valsList.length; i += 1) {
- let decl = valsList[i];
- let trDom = domListValues.children[i];
-
- let tdName = trDom.children[0];
- let tdNameA = tdName.children[0];
- let tdType = trDom.children[1];
- let preType = tdType.children[0];
- let tdDesc = trDom.children[2];
-
- tdNameA.setAttribute("href", navLinkDecl(decl.name));
- tdNameA.textContent = decl.name;
-
- preType.innerHTML = renderTokens(ex(walkResultTypeRef(decl.value), {}));
-
- let docs = getAstNode(decl.src).docs;
- if (docs != null) {
- tdDesc.innerHTML = shortDescMarkdown(docs);
- } else {
- tdDesc.textContent = "";
- }
- }
- domSectValues.classList.remove("hidden");
- }
-
- if (testsList.length !== 0) {
- resizeDomList(
- domListTests,
- testsList.length,
- '
| | |
'
- );
- for (let i = 0; i < testsList.length; i += 1) {
- let decl = testsList[i];
- let trDom = domListTests.children[i];
-
- let tdName = trDom.children[0];
- let tdNamePre = tdName.children[0];
- let tdType = trDom.children[1];
- let tdTypePre = tdType.children[0];
- let tdDesc = trDom.children[2];
-
- tdNamePre.innerHTML = renderSingleToken(Tok.identifier(decl.name));
-
- tdTypePre.innerHTML = ex(walkResultTypeRef(decl.value), {});
-
- let docs = getAstNode(decl.src).docs;
- if (docs != null) {
- tdDesc.innerHTML = shortDescMarkdown(docs);
- } else {
- tdDesc.textContent = "";
- }
- }
- domSectTests.classList.remove("hidden");
- }
-
- if (container.kind !== typeKinds.Struct || containerNode.fields.length > 0) {
- domHdrName.innerHTML = "
" +
- zigAnalysis.typeKinds[container.kind] +
- "
";
- domHdrName.classList.remove("hidden");
- }
- }
-
- function operatorCompare(a, b) {
- if (a === b) {
- return 0;
- } else if (a < b) {
- return -1;
- } else {
- return 1;
- }
- }
-
- function detectRootIsStd() {
- let rootMod = zigAnalysis.modules[zigAnalysis.rootMod];
- if (rootMod.table["std"] == null) {
- // no std mapped into the root module
- return false;
- }
- let stdMod = zigAnalysis.modules[rootMod.table["std"]];
- if (stdMod == null) return false;
- return rootMod.file === stdMod.file;
- }
-
- function indexTypeKinds() {
- let map = {};
- for (let i = 0; i < zigAnalysis.typeKinds.length; i += 1) {
- map[zigAnalysis.typeKinds[i]] = i;
- }
- // This is just for debugging purposes, not needed to function
- let assertList = [
- "Type",
- "Void",
- "Bool",
- "NoReturn",
- "Int",
- "Float",
- "Pointer",
- "Array",
- "Struct",
- "ComptimeFloat",
- "ComptimeInt",
- "Undefined",
- "Null",
- "Optional",
- "ErrorUnion",
- "ErrorSet",
- "Enum",
- "Union",
- "Fn",
- "Opaque",
- "Frame",
- "AnyFrame",
- "Vector",
- "EnumLiteral",
- ];
- for (let i = 0; i < assertList.length; i += 1) {
- if (map[assertList[i]] == null)
- throw new Error("No type kind '" + assertList[i] + "' found");
- }
- return map;
- }
-
- function findTypeTypeId() {
- for (let i = 0; i < zigAnalysis.types.length; i += 1) {
- if (getType(i).kind == typeKinds.Type) {
- return i;
- }
- }
- throw new Error("No type 'type' found");
- }
-
-
- function updateCurNav() {
- curNav = {
- hash: location.hash,
- mode: NAV_MODES.API,
- modNames: [],
- modObjs: [],
- declNames: [],
- declObjs: [],
- callName: null,
- activeGuide: null,
- activeGuideScrollTo: null,
+ const CAT_namespace = 0;
+ const CAT_global_variable = 1;
+ const CAT_function = 2;
+ const CAT_primitive = 3;
+ const CAT_error_set = 4;
+ const CAT_global_const = 5;
+ const CAT_alias = 6;
+ const CAT_type = 7;
+
+ const domDocTestsCode = document.getElementById("docTestsCode");
+ const domFnErrorsAnyError = document.getElementById("fnErrorsAnyError");
+ const domFnProto = document.getElementById("fnProto");
+ const domFnProtoCode = document.getElementById("fnProtoCode");
+ const domHdrName = document.getElementById("hdrName");
+ const domHelpModal = document.getElementById("helpDialog");
+ const domListErrSets = document.getElementById("listErrSets");
+ const domListFields = document.getElementById("listFields");
+ const domListFnErrors = document.getElementById("listFnErrors");
+ const domListFns = document.getElementById("listFns");
+ const domListGlobalVars = document.getElementById("listGlobalVars");
+ const domListInfo = document.getElementById("listInfo");
+ const domListNamespaces = document.getElementById("listNamespaces");
+ const domListNav = document.getElementById("listNav");
+ const domListSearchResults = document.getElementById("listSearchResults");
+ const domListTypes = document.getElementById("listTypes");
+ const domListValues = document.getElementById("listValues");
+ const domSearch = document.getElementById("search");
+ const domSectDocTests = document.getElementById("sectDocTests");
+ const domSectErrSets = document.getElementById("sectErrSets");
+ const domSectFields = document.getElementById("sectFields");
+ const domSectFnErrors = document.getElementById("sectFnErrors");
+ const domSectFns = document.getElementById("sectFns");
+ const domSectGlobalVars = document.getElementById("sectGlobalVars");
+ const domSectNamespaces = document.getElementById("sectNamespaces");
+ const domSectNav = document.getElementById("sectNav");
+ const domSectSearchNoResults = document.getElementById("sectSearchNoResults");
+ const domSectSearchResults = document.getElementById("sectSearchResults");
+ const domSectSource = document.getElementById("sectSource");
+ const domSectTypes = document.getElementById("sectTypes");
+ const domSectValues = document.getElementById("sectValues");
+ const domSourceText = document.getElementById("sourceText");
+ const domStatus = document.getElementById("status");
+ const domTableFnErrors = document.getElementById("tableFnErrors");
+ const domTldDocs = document.getElementById("tldDocs");
+
+ var searchTimer = null;
+
+ const curNav = {
+ // 0 = home
+ // 1 = decl (decl)
+ // 2 = source (path)
+ tag: 0,
+ // unsigned int: decl index
+ decl: null,
+ // string file name matching tarball path
+ path: null,
+
+ // when this is populated, pressing the "view source" command will
+ // navigate to this hash.
+ viewSourceHash: null,
};
- curNavSearch = "";
-
- const mode = location.hash.substring(0, 3);
- let query = location.hash.substring(3);
-
- let qpos = query.indexOf("?");
- let nonSearchPart;
- if (qpos === -1) {
- nonSearchPart = query;
- } else {
- nonSearchPart = query.substring(0, qpos);
- curNavSearch = decodeURIComponent(query.substring(qpos + 1));
- }
-
- const DEFAULT_HASH = NAV_MODES.API + zigAnalysis.modules[zigAnalysis.rootMod].name;
- switch (mode) {
- case NAV_MODES.API:
- // #A;MODULE:decl.decl.decl?search-term
- curNav.mode = mode;
- {
- let parts = nonSearchPart.split(":");
- if (parts[0] == "") {
- location.hash = DEFAULT_HASH;
- } else {
- curNav.modNames = decodeURIComponent(parts[0]).split(".");
- }
+ var curNavSearch = "";
+ var curSearchIndex = -1;
+ var imFeelingLucky = false;
- if (parts[1] != null) {
- curNav.declNames = decodeURIComponent(parts[1]).split(".");
- }
- }
- return;
- case NAV_MODES.GUIDES:
- curNav.mode = mode;
-
- {
- let parts = nonSearchPart.split(":");
- curNav.activeGuide = parts[0];
- if (parts[1] != null) {
- curNav.activeGuideScrollTo = decodeURIComponent(":" + parts[1]);
- }
- }
- return;
- default:
- location.hash = DEFAULT_HASH;
- return;
- }
- }
+ // names of packages in the same order as wasm
+ const packageList = [];
- function onHashChange(ev) {
- scrollHistory[curNav.hash] = scrollMonitor.map(function (x) {
- return [x, x.scrollTop]
+ let wasm_promise = fetch("main.wasm");
+ let sources_promise = fetch("sources.tar").then(function(response) {
+ if (!response.ok) throw new Error("unable to download sources");
+ return response.arrayBuffer();
+ });
+ var wasm_exports = null;
+
+ const text_decoder = new TextDecoder();
+ const text_encoder = new TextEncoder();
+
+ WebAssembly.instantiateStreaming(wasm_promise, {
+ js: {
+ log: function(ptr, len) {
+ const msg = decodeString(ptr, len);
+ console.log(msg);
+ },
+ panic: function (ptr, len) {
+ const msg = decodeString(ptr, len);
+ throw new Error("panic: " + msg);
+ },
+ },
+ }).then(function(obj) {
+ wasm_exports = obj.instance.exports;
+ window.wasm = obj; // for debugging
+
+ sources_promise.then(function(buffer) {
+ const js_array = new Uint8Array(buffer);
+ const ptr = wasm_exports.alloc(js_array.length);
+ const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
+ wasm_array.set(js_array);
+ wasm_exports.unpack(ptr, js_array.length);
+
+ updatePackageList();
+
+ window.addEventListener('hashchange', onHashChange, false);
+ domSearch.addEventListener('keydown', onSearchKeyDown, false);
+ domSearch.addEventListener('input', onSearchChange, false);
+ window.addEventListener('keydown', onWindowKeyDown, false);
+ onHashChange();
+ });
});
-
- if (skipNextHashChange == decodeURIComponent(location.hash)) {
- skipNextHashChange = null;
- return;
+
+ function renderTitle() {
+ const suffix = " - Zig Documentation";
+ if (curNavSearch.length > 0) {
+ document.title = curNavSearch + " - Search" + suffix;
+ } else if (curNav.decl != null) {
+ document.title = fullyQualifiedName(curNav.decl) + suffix;
+ } else if (curNav.path != null) {
+ document.title = curNav.path + suffix;
+ } else {
+ document.title = packageList[0] + suffix; // Home
+ }
}
- skipNextHashChange = null;
- updateCurNav();
-
- if (domSearch.value !== curNavSearch) {
- domSearch.value = curNavSearch;
- if (domSearch.value.length == 0)
- domSearchPlaceholder.classList.remove("hidden");
- else
- domSearchPlaceholder.classList.add("hidden");
+
+ function render() {
+ domFnErrorsAnyError.classList.add("hidden");
+ domFnProto.classList.add("hidden");
+ domHdrName.classList.add("hidden");
+ domHelpModal.classList.add("hidden");
+ domSectErrSets.classList.add("hidden");
+ domSectDocTests.classList.add("hidden");
+ domSectFields.classList.add("hidden");
+ domSectFnErrors.classList.add("hidden");
+ domSectFns.classList.add("hidden");
+ domSectGlobalVars.classList.add("hidden");
+ domSectNamespaces.classList.add("hidden");
+ domSectNav.classList.add("hidden");
+ domSectSearchNoResults.classList.add("hidden");
+ domSectSearchResults.classList.add("hidden");
+ domSectSource.classList.add("hidden");
+ domSectTypes.classList.add("hidden");
+ domSectValues.classList.add("hidden");
+ domStatus.classList.add("hidden");
+ domTableFnErrors.classList.add("hidden");
+ domTldDocs.classList.add("hidden");
+
+ renderTitle();
+
+ if (curNavSearch !== "") return renderSearch();
+
+ switch (curNav.tag) {
+ case 0: return renderHome();
+ case 1:
+ if (curNav.decl == null) {
+ return render404();
+ } else {
+ return renderDecl(curNav.decl);
+ }
+ case 2: return renderSource(curNav.path);
+ default: throw new Error("invalid navigation state");
+ }
}
- render();
- if (imFeelingLucky) {
- imFeelingLucky = false;
- activateSelectedResult();
+
+ function renderHome() {
+ if (packageList.length == 1) return renderPackage(0);
+
+ domStatus.textContent = "TODO implement renderHome for multiple packages";
+ domStatus.classList.remove("hidden");
}
- scroll();
- }
+ function renderPackage(pkg_index) {
+ const root_decl = wasm_exports.find_package_root(pkg_index);
+ return renderDecl(root_decl);
+ }
- function scroll() {
- const cur = scrollHistory[location.hash];
- if (cur) {
- for (let [elem, offset] of cur) {
- elem.scrollTo(0, offset);
- }
- } else {
- if (curNav.activeGuideScrollTo) return;
- for (let elem of scrollMonitor) {
- elem.scrollTo(0, 0);
+ function renderDecl(decl_index) {
+ const category = wasm_exports.categorize_decl(decl_index, 0);
+ switch (category) {
+ case CAT_namespace: return renderNamespace(decl_index);
+ case CAT_global_variable: throw new Error("TODO: CAT_GLOBAL_VARIABLE");
+ case CAT_function: return renderFunction(decl_index);
+ case CAT_primitive: throw new Error("TODO CAT_primitive");
+ case CAT_error_set: throw new Error("TODO CAT_error_set");
+ case CAT_global_const: return renderGlobalConst(decl_index);
+ case CAT_alias: return renderDecl(wasm_exports.get_aliasee());
+ case CAT_type: throw new Error("TODO CAT_type");
+ default: throw new Error("unrecognized category " + category);
}
}
- }
-
- function findSubDecl(parentTypeOrDecl, childName) {
- let parentType = parentTypeOrDecl;
- {
- // Generic functions / resolving decls
- if ("value" in parentType) {
- const rv = resolveValue(parentType.value);
- if ("type" in rv.expr) {
- const t = getType(rv.expr.type);
- parentType = t;
- if (t.kind == typeKinds.Fn && t.generic_ret != null) {
- let resolvedGenericRet = resolveValue({ expr: t.generic_ret });
-
- if ("call" in resolvedGenericRet.expr) {
- let call = zigAnalysis.calls[resolvedGenericRet.expr.call];
- let resolvedFunc = resolveValue({ expr: call.func });
- if (!("type" in resolvedFunc.expr)) return null;
- let callee = getType(resolvedFunc.expr.type);
- if (!callee.generic_ret) return null;
- resolvedGenericRet = resolveValue({ expr: callee.generic_ret });
- }
- if ("type" in resolvedGenericRet.expr) {
- parentType = getType(resolvedGenericRet.expr.type);
- }
- }
- }
- }
+ function renderSource(path) {
+ const decl_index = findFileRoot(path);
+ if (decl_index == null) return render404();
+
+ renderNav(decl_index);
+
+ domSourceText.innerHTML = declSourceHtml(decl_index);
+
+ domSectSource.classList.remove("hidden");
}
- if (parentType.pubDecls) {
- for (let i = 0; i < parentType.pubDecls.length; i += 1) {
- let declIndex = parentType.pubDecls[i];
- let childDecl = getDecl(declIndex);
- if (childDecl.name === childName) {
- childDecl.find_subdecl_idx = declIndex;
- return childDecl;
- } else if (childDecl.is_uns) {
- let declValue = resolveValue(childDecl.value);
- if (!("type" in declValue.expr)) continue;
- let uns_container = getType(declValue.expr.type);
- let uns_res = findSubDecl(uns_container, childName);
- if (uns_res !== null) return uns_res;
- }
- }
+ function renderDeclHeading(decl_index) {
+ domHdrName.innerText = unwrapString(wasm_exports.decl_category_name(decl_index));
+ domHdrName.classList.remove("hidden");
+
+ renderTopLevelDocs(decl_index);
}
- if (parentType.privDecls) {
- for (let i = 0; i < parentType.privDecls.length; i += 1) {
- let declIndex = parentType.privDecls[i];
- let childDecl = getDecl(declIndex);
- if (childDecl.name === childName) {
- childDecl.find_subdecl_idx = declIndex;
- childDecl.is_private = true;
- return childDecl;
- } else if (childDecl.is_uns) {
- let declValue = resolveValue(childDecl.value);
- if (!("type" in declValue.expr)) continue;
- let uns_container = getType(declValue.expr.type);
- let uns_res = findSubDecl(uns_container, childName);
- uns_res.is_private = true;
- if (uns_res !== null) return uns_res;
- }
+ function renderTopLevelDocs(decl_index) {
+ const tld_docs_html = unwrapString(wasm_exports.decl_docs_html(decl_index, false));
+ if (tld_docs_html.length > 0) {
+ domTldDocs.innerHTML = tld_docs_html;
+ domTldDocs.classList.remove("hidden");
}
}
- return null;
- }
-
- function computeCanonicalModulePaths() {
- let list = new Array(zigAnalysis.modules.length);
- // Now we try to find all the modules from root.
- let rootMod = zigAnalysis.modules[zigAnalysis.rootMod];
- // Breadth-first to keep the path shortest possible.
- let stack = [
+ function renderNav(cur_nav_decl) {
+ const list = [];
{
- path: [],
- mod: rootMod,
- },
- ];
- while (stack.length !== 0) {
- let item = stack.shift();
- for (let key in item.mod.table) {
- let childModIndex = item.mod.table[key];
- if (list[childModIndex] != null) continue;
- let childMod = zigAnalysis.modules[childModIndex];
- if (childMod == null) continue;
-
- let newPath = item.path.concat([key]);
- list[childModIndex] = newPath;
- stack.push({
- path: newPath,
- mod: childMod,
- });
- }
- }
-
- for (let i = 0; i < zigAnalysis.modules.length; i += 1) {
- const p = zigAnalysis.modules[i];
- // TODO
- // declSearchIndex.add(p.name, {moduleId: i});
- }
- return list;
- }
-
- function computeCanonDeclPaths() {
- let list = new Array(zigAnalysis.decls.length);
- canonTypeDecls = new Array(zigAnalysis.types.length);
-
- for (let modI = 0; modI < zigAnalysis.modules.length; modI += 1) {
- let mod = zigAnalysis.modules[modI];
- let modNames = canonModPaths[modI];
- if (modNames === undefined) continue;
-
- let stack = [
- {
- declNames: [],
- declIndexes: [],
- type: getType(mod.main),
- },
- ];
- while (stack.length !== 0) {
- let item = stack.shift();
-
- if (isContainerType(item.type)) {
- let t = item.type;
-
- let len = t.pubDecls ? t.pubDecls.length : 0;
- for (let declI = 0; declI < len; declI += 1) {
- let declIndex = t.pubDecls[declI];
- if (list[declIndex] != null) continue;
-
- let decl = getDecl(declIndex);
-
- if (decl.is_uns) {
- let unsDeclList = [decl];
- while (unsDeclList.length != 0) {
- let unsDecl = unsDeclList.pop();
- let unsDeclVal = resolveValue(unsDecl.value);
- if (!("type" in unsDeclVal.expr)) continue;
- let unsType = getType(unsDeclVal.expr.type);
- if (!isContainerType(unsType)) continue;
- let unsPubDeclLen = unsType.pubDecls ? unsType.pubDecls.length : 0;
- for (let unsDeclI = 0; unsDeclI < unsPubDeclLen; unsDeclI += 1) {
- let childDeclIndex = unsType.pubDecls[unsDeclI];
- let childDecl = getDecl(childDeclIndex);
-
- if (childDecl.is_uns) {
- unsDeclList.push(childDecl);
- } else {
- addDeclToSearchResults(childDecl, childDeclIndex, modNames, item, list, stack);
- }
- }
- }
- } else {
- addDeclToSearchResults(decl, declIndex, modNames, item, list, stack);
- }
- }
+ // First, walk backwards the decl parents within a file.
+ let decl_it = cur_nav_decl;
+ let prev_decl_it = null;
+ while (decl_it != null) {
+ list.push({
+ name: declIndexName(decl_it),
+ href: navLinkDeclIndex(decl_it),
+ });
+ prev_decl_it = decl_it;
+ decl_it = declParent(decl_it);
}
- }
- }
- window.cdp = list;
- return list;
- }
- function addDeclToSearchResults(decl, declIndex, modNames, item, list, stack) {
- let {value: declVal, seenDecls} = resolveValue(decl.value, true);
- let declNames = item.declNames.concat([decl.name]);
- let declIndexes = item.declIndexes.concat([declIndex]);
-
- if (list[declIndex] != null) return;
- list[declIndex] = {
- modNames: modNames,
- declNames: declNames,
- declIndexes: declIndexes,
- };
-
- for (let sd of seenDecls) {
- if (list[sd] != null) continue;
- list[sd] = {
- modNames: modNames,
- declNames: declNames,
- declIndexes: declIndexes,
- };
- }
-
- // add to search index
- {
- declSearchIndex.add(decl.name, { declIndex });
- }
-
-
- if ("type" in declVal.expr) {
- let value = getType(declVal.expr.type);
- if (declCanRepresentTypeKind(value.kind)) {
- canonTypeDecls[declVal.type] = declIndex;
- }
-
- if (isContainerType(value)) {
- stack.push({
- declNames: declNames,
- declIndexes: declIndexes,
- type: value,
- });
- }
-
- // Generic function
- if (typeIsGenericFn(declVal.expr.type)) {
- let ret = resolveGenericRet(value);
- if (ret != null && "type" in ret.expr) {
- let generic_type = getType(ret.expr.type);
- if (isContainerType(generic_type)) {
- stack.push({
- declNames: declNames,
- declIndexes: declIndexes,
- type: generic_type,
+ // Next, walk backwards the file path segments.
+ if (prev_decl_it != null) {
+ const file_path = fullyQualifiedName(prev_decl_it);
+ const parts = file_path.split(".");
+ parts.pop(); // skip last
+ for (;;) {
+ let part = parts.pop();
+ if (!part) break;
+ list.push({
+ name: part,
+ href: navLinkFqn(parts.join(".")),
});
}
}
+
+ list.reverse();
}
- }
- }
-
- function declLinkOrSrcLink(index) {
-
- let match = getCanonDeclPath(index);
- if (match) return navLink(match.modNames, match.declNames);
-
- // could not find a precomputed decl path
- const decl = getDecl(index);
-
- // try to find a public decl by scanning declRefs and declPaths
- let value = decl.value;
- let i = 0;
- while (true) {
- i += 1;
- if (i >= 10000) {
- throw "getCanonDeclPath quota exceeded"
+ resizeDomList(domListNav, list.length, '
');
+
+ for (let i = 0; i < list.length; i += 1) {
+ const liDom = domListNav.children[i];
+ const aDom = liDom.children[0];
+ aDom.textContent = list[i].name;
+ aDom.setAttribute('href', list[i].href);
+ if (i + 1 == list.length) {
+ aDom.classList.add("active");
+ } else {
+ aDom.classList.remove("active");
+ }
}
- if ("refPath" in value.expr) {
- value = { expr: value.expr.refPath[value.expr.refPath.length - 1] };
- continue;
- }
+ domSectNav.classList.remove("hidden");
+ }
- if ("declRef" in value.expr) {
- let cp = canonDeclPaths[value.expr.declRef];
- if (cp) return navLink(cp.modNames, cp.declNames);
-
- value = getDecl(value.expr.declRef).value;
- continue;
- }
+ function render404() {
+ domStatus.textContent = "404 Not Found";
+ domStatus.classList.remove("hidden");
+ }
- if ("as" in value.expr) {
- value = {
- typeRef: zigAnalysis.exprs[value.expr.as.typeRefArg],
- expr: zigAnalysis.exprs[value.expr.as.exprArg],
- };
- continue;
- }
+ function navLinkFqn(full_name) {
+ return '#' + full_name;
+ }
+
+ function navLinkDeclIndex(decl_index) {
+ return navLinkFqn(fullyQualifiedName(decl_index));
+ }
- // if we got here it means that we failed
- // produce a link to source code instead
- return sourceFileLink(decl);
+ function resizeDomList(listDom, desiredLen, templateHtml) {
+ // add the missing dom entries
+ var i, ev;
+ for (i = listDom.childElementCount; i < desiredLen; i += 1) {
+ listDom.insertAdjacentHTML('beforeend', templateHtml);
+ }
+ // remove extra dom entries
+ while (desiredLen < listDom.childElementCount) {
+ listDom.removeChild(listDom.lastChild);
+ }
+ }
+ function setViewSourceDecl(decl_index) {
+ curNav.viewSourceHash = "#src/" + unwrapString(wasm_exports.decl_file_path(decl_index));
}
-
- }
- function getCanonDeclPath(index) {
- if (canonDeclPaths == null) {
- canonDeclPaths = computeCanonDeclPaths();
+ function renderFunction(decl_index) {
+ renderNav(decl_index);
+ setViewSourceDecl(decl_index);
+
+ domFnProtoCode.innerHTML = fnProtoHtml(decl_index);
+ renderTopLevelDocs(decl_index);
+ domSourceText.innerHTML = declSourceHtml(decl_index);
+
+ const doctest_html = declDoctestHtml(decl_index);
+ if (doctest_html.length > 0) {
+ domDocTestsCode.innerHTML = doctest_html;
+ domSectDocTests.classList.remove("hidden");
+ }
+
+ domSectSource.classList.remove("hidden");
+ domFnProto.classList.remove("hidden");
}
-
- return canonDeclPaths[index];
-
-
- }
-
- function getCanonTypeDecl(index) {
- getCanonDeclPath(0);
- //let ct = (canonTypeDecls);
- return canonTypeDecls[index];
- }
-
- function escapeHtml(text) {
- return text.replace(/[&"<>]/g, function(m) {
- return escapeHtmlReplacements[m];
- });
- }
-
- function shortDesc(docs) {
- const trimmed_docs = docs.trim();
- let index = trimmed_docs.indexOf("\n\n");
- let cut = false;
-
- if (index < 0 || index > 130) {
- if (trimmed_docs.length > 130) {
- index = 130;
- cut = true;
- } else {
- index = trimmed_docs.length;
+
+ function renderGlobalConst(decl_index) {
+ renderNav(decl_index);
+ setViewSourceDecl(decl_index);
+
+ const docs_html = declDocsHtmlShort(decl_index);
+ if (docs_html.length > 0) {
+ domTldDocs.innerHTML = docs_html;
+ domTldDocs.classList.remove("hidden");
}
+
+ domSourceText.innerHTML = declSourceHtml(decl_index);
+ domSectSource.classList.remove("hidden");
}
- let slice = trimmed_docs.slice(0, index);
- if (cut) slice += "...";
- return slice;
- }
-
- function shortDescMarkdown(docs) {
- return markdown(shortDesc(docs));
- }
-
- function parseGuides() {
- for (let j = 0; j < zigAnalysis.guideSections.length; j += 1) {
- const section = zigAnalysis.guideSections[j];
- for (let i = 0; i < section.guides.length; i += 1) {
- let reader = new commonmark.Parser({ smart: true });
- const guide = section.guides[i];
-
- // Find the first text thing to use as a sidebar title
- guide.title = null;
- guide.toc = "";
-
- // Discover Title & TOC for this guide
- {
- let reader = new commonmark.Parser({smart: true});
- let ast = reader.parse(guide.body);
- let walker = ast.walker();
- let heading_idx = 0;
- let event, node, doc, last, last_ul;
- while ((event = walker.next())) {
- node = event.node;
- if (event.entering) {
- if (node.type === 'document') {
- doc = node;
+ function renderNamespace(decl_index) {
+ renderNav(decl_index);
+ renderDeclHeading(decl_index);
+ setViewSourceDecl(decl_index);
+
+ const typesList = [];
+ const namespacesList = [];
+ const errSetsList = [];
+ const fnsList = [];
+ const varsList = [];
+ const valsList = [];
+ const members = namespaceMembers(decl_index, false);
+
+ member_loop: for (let i = 0; i < members.length; i += 1) {
+ let member = members[i];
+ while (true) {
+ const member_category = wasm_exports.categorize_decl(member, 0);
+ switch (member_category) {
+ case CAT_namespace:
+ namespacesList.push(member);
+ continue member_loop;
+ case CAT_global_variable:
+ varsList.push(member);
+ continue member_loop;
+ case CAT_function:
+ fnsList.push(member);
+ continue member_loop;
+ case CAT_type:
+ typesList.push(member);
+ continue member_loop;
+ case CAT_error_set:
+ errSetsList.push(member);
+ continue member_loop;
+ case CAT_global_const:
+ case CAT_primitive:
+ valsList.push(member);
+ continue member_loop;
+ case CAT_alias:
+ // TODO: handle aliasing loop
+ member = wasm_exports.get_aliasee();
continue;
- }
+ default:
+ throw new Error("uknown category: " + member_category);
+ }
+ }
+ }
-
- if (node.next) {
- walker.resumeAt(node.next, true);
+ typesList.sort(byDeclIndexName);
+ namespacesList.sort(byDeclIndexName);
+ errSetsList.sort(byDeclIndexName);
+ fnsList.sort(byDeclIndexName);
+ varsList.sort(byDeclIndexName);
+ valsList.sort(byDeclIndexName);
+
+ if (typesList.length !== 0) {
+ resizeDomList(domListTypes, typesList.length, '
');
+ for (let i = 0; i < typesList.length; i += 1) {
+ const liDom = domListTypes.children[i];
+ const aDom = liDom.children[0];
+ const decl = typesList[i];
+ aDom.textContent = declIndexName(decl);
+ aDom.setAttribute('href', navLinkDeclIndex(decl));
+ }
+ domSectTypes.classList.remove("hidden");
+ }
+ if (namespacesList.length !== 0) {
+ resizeDomList(domListNamespaces, namespacesList.length, '
');
+ for (let i = 0; i < namespacesList.length; i += 1) {
+ const liDom = domListNamespaces.children[i];
+ const aDom = liDom.children[0];
+ const decl = namespacesList[i];
+ aDom.textContent = declIndexName(decl);
+ aDom.setAttribute('href', navLinkDeclIndex(decl));
+ }
+ domSectNamespaces.classList.remove("hidden");
+ }
+
+ if (errSetsList.length !== 0) {
+ resizeDomList(domListErrSets, errSetsList.length, '
');
+ for (let i = 0; i < errSetsList.length; i += 1) {
+ const liDom = domListErrSets.children[i];
+ const aDom = liDom.children[0];
+ const decl = errSetsList[i];
+ aDom.textContent = declIndexName(decl);
+ aDom.setAttribute('href', navLinkDeclIndex(decl));
+ }
+ domSectErrSets.classList.remove("hidden");
+ }
+
+ if (fnsList.length !== 0) {
+ resizeDomList(domListFns, fnsList.length,
+ '
');
+ for (let i = 0; i < fnsList.length; i += 1) {
+ const decl = fnsList[i];
+ const divDom = domListFns.children[i];
+
+ const dtName = divDom.children[0];
+ const ddDocs = divDom.children[1];
+ const codeDom = divDom.children[2].children[1].children[0];
+
+ const nameLinkDom = dtName.children[0];
+ const expandSourceDom = dtName.children[1];
+
+ nameLinkDom.setAttribute('href', navLinkDeclIndex(decl));
+ nameLinkDom.textContent = declIndexName(decl);
+
+ ddDocs.innerHTML = declDocsHtmlShort(decl);
+
+ codeDom.innerHTML = declSourceHtml(decl);
+ }
+ domSectFns.classList.remove("hidden");
+ }
+
+ // Prevent fields from being emptied next time wasm calls memory.grow.
+ const fields = declFields(decl_index).slice();
+ if (fields.length !== 0) {
+ resizeDomList(domListFields, fields.length, '
');
+ for (let i = 0; i < fields.length; i += 1) {
+ const divDom = domListFields.children[i];
+ divDom.innerHTML = unwrapString(wasm_exports.decl_field_html(decl_index, fields[i]));
+ }
+ domSectFields.classList.remove("hidden");
+ }
+
+ if (varsList.length !== 0) {
+ resizeDomList(domListGlobalVars, varsList.length,
+ '
| | |
');
+ for (let i = 0; i < varsList.length; i += 1) {
+ const decl = varsList[i];
+ const trDom = domListGlobalVars.children[i];
+
+ const tdName = trDom.children[0];
+ const tdNameA = tdName.children[0];
+ const tdType = trDom.children[1];
+ const tdDesc = trDom.children[2];
+
+ tdNameA.setAttribute('href', navLinkDeclIndex(decl));
+ tdNameA.textContent = declIndexName(decl);
+
+ tdType.innerHTML = declTypeHtml(decl);
+ tdDesc.innerHTML = declDocsHtmlShort(decl);
+ }
+ domSectGlobalVars.classList.remove("hidden");
+ }
+
+ if (valsList.length !== 0) {
+ resizeDomList(domListValues, valsList.length,
+ '
| | |
');
+ for (let i = 0; i < valsList.length; i += 1) {
+ const decl = valsList[i];
+ const trDom = domListValues.children[i];
+
+ const tdName = trDom.children[0];
+ const tdNameA = tdName.children[0];
+ const tdType = trDom.children[1];
+ const tdDesc = trDom.children[2];
+
+ tdNameA.setAttribute('href', navLinkDeclIndex(decl));
+ tdNameA.textContent = declIndexName(decl);
+
+ tdType.innerHTML = declTypeHtml(decl);
+ tdDesc.innerHTML = declDocsHtmlShort(decl);
+ }
+ domSectValues.classList.remove("hidden");
+ }
+ }
+
+ function operatorCompare(a, b) {
+ if (a === b) {
+ return 0;
+ } else if (a < b) {
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+
+ function updateCurNav(location_hash) {
+ curNav.tag = 0;
+ curNav.decl = null;
+ curNav.path = null;
+ curNav.viewSourceHash = null;
+ curNavSearch = "";
+
+ if (location_hash[0] === '#' && location_hash.length > 1) {
+ const query = location_hash.substring(1);
+ const qpos = query.indexOf("?");
+ let nonSearchPart;
+ if (qpos === -1) {
+ nonSearchPart = query;
+ } else {
+ nonSearchPart = query.substring(0, qpos);
+ curNavSearch = decodeURIComponent(query.substring(qpos + 1));
+ }
+
+ if (nonSearchPart.length > 0) {
+ const source_mode = nonSearchPart.startsWith("src/");
+ if (source_mode) {
+ curNav.tag = 2;
+ curNav.path = nonSearchPart.substring(4);
} else {
- walker.resumeAt(node, false);
+ curNav.tag = 1;
+ curNav.decl = findDecl(nonSearchPart);
}
- node.unlink();
-
- if (node.type === 'heading') {
- if (node.level == 1) {
- if (guide.title == null) {
- let doc_node = new commonmark.Node("document", node.sourcepos);
- while (node.firstChild) {
- doc_node.appendChild(node.firstChild);
- }
- let writer = new commonmark.HtmlRenderer();
- let result = writer.render(doc_node);
- guide.title = result;
- }
-
- continue; // don't index H1
- }
-
- // turn heading node into list item & add link node to it
- {
- node._type = "link";
- node.destination = NAV_MODES.GUIDES + guide.name + ":" + heading_idx;
- heading_idx += 1;
- let listItem = new commonmark.Node("item", node.sourcepos);
- // TODO: strip links from inside node
- listItem.appendChild(node);
- listItem.level = node.level;
- node = listItem;
- }
-
- if (last_ul) {
- // are we inside or outside of it?
-
- let target_ul = last_ul;
- while(target_ul.level > node.level) {
- target_ul = target_ul.parent;
- }
- while(target_ul.level < node.level) {
- let ul_node = new commonmark.Node("list", node.sourcepos);
- ul_node.level = target_ul.level + 1;
- ul_node.listType = "bullet";
- ul_node.listStart = null;
- target_ul.appendChild(ul_node);
- target_ul = ul_node;
- }
-
- target_ul.appendChild(node);
- last_ul = target_ul;
- } else {
- let ul_node = new commonmark.Node("list", node.sourcepos);
- ul_node.level = 2;
- ul_node.listType = "bullet";
- ul_node.listStart = null;
- doc.prependChild(ul_node);
-
- while (ul_node.level < node.level) {
- let current_ul_node = new commonmark.Node("list", node.sourcepos);
- current_ul_node.level = ul_node.level + 1;
- current_ul_node.listType = "bullet";
- current_ul_node.listStart = null;
- ul_node.appendChild(current_ul_node);
- ul_node = current_ul_node;
- }
-
- last_ul = ul_node;
-
- ul_node.appendChild(node);
}
- }
}
- }
-
- let writer = new commonmark.HtmlRenderer();
- let result = writer.render(ast);
- guide.toc = result;
- }
-
- // Index this guide
- {
- // let walker = guide.ast.walker();
- // let event, node;
- // while ((event = walker.next())) {
- // node = event.node;
- // if (event.entering == true && node.type === 'text') {
- // indexTextForGuide(j, i, node);
- // }
- // }
- }
- }
}
- }
- function indexTextForGuide(section_idx, guide_idx, node) {
- const terms = node.literal.split(" ");
- for (let i = 0; i < terms.length; i += 1) {
- const t = terms[i];
- if (!guidesSearchIndex[t]) guidesSearchIndex[t] = new Set();
- node.guide = { section_idx, guide_idx };
- guidesSearchIndex[t].add(node);
+ function onHashChange() {
+ navigate(location.hash);
}
- }
-
- function markdown(input, contextType) {
- const parsed = new commonmark.Parser({ smart: true }).parse(input);
-
- // Look for decl references in inline code (`ref`)
- const walker = parsed.walker();
- let event;
- while ((event = walker.next())) {
- const node = event.node;
- if (node.type === "code") {
- const declHash = detectDeclPath(node.literal, contextType);
- if (declHash) {
- const link = new commonmark.Node("link");
- link.destination = declHash;
- node.insertBefore(link);
- link.appendChild(node);
- }
+ function navigate(location_hash) {
+ updateCurNav(location_hash);
+ if (domSearch.value !== curNavSearch) {
+ domSearch.value = curNavSearch;
+ }
+ render();
+ if (imFeelingLucky) {
+ imFeelingLucky = false;
+ activateSelectedResult();
}
}
- return new commonmark.HtmlRenderer({ safe: true }).render(parsed);
-
- }
-
-
-
- // function detectDeclPath(text, context) {
- // let result = "";
- // let separator = ":";
- // const components = text.split(".");
- // let curDeclOrType = undefined;
-
- // let curContext = context;
- // let limit = 10000;
- // while (curContext) {
- // limit -= 1;
-
- // if (limit == 0) {
- // throw "too many iterations";
- // }
-
- // curDeclOrType = findSubDecl(curContext, components[0]);
-
- // if (!curDeclOrType) {
- // if (curContext.parent_container == null) break;
- // curContext = getType(curContext.parent_container);
- // continue;
- // }
-
- // if (curContext == context) {
- // separator = '.';
- // result = location.hash + separator + components[0];
- // } else {
- // // We had to go up, which means we need a new path!
- // const canonPath = getCanonDeclPath(curDeclOrType.find_subdecl_idx);
- // if (!canonPath) return;
-
- // let lastModName = canonPath.modNames[canonPath.modNames.length - 1];
- // let fullPath = lastModName + ":" + canonPath.declNames.join(".");
-
- // separator = '.';
- // result = "#A;" + fullPath;
- // }
-
- // break;
- // }
-
- // if (!curDeclOrType) {
- // for (let i = 0; i < zigAnalysis.modules.length; i += 1) {
- // const p = zigAnalysis.modules[i];
- // if (p.name == components[0]) {
- // curDeclOrType = getType(p.main);
- // result += "#A;" + components[0];
- // break;
- // }
- // }
- // }
-
- // if (!curDeclOrType) return null;
-
- // for (let i = 1; i < components.length; i += 1) {
- // curDeclOrType = findSubDecl(curDeclOrType, components[i]);
- // if (!curDeclOrType) return null;
- // result += separator + components[i];
- // separator = '.';
- // }
-
- // return result;
-
- // }
-
- function activateSelectedResult() {
- if (domSectSearchResults.classList.contains("hidden")) {
- return;
- }
-
- const searchResults = domListSearchResults.getElementsByTagName("li");
- let liDom = searchResults[curSearchIndex];
- if (liDom == null && searchResults.length !== 0) {
- liDom = searchResults[0];
- }
- if (liDom != null) {
- let aDom = liDom.children[0];
- location.href = aDom.getAttribute("href");
- curSearchIndex = -1;
- }
- domSearch.blur();
- }
+ function activateSelectedResult() {
+ if (domSectSearchResults.classList.contains("hidden")) {
+ return;
+ }
- // hide the modal if it's visible or return to the previous result page and unfocus the search
- function onEscape(ev) {
- if (isModalVisible(domHelpModal)) {
- hideModal(domHelpModal);
- ev.preventDefault();
- ev.stopPropagation();
- } else if (isModalVisible(domPrefsModal)) {
- hideModal(domPrefsModal);
- ev.preventDefault();
- ev.stopPropagation();
- } else {
- domSearch.value = "";
- domSearch.blur();
- domSearchPlaceholder.classList.remove("hidden");
- curSearchIndex = -1;
- ev.preventDefault();
- ev.stopPropagation();
- startSearch();
+ var liDom = domListSearchResults.children[curSearchIndex];
+ if (liDom == null && domListSearchResults.children.length !== 0) {
+ liDom = domListSearchResults.children[0];
+ }
+ if (liDom != null) {
+ var aDom = liDom.children[0];
+ location.href = aDom.getAttribute("href");
+ curSearchIndex = -1;
+ }
+ domSearch.blur();
}
- }
+ function onSearchKeyDown(ev) {
+ switch (ev.which) {
+ case 13:
+ if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
- function onSearchKeyDown(ev) {
- switch (getKeyString(ev)) {
- case "Enter":
- // detect if this search changes anything
- let terms1 = getSearchTerms();
- startSearch();
- updateCurNav();
- let terms2 = getSearchTerms();
- // we might have to wait for onHashChange to trigger
- imFeelingLucky = terms1.join(" ") !== terms2.join(" ");
- if (!imFeelingLucky) activateSelectedResult();
+ clearAsyncSearch();
+ imFeelingLucky = true;
+ location.hash = computeSearchHash();
- ev.preventDefault();
- ev.stopPropagation();
- return;
- case "Esc":
- onEscape(ev);
- return
- case "Up":
- moveSearchCursor(-1);
- ev.preventDefault();
- ev.stopPropagation();
- return;
- case "Down":
- // TODO: make the page scroll down if the search cursor is out of the screen
- moveSearchCursor(1);
- ev.preventDefault();
- ev.stopPropagation();
- return;
- default:
- // Search is triggered via an `input` event handler, not on arbitrary `keydown` events.
- ev.stopPropagation();
- return;
- }
- }
+ ev.preventDefault();
+ ev.stopPropagation();
+ return;
+ case 27:
+ if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
- let domDotsToggleTimeout = null;
- function onSearchInput(ev) {
- curSearchIndex = -1;
-
- let replaced = domSearch.value.replaceAll(".", " ")
+ domSearch.value = "";
+ domSearch.blur();
+ curSearchIndex = -1;
+ ev.preventDefault();
+ ev.stopPropagation();
+ startSearch();
+ return;
+ case 38:
+ if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
- // Ping red the help text if the user typed a dot.
- if (replaced != domSearch.value) {
- domSearchHelpSummary.classList.remove("normal");
- if (domDotsToggleTimeout != null) {
- clearTimeout(domDotsToggleTimeout);
- domDotsToggleTimeout = null;
- }
- domDotsToggleTimeout = setTimeout(function () {
- domSearchHelpSummary.classList.add("normal");
- }, 1000);
- }
-
- replaced = replaced.replace(/ +/g, ' ');
- if (replaced != domSearch.value) {
- domSearch.value = replaced;
- }
-
- startAsyncSearch();
- }
+ moveSearchCursor(-1);
+ ev.preventDefault();
+ ev.stopPropagation();
+ return;
+ case 40:
+ if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
- function moveSearchCursor(dir) {
- const searchResults = domListSearchResults.getElementsByTagName("li");
- if (
- curSearchIndex < 0 ||
- curSearchIndex >= searchResults.length
- ) {
- if (dir > 0) {
- curSearchIndex = -1 + dir;
- } else if (dir < 0) {
- curSearchIndex = searchResults.length + dir;
+ moveSearchCursor(1);
+ ev.preventDefault();
+ ev.stopPropagation();
+ return;
+ default:
+ ev.stopPropagation(); // prevent keyboard shortcuts
+ return;
}
- } else {
- curSearchIndex += dir;
- }
- if (curSearchIndex < 0) {
- curSearchIndex = 0;
}
- if (curSearchIndex >= searchResults.length) {
- curSearchIndex = searchResults.length - 1;
- }
- renderSearchCursor();
- }
- function getKeyString(ev) {
- let name;
- let ignoreShift = false;
- switch (ev.which) {
- case 13:
- name = "Enter";
- break;
- case 27:
- name = "Esc";
- break;
- case 38:
- name = "Up";
- break;
- case 40:
- name = "Down";
- break;
- default:
- ignoreShift = true;
- name =
- ev.key != null
- ? ev.key
- : String.fromCharCode(ev.charCode || ev.keyCode);
+ function onSearchChange(ev) {
+ curSearchIndex = -1;
+ startAsyncSearch();
}
- if (!ignoreShift && ev.shiftKey) name = "Shift+" + name;
- if (ev.altKey) name = "Alt+" + name;
- if (ev.ctrlKey) name = "Ctrl+" + name;
- return name;
- }
-
- function onWindowKeyDown(ev) {
- switch (getKeyString(ev)) {
- case "Esc":
- onEscape(ev);
- break;
- case "/":
- if (!getPrefSlashSearch()) break;
- // fallthrough
- case "s":
- if (!isModalVisible(domHelpModal) && !isModalVisible(domPrefsModal)) {
- if (ev.target == domSearch) break;
-
- domSearch.focus();
- domSearch.select();
- domDocs.scrollTo(0, 0);
- ev.preventDefault();
- ev.stopPropagation();
- startAsyncSearch();
- }
- break;
- case "?":
- if (!canToggleModal) break;
-
- if (isModalVisible(domPrefsModal)) {
- hideModal(domPrefsModal);
- }
- // toggle the help modal
- if (isModalVisible(domHelpModal)) {
- hideModal(domHelpModal);
+ function moveSearchCursor(dir) {
+ if (curSearchIndex < 0 || curSearchIndex >= domListSearchResults.children.length) {
+ if (dir > 0) {
+ curSearchIndex = -1 + dir;
+ } else if (dir < 0) {
+ curSearchIndex = domListSearchResults.children.length + dir;
+ }
} else {
- showModal(domHelpModal);
+ curSearchIndex += dir;
}
- ev.preventDefault();
- ev.stopPropagation();
- break;
- case "p":
- if (!canToggleModal) break;
-
- if (isModalVisible(domHelpModal)) {
- hideModal(domHelpModal);
+ if (curSearchIndex < 0) {
+ curSearchIndex = 0;
}
-
- // toggle the preferences modal
- if (isModalVisible(domPrefsModal)) {
- hideModal(domPrefsModal);
- } else {
- showModal(domPrefsModal);
+ if (curSearchIndex >= domListSearchResults.children.length) {
+ curSearchIndex = domListSearchResults.children.length - 1;
}
- ev.preventDefault();
- ev.stopPropagation();
+ renderSearchCursor();
}
- }
-
- function isModalVisible(modal) {
- return !modal.classList.contains("hidden");
- }
- function showModal(modal) {
- modal.classList.remove("hidden");
- modal.style.left =
- window.innerWidth / 2 - modal.clientWidth / 2 + "px";
- modal.style.top =
- window.innerHeight / 2 - modal.clientHeight / 2 + "px";
- const firstInput = modal.querySelector("input");
- if (firstInput) {
- firstInput.focus();
- } else {
- modal.focus();
+ function onWindowKeyDown(ev) {
+ switch (ev.which) {
+ case 27:
+ if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
+ if (!domHelpModal.classList.contains("hidden")) {
+ domHelpModal.classList.add("hidden");
+ ev.preventDefault();
+ ev.stopPropagation();
+ }
+ break;
+ case 83:
+ if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
+ domSearch.focus();
+ domSearch.select();
+ ev.preventDefault();
+ ev.stopPropagation();
+ startAsyncSearch();
+ break;
+ case 85:
+ if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
+ ev.preventDefault();
+ ev.stopPropagation();
+ navigateToSource();
+ break;
+ case 191:
+ if (!ev.shiftKey || ev.ctrlKey || ev.altKey) return;
+ ev.preventDefault();
+ ev.stopPropagation();
+ showHelpModal();
+ break;
+ }
}
- domSearch.blur();
- domBanner.inert = true;
- domMain.inert = true;
- }
-
- function hideModal(modal) {
- modal.classList.add("hidden");
- domBanner.inert = false;
- domMain.inert = false;
- modal.blur();
- }
- function clearAsyncSearch() {
- if (searchTimer != null) {
- clearTimeout(searchTimer);
- searchTimer = null;
+ function showHelpModal() {
+ domHelpModal.classList.remove("hidden");
+ domHelpModal.style.left = (window.innerWidth / 2 - domHelpModal.clientWidth / 2) + "px";
+ domHelpModal.style.top = (window.innerHeight / 2 - domHelpModal.clientHeight / 2) + "px";
+ domHelpModal.focus();
}
- }
- function startAsyncSearch() {
- clearAsyncSearch();
- searchTimer = setTimeout(startSearch, 100);
- }
- function startSearch() {
- clearAsyncSearch();
- let oldHash = location.hash;
- let parts = oldHash.split("?");
- let newPart2 = domSearch.value === "" ? "" : "?" + domSearch.value;
- location.replace(parts.length === 1 ? oldHash + newPart2 : parts[0] + newPart2);
- }
- function getSearchTerms() {
- let list = curNavSearch.trim().split(/[ \r\n\t]+/);
- return list;
- }
-
- function renderSearchGuides() {
- const searchTrimmed = false;
- let ignoreCase = curNavSearch.toLowerCase() === curNavSearch;
-
- let terms = getSearchTerms();
- let matchedItems = new Set();
-
- for (let i = 0; i < terms.length; i += 1) {
- const nodes = guidesSearchIndex[terms[i]];
- if (nodes) {
- for (const n of nodes) {
- matchedItems.add(n);
- }
+ function navigateToSource() {
+ if (curNav.viewSourceHash != null) {
+ location.hash = curNav.viewSourceHash;
}
}
-
-
- if (matchedItems.size !== 0) {
- // Build up the list of search results
- let matchedItemsHTML = "";
-
- for (const node of matchedItems) {
- const text = node.literal;
- const href = "";
-
- matchedItemsHTML += "
" + text + "";
- }
-
- // Replace the search results using our newly constructed HTML string
- domListSearchResults.innerHTML = matchedItemsHTML;
- if (searchTrimmed) {
- domSectSearchAllResultsLink.classList.remove("hidden");
- }
- renderSearchCursor();
-
- domSectSearchResults.classList.remove("hidden");
- } else {
- domSectSearchNoResults.classList.remove("hidden");
+ function clearAsyncSearch() {
+ if (searchTimer != null) {
+ clearTimeout(searchTimer);
+ searchTimer = null;
+ }
}
- }
- function renderSearchAPI() {
- domSectSearchResults.prepend(
- domSearchHelp.parentElement.removeChild(domSearchHelp)
- );
- if (canonDeclPaths == null) {
- canonDeclPaths = computeCanonDeclPaths();
+ function startAsyncSearch() {
+ clearAsyncSearch();
+ searchTimer = setTimeout(startSearch, 100);
}
- let declSet = new Set();
- let otherDeclSet = new Set(); // for low quality results
- let declScores = {};
-
- let ignoreCase = curNavSearch.toLowerCase() === curNavSearch;
- let term_list = getSearchTerms();
- for (let i = 0; i < term_list.length; i += 1) {
- let term = term_list[i];
- let result = declSearchIndex.search(term.toLowerCase());
- if (result == null) {
- domSectSearchNoResults.prepend(
- domSearchHelp.parentElement.removeChild(domSearchHelp)
- );
- domSectSearchNoResults.classList.remove("hidden");
-
- domSectSearchResults.classList.add("hidden");
- return;
- }
+ function computeSearchHash() {
+ const oldHash = location.hash;
+ const parts = oldHash.split("?");
+ const newPart2 = (domSearch.value === "") ? "" : ("?" + domSearch.value);
+ return (parts.length === 1) ? (oldHash + newPart2) : ("#" + parts[0] + newPart2);
+ }
+ function startSearch() {
+ clearAsyncSearch();
+ navigate(computeSearchHash());
+ }
+ function renderSearch() {
+ renderNav(curNav.decl);
- let termSet = new Set();
- let termOtherSet = new Set();
+ const ignoreCase = (curNavSearch.toLowerCase() === curNavSearch);
+ const results = executeQuery(curNavSearch, ignoreCase);
- for (let list of [result.full, result.partial]) {
- for (let r of list) {
- const d = r.declIndex;
- const decl = getDecl(d);
- const canonPath = getCanonDeclPath(d);
+ if (results.length !== 0) {
+ resizeDomList(domListSearchResults, results.length, '
');
- // collect unconditionally for the first term
- if (i == 0) {
- declSet.add(d);
- } else {
- // path intersection for subsequent terms
- let found = false;
- for (let p of canonPath.declIndexes) {
- if (declSet.has(p)) {
- found = true;
- break;
- }
+ for (let i = 0; i < results.length; i += 1) {
+ const liDom = domListSearchResults.children[i];
+ const aDom = liDom.children[0];
+ const match = results[i];
+ const full_name = fullyQualifiedName(match);
+ aDom.textContent = full_name;
+ aDom.setAttribute('href', navLinkFqn(full_name));
}
- if (!found) {
- otherDeclSet.add(d);
+ renderSearchCursor();
+
+ domSectSearchResults.classList.remove("hidden");
+ } else {
+ domSectSearchNoResults.classList.remove("hidden");
+ }
+ }
+
+ function renderSearchCursor() {
+ for (let i = 0; i < domListSearchResults.children.length; i += 1) {
+ var liDom = domListSearchResults.children[i];
+ if (curSearchIndex === i) {
+ liDom.classList.add("selected");
} else {
- termSet.add(d);
+ liDom.classList.remove("selected");
}
- }
-
- if (declScores[d] == undefined) declScores[d] = 0;
-
- // scores (lower is better)
- let decl_name = decl.name;
- if (ignoreCase) decl_name = decl_name.toLowerCase();
-
- // shallow path are preferable
- const path_depth = canonPath.declNames.length * 50;
- // matching the start of a decl name is good
- const match_from_start = decl_name.startsWith(term) ? -term.length * (2 - ignoreCase) : (decl_name.length - term.length) + 1;
- // being a perfect match is good
- const is_full_match = (decl_name === term) ? -decl_name.length * (1 - ignoreCase) : Math.abs(decl_name.length - term.length);
- // matching the end of a decl name is good
- const matches_the_end = decl_name.endsWith(term) ? -term.length * (1 - ignoreCase) : (decl_name.length - term.length) + 1;
- // explicitly penalizing scream case decls
- const decl_is_scream_case = decl.name.toUpperCase() != decl.name ? 0 : decl.name.length;
-
- const score = path_depth
- + match_from_start
- + is_full_match
- + matches_the_end
- + decl_is_scream_case;
-
- declScores[d] += score;
- }
- }
- if (i != 0) {
- for (let d of declSet) {
- if (termSet.has(d)) continue;
- let found = false;
- for (let p of getCanonDeclPath(d).declIndexes) {
- if (termSet.has(p) || otherDeclSet.has(p)) {
- found = true;
- break;
- }
- }
- if (found) {
- declScores[d] = declScores[d] / term_list.length;
- }
-
- termOtherSet.add(d);
- }
- declSet = termSet;
- for (let d of termOtherSet) {
- otherDeclSet.add(d);
}
+ }
+ function updatePackageList() {
+ packageList.length = 0;
+ for (let i = 0;; i += 1) {
+ const name = unwrapString(wasm_exports.package_name(i));
+ if (name.length == 0) break;
+ packageList.push(name);
}
}
- let matchedItems = {
- high_quality: [],
- low_quality: [],
- };
- for (let idx of declSet) {
- matchedItems.high_quality.push({ points: declScores[idx], declIndex: idx })
- }
- for (let idx of otherDeclSet) {
- matchedItems.low_quality.push({ points: declScores[idx], declIndex: idx })
+ function byDeclIndexName(a, b) {
+ const a_name = declIndexName(a);
+ const b_name = declIndexName(b);
+ return operatorCompare(a_name, b_name);
}
- matchedItems.high_quality.sort(function(a, b) {
- let cmp = operatorCompare(a.points, b.points);
- return cmp;
- });
- matchedItems.low_quality.sort(function(a, b) {
- let cmp = operatorCompare(a.points, b.points);
- return cmp;
- });
-
- // Build up the list of search results
- let matchedItemsHTML = "";
-
- for (let list of [matchedItems.high_quality, matchedItems.low_quality]) {
- if (list == matchedItems.low_quality && list.length > 0) {
- matchedItemsHTML += "
"
- }
- for (let result of list) {
- const points = result.points;
- const match = result.declIndex;
-
- let canonPath = getCanonDeclPath(match);
- if (canonPath == null) continue;
-
- let lastModName = canonPath.modNames[canonPath.modNames.length - 1];
- let text = lastModName + "." + canonPath.declNames.join(".");
-
-
- const href = navLink(canonPath.modNames, canonPath.declNames);
-
- matchedItemsHTML += "
" + text + "";
- }
+ function decodeString(ptr, len) {
+ if (len === 0) return "";
+ return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
}
- // Replace the search results using our newly constructed HTML string
- domListSearchResults.innerHTML = matchedItemsHTML;
- renderSearchCursor();
-
- domSectSearchResults.classList.remove("hidden");
- }
+ function unwrapString(bigint) {
+ const ptr = Number(bigint & 0xffffffffn);
+ const len = Number(bigint >> 32n);
+ return decodeString(ptr, len);
+ }
-
+ function declTypeHtml(decl_index) {
+ return unwrapString(wasm_exports.decl_type_html(decl_index));
+ }
- function renderSearchCursor() {
- const searchResults = domListSearchResults.getElementsByTagName("li");
- for (let i = 0; i < searchResults.length; i += 1) {
- let liDom = searchResults[i];
- if (curSearchIndex === i) {
- liDom.classList.add("selected");
- } else {
- liDom.classList.remove("selected");
- }
+ function declDocsHtmlShort(decl_index) {
+ return unwrapString(wasm_exports.decl_docs_html(decl_index, true));
}
- }
- function scrollGuidesTop(ev) {
- document.getElementById("activeGuide").children[0].scrollIntoView({
- behavior: "smooth",
- });
- ev.preventDefault();
- ev.stopPropagation();
- }
- document.scrollGuidesTop = scrollGuidesTop;
+ function fullyQualifiedName(decl_index) {
+ return unwrapString(wasm_exports.decl_fqn(decl_index));
+ }
- function scrollToHeading(id, alreadyThere) {
- // Don't scroll if the current location has a scrolling history.
- if (scrollHistory[location.hash]) return;
+ function declIndexName(decl_index) {
+ return unwrapString(wasm_exports.decl_name(decl_index));
+ }
- const c = document.getElementById(id);
- if (c && alreadyThere) {
- requestAnimationFrame(() => c.scrollIntoView({behavior: "smooth"}));
- } else {
- requestAnimationFrame(() => c.scrollIntoView());
- }
- return;
- }
- // function indexNodesToCalls() {
- // let map = {};
- // for (let i = 0; i < zigAnalysis.calls.length; i += 1) {
- // let call = zigAnalysis.calls[i];
- // let fn = zigAnalysis.fns[call.fn];
- // if (map[fn.src] == null) {
- // map[fn.src] = [i];
- // } else {
- // map[fn.src].push(i);
- // }
- // }
- // return map;
- // }
+ function declSourceHtml(decl_index) {
+ return unwrapString(wasm_exports.decl_source_html(decl_index));
+ }
- function byNameProperty(a, b) {
- return operatorCompare(a.name, b.name);
- }
+ function declDoctestHtml(decl_index) {
+ return unwrapString(wasm_exports.decl_doctest_html(decl_index));
+ }
+ function fnProtoHtml(decl_index) {
+ return unwrapString(wasm_exports.decl_fn_proto_html(decl_index));
+ }
- function getDecl(idx) {
- const decl = zigAnalysis.decls[idx];
- return {
- name: decl[0],
- kind: decl[1],
- src: decl[2],
- value: decl[3],
- decltest: decl[4],
- is_uns: decl[5],
- parent_container: decl[6],
- };
- }
+ function setQueryString(s) {
+ const jsArray = text_encoder.encode(s);
+ const len = jsArray.length;
+ const ptr = wasm_exports.query_begin(len);
+ const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
+ wasmArray.set(jsArray);
+ }
- function getAstNode(idx) {
- const ast = zigAnalysis.astNodes[idx];
- return {
- file: ast[0],
- line: ast[1],
- col: ast[2],
- name: ast[3],
- code: ast[4],
- docs: ast[5],
- fields: ast[6],
- comptime: ast[7],
- };
- }
+ function executeQuery(query_string, ignore_case) {
+ setQueryString(query_string);
+ const ptr = wasm_exports.query_exec(ignore_case);
+ const head = new Uint32Array(wasm_exports.memory.buffer, ptr, 1);
+ const len = head[0];
+ return new Uint32Array(wasm_exports.memory.buffer, ptr + 4, len);
+ }
- function getFile(idx) {
- const file = zigAnalysis.files[idx];
- return {
- name: file[0],
- modIndex: file[1],
- };
- }
+ function namespaceMembers(decl_index, include_private) {
+ const bigint = wasm_exports.namespace_members(decl_index, include_private);
+ const ptr = Number(bigint & 0xffffffffn);
+ const len = Number(bigint >> 32n);
+ if (len == 0) return [];
+ return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
+ }
- function getType(idx) {
- const ty = zigAnalysis.types[idx];
- switch (ty[0]) {
- default:
- throw "unhandled type kind!";
- case typeKinds.Unanalyzed:
- throw "unanalyzed type!";
- case typeKinds.Type:
- case typeKinds.Void:
- case typeKinds.Bool:
- case typeKinds.NoReturn:
- case typeKinds.Int:
- case typeKinds.Float:
- return { kind: ty[0], name: ty[1] };
- case typeKinds.Pointer:
- return {
- kind: ty[0],
- size: ty[1],
- child: ty[2],
- sentinel: ty[3],
- align: ty[4],
- address_space: ty[5],
- bit_start: ty[6],
- host_size: ty[7],
- is_ref: ty[8],
- is_allowzero: ty[9],
- is_mutable: ty[10],
- is_volatile: ty[11],
- has_sentinel: ty[12],
- has_align: ty[13],
- has_addrspace: ty[14],
- has_bit_range: ty[15],
- };
- case typeKinds.Array:
- return {
- kind: ty[0],
- len: ty[1],
- child: ty[2],
- sentinel: ty[3],
- };
- case typeKinds.Struct:
- return {
- kind: ty[0],
- name: ty[1],
- src: ty[2],
- privDecls: ty[3],
- pubDecls: ty[4],
- field_types: ty[5],
- field_defaults: ty[6],
- backing_int: ty[7],
- is_tuple: ty[8],
- line_number: ty[9],
- parent_container: ty[10],
- layout: ty[11],
- };
- case typeKinds.ComptimeExpr:
- case typeKinds.ComptimeFloat:
- case typeKinds.ComptimeInt:
- case typeKinds.Undefined:
- case typeKinds.Null:
- return { kind: ty[0], name: ty[1] };
- case typeKinds.Optional:
- return {
- kind: ty[0],
- name: ty[1],
- child: ty[2],
- };
- case typeKinds.ErrorUnion:
- return {
- kind: ty[0],
- lhs: ty[1],
- rhs: ty[2],
- };
- case typeKinds.InferredErrorUnion:
- return {
- kind: ty[0],
- payload: ty[1],
- };
- case typeKinds.ErrorSet:
- return {
- kind: ty[0],
- name: ty[1],
- fields: ty[2],
- };
- case typeKinds.Enum:
- return {
- kind: ty[0],
- name: ty[1],
- src: ty[2],
- privDecls: ty[3],
- pubDecls: ty[4],
- tag: ty[5],
- values: ty[6],
- nonexhaustive: ty[7],
- parent_container: ty[8],
- };
- case typeKinds.Union:
- return {
- kind: ty[0],
- name: ty[1],
- src: ty[2],
- privDecls: ty[3],
- pubDecls: ty[4],
- field_types: ty[5],
- tag: ty[6],
- auto_tag: ty[7],
- parent_container: ty[8],
- layout: ty[9],
- };
- case typeKinds.Fn:
- return {
- kind: ty[0],
- name: ty[1],
- src: ty[2],
- ret: ty[3],
- generic_ret: ty[4],
- params: ty[5],
- lib_name: ty[6],
- is_var_args: ty[7],
- is_inferred_error: ty[8],
- has_lib_name: ty[9],
- has_cc: ty[10],
- cc: ty[11],
- align: ty[12],
- has_align: ty[13],
- is_test: ty[14],
- is_extern: ty[15],
- };
- case typeKinds.Opaque:
- return {
- kind: ty[0],
- name: ty[1],
- src: ty[2],
- privDecls: ty[3],
- pubDecls: ty[4],
- parent_container: ty[5],
- };
- case typeKinds.Frame:
- case typeKinds.AnyFrame:
- case typeKinds.Vector:
- case typeKinds.EnumLiteral:
- return { kind: ty[0], name: ty[1] };
+ function declFields(decl_index) {
+ const bigint = wasm_exports.decl_fields(decl_index);
+ const ptr = Number(bigint & 0xffffffffn);
+ const len = Number(bigint >> 32n);
+ if (len === 0) return [];
+ return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
}
- }
- function getLocalStorage() {
- if ("localStorage" in window) {
- try {
- return window.localStorage;
- } catch (ignored) {
- // localStorage may be disabled (SecurityError)
- }
+ function findDecl(fqn) {
+ setInputString(fqn);
+ const result = wasm_exports.find_decl();
+ if (result === -1) return null;
+ return result;
}
- // If localStorage isn't available, persist preferences only for the current session
- const sessionPrefs = {};
- return {
- getItem(key) {
- return key in sessionPrefs ? sessionPrefs[key] : null;
- },
- setItem(key, value) {
- sessionPrefs[key] = String(value);
- },
- };
- }
- function loadPrefs() {
- const storedPrefSlashSearch = prefs.getItem("slashSearch");
- if (storedPrefSlashSearch === null) {
- // Slash search defaults to enabled for all browsers except Firefox
- setPrefSlashSearch(navigator.userAgent.indexOf("Firefox") === -1);
- } else {
- setPrefSlashSearch(storedPrefSlashSearch === "true");
+ function findFileRoot(path) {
+ setInputString(path);
+ const result = wasm_exports.find_file_root();
+ if (result === -1) return null;
+ return result;
}
- }
- function getPrefSlashSearch() {
- return prefs.getItem("slashSearch") === "true";
- }
+ function declParent(decl_index) {
+ const result = wasm_exports.decl_parent(decl_index);
+ if (result === -1) return null;
+ return result;
+ }
- function setPrefSlashSearch(enabled) {
- prefs.setItem("slashSearch", String(enabled));
- domPrefSlashSearch.checked = enabled;
- const searchKeys = enabled ? "
/ or
s" : "
s";
- domSearchKeys.innerHTML = searchKeys;
- domSearchPlaceholderText.innerHTML = searchKeys + " to search,
? for more options";
- }
+ function setInputString(s) {
+ const jsArray = text_encoder.encode(s);
+ const len = jsArray.length;
+ const ptr = wasm_exports.set_input_string(len);
+ const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
+ wasmArray.set(jsArray);
+ }
})();
-function toggleExpand(event) {
- const parent = event.target.parentElement;
- parent.toggleAttribute("open");
-
- if (!parent.open && parent.getBoundingClientRect().top < 0) {
- parent.parentElement.parentElement.scrollIntoView(true);
- }
-}
-
-function RadixTree() {
- this.root = null;
-
- RadixTree.prototype.search = function(query) {
- return this.root.search(query);
-
- }
-
- RadixTree.prototype.add = function(declName, value) {
- if (this.root == null) {
- this.root = new Node(declName.toLowerCase(), null, [value]);
- } else {
- this.root.add(declName.toLowerCase(), value);
- }
-
- const not_scream_case = declName.toUpperCase() != declName;
- let found_separator = false;
- for (let i = 1; i < declName.length; i += 1) {
- if (declName[i] == '_' || declName[i] == '.') {
- found_separator = true;
- continue;
- }
-
-
- if (found_separator || (declName[i].toLowerCase() !== declName[i])) {
- if (declName.length > i + 1
- && declName[i + 1].toLowerCase() != declName[i + 1]) continue;
- let suffix = declName.slice(i);
- this.root.add(suffix.toLowerCase(), value);
- found_separator = false;
- }
- }
- }
-
- function Node(labels, next, values) {
- this.labels = labels;
- this.next = next;
- this.values = values;
- }
-
- Node.prototype.isCompressed = function() {
- return !Array.isArray(this.next);
- }
-
- Node.prototype.search = function(word) {
- let full_matches = [];
- let partial_matches = [];
- let subtree_root = null;
-
- let cn = this;
- char_loop: for (let i = 0; i < word.length;) {
- if (cn.isCompressed()) {
- for (let j = 0; j < cn.labels.length; j += 1) {
- let current_idx = i + j;
-
- if (current_idx == word.length) {
- partial_matches = cn.values;
- subtree_root = cn.next;
- break char_loop;
- }
-
- if (word[current_idx] != cn.labels[j]) return null;
- }
-
- // the full label matched
- let new_idx = i + cn.labels.length;
- if (new_idx == word.length) {
- full_matches = cn.values;
- subtree_root = cn.next;
- break char_loop;
- }
-
-
- i = new_idx;
- cn = cn.next;
- continue;
- } else {
- for (let j = 0; j < cn.labels.length; j += 1) {
- if (word[i] == cn.labels[j]) {
- if (i == word.length - 1) {
- full_matches = cn.values[j];
- subtree_root = cn.next[j];
- break char_loop;
- }
-
- let next = cn.next[j];
- if (next == null) return null;
- cn = next;
- i += 1;
- continue char_loop;
- }
- }
-
- // didn't find a match
- return null;
- }
- }
-
- // Match was found, let's collect all other
- // partial matches from the subtree
- let stack = [subtree_root];
- let node;
- while (node = stack.pop()) {
- if (node.isCompressed()) {
- partial_matches = partial_matches.concat(node.values);
- if (node.next != null) {
- stack.push(node.next);
- }
- } else {
- for (let v of node.values) {
- partial_matches = partial_matches.concat(v);
- }
-
- for (let n of node.next) {
- if (n != null) stack.push(n);
- }
- }
- }
-
- return { full: full_matches, partial: partial_matches };
- }
-
- Node.prototype.add = function(word, value) {
- let cn = this;
- char_loop: for (let i = 0; i < word.length;) {
- if (cn.isCompressed()) {
- for (let j = 0; j < cn.labels.length; j += 1) {
- let current_idx = i + j;
-
- if (current_idx == word.length) {
- if (j < cn.labels.length - 1) {
- let node = new Node(cn.labels.slice(j), cn.next, cn.values);
- cn.labels = cn.labels.slice(0, j);
- cn.next = node;
- cn.values = [];
- }
- cn.values.push(value);
- return;
- }
-
- if (word[current_idx] == cn.labels[j]) continue;
-
- // if we're here, a mismatch was found
- if (j != cn.labels.length - 1) {
- // create a suffix node
- const label_suffix = cn.labels.slice(j + 1);
- let node = new Node(label_suffix, cn.next, [...cn.values]);
- cn.next = node;
- cn.values = [];
- }
-
- // turn current node into a split node
- let node = null;
- let word_values = [];
- if (current_idx == word.length - 1) {
- // mismatch happened in the last character of word
- // meaning that the current node should hold its value
- word_values.push(value);
- } else {
- node = new Node(word.slice(current_idx + 1), null, [value]);
- }
-
- cn.labels = cn.labels[j] + word[current_idx];
- cn.next = [cn.next, node];
- cn.values = [cn.values, word_values];
-
- if (j != 0) {
- // current node must be turned into a prefix node
- let splitNode = new Node(cn.labels, cn.next, cn.values);
- cn.labels = word.slice(i, current_idx);
- cn.next = splitNode;
- cn.values = [];
- }
-
- return;
- }
- // label matched fully with word, are there any more chars?
- const new_idx = i + cn.labels.length;
- if (new_idx == word.length) {
- cn.values.push(value);
- return;
- } else {
- if (cn.next == null) {
- let node = new Node(word.slice(new_idx), null, [value]);
- cn.next = node;
- return;
- } else {
- cn = cn.next;
- i = new_idx;
- continue;
- }
- }
- } else { // node is not compressed
- let letter = word[i];
- for (let j = 0; j < cn.labels.length; j += 1) {
- if (letter == cn.labels[j]) {
- if (i == word.length - 1) {
- cn.values[j].push(value);
- return;
- }
- if (cn.next[j] == null) {
- let node = new Node(word.slice(i + 1), null, [value]);
- cn.next[j] = node;
- return;
- } else {
- cn = cn.next[j];
- i += 1;
- continue char_loop;
- }
- }
- }
-
- // if we're here we didn't find a match
- cn.labels += letter;
- if (i == word.length - 1) {
- cn.next.push(null);
- cn.values.push([value]);
- } else {
- let node = new Node(word.slice(i + 1), null, [value]);
- cn.next.push(node);
- cn.values.push([]);
- }
- return;
- }
- }
- }
-}
-
-
-function slugify(str) {
- return str.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, '');
-}
-
diff --git a/lib/docs/wasm/Decl.zig b/lib/docs/wasm/Decl.zig
new file mode 100644
index 0000000000000000000000000000000000000000..08a45eebf9d2420e48998dba7d59783b1e0d1b3e
--- /dev/null
+++ b/lib/docs/wasm/Decl.zig
@@ -0,0 +1,180 @@
+ast_node: Ast.Node.Index,
+file: Walk.File.Index,
+/// The decl whose namespace this is in.
+parent: Index,
+
+pub const ExtraInfo = struct {
+ is_pub: bool,
+ name: []const u8,
+ /// This might not be a doc_comment token in which case there are no doc comments.
+ first_doc_comment: Ast.TokenIndex,
+};
+
+pub const Index = enum(u32) {
+ none = std.math.maxInt(u32),
+ _,
+
+ pub fn get(i: Index) *Decl {
+ return &Walk.decls.items[@intFromEnum(i)];
+ }
+};
+
+pub fn is_pub(d: *const Decl) bool {
+ return d.extra_info().is_pub;
+}
+
+pub fn extra_info(d: *const Decl) ExtraInfo {
+ const ast = d.file.get_ast();
+ const token_tags = ast.tokens.items(.tag);
+ const node_tags = ast.nodes.items(.tag);
+ switch (node_tags[d.ast_node]) {
+ .root => return .{
+ .name = "",
+ .is_pub = true,
+ .first_doc_comment = if (token_tags[0] == .container_doc_comment)
+ 0
+ else
+ token_tags.len - 1,
+ },
+
+ .global_var_decl,
+ .local_var_decl,
+ .simple_var_decl,
+ .aligned_var_decl,
+ => {
+ const var_decl = ast.fullVarDecl(d.ast_node).?;
+ const name_token = var_decl.ast.mut_token + 1;
+ assert(token_tags[name_token] == .identifier);
+ const ident_name = ast.tokenSlice(name_token);
+ return .{
+ .name = ident_name,
+ .is_pub = var_decl.visib_token != null,
+ .first_doc_comment = findFirstDocComment(ast, var_decl.firstToken()),
+ };
+ },
+
+ .fn_proto,
+ .fn_proto_multi,
+ .fn_proto_one,
+ .fn_proto_simple,
+ .fn_decl,
+ => {
+ var buf: [1]Ast.Node.Index = undefined;
+ const fn_proto = ast.fullFnProto(&buf, d.ast_node).?;
+ const name_token = fn_proto.name_token.?;
+ assert(token_tags[name_token] == .identifier);
+ const ident_name = ast.tokenSlice(name_token);
+ return .{
+ .name = ident_name,
+ .is_pub = fn_proto.visib_token != null,
+ .first_doc_comment = findFirstDocComment(ast, fn_proto.firstToken()),
+ };
+ },
+
+ else => |t| {
+ log.debug("hit '{s}'", .{@tagName(t)});
+ unreachable;
+ },
+ }
+}
+
+pub fn value_node(d: *const Decl) ?Ast.Node.Index {
+ const ast = d.file.get_ast();
+ const node_tags = ast.nodes.items(.tag);
+ const token_tags = ast.tokens.items(.tag);
+ return switch (node_tags[d.ast_node]) {
+ .fn_proto,
+ .fn_proto_multi,
+ .fn_proto_one,
+ .fn_proto_simple,
+ .fn_decl,
+ .root,
+ => d.ast_node,
+
+ .global_var_decl,
+ .local_var_decl,
+ .simple_var_decl,
+ .aligned_var_decl,
+ => {
+ const var_decl = ast.fullVarDecl(d.ast_node).?;
+ if (token_tags[var_decl.ast.mut_token] == .keyword_const)
+ return var_decl.ast.init_node;
+
+ return null;
+ },
+
+ else => null,
+ };
+}
+
+pub fn categorize(decl: *const Decl) Walk.Category {
+ return decl.file.categorize_decl(decl.ast_node);
+}
+
+pub fn fqn(decl: *const Decl, out: *std.ArrayListUnmanaged(u8)) Oom!void {
+ try decl.reset_with_path(out);
+ if (decl.parent != .none) {
+ try append_parent_ns(out, decl.parent);
+ try out.appendSlice(gpa, decl.extra_info().name);
+ } else {
+ out.items.len -= 1; // remove the trailing '.'
+ }
+}
+
+pub fn reset_with_path(decl: *const Decl, list: *std.ArrayListUnmanaged(u8)) Oom!void {
+ list.clearRetainingCapacity();
+
+ // Prefer the package name alias.
+ for (Walk.packages.keys(), Walk.packages.values()) |pkg_name, pkg_file| {
+ if (pkg_file == decl.file) {
+ try list.ensureUnusedCapacity(gpa, pkg_name.len + 1);
+ list.appendSliceAssumeCapacity(pkg_name);
+ list.appendAssumeCapacity('.');
+ return;
+ }
+ }
+
+ const file_path = decl.file.path();
+ try list.ensureUnusedCapacity(gpa, file_path.len + 1);
+ list.appendSliceAssumeCapacity(file_path);
+ for (list.items) |*byte| switch (byte.*) {
+ '/' => byte.* = '.',
+ else => continue,
+ };
+ if (std.mem.endsWith(u8, list.items, ".zig")) {
+ list.items.len -= 3;
+ } else {
+ list.appendAssumeCapacity('.');
+ }
+}
+
+pub fn append_parent_ns(list: *std.ArrayListUnmanaged(u8), parent: Decl.Index) Oom!void {
+ assert(parent != .none);
+ const decl = parent.get();
+ if (decl.parent != .none) {
+ try append_parent_ns(list, decl.parent);
+ try list.appendSlice(gpa, decl.extra_info().name);
+ try list.append(gpa, '.');
+ }
+}
+
+pub fn findFirstDocComment(ast: *const Ast, token: Ast.TokenIndex) Ast.TokenIndex {
+ const token_tags = ast.tokens.items(.tag);
+ var it = token;
+ while (it > 0) {
+ it -= 1;
+ if (token_tags[it] != .doc_comment) {
+ return it + 1;
+ }
+ }
+ return it;
+}
+
+const Decl = @This();
+const std = @import("std");
+const Ast = std.zig.Ast;
+const Walk = @import("Walk.zig");
+const gpa = std.heap.wasm_allocator;
+const assert = std.debug.assert;
+const log = std.log;
+const Oom = error{OutOfMemory};
diff --git a/lib/docs/wasm/Walk.zig b/lib/docs/wasm/Walk.zig
new file mode 100644
index 0000000000000000000000000000000000000000..7714c3ce42c13e579509da5656ca06b7e1cbe140
--- /dev/null
+++ b/lib/docs/wasm/Walk.zig
@@ -0,0 +1,928 @@
+//! Find and annotate identifiers with links to their declarations.
+pub var files: std.StringArrayHashMapUnmanaged(File) = .{};
+pub var decls: std.ArrayListUnmanaged(Decl) = .{};
+pub var packages: std.StringArrayHashMapUnmanaged(File.Index) = .{};
+
+arena: std.mem.Allocator,
+file: File.Index,
+
+/// keep in sync with "CAT_" constants in main.js
+pub const Category = union(enum(u8)) {
+ namespace: Ast.Node.Index,
+ global_variable: Ast.Node.Index,
+ function: Ast.Node.Index,
+ primitive: Ast.Node.Index,
+ error_set: Ast.Node.Index,
+ global_const: Ast.Node.Index,
+ alias: Decl.Index,
+ type,
+
+ pub const Tag = @typeInfo(Category).Union.tag_type.?;
+};
+
+pub const File = struct {
+ ast: Ast,
+ /// Maps identifiers to the declarations they point to.
+ ident_decls: std.AutoArrayHashMapUnmanaged(Ast.TokenIndex, Ast.Node.Index) = .{},
+ /// Maps field access identifiers to the containing field access node.
+ token_parents: std.AutoArrayHashMapUnmanaged(Ast.TokenIndex, Ast.Node.Index) = .{},
+ /// Maps declarations to their global index.
+ node_decls: std.AutoArrayHashMapUnmanaged(Ast.Node.Index, Decl.Index) = .{},
+ /// Maps function declarations to doctests.
+ doctests: std.AutoArrayHashMapUnmanaged(Ast.Node.Index, Ast.Node.Index) = .{},
+
+ pub fn lookup_token(file: *File, token: Ast.TokenIndex) Decl.Index {
+ const decl_node = file.ident_decls.get(token) orelse return .none;
+ return file.node_decls.get(decl_node) orelse return .none;
+ }
+
+ pub const Index = enum(u32) {
+ _,
+
+ fn add_decl(i: Index, node: Ast.Node.Index, parent_decl: Decl.Index) Oom!Decl.Index {
+ try decls.append(gpa, .{
+ .ast_node = node,
+ .file = i,
+ .parent = parent_decl,
+ });
+ const decl_index: Decl.Index = @enumFromInt(decls.items.len - 1);
+ try i.get().node_decls.put(gpa, node, decl_index);
+ return decl_index;
+ }
+
+ pub fn get(i: File.Index) *File {
+ return &files.values()[@intFromEnum(i)];
+ }
+
+ pub fn get_ast(i: File.Index) *Ast {
+ return &i.get().ast;
+ }
+
+ pub fn path(i: File.Index) []const u8 {
+ return files.keys()[@intFromEnum(i)];
+ }
+
+ pub fn findRootDecl(file_index: File.Index) Decl.Index {
+ return file_index.get().node_decls.values()[0];
+ }
+
+ pub fn categorize_decl(file_index: File.Index, node: Ast.Node.Index) Category {
+ const ast = file_index.get_ast();
+ const node_tags = ast.nodes.items(.tag);
+ const token_tags = ast.tokens.items(.tag);
+ switch (node_tags[node]) {
+ .root => return .{ .namespace = node },
+
+ .global_var_decl,
+ .local_var_decl,
+ .simple_var_decl,
+ .aligned_var_decl,
+ => {
+ const var_decl = ast.fullVarDecl(node).?;
+ if (token_tags[var_decl.ast.mut_token] == .keyword_var)
+ return .{ .global_variable = node };
+
+ return categorize_expr(file_index, var_decl.ast.init_node);
+ },
+
+ .fn_proto,
+ .fn_proto_multi,
+ .fn_proto_one,
+ .fn_proto_simple,
+ .fn_decl,
+ => return .{ .function = node },
+
+ else => unreachable,
+ }
+ }
+
+ pub fn categorize_expr(file_index: File.Index, node: Ast.Node.Index) Category {
+ const ast = file_index.get_ast();
+ const node_tags = ast.nodes.items(.tag);
+ const node_datas = ast.nodes.items(.data);
+ return switch (node_tags[node]) {
+ .container_decl,
+ .container_decl_trailing,
+ .container_decl_arg,
+ .container_decl_arg_trailing,
+ .container_decl_two,
+ .container_decl_two_trailing,
+ .tagged_union,
+ .tagged_union_trailing,
+ .tagged_union_enum_tag,
+ .tagged_union_enum_tag_trailing,
+ .tagged_union_two,
+ .tagged_union_two_trailing,
+ => .{ .namespace = node },
+
+ .error_set_decl,
+ => .{ .error_set = node },
+
+ .identifier => {
+ const name_token = ast.nodes.items(.main_token)[node];
+ const ident_name = ast.tokenSlice(name_token);
+ if (std.zig.primitives.isPrimitive(ident_name)) {
+ return .{ .primitive = node };
+ }
+
+ const decl_index = file_index.get().lookup_token(name_token);
+ if (decl_index != .none) return .{ .alias = decl_index };
+
+ return .{ .global_const = node };
+ },
+
+ .field_access => {
+ // TODO:
+ //return .alias;
+ return .{ .global_const = node };
+ },
+
+ .builtin_call_two, .builtin_call_two_comma => {
+ if (node_datas[node].lhs == 0) {
+ const params = [_]Ast.Node.Index{};
+ return categorize_builtin_call(file_index, node, ¶ms);
+ } else if (node_datas[node].rhs == 0) {
+ const params = [_]Ast.Node.Index{node_datas[node].lhs};
+ return categorize_builtin_call(file_index, node, ¶ms);
+ } else {
+ const params = [_]Ast.Node.Index{ node_datas[node].lhs, node_datas[node].rhs };
+ return categorize_builtin_call(file_index, node, ¶ms);
+ }
+ },
+ .builtin_call, .builtin_call_comma => {
+ const params = ast.extra_data[node_datas[node].lhs..node_datas[node].rhs];
+ return categorize_builtin_call(file_index, node, params);
+ },
+
+ else => .{ .global_const = node },
+ };
+ }
+
+ fn categorize_builtin_call(
+ file_index: File.Index,
+ node: Ast.Node.Index,
+ params: []const Ast.Node.Index,
+ ) Category {
+ const ast = file_index.get_ast();
+ const main_tokens = ast.nodes.items(.main_token);
+ const builtin_token = main_tokens[node];
+ const builtin_name = ast.tokenSlice(builtin_token);
+ if (std.mem.eql(u8, builtin_name, "@import")) {
+ const str_lit_token = main_tokens[params[0]];
+ const str_bytes = ast.tokenSlice(str_lit_token);
+ const file_path = std.zig.string_literal.parseAlloc(gpa, str_bytes) catch @panic("OOM");
+ defer gpa.free(file_path);
+ const base_path = file_index.path();
+ const resolved_path = std.fs.path.resolvePosix(gpa, &.{
+ base_path, "..", file_path,
+ }) catch @panic("OOM");
+ defer gpa.free(resolved_path);
+ log.debug("from '{s}' @import '{s}' resolved='{s}'", .{
+ base_path, file_path, resolved_path,
+ });
+ if (Walk.files.getIndex(resolved_path)) |imported_file_index| {
+ return .{ .alias = Walk.File.Index.findRootDecl(@enumFromInt(imported_file_index)) };
+ } else {
+ log.warn("import target '{s}' did not resolve to any file", .{resolved_path});
+ }
+ } else if (std.mem.eql(u8, builtin_name, "@This")) {
+ if (file_index.get().node_decls.get(node)) |decl_index| {
+ return .{ .alias = decl_index };
+ } else {
+ log.warn("@This() is missing link to Decl.Index", .{});
+ }
+ }
+
+ return .{ .global_const = node };
+ }
+ };
+};
+
+pub const PackageIndex = enum(u32) {
+ _,
+};
+
+pub fn add_file(file_name: []const u8, bytes: []u8) !File.Index {
+ const ast = try parse(bytes);
+ const file_index: File.Index = @enumFromInt(files.entries.len);
+ try files.put(gpa, file_name, .{ .ast = ast });
+
+ if (ast.errors.len > 0) {
+ // TODO: expose this in the UI
+ log.err("can't index '{s}' because it has syntax errors", .{file_index.path()});
+ return file_index;
+ }
+
+ var arena_instance = std.heap.ArenaAllocator.init(gpa);
+ defer arena_instance.deinit();
+
+ var w: Walk = .{
+ .arena = arena_instance.allocator(),
+ .file = file_index,
+ };
+ var scope: Scope = .{ .tag = .top };
+
+ const decl_index = try file_index.add_decl(0, .none);
+ try struct_decl(&w, &scope, decl_index, ast.containerDeclRoot());
+
+ const file = file_index.get();
+ shrinkToFit(&file.ident_decls);
+ shrinkToFit(&file.token_parents);
+ shrinkToFit(&file.node_decls);
+ shrinkToFit(&file.doctests);
+
+ return file_index;
+}
+
+fn parse(source: []u8) Oom!Ast {
+ // Require every source file to end with a newline so that Zig's tokenizer
+ // can continue to require null termination and Autodoc implementation can
+ // avoid copying source bytes from the decompressed tar file buffer.
+ const adjusted_source: [:0]const u8 = s: {
+ if (source.len == 0)
+ break :s "";
+
+ assert(source[source.len - 1] == '\n');
+ source[source.len - 1] = 0;
+ break :s source[0 .. source.len - 1 :0];
+ };
+
+ return Ast.parse(gpa, adjusted_source, .zig);
+}
+
+const Scope = struct {
+ tag: Tag,
+
+ const Tag = enum { top, local, namespace };
+
+ const Local = struct {
+ base: Scope = .{ .tag = .local },
+ parent: *Scope,
+ var_node: Ast.Node.Index,
+ };
+
+ const Namespace = struct {
+ base: Scope = .{ .tag = .namespace },
+ parent: *Scope,
+ names: std.StringArrayHashMapUnmanaged(Ast.Node.Index) = .{},
+ doctests: std.StringArrayHashMapUnmanaged(Ast.Node.Index) = .{},
+ decl_index: Decl.Index,
+ };
+
+ fn getNamespaceDecl(start_scope: *Scope) Decl.Index {
+ var it: *Scope = start_scope;
+ while (true) switch (it.tag) {
+ .top => unreachable,
+ .local => {
+ const local = @fieldParentPtr(Local, "base", it);
+ it = local.parent;
+ },
+ .namespace => {
+ const namespace = @fieldParentPtr(Namespace, "base", it);
+ return namespace.decl_index;
+ },
+ };
+ }
+
+ fn lookup(start_scope: *Scope, ast: *const Ast, name: []const u8) ?Ast.Node.Index {
+ const main_tokens = ast.nodes.items(.main_token);
+ var it: *Scope = start_scope;
+ while (true) switch (it.tag) {
+ .top => break,
+ .local => {
+ const local = @fieldParentPtr(Local, "base", it);
+ const name_token = main_tokens[local.var_node] + 1;
+ const ident_name = ast.tokenSlice(name_token);
+ if (std.mem.eql(u8, ident_name, name)) {
+ return local.var_node;
+ }
+ it = local.parent;
+ },
+ .namespace => {
+ const namespace = @fieldParentPtr(Namespace, "base", it);
+ if (namespace.names.get(name)) |node| {
+ return node;
+ }
+ it = namespace.parent;
+ },
+ };
+ return null;
+ }
+};
+
+fn struct_decl(
+ w: *Walk,
+ scope: *Scope,
+ parent_decl: Decl.Index,
+ container_decl: Ast.full.ContainerDecl,
+) Oom!void {
+ const ast = w.file.get_ast();
+ const node_tags = ast.nodes.items(.tag);
+ const node_datas = ast.nodes.items(.data);
+
+ var namespace: Scope.Namespace = .{
+ .parent = scope,
+ .decl_index = parent_decl,
+ };
+ try w.scanDecls(&namespace, container_decl.ast.members);
+
+ for (container_decl.ast.members) |member| switch (node_tags[member]) {
+ .container_field_init,
+ .container_field_align,
+ .container_field,
+ => try w.container_field(&namespace.base, parent_decl, ast.fullContainerField(member).?),
+
+ .fn_proto,
+ .fn_proto_multi,
+ .fn_proto_one,
+ .fn_proto_simple,
+ .fn_decl,
+ => {
+ var buf: [1]Ast.Node.Index = undefined;
+ const full = ast.fullFnProto(&buf, member).?;
+ const fn_name_token = full.ast.fn_token + 1;
+ const fn_name = ast.tokenSlice(fn_name_token);
+ if (namespace.doctests.get(fn_name)) |doctest_node| {
+ try w.file.get().doctests.put(gpa, member, doctest_node);
+ }
+ const decl_index = try w.file.add_decl(member, parent_decl);
+ const body = if (node_tags[member] == .fn_decl) node_datas[member].rhs else 0;
+ try w.fn_decl(&namespace.base, decl_index, body, full);
+ },
+
+ .global_var_decl,
+ .local_var_decl,
+ .simple_var_decl,
+ .aligned_var_decl,
+ => {
+ const decl_index = try w.file.add_decl(member, parent_decl);
+ try w.global_var_decl(&namespace.base, decl_index, ast.fullVarDecl(member).?);
+ },
+
+ .@"comptime",
+ .@"usingnamespace",
+ => try w.expr(&namespace.base, parent_decl, node_datas[member].lhs),
+
+ .test_decl => try w.expr(&namespace.base, parent_decl, node_datas[member].rhs),
+
+ else => unreachable,
+ };
+}
+
+fn comptime_decl(
+ w: *Walk,
+ scope: *Scope,
+ parent_decl: Decl.Index,
+ full: Ast.full.VarDecl,
+) Oom!void {
+ try w.expr(scope, parent_decl, full.ast.type_node);
+ try w.maybe_expr(scope, parent_decl, full.ast.align_node);
+ try w.maybe_expr(scope, parent_decl, full.ast.addrspace_node);
+ try w.maybe_expr(scope, parent_decl, full.ast.section_node);
+ try w.expr(scope, parent_decl, full.ast.init_node);
+}
+
+fn global_var_decl(
+ w: *Walk,
+ scope: *Scope,
+ parent_decl: Decl.Index,
+ full: Ast.full.VarDecl,
+) Oom!void {
+ try w.maybe_expr(scope, parent_decl, full.ast.type_node);
+ try w.maybe_expr(scope, parent_decl, full.ast.align_node);
+ try w.maybe_expr(scope, parent_decl, full.ast.addrspace_node);
+ try w.maybe_expr(scope, parent_decl, full.ast.section_node);
+ try w.maybe_expr(scope, parent_decl, full.ast.init_node);
+}
+
+fn container_field(
+ w: *Walk,
+ scope: *Scope,
+ parent_decl: Decl.Index,
+ full: Ast.full.ContainerField,
+) Oom!void {
+ try w.maybe_expr(scope, parent_decl, full.ast.type_expr);
+ try w.maybe_expr(scope, parent_decl, full.ast.align_expr);
+ try w.maybe_expr(scope, parent_decl, full.ast.value_expr);
+}
+
+fn fn_decl(
+ w: *Walk,
+ scope: *Scope,
+ parent_decl: Decl.Index,
+ body: Ast.Node.Index,
+ full: Ast.full.FnProto,
+) Oom!void {
+ for (full.ast.params) |param| {
+ try expr(w, scope, parent_decl, param);
+ }
+ try expr(w, scope, parent_decl, full.ast.return_type);
+ try maybe_expr(w, scope, parent_decl, full.ast.align_expr);
+ try maybe_expr(w, scope, parent_decl, full.ast.addrspace_expr);
+ try maybe_expr(w, scope, parent_decl, full.ast.section_expr);
+ try maybe_expr(w, scope, parent_decl, full.ast.callconv_expr);
+ try maybe_expr(w, scope, parent_decl, body);
+}
+
+fn maybe_expr(w: *Walk, scope: *Scope, parent_decl: Decl.Index, node: Ast.Node.Index) Oom!void {
+ if (node != 0) return expr(w, scope, parent_decl, node);
+}
+
+fn expr(w: *Walk, scope: *Scope, parent_decl: Decl.Index, node: Ast.Node.Index) Oom!void {
+ assert(node != 0);
+ const ast = w.file.get_ast();
+ const node_tags = ast.nodes.items(.tag);
+ const node_datas = ast.nodes.items(.data);
+ const main_tokens = ast.nodes.items(.main_token);
+ switch (node_tags[node]) {
+ .root => unreachable, // Top-level declaration.
+ .@"usingnamespace" => unreachable, // Top-level declaration.
+ .test_decl => unreachable, // Top-level declaration.
+ .container_field_init => unreachable, // Top-level declaration.
+ .container_field_align => unreachable, // Top-level declaration.
+ .container_field => unreachable, // Top-level declaration.
+ .fn_decl => unreachable, // Top-level declaration.
+
+ .global_var_decl => unreachable, // Handled in `block`.
+ .local_var_decl => unreachable, // Handled in `block`.
+ .simple_var_decl => unreachable, // Handled in `block`.
+ .aligned_var_decl => unreachable, // Handled in `block`.
+ .@"defer" => unreachable, // Handled in `block`.
+ .@"errdefer" => unreachable, // Handled in `block`.
+
+ .switch_case => unreachable, // Handled in `switchExpr`.
+ .switch_case_inline => unreachable, // Handled in `switchExpr`.
+ .switch_case_one => unreachable, // Handled in `switchExpr`.
+ .switch_case_inline_one => unreachable, // Handled in `switchExpr`.
+
+ .asm_output => unreachable, // Handled in `asmExpr`.
+ .asm_input => unreachable, // Handled in `asmExpr`.
+
+ .for_range => unreachable, // Handled in `forExpr`.
+
+ .assign,
+ .assign_shl,
+ .assign_shl_sat,
+ .assign_shr,
+ .assign_bit_and,
+ .assign_bit_or,
+ .assign_bit_xor,
+ .assign_div,
+ .assign_sub,
+ .assign_sub_wrap,
+ .assign_sub_sat,
+ .assign_mod,
+ .assign_add,
+ .assign_add_wrap,
+ .assign_add_sat,
+ .assign_mul,
+ .assign_mul_wrap,
+ .assign_mul_sat,
+ .shl,
+ .shr,
+ .add,
+ .add_wrap,
+ .add_sat,
+ .sub,
+ .sub_wrap,
+ .sub_sat,
+ .mul,
+ .mul_wrap,
+ .mul_sat,
+ .div,
+ .mod,
+ .shl_sat,
+
+ .bit_and,
+ .bit_or,
+ .bit_xor,
+ .bang_equal,
+ .equal_equal,
+ .greater_than,
+ .greater_or_equal,
+ .less_than,
+ .less_or_equal,
+ .array_cat,
+
+ .array_mult,
+ .error_union,
+ .merge_error_sets,
+ .bool_and,
+ .bool_or,
+ .@"catch",
+ .@"orelse",
+ .array_type,
+ .array_access,
+ .switch_range,
+ => {
+ try expr(w, scope, parent_decl, node_datas[node].lhs);
+ try expr(w, scope, parent_decl, node_datas[node].rhs);
+ },
+
+ .assign_destructure => {
+ const extra_index = node_datas[node].lhs;
+ const lhs_count = ast.extra_data[extra_index];
+ const lhs_nodes: []const Ast.Node.Index = @ptrCast(ast.extra_data[extra_index + 1 ..][0..lhs_count]);
+ const rhs = node_datas[node].rhs;
+ for (lhs_nodes) |lhs_node| try expr(w, scope, parent_decl, lhs_node);
+ _ = try expr(w, scope, parent_decl, rhs);
+ },
+
+ .bool_not,
+ .bit_not,
+ .negation,
+ .negation_wrap,
+ .@"return",
+ .deref,
+ .address_of,
+ .optional_type,
+ .unwrap_optional,
+ .grouped_expression,
+ .@"comptime",
+ .@"nosuspend",
+ .@"suspend",
+ .@"await",
+ .@"resume",
+ .@"try",
+ => try maybe_expr(w, scope, parent_decl, node_datas[node].lhs),
+
+ .anyframe_type,
+ .@"break",
+ => try maybe_expr(w, scope, parent_decl, node_datas[node].rhs),
+
+ .identifier => {
+ const ident_token = main_tokens[node];
+ const ident_name = ast.tokenSlice(ident_token);
+ if (scope.lookup(ast, ident_name)) |var_node| {
+ try w.file.get().ident_decls.put(gpa, ident_token, var_node);
+ }
+ },
+ .field_access => {
+ const object_node = node_datas[node].lhs;
+ const dot_token = main_tokens[node];
+ const field_ident = dot_token + 1;
+ try w.file.get().token_parents.put(gpa, field_ident, node);
+ // This will populate the left-most field object if it is an
+ // identifier, allowing rendering code to piece together the link.
+ try expr(w, scope, parent_decl, object_node);
+ },
+
+ .string_literal,
+ .multiline_string_literal,
+ .number_literal,
+ .unreachable_literal,
+ .enum_literal,
+ .error_value,
+ .anyframe_literal,
+ .@"continue",
+ .char_literal,
+ .error_set_decl,
+ => {},
+
+ .asm_simple,
+ .@"asm",
+ => {
+ const full = ast.fullAsm(node).?;
+ for (full.ast.items) |n| {
+ // TODO handle .asm_input, .asm_output
+ _ = n;
+ }
+ try expr(w, scope, parent_decl, full.ast.template);
+ },
+
+ .builtin_call_two, .builtin_call_two_comma => {
+ if (node_datas[node].lhs == 0) {
+ const params = [_]Ast.Node.Index{};
+ return builtin_call(w, scope, parent_decl, node, ¶ms);
+ } else if (node_datas[node].rhs == 0) {
+ const params = [_]Ast.Node.Index{node_datas[node].lhs};
+ return builtin_call(w, scope, parent_decl, node, ¶ms);
+ } else {
+ const params = [_]Ast.Node.Index{ node_datas[node].lhs, node_datas[node].rhs };
+ return builtin_call(w, scope, parent_decl, node, ¶ms);
+ }
+ },
+ .builtin_call, .builtin_call_comma => {
+ const params = ast.extra_data[node_datas[node].lhs..node_datas[node].rhs];
+ return builtin_call(w, scope, parent_decl, node, params);
+ },
+
+ .call_one,
+ .call_one_comma,
+ .async_call_one,
+ .async_call_one_comma,
+ .call,
+ .call_comma,
+ .async_call,
+ .async_call_comma,
+ => {
+ var buf: [1]Ast.Node.Index = undefined;
+ const full = ast.fullCall(&buf, node).?;
+ try expr(w, scope, parent_decl, full.ast.fn_expr);
+ for (full.ast.params) |param| {
+ try expr(w, scope, parent_decl, param);
+ }
+ },
+
+ .if_simple,
+ .@"if",
+ => {
+ const full = ast.fullIf(node).?;
+ try expr(w, scope, parent_decl, full.ast.cond_expr);
+ try expr(w, scope, parent_decl, full.ast.then_expr);
+ try maybe_expr(w, scope, parent_decl, full.ast.else_expr);
+ },
+
+ .while_simple,
+ .while_cont,
+ .@"while",
+ => {
+ try while_expr(w, scope, parent_decl, ast.fullWhile(node).?);
+ },
+
+ .for_simple, .@"for" => {
+ const full = ast.fullFor(node).?;
+ for (full.ast.inputs) |input| {
+ if (node_tags[input] == .for_range) {
+ try expr(w, scope, parent_decl, node_datas[input].lhs);
+ try maybe_expr(w, scope, parent_decl, node_datas[input].rhs);
+ } else {
+ try expr(w, scope, parent_decl, input);
+ }
+ }
+ try expr(w, scope, parent_decl, full.ast.then_expr);
+ try maybe_expr(w, scope, parent_decl, full.ast.else_expr);
+ },
+
+ .slice => return slice(w, scope, parent_decl, ast.slice(node)),
+ .slice_open => return slice(w, scope, parent_decl, ast.sliceOpen(node)),
+ .slice_sentinel => return slice(w, scope, parent_decl, ast.sliceSentinel(node)),
+
+ .block_two, .block_two_semicolon => {
+ const statements = [2]Ast.Node.Index{ node_datas[node].lhs, node_datas[node].rhs };
+ if (node_datas[node].lhs == 0) {
+ return block(w, scope, parent_decl, statements[0..0]);
+ } else if (node_datas[node].rhs == 0) {
+ return block(w, scope, parent_decl, statements[0..1]);
+ } else {
+ return block(w, scope, parent_decl, statements[0..2]);
+ }
+ },
+ .block, .block_semicolon => {
+ const statements = ast.extra_data[node_datas[node].lhs..node_datas[node].rhs];
+ return block(w, scope, parent_decl, statements);
+ },
+
+ .ptr_type_aligned,
+ .ptr_type_sentinel,
+ .ptr_type,
+ .ptr_type_bit_range,
+ => {
+ const full = ast.fullPtrType(node).?;
+ try maybe_expr(w, scope, parent_decl, full.ast.align_node);
+ try maybe_expr(w, scope, parent_decl, full.ast.addrspace_node);
+ try maybe_expr(w, scope, parent_decl, full.ast.sentinel);
+ try maybe_expr(w, scope, parent_decl, full.ast.bit_range_start);
+ try maybe_expr(w, scope, parent_decl, full.ast.bit_range_end);
+ try expr(w, scope, parent_decl, full.ast.child_type);
+ },
+
+ .container_decl,
+ .container_decl_trailing,
+ .container_decl_arg,
+ .container_decl_arg_trailing,
+ .container_decl_two,
+ .container_decl_two_trailing,
+ .tagged_union,
+ .tagged_union_trailing,
+ .tagged_union_enum_tag,
+ .tagged_union_enum_tag_trailing,
+ .tagged_union_two,
+ .tagged_union_two_trailing,
+ => {
+ var buf: [2]Ast.Node.Index = undefined;
+ return struct_decl(w, scope, parent_decl, ast.fullContainerDecl(&buf, node).?);
+ },
+
+ .array_type_sentinel => {
+ const extra = ast.extraData(node_datas[node].rhs, Ast.Node.ArrayTypeSentinel);
+ try expr(w, scope, parent_decl, node_datas[node].lhs);
+ try expr(w, scope, parent_decl, extra.elem_type);
+ try expr(w, scope, parent_decl, extra.sentinel);
+ },
+ .@"switch", .switch_comma => {
+ const operand_node = node_datas[node].lhs;
+ try expr(w, scope, parent_decl, operand_node);
+ const extra = ast.extraData(node_datas[node].rhs, Ast.Node.SubRange);
+ const case_nodes = ast.extra_data[extra.start..extra.end];
+ for (case_nodes) |case_node| {
+ const case = ast.fullSwitchCase(case_node).?;
+ for (case.ast.values) |value_node| {
+ try expr(w, scope, parent_decl, value_node);
+ }
+ try expr(w, scope, parent_decl, case.ast.target_expr);
+ }
+ },
+
+ .array_init_one,
+ .array_init_one_comma,
+ .array_init_dot_two,
+ .array_init_dot_two_comma,
+ .array_init_dot,
+ .array_init_dot_comma,
+ .array_init,
+ .array_init_comma,
+ => {
+ var buf: [2]Ast.Node.Index = undefined;
+ const full = ast.fullArrayInit(&buf, node).?;
+ try maybe_expr(w, scope, parent_decl, full.ast.type_expr);
+ for (full.ast.elements) |elem| {
+ try expr(w, scope, parent_decl, elem);
+ }
+ },
+
+ .struct_init_one,
+ .struct_init_one_comma,
+ .struct_init_dot_two,
+ .struct_init_dot_two_comma,
+ .struct_init_dot,
+ .struct_init_dot_comma,
+ .struct_init,
+ .struct_init_comma,
+ => {
+ var buf: [2]Ast.Node.Index = undefined;
+ const full = ast.fullStructInit(&buf, node).?;
+ try maybe_expr(w, scope, parent_decl, full.ast.type_expr);
+ for (full.ast.fields) |field| {
+ try expr(w, scope, parent_decl, field);
+ }
+ },
+
+ .fn_proto_simple,
+ .fn_proto_multi,
+ .fn_proto_one,
+ .fn_proto,
+ => {
+ var buf: [1]Ast.Node.Index = undefined;
+ return fn_decl(w, scope, parent_decl, 0, ast.fullFnProto(&buf, node).?);
+ },
+ }
+}
+
+fn slice(w: *Walk, scope: *Scope, parent_decl: Decl.Index, full: Ast.full.Slice) Oom!void {
+ try expr(w, scope, parent_decl, full.ast.sliced);
+ try expr(w, scope, parent_decl, full.ast.start);
+ try maybe_expr(w, scope, parent_decl, full.ast.end);
+ try maybe_expr(w, scope, parent_decl, full.ast.sentinel);
+}
+
+fn builtin_call(
+ w: *Walk,
+ scope: *Scope,
+ parent_decl: Decl.Index,
+ node: Ast.Node.Index,
+ params: []const Ast.Node.Index,
+) Oom!void {
+ const ast = w.file.get_ast();
+ const main_tokens = ast.nodes.items(.main_token);
+ const builtin_token = main_tokens[node];
+ const builtin_name = ast.tokenSlice(builtin_token);
+ if (std.mem.eql(u8, builtin_name, "@This")) {
+ try w.file.get().node_decls.put(gpa, node, scope.getNamespaceDecl());
+ }
+
+ for (params) |param| {
+ try expr(w, scope, parent_decl, param);
+ }
+}
+
+fn block(
+ w: *Walk,
+ parent_scope: *Scope,
+ parent_decl: Decl.Index,
+ statements: []const Ast.Node.Index,
+) Oom!void {
+ const ast = w.file.get_ast();
+ const arena = w.arena;
+ const node_tags = ast.nodes.items(.tag);
+ const node_datas = ast.nodes.items(.data);
+
+ var scope = parent_scope;
+
+ for (statements) |node| {
+ switch (node_tags[node]) {
+ .global_var_decl,
+ .local_var_decl,
+ .simple_var_decl,
+ .aligned_var_decl,
+ => {
+ const full = ast.fullVarDecl(node).?;
+ try global_var_decl(w, scope, parent_decl, full);
+ const local = try arena.create(Scope.Local);
+ local.* = .{
+ .parent = scope,
+ .var_node = node,
+ };
+ scope = &local.base;
+ },
+
+ .assign_destructure => {
+ // TODO
+ },
+
+ .grouped_expression => try expr(w, scope, parent_decl, node_datas[node].lhs),
+
+ .@"defer",
+ .@"errdefer",
+ => try expr(w, scope, parent_decl, node_datas[node].rhs),
+
+ else => try expr(w, scope, parent_decl, node),
+ }
+ }
+}
+
+fn while_expr(w: *Walk, scope: *Scope, parent_decl: Decl.Index, full: Ast.full.While) Oom!void {
+ try expr(w, scope, parent_decl, full.ast.cond_expr);
+ try maybe_expr(w, scope, parent_decl, full.ast.cont_expr);
+ try expr(w, scope, parent_decl, full.ast.then_expr);
+ try maybe_expr(w, scope, parent_decl, full.ast.else_expr);
+}
+
+fn scanDecls(w: *Walk, namespace: *Scope.Namespace, members: []const Ast.Node.Index) Oom!void {
+ const arena = w.arena;
+ const ast = w.file.get_ast();
+ const node_tags = ast.nodes.items(.tag);
+ const main_tokens = ast.nodes.items(.main_token);
+ const token_tags = ast.tokens.items(.tag);
+ const node_datas = ast.nodes.items(.data);
+
+ for (members) |member_node| {
+ const name_token = switch (node_tags[member_node]) {
+ .global_var_decl,
+ .local_var_decl,
+ .simple_var_decl,
+ .aligned_var_decl,
+ => main_tokens[member_node] + 1,
+
+ .fn_proto_simple,
+ .fn_proto_multi,
+ .fn_proto_one,
+ .fn_proto,
+ .fn_decl,
+ => blk: {
+ const ident = main_tokens[member_node] + 1;
+ if (token_tags[ident] != .identifier) continue;
+ break :blk ident;
+ },
+
+ .test_decl => {
+ const ident_token = node_datas[member_node].lhs;
+ const is_doctest = token_tags[ident_token] == .identifier;
+ if (is_doctest) {
+ const token_bytes = ast.tokenSlice(ident_token);
+ try namespace.doctests.put(arena, token_bytes, member_node);
+ }
+ continue;
+ },
+
+ else => continue,
+ };
+
+ const token_bytes = ast.tokenSlice(name_token);
+ try namespace.names.put(arena, token_bytes, member_node);
+ }
+}
+
+//test {
+// const gpa = std.testing.allocator;
+//
+// var arena_instance = std.heap.ArenaAllocator.init(gpa);
+// defer arena_instance.deinit();
+// const arena = arena_instance.allocator();
+//
+// // example test command:
+// // zig test --dep input.zig -Mroot=src/Walk.zig -Minput.zig=/home/andy/dev/zig/lib/std/fs/File/zig
+// var ast = try Ast.parse(gpa, @embedFile("input.zig"), .zig);
+// defer ast.deinit(gpa);
+//
+// var w: Walk = .{
+// .arena = arena,
+// .token_links = .{},
+// .ast = &ast,
+// };
+//
+// try w.root();
+//}
+
+const Walk = @This();
+const std = @import("std");
+const Ast = std.zig.Ast;
+const assert = std.debug.assert;
+const Decl = @import("Decl.zig");
+const log = std.log;
+const gpa = std.heap.wasm_allocator;
+const Oom = error{OutOfMemory};
+
+fn shrinkToFit(m: anytype) void {
+ m.shrinkAndFree(gpa, m.entries.len);
+}
diff --git a/lib/docs/wasm/main.zig b/lib/docs/wasm/main.zig
new file mode 100644
index 0000000000000000000000000000000000000000..d5f1f7951c524531a13f06cab2ea582ad7b6ef4b
--- /dev/null
+++ b/lib/docs/wasm/main.zig
@@ -0,0 +1,920 @@
+const gpa = std.heap.wasm_allocator;
+
+const std = @import("std");
+const log = std.log;
+const assert = std.debug.assert;
+const Ast = std.zig.Ast;
+const Walk = @import("Walk.zig");
+const markdown = @import("markdown.zig");
+const Decl = @import("Decl.zig");
+
+const js = struct {
+ extern "js" fn log(ptr: [*]const u8, len: usize) void;
+ extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
+};
+
+pub const std_options: std.Options = .{
+ .logFn = logFn,
+ //.log_level = .debug,
+};
+
+pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn {
+ _ = st;
+ _ = addr;
+ log.err("panic: {s}", .{msg});
+ @trap();
+}
+
+fn logFn(
+ comptime message_level: log.Level,
+ comptime scope: @TypeOf(.enum_literal),
+ comptime format: []const u8,
+ args: anytype,
+) void {
+ const level_txt = comptime message_level.asText();
+ const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
+ var buf: [500]u8 = undefined;
+ const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: {
+ buf[buf.len - 3 ..][0..3].* = "...".*;
+ break :l &buf;
+ };
+ js.log(line.ptr, line.len);
+}
+
+export fn alloc(n: usize) [*]u8 {
+ const slice = gpa.alloc(u8, n) catch @panic("OOM");
+ return slice.ptr;
+}
+
+export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
+ const tar_bytes = tar_ptr[0..tar_len];
+ //log.debug("received {d} bytes of tar file", .{tar_bytes.len});
+
+ unpack_inner(tar_bytes) catch |err| {
+ fatal("unable to unpack tar: {s}", .{@errorName(err)});
+ };
+}
+
+var query_string: std.ArrayListUnmanaged(u8) = .{};
+var query_results: std.ArrayListUnmanaged(Decl.Index) = .{};
+
+/// Resizes the query string to be the correct length; returns the pointer to
+/// the query string.
+export fn query_begin(query_string_len: usize) [*]u8 {
+ query_string.resize(gpa, query_string_len) catch @panic("OOM");
+ return query_string.items.ptr;
+}
+
+/// Executes the query. Returns the pointer to the query results which is an
+/// array of u32.
+/// The first element is the length of the array.
+/// Subsequent elements are Decl.Index values which are all public
+/// declarations.
+export fn query_exec(ignore_case: bool) [*]Decl.Index {
+ const query = query_string.items;
+ log.debug("querying '{s}'", .{query});
+ query_exec_fallible(query, ignore_case) catch |err| switch (err) {
+ error.OutOfMemory => @panic("OOM"),
+ };
+ query_results.items[0] = @enumFromInt(query_results.items.len - 1);
+ return query_results.items.ptr;
+}
+
+const max_matched_items = 2000;
+
+fn query_exec_fallible(query: []const u8, ignore_case: bool) !void {
+ const Score = packed struct(u32) {
+ points: u16,
+ segments: u16,
+ };
+ const g = struct {
+ var full_path_search_text: std.ArrayListUnmanaged(u8) = .{};
+ var full_path_search_text_lower: std.ArrayListUnmanaged(u8) = .{};
+ var doc_search_text: std.ArrayListUnmanaged(u8) = .{};
+ /// Each element matches a corresponding query_results element.
+ var scores: std.ArrayListUnmanaged(Score) = .{};
+ };
+
+ // First element stores the size of the list.
+ try query_results.resize(gpa, 1);
+ // Corresponding point value is meaningless and therefore undefined.
+ try g.scores.resize(gpa, 1);
+
+ decl_loop: for (Walk.decls.items, 0..) |*decl, decl_index| {
+ const info = decl.extra_info();
+ if (!info.is_pub) continue;
+
+ try decl.reset_with_path(&g.full_path_search_text);
+ if (decl.parent != .none)
+ try Decl.append_parent_ns(&g.full_path_search_text, decl.parent);
+ try g.full_path_search_text.appendSlice(gpa, info.name);
+
+ try g.full_path_search_text_lower.resize(gpa, g.full_path_search_text.items.len);
+ @memcpy(g.full_path_search_text_lower.items, g.full_path_search_text.items);
+
+ const ast = decl.file.get_ast();
+ try collect_docs(&g.doc_search_text, ast, info.first_doc_comment);
+
+ if (ignore_case) {
+ ascii_lower(g.full_path_search_text_lower.items);
+ ascii_lower(g.doc_search_text.items);
+ }
+
+ var it = std.mem.tokenizeScalar(u8, query, ' ');
+ var points: u16 = 0;
+ var bypass_limit = false;
+ while (it.next()) |term| {
+ // exact, case sensitive match of full decl path
+ if (std.mem.eql(u8, g.full_path_search_text.items, term)) {
+ points += 4;
+ bypass_limit = true;
+ continue;
+ }
+ // exact, case sensitive match of just decl name
+ if (std.mem.eql(u8, info.name, term)) {
+ points += 3;
+ bypass_limit = true;
+ continue;
+ }
+ // substring, case insensitive match of full decl path
+ if (std.mem.indexOf(u8, g.full_path_search_text_lower.items, term) != null) {
+ points += 2;
+ continue;
+ }
+ if (std.mem.indexOf(u8, g.doc_search_text.items, term) != null) {
+ points += 1;
+ continue;
+ }
+ continue :decl_loop;
+ }
+
+ if (query_results.items.len < max_matched_items or bypass_limit) {
+ try query_results.append(gpa, @enumFromInt(decl_index));
+ try g.scores.append(gpa, .{
+ .points = points,
+ .segments = @intCast(count_scalar(g.full_path_search_text.items, '.')),
+ });
+ }
+ }
+
+ const sort_context: struct {
+ pub fn swap(sc: @This(), a_index: usize, b_index: usize) void {
+ _ = sc;
+ std.mem.swap(Score, &g.scores.items[a_index], &g.scores.items[b_index]);
+ std.mem.swap(Decl.Index, &query_results.items[a_index], &query_results.items[b_index]);
+ }
+
+ pub fn lessThan(sc: @This(), a_index: usize, b_index: usize) bool {
+ _ = sc;
+ const a_score = g.scores.items[a_index];
+ const b_score = g.scores.items[b_index];
+ if (b_score.points < a_score.points) {
+ return true;
+ } else if (b_score.points > a_score.points) {
+ return false;
+ } else if (a_score.segments < b_score.segments) {
+ return true;
+ } else if (a_score.segments > b_score.segments) {
+ return false;
+ } else {
+ const a_decl = query_results.items[a_index];
+ const b_decl = query_results.items[b_index];
+ const a_file_path = a_decl.get().file.path();
+ const b_file_path = b_decl.get().file.path();
+ // TODO Also check the local namespace inside the file
+ return std.mem.lessThan(u8, b_file_path, a_file_path);
+ }
+ }
+ } = .{};
+
+ std.mem.sortUnstableContext(1, query_results.items.len, sort_context);
+
+ if (query_results.items.len > max_matched_items)
+ query_results.shrinkRetainingCapacity(max_matched_items);
+}
+
+const String = Slice(u8);
+
+fn Slice(T: type) type {
+ return packed struct(u64) {
+ ptr: u32,
+ len: u32,
+
+ fn init(s: []const T) @This() {
+ return .{
+ .ptr = @intFromPtr(s.ptr),
+ .len = s.len,
+ };
+ }
+ };
+}
+
+var string_result: std.ArrayListUnmanaged(u8) = .{};
+
+export fn decl_fields(decl_index: Decl.Index) Slice(Ast.Node.Index) {
+ return Slice(Ast.Node.Index).init(decl_fields_fallible(decl_index) catch @panic("OOM"));
+}
+
+fn decl_fields_fallible(decl_index: Decl.Index) ![]Ast.Node.Index {
+ const g = struct {
+ var result: std.ArrayListUnmanaged(Ast.Node.Index) = .{};
+ };
+ g.result.clearRetainingCapacity();
+ const decl = decl_index.get();
+ const ast = decl.file.get_ast();
+ const node_tags = ast.nodes.items(.tag);
+ const value_node = decl.value_node() orelse return &.{};
+ var buf: [2]Ast.Node.Index = undefined;
+ const container_decl = ast.fullContainerDecl(&buf, value_node) orelse return &.{};
+ for (container_decl.ast.members) |member_node| switch (node_tags[member_node]) {
+ .container_field_init,
+ .container_field_align,
+ .container_field,
+ => try g.result.append(gpa, member_node),
+
+ else => continue,
+ };
+ return g.result.items;
+}
+
+export fn decl_field_html(decl_index: Decl.Index, field_node: Ast.Node.Index) String {
+ string_result.clearRetainingCapacity();
+ decl_field_html_fallible(&string_result, decl_index, field_node) catch @panic("OOM");
+ return String.init(string_result.items);
+}
+
+fn decl_field_html_fallible(
+ out: *std.ArrayListUnmanaged(u8),
+ decl_index: Decl.Index,
+ field_node: Ast.Node.Index,
+) !void {
+ const decl = decl_index.get();
+ const ast = decl.file.get_ast();
+ try out.appendSlice(gpa, "
");
+ try file_source_html(decl.file, out, field_node);
+ try out.appendSlice(gpa, "
");
+
+ const field = ast.fullContainerField(field_node).?;
+ const first_doc_comment = Decl.findFirstDocComment(ast, field.firstToken());
+
+ if (ast.tokens.items(.tag)[first_doc_comment] == .doc_comment) {
+ try out.appendSlice(gpa, "
");
+ try render_docs(out, ast, first_doc_comment, false);
+ try out.appendSlice(gpa, "
");
+ }
+}
+
+export fn decl_fn_proto_html(decl_index: Decl.Index) String {
+ const decl = decl_index.get();
+ const ast = decl.file.get_ast();
+ const node_tags = ast.nodes.items(.tag);
+ const node_datas = ast.nodes.items(.data);
+ const proto_node = switch (node_tags[decl.ast_node]) {
+ .fn_decl => node_datas[decl.ast_node].lhs,
+
+ .fn_proto,
+ .fn_proto_one,
+ .fn_proto_simple,
+ .fn_proto_multi,
+ => decl.ast_node,
+
+ else => unreachable,
+ };
+
+ string_result.clearRetainingCapacity();
+ file_source_html(decl.file, &string_result, proto_node) catch |err| {
+ fatal("unable to render source: {s}", .{@errorName(err)});
+ };
+ return String.init(string_result.items);
+}
+
+export fn decl_source_html(decl_index: Decl.Index) String {
+ const decl = decl_index.get();
+
+ string_result.clearRetainingCapacity();
+ file_source_html(decl.file, &string_result, decl.ast_node) catch |err| {
+ fatal("unable to render source: {s}", .{@errorName(err)});
+ };
+ return String.init(string_result.items);
+}
+
+export fn decl_doctest_html(decl_index: Decl.Index) String {
+ const decl = decl_index.get();
+ const doctest_ast_node = decl.file.get().doctests.get(decl.ast_node) orelse
+ return String.init("");
+
+ string_result.clearRetainingCapacity();
+ file_source_html(decl.file, &string_result, doctest_ast_node) catch |err| {
+ fatal("unable to render source: {s}", .{@errorName(err)});
+ };
+ return String.init(string_result.items);
+}
+
+export fn decl_fqn(decl_index: Decl.Index) String {
+ const decl = decl_index.get();
+ decl.fqn(&string_result) catch @panic("OOM");
+ return String.init(string_result.items);
+}
+
+export fn decl_parent(decl_index: Decl.Index) Decl.Index {
+ const decl = decl_index.get();
+ return decl.parent;
+}
+
+export fn decl_file_path(decl_index: Decl.Index) String {
+ string_result.clearRetainingCapacity();
+ string_result.appendSlice(gpa, decl_index.get().file.path()) catch @panic("OOM");
+ return String.init(string_result.items);
+}
+
+export fn decl_category_name(decl_index: Decl.Index) String {
+ const decl = decl_index.get();
+ const ast = decl.file.get_ast();
+ const token_tags = ast.tokens.items(.tag);
+ const name = switch (decl.categorize()) {
+ .namespace => |node| {
+ const node_tags = ast.nodes.items(.tag);
+ if (node_tags[decl.ast_node] == .root)
+ return String.init("struct");
+ string_result.clearRetainingCapacity();
+ var buf: [2]Ast.Node.Index = undefined;
+ const container_decl = ast.fullContainerDecl(&buf, node).?;
+ if (container_decl.layout_token) |t| {
+ if (token_tags[t] == .keyword_extern) {
+ string_result.appendSlice(gpa, "extern ") catch @panic("OOM");
+ }
+ }
+ const main_token_tag = token_tags[container_decl.ast.main_token];
+ string_result.appendSlice(gpa, main_token_tag.lexeme().?) catch @panic("OOM");
+ return String.init(string_result.items);
+ },
+ .global_variable => "Global Variable",
+ .function => "Function",
+ .type => "Type",
+ .error_set => "Error Set",
+ .global_const => "Constant",
+ .primitive => "Primitive Value",
+ .alias => "Alias",
+ };
+ return String.init(name);
+}
+
+export fn decl_name(decl_index: Decl.Index) String {
+ const decl = decl_index.get();
+ string_result.clearRetainingCapacity();
+ const name = n: {
+ if (decl.parent == .none) {
+ // Then it is the root struct of a file.
+ break :n std.fs.path.stem(decl.file.path());
+ }
+ break :n decl.extra_info().name;
+ };
+ string_result.appendSlice(gpa, name) catch @panic("OOM");
+ return String.init(string_result.items);
+}
+
+export fn decl_docs_html(decl_index: Decl.Index, short: bool) String {
+ const decl = decl_index.get();
+ const ast = decl.file.get_ast();
+ string_result.clearRetainingCapacity();
+ render_docs(&string_result, ast, decl.extra_info().first_doc_comment, short) catch @panic("OOM");
+ return String.init(string_result.items);
+}
+
+fn collect_docs(
+ list: *std.ArrayListUnmanaged(u8),
+ ast: *const Ast,
+ first_doc_comment: Ast.TokenIndex,
+) Oom!void {
+ const token_tags = ast.tokens.items(.tag);
+ list.clearRetainingCapacity();
+ var it = first_doc_comment;
+ while (true) : (it += 1) switch (token_tags[it]) {
+ .doc_comment, .container_doc_comment => {
+ // It is tempting to trim this string but think carefully about how
+ // that will affect the markdown parser.
+ const line = ast.tokenSlice(it)[3..];
+ try list.appendSlice(gpa, line);
+ },
+ else => break,
+ };
+}
+
+fn render_docs(
+ out: *std.ArrayListUnmanaged(u8),
+ ast: *const Ast,
+ first_doc_comment: Ast.TokenIndex,
+ short: bool,
+) Oom!void {
+ const token_tags = ast.tokens.items(.tag);
+
+ var parser = try markdown.Parser.init(gpa);
+ defer parser.deinit();
+ var it = first_doc_comment;
+ while (true) : (it += 1) switch (token_tags[it]) {
+ .doc_comment, .container_doc_comment => {
+ const line = ast.tokenSlice(it)[3..];
+ if (short and line.len == 0) break;
+ try parser.feedLine(line);
+ },
+ else => break,
+ };
+
+ var parsed_doc = try parser.endInput();
+ defer parsed_doc.deinit(gpa);
+
+ const Writer = std.ArrayListUnmanaged(u8).Writer;
+ const Renderer = markdown.Renderer(Writer, void);
+ const renderer: Renderer = .{
+ .context = {},
+ .renderFn = struct {
+ fn render(
+ r: Renderer,
+ doc: markdown.Document,
+ node: markdown.Document.Node.Index,
+ writer: Writer,
+ ) !void {
+ const data = doc.nodes.items(.data)[@intFromEnum(node)];
+ switch (doc.nodes.items(.tag)[@intFromEnum(node)]) {
+ // TODO: detect identifier references (dotted paths) in
+ // these three node types and render them appropriately.
+ // Also, syntax highlighting can be applied in code blocks
+ // unless the tag says otherwise.
+ .code_block => {
+ const tag = doc.string(data.code_block.tag);
+ _ = tag;
+ const content = doc.string(data.code_block.content);
+ try writer.print("
{}
\n", .{markdown.fmtHtml(content)});
+ },
+ .code_span => {
+ const content = doc.string(data.text.content);
+ try writer.print("
{}", .{markdown.fmtHtml(content)});
+ },
+ .text => {
+ const content = doc.string(data.text.content);
+ try writer.print("{}", .{markdown.fmtHtml(content)});
+ },
+
+ else => try Renderer.renderDefault(r, doc, node, writer),
+ }
+ }
+ }.render,
+ };
+ try renderer.render(parsed_doc, out.writer(gpa));
+}
+
+export fn decl_type_html(decl_index: Decl.Index) String {
+ const decl = decl_index.get();
+ const ast = decl.file.get_ast();
+ string_result.clearRetainingCapacity();
+ _ = ast; // TODO
+ string_result.appendSlice(gpa, "TODO_type_here") catch @panic("OOM");
+ return String.init(string_result.items);
+}
+
+const Oom = error{OutOfMemory};
+
+fn unpack_inner(tar_bytes: []u8) !void {
+ var fbs = std.io.fixedBufferStream(tar_bytes);
+ var file_name_buffer: [1024]u8 = undefined;
+ var link_name_buffer: [1024]u8 = undefined;
+ var it = std.tar.iterator(fbs.reader(), .{
+ .file_name_buffer = &file_name_buffer,
+ .link_name_buffer = &link_name_buffer,
+ });
+ while (try it.next()) |tar_file| {
+ switch (tar_file.kind) {
+ .normal => {
+ if (tar_file.size == 0 and tar_file.name.len == 0) break;
+ if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
+ log.debug("found file: '{s}'", .{tar_file.name});
+ const file_name = try gpa.dupe(u8, tar_file.name);
+ if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
+ const pkg_name = file_name[0..pkg_name_end];
+ const gop = try Walk.packages.getOrPut(gpa, pkg_name);
+ const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
+ if (!gop.found_existing or
+ std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
+ std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
+ {
+ gop.value_ptr.* = file;
+ }
+ const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)];
+ assert(file == try Walk.add_file(file_name, file_bytes));
+ }
+ } else {
+ log.warn("skipping: '{s}' - the tar creation should have done that", .{
+ tar_file.name,
+ });
+ }
+ try tar_file.skip();
+ },
+ else => continue,
+ }
+ }
+}
+
+fn fatal(comptime format: []const u8, args: anytype) noreturn {
+ var buf: [500]u8 = undefined;
+ const line = std.fmt.bufPrint(&buf, format, args) catch l: {
+ buf[buf.len - 3 ..][0..3].* = "...".*;
+ break :l &buf;
+ };
+ js.panic(line.ptr, line.len);
+}
+
+fn ascii_lower(bytes: []u8) void {
+ for (bytes) |*b| b.* = std.ascii.toLower(b.*);
+}
+
+export fn package_name(index: u32) String {
+ const names = Walk.packages.keys();
+ return String.init(if (index >= names.len) "" else names[index]);
+}
+
+export fn find_package_root(pkg: Walk.PackageIndex) Decl.Index {
+ const root_file = Walk.packages.values()[@intFromEnum(pkg)];
+ const result = root_file.findRootDecl();
+ assert(result != .none);
+ return result;
+}
+
+/// Set by `set_input_string`.
+var input_string: std.ArrayListUnmanaged(u8) = .{};
+
+export fn set_input_string(len: usize) [*]u8 {
+ input_string.resize(gpa, len) catch @panic("OOM");
+ return input_string.items.ptr;
+}
+
+/// Looks up the root struct decl corresponding to a file by path.
+/// Uses `input_string`.
+export fn find_file_root() Decl.Index {
+ const file: Walk.File.Index = @enumFromInt(Walk.files.getIndex(input_string.items) orelse return .none);
+ return file.findRootDecl();
+}
+
+/// Uses `input_string`.
+export fn find_decl() Decl.Index {
+ const g = struct {
+ var match_fqn: std.ArrayListUnmanaged(u8) = .{};
+ };
+ for (Walk.decls.items, 0..) |*decl, decl_index| {
+ decl.fqn(&g.match_fqn) catch @panic("OOM");
+ if (std.mem.eql(u8, g.match_fqn.items, input_string.items)) {
+ //const path = @as(Decl.Index, @enumFromInt(decl_index)).get().file.path();
+ //log.debug("find_decl '{s}' found in {s}", .{ input_string.items, path });
+ return @enumFromInt(decl_index);
+ }
+ }
+ return .none;
+}
+
+/// Set only by `categorize_decl`; read only by `get_aliasee`, valid only
+/// when `categorize_decl` returns `.alias`.
+var global_aliasee: Decl.Index = .none;
+
+export fn get_aliasee() Decl.Index {
+ return global_aliasee;
+}
+export fn categorize_decl(decl_index: Decl.Index, resolve_alias_count: usize) Walk.Category.Tag {
+ global_aliasee = .none;
+ var chase_alias_n = resolve_alias_count;
+ var decl = decl_index.get();
+ while (true) {
+ const result = decl.categorize();
+ switch (decl.categorize()) {
+ .alias => |new_index| {
+ assert(new_index != .none);
+ global_aliasee = new_index;
+ if (chase_alias_n > 0) {
+ chase_alias_n -= 1;
+ decl = new_index.get();
+ continue;
+ }
+ },
+ else => {},
+ }
+ return result;
+ }
+}
+
+export fn namespace_members(parent: Decl.Index, include_private: bool) Slice(Decl.Index) {
+ const g = struct {
+ var members: std.ArrayListUnmanaged(Decl.Index) = .{};
+ };
+
+ g.members.clearRetainingCapacity();
+
+ for (Walk.decls.items, 0..) |*decl, i| {
+ if (decl.parent == parent) {
+ if (include_private or decl.is_pub()) {
+ g.members.append(gpa, @enumFromInt(i)) catch @panic("OOM");
+ }
+ }
+ }
+
+ return Slice(Decl.Index).init(g.members.items);
+}
+
+fn file_source_html(
+ file_index: Walk.File.Index,
+ out: *std.ArrayListUnmanaged(u8),
+ root_node: Ast.Node.Index,
+) !void {
+ const ast = file_index.get_ast();
+ const file = file_index.get();
+
+ const g = struct {
+ var field_access_buffer: std.ArrayListUnmanaged(u8) = .{};
+ };
+
+ const token_tags = ast.tokens.items(.tag);
+ const token_starts = ast.tokens.items(.start);
+
+ const start_token = ast.firstToken(root_node);
+ const end_token = ast.lastToken(root_node) + 1;
+
+ var cursor: usize = token_starts[start_token];
+
+ for (
+ token_tags[start_token..end_token],
+ token_starts[start_token..end_token],
+ start_token..,
+ ) |tag, start, token_index| {
+ const between = ast.source[cursor..start];
+ try appendEscaped(out, between);
+ if (tag == .eof) break;
+ const slice = ast.tokenSlice(token_index);
+ cursor = start + slice.len;
+ switch (tag) {
+ .eof => unreachable,
+
+ .keyword_addrspace,
+ .keyword_align,
+ .keyword_and,
+ .keyword_asm,
+ .keyword_async,
+ .keyword_await,
+ .keyword_break,
+ .keyword_catch,
+ .keyword_comptime,
+ .keyword_const,
+ .keyword_continue,
+ .keyword_defer,
+ .keyword_else,
+ .keyword_enum,
+ .keyword_errdefer,
+ .keyword_error,
+ .keyword_export,
+ .keyword_extern,
+ .keyword_for,
+ .keyword_if,
+ .keyword_inline,
+ .keyword_noalias,
+ .keyword_noinline,
+ .keyword_nosuspend,
+ .keyword_opaque,
+ .keyword_or,
+ .keyword_orelse,
+ .keyword_packed,
+ .keyword_anyframe,
+ .keyword_pub,
+ .keyword_resume,
+ .keyword_return,
+ .keyword_linksection,
+ .keyword_callconv,
+ .keyword_struct,
+ .keyword_suspend,
+ .keyword_switch,
+ .keyword_test,
+ .keyword_threadlocal,
+ .keyword_try,
+ .keyword_union,
+ .keyword_unreachable,
+ .keyword_usingnamespace,
+ .keyword_var,
+ .keyword_volatile,
+ .keyword_allowzero,
+ .keyword_while,
+ .keyword_anytype,
+ .keyword_fn,
+ => {
+ try out.appendSlice(gpa, "
");
+ try appendEscaped(out, slice);
+ try out.appendSlice(gpa, "");
+ },
+
+ .string_literal,
+ .char_literal,
+ .multiline_string_literal_line,
+ => {
+ try out.appendSlice(gpa, "
");
+ try appendEscaped(out, slice);
+ try out.appendSlice(gpa, "");
+ },
+
+ .builtin => {
+ try out.appendSlice(gpa, "
");
+ try appendEscaped(out, slice);
+ try out.appendSlice(gpa, "");
+ },
+
+ .doc_comment,
+ .container_doc_comment,
+ => {
+ try out.appendSlice(gpa, "");
+ },
+
+ .identifier => i: {
+ if (std.mem.eql(u8, slice, "undefined") or
+ std.mem.eql(u8, slice, "null") or
+ std.mem.eql(u8, slice, "true") or
+ std.mem.eql(u8, slice, "false"))
+ {
+ try out.appendSlice(gpa, "
");
+ try appendEscaped(out, slice);
+ try out.appendSlice(gpa, "");
+ break :i;
+ }
+
+ if (std.zig.primitives.isPrimitive(slice)) {
+ try out.appendSlice(gpa, "
");
+ try appendEscaped(out, slice);
+ try out.appendSlice(gpa, "");
+ break :i;
+ }
+
+ if (file.token_parents.get(token_index)) |field_access_node| {
+ g.field_access_buffer.clearRetainingCapacity();
+ try walk_field_accesses(file_index, &g.field_access_buffer, field_access_node);
+ if (g.field_access_buffer.items.len > 0) {
+ try out.appendSlice(gpa, "
");
+ try appendEscaped(out, slice);
+ try out.appendSlice(gpa, "");
+ } else {
+ try appendEscaped(out, slice);
+ }
+ break :i;
+ }
+
+ {
+ g.field_access_buffer.clearRetainingCapacity();
+ try resolve_ident_link(file_index, &g.field_access_buffer, token_index);
+ if (g.field_access_buffer.items.len > 0) {
+ try out.appendSlice(gpa, "
");
+ try appendEscaped(out, slice);
+ try out.appendSlice(gpa, "");
+ break :i;
+ }
+ }
+
+ try appendEscaped(out, slice);
+ },
+
+ .number_literal => {
+ try out.appendSlice(gpa, "
");
+ try appendEscaped(out, slice);
+ try out.appendSlice(gpa, "");
+ },
+
+ .bang,
+ .pipe,
+ .pipe_pipe,
+ .pipe_equal,
+ .equal,
+ .equal_equal,
+ .equal_angle_bracket_right,
+ .bang_equal,
+ .l_paren,
+ .r_paren,
+ .semicolon,
+ .percent,
+ .percent_equal,
+ .l_brace,
+ .r_brace,
+ .l_bracket,
+ .r_bracket,
+ .period,
+ .period_asterisk,
+ .ellipsis2,
+ .ellipsis3,
+ .caret,
+ .caret_equal,
+ .plus,
+ .plus_plus,
+ .plus_equal,
+ .plus_percent,
+ .plus_percent_equal,
+ .plus_pipe,
+ .plus_pipe_equal,
+ .minus,
+ .minus_equal,
+ .minus_percent,
+ .minus_percent_equal,
+ .minus_pipe,
+ .minus_pipe_equal,
+ .asterisk,
+ .asterisk_equal,
+ .asterisk_asterisk,
+ .asterisk_percent,
+ .asterisk_percent_equal,
+ .asterisk_pipe,
+ .asterisk_pipe_equal,
+ .arrow,
+ .colon,
+ .slash,
+ .slash_equal,
+ .comma,
+ .ampersand,
+ .ampersand_equal,
+ .question_mark,
+ .angle_bracket_left,
+ .angle_bracket_left_equal,
+ .angle_bracket_angle_bracket_left,
+ .angle_bracket_angle_bracket_left_equal,
+ .angle_bracket_angle_bracket_left_pipe,
+ .angle_bracket_angle_bracket_left_pipe_equal,
+ .angle_bracket_right,
+ .angle_bracket_right_equal,
+ .angle_bracket_angle_bracket_right,
+ .angle_bracket_angle_bracket_right_equal,
+ .tilde,
+ => try appendEscaped(out, slice),
+
+ .invalid, .invalid_periodasterisks => return error.InvalidToken,
+ }
+ }
+}
+
+fn resolve_ident_link(
+ file_index: Walk.File.Index,
+ out: *std.ArrayListUnmanaged(u8),
+ ident_token: Ast.TokenIndex,
+) Oom!void {
+ const decl_index = file_index.get().lookup_token(ident_token);
+ if (decl_index == .none) return;
+
+ const decl = decl_index.get();
+ switch (decl.categorize()) {
+ .alias => |alias_decl| try alias_decl.get().fqn(out),
+ else => try decl.fqn(out),
+ }
+}
+
+fn walk_field_accesses(
+ file_index: Walk.File.Index,
+ out: *std.ArrayListUnmanaged(u8),
+ node: Ast.Node.Index,
+) Oom!void {
+ const ast = file_index.get_ast();
+ const node_tags = ast.nodes.items(.tag);
+ assert(node_tags[node] == .field_access);
+ const node_datas = ast.nodes.items(.data);
+ const main_tokens = ast.nodes.items(.main_token);
+ const object_node = node_datas[node].lhs;
+ const dot_token = main_tokens[node];
+ const field_ident = dot_token + 1;
+ switch (node_tags[object_node]) {
+ .identifier => {
+ const lhs_ident = main_tokens[object_node];
+ try resolve_ident_link(file_index, out, lhs_ident);
+ },
+ .field_access => {
+ try walk_field_accesses(file_index, out, object_node);
+ },
+ else => {},
+ }
+ if (out.items.len > 0) {
+ try out.append(gpa, '.');
+ try out.appendSlice(gpa, ast.tokenSlice(field_ident));
+ }
+}
+
+fn appendEscaped(out: *std.ArrayListUnmanaged(u8), s: []const u8) !void {
+ for (s) |c| {
+ try out.ensureUnusedCapacity(gpa, 6);
+ switch (c) {
+ '&' => out.appendSliceAssumeCapacity("&"),
+ '<' => out.appendSliceAssumeCapacity("<"),
+ '>' => out.appendSliceAssumeCapacity(">"),
+ '"' => out.appendSliceAssumeCapacity("""),
+ else => out.appendAssumeCapacity(c),
+ }
+ }
+}
+
+fn count_scalar(haystack: []const u8, needle: u8) usize {
+ var total: usize = 0;
+ for (haystack) |elem| {
+ if (elem == needle)
+ total += 1;
+ }
+ return total;
+}
diff --git a/lib/docs/wasm/markdown.zig b/lib/docs/wasm/markdown.zig
new file mode 100644
index 0000000000000000000000000000000000000000..7e6fb31b4aab989a8abdb4dc4deac26985cd7169
--- /dev/null
+++ b/lib/docs/wasm/markdown.zig
@@ -0,0 +1,933 @@
+//! Markdown parsing and rendering support.
+//!
+//! A Markdown document consists of a series of blocks. Depending on its type,
+//! each block may contain other blocks, inline content, or nothing. The
+//! supported blocks are as follows:
+//!
+//! - **List** - a sequence of list items of the same type.
+//!
+//! - **List item** - unordered list items start with `-`, `*`, or `+` followed
+//! by a space. Ordered list items start with a number between 0 and
+//! 999,999,999, followed by a `.` or `)` and a space. The number of an
+//! ordered list item only matters for the first item in the list (to
+//! determine the starting number of the list). All subsequent ordered list
+//! items will have sequentially increasing numbers.
+//!
+//! All list items may contain block content. Any content indented at least as
+//! far as the end of the list item marker (including the space after it) is
+//! considered part of the list item.
+//!
+//! Lists which have no blank lines between items or between direct children
+//! of items are considered _tight_, and direct child paragraphs of tight list
+//! items are rendered without `
` tags.
+//!
+//! - **Table** - a sequence of adjacent table row lines, where each line starts
+//! and ends with a `|`, and cells within the row are delimited by `|`s.
+//!
+//! The first or second row of a table may be a _header delimiter row_, which
+//! is a row consisting of cells of the pattern `---` (for unset column
+//! alignment), `:--` (for left alignment), `:-:` (for center alignment), or
+//! `--:` (for right alignment). The number of `-`s must be at least one, but
+//! is otherwise arbitrary. If there is a row just before the header delimiter
+//! row, it becomes the header row for the table (a table need not have a
+//! header row at all).
+//!
+//! - **Heading** - a sequence of between 1 and 6 `#` characters, followed by a
+//! space and further inline content on the same line.
+//!
+//! - **Code block** - a sequence of at least 3 `` ` `` characters (a _fence_),
+//! optionally followed by a "tag" on the same line, and continuing until a
+//! line consisting only of a closing fence whose length matches the opening
+//! fence, or until the end of the containing block.
+//!
+//! The content of a code block is not parsed as inline content. It is
+//! included verbatim in the output document (minus leading indentation up to
+//! the position of the opening fence).
+//!
+//! - **Blockquote** - a sequence of lines preceded by `>` characters.
+//!
+//! - **Paragraph** - ordinary text, parsed as inline content, ending with a
+//! blank line or the end of the containing block.
+//!
+//! Paragraphs which are part of another block may be "lazily" continued by
+//! subsequent paragraph lines even if those lines would not ordinarily be
+//! considered part of the containing block. For example, this is a single
+//! list item, not a list item followed by a paragraph:
+//!
+//! ```markdown
+//! - First line of content.
+//! This content is still part of the paragraph,
+//! even though it isn't indented far enough.
+//! ```
+//!
+//! - **Thematic break** - a line consisting of at least three matching `-`,
+//! `_`, or `*` characters and, optionally, spaces.
+//!
+//! Indentation may consist of spaces and tabs. The use of tabs is not
+//! recommended: a tab is treated the same as a single space for the purpose of
+//! determining the indentation level, and is not recognized as a space for
+//! block starters which require one (for example, `-` followed by a tab is not
+//! a valid list item).
+//!
+//! The supported inlines are as follows:
+//!
+//! - **Link** - of the format `[text](target)`. `text` may contain inline
+//! content. `target` may contain `\`-escaped characters and balanced
+//! parentheses.
+//!
+//! - **Image** - a link directly preceded by a `!`. The link text is
+//! interpreted as the alt text of the image.
+//!
+//! - **Emphasis** - a run of `*` or `_` characters may be an emphasis opener,
+//! closer, or both. For `*` characters, the run may be an opener as long as
+//! it is not directly followed by a whitespace character (or the end of the
+//! inline content) and a closer as long as it is not directly preceded by
+//! one. For `_` characters, this rule is strengthened by requiring that the
+//! run also be preceded by a whitespace or punctuation character (for
+//! openers) or followed by one (for closers), to avoid mangling `snake_case`
+//! words.
+//!
+//! The rule for emphasis handling is greedy: any run that can close existing
+//! emphasis will do so, otherwise it will open emphasis. A single run may
+//! serve both functions: the middle `**` in the following example both closes
+//! the initial emphasis and opens a new one:
+//!
+//! ```markdown
+//! *one**two*
+//! ```
+//!
+//! A single `*` or `_` is used for normal emphasis (HTML ``), and a
+//! double `**` or `__` is used for strong emphasis (HTML ``). Even
+//! longer runs may be used to produce further nested emphasis (though only
+//! `***` and `___` to produce `` is really useful).
+//!
+//! - **Code span** - a run of `` ` `` characters, terminated by a matching run
+//! or the end of inline content. The content of a code span is not parsed
+//! further.
+//!
+//! - **Text** - normal text is interpreted as-is, except that `\` may be used
+//! to escape any punctuation character, preventing it from being interpreted
+//! according to other syntax rules. A `\` followed by a line break within a
+//! paragraph is interpreted as a hard line break.
+//!
+//! Any null bytes or invalid UTF-8 bytes within text are replaced with Unicode
+//! replacement characters, `U+FFFD`.
+
+const std = @import("std");
+const testing = std.testing;
+
+pub const Document = @import("markdown/Document.zig");
+pub const Parser = @import("markdown/Parser.zig");
+pub const Renderer = @import("markdown/renderer.zig").Renderer;
+pub const renderNodeInlineText = @import("markdown/renderer.zig").renderNodeInlineText;
+pub const fmtHtml = @import("markdown/renderer.zig").fmtHtml;
+
+// Avoid exposing main to other files merely importing this one.
+pub const main = if (@import("root") == @This())
+ mainImpl
+else
+ @compileError("only available as root source file");
+
+fn mainImpl() !void {
+ const gpa = std.heap.c_allocator;
+
+ var parser = try Parser.init(gpa);
+ defer parser.deinit();
+
+ var stdin_buf = std.io.bufferedReader(std.io.getStdIn().reader());
+ var line_buf = std.ArrayList(u8).init(gpa);
+ defer line_buf.deinit();
+ while (stdin_buf.reader().streamUntilDelimiter(line_buf.writer(), '\n', null)) {
+ if (line_buf.getLastOrNull() == '\r') _ = line_buf.pop();
+ try parser.feedLine(line_buf.items);
+ line_buf.clearRetainingCapacity();
+ } else |err| switch (err) {
+ error.EndOfStream => {},
+ else => |e| return e,
+ }
+
+ var doc = try parser.endInput();
+ defer doc.deinit(gpa);
+
+ var stdout_buf = std.io.bufferedWriter(std.io.getStdOut().writer());
+ try doc.render(stdout_buf.writer());
+ try stdout_buf.flush();
+}
+
+test "empty document" {
+ try testRender("", "");
+ try testRender(" ", "");
+ try testRender("\n \n\t\n \n", "");
+}
+
+test "unordered lists" {
+ try testRender(
+ \\- Spam
+ \\- Spam
+ \\- Spam
+ \\- Eggs
+ \\- Bacon
+ \\- Spam
+ \\
+ \\* Spam
+ \\* Spam
+ \\* Spam
+ \\* Eggs
+ \\* Bacon
+ \\* Spam
+ \\
+ \\+ Spam
+ \\+ Spam
+ \\+ Spam
+ \\+ Eggs
+ \\+ Bacon
+ \\+ Spam
+ \\
+ ,
+ \\
+ \\- Spam
+ \\- Spam
+ \\- Spam
+ \\- Eggs
+ \\- Bacon
+ \\- Spam
+ \\
+ \\
+ \\- Spam
+ \\- Spam
+ \\- Spam
+ \\- Eggs
+ \\- Bacon
+ \\- Spam
+ \\
+ \\
+ \\- Spam
+ \\- Spam
+ \\- Spam
+ \\- Eggs
+ \\- Bacon
+ \\- Spam
+ \\
+ \\
+ );
+}
+
+test "ordered lists" {
+ try testRender(
+ \\1. Breakfast
+ \\2. Second breakfast
+ \\3. Lunch
+ \\2. Afternoon snack
+ \\1. Dinner
+ \\6. Dessert
+ \\7. Midnight snack
+ \\
+ \\1) Breakfast
+ \\2) Second breakfast
+ \\3) Lunch
+ \\2) Afternoon snack
+ \\1) Dinner
+ \\6) Dessert
+ \\7) Midnight snack
+ \\
+ \\1001. Breakfast
+ \\2. Second breakfast
+ \\3. Lunch
+ \\2. Afternoon snack
+ \\1. Dinner
+ \\6. Dessert
+ \\7. Midnight snack
+ \\
+ \\1001) Breakfast
+ \\2) Second breakfast
+ \\3) Lunch
+ \\2) Afternoon snack
+ \\1) Dinner
+ \\6) Dessert
+ \\7) Midnight snack
+ \\
+ ,
+ \\
+ \\- Breakfast
+ \\- Second breakfast
+ \\- Lunch
+ \\- Afternoon snack
+ \\- Dinner
+ \\- Dessert
+ \\- Midnight snack
+ \\
+ \\
+ \\- Breakfast
+ \\- Second breakfast
+ \\- Lunch
+ \\- Afternoon snack
+ \\- Dinner
+ \\- Dessert
+ \\- Midnight snack
+ \\
+ \\
+ \\- Breakfast
+ \\- Second breakfast
+ \\- Lunch
+ \\- Afternoon snack
+ \\- Dinner
+ \\- Dessert
+ \\- Midnight snack
+ \\
+ \\
+ \\- Breakfast
+ \\- Second breakfast
+ \\- Lunch
+ \\- Afternoon snack
+ \\- Dinner
+ \\- Dessert
+ \\- Midnight snack
+ \\
+ \\
+ );
+}
+
+test "nested lists" {
+ try testRender(
+ \\- - Item 1.
+ \\ - Item 2.
+ \\Item 2 continued.
+ \\ * New list.
+ \\
+ ,
+ \\
+ \\
+ \\- Item 1.
+ \\- Item 2.
+ \\Item 2 continued.
+ \\
+ \\
+ \\
+ \\
+ \\
+ );
+}
+
+test "lists with block content" {
+ try testRender(
+ \\1. Item 1.
+ \\2. Item 2.
+ \\
+ \\ This one has another paragraph.
+ \\3. Item 3.
+ \\
+ \\- > Blockquote.
+ \\- - Sub-list.
+ \\ - Sub-list continued.
+ \\ * Different sub-list.
+ \\- ## Heading.
+ \\
+ \\ Some contents below the heading.
+ \\ 1. Item 1.
+ \\ 2. Item 2.
+ \\ 3. Item 3.
+ \\
+ ,
+ \\
+ \\Item 1.
+ \\
+ \\Item 2.
+ \\This one has another paragraph.
+ \\
+ \\Item 3.
+ \\
+ \\
+ \\
+ \\
+ );
+}
+
+test "tables" {
+ try testRender(
+ \\| Operator | Meaning |
+ \\| :------: | ---------------- |
+ \\| `+` | Add |
+ \\| `-` | Subtract |
+ \\| `*` | Multiply |
+ \\| `/` | Divide |
+ \\| `??` | **Not sure yet** |
+ \\
+ \\| Item 1 | Value 1 |
+ \\| Item 2 | Value 2 |
+ \\| Item 3 | Value 3 |
+ \\| Item 4 | Value 4 |
+ \\
+ \\| :--- | :----: | ----: |
+ \\| Left | Center | Right |
+ \\
+ ,
+ \\
+ \\
+ \\| Operator |
+ \\Meaning |
+ \\
+ \\
+ \\+ |
+ \\Add |
+ \\
+ \\
+ \\- |
+ \\Subtract |
+ \\
+ \\
+ \\* |
+ \\Multiply |
+ \\
+ \\
+ \\/ |
+ \\Divide |
+ \\
+ \\
+ \\?? |
+ \\Not sure yet |
+ \\
+ \\
+ \\
+ \\
+ \\| Item 1 |
+ \\Value 1 |
+ \\
+ \\
+ \\| Item 2 |
+ \\Value 2 |
+ \\
+ \\
+ \\| Item 3 |
+ \\Value 3 |
+ \\
+ \\
+ \\| Item 4 |
+ \\Value 4 |
+ \\
+ \\
+ \\
+ \\
+ \\| Left |
+ \\Center |
+ \\Right |
+ \\
+ \\
+ \\
+ );
+}
+
+test "table with uneven number of columns" {
+ try testRender(
+ \\| One |
+ \\| :-- | :--: |
+ \\| One | Two | Three |
+ \\
+ ,
+ \\
+ \\
+ \\| One |
+ \\
+ \\
+ \\| One |
+ \\Two |
+ \\Three |
+ \\
+ \\
+ \\
+ );
+}
+
+test "table with escaped pipes" {
+ try testRender(
+ \\| One \| Two |
+ \\| --- | --- |
+ \\| One \| Two |
+ \\
+ ,
+ \\
+ \\
+ \\| One | Two |
+ \\
+ \\
+ \\| One | Two |
+ \\
+ \\
+ \\
+ );
+}
+
+test "table with pipes in code spans" {
+ try testRender(
+ \\| `|` | Bitwise _OR_ |
+ \\| `||` | Combines error sets |
+ \\| `` `||` `` | Escaped version |
+ \\| ` ``||`` ` | Another escaped version |
+ \\| `Oops unterminated code span |
+ \\
+ ,
+ \\
+ \\
+ \\| |
+ \\Bitwise OR |
+ \\
+ \\
+ \\|| |
+ \\Combines error sets |
+ \\
+ \\
+ \\`||` |
+ \\Escaped version |
+ \\
+ \\
+ \\``||`` |
+ \\Another escaped version |
+ \\
+ \\
+ \\| Oops unterminated code span |
+ \\
+ );
+}
+
+test "tables require leading and trailing pipes" {
+ try testRender(
+ \\Not | a | table
+ \\
+ \\| But | this | is |
+ \\
+ ,
+ \\Not | a | table
+ \\
+ \\
+ \\| But |
+ \\this |
+ \\is |
+ \\
+ \\
+ \\
+ );
+}
+
+test "headings" {
+ try testRender(
+ \\# Level one
+ \\## Level two
+ \\### Level three
+ \\#### Level four
+ \\##### Level five
+ \\###### Level six
+ \\####### Not a heading
+ \\
+ ,
+ \\Level one
+ \\Level two
+ \\Level three
+ \\Level four
+ \\Level five
+ \\Level six
+ \\####### Not a heading
+ \\
+ );
+}
+
+test "headings with inline content" {
+ try testRender(
+ \\# Outline of `std.zig`
+ \\## **Important** notes
+ \\### ***Nested* inline content**
+ \\
+ ,
+ \\Outline of std.zig
+ \\Important notes
+ \\Nested inline content
+ \\
+ );
+}
+
+test "code blocks" {
+ try testRender(
+ \\```
+ \\Hello, world!
+ \\This is some code.
+ \\```
+ \\``` zig test
+ \\const std = @import("std");
+ \\
+ \\test {
+ \\ try std.testing.expect(2 + 2 == 4);
+ \\}
+ \\```
+ \\
+ ,
+ \\Hello, world!
+ \\This is some code.
+ \\
+ \\const std = @import("std");
+ \\
+ \\test {
+ \\ try std.testing.expect(2 + 2 == 4);
+ \\}
+ \\
+ \\
+ );
+}
+
+test "blockquotes" {
+ try testRender(
+ \\> > You miss 100% of the shots you don't take.
+ \\> >
+ \\> > ~ Wayne Gretzky
+ \\>
+ \\> ~ Michael Scott
+ \\
+ ,
+ \\
+ \\
+ \\You miss 100% of the shots you don't take.
+ \\~ Wayne Gretzky
+ \\
+ \\~ Michael Scott
+ \\
+ \\
+ );
+}
+
+test "blockquote lazy continuation lines" {
+ try testRender(
+ \\>>>>Deeply nested blockquote
+ \\>>which continues on another line
+ \\and then yet another one.
+ \\>>
+ \\>> But now two of them have been closed.
+ \\
+ \\And then there were none.
+ \\
+ ,
+ \\
+ \\
+ \\
+ \\
+ \\Deeply nested blockquote
+ \\which continues on another line
+ \\and then yet another one.
+ \\
+ \\
+ \\But now two of them have been closed.
+ \\
+ \\
+ \\And then there were none.
+ \\
+ );
+}
+
+test "paragraphs" {
+ try testRender(
+ \\Paragraph one.
+ \\
+ \\Paragraph two.
+ \\Still in the paragraph.
+ \\ So is this.
+ \\
+ \\
+ \\
+ \\
+ \\ Last paragraph.
+ \\
+ ,
+ \\Paragraph one.
+ \\Paragraph two.
+ \\Still in the paragraph.
+ \\So is this.
+ \\Last paragraph.
+ \\
+ );
+}
+
+test "thematic breaks" {
+ try testRender(
+ \\---
+ \\***
+ \\___
+ \\ ---
+ \\ - - - - - - - - - - -
+ \\
+ ,
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ );
+}
+
+test "links" {
+ try testRender(
+ \\[Link](https://example.com)
+ \\[Link *with inlines*](https://example.com)
+ \\[Nested parens](https://example.com/nested(parens(inside)))
+ \\[Escaped parens](https://example.com/\)escaped\()
+ \\[Line break in target](test\
+ \\target)
+ \\
+ ,
+ \\Link
+ \\Link with inlines
+ \\Nested parens
+ \\Escaped parens
+ \\Line break in target
+ \\
+ );
+}
+
+test "images" {
+ try testRender(
+ \\
+ \\
+ \\).png)
+ \\escaped\(.png)
+ \\
+ \\
+ ,
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ );
+}
+
+test "emphasis" {
+ try testRender(
+ \\*Emphasis.*
+ \\**Strong.**
+ \\***Strong emphasis.***
+ \\****More...****
+ \\*****MORE...*****
+ \\******Even more...******
+ \\*******OK, this is enough.*******
+ \\
+ ,
+ \\Emphasis.
+ \\Strong.
+ \\Strong emphasis.
+ \\More...
+ \\MORE...
+ \\Even more...
+ \\OK, this is enough.
+ \\
+ );
+ try testRender(
+ \\_Emphasis._
+ \\__Strong.__
+ \\___Strong emphasis.___
+ \\____More...____
+ \\_____MORE..._____
+ \\______Even more...______
+ \\_______OK, this is enough._______
+ \\
+ ,
+ \\Emphasis.
+ \\Strong.
+ \\Strong emphasis.
+ \\More...
+ \\MORE...
+ \\Even more...
+ \\OK, this is enough.
+ \\
+ );
+}
+
+test "nested emphasis" {
+ try testRender(
+ \\**Hello, *world!***
+ \\*Hello, **world!***
+ \\**Hello, _world!_**
+ \\_Hello, **world!**_
+ \\*Hello, **nested** *world!**
+ \\***Hello,* world!**
+ \\__**Hello, world!**__
+ \\****Hello,** world!**
+ \\__Hello,_ world!_
+ \\*Test**123*
+ \\__Test____123__
+ \\
+ ,
+ \\Hello, world!
+ \\Hello, world!
+ \\Hello, world!
+ \\Hello, world!
+ \\Hello, nested world!
+ \\Hello, world!
+ \\Hello, world!
+ \\Hello, world!
+ \\Hello, world!
+ \\Test123
+ \\Test____123
+ \\
+ );
+}
+
+test "emphasis precedence" {
+ try testRender(
+ \\*First one _wins*_.
+ \\_*No other __rule matters.*_
+ \\
+ ,
+ \\First one _wins_.
+ \\No other __rule matters.
+ \\
+ );
+}
+
+test "emphasis open and close" {
+ try testRender(
+ \\Cannot open: *
+ \\Cannot open: _
+ \\*Cannot close: *
+ \\_Cannot close: _
+ \\
+ \\foo*bar*baz
+ \\foo_bar_baz
+ \\foo**bar**baz
+ \\foo__bar__baz
+ \\
+ ,
+ \\Cannot open: *
+ \\Cannot open: _
+ \\*Cannot close: *
+ \\_Cannot close: _
+ \\foobarbaz
+ \\foo_bar_baz
+ \\foobarbaz
+ \\foo__bar__baz
+ \\
+ );
+}
+
+test "code spans" {
+ try testRender(
+ \\`Hello, world!`
+ \\```Multiple `backticks` can be used.```
+ \\`**This** does not produce emphasis.`
+ \\`` `Backtick enclosed string.` ``
+ \\`Delimiter lengths ```must``` match.`
+ \\
+ \\Unterminated ``code...
+ \\
+ \\Weird empty code span: `
+ \\
+ \\**Very important code: `hi`**
+ \\
+ ,
+ \\Hello, world!
+ \\Multiple `backticks` can be used.
+ \\**This** does not produce emphasis.
+ \\`Backtick enclosed string.`
+ \\Delimiter lengths ```must``` match.
+ \\Unterminated code...
+ \\Weird empty code span:
+ \\Very important code: hi
+ \\
+ );
+}
+
+test "backslash escapes" {
+ try testRender(
+ \\Not \*emphasized\*.
+ \\Literal \\backslashes\\.
+ \\Not code: \`hi\`.
+ \\\# Not a title.
+ \\#\# Also not a title.
+ \\\> Not a blockquote.
+ \\\- Not a list item.
+ \\\| Not a table. |
+ \\| Also not a table. \|
+ \\Any \punctuation\ characte\r can be escaped:
+ \\\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~
+ \\
+ ,
+ \\Not *emphasized*.
+ \\Literal \backslashes\.
+ \\Not code: `hi`.
+ \\# Not a title.
+ \\## Also not a title.
+ \\> Not a blockquote.
+ \\- Not a list item.
+ \\| Not a table. |
+ \\| Also not a table. |
+ \\Any \punctuation\ characte\r can be escaped:
+ \\!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
+ \\
+ );
+}
+
+test "hard line breaks" {
+ try testRender(
+ \\The iguana sits\
+ \\Perched atop a short desk chair\
+ \\Writing code in Zig
+ \\
+ ,
+ \\The iguana sits
+ \\Perched atop a short desk chair
+ \\Writing code in Zig
+ \\
+ );
+}
+
+test "Unicode handling" {
+ // Null bytes must be replaced.
+ try testRender("\x00\x00\x00", "\u{FFFD}\u{FFFD}\u{FFFD}
\n");
+
+ // Invalid UTF-8 must be replaced.
+ try testRender("\xC0\x80\xE0\x80\x80\xF0\x80\x80\x80", "\u{FFFD}\u{FFFD}\u{FFFD}
\n");
+ try testRender("\xED\xA0\x80\xED\xBF\xBF", "\u{FFFD}\u{FFFD}
\n");
+
+ // Incomplete UTF-8 must be replaced.
+ try testRender("\xE2\x82", "\u{FFFD}
\n");
+}
+
+fn testRender(input: []const u8, expected: []const u8) !void {
+ var parser = try Parser.init(testing.allocator);
+ defer parser.deinit();
+
+ var lines = std.mem.split(u8, input, "\n");
+ while (lines.next()) |line| {
+ try parser.feedLine(line);
+ }
+ var doc = try parser.endInput();
+ defer doc.deinit(testing.allocator);
+
+ var actual = std.ArrayList(u8).init(testing.allocator);
+ defer actual.deinit();
+ try doc.render(actual.writer());
+
+ try testing.expectEqualStrings(expected, actual.items);
+}
diff --git a/lib/docs/wasm/markdown/Document.zig b/lib/docs/wasm/markdown/Document.zig
new file mode 100644
index 0000000000000000000000000000000000000000..9e43e357957b21b5f3e607a8e379d88c6dae63b0
--- /dev/null
+++ b/lib/docs/wasm/markdown/Document.zig
@@ -0,0 +1,192 @@
+//! An abstract tree representation of a Markdown document.
+
+const std = @import("std");
+const builtin = @import("builtin");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const Renderer = @import("renderer.zig").Renderer;
+
+nodes: Node.List.Slice,
+extra: []u32,
+string_bytes: []u8,
+
+const Document = @This();
+
+pub const Node = struct {
+ tag: Tag,
+ data: Data,
+
+ pub const Index = enum(u32) {
+ root = 0,
+ _,
+ };
+ pub const List = std.MultiArrayList(Node);
+
+ pub const Tag = enum {
+ /// Data is `container`.
+ root,
+
+ // Blocks
+ /// Data is `list`.
+ list,
+ /// Data is `list_item`.
+ list_item,
+ /// Data is `container`.
+ table,
+ /// Data is `container`.
+ table_row,
+ /// Data is `table_cell`.
+ table_cell,
+ /// Data is `heading`.
+ heading,
+ /// Data is `code_block`.
+ code_block,
+ /// Data is `container`.
+ blockquote,
+ /// Data is `container`.
+ paragraph,
+ /// Data is `none`.
+ thematic_break,
+
+ // Inlines
+ /// Data is `link`.
+ link,
+ /// Data is `link`.
+ image,
+ /// Data is `container`.
+ strong,
+ /// Data is `container`.
+ emphasis,
+ /// Data is `text`.
+ code_span,
+ /// Data is `text`.
+ text,
+ /// Data is `none`.
+ line_break,
+ };
+
+ pub const Data = union {
+ none: void,
+ container: struct {
+ children: ExtraIndex,
+ },
+ text: struct {
+ content: StringIndex,
+ },
+ list: struct {
+ start: ListStart,
+ children: ExtraIndex,
+ },
+ list_item: struct {
+ tight: bool,
+ children: ExtraIndex,
+ },
+ table_cell: struct {
+ info: packed struct {
+ alignment: TableCellAlignment,
+ header: bool,
+ },
+ children: ExtraIndex,
+ },
+ heading: struct {
+ /// Between 1 and 6, inclusive.
+ level: u3,
+ children: ExtraIndex,
+ },
+ code_block: struct {
+ tag: StringIndex,
+ content: StringIndex,
+ },
+ link: struct {
+ target: StringIndex,
+ children: ExtraIndex,
+ },
+
+ comptime {
+ // In Debug and ReleaseSafe builds, there may be hidden extra fields
+ // included for safety checks. Without such safety checks enabled,
+ // we always want this union to be 8 bytes.
+ if (builtin.mode != .Debug and builtin.mode != .ReleaseSafe) {
+ assert(@sizeOf(Data) == 8);
+ }
+ }
+ };
+
+ /// The starting number of a list. This is either a number between 0 and
+ /// 999,999,999, inclusive, or `unordered` to indicate an unordered list.
+ pub const ListStart = enum(u30) {
+ // When https://github.com/ziglang/zig/issues/104 is implemented, this
+ // type can be more naturally expressed as ?u30. As it is, we want
+ // values to fit within 4 bytes, so ?u30 does not yet suffice for
+ // storage.
+ unordered = std.math.maxInt(u30),
+ _,
+
+ pub fn asNumber(start: ListStart) ?u30 {
+ if (start == .unordered) return null;
+ assert(@intFromEnum(start) <= 999_999_999);
+ return @intFromEnum(start);
+ }
+ };
+
+ pub const TableCellAlignment = enum {
+ unset,
+ left,
+ center,
+ right,
+ };
+
+ /// Trailing: `len` times `Node.Index`
+ pub const Children = struct {
+ len: u32,
+ };
+};
+
+pub const ExtraIndex = enum(u32) { _ };
+
+/// The index of a null-terminated string in `string_bytes`.
+pub const StringIndex = enum(u32) {
+ empty = 0,
+ _,
+};
+
+pub fn deinit(doc: *Document, allocator: Allocator) void {
+ doc.nodes.deinit(allocator);
+ allocator.free(doc.extra);
+ allocator.free(doc.string_bytes);
+ doc.* = undefined;
+}
+
+/// Renders a document directly to a writer using the default renderer.
+pub fn render(doc: Document, writer: anytype) @TypeOf(writer).Error!void {
+ const renderer: Renderer(@TypeOf(writer), void) = .{ .context = {} };
+ try renderer.render(doc, writer);
+}
+
+pub fn ExtraData(comptime T: type) type {
+ return struct { data: T, end: usize };
+}
+
+pub fn extraData(doc: Document, comptime T: type, index: ExtraIndex) ExtraData(T) {
+ const fields = @typeInfo(T).Struct.fields;
+ var i: usize = @intFromEnum(index);
+ var result: T = undefined;
+ inline for (fields) |field| {
+ @field(result, field.name) = switch (field.type) {
+ u32 => doc.extra[i],
+ else => @compileError("bad field type"),
+ };
+ i += 1;
+ }
+ return .{ .data = result, .end = i };
+}
+
+pub fn extraChildren(doc: Document, index: ExtraIndex) []const Node.Index {
+ const children = doc.extraData(Node.Children, index);
+ return @ptrCast(doc.extra[children.end..][0..children.data.len]);
+}
+
+pub fn string(doc: Document, index: StringIndex) [:0]const u8 {
+ const start = @intFromEnum(index);
+ return std.mem.span(@as([*:0]u8, @ptrCast(doc.string_bytes[start..].ptr)));
+}
diff --git a/lib/docs/wasm/markdown/Parser.zig b/lib/docs/wasm/markdown/Parser.zig
new file mode 100644
index 0000000000000000000000000000000000000000..6a2d7e4f4e0b833478e37fa48c605e44b4bf5272
--- /dev/null
+++ b/lib/docs/wasm/markdown/Parser.zig
@@ -0,0 +1,1500 @@
+//! A Markdown parser producing `Document`s.
+//!
+//! The parser operates at two levels: at the outer level, the parser accepts
+//! the content of an input document line by line and begins building the _block
+//! structure_ of the document. This creates a stack of currently open blocks.
+//!
+//! When the parser detects the end of a block, it closes the block, popping it
+//! from the open block stack and completing any additional parsing of the
+//! block's content. For blocks which contain parseable inline content, this
+//! invokes the inner level of the parser, handling the _inline structure_ of
+//! the block.
+//!
+//! Inline parsing scans through the collected inline content of a block. When
+//! it encounters a character that could indicate the beginning of an inline, it
+//! either handles the inline right away (if possible) or adds it to a pending
+//! inlines stack. When an inline is completed, it is added to a list of
+//! completed inlines, which (along with any surrounding text nodes) will become
+//! the children of the parent inline or the block whose inline content is being
+//! parsed.
+
+const std = @import("std");
+const mem = std.mem;
+const assert = std.debug.assert;
+const isWhitespace = std.ascii.isWhitespace;
+const Allocator = mem.Allocator;
+const expectEqual = std.testing.expectEqual;
+const Document = @import("Document.zig");
+const Node = Document.Node;
+const ExtraIndex = Document.ExtraIndex;
+const ExtraData = Document.ExtraData;
+const StringIndex = Document.StringIndex;
+
+nodes: Node.List = .{},
+extra: std.ArrayListUnmanaged(u32) = .{},
+scratch_extra: std.ArrayListUnmanaged(u32) = .{},
+string_bytes: std.ArrayListUnmanaged(u8) = .{},
+scratch_string: std.ArrayListUnmanaged(u8) = .{},
+pending_blocks: std.ArrayListUnmanaged(Block) = .{},
+allocator: Allocator,
+
+const Parser = @This();
+
+/// An arbitrary limit on the maximum number of columns in a table so that
+/// table-related metadata maintained by the parser does not require dynamic
+/// memory allocation.
+const max_table_columns = 128;
+
+/// A block element which is still receiving children.
+const Block = struct {
+ tag: Tag,
+ data: Data,
+ extra_start: usize,
+ string_start: usize,
+
+ const Tag = enum {
+ /// Data is `list`.
+ list,
+ /// Data is `list_item`.
+ list_item,
+ /// Data is `table`.
+ table,
+ /// Data is `none`.
+ table_row,
+ /// Data is `heading`.
+ heading,
+ /// Data is `code_block`.
+ code_block,
+ /// Data is `none`.
+ blockquote,
+ /// Data is `none`.
+ paragraph,
+ /// Data is `none`.
+ thematic_break,
+ };
+
+ const Data = union {
+ none: void,
+ list: struct {
+ marker: ListMarker,
+ /// Between 0 and 999,999,999, inclusive.
+ start: u30,
+ tight: bool,
+ last_line_blank: bool = false,
+ },
+ list_item: struct {
+ continuation_indent: usize,
+ },
+ table: struct {
+ column_alignments: std.BoundedArray(Node.TableCellAlignment, max_table_columns) = .{},
+ },
+ heading: struct {
+ /// Between 1 and 6, inclusive.
+ level: u3,
+ },
+ code_block: struct {
+ tag: StringIndex,
+ fence_len: usize,
+ indent: usize,
+ },
+
+ const ListMarker = enum {
+ @"-",
+ @"*",
+ @"+",
+ number_dot,
+ number_paren,
+ };
+ };
+
+ const ContentType = enum {
+ blocks,
+ inlines,
+ raw_inlines,
+ nothing,
+ };
+
+ fn canAccept(b: Block) ContentType {
+ return switch (b.tag) {
+ .list,
+ .list_item,
+ .table,
+ .blockquote,
+ => .blocks,
+
+ .heading,
+ .paragraph,
+ => .inlines,
+
+ .code_block,
+ => .raw_inlines,
+
+ .table_row,
+ .thematic_break,
+ => .nothing,
+ };
+ }
+
+ /// Attempts to continue `b` using the contents of `line`. If successful,
+ /// returns the remaining portion of `line` to be considered part of `b`
+ /// (e.g. for a blockquote, this would be everything except the leading
+ /// `>`). If unsuccessful, returns null.
+ fn match(b: Block, line: []const u8) ?[]const u8 {
+ const unindented = mem.trimLeft(u8, line, " \t");
+ const indent = line.len - unindented.len;
+ return switch (b.tag) {
+ .list => line,
+ .list_item => if (indent >= b.data.list_item.continuation_indent)
+ line[b.data.list_item.continuation_indent..]
+ else if (unindented.len == 0)
+ // Blank lines should not close list items, since there may be
+ // more indented contents to follow after the blank line.
+ ""
+ else
+ null,
+ .table => if (unindented.len > 0) unindented else null,
+ .table_row => null,
+ .heading => null,
+ .code_block => code_block: {
+ const trimmed = mem.trimRight(u8, unindented, " \t");
+ if (mem.indexOfNone(u8, trimmed, "`") != null or trimmed.len != b.data.code_block.fence_len) {
+ const effective_indent = @min(indent, b.data.code_block.indent);
+ break :code_block line[effective_indent..];
+ } else {
+ break :code_block null;
+ }
+ },
+ .blockquote => if (mem.startsWith(u8, unindented, ">"))
+ unindented[1..]
+ else
+ null,
+ .paragraph => if (unindented.len > 0) unindented else null,
+ .thematic_break => null,
+ };
+ }
+};
+
+pub fn init(allocator: Allocator) Allocator.Error!Parser {
+ var p: Parser = .{ .allocator = allocator };
+ try p.nodes.append(allocator, .{
+ .tag = .root,
+ .data = undefined,
+ });
+ try p.string_bytes.append(allocator, 0);
+ return p;
+}
+
+pub fn deinit(p: *Parser) void {
+ p.nodes.deinit(p.allocator);
+ p.extra.deinit(p.allocator);
+ p.scratch_extra.deinit(p.allocator);
+ p.string_bytes.deinit(p.allocator);
+ p.scratch_string.deinit(p.allocator);
+ p.pending_blocks.deinit(p.allocator);
+ p.* = undefined;
+}
+
+/// Accepts a single line of content. `line` should not have a trailing line
+/// ending character.
+pub fn feedLine(p: *Parser, line: []const u8) Allocator.Error!void {
+ var rest_line = line;
+ const first_unmatched = for (p.pending_blocks.items, 0..) |b, i| {
+ if (b.match(rest_line)) |rest| {
+ rest_line = rest;
+ } else {
+ break i;
+ }
+ } else p.pending_blocks.items.len;
+
+ const in_code_block = p.pending_blocks.items.len > 0 and
+ p.pending_blocks.getLast().tag == .code_block;
+ const code_block_end = in_code_block and
+ first_unmatched + 1 == p.pending_blocks.items.len;
+ // New blocks cannot be started if we are actively inside a code block or
+ // are just closing one (to avoid interpreting the closing ``` as a new code
+ // block start).
+ var maybe_block_start = if (!in_code_block or first_unmatched + 2 <= p.pending_blocks.items.len)
+ try p.startBlock(rest_line)
+ else
+ null;
+
+ // This is a lazy continuation line if there are no new blocks to open and
+ // the last open block is a paragraph.
+ if (maybe_block_start == null and
+ !isBlank(rest_line) and
+ p.pending_blocks.items.len > 0 and
+ p.pending_blocks.getLast().tag == .paragraph)
+ {
+ try p.addScratchStringLine(rest_line);
+ return;
+ }
+
+ // If a new block needs to be started, any paragraph needs to be closed,
+ // even though this isn't detected as part of the closing condition for
+ // paragraphs.
+ if (maybe_block_start != null and
+ p.pending_blocks.items.len > 0 and
+ p.pending_blocks.getLast().tag == .paragraph)
+ {
+ try p.closeLastBlock();
+ }
+
+ while (p.pending_blocks.items.len > first_unmatched) {
+ try p.closeLastBlock();
+ }
+
+ while (maybe_block_start) |block_start| : (maybe_block_start = try p.startBlock(rest_line)) {
+ try p.appendBlockStart(block_start);
+ // There may be more blocks to start within the same line.
+ rest_line = block_start.rest;
+ // Headings may only contain inline content.
+ if (block_start.tag == .heading) break;
+ // An opening code fence does not contain any additional block or inline
+ // content to process.
+ if (block_start.tag == .code_block) return;
+ }
+
+ // Do not append the end of a code block (```) as textual content.
+ if (code_block_end) return;
+
+ const can_accept = if (p.pending_blocks.getLastOrNull()) |last_pending_block|
+ last_pending_block.canAccept()
+ else
+ .blocks;
+ const rest_line_trimmed = mem.trimLeft(u8, rest_line, " \t");
+ switch (can_accept) {
+ .blocks => {
+ // If we're inside a list item and the rest of the line is blank, it
+ // means that any subsequent child of the list item (or subsequent
+ // item in the list) will cause the containing list to be considered
+ // loose. However, we can't immediately declare that the list is
+ // loose, since we might just be looking at a blank line after the
+ // end of the last item in the list. The final determination will be
+ // made when appending the next child of the list or list item.
+ const maybe_containing_list = if (p.pending_blocks.items.len > 0 and p.pending_blocks.getLast().tag == .list_item)
+ &p.pending_blocks.items[p.pending_blocks.items.len - 2]
+ else
+ null;
+
+ if (rest_line_trimmed.len > 0) {
+ try p.appendBlockStart(.{
+ .tag = .paragraph,
+ .data = .{ .none = {} },
+ .rest = undefined,
+ });
+ try p.addScratchStringLine(rest_line_trimmed);
+ }
+
+ if (maybe_containing_list) |containing_list| {
+ containing_list.data.list.last_line_blank = rest_line_trimmed.len == 0;
+ }
+ },
+ .inlines => try p.addScratchStringLine(rest_line_trimmed),
+ .raw_inlines => try p.addScratchStringLine(rest_line),
+ .nothing => {},
+ }
+}
+
+/// Completes processing of the input and returns the parsed document.
+pub fn endInput(p: *Parser) Allocator.Error!Document {
+ while (p.pending_blocks.items.len > 0) {
+ try p.closeLastBlock();
+ }
+ // There should be no inline content pending after closing the last open
+ // block.
+ assert(p.scratch_string.items.len == 0);
+
+ const children = try p.addExtraChildren(@ptrCast(p.scratch_extra.items));
+ p.nodes.items(.data)[0] = .{ .container = .{ .children = children } };
+ p.scratch_string.items.len = 0;
+ p.scratch_extra.items.len = 0;
+
+ var nodes = p.nodes.toOwnedSlice();
+ errdefer nodes.deinit(p.allocator);
+ const extra = try p.extra.toOwnedSlice(p.allocator);
+ errdefer p.allocator.free(extra);
+ const string_bytes = try p.string_bytes.toOwnedSlice(p.allocator);
+ errdefer p.allocator.free(string_bytes);
+
+ return .{
+ .nodes = nodes,
+ .extra = extra,
+ .string_bytes = string_bytes,
+ };
+}
+
+/// Data describing the start of a new block element.
+const BlockStart = struct {
+ tag: Tag,
+ data: Data,
+ rest: []const u8,
+
+ const Tag = enum {
+ /// Data is `list_item`.
+ list_item,
+ /// Data is `table_row`.
+ table_row,
+ /// Data is `heading`.
+ heading,
+ /// Data is `code_block`.
+ code_block,
+ /// Data is `none`.
+ blockquote,
+ /// Data is `none`.
+ paragraph,
+ /// Data is `none`.
+ thematic_break,
+ };
+
+ const Data = union {
+ none: void,
+ list_item: struct {
+ marker: Block.Data.ListMarker,
+ number: u30,
+ continuation_indent: usize,
+ },
+ table_row: struct {
+ cells: std.BoundedArray([]const u8, max_table_columns),
+ },
+ heading: struct {
+ /// Between 1 and 6, inclusive.
+ level: u3,
+ },
+ code_block: struct {
+ tag: StringIndex,
+ fence_len: usize,
+ indent: usize,
+ },
+ };
+};
+
+fn appendBlockStart(p: *Parser, block_start: BlockStart) !void {
+ if (p.pending_blocks.getLastOrNull()) |last_pending_block| {
+ // Close the last block if it is a list and the new block is not a list item
+ // or not of the same marker type.
+ const should_close_list = last_pending_block.tag == .list and
+ (block_start.tag != .list_item or
+ block_start.data.list_item.marker != last_pending_block.data.list.marker);
+ // The last block should also be closed if the new block is not a table
+ // row, which is the only allowed child of a table.
+ const should_close_table = last_pending_block.tag == .table and
+ block_start.tag != .table_row;
+ if (should_close_list or should_close_table) {
+ try p.closeLastBlock();
+ }
+ }
+
+ if (p.pending_blocks.getLastOrNull()) |last_pending_block| {
+ // If the last block is a list or list item, check for tightness based
+ // on the last line.
+ const maybe_containing_list = switch (last_pending_block.tag) {
+ .list => &p.pending_blocks.items[p.pending_blocks.items.len - 1],
+ .list_item => &p.pending_blocks.items[p.pending_blocks.items.len - 2],
+ else => null,
+ };
+ if (maybe_containing_list) |containing_list| {
+ if (containing_list.data.list.last_line_blank) {
+ containing_list.data.list.tight = false;
+ }
+ }
+ }
+
+ // Start a new list if the new block is a list item and there is no
+ // containing list yet.
+ if (block_start.tag == .list_item and
+ (p.pending_blocks.items.len == 0 or p.pending_blocks.getLast().tag != .list))
+ {
+ try p.pending_blocks.append(p.allocator, .{
+ .tag = .list,
+ .data = .{ .list = .{
+ .marker = block_start.data.list_item.marker,
+ .start = block_start.data.list_item.number,
+ .tight = true,
+ } },
+ .string_start = p.scratch_string.items.len,
+ .extra_start = p.scratch_extra.items.len,
+ });
+ }
+
+ if (block_start.tag == .table_row) {
+ // Likewise, table rows start a table implicitly.
+ if (p.pending_blocks.items.len == 0 or p.pending_blocks.getLast().tag != .table) {
+ try p.pending_blocks.append(p.allocator, .{
+ .tag = .table,
+ .data = .{ .table = .{
+ .column_alignments = .{},
+ } },
+ .string_start = p.scratch_string.items.len,
+ .extra_start = p.scratch_extra.items.len,
+ });
+ }
+
+ const current_row = p.scratch_extra.items.len - p.pending_blocks.getLast().extra_start;
+ if (current_row <= 1) {
+ if (parseTableHeaderDelimiter(block_start.data.table_row.cells)) |alignments| {
+ p.pending_blocks.items[p.pending_blocks.items.len - 1].data.table.column_alignments = alignments;
+ if (current_row == 1) {
+ // We need to go back and mark the header row and its column
+ // alignments.
+ const datas = p.nodes.items(.data);
+ const header_data = datas[p.scratch_extra.getLast()];
+ for (p.extraChildren(header_data.container.children), 0..) |header_cell, i| {
+ const alignment = if (i < alignments.len) alignments.buffer[i] else .unset;
+ const cell_data = &datas[@intFromEnum(header_cell)].table_cell;
+ cell_data.info.alignment = alignment;
+ cell_data.info.header = true;
+ }
+ }
+ return;
+ }
+ }
+ }
+
+ const tag: Block.Tag, const data: Block.Data = switch (block_start.tag) {
+ .list_item => .{ .list_item, .{ .list_item = .{
+ .continuation_indent = block_start.data.list_item.continuation_indent,
+ } } },
+ .table_row => .{ .table_row, .{ .none = {} } },
+ .heading => .{ .heading, .{ .heading = .{
+ .level = block_start.data.heading.level,
+ } } },
+ .code_block => .{ .code_block, .{ .code_block = .{
+ .tag = block_start.data.code_block.tag,
+ .fence_len = block_start.data.code_block.fence_len,
+ .indent = block_start.data.code_block.indent,
+ } } },
+ .blockquote => .{ .blockquote, .{ .none = {} } },
+ .paragraph => .{ .paragraph, .{ .none = {} } },
+ .thematic_break => .{ .thematic_break, .{ .none = {} } },
+ };
+
+ try p.pending_blocks.append(p.allocator, .{
+ .tag = tag,
+ .data = data,
+ .string_start = p.scratch_string.items.len,
+ .extra_start = p.scratch_extra.items.len,
+ });
+
+ if (tag == .table_row) {
+ // Table rows are unique, since we already have all the children
+ // available in the BlockStart. We can immediately parse and append
+ // these children now.
+ const containing_table = p.pending_blocks.items[p.pending_blocks.items.len - 2];
+ const column_alignments = containing_table.data.table.column_alignments.slice();
+ for (block_start.data.table_row.cells.slice(), 0..) |cell_content, i| {
+ const cell_children = try p.parseInlines(cell_content);
+ const alignment = if (i < column_alignments.len) column_alignments[i] else .unset;
+ const cell = try p.addNode(.{
+ .tag = .table_cell,
+ .data = .{ .table_cell = .{
+ .info = .{
+ .alignment = alignment,
+ .header = false,
+ },
+ .children = cell_children,
+ } },
+ });
+ try p.addScratchExtraNode(cell);
+ }
+ }
+}
+
+fn startBlock(p: *Parser, line: []const u8) !?BlockStart {
+ const unindented = mem.trimLeft(u8, line, " \t");
+ const indent = line.len - unindented.len;
+ if (isThematicBreak(line)) {
+ // Thematic breaks take precedence over list items.
+ return .{
+ .tag = .thematic_break,
+ .data = .{ .none = {} },
+ .rest = "",
+ };
+ } else if (startListItem(unindented)) |list_item| {
+ return .{
+ .tag = .list_item,
+ .data = .{ .list_item = .{
+ .marker = list_item.marker,
+ .number = list_item.number,
+ .continuation_indent = list_item.continuation_indent,
+ } },
+ .rest = list_item.rest,
+ };
+ } else if (startTableRow(unindented)) |table_row| {
+ return .{
+ .tag = .table_row,
+ .data = .{ .table_row = .{
+ .cells = table_row.cells,
+ } },
+ .rest = "",
+ };
+ } else if (startHeading(unindented)) |heading| {
+ return .{
+ .tag = .heading,
+ .data = .{ .heading = .{
+ .level = heading.level,
+ } },
+ .rest = heading.rest,
+ };
+ } else if (try p.startCodeBlock(unindented)) |code_block| {
+ return .{
+ .tag = .code_block,
+ .data = .{ .code_block = .{
+ .tag = code_block.tag,
+ .fence_len = code_block.fence_len,
+ .indent = indent,
+ } },
+ .rest = "",
+ };
+ } else if (startBlockquote(unindented)) |rest| {
+ return .{
+ .tag = .blockquote,
+ .data = .{ .none = {} },
+ .rest = rest,
+ };
+ } else {
+ return null;
+ }
+}
+
+const ListItemStart = struct {
+ marker: Block.Data.ListMarker,
+ number: u30,
+ continuation_indent: usize,
+ rest: []const u8,
+};
+
+fn startListItem(unindented_line: []const u8) ?ListItemStart {
+ if (mem.startsWith(u8, unindented_line, "- ")) {
+ return .{
+ .marker = .@"-",
+ .number = undefined,
+ .continuation_indent = 2,
+ .rest = unindented_line[2..],
+ };
+ } else if (mem.startsWith(u8, unindented_line, "* ")) {
+ return .{
+ .marker = .@"*",
+ .number = undefined,
+ .continuation_indent = 2,
+ .rest = unindented_line[2..],
+ };
+ } else if (mem.startsWith(u8, unindented_line, "+ ")) {
+ return .{
+ .marker = .@"+",
+ .number = undefined,
+ .continuation_indent = 2,
+ .rest = unindented_line[2..],
+ };
+ }
+
+ const number_end = mem.indexOfNone(u8, unindented_line, "0123456789") orelse return null;
+ const after_number = unindented_line[number_end..];
+ const marker: Block.Data.ListMarker = if (mem.startsWith(u8, after_number, ". "))
+ .number_dot
+ else if (mem.startsWith(u8, after_number, ") "))
+ .number_paren
+ else
+ return null;
+ const number = std.fmt.parseInt(u30, unindented_line[0..number_end], 10) catch return null;
+ if (number > 999_999_999) return null;
+ return .{
+ .marker = marker,
+ .number = number,
+ .continuation_indent = number_end + 2,
+ .rest = after_number[2..],
+ };
+}
+
+const TableRowStart = struct {
+ cells: std.BoundedArray([]const u8, max_table_columns),
+};
+
+fn startTableRow(unindented_line: []const u8) ?TableRowStart {
+ if (!mem.startsWith(u8, unindented_line, "|") or
+ mem.endsWith(u8, unindented_line, "\\|") or
+ !mem.endsWith(u8, unindented_line, "|")) return null;
+
+ var cells: std.BoundedArray([]const u8, max_table_columns) = .{};
+ const table_row_content = unindented_line[1 .. unindented_line.len - 1];
+ var cell_start: usize = 0;
+ var i: usize = 0;
+ while (i < table_row_content.len) : (i += 1) {
+ switch (table_row_content[i]) {
+ '\\' => i += 1,
+ '|' => {
+ cells.append(table_row_content[cell_start..i]) catch return null;
+ cell_start = i + 1;
+ },
+ '`' => {
+ // Ignoring pipes in code spans allows table cells to contain
+ // code using ||, for example.
+ const open_start = i;
+ i = mem.indexOfNonePos(u8, table_row_content, i, "`") orelse return null;
+ const open_len = i - open_start;
+ while (mem.indexOfScalarPos(u8, table_row_content, i, '`')) |close_start| {
+ i = mem.indexOfNonePos(u8, table_row_content, close_start, "`") orelse return null;
+ const close_len = i - close_start;
+ if (close_len == open_len) break;
+ } else return null;
+ },
+ else => {},
+ }
+ }
+ cells.append(table_row_content[cell_start..]) catch return null;
+
+ return .{ .cells = cells };
+}
+
+fn parseTableHeaderDelimiter(
+ row_cells: std.BoundedArray([]const u8, max_table_columns),
+) ?std.BoundedArray(Node.TableCellAlignment, max_table_columns) {
+ var alignments: std.BoundedArray(Node.TableCellAlignment, max_table_columns) = .{};
+ for (row_cells.slice()) |content| {
+ const alignment = parseTableHeaderDelimiterCell(content) orelse return null;
+ alignments.appendAssumeCapacity(alignment);
+ }
+ return alignments;
+}
+
+fn parseTableHeaderDelimiterCell(content: []const u8) ?Node.TableCellAlignment {
+ var state: enum {
+ before_rule,
+ after_left_anchor,
+ in_rule,
+ after_right_anchor,
+ after_rule,
+ } = .before_rule;
+ var left_anchor = false;
+ var right_anchor = false;
+ for (content) |c| {
+ switch (state) {
+ .before_rule => switch (c) {
+ ' ' => {},
+ ':' => {
+ left_anchor = true;
+ state = .after_left_anchor;
+ },
+ '-' => state = .in_rule,
+ else => return null,
+ },
+ .after_left_anchor => switch (c) {
+ '-' => state = .in_rule,
+ else => return null,
+ },
+ .in_rule => switch (c) {
+ '-' => {},
+ ':' => {
+ right_anchor = true;
+ state = .after_right_anchor;
+ },
+ ' ' => state = .after_rule,
+ else => return null,
+ },
+ .after_right_anchor => switch (c) {
+ ' ' => state = .after_rule,
+ else => return null,
+ },
+ .after_rule => switch (c) {
+ ' ' => {},
+ else => return null,
+ },
+ }
+ }
+
+ switch (state) {
+ .before_rule,
+ .after_left_anchor,
+ => return null,
+
+ .in_rule,
+ .after_right_anchor,
+ .after_rule,
+ => {},
+ }
+
+ return if (left_anchor and right_anchor)
+ .center
+ else if (left_anchor)
+ .left
+ else if (right_anchor)
+ .right
+ else
+ .unset;
+}
+
+test parseTableHeaderDelimiterCell {
+ try expectEqual(null, parseTableHeaderDelimiterCell(""));
+ try expectEqual(null, parseTableHeaderDelimiterCell(" "));
+ try expectEqual(.unset, parseTableHeaderDelimiterCell("-"));
+ try expectEqual(.unset, parseTableHeaderDelimiterCell(" - "));
+ try expectEqual(.unset, parseTableHeaderDelimiterCell("----"));
+ try expectEqual(.unset, parseTableHeaderDelimiterCell(" ---- "));
+ try expectEqual(null, parseTableHeaderDelimiterCell(":"));
+ try expectEqual(null, parseTableHeaderDelimiterCell("::"));
+ try expectEqual(.left, parseTableHeaderDelimiterCell(":-"));
+ try expectEqual(.left, parseTableHeaderDelimiterCell(" :----"));
+ try expectEqual(.center, parseTableHeaderDelimiterCell(":-:"));
+ try expectEqual(.center, parseTableHeaderDelimiterCell(":----:"));
+ try expectEqual(.center, parseTableHeaderDelimiterCell(" :----: "));
+ try expectEqual(.right, parseTableHeaderDelimiterCell("-:"));
+ try expectEqual(.right, parseTableHeaderDelimiterCell("----:"));
+ try expectEqual(.right, parseTableHeaderDelimiterCell(" ----: "));
+}
+
+const HeadingStart = struct {
+ level: u3,
+ rest: []const u8,
+};
+
+fn startHeading(unindented_line: []const u8) ?HeadingStart {
+ var level: u3 = 0;
+ return for (unindented_line, 0..) |c, i| {
+ switch (c) {
+ '#' => {
+ if (level == 6) break null;
+ level += 1;
+ },
+ ' ' => {
+ // We must have seen at least one # by this point, since
+ // unindented_line has no leading spaces.
+ assert(level > 0);
+ break .{
+ .level = level,
+ .rest = unindented_line[i + 1 ..],
+ };
+ },
+ else => break null,
+ }
+ } else null;
+}
+
+const CodeBlockStart = struct {
+ tag: StringIndex,
+ fence_len: usize,
+};
+
+fn startCodeBlock(p: *Parser, unindented_line: []const u8) !?CodeBlockStart {
+ var fence_len: usize = 0;
+ const tag_bytes = for (unindented_line, 0..) |c, i| {
+ switch (c) {
+ '`' => fence_len += 1,
+ else => break unindented_line[i..],
+ }
+ } else "";
+ // Code block tags may not contain backticks, since that would create
+ // potential confusion with inline code spans.
+ if (fence_len < 3 or mem.indexOfScalar(u8, tag_bytes, '`') != null) return null;
+ return .{
+ .tag = try p.addString(mem.trim(u8, tag_bytes, " ")),
+ .fence_len = fence_len,
+ };
+}
+
+fn startBlockquote(unindented_line: []const u8) ?[]const u8 {
+ return if (mem.startsWith(u8, unindented_line, ">"))
+ unindented_line[1..]
+ else
+ null;
+}
+
+fn isThematicBreak(line: []const u8) bool {
+ var char: ?u8 = null;
+ var count: usize = 0;
+ for (line) |c| {
+ switch (c) {
+ ' ' => {},
+ '-', '_', '*' => {
+ if (char != null and c != char.?) return false;
+ char = c;
+ count += 1;
+ },
+ else => return false,
+ }
+ }
+ return count >= 3;
+}
+
+fn closeLastBlock(p: *Parser) !void {
+ const b = p.pending_blocks.pop();
+ const node = switch (b.tag) {
+ .list => list: {
+ assert(b.string_start == p.scratch_string.items.len);
+
+ // Although tightness is parsed as a property of the list, it is
+ // stored at the list item level to make it possible to render each
+ // node without any context from its parents.
+ const list_items = p.scratch_extra.items[b.extra_start..];
+ const node_datas = p.nodes.items(.data);
+ if (!b.data.list.tight) {
+ for (list_items) |list_item| {
+ node_datas[list_item].list_item.tight = false;
+ }
+ }
+
+ const children = try p.addExtraChildren(@ptrCast(list_items));
+ break :list try p.addNode(.{
+ .tag = .list,
+ .data = .{ .list = .{
+ .start = switch (b.data.list.marker) {
+ .number_dot, .number_paren => @enumFromInt(b.data.list.start),
+ .@"-", .@"*", .@"+" => .unordered,
+ },
+ .children = children,
+ } },
+ });
+ },
+ .list_item => list_item: {
+ assert(b.string_start == p.scratch_string.items.len);
+ const children = try p.addExtraChildren(@ptrCast(p.scratch_extra.items[b.extra_start..]));
+ break :list_item try p.addNode(.{
+ .tag = .list_item,
+ .data = .{ .list_item = .{
+ .tight = true,
+ .children = children,
+ } },
+ });
+ },
+ .table => table: {
+ assert(b.string_start == p.scratch_string.items.len);
+ const children = try p.addExtraChildren(@ptrCast(p.scratch_extra.items[b.extra_start..]));
+ break :table try p.addNode(.{
+ .tag = .table,
+ .data = .{ .container = .{
+ .children = children,
+ } },
+ });
+ },
+ .table_row => table_row: {
+ assert(b.string_start == p.scratch_string.items.len);
+ const children = try p.addExtraChildren(@ptrCast(p.scratch_extra.items[b.extra_start..]));
+ break :table_row try p.addNode(.{
+ .tag = .table_row,
+ .data = .{ .container = .{
+ .children = children,
+ } },
+ });
+ },
+ .heading => heading: {
+ const children = try p.parseInlines(p.scratch_string.items[b.string_start..]);
+ break :heading try p.addNode(.{
+ .tag = .heading,
+ .data = .{ .heading = .{
+ .level = b.data.heading.level,
+ .children = children,
+ } },
+ });
+ },
+ .code_block => code_block: {
+ const content = try p.addString(p.scratch_string.items[b.string_start..]);
+ break :code_block try p.addNode(.{
+ .tag = .code_block,
+ .data = .{ .code_block = .{
+ .tag = b.data.code_block.tag,
+ .content = content,
+ } },
+ });
+ },
+ .blockquote => blockquote: {
+ assert(b.string_start == p.scratch_string.items.len);
+ const children = try p.addExtraChildren(@ptrCast(p.scratch_extra.items[b.extra_start..]));
+ break :blockquote try p.addNode(.{
+ .tag = .blockquote,
+ .data = .{ .container = .{
+ .children = children,
+ } },
+ });
+ },
+ .paragraph => paragraph: {
+ const children = try p.parseInlines(p.scratch_string.items[b.string_start..]);
+ break :paragraph try p.addNode(.{
+ .tag = .paragraph,
+ .data = .{ .container = .{
+ .children = children,
+ } },
+ });
+ },
+ .thematic_break => try p.addNode(.{
+ .tag = .thematic_break,
+ .data = .{ .none = {} },
+ }),
+ };
+ p.scratch_string.items.len = b.string_start;
+ p.scratch_extra.items.len = b.extra_start;
+ try p.addScratchExtraNode(node);
+}
+
+const InlineParser = struct {
+ parent: *Parser,
+ content: []const u8,
+ pos: usize = 0,
+ pending_inlines: std.ArrayListUnmanaged(PendingInline) = .{},
+ completed_inlines: std.ArrayListUnmanaged(CompletedInline) = .{},
+
+ const PendingInline = struct {
+ tag: Tag,
+ data: Data,
+ start: usize,
+
+ const Tag = enum {
+ /// Data is `emphasis`.
+ emphasis,
+ /// Data is `none`.
+ link,
+ /// Data is `none`.
+ image,
+ };
+
+ const Data = union {
+ none: void,
+ emphasis: struct {
+ underscore: bool,
+ run_len: usize,
+ },
+ };
+ };
+
+ const CompletedInline = struct {
+ node: Node.Index,
+ start: usize,
+ len: usize,
+ };
+
+ fn deinit(ip: *InlineParser) void {
+ ip.pending_inlines.deinit(ip.parent.allocator);
+ ip.completed_inlines.deinit(ip.parent.allocator);
+ }
+
+ /// Parses all of `ip.content`, returning the children of the node
+ /// containing the inline content.
+ fn parse(ip: *InlineParser) Allocator.Error!ExtraIndex {
+ while (ip.pos < ip.content.len) : (ip.pos += 1) {
+ switch (ip.content[ip.pos]) {
+ '\\' => ip.pos += 1,
+ '[' => try ip.pending_inlines.append(ip.parent.allocator, .{
+ .tag = .link,
+ .data = .{ .none = {} },
+ .start = ip.pos,
+ }),
+ '!' => if (ip.pos + 1 < ip.content.len and ip.content[ip.pos + 1] == '[') {
+ try ip.pending_inlines.append(ip.parent.allocator, .{
+ .tag = .image,
+ .data = .{ .none = {} },
+ .start = ip.pos,
+ });
+ ip.pos += 1;
+ },
+ ']' => try ip.parseLink(),
+ '*', '_' => try ip.parseEmphasis(),
+ '`' => try ip.parseCodeSpan(),
+ else => {},
+ }
+ }
+
+ const children = try ip.encodeChildren(0, ip.content.len);
+ // There may be pending inlines after parsing (e.g. unclosed emphasis
+ // runs), but there must not be any completed inlines, since those
+ // should all be part of `children`.
+ assert(ip.completed_inlines.items.len == 0);
+ return children;
+ }
+
+ /// Parses a link, starting at the `]` at the end of the link text. `ip.pos`
+ /// is left at the closing `)` of the link target or at the closing `]` if
+ /// there is none.
+ fn parseLink(ip: *InlineParser) !void {
+ var i = ip.pending_inlines.items.len;
+ while (i > 0) {
+ i -= 1;
+ if (ip.pending_inlines.items[i].tag == .link or
+ ip.pending_inlines.items[i].tag == .image) break;
+ } else return;
+ const opener = ip.pending_inlines.items[i];
+ ip.pending_inlines.shrinkRetainingCapacity(i);
+ const text_start = switch (opener.tag) {
+ .link => opener.start + 1,
+ .image => opener.start + 2,
+ else => unreachable,
+ };
+
+ if (ip.pos + 1 >= ip.content.len or ip.content[ip.pos + 1] != '(') return;
+ const text_end = ip.pos;
+
+ const target_start = text_end + 2;
+ var target_end = target_start;
+ var nesting_level: usize = 1;
+ while (target_end < ip.content.len) : (target_end += 1) {
+ switch (ip.content[target_end]) {
+ '\\' => target_end += 1,
+ '(' => nesting_level += 1,
+ ')' => {
+ if (nesting_level == 1) break;
+ nesting_level -= 1;
+ },
+ else => {},
+ }
+ } else return;
+ ip.pos = target_end;
+
+ const children = try ip.encodeChildren(text_start, text_end);
+ const target = try ip.encodeLinkTarget(target_start, target_end);
+
+ const link = try ip.parent.addNode(.{
+ .tag = switch (opener.tag) {
+ .link => .link,
+ .image => .image,
+ else => unreachable,
+ },
+ .data = .{ .link = .{
+ .target = target,
+ .children = children,
+ } },
+ });
+ try ip.completed_inlines.append(ip.parent.allocator, .{
+ .node = link,
+ .start = opener.start,
+ .len = ip.pos - opener.start + 1,
+ });
+ }
+
+ fn encodeLinkTarget(ip: *InlineParser, start: usize, end: usize) !StringIndex {
+ // For efficiency, we can encode directly into string_bytes rather than
+ // creating a temporary string and then encoding it, since this process
+ // is entirely linear.
+ const string_top = ip.parent.string_bytes.items.len;
+ errdefer ip.parent.string_bytes.shrinkRetainingCapacity(string_top);
+
+ var text_iter: TextIterator = .{ .content = ip.content[start..end] };
+ while (text_iter.next()) |content| {
+ switch (content) {
+ .char => |c| try ip.parent.string_bytes.append(ip.parent.allocator, c),
+ .text => |s| try ip.parent.string_bytes.appendSlice(ip.parent.allocator, s),
+ .line_break => try ip.parent.string_bytes.appendSlice(ip.parent.allocator, "\\\n"),
+ }
+ }
+ try ip.parent.string_bytes.append(ip.parent.allocator, 0);
+ return @enumFromInt(string_top);
+ }
+
+ /// Parses emphasis, starting at the beginning of a run of `*` or `_`
+ /// characters. `ip.pos` is left at the last character in the run after
+ /// parsing.
+ fn parseEmphasis(ip: *InlineParser) !void {
+ const char = ip.content[ip.pos];
+ var start = ip.pos;
+ while (ip.pos + 1 < ip.content.len and ip.content[ip.pos + 1] == char) {
+ ip.pos += 1;
+ }
+ var len = ip.pos - start + 1;
+ const underscore = char == '_';
+ const space_before = start == 0 or isWhitespace(ip.content[start - 1]);
+ const space_after = start + len == ip.content.len or isWhitespace(ip.content[start + len]);
+ const punct_before = start == 0 or isPunctuation(ip.content[start - 1]);
+ const punct_after = start + len == ip.content.len or isPunctuation(ip.content[start + len]);
+ // The rules for when emphasis may be closed or opened are stricter for
+ // underscores to avoid inappropriately interpreting snake_case words as
+ // containing emphasis markers.
+ const can_open = if (underscore)
+ !space_after and (space_before or punct_before)
+ else
+ !space_after;
+ const can_close = if (underscore)
+ !space_before and (space_after or punct_after)
+ else
+ !space_before;
+
+ if (can_close and ip.pending_inlines.items.len > 0) {
+ var i = ip.pending_inlines.items.len;
+ while (i > 0 and len > 0) {
+ i -= 1;
+ const opener = &ip.pending_inlines.items[i];
+ if (opener.tag != .emphasis or
+ opener.data.emphasis.underscore != underscore) continue;
+
+ const close_len = @min(opener.data.emphasis.run_len, len);
+ const opener_end = opener.start + opener.data.emphasis.run_len;
+
+ const emphasis = try ip.encodeEmphasis(opener_end, start, close_len);
+ const emphasis_start = opener_end - close_len;
+ const emphasis_len = start - emphasis_start + close_len;
+ try ip.completed_inlines.append(ip.parent.allocator, .{
+ .node = emphasis,
+ .start = emphasis_start,
+ .len = emphasis_len,
+ });
+
+ // There may still be other openers further down in the
+ // stack to close, or part of this run might serve as an
+ // opener itself.
+ start += close_len;
+ len -= close_len;
+
+ // Remove any pending inlines above this on the stack, since
+ // closing this emphasis will prevent them from being closed.
+ // Additionally, if this opener is completely consumed by
+ // being closed, it can be removed.
+ opener.data.emphasis.run_len -= close_len;
+ if (opener.data.emphasis.run_len == 0) {
+ ip.pending_inlines.shrinkRetainingCapacity(i);
+ } else {
+ ip.pending_inlines.shrinkRetainingCapacity(i + 1);
+ }
+ }
+ }
+
+ if (can_open and len > 0) {
+ try ip.pending_inlines.append(ip.parent.allocator, .{
+ .tag = .emphasis,
+ .data = .{ .emphasis = .{
+ .underscore = underscore,
+ .run_len = len,
+ } },
+ .start = start,
+ });
+ }
+ }
+
+ /// Encodes emphasis specified by a run of `run_len` emphasis characters,
+ /// with `start..end` being the range of content contained within the
+ /// emphasis.
+ fn encodeEmphasis(ip: *InlineParser, start: usize, end: usize, run_len: usize) !Node.Index {
+ const children = try ip.encodeChildren(start, end);
+ var inner = switch (run_len % 3) {
+ 1 => try ip.parent.addNode(.{
+ .tag = .emphasis,
+ .data = .{ .container = .{
+ .children = children,
+ } },
+ }),
+ 2 => try ip.parent.addNode(.{
+ .tag = .strong,
+ .data = .{ .container = .{
+ .children = children,
+ } },
+ }),
+ 0 => strong_emphasis: {
+ const strong = try ip.parent.addNode(.{
+ .tag = .strong,
+ .data = .{ .container = .{
+ .children = children,
+ } },
+ });
+ break :strong_emphasis try ip.parent.addNode(.{
+ .tag = .emphasis,
+ .data = .{ .container = .{
+ .children = try ip.parent.addExtraChildren(&.{strong}),
+ } },
+ });
+ },
+ else => unreachable,
+ };
+
+ var run_left = run_len;
+ while (run_left > 3) : (run_left -= 3) {
+ const strong = try ip.parent.addNode(.{
+ .tag = .strong,
+ .data = .{ .container = .{
+ .children = try ip.parent.addExtraChildren(&.{inner}),
+ } },
+ });
+ inner = try ip.parent.addNode(.{
+ .tag = .emphasis,
+ .data = .{ .container = .{
+ .children = try ip.parent.addExtraChildren(&.{strong}),
+ } },
+ });
+ }
+
+ return inner;
+ }
+
+ /// Parses a code span, starting at the beginning of the opening backtick
+ /// run. `ip.pos` is left at the last character in the closing run after
+ /// parsing.
+ fn parseCodeSpan(ip: *InlineParser) !void {
+ const opener_start = ip.pos;
+ ip.pos = mem.indexOfNonePos(u8, ip.content, ip.pos, "`") orelse ip.content.len;
+ const opener_len = ip.pos - opener_start;
+
+ const start = ip.pos;
+ const end = while (mem.indexOfScalarPos(u8, ip.content, ip.pos, '`')) |closer_start| {
+ ip.pos = mem.indexOfNonePos(u8, ip.content, closer_start, "`") orelse ip.content.len;
+ const closer_len = ip.pos - closer_start;
+
+ if (closer_len == opener_len) break closer_start;
+ } else unterminated: {
+ ip.pos = ip.content.len;
+ break :unterminated ip.content.len;
+ };
+
+ var content = if (start < ip.content.len)
+ ip.content[start..end]
+ else
+ "";
+ // This single space removal rule allows code spans to be written which
+ // start or end with backticks.
+ if (mem.startsWith(u8, content, " `")) content = content[1..];
+ if (mem.endsWith(u8, content, "` ")) content = content[0 .. content.len - 1];
+
+ const text = try ip.parent.addNode(.{
+ .tag = .code_span,
+ .data = .{ .text = .{
+ .content = try ip.parent.addString(content),
+ } },
+ });
+ try ip.completed_inlines.append(ip.parent.allocator, .{
+ .node = text,
+ .start = opener_start,
+ .len = ip.pos - opener_start,
+ });
+ // Ensure ip.pos is pointing at the last character of the
+ // closer, not after it.
+ ip.pos -= 1;
+ }
+
+ /// Encodes children parsed in the content range `start..end`. The children
+ /// will be text nodes and any completed inlines within the range.
+ fn encodeChildren(ip: *InlineParser, start: usize, end: usize) !ExtraIndex {
+ const scratch_extra_top = ip.parent.scratch_extra.items.len;
+ defer ip.parent.scratch_extra.shrinkRetainingCapacity(scratch_extra_top);
+
+ var child_index = ip.completed_inlines.items.len;
+ while (child_index > 0 and ip.completed_inlines.items[child_index - 1].start >= start) {
+ child_index -= 1;
+ }
+ const start_child_index = child_index;
+
+ var pos = start;
+ while (child_index < ip.completed_inlines.items.len) : (child_index += 1) {
+ const child_inline = ip.completed_inlines.items[child_index];
+ // Completed inlines must be strictly nested within the encodable
+ // content.
+ assert(child_inline.start >= pos and child_inline.start + child_inline.len <= end);
+
+ if (child_inline.start > pos) {
+ try ip.encodeTextNode(pos, child_inline.start);
+ }
+ try ip.parent.addScratchExtraNode(child_inline.node);
+
+ pos = child_inline.start + child_inline.len;
+ }
+ ip.completed_inlines.shrinkRetainingCapacity(start_child_index);
+
+ if (pos < end) {
+ try ip.encodeTextNode(pos, end);
+ }
+
+ const children = ip.parent.scratch_extra.items[scratch_extra_top..];
+ return try ip.parent.addExtraChildren(@ptrCast(children));
+ }
+
+ /// Encodes textual content `ip.content[start..end]` to `scratch_extra`. The
+ /// encoded content may include both `text` and `line_break` nodes.
+ fn encodeTextNode(ip: *InlineParser, start: usize, end: usize) !void {
+ // For efficiency, we can encode directly into string_bytes rather than
+ // creating a temporary string and then encoding it, since this process
+ // is entirely linear.
+ const string_top = ip.parent.string_bytes.items.len;
+ errdefer ip.parent.string_bytes.shrinkRetainingCapacity(string_top);
+
+ var string_start = string_top;
+ var text_iter: TextIterator = .{ .content = ip.content[start..end] };
+ while (text_iter.next()) |content| {
+ switch (content) {
+ .char => |c| try ip.parent.string_bytes.append(ip.parent.allocator, c),
+ .text => |s| try ip.parent.string_bytes.appendSlice(ip.parent.allocator, s),
+ .line_break => {
+ if (ip.parent.string_bytes.items.len > string_start) {
+ try ip.parent.string_bytes.append(ip.parent.allocator, 0);
+ try ip.parent.addScratchExtraNode(try ip.parent.addNode(.{
+ .tag = .text,
+ .data = .{ .text = .{
+ .content = @enumFromInt(string_start),
+ } },
+ }));
+ string_start = ip.parent.string_bytes.items.len;
+ }
+ try ip.parent.addScratchExtraNode(try ip.parent.addNode(.{
+ .tag = .line_break,
+ .data = .{ .none = {} },
+ }));
+ },
+ }
+ }
+ if (ip.parent.string_bytes.items.len > string_start) {
+ try ip.parent.string_bytes.append(ip.parent.allocator, 0);
+ try ip.parent.addScratchExtraNode(try ip.parent.addNode(.{
+ .tag = .text,
+ .data = .{ .text = .{
+ .content = @enumFromInt(string_start),
+ } },
+ }));
+ }
+ }
+
+ /// An iterator over parts of textual content, handling unescaping of
+ /// escaped characters and line breaks.
+ const TextIterator = struct {
+ content: []const u8,
+ pos: usize = 0,
+
+ const Content = union(enum) {
+ char: u8,
+ text: []const u8,
+ line_break,
+ };
+
+ const replacement = "\u{FFFD}";
+
+ fn next(iter: *TextIterator) ?Content {
+ if (iter.pos >= iter.content.len) return null;
+ if (iter.content[iter.pos] == '\\') {
+ iter.pos += 1;
+ if (iter.pos == iter.content.len) {
+ return .{ .char = '\\' };
+ } else if (iter.content[iter.pos] == '\n') {
+ iter.pos += 1;
+ return .line_break;
+ } else if (isPunctuation(iter.content[iter.pos])) {
+ const c = iter.content[iter.pos];
+ iter.pos += 1;
+ return .{ .char = c };
+ } else {
+ return .{ .char = '\\' };
+ }
+ }
+ return iter.nextCodepoint();
+ }
+
+ fn nextCodepoint(iter: *TextIterator) ?Content {
+ switch (iter.content[iter.pos]) {
+ 0 => {
+ iter.pos += 1;
+ return .{ .text = replacement };
+ },
+ 1...127 => |c| {
+ iter.pos += 1;
+ return .{ .char = c };
+ },
+ else => |b| {
+ const cp_len = std.unicode.utf8ByteSequenceLength(b) catch {
+ iter.pos += 1;
+ return .{ .text = replacement };
+ };
+ const is_valid = iter.pos + cp_len < iter.content.len and
+ std.unicode.utf8ValidateSlice(iter.content[iter.pos..][0..cp_len]);
+ const cp_encoded = if (is_valid)
+ iter.content[iter.pos..][0..cp_len]
+ else
+ replacement;
+ iter.pos += cp_len;
+ return .{ .text = cp_encoded };
+ },
+ }
+ }
+ };
+};
+
+fn parseInlines(p: *Parser, content: []const u8) !ExtraIndex {
+ var ip: InlineParser = .{
+ .parent = p,
+ .content = mem.trim(u8, content, " \t\n"),
+ };
+ defer ip.deinit();
+ return try ip.parse();
+}
+
+pub fn extraData(p: Parser, comptime T: type, index: ExtraIndex) ExtraData(T) {
+ const fields = @typeInfo(T).Struct.fields;
+ var i: usize = @intFromEnum(index);
+ var result: T = undefined;
+ inline for (fields) |field| {
+ @field(result, field.name) = switch (field.type) {
+ u32 => p.extra.items[i],
+ else => @compileError("bad field type"),
+ };
+ i += 1;
+ }
+ return .{ .data = result, .end = i };
+}
+
+pub fn extraChildren(p: Parser, index: ExtraIndex) []const Node.Index {
+ const children = p.extraData(Node.Children, index);
+ return @ptrCast(p.extra.items[children.end..][0..children.data.len]);
+}
+
+fn addNode(p: *Parser, node: Node) !Node.Index {
+ const index: Node.Index = @enumFromInt(@as(u32, @intCast(p.nodes.len)));
+ try p.nodes.append(p.allocator, node);
+ return index;
+}
+
+fn addString(p: *Parser, s: []const u8) !StringIndex {
+ if (s.len == 0) return .empty;
+
+ const index: StringIndex = @enumFromInt(@as(u32, @intCast(p.string_bytes.items.len)));
+ try p.string_bytes.ensureUnusedCapacity(p.allocator, s.len + 1);
+ p.string_bytes.appendSliceAssumeCapacity(s);
+ p.string_bytes.appendAssumeCapacity(0);
+ return index;
+}
+
+fn addExtraChildren(p: *Parser, nodes: []const Node.Index) !ExtraIndex {
+ const index: ExtraIndex = @enumFromInt(@as(u32, @intCast(p.extra.items.len)));
+ try p.extra.ensureUnusedCapacity(p.allocator, nodes.len + 1);
+ p.extra.appendAssumeCapacity(@intCast(nodes.len));
+ p.extra.appendSliceAssumeCapacity(@ptrCast(nodes));
+ return index;
+}
+
+fn addScratchExtraNode(p: *Parser, node: Node.Index) !void {
+ try p.scratch_extra.append(p.allocator, @intFromEnum(node));
+}
+
+fn addScratchStringLine(p: *Parser, line: []const u8) !void {
+ try p.scratch_string.ensureUnusedCapacity(p.allocator, line.len + 1);
+ p.scratch_string.appendSliceAssumeCapacity(line);
+ p.scratch_string.appendAssumeCapacity('\n');
+}
+
+fn isBlank(line: []const u8) bool {
+ return mem.indexOfNone(u8, line, " \t") == null;
+}
+
+fn isPunctuation(c: u8) bool {
+ return switch (c) {
+ '!',
+ '"',
+ '#',
+ '$',
+ '%',
+ '&',
+ '\'',
+ '(',
+ ')',
+ '*',
+ '+',
+ ',',
+ '-',
+ '.',
+ '/',
+ ':',
+ ';',
+ '<',
+ '=',
+ '>',
+ '?',
+ '@',
+ '[',
+ '\\',
+ ']',
+ '^',
+ '_',
+ '`',
+ '{',
+ '|',
+ '}',
+ '~',
+ => true,
+ else => false,
+ };
+}
diff --git a/lib/docs/wasm/markdown/renderer.zig b/lib/docs/wasm/markdown/renderer.zig
new file mode 100644
index 0000000000000000000000000000000000000000..272714aa75847c46f07bf3e7c70719e59960ef34
--- /dev/null
+++ b/lib/docs/wasm/markdown/renderer.zig
@@ -0,0 +1,254 @@
+const std = @import("std");
+const Document = @import("Document.zig");
+const Node = Document.Node;
+
+/// A Markdown document renderer.
+///
+/// Each concrete `Renderer` type has a `renderDefault` function, with the
+/// intention that custom `renderFn` implementations can call `renderDefault`
+/// for node types for which they require no special rendering.
+pub fn Renderer(comptime Writer: type, comptime Context: type) type {
+ return struct {
+ renderFn: *const fn (
+ r: Self,
+ doc: Document,
+ node: Node.Index,
+ writer: Writer,
+ ) Writer.Error!void = renderDefault,
+ context: Context,
+
+ const Self = @This();
+
+ pub fn render(r: Self, doc: Document, writer: Writer) Writer.Error!void {
+ try r.renderFn(r, doc, .root, writer);
+ }
+
+ pub fn renderDefault(
+ r: Self,
+ doc: Document,
+ node: Node.Index,
+ writer: Writer,
+ ) Writer.Error!void {
+ const data = doc.nodes.items(.data)[@intFromEnum(node)];
+ switch (doc.nodes.items(.tag)[@intFromEnum(node)]) {
+ .root => {
+ for (doc.extraChildren(data.container.children)) |child| {
+ try r.renderFn(r, doc, child, writer);
+ }
+ },
+ .list => {
+ if (data.list.start.asNumber()) |start| {
+ if (start == 1) {
+ try writer.writeAll("\n");
+ } else {
+ try writer.print("\n", .{start});
+ }
+ } else {
+ try writer.writeAll("\n");
+ }
+ for (doc.extraChildren(data.list.children)) |child| {
+ try r.renderFn(r, doc, child, writer);
+ }
+ if (data.list.start.asNumber() != null) {
+ try writer.writeAll("
\n");
+ } else {
+ try writer.writeAll("\n");
+ }
+ },
+ .list_item => {
+ try writer.writeAll("- ");
+ for (doc.extraChildren(data.list_item.children)) |child| {
+ if (data.list_item.tight and doc.nodes.items(.tag)[@intFromEnum(child)] == .paragraph) {
+ const para_data = doc.nodes.items(.data)[@intFromEnum(child)];
+ for (doc.extraChildren(para_data.container.children)) |para_child| {
+ try r.renderFn(r, doc, para_child, writer);
+ }
+ } else {
+ try r.renderFn(r, doc, child, writer);
+ }
+ }
+ try writer.writeAll("
\n");
+ },
+ .table => {
+ try writer.writeAll("\n");
+ for (doc.extraChildren(data.container.children)) |child| {
+ try r.renderFn(r, doc, child, writer);
+ }
+ try writer.writeAll("
\n");
+ },
+ .table_row => {
+ try writer.writeAll("\n");
+ for (doc.extraChildren(data.container.children)) |child| {
+ try r.renderFn(r, doc, child, writer);
+ }
+ try writer.writeAll("
\n");
+ },
+ .table_cell => {
+ if (data.table_cell.info.header) {
+ try writer.writeAll(" try writer.writeAll(">"),
+ else => |a| try writer.print(" style=\"text-align: {s}\">", .{@tagName(a)}),
+ }
+
+ for (doc.extraChildren(data.table_cell.children)) |child| {
+ try r.renderFn(r, doc, child, writer);
+ }
+
+ if (data.table_cell.info.header) {
+ try writer.writeAll(" | \n");
+ } else {
+ try writer.writeAll("\n");
+ }
+ },
+ .heading => {
+ try writer.print("", .{data.heading.level});
+ for (doc.extraChildren(data.heading.children)) |child| {
+ try r.renderFn(r, doc, child, writer);
+ }
+ try writer.print("\n", .{data.heading.level});
+ },
+ .code_block => {
+ const tag = doc.string(data.code_block.tag);
+ const content = doc.string(data.code_block.content);
+ if (tag.len > 0) {
+ try writer.print("{}
\n", .{ fmtHtml(tag), fmtHtml(content) });
+ } else {
+ try writer.print("{}
\n", .{fmtHtml(content)});
+ }
+ },
+ .blockquote => {
+ try writer.writeAll("\n");
+ for (doc.extraChildren(data.container.children)) |child| {
+ try r.renderFn(r, doc, child, writer);
+ }
+ try writer.writeAll("
\n");
+ },
+ .paragraph => {
+ try writer.writeAll("");
+ for (doc.extraChildren(data.container.children)) |child| {
+ try r.renderFn(r, doc, child, writer);
+ }
+ try writer.writeAll("
\n");
+ },
+ .thematic_break => {
+ try writer.writeAll("
\n");
+ },
+ .link => {
+ const target = doc.string(data.link.target);
+ try writer.print("", .{fmtHtml(target)});
+ for (doc.extraChildren(data.link.children)) |child| {
+ try r.renderFn(r, doc, child, writer);
+ }
+ try writer.writeAll("");
+ },
+ .image => {
+ const target = doc.string(data.link.target);
+ try writer.print("
");
+ },
+ .strong => {
+ try writer.writeAll("");
+ for (doc.extraChildren(data.container.children)) |child| {
+ try r.renderFn(r, doc, child, writer);
+ }
+ try writer.writeAll("");
+ },
+ .emphasis => {
+ try writer.writeAll("");
+ for (doc.extraChildren(data.container.children)) |child| {
+ try r.renderFn(r, doc, child, writer);
+ }
+ try writer.writeAll("");
+ },
+ .code_span => {
+ const content = doc.string(data.text.content);
+ try writer.print("{}", .{fmtHtml(content)});
+ },
+ .text => {
+ const content = doc.string(data.text.content);
+ try writer.print("{}", .{fmtHtml(content)});
+ },
+ .line_break => {
+ try writer.writeAll("
\n");
+ },
+ }
+ }
+ };
+}
+
+/// Renders an inline node as plain text. Asserts that the node is an inline and
+/// has no non-inline children.
+pub fn renderInlineNodeText(
+ doc: Document,
+ node: Node.Index,
+ writer: anytype,
+) @TypeOf(writer).Error!void {
+ const data = doc.nodes.items(.data)[@intFromEnum(node)];
+ switch (doc.nodes.items(.tag)[@intFromEnum(node)]) {
+ .root,
+ .list,
+ .list_item,
+ .table,
+ .table_row,
+ .table_cell,
+ .heading,
+ .code_block,
+ .blockquote,
+ .paragraph,
+ .thematic_break,
+ => unreachable, // Blocks
+
+ .link, .image => {
+ for (doc.extraChildren(data.link.children)) |child| {
+ try renderInlineNodeText(doc, child, writer);
+ }
+ },
+ .strong => {
+ for (doc.extraChildren(data.container.children)) |child| {
+ try renderInlineNodeText(doc, child, writer);
+ }
+ },
+ .emphasis => {
+ for (doc.extraChildren(data.container.children)) |child| {
+ try renderInlineNodeText(doc, child, writer);
+ }
+ },
+ .code_span, .text => {
+ const content = doc.string(data.text.content);
+ try writer.print("{}", .{fmtHtml(content)});
+ },
+ .line_break => {
+ try writer.writeAll("\n");
+ },
+ }
+}
+
+pub fn fmtHtml(bytes: []const u8) std.fmt.Formatter(formatHtml) {
+ return .{ .data = bytes };
+}
+
+fn formatHtml(
+ bytes: []const u8,
+ comptime fmt: []const u8,
+ options: std.fmt.FormatOptions,
+ writer: anytype,
+) !void {
+ _ = fmt;
+ _ = options;
+ for (bytes) |b| {
+ switch (b) {
+ '<' => try writer.writeAll("<"),
+ '>' => try writer.writeAll(">"),
+ '&' => try writer.writeAll("&"),
+ '"' => try writer.writeAll("""),
+ else => try writer.writeByte(b),
+ }
+ }
+}