mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f75429d8a8 | |||
| c91c37728f | |||
| de68e049e4 | |||
| 2324d9b15a | |||
| b494e9b5f7 | |||
| 5d7e446ce2 | |||
| 00c0876626 | |||
| 9c3aeb99bb | |||
| 582aac49a7 | |||
| 5256016695 | |||
| d2d72e369c | |||
| 641808fa72 | |||
| f859d5c156 | |||
| cbb36fc1ab | |||
| 4d8da40574 | |||
| 614c311a01 | |||
| ed91b03e0f | |||
| 5007970c3d | |||
| c068045d69 | |||
| 9814767d34 | |||
| 93bdbbc812 | |||
| 07607716d4 | |||
| d9c1004a35 | |||
| bb29c8bbd7 | |||
| 8e1d6c71fc | |||
| 98c6c05c6f | |||
| 3b7162d03c | |||
| 3bc28678fa | |||
| 97fd311b6a | |||
| 1853bbb061 | |||
| 3ecf620cc5 | |||
| 36aa3af77b | |||
| adf32746c2 | |||
| 5501e9c8ed | |||
| 389933f079 | |||
| 674aed5984 | |||
| b14c864170 | |||
| 10fce207e0 | |||
| eba3a0ade0 | |||
| 3d48a72193 | |||
| 5d2e243c76 | |||
| 5e9661d69c | |||
| df42d2be16 | |||
| 106917af87 | |||
| b74b5f0c06 | |||
| 4231404065 | |||
| adcdfeca16 | |||
| 4f8c406664 | |||
| 206b7be7f7 | |||
| 804925b114 | |||
| 6a953c6ed4 | |||
| 01cae95596 | |||
| 371ad098d5 | |||
| 73ad6ea647 | |||
| d3366815ee | |||
| 76ec60d73c | |||
| 7bfe81700b | |||
| 3067633bc7 | |||
| be82f282a4 | |||
| 03b35270b0 | |||
| fe9005ce8e | |||
| 03ec620e31 | |||
| 6d3a2af146 | |||
| 223b7857ef | |||
| 5bdae0387f | |||
| 034b32c133 | |||
| 2a89e184e0 | |||
| 9b6865ec03 | |||
| 6059ca8f1f | |||
| 800f6c8ca3 | |||
| 87c594b90b | |||
| f133489a16 | |||
| 09deb24d74 | |||
| f1bc6f895b | |||
| 5f95afb331 | |||
| b30bc4531a | |||
| a6225a6cdd | |||
| 69b20d8391 | |||
| 6089772fe8 | |||
| 1a5f5ee5ce | |||
| 76808cc328 | |||
| 980c64abaa | |||
| df3e9c1661 | |||
| cb7d817edd | |||
| 8645152f8c | |||
| 0979ca0354 | |||
| af20b2a161 | |||
| b5719ba5eb | |||
| c4901d4dfe | |||
| 1682c60938 | |||
| c65ab136bf | |||
| c89be37193 | |||
| f723288adf | |||
| 58e00deb34 | |||
| 57db366522 | |||
| 3e6ee7eaff | |||
| 086b754cef | |||
| 4bd9ba714c | |||
| 3b63082384 | |||
| 25bc810f83 | |||
| 9de30a7b1c | |||
| b1ed7e2b5f | |||
| 6b4357fae4 | |||
| 0bb649b8fa | |||
| 286012fb89 | |||
| 9155f75e36 | |||
| 93410eda0a | |||
| d49e083999 | |||
| e50d239e80 | |||
| e30c60e89f | |||
| 8104fc2126 | |||
| 7accd9d885 | |||
| 334c99f968 | |||
| b1a9d0f4de | |||
| 5d76401ff5 | |||
| de85979648 | |||
| bbc970bb0b | |||
| 87d00d26ff | |||
| 84752069f2 | |||
| d3b46ade94 | |||
| 1828d11d84 | |||
| b8c82fa2f7 | |||
| fa77c9e5b4 | |||
| 5d16c286bf | |||
| 604270619a | |||
| 1b546975f0 | |||
| 16ae748257 | |||
| ecb0f8f161 | |||
| cea3e4b594 | |||
| 448279f57e | |||
| 5558e275ee | |||
| 86216792fd | |||
| cab6c23602 | |||
| 296216cbe1 | |||
| 9af760eb51 | |||
| b16dd6d67f | |||
| 02d613ef9c |
@@ -158,9 +158,8 @@ test:saucelabs --flaky_test_attempts=1
|
||||
|
||||
# --ng_perf will ask the Ivy compiler to produce performance results for each build.
|
||||
build --flag_alias=ng_perf=//packages/compiler-cli:ng_perf
|
||||
# --adev_fast will run adev build/serve in a faster mode, skipping things like prerendering
|
||||
# for local development.
|
||||
build --flag_alias=fast_adev=//adev:fast_build_mode
|
||||
# --prerender_adev will run adev build/serve in a full mode, performing prerendering
|
||||
build --flag_alias=prerender_adev=//adev:prerender_adev
|
||||
|
||||
####################################################
|
||||
# User bazel configuration
|
||||
|
||||
@@ -103,7 +103,7 @@ export async function setupRedirect(deployment: Deployment) {
|
||||
}
|
||||
|
||||
function firebase(cmd: string, cwd?: string) {
|
||||
spawnSync('npx', `-y firebase-tools@latest ${cmd}`.split(' '), {
|
||||
spawnSync('npx', `-y firebase-tools@13.15.1 ${cmd}`.split(' '), {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
shell: true,
|
||||
|
||||
@@ -11415,7 +11415,7 @@ async function setupRedirect(deployment) {
|
||||
await rm(tmpRedirectDir, { recursive: true });
|
||||
}
|
||||
function firebase(cmd, cwd) {
|
||||
spawnSync("npx", `-y firebase-tools@latest ${cmd}`.split(" "), {
|
||||
spawnSync("npx", `-y firebase-tools@13.15.1 ${cmd}`.split(" "), {
|
||||
cwd,
|
||||
encoding: "utf-8",
|
||||
shell: true,
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel build //adev:build --config=release
|
||||
run: yarn bazel build //adev:build --prerender_adev --config=release
|
||||
- uses: angular/dev-infra/github-actions/previews/pack-and-upload-artifact@03b8a7dffd1205e061f0bee949024ebefc2a6592
|
||||
with:
|
||||
workflow-artifact-name: 'adev-preview'
|
||||
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev in fast mode to ensure it continues to work
|
||||
run: yarn bazel build //adev:build --fast_adev --config=release
|
||||
run: yarn bazel build //adev:build --config=release
|
||||
- name: Run tests
|
||||
run: yarn bazel test //adev:test
|
||||
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev to ensure it continues to work
|
||||
run: yarn bazel build //adev:build --config=release
|
||||
run: yarn bazel build //adev:build --prerender_adev --config=release
|
||||
- name: Deploy to firebase
|
||||
uses: ./.github/actions/deploy-docs-site
|
||||
with:
|
||||
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
- name: Install node modules
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build adev in fast mode to ensure it continues to work
|
||||
run: yarn bazel build //adev:build --fast_adev --config=release
|
||||
run: yarn bazel build //adev:build --config=release
|
||||
- name: Run tests
|
||||
run: yarn bazel test //adev:test
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
# Upload the results as artifacts.
|
||||
- name: 'Upload artifact'
|
||||
uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: 'Upload to code-scanning'
|
||||
uses: github/codeql-action/upload-sarif@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
|
||||
uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
+1
-1
@@ -372,7 +372,7 @@ groups:
|
||||
<<: *defaults
|
||||
conditions:
|
||||
- >
|
||||
contains_any_globs(files.exclude('packages/compiler-cli/*').exclude('packages/language-service/*').exclude('packages/service-worker/*'), [
|
||||
contains_any_globs(files.exclude('packages/compiler-cli/*').exclude('packages/language-service/*').exclude('packages/service-worker/*').exclude('packages/core/schematics/*'), [
|
||||
'packages/**/testing/**/{*,.*}',
|
||||
])
|
||||
reviewers:
|
||||
|
||||
+3278
-2137
File diff suppressed because it is too large
Load Diff
@@ -181,3 +181,15 @@ http_archive(
|
||||
strip_prefix = "sc-4.8.2-osx",
|
||||
url = "https://saucelabs.com/downloads/sc-4.8.2-osx.zip",
|
||||
)
|
||||
|
||||
yarn_install(
|
||||
name = "npm_ts_versions",
|
||||
data = [
|
||||
YARN_LABEL,
|
||||
"//:.yarnrc",
|
||||
],
|
||||
exports_directories_only = False,
|
||||
package_json = "//packages/core/schematics/migrations/signal-migration/test/ts-versions:package.json",
|
||||
yarn = YARN_LABEL,
|
||||
yarn_lock = "//packages/core/schematics/migrations/signal-migration/test/ts-versions:yarn.lock",
|
||||
)
|
||||
|
||||
+13
-20
@@ -1,4 +1,5 @@
|
||||
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
|
||||
load("@npm//@angular/build-tooling/bazel/http-server:index.bzl", "http_server")
|
||||
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
|
||||
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
|
||||
@@ -113,27 +114,27 @@ copy_to_bin(
|
||||
)
|
||||
|
||||
bool_flag(
|
||||
name = "fast_build_mode",
|
||||
name = "prerender_adev",
|
||||
build_setting_default = False,
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "fast",
|
||||
name = "no_prerender",
|
||||
flag_values = {
|
||||
":fast_build_mode": "true",
|
||||
":prerender_adev": "false",
|
||||
},
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "full",
|
||||
name = "prerender",
|
||||
flag_values = {
|
||||
":fast_build_mode": "false",
|
||||
":prerender_adev": "true",
|
||||
},
|
||||
)
|
||||
|
||||
config_based_architect_flags = select({
|
||||
":fast": ["--no-prerender"],
|
||||
":full": ["--prerender"],
|
||||
":no_prerender": ["--no-prerender"],
|
||||
":prerender": ["--prerender"],
|
||||
})
|
||||
|
||||
architect(
|
||||
@@ -154,21 +155,13 @@ architect(
|
||||
],
|
||||
)
|
||||
|
||||
architect(
|
||||
http_server(
|
||||
name = "serve",
|
||||
args = [
|
||||
"angular-dev:serve",
|
||||
"--poll=1000",
|
||||
"--live-reload",
|
||||
"--watch",
|
||||
],
|
||||
chdir = package_name(),
|
||||
data = ensure_local_package_deps(APPLICATION_DEPS) + APPLICATION_ASSETS + [
|
||||
":application_files_bin",
|
||||
],
|
||||
tags = [
|
||||
"no-remote-exec",
|
||||
additional_root_paths = [
|
||||
"angular/adev/build/browser",
|
||||
],
|
||||
enable_dev_ui = True,
|
||||
deps = [":build"],
|
||||
)
|
||||
|
||||
architect_test(
|
||||
|
||||
@@ -22,6 +22,7 @@ ts_library(
|
||||
"//adev/shared-docs/components/slide-toggle",
|
||||
"//adev/shared-docs/components/table-of-contents",
|
||||
"//adev/shared-docs/components/text-field",
|
||||
"//adev/shared-docs/components/top-level-banner",
|
||||
"//adev/shared-docs/components/viewers",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ ng_module(
|
||||
"//adev/shared-docs/components/navigation-list:__pkg__",
|
||||
"//adev/shared-docs/components/table-of-contents:__pkg__",
|
||||
"//adev/shared-docs/components/text-field:__pkg__",
|
||||
"//adev/shared-docs/components/top-level-banner:__pkg__",
|
||||
"//adev/shared-docs/components/viewers:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
|
||||
@@ -15,3 +15,4 @@ export * from './table-of-contents/table-of-contents.component';
|
||||
export * from './text-field/text-field.component';
|
||||
export * from './icon/icon.component';
|
||||
export * from './search-dialog/search-dialog.component';
|
||||
export * from './top-level-banner/top-level-banner.component';
|
||||
|
||||
@@ -22,6 +22,7 @@ ng_module(
|
||||
"//adev/shared-docs/interfaces",
|
||||
"//adev/shared-docs/pipes",
|
||||
"//adev/shared-docs/services",
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
"//packages/forms",
|
||||
"//packages/router",
|
||||
|
||||
@@ -10,65 +10,62 @@
|
||||
></docs-text-field>
|
||||
|
||||
@if (searchResults() && searchResults()!.length > 0) {
|
||||
<ul class="docs-search-results docs-mini-scroll-track">
|
||||
@for (result of searchResults(); track result.objectID) {
|
||||
<li docsSearchItem [item]="result">
|
||||
@if (result.url) {
|
||||
<a [routerLink]="'/' + result.url | relativeLink: 'pathname'" [fragment]="result.url | relativeLink: 'hash'">
|
||||
<div>
|
||||
<div class="docs-result-icon-and-type">
|
||||
<!-- Icon -->
|
||||
<span class="docs-search-result-icon" aria-hidden="true">
|
||||
@if (result.hierarchy?.lvl0 === 'Docs') {
|
||||
<i role="presentation" class="material-symbols-outlined docs-icon-small">
|
||||
description
|
||||
</i>
|
||||
} @else if (result.hierarchy?.lvl0 === 'Tutorials') {
|
||||
<i role="presentation" class="material-symbols-outlined docs-icon-small">code</i>
|
||||
} @else if (result.hierarchy?.lvl0 === 'Reference') {
|
||||
<i role="presentation" class="material-symbols-outlined docs-icon-small">
|
||||
description
|
||||
</i>
|
||||
}
|
||||
</span>
|
||||
<!-- Results type -->
|
||||
<span class="docs-search-results__type">{{ result.hierarchy?.lvl1 }}</span>
|
||||
<ul class="docs-search-results docs-mini-scroll-track">
|
||||
@for (result of searchResults(); track result.objectID) {
|
||||
<li docsSearchItem [item]="result">
|
||||
<a
|
||||
[routerLink]="'/' + result.url | relativeLink: 'pathname'"
|
||||
[fragment]="result.url | relativeLink: 'hash'"
|
||||
>
|
||||
<div>
|
||||
<div class="docs-result-icon-and-type">
|
||||
<!-- Icon -->
|
||||
<span class="docs-search-result-icon" aria-hidden="true">
|
||||
<i role="presentation" class="material-symbols-outlined docs-icon-small">
|
||||
{{ result.hierarchy.lvl0 === 'Tutorials' ? 'code' : 'description'}}
|
||||
</i>
|
||||
</span>
|
||||
<!-- Results type -->
|
||||
<span class="docs-search-results__type">
|
||||
@let snippet = result._snippetResult.hierarchy?.lvl1?.value ?? '';
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="highlightSnippet"
|
||||
[ngTemplateOutletContext]="{snippet}"
|
||||
></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@let content = result._snippetResult.content;
|
||||
@let hierarchy = result._snippetResult.hierarchy;
|
||||
@if (content || hierarchy?.lvl2 || hierarchy?.lvl3 || hierarchy?.lvl4) {
|
||||
<span class="docs-search-results__type docs-search-results__lvl2">
|
||||
@let snippet = getBestSnippetForMatch(result);
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="highlightSnippet"
|
||||
[ngTemplateOutletContext]="{snippet}"
|
||||
></ng-container>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Hide level 2 if level 3 exists -->
|
||||
<!-- Level 2 -->
|
||||
@if (result.hierarchy?.lvl2 && !result.hierarchy?.lvl3) {
|
||||
<span class="docs-search-results__type docs-search-results__lvl2">
|
||||
{{ result.hierarchy?.lvl2 }}
|
||||
</span>
|
||||
}
|
||||
<!-- Level 3 -->
|
||||
@if (result.hierarchy?.lvl3) {
|
||||
<span class="docs-search-results__type docs-search-results__lvl3">
|
||||
{{ result.hierarchy?.lvl3 }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Page title -->
|
||||
<span class="docs-result-page-title">{{ result.hierarchy?.lvl0 }}</span>
|
||||
</a>
|
||||
<!-- Page title -->
|
||||
<span class="docs-result-page-title">{{ result.hierarchy?.lvl0 }}</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</ul>
|
||||
} @else {
|
||||
<div class="docs-search-results docs-mini-scroll-track">
|
||||
@if (searchResults() === undefined) {
|
||||
<div class="docs-search-results__start-typing">
|
||||
<span>Start typing to see results</span>
|
||||
<div class="docs-search-results docs-mini-scroll-track">
|
||||
@if (searchResults() === undefined) {
|
||||
<div class="docs-search-results__start-typing">
|
||||
<span>Start typing to see results</span>
|
||||
</div>
|
||||
} @else if (searchResults()?.length === 0) {
|
||||
<div class="docs-search-results__no-results">
|
||||
<span>No results found</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else if (searchResults()?.length === 0) {
|
||||
<div class="docs-search-results__no-results">
|
||||
<span>No results found</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="docs-algolia">
|
||||
@@ -79,3 +76,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<ng-template #highlightSnippet let-snippet="snippet">
|
||||
@let parts = splitMarkedText(snippet);
|
||||
@for (part of parts; track $index) {
|
||||
@if (part.highlight) {
|
||||
<mark>{{part.text}}</mark>
|
||||
} @else {
|
||||
<span>{{part.text}}</span>
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
@@ -48,6 +48,14 @@ dialog {
|
||||
padding-inline-end: 1rem;
|
||||
padding-block: 0.25rem;
|
||||
|
||||
mark {
|
||||
background: #e62600;
|
||||
background: var(--red-to-orange-horizontal-gradient);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--secondary-contrast);
|
||||
display: flex;
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
viewChild,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import {NgTemplateOutlet} from '@angular/common';
|
||||
|
||||
import {WINDOW} from '../../providers/index';
|
||||
import {ClickOutside} from '../../directives/index';
|
||||
@@ -34,6 +35,7 @@ import {Router, RouterLink} from '@angular/router';
|
||||
import {filter, fromEvent} from 'rxjs';
|
||||
import {AlgoliaIcon} from '../algolia-icon/algolia-icon.component';
|
||||
import {RelativeLink} from '../../pipes/relative-link.pipe';
|
||||
import {SearchResult, SnippetResult} from '../../interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-search-dialog',
|
||||
@@ -47,6 +49,7 @@ import {RelativeLink} from '../../pipes/relative-link.pipe';
|
||||
AlgoliaIcon,
|
||||
RelativeLink,
|
||||
RouterLink,
|
||||
NgTemplateOutlet,
|
||||
],
|
||||
templateUrl: './search-dialog.component.html',
|
||||
styleUrls: ['./search-dialog.component.scss'],
|
||||
@@ -104,6 +107,46 @@ export class SearchDialog implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
splitMarkedText(snippet: string): Array<{highlight: boolean; text: string}> {
|
||||
const parts: Array<{highlight: boolean; text: string}> = [];
|
||||
while (snippet.indexOf('<ɵ>') !== -1) {
|
||||
const beforeMatch = snippet.substring(0, snippet.indexOf('<ɵ>'));
|
||||
const match = snippet.substring(snippet.indexOf('<ɵ>') + 3, snippet.indexOf('</ɵ>'));
|
||||
parts.push({highlight: false, text: beforeMatch});
|
||||
parts.push({highlight: true, text: match});
|
||||
snippet = snippet.substring(snippet.indexOf('</ɵ>') + 4);
|
||||
}
|
||||
parts.push({highlight: false, text: snippet});
|
||||
return parts;
|
||||
}
|
||||
|
||||
getBestSnippetForMatch(result: SearchResult): string {
|
||||
// if there is content, return it
|
||||
if (result._snippetResult.content !== undefined) {
|
||||
return result._snippetResult.content.value;
|
||||
}
|
||||
|
||||
const hierarchy = result._snippetResult.hierarchy;
|
||||
if (hierarchy === undefined) {
|
||||
return '';
|
||||
}
|
||||
function matched(snippet: SnippetResult | undefined) {
|
||||
return snippet?.matchLevel !== undefined && snippet.matchLevel !== 'none';
|
||||
}
|
||||
// return the most specific subheader match
|
||||
if (matched(hierarchy.lvl4)) {
|
||||
return hierarchy.lvl4!.value;
|
||||
}
|
||||
if (matched(hierarchy.lvl3)) {
|
||||
return hierarchy.lvl3!.value;
|
||||
}
|
||||
if (matched(hierarchy.lvl2)) {
|
||||
return hierarchy.lvl2!.value;
|
||||
}
|
||||
// if no subheader matched the query, fall back to just returning the most specific one
|
||||
return hierarchy.lvl3?.value ?? hierarchy.lvl2?.value ?? '';
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.keyManager.destroy();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
load("//tools:defaults.bzl", "karma_web_test_suite", "ng_module", "ts_library")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
|
||||
package(default_visibility = ["//visibility:private"])
|
||||
|
||||
ng_module(
|
||||
name = "top-level-banner",
|
||||
srcs = [
|
||||
"top-level-banner.component.ts",
|
||||
],
|
||||
assets = [
|
||||
":top-level-banner.component.css",
|
||||
"top-level-banner.component.html",
|
||||
],
|
||||
visibility = [
|
||||
"//adev/shared-docs/components:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//adev/shared-docs/components/icon",
|
||||
"//adev/shared-docs/directives",
|
||||
"//adev/shared-docs/providers",
|
||||
"//packages/common",
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "style",
|
||||
src = "top-level-banner.component.scss",
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["*.spec.ts"],
|
||||
),
|
||||
deps = [
|
||||
":top-level-banner",
|
||||
"//adev/shared-docs/providers",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [":test_lib"],
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
@if (!hasClosed()) {
|
||||
@if (link()) {
|
||||
<a [href]="link()" class="docs-top-level-banner">
|
||||
<h1 tabindex="-1" class="docs-top-level-banner-cta">{{ text() }}</h1>
|
||||
</a>
|
||||
} @else {
|
||||
<div class="docs-top-level-banner">
|
||||
<h1 tabindex="-1" class="docs-top-level-banner-cta">{{ text() }}</h1>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button class="docs-top-level-banner-close" type="button" (click)="close()">
|
||||
<docs-icon class="docs-icon_high-contrast">close</docs-icon>
|
||||
</button>
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
:host {
|
||||
&:not(:empty) {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
height: 2rem;
|
||||
width: 100vw;
|
||||
border-bottom: 1px solid var(--septenary-contrast);
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
backdrop-filter: blur(16px);
|
||||
background-color: color-mix(in srgb, var(--page-background) 70%, transparent);
|
||||
}
|
||||
|
||||
a.docs-top-level-banner {
|
||||
width: 100%;
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
h1.docs-top-level-banner-cta {
|
||||
display: inline;
|
||||
position: relative;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
background-image: var(--red-to-pink-to-purple-horizontal-gradient);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
width: fit-content;
|
||||
font-weight: 500;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
transform: scaleX(0);
|
||||
height: 1px;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
background: var(--tertiary-contrast);
|
||||
animation-name: shimmer;
|
||||
-webkit-animation-duration: 5s;
|
||||
-moz-animation-duration: 5s;
|
||||
animation-duration: 5s;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
h1.docs-top-level-banner-cta {
|
||||
&::after {
|
||||
transform: scaleX(1);
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.docs-top-level-banner-close {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.5rem;
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: scaleX(0);
|
||||
transform-origin: bottom right;
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(1);
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {STORAGE_KEY_PREFIX, TopLevelBannerComponent} from './top-level-banner.component';
|
||||
import {LOCAL_STORAGE} from '../../providers';
|
||||
|
||||
describe('TopLevelBannerComponent', () => {
|
||||
let component: TopLevelBannerComponent;
|
||||
let fixture: ComponentFixture<TopLevelBannerComponent>;
|
||||
let mockLocalStorage: jasmine.SpyObj<Storage>;
|
||||
|
||||
const EXAMPLE_TEXT = 'Click Here';
|
||||
const EXAMPLE_LINK = 'https://example.com';
|
||||
const EXAMPLE_ID = 'banner-id';
|
||||
|
||||
beforeEach(async () => {
|
||||
mockLocalStorage = jasmine.createSpyObj('Storage', ['getItem', 'setItem']);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TopLevelBannerComponent],
|
||||
providers: [{provide: LOCAL_STORAGE, useValue: mockLocalStorage}],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TopLevelBannerComponent);
|
||||
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
|
||||
fixture.componentRef.setInput('id', EXAMPLE_ID);
|
||||
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render an anchor element when link is provided', () => {
|
||||
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
|
||||
fixture.componentRef.setInput('link', EXAMPLE_LINK);
|
||||
fixture.detectChanges();
|
||||
|
||||
const bannerElement = fixture.nativeElement.querySelector('a.adev-top-level-banner');
|
||||
expect(bannerElement).toBeTruthy();
|
||||
expect(bannerElement.getAttribute('href')).toBe(EXAMPLE_LINK);
|
||||
expect(bannerElement.textContent).toContain(EXAMPLE_TEXT);
|
||||
});
|
||||
|
||||
it('should render a div element when link is not provided', () => {
|
||||
const EXAMPLE_TEXT = 'No Link Available';
|
||||
|
||||
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
|
||||
fixture.detectChanges();
|
||||
|
||||
const bannerElement = fixture.nativeElement.querySelector('div.adev-top-level-banner');
|
||||
expect(bannerElement).toBeTruthy();
|
||||
expect(bannerElement.textContent).toContain(EXAMPLE_TEXT);
|
||||
});
|
||||
|
||||
it('should correctly render the text input', () => {
|
||||
const EXAMPLE_TEXT = 'Lorem ipsum dolor...';
|
||||
|
||||
fixture.componentRef.setInput('text', EXAMPLE_TEXT);
|
||||
fixture.detectChanges();
|
||||
|
||||
const bannerElement = fixture.nativeElement.querySelector('.adev-top-level-banner-cta');
|
||||
expect(bannerElement).toBeTruthy();
|
||||
expect(bannerElement.textContent).toBe(EXAMPLE_TEXT);
|
||||
});
|
||||
|
||||
it('should set hasClosed to true if the banner was closed before', () => {
|
||||
mockLocalStorage.getItem.and.returnValue('true');
|
||||
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.hasClosed()).toBeTrue();
|
||||
expect(mockLocalStorage.getItem).toHaveBeenCalledWith(`${STORAGE_KEY_PREFIX}${EXAMPLE_ID}`);
|
||||
});
|
||||
|
||||
it('should set hasClosed to false if the banner was not closed before', () => {
|
||||
mockLocalStorage.getItem.and.returnValue('false');
|
||||
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.hasClosed()).toBeFalse();
|
||||
expect(mockLocalStorage.getItem).toHaveBeenCalledWith(`${STORAGE_KEY_PREFIX}${EXAMPLE_ID}`);
|
||||
});
|
||||
|
||||
it('should set hasClosed to false if accessing localStorage throws an error', () => {
|
||||
mockLocalStorage.getItem.and.throwError('Local storage error');
|
||||
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.hasClosed()).toBeFalse();
|
||||
});
|
||||
|
||||
it('should set the banner as closed in localStorage and update hasClosed', () => {
|
||||
component.close();
|
||||
|
||||
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
|
||||
`${STORAGE_KEY_PREFIX}${EXAMPLE_ID}`,
|
||||
'true',
|
||||
);
|
||||
expect(component.hasClosed()).toBeTrue();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import {ChangeDetectionStrategy, Component, inject, input, OnInit, signal} from '@angular/core';
|
||||
import {ExternalLink} from '../../directives';
|
||||
import {LOCAL_STORAGE} from '../../providers';
|
||||
import {IconComponent} from '../icon/icon.component';
|
||||
|
||||
export const STORAGE_KEY_PREFIX = 'docs-was-closed-top-banner-';
|
||||
|
||||
@Component({
|
||||
selector: 'docs-top-level-banner',
|
||||
standalone: true,
|
||||
imports: [ExternalLink, IconComponent],
|
||||
templateUrl: './top-level-banner.component.html',
|
||||
styleUrl: './top-level-banner.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TopLevelBannerComponent implements OnInit {
|
||||
private readonly localStorage = inject(LOCAL_STORAGE);
|
||||
|
||||
/**
|
||||
* Unique identifier for the banner. This ID is required to ensure that
|
||||
* the state of the banner (e.g., whether it has been closed) is tracked
|
||||
* separately for different events or instances. Without a unique ID,
|
||||
* closing one banner could inadvertently hide other banners for different events.
|
||||
*/
|
||||
id = input.required<string>();
|
||||
// Optional URL link that the banner should navigate to when clicked.
|
||||
link = input<string>();
|
||||
// Text content to be displayed in the banner.
|
||||
text = input.required<string>();
|
||||
|
||||
// Whether the user has closed the banner.
|
||||
hasClosed = signal<boolean>(false);
|
||||
|
||||
ngOnInit(): void {
|
||||
// Needs to be in a try/catch, because some browsers will
|
||||
// throw when using `localStorage` in private mode.
|
||||
try {
|
||||
this.hasClosed.set(this.localStorage?.getItem(this.getBannerStorageKey()) === 'true');
|
||||
} catch {
|
||||
this.hasClosed.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.localStorage?.setItem(this.getBannerStorageKey(), 'true');
|
||||
this.hasClosed.set(true);
|
||||
}
|
||||
|
||||
private getBannerStorageKey(): string {
|
||||
return `${STORAGE_KEY_PREFIX}${this.id()}`;
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,39 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export interface SnippetResult {
|
||||
value: string;
|
||||
matchLevel: 'none' | 'full' | string;
|
||||
}
|
||||
|
||||
/* The interface represents Algolia search result item. */
|
||||
export interface SearchResult {
|
||||
/* The url link to the search result page */
|
||||
url?: string;
|
||||
url: string;
|
||||
/* The hierarchy of the item */
|
||||
hierarchy?: Hierarchy;
|
||||
hierarchy: Hierarchy;
|
||||
/* The unique id of the search result item */
|
||||
objectID: string;
|
||||
/**
|
||||
* The type of the result. A content result will have
|
||||
* matched the content. A result of type 'lvl#' may have i
|
||||
* matched a lvl above it. For example, a type 'lvl3' may be
|
||||
* included in results because its 'lvl2' header matched the query.
|
||||
*/
|
||||
type: string;
|
||||
/** Documentation content (not headers) */
|
||||
content: string | null;
|
||||
/** Snippets of the matched text */
|
||||
_snippetResult: {
|
||||
hierarchy?: {
|
||||
lvl0?: SnippetResult;
|
||||
lvl1?: SnippetResult;
|
||||
lvl2?: SnippetResult;
|
||||
lvl3?: SnippetResult;
|
||||
lvl4?: SnippetResult;
|
||||
};
|
||||
content?: SnippetResult;
|
||||
};
|
||||
}
|
||||
|
||||
/* The hierarchy of the item */
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
"fast-glob": "~3.3.2",
|
||||
"fflate": "^0.8.2",
|
||||
"html-entities": "~2.5.2",
|
||||
"jsdom": "~24.1.0",
|
||||
"marked": "~12.0.2",
|
||||
"mermaid": "^10.8.0",
|
||||
"jsdom": "~25.0.0",
|
||||
"marked": "~14.0.0",
|
||||
"mermaid": "^11.0.0",
|
||||
"shiki": "^1.10.3"
|
||||
},
|
||||
"exports": {
|
||||
|
||||
@@ -13,6 +13,9 @@ def _extract_api_to_json(ctx):
|
||||
# Pass the module_name for the extracted APIs. This will be something like "@angular/core".
|
||||
args.add(ctx.attr.module_name)
|
||||
|
||||
# Pass the module_label for the extracted APIs, This is something like core for "@angular/core".
|
||||
args.add(ctx.attr.module_label)
|
||||
|
||||
# Pass the entry_point for from which to extract public symbols.
|
||||
args.add(ctx.file.entry_point)
|
||||
|
||||
@@ -82,6 +85,9 @@ extract_api_to_json = rule(
|
||||
doc = """JS Module name to be used for the extracted symbols""",
|
||||
mandatory = True,
|
||||
),
|
||||
"module_label": attr.string(
|
||||
doc = """Module label to be used for the extracted symbols. To be used as display name, for example in API docs""",
|
||||
),
|
||||
"extra_entries": attr.label_list(
|
||||
doc = """JSON files that contain extra entries to append to the final collection.""",
|
||||
allow_files = True,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import {readFileSync, writeFileSync} from 'fs';
|
||||
import path from 'path';
|
||||
// @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context.
|
||||
import {NgtscProgram, CompilerOptions, createCompilerHost, DocEntry} from '@angular/compiler-cli';
|
||||
import {
|
||||
NgtscProgram,
|
||||
CompilerOptions,
|
||||
createCompilerHost,
|
||||
DocEntry,
|
||||
EntryCollection,
|
||||
} from '@angular/compiler-cli';
|
||||
import ts from 'typescript';
|
||||
|
||||
function main() {
|
||||
@@ -10,6 +16,7 @@ function main() {
|
||||
|
||||
const [
|
||||
moduleName,
|
||||
moduleLabel,
|
||||
entryPointExecRootRelativePath,
|
||||
srcs,
|
||||
outputFilenameExecRootRelativePath,
|
||||
@@ -54,13 +61,25 @@ function main() {
|
||||
return result.concat(JSON.parse(readFileSync(path, {encoding: 'utf8'})) as DocEntry[]);
|
||||
}, []);
|
||||
|
||||
const extractedEntries = program.getApiDocumentation(entryPointExecRootRelativePath);
|
||||
const apiDoc = program.getApiDocumentation(entryPointExecRootRelativePath);
|
||||
const extractedEntries = apiDoc.entries;
|
||||
const combinedEntries = extractedEntries.concat(extraEntries);
|
||||
|
||||
const normalized = moduleName.replace('@', '').replace(/[\/]/g, '_');
|
||||
|
||||
const output = JSON.stringify({
|
||||
moduleLabel: moduleLabel || moduleName,
|
||||
moduleName: moduleName,
|
||||
normalizedModuleName: normalized,
|
||||
entries: combinedEntries,
|
||||
});
|
||||
symbols: [
|
||||
// Symbols referenced, originating from other packages
|
||||
...apiDoc.symbols.entries(),
|
||||
|
||||
// Exported symbols from the current package
|
||||
...apiDoc.entries.map((entry) => [entry.name, moduleName]),
|
||||
],
|
||||
} as EntryCollection);
|
||||
|
||||
writeFileSync(outputFilenameExecRootRelativePath, output, {encoding: 'utf8'});
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
load("//adev/shared-docs/pipeline/api-gen/extraction:extract_api_to_json.bzl", "extract_api_to_json")
|
||||
load("//adev/shared-docs/pipeline/api-gen/rendering:render_api_to_html.bzl", "render_api_to_html")
|
||||
|
||||
def generate_api_docs(name, module_name, entry_point, srcs, import_map = {}, extra_entries = []):
|
||||
def generate_api_docs(name, module_name, entry_point, srcs, module_label = None, import_map = {}, extra_entries = []):
|
||||
"""Generates API documentation reference pages for the given sources."""
|
||||
json_outfile = name + "_api.json"
|
||||
|
||||
extract_api_to_json(
|
||||
name = name + "_extraction",
|
||||
module_name = module_name,
|
||||
module_label = module_label,
|
||||
entry_point = entry_point,
|
||||
srcs = srcs,
|
||||
output_name = json_outfile,
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
// @ts-ignore This compiles fine, but Webstorm doesn't like the ESM import in a CJS context.
|
||||
import type {DocEntry, JsDocTagEntry} from '@angular/compiler-cli';
|
||||
|
||||
/** The JSON data file format for extracted API reference info. */
|
||||
export interface EntryCollection {
|
||||
moduleName: string;
|
||||
entries: DocEntry[];
|
||||
}
|
||||
import type {DocEntry, EntryCollection, JsDocTagEntry} from '@angular/compiler-cli';
|
||||
|
||||
export interface ManifestEntry {
|
||||
name: string;
|
||||
@@ -16,7 +10,12 @@ export interface ManifestEntry {
|
||||
}
|
||||
|
||||
/** Manifest that maps each module name to a list of API symbols. */
|
||||
export type Manifest = Record<string, ManifestEntry[]>;
|
||||
export type Manifest = {
|
||||
moduleName: string;
|
||||
normalizedModuleName: string;
|
||||
moduleLabel: string;
|
||||
entries: ManifestEntry[];
|
||||
}[];
|
||||
|
||||
/** Gets a unique lookup key for an API, e.g. "@angular/core/ElementRef". */
|
||||
function getApiLookupKey(moduleName: string, name: string) {
|
||||
@@ -114,22 +113,39 @@ export function generateManifest(apiCollections: EntryCollection[]): Manifest {
|
||||
});
|
||||
}
|
||||
|
||||
const manifest: Manifest = {};
|
||||
const manifest: Manifest = [];
|
||||
for (const collection of apiCollections) {
|
||||
if (!manifest[collection.moduleName]) {
|
||||
manifest[collection.moduleName] = [];
|
||||
const entries = collection.entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
type: entry.entryType,
|
||||
isDeprecated: isDeprecated(entryLookup, collection.moduleName, entry),
|
||||
isDeveloperPreview: isDeveloperPreview(entryLookup, collection.moduleName, entry),
|
||||
isExperimental: isExperimental(entryLookup, collection.moduleName, entry),
|
||||
}));
|
||||
|
||||
const existingEntry = manifest.find((entry) => entry.moduleName === collection.moduleName);
|
||||
if (existingEntry) {
|
||||
existingEntry.entries.push(...entries);
|
||||
} else {
|
||||
manifest.push({
|
||||
moduleName: collection.moduleName,
|
||||
normalizedModuleName: collection.normalizedModuleName,
|
||||
moduleLabel: collection.moduleLabel ?? collection.moduleName,
|
||||
entries,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
manifest.sort((entry1, entry2) => {
|
||||
// Ensure that labels that start with a `code` tag like `window.ng` are last
|
||||
if (entry1.moduleLabel.startsWith('<')) {
|
||||
return 1;
|
||||
} else if (entry2.moduleLabel.startsWith('<')) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
manifest[collection.moduleName].push(
|
||||
...collection.entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
type: entry.entryType,
|
||||
isDeprecated: isDeprecated(entryLookup, collection.moduleName, entry),
|
||||
isDeveloperPreview: isDeveloperPreview(entryLookup, collection.moduleName, entry),
|
||||
isExperimental: isExperimental(entryLookup, collection.moduleName, entry),
|
||||
})),
|
||||
);
|
||||
}
|
||||
return entry1.moduleLabel.localeCompare(entry2.moduleLabel);
|
||||
});
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {readFileSync, writeFileSync} from 'fs';
|
||||
import {EntryCollection, generateManifest} from './generate_manifest';
|
||||
import {generateManifest} from './generate_manifest';
|
||||
import type {EntryCollection} from '@angular/compiler-cli';
|
||||
|
||||
function main() {
|
||||
const [paramFilePath] = process.argv.slice(2);
|
||||
|
||||
@@ -101,6 +101,8 @@ export interface ClassEntry extends DocEntry {
|
||||
isAbstract: boolean;
|
||||
members: MemberEntry[];
|
||||
generics: GenericEntry[];
|
||||
extends?: string;
|
||||
implements: string[];
|
||||
}
|
||||
|
||||
// From an API doc perspective, class and interfaces are identical.
|
||||
@@ -132,15 +134,25 @@ export interface PipeEntry extends ClassEntry {
|
||||
// TODO: add `isPure`.
|
||||
}
|
||||
|
||||
export interface FunctionEntry extends DocEntry {
|
||||
export interface FunctionSignatureMetadata extends DocEntry {
|
||||
params: ParameterEntry[];
|
||||
returnType: string;
|
||||
returnDescription?: string;
|
||||
|
||||
generics: GenericEntry[];
|
||||
isNewType: boolean;
|
||||
}
|
||||
|
||||
export interface FunctionWithOverloadsEntry extends FunctionEntry {
|
||||
overloads: FunctionEntry[] | null;
|
||||
export type FunctionEntry = FunctionDefinitionEntry &
|
||||
DocEntry & {
|
||||
implementation: FunctionSignatureMetadata;
|
||||
};
|
||||
|
||||
/** Interface describing a function with overload signatures. */
|
||||
export interface FunctionDefinitionEntry {
|
||||
name: string;
|
||||
signatures: FunctionSignatureMetadata[];
|
||||
implementation: FunctionSignatureMetadata | null;
|
||||
}
|
||||
|
||||
/** Sub-entry for a single class or enum member. */
|
||||
@@ -178,15 +190,9 @@ export interface ParameterEntry {
|
||||
isRestParam: boolean;
|
||||
}
|
||||
|
||||
export interface FunctionWithOverloads {
|
||||
name: string;
|
||||
signatures: FunctionEntry[];
|
||||
implementation: FunctionEntry | null;
|
||||
}
|
||||
|
||||
export interface InitializerApiFunctionEntry extends DocEntry {
|
||||
callFunction: FunctionWithOverloads;
|
||||
subFunctions: FunctionWithOverloads[];
|
||||
callFunction: FunctionDefinitionEntry;
|
||||
subFunctions: FunctionDefinitionEntry[];
|
||||
|
||||
__docsMetadata__?: {
|
||||
/**
|
||||
@@ -204,7 +210,3 @@ export interface InitializerApiFunctionEntry extends DocEntry {
|
||||
export function isDocEntryWithSourceInfo(entry: DocEntry): entry is DocEntryWithSourceInfo {
|
||||
return 'source' in entry;
|
||||
}
|
||||
|
||||
export function isFunctionEntryWithOverloads(entry: DocEntry): entry is FunctionWithOverloadsEntry {
|
||||
return 'overloads' in entry;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DocEntry,
|
||||
EnumEntry,
|
||||
FunctionEntry,
|
||||
FunctionSignatureMetadata,
|
||||
InitializerApiFunctionEntry,
|
||||
JsDocTagEntry,
|
||||
MemberEntry,
|
||||
@@ -55,7 +56,7 @@ export type TypeAliasEntryRenderable = TypeAliasEntry & DocEntryRenderable & Has
|
||||
export type ClassEntryRenderable = ClassEntry &
|
||||
DocEntryRenderable &
|
||||
HasRenderableToc & {
|
||||
membersGroups: Map<string, MemberEntryRenderable[]>;
|
||||
members: MemberEntryRenderable[];
|
||||
};
|
||||
|
||||
/** Documentation entity for a TypeScript enum augmented transformed content for rendering. */
|
||||
@@ -71,9 +72,12 @@ export type InterfaceEntryRenderable = ClassEntryRenderable;
|
||||
export type FunctionEntryRenderable = FunctionEntry &
|
||||
DocEntryRenderable &
|
||||
HasRenderableToc & {
|
||||
params: ParameterEntryRenderable[];
|
||||
deprecationMessage: string | null;
|
||||
overloads: FunctionEntryRenderable[] | null;
|
||||
};
|
||||
|
||||
export type FunctionSignatureMetadataRenderable = FunctionSignatureMetadata &
|
||||
DocEntryRenderable & {
|
||||
params: ParameterEntryRenderable[];
|
||||
};
|
||||
|
||||
/** Sub-entry for a single class or enum member augmented with transformed content for rendering. */
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {FunctionEntry, JsDocTagEntry, MemberEntry, ParameterEntry} from '../entities';
|
||||
import {JsDocTagEntry, MemberEntry, ParameterEntry} from '../entities';
|
||||
|
||||
import {
|
||||
CodeLineRenderable,
|
||||
FunctionEntryRenderable,
|
||||
JsDocTagRenderable,
|
||||
LinkEntryRenderable,
|
||||
MemberEntryRenderable,
|
||||
@@ -81,14 +80,6 @@ export interface HasRenderableParams {
|
||||
params: ParameterEntryRenderable[];
|
||||
}
|
||||
|
||||
export interface HasOverloads {
|
||||
overloads: FunctionEntry[] | null;
|
||||
}
|
||||
|
||||
export interface HasRenderableOverloads {
|
||||
overloads: FunctionEntryRenderable[] | null;
|
||||
}
|
||||
|
||||
export interface HasDeprecatedFlag {
|
||||
isDeprecated: boolean;
|
||||
deprecationMessage: string | null;
|
||||
|
||||
@@ -7,20 +7,27 @@ import {configureMarkedGlobally} from './marked/configuration';
|
||||
import {getRenderable} from './processing';
|
||||
import {renderEntry} from './rendering';
|
||||
import {initHighlighter} from './shiki/shiki';
|
||||
import {setSymbols} from './symbol-context';
|
||||
|
||||
/** The JSON data file format for extracted API reference info. */
|
||||
interface EntryCollection {
|
||||
moduleName: string;
|
||||
moduleLabel?: string;
|
||||
normalizedModuleName: string;
|
||||
entries: DocEntry[];
|
||||
symbols: Map<string, string>;
|
||||
}
|
||||
|
||||
/** Parse all JSON data source files into an array of collections. */
|
||||
function parseEntryData(srcs: string[]): EntryCollection[] {
|
||||
return srcs.flatMap((jsonDataFilePath) => {
|
||||
return srcs.flatMap((jsonDataFilePath): EntryCollection | EntryCollection[] => {
|
||||
const fileContent = readFileSync(jsonDataFilePath, {encoding: 'utf8'});
|
||||
const fileContentJson = JSON.parse(fileContent) as unknown;
|
||||
if ((fileContentJson as EntryCollection).entries) {
|
||||
return fileContentJson as EntryCollection;
|
||||
return {
|
||||
...(fileContentJson as EntryCollection),
|
||||
symbols: new Map((fileContentJson as any).symbols ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
// CLI subcommands should generate a separate file for each subcommand.
|
||||
@@ -30,12 +37,16 @@ function parseEntryData(srcs: string[]): EntryCollection[] {
|
||||
return [
|
||||
{
|
||||
moduleName: 'unknown',
|
||||
normalizedModuleName: 'unknown',
|
||||
entries: [fileContentJson as DocEntry],
|
||||
symbols: new Map(),
|
||||
},
|
||||
...command.subcommands!.map((subCommand) => {
|
||||
return {
|
||||
moduleName: 'unknown',
|
||||
normalizedModuleName: 'unknown',
|
||||
entries: [{...subCommand, parentCommand: command} as any],
|
||||
symbols: new Map(),
|
||||
};
|
||||
}),
|
||||
];
|
||||
@@ -43,13 +54,15 @@ function parseEntryData(srcs: string[]): EntryCollection[] {
|
||||
|
||||
return {
|
||||
moduleName: 'unknown',
|
||||
normalizedModuleName: 'unknown',
|
||||
entries: [fileContentJson as DocEntry], // TODO: fix the typing cli entries aren't DocEntry
|
||||
symbols: new Map(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Gets a normalized filename for a doc entry. */
|
||||
function getNormalizedFilename(moduleName: string, entry: DocEntry | CliCommand): string {
|
||||
function getNormalizedFilename(normalizedModuleName: string, entry: DocEntry | CliCommand): string {
|
||||
if (isCliEntry(entry)) {
|
||||
return entry.parentCommand
|
||||
? `${entry.parentCommand.name}/${entry.name}.html`
|
||||
@@ -57,9 +70,6 @@ function getNormalizedFilename(moduleName: string, entry: DocEntry | CliCommand)
|
||||
}
|
||||
|
||||
entry = entry as DocEntry;
|
||||
// Angular entry points all contain an "@" character, which we want to remove
|
||||
// from the filename. We also swap `/` with an underscore.
|
||||
const normalizedModuleName = moduleName.replace('@', '').replace(/\//g, '_');
|
||||
|
||||
// Append entry type as suffix to prevent writing to file that only differs in casing or query string from already written file.
|
||||
// This will lead to a race-condition and corrupted files on case-insensitive file systems.
|
||||
@@ -96,6 +106,10 @@ async function main() {
|
||||
|
||||
for (const collection of entryCollections) {
|
||||
const extractedEntries = collection.entries;
|
||||
|
||||
// Setting the symbols are a global context for the rendering templates of this entry
|
||||
setSymbols(collection.symbols);
|
||||
|
||||
const renderableEntries = extractedEntries.map((entry) =>
|
||||
getRenderable(entry, collection.moduleName),
|
||||
);
|
||||
@@ -103,7 +117,10 @@ async function main() {
|
||||
const htmlOutputs = renderableEntries.map(renderEntry);
|
||||
|
||||
for (let i = 0; i < htmlOutputs.length; i++) {
|
||||
const filename = getNormalizedFilename(collection.moduleName, collection.entries[i]);
|
||||
const filename = getNormalizedFilename(
|
||||
collection.normalizedModuleName,
|
||||
collection.entries[i],
|
||||
);
|
||||
const outputPath = path.join(outputFilenameExecRootRelativePath, filename);
|
||||
|
||||
// in case the output path is nested, ensure the directory exists
|
||||
|
||||
@@ -6,16 +6,18 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {Renderer as MarkedRenderer} from 'marked';
|
||||
import {Renderer, Tokens} from 'marked';
|
||||
import {codeToHtml} from '../shiki/shiki';
|
||||
|
||||
/**
|
||||
* Custom renderer for marked that will be used to transform markdown files to HTML
|
||||
* files that can be used in the Angular docs.
|
||||
*/
|
||||
export const renderer: Partial<MarkedRenderer> = {
|
||||
code(code: string, language: string, isEscaped: boolean): string {
|
||||
const highlightResult = codeToHtml(code, language).replace(/>\s+</g, '><');
|
||||
export const renderer: Partial<Renderer> = {
|
||||
code({lang, text}): string {
|
||||
const highlightResult = codeToHtml(text, lang)
|
||||
// remove spaces/line-breaks between elements to not mess-up `pre` style
|
||||
.replace(/>\s+</g, '><');
|
||||
|
||||
return `
|
||||
<div class="docs-code" role="group">
|
||||
@@ -25,37 +27,46 @@ export const renderer: Partial<MarkedRenderer> = {
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
image(href: string | null, title: string | null, text: string): string {
|
||||
image({href, title, text}): string {
|
||||
return `
|
||||
<img src="${href}" alt="${text}" title="${title}" class="docs-image">
|
||||
`;
|
||||
},
|
||||
link(href: string, title: string, text: string): string {
|
||||
return `<a href="${href}">${text}</a>`;
|
||||
link(this: Renderer, {href, tokens}): string {
|
||||
return `<a href="${href}">${this.parser.parseInline(tokens)}</a>`;
|
||||
},
|
||||
list(body: string, ordered: boolean, start: number) {
|
||||
list(this: Renderer, {items, ordered, start}) {
|
||||
if (ordered) {
|
||||
return `
|
||||
<ol class="docs-ordered-list">
|
||||
${body}
|
||||
${items.map((item) => this.listitem(item)).join('')}
|
||||
</ol>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<ul class="docs-list">
|
||||
${body}
|
||||
${items.map((item) => this.listitem(item)).join('')}
|
||||
</ul>
|
||||
`;
|
||||
},
|
||||
table(header: string, body: string): string {
|
||||
|
||||
table(this: Renderer, {header, rows}: Tokens.Table) {
|
||||
return `
|
||||
<div class="docs-table docs-scroll-track-transparent">
|
||||
<table>
|
||||
<thead>
|
||||
${header}
|
||||
${this.tablerow({
|
||||
text: header.map((cell) => this.tablecell(cell)).join(''),
|
||||
})}
|
||||
</thead>
|
||||
<tbody>
|
||||
${body}
|
||||
${rows
|
||||
.map((row) =>
|
||||
this.tablerow({
|
||||
text: row.map((cell) => this.tablecell(cell)).join(''),
|
||||
}),
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -21,8 +21,12 @@ export async function initHighlighter() {
|
||||
});
|
||||
}
|
||||
|
||||
export function codeToHtml(code: string, language: string | undefined): string {
|
||||
return highlighter.codeToHtml(code, {
|
||||
export function codeToHtml(
|
||||
code: string,
|
||||
language: string | undefined,
|
||||
options?: {removeFunctionKeyword?: boolean},
|
||||
): string {
|
||||
const html = highlighter.codeToHtml(code, {
|
||||
lang: language ?? 'text',
|
||||
themes: {
|
||||
light: 'github-light',
|
||||
@@ -31,4 +35,20 @@ export function codeToHtml(code: string, language: string | undefined): string {
|
||||
cssVariablePrefix: '--shiki-',
|
||||
defaultColor: false,
|
||||
});
|
||||
|
||||
if (options?.removeFunctionKeyword) {
|
||||
return removeFunctionKeywordFromShikiHtml(html);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
export function removeFunctionKeywordFromShikiHtml(shikiHtml: string): string {
|
||||
return (
|
||||
shikiHtml
|
||||
// remove the leading space of the element after the "function" element
|
||||
.replace(/(<[^>]*>function<\/\w+><[^>]*>)(\s)(\w+<\/\w+>)/g, '$1$3')
|
||||
// Shiki requires the keyword function for highlighting functions signatures
|
||||
// We don't want to display it so we remove elements with the keyword
|
||||
.replace(/<[^>]*>function<\/\w+>/g, '')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export const REFERENCE_MEMBERS = 'docs-reference-members';
|
||||
export const REFERENCE_DEPRECATED = 'docs-reference-deprecated';
|
||||
export const REFERENCE_MEMBERS_CONTAINER = 'docs-reference-members-container';
|
||||
export const REFERENCE_MEMBER_CARD = 'docs-reference-member-card';
|
||||
export const REFERENCE_MEMBER_CARD_HEADER = 'docs-reference-card-header';
|
||||
export const REFERENCE_MEMBER_CARD_BODY = 'docs-reference-card-body';
|
||||
export const REFERENCE_MEMBER_CARD_ITEM = 'docs-reference-card-item';
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* API pages are generated each package at a time.
|
||||
* This allows to use a global context to store the symbols and their corresponding module names.
|
||||
*/
|
||||
|
||||
let symbols = new Map<string, string>();
|
||||
|
||||
export function setSymbols(newSymbols: Map<string, string>): void {
|
||||
symbols = newSymbols;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the module name of a symbol.
|
||||
* eg: 'ApplicationRef' => 'core', 'FormControl' => 'forms'
|
||||
*/
|
||||
export function getModuleName(symbol: string): string | undefined {
|
||||
return symbols.get(symbol)?.replace('@angular/', '');
|
||||
}
|
||||
@@ -10,11 +10,11 @@ import {h} from 'preact';
|
||||
import {MemberEntryRenderable} from '../entities/renderables';
|
||||
import {ClassMember} from './class-member';
|
||||
|
||||
export function ClassMemberList(props: {membersGroups: Map<string, MemberEntryRenderable[]>}) {
|
||||
export function ClassMemberList(props: {members: MemberEntryRenderable[]}) {
|
||||
return (
|
||||
<div class="docs-reference-members">
|
||||
{Array.from(props.membersGroups).map(([_, group]) => (
|
||||
<ClassMember members={group} />
|
||||
{props.members.map((member) => (
|
||||
<ClassMember member={member} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,55 +13,54 @@ import {
|
||||
isPropertyEntry,
|
||||
isSetterEntry,
|
||||
} from '../entities/categorization';
|
||||
import {MemberEntryRenderable} from '../entities/renderables';
|
||||
import {
|
||||
REFERENCE_HEADER,
|
||||
FunctionSignatureMetadataRenderable,
|
||||
MemberEntryRenderable,
|
||||
MethodEntryRenderable,
|
||||
} from '../entities/renderables';
|
||||
import {
|
||||
REFERENCE_MEMBER_CARD,
|
||||
REFERENCE_MEMBER_CARD_BODY,
|
||||
REFERENCE_MEMBER_CARD_HEADER,
|
||||
REFERENCE_MEMBER_CARD_ITEM,
|
||||
} from '../styling/css-classes';
|
||||
import {ClassMethodInfo} from './class-method-info';
|
||||
import {DeprecatedLabel} from './deprecated-label';
|
||||
import {RawHtml} from './raw-html';
|
||||
import {getFunctionMetadataRenderable} from '../transforms/function-transforms';
|
||||
import {CodeSymbol} from './code-symbols';
|
||||
|
||||
export function ClassMember(props: {members: MemberEntryRenderable[]}) {
|
||||
const memberName = props.members[0].name;
|
||||
const returnType = getMemberType(props.members[0]);
|
||||
|
||||
// Do not create body element when there is no description
|
||||
const body = props.members.every(
|
||||
(member) => !member.htmlDescription && !isClassMethodEntry(member),
|
||||
) ? (
|
||||
<></>
|
||||
) : (
|
||||
export function ClassMember(props: {member: MemberEntryRenderable}) {
|
||||
const body = (
|
||||
<div className={REFERENCE_MEMBER_CARD_BODY}>
|
||||
{props.members.map((member) => {
|
||||
return isClassMethodEntry(member) ? (
|
||||
<ClassMethodInfo entry={member} isOverloaded={props.members.length > 1} />
|
||||
) : (
|
||||
<div className={REFERENCE_MEMBER_CARD_ITEM}>
|
||||
{props.members.every((member) => member.deprecationMessage !== null) ? (
|
||||
<DeprecatedLabel entry={props.members[0]} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<RawHtml value={member.htmlDescription} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{isClassMethodEntry(props.member) ? (
|
||||
props.member.signatures.map((sig, i, signatures) => {
|
||||
const renderableMember = getFunctionMetadataRenderable(sig);
|
||||
return <ClassMethodInfo entry={renderableMember} options={{showUsageNotes: true}} />;
|
||||
})
|
||||
) : props.member.htmlDescription || props.member.deprecationMessage ? (
|
||||
<div className={REFERENCE_MEMBER_CARD_ITEM}>
|
||||
<DeprecatedLabel entry={props.member} />
|
||||
<RawHtml value={props.member.htmlDescription} />
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const memberName = props.member.name;
|
||||
const returnType = getMemberType(props.member);
|
||||
return (
|
||||
<div id={memberName} className={REFERENCE_MEMBER_CARD} tabIndex={-1}>
|
||||
<header>
|
||||
<div className={REFERENCE_HEADER}>
|
||||
<div className={REFERENCE_MEMBER_CARD_HEADER}>
|
||||
<h3>{memberName}</h3>
|
||||
<div>
|
||||
{props.members.length > 1 ? (
|
||||
<span>{props.members.length} overloads</span>
|
||||
{isClassMethodEntry(props.member) && props.member.signatures.length > 1 ? (
|
||||
<span>{props.member.signatures.length} overloads</span>
|
||||
) : returnType ? (
|
||||
<code>{returnType}</code>
|
||||
<CodeSymbol code={returnType} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
@@ -75,7 +74,7 @@ export function ClassMember(props: {members: MemberEntryRenderable[]}) {
|
||||
|
||||
function getMemberType(entry: MemberEntryRenderable): string | null {
|
||||
if (isClassMethodEntry(entry)) {
|
||||
return entry.returnType;
|
||||
return entry.implementation.returnType;
|
||||
} else if (isPropertyEntry(entry) || isGetterEntry(entry) || isSetterEntry(entry)) {
|
||||
return entry.type;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import {Fragment, h} from 'preact';
|
||||
import {
|
||||
FunctionEntryRenderable,
|
||||
FunctionSignatureMetadataRenderable,
|
||||
MethodEntryRenderable,
|
||||
ParameterEntryRenderable,
|
||||
} from '../entities/renderables';
|
||||
@@ -16,13 +17,16 @@ import {PARAM_KEYWORD_CLASS_NAME, REFERENCE_MEMBER_CARD_ITEM} from '../styling/c
|
||||
import {DeprecatedLabel} from './deprecated-label';
|
||||
import {Parameter} from './parameter';
|
||||
import {RawHtml} from './raw-html';
|
||||
import {CodeSymbol} from './code-symbols';
|
||||
|
||||
/**
|
||||
* Component to render the method-specific parts of a class's API reference.
|
||||
*/
|
||||
export function ClassMethodInfo(props: {
|
||||
entry: MethodEntryRenderable | FunctionEntryRenderable;
|
||||
isOverloaded?: boolean;
|
||||
entry: FunctionSignatureMetadataRenderable;
|
||||
options?: {
|
||||
showUsageNotes?: boolean;
|
||||
};
|
||||
}) {
|
||||
const entry = props.entry;
|
||||
|
||||
@@ -32,21 +36,21 @@ export function ClassMethodInfo(props: {
|
||||
>
|
||||
<RawHtml value={entry.htmlDescription} className={'docs-function-definition'} />
|
||||
{/* In case when method is overloaded we need to indicate which overload is deprecated */}
|
||||
{!props.isOverloaded ? (
|
||||
<></>
|
||||
) : (
|
||||
{entry.isDeprecated ? (
|
||||
<div>
|
||||
<DeprecatedLabel entry={entry} />
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{entry.params.map((param: ParameterEntryRenderable) => (
|
||||
<Parameter param={param} />
|
||||
))}
|
||||
<div className={'docs-return-type'}>
|
||||
<span className={PARAM_KEYWORD_CLASS_NAME}>@returns</span>
|
||||
<code>{entry.returnType}</code>
|
||||
<CodeSymbol code={entry.returnType} />
|
||||
</div>
|
||||
{entry.htmlUsageNotes ? (
|
||||
{entry.htmlUsageNotes && props.options?.showUsageNotes ? (
|
||||
<div className={'docs-usage-notes'}>
|
||||
<span className={PARAM_KEYWORD_CLASS_NAME}>Usage notes</span>
|
||||
<RawHtml value={entry.htmlUsageNotes} />
|
||||
|
||||
@@ -24,9 +24,9 @@ export function ClassReference(entry: ClassEntryRenderable) {
|
||||
<TabDescription entry={entry} />
|
||||
<TabUsageNotes entry={entry} />
|
||||
{
|
||||
entry.membersGroups.size > 0
|
||||
entry.members.length > 0
|
||||
? (<div class={REFERENCE_MEMBERS_CONTAINER}>
|
||||
<ClassMemberList membersGroups={entry.membersGroups} />
|
||||
<ClassMemberList members={entry.members} />
|
||||
</div>)
|
||||
: (<></>)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
import {Fragment, h} from 'preact';
|
||||
import {CliCardRenderable} from '../entities/renderables';
|
||||
import {DeprecatedLabel} from './deprecated-label';
|
||||
import { REFERENCE_MEMBER_CARD, REFERENCE_MEMBER_CARD_HEADER } from '../styling/css-classes';
|
||||
|
||||
export function CliCard(props: {card: CliCardRenderable}) {
|
||||
return (
|
||||
<div id={props.card.type} class="docs-reference-member-card" tabIndex={-1}>
|
||||
<div id={props.card.type} class={REFERENCE_MEMBER_CARD} tabIndex={-1}>
|
||||
<header>
|
||||
<div class="docs-card-ref-header">
|
||||
<div class={REFERENCE_MEMBER_CARD_HEADER}>
|
||||
<h3>{props.card.type}</h3>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -23,7 +23,7 @@ export function CliCommandReference(entry: CliCommandRenderable) {
|
||||
<div class="docs-code docs-reference-cli-toc">
|
||||
<pre class="docs-mini-scroll-track">
|
||||
<code>
|
||||
<div className={'shiki-ln-line'}>
|
||||
<div className={'shiki line cli'}>
|
||||
ng {commandName(entry, command)}
|
||||
{entry.argumentsLabel ? <button member-id={'Arguments'} className="shiki-ln-line-argument">{entry.argumentsLabel}</button> : <></>}
|
||||
{entry.hasOptions ? <button member-id={'Options'} className="shiki-ln-line-option">[options]</button> : <></>}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {h} from 'preact';
|
||||
import {getModuleName} from '../symbol-context';
|
||||
import {getLinkToModule} from '../transforms/url-transforms';
|
||||
|
||||
const symbolRegex = /([a-zA-Z_$][a-zA-Z_$0-9\.]*)/;
|
||||
|
||||
/**
|
||||
* Component that generates a code block with a link to a Symbol if it's known,
|
||||
* else generates a string code block
|
||||
*/
|
||||
export function CodeSymbol(props: {code: string}) {
|
||||
return (
|
||||
<code>
|
||||
{props.code.split(symbolRegex).map((rawSymbol, index) => {
|
||||
// Every even index is a non-match when the regex has 1 capturing group
|
||||
if (index % 2 === 0) return rawSymbol;
|
||||
|
||||
let [symbol, subSymbol] = rawSymbol.split('.'); // Also takes care of methods, enum value etc.
|
||||
const moduleName = getModuleName(symbol);
|
||||
|
||||
if (moduleName) {
|
||||
const url = getLinkToModule(moduleName, symbol, subSymbol);
|
||||
return <a href={url}>{rawSymbol}</a>;
|
||||
}
|
||||
|
||||
return rawSymbol;
|
||||
})}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {h, Fragment} from 'preact';
|
||||
import {EnumEntryRenderable} from '../entities/renderables';
|
||||
import {EnumEntryRenderable, MemberEntryRenderable} from '../entities/renderables';
|
||||
import {HeaderApi} from './header-api';
|
||||
import {TabDescription} from './tab-description';
|
||||
import {TabApi} from './tab-api';
|
||||
@@ -26,7 +26,7 @@ export function EnumReference(entry: EnumEntryRenderable) {
|
||||
? (
|
||||
<div class={REFERENCE_MEMBERS_CONTAINER}>
|
||||
<div class={REFERENCE_MEMBERS}>
|
||||
{entry.members.map((member: any) => (<ClassMember members={[member]}/>))}
|
||||
{entry.members.map((member: MemberEntryRenderable) => (<ClassMember member={member}/>))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -7,23 +7,66 @@
|
||||
*/
|
||||
|
||||
import {h} from 'preact';
|
||||
import {FunctionEntryRenderable} from '../entities/renderables';
|
||||
import {FunctionEntryRenderable, FunctionSignatureMetadataRenderable} from '../entities/renderables';
|
||||
import {
|
||||
PARAM_KEYWORD_CLASS_NAME,
|
||||
REFERENCE_HEADER,
|
||||
REFERENCE_MEMBERS,
|
||||
REFERENCE_MEMBERS_CONTAINER,
|
||||
REFERENCE_MEMBER_CARD,
|
||||
REFERENCE_MEMBER_CARD_BODY,
|
||||
REFERENCE_MEMBER_CARD_HEADER,
|
||||
} from '../styling/css-classes';
|
||||
import {ClassMethodInfo} from './class-method-info';
|
||||
import {HeaderApi} from './header-api';
|
||||
import {TabApi} from './tab-api';
|
||||
import {TabDescription} from './tab-description';
|
||||
import {TabUsageNotes} from './tab-usage-notes';
|
||||
import {HighlightTypeScript} from './highlight-ts';
|
||||
import {printInitializerFunctionSignatureLine} from '../transforms/code-transforms';
|
||||
import {getFunctionMetadataRenderable} from '../transforms/function-transforms';
|
||||
import {CodeSymbol} from './code-symbols';
|
||||
|
||||
export const signatureCard = (
|
||||
name: string,
|
||||
signature: FunctionSignatureMetadataRenderable,
|
||||
opts: {id: string},
|
||||
printSignaturesAsHeader: boolean,
|
||||
) => {
|
||||
return (
|
||||
<div class={REFERENCE_MEMBER_CARD} id={opts.id} tabIndex={-1}>
|
||||
<header>
|
||||
{printSignaturesAsHeader ? (
|
||||
<code>
|
||||
<HighlightTypeScript
|
||||
code={printInitializerFunctionSignatureLine(
|
||||
name,
|
||||
signature,
|
||||
// Always omit types in signature headers, to keep them short.
|
||||
true,
|
||||
)}
|
||||
removeFunctionKeyword={true}
|
||||
/>
|
||||
</code>
|
||||
) : (
|
||||
<div className={REFERENCE_MEMBER_CARD_HEADER}>
|
||||
<h3>{name}</h3>
|
||||
<div>
|
||||
<CodeSymbol code={signature.returnType} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<div class={REFERENCE_MEMBER_CARD_BODY}>
|
||||
<ClassMethodInfo entry={signature} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/** Component to render a function API reference document. */
|
||||
export function FunctionReference(entry: FunctionEntryRenderable) {
|
||||
// Use signatures as header if there are multiple signatures.
|
||||
const printSignaturesAsHeader = entry.signatures.length > 1;
|
||||
|
||||
return (
|
||||
<div class="api">
|
||||
<HeaderApi entry={entry} />
|
||||
@@ -32,26 +75,16 @@ export function FunctionReference(entry: FunctionEntryRenderable) {
|
||||
<TabUsageNotes entry={entry} />
|
||||
<div className={REFERENCE_MEMBERS_CONTAINER}>
|
||||
<div className={REFERENCE_MEMBERS}>
|
||||
<div className={REFERENCE_MEMBER_CARD}>
|
||||
<header>
|
||||
<div className={REFERENCE_HEADER}>
|
||||
<h3>{entry.name}</h3>
|
||||
<div>
|
||||
<code>{entry.returnType}</code>
|
||||
</div>
|
||||
</div>
|
||||
{entry.isDeprecated && (
|
||||
<span className={`${PARAM_KEYWORD_CLASS_NAME} docs-deprecated`}>@deprecated</span>
|
||||
)}
|
||||
</header>
|
||||
<div className={REFERENCE_MEMBER_CARD_BODY}>
|
||||
{entry.overloads ? (
|
||||
entry.overloads.map((overload) => <ClassMethodInfo entry={overload} />)
|
||||
) : (
|
||||
<ClassMethodInfo entry={entry} isOverloaded={true} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{entry.signatures.map((s, i) =>
|
||||
signatureCard(
|
||||
s.name,
|
||||
getFunctionMetadataRenderable(s, entry.moduleName),
|
||||
{
|
||||
id: `${s.name}_${i}`,
|
||||
},
|
||||
printSignaturesAsHeader,
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -74,12 +74,12 @@ export function HeaderApi(props: {entry: DocEntryRenderable; showFullDescription
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p
|
||||
<section
|
||||
className={'docs-reference-description'}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: props.showFullDescription ? entry.htmlDescription : entry.shortHtmlDescription,
|
||||
}}
|
||||
></p>
|
||||
></section>
|
||||
|
||||
<DocsPillRow links={entry.additionalLinks} />
|
||||
</header>
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import { h } from 'preact';
|
||||
import { RawHtml } from './raw-html';
|
||||
import { codeToHtml } from '../shiki/shiki';
|
||||
import {h} from 'preact';
|
||||
import {RawHtml} from './raw-html';
|
||||
import {codeToHtml} from '../shiki/shiki';
|
||||
|
||||
/** Component to render a header of the CLI page. */
|
||||
export function HighlightTypeScript(props: {code: string}) {
|
||||
const result = codeToHtml(props.code, 'typescript');
|
||||
export function HighlightTypeScript(props: {code: string; removeFunctionKeyword?: boolean}) {
|
||||
const result = codeToHtml(props.code, 'typescript', {
|
||||
removeFunctionKeyword: props.removeFunctionKeyword,
|
||||
});
|
||||
|
||||
return (
|
||||
<RawHtml value={result} />
|
||||
);
|
||||
return <RawHtml value={result} />;
|
||||
}
|
||||
|
||||
+14
-41
@@ -7,20 +7,13 @@
|
||||
*/
|
||||
|
||||
import {h, JSX} from 'preact';
|
||||
import {FunctionEntryRenderable, InitializerApiFunctionRenderable} from '../entities/renderables';
|
||||
import {InitializerApiFunctionRenderable} from '../entities/renderables';
|
||||
import {HeaderApi} from './header-api';
|
||||
import {TabApi} from './tab-api';
|
||||
import {TabUsageNotes} from './tab-usage-notes';
|
||||
import {
|
||||
REFERENCE_MEMBERS,
|
||||
REFERENCE_MEMBERS_CONTAINER,
|
||||
REFERENCE_MEMBER_CARD,
|
||||
REFERENCE_MEMBER_CARD_BODY,
|
||||
} from '../styling/css-classes';
|
||||
import {printInitializerFunctionSignatureLine} from '../transforms/code-transforms';
|
||||
import {HighlightTypeScript} from './highlight-ts';
|
||||
import {ClassMethodInfo} from './class-method-info';
|
||||
import {getFunctionRenderable} from '../transforms/function-transforms';
|
||||
import {REFERENCE_MEMBERS, REFERENCE_MEMBERS_CONTAINER} from '../styling/css-classes';
|
||||
import {getFunctionMetadataRenderable} from '../transforms/function-transforms';
|
||||
import {signatureCard} from './function-reference';
|
||||
|
||||
/** Component to render a constant API reference document. */
|
||||
export function InitializerApiFunction(entry: InitializerApiFunctionRenderable) {
|
||||
@@ -40,32 +33,6 @@ export function InitializerApiFunction(entry: InitializerApiFunctionRenderable)
|
||||
entry.callFunction.signatures[0].description = '';
|
||||
}
|
||||
|
||||
const signatureCard = (name: string, signature: FunctionEntryRenderable, opts: {id: string}) => {
|
||||
return (
|
||||
<div class={REFERENCE_MEMBER_CARD} id={opts.id} tabIndex={-1}>
|
||||
<header>
|
||||
{printSignaturesAsHeader ? (
|
||||
<code>
|
||||
<HighlightTypeScript
|
||||
code={printInitializerFunctionSignatureLine(
|
||||
name,
|
||||
signature,
|
||||
// Always omit types in signature headers, to keep them short.
|
||||
true,
|
||||
)}
|
||||
/>
|
||||
</code>
|
||||
) : (
|
||||
<h3>{`${name}()`}</h3>
|
||||
)}
|
||||
</header>
|
||||
<div class={REFERENCE_MEMBER_CARD_BODY}>
|
||||
<ClassMethodInfo entry={signature} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="api">
|
||||
<HeaderApi entry={entry} showFullDescription={true} />
|
||||
@@ -75,9 +42,14 @@ export function InitializerApiFunction(entry: InitializerApiFunctionRenderable)
|
||||
<div class={REFERENCE_MEMBERS_CONTAINER}>
|
||||
<div class={REFERENCE_MEMBERS}>
|
||||
{entry.callFunction.signatures.map((s, i) =>
|
||||
signatureCard(s.name, getFunctionRenderable(s, entry.moduleName), {
|
||||
id: `${s.name}_${i}`,
|
||||
}),
|
||||
signatureCard(
|
||||
s.name,
|
||||
getFunctionMetadataRenderable(s, entry.moduleName),
|
||||
{
|
||||
id: `${s.name}_${i}`,
|
||||
},
|
||||
printSignaturesAsHeader,
|
||||
),
|
||||
)}
|
||||
|
||||
{entry.subFunctions.reduce(
|
||||
@@ -86,10 +58,11 @@ export function InitializerApiFunction(entry: InitializerApiFunctionRenderable)
|
||||
...subFunction.signatures.map((s, i) =>
|
||||
signatureCard(
|
||||
`${entry.name}.${s.name}`,
|
||||
getFunctionRenderable(s, entry.moduleName),
|
||||
getFunctionMetadataRenderable(s, entry.moduleName),
|
||||
{
|
||||
id: `${entry.name}_${s.name}_${i}`,
|
||||
},
|
||||
printSignaturesAsHeader,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -10,7 +10,7 @@ import {h} from 'preact';
|
||||
import {ParameterEntryRenderable} from '../entities/renderables';
|
||||
import {RawHtml} from './raw-html';
|
||||
import {PARAM_GROUP_CLASS_NAME} from '../styling/css-classes';
|
||||
|
||||
import {CodeSymbol} from './code-symbols';
|
||||
|
||||
/** Component to render a function or method parameter reference doc fragment. */
|
||||
export function Parameter(props: {param: ParameterEntryRenderable}) {
|
||||
@@ -21,7 +21,7 @@ export function Parameter(props: {param: ParameterEntryRenderable}) {
|
||||
{/*TODO: isOptional, isRestParam*/}
|
||||
<span class="docs-param-keyword">@param</span>
|
||||
<span class="docs-param-name">{param.name}</span>
|
||||
<code>{param.type}</code>
|
||||
<CodeSymbol code={param.type} />
|
||||
<RawHtml value={param.htmlDescription} className="docs-parameter-description" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,10 +19,18 @@ ts_library(
|
||||
),
|
||||
deps = [
|
||||
"//adev/shared-docs/pipeline/api-gen/rendering:render_api_to_html_lib",
|
||||
"@npm//@bazel/runfiles",
|
||||
"@npm//@types/jsdom",
|
||||
"@npm//jsdom",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "unit_tests",
|
||||
data = [
|
||||
"@npm//jsdom",
|
||||
] + glob([
|
||||
"**/*.json",
|
||||
]),
|
||||
deps = [":unit_test_lib"],
|
||||
)
|
||||
|
||||
@@ -3,81 +3,671 @@
|
||||
"entries": [
|
||||
{
|
||||
"name": "NgTemplateOutlet",
|
||||
"description": "*one* directive",
|
||||
"isAbstract": false,
|
||||
"entryType": "directive",
|
||||
"members": [],
|
||||
"jsdocTags": [],
|
||||
"rawComment": "",
|
||||
"source": {
|
||||
"fileName": "/packages/core/src/ng_template_outlet.ts",
|
||||
"line": 7,
|
||||
"character": 7
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "UserProfile",
|
||||
"entryType": "undecorated_class",
|
||||
"members": [
|
||||
{
|
||||
"name": "userId",
|
||||
"type": "number",
|
||||
"name": "ngTemplateOutletContext",
|
||||
"type": "C",
|
||||
"memberType": "property",
|
||||
"memberTags": [],
|
||||
"description": "A user identifier.",
|
||||
"jsdocTags": []
|
||||
"memberTags": ["input"],
|
||||
"description": "A context object to attach to the {@link EmbeddedViewRef}. This should be an\nobject, the object's keys will be available for binding by the local template `let`\ndeclarations.\nUsing the key `$implicit` in the context object will set its value as default.",
|
||||
"jsdocTags": [],
|
||||
"inputAlias": "ngTemplateOutletContext",
|
||||
"isRequiredInput": false
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"memberType": "getter",
|
||||
"memberTags": [],
|
||||
"description": "Name of the user",
|
||||
"jsdocTags": []
|
||||
"name": "ngTemplateOutlet",
|
||||
"type": "TemplateRef<C>",
|
||||
"memberType": "property",
|
||||
"memberTags": ["input"],
|
||||
"description": "A string defining the template reference and optionally the context object for the template.",
|
||||
"jsdocTags": [],
|
||||
"inputAlias": "ngTemplateOutlet",
|
||||
"isRequiredInput": false
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"memberType": "setter",
|
||||
"memberTags": [],
|
||||
"description": "Name of the user",
|
||||
"jsdocTags": []
|
||||
"name": "ngTemplateOutletInjector",
|
||||
"type": "Injector",
|
||||
"memberType": "property",
|
||||
"memberTags": ["input"],
|
||||
"description": "Injector to be used within the embedded view.",
|
||||
"jsdocTags": [],
|
||||
"inputAlias": "ngTemplateOutletInjector",
|
||||
"isRequiredInput": false
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"name": "ngOnChanges",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "config",
|
||||
"description": "Setting for saving.",
|
||||
"type": "object",
|
||||
"isOptional": false,
|
||||
"isRestParam": false
|
||||
"name": "ngOnChanges",
|
||||
"entryType": "function",
|
||||
"description": "",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [],
|
||||
"params": [
|
||||
{
|
||||
"name": "changes",
|
||||
"description": "",
|
||||
"type": "SimpleChanges",
|
||||
"isOptional": false,
|
||||
"isRestParam": false
|
||||
}
|
||||
],
|
||||
"rawComment": "",
|
||||
"returnType": "void"
|
||||
}
|
||||
],
|
||||
"name": "save",
|
||||
"returnType": "boolean",
|
||||
"implementation": {
|
||||
"params": [
|
||||
{
|
||||
"name": "changes",
|
||||
"description": "",
|
||||
"type": "SimpleChanges",
|
||||
"isOptional": false,
|
||||
"isRestParam": false
|
||||
}
|
||||
],
|
||||
"isNewType": false,
|
||||
"returnType": "void",
|
||||
"generics": [],
|
||||
"name": "ngOnChanges",
|
||||
"description": "",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [],
|
||||
"rawComment": ""
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "Save the user.",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "param",
|
||||
"comment": "Setting for saving."
|
||||
},
|
||||
{
|
||||
"name": "returns",
|
||||
"comment": "Whether it succeeded"
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * Save the user.\n * @param config Setting for saving.\n * @returns Whether it succeeded\n */",
|
||||
"description": "",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "",
|
||||
"memberType": "method",
|
||||
"memberTags": []
|
||||
}
|
||||
],
|
||||
"generics": [{"name": "C", "default": "unknown"}],
|
||||
"description": "",
|
||||
"jsdocTags": [
|
||||
{"name": "ngModule", "comment": "CommonModule"},
|
||||
{
|
||||
"name": "description",
|
||||
"comment": "Inserts an embedded view from a prepared `TemplateRef`.\n\nYou can attach a context object to the `EmbeddedViewRef` by setting `[ngTemplateOutletContext]`.\n`[ngTemplateOutletContext]` should be an object, the object's keys will be available for binding\nby the local template `let` declarations."
|
||||
},
|
||||
{
|
||||
"name": "usageNotes",
|
||||
"comment": "```\n<ng-container *ngTemplateOutlet=\"templateRefExp; context: contextExp\"></ng-container>\n```\n\nUsing the key `$implicit` in the context object will set its value as default.\n\n### Example\n\n{@example common/ngTemplateOutlet/ts/module.ts region='NgTemplateOutlet'}"
|
||||
},
|
||||
{"name": "publicApi", "comment": ""}
|
||||
],
|
||||
"rawComment": "/**\n * @ngModule CommonModule\n *\n * @description\n *\n * Inserts an embedded view from a prepared `TemplateRef`.\n *\n * You can attach a context object to the `EmbeddedViewRef` by setting `[ngTemplateOutletContext]`.\n * `[ngTemplateOutletContext]` should be an object, the object's keys will be available for binding\n * by the local template `let` declarations.\n *\n * @usageNotes\n * ```\n * <ng-container *ngTemplateOutlet=\"templateRefExp; context: contextExp\"></ng-container>\n * ```\n *\n * Using the key `$implicit` in the context object will set its value as default.\n *\n * ### Example\n *\n * {@example common/ngTemplateOutlet/ts/module.ts region='NgTemplateOutlet'}\n *\n * @publicApi\n */",
|
||||
"implements": ["OnChanges"],
|
||||
"isStandalone": true,
|
||||
"selector": "[ngTemplateOutlet]",
|
||||
"exportAs": [],
|
||||
"source": {
|
||||
"filePath": "/packages/common/src/directives/ng_template_outlet.ts",
|
||||
"startLine": 45,
|
||||
"endLine": 126
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ViewRef",
|
||||
"isAbstract": true,
|
||||
"entryType": "undecorated_class",
|
||||
"members": [
|
||||
{
|
||||
"name": "destroy",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "destroy",
|
||||
"entryType": "function",
|
||||
"description": "Destroys this view and all of the data structures associated with it.",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [],
|
||||
"params": [],
|
||||
"rawComment": "/**\n * Destroys this view and all of the data structures associated with it.\n */",
|
||||
"returnType": "void"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [],
|
||||
"isNewType": false,
|
||||
"returnType": "void",
|
||||
"generics": [],
|
||||
"name": "destroy",
|
||||
"description": "Destroys this view and all of the data structures associated with it.",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "/**\n * Destroys this view and all of the data structures associated with it.\n */"
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "Destroys this view and all of the data structures associated with it.",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "/**\n * Destroys this view and all of the data structures associated with it.\n */",
|
||||
"memberType": "method",
|
||||
"memberTags": ["abstract"]
|
||||
},
|
||||
{
|
||||
"name": "destroyed",
|
||||
"type": "boolean",
|
||||
"memberType": "getter",
|
||||
"memberTags": ["abstract"],
|
||||
"description": "Reports whether this view has been destroyed.",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "returns",
|
||||
"comment": "True after the `destroy()` method has been called, false otherwise."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onDestroy",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "onDestroy",
|
||||
"entryType": "function",
|
||||
"description": "A lifecycle hook that provides additional developer-defined cleanup\nfunctionality for views.",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "param",
|
||||
"comment": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked."
|
||||
}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"name": "callback",
|
||||
"description": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked.",
|
||||
"type": "Function",
|
||||
"isOptional": false,
|
||||
"isRestParam": false
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * A lifecycle hook that provides additional developer-defined cleanup\n * functionality for views.\n * @param callback A handler function that cleans up developer-defined data\n * associated with a view. Called when the `destroy()` method is invoked.\n */",
|
||||
"returnType": "void"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [
|
||||
{
|
||||
"name": "callback",
|
||||
"description": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked.",
|
||||
"type": "Function",
|
||||
"isOptional": false,
|
||||
"isRestParam": false
|
||||
}
|
||||
],
|
||||
"isNewType": false,
|
||||
"returnType": "void",
|
||||
"generics": [],
|
||||
"name": "onDestroy",
|
||||
"description": "A lifecycle hook that provides additional developer-defined cleanup\nfunctionality for views.",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "param",
|
||||
"comment": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked."
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * A lifecycle hook that provides additional developer-defined cleanup\n * functionality for views.\n * @param callback A handler function that cleans up developer-defined data\n * associated with a view. Called when the `destroy()` method is invoked.\n */"
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "A lifecycle hook that provides additional developer-defined cleanup\nfunctionality for views.",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "param",
|
||||
"comment": "A handler function that cleans up developer-defined data\nassociated with a view. Called when the `destroy()` method is invoked."
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * A lifecycle hook that provides additional developer-defined cleanup\n * functionality for views.\n * @param callback A handler function that cleans up developer-defined data\n * associated with a view. Called when the `destroy()` method is invoked.\n */",
|
||||
"memberType": "method",
|
||||
"memberTags": ["abstract"]
|
||||
},
|
||||
{
|
||||
"name": "markForCheck",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "markForCheck",
|
||||
"entryType": "function",
|
||||
"description": "When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\nchange detection strategy, explicitly marks the view as changed so that\nit can be checked again.\n\nComponents are normally marked as dirty (in need of rerendering) when inputs\nhave changed or events have fired in the view. Call this method to ensure that\na component is checked even if these triggers have not occurred.\n\n<!-- TODO: Add a link to a chapter on OnPush components -->",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [],
|
||||
"params": [],
|
||||
"rawComment": "/**\n * When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\n * change detection strategy, explicitly marks the view as changed so that\n * it can be checked again.\n *\n * Components are normally marked as dirty (in need of rerendering) when inputs\n * have changed or events have fired in the view. Call this method to ensure that\n * a component is checked even if these triggers have not occurred.\n *\n * <!-- TODO: Add a link to a chapter on OnPush components -->\n *\n */",
|
||||
"returnType": "void"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [],
|
||||
"isNewType": false,
|
||||
"returnType": "void",
|
||||
"generics": [],
|
||||
"name": "markForCheck",
|
||||
"description": "When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\nchange detection strategy, explicitly marks the view as changed so that\nit can be checked again.\n\nComponents are normally marked as dirty (in need of rerendering) when inputs\nhave changed or events have fired in the view. Call this method to ensure that\na component is checked even if these triggers have not occurred.\n\n<!-- TODO: Add a link to a chapter on OnPush components -->",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "/**\n * When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\n * change detection strategy, explicitly marks the view as changed so that\n * it can be checked again.\n *\n * Components are normally marked as dirty (in need of rerendering) when inputs\n * have changed or events have fired in the view. Call this method to ensure that\n * a component is checked even if these triggers have not occurred.\n *\n * <!-- TODO: Add a link to a chapter on OnPush components -->\n *\n */"
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\nchange detection strategy, explicitly marks the view as changed so that\nit can be checked again.\n\nComponents are normally marked as dirty (in need of rerendering) when inputs\nhave changed or events have fired in the view. Call this method to ensure that\na component is checked even if these triggers have not occurred.\n\n<!-- TODO: Add a link to a chapter on OnPush components -->",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "/**\n * When a view uses the {@link ChangeDetectionStrategy#OnPush} (checkOnce)\n * change detection strategy, explicitly marks the view as changed so that\n * it can be checked again.\n *\n * Components are normally marked as dirty (in need of rerendering) when inputs\n * have changed or events have fired in the view. Call this method to ensure that\n * a component is checked even if these triggers have not occurred.\n *\n * <!-- TODO: Add a link to a chapter on OnPush components -->\n *\n */",
|
||||
"memberType": "method",
|
||||
"memberTags": ["abstract", "override"]
|
||||
},
|
||||
{
|
||||
"name": "detach",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "detach",
|
||||
"entryType": "function",
|
||||
"description": "Detaches this view from the change-detection tree.\nA detached view is not checked until it is reattached.\nUse in combination with `detectChanges()` to implement local change detection checks.\n\nDetached views are not checked during change detection runs until they are\nre-attached, even if they are marked as dirty.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [],
|
||||
"params": [],
|
||||
"rawComment": "/**\n * Detaches this view from the change-detection tree.\n * A detached view is not checked until it is reattached.\n * Use in combination with `detectChanges()` to implement local change detection checks.\n *\n * Detached views are not checked during change detection runs until they are\n * re-attached, even if they are marked as dirty.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */",
|
||||
"returnType": "void"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [],
|
||||
"isNewType": false,
|
||||
"returnType": "void",
|
||||
"generics": [],
|
||||
"name": "detach",
|
||||
"description": "Detaches this view from the change-detection tree.\nA detached view is not checked until it is reattached.\nUse in combination with `detectChanges()` to implement local change detection checks.\n\nDetached views are not checked during change detection runs until they are\nre-attached, even if they are marked as dirty.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "/**\n * Detaches this view from the change-detection tree.\n * A detached view is not checked until it is reattached.\n * Use in combination with `detectChanges()` to implement local change detection checks.\n *\n * Detached views are not checked during change detection runs until they are\n * re-attached, even if they are marked as dirty.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */"
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "Detaches this view from the change-detection tree.\nA detached view is not checked until it is reattached.\nUse in combination with `detectChanges()` to implement local change detection checks.\n\nDetached views are not checked during change detection runs until they are\nre-attached, even if they are marked as dirty.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "/**\n * Detaches this view from the change-detection tree.\n * A detached view is not checked until it is reattached.\n * Use in combination with `detectChanges()` to implement local change detection checks.\n *\n * Detached views are not checked during change detection runs until they are\n * re-attached, even if they are marked as dirty.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */",
|
||||
"memberType": "method",
|
||||
"memberTags": ["abstract", "override"]
|
||||
},
|
||||
{
|
||||
"name": "detectChanges",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "detectChanges",
|
||||
"entryType": "function",
|
||||
"description": "Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\nto implement local change detection checks.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [],
|
||||
"params": [],
|
||||
"rawComment": "/**\n * Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\n * to implement local change detection checks.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */",
|
||||
"returnType": "void"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [],
|
||||
"isNewType": false,
|
||||
"returnType": "void",
|
||||
"generics": [],
|
||||
"name": "detectChanges",
|
||||
"description": "Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\nto implement local change detection checks.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "/**\n * Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\n * to implement local change detection checks.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */"
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\nto implement local change detection checks.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n<!-- TODO: Add a live demo once ref.detectChanges is merged into master -->",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "/**\n * Checks this view and its children. Use in combination with {@link ChangeDetectorRef#detach}\n * to implement local change detection checks.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n * <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->\n *\n */",
|
||||
"memberType": "method",
|
||||
"memberTags": ["abstract", "override"]
|
||||
},
|
||||
{
|
||||
"name": "checkNoChanges",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "checkNoChanges",
|
||||
"entryType": "function",
|
||||
"description": "Checks the change detector and its children, and throws if any changes are detected.\n\nUse in development mode to verify that running change detection doesn't introduce\nother changes. Calling it in production mode is a noop.",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "deprecated",
|
||||
"comment": "This is a test-only API that does not have a place in production interface.\n`checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\nmode. For more granular `checkNoChanges` validation, use `ComponentFixture`."
|
||||
}
|
||||
],
|
||||
"params": [],
|
||||
"rawComment": "/**\n * Checks the change detector and its children, and throws if any changes are detected.\n *\n * Use in development mode to verify that running change detection doesn't introduce\n * other changes. Calling it in production mode is a noop.\n *\n * @deprecated This is a test-only API that does not have a place in production interface.\n * `checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\n * mode. For more granular `checkNoChanges` validation, use `ComponentFixture`.\n */",
|
||||
"returnType": "void"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [],
|
||||
"isNewType": false,
|
||||
"returnType": "void",
|
||||
"generics": [],
|
||||
"name": "checkNoChanges",
|
||||
"description": "Checks the change detector and its children, and throws if any changes are detected.\n\nUse in development mode to verify that running change detection doesn't introduce\nother changes. Calling it in production mode is a noop.",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "deprecated",
|
||||
"comment": "This is a test-only API that does not have a place in production interface.\n`checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\nmode. For more granular `checkNoChanges` validation, use `ComponentFixture`."
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * Checks the change detector and its children, and throws if any changes are detected.\n *\n * Use in development mode to verify that running change detection doesn't introduce\n * other changes. Calling it in production mode is a noop.\n *\n * @deprecated This is a test-only API that does not have a place in production interface.\n * `checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\n * mode. For more granular `checkNoChanges` validation, use `ComponentFixture`.\n */"
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "Checks the change detector and its children, and throws if any changes are detected.\n\nUse in development mode to verify that running change detection doesn't introduce\nother changes. Calling it in production mode is a noop.",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "deprecated",
|
||||
"comment": "This is a test-only API that does not have a place in production interface.\n`checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\nmode. For more granular `checkNoChanges` validation, use `ComponentFixture`."
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * Checks the change detector and its children, and throws if any changes are detected.\n *\n * Use in development mode to verify that running change detection doesn't introduce\n * other changes. Calling it in production mode is a noop.\n *\n * @deprecated This is a test-only API that does not have a place in production interface.\n * `checkNoChanges` is already part of an `ApplicationRef` tick when the app is running in dev\n * mode. For more granular `checkNoChanges` validation, use `ComponentFixture`.\n */",
|
||||
"memberType": "method",
|
||||
"memberTags": ["abstract", "override"]
|
||||
},
|
||||
{
|
||||
"name": "reattach",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "reattach",
|
||||
"entryType": "function",
|
||||
"description": "Re-attaches the previously detached view to the change detection tree.\nViews are attached to the tree by default.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [],
|
||||
"params": [],
|
||||
"rawComment": "/**\n * Re-attaches the previously detached view to the change detection tree.\n * Views are attached to the tree by default.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n *\n */",
|
||||
"returnType": "void"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [],
|
||||
"isNewType": false,
|
||||
"returnType": "void",
|
||||
"generics": [],
|
||||
"name": "reattach",
|
||||
"description": "Re-attaches the previously detached view to the change detection tree.\nViews are attached to the tree by default.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "/**\n * Re-attaches the previously detached view to the change detection tree.\n * Views are attached to the tree by default.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n *\n */"
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "Re-attaches the previously detached view to the change detection tree.\nViews are attached to the tree by default.\n\n<!-- TODO: Add a link to a chapter on detach/reattach/local digest -->",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "/**\n * Re-attaches the previously detached view to the change detection tree.\n * Views are attached to the tree by default.\n *\n * <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->\n *\n */",
|
||||
"memberType": "method",
|
||||
"memberTags": ["abstract", "override"]
|
||||
}
|
||||
],
|
||||
"generics": [],
|
||||
"description": "Represents an Angular view.",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "see",
|
||||
"comment": "[Change detection usage](/api/core/ChangeDetectorRef?tab=usage-notes)"
|
||||
},
|
||||
{"name": "publicApi", "comment": ""}
|
||||
],
|
||||
"rawComment": "/**\n * Represents an Angular view.\n *\n * @see [Change detection usage](/api/core/ChangeDetectorRef?tab=usage-notes)\n *\n * @publicApi\n */",
|
||||
"extends": "ChangeDetectorRef",
|
||||
"implements": [],
|
||||
"source": {
|
||||
"filePath": "/packages/core/src/linker/view_ref.ts",
|
||||
"startLine": 18,
|
||||
"endLine": 37
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "afterNextRender",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "afterNextRender",
|
||||
"entryType": "function",
|
||||
"description": "Register callbacks to be invoked the next time the application finishes rendering, during the\nspecified phases. The available phases are:\n- `earlyRead`\n Use this phase to **read** from the DOM before a subsequent `write` callback, for example to\n perform custom layout that the browser doesn't natively support. Prefer the `read` phase if\n reading can wait until after the write phase. **Never** write to the DOM in this phase.\n- `write`\n Use this phase to **write** to the DOM. **Never** read from the DOM in this phase.\n- `mixedReadWrite`\n Use this phase to read from and write to the DOM simultaneously. **Never** use this phase if\n it is possible to divide the work among the other phases instead.\n- `read`\n Use this phase to **read** from the DOM. **Never** write to the DOM in this phase.\n\n<div class=\"alert is-critical\">\n\nYou should prefer using the `read` and `write` phases over the `earlyRead` and `mixedReadWrite`\nphases when possible, to avoid performance degradation.\n\n</div>\n\nNote that:\n- Callbacks run in the following phase order *once, after the next render*:\n 1. `earlyRead`\n 2. `write`\n 3. `mixedReadWrite`\n 4. `read`\n- Callbacks in the same phase run in the order they are registered.\n- Callbacks run on browser platforms only, they will not run on the server.\n\nThe first phase callback to run as part of this spec will receive no parameters. Each\nsubsequent phase callback in this spec will receive the return value of the previously run\nphase callback as a parameter. This can be used to coordinate work across multiple phases.\n\nAngular is unable to verify or enforce that phases are used correctly, and instead\nrelies on each developer to follow the guidelines documented for each value and\ncarefully choose the appropriate one, refactoring their code if necessary. By doing\nso, Angular is better able to minimize the performance degradation associated with\nmanual DOM access, ensuring the best experience for the end users of your application\nor library.\n\n<div class=\"alert is-important\">\n\nComponents are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\nYou must use caution when directly reading or writing the DOM and layout.\n\n</div>",
|
||||
"generics": [
|
||||
{"name": "E", "default": "never"},
|
||||
{"name": "W", "default": "never"},
|
||||
{"name": "M", "default": "never"}
|
||||
],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [
|
||||
{"name": "param", "comment": "The callback functions to register"},
|
||||
{"name": "param", "comment": "Options to control the behavior of the callback"},
|
||||
{
|
||||
"name": "usageNotes",
|
||||
"comment": "Use `afterNextRender` to read or write the DOM once,\nfor example to initialize a non-Angular library.\n\n### Example\n```ts\n@Component({\n selector: 'my-chart-cmp',\n template: `<div #chart>{{ ... }}</div>`,\n})\nexport class MyChartCmp {\n @ViewChild('chart') chartRef: ElementRef;\n chart: MyChart|null;\n\n constructor() {\n afterNextRender({\n write: () => {\n this.chart = new MyChart(this.chartRef.nativeElement);\n }\n });\n }\n}\n```"
|
||||
},
|
||||
{"name": "developerPreview", "comment": ""}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"name": "spec",
|
||||
"description": "The callback functions to register",
|
||||
"type": "{ earlyRead?: () => E; write?: (...args: [E] extends [never] ? [] : [E]) => W; mixedReadWrite?: (...args: [W] extends [never] ? [E] extends [never] ? [] : [E] : [W]) => M; read?: (...args: [...] extends [...] ? [...] extends [...] ? [...] extends [...] ? [] : [...] : [...] : [...]) => void; }",
|
||||
"isOptional": false,
|
||||
"isRestParam": false
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"description": "Options to control the behavior of the callback",
|
||||
"type": "Omit<AfterRenderOptions, \"phase\">",
|
||||
"isOptional": true,
|
||||
"isRestParam": false
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * Register callbacks to be invoked the next time the application finishes rendering, during the\n * specified phases. The available phases are:\n * - `earlyRead`\n * Use this phase to **read** from the DOM before a subsequent `write` callback, for example to\n * perform custom layout that the browser doesn't natively support. Prefer the `read` phase if\n * reading can wait until after the write phase. **Never** write to the DOM in this phase.\n * - `write`\n * Use this phase to **write** to the DOM. **Never** read from the DOM in this phase.\n * - `mixedReadWrite`\n * Use this phase to read from and write to the DOM simultaneously. **Never** use this phase if\n * it is possible to divide the work among the other phases instead.\n * - `read`\n * Use this phase to **read** from the DOM. **Never** write to the DOM in this phase.\n *\n * <div class=\"alert is-critical\">\n *\n * You should prefer using the `read` and `write` phases over the `earlyRead` and `mixedReadWrite`\n * phases when possible, to avoid performance degradation.\n *\n * </div>\n *\n * Note that:\n * - Callbacks run in the following phase order *once, after the next render*:\n * 1. `earlyRead`\n * 2. `write`\n * 3. `mixedReadWrite`\n * 4. `read`\n * - Callbacks in the same phase run in the order they are registered.\n * - Callbacks run on browser platforms only, they will not run on the server.\n *\n * The first phase callback to run as part of this spec will receive no parameters. Each\n * subsequent phase callback in this spec will receive the return value of the previously run\n * phase callback as a parameter. This can be used to coordinate work across multiple phases.\n *\n * Angular is unable to verify or enforce that phases are used correctly, and instead\n * relies on each developer to follow the guidelines documented for each value and\n * carefully choose the appropriate one, refactoring their code if necessary. By doing\n * so, Angular is better able to minimize the performance degradation associated with\n * manual DOM access, ensuring the best experience for the end users of your application\n * or library.\n *\n * <div class=\"alert is-important\">\n *\n * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\n * You must use caution when directly reading or writing the DOM and layout.\n *\n * </div>\n *\n * @param spec The callback functions to register\n * @param options Options to control the behavior of the callback\n *\n * @usageNotes\n *\n * Use `afterNextRender` to read or write the DOM once,\n * for example to initialize a non-Angular library.\n *\n * ### Example\n * ```ts\n * @Component({\n * selector: 'my-chart-cmp',\n * template: `<div #chart>{{ ... }}</div>`,\n * })\n * export class MyChartCmp {\n * @ViewChild('chart') chartRef: ElementRef;\n * chart: MyChart|null;\n *\n * constructor() {\n * afterNextRender({\n * write: () => {\n * this.chart = new MyChart(this.chartRef.nativeElement);\n * }\n * });\n * }\n * }\n * ```\n *\n * @developerPreview\n */",
|
||||
"returnType": "AfterRenderRef"
|
||||
},
|
||||
{
|
||||
"name": "afterNextRender",
|
||||
"entryType": "function",
|
||||
"description": "Register a callback to be invoked the next time the application finishes rendering, during the\n`mixedReadWrite` phase.\n\n<div class=\"alert is-critical\">\n\nYou should prefer specifying an explicit phase for the callback instead, or you risk significant\nperformance degradation.\n\n</div>\n\nNote that the callback will run\n- in the order it was registered\n- on browser platforms only\n- during the `mixedReadWrite` phase\n\n<div class=\"alert is-important\">\n\nComponents are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\nYou must use caution when directly reading or writing the DOM and layout.\n\n</div>",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [
|
||||
{"name": "param", "comment": "A callback function to register"},
|
||||
{"name": "param", "comment": "Options to control the behavior of the callback"},
|
||||
{
|
||||
"name": "usageNotes",
|
||||
"comment": "Use `afterNextRender` to read or write the DOM once,\nfor example to initialize a non-Angular library.\n\n### Example\n```ts\n@Component({\n selector: 'my-chart-cmp',\n template: `<div #chart>{{ ... }}</div>`,\n})\nexport class MyChartCmp {\n @ViewChild('chart') chartRef: ElementRef;\n chart: MyChart|null;\n\n constructor() {\n afterNextRender({\n write: () => {\n this.chart = new MyChart(this.chartRef.nativeElement);\n }\n });\n }\n}\n```"
|
||||
},
|
||||
{"name": "developerPreview", "comment": ""}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"name": "callback",
|
||||
"description": "A callback function to register",
|
||||
"type": "VoidFunction",
|
||||
"isOptional": false,
|
||||
"isRestParam": false
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"description": "Options to control the behavior of the callback",
|
||||
"type": "AfterRenderOptions",
|
||||
"isOptional": true,
|
||||
"isRestParam": false
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * Register a callback to be invoked the next time the application finishes rendering, during the\n * `mixedReadWrite` phase.\n *\n * <div class=\"alert is-critical\">\n *\n * You should prefer specifying an explicit phase for the callback instead, or you risk significant\n * performance degradation.\n *\n * </div>\n *\n * Note that the callback will run\n * - in the order it was registered\n * - on browser platforms only\n * - during the `mixedReadWrite` phase\n *\n * <div class=\"alert is-important\">\n *\n * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs.\n * You must use caution when directly reading or writing the DOM and layout.\n *\n * </div>\n *\n * @param callback A callback function to register\n * @param options Options to control the behavior of the callback\n *\n * @usageNotes\n *\n * Use `afterNextRender` to read or write the DOM once,\n * for example to initialize a non-Angular library.\n *\n * ### Example\n * ```ts\n * @Component({\n * selector: 'my-chart-cmp',\n * template: `<div #chart>{{ ... }}</div>`,\n * })\n * export class MyChartCmp {\n * @ViewChild('chart') chartRef: ElementRef;\n * chart: MyChart|null;\n *\n * constructor() {\n * afterNextRender({\n * write: () => {\n * this.chart = new MyChart(this.chartRef.nativeElement);\n * }\n * });\n * }\n * }\n * ```\n *\n * @developerPreview\n */",
|
||||
"returnType": "AfterRenderRef"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [
|
||||
{
|
||||
"name": "callbackOrSpec",
|
||||
"description": "",
|
||||
"type": "VoidFunction | { earlyRead?: () => unknown; write?: (r?: unknown) => unknown; mixedReadWrite?: (r?: unknown) => unknown; read?: (r?: unknown) => void; }",
|
||||
"isOptional": false,
|
||||
"isRestParam": false
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"description": "",
|
||||
"type": "AfterRenderOptions",
|
||||
"isOptional": true,
|
||||
"isRestParam": false
|
||||
}
|
||||
],
|
||||
"isNewType": false,
|
||||
"returnType": "AfterRenderRef",
|
||||
"generics": [],
|
||||
"name": "afterNextRender",
|
||||
"description": "",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [],
|
||||
"rawComment": ""
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "",
|
||||
"jsdocTags": [],
|
||||
"rawComment": "",
|
||||
"source": {
|
||||
"fileName": "/packages/core/src/user-profile.ts",
|
||||
"line": 3,
|
||||
"character": 7
|
||||
"filePath": "/packages/core/src/render3/after_render_hooks.ts",
|
||||
"startLine": 442,
|
||||
"endLine": 450
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "provideClientHydration",
|
||||
"signatures": [
|
||||
{
|
||||
"name": "provideClientHydration",
|
||||
"entryType": "function",
|
||||
"description": "Sets up providers necessary to enable hydration functionality for the application.\n\nBy default, the function enables the recommended set of features for the optimal\nperformance for most of the applications. It includes the following features:\n\n* Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n* [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\ntransferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n[here](guide/ssr#caching-data-when-using-httpclient).\n\nThese functions allow you to disable some of the default features or enable new ones:\n\n* {@link withNoHttpTransferCache} to disable HTTP transfer cache\n* {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n* {@link withI18nSupport} to enable hydration support for i18n blocks\n* {@link withEventReplay} to enable support for replaying user events",
|
||||
"generics": [],
|
||||
"isNewType": false,
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "usageNotes",
|
||||
"comment": "Basic example of how you can enable hydration in your application when\n`bootstrapApplication` function is used:\n```\nbootstrapApplication(AppComponent, {\n providers: [provideClientHydration()]\n});\n```\n\nAlternatively if you are using NgModules, you would add `provideClientHydration`\nto your root app module's provider list.\n```\n@NgModule({\n declarations: [RootCmp],\n bootstrap: [RootCmp],\n providers: [provideClientHydration()],\n})\nexport class AppModule {}\n```"
|
||||
},
|
||||
{"name": "see", "comment": "{@link withNoHttpTransferCache}"},
|
||||
{"name": "see", "comment": "{@link withHttpTransferCacheOptions}"},
|
||||
{"name": "see", "comment": "{@link withI18nSupport}"},
|
||||
{"name": "see", "comment": "{@link withEventReplay}"},
|
||||
{
|
||||
"name": "param",
|
||||
"comment": "Optional features to configure additional router behaviors."
|
||||
},
|
||||
{"name": "returns", "comment": "A set of providers to enable hydration."},
|
||||
{"name": "publicApi", "comment": ""}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"name": "features",
|
||||
"description": "Optional features to configure additional router behaviors.",
|
||||
"type": "HydrationFeature<HydrationFeatureKind>[]",
|
||||
"isOptional": false,
|
||||
"isRestParam": true
|
||||
}
|
||||
],
|
||||
"rawComment": "/**\n * Sets up providers necessary to enable hydration functionality for the application.\n *\n * By default, the function enables the recommended set of features for the optimal\n * performance for most of the applications. It includes the following features:\n *\n * * Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n * * [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\n * transferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n * [here](guide/ssr#caching-data-when-using-httpclient).\n *\n * These functions allow you to disable some of the default features or enable new ones:\n *\n * * {@link withNoHttpTransferCache} to disable HTTP transfer cache\n * * {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n * * {@link withI18nSupport} to enable hydration support for i18n blocks\n * * {@link withEventReplay} to enable support for replaying user events\n *\n * @usageNotes\n *\n * Basic example of how you can enable hydration in your application when\n * `bootstrapApplication` function is used:\n * ```\n * bootstrapApplication(AppComponent, {\n * providers: [provideClientHydration()]\n * });\n * ```\n *\n * Alternatively if you are using NgModules, you would add `provideClientHydration`\n * to your root app module's provider list.\n * ```\n * @NgModule({\n * declarations: [RootCmp],\n * bootstrap: [RootCmp],\n * providers: [provideClientHydration()],\n * })\n * export class AppModule {}\n * ```\n *\n * @see {@link withNoHttpTransferCache}\n * @see {@link withHttpTransferCacheOptions}\n * @see {@link withI18nSupport}\n * @see {@link withEventReplay}\n *\n * @param features Optional features to configure additional router behaviors.\n * @returns A set of providers to enable hydration.\n *\n * @publicApi\n */",
|
||||
"returnType": "EnvironmentProviders"
|
||||
}
|
||||
],
|
||||
"implementation": {
|
||||
"params": [
|
||||
{
|
||||
"name": "features",
|
||||
"description": "Optional features to configure additional router behaviors.",
|
||||
"type": "HydrationFeature<HydrationFeatureKind>[]",
|
||||
"isOptional": false,
|
||||
"isRestParam": true
|
||||
}
|
||||
],
|
||||
"isNewType": false,
|
||||
"returnType": "EnvironmentProviders",
|
||||
"returnDescription": "A set of providers to enable hydration.",
|
||||
"generics": [],
|
||||
"name": "provideClientHydration",
|
||||
"description": "Sets up providers necessary to enable hydration functionality for the application.\n\nBy default, the function enables the recommended set of features for the optimal\nperformance for most of the applications. It includes the following features:\n\n* Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n* [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\ntransferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n[here](guide/ssr#caching-data-when-using-httpclient).\n\nThese functions allow you to disable some of the default features or enable new ones:\n\n* {@link withNoHttpTransferCache} to disable HTTP transfer cache\n* {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n* {@link withI18nSupport} to enable hydration support for i18n blocks\n* {@link withEventReplay} to enable support for replaying user events",
|
||||
"entryType": "function",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "usageNotes",
|
||||
"comment": "Basic example of how you can enable hydration in your application when\n`bootstrapApplication` function is used:\n```\nbootstrapApplication(AppComponent, {\n providers: [provideClientHydration()]\n});\n```\n\nAlternatively if you are using NgModules, you would add `provideClientHydration`\nto your root app module's provider list.\n```\n@NgModule({\n declarations: [RootCmp],\n bootstrap: [RootCmp],\n providers: [provideClientHydration()],\n})\nexport class AppModule {}\n```"
|
||||
},
|
||||
{"name": "see", "comment": "{@link withNoHttpTransferCache}"},
|
||||
{"name": "see", "comment": "{@link withHttpTransferCacheOptions}"},
|
||||
{"name": "see", "comment": "{@link withI18nSupport}"},
|
||||
{"name": "see", "comment": "{@link withEventReplay}"},
|
||||
{
|
||||
"name": "param",
|
||||
"comment": "Optional features to configure additional router behaviors."
|
||||
},
|
||||
{"name": "returns", "comment": "A set of providers to enable hydration."},
|
||||
{"name": "publicApi", "comment": ""}
|
||||
],
|
||||
"rawComment": "/**\n * Sets up providers necessary to enable hydration functionality for the application.\n *\n * By default, the function enables the recommended set of features for the optimal\n * performance for most of the applications. It includes the following features:\n *\n * * Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n * * [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\n * transferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n * [here](guide/ssr#caching-data-when-using-httpclient).\n *\n * These functions allow you to disable some of the default features or enable new ones:\n *\n * * {@link withNoHttpTransferCache} to disable HTTP transfer cache\n * * {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n * * {@link withI18nSupport} to enable hydration support for i18n blocks\n * * {@link withEventReplay} to enable support for replaying user events\n *\n * @usageNotes\n *\n * Basic example of how you can enable hydration in your application when\n * `bootstrapApplication` function is used:\n * ```\n * bootstrapApplication(AppComponent, {\n * providers: [provideClientHydration()]\n * });\n * ```\n *\n * Alternatively if you are using NgModules, you would add `provideClientHydration`\n * to your root app module's provider list.\n * ```\n * @NgModule({\n * declarations: [RootCmp],\n * bootstrap: [RootCmp],\n * providers: [provideClientHydration()],\n * })\n * export class AppModule {}\n * ```\n *\n * @see {@link withNoHttpTransferCache}\n * @see {@link withHttpTransferCacheOptions}\n * @see {@link withI18nSupport}\n * @see {@link withEventReplay}\n *\n * @param features Optional features to configure additional router behaviors.\n * @returns A set of providers to enable hydration.\n *\n * @publicApi\n */"
|
||||
},
|
||||
"entryType": "function",
|
||||
"description": "Sets up providers necessary to enable hydration functionality for the application.\n\nBy default, the function enables the recommended set of features for the optimal\nperformance for most of the applications. It includes the following features:\n\n* Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n* [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\ntransferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n[here](guide/ssr#caching-data-when-using-httpclient).\n\nThese functions allow you to disable some of the default features or enable new ones:\n\n* {@link withNoHttpTransferCache} to disable HTTP transfer cache\n* {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n* {@link withI18nSupport} to enable hydration support for i18n blocks\n* {@link withEventReplay} to enable support for replaying user events",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "usageNotes",
|
||||
"comment": "Basic example of how you can enable hydration in your application when\n`bootstrapApplication` function is used:\n```\nbootstrapApplication(AppComponent, {\n providers: [provideClientHydration()]\n});\n```\n\nAlternatively if you are using NgModules, you would add `provideClientHydration`\nto your root app module's provider list.\n```\n@NgModule({\n declarations: [RootCmp],\n bootstrap: [RootCmp],\n providers: [provideClientHydration()],\n})\nexport class AppModule {}\n```"
|
||||
},
|
||||
{"name": "see", "comment": "{@link withNoHttpTransferCache}"},
|
||||
{"name": "see", "comment": "{@link withHttpTransferCacheOptions}"},
|
||||
{"name": "see", "comment": "{@link withI18nSupport}"},
|
||||
{"name": "see", "comment": "{@link withEventReplay}"},
|
||||
{"name": "param", "comment": "Optional features to configure additional router behaviors."},
|
||||
{"name": "returns", "comment": "A set of providers to enable hydration."},
|
||||
{"name": "publicApi", "comment": ""}
|
||||
],
|
||||
"rawComment": "/**\n * Sets up providers necessary to enable hydration functionality for the application.\n *\n * By default, the function enables the recommended set of features for the optimal\n * performance for most of the applications. It includes the following features:\n *\n * * Reconciling DOM hydration. Learn more about it [here](guide/hydration).\n * * [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and\n * transferring this cache to the client to avoid extra HTTP requests. Learn more about data caching\n * [here](guide/ssr#caching-data-when-using-httpclient).\n *\n * These functions allow you to disable some of the default features or enable new ones:\n *\n * * {@link withNoHttpTransferCache} to disable HTTP transfer cache\n * * {@link withHttpTransferCacheOptions} to configure some HTTP transfer cache options\n * * {@link withI18nSupport} to enable hydration support for i18n blocks\n * * {@link withEventReplay} to enable support for replaying user events\n *\n * @usageNotes\n *\n * Basic example of how you can enable hydration in your application when\n * `bootstrapApplication` function is used:\n * ```\n * bootstrapApplication(AppComponent, {\n * providers: [provideClientHydration()]\n * });\n * ```\n *\n * Alternatively if you are using NgModules, you would add `provideClientHydration`\n * to your root app module's provider list.\n * ```\n * @NgModule({\n * declarations: [RootCmp],\n * bootstrap: [RootCmp],\n * providers: [provideClientHydration()],\n * })\n * export class AppModule {}\n * ```\n *\n * @see {@link withNoHttpTransferCache}\n * @see {@link withHttpTransferCacheOptions}\n * @see {@link withI18nSupport}\n * @see {@link withEventReplay}\n *\n * @param features Optional features to configure additional router behaviors.\n * @returns A set of providers to enable hydration.\n *\n * @publicApi\n */",
|
||||
"source": {
|
||||
"filePath": "/packages/platform-browser/src/hydration.ts",
|
||||
"startLine": 201,
|
||||
"endLine": 238
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "AfterRenderOptions",
|
||||
"isAbstract": false,
|
||||
"entryType": "interface",
|
||||
"members": [
|
||||
{
|
||||
"name": "injector",
|
||||
"type": "Injector",
|
||||
"memberType": "property",
|
||||
"memberTags": ["optional"],
|
||||
"description": "The `Injector` to use during creation.\n\nIf this is not provided, the current injection context will be used instead (via `inject`).",
|
||||
"jsdocTags": []
|
||||
},
|
||||
{
|
||||
"name": "phase",
|
||||
"type": "AfterRenderPhase",
|
||||
"memberType": "property",
|
||||
"memberTags": ["optional"],
|
||||
"description": "The phase the callback should be invoked in.\n\n<div class=\"alert is-critical\">\n\nDefaults to `AfterRenderPhase.MixedReadWrite`. You should choose a more specific\nphase instead. See `AfterRenderPhase` for more information.\n\n</div>",
|
||||
"jsdocTags": [
|
||||
{
|
||||
"name": "deprecated",
|
||||
"comment": "Specify the phase for your callback to run in by passing a spec-object as the first\nparameter to `afterRender` or `afterNextRender` instead of a function."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generics": [],
|
||||
"description": "Options passed to `afterRender` and `afterNextRender`.",
|
||||
"jsdocTags": [{"name": "developerPreview", "comment": ""}],
|
||||
"rawComment": "/**\n * Options passed to `afterRender` and `afterNextRender`.\n *\n * @developerPreview\n */",
|
||||
"implements": [],
|
||||
"source": {
|
||||
"filePath": "/packages/core/src/render3/after_render_hooks.ts",
|
||||
"startLine": 103,
|
||||
"endLine": 125
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import {runfiles} from '@bazel/runfiles';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {JSDOM} from 'jsdom';
|
||||
import {renderEntry} from '../rendering';
|
||||
import {getRenderable} from '../processing';
|
||||
import {initHighlighter} from '../shiki/shiki';
|
||||
import {configureMarkedGlobally} from '../marked/configuration';
|
||||
import {setSymbols} from '../symbol-context';
|
||||
|
||||
// Note: The tests will probably break if the schema of the api extraction changes.
|
||||
// All entries in the fake-entries are extracted from Angular's api.
|
||||
// You can just generate them an copy/replace the items in the fake-entries file.
|
||||
|
||||
describe('markdown to html', () => {
|
||||
const entries = new Map<string, DocumentFragment>();
|
||||
const entries2 = new Map<string, string>();
|
||||
|
||||
beforeAll(async () => {
|
||||
await initHighlighter();
|
||||
await configureMarkedGlobally();
|
||||
|
||||
const entryContent = await readFile(runfiles.resolvePackageRelative('fake-entries.json'), {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const entryJson = JSON.parse(entryContent) as any;
|
||||
const symbols = new Map<string, string>([
|
||||
['AfterRenderPhase', 'core'],
|
||||
['afterRender', 'core'],
|
||||
]);
|
||||
setSymbols(symbols);
|
||||
for (const entry of entryJson.entries) {
|
||||
const renderableJson = getRenderable(entry, '@angular/fakeentry');
|
||||
const fragment = JSDOM.fragment(await renderEntry(renderableJson));
|
||||
entries.set(entry['name'], fragment);
|
||||
entries2.set(entry['name'], await renderEntry(renderableJson));
|
||||
}
|
||||
});
|
||||
|
||||
it('should render description correctly', () => {
|
||||
const afterNextRenderEntry = entries.get('afterNextRender')!;
|
||||
const header = afterNextRenderEntry.querySelector('.docs-reference-header')!;
|
||||
expect(header).toBeDefined();
|
||||
expect(header.outerHTML).not.toContain('```');
|
||||
|
||||
console.log(entries2.get('afterNextRender'));
|
||||
const list = afterNextRenderEntry.querySelector('ul')!;
|
||||
expect(list).toBeDefined();
|
||||
|
||||
// List are rendered
|
||||
expect(list.outerHTML).toContain('<li>');
|
||||
|
||||
// Code blocks are rendered
|
||||
expect(list.outerHTML).toContain('<code>mixedReadWrite</code>');
|
||||
});
|
||||
|
||||
it('should render multiple {@link} blocks', () => {
|
||||
const provideClientHydrationEntry = entries.get('provideClientHydration')!;
|
||||
expect(provideClientHydrationEntry).toBeDefined();
|
||||
const cardItem = provideClientHydrationEntry.querySelector('.docs-reference-card-item')!;
|
||||
expect(cardItem.innerHTML).not.toContain('@link');
|
||||
});
|
||||
|
||||
it('should create cross-links', () => {
|
||||
const entry = entries.get('AfterRenderOptions')!;
|
||||
expect(entry).toBeDefined();
|
||||
|
||||
// In the description
|
||||
const descriptionItem = entry.querySelector('.docs-reference-description')!;
|
||||
expect(descriptionItem.innerHTML).toContain('<a href="/api/core/afterRender">afterRender</a>');
|
||||
|
||||
// In the card
|
||||
const cardItem = entry.querySelectorAll('.docs-reference-card-item')[1];
|
||||
expect(cardItem.innerHTML).toContain(
|
||||
'<a href="/api/core/AfterRenderPhase#MixedReadWrite">AfterRenderPhase.MixedReadWrite</a>',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
addHtmlUsageNotes,
|
||||
setEntryFlags,
|
||||
} from './jsdoc-transforms';
|
||||
import {addRenderableGroupMembers} from './member-transforms';
|
||||
import {addRenderableMembers} from './member-transforms';
|
||||
import {addModuleName} from './module-name';
|
||||
|
||||
/** Given an unprocessed class entry, get the fully renderable class entry. */
|
||||
@@ -26,7 +26,7 @@ export function getClassRenderable(
|
||||
): ClassEntryRenderable {
|
||||
return setEntryFlags(
|
||||
addRenderableCodeToc(
|
||||
addRenderableGroupMembers(
|
||||
addRenderableMembers(
|
||||
addHtmlAdditionalLinks(
|
||||
addHtmlUsageNotes(
|
||||
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(classEntry, moduleName))),
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
|
||||
import {
|
||||
DocEntry,
|
||||
FunctionEntry,
|
||||
FunctionSignatureMetadata,
|
||||
MemberEntry,
|
||||
MemberTags,
|
||||
ParameterEntry,
|
||||
PropertyEntry,
|
||||
isFunctionEntryWithOverloads,
|
||||
} from '../entities';
|
||||
|
||||
import {
|
||||
@@ -32,8 +31,10 @@ import {
|
||||
import {CodeLineRenderable} from '../entities/renderables';
|
||||
import {HasModuleName, HasRenderableToc} from '../entities/traits';
|
||||
import {codeToHtml} from '../shiki/shiki';
|
||||
import {getModuleName} from '../symbol-context';
|
||||
|
||||
import {filterLifecycleMethods, mergeGettersAndSetters} from './member-transforms';
|
||||
import {getLinkToModule} from './url-transforms';
|
||||
|
||||
// Allows to generate links for code lines.
|
||||
interface CodeTableOfContentsData {
|
||||
@@ -64,7 +65,9 @@ export function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
|
||||
const metadata = mapDocEntryToCode(entry);
|
||||
appendPrefixAndSuffix(entry, metadata);
|
||||
|
||||
const codeWithSyntaxHighlighting = codeToHtml(metadata.contents, 'typescript');
|
||||
const codeWithSyntaxHighlighting = codeToHtml(metadata.contents, 'typescript', {
|
||||
removeFunctionKeyword: true,
|
||||
});
|
||||
|
||||
// shiki returns the lines wrapped by 2 node : 1 pre node, 1 code node.
|
||||
// As leveraging jsdom isn't trivial here, we rely on a regex to extract the line nodes
|
||||
@@ -77,8 +80,13 @@ export function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
|
||||
const insideCode = match[2];
|
||||
const afterCode = match[3];
|
||||
|
||||
const lines = splitLines(insideCode);
|
||||
const groups = groupCodeLines(lines, metadata);
|
||||
// Note: Don't expect enum value in signatures to be linked correctly
|
||||
// as skihi already splits them into separate span blocks.
|
||||
// Only the enum itself will recieve a link
|
||||
const codeWithLinks = addApiLinksToHtml(insideCode);
|
||||
|
||||
const lines = splitLines(codeWithLinks);
|
||||
const groups = groupCodeLines(lines, metadata, entry);
|
||||
|
||||
return {
|
||||
...entry,
|
||||
@@ -89,11 +97,12 @@ export function addRenderableCodeToc<T extends DocEntry & HasModuleName>(
|
||||
}
|
||||
|
||||
/** Group overloaded methods */
|
||||
function groupCodeLines(lines: string[], metadata: CodeTableOfContentsData) {
|
||||
function groupCodeLines(lines: string[], metadata: CodeTableOfContentsData, entry: DocEntry) {
|
||||
const hasSingleSignature = isFunctionEntry(entry) && entry.signatures.length === 1;
|
||||
return lines.reduce((groups, line, index) => {
|
||||
const tocItem = {
|
||||
const tocItem: CodeLineRenderable = {
|
||||
contents: line,
|
||||
id: metadata.codeLineNumbersWithIdentifiers.get(index),
|
||||
id: hasSingleSignature ? undefined : metadata.codeLineNumbersWithIdentifiers.get(index),
|
||||
isDeprecated: metadata.deprecatedLineNumbers.some((lineNumber) => lineNumber === index),
|
||||
};
|
||||
|
||||
@@ -109,46 +118,52 @@ function groupCodeLines(lines: string[], metadata: CodeTableOfContentsData) {
|
||||
}
|
||||
|
||||
export function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData {
|
||||
const isDeprecated = isDeprecatedEntry(entry);
|
||||
const deprecatedLineNumbers = isDeprecated ? [0] : [];
|
||||
|
||||
if (isClassEntry(entry)) {
|
||||
const members = filterLifecycleMethods(mergeGettersAndSetters(entry.members));
|
||||
return getCodeTocData(members, true);
|
||||
return getCodeTocData(members, true, isDeprecated);
|
||||
}
|
||||
|
||||
if (isConstantEntry(entry)) {
|
||||
const isDeprecated = isDeprecatedEntry(entry);
|
||||
return {
|
||||
contents: `const ${entry.name}: ${entry.type};`,
|
||||
codeLineNumbersWithIdentifiers: new Map(),
|
||||
deprecatedLineNumbers: isDeprecated ? [0] : [],
|
||||
deprecatedLineNumbers,
|
||||
};
|
||||
}
|
||||
|
||||
if (isEnumEntry(entry)) {
|
||||
return getCodeTocData(entry.members, true);
|
||||
return getCodeTocData(entry.members, true, isDeprecated);
|
||||
}
|
||||
|
||||
if (isInterfaceEntry(entry)) {
|
||||
return getCodeTocData(mergeGettersAndSetters(entry.members), true);
|
||||
return getCodeTocData(mergeGettersAndSetters(entry.members), true, isDeprecated);
|
||||
}
|
||||
|
||||
if (isFunctionEntry(entry)) {
|
||||
const isDeprecated = isDeprecatedEntry(entry);
|
||||
const codeLineNumbersWithIdentifiers = new Map<number, string>();
|
||||
const hasSingleSignature = entry.signatures.length === 1;
|
||||
|
||||
if (isFunctionEntryWithOverloads(entry) && entry.overloads) {
|
||||
if (entry.signatures.length > 0) {
|
||||
const initialMetadata: CodeTableOfContentsData = {
|
||||
contents: '',
|
||||
codeLineNumbersWithIdentifiers: new Map<number, string>(),
|
||||
deprecatedLineNumbers: [],
|
||||
deprecatedLineNumbers,
|
||||
};
|
||||
|
||||
return entry.overloads.reduce(
|
||||
(acc: CodeTableOfContentsData, curr: FunctionEntry, index: number) => {
|
||||
return entry.signatures.reduce(
|
||||
(acc: CodeTableOfContentsData, curr: FunctionSignatureMetadata, index: number) => {
|
||||
const lineNumber = index;
|
||||
acc.codeLineNumbersWithIdentifiers.set(lineNumber, curr.name);
|
||||
acc.contents += `${curr.name}(${curr.params
|
||||
.map((param) => mapParamEntry(param))
|
||||
.join(`, `)}): ${curr.returnType}\n`;
|
||||
acc.codeLineNumbersWithIdentifiers.set(lineNumber, `${curr.name}_${index}`);
|
||||
acc.contents += getMethodCodeLine(curr, [], hasSingleSignature, true);
|
||||
|
||||
// We don't want to add line break after the last item
|
||||
if (!hasSingleSignature && index < entry.signatures.length - 1) {
|
||||
acc.contents += '\n';
|
||||
}
|
||||
|
||||
if (isDeprecatedEntry(curr)) {
|
||||
acc.deprecatedLineNumbers.push(lineNumber);
|
||||
}
|
||||
@@ -157,11 +172,12 @@ export function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData {
|
||||
initialMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
// It is important to add the function keyword as shiki will only highlight valid ts
|
||||
contents: `function ${getMethodCodeLine(entry, [], true)}`,
|
||||
contents: `function ${getMethodCodeLine(entry.implementation, [], true)}`,
|
||||
codeLineNumbersWithIdentifiers,
|
||||
deprecatedLineNumbers: isDeprecated ? [0] : [],
|
||||
deprecatedLineNumbers,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -206,16 +222,13 @@ export function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData {
|
||||
return {
|
||||
contents: lines.join('\n'),
|
||||
codeLineNumbersWithIdentifiers,
|
||||
deprecatedLineNumbers: [],
|
||||
deprecatedLineNumbers,
|
||||
};
|
||||
}
|
||||
|
||||
if (isTypeAliasEntry(entry)) {
|
||||
const isDeprecated = isDeprecatedEntry(entry);
|
||||
const contents = `type ${entry.name} = ${entry.type}`;
|
||||
|
||||
let deprecatedLineNumbers = [];
|
||||
|
||||
if (isDeprecated) {
|
||||
const numberOfLinesOfCode = getNumberOfLinesOfCode(contents);
|
||||
|
||||
@@ -234,36 +247,54 @@ export function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData {
|
||||
return {
|
||||
contents: '',
|
||||
codeLineNumbersWithIdentifiers: new Map(),
|
||||
deprecatedLineNumbers: [],
|
||||
deprecatedLineNumbers,
|
||||
};
|
||||
}
|
||||
|
||||
/** Generate code ToC data for list of members. */
|
||||
function getCodeTocData(members: MemberEntry[], hasPrefixLine: boolean): CodeTableOfContentsData {
|
||||
function getCodeTocData(
|
||||
members: MemberEntry[],
|
||||
hasPrefixLine: boolean,
|
||||
isDeprecated: boolean,
|
||||
): CodeTableOfContentsData {
|
||||
const initialMetadata: CodeTableOfContentsData = {
|
||||
contents: '',
|
||||
codeLineNumbersWithIdentifiers: new Map<number, string>(),
|
||||
deprecatedLineNumbers: [],
|
||||
deprecatedLineNumbers: isDeprecated ? [0] : [],
|
||||
};
|
||||
// In case when hasPrefixLine is true we should take it into account when we're generating
|
||||
// `codeLineNumbersWithIdentifiers` below.
|
||||
const skip = !!hasPrefixLine ? 1 : 0;
|
||||
let lineNumber = skip;
|
||||
|
||||
return members.reduce((acc: CodeTableOfContentsData, curr: MemberEntry, index: number) => {
|
||||
const lineNumber = index + skip;
|
||||
acc.codeLineNumbersWithIdentifiers.set(lineNumber, curr.name);
|
||||
acc.contents += ` ${getCodeLine(curr).trim()}\n`;
|
||||
if (isDeprecatedEntry(curr)) {
|
||||
acc.deprecatedLineNumbers.push(lineNumber);
|
||||
const setTocData = (entry: DocEntry | MemberEntry, content: string) => {
|
||||
acc.contents += ` ${content.trim()}\n`;
|
||||
acc.codeLineNumbersWithIdentifiers.set(lineNumber, entry.name);
|
||||
if (isDeprecatedEntry(entry)) {
|
||||
acc.deprecatedLineNumbers.push(lineNumber);
|
||||
}
|
||||
|
||||
lineNumber++;
|
||||
};
|
||||
|
||||
if (isClassMethodEntry(curr)) {
|
||||
if (curr.signatures.length > 0) {
|
||||
curr.signatures.forEach((signature) => {
|
||||
setTocData(signature, getMethodCodeLine(signature, curr.memberTags));
|
||||
});
|
||||
} else {
|
||||
setTocData(curr, getMethodCodeLine(curr.implementation, curr.memberTags));
|
||||
}
|
||||
} else {
|
||||
setTocData(curr, getCodeLine(curr));
|
||||
}
|
||||
return acc;
|
||||
}, initialMetadata);
|
||||
}
|
||||
|
||||
function getCodeLine(member: MemberEntry) {
|
||||
if (isClassMethodEntry(member)) {
|
||||
return getMethodCodeLine(member, member.memberTags);
|
||||
} else if (isGetterEntry(member)) {
|
||||
function getCodeLine(member: MemberEntry): string {
|
||||
if (isGetterEntry(member)) {
|
||||
return getGetterCodeLine(member);
|
||||
} else if (isSetterEntry(member)) {
|
||||
return getSetterCodeLine(member);
|
||||
@@ -281,11 +312,13 @@ function getPropertyCodeLine(member: PropertyEntry): string {
|
||||
|
||||
/** Map method entry to text */
|
||||
function getMethodCodeLine(
|
||||
member: FunctionEntry,
|
||||
member: FunctionSignatureMetadata,
|
||||
memberTags: MemberTags[] = [],
|
||||
displayParamsInNewLines: boolean = false,
|
||||
isFunction: boolean = false,
|
||||
): string {
|
||||
return `${memberTags.join(' ')} ${member.name}(${displayParamsInNewLines ? '\n ' : ''}${member.params
|
||||
displayParamsInNewLines &&= member.params.length > 0;
|
||||
return `${isFunction ? 'function' : ''}${memberTags.join(' ')} ${member.name}(${displayParamsInNewLines ? '\n ' : ''}${member.params
|
||||
.map((param) => mapParamEntry(param))
|
||||
.join(`,${displayParamsInNewLines ? '\n ' : ' '}`)}${
|
||||
displayParamsInNewLines ? '\n' : ''
|
||||
@@ -344,7 +377,7 @@ function getNumberOfLinesOfCode(contents: string): number {
|
||||
/** Prints an initializer function signature into a single line. */
|
||||
export function printInitializerFunctionSignatureLine(
|
||||
name: string,
|
||||
signature: FunctionEntry,
|
||||
signature: FunctionSignatureMetadata,
|
||||
showTypesInSignaturePreview: boolean,
|
||||
): string {
|
||||
let res = name;
|
||||
@@ -381,7 +414,7 @@ export function printInitializerFunctionSignatureLine(
|
||||
res += `: ${signature.returnType}`;
|
||||
}
|
||||
res += ';';
|
||||
return res;
|
||||
return `function ${res}`;
|
||||
}
|
||||
|
||||
function appendPrefixAndSuffix(entry: DocEntry, codeTocData: CodeTableOfContentsData): void {
|
||||
@@ -393,9 +426,28 @@ function appendPrefixAndSuffix(entry: DocEntry, codeTocData: CodeTableOfContents
|
||||
data.contents = `${firstLine}\n${data.contents}${lastLine}`;
|
||||
};
|
||||
|
||||
if (isClassEntry(entry)) {
|
||||
const abstractPrefix = entry.isAbstract ? 'abstract ' : '';
|
||||
appendFirstAndLastLines(codeTocData, `${abstractPrefix}class ${entry.name} {`, `}`);
|
||||
if (isClassEntry(entry) || isInterfaceEntry(entry)) {
|
||||
const generics =
|
||||
entry.generics?.length > 0
|
||||
? `<${entry.generics
|
||||
.map((g) => (g.constraint ? `${g.name} extends ${g.constraint}` : g.name))
|
||||
.join(', ')}>`
|
||||
: '';
|
||||
|
||||
const extendsStr = entry.extends ? ` extends ${entry.extends}` : '';
|
||||
// TODO: remove the ? when we distinguish Class & Decorator entries
|
||||
const implementsStr =
|
||||
entry.implements?.length > 0 ? ` implements ${entry.implements.join(' ,')}` : '';
|
||||
|
||||
const signature = `${entry.name}${generics}${extendsStr}${implementsStr}`;
|
||||
if (isClassEntry(entry)) {
|
||||
const abstractPrefix = entry.isAbstract ? 'abstract ' : '';
|
||||
appendFirstAndLastLines(codeTocData, `${abstractPrefix}class ${signature} {`, `}`);
|
||||
}
|
||||
|
||||
if (isInterfaceEntry(entry)) {
|
||||
appendFirstAndLastLines(codeTocData, `interface ${signature} {`, `}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnumEntry(entry)) {
|
||||
@@ -406,3 +458,31 @@ function appendPrefixAndSuffix(entry: DocEntry, codeTocData: CodeTableOfContents
|
||||
appendFirstAndLastLines(codeTocData, `interface ${entry.name} {`, `}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces any code block that isn't already wrapped by an anchor element
|
||||
* by a link if the symbol is known
|
||||
*/
|
||||
export function addApiLinksToHtml(htmlString: string): string {
|
||||
const result = htmlString.replace(
|
||||
// This regex looks for span/code blocks not wrapped by an anchor block.
|
||||
// Their content are then replaced with a link if the symbol is known
|
||||
// The captured content ==> vvvvvvvv
|
||||
/(?<!<a[^>]*>)(<(?:(?:span)|(?:code))[^>]*>\s*)([^<]*?)(\s*<\/(?:span|code)>)/g,
|
||||
(type: string, span1: string, potentialSymbolName: string, span2: string) => {
|
||||
let [symbol, subSymbol] = potentialSymbolName.split(/(?:#|\.)/) as [string, string?];
|
||||
|
||||
// mySymbol() => mySymbol
|
||||
const symbolWithoutInvocation = symbol.replace(/\([^)]*\);?/g, '');
|
||||
const moduleName = getModuleName(symbolWithoutInvocation)!;
|
||||
|
||||
if (moduleName) {
|
||||
return `${span1}<a href="${getLinkToModule(moduleName, symbol, subSymbol)}">${potentialSymbolName}</a>${span2}`;
|
||||
}
|
||||
|
||||
return type;
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {FunctionEntry, isFunctionEntryWithOverloads} from '../entities';
|
||||
import {FunctionEntryRenderable} from '../entities/renderables';
|
||||
import {HasRenderableOverloads} from '../entities/traits';
|
||||
import {FunctionEntry, FunctionSignatureMetadata} from '../entities';
|
||||
import {
|
||||
FunctionEntryRenderable,
|
||||
FunctionSignatureMetadataRenderable,
|
||||
} from '../entities/renderables';
|
||||
import {addRenderableCodeToc} from './code-transforms';
|
||||
import {
|
||||
addHtmlAdditionalLinks,
|
||||
@@ -27,15 +29,10 @@ export function getFunctionRenderable(
|
||||
): FunctionEntryRenderable {
|
||||
return setEntryFlags(
|
||||
addRenderableCodeToc(
|
||||
addRenderableFunctionParams(
|
||||
addOverloads(
|
||||
moduleName,
|
||||
addHtmlAdditionalLinks(
|
||||
addHtmlUsageNotes(
|
||||
setEntryFlags(
|
||||
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(entry, moduleName))),
|
||||
),
|
||||
),
|
||||
addHtmlAdditionalLinks(
|
||||
addHtmlUsageNotes(
|
||||
setEntryFlags(
|
||||
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(entry, moduleName))),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -43,15 +40,17 @@ export function getFunctionRenderable(
|
||||
);
|
||||
}
|
||||
|
||||
function addOverloads<T extends FunctionEntry>(
|
||||
moduleName: string,
|
||||
entry: T,
|
||||
): T & HasRenderableOverloads {
|
||||
return {
|
||||
...entry,
|
||||
overloads:
|
||||
isFunctionEntryWithOverloads(entry) && entry.overloads
|
||||
? entry.overloads.map((overload) => getFunctionRenderable(overload, moduleName))
|
||||
: null,
|
||||
};
|
||||
export function getFunctionMetadataRenderable(
|
||||
entry: FunctionSignatureMetadata,
|
||||
moduleName: string = '',
|
||||
): FunctionSignatureMetadataRenderable {
|
||||
return addHtmlAdditionalLinks(
|
||||
addRenderableFunctionParams(
|
||||
addHtmlUsageNotes(
|
||||
setEntryFlags(
|
||||
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(entry, moduleName))),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
addHtmlUsageNotes,
|
||||
setEntryFlags,
|
||||
} from './jsdoc-transforms';
|
||||
import {addRenderableGroupMembers} from './member-transforms';
|
||||
import {addRenderableMembers} from './member-transforms';
|
||||
import {addModuleName} from './module-name';
|
||||
|
||||
/** Given an unprocessed interface entry, get the fully renderable interface entry. */
|
||||
@@ -26,7 +26,7 @@ export function getInterfaceRenderable(
|
||||
): InterfaceEntryRenderable {
|
||||
return setEntryFlags(
|
||||
addRenderableCodeToc(
|
||||
addRenderableGroupMembers(
|
||||
addRenderableMembers(
|
||||
addHtmlAdditionalLinks(
|
||||
addHtmlUsageNotes(
|
||||
addHtmlJsDocTagComments(addHtmlDescription(addModuleName(entry, moduleName))),
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
} from '../entities/traits';
|
||||
|
||||
import {getLinkToModule} from './url-transforms';
|
||||
import {addApiLinksToHtml} from './code-transforms';
|
||||
import {getModuleName} from '../symbol-context';
|
||||
|
||||
export const JS_DOC_USAGE_NOTES_TAG = 'usageNotes';
|
||||
export const JS_DOC_SEE_TAG = 'see';
|
||||
@@ -37,6 +39,7 @@ export const JS_DOC_DESCRIPTION_TAG = 'description';
|
||||
|
||||
// Some links are written in the following format: {@link Route}
|
||||
const jsDoclinkRegex = /\{\s*@link\s+([^}]+)\s*\}/;
|
||||
const jsDoclinkRegexGlobal = new RegExp(jsDoclinkRegex.source, 'g');
|
||||
|
||||
/** Given an entity with a description, gets the entity augmented with an `htmlDescription`. */
|
||||
export function addHtmlDescription<T extends HasDescription & HasModuleName>(
|
||||
@@ -100,15 +103,18 @@ export function addHtmlUsageNotes<T extends HasJsDocTags>(entry: T): T & HasHtml
|
||||
) as string)
|
||||
: '';
|
||||
|
||||
const transformedHtml = addApiLinksToHtml(htmlUsageNotes);
|
||||
|
||||
return {
|
||||
...entry,
|
||||
htmlUsageNotes,
|
||||
htmlUsageNotes: transformedHtml,
|
||||
};
|
||||
}
|
||||
|
||||
/** Given a markdown JsDoc text, gets the rendered HTML. */
|
||||
function getHtmlForJsDocText<T extends HasModuleName>(text: string, entry: T): string {
|
||||
return marked.parse(convertLinks(wrapExampleHtmlElementsWithCode(text), entry)) as string;
|
||||
const parsed = marked.parse(convertLinks(wrapExampleHtmlElementsWithCode(text))) as string;
|
||||
return addApiLinksToHtml(parsed);
|
||||
}
|
||||
|
||||
export function setEntryFlags<T extends HasJsDocTags & HasModuleName>(
|
||||
@@ -126,9 +132,7 @@ export function setEntryFlags<T extends HasJsDocTags & HasModuleName>(
|
||||
};
|
||||
}
|
||||
|
||||
function getHtmlAdditionalLinks<T extends HasJsDocTags & HasModuleName>(
|
||||
entry: T,
|
||||
): LinkEntryRenderable[] {
|
||||
function getHtmlAdditionalLinks<T extends HasJsDocTags>(entry: T): LinkEntryRenderable[] {
|
||||
const markdownLinkRule = /\[(.*?)\]\((.*?)(?: "(.*?)")?\)/;
|
||||
|
||||
const seeAlsoLinks = entry.jsdocTags
|
||||
@@ -149,21 +153,8 @@ function getHtmlAdditionalLinks<T extends HasJsDocTags & HasModuleName>(
|
||||
|
||||
if (linkMatch) {
|
||||
const link = linkMatch[1];
|
||||
|
||||
// handling links like {@link Route Some route with description}
|
||||
const [symbol, description] = link.split(/\s(.+)/);
|
||||
if (entry && description) {
|
||||
return {
|
||||
label: description.trim(),
|
||||
url: `${getLinkToModule(entry.moduleName)}/${symbol}`,
|
||||
};
|
||||
}
|
||||
|
||||
// handling links like {@link Route}
|
||||
return {
|
||||
label: linkMatch[1].trim(),
|
||||
url: `${getLinkToModule(entry.moduleName)}/${linkMatch[1].trim()}`,
|
||||
};
|
||||
const {url, label} = parseAtLink(link);
|
||||
return {label, url};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -195,15 +186,38 @@ function convertJsDocExampleToHtmlExample(text: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
function convertLinks(text: string, entry: HasModuleName) {
|
||||
return text.replace(jsDoclinkRegex, (_, link) => {
|
||||
const [symbol, description] = link.split(/\s(.+)/);
|
||||
if (symbol && description) {
|
||||
// {@link Route Some route with description}
|
||||
return `<a href="${getLinkToModule(entry.moduleName)}/${symbol}"><code>${description}</code></a>`;
|
||||
} else {
|
||||
// {@link Route}
|
||||
return `<a href="${getLinkToModule(entry.moduleName)}/${symbol}"><code>${symbol}</code></a>`;
|
||||
}
|
||||
/**
|
||||
* Converts {@link } tags into html anchor elements
|
||||
*/
|
||||
function convertLinks(text: string) {
|
||||
return text.replace(jsDoclinkRegexGlobal, (_, link) => {
|
||||
const {label, url} = parseAtLink(link);
|
||||
|
||||
return `<a href="${url}"><code>${label}</code></a>`;
|
||||
});
|
||||
}
|
||||
|
||||
function parseAtLink(link: string) {
|
||||
// Because of microsoft/TypeScript/issues/59679
|
||||
// getTextOfJSDocComment introduces an extra space between the symbol and a trailing ()
|
||||
link = link.replace(/ \(\)$/, '');
|
||||
|
||||
let [rawSymbol, description] = link.split(/\s(.+)/);
|
||||
let [symbol, subSymbol] = rawSymbol.split(/(?:#|\.)/);
|
||||
|
||||
const moduleName = getModuleName(symbol)!;
|
||||
if (!moduleName) {
|
||||
logWarning(link, symbol);
|
||||
}
|
||||
|
||||
return {
|
||||
label: description ?? rawSymbol,
|
||||
url: getLinkToModule(moduleName, symbol, subSymbol),
|
||||
};
|
||||
}
|
||||
|
||||
function logWarning(link: string, symbol: string) {
|
||||
// TODO: remove the links that generate this error
|
||||
// TODO: throw an error when there are no more warning generated
|
||||
console.warn(`WARNING: {@link ${link}} is invalid, ${symbol} is unknown in this context`);
|
||||
}
|
||||
|
||||
@@ -8,14 +8,7 @@
|
||||
|
||||
import {MemberEntry, MemberTags, MemberType} from '../entities';
|
||||
|
||||
import {isClassMethodEntry} from '../entities/categorization';
|
||||
import {MemberEntryRenderable} from '../entities/renderables';
|
||||
import {
|
||||
HasMembers,
|
||||
HasModuleName,
|
||||
HasRenderableMembers,
|
||||
HasRenderableMembersGroups,
|
||||
} from '../entities/traits';
|
||||
import {HasMembers, HasModuleName, HasRenderableMembers} from '../entities/traits';
|
||||
|
||||
import {
|
||||
addHtmlDescription,
|
||||
@@ -69,44 +62,13 @@ export function mergeGettersAndSetters(members: MemberEntry[]): MemberEntry[] {
|
||||
);
|
||||
}
|
||||
|
||||
/** Given an entity with members, gets the entity augmented with renderable members. */
|
||||
export function addRenderableGroupMembers<T extends HasMembers & HasModuleName>(
|
||||
entry: T,
|
||||
): T & HasRenderableMembersGroups {
|
||||
const members = filterLifecycleMethods(entry.members);
|
||||
|
||||
const membersGroups = members.reduce((groups, item) => {
|
||||
const member = setEntryFlags(
|
||||
addMethodParamsDescription(
|
||||
addHtmlDescription(
|
||||
addHtmlUsageNotes(addHtmlJsDocTagComments(addModuleName(item, entry.moduleName))),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (groups.has(member.name)) {
|
||||
const group = groups.get(member.name);
|
||||
group?.push(member);
|
||||
} else {
|
||||
groups.set(member.name, [member]);
|
||||
}
|
||||
return groups;
|
||||
}, new Map<string, MemberEntryRenderable[]>());
|
||||
|
||||
return {
|
||||
...entry,
|
||||
membersGroups,
|
||||
};
|
||||
}
|
||||
|
||||
export function addRenderableMembers<T extends HasMembers & HasModuleName>(
|
||||
entry: T,
|
||||
): T & HasRenderableMembers {
|
||||
const members = entry.members.map((member) =>
|
||||
setEntryFlags(
|
||||
addMethodParamsDescription(
|
||||
addHtmlDescription(
|
||||
addHtmlUsageNotes(addHtmlJsDocTagComments(addModuleName(member, entry.moduleName))),
|
||||
),
|
||||
addHtmlDescription(
|
||||
addHtmlUsageNotes(addHtmlJsDocTagComments(addModuleName(member, entry.moduleName))),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -116,15 +78,3 @@ export function addRenderableMembers<T extends HasMembers & HasModuleName>(
|
||||
members,
|
||||
};
|
||||
}
|
||||
|
||||
function addMethodParamsDescription<T extends MemberEntry & HasModuleName>(entry: T): T {
|
||||
if (isClassMethodEntry(entry)) {
|
||||
return {
|
||||
...entry,
|
||||
params: entry.params.map((param) =>
|
||||
addHtmlDescription(addModuleName(param, entry.moduleName)),
|
||||
),
|
||||
};
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -9,12 +9,8 @@
|
||||
export const API_PREFIX = 'api';
|
||||
export const MODULE_NAME_PREFIX = '@angular/';
|
||||
|
||||
export function removeAngularPrefixFromModule(moduleName: string): string {
|
||||
return moduleName.replace(MODULE_NAME_PREFIX, '');
|
||||
}
|
||||
|
||||
export function getLinkToModule(moduleName: string) {
|
||||
return `${API_PREFIX}/${removeAngularPrefixFromModule(moduleName)}`;
|
||||
export function getLinkToModule(moduleName: string, symbol: string, subSymbol?: string) {
|
||||
return `${API_PREFIX}/${moduleName}/${symbol}${subSymbol ? `#${subSymbol}` : ''}`;
|
||||
}
|
||||
|
||||
export const normalizePath = (path: string): string => {
|
||||
|
||||
@@ -18,8 +18,8 @@ export interface DocsCodeToken extends CodeToken {
|
||||
|
||||
// Capture group 1: all attributes on the opening tag
|
||||
// Capture group 2: all content between the open and close tags
|
||||
const singleFileSelfClosingCodeRule = /^\s*<docs-code\s([^>]*)((?:.(?!\/>))*)\/>/s;
|
||||
const singleFileCodeRule = /^\s*<docs-code\s([^>]*)>((?:.(?!\/docs-code))*)<\/docs-code>/s;
|
||||
const singleFileCodeRule =
|
||||
/^\s*<docs-code((?:\s+[\w-]+(?:="[^"]*"|='[^']*'|=[^\s>]*)?)*)\s*(?:\/>|>(.*?)<\/docs-code>)/s;
|
||||
|
||||
const pathRule = /path="([^"]*)"/;
|
||||
const headerRule = /header="([^"]*)"/;
|
||||
@@ -38,10 +38,7 @@ export const docsCodeExtension = {
|
||||
return src.match(/^<docs-code\s/)?.index;
|
||||
},
|
||||
tokenizer(this: TokenizerThis, src: string): DocsCodeToken | undefined {
|
||||
const code = singleFileCodeRule.exec(src);
|
||||
const selfClosingCode = singleFileSelfClosingCodeRule.exec(src);
|
||||
const match = selfClosingCode ?? code;
|
||||
|
||||
const match = singleFileCodeRule.exec(src);
|
||||
if (match) {
|
||||
const attr = match[1].trim();
|
||||
|
||||
@@ -55,7 +52,7 @@ export const docsCodeExtension = {
|
||||
const visibleRegion = visibleRegionRule.exec(attr);
|
||||
const preview = previewRule.exec(attr) ? true : false;
|
||||
|
||||
let code = match[2].trim();
|
||||
let code = match[2]?.trim() ?? '';
|
||||
if (path && path[1]) {
|
||||
code = loadWorkspaceRelativeFile(path[1]);
|
||||
// Remove ESLint Comments
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {Token, Tokens, RendererThis, TokenizerThis} from 'marked';
|
||||
import {headingRender} from '../../tranformations/heading';
|
||||
import {formatHeading, headingRender} from '../../tranformations/heading';
|
||||
|
||||
interface DocsStepToken extends Tokens.Generic {
|
||||
type: 'docs-step';
|
||||
@@ -51,7 +51,7 @@ export const docsStepExtension = {
|
||||
return `
|
||||
<li>
|
||||
<span class="docs-step-number" aria-hidden="true"></span>
|
||||
${headingRender(token.title, 3, token.title)}
|
||||
${formatHeading({text: token.title, depth: 3})}
|
||||
${this.parser.parse(token.tokens)}
|
||||
</li>
|
||||
`;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import {marked} from 'marked';
|
||||
import {hooks} from './hooks';
|
||||
import {renderer} from './renderer';
|
||||
import {Renderer} from './renderer';
|
||||
import {docsAlertExtension} from './extensions/docs-alert';
|
||||
import {docsCalloutExtension} from './extensions/docs-callout';
|
||||
import {docsPillExtension} from './extensions/docs-pill/docs-pill';
|
||||
@@ -33,7 +33,6 @@ export async function parseMarkdown(
|
||||
|
||||
marked.use({
|
||||
hooks,
|
||||
renderer,
|
||||
extensions: [
|
||||
docsAlertExtension,
|
||||
docsCalloutExtension,
|
||||
@@ -55,5 +54,5 @@ export async function parseMarkdown(
|
||||
async: true,
|
||||
});
|
||||
|
||||
return marked.parse(markdownContent);
|
||||
return marked.parse(markdownContent, {renderer: new Renderer()});
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import {RendererObject} from 'marked';
|
||||
import {Renderer as _Renderer} from 'marked';
|
||||
import {linkRender} from './tranformations/link';
|
||||
import {tableRender} from './tranformations/table';
|
||||
import {listRender} from './tranformations/list';
|
||||
import {imageRender} from './tranformations/image';
|
||||
import {textRender} from './tranformations/text';
|
||||
import {headingRender} from './tranformations/heading';
|
||||
|
||||
/**
|
||||
* Custom renderer for marked that will be used to transform markdown files to HTML
|
||||
* files that can be used in the Angular docs.
|
||||
*/
|
||||
export const renderer: RendererObject = {
|
||||
link: linkRender,
|
||||
table: tableRender,
|
||||
list: listRender,
|
||||
image: imageRender,
|
||||
text: textRender,
|
||||
heading: headingRender,
|
||||
};
|
||||
export class Renderer extends _Renderer {
|
||||
override link = linkRender;
|
||||
override table = tableRender;
|
||||
override list = listRender;
|
||||
override image = imageRender;
|
||||
override text = textRender;
|
||||
override heading = headingRender;
|
||||
}
|
||||
|
||||
@@ -8,3 +8,5 @@ this is code
|
||||
|
||||
<docs-code path="adev/shared-docs/pipeline/guides/testing/docs-code/new-code.ts"
|
||||
diff="adev/shared-docs/pipeline/guides/testing/docs-code/old-code.ts" />
|
||||
|
||||
<docs-code header="src/locale/messages.fr.xlf (<trans-unit>)" path="docs/pipeline/guides/testing/docs-code/messages.fr.xlf.html" />
|
||||
|
||||
@@ -52,4 +52,9 @@ describe('markdown to html', () => {
|
||||
expect(codeLines[2].classList.contains('add')).toBeFalse();
|
||||
expect(codeLines[2].classList.contains('remove')).toBeFalse();
|
||||
});
|
||||
|
||||
it('should load header and html code', () => {
|
||||
const codeBlock = markdownDocument.querySelectorAll('code')[4];
|
||||
expect(codeBlock).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<!-- The `messages.fr.xlf` after translation for documentation purposes -->
|
||||
<!-- #docregion -->
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<!-- #docregion translated-hello-before -->
|
||||
<trans-unit id="introductionHeader" datatype="html">
|
||||
<source>Hello i18n!</source>
|
||||
<note priority="1" from="description">An introduction header for this sample</note>
|
||||
<note priority="1" from="meaning">User welcome</note>
|
||||
</trans-unit>
|
||||
<!-- #enddocregion translated-hello-before -->
|
||||
<!-- #docregion translated-hello -->
|
||||
<!-- #docregion custom-id -->
|
||||
<trans-unit id="introductionHeader" datatype="html">
|
||||
<!-- #enddocregion custom-id -->
|
||||
<source>Hello i18n!</source>
|
||||
<target>Bonjour i18n !</target>
|
||||
<note priority="1" from="description">An introduction header for this sample</note>
|
||||
<note priority="1" from="meaning">User welcome</note>
|
||||
</trans-unit>
|
||||
<!-- #enddocregion translated-hello -->
|
||||
<!-- #docregion translated-other-nodes -->
|
||||
<!-- #docregion generated-id -->
|
||||
<trans-unit id="ba0cc104d3d69bf669f97b8d96a4c5d8d9559aa3" datatype="html">
|
||||
<!-- #enddocregion generated-id -->
|
||||
<source>I don't output any element</source>
|
||||
<target>Je n'affiche aucun élément</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="701174153757adf13e7c24a248c8a873ac9f5193" datatype="html">
|
||||
<source>Angular logo</source>
|
||||
<target>Logo d'Angular</target>
|
||||
</trans-unit>
|
||||
<!-- #enddocregion translated-other-nodes -->
|
||||
<!-- #docregion translated-plural -->
|
||||
<trans-unit id="5a134dee893586d02bffc9611056b9cadf9abfad" datatype="html">
|
||||
<source>{VAR_PLURAL, plural, =0 {just now} =1 {one minute ago} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes ago} }</source>
|
||||
<target>{VAR_PLURAL, plural, =0 {à l'instant} =1 {il y a une minute} other {il y a <x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes} }</target>
|
||||
</trans-unit>
|
||||
<!-- #enddocregion translated-plural -->
|
||||
<!-- #docregion translated-select -->
|
||||
<!-- #docregion translate-select-1 -->
|
||||
<trans-unit id="f99f34ac9bd4606345071bd813858dec29f3b7d1" datatype="html">
|
||||
<source>The author is <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></source>
|
||||
<target>L'auteur est <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></target>
|
||||
</trans-unit>
|
||||
<!-- #enddocregion translate-select-1 -->
|
||||
<!-- #docregion translate-select-2 -->
|
||||
<trans-unit id="eff74b75ab7364b6fa888f1cbfae901aaaf02295" datatype="html">
|
||||
<source>{VAR_SELECT, select, male {male} female {female} other {other} }</source>
|
||||
<target>{VAR_SELECT, select, male {un homme} female {une femme} other {autre} }</target>
|
||||
</trans-unit>
|
||||
<!-- #enddocregion translate-select-2 -->
|
||||
<!-- #enddocregion translated-select -->
|
||||
<!-- #docregion translate-nested -->
|
||||
<!-- #docregion translate-nested-1 -->
|
||||
<trans-unit id="972cb0cf3e442f7b1c00d7dab168ac08d6bdf20c" datatype="html">
|
||||
<source>Updated: <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></source>
|
||||
<target>Mis à jour: <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></target>
|
||||
</trans-unit>
|
||||
<!-- #enddocregion translate-nested-1 -->
|
||||
<!-- #docregion translate-nested-2 -->
|
||||
<trans-unit id="7151c2e67748b726f0864fc443861d45df21d706" datatype="html">
|
||||
<source>{VAR_PLURAL, plural, =0 {just now} =1 {one minute ago} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes ago by {VAR_SELECT, select, male {male} female {female} other {other} }} }</source>
|
||||
<target>{VAR_PLURAL, plural, =0 {à l'instant} =1 {il y a une minute} other {il y a <x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes par {VAR_SELECT, select, male {un homme} female {une femme} other {autre} }} }</target>
|
||||
</trans-unit>
|
||||
<!-- #enddocregion translate-nested-2 -->
|
||||
<!-- #enddocregion translate-nested -->
|
||||
<!-- #docregion i18n-duplicate-custom-id -->
|
||||
<trans-unit id="myId" datatype="html">
|
||||
<source>Hello</source>
|
||||
<target state="new">Bonjour</target>
|
||||
</trans-unit>
|
||||
<!-- #enddocregion i18n-duplicate-custom-id -->
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -9,4 +9,5 @@
|
||||
## Duplicate Anchor
|
||||
## `myClass.myMethod` is the best
|
||||
## ステップ 2 - アプリケーションのレイアウトに新しいコンポーネントを追加
|
||||
## My heading {# my-custom-id }
|
||||
## My heading {# my-custom-id }
|
||||
## Query for the `<h1>`
|
||||
@@ -83,4 +83,15 @@ describe('markdown to html', () => {
|
||||
expect(h2HeaderId).toBe('my-custom-id');
|
||||
expect(h2AnchorHref).toBe(`#${h2HeaderId}`);
|
||||
});
|
||||
|
||||
it('should be able to parse heading with a valid tag in a code block', () => {
|
||||
const h2List = markdownDocument.querySelectorAll('h2');
|
||||
const h2 = h2List[6];
|
||||
|
||||
// The anchor element should be to only child
|
||||
expect(h2.children.length).toBe(1);
|
||||
expect(h2.firstElementChild?.tagName).toBe('A');
|
||||
|
||||
expect(h2.firstElementChild!.innerHTML).toBe('Query for the <code><h1></code>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,4 +7,6 @@
|
||||
- Order
|
||||
- here
|
||||
- matter
|
||||
- doesn't
|
||||
- doesn't
|
||||
- [some link](https://angular.dev)
|
||||
- Code block `SomeClass`
|
||||
@@ -21,7 +21,16 @@ describe('markdown to html', () => {
|
||||
|
||||
const unorderedList = markdownDocument.querySelector('ul');
|
||||
expect(unorderedList?.className).toBe('docs-list');
|
||||
expect(unorderedList?.childElementCount).toBe(4);
|
||||
expect(unorderedList?.childElementCount).toBe(6);
|
||||
expect(unorderedList?.textContent).toContain('matter');
|
||||
});
|
||||
|
||||
it('should render list items', () => {
|
||||
const unorderedList = markdownDocument.querySelector('ul');
|
||||
const linkItem = unorderedList!.children[4];
|
||||
expect(linkItem.outerHTML).toContain('href="https://angular.dev"');
|
||||
|
||||
const codeItem = unorderedList!.children[5];
|
||||
expect(codeItem.outerHTML).toContain('<code>SomeClass</code>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,13 +6,18 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {RendererApi} from 'marked';
|
||||
import {Renderer, Tokens} from 'marked';
|
||||
|
||||
import {getHeaderId} from '../state';
|
||||
import {getPageTitle} from '../utils';
|
||||
|
||||
export const headingRender: RendererApi['heading'] = (text, level, raw) => {
|
||||
if (level === 1) {
|
||||
export function headingRender(this: Renderer, {depth, tokens}: Tokens.Heading): string {
|
||||
const text = this?.parser.parseInline(tokens);
|
||||
return formatHeading({text, depth});
|
||||
}
|
||||
|
||||
export function formatHeading({text, depth}: {text: string; depth: number}): string {
|
||||
if (depth === 1) {
|
||||
return `
|
||||
<header class="docs-header">
|
||||
<docs-breadcrumb></docs-breadcrumb>
|
||||
@@ -36,8 +41,8 @@ export const headingRender: RendererApi['heading'] = (text, level, raw) => {
|
||||
const label = anchorLessText.replace(/`(.*?)`/g, '<code>$1</code>').replace(customIdRegex, '');
|
||||
|
||||
return `
|
||||
<h${level} id="${link}">
|
||||
<h${depth} id="${link}">
|
||||
<a href="#${link}" class="docs-anchor" tabindex="-1" aria-label="Link to ${label}">${label}</a>
|
||||
</h${level}>
|
||||
</h${depth}>
|
||||
`;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,15 +7,16 @@
|
||||
*/
|
||||
|
||||
import {normalize} from 'path';
|
||||
import {RendererApi} from 'marked';
|
||||
|
||||
import {Renderer, Tokens} from 'marked';
|
||||
|
||||
// TODO(josephperrott): Determine how we can define/know the image content base path.
|
||||
const imageContentBasePath = 'unknown';
|
||||
|
||||
export const imageRender: RendererApi['image'] = (href, title, text) => {
|
||||
export function imageRender(this: Renderer, {href, title, text}: Tokens.Image) {
|
||||
const isRelativeSrc = href?.startsWith('./');
|
||||
const src = isRelativeSrc ? `${imageContentBasePath}/${normalize(href)}` : href;
|
||||
return `
|
||||
<img src="${src}" alt="${text}" title="${title}" class="docs-image">
|
||||
`;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
*/
|
||||
|
||||
import {anchorTarget} from '../helpers';
|
||||
import {RendererApi} from 'marked';
|
||||
import {Renderer, Tokens} from 'marked';
|
||||
|
||||
export const linkRender: RendererApi['link'] = (href, title, text) => {
|
||||
export function linkRender(this: Renderer, {href, title, tokens}: Tokens.Link) {
|
||||
const titleAttribute = title ? ` title=${title}` : '';
|
||||
return `<a href="${href}"${titleAttribute}${anchorTarget(href)}>${text}</a>`;
|
||||
};
|
||||
return `<a href="${href}"${titleAttribute}${anchorTarget(href)}>${this.parser.parseInline(tokens)}</a>`;
|
||||
}
|
||||
|
||||
@@ -6,19 +6,19 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {RendererApi} from 'marked';
|
||||
import {Renderer, Tokens} from 'marked';
|
||||
|
||||
export const listRender: RendererApi['list'] = (body, ordered, start) => {
|
||||
export function listRender(this: Renderer, {items, ordered}: Tokens.List) {
|
||||
if (ordered) {
|
||||
return `
|
||||
<ol class="docs-ordered-list">
|
||||
${body}
|
||||
${items.map((item) => this.listitem(item)).join('')}
|
||||
</ol>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<ul class="docs-list">
|
||||
${body}
|
||||
${items.map((item) => this.listitem(item)).join('')}
|
||||
</ul>
|
||||
`;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,19 +6,27 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {RendererApi} from 'marked';
|
||||
import {Renderer, Tokens} from 'marked';
|
||||
|
||||
export const tableRender: RendererApi['table'] = (header, body) => {
|
||||
export function tableRender(this: Renderer, {header, rows}: Tokens.Table) {
|
||||
return `
|
||||
<div class="docs-table docs-scroll-track-transparent">
|
||||
<table>
|
||||
<thead>
|
||||
${header}
|
||||
${this.tablerow({
|
||||
text: header.map((cell) => this.tablecell(cell)).join(''),
|
||||
})}
|
||||
</thead>
|
||||
<tbody>
|
||||
${body}
|
||||
${rows
|
||||
.map((row) =>
|
||||
this.tablerow({
|
||||
text: row.map((cell) => this.tablecell(cell)).join(''),
|
||||
}),
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {RendererApi} from 'marked';
|
||||
import {Renderer, Tokens} from 'marked';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
/** Regex to find unicode emojis. */
|
||||
@@ -15,8 +15,9 @@ const UNICODE_EMOJI_REGEX = /&#x[\dA-Fa-f]+;/g;
|
||||
/** Regex to find emojis. */
|
||||
const regex = emojiRegex();
|
||||
|
||||
export const textRender: RendererApi['text'] = (text) => {
|
||||
return regex.test(text) || UNICODE_EMOJI_REGEX.test(text)
|
||||
export function textRender(this: Renderer, token: Tokens.Text) {
|
||||
const text = token.tokens ? this.parser.parseInline(token.tokens) : token.text;
|
||||
return regex.test(token.text) || UNICODE_EMOJI_REGEX.test(token.text)
|
||||
? `<span class="docs-emoji">${text}</span>`
|
||||
: text;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,6 +43,32 @@ export class Search {
|
||||
? from(
|
||||
this.index.search(query, {
|
||||
maxValuesPerFacet: MAX_VALUE_PER_FACET,
|
||||
attributesToRetrieve: [
|
||||
'hierarchy.lvl0',
|
||||
'hierarchy.lvl1',
|
||||
'hierarchy.lvl2',
|
||||
'hierarchy.lvl3',
|
||||
'hierarchy.lvl4',
|
||||
'hierarchy.lvl5',
|
||||
'hierarchy.lvl6',
|
||||
'content',
|
||||
'type',
|
||||
'url',
|
||||
],
|
||||
hitsPerPage: 20,
|
||||
snippetEllipsisText: '…',
|
||||
highlightPreTag: '<ɵ>',
|
||||
highlightPostTag: '</ɵ>',
|
||||
attributesToHighlight: [],
|
||||
attributesToSnippet: [
|
||||
'hierarchy.lvl1:10',
|
||||
'hierarchy.lvl2:10',
|
||||
'hierarchy.lvl3:10',
|
||||
'hierarchy.lvl4:10',
|
||||
'hierarchy.lvl5:10',
|
||||
'hierarchy.lvl6:10',
|
||||
'content:10',
|
||||
],
|
||||
}),
|
||||
)
|
||||
: of(undefined);
|
||||
@@ -72,6 +98,20 @@ export class Search {
|
||||
const uniqueUrls = new Set<string>();
|
||||
|
||||
return items.filter((item) => {
|
||||
if (item.type === 'content' && !item._snippetResult.content) {
|
||||
return false;
|
||||
}
|
||||
// Ensure that this result actually matched on the type.
|
||||
// If not, this is going to be a duplicate. There should be another result in
|
||||
// the list that already matched on its type.
|
||||
// A lvl2 match will also return all its lvl3 results as well, even if those
|
||||
// values don't also match the query.
|
||||
if (
|
||||
item.type.indexOf('lvl') === 0 &&
|
||||
item._snippetResult.hierarchy?.[item.type as 'lvl1']?.matchLevel === 'none'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (item.url && !uniqueUrls.has(item.url)) {
|
||||
uniqueUrls.add(item.url);
|
||||
return true;
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
afterNextRender,
|
||||
inject,
|
||||
signal,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import {RESIZE_EVENT_DELAY} from '../constants/index';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
@@ -34,7 +33,6 @@ export class TableOfContentsScrollSpy {
|
||||
private readonly tableOfContentsLoader = inject(TableOfContentsLoader);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly window = inject(WINDOW);
|
||||
private readonly ngZone = inject(NgZone);
|
||||
private readonly viewportScroller = inject(ViewportScroller);
|
||||
private readonly injector = inject(EnvironmentInjector);
|
||||
private contentSourceElement: HTMLElement | null = null;
|
||||
@@ -112,9 +110,7 @@ export class TableOfContentsScrollSpy {
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
);
|
||||
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
scroll$.subscribe(() => this.setActiveItemId());
|
||||
});
|
||||
scroll$.subscribe(() => this.setActiveItemId());
|
||||
}
|
||||
|
||||
private setActiveItemId(): void {
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
p > a,
|
||||
td > a,
|
||||
div > a:not(.docs-card),
|
||||
code > a,
|
||||
li:not(.docs-faceted-list *) a {
|
||||
color: var(--bright-blue);
|
||||
&:hover {
|
||||
|
||||
@@ -90,21 +90,20 @@ $theme: mat.m2-define-light-theme(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.docs-dark-mode .shiki {
|
||||
color: var(--shiki-dark) ;
|
||||
background-color: var(--shiki-dark-bg) ;
|
||||
color: var(--shiki-dark);
|
||||
background-color: var(--shiki-dark-bg);
|
||||
|
||||
span {
|
||||
color: var(--shiki-dark) ;
|
||||
background-color: var(--shiki-dark-bg) ;
|
||||
color: var(--shiki-dark);
|
||||
background-color: var(--shiki-dark-bg);
|
||||
/* Optional, if you also want font styles */
|
||||
font-style: var(--shiki-dark-font-style) ;
|
||||
font-weight: var(--shiki-dark-font-weight) ;
|
||||
text-decoration: var(--shiki-dark-text-decoration) ;
|
||||
font-style: var(--shiki-dark-font-style);
|
||||
font-weight: var(--shiki-dark-font-weight);
|
||||
}
|
||||
|
||||
.shiki-ln-line-highlighted, button:hover {
|
||||
.shiki-ln-line-highlighted,
|
||||
button:hover {
|
||||
span {
|
||||
background-color: inherit;
|
||||
}
|
||||
@@ -113,24 +112,36 @@ $theme: mat.m2-define-light-theme(
|
||||
|
||||
.shiki {
|
||||
padding-block: 1rem;
|
||||
|
||||
&.cli {
|
||||
padding-inline-start: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.docs-light-mode .shiki {
|
||||
color: var(--shiki-light);
|
||||
background-color: var(--shiki-light-bg) ;
|
||||
background-color: var(--shiki-light-bg);
|
||||
|
||||
span {
|
||||
color: var(--shiki-light) ;
|
||||
background-color: var(--shiki-light-bg) ;
|
||||
color: var(--shiki-light);
|
||||
background-color: var(--shiki-light-bg);
|
||||
/* Optional, if you also want font styles */
|
||||
font-style: var(--shiki-light-font-style) ;
|
||||
font-weight: var(--shiki-light-font-weight) ;
|
||||
text-decoration: var(--shiki-light-text-decoration) ;
|
||||
font-style: var(--shiki-light-font-style);
|
||||
font-weight: var(--shiki-light-font-weight);
|
||||
text-decoration: var(--shiki-light-text-decoration);
|
||||
}
|
||||
|
||||
.shiki-ln-line-highlighted, button:hover {
|
||||
.shiki-ln-line-highlighted,
|
||||
button:hover {
|
||||
span {
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,13 @@ export class AppScroller {
|
||||
this._lastScrollEvent = e;
|
||||
}),
|
||||
filter(() => !this.disableScrolling),
|
||||
filter(() => {
|
||||
const info = this.router.lastSuccessfulNavigation?.extras.info as Record<
|
||||
'disableScrolling',
|
||||
boolean
|
||||
>;
|
||||
return !info?.['disableScrolling'];
|
||||
}),
|
||||
switchMap((e) => {
|
||||
return firstValueFrom(
|
||||
this.appRef.isStable.pipe(
|
||||
@@ -64,7 +71,7 @@ export class AppScroller {
|
||||
const {anchor, position} = this._lastScrollEvent;
|
||||
|
||||
// Don't scroll during rendering
|
||||
this.cancelScroll = afterNextRender(
|
||||
const ref = afterNextRender(
|
||||
{
|
||||
write: () => {
|
||||
if (position) {
|
||||
@@ -77,6 +84,9 @@ export class AppScroller {
|
||||
},
|
||||
},
|
||||
{injector: this.injector},
|
||||
).destroy;
|
||||
);
|
||||
this.cancelScroll = () => {
|
||||
ref.destroy();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@defer (when isBrowser) {
|
||||
<adev-progress-bar />
|
||||
<!-- <docs-top-level-banner id="ng-survey-2024" link="https://goo.gle/angular-v18" text="Take the Angular Developer Survey today!" /> -->
|
||||
}
|
||||
<button (click)="focusFirstHeading()" class="adev-skip">Skip to main content</button>
|
||||
|
||||
|
||||
@@ -8,6 +8,38 @@
|
||||
align-items: flex-start;
|
||||
min-height: 100vh;
|
||||
|
||||
// Display top level banner below the navigation.
|
||||
docs-top-level-banner {
|
||||
@include mq.for-tablet {
|
||||
top: 4.6875rem;
|
||||
}
|
||||
|
||||
@include mq.for-phone-only {
|
||||
top: 3.75rem;
|
||||
transform: translateY(0);
|
||||
transition: transform 0.3s ease-out 0.6s;
|
||||
}
|
||||
}
|
||||
|
||||
// Case: If secondary navigation exists, display banner below secondary navigation for tablets.
|
||||
&:has(adev-secondary-navigation) {
|
||||
docs-top-level-banner {
|
||||
@include mq.for-tablet {
|
||||
top: 8.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case: If primary navigation is opened, display banner at the top of the page.
|
||||
&:has(.adev-nav-primary--open) {
|
||||
docs-top-level-banner {
|
||||
@include mq.for-phone-only {
|
||||
transform: translateY(-3.75rem);
|
||||
transition: transform 0.3s ease-in;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mq.for-tablet-landscape-down {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
getActivatedRouteSnapshotFromRouter,
|
||||
IS_SEARCH_DIALOG_OPEN,
|
||||
SearchDialog,
|
||||
TopLevelBannerComponent,
|
||||
} from '@angular/docs';
|
||||
import {Footer} from './core/layout/footer/footer.component';
|
||||
import {Navigation} from './core/layout/navigation/navigation.component';
|
||||
@@ -46,6 +47,7 @@ import {HeaderService} from './core/services/header.service';
|
||||
RouterLink,
|
||||
SearchDialog,
|
||||
ProgressBarComponent,
|
||||
TopLevelBannerComponent,
|
||||
],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {isPlatformBrowser} from '@angular/common';
|
||||
import {NgProgressComponent} from 'ngx-progressbar';
|
||||
import {NgProgressbar} from 'ngx-progressbar';
|
||||
import {
|
||||
NavigationCancel,
|
||||
NavigationEnd,
|
||||
@@ -32,7 +32,7 @@ export const PROGRESS_BAR_DELAY = 30;
|
||||
@Component({
|
||||
selector: 'adev-progress-bar',
|
||||
standalone: true,
|
||||
imports: [NgProgressComponent],
|
||||
imports: [NgProgressbar],
|
||||
template: `
|
||||
<ng-progress aria-label="Page load progress" />
|
||||
`,
|
||||
@@ -41,7 +41,7 @@ export const PROGRESS_BAR_DELAY = 30;
|
||||
export class ProgressBarComponent implements OnInit {
|
||||
private readonly router = inject(Router);
|
||||
|
||||
@ViewChild(NgProgressComponent, {static: true}) progressBar!: NgProgressComponent;
|
||||
@ViewChild(NgProgressbar, {static: true}) progressBar!: NgProgressbar;
|
||||
|
||||
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import {HttpClient} from '@angular/common/http';
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {DocContent, DocsContentLoader} from '@angular/docs';
|
||||
import {Router} from '@angular/router';
|
||||
import {firstValueFrom} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {firstValueFrom, of} from 'rxjs';
|
||||
import {catchError, map} from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class ContentLoader implements DocsContentLoader {
|
||||
|
||||
+15
-5
@@ -3,14 +3,26 @@
|
||||
@if (group.isFeatured) {
|
||||
<docs-icon aria-hidden>star</docs-icon>
|
||||
}
|
||||
<a routerLink="/api" [fragment]="group.id" class="adev-api-anchor" tabindex="-1">{{ group.title }}</a>
|
||||
<!-- we use innerHtml because the title can be an html string-->
|
||||
<a
|
||||
routerLink="/api"
|
||||
[fragment]="group.id"
|
||||
queryParamsHandling="preserve"
|
||||
class="adev-api-anchor"
|
||||
tabindex="-1"
|
||||
[innerHtml]="group.title"
|
||||
></a>
|
||||
</h3>
|
||||
</header>
|
||||
|
||||
<ul class="adev-api-items-section-grid">
|
||||
@for (apiItem of group.items; track apiItem.url) {
|
||||
<li [class.adev-api-items-section-item-deprecated]="apiItem.isDeprecated">
|
||||
<a [routerLink]="'/' + apiItem.url" class="adev-api-items-section-item" [attr.aria-describedby]="apiItem.isDeprecated ? 'deprecated-description' : null">
|
||||
<a
|
||||
[routerLink]="'/' + apiItem.url"
|
||||
class="adev-api-items-section-item"
|
||||
[attr.aria-describedby]="apiItem.isDeprecated ? 'deprecated-description' : null"
|
||||
>
|
||||
<docs-api-item-label
|
||||
[type]="apiItem.itemType"
|
||||
mode="short"
|
||||
@@ -20,9 +32,7 @@
|
||||
<span class="adev-item-title">{{ apiItem.title }}</span>
|
||||
</a>
|
||||
@if (apiItem.isDeprecated) {
|
||||
<span class="docs-deprecated">
|
||||
<!>
|
||||
</span>
|
||||
<span class="docs-deprecated"> <!> </span>
|
||||
}
|
||||
@if (apiItem.isFeatured) {
|
||||
<docs-icon
|
||||
|
||||
+34
-21
@@ -232,6 +232,20 @@
|
||||
letter-spacing: -0.00875rem;
|
||||
}
|
||||
|
||||
.docs-reference-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
padding: 0.7rem 1rem;
|
||||
|
||||
code:not(pre *) {
|
||||
padding: 0 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-reference-member-card {
|
||||
border: 1px solid var(--senary-contrast);
|
||||
border-radius: 0.25rem;
|
||||
@@ -255,17 +269,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:has(.docs-reference-card-body) {
|
||||
header {
|
||||
border-radius: 0.25rem 0.25rem 0 0;
|
||||
border-bottom: 1px solid var(--senary-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.7rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: var(--octonary-contrast);
|
||||
position: relative;
|
||||
@@ -275,20 +281,23 @@
|
||||
background-color 0.3s ease,
|
||||
border 0.3s ease;
|
||||
|
||||
// h3 + code || # of overloads
|
||||
.docs-reference-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
& > code {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
code:has(pre) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
|
||||
/* Do we have a better alternative ? */
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-reference-card-header {
|
||||
h3 {
|
||||
display: inline-block;
|
||||
font-family: var(--code-font);
|
||||
@@ -323,6 +332,10 @@
|
||||
.docs-code {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// when it's not the only card...
|
||||
@@ -386,7 +399,7 @@
|
||||
margin-inline-end: 0.5rem;
|
||||
}
|
||||
|
||||
.adev-param-name {
|
||||
.docs-param-name {
|
||||
color: var(--vivid-pink);
|
||||
font-family: var(--code-font);
|
||||
margin-inline-end: 0.25rem;
|
||||
|
||||
+3
-2
@@ -11,7 +11,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
EnvironmentInjector,
|
||||
Injector,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
afterNextRender,
|
||||
@@ -56,6 +56,7 @@ export default class ApiReferenceDetailsPage implements OnInit, AfterViewInit {
|
||||
private readonly router = inject(Router);
|
||||
private readonly scrollHandler = inject(ReferenceScrollHandler);
|
||||
private readonly appScroller = inject(AppScroller);
|
||||
private readonly injector = inject(Injector);
|
||||
|
||||
ApiItemType = ApiItemType;
|
||||
|
||||
@@ -96,7 +97,7 @@ export default class ApiReferenceDetailsPage implements OnInit, AfterViewInit {
|
||||
)
|
||||
.subscribe((doc: DocContent | undefined) => {
|
||||
this.setContentForPageSections(doc);
|
||||
this.setActiveTab();
|
||||
afterNextRender(() => this.setActiveTab(), {injector: this.injector});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+27
@@ -14,6 +14,7 @@ import {signal} from '@angular/core';
|
||||
import {ApiItemType} from '../interfaces/api-item-type';
|
||||
import {RouterTestingHarness} from '@angular/router/testing';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {Location} from '@angular/common';
|
||||
|
||||
describe('ApiReferenceList', () => {
|
||||
let component: ApiReferenceList;
|
||||
@@ -117,4 +118,30 @@ describe('ApiReferenceList', () => {
|
||||
harness.navigateByUrl(`/api`);
|
||||
expect(component.type()).toBe(ALL_STATUSES_KEY);
|
||||
});
|
||||
|
||||
it('should set the value of the queryParam equal to the query value', async () => {
|
||||
const location = TestBed.inject(Location);
|
||||
component.query.set('item1');
|
||||
await fixture.whenStable();
|
||||
expect(location.path()).toBe(`?query=item1&type=All`);
|
||||
});
|
||||
|
||||
it('should keep the values of existing queryParams and set new queryParam equal to the type', async () => {
|
||||
const location = TestBed.inject(Location);
|
||||
|
||||
component.query.set('item1');
|
||||
await fixture.whenStable();
|
||||
expect(location.path()).toBe(`?query=item1&type=All`);
|
||||
|
||||
component.filterByItemType(ApiItemType.BLOCK);
|
||||
await fixture.whenStable();
|
||||
expect(location.path()).toBe(`?query=item1&type=${ApiItemType.BLOCK}`);
|
||||
});
|
||||
|
||||
it('should display all items when query and type are undefined', async () => {
|
||||
component.query.set(undefined);
|
||||
component.type.set(undefined);
|
||||
await fixture.whenStable();
|
||||
expect(component.filteredGroups()![0].items).toEqual([fakeItem1, fakeItem2]);
|
||||
});
|
||||
});
|
||||
|
||||
+26
-3
@@ -23,6 +23,7 @@ import ApiItemsSection from '../api-items-section/api-items-section.component';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {SlideToggle, TextField} from '@angular/docs';
|
||||
import {NgFor, NgIf} from '@angular/common';
|
||||
import {Params, Router} from '@angular/router';
|
||||
import {ApiItemType} from '../interfaces/api-item-type';
|
||||
import {ApiReferenceManager} from './api-reference-manager.service';
|
||||
import ApiItemLabel from '../api-item-label/api-item-label.component';
|
||||
@@ -50,6 +51,7 @@ export const ALL_STATUSES_KEY = 'All';
|
||||
})
|
||||
export default class ApiReferenceList {
|
||||
private readonly apiReferenceManager = inject(ApiReferenceManager);
|
||||
private readonly router = inject(Router);
|
||||
filterInput = viewChild.required(TextField, {read: ElementRef});
|
||||
private readonly injector = inject(EnvironmentInjector);
|
||||
|
||||
@@ -71,9 +73,28 @@ export default class ApiReferenceList {
|
||||
{injector: this.injector},
|
||||
);
|
||||
});
|
||||
|
||||
effect(
|
||||
() => {
|
||||
const params: Params = {
|
||||
'query': this.query() ? this.query() : null,
|
||||
'type': this.type() ? this.type() : null,
|
||||
};
|
||||
|
||||
this.router.navigate([], {
|
||||
queryParams: params,
|
||||
replaceUrl: true,
|
||||
preserveFragment: true,
|
||||
info: {
|
||||
disableScrolling: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
{allowSignalWrites: true},
|
||||
);
|
||||
}
|
||||
|
||||
query = signal('');
|
||||
query = model<string | undefined>('');
|
||||
includeDeprecated = signal(false);
|
||||
|
||||
type = model<string | undefined>(ALL_STATUSES_KEY);
|
||||
@@ -87,8 +108,10 @@ export default class ApiReferenceList {
|
||||
id: group.id,
|
||||
items: group.items.filter((apiItem) => {
|
||||
return (
|
||||
(this.query()
|
||||
? apiItem.title.toLocaleLowerCase().includes(this.query().toLocaleLowerCase())
|
||||
(this.query() !== undefined
|
||||
? apiItem.title
|
||||
.toLocaleLowerCase()
|
||||
.includes((this.query() as string).toLocaleLowerCase())
|
||||
: true) &&
|
||||
(this.includeDeprecated() ? true : apiItem.isDeprecated === this.includeDeprecated()) &&
|
||||
(this.type() === undefined ||
|
||||
|
||||
+8
-12
@@ -9,7 +9,7 @@
|
||||
import {Injectable, signal} from '@angular/core';
|
||||
// This file is generated at build-time, error is expected here.
|
||||
import API_MANIFEST_JSON from '../../../../../src/assets/api/manifest.json';
|
||||
import {ANGULAR_PACKAGE_PREFIX, getApiUrl} from '../helpers/manifest.helper';
|
||||
import {getApiUrl} from '../helpers/manifest.helper';
|
||||
import {ApiItem} from '../interfaces/api-item';
|
||||
import {ApiItemsGroup} from '../interfaces/api-items-group';
|
||||
import {ApiManifest} from '../interfaces/api-manifest';
|
||||
@@ -34,6 +34,8 @@ export const FEATURED_ITEMS_URLS = [
|
||||
'api/router/CanActivate',
|
||||
];
|
||||
|
||||
const manifest = API_MANIFEST_JSON as ApiManifest;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@@ -50,20 +52,14 @@ export class ApiReferenceManager {
|
||||
|
||||
private mapManifestToApiGroups(): ApiItemsGroup[] {
|
||||
const groups: ApiItemsGroup[] = [];
|
||||
const manifest = API_MANIFEST_JSON as ApiManifest;
|
||||
|
||||
const packageNames = Object.keys(API_MANIFEST_JSON);
|
||||
|
||||
for (const packageName of packageNames) {
|
||||
const packageNameWithoutPrefix = packageName.replace(ANGULAR_PACKAGE_PREFIX, '');
|
||||
const packageApis = manifest[packageName];
|
||||
|
||||
for (const module of manifest) {
|
||||
groups.push({
|
||||
title: packageNameWithoutPrefix,
|
||||
id: packageNameWithoutPrefix.replace(/\//g, '-'),
|
||||
items: packageApis
|
||||
title: module.moduleLabel.replace('@angular/', ''),
|
||||
id: module.normalizedModuleName,
|
||||
items: module.entries
|
||||
.map((api) => {
|
||||
const url = getApiUrl(packageNameWithoutPrefix, api.name);
|
||||
const url = getApiUrl(module, api.name);
|
||||
const isFeatured = FEATURED_ITEMS_URLS.some((featuredUrl) => featuredUrl === url);
|
||||
const apiItem = {
|
||||
itemType: api.type,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {ApiManifestPackage} from '../interfaces/api-manifest';
|
||||
import {getApiUrl} from './manifest.helper';
|
||||
|
||||
describe('ManiferHelper', () => {
|
||||
describe('getApiUrl', () => {
|
||||
it('should return the correct URL for a given package and API name', () => {
|
||||
const packageEntry: ApiManifestPackage = {
|
||||
moduleName: '@angular/common',
|
||||
moduleLabel: 'common',
|
||||
normalizedModuleName: 'angular_common',
|
||||
entries: [],
|
||||
};
|
||||
const apiName = 'DatePipe';
|
||||
const result = getApiUrl(packageEntry, apiName);
|
||||
expect(result).toBe('api/common/DatePipe');
|
||||
|
||||
const packageEntry2: ApiManifestPackage = {
|
||||
moduleName: '@angular/animations/browser',
|
||||
moduleLabel: 'animations/browser',
|
||||
normalizedModuleName: 'angular_animations_browser',
|
||||
entries: [],
|
||||
};
|
||||
const result2 = getApiUrl(packageEntry2, apiName);
|
||||
expect(result2).toBe('api/animations/browser/DatePipe');
|
||||
|
||||
const packageEntry3: ApiManifestPackage = {
|
||||
moduleName: '@angular/common/http/testing',
|
||||
moduleLabel: 'common/http/testing',
|
||||
normalizedModuleName: 'angular_common_http_testing',
|
||||
entries: [],
|
||||
};
|
||||
const result3 = getApiUrl(packageEntry3, apiName);
|
||||
expect(result3).toBe('api/common/http/testing/DatePipe');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,31 +8,23 @@
|
||||
|
||||
import {Route} from '@angular/router';
|
||||
import API_MANIFEST_JSON from '../../../../../src/assets/api/manifest.json';
|
||||
import {ApiManifest, ApiManifestItem} from '../interfaces/api-manifest';
|
||||
import {ApiManifest, ApiManifestEntry, ApiManifestPackage} from '../interfaces/api-manifest';
|
||||
import {PagePrefix} from '../../../core/enums/pages';
|
||||
import {NavigationItem, contentResolver} from '@angular/docs';
|
||||
|
||||
export const ANGULAR_PACKAGE_PREFIX = '@angular/';
|
||||
const manifest = API_MANIFEST_JSON as ApiManifest;
|
||||
|
||||
export function mapApiManifestToRoutes(): Route[] {
|
||||
const manifest = API_MANIFEST_JSON as ApiManifest;
|
||||
const packageNames = Object.keys(API_MANIFEST_JSON);
|
||||
|
||||
const apiRoutes: Route[] = [];
|
||||
|
||||
for (const packageName of packageNames) {
|
||||
const packageNameWithoutPrefix = packageName.replace(ANGULAR_PACKAGE_PREFIX, '');
|
||||
const packageApis = manifest[packageName];
|
||||
|
||||
for (const api of packageApis) {
|
||||
for (const packageEntry of manifest) {
|
||||
for (const api of packageEntry.entries) {
|
||||
apiRoutes.push({
|
||||
path: getApiUrl(packageNameWithoutPrefix, api.name),
|
||||
path: getApiUrl(packageEntry, api.name),
|
||||
loadComponent: () =>
|
||||
import('./../api-reference-details-page/api-reference-details-page.component'),
|
||||
resolve: {
|
||||
docContent: contentResolver(
|
||||
`api/${getNormalizedFilename(packageNameWithoutPrefix, api)}`,
|
||||
),
|
||||
docContent: contentResolver(`api/${getNormalizedFilename(packageEntry, api)}`),
|
||||
},
|
||||
data: {
|
||||
label: api.name,
|
||||
@@ -46,20 +38,14 @@ export function mapApiManifestToRoutes(): Route[] {
|
||||
}
|
||||
|
||||
export function getApiNavigationItems(): NavigationItem[] {
|
||||
const manifest = API_MANIFEST_JSON as ApiManifest;
|
||||
const packageNames = Object.keys(API_MANIFEST_JSON);
|
||||
|
||||
const apiNavigationItems: NavigationItem[] = [];
|
||||
|
||||
for (const packageName of packageNames) {
|
||||
const packageNameWithoutPrefix = packageName.replace(ANGULAR_PACKAGE_PREFIX, '');
|
||||
const packageApis = manifest[packageName];
|
||||
|
||||
for (const packageEntry of manifest) {
|
||||
const packageNavigationItem: NavigationItem = {
|
||||
label: packageNameWithoutPrefix,
|
||||
children: packageApis
|
||||
label: packageEntry.moduleLabel,
|
||||
children: packageEntry.entries
|
||||
.map((api) => ({
|
||||
path: getApiUrl(packageNameWithoutPrefix, api.name),
|
||||
path: getApiUrl(packageEntry, api.name),
|
||||
label: api.name,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label)),
|
||||
@@ -71,12 +57,18 @@ export function getApiNavigationItems(): NavigationItem[] {
|
||||
return apiNavigationItems;
|
||||
}
|
||||
|
||||
export function getApiUrl(packageNameWithoutPrefix: string, apiName: string): string {
|
||||
return `${PagePrefix.API}/${packageNameWithoutPrefix}/${apiName}`;
|
||||
export function getApiUrl(packageEntry: ApiManifestPackage, apiName: string): string {
|
||||
const packageName = packageEntry.normalizedModuleName
|
||||
// packages like `angular_core` should be `core`
|
||||
// packages like `angular_animation_browser` should be `animation/browser`
|
||||
.replace('angular_', '')
|
||||
.replaceAll('_', '/');
|
||||
return `${PagePrefix.API}/${packageName}/${apiName}`;
|
||||
}
|
||||
|
||||
function getNormalizedFilename(moduleName: string, entry: ApiManifestItem): string {
|
||||
// Angular entry points can contain `/`, we would like to swap `/` with an underscore
|
||||
const normalizedModuleName = moduleName.replace(/\//g, '_');
|
||||
return `angular_${normalizedModuleName}_${entry.name}_${entry.type}.html`;
|
||||
function getNormalizedFilename(
|
||||
manifestPackage: ApiManifestPackage,
|
||||
entry: ApiManifestEntry,
|
||||
): string {
|
||||
return `${manifestPackage.normalizedModuleName}_${entry.name}_${entry.type}.html`;
|
||||
}
|
||||
|
||||
@@ -8,12 +8,17 @@
|
||||
|
||||
import {ApiItemType} from './api-item-type';
|
||||
|
||||
export interface ApiManifestItem {
|
||||
export interface ApiManifestEntry {
|
||||
name: string;
|
||||
type: ApiItemType;
|
||||
isDeprecated?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiManifest {
|
||||
[packageName: string]: ApiManifestItem[];
|
||||
export interface ApiManifestPackage {
|
||||
moduleName: string;
|
||||
normalizedModuleName: string;
|
||||
moduleLabel: string;
|
||||
entries: ApiManifestEntry[];
|
||||
}
|
||||
|
||||
export type ApiManifest = ApiManifestPackage[];
|
||||
|
||||
@@ -102,6 +102,11 @@ export class ReferenceScrollHandler implements OnDestroy {
|
||||
fromEvent(tocContainer, 'click')
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((event) => {
|
||||
if (event.target instanceof HTMLAnchorElement) {
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the card member ID from the attributes
|
||||
const target =
|
||||
event.target instanceof HTMLButtonElement
|
||||
@@ -125,7 +130,12 @@ export class ReferenceScrollHandler implements OnDestroy {
|
||||
}
|
||||
fromEvent(header, 'click')
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => {
|
||||
.subscribe((event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target instanceof HTMLAnchorElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.navigate([], {fragment: card.id, replaceUrl: true});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2094,7 +2094,7 @@ export const RECOMMENDATIONS: Step[] = [
|
||||
level: ApplicationComplexity.Basic,
|
||||
step: 'v17 zone.js support',
|
||||
action:
|
||||
'Make sure that you are using a supported version of Zone.js before you upgrade your application. Angular v16 supports Zone.js version 0.14.x or later.',
|
||||
'Make sure that you are using a supported version of Zone.js before you upgrade your application. Angular v17 supports Zone.js version 0.14.x or later.',
|
||||
},
|
||||
{
|
||||
possibleIn: 1700,
|
||||
|
||||
@@ -1462,16 +1462,6 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
path: 'reference/migrations/standalone',
|
||||
contentPath: 'reference/migrations/standalone',
|
||||
},
|
||||
{
|
||||
label: 'ModuleWithProviders',
|
||||
path: 'reference/migrations/module-with-providers',
|
||||
contentPath: 'reference/migrations/module-with-providers',
|
||||
},
|
||||
{
|
||||
label: 'Typed Forms',
|
||||
path: 'reference/migrations/typed-forms',
|
||||
contentPath: 'reference/migrations/typed-forms',
|
||||
},
|
||||
{
|
||||
label: 'Control Flow Syntax',
|
||||
path: 'reference/migrations/control-flow',
|
||||
@@ -1482,6 +1472,11 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
|
||||
path: 'reference/migrations/inject-function',
|
||||
contentPath: 'reference/migrations/inject-function',
|
||||
},
|
||||
{
|
||||
label: 'Lazy-loaded routes',
|
||||
path: 'reference/migrations/route-lazy-loading',
|
||||
contentPath: 'reference/migrations/route-lazy-loading',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user