diff --git a/.projen/deps.json b/.projen/deps.json index 501c6844..b6a2129e 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -24,6 +24,10 @@ "name": "@types/lunr", "type": "build" }, + { + "name": "@types/node-emoji", + "type": "build" + }, { "name": "@types/node", "version": "^12.20.0", @@ -196,6 +200,10 @@ "name": "lunr", "type": "runtime" }, + { + "name": "node-emoji", + "type": "runtime" + }, { "name": "prism-react-renderer", "type": "runtime" diff --git a/.projen/tasks.json b/.projen/tasks.json index 91cff227..2ff8ed07 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -319,7 +319,7 @@ "exec": "yarn install --check-files" }, { - "exec": "yarn upgrade @testing-library/jest-dom @testing-library/react @testing-library/react-hooks @testing-library/user-event @types/jest @types/lunr @types/node @types/react @types/react-dom @types/react-helmet @types/react-router-dom @typescript-eslint/eslint-plugin @typescript-eslint/parser cypress eslint eslint-config-prettier eslint-import-resolver-node eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prefer-arrow eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks express express-http-proxy json-schema npm-check-updates prettier projen react-app-rewired standard-version ts-unused-exports typescript @chakra-ui/anatomy @chakra-ui/icons @chakra-ui/theme-tools @emotion/react @emotion/styled @jsii/spec copy-to-clipboard date-fns framer-motion hast-util-sanitize jsii-reflect lunr prism-react-renderer react react-dom react-helmet react-markdown react-router-dom react-scripts rehype-raw rehype-sanitize remark-emoji remark-gfm web-vitals workbox-core workbox-expiration workbox-precaching workbox-routing workbox-strategies" + "exec": "yarn upgrade @testing-library/jest-dom @testing-library/react @testing-library/react-hooks @testing-library/user-event @types/jest @types/lunr @types/node-emoji @types/node @types/react @types/react-dom @types/react-helmet @types/react-router-dom @typescript-eslint/eslint-plugin @typescript-eslint/parser cypress eslint eslint-config-prettier eslint-import-resolver-node eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prefer-arrow eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks express express-http-proxy json-schema npm-check-updates prettier projen react-app-rewired standard-version ts-unused-exports typescript @chakra-ui/anatomy @chakra-ui/icons @chakra-ui/theme-tools @emotion/react @emotion/styled @jsii/spec copy-to-clipboard date-fns framer-motion hast-util-sanitize jsii-reflect lunr node-emoji prism-react-renderer react react-dom react-helmet react-markdown react-router-dom react-scripts rehype-raw rehype-sanitize remark-emoji remark-gfm web-vitals workbox-core workbox-expiration workbox-precaching workbox-routing workbox-strategies" }, { "exec": "npx projen" diff --git a/.projenrc.js b/.projenrc.js index 5a318ed2..555ed744 100644 --- a/.projenrc.js +++ b/.projenrc.js @@ -49,6 +49,7 @@ const project = new web.ReactTypeScriptProject({ "framer-motion@^4", "jsii-reflect", "lunr", + "node-emoji", "prism-react-renderer", "react-helmet", "react-markdown", @@ -67,6 +68,7 @@ const project = new web.ReactTypeScriptProject({ devDeps: [ "@types/lunr", + "@types/node-emoji", "@types/react-helmet", "@types/react-router-dom", "eslint-plugin-jsx-a11y", diff --git a/package.json b/package.json index 875c97ec..a0590791 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@types/jest": "^26.0.24", "@types/lunr": "^2.3.4", "@types/node": "^12.20.0", + "@types/node-emoji": "^1.8.1", "@types/react": "^17.0.34", "@types/react-dom": "^17.0.11", "@types/react-helmet": "^6.1.4", @@ -88,6 +89,7 @@ "hast-util-sanitize": "^3.0.2", "jsii-reflect": "^1.43.0", "lunr": "^2.3.9", + "node-emoji": "^1.11.0", "prism-react-renderer": "^1.2.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/src/__mocks__/remark-emoji.ts b/src/__mocks__/remark-emoji.ts new file mode 100644 index 00000000..b1ea52e2 --- /dev/null +++ b/src/__mocks__/remark-emoji.ts @@ -0,0 +1,2 @@ +// Don't process emoji for tests +export default (i: any) => i; diff --git a/src/components/Markdown/Markdown.tsx b/src/components/Markdown/Markdown.tsx index bcd6f0f3..2bb3c6ca 100644 --- a/src/components/Markdown/Markdown.tsx +++ b/src/components/Markdown/Markdown.tsx @@ -11,7 +11,6 @@ import rehypeRaw from "rehype-raw"; import rehypeSanitize from "rehype-sanitize"; import remarkEmoji from "remark-emoji"; import remarkGfm from "remark-gfm"; -import { CONSTRUCT_HUB_REPO_URL } from "../../constants/links"; import { Code } from "./Code"; import { Headings } from "./Headings"; import { Hr } from "./Hr"; @@ -21,8 +20,6 @@ import { Table, Thead, Tbody, Tfoot, Tr, Th, Td, TableCaption } from "./Table"; import testIds from "./testIds"; import { A, Blockquote, Em, P, Pre, Sup } from "./Text"; -const ONE_MEGABYTE = 1024 * 1024; - const components: ReactMarkdownOptions["components"] = { a: A, blockquote: Blockquote, @@ -126,16 +123,6 @@ export const Markdown: FunctionComponent<{ return `https://${githubPrefix}/${owner}/${repo}/${githubSuffix}/${url}`; }; - const byteLength = Buffer.byteLength(children); - if (byteLength > ONE_MEGABYTE) { - children = children.substring(0, children.lastIndexOf("# API Reference")); - children = [ - children, - "# API Reference", - "The API Reference for this package could not be rendered.", - `If this issue persists, please let us know by creating an [issue](${CONSTRUCT_HUB_REPO_URL}/issues/new)`, - ].join("\n"); - } return ( = ({ +interface NavItemWrapperProps { + path?: string; + title: string; + showToggle: boolean; + children: ReactNode; +} + +const NavItemWrapper: FunctionComponent = ({ children, - display, - url, + path, + title, + showToggle, +}) => { + const sharedProps = { + _hover: { bg: "rgba(0, 124, 253, 0.05)" }, + overflow: "hidden", + pl: showToggle ? 1 : 2, + py: 1.5, + textOverflow: "ellipsis", + w: "100%", + }; + + return path ? ( + + {children} + + ) : ( + {children} + ); +}; + +export const NavItem: FunctionComponent = ({ + children, + title, + path, onOpen, }) => { - const { pathname, hash } = useLocation(); - const isHashUrl = url.startsWith("#"); - const linkIsActive = isHashUrl ? hash === url : pathname === url; + const linkIsActive = false; const disclosure = useDisclosure({ onOpen, defaultIsOpen: true }); const showToggle = (children?.length ?? 0) > 0; const showChildren = disclosure.isOpen && showToggle; - const LinkComponent = isHashUrl ? Link : NavLink; - const nestedItems = useMemo( () => children?.map((item, idx) => { @@ -74,20 +100,9 @@ const NavItem: FunctionComponent = ({ w={4} /> )} - - {display} - + + {title} + { - if (!(item instanceof HTMLElement)) { - return itemTree; - } - - const { headingId, headingLevel = "100" } = item.dataset; - const { innerText } = item; - const level = parseInt(headingLevel); - - // Don't create nav items for items with no title / url - if (level > 3 || !innerText || !headingId) { - return itemTree; - } - - const last = itemTree[itemTree.length - 1]; - - if (last == null || last.level >= level) { - return [ - ...itemTree, - { - display: innerText, - url: `#${headingId}`, - level, - children: [], - }, - ]; - } else { - last.children = appendItem(last.children, item); - return itemTree; - } -}; +import { PackageReadme } from "./PackageReadme"; +import { usePackageState } from "./PackageState"; +import { PackageTypeDocs } from "./PackageTypeDocs"; // We want the nav to be sticky, but it should account for the sticky heading as well, which is 72px const TOP_OFFSET = "4.5rem"; -export const PackageDocs: FunctionComponent = ({ - markdown: source, - assembly, -}) => { - const [navItems, setNavItems] = useState([]); - - useEffect(() => { - const tree = [ - ...document.querySelectorAll( - `[data-heading-id][data-heading-title][data-heading-level]` - ), - ].reduce(appendItem, []); +const SubmoduleSelector: FunctionComponent = () => { + const { + assembly: { data }, + } = usePackageState(); - setNavItems(tree); - }, [source]); + return Object.keys(data?.submodules ?? {}).length > 0 ? ( + + + + ) : null; +}; +export const PackageDocs: FunctionComponent = () => { + const { path } = useRouteMatch(); + const { menuItems } = usePackageState(); const { hash } = useLocation(); useEffect(() => { if (hash) { const target = document.querySelector(`${hash}`) as HTMLElement; target?.scrollIntoView(true); + } else { + window.scrollTo(0, 0); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [source]); - - const markdown = useMemo( - () => {source}, - [assembly.repository, source] - ); + }); return ( = ({ pr={4} top={TOP_OFFSET} > - {Object.keys(assembly?.submodules ?? {}).length > 0 && ( - - - - )} + - + = ({ }, }} > - {markdown} + + + + + + + + ); diff --git a/src/views/Package/PackageLayout.tsx b/src/views/Package/PackageLayout.tsx index f5275a3e..df28cbb8 100644 --- a/src/views/Package/PackageLayout.tsx +++ b/src/views/Package/PackageLayout.tsx @@ -1,6 +1,4 @@ import { - Center, - Spinner, Grid, Tab, TabList, @@ -13,23 +11,12 @@ import { Page } from "../../components/Page"; import { DependenciesList } from "./DependenciesList"; import { FeedbackLinks } from "./FeedbackLinks"; import { PackageDocs } from "./PackageDocs"; -import { PackageDocsError } from "./PackageDocsError"; -import { PackageDocsUnsupported } from "./PackageDocsUnsupported"; import { PackageHeader } from "./PackageHeader"; import { usePackageState } from "./PackageState"; import testIds from "./testIds"; export const PackageLayout: FunctionComponent = () => { - const { - assembly, - hasDocs, - hasError, - isLoadingDocs, - isSupported, - markdown, - pageDescription, - pageTitle, - } = usePackageState(); + const { pageDescription, pageTitle } = usePackageState(); const [tabIndex, setTabIndex] = useState(0); @@ -58,25 +45,7 @@ export const PackageLayout: FunctionComponent = () => { - {/* Readme and Api Reference Area */} - {isSupported ? ( - hasError ? ( - - ) : isLoadingDocs ? ( -
- -
- ) : ( - hasDocs && ( - - ) - ) - ) : ( - - )} +
diff --git a/src/views/Package/PackageReadme.tsx b/src/views/Package/PackageReadme.tsx new file mode 100644 index 00000000..8618281a --- /dev/null +++ b/src/views/Package/PackageReadme.tsx @@ -0,0 +1,17 @@ +import { FunctionComponent } from "react"; +import { Markdown } from "../../components/Markdown"; +import { usePackageState } from "./PackageState"; + +export const PackageReadme: FunctionComponent = () => { + const { + isLoadingDocs, + readme, + assembly: { data: assembly }, + } = usePackageState(); + + if (isLoadingDocs || !readme || !assembly) { + return null; + } + + return {readme}; +}; diff --git a/src/views/Package/PackageState.tsx b/src/views/Package/PackageState.tsx index eb24be7f..178f206e 100644 --- a/src/views/Package/PackageState.tsx +++ b/src/views/Package/PackageState.tsx @@ -1,5 +1,11 @@ import type { Assembly } from "@jsii/spec"; -import { createContext, FunctionComponent, useContext, useEffect } from "react"; +import { + createContext, + FunctionComponent, + useContext, + useEffect, + useMemo, +} from "react"; import { useParams } from "react-router-dom"; import { fetchAssembly } from "../../api/package/assembly"; import { fetchMarkdown } from "../../api/package/docs"; @@ -10,6 +16,7 @@ import { useLanguage } from "../../hooks/useLanguage"; import { useQueryParams } from "../../hooks/useQueryParams"; import { useRequest, UseRequestResponse } from "../../hooks/useRequest"; import { NotFound } from "../NotFound"; +import { Types, MenuItem, parseMarkdownStructure } from "./util"; interface PathParams { name: string; @@ -18,6 +25,7 @@ interface PathParams { } interface PackageState { + apiReference?: Types; assembly: UseRequestResponse; hasDocs: boolean; hasError: boolean; @@ -29,8 +37,10 @@ interface PackageState { name: string; pageDescription: string; pageTitle: string; + readme?: string; scope?: string; version: string; + menuItems: MenuItem[]; } const PackageStateContext = createContext(undefined); @@ -104,6 +114,17 @@ export const PackageStateProvider: FunctionComponent = ({ children }) => { (assemblyResponse.loading || markdownResponse.loading) ); + const parsedMd = useMemo(() => { + if (!markdownResponse.data) return { menuItems: [] }; + + return parseMarkdownStructure(markdownResponse.data, { + scope, + name, + version, + language, + }); + }, [markdownResponse.data, name, scope, version, language]); + // Handle missing JSON for assembly if (assemblyResponse.error) { return ; @@ -125,6 +146,7 @@ export const PackageStateProvider: FunctionComponent = ({ children }) => { pageTitle, scope, version, + ...parsedMd, }} > {children} diff --git a/src/views/Package/PackageTypeDocs.tsx b/src/views/Package/PackageTypeDocs.tsx new file mode 100644 index 00000000..2e1bfb0f --- /dev/null +++ b/src/views/Package/PackageTypeDocs.tsx @@ -0,0 +1,40 @@ +import { Heading } from "@chakra-ui/react"; +import { FunctionComponent } from "react"; +import { useParams } from "react-router-dom"; +import { Markdown } from "../../components/Markdown"; +import { PackageDocsError } from "./PackageDocsError"; +import { usePackageState } from "./PackageState"; + +const usePackageTypeDocs = () => { + const { typeId }: { typeId?: string } = useParams(); + const { apiReference } = usePackageState(); + + if (typeId) { + return apiReference?.[typeId]; + } + return; +}; + +export const PackageTypeDocs: FunctionComponent = () => { + const { + isLoadingDocs, + assembly: { data: assembly }, + } = usePackageState(); + const docs = usePackageTypeDocs(); + + if (isLoadingDocs) { + return null; + } else if (!docs || !assembly) { + return ; + } + const { title, content } = docs; + + return ( + <> + + {title} + + {content} + + ); +}; diff --git a/src/views/Package/util.test.ts b/src/views/Package/util.test.ts new file mode 100644 index 00000000..f8a00c3c --- /dev/null +++ b/src/views/Package/util.test.ts @@ -0,0 +1,170 @@ +import { Language } from "../../constants/languages"; +import { parseMarkdownStructure } from "./util"; + +jest.mock("remark-emoji"); +const README_MARKDOWN = ` +# Title 1 + +SomeBodyText + +## Title 2 + +SomeBodyText +`; + +// Data types correspond to headings for `Constructs`, `Structs`, etc. +const DATA_TYPE_1 = "DATA_TYPE_1"; +const DATA_TYPE_2 = "DATA_TYPE_2"; + +// Instance of given data type, aka a name of a class, construct, etc. +const MY_DATA_TYPE_1 = "MY_DATA_TYPE_1"; +const MY_DATA_TYPE_2 = "MY_DATA_TYPE_2"; + +const MY_DATA_TYPE_22Body = `${MY_DATA_TYPE_2}-2Body + +#### HEADER4 + +This is not parsed out +This is another line not parsed out`; + +const MARKDOWN_INPUT = `${README_MARKDOWN}# API Reference + +## ${DATA_TYPE_1} + +### ${MY_DATA_TYPE_1}-1 + +${MY_DATA_TYPE_1}-1Body + +### ${MY_DATA_TYPE_1}-2 + +${MY_DATA_TYPE_1}-2Body + +## ${DATA_TYPE_2} + +### ${MY_DATA_TYPE_2}-1 + +${MY_DATA_TYPE_2}-1Body + +### ${MY_DATA_TYPE_2}-2 +${MY_DATA_TYPE_22Body}`; + +const packageData = { + scope: "@packageScope", + name: "packageName", + version: "0.0.0", + language: Language.TypeScript, +}; + +describe("parseMarkdownStructure", () => { + it("separates readme", () => { + const { readme } = parseMarkdownStructure(MARKDOWN_INPUT, packageData); + expect(readme).toEqual(README_MARKDOWN); + }); + + it("parses api reference structure", () => { + const { apiReference } = parseMarkdownStructure( + MARKDOWN_INPUT, + packageData + ); + expect(apiReference).toEqual({ + [`${MY_DATA_TYPE_1}-1`]: { + title: `${MY_DATA_TYPE_1}-1`, + content: `${MY_DATA_TYPE_1}-1Body`, + }, + [`${MY_DATA_TYPE_1}-2`]: { + title: `${MY_DATA_TYPE_1}-2`, + content: `${MY_DATA_TYPE_1}-2Body`, + }, + [`${MY_DATA_TYPE_2}-1`]: { + title: `${MY_DATA_TYPE_2}-1`, + content: `${MY_DATA_TYPE_2}-1Body`, + }, + [`${MY_DATA_TYPE_2}-2`]: { + title: `${MY_DATA_TYPE_2}-2`, + content: MY_DATA_TYPE_22Body, + }, + }); + }); + + it("parses out menu items", () => { + const { menuItems } = parseMarkdownStructure(MARKDOWN_INPUT, packageData); + const basePath = "/packages/@packageScope/packageName/v/0.0.0"; + const baseApiPath = `${basePath}/api`; + const langQuery = `?lang=${Language.TypeScript}`; + const baseHashPath = `${basePath}${langQuery}`; + expect(menuItems).toEqual([ + { + level: 1, + id: "Readme", + title: "Readme", + path: baseHashPath, + children: [ + { + level: 1, + id: "title-1", + title: "Title 1", + path: `${baseHashPath}#title-1`, + children: [ + { + level: 2, + id: "title-2", + title: "Title 2", + path: `${baseHashPath}#title-2`, + children: [], + }, + ], + }, + ], + }, + { + level: 1, + id: "api-reference", + title: "API Reference", + children: [ + { + level: 2, + id: DATA_TYPE_1, + title: DATA_TYPE_1, + children: [ + { + level: 3, + id: `${MY_DATA_TYPE_1}-1`, + title: `${MY_DATA_TYPE_1}-1`, + path: `${baseApiPath}/${MY_DATA_TYPE_1}-1${langQuery}`, + children: [], + }, + { + level: 3, + id: `${MY_DATA_TYPE_1}-2`, + title: `${MY_DATA_TYPE_1}-2`, + path: `${baseApiPath}/${MY_DATA_TYPE_1}-2${langQuery}`, + children: [], + }, + ], + }, + { + level: 2, + id: DATA_TYPE_2, + title: DATA_TYPE_2, + children: [ + { + level: 3, + id: `${MY_DATA_TYPE_2}-1`, + title: `${MY_DATA_TYPE_2}-1`, + path: `${baseApiPath}/${MY_DATA_TYPE_2}-1${langQuery}`, + children: [], + }, + { + level: 3, + id: `${MY_DATA_TYPE_2}-2`, + title: `${MY_DATA_TYPE_2}-2`, + path: `${baseApiPath}/${MY_DATA_TYPE_2}-2${langQuery}`, + children: [], + }, + ], + }, + ], + }, + ]); + }); +}); diff --git a/src/views/Package/util.ts b/src/views/Package/util.ts new file mode 100644 index 00000000..f577389a --- /dev/null +++ b/src/views/Package/util.ts @@ -0,0 +1,188 @@ +import emoji from "node-emoji"; +import { Language } from "../../constants/languages"; +import { QUERY_PARAMS } from "../../constants/url"; +import { sanitize } from "../../util/sanitize-anchor"; + +export interface MenuItem { + id: string; + path?: string; + title: string; + children: MenuItem[]; + level: number; +} + +export interface Types { + [id: string]: { + title: string; + content: string; + }; +} + +/** + * Recursively insert menu items into appropriate parent's `children` array. + */ +const appendMenuItem = (items: MenuItem[], item: MenuItem): MenuItem[] => { + const last = items[items.length - 1]; + + if (last && last.level < item.level) { + last.children = appendMenuItem(last.children, item); + return items; + } + + items.push(item); + return items; +}; + +/** + * Split markdown string on header lines. Accepts a `maxLevel` to only + * parse to a specified level. Defaults to markdown maximum of `6` + */ +const splitOnHeaders = (md: string, maxLevel: number = 6): string[] => { + const regex = new RegExp(`(^#{1,${maxLevel}}[^#].*)`, "gm"); + return ( + md + .split(regex) + // Trim lines and remove whitespace only entries + .reduce((accum: string[], str: string) => { + const newStr = str.trim(); + if (newStr === "") return accum; + return [...accum, newStr]; + }, []) + ); +}; + +/** + * Extract relevant data from markdown string for use as menu item. Attempts + * to parse known data attributes of `title` and `id` while defaulting to use + * the raw heading value as the default for both if data attributes are not + * present. + */ +const getHeaderAttributes = (hdr: string): { id: string; title: string } => { + const attrStrings = hdr.match(/(\S+)\s*=\s*(\"?)([^"]*)(\2|\s|$)/g) ?? []; + const attrs: { [key: string]: string } = attrStrings.reduce((accum, str) => { + const [key, value] = str.split("="); + const [_, parsedValue] = /['"](.*?)['"]/.exec(value) ?? []; + + return { + ...accum, + [key]: parsedValue, + }; + }, {}); + + // Use raw title for items that don't specify data attributes, like readme + // headers. + const [_, rawTitle] = /^#*\s*([^<]+?)\s*(?:<|$)/.exec(hdr) ?? []; + const wEmoji = rawTitle.replace( + /:\+1:|:-1:|:[\w-]+:/g, + (match: string): string => { + return emoji.get(match) ?? match; + } + ); + const title: string = attrs["data-heading-title"] ?? wEmoji; + const id = attrs["data-heading-id"] ?? encodeURIComponent(sanitize(title)); + + return { id, title }; +}; + +/** + * Accept's markdown document from jsii-docgen with readme and api reference + * documentation and parses the content into a traversable map of menu items + * and types. This allows splitting the rendering of the readme and each item + * in the api reference. + * + * NOTE: currently does not support setext style headings in readme documents. + */ +export const parseMarkdownStructure = ( + input: string, + { + scope, + language, + name, + version, + }: { scope?: string; language: Language; name: string; version: string } +): { readme: string; apiReference: Types; menuItems: MenuItem[] } => { + const nameSegment = scope ? `${scope}/${name}` : `${name}`; + const basePath = `/packages/${nameSegment}/v/${version}`; + const langQuery = `?${QUERY_PARAMS.LANGUAGE}=${language}`; + const separator = + '# API Reference \n'; + + // split into readme and api reference + const segments = input.split(separator); + + // Take the last chunk after the separator + // segments.pop() always returns when length > 1; + const apiReferenceStr = segments.length > 1 ? segments.pop()! : ""; + + // Rejoin all the previous chunks in case the readme has the same Separator + const readmeStr = segments.join(separator); + + //split each on headers + const apiReferenceSplit = splitOnHeaders(apiReferenceStr, 3); + const readmeSplit = splitOnHeaders(readmeStr); + + // Add back api reference title for use as menu item + const apiReferenceParsed = [separator.trim(), ...(apiReferenceSplit ?? [])]; + + const baseReadmePath = `${basePath}${langQuery}`; + const readmeMenuItems = [ + { + level: 1, + id: "Readme", + title: "Readme", + path: baseReadmePath, + children: readmeSplit.reduce((accum: MenuItem[], str: string) => { + if (str.startsWith("#")) { + const { id, title } = getHeaderAttributes(str); + const level = str.match(/(#)/gm)?.length ?? 1; + const menuItem = { + level, + id, + title, + // root package path plus hash for header on readme item + path: `${baseReadmePath}#${id}`, + children: [], + }; + return appendMenuItem(accum, menuItem); + } + return accum; + }, []), + }, + ]; + + let menuItems: MenuItem[] = [...readmeMenuItems]; + const types: Types = {}; + + const getApiPath = (id: string) => `${basePath}/api/${id}${langQuery}`; + let prevType: { id: string; title: string }; + apiReferenceParsed?.forEach((str) => { + // TODO get attributes off of embedded html + const isHeader = str.match(/(^#{1,3}[^#].*)/gm); + if (isHeader?.length) { + const { id, title } = getHeaderAttributes(str); + const level = str.match(/(#)/gm)?.length ?? 1; + + // root package path plus type id segment + // only level 3 headers are types in api reference + const path = level === 3 ? getApiPath(id) : undefined; + const menuItem = { + level, + id, + title, + children: [], + ...(path ? { path } : {}), + }; + + menuItems = appendMenuItem(menuItems, menuItem); + prevType = { id, title }; + } else { + types[prevType.id] = { title: prevType.title, content: str }; + } + }); + + return { + readme: readmeStr, + apiReference: types, + menuItems, + }; +}; diff --git a/yarn.lock b/yarn.lock index 628c4cf7..8ba90ab3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2801,6 +2801,11 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@types/node-emoji@^1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@types/node-emoji/-/node-emoji-1.8.1.tgz#689cb74fdf6e84309bcafce93a135dfecd01de3f" + integrity sha512-0fRfA90FWm6KJfw6P9QGyo0HDTCmthZ7cWaBQndITlaWLTZ6njRyKwrwpzpg+n6kBXBIGKeUHEQuBx7bphGJkA== + "@types/node@*": version "16.11.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42"