Skip to content

Commit

Permalink
feat(docs-utils,dgeni): generate breadcrumbs from doc path (#3047)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: xelaint <xelaint@gmail.com>
  • Loading branch information
griest024 and xelaint authored Sep 19, 2024
1 parent 4427fe1 commit 0e0ca5b
Show file tree
Hide file tree
Showing 24 changed files with 393 additions and 38 deletions.
3 changes: 3 additions & 0 deletions apps/daffio/src/app/docs/api/models/api-reference.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DaffBreadcrumb } from '@daffodil/docs-utils';

import { DaffioGenericDocList } from '../../models/doc-list';

/**
Expand All @@ -7,4 +9,5 @@ export interface DaffioApiReference extends DaffioGenericDocList<DaffioApiRefere
docType: string;
docTypeShorthand: string;
description?: string;
breadcrumbs?: Array<DaffBreadcrumb>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,31 @@
<div>Menu</div>
</button>
<div class="daffio-doc-viewer__grid">
@if (isApiPackage) {
<daffio-api-package [doc]="doc"></daffio-api-package>
} @else {
<daff-article
class="daffio-doc-viewer__content"
[innerHtml]="getInnerHtml(doc)">
</daff-article>
}
<div class="daffio-doc-viewer__content">
@if (doc.breadcrumbs?.length > 0) {
<nav aria-label="Breadcrumb">
<ol daff-breadcrumb>
@for (breadcrumb of doc.breadcrumbs; track $index) {
<li daffBreadcrumbItem [active]="$last">
@if ($last) {
{{breadcrumb.label}}
} @else {
<a [routerLink]="breadcrumb.path">{{breadcrumb.label}}</a>
}
</li>
}
</ol>
</nav>
}

@if (isApiPackage) {
<daffio-api-package [doc]="doc"></daffio-api-package>
} @else {
<daff-article
[innerHtml]="getInnerHtml(doc)">
</daff-article>
}
</div>
<daffio-docs-table-of-contents
*ngIf="doc.tableOfContents"
class="daffio-doc-viewer__table-of-contents"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@

&__content {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 24px;
}

&__table-of-contents {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterLink } from '@angular/router';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';

import { DaffArticleModule } from '@daffodil/design/article';
import {
DaffBreadcrumbComponent,
DaffBreadcrumbItemDirective,
} from '@daffodil/design/breadcrumb';
import { DaffButtonModule } from '@daffodil/design/button';
import { DaffContainerModule } from '@daffodil/design/container';
import { DaffSidebarModule } from '@daffodil/design/sidebar';
Expand All @@ -26,7 +31,10 @@ import { DaffioDocsTableOfContentsModule } from '../table-of-contents/table-of-c
DaffioApiPackageComponent,
DaffSidebarModule,
DaffButtonModule,
DaffBreadcrumbComponent,
DaffBreadcrumbItemDirective,
FontAwesomeModule,
RouterLink,
],
})
export class DaffioDocViewerModule { }
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
max-height: 100vh;
overflow: auto;
position: sticky;
top: calc(var(--daff-sidebar-side-fixed-top-shift) + 48px);
top: calc(var(--daff-sidebar-side-fixed-top-shift) + 96px);

&__title {
@include daff.embolden();
Expand Down
3 changes: 3 additions & 0 deletions apps/daffio/src/app/docs/models/doc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DaffBreadcrumb } from '@daffodil/docs-utils';

export interface DaffioDoc {
id: string;
title: string;
Expand All @@ -9,4 +11,5 @@ export interface DaffioDoc {
slug: string;
}[];
};
breadcrumbs?: Array<DaffBreadcrumb>;
}
1 change: 1 addition & 0 deletions libs/docs-utils/src/breadcrumb/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './type';
14 changes: 14 additions & 0 deletions libs/docs-utils/src/breadcrumb/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* A breadcrumb represents a level in the navigation hierarchy of a webpage.
* A common example is the category that contains a product, e.g. T-Shirts > Men > BallerTee.
*/
export interface DaffBreadcrumb {
/**
* The human-readable label.
*/
label: string;
/**
* The path of the breadcrumb.
*/
path: string;
}
1 change: 1 addition & 0 deletions libs/docs-utils/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { crossOsFilename } from './cross-os-filename';
export * from './kind/public_api';
export * from './breadcrumb/public_api';
export * from './path';
34 changes: 34 additions & 0 deletions tools/dgeni/src/processors/absolutify-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Document } from 'dgeni';

import { FilterableProcessor } from '../utils/filterable-processor.type';

