From 0b1b3f02252ec92dc361480d7659d28eb685e610 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 6 Mar 2024 14:21:22 -0700 Subject: [PATCH] upstream new autodocs implementation --- lib/docs/index.html | 1350 ++---- lib/docs/main.js | 5927 ++++----------------------- lib/docs/wasm/Decl.zig | 180 + lib/docs/wasm/Walk.zig | 928 +++++ lib/docs/wasm/main.zig | 920 +++++ lib/docs/wasm/markdown.zig | 933 +++++ lib/docs/wasm/markdown/Document.zig | 192 + lib/docs/wasm/markdown/Parser.zig | 1500 +++++++ lib/docs/wasm/markdown/renderer.zig | 254 ++ 9 files changed, 5922 insertions(+), 6262 deletions(-) create mode 100644 lib/docs/wasm/Decl.zig create mode 100644 lib/docs/wasm/Walk.zig create mode 100644 lib/docs/wasm/main.zig create mode 100644 lib/docs/wasm/markdown.zig create mode 100644 lib/docs/wasm/markdown/Document.zig create mode 100644 lib/docs/wasm/markdown/Parser.zig create mode 100644 lib/docs/wasm/markdown/renderer.zig diff --git a/lib/docs/index.html b/lib/docs/index.html index 73f903d9fc..5e74723410 100644 --- a/lib/docs/index.html +++ b/lib/docs/index.html @@ -1,476 +1,151 @@ - + - - Documentation - Zig - + Zig Documentation - - - - - -
-
-
- -
- -
-
-
-
-
- - Search -
- -
- Use spaces instead of dots. See $resource for more info. -
-
-
    - -
    + + + +
    +

    Loading...

    + +

    + + + + + -
    + - - - - - - - - - - - - - + diff --git a/lib/docs/main.js b/lib/docs/main.js index 4991c31b57..95e0a445e8 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 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 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 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"); - const domPrefSlashSearch = document.getElementById("prefSlashSearch"); - const prefs = getLocalStorage(); - loadPrefs(); + var searchTimer = null; - domPrefSlashSearch.addEventListener("change", () => setPrefSlashSearch(domPrefSlashSearch.checked)); + 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, - const scrollMonitor = [ - domActiveGuide, - domGuideTocList, - domDocsScroll, - domSectSearchResults, - ]; + // when this is populated, pressing the "view source" command will + // navigate to this hash. + viewSourceHash: null, + }; + var curNavSearch = ""; + var curSearchIndex = -1; + var imFeelingLucky = false; - computeGuideHashes(); + // names of packages in the same order as wasm + const packageList = []; - let searchTimer = null; - let searchTrimResults = true; + 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; - let escapeHtmlReplacements = { - "&": "&", - '"': """, - "<": "<", - ">": ">", - }; + const text_decoder = new TextDecoder(); + const text_encoder = new TextEncoder(); - let typeKinds = indexTypeKinds(); - let typeTypeId = findTypeTypeId(); - let pointerSizeEnum = { One: 0, Many: 1, Slice: 2, C: 3 }; + 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 - let declSearchIndex = new RadixTree(); - window.search = declSearchIndex; + 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); - // for each module, is an array with modules to get to this one - let canonModPaths = computeCanonicalModulePaths(); + updatePackageList(); - // 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, - } + window.addEventListener('hashchange', onHashChange, false); + domSearch.addEventListener('keydown', onSearchKeyDown, false); + domSearch.addEventListener('input', onSearchChange, false); + window.addEventListener('keydown', onWindowKeyDown, false); + onHashChange(); }); - 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(); + }); + + 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 { - noGuidesAtAll(); + document.title = packageList[0] + suffix; // Home } } - 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 += "

    " + section.name + "

    "; - } - for (let guide of section.guides) { - html += "
    1. " + (guide.title || guide.name) + "
    2. "; - html += guide.toc + "
    "; - } - } - 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 += "
  • " + section.name + "
  • "; - } - domGuideTocList.innerHTML = "
      "+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 = "[src]"; - - 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 = "" + renderTokens((function*() { - yield Tok.identifier(fieldNode.name); - yield Tok.colon; - yield Tok.space; - if (isVarArgs && i === typeObj.params.length - 1) { - yield Tok.period; - yield Tok.period; - yield Tok.period; - } else { - yield* ex(value, {}); - } - yield Tok.comma; - }())); - - html += ""; - - if (docsNonEmpty) { - html += '
    ' + markdown(docs) + "
    "; - } - 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 = `${src}`; - } else if (t.tag == Tag.identifier && isSpecialIndentifier(t.src)) { - result = `${src}`; - } else if (t.tag == Tag.identifier && t.fnDecl) { - result = `${src}`; - } else if (t.tag == Tag.identifier && t.isDecl) { - result = `${src}`; - } else { - result = `${src}`; - } - - if (t.link) { - result = `` + 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; + 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 { - yield Tok.space; + return renderDecl(curNav.decl); } - - 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 += '' + name + ""; - } 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 2: return renderSource(curNav.path); + default: throw new Error("invalid navigation state"); } - } + } - 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; - } + function renderHome() { + if (packageList.length == 1) return renderPackage(0); - case "builtinField": { - yield { src: expr.builtinField, tag: Tag.identifier }; - return; + domStatus.textContent = "TODO implement renderHome for multiple packages"; + domStatus.classList.remove("hidden"); + } + + function renderPackage(pkg_index) { + const root_decl = wasm_exports.find_package_root(pkg_index); + return renderDecl(root_decl); + } + + 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 renderSource(path) { + const decl_index = findFileRoot(path); + if (decl_index == null) return render404(); - } + renderNav(decl_index); + domSourceText.innerHTML = declSourceHtml(decl_index); - - 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; - } - } + domSectSource.classList.remove("hidden"); } - 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 = "
    " + name + "
    (" - + zigAnalysis.typeKinds[typeObj.kind] + ")"; + function renderDeclHeading(decl_index) { + domHdrName.innerText = unwrapString(wasm_exports.decl_category_name(decl_index)); 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; - })()); + renderTopLevelDocs(decl_index); } - 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); + 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"); } } - 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 = "

    No documentation provided.

    "; - } - 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 = "[src]"; - - 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 = - '
    ' +
    -          escapeHtml(fieldName);
    -
    -        if (container.kind === typeKinds.Enum) {
    -          let value = container.values[i];
    -          if (value !== null) {
    -            html += renderTokens((function*() {
    -              yield Tok.space;
    -              yield Tok.eql;
    -              yield Tok.space;
    -              yield* ex(value, {});
    -            })());
    -          }
    -        } else {
    -          let fieldTypeExpr = container.field_types[i];
    -          if (container.kind !== typeKinds.Struct || !container.is_tuple) {
    -            html += renderTokens((function*() {
    -              yield Tok.colon;
    -              yield Tok.space;
    -            })());
    -          }
    -          html += renderTokens(ex(fieldTypeExpr, {}));
    -          let tsn = typeShorthandName(fieldTypeExpr);
    -          if (tsn) {
    -            html += " (" + tsn + ")";
    -          }
    -          if (container.kind === typeKinds.Struct && !container.is_tuple) {
    -            let defaultInitExpr = container.field_defaults[i];
    -            if (defaultInitExpr !== null) {
    -              html += renderTokens((function*() {
    -                yield Tok.space;
    -                yield Tok.eql;
    -                yield Tok.space;
    -                yield* ex(defaultInitExpr, {});
    -              })());
    -            }
    -          }
    -        }
    -
    -        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, - }; - 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("."); - } - - 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; - } - } - - function onHashChange(ev) { - scrollHistory[curNav.hash] = scrollMonitor.map(function (x) { - return [x, x.scrollTop] - }); - - if (skipNextHashChange == decodeURIComponent(location.hash)) { - skipNextHashChange = null; - return; - } - skipNextHashChange = null; - updateCurNav(); - - if (domSearch.value !== curNavSearch) { - domSearch.value = curNavSearch; - if (domSearch.value.length == 0) - domSearchPlaceholder.classList.remove("hidden"); - else - domSearchPlaceholder.classList.add("hidden"); - } - render(); - if (imFeelingLucky) { - imFeelingLucky = false; - activateSelectedResult(); - } - - scroll(); - } - - 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 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); - } - } - } - } - } - - 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; - } - } - } - - 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; - } - } - } - - 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(); } - } - } + resizeDomList(domListNav, list.length, '
  • '); - 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" + 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; - } - - 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; - } - - if ("as" in value.expr) { - value = { - typeRef: zigAnalysis.exprs[value.expr.as.typeRefArg], - expr: zigAnalysis.exprs[value.expr.as.exprArg], - }; - continue; - } - - // if we got here it means that we failed - // produce a link to source code instead - return sourceFileLink(decl); - - } - - } - - function getCanonDeclPath(index) { - if (canonDeclPaths == null) { - canonDeclPaths = computeCanonDeclPaths(); - } - - 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; - } + domSectNav.classList.remove("hidden"); } - let slice = trimmed_docs.slice(0, index); - if (cut) slice += "..."; - return slice; - } + function render404() { + domStatus.textContent = "404 Not Found"; + domStatus.classList.remove("hidden"); + } - function shortDescMarkdown(docs) { - return markdown(shortDesc(docs)); - } + function navLinkFqn(full_name) { + return '#' + full_name; + } - 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]; + function navLinkDeclIndex(decl_index) { + return navLinkFqn(fullyQualifiedName(decl_index)); + } - // Find the first text thing to use as a sidebar title - guide.title = null; - guide.toc = ""; + 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); + } + } - // 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 setViewSourceDecl(decl_index) { + curNav.viewSourceHash = "#src/" + unwrapString(wasm_exports.decl_file_path(decl_index)); + } + + 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"); + } + + 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"); + } + + 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; - } - - - if (node.next) { - walker.resumeAt(node.next, true); - } else { - walker.resumeAt(node, false); - } - 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); + default: + throw new Error("uknown category: " + member_category); } } } - } - - let writer = new commonmark.HtmlRenderer(); - let result = writer.render(ast); - guide.toc = result; + + 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"); } - - // 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); - // } - // } + 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, + '
    source
    '); + 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 { + curNav.tag = 1; + curNav.decl = findDecl(nonSearchPart); + } + } + } + } + + function onHashChange() { + navigate(location.hash); + } + + function navigate(location_hash) { + updateCurNav(location_hash); + if (domSearch.value !== curNavSearch) { + domSearch.value = curNavSearch; } - } - } - - 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 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); - } + render(); + if (imFeelingLucky) { + imFeelingLucky = false; + activateSelectedResult(); } } - return new commonmark.HtmlRenderer({ safe: true }).render(parsed); + function activateSelectedResult() { + if (domSectSearchResults.classList.contains("hidden")) { + 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 activateSelectedResult() { - if (domSectSearchResults.classList.contains("hidden")) { - return; + 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(); } - 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 onSearchKeyDown(ev) { + switch (ev.which) { + case 13: + if (ev.shiftKey || ev.ctrlKey || ev.altKey) 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(); - } - } + clearAsyncSearch(); + imFeelingLucky = true; + location.hash = computeSearchHash(); - - 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(); - - 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; - } - } - - let domDotsToggleTimeout = null; - function onSearchInput(ev) { - curSearchIndex = -1; - - let replaced = domSearch.value.replaceAll(".", " ") - - // 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(); - } - - 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; - } - } 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); - } - 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); - } else { - showModal(domHelpModal); - } - ev.preventDefault(); - ev.stopPropagation(); - break; - case "p": - if (!canToggleModal) break; - - if (isModalVisible(domHelpModal)) { - hideModal(domHelpModal); - } - - // toggle the preferences modal - if (isModalVisible(domPrefsModal)) { - hideModal(domPrefsModal); - } else { - showModal(domPrefsModal); - } - ev.preventDefault(); - ev.stopPropagation(); - } - } - - 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(); - } - 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 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); - } - } - } - - - - 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 renderSearchAPI() { - domSectSearchResults.prepend( - domSearchHelp.parentElement.removeChild(domSearchHelp) - ); - if (canonDeclPaths == null) { - canonDeclPaths = computeCanonDeclPaths(); - } - 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; - } - - let termSet = new Set(); - let termOtherSet = new Set(); - - 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); - - // 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; - } - } - if (!found) { - otherDeclSet.add(d); - } else { - termSet.add(d); - } - } - - 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); - } - - } - } - - 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 }) - } - - 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 + "
  • "; - } - } - - // Replace the search results using our newly constructed HTML string - domListSearchResults.innerHTML = matchedItemsHTML; - renderSearchCursor(); - - domSectSearchResults.classList.remove("hidden"); - } - - - - 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 scrollGuidesTop(ev) { - document.getElementById("activeGuide").children[0].scrollIntoView({ - behavior: "smooth", - }); - ev.preventDefault(); - ev.stopPropagation(); - } - document.scrollGuidesTop = scrollGuidesTop; - - function scrollToHeading(id, alreadyThere) { - // Don't scroll if the current location has a scrolling history. - if (scrollHistory[location.hash]) return; - - 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; - // } + case 27: + if (ev.shiftKey || ev.ctrlKey || ev.altKey) return; - function byNameProperty(a, b) { - return operatorCompare(a.name, b.name); - } + domSearch.value = ""; + domSearch.blur(); + curSearchIndex = -1; + ev.preventDefault(); + ev.stopPropagation(); + startSearch(); + return; + case 38: + if (ev.shiftKey || ev.ctrlKey || ev.altKey) return; + moveSearchCursor(-1); + ev.preventDefault(); + ev.stopPropagation(); + return; + case 40: + if (ev.shiftKey || ev.ctrlKey || ev.altKey) return; - 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 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 getFile(idx) { - const file = zigAnalysis.files[idx]; - return { - name: file[0], - modIndex: file[1], - }; - } - - 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 getLocalStorage() { - if ("localStorage" in window) { - try { - return window.localStorage; - } catch (ignored) { - // localStorage may be disabled (SecurityError) + moveSearchCursor(1); + ev.preventDefault(); + ev.stopPropagation(); + return; + default: + ev.stopPropagation(); // prevent keyboard shortcuts + return; } } - // 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 onSearchChange(ev) { + curSearchIndex = -1; + startAsyncSearch(); } - } - function getPrefSlashSearch() { - return prefs.getItem("slashSearch") === "true"; - } + 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 { + curSearchIndex += dir; + } + if (curSearchIndex < 0) { + curSearchIndex = 0; + } + if (curSearchIndex >= domListSearchResults.children.length) { + curSearchIndex = domListSearchResults.children.length - 1; + } + renderSearchCursor(); + } - 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 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; + } + } + + 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 navigateToSource() { + if (curNav.viewSourceHash != null) { + location.hash = curNav.viewSourceHash; + } + } + + function clearAsyncSearch() { + if (searchTimer != null) { + clearTimeout(searchTimer); + searchTimer = null; + } + } + + function startAsyncSearch() { + clearAsyncSearch(); + searchTimer = setTimeout(startSearch, 100); + } + 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); + + const ignoreCase = (curNavSearch.toLowerCase() === curNavSearch); + const results = executeQuery(curNavSearch, ignoreCase); + + if (results.length !== 0) { + resizeDomList(domListSearchResults, results.length, '
  • '); + + 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)); + } + 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 { + liDom.classList.remove("selected"); + } + } + } + + 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); + } + } + + function byDeclIndexName(a, b) { + const a_name = declIndexName(a); + const b_name = declIndexName(b); + return operatorCompare(a_name, b_name); + } + + function decodeString(ptr, len) { + if (len === 0) return ""; + return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len)); + } + + 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 declDocsHtmlShort(decl_index) { + return unwrapString(wasm_exports.decl_docs_html(decl_index, true)); + } + + function fullyQualifiedName(decl_index) { + return unwrapString(wasm_exports.decl_fqn(decl_index)); + } + + function declIndexName(decl_index) { + return unwrapString(wasm_exports.decl_name(decl_index)); + } + + function declSourceHtml(decl_index) { + return unwrapString(wasm_exports.decl_source_html(decl_index)); + } + + 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 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 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 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 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 findDecl(fqn) { + setInputString(fqn); + const result = wasm_exports.find_decl(); + if (result === -1) return null; + return result; + } + + function findFileRoot(path) { + setInputString(path); + const result = wasm_exports.find_file_root(); + if (result === -1) return null; + return result; + } + + function declParent(decl_index) { + const result = wasm_exports.decl_parent(decl_index); + if (result === -1) return null; + return result; + } + + 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 0000000000..08a45eebf9 --- /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 0000000000..7714c3ce42 --- /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 0000000000..d5f1f7951c --- /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, ""); + try appendEscaped(out, slice); + 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 0000000000..7e6fb31b4a --- /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 + \\ + , + \\
      + \\
    1. Breakfast
    2. + \\
    3. Second breakfast
    4. + \\
    5. Lunch
    6. + \\
    7. Afternoon snack
    8. + \\
    9. Dinner
    10. + \\
    11. Dessert
    12. + \\
    13. Midnight snack
    14. + \\
    + \\
      + \\
    1. Breakfast
    2. + \\
    3. Second breakfast
    4. + \\
    5. Lunch
    6. + \\
    7. Afternoon snack
    8. + \\
    9. Dinner
    10. + \\
    11. Dessert
    12. + \\
    13. Midnight snack
    14. + \\
    + \\
      + \\
    1. Breakfast
    2. + \\
    3. Second breakfast
    4. + \\
    5. Lunch
    6. + \\
    7. Afternoon snack
    8. + \\
    9. Dinner
    10. + \\
    11. Dessert
    12. + \\
    13. Midnight snack
    14. + \\
    + \\
      + \\
    1. Breakfast
    2. + \\
    3. Second breakfast
    4. + \\
    5. Lunch
    6. + \\
    7. Afternoon snack
    8. + \\
    9. Dinner
    10. + \\
    11. Dessert
    12. + \\
    13. Midnight snack
    14. + \\
    + \\ + ); +} + +test "nested lists" { + try testRender( + \\- - Item 1. + \\ - Item 2. + \\Item 2 continued. + \\ * New list. + \\ + , + \\
      + \\
      • + \\
      • Item 1.
      • + \\
      • Item 2. + \\Item 2 continued.
      • + \\
      + \\
        + \\
      • New list.
      • + \\
      + \\
    • + \\
    + \\ + ); +} + +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. + \\ + , + \\
      + \\
    1. Item 1.

      + \\
    2. + \\
    3. Item 2.

      + \\

      This one has another paragraph.

      + \\
    4. + \\
    5. Item 3.

      + \\
    6. + \\
    + \\
      + \\
    • + \\

      Blockquote.

      + \\
      + \\
    • + \\
      • + \\
      • Sub-list.
      • + \\
      • Sub-list continued.
      • + \\
      + \\
        + \\
      • Different sub-list.
      • + \\
      + \\
    • + \\
    • Heading.

      + \\

      Some contents below the heading.

      + \\
        + \\
      1. Item 1.
      2. + \\
      3. Item 2.
      4. + \\
      5. Item 3.
      6. + \\
      + \\
    • + \\
    + \\ + ); +} + +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 | + \\ + , + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\
    OperatorMeaning
    +Add
    -Subtract
    *Multiply
    /Divide
    ??Not sure yet
    + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\
    Item 1Value 1
    Item 2Value 2
    Item 3Value 3
    Item 4Value 4
    + \\ + \\ + \\ + \\ + \\ + \\ + \\
    LeftCenterRight
    + \\ + ); +} + +test "table with uneven number of columns" { + try testRender( + \\| One | + \\| :-- | :--: | + \\| One | Two | Three | + \\ + , + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\ + \\
    One
    OneTwoThree
    + \\ + ); +} + +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

    + \\ + \\ + \\ + \\ + \\ + \\ + \\
    Butthisis
    + \\ + ); +} + +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( + \\![Alt text](https://example.com/image.png) + \\![Alt text *with inlines*](https://example.com/image.png) + \\![Nested parens](https://example.com/nested(parens(inside)).png) + \\![Escaped parens](https://example.com/\)escaped\(.png) + \\![Line break in target](test\ + \\target) + \\ + , + \\

    Alt text + \\Alt text with inlines + \\Nested parens + \\Escaped parens + \\Line break in target

    + \\ + ); +} + +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 0000000000..9e43e35795 --- /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 0000000000..6a2d7e4f4e --- /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 0000000000..272714aa75 --- /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("
    1. "); + 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("
    2. \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), + } + } +}