diff --git a/apps/daffio/src/app/docs/components/doc-viewer/doc-viewer.component.html b/apps/daffio/src/app/docs/components/doc-viewer/doc-viewer.component.html index 06df339fa7..31836fe1a7 100644 --- a/apps/daffio/src/app/docs/components/doc-viewer/doc-viewer.component.html +++ b/apps/daffio/src/app/docs/components/doc-viewer/doc-viewer.component.html @@ -4,29 +4,31 @@
- @if (doc.breadcrumbs?.length > 0) { - - } - - @if (isApiPackage) { - - } @else { - - - } + + @if (doc.breadcrumbs?.length > 0) { + + } + + + @if (isApiPackage) { + + } @else { +
+ } +
+
@if (isGuideDoc) { { + path: '**', + component: DaffioDocsDesignComponentPageComponent, + resolve: { + doc: DaffioDesignComponentDocResolver, + }, + data: { + sidebarMode: DaffSidebarModeEnum.SideFixed, + }, + }, + ], + }, { path: '**', component: DaffioDocsPageComponent, diff --git a/apps/daffio/src/app/docs/design/pages/component/component.html b/apps/daffio/src/app/docs/design/pages/component/component.html new file mode 100644 index 0000000000..197fda0a05 --- /dev/null +++ b/apps/daffio/src/app/docs/design/pages/component/component.html @@ -0,0 +1,27 @@ + + + + + + Usage + + + + + + + + + + API + + + @for (apiDoc of doc.api; track $index) { +
+ } +
+
+
+
+
diff --git a/apps/daffio/src/app/docs/design/pages/component/component.spec.ts b/apps/daffio/src/app/docs/design/pages/component/component.spec.ts new file mode 100644 index 0000000000..a8363963d1 --- /dev/null +++ b/apps/daffio/src/app/docs/design/pages/component/component.spec.ts @@ -0,0 +1,39 @@ +import { + provideHttpClient, + withInterceptorsFromDi, +} from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { DaffioDocsDesignComponentPageComponent } from './docs-list.component'; + +describe('DaffioDocsDesignComponentPageComponent', () => { + let component: DaffioDocsDesignComponentPageComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [DaffioDocsDesignComponentPageComponent, + RouterTestingModule, + NoopAnimationsModule], + providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DaffioDocsDesignComponentPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/daffio/src/app/docs/design/pages/component/component.ts b/apps/daffio/src/app/docs/design/pages/component/component.ts new file mode 100644 index 0000000000..72b0672832 --- /dev/null +++ b/apps/daffio/src/app/docs/design/pages/component/component.ts @@ -0,0 +1,55 @@ +import { + ChangeDetectionStrategy, + Component, + OnInit, +} from '@angular/core'; +import { + DomSanitizer, + SafeHtml, +} from '@angular/platform-browser'; +import { ActivatedRoute } from '@angular/router'; +import { LetDirective } from '@ngrx/component'; +import { + map, + Observable, +} from 'rxjs'; + +import { DAFF_ARTICLE_COMPONENTS } from '@daffodil/design/article'; +import { DAFF_TABS_COMPONENTS } from '@daffodil/design/tabs'; +import { + DaffDoc, + DaffGuideDoc, +} from '@daffodil/docs-utils'; + +import { DaffioDocViewerModule } from '../../../components/doc-viewer/doc-viewer.module'; +import { DaffioDesignComponentDoc } from '../../resolvers/component-doc-resolver.service'; + +@Component({ + selector: 'daffio-docs-design-component-page', + templateUrl: './component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + LetDirective, + DAFF_TABS_COMPONENTS, + DAFF_ARTICLE_COMPONENTS, + DaffioDocViewerModule, + ], +}) +export class DaffioDocsDesignComponentPageComponent implements OnInit { + doc$: Observable; + + constructor( + private route: ActivatedRoute, + private sanitizer: DomSanitizer, + ) {} + + ngOnInit() { + this.doc$ = this.route.data.pipe(map((data: { doc: DaffioDesignComponentDoc }) => data.doc)); + } + + getInnerHtml(doc: DaffDoc | DaffGuideDoc): SafeHtml { + //It is necessary to bypass the default angular sanitization to keep id tags in the injected html. These id tags are used for fragment routing. + return this.sanitizer.bypassSecurityTrustHtml(doc.contents); + } +} diff --git a/apps/daffio/src/app/docs/design/resolvers/component-doc-resolver.service.spec.ts b/apps/daffio/src/app/docs/design/resolvers/component-doc-resolver.service.spec.ts new file mode 100644 index 0000000000..33bec1e399 --- /dev/null +++ b/apps/daffio/src/app/docs/design/resolvers/component-doc-resolver.service.spec.ts @@ -0,0 +1,71 @@ +import { TestBed } from '@angular/core/testing'; +import { + RouterStateSnapshot, + Router, +} from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { cold } from 'jasmine-marbles'; +import { + of, + Observable, + throwError, +} from 'rxjs'; + +import { DaffDoc } from '@daffodil/docs-utils'; + +import { DocsResolver } from './component-doc-resolver.service'; +import { DaffioDocsServiceInterface } from '../services/docs-service.interface'; +import { DaffioDocsService } from '../services/docs.service'; +import { DaffioDocsFactory } from '../testing/factories/docs.factory'; + +describe('DocsResolver', () => { + let resolver: DocsResolver; + let docsService: DaffioDocsService; + let router: Router; + + const doc = new DaffioDocsFactory().create(); + const stubDocService: DaffioDocsServiceInterface = { + get: (path: string): Observable => of(doc), + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + ], + providers: [ + { provide: DaffioDocsService, useValue: stubDocService }, + ], + }); + + router = TestBed.inject(Router); + docsService = TestBed.inject(DaffioDocsService); + resolver = TestBed.inject(DocsResolver); + }); + + it('should be created', () => { + expect(resolver).toBeTruthy(); + }); + + it('should complete with a doc', () => { + const expected = cold('(a|)', { a: doc }); + expect(resolver.resolve(null, { url: 'my/path' })).toBeObservable(expected); + }); + + describe('if the doc doesn\'t exist (the doc service errors)', () => { + beforeEach(() => { + spyOn(docsService, 'get').and.returnValue(throwError('error')); + spyOn(router, 'navigate'); + }); + + it('should resolve with an empty observable', () => { + const expected = cold('(|)'); + expect(resolver.resolve(null, { url: 'my/path' })).toBeObservable(expected); + }); + + it('should redirect to the 404 page', () => { + resolver.resolve(null, { url: 'my/path' }).subscribe(); + expect(router.navigate).toHaveBeenCalledWith(['/404'], { skipLocationChange: true }); + }); + }); +}); diff --git a/apps/daffio/src/app/docs/design/resolvers/component-doc-resolver.service.ts b/apps/daffio/src/app/docs/design/resolvers/component-doc-resolver.service.ts new file mode 100644 index 0000000000..b7e20a2495 --- /dev/null +++ b/apps/daffio/src/app/docs/design/resolvers/component-doc-resolver.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@angular/core'; +import { + ActivatedRouteSnapshot, + RouterStateSnapshot, + Router, +} from '@angular/router'; +import { + Observable, + EMPTY, + combineLatest, +} from 'rxjs'; +import { + take, + catchError, + switchMap, + withLatestFrom, + map, +} from 'rxjs/operators'; + +import { + daffUriTruncateLeadingSlash, + daffUriTruncateQueryFragment, +} from '@daffodil/core/routing'; +import { + DaffApiDoc, + DaffPackageGuideDoc, +} from '@daffodil/docs-utils'; + +import { DaffioDocsService } from '../../services/docs.service'; + +export interface DaffioDesignComponentDoc { + guide: DaffPackageGuideDoc; + api: Array; +} + +@Injectable({ + providedIn: 'root', +}) +export class DaffioDesignComponentDocResolver { + + constructor(private docService: DaffioDocsService, private router: Router) { } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.docService + //remove any route fragment and initial slash from the route. + .get(daffUriTruncateLeadingSlash(daffUriTruncateQueryFragment(state.url))) + .pipe( + take(1), + switchMap((packageDoc) => + combineLatest(packageDoc.symbols?.map((symbol) => + this.docService.get(symbol)), + ).pipe( + map((api) => ({ + guide: packageDoc, + api, + })), + ), + ), + catchError(() => { + this.router.navigate(['/404'], { skipLocationChange: true }); + return EMPTY; + }), + ); + } +} diff --git a/apps/daffio/src/app/docs/services/docs.service.ts b/apps/daffio/src/app/docs/services/docs.service.ts index 00ba30814f..27a3bc9a26 100644 --- a/apps/daffio/src/app/docs/services/docs.service.ts +++ b/apps/daffio/src/app/docs/services/docs.service.ts @@ -26,7 +26,7 @@ export class DaffioDocsService implements DaffioDoc @Inject(DAFFIO_DOCS_PATH_TOKEN) private docsPath: string, ) {} - get(path: string): Observable { - return this.fetchAsset.fetch(`${this.docsPath}/${crossOsFilename(path)}.json`); + get(path: string): Observable { + return this.fetchAsset.fetch(`${this.docsPath}/${crossOsFilename(path)}.json`); } } diff --git a/libs/docs-utils/src/doc/package-guide.type.ts b/libs/docs-utils/src/doc/package-guide.type.ts new file mode 100644 index 0000000000..47fc1911b5 --- /dev/null +++ b/libs/docs-utils/src/doc/package-guide.type.ts @@ -0,0 +1,11 @@ +import { DaffGuideDoc } from './guide.type'; + +/** + * A guide doc for a package. + */ +export interface DaffPackageGuideDoc extends DaffGuideDoc { + /** + * A list of symbol paths exported from the package. + */ + symbols?: Array; +} diff --git a/libs/docs-utils/src/doc/public_api.ts b/libs/docs-utils/src/doc/public_api.ts index 5507162d3f..c0f4044713 100644 --- a/libs/docs-utils/src/doc/public_api.ts +++ b/libs/docs-utils/src/doc/public_api.ts @@ -1,3 +1,4 @@ export * from './api.type'; export * from './guide.type'; +export * from './package-guide.type'; export * from './type'; diff --git a/tools/dgeni/src/processors/add-api-symbols-to-package.ts b/tools/dgeni/src/processors/add-api-symbols-to-package.ts new file mode 100644 index 0000000000..1036ce1f4d --- /dev/null +++ b/tools/dgeni/src/processors/add-api-symbols-to-package.ts @@ -0,0 +1,29 @@ +import { Document } from 'dgeni'; + +import { CollectLinkableSymbolsProcessor } from './collect-linkable-symbols'; +import { FilterableProcessor } from '../utils/filterable-processor.type'; + +export const ADD_API_SYMBOLS_TO_PACKAGES_PROCESSOR_NAME = 'addApiSymbolsToPackages'; + +export class AddApiSymbolsToPackagesProcessor implements FilterableProcessor { + readonly name = ADD_API_SYMBOLS_TO_PACKAGES_PROCESSOR_NAME; + readonly $runAfter = ['paths-absolutified']; + readonly $runBefore = ['rendering-docs']; + + docTypes = []; + lookup = (doc: Document) => doc.id; + + $process(docs: Array): Array { + return docs.map(doc => { + if (this.docTypes.includes(doc.docType)) { + doc.symbols = CollectLinkableSymbolsProcessor.packages.get(this.lookup(doc)); + } + return doc; + }); + } +}; + +export const ADD_API_SYMBOLS_TO_PACKAGES_PROCESSOR_PROVIDER = [ + ADD_API_SYMBOLS_TO_PACKAGES_PROCESSOR_NAME, + () => new AddApiSymbolsToPackagesProcessor(), +]; diff --git a/tools/dgeni/src/processors/collect-linkable-symbols.ts b/tools/dgeni/src/processors/collect-linkable-symbols.ts index 12d7f33c81..dacecd2fe2 100644 --- a/tools/dgeni/src/processors/collect-linkable-symbols.ts +++ b/tools/dgeni/src/processors/collect-linkable-symbols.ts @@ -10,11 +10,16 @@ export const COLLECT_LINKABLE_SYMBOLS_PROCESSOR_NAME = 'collectLinkableSymbols'; */ export class CollectLinkableSymbolsProcessor implements Processor { private static readonly _symbols = new Map(); + private static readonly _packages = new Map>(); public static get symbols(): ReadonlyMap { return this._symbols; } + public static get packages(): ReadonlyMap> { + return this._packages; + } + name = COLLECT_LINKABLE_SYMBOLS_PROCESSOR_NAME; $runAfter = ['paths-absolutified']; $runBefore = ['markdown']; @@ -27,6 +32,13 @@ export class CollectLinkableSymbolsProcessor implements Processor { this.log.warn(this.createDocMessage(`Linkable symbol collision for name ${doc.name}. Existing path: ${CollectLinkableSymbolsProcessor._symbols.get(doc.name)}, new path: ${doc.path}`)); } CollectLinkableSymbolsProcessor._symbols.set(doc.name, doc.path); + if (doc.docType !== 'package') { + const packageName = doc.id.match(/(.*)\/src/)[1]; + if (!CollectLinkableSymbolsProcessor._packages.get(packageName)) { + CollectLinkableSymbolsProcessor._packages.set(packageName, []); + } + CollectLinkableSymbolsProcessor._packages.get(packageName).push(doc.path); + } }); return docs; diff --git a/tools/dgeni/src/transforms/daffodil-guides-package/index.ts b/tools/dgeni/src/transforms/daffodil-guides-package/index.ts index cab373a057..ec3a5bc7b6 100644 --- a/tools/dgeni/src/transforms/daffodil-guides-package/index.ts +++ b/tools/dgeni/src/transforms/daffodil-guides-package/index.ts @@ -18,6 +18,10 @@ import { } from './reader/guide-file.reader'; import { DAFF_DGENI_EXCLUDED_PACKAGES_REGEX } from '../../constants/excluded-packages'; import { AbsolutifyPathsProcessor } from '../../processors/absolutify-paths'; +import { + ADD_API_SYMBOLS_TO_PACKAGES_PROCESSOR_PROVIDER, + AddApiSymbolsToPackagesProcessor, +} from '../../processors/add-api-symbols-to-package'; import { AddKindProcessor } from '../../processors/add-kind'; import { BreadcrumbProcessor } from '../../processors/breadcrumb'; import { ConvertToJsonProcessor } from '../../processors/convertToJson'; @@ -123,6 +127,7 @@ const design = new Package('design-base', [base]) export const designDocsPackage = new Package('design-docs', [design]) .processor(...GENERATE_NAV_LIST_PROCESSOR_PROVIDER) .processor(...FILTER_NAV_INDEX_PROCESSOR_PROVIDER) + .processor(...ADD_API_SYMBOLS_TO_PACKAGES_PROCESSOR_PROVIDER) .config((generateNavList: GenerateNavListProcessor) => { generateNavList.outputFolder = `${DAFF_DOCS_PATH}/${DAFF_DOCS_DESIGN_PATH}`; }) @@ -143,6 +148,10 @@ export const designDocsPackage = new Package('design-docs', [design]) getAliases: (doc) => [doc.id], }); }) + .config((addApiSymbolsToPackages: AddApiSymbolsToPackagesProcessor) => { + addApiSymbolsToPackages.docTypes.push('package-guide'); + addApiSymbolsToPackages.lookup = (doc) => doc.id.replace('components/', ''); + }) .config((readFilesProcessor) => { readFilesProcessor.basePath = DESIGN_PATH; readFilesProcessor.sourceFiles = [ @@ -150,7 +159,7 @@ export const designDocsPackage = new Package('design-docs', [design]) ]; }) .config((convertToJson: ConvertToJsonProcessor) => { - convertToJson.extraFields.push('description'); + convertToJson.extraFields.push('description', 'symbols'); }) .config((absolutifyPaths: AbsolutifyPathsProcessor) => { absolutifyPaths.docTypes = docTypes;