Skip to content

Commit bc280f3

Browse files
authored
refactor(components-example): extract regions from component example source files (#19376)
* refactor: functionality to extract regions from component example source files * add README for region-parser * lint issues * add more descriptive comments for changes in bazel * calculate file extension from base path instead of passing it in * remove expanded field Co-authored-by: Annie Wang <[email protected]>
1 parent 4ad33e8 commit bc280f3

File tree

15 files changed

+287
-91
lines changed

15 files changed

+287
-91
lines changed

src/components-examples/BUILD.bazel

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ filegroup(
7272
)
7373

7474
highlight_files(
75-
name = "highlighted-source-files",
75+
name = "examples-highlighted",
7676
srcs = [":example-source-files"],
7777
tags = ["docs-package"],
7878
)
@@ -97,16 +97,20 @@ package_docs_content(
9797

9898
# For the live-examples in our docs, we want to package the highlighted files
9999
# into the docs content. These will be used to show the source code for examples.
100-
":highlighted-source-files": "examples-highlighted",
100+
# Note: `examples-highlighted` is a tree artifact that we want to store as is
101+
# in the docs-content. Hence there is no target section name.
102+
":examples-highlighted": "",
101103
},
102104
tags = ["docs-package"],
103105
)
104106

105107
ng_package(
106108
name = "npm_package",
107109
srcs = ["package.json"],
108-
data = [":docs-content"],
109110
entry_point = ":public-api.ts",
111+
# this is a workaround to store a tree artifact in the ng_package.
112+
# ng_package does not properly handle tree artifacts currently so we escalate to nested_packages
113+
nested_packages = [":docs-content"],
110114
tags = ["docs-package"],
111115
deps = [":components-examples"] + ALL_EXAMPLES,
112116
)

