istio

Форк
0
/
control.go 
720 строк · 17.8 Кб
1
//go:build !agent
2
// +build !agent
3

4
// Copyright Istio Authors
5
//
6
// Licensed under the Apache License, Version 2.0 (the "License");
7
// you may not use this file except in compliance with the License.
8
// You may obtain a copy of the License at
9
//
10
//     http://www.apache.org/licenses/LICENSE-2.0
11
//
12
// Unless required by applicable law or agreed to in writing, software
13
// distributed under the License is distributed on an "AS IS" BASIS,
14
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
// See the License for the specific language governing permissions and
16
// limitations under the License.
17

18
/*
19
NOTICE: The zsh constants are derived from the kubectl completion code,
20
(k8s.io/kubernetes/pkg/kubectl/cmd/completion/completion.go), with the
21
following copyright/license:
22

23
Copyright 2016 The Kubernetes Authors.
24
Licensed under the Apache License, Version 2.0 (the "License");
25
you may not use this file except in compliance with the License.
26
You may obtain a copy of the License at
27
    http://www.apache.org/licenses/LICENSE-2.0
28
Unless required by applicable law or agreed to in writing, software
29
distributed under the License is distributed on an "AS IS" BASIS,
30
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31
See the License for the specific language governing permissions and
32
limitations under the License.
33
*/
34

35
package collateral
36

37
import (
38
	"bytes"
39
	"fmt"
40
	"html"
41
	"os"
42
	"sort"
43
	"strconv"
44
	"strings"
45

46
	"github.com/spf13/cobra"
47
	"github.com/spf13/cobra/doc"
48
	"github.com/spf13/pflag"
49
	"sigs.k8s.io/yaml"
50

51
	"istio.io/istio/pkg/collateral/metrics"
52
	"istio.io/istio/pkg/env"
53
)
54

55
// Control determines the behavior of the EmitCollateral function
56
type Control struct {
57
	// OutputDir specifies the directory to output the collateral files
58
	OutputDir string
59

60
	// EmitManPages controls whether to produce man pages.
61
	EmitManPages bool
62

63
	// EmitYAML controls whether to produce YAML files.
64
	EmitYAML bool
65

66
	// EmitBashCompletion controls whether to produce bash completion files.
67
	EmitBashCompletion bool
68

69
	// EmitZshCompletion controls whether to produce zsh completion files.
70
	EmitZshCompletion bool
71

72
	// EmitMarkdown controls whether to produce markdown documentation files.
73
	EmitMarkdown bool
74

75
	// EmitHTMLFragmentWithFrontMatter controls whether to produce HTML fragments with Jekyll/Hugo front matter.
76
	EmitHTMLFragmentWithFrontMatter bool
77

78
	// ManPageInfo provides extra information necessary when emitting man pages.
79
	ManPageInfo doc.GenManHeader
80

81
	// Predicates to use to filter environment variables and metrics. If not set, all items will be selected.
82
	Predicates Predicates
83
}
84

