@mathstack/app-kit 1.1.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +34 -0
- package/LICENSE +21 -21
- package/README.md +36 -31
- package/ng-package.json +15 -0
- package/package.json +9 -13
- package/project.json +76 -0
- package/src/lib/assets/assets-resource.ts +18 -0
- package/src/lib/assets/assets-service.spec.ts +54 -0
- package/src/lib/assets/assets-service.ts +73 -0
- package/src/lib/assets/index.ts +2 -0
- package/{baseline-styles → src/lib/baseline-styles}/_css-reset.scss +118 -118
- package/{baseline-styles → src/lib/baseline-styles}/_grid.scss +4 -4
- package/{baseline-styles → src/lib/baseline-styles}/_index.scss +6 -6
- package/{baseline-styles → src/lib/baseline-styles}/_sass-utils.scss +23 -23
- package/{baseline-styles → src/lib/baseline-styles}/_themes.scss +19 -19
- package/{baseline-styles → src/lib/baseline-styles}/_typography.scss +232 -232
- package/{baseline-styles → src/lib/baseline-styles}/_webkit-scrollbar.scss +22 -22
- package/{baseline-styles → src/lib/baseline-styles}/theming/_colors.scss +46 -46
- package/{baseline-styles → src/lib/baseline-styles}/theming/_index.scss +2 -2
- package/{baseline-styles → src/lib/baseline-styles}/theming/_typography.scss +32 -32
- package/{lib/content-parsing/index.d.ts → src/lib/content-parsing/index.ts} +2 -2
- package/src/lib/content-parsing/markdown-parser.ts +197 -0
- package/src/lib/content-parsing/shiki-highlighter.ts +219 -0
- package/src/lib/core/scss/_colors.scss +37 -0
- package/src/lib/core/scss/_functions.scss +4 -0
- package/src/lib/core/scss/_placeholders.scss +1 -0
- package/src/lib/core/scss/_vars.scss +15 -0
- package/src/lib/core/testing/stubs/services/assets.resource.stub.ts +3 -0
- package/src/lib/core/testing/stubs/services/main.service.stub.ts +5 -0
- package/src/lib/core/utilities/deep-merge.spec.ts +29 -0
- package/src/lib/core/utilities/deep-merge.ts +21 -0
- package/{lib/core/utilities/index.d.ts → src/lib/core/utilities/index.ts} +2 -2
- package/src/lib/core/utilities/safe-assign.spec.ts +41 -0
- package/{lib/core/utilities/safe-assign.d.ts → src/lib/core/utilities/safe-assign.ts} +42 -18
- package/src/lib/documentation-display/active-heading-tracker.ts +61 -0
- package/src/lib/documentation-display/document-index/document-index.component.html +34 -0
- package/src/lib/documentation-display/document-index/document-index.component.scss +56 -0
- package/src/lib/documentation-display/document-index/document-index.component.spec.ts +0 -0
- package/src/lib/documentation-display/document-index/document-index.component.ts +39 -0
- package/src/lib/documentation-display/documentation-config-parser.ts +92 -0
- package/src/lib/documentation-display/documentation-content-service.ts +118 -0
- package/src/lib/documentation-display/documentation-display.component.html +33 -0
- package/src/lib/documentation-display/documentation-display.component.scss +142 -0
- package/src/lib/documentation-display/documentation-display.component.spec.ts +0 -0
- package/src/lib/documentation-display/documentation-display.component.ts +129 -0
- package/{lib/documentation-display/index.d.ts → src/lib/documentation-display/index.ts} +6 -6
- package/src/lib/documentation-display/navigation-siblings/navigation-siblings.component.html +31 -0
- package/src/lib/documentation-display/navigation-siblings/navigation-siblings.component.scss +68 -0
- package/src/lib/documentation-display/navigation-siblings/navigation-siblings.component.spec.ts +0 -0
- package/src/lib/documentation-display/navigation-siblings/navigation-siblings.component.ts +52 -0
- package/{lib/ng-utilities/index.d.ts → src/lib/ng-utilities/index.ts} +2 -2
- package/src/lib/ng-utilities/ng-on-changes.spec.ts +117 -0
- package/src/lib/ng-utilities/ng-on-changes.ts +50 -0
- package/{lib/ng-utilities/run-ng-change-detection-then.d.ts → src/lib/ng-utilities/run-ng-change-detection-then.ts} +40 -22
- package/{public-api.d.ts → src/public-api.ts} +6 -6
- package/tsconfig.lib.json +16 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +10 -0
- package/fesm2022/mathstack-app-kit.mjs +0 -846
- package/fesm2022/mathstack-app-kit.mjs.map +0 -1
- package/index.d.ts +0 -5
- package/lib/assets/assets-resource.d.ts +0 -11
- package/lib/assets/assets-service.d.ts +0 -35
- package/lib/assets/index.d.ts +0 -1
- package/lib/content-parsing/markdown-parser.d.ts +0 -54
- package/lib/content-parsing/shiki-highlighter.d.ts +0 -77
- package/lib/core/utilities/deep-merge.d.ts +0 -1
- package/lib/documentation-display/active-heading-tracker.d.ts +0 -14
- package/lib/documentation-display/document-index/document-index.component.d.ts +0 -17
- package/lib/documentation-display/documentation-config-parser.d.ts +0 -41
- package/lib/documentation-display/documentation-content-service.d.ts +0 -50
- package/lib/documentation-display/documentation-display.component.d.ts +0 -39
- package/lib/documentation-display/navigation-siblings/navigation-siblings.component.d.ts +0 -18
- package/lib/ng-utilities/ng-on-changes.d.ts +0 -22
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function deepMerge<T>(target: T, source: Partial<T>): T {
|
|
2
|
+
const output = Object.assign({}, target);
|
|
3
|
+
if (isObject(target) && isObject(source)) {
|
|
4
|
+
Object.keys(source).forEach((key) => {
|
|
5
|
+
if (isObject(source[key])) {
|
|
6
|
+
if (!(key in (target as object))) {
|
|
7
|
+
Object.assign(output, { [key]: source[key] });
|
|
8
|
+
} else {
|
|
9
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
10
|
+
}
|
|
11
|
+
} else {
|
|
12
|
+
Object.assign(output, { [key]: source[key] });
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return output;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isObject(item: unknown): item is object {
|
|
20
|
+
return item && typeof item === 'object' && !Array.isArray(item);
|
|
21
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './deep-merge';
|
|
2
|
-
export * from './safe-assign';
|
|
1
|
+
export * from './deep-merge';
|
|
2
|
+
export * from './safe-assign';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { safeAssign } from './safe-assign';
|
|
3
|
+
|
|
4
|
+
describe('applyDefaultsSafely', () => {
|
|
5
|
+
it('should apply defaults to the target object', () => {
|
|
6
|
+
const target = { a: 1, b: 2 };
|
|
7
|
+
const defaults = { b: 3, c: 4 };
|
|
8
|
+
|
|
9
|
+
safeAssign(target, defaults);
|
|
10
|
+
|
|
11
|
+
expect(target).toEqual({ a: 1, b: 3, c: 4 } as any);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should not overwrite existing function properties', () => {
|
|
15
|
+
const target = { a: () => 'existing', b: 2 };
|
|
16
|
+
const defaults = { a: () => 'new', b: 3 };
|
|
17
|
+
|
|
18
|
+
safeAssign(target, defaults);
|
|
19
|
+
|
|
20
|
+
expect(target.a()).toBe('existing');
|
|
21
|
+
expect(target.b).toBe(3);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should handle empty defaults', () => {
|
|
25
|
+
const target = { a: 1, b: 2 };
|
|
26
|
+
const defaults = {};
|
|
27
|
+
|
|
28
|
+
safeAssign(target, defaults);
|
|
29
|
+
|
|
30
|
+
expect(target).toEqual({ a: 1, b: 2 });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should handle empty target', () => {
|
|
34
|
+
const target = {};
|
|
35
|
+
const defaults = { a: 1, b: 2 };
|
|
36
|
+
|
|
37
|
+
safeAssign(target, defaults);
|
|
38
|
+
|
|
39
|
+
expect(target).toEqual({ a: 1, b: 2 });
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -1,18 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @
|
|
12
|
-
* @
|
|
13
|
-
* @param
|
|
14
|
-
*
|
|
15
|
-
* @
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safely assigns properties from a source object to a target object.
|
|
5
|
+
*
|
|
6
|
+
* Copies all own, enumerable properties from `source` to `target`,
|
|
7
|
+
* except when the target already has a property of the same name that is a function.
|
|
8
|
+
*
|
|
9
|
+
* Logs a warning for each skipped property if `logConflicts` is true.
|
|
10
|
+
*
|
|
11
|
+
* @template TTarget The type of the target object.
|
|
12
|
+
* @template TSource The type of the source object.
|
|
13
|
+
* @param target The object to assign properties to.
|
|
14
|
+
* @param source The object providing properties to assign.
|
|
15
|
+
* @param logConflicts If true, logs when a method/property on the target prevents assignment. Default is false.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* safeAssign(this, { log: 'hello' }, true);
|
|
19
|
+
*/
|
|
20
|
+
export function safeAssign<TTarget extends object, TSource extends object>(
|
|
21
|
+
target: TTarget,
|
|
22
|
+
source: TSource,
|
|
23
|
+
logConflicts: boolean = false
|
|
24
|
+
): void {
|
|
25
|
+
if (!source) return;
|
|
26
|
+
|
|
27
|
+
for (const key of Object.keys(source)) {
|
|
28
|
+
const existing = (target as any)[key];
|
|
29
|
+
const incoming = (source as any)[key];
|
|
30
|
+
|
|
31
|
+
if (typeof existing === 'function') {
|
|
32
|
+
if (logConflicts) {
|
|
33
|
+
console.warn(
|
|
34
|
+
`safeAssign: Skipped assignment for key "${key}" because it would overwrite an existing method.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
(target as any)[key] = incoming;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { DestroyRef, inject, Injectable, NgZone } from '@angular/core';
|
|
2
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
3
|
+
import {
|
|
4
|
+
BehaviorSubject,
|
|
5
|
+
debounceTime,
|
|
6
|
+
distinctUntilChanged,
|
|
7
|
+
fromEvent,
|
|
8
|
+
} from 'rxjs';
|
|
9
|
+
|
|
10
|
+
@Injectable({
|
|
11
|
+
providedIn: 'root',
|
|
12
|
+
})
|
|
13
|
+
export class AdkActiveHeadingTracker {
|
|
14
|
+
private activeHeadingId = new BehaviorSubject<string | null>(null);
|
|
15
|
+
activeHeadingId$ = this.activeHeadingId.asObservable();
|
|
16
|
+
|
|
17
|
+
private ngZone = inject(NgZone);
|
|
18
|
+
|
|
19
|
+
initScrollListener(contentEl: HTMLElement, destroyRef: DestroyRef): void {
|
|
20
|
+
this.ngZone.runOutsideAngular(() => {
|
|
21
|
+
fromEvent(window, 'scroll')
|
|
22
|
+
.pipe(
|
|
23
|
+
takeUntilDestroyed(destroyRef),
|
|
24
|
+
debounceTime(50),
|
|
25
|
+
distinctUntilChanged()
|
|
26
|
+
)
|
|
27
|
+
.subscribe(() => {
|
|
28
|
+
const headings = Array.from(
|
|
29
|
+
contentEl.querySelectorAll('h1, h2, h3, h4, h5, h6')
|
|
30
|
+
) as HTMLHeadingElement[];
|
|
31
|
+
const activeHeading = this.findActiveHeading(headings);
|
|
32
|
+
this.ngZone.run(() => {
|
|
33
|
+
if (activeHeading) {
|
|
34
|
+
this.activeHeadingId.next(activeHeading.id);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private findActiveHeading(
|
|
42
|
+
headings: HTMLHeadingElement[]
|
|
43
|
+
): HTMLHeadingElement | null {
|
|
44
|
+
const scrollPosition = window.scrollY;
|
|
45
|
+
for (let i = headings.length - 1; i >= 0; i--) {
|
|
46
|
+
const heading = headings[i];
|
|
47
|
+
if (heading.offsetTop <= scrollPosition + 50) {
|
|
48
|
+
return heading;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return headings[0] || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setActiveHeading(heading: HTMLHeadingElement): void {
|
|
55
|
+
this.activeHeadingId.next(heading.id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
scrollToTop(): void {
|
|
59
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<nav class="toc">
|
|
2
|
+
@if (headings.length > 0) {
|
|
3
|
+
<p class="index-title">On this page</p>
|
|
4
|
+
<ul class="links-container">
|
|
5
|
+
@for (heading of headings; track $index) {
|
|
6
|
+
<li class="toc-item">
|
|
7
|
+
<button
|
|
8
|
+
type="button"
|
|
9
|
+
class="toc-link"
|
|
10
|
+
[style.--level]="heading.level"
|
|
11
|
+
(click)="setActiveHeading($event, heading)"
|
|
12
|
+
(keydown.enter)="setActiveHeading($event, heading)"
|
|
13
|
+
[class.active]="
|
|
14
|
+
heading.id === (activeHeadingTracker.activeHeadingId$ | async)
|
|
15
|
+
"
|
|
16
|
+
>{{ heading.text }}</button
|
|
17
|
+
>
|
|
18
|
+
</li>
|
|
19
|
+
}
|
|
20
|
+
</ul>
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
class="back-to-top"
|
|
24
|
+
(click)="scrollToTop()"
|
|
25
|
+
(keydown.enter)="scrollToTop()"
|
|
26
|
+
>
|
|
27
|
+
Back to top<span
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
class="material-symbols-outlined arrow"
|
|
30
|
+
>arrow_upward</span
|
|
31
|
+
>
|
|
32
|
+
</button>
|
|
33
|
+
}
|
|
34
|
+
</nav>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
@use '../../baseline-styles/grid' as *;
|
|
2
|
+
@use '../../core/scss/placeholders' as *;
|
|
3
|
+
@use '../../core/scss/colors';
|
|
4
|
+
|
|
5
|
+
.links-container {
|
|
6
|
+
list-style: none;
|
|
7
|
+
flex-basis: 28%;
|
|
8
|
+
padding: 0;
|
|
9
|
+
margin: 0;
|
|
10
|
+
width: 250px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.index-title {
|
|
14
|
+
@include label-2xs-caps;
|
|
15
|
+
padding-bottom: grid(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.toc-link {
|
|
19
|
+
@include body-xs;
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
padding-top: grid(1);
|
|
22
|
+
padding-bottom: grid(1);
|
|
23
|
+
padding-left: calc((var(--level) - 1) * grid(4));
|
|
24
|
+
display: block;
|
|
25
|
+
border-left: 2px solid colors.$muted-primary-90;
|
|
26
|
+
color: colors.$muted-primary-50;
|
|
27
|
+
text-align: left;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.toc-link:hover {
|
|
31
|
+
color: colors.$muted-primary-25;
|
|
32
|
+
border-left: 2px solid colors.$muted-primary-70;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.toc-link.active {
|
|
36
|
+
color: colors.$primary-40;
|
|
37
|
+
border-left: 2px solid colors.$primary-40;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.back-to-top {
|
|
41
|
+
@include body-xs;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
padding: grid(2) 0;
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
text-align: left;
|
|
47
|
+
color: colors.$muted-primary-40;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.back-to-top:hover {
|
|
51
|
+
color: colors.$muted-primary-10;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.arrow {
|
|
55
|
+
transform: scale(0.7);
|
|
56
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { CommonModule } from '@angular/common';
|
|
2
|
+
import {
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
Component,
|
|
5
|
+
EventEmitter,
|
|
6
|
+
inject,
|
|
7
|
+
Input,
|
|
8
|
+
Output,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { AdkActiveHeadingTracker } from '../active-heading-tracker';
|
|
11
|
+
import { AdkHtmlHeader } from '../documentation-content-service';
|
|
12
|
+
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'hsi-adk-document-index',
|
|
15
|
+
imports: [CommonModule],
|
|
16
|
+
templateUrl: './document-index.component.html',
|
|
17
|
+
styleUrls: ['./document-index.component.scss'],
|
|
18
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19
|
+
})
|
|
20
|
+
export class AdkDocumentIndexComponent {
|
|
21
|
+
@Input() headings: AdkHtmlHeader[] = [];
|
|
22
|
+
@Output() activeHeading = new EventEmitter<{
|
|
23
|
+
heading: AdkHtmlHeader;
|
|
24
|
+
event: KeyboardEvent | PointerEvent | MouseEvent | Event;
|
|
25
|
+
}>();
|
|
26
|
+
|
|
27
|
+
public activeHeadingTracker = inject(AdkActiveHeadingTracker);
|
|
28
|
+
|
|
29
|
+
setActiveHeading(
|
|
30
|
+
event: KeyboardEvent | PointerEvent | MouseEvent | Event,
|
|
31
|
+
heading: AdkHtmlHeader
|
|
32
|
+
): void {
|
|
33
|
+
this.activeHeading.emit({ heading, event });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
scrollToTop(): void {
|
|
37
|
+
this.activeHeadingTracker.scrollToTop();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export interface AdkNestedObject {
|
|
4
|
+
[key: string]: string | AdkNestedObject;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface AdkDocumentationNavigationSiblings {
|
|
8
|
+
previous: string | undefined;
|
|
9
|
+
next: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class AdkDocumentationConfigParser {
|
|
14
|
+
basePath: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* This method takes a path from the application and a configuration object, and returns a path to a file that will be fetched.
|
|
18
|
+
*
|
|
19
|
+
* This assumes that the pathFromApp matches a path through the keys of the Nested Object.
|
|
20
|
+
*
|
|
21
|
+
* If the path is 'viz/content/items/bars', the config object might look like this:
|
|
22
|
+
* viz: {
|
|
23
|
+
* content: {
|
|
24
|
+
* items: {
|
|
25
|
+
* bars: 'bars.md'
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* @param pathFromApp a string representing the path from the application, for example 'viz/content/items/bars'
|
|
31
|
+
* @param config a nested object representing the configuration
|
|
32
|
+
* @returns a string representing the file path
|
|
33
|
+
*/
|
|
34
|
+
getPathToFile(
|
|
35
|
+
pathFromApp: string,
|
|
36
|
+
config: AdkNestedObject,
|
|
37
|
+
basePath: string
|
|
38
|
+
): string {
|
|
39
|
+
const pathParts = pathFromApp.split('/');
|
|
40
|
+
const fileName = this.getFileNameFromPath(pathParts, config);
|
|
41
|
+
const pathToFile = basePath ? `${basePath}/${fileName}` : fileName;
|
|
42
|
+
return pathToFile;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
*
|
|
47
|
+
* @param pathParts a string array of path parts, for example ['viz', 'content', 'items', 'bars]
|
|
48
|
+
* @param config a nested object representing the configuration
|
|
49
|
+
* @returns a string representing the file name
|
|
50
|
+
*/
|
|
51
|
+
private getFileNameFromPath(
|
|
52
|
+
pathParts: string[],
|
|
53
|
+
config: string | AdkNestedObject
|
|
54
|
+
): string {
|
|
55
|
+
const fileName = pathParts.reduce((acc, part) => {
|
|
56
|
+
const level = acc[part];
|
|
57
|
+
acc = level;
|
|
58
|
+
return acc;
|
|
59
|
+
}, config);
|
|
60
|
+
return fileName as string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
findPreviousAndNextByPath(
|
|
64
|
+
obj: AdkNestedObject,
|
|
65
|
+
targetPath: string
|
|
66
|
+
): AdkDocumentationNavigationSiblings {
|
|
67
|
+
const flatPaths = this.flattenObjectWithPath(obj);
|
|
68
|
+
const index = flatPaths.indexOf(targetPath);
|
|
69
|
+
|
|
70
|
+
if (index === -1) {
|
|
71
|
+
return { previous: undefined, next: undefined };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
previous: index > 0 ? flatPaths[index - 1] : undefined,
|
|
76
|
+
next: index < flatPaths.length - 1 ? flatPaths[index + 1] : undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
flattenObjectWithPath(obj: AdkNestedObject, prefix: string = ''): string[] {
|
|
81
|
+
let result: string[] = [];
|
|
82
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
83
|
+
const newPath = prefix ? `${prefix}/${key}` : key;
|
|
84
|
+
if (typeof value === 'object' && value !== null) {
|
|
85
|
+
result = result.concat(this.flattenObjectWithPath(value, newPath));
|
|
86
|
+
} else {
|
|
87
|
+
result.push(newPath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { inject, Injectable } from '@angular/core';
|
|
2
|
+
import { map, Observable, switchMap, withLatestFrom } from 'rxjs';
|
|
3
|
+
import { AdkAssetResponse, AdkAssetsService } from '../assets/assets-service';
|
|
4
|
+
import {
|
|
5
|
+
AdkMarkdownParser,
|
|
6
|
+
AdkMarkdownParsingOptions,
|
|
7
|
+
AdkParsedContentSection,
|
|
8
|
+
} from '../content-parsing';
|
|
9
|
+
import {
|
|
10
|
+
AdkDocumentationConfigParser,
|
|
11
|
+
AdkDocumentationNavigationSiblings,
|
|
12
|
+
AdkNestedObject,
|
|
13
|
+
} from './documentation-config-parser';
|
|
14
|
+
|
|
15
|
+
export interface AdkDocsConfig {
|
|
16
|
+
title: string;
|
|
17
|
+
items: AdkFilesConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AdkFilesConfig {
|
|
21
|
+
[key: string]: AdkFilesItem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type AdkFilesItem = string | null | AdkFilesConfig;
|
|
25
|
+
|
|
26
|
+
export interface AdkParsedMarkdownFile<Content = string> {
|
|
27
|
+
sections: AdkParsedContentSection<Content>[];
|
|
28
|
+
headings: AdkHtmlHeader[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AdkParsedDocumentation<
|
|
32
|
+
Content = string,
|
|
33
|
+
> extends AdkParsedMarkdownFile<Content> {
|
|
34
|
+
siblings: AdkDocumentationNavigationSiblings;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AdkHtmlHeader {
|
|
38
|
+
id: string;
|
|
39
|
+
level: number; // starts at 1
|
|
40
|
+
text: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AdkDocumentationContentOptions {
|
|
44
|
+
fileConfig: AdkNestedObject;
|
|
45
|
+
basePath: string;
|
|
46
|
+
parsingOptions?: AdkMarkdownParsingOptions;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Injectable()
|
|
50
|
+
export class AdkDocumentationContentService {
|
|
51
|
+
private files: {
|
|
52
|
+
[name: string]: Observable<AdkParsedContentSection[]>;
|
|
53
|
+
} = {};
|
|
54
|
+
|
|
55
|
+
private assetsService = inject(AdkAssetsService);
|
|
56
|
+
private docsConfigParser = inject(AdkDocumentationConfigParser);
|
|
57
|
+
private markdownParser = inject(AdkMarkdownParser);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* This service requires the consuming app to provide the AdkDocumentationDisplayService, the AdkAssetsService, and the AdkDocumentationConfigParser at the level at which this service is configured.
|
|
61
|
+
*
|
|
62
|
+
* OPTIONAL: If the consuming app wants to use a different assets directory, they can pass it in here.
|
|
63
|
+
*/
|
|
64
|
+
initialize(assetsDirectory?: string): void {
|
|
65
|
+
this.assetsService.setAssetsPath(assetsDirectory || 'assets/');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Assumes that the content path matches
|
|
70
|
+
*/
|
|
71
|
+
getContentForCurrentContentPath(
|
|
72
|
+
contentPath$: Observable<string>,
|
|
73
|
+
options: AdkDocumentationContentOptions
|
|
74
|
+
): Observable<AdkParsedDocumentation> {
|
|
75
|
+
return contentPath$.pipe(
|
|
76
|
+
switchMap((contentPath) => {
|
|
77
|
+
const pathToFile = this.docsConfigParser.getPathToFile(
|
|
78
|
+
contentPath,
|
|
79
|
+
options.fileConfig,
|
|
80
|
+
options.basePath
|
|
81
|
+
);
|
|
82
|
+
return this.getParsedMarkdownFile(pathToFile, options.parsingOptions);
|
|
83
|
+
}),
|
|
84
|
+
withLatestFrom(contentPath$),
|
|
85
|
+
map(([markdownSections, configPath]) => {
|
|
86
|
+
return {
|
|
87
|
+
sections: markdownSections,
|
|
88
|
+
headings: markdownSections.reduce((acc, section) => {
|
|
89
|
+
if (section.headers.length > 0) {
|
|
90
|
+
acc.push(...section.headers);
|
|
91
|
+
}
|
|
92
|
+
return acc;
|
|
93
|
+
}, [] as AdkHtmlHeader[]),
|
|
94
|
+
siblings: this.docsConfigParser.findPreviousAndNextByPath(
|
|
95
|
+
options.fileConfig,
|
|
96
|
+
configPath
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private getParsedMarkdownFile(
|
|
104
|
+
filePathFromAssets: string,
|
|
105
|
+
parsingOptions?: AdkMarkdownParsingOptions
|
|
106
|
+
): Observable<AdkParsedContentSection[]> {
|
|
107
|
+
if (!this.files[filePathFromAssets]) {
|
|
108
|
+
this.files[filePathFromAssets] = this.assetsService
|
|
109
|
+
.getAsset(filePathFromAssets, AdkAssetResponse.Text)
|
|
110
|
+
.pipe(
|
|
111
|
+
switchMap((text) =>
|
|
112
|
+
this.markdownParser.parse(text as string, parsingOptions)
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return this.files[filePathFromAssets];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<div class="documentation-container" #file>
|
|
2
|
+
@if (content$ | async; as content) {
|
|
3
|
+
<div class="documentation-content">
|
|
4
|
+
<div class="main-content">
|
|
5
|
+
@for (section of content.sections; track $index) {
|
|
6
|
+
@if (section.type === 'markdown') {
|
|
7
|
+
<div class="injected-html" [innerHTML]="section.html"></div>
|
|
8
|
+
} @else if (section.type === 'escaped') {
|
|
9
|
+
@if (escapedContentTemplateRef) {
|
|
10
|
+
<ng-container
|
|
11
|
+
[ngTemplateOutlet]="escapedContentTemplateRef"
|
|
12
|
+
[ngTemplateOutletContext]="{ $implicit: section }"
|
|
13
|
+
>
|
|
14
|
+
</ng-container>
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
@if (showPrevNextNavigation) {
|
|
19
|
+
<hsi-adk-navigation-siblings
|
|
20
|
+
class="nav-siblings"
|
|
21
|
+
[siblings]="content.siblings"
|
|
22
|
+
(nextDoc)="navigateToDoc($event)"
|
|
23
|
+
></hsi-adk-navigation-siblings>
|
|
24
|
+
}
|
|
25
|
+
</div>
|
|
26
|
+
<hsi-adk-document-index
|
|
27
|
+
class="document-index"
|
|
28
|
+
[headings]="content.headings.slice(1)"
|
|
29
|
+
(activeHeading)="scrollToHeading($event)"
|
|
30
|
+
></hsi-adk-document-index>
|
|
31
|
+
</div>
|
|
32
|
+
}
|
|
33
|
+
</div>
|