tools/highlight-files/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ ts_library(
88
srcs = glob(["**/*.ts"]),
99
tsconfig = ":tsconfig.json",
1010
deps = [
11+
"//tools/region-parser",
12+
"@npm//@types/fs-extra",
1113
"@npm//@types/node",
1214
],
1315
)
@@ -16,6 +18,7 @@ nodejs_binary(
1618
name = "highlight-files",
1719
data = [
1820
":sources",
21+
"@npm//fs-extra",
1922
"@npm//highlight.js",
2023
],
2124
entry_point = ":highlight-files.ts",

tools/highlight-files/highlight-files.ts

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
* multiple input files using highlight.js. The output will be HTML files.
44
*/
55

6-
import {readFileSync, writeFileSync} from 'fs';
7-
import {extname, join} from 'path';
6+
import {readFileSync, writeFileSync, ensureDirSync} from 'fs-extra';
7+
import {dirname, extname, join, relative} from 'path';
88
import {highlightCodeBlock} from './highlight-code-block';
9+
import {regionParser} from '../region-parser/region-parser';
910

1011
/**
1112
* Determines the command line arguments for the current Bazel action. Since this action can
@@ -25,21 +26,48 @@ function getBazelActionArguments() {
2526
return args;
2627
}
2728

29+
function detectAndHighlightRegionBlocks(parsed:
30+
{ contents: string, regions: { [p: string]: string } },
31+
basePath: string,
32+
outDir: string) {
33+
const fileExtension = extname(basePath).substring(1);
34+
for (const [regionName, regionSnippet] of Object.entries(parsed.regions)) {
35+
// Create files for each found region
36+
if (!regionName) {
37+
continue;
38+
}
39+
const highlightedRegion = highlightCodeBlock(regionSnippet, fileExtension);
40+
// Convert "my-component-example.ts" into "my-component-example_region-ts.html"
41+
const regionBaseOutputPath = basePath.replace(`.${fileExtension}`,
42+
`_${regionName}-${fileExtension}.html`);
43+
const regionOutputPath = join(outDir, regionBaseOutputPath);
44+
ensureDirSync(dirname(regionOutputPath));
45+
writeFileSync(regionOutputPath, highlightedRegion);
46+
}
47+
}
48+
2849
if (require.main === module) {
29-
// The script expects the bazel-bin path as first argument. All remaining arguments will be
30-
// considered as markdown input files that need to be transformed.
31-
const [bazelBinPath, ...inputFiles] = getBazelActionArguments();
32-
33-
// Walk through each input file and write transformed markdown output to the specified
34-
// Bazel bin directory.
35-
inputFiles.forEach(inputPath => {
36-
const fileExtension = extname(inputPath).substring(1);
37-
// Convert "my-component-example.ts" into "my-component-example-ts.html"
38-
const baseOutputPath = inputPath.replace(`.${fileExtension}`, `-${fileExtension}.html`);
39-
const outputPath = join(bazelBinPath, baseOutputPath);
40-
const htmlOutput = highlightCodeBlock(readFileSync(inputPath, 'utf8'), fileExtension);
41-
42-
writeFileSync(outputPath, htmlOutput);
43-
});
50+
// The script expects the output directory as first argument. Second is the name of the
51+
// package where this the highlight target is declared. All remaining arguments will be
52+
// considered as markdown input files that need to be transformed.
53+
const [outDir, packageName, ...inputFiles] = getBazelActionArguments();
54+
55+
// Walk through each input file and write transformed markdown output
56+
// to the specified output directory.
57+
for (const execPath of inputFiles) {
58+
// Compute a relative path from the package to the actual input file.
59+
// e.g `src/components-examples/cdk/<..>/example.ts` becomes `cdk/<..>/example.ts`.
60+
const basePath = relative(packageName, execPath);
61+
const fileExtension = extname(basePath).substring(1);
62+
const parsed = regionParser(readFileSync(execPath, 'utf8'), fileExtension);
63+
detectAndHighlightRegionBlocks(parsed, basePath, outDir);
64+
// Convert "my-component-example.ts" into "my-component-example-ts.html"
65+
const baseOutputPath = basePath.replace(`.${fileExtension}`, `-${fileExtension}.html`);
66+
const outputPath = join(outDir, baseOutputPath);
67+
const htmlOutput = highlightCodeBlock(parsed.contents, fileExtension);
68+
69+
ensureDirSync(dirname(outputPath));
70+
writeFileSync(outputPath, htmlOutput);
71+
}
4472

4573
}

tools/highlight-files/index.bzl

Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,3 @@
1-
"""
2-
Gets a path relative to the specified label. This is achieved by just removing the label
3-
package path from the specified path. e.g. the path is "guides/test/my-text.md" and the
4-
label package is "guides/". The expected path would be "test/my-text.md".
5-
"""
6-
7-
def _relative_to_label(label, short_path):
8-
# TODO(devversion): extract into generic utility under tools/
9-
return short_path[len(label.package) + 1:]
10-
111
"""
122
Implementation of the "highlight_files" rule. The implementation runs the
133
highlight-files executable in order to highlight the specified source files.
@@ -16,7 +6,7 @@ def _relative_to_label(label, short_path):
166
def _highlight_files(ctx):
177
input_files = ctx.files.srcs
188
args = ctx.actions.args()
19-
expected_outputs = []
9+
output_dir = ctx.actions.declare_directory(ctx.label.name)
2010

2111
# Do nothing if there are no input files. Bazel will throw if we schedule an action
2212
# that returns no outputs.
@@ -29,41 +19,27 @@ def _highlight_files(ctx):
2919
# Read more here: https://docs.bazel.build/versions/master/skylark/lib/Args.html#use_param_file
3020
args.use_param_file(param_file_arg = "--param-file=%s")
3121

32-
# Add the bazel bin directory to the command arguments. The script needs to know about
33-
# the output directory because the input files are not in the same location as the bazel
34-
# bin directory.
35-
args.add(ctx.bin_dir.path)
36-
37-
for input_file in input_files:
38-
# Extension of the input file (e.g. "ts" or "css")
39-
file_extension = input_file.extension
40-
41-
# Determine the input file path relatively to the current package path. This is necessary
42-
# because we want to preserve directories for the input files and `declare_file` expects a
43-
# path that is relative to the current package. We remove the file extension including the dot
44-
# because we will constructo an output file using a different extension.
45-
relative_basepath = _relative_to_label(ctx.label, input_file.short_path)[:-len(file_extension) - 1]
22+
# Add the output directory path to the command arguments
23+
args.add(output_dir.path)
4624

47-
# Construct the output path from the relative basepath and file extension. For example:
48-
# "autocomplete.ts" should result in "autocomplete-ts.html".
49-
expected_outputs += [
50-
ctx.actions.declare_file("%s-%s.html" % (relative_basepath, file_extension)),
51-
]
25+
# Add the name of the label package. This will be used in the
26+
# action to compute package-relative paths.
27+
args.add(ctx.label.package)
5228

53-
# Add the path for the input file to the command line arguments, so that the executable
54-
# can process it.
55-
args.add(input_file.path)
29+
# Add the input files to the command arguments. These files will
30+
# be processed by the highlight binary.
31+
args.add_all(input_files)
5632

5733
# Run the highlight-files executable that highlights the specified source files.
5834
ctx.actions.run(
5935
inputs = input_files,
6036
executable = ctx.executable._highlight_files,
61-
outputs = expected_outputs,
37+
outputs = [output_dir],
6238
arguments = [args],
6339
progress_message = "HighlightFiles",
6440
)
6541

66-
return DefaultInfo(files = depset(expected_outputs))
42+
return DefaultInfo(files = depset([output_dir]))
6743

6844
"""
6945
Rule definition for the "highlight_files" rule that can accept arbritary source files

tools/markdown-to-html/docs-marked-renderer.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,13 @@ export class DocsMarkdownRenderer extends Renderer {
6464
* {
6565
* "example": "exampleName",
6666
* "file": "example-html.html",
67-
* "lines": [5, 10],
68-
* "expanded": true
67+
* "region": "some-region",
6968
* }
7069
* ) -->`
7170
* turns into
7271
* `<div material-docs-example="exampleName"
7372
* file="example-html.html"
74-
* lines="[5, 10]"
75-
* expanded="true"></div>`
73+
* region="some-region"></div>`
7674
*
7775
* (old API)
7876
* `<!-- example(name) -->`
@@ -82,11 +80,10 @@ export class DocsMarkdownRenderer extends Renderer {
8280
html(html: string) {
8381
html = html.replace(exampleCommentRegex, (_match: string, content: string) => {
8482
if (content.startsWith('{')) {
85-
const {example, file, lines, expanded} = JSON.parse(content);
83+
const {example, file, region} = JSON.parse(content);
8684
return `<div material-docs-example="${example}"
8785
file="${file}"
88-
lines="${JSON.stringify(lines)}"
89-
expanded="${!!expanded}"></div>`;
86+
region="${region}"></div>`;
9087
} else {
9188
return `<div material-docs-example="${content}"></div>`;
9289
}

tools/package-docs-content/BUILD.bazel

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ nodejs_binary(
77
name = "package-docs-content",
88
data = [
99
":sources",
10+
"@npm//fs-extra",
1011
],
1112
entry_point = ":package-docs-content.ts",
1213
)
@@ -15,5 +16,8 @@ ts_library(
1516
name = "sources",
1617
srcs = glob(["**/*.ts"]),
1718
tsconfig = ":tsconfig.json",
18-
deps = ["@npm//@types/node"],
19+
deps = [
20+
"@npm//@types/fs-extra",
21+
"@npm//@types/node",
22+
],
1923
)