export interface PathedDocument extends Document {
path: string;
}

export const ABSOLUTIFY_PATHS_PROCESSOR_NAME = 'absolutifyPaths';

/**
* Converts paths to absolute if they are not already.
*/
export class AbsolutifyPathsProcessor implements FilterableProcessor {
readonly name = ABSOLUTIFY_PATHS_PROCESSOR_NAME;
readonly $runAfter = ['absolutify-paths'];
readonly $runBefore = ['paths-absolutified'];

docTypes = ['package'];

$process(docs: Array<PathedDocument>): Array<PathedDocument> {
return docs.map((doc) => {
if (doc.path[0] !== '/') {
doc.path = `/${doc.path}`;
}
return doc;
});
}
}

export const ABSOLUTIFY_PATHS_PROCESSOR_PROVIDER = <const>[
ABSOLUTIFY_PATHS_PROCESSOR_NAME,
() => new AbsolutifyPathsProcessor(),
];
9 changes: 8 additions & 1 deletion tools/dgeni/src/processors/add-kind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ export interface KindedDocument extends Document {
kind: DaffDocKind;
}

export const ADD_KIND_PROCESSOR_NAME = 'addKind';

/**
* Adds doc kind based on file path.
*/
export class AddKindProcessor implements FilterableProcessor {
readonly name = 'addKind';
readonly name = ADD_KIND_PROCESSOR_NAME;
readonly $runAfter = ['files-read'];
readonly $runBefore = ['processing-docs'];

Expand All @@ -33,3 +35,8 @@ export class AddKindProcessor implements FilterableProcessor {
});
}
}

export const ADD_KIND_PROCESSOR_PROVIDER = <const>[
ADD_KIND_PROCESSOR_NAME,
() => new AddKindProcessor(),
];
172 changes: 172 additions & 0 deletions tools/dgeni/src/processors/breadcrumb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { Document } from 'dgeni';

import {
DAFF_DOC_KIND_PATH_SEGMENT_MAP,
DAFF_DOCS_DESIGN_PATH,
DAFF_DOCS_PATH,
DaffBreadcrumb,
DaffDocKind,
} from '@daffodil/docs-utils';

import { KindedDocument } from './add-kind';
import { ParentedDocument } from '../transforms/daffodil-api-package/processors/add-subpackage-exports';
import { FilterableProcessor } from '../utils/filterable-processor.type';

const getStaticBreadcrumb = (segment: string, parent: string): DaffBreadcrumb => {
switch (segment) {
case DAFF_DOCS_PATH:
return {
label: 'Docs',
path: `${parent}/${DAFF_DOCS_PATH}`,
};

case DAFF_DOCS_DESIGN_PATH:
return {
label: 'Design',
path: `${parent}/${DAFF_DOCS_DESIGN_PATH}`,
};

case DAFF_DOC_KIND_PATH_SEGMENT_MAP[DaffDocKind.GUIDE]:
return {
label: 'Guides',
path: `${parent}/${DAFF_DOC_KIND_PATH_SEGMENT_MAP[DaffDocKind.GUIDE]}`,
};

case DAFF_DOC_KIND_PATH_SEGMENT_MAP[DaffDocKind.EXPLANATION]:
return {
label: 'Explanations',
path: `${parent}/${DAFF_DOC_KIND_PATH_SEGMENT_MAP[DaffDocKind.EXPLANATION]}`,
};

case DAFF_DOC_KIND_PATH_SEGMENT_MAP[DaffDocKind.PACKAGE]:
return {
label: 'Packages',
path: `${parent}/${DAFF_DOC_KIND_PATH_SEGMENT_MAP[DaffDocKind.PACKAGE]}`,
};

case DAFF_DOC_KIND_PATH_SEGMENT_MAP[DaffDocKind.EXAMPLE]:
return {
label: 'Examples',
path: `${parent}/${DAFF_DOC_KIND_PATH_SEGMENT_MAP[DaffDocKind.EXAMPLE]}`,
};

case DAFF_DOC_KIND_PATH_SEGMENT_MAP[DaffDocKind.API]:
return {
label: 'API',
path: `${parent}/${DAFF_DOC_KIND_PATH_SEGMENT_MAP[DaffDocKind.API]}`,
};

default:
return null;
}
};

/**
* A dgeni document which has breadcrumbs added.
*/
export interface BreadcrumbedDocument extends Document {
breadcrumbs: Array<DaffBreadcrumb>;
}