85
// EmitCollateral produces a set of collateral files for a CLI command. You can
86
// select to emit markdown to describe a command's function, man pages, YAML
87
// descriptions, and bash completion files.
88
func EmitCollateral(root *cobra.Command, c *Control) error {
89
	if c.EmitManPages {
90
		if err := doc.GenManTree(root, &c.ManPageInfo, c.OutputDir); err != nil {
91
			return fmt.Errorf("unable to output manpage tree: %v", err)
92
		}
93
	}
94

95
	if c.EmitMarkdown {
96
		if err := doc.GenMarkdownTree(root, c.OutputDir); err != nil {
97
			return fmt.Errorf("unable to output markdown tree: %v", err)
98
		}
99
	}
100

101
	if c.EmitHTMLFragmentWithFrontMatter {
102
		if err := genHTMLFragment(root, c.OutputDir+"/"+root.Name()+".html", c.Predicates); err != nil {
103
			return fmt.Errorf("unable to output HTML fragment file: %v", err)
104
		}
105
	}
106

107
	if c.EmitYAML {
108
		if err := doc.GenYamlTree(root, c.OutputDir); err != nil {
109
			return fmt.Errorf("unable to output YAML tree: %v", err)
110
		}
111
	}
112

113
	if c.EmitBashCompletion {
114
		if err := root.GenBashCompletionFile(c.OutputDir + "/" + root.Name() + ".bash"); err != nil {
115
			return fmt.Errorf("unable to output bash completion file: %v", err)
116
		}
117
	}
118

119
	if c.EmitZshCompletion {
120

121
		// Constants used in zsh completion file
122
		zshInitialization := `#compdef ` + root.Name() + `
123

124
__istio_bash_source() {
125
	alias shopt=':'
126
	alias _expand=_bash_expand
127
	alias _complete=_bash_comp
128
	emulate -L sh
129
	setopt kshglob noshglob braceexpand
130
	source "$@"
131
}
132
__istio_type() {
133
	# -t is not supported by zsh
134
	if [ "$1" == "-t" ]; then
135
		shift
136
		# fake Bash 4 to disable "complete -o nospace". Instead
137
		# "compopt +-o nospace" is used in the code to toggle trailing
138
		# spaces. We don't support that, but leave trailing spaces on
139
		# all the time
140
		if [ "$1" = "__istio_compopt" ]; then
141
			echo builtin
142
			return 0
143
		fi
144
	fi
145
	type "$@"
146
}
147
__istio_compgen() {
148
	local completions w
149
	completions=( $(compgen "$@") ) || return $?
150
	# filter by given word as prefix
151
	while [[ "$1" = -* && "$1" != -- ]]; do
152
		shift
153
		shift
154
	done
155
	if [[ "$1" == -- ]]; then
156
		shift
157
	fi
158
	for w in "${completions[@]}"; do
159
		if [[ "${w}" = "$1"* ]]; then
160
			echo "${w}"
161
		fi
162
	done
163
}
164
__istio_compopt() {
165
	true # don't do anything. Not supported by bashcompinit in zsh
166
}
167
__istio_ltrim_colon_completions()
168
{
169
	if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
170
		# Remove colon-word prefix from COMPREPLY items
171
		local colon_word=${1%${1##*:}}
172
		local i=${#COMPREPLY[*]}
173
		while [[ $((--i)) -ge 0 ]]; do
174
			COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
175
		done
176
	fi
177
}
178
__istio_get_comp_words_by_ref() {
179
	cur="${COMP_WORDS[COMP_CWORD]}"
180
	prev="${COMP_WORDS[${COMP_CWORD}-1]}"
181
	words=("${COMP_WORDS[@]}")
182
	cword=("${COMP_CWORD[@]}")
183
}
184
__istio_filedir() {
185
	local RET OLD_IFS w qw
186
	__istio_debug "_filedir $@ cur=$cur"
187
	if [[ "$1" = \~* ]]; then
188
		# somehow does not work. Maybe, zsh does not call this at all
189
		eval echo "$1"
190
		return 0
191
	fi
192
	OLD_IFS="$IFS"
193
	IFS=$'\n'
194
	if [ "$1" = "-d" ]; then
195
		shift
196
		RET=( $(compgen -d) )
197
	else
198
		RET=( $(compgen -f) )
199
	fi
200
	IFS="$OLD_IFS"
201
	IFS="," __istio_debug "RET=${RET[@]} len=${#RET[@]}"
202
	for w in ${RET[@]}; do
203
		if [[ ! "${w}" = "${cur}"* ]]; then
204
			continue
205
		fi
206
		if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then
207
			qw="$(__istio_quote "${w}")"
208
			if [ -d "${w}" ]; then
209
				COMPREPLY+=("${qw}/")
210
			else
211
				COMPREPLY+=("${qw}")
212
			fi
213
		fi
214
	done
215
}
216
__istio_quote() {
217
	if [[ $1 == \'* || $1 == \"* ]]; then
218
		# Leave out first character
219
		printf %q "${1:1}"
220
	else
221
		printf %q "$1"
222
	fi
223
}
224
autoload -U +X bashcompinit && bashcompinit
225
# use word boundary patterns for BSD or GNU sed
226
LWORD='[[:<:]]'
227
RWORD='[[:>:]]'
228
if sed --help 2>&1 | grep -q GNU; then
229
	LWORD='\<'
230
	RWORD='\>'
231
fi
232
__istio_convert_bash_to_zsh() {
233
	sed \
234
	-e 's/declare -F/whence -w/' \
235
	-e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \
236
	-e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \
237
	-e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \
238
	-e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \
239
	-e "s/${LWORD}_filedir${RWORD}/__istio_filedir/g" \
240
	-e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__istio_get_comp_words_by_ref/g" \
241
	-e "s/${LWORD}__ltrim_colon_completions${RWORD}/__istio_ltrim_colon_completions/g" \
242
	-e "s/${LWORD}compgen${RWORD}/__istio_compgen/g" \
243
	-e "s/${LWORD}compopt${RWORD}/__istio_compopt/g" \
244
	-e "s/${LWORD}declare${RWORD}/builtin declare/g" \
245
	-e "s/\\\$(type${RWORD}/\$(__istio_type/g" \
246
	<<'BASH_COMPLETION_EOF'
247
`
248

249
		zshTail := `
250
BASH_COMPLETION_EOF
251
}
252

253
__istio_bash_source <(__istio_convert_bash_to_zsh)
254
_complete istio 2>/dev/null
255
`
256

257
		// Create the output file.
258
		outFile, err := os.Create(c.OutputDir + "/_" + root.Name())
259
		if err != nil {
260
			return fmt.Errorf("unable to create zsh completion file: %v", err)
261
		}
262
		defer func() { _ = outFile.Close() }()
263

264
		// Concatenate the head, initialization, generated bash, and tail to the file
265
		if _, err = outFile.WriteString(zshInitialization); err != nil {
266
			return fmt.Errorf("unable to output zsh initialization: %v", err)
267
		}
268
		if err = root.GenBashCompletion(outFile); err != nil {
269
			return fmt.Errorf("unable to output zsh completion file: %v", err)
270
		}
271
		if _, err = outFile.WriteString(zshTail); err != nil {
272
			return fmt.Errorf("unable to output zsh tail: %v", err)
273
		}
274
	}
275

276
	return nil
277
}
278

279
type generator struct {
280
	buffer *bytes.Buffer
281
}
282

283
func (g *generator) emit(str ...string) {
284
	for _, s := range str {
285
		g.buffer.WriteString(s)
286
	}
287
	g.buffer.WriteByte('\n')
288
}
289

290
func findCommands(commands map[string]*cobra.Command, cmd *cobra.Command) {
291
	cmd.InitDefaultHelpCmd()
292
	cmd.InitDefaultHelpFlag()
293

294
	commands[cmd.CommandPath()] = cmd
295
	for _, c := range cmd.Commands() {
296
		findCommands(commands, c)
297
	}
298
}
299

300
const help = "help"
301

302
func genHTMLFragment(cmd *cobra.Command, path string, p Predicates) error {
303
	commands := make(map[string]*cobra.Command)
304
	findCommands(commands, cmd)
305

306
	names := make([]string, len(commands))
307
	i := 0
308
	for n := range commands {
309
		names[i] = n
310
		i++
311
	}
312
	sort.Strings(names)
313

314
	g := &generator{
315
		buffer: &bytes.Buffer{},
316
	}
317

318
	count := 0
319
	for _, n := range names {
320
		if commands[n].Name() == help {
321
			continue
322
		}
323

324
		count++
325
	}
326

327
	g.genFrontMatter(cmd, count)
328
	for _, n := range names {
329
		if commands[n].Name() == help {
330
			continue
331
		}
332

333
		g.genCommand(commands[n])
334
		_ = g.genConfigFile(commands[n].Flags())
335
	}
336

337
	g.genVars(cmd, p.SelectEnv)
338
	g.genMetrics(p.SelectMetric)
339

340
	f, err := os.Create(path)
341
	if err != nil {
342
		return err
343
	}
344
	_, err = g.buffer.WriteTo(f)
345
	_ = f.Close()
346

347
	return err
348
}
349

350
func (g *generator) genConfigFile(flags *pflag.FlagSet) error {
351
	// find all flag names and aliases which contain dots, as these allow
352
	// structured config files, which aren't intuitive
353
	deepkeys := make(map[string]string)
354
	flags.VisitAll(func(f *pflag.Flag) {
355
		if strings.Contains(f.Name, ".") {
356
			deepkeys[f.Name] = "--" + f.Name
357
		}
358
	})
359

360
	if len(deepkeys) < 1 {
361
		return nil
362
	}
363

364
	deepConfig := buildNestedMap(deepkeys)
365
	strb, err := yaml.Marshal(deepConfig)
366
	if err != nil {
367
		return err
368
	}
369
	str := string(strb)
370

371
	g.emit("<p/>Accepts deep config files, like:")
372
	g.emit("<pre class=\"language-yaml\"><code>", str)
373
	g.emit("</code></pre>")
374
	return nil
375
}
376

377
func dereferenceMap(m map[string]string) (result map[string]string) {
378
	// return a map keyed by alias, whose value is the alias' ultimate target
379
	// consider the map as the list of edges in a set of trees
380
	// return a map where each node has one key pointing to the root node of its tree
381
	reversemap := make(map[string][]string)
382
	result = make(map[string]string)
383
	for key, value := range m {
384
		if deepvalue, ok := result[value]; ok {
385
			value = deepvalue
386
		}
387
		if deepkeys, ok := reversemap[key]; ok {
388
			// we have found a new candidate root node for this tree
389
			// look through our results so far and update to new candidate root
390
			for _, deepkey := range deepkeys {
391
				result[deepkey] = value
392
			}
393
			delete(reversemap, key)
394
			reversemap[value] = append(reversemap[value], deepkeys...)
395
		}
396
		result[key] = value
397
		reversemap[value] = append(reversemap[value], key)
398
	}
399
	return
400
}
401

402
func buildNestedMap(flatMap map[string]string) (result map[string]any) {
403
	result = make(map[string]any)
404
	for complexkey, value := range flatMap {
405
		buildMapRecursive(strings.Split(complexkey, "."), result, value)
406
	}
407
	return
408
}
409

410
func buildMapRecursive(remainingPath []string, currentPointer map[string]any, value string) {
411
	if len(remainingPath) == 1 {
412
		currentPointer[remainingPath[0]] = value
413
		return
414
	}
415
	var nextPointer any
416
	var existingPath bool
417
	if nextPointer, existingPath = currentPointer[remainingPath[0]]; !existingPath {
418
		nextPointer = make(map[string]any)
419
		currentPointer[remainingPath[0]] = nextPointer
420
	}
421
	buildMapRecursive(remainingPath[1:], nextPointer.(map[string]any), value)
422
}
423

424
func (g *generator) genFrontMatter(root *cobra.Command, numEntries int) {
425
	g.emit("---")
426
	g.emit("title: ", root.Name())
427
	g.emit("description: ", root.Short)
428
	g.emit("generator: pkg-collateral-docs")
429
	g.emit("number_of_entries: ", strconv.Itoa(numEntries))
430
	g.emit("max_toc_level: 2")
431
	g.emit("remove_toc_prefix: '" + root.Name() + " '")
432
	g.emit("---")
433
}
434

435
func (g *generator) genCommand(cmd *cobra.Command) {
436
	if cmd.Hidden || cmd.Deprecated != "" {
437
		return
438
	}
439

440
	if cmd.HasParent() {
441
		g.emit("<h2 id=\"", normalizeID(cmd.CommandPath()), "\">", cmd.CommandPath(), "</h2>")
442
	}
443

444
	if cmd.Long != "" {
445
		g.emitText(cmd.Long)
446
	} else if cmd.Short != "" {
447
		g.emitText(cmd.Short)
448
	}
449

450
	if cmd.Runnable() {
451
		g.emit("<pre class=\"language-bash\"><code>", html.EscapeString(cmd.UseLine()))
452
		g.emit("</code></pre>")
453

454
		if len(cmd.Aliases) > 0 {
455
			// first word in cmd.Use represents the command that is being aliased
456
			word := cmd.Use
457
			index := strings.Index(word, " ")
458
			if index > 0 {
459
				word = word[0:index]
460
			}
461

462
			g.emit("<div class=\"aliases\">")
463
			line := cmd.UseLine()
464
			for i, alias := range cmd.Aliases {
465
				r := strings.Replace(line, word, alias, 1)
466
				if i == 0 {
467
					g.emit("<pre class=\"language-bash\"><code>", html.EscapeString(r))
468
				} else {
469
					g.emit(html.EscapeString(r))
470
				}
471
			}
472
			g.emit("</code></pre></div>")
473
		}
474
	}
475

476
	flags := cmd.NonInheritedFlags()
477
	flags.SetOutput(g.buffer)
478

479
	parentFlags := cmd.InheritedFlags()
480
	parentFlags.SetOutput(g.buffer)
481

482
	if flags.HasFlags() || parentFlags.HasFlags() {
483
		f := make(map[string]*pflag.Flag)
484
		addFlags(f, flags)
485
		addFlags(f, parentFlags)
486

487
		if len(f) > 0 {
488
			names := make([]string, len(f))
489
			i := 0
490
			for n := range f {
491
				names[i] = n
492
				i++
493
			}
494
			sort.Strings(names)
495

496
			genShorthand := false
497
			for _, v := range f {
498
				if v.Shorthand != "" && v.ShorthandDeprecated == "" {
499
					genShorthand = true
500
					break
501
				}
502
			}
503

504
			g.emit("<table class=\"command-flags\">")
505
			g.emit("<thead>")
506
			g.emit("<tr>")
507
			g.emit("<th>Flags</th>")
508
			if genShorthand {
509
				g.emit("<th>Shorthand</th>")
510
			}
511
			g.emit("<th>Description</th>")
512
			g.emit("</tr>")
513
			g.emit("</thead>")
514
			g.emit("<tbody>")
515

516
			for _, n := range names {
517
				g.genFlag(f[n], genShorthand)
518
			}
519

520
			g.emit("</tbody>")
521
			g.emit("</table>")
522
		}
523
	}
524

525
	if len(cmd.Example) > 0 {
526
		g.emit("<h3 id=\"", normalizeID(cmd.CommandPath()), " Examples\">", "Examples", "</h3>")
527
		g.emit("<pre class=\"language-bash\"><code>", html.EscapeString(cmd.Example))
528
		g.emit("</code></pre>")
529
	}
530
}
531

532
func addFlags(f map[string]*pflag.Flag, s *pflag.FlagSet) {
533
	s.VisitAll(func(flag *pflag.Flag) {
534
		if flag.Deprecated != "" || flag.Hidden {
535
			return
536
		}
537

538
		if flag.Name == help {
539
			return
540
		}
541

542
		f[flag.Name] = flag
543
	})
544
}
545

546
func (g *generator) genFlag(flag *pflag.Flag, genShorthand bool) {
547
	varname, usage := unquoteUsage(flag)
548
	if varname != "" {
549
		varname = " <" + varname + ">"
550
	}
551

552
	def := ""
553
	if flag.Value.Type() == "string" {
554
		def = fmt.Sprintf(" (default `%s`)", flag.DefValue)
555
	} else if flag.Value.Type() != "bool" {
556
		def = fmt.Sprintf(" (default `%s`)", flag.DefValue)
557
	}
558

559
	g.emit("<tr>")
560
	g.emit("<td><code>", "--", flag.Name, html.EscapeString(varname), "</code></td>")
561

562
	if genShorthand {
563
		if flag.Shorthand != "" && flag.ShorthandDeprecated == "" {
564
			g.emit("<td><code>", "-", flag.Shorthand, "</code></td>")
565
		} else {
566
			g.emit("<td></td>")
567
		}
568
	}
569

570
	g.emit("<td>", html.EscapeString(usage), " ", def, "</td>")
571
	g.emit("</tr>")
572
}
573

574
func (g *generator) emitText(text string) {
575
	paras := strings.Split(text, "\n\n")
576
	for _, p := range paras {
577
		g.emit("<p>", html.EscapeString(p), "</p>")
578
	}
579
}
580

581
// unquoteUsage extracts a back-quoted name from the usage
582
// string for a flag and returns it and the un-quoted usage.
583
// Given "a `name` to show" it returns ("name", "a name to show").
584
// If there are no back quotes, the name is an educated guess of the
585
// type of the flag's value, or the empty string if the flag is boolean.
586
func unquoteUsage(flag *pflag.Flag) (name string, usage string) {
587
	// Look for a back-quoted name, but avoid the strings package.
588
	usage = flag.Usage
589
	for i := 0; i < len(usage); i++ {
590
		if usage[i] == '`' {
591
			for j := i + 1; j < len(usage); j++ {
592
				if usage[j] == '`' {
593
					name = usage[i+1 : j]
594
					usage = usage[:i] + name + usage[j+1:]
595
					return name, usage
596
				}
597
			}
598
			break // Only one back quote; use type name.
599
		}
600
	}
601

602
	name = flag.Value.Type()
603
	switch name {
604
	case "bool":
605
		name = ""
606
	case "float64":
607
		name = "float"
608
	case "int64":
609
		name = "int"
610
	case "uint64":
611
		name = "uint"
612
	}
613

614
	return
615
}
616

617
func normalizeID(id string) string {
618
	id = strings.Replace(id, " ", "-", -1)
619
	return strings.Replace(id, ".", "-", -1)
620
}
621

622
func (g *generator) genVars(root *cobra.Command, selectFn SelectEnvFn) {
623
	if selectFn == nil {
624
		selectFn = DefaultSelectEnvFn
625
	}
626

627
	envVars := env.VarDescriptions()
628

629
	count := 0
630
	for _, v := range envVars {
631
		if v.Hidden {
632
			continue
633
		}
634
		if !selectFn(v) {
635
			continue
636
		}
637
		count++
638
	}
639

640
	if count == 0 {
641
		return
642
	}
643

644
	g.emit("<h2 id=\"envvars\">Environment variables</h2>")
645

646
	g.emit("These environment variables affect the behavior of the <code>", root.Name(), "</code> command. "+
647
		"Please use with caution as these environment variables are experimental and can change anytime.")
648

649
	g.emit("<table class=\"envvars\">")
650
	g.emit("<thead>")
651
	g.emit("<tr>")
652
	g.emit("<th>Variable Name</th>")
653
	g.emit("<th>Type</th>")
654
	g.emit("<th>Default Value</th>")
655
	g.emit("<th>Description</th>")
656
	g.emit("</tr>")
657
	g.emit("</thead>")
658
	g.emit("<tbody>")
659

660
	for _, v := range envVars {
661
		if v.Hidden {
662
			continue
663
		}
664
		if !selectFn(v) {
665
			continue
666
		}
667

668
		if v.Deprecated {
669
			g.emit("<tr class='deprecated'>")
670
		} else {
671
			g.emit("<tr>")
672
		}
673
		g.emit("<td><code>", html.EscapeString(v.Name), "</code></td>")
674

675
		switch v.Type {
676
		case env.STRING:
677
			g.emit("<td>String</td>")
678
		case env.BOOL:
679
			g.emit("<td>Boolean</td>")
680
		case env.INT:
681
			g.emit("<td>Integer</td>")
682
		case env.FLOAT:
683
			g.emit("<td>Floating-Point</td>")
684
		case env.DURATION:
685
			g.emit("<td>Time Duration</td>")
686
		case env.OTHER:
687
			g.emit(fmt.Sprintf("<td>%s</td>", v.GoType))
688
		}
689

690
		g.emit("<td><code>", html.EscapeString(v.DefaultValue), "</code></td>")
691
		g.emit("<td>", html.EscapeString(v.Description), "</td>")
692
		g.emit("</tr>")
693
	}
694

695
	g.emit("</tbody>")
696
	g.emit("</table>")
697
}
698

699
func (g *generator) genMetrics(selectFn SelectMetricFn) {
700
	if selectFn == nil {
701
		selectFn = DefaultSelectMetricFn
702
	}
703

704
	g.emit(`<h2 id="metrics">Exported metrics</h2>
705
<table class="metrics">
706
<thead>
707
<tr><th>Metric Name</th><th>Type</th><th>Description</th></tr>
708
</thead>
709
<tbody>`)
710

711
	for _, metric := range metrics.ExportedMetrics() {
712
		if !selectFn(metric) {
713
			continue
714
		}
715
		g.emit("<tr><td><code>", metric.Name, "</code></td><td><code>", metric.Type, "</code></td><td>", metric.Description, "</td></tr>")
716
	}
717

718
	g.emit(`</tbody>
719
</table>`)
720
}
721

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.