tools/package-docs-content/index.bzl

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,25 @@
44
"""
55

66
def _package_docs_content(ctx):
7-
# Directory that will contain all grouped input files. This directory will be created
8-
# relatively to the current target package. (e.g. "bin/src/components-examples/docs-content")
9-
output_dir = ctx.attr.name
10-
117
# Arguments that will be passed to the packager executable.
128
args = ctx.actions.args()
139

14-
# List of outputs that should be generated by the packager action. Bazel will automatically
15-
# throw an error if any output has not been generated properly.
16-
expected_outputs = []
10+
# Directory that will contain all grouped input files. This directory will be
11+
# created relatively to the current target package. For example:
12+
# "bin/src/components-examples/docs-content/docs-content". The reason we need to
13+
# repeat `docs-content` is that the ng_package rule does not properly handle tree
14+
# artifacts in data. Instead, we create a tree artifact that can be put into nested_packages.
15+
# Nested packages do not preserve the tree artifact name (i.e. the directory name),
16+
# so all contents of the docs-content would be put directly into the @angular/components-examples package.
17+
# To avoid that, we create another folder like docs-content in the tree artifact that
18+
# is preserved as content of the tree artifact.
19+
output_dir = ctx.actions.declare_directory("%s/%s" % (ctx.attr.name, ctx.attr.name))
1720

1821
# Support passing arguments through a parameter file. This is necessary because on Windows
1922
# there is an argument limit and we need to handle a large amount of input files. Bazel
2023
# switches between parameter file and normal argument passing based on the operating system.
2124
# Read more here: https://docs.bazel.build/versions/master/skylark/lib/Args.html#use_param_file
22-
args.use_param_file(param_file_arg = "--param-file=%s")
25+
args.use_param_file(param_file_arg = "--param-file=%s", use_always = True)
2326

2427
# Walk through each defined input target and the associated section and compute the
2528
# output file which will be added to the executable arguments.
@@ -29,25 +32,25 @@ def _package_docs_content(ctx):
2932

3033
for input_file in section_files:
3134
# Creates a relative path from the input file. We don't want to include the full
32-
# path in docs content. e.g. `docs-content/overviews/cdk/src/cdk/a11y/a11y.html`.
33-
# Instead, we want the path to be: `docs-content/overviews/cdk/a11y/a11y.html`.
34-
section_relative_file_name = input_file.short_path[len(base_dir):]
35-
36-
# For each input file, we want to create a copy that is stored in the output directory
37-
# within its specified section. e.g. "pkg_bin/docs-content/guides/getting-started.html"
38-
output_file = ctx.actions.declare_file(
39-
"%s/%s/%s" % (output_dir, section_name, section_relative_file_name),
40-
)
35+
# path in docs content. e.g. `/docs-content/overviews/cdk/src/cdk/a11y/a11y.html`. Instead,
36+
# we want the path to be: `/docs-content/overviews/cdk/a11y/a11y.html`.
37+
section_relative_file_name = input_file.short_path[len(base_dir) + 1:]
4138

42-
# Add the output file to the expected outputs so that Bazel throws an error if the file
43-
# hasn't been generated properly.
44-
expected_outputs += [output_file]
39+
# The section name can be empty. This is reasonable when tree artifacts are copied
40+
# over to the resulting package so that only their contents are transferred.
41+
# TODO(devversion): Revisit if this can be improved so that the section name
42+
# is respected. `args.add_all` can unfold tree artifact contents.
43+
# https://cs.opensource.google/bazel/bazel/+/master:src/main/java/com/google/devtools/build/lib/analysis/skylark/Args.java;l=381-382;drc=9a6997d595fbf3447e911346034edfbde7d8b57e?q=addAll&ss=bazel
44+
if section_name:
45+
expected_out_path = "%s/%s/%s" % (output_dir.path, section_name, section_relative_file_name)
46+
else:
47+
expected_out_path = "%s/%s" % (output_dir.path, section_relative_file_name)
4548

4649
# Pass the input file path and the output file path to the packager executable. We need
4750
# to do this for each file because we cannot determine the general path to the output
4851
# directory in a reliable way because Bazel targets cannot just "declare" a directory.
4952
# See: https://docs.bazel.build/versions/master/skylark/lib/actions.html
50-
args.add("%s,%s" % (input_file.path, output_file.path))
53+
args.add("%s,%s" % (input_file.path, expected_out_path))
5154

5255
# Do nothing if there are no input files. Bazel will throw if we schedule an action
5356
# that returns no outputs.
@@ -59,12 +62,12 @@ def _package_docs_content(ctx):
5962
ctx.actions.run(
6063
inputs = ctx.files.srcs,
6164
executable = ctx.executable._packager,
62-
outputs = expected_outputs,
65+
outputs = [output_dir],
6366
arguments = [args],
6467
progress_message = "PackageDocsContent",
6568
)
6669

67-
return DefaultInfo(files = depset(expected_outputs))
70+
return DefaultInfo(files = depset([output_dir]))
6871

6972
"""
7073
Rule definition for the "package_docs_content" rule that can accept arbritary source files