const getParents = (doc: ParentedDocument): Array<ParentedDocument> =>
doc.parent
? [...getParents(doc.parent), doc.parent]
: [];

/**
* Truncates the label of a breadcrumb such that it will only be the info not contained in the parent.
*/
const truncateLabel = (label: string, parent: string): string =>
label.replace(`${parent}/`, '');

export const BREADCRUMB_PROCESSOR_NAME = 'breadcrumb';

export class BreadcrumbProcessor implements FilterableProcessor {
readonly name = BREADCRUMB_PROCESSOR_NAME;
readonly $runAfter = ['paths-absolutified'];
readonly $runBefore = ['rendering-docs'];

docTypes = [];

constructor(
private aliasMap,
) {}

private getBreadcrumbs(doc: ParentedDocument & KindedDocument): Array<DaffBreadcrumb> {
const segments = doc.path.split('/');
const breadcrumbs = segments
.map((segment, i) => getStaticBreadcrumb(segment, segments.slice(0, i).join('/')))
.filter((b) => !!b);

// once all static breadcrumbs are generated,
// create dynamic breadcrumbs for doc kinds that need them
// TODO: determine actual requirements for this feature
switch (doc.kind) {
case DaffDocKind.PACKAGE:
const parents_ = segments
// get all the dynamic segments not including the last (which is the current doc)
// we only want to process dynamic parents here
.slice(breadcrumbs.length + 1, segments.length - 1)
// look up parents based on an alias built from segments
.flatMap((_, i, ids) => this.aliasMap.getDocs(ids.slice(0, i + 1).join('/')));
breadcrumbs.push(
// turn the parent docs into breadcrumbs
...parents_.map((parent, i) => ({
label: truncateLabel(parent.title, parents_[i - 1]?.title),
path: parent.path,
})),
{
label: parents_.length > 0 ? truncateLabel(doc.title, parents_[parents_.length - 1].title) : doc.title,
path: doc.path,
},
);
break;

case DaffDocKind.API:
if (doc.parent) {
// build a list of parents to this doc and turn them into breadcrumbs
const parents = [
...getParents(doc.parent),
doc.parent,
];
breadcrumbs.push(
...parents.map((parent, i) => ({
label: truncateLabel(parent.name, parents[i - 1]?.name),
path: parent.path,
})),
{
label: parents.length > 0 ? truncateLabel(doc.name, parents[parents.length - 1].name) : doc.name,
path: doc.path,
},
);
}
break;

case DaffDocKind.GUIDE:
case DaffDocKind.EXPLANATION:
case DaffDocKind.EXAMPLE:
default:
breadcrumbs.push({
label: doc.name || doc.title,
path: doc.path,
});
break;
}

return breadcrumbs;
}

$process(docs: Array<ParentedDocument & KindedDocument>): Array<BreadcrumbedDocument> {
return docs.map(doc => ({
...doc,
breadcrumbs: this.docTypes.includes(doc.docType)
? this.getBreadcrumbs(doc)
: [],
}));
}
};

export const BREADCRUMB_PROCESSOR_PROVIDER = <const>[
BREADCRUMB_PROCESSOR_NAME,
(aliasMap) => new BreadcrumbProcessor(aliasMap),
];
2 changes: 1 addition & 1 deletion tools/dgeni/src/processors/collect-linkable-symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class CollectLinkableSymbolsProcessor implements Processor {
}

name = COLLECT_LINKABLE_SYMBOLS_PROCESSOR_NAME;
$runAfter = ['paths-computed'];
$runAfter = ['paths-absolutified'];
$runBefore = ['markdown'];

constructor(private log, private createDocMessage) {}
Expand Down
9 changes: 8 additions & 1 deletion tools/dgeni/src/processors/convertToJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
Document,
} from 'dgeni';

export const CONVERT_TO_JSON_PROCESSOR_NAME = 'convertToJson';

export class ConvertToJsonProcessor implements Processor {
name = 'convertToJson';
name = CONVERT_TO_JSON_PROCESSOR_NAME;
$runBefore = ['writeFilesProcessor'];
docTypes = [];
/**
Expand Down Expand Up @@ -53,3 +55,8 @@ export class ConvertToJsonProcessor implements Processor {
});
}
};

export const CONVERT_TO_JSON_PROCESSOR_PROVIDER = <const>[
CONVERT_TO_JSON_PROCESSOR_NAME,
(log, createDocMessage) => new ConvertToJsonProcessor(log, createDocMessage),
];
Loading

0 comments on commit 0e0ca5b

Please sign in to comment.