chore(docutils): split huge updateNav into smaller functions
per peer review by
MODIFY
packages/docutils/lib/builder/nav.ts
packages/docutils/lib/builder/nav.ts
@@ -85,7 +85,7 @@ export function parseNav(nav: MkDocsYmlNav): ParsedNavData[] {
// if the key is not convertible to a number, it's a name
// which was manually put there by somebody.
- if (isNaN(Number(key))) {
+ if (Number.isNaN(Number(key))) {
navData.name = key;
}
parsedNav = [...parsedNav, navData];
@@ -104,10 +104,131 @@ export function parseNav(nav: MkDocsYmlNav): ParsedNavData[] {
* @param navData Some parsed nav data
* @param header Header name
*/
-function itemsForHeader(navData: ParsedNavData[], header: string) {
+function filterHeaderItems(navData: ParsedNavData[], header: string) {
return _.filter(navData, (item) => _.toPath(item.keypath).includes(header));
}
+/**
+ *
+ * @param rootHeaderKeypath Root keypath as determined by {@linkcode getRootHeaderKeypath}
+ * @param offset Numeric offset within the array of items for the header
+ * @param data Any parsed nav data found. For new items, this will be `undefined`. If not new, it
+ * may or may not have a `name` prop, and if it does, we want to retain it.
+ * @returns Complete keypath for a nav item beginning with the root keypath
+ */
+function getKeypathForHeaderItem(rootHeaderKeypath: string, offset: number, data?: ParsedNavData) {
+ return data?.name
+ ? `${rootHeaderKeypath}.${offset}.${data.name}`
+ : `${rootHeaderKeypath}.${offset}`;
+}
+
+/**
+ * Compares two sets of nav data and determines if they are different.
+ *
+ * This does not compare the entire `nav` object with a new one; it works per-header
+ * @param newNavData Nav data as computed by {@linkcode getNavItemsForDir} corresponding to a
+ * particular header
+ * @param navData Subset of original nav data as parsed by {@linkcode parseNav}, corresponding to
+ * the same header
+ */
+function navDataDidChange(
+ newNavData: Array<Omit<ParsedNavData, 'name'>>,
+ navData: ParsedNavData[]
+): boolean {
+ return Boolean(_.xor(newNavData, _.map(navData, _.partial(_.omit, _, 'name'))).length);
+}
+
+/**
+ * Find the "root" keypath for a particular header
+ * @param headerItems Some subset of the nav data having keypaths corresponding to `header`
+ * @param header Header string
+ * @returns The keypath up to the header string, inclusive
+ */
+function getRootHeaderKeypath(headerItems: ParsedNavData[], header: string) {
+ // these are the parts of the keypath of the first item, which will contain the header string. it
+ // dosn't matter whether we pick the first one or any one; they will all contain the same root
+ // keypath by definition.
+ const rootHeaderKeypathParts = _.toPath(_.first(headerItems)!.keypath);
+
+ // this is the keypath up to the header string, inclusive.
+ // we append indices or names to this keypath
+ return rootHeaderKeypathParts.slice(0, rootHeaderKeypathParts.indexOf(header) + 1).join('.');
+}
+
+/**
+ * This examines the `navData` looking for items matching the header string (which _may_ be defined
+ * by `dir`; if `all` is true, we just use the default header, because we will be returning a whole
+ * lot of headers).
+ *
+ *
+ * @param dir Abs path to directory containing markdown files generated by TypeDoc
+ * @param mkDocsDocsDir Configured `docs_dir` or via options
+ * @param navData Nav data parsed by {@linkcode parseNav}
+ * @param all If `true`, process all markdown files, not just commands
+ * @returns
+ */
+async function getNavItemsForDir(
+ dir: string,
+ mkDocsDocsDir: string,
+ navData: ParsedNavData[],
+ nav: MkDocsYmlNav,
+ all = false
+) {
+ let dataChanged = false;
+ const newHeaderItems: Omit<ParsedNavData, 'name'>[] = [];
+
+ const newRefFilepaths = await findRelativeMarkdownFiles(dir, mkDocsDocsDir);
+
+ const header = all ? _.startCase(path.basename(dir)) : DEFAULT_NAV_HEADER;
+
+ const headerItems = filterHeaderItems(navData, header);
+
+ // if we found items with this header already, we are going
+ // to replace them all wholesale
+ if (headerItems.length) {
+ // we append indices or names to this keypath
+ const rootHeaderKeypath = getRootHeaderKeypath(headerItems, header);
+
+ for (const [offset, fileOrUrl] of newRefFilepaths.entries()) {
+ const data = headerItems.find((item) => item.fileOrUrl === fileOrUrl);
+ const keypath = getKeypathForHeaderItem(rootHeaderKeypath, offset, data);
+ newHeaderItems.push({keypath, fileOrUrl});
+ }
+
+ // look for any differences between what we have and what's in the file
+ if (navDataDidChange(newHeaderItems, headerItems)) {
+ log.debug('Will write new nav data for header %s', header);
+ dataChanged = true;
+ } else {
+ log.debug('No changes for header %s', header);
+ }
+ } else {
+ let navOffset = nav.length;
+ for (const [idx, newRefFilepath] of newRefFilepaths.entries()) {
+ newHeaderItems.push({
+ keypath: `${navOffset}.${header}.${idx}`,
+ fileOrUrl: newRefFilepath,
+ });
+ }
+ log.debug('Will create nav data for header %s', header);
+ dataChanged = true;
+ }
+ return {data: newHeaderItems, changed: dataChanged};
+}
+
+/**
+ * Applies the changes in `newData` to the `mkDocsYml` object.
+ *
+ * **This function mutates `mkDocsYml`.**
+ * @param newData New nav data
+ * @param mkDocsYml Original `mkdocs.yml`
+ */
+function applyNavData(newData: ParsedNavData[], mkDocsYml: MkDocsYml) {
+ for (const {keypath, fileOrUrl} of newData) {
+ _.set(mkDocsYml, `nav.${keypath}`, fileOrUrl);
+ }
+}
+
/**
* Update the `nav` property of `mkdocs.yml` with a list of "command" files generated by TypeDoc via
* `@appium/typedoc-plugin-appium`.
@@ -191,69 +312,16 @@ export async function updateNav({
// we don't need the `name` prop, since the name is already present in the keypath.
const newData: Omit<ParsedNavData, 'name'>[] = [];
- for (const dir of dirs) {
- const newDataForDir: Omit<ParsedNavData, 'name'>[] = [];
- const newRefFilepaths = await findRelativeMarkdownFiles(dir, mkDocsDocsDir);
-
- const header = all ? _.startCase(path.basename(dir)) : DEFAULT_NAV_HEADER;
-
- const headerItems = itemsForHeader(navData, header);
-
- // if we found items with this header already, we are going
- // to replace them all wholesale
- if (headerItems.length) {
+ for await (const dir of dirs) {
+ const {data, changed} = await getNavItemsForDir(dir, mkDocsDocsDir, navData, nav, all);
+ if (changed) {
shouldWriteMkDocsYml = true;
- // these are the parts of the keypath of the first item, which will contain the header string.
- const rootHeaderKeypathParts = _.toPath(_.first(headerItems)!.keypath);
-
- // this is the keypath up to the header string, inclusive.
- // we append indices or names to this keypath
- const rootHeaderKeypath = rootHeaderKeypathParts
- .slice(0, rootHeaderKeypathParts.indexOf(header) + 1)
- .join('.');
-
- for (const [offset, newRefFilepath] of newRefFilepaths.entries()) {
- const data = headerItems.find((item) => item.fileOrUrl === newRefFilepath);
- if (data?.name) {
- newDataForDir.push({
- keypath: `${rootHeaderKeypath}.${offset}.${data.name}`,
- fileOrUrl: newRefFilepath,
- });
- } else {
- newDataForDir.push({
- keypath: `${rootHeaderKeypath}.${offset}`,
- fileOrUrl: newRefFilepath,
- });
- }
- }
-
- // look for any differences between what we have and what's in the file
- if (_.xor(newDataForDir, _.map(headerItems, _.partial(_.omit, _, 'name')))) {
- newData.push(...newDataForDir);
- shouldWriteMkDocsYml = true;
- log.debug('Will write new nav data for header %s', header);
- } else {
- log.debug('No changes for header %s', header);
- }
- } else {
- let navOffset = nav.length;
- for (const [idx, newRefFilepath] of newRefFilepaths.entries()) {
- newDataForDir.push({
- keypath: `${navOffset}.${header}.${idx}`,
- fileOrUrl: newRefFilepath,
- });
- }
- newData.push(...newDataForDir);
- shouldWriteMkDocsYml = true;
- log.debug('Will create nav data for header %s', header);
- }
}
-
- for (const {keypath, fileOrUrl} of newData) {
- _.set(mkDocsYml, `nav.${keypath}`, fileOrUrl);
+ newData.push(...data);
}
if (shouldWriteMkDocsYml) {
+ applyNavData(newData, mkDocsYml);
const yaml = stringifyYaml(mkDocsYml);
log.debug('Writing to %s:\n%s', mkDocsYmlPath, yaml);
await safeWriteFile(mkDocsYmlPath, yaml, true);

chore(docutils): split huge updateNav into smaller functions
per peer review by @mykola-mokhnach