tools/package-docs-content/package-docs-content.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
* multiple times.
66
*/
77

8-
import {readFileSync, writeFileSync} from 'fs';
8+
import {readFileSync, writeFileSync, ensureDirSync, statSync, copySync} from 'fs-extra';
9+
import {dirname} from 'path';
910

1011
/**
1112
* Determines the command line arguments for the current Bazel action. Since this action can
@@ -29,10 +30,19 @@ if (require.main === module) {
2930
// Process all file pairs that have been passed to this executable. Each argument will
3031
// consist of the input file path and the desired output location.
3132
getBazelActionArguments().forEach(argument => {
32-
// Each argument that has been passed consists of an input file path and the expected
33-
// output path. e.g. {path_to_input_file},{expected_output_path}
34-
const [inputFilePath, outputPath] = argument.split(',', 2);
33+
// Each argument that has been passed consists of an input file path and the expected
34+
// output path. e.g. {path_to_input_file},{expected_output_path}
35+
const [execFilePath, expectedOutput] = argument.split(',', 2);
36+
37+
// Ensure the directory exists. Bazel does not create the tree
38+
// artifact by default.
39+
ensureDirSync(dirname(expectedOutput));
40+
41+
if (statSync(execFilePath).isDirectory()) {
42+
copySync(execFilePath, expectedOutput);
43+
} else {
44+
writeFileSync(expectedOutput, readFileSync(execFilePath, 'utf8'));
45+
}
3546

36-
writeFileSync(outputPath, readFileSync(inputFilePath, 'utf8'));
3747
});
3848
}

tools/region-parser/BUILD.bazel

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_library(
6+
name = "region-parser",
7+
srcs = glob(["**/*.ts"]),
8+
tsconfig = ":tsconfig.json",
9+
deps = [
10+
"@npm//@types/node",
11+
],
12+
)

tools/region-parser/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The implementation of `region-parser` is copied from the `angular/angular` repo.
2+
It has been adapted from JavaScript to Typescript along with a few minor changes.
3+
This is part of an ongoing effort to share more components between [angular.io] and [material.angular.io].
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// These kind of comments are used CSS and other languages that do not support inline comments
2+
export const blockC = {
3+
regionStartMatcher: /^\s*\/\*\s*#docregion\s*(.*)\s*\*\/\s*$/,
4+
regionEndMatcher: /^\s*\/\*\s*#enddocregion\s*(.*)\s*\*\/\s*$/,
5+
};

0 commit comments

Comments
 (0)