Skip to content

Commit be87f9f

Browse files
feat: expose OS/stability tags in the API structure
1 parent 809c982 commit be87f9f

File tree

5 files changed

+103
-7
lines changed

5 files changed

+103
-7
lines changed

src/DocsParser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ export class DocsParser {
254254
name: typedKey.key,
255255
description: typedKey.description,
256256
required: typedKey.required,
257+
additionalTags: typedKey.additionalTags,
257258
...typedKey.type,
258259
})),
259260
};

src/ParsedDocumentation.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
export enum DocumentationTag {
2+
OS_MACOS = 'os_macos',
3+
OS_MAS = 'os_mas',
4+
OS_WINDOWS = 'os_windows',
5+
OS_LINUX = 'os_linux',
6+
STABILITY_EXPERIMENTAL = 'stability_experimental',
7+
STABILITY_DEPRECATED = 'stability_deprecated',
8+
AVAILABILITY_READONLY = 'availability_readonly',
9+
}
110
export declare type PossibleStringValue = {
211
value: string;
312
description: string;
@@ -43,6 +52,7 @@ export declare type EventParameterDocumentation = {
4352
export declare type DocumentationBlock = {
4453
name: string;
4554
description: string;
55+
additionalTags: DocumentationTag[];
4656
};
4757
export declare type MethodDocumentationBlock = DocumentationBlock & {
4858
signature: string;

src/__tests__/markdown-helpers.spec.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,43 @@ import * as fs from 'fs';
22
import * as path from 'path';
33
import MarkdownIt from 'markdown-it';
44

5-
import { safelyJoinTokens, extractStringEnum, rawTypeToTypeInformation } from '../markdown-helpers';
5+
import {
6+
safelyJoinTokens,
7+
extractStringEnum,
8+
rawTypeToTypeInformation,
9+
parseHeadingTags,
10+
} from '../markdown-helpers';
11+
import { DocumentationTag } from '../ParsedDocumentation';
612

713
describe('markdown-helpers', () => {
14+
describe('parseHeadingTags', () => {
15+
it('should return an empty array for null input', () => {
16+
expect(parseHeadingTags(null)).toEqual([]);
17+
});
18+
19+
it('should return an empty array if there are no tags in the input', () => {
20+
expect(parseHeadingTags('String thing no tags')).toEqual([]);
21+
});
22+
23+
it('should return a list of tags if there is one tag', () => {
24+
expect(parseHeadingTags(' _macOS_')).toEqual([DocumentationTag.OS_MACOS]);
25+
});
26+
27+
it('should return a list of tags if there are multiple tags', () => {
28+
expect(parseHeadingTags(' _macOS_ _Windows_ _Experimental_')).toEqual([
29+
DocumentationTag.OS_MACOS,
30+
DocumentationTag.OS_WINDOWS,
31+
DocumentationTag.STABILITY_EXPERIMENTAL,
32+
]);
33+
});
34+
35+
it('should throw an error if there is a tag not on the whitelist', () => {
36+
expect(() => parseHeadingTags(' _Awesome_')).toThrowErrorMatchingInlineSnapshot(
37+
`"heading tags must be from the whitelist: [\\"macOS\\",\\"mas\\",\\"Windows\\",\\"Linux\\",\\"Experimental\\",\\"Readonly\\",\\"Deprecated\\"]: expected [ Array(7) ] to include 'Awesome'"`,
38+
);
39+
});
40+
});
41+
842
describe('safelyJoinTokens', () => {
943
it('should join no tokens to an empty string', () => {
1044
expect(safelyJoinTokens([])).toBe('');

src/block-parsers.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect } from 'chai';
22
import Token from 'markdown-it/lib/token';
33

44
import {
5+
parseHeadingTags,
56
headingsAndContent,
67
findNextList,
78
convertListToTypedKeys,
@@ -78,14 +79,14 @@ export const _headingToMethodBlock = (
7879
): MethodDocumentationBlock | null => {
7980
if (!heading) return null;
8081

81-
const methodStringRegexp = /`(?:.+\.)?(.+?)(\(.*?\))`/g;
82+
const methodStringRegexp = /`(?:.+\.)?(.+?)(\(.*?\))`( _(?:[^_]+?)_)*/g;
8283
const methodStringMatch = methodStringRegexp.exec(heading.heading)!;
8384
methodStringRegexp.lastIndex = -1;
8485
expect(heading.heading).to.match(
8586
methodStringRegexp,
8687
'each method should have a code blocked method name',
8788
);
88-
const [, methodString, methodSignature] = methodStringMatch;
89+
const [, methodString, methodSignature, headingTags] = methodStringMatch;
8990

9091
let parameters: MethodDocumentationBlock['parameters'] = [];
9192
if (methodSignature !== '()') {
@@ -129,18 +130,19 @@ export const _headingToMethodBlock = (
129130
description: parsedDescription,
130131
parameters,
131132
returns: parsedReturnType,
133+
additionalTags: parseHeadingTags(headingTags),
132134
};
133135
};
134136

135137
export const _headingToPropertyBlock = (heading: HeadingContent): PropertyDocumentationBlock => {
136-
const propertyStringRegexp = /`(?:.+\.)?(.+?)`/g;
138+
const propertyStringRegexp = /`(?:.+\.)?(.+?)`( _(?:[^_]+?)_)*/g;
137139
const propertyStringMatch = propertyStringRegexp.exec(heading.heading)!;
138140
propertyStringRegexp.lastIndex = -1;
139141
expect(heading.heading).to.match(
140142
propertyStringRegexp,
141143
'each property should have a code blocked property name',
142144
);
143-
const [, propertyString] = propertyStringMatch;
145+
const [, propertyString, headingTags] = propertyStringMatch;
144146

145147
const { parsedDescription, parsedReturnType } = extractReturnType(
146148
findContentAfterHeadingClose(heading.content),
@@ -157,16 +159,17 @@ export const _headingToPropertyBlock = (heading: HeadingContent): PropertyDocume
157159
name: propertyString,
158160
description: parsedDescription,
159161
required: !/\(optional\)/i.test(parsedDescription),
162+
additionalTags: parseHeadingTags(headingTags),
160163
...parsedReturnType!,
161164
};
162165
};
163166

164167
export const _headingToEventBlock = (heading: HeadingContent): EventDocumentationBlock => {
165-
const eventNameRegexp = /^Event: '(.+)'/g;
168+
const eventNameRegexp = /^Event: '(.+)'( _(?:[^_]+?)_)*/g;
166169
const eventNameMatch = eventNameRegexp.exec(heading.heading)!;
167170
eventNameRegexp.lastIndex = -1;
168171
expect(heading.heading).to.match(eventNameRegexp, 'each event should have a quoted event name');
169-
const [, eventName] = eventNameMatch;
172+
const [, eventName, headingTags] = eventNameMatch;
170173

171174
expect(eventName).to.not.equal('', 'should have a non-zero-length event name');
172175

@@ -194,6 +197,7 @@ export const _headingToEventBlock = (heading: HeadingContent): EventDocumentatio
194197
name: eventName,
195198
description,
196199
parameters,
200+
additionalTags: parseHeadingTags(headingTags),
197201
};
198202
};
199203

src/markdown-helpers.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,49 @@ import {
55
PropertyDocumentationBlock,
66
MethodParameterDocumentation,
77
PossibleStringValue,
8+
DocumentationTag,
89
} from './ParsedDocumentation';
910

11+
const ALLOWED_TAGS = ['macOS', 'mas', 'Windows', 'Linux', 'Experimental', 'Readonly', 'Deprecated'];
12+
13+
export const parseHeadingTags = (tags: string | null): DocumentationTag[] => {
14+
if (!tags) return [];
15+
16+
const parsedTags: string[] = [];
17+
const matcher = / _([^_]+)_/g;
18+
let match: RegExpMatchArray | null;
19+
while ((match = matcher.exec(tags))) {
20+
expect(ALLOWED_TAGS).to.contain(
21+
match[1],
22+
`heading tags must be from the whitelist: ${JSON.stringify(ALLOWED_TAGS)}`,
23+
);
24+
parsedTags.push(match[1]);
25+
}
26+
27+
return parsedTags.map(value => {
28+
switch (value) {
29+
case 'macOS':
30+
return DocumentationTag.OS_MACOS;
31+
case 'mas':
32+
return DocumentationTag.OS_MAS;
33+
case 'Windows':
34+
return DocumentationTag.OS_WINDOWS;
35+
case 'Linux':
36+
return DocumentationTag.OS_LINUX;
37+
case 'Experimental':
38+
return DocumentationTag.STABILITY_EXPERIMENTAL;
39+
case 'Deprecated':
40+
return DocumentationTag.STABILITY_DEPRECATED;
41+
case 'Readonly':
42+
return DocumentationTag.AVAILABILITY_READONLY;
43+
default:
44+
throw new Error(
45+
`Impossible scenario detected, "${value}" is not an allowed tag but it got past the allowed tags check`,
46+
);
47+
}
48+
});
49+
};
50+
1051
export const findNextList = (tokens: Token[]) => {
1152
const start = tokens.findIndex(t => t.type === 'bullet_list_open');
1253
if (start === -1) return null;
@@ -237,6 +278,7 @@ export const rawTypeToTypeInformation = (
237278
name: typedKey.key,
238279
description: typedKey.description,
239280
required: typedKey.required,
281+
additionalTags: typedKey.additionalTags,
240282
...typedKey.type,
241283
}))
242284
: [],
@@ -271,6 +313,7 @@ export const rawTypeToTypeInformation = (
271313
name: typedKey.key,
272314
description: typedKey.description,
273315
required: typedKey.required,
316+
additionalTags: typedKey.additionalTags,
274317
...typedKey.type,
275318
}))
276319
: [],
@@ -514,6 +557,7 @@ type TypedKey = {
514557
type: TypeInformation;
515558
description: string;
516559
required: boolean;
560+
additionalTags: DocumentationTag[];
517561
};
518562

519563
type List = { items: ListItem[] };
@@ -608,6 +652,8 @@ const convertNestedListToTypedKeys = (list: List): TypedKey[] => {
608652
/ ?\(Optional\) ?/,
609653
'optionality should be defined with "(optional)", all lower case, no capital "O"',
610654
);
655+
const tagMatcher = /.+?((?: _(?:[^_]+?)_)+)/g;
656+
const tagMatch = tagMatcher.exec(rawType);
611657
const cleanedType = rawType.replace(/ ?\(optional\) ?/i, '').replace(/_.+?_/g, '');
612658
const subTypedKeys = item.nestedList ? convertNestedListToTypedKeys(item.nestedList) : null;
613659
const type = rawTypeToTypeInformation(cleanedType.trim(), rawDescription, subTypedKeys);
@@ -617,6 +663,7 @@ const convertNestedListToTypedKeys = (list: List): TypedKey[] => {
617663
key: keyToken.content,
618664
description: rawDescription.trim().replace(/^- ?/, ''),
619665
required: !isRootOptional,
666+
additionalTags: tagMatch ? parseHeadingTags(tagMatch[1]) : [],
620667
});
621668
}
622669

0 commit comments

Comments
 (0)