recker 1.0.27 → 1.0.28-next.32fe8ef
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/scrape/extractors.js +2 -1
- package/dist/browser/scrape/types.d.ts +2 -1
- package/dist/cli/index.js +142 -3
- package/dist/cli/tui/shell.d.ts +1 -0
- package/dist/cli/tui/shell.js +157 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/scrape/extractors.js +2 -1
- package/dist/scrape/types.d.ts +2 -1
- package/dist/seo/analyzer.d.ts +42 -0
- package/dist/seo/analyzer.js +715 -0
- package/dist/seo/index.d.ts +5 -0
- package/dist/seo/index.js +2 -0
- package/dist/seo/rules/accessibility.d.ts +2 -0
- package/dist/seo/rules/accessibility.js +128 -0
- package/dist/seo/rules/content.d.ts +2 -0
- package/dist/seo/rules/content.js +236 -0
- package/dist/seo/rules/images.d.ts +2 -0
- package/dist/seo/rules/images.js +180 -0
- package/dist/seo/rules/index.d.ts +20 -0
- package/dist/seo/rules/index.js +72 -0
- package/dist/seo/rules/links.d.ts +2 -0
- package/dist/seo/rules/links.js +150 -0
- package/dist/seo/rules/meta.d.ts +2 -0
- package/dist/seo/rules/meta.js +523 -0
- package/dist/seo/rules/mobile.d.ts +2 -0
- package/dist/seo/rules/mobile.js +71 -0
- package/dist/seo/rules/performance.d.ts +2 -0
- package/dist/seo/rules/performance.js +246 -0
- package/dist/seo/rules/schema.d.ts +2 -0
- package/dist/seo/rules/schema.js +54 -0
- package/dist/seo/rules/security.d.ts +2 -0
- package/dist/seo/rules/security.js +147 -0
- package/dist/seo/rules/structural.d.ts +2 -0
- package/dist/seo/rules/structural.js +155 -0
- package/dist/seo/rules/technical.d.ts +2 -0
- package/dist/seo/rules/technical.js +223 -0
- package/dist/seo/rules/thresholds.d.ts +196 -0
- package/dist/seo/rules/thresholds.js +118 -0
- package/dist/seo/rules/types.d.ts +191 -0
- package/dist/seo/rules/types.js +11 -0
- package/dist/seo/types.d.ts +160 -0
- package/dist/seo/types.js +1 -0
- package/dist/utils/columns.d.ts +14 -0
- package/dist/utils/columns.js +69 -0
- package/package.json +1 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { SeoStatus } from '../types.js';
|
|
2
|
+
import type { ExtractedLink } from '../../scrape/types.js';
|
|
3
|
+
export type RuleSeverity = 'error' | 'warning' | 'info';
|
|
4
|
+
export type RuleCategory = 'title' | 'meta' | 'og' | 'twitter' | 'headings' | 'images' | 'links' | 'content' | 'technical' | 'security' | 'mobile' | 'structured-data' | 'performance' | 'accessibility';
|
|
5
|
+
export interface RuleContext {
|
|
6
|
+
title?: string;
|
|
7
|
+
titleLength?: number;
|
|
8
|
+
metaDescription?: string;
|
|
9
|
+
metaDescriptionLength?: number;
|
|
10
|
+
metaKeywords?: string[];
|
|
11
|
+
metaRobots?: string[];
|
|
12
|
+
ogTitle?: string;
|
|
13
|
+
ogDescription?: string;
|
|
14
|
+
ogImage?: string;
|
|
15
|
+
ogUrl?: string;
|
|
16
|
+
ogType?: string;
|
|
17
|
+
ogSiteName?: string;
|
|
18
|
+
twitterCard?: string;
|
|
19
|
+
twitterTitle?: string;
|
|
20
|
+
twitterDescription?: string;
|
|
21
|
+
twitterImage?: string;
|
|
22
|
+
twitterSite?: string;
|
|
23
|
+
h1Count?: number;
|
|
24
|
+
h1Text?: string;
|
|
25
|
+
h1Length?: number;
|
|
26
|
+
h2Count?: number;
|
|
27
|
+
headingHierarchyValid?: boolean;
|
|
28
|
+
headingSkippedLevels?: string[];
|
|
29
|
+
sectionWordCounts?: number[];
|
|
30
|
+
totalImages?: number;
|
|
31
|
+
imagesWithAlt?: number;
|
|
32
|
+
imagesWithoutAlt?: number;
|
|
33
|
+
imagesWithLazyLoad?: number;
|
|
34
|
+
imagesWithDimensions?: number;
|
|
35
|
+
imagesMissingDimensions?: number;
|
|
36
|
+
imagesWithEmptyAlt?: number;
|
|
37
|
+
imagesDecorativeCount?: number;
|
|
38
|
+
imagesUsingModernFormats?: number;
|
|
39
|
+
altTextLengths?: number[];
|
|
40
|
+
imageFilenames?: string[];
|
|
41
|
+
imagesWithAsyncDecoding?: number;
|
|
42
|
+
buttonsWithoutAriaLabel?: number;
|
|
43
|
+
linksWithoutAriaLabel?: number;
|
|
44
|
+
inputsWithoutLabel?: number;
|
|
45
|
+
formsWithoutAction?: number;
|
|
46
|
+
tablesWithoutCaption?: number;
|
|
47
|
+
iframesWithoutTitle?: number;
|
|
48
|
+
svgsWithoutTitle?: number;
|
|
49
|
+
interactiveElementsCount?: number;
|
|
50
|
+
ariaLabelledByMissing?: number;
|
|
51
|
+
allLinks?: ExtractedLink[];
|
|
52
|
+
totalLinks?: number;
|
|
53
|
+
internalLinks?: number;
|
|
54
|
+
externalLinks?: number;
|
|
55
|
+
linksWithoutText?: number;
|
|
56
|
+
nofollowLinks?: number;
|
|
57
|
+
sponsoredLinks?: number;
|
|
58
|
+
ugcLinks?: number;
|
|
59
|
+
brokenLinks?: number;
|
|
60
|
+
linksWithGenericText?: number;
|
|
61
|
+
externalLinksWithoutNoopener?: number;
|
|
62
|
+
externalLinksWithoutNoreferrer?: number;
|
|
63
|
+
problematicLinks?: {
|
|
64
|
+
withoutText?: ExtractedLink[];
|
|
65
|
+
genericText?: ExtractedLink[];
|
|
66
|
+
missingNoopener?: ExtractedLink[];
|
|
67
|
+
missingNoreferrer?: ExtractedLink[];
|
|
68
|
+
};
|
|
69
|
+
wordCount?: number;
|
|
70
|
+
characterCount?: number;
|
|
71
|
+
sentenceCount?: number;
|
|
72
|
+
paragraphCount?: number;
|
|
73
|
+
avgWordsPerSentence?: number;
|
|
74
|
+
avgParagraphLength?: number;
|
|
75
|
+
listCount?: number;
|
|
76
|
+
strongTagCount?: number;
|
|
77
|
+
emTagCount?: number;
|
|
78
|
+
subheadingFrequency?: number;
|
|
79
|
+
paragraphWordCounts?: number[];
|
|
80
|
+
avgSentenceLength?: number;
|
|
81
|
+
faqCount?: number;
|
|
82
|
+
imagePerWordRatio?: number;
|
|
83
|
+
mainKeyword?: string;
|
|
84
|
+
keywordDensity?: number;
|
|
85
|
+
fleschReadingEase?: number;
|
|
86
|
+
hasQuestionHeadings?: boolean;
|
|
87
|
+
hasHeader?: boolean;
|
|
88
|
+
hasNav?: boolean;
|
|
89
|
+
hasMain?: boolean;
|
|
90
|
+
hasArticle?: boolean;
|
|
91
|
+
hasSection?: boolean;
|
|
92
|
+
hasFooter?: boolean;
|
|
93
|
+
hasAboutPageLink?: boolean;
|
|
94
|
+
hasContactPageLink?: boolean;
|
|
95
|
+
hasPrivacyPolicyLink?: boolean;
|
|
96
|
+
hasTermsOfServiceLink?: boolean;
|
|
97
|
+
hasBreadcrumbsHtml?: boolean;
|
|
98
|
+
hasBreadcrumbsSchema?: boolean;
|
|
99
|
+
videoCount?: number;
|
|
100
|
+
audioCount?: number;
|
|
101
|
+
hasCanonical?: boolean;
|
|
102
|
+
canonicalUrl?: string;
|
|
103
|
+
hasViewport?: boolean;
|
|
104
|
+
viewportContent?: string;
|
|
105
|
+
hasCharset?: boolean;
|
|
106
|
+
charset?: string;
|
|
107
|
+
hasLang?: boolean;
|
|
108
|
+
langValue?: string;
|
|
109
|
+
isHttps?: boolean;
|
|
110
|
+
hasMixedContent?: boolean;
|
|
111
|
+
responseHeaders?: Record<string, string | string[]>;
|
|
112
|
+
textHtmlRatio?: number;
|
|
113
|
+
hasFavicon?: boolean;
|
|
114
|
+
faviconUrl?: string;
|
|
115
|
+
hasPreconnect?: boolean;
|
|
116
|
+
preconnectCount?: number;
|
|
117
|
+
hasDnsPrefetch?: boolean;
|
|
118
|
+
dnsPrefetchCount?: number;
|
|
119
|
+
hasPreload?: boolean;
|
|
120
|
+
preloadCount?: number;
|
|
121
|
+
renderBlockingResources?: number;
|
|
122
|
+
inlineScriptsCount?: number;
|
|
123
|
+
inlineStylesCount?: number;
|
|
124
|
+
lcpHints?: {
|
|
125
|
+
hasLargeImages?: boolean;
|
|
126
|
+
hasLazyLcp?: boolean;
|
|
127
|
+
hasPriorityHints?: boolean;
|
|
128
|
+
};
|
|
129
|
+
clsHints?: {
|
|
130
|
+
imagesWithoutDimensions?: number;
|
|
131
|
+
dynamicContent?: number;
|
|
132
|
+
};
|
|
133
|
+
jsonLdCount?: number;
|
|
134
|
+
jsonLdTypes?: string[];
|
|
135
|
+
url?: string;
|
|
136
|
+
urlLength?: number;
|
|
137
|
+
titleMatchesH1?: boolean;
|
|
138
|
+
urlHasUppercase?: boolean;
|
|
139
|
+
urlHasSpecialChars?: boolean;
|
|
140
|
+
urlHasAccents?: boolean;
|
|
141
|
+
bodyTextLength?: number;
|
|
142
|
+
scriptCount?: number;
|
|
143
|
+
hasNoscriptContent?: boolean;
|
|
144
|
+
timings?: {
|
|
145
|
+
ttfb?: number;
|
|
146
|
+
dnsLookup?: number;
|
|
147
|
+
tcpConnect?: number;
|
|
148
|
+
tlsHandshake?: number;
|
|
149
|
+
download?: number;
|
|
150
|
+
total?: number;
|
|
151
|
+
};
|
|
152
|
+
responseSize?: number;
|
|
153
|
+
htmlSize?: number;
|
|
154
|
+
compressedSize?: number;
|
|
155
|
+
isCompressed?: boolean;
|
|
156
|
+
}
|
|
157
|
+
export interface RuleEvidence {
|
|
158
|
+
found?: string | number | string[];
|
|
159
|
+
expected?: string | number | string[];
|
|
160
|
+
location?: string;
|
|
161
|
+
issue?: string;
|
|
162
|
+
impact?: string;
|
|
163
|
+
example?: string;
|
|
164
|
+
learnMore?: string;
|
|
165
|
+
}
|
|
166
|
+
export interface RuleResult {
|
|
167
|
+
id: string;
|
|
168
|
+
name: string;
|
|
169
|
+
category: RuleCategory;
|
|
170
|
+
severity: RuleSeverity;
|
|
171
|
+
status: SeoStatus;
|
|
172
|
+
message: string;
|
|
173
|
+
value?: string | number;
|
|
174
|
+
recommendation?: string;
|
|
175
|
+
evidence?: RuleEvidence;
|
|
176
|
+
details?: Record<string, unknown>;
|
|
177
|
+
}
|
|
178
|
+
export interface SeoRule {
|
|
179
|
+
id: string;
|
|
180
|
+
name: string;
|
|
181
|
+
category: RuleCategory;
|
|
182
|
+
severity: RuleSeverity;
|
|
183
|
+
description: string;
|
|
184
|
+
check: (ctx: RuleContext) => RuleResult | null;
|
|
185
|
+
}
|
|
186
|
+
export declare function createResult(rule: Pick<SeoRule, 'id' | 'name' | 'category' | 'severity'>, status: SeoStatus, message: string, options?: {
|
|
187
|
+
value?: string | number;
|
|
188
|
+
recommendation?: string;
|
|
189
|
+
evidence?: RuleEvidence;
|
|
190
|
+
details?: Record<string, unknown>;
|
|
191
|
+
}): RuleResult;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export type SeoStatus = 'pass' | 'warn' | 'fail' | 'info';
|
|
2
|
+
export interface SeoCheckEvidence {
|
|
3
|
+
found?: string | number | string[];
|
|
4
|
+
expected?: string | number | string[];
|
|
5
|
+
location?: string;
|
|
6
|
+
issue?: string;
|
|
7
|
+
impact?: string;
|
|
8
|
+
example?: string;
|
|
9
|
+
learnMore?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SeoCheckResult {
|
|
12
|
+
name: string;
|
|
13
|
+
status: SeoStatus;
|
|
14
|
+
message: string;
|
|
15
|
+
value?: string | number;
|
|
16
|
+
recommendation?: string;
|
|
17
|
+
evidence?: SeoCheckEvidence;
|
|
18
|
+
}
|
|
19
|
+
export interface HeadingInfo {
|
|
20
|
+
level: number;
|
|
21
|
+
text: string;
|
|
22
|
+
count: number;
|
|
23
|
+
}
|
|
24
|
+
export interface HeadingAnalysis {
|
|
25
|
+
structure: HeadingInfo[];
|
|
26
|
+
h1Count: number;
|
|
27
|
+
hasProperHierarchy: boolean;
|
|
28
|
+
issues: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface ContentMetrics {
|
|
31
|
+
wordCount: number;
|
|
32
|
+
characterCount: number;
|
|
33
|
+
sentenceCount: number;
|
|
34
|
+
paragraphCount: number;
|
|
35
|
+
readingTimeMinutes: number;
|
|
36
|
+
avgWordsPerSentence: number;
|
|
37
|
+
avgParagraphLength: number;
|
|
38
|
+
listCount: number;
|
|
39
|
+
strongTagCount: number;
|
|
40
|
+
emTagCount: number;
|
|
41
|
+
}
|
|
42
|
+
export interface LinkAnalysis {
|
|
43
|
+
total: number;
|
|
44
|
+
internal: number;
|
|
45
|
+
external: number;
|
|
46
|
+
nofollow: number;
|
|
47
|
+
broken: number;
|
|
48
|
+
withoutText: number;
|
|
49
|
+
}
|
|
50
|
+
export interface ImageAnalysis {
|
|
51
|
+
total: number;
|
|
52
|
+
withAlt: number;
|
|
53
|
+
withoutAlt: number;
|
|
54
|
+
lazy: number;
|
|
55
|
+
missingDimensions: number;
|
|
56
|
+
modernFormats: number;
|
|
57
|
+
altTextLengths: number[];
|
|
58
|
+
imageFilenames: string[];
|
|
59
|
+
imagesWithAsyncDecoding: number;
|
|
60
|
+
}
|
|
61
|
+
export interface SocialMetaAnalysis {
|
|
62
|
+
openGraph: {
|
|
63
|
+
present: boolean;
|
|
64
|
+
hasTitle: boolean;
|
|
65
|
+
hasDescription: boolean;
|
|
66
|
+
hasImage: boolean;
|
|
67
|
+
hasUrl: boolean;
|
|
68
|
+
issues: string[];
|
|
69
|
+
};
|
|
70
|
+
twitterCard: {
|
|
71
|
+
present: boolean;
|
|
72
|
+
hasCard: boolean;
|
|
73
|
+
hasTitle: boolean;
|
|
74
|
+
hasDescription: boolean;
|
|
75
|
+
hasImage: boolean;
|
|
76
|
+
issues: string[];
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export interface TechnicalSeo {
|
|
80
|
+
hasCanonical: boolean;
|
|
81
|
+
canonicalUrl?: string;
|
|
82
|
+
hasRobotsMeta: boolean;
|
|
83
|
+
robotsContent?: string[];
|
|
84
|
+
hasViewport: boolean;
|
|
85
|
+
hasCharset: boolean;
|
|
86
|
+
hasLang: boolean;
|
|
87
|
+
langValue?: string;
|
|
88
|
+
}
|
|
89
|
+
export interface SeoReport {
|
|
90
|
+
url: string;
|
|
91
|
+
timestamp: Date;
|
|
92
|
+
grade: string;
|
|
93
|
+
score: number;
|
|
94
|
+
checks: SeoCheckResult[];
|
|
95
|
+
title?: {
|
|
96
|
+
text: string;
|
|
97
|
+
length: number;
|
|
98
|
+
};
|
|
99
|
+
metaDescription?: {
|
|
100
|
+
text: string;
|
|
101
|
+
length: number;
|
|
102
|
+
};
|
|
103
|
+
headings: HeadingAnalysis;
|
|
104
|
+
content: ContentMetrics;
|
|
105
|
+
links: LinkAnalysis;
|
|
106
|
+
images: ImageAnalysis;
|
|
107
|
+
social: SocialMetaAnalysis;
|
|
108
|
+
technical: TechnicalSeo;
|
|
109
|
+
jsonLd: {
|
|
110
|
+
count: number;
|
|
111
|
+
types: string[];
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export interface SeoAnalyzerOptions {
|
|
115
|
+
baseUrl?: string;
|
|
116
|
+
analyzeContent?: boolean;
|
|
117
|
+
checkBrokenLinks?: boolean;
|
|
118
|
+
responseHeaders?: Record<string, string | string[]>;
|
|
119
|
+
}
|
|
120
|
+
export interface ExtractedLink {
|
|
121
|
+
href: string;
|
|
122
|
+
text: string;
|
|
123
|
+
rel?: string;
|
|
124
|
+
target?: string;
|
|
125
|
+
title?: string;
|
|
126
|
+
type?: 'internal' | 'external' | 'anchor' | 'mailto' | 'tel';
|
|
127
|
+
}
|
|
128
|
+
export interface ExtractedImage {
|
|
129
|
+
src: string;
|
|
130
|
+
alt?: string;
|
|
131
|
+
title?: string;
|
|
132
|
+
width?: number;
|
|
133
|
+
height?: number;
|
|
134
|
+
srcset?: string;
|
|
135
|
+
loading?: 'lazy' | 'eager';
|
|
136
|
+
}
|
|
137
|
+
export interface LinkAnalysis {
|
|
138
|
+
total: number;
|
|
139
|
+
internal: number;
|
|
140
|
+
external: number;
|
|
141
|
+
nofollow: number;
|
|
142
|
+
broken: number;
|
|
143
|
+
withoutText: number;
|
|
144
|
+
sponsoredLinks: number;
|
|
145
|
+
ugcLinks: number;
|
|
146
|
+
}
|
|
147
|
+
export interface ContentMetrics {
|
|
148
|
+
wordCount: number;
|
|
149
|
+
characterCount: number;
|
|
150
|
+
sentenceCount: number;
|
|
151
|
+
paragraphCount: number;
|
|
152
|
+
readingTimeMinutes: number;
|
|
153
|
+
avgWordsPerSentence: number;
|
|
154
|
+
avgParagraphLength: number;
|
|
155
|
+
listCount: number;
|
|
156
|
+
strongTagCount: number;
|
|
157
|
+
emTagCount: number;
|
|
158
|
+
fleschReadingEase?: number;
|
|
159
|
+
hasQuestionHeadings?: boolean;
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ColumnOptions {
|
|
2
|
+
minWidth?: number;
|
|
3
|
+
maxColumns?: number;
|
|
4
|
+
padding?: number;
|
|
5
|
+
prefix?: string;
|
|
6
|
+
indent?: number;
|
|
7
|
+
transform?: (item: string) => string;
|
|
8
|
+
}
|
|
9
|
+
export declare function getTerminalWidth(): number;
|
|
10
|
+
export declare function formatColumns(items: string[], options?: ColumnOptions): string;
|
|
11
|
+
export declare function formatColumnsRowMajor(items: string[], options?: ColumnOptions): string;
|
|
12
|
+
export declare function formatGroupedColumns(groups: Record<string, string[]>, options?: ColumnOptions & {
|
|
13
|
+
groupPrefix?: string;
|
|
14
|
+
}): string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function getTerminalWidth() {
|
|
2
|
+
return process.stdout.columns || 80;
|
|
3
|
+
}
|
|
4
|
+
export function formatColumns(items, options = {}) {
|
|
5
|
+
const { minWidth = 15, maxColumns = Infinity, padding = 2, prefix = '', indent = 2, transform, } = options;
|
|
6
|
+
if (items.length === 0)
|
|
7
|
+
return '';
|
|
8
|
+
const prefixedItems = items.map(item => prefix + item);
|
|
9
|
+
const maxItemWidth = Math.max(...prefixedItems.map(item => item.length));
|
|
10
|
+
const columnWidth = Math.max(minWidth, maxItemWidth + padding);
|
|
11
|
+
const terminalWidth = getTerminalWidth();
|
|
12
|
+
const availableWidth = terminalWidth - indent;
|
|
13
|
+
const calculatedColumns = Math.floor(availableWidth / columnWidth);
|
|
14
|
+
const numColumns = Math.min(Math.max(1, calculatedColumns), maxColumns, items.length);
|
|
15
|
+
const actualColumnWidth = Math.floor(availableWidth / numColumns);
|
|
16
|
+
const numRows = Math.ceil(prefixedItems.length / numColumns);
|
|
17
|
+
const lines = [];
|
|
18
|
+
const indentStr = ' '.repeat(indent);
|
|
19
|
+
for (let row = 0; row < numRows; row++) {
|
|
20
|
+
let line = indentStr;
|
|
21
|
+
for (let col = 0; col < numColumns; col++) {
|
|
22
|
+
const index = col * numRows + row;
|
|
23
|
+
if (index < prefixedItems.length) {
|
|
24
|
+
const rawItem = prefixedItems[index];
|
|
25
|
+
const displayItem = transform ? transform(rawItem) : rawItem;
|
|
26
|
+
const paddingNeeded = actualColumnWidth - rawItem.length;
|
|
27
|
+
line += displayItem + ' '.repeat(Math.max(0, paddingNeeded));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
lines.push(line.trimEnd());
|
|
31
|
+
}
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
export function formatColumnsRowMajor(items, options = {}) {
|
|
35
|
+
const { minWidth = 15, maxColumns = Infinity, padding = 2, prefix = '', indent = 2, } = options;
|
|
36
|
+
if (items.length === 0)
|
|
37
|
+
return '';
|
|
38
|
+
const prefixedItems = items.map(item => prefix + item);
|
|
39
|
+
const maxItemWidth = Math.max(...prefixedItems.map(item => item.length));
|
|
40
|
+
const columnWidth = Math.max(minWidth, maxItemWidth + padding);
|
|
41
|
+
const terminalWidth = getTerminalWidth();
|
|
42
|
+
const availableWidth = terminalWidth - indent;
|
|
43
|
+
const calculatedColumns = Math.floor(availableWidth / columnWidth);
|
|
44
|
+
const numColumns = Math.min(Math.max(1, calculatedColumns), maxColumns, items.length);
|
|
45
|
+
const actualColumnWidth = Math.floor(availableWidth / numColumns);
|
|
46
|
+
const lines = [];
|
|
47
|
+
const indentStr = ' '.repeat(indent);
|
|
48
|
+
for (let i = 0; i < prefixedItems.length; i += numColumns) {
|
|
49
|
+
let line = indentStr;
|
|
50
|
+
for (let col = 0; col < numColumns && i + col < prefixedItems.length; col++) {
|
|
51
|
+
const item = prefixedItems[i + col];
|
|
52
|
+
line += item.padEnd(actualColumnWidth);
|
|
53
|
+
}
|
|
54
|
+
lines.push(line.trimEnd());
|
|
55
|
+
}
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
export function formatGroupedColumns(groups, options = {}) {
|
|
59
|
+
const lines = [];
|
|
60
|
+
const { groupPrefix = '', ...columnOptions } = options;
|
|
61
|
+
for (const [groupName, items] of Object.entries(groups)) {
|
|
62
|
+
if (items.length === 0)
|
|
63
|
+
continue;
|
|
64
|
+
lines.push(groupPrefix + groupName + ':');
|
|
65
|
+
lines.push(formatColumns(items, columnOptions));
|
|
66
|
+
lines.push('');
|
|
67
|
+
}
|
|
68
|
+
return lines.join('\n').trimEnd();
|
|
69
|
+
}
|