@siemens/ix-docs 0.0.0-pr-2238-20251103095443

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.
@@ -0,0 +1,111 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2025 Siemens AG
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ import fs from 'fs-extra';
10
+ import path from 'node:path';
11
+
12
+ export function getExampleNameFromRelativePath(relativeExamplePath: string) {
13
+ const lastSegment = relativeExamplePath.split('/').pop();
14
+ if (!lastSegment) {
15
+ throw new Error(`Invalid example path: ${relativeExamplePath}`);
16
+ }
17
+ return lastSegment.replace('.html', '');
18
+ }
19
+
20
+ /*
21
+ * Generic helpers to de-duplicate logic across framework example collectors.
22
+ */
23
+ export type BuildExampleFn = (
24
+ exampleName: string,
25
+ relativeExamplePath: string
26
+ ) => Promise<{ exampleName: string; example: string } | null>;
27
+
28
+ /**
29
+ * Read and parse the usage JSON file which maps component tags to arrays of example paths.
30
+ */
31
+ export async function readUsageJson(
32
+ componentUsageJsonPath: string
33
+ ): Promise<Record<string, string[]>> {
34
+ const exampleUsage = await fs.readFile(componentUsageJsonPath, 'utf-8');
35
+ return JSON.parse(exampleUsage) as Record<string, string[]>;
36
+ }
37
+
38
+ /**
39
+ * Collect examples for a tag using a provided build function.
40
+ * The build function is responsible for locating files and returning a markdown body.
41
+ */
42
+ export async function collectExamplesForTag(
43
+ usageJsonPath: string,
44
+ tag: string,
45
+ buildExample: BuildExampleFn
46
+ ): Promise<string> {
47
+ const usage = await readUsageJson(usageJsonPath);
48
+ const examples = usage[tag];
49
+
50
+ if (!examples) {
51
+ console.log(`No examples found for ${tag}`);
52
+ return '';
53
+ }
54
+
55
+ const markdown: string[] = [];
56
+
57
+ for (const relativeExamplePath of examples) {
58
+ const exampleName = getExampleNameFromRelativePath(relativeExamplePath);
59
+ const built = await buildExample(exampleName, relativeExamplePath);
60
+ if (!built) continue; // build function logged reason (e.g., file not found)
61
+ markdown.push(
62
+ [`### Example: ${built.exampleName}`, built.example].join('\n')
63
+ );
64
+ }
65
+
66
+ return markdown.join('\n\n');
67
+ }
68
+
69
+ /**
70
+ * Helper to read file content if it exists; returns null when missing.
71
+ */
72
+ export async function readIfExists(filePath: string): Promise<string | null> {
73
+ if (!fs.existsSync(filePath)) return null;
74
+ return fs.readFile(filePath, 'utf-8');
75
+ }
76
+
77
+ /**
78
+ * Compose fenced code blocks conveniently.
79
+ */
80
+ export function fenced(lang: string, code: string): string {
81
+ return ['```' + lang, code, '```'].join('\n');
82
+ }
83
+
84
+ /**
85
+ * Build an example made of multiple named sections (e.g., Typescript / HTML / CSS)
86
+ */
87
+ export function sectionedExample(
88
+ sections: Array<{
89
+ heading: string;
90
+ lang: string;
91
+ code: string | null;
92
+ }>
93
+ ): string {
94
+ return sections
95
+ .filter((s) => s.code)
96
+ .map((s) =>
97
+ [`#### ${s.heading}`, fenced(s.lang, s.code as string)].join('\n')
98
+ )
99
+ .join('\n');
100
+ }
101
+
102
+ /**
103
+ * Resolve a file by base name + extension inside a root directory.
104
+ */
105
+ export function resolveExampleFile(
106
+ root: string,
107
+ exampleBaseName: string,
108
+ extension: string
109
+ ): string {
110
+ return path.join(root, `${exampleBaseName}.${extension}`);
111
+ }
@@ -0,0 +1,24 @@
1
+ import {SinceTag, DeprecatedTag} from '@site/src/components/UI/Tags';
2
+ import FrameworkSelection from '@site/src/components/UI/FrameworkSelection';
3
+ import ApiTable from '@site/src/components/ApiTable';
4
+ import ReactMarkdown from 'react-markdown';
5
+
6
+ ## API for {{ tag }}{{#singleFramework}} ({{{ framework }}}){{/singleFramework}}
7
+
8
+ {{#hasProps}}
9
+ ### Properties
10
+
11
+ {{{ properties }}}
12
+ {{/hasProps}}
13
+
14
+ {{#hasEvents}}
15
+ ### Events
16
+
17
+ {{{ events }}}
18
+ {{/hasEvents}}
19
+
20
+ {{#hasSlots}}
21
+ ### Slot
22
+
23
+ {{{ slots }}}
24
+ {{/hasSlots}}
@@ -0,0 +1,21 @@
1
+ {{#events}}
2
+ <ApiTable id="event-{{event}}">
3
+ <ApiTable.EventHeader name="{{event}}">
4
+ {{#docsTags}}
5
+ {{{ rTag }}}
6
+ {{/docsTags}}
7
+ </ApiTable.EventHeader>
8
+
9
+ <ApiTable.Text name="Description">
10
+ <ReactMarkdown>{`{{{ docs }}}`}</ReactMarkdown>
11
+ </ApiTable.Text>
12
+
13
+ <ApiTable.Code name="Event">
14
+ {`{{{ event }}}`}
15
+ </ApiTable.Code>
16
+
17
+ <ApiTable.Code name="Detail">
18
+ {`{{{ detail }}}`}
19
+ </ApiTable.Code>
20
+ </ApiTable>
21
+ {{/events}}
@@ -0,0 +1,25 @@
1
+ react: {
2
+ <%#react%>
3
+ '<%key%>': '<%{value}%>',
4
+ <%/react%>
5
+ },
6
+ angular: {
7
+ <%#angular%>
8
+ '<%key%>': '<%{value}%>',
9
+ <%/angular%>
10
+ },
11
+ angular_standalone: {
12
+ <%#angular_standalone%>
13
+ '<%key%>': '<%{value}%>',
14
+ <%/angular_standalone%>
15
+ },
16
+ vue: {
17
+ <%#vue%>
18
+ '<%key%>': '<%{value}%>',
19
+ <%/vue%>
20
+ },
21
+ html: {
22
+ <%#html%>
23
+ '<%key%>': '<%{value}%>',
24
+ <%/html%>
25
+ }
@@ -0,0 +1,25 @@
1
+ react: {
2
+ <%#reactMarkdown%>
3
+ '<%key%>': <%{value}%>,
4
+ <%/reactMarkdown%>
5
+ },
6
+ angular: {
7
+ <%#angularMarkdown%>
8
+ '<%key%>': <%{value}%>,
9
+ <%/angularMarkdown%>
10
+ },
11
+ angular_standalone: {
12
+ <%#angularStandaloneMarkdown%>
13
+ '<%key%>': <%{value}%>,
14
+ <%/angularStandaloneMarkdown%>
15
+ },
16
+ vue: {
17
+ <%#vueMarkdown%>
18
+ '<%key%>': <%{value}%>,
19
+ <%/vueMarkdown%>
20
+ },
21
+ html: {
22
+ <%#htmlMarkdown%>
23
+ '<%key%>': <%{value}%>,
24
+ <%/htmlMarkdown%>
25
+ }
@@ -0,0 +1,14 @@
1
+ import Playground from '@site/src/components/Playground'
2
+
3
+ <%#imports%>
4
+ <%{.}%>
5
+ <%/imports%>
6
+
7
+ <Playground
8
+ name="<%name%>"
9
+ <%#isPreviewAvailable%>noPreview<%/isPreviewAvailable%>
10
+ source={{<%> markdown%>}}
11
+ files={{<%> files%>}}
12
+ height={props.height}
13
+ onlyFramework={props.onlyFramework}
14
+ ></Playground>
@@ -0,0 +1,30 @@
1
+ {{#props}}
2
+ <ApiTable id="property-{{name}}">
3
+ <ApiTable.PropertyHeader name="{{name}}" singleFramework="{{singleFramework}}">
4
+ {{#docsTags}}
5
+ {{{ rTag }}}
6
+ {{/docsTags}}
7
+ </ApiTable.PropertyHeader>
8
+
9
+ <ApiTable.Text name="Description">
10
+ <ReactMarkdown>{`{{{ docs }}}`}</ReactMarkdown>
11
+ </ApiTable.Text>
12
+
13
+ {{#attr}}
14
+ <ApiTable.Code name="Attribute">
15
+ {`{{{ attr }}}`}
16
+ </ApiTable.Code>
17
+ {{/attr}}
18
+
19
+ <ApiTable.Code name="Type">
20
+ {`{{{ type }}}`}
21
+ </ApiTable.Code>
22
+
23
+ {{#default}}
24
+ <ApiTable.Code name="Default">
25
+ {`{{{ default }}}`}
26
+ </ApiTable.Code>
27
+ {{/default}}
28
+
29
+ </ApiTable>
30
+ {{/props}}
@@ -0,0 +1,14 @@
1
+ {{#slots}}
2
+ <ApiTable id="slot-{{name}}">
3
+ <ApiTable.SlotHeader name="{{name}}">
4
+ {{#docsTags}}
5
+ {{{ rTag }}}
6
+ {{/docsTags}}
7
+ </ApiTable.SlotHeader>
8
+
9
+ <ApiTable.Text name="Description">
10
+ <ReactMarkdown>{ `{{{ docs }}}` }</ReactMarkdown>
11
+ </ApiTable.Text>
12
+ </ApiTable>
13
+ {{/slots}}
14
+
@@ -0,0 +1,9 @@
1
+ import {
2
+ SinceTag,
3
+ DeprecatedTag,
4
+ FormReady,
5
+ } from '@site/src/components/UI/Tags';
6
+
7
+ {{#docsTags}}
8
+ {{{ rTag }}}
9
+ {{/docsTags}}
@@ -0,0 +1,3 @@
1
+ ```{{type}}
2
+ {{{code}}}
3
+ ```
@@ -0,0 +1,307 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2024 Siemens AG
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+ import path from 'path';
11
+ import fs from 'fs-extra';
12
+ import Mustache from 'mustache';
13
+ import {
14
+ Application,
15
+ IntrinsicType,
16
+ ReferenceType,
17
+ TSConfigReader,
18
+ UnionType,
19
+ } from 'typedoc';
20
+ import { toKebabCase } from './utils/string-utils';
21
+
22
+ export type TypeDocTarget = {
23
+ name: string;
24
+ properties: TypeDocProperty[];
25
+ type: 'Function' | 'Type';
26
+ source: string;
27
+ };
28
+
29
+ export type TypeDocProperty = {
30
+ name: string;
31
+ defaultValue?: string;
32
+ type: string;
33
+ comment: string;
34
+ tags: Array<{ tag: string; text?: string }>;
35
+ };
36
+
37
+ async function generateDocsForEntrypoint(
38
+ entrypoint: string,
39
+ targetPath: string
40
+ ) {
41
+ const __root = path.resolve(__dirname, '../');
42
+ const __templates = path.join(__dirname, 'templates');
43
+ const tsconfig = path.join(
44
+ __dirname,
45
+ '..',
46
+ '..',
47
+ '..',
48
+ 'tsconfig.typedoc.json'
49
+ );
50
+
51
+ const app = await Application.bootstrap({
52
+ tsconfig: tsconfig,
53
+ skipErrorChecking: true,
54
+ entryPoints: [entrypoint],
55
+ });
56
+
57
+ app.options.addReader(new TSConfigReader());
58
+
59
+ const project = await app.convert();
60
+
61
+ if (!project) {
62
+ throw new Error(`No project generated for ${entrypoint}`);
63
+ }
64
+
65
+ const types = processProjectChildren(project, __root);
66
+ await generateTypeDocs(types, targetPath, __templates);
67
+ }
68
+
69
+ function getPropertyType(property: any): string {
70
+ if (property.type instanceof IntrinsicType) {
71
+ return property.type.name;
72
+ } else if (property.type instanceof ReferenceType) {
73
+ return property.type.qualifiedName;
74
+ } else if (property.type instanceof UnionType) {
75
+ return property.type.types
76
+ .filter((t: any) => 'name' in t)
77
+ .map((t: any) => t.name)
78
+ .join(' | ');
79
+ } else {
80
+ console.log(`=== Type ${property.name} is unknown`);
81
+ return 'unknown';
82
+ }
83
+ }
84
+
85
+ function extractCommentTags(
86
+ property: any
87
+ ): Array<{ tag: string; text?: string }> {
88
+ const tags: Array<{ tag: string; text?: string }> = [];
89
+
90
+ if (!property.comment?.blockTags) {
91
+ return tags;
92
+ }
93
+
94
+ for (const tag of property.comment.blockTags) {
95
+ const tagName = tag.tag.substring(1); // Remove @ symbol
96
+ const tagText = tag.content
97
+ .filter((content: any) => content.kind === 'text')
98
+ .map((content: any) => content.text)
99
+ .join('');
100
+
101
+ tags.push({ tag: tagName, text: tagText });
102
+ }
103
+
104
+ return tags;
105
+ }
106
+
107
+ function getCommentSummary(property: any): string {
108
+ const summary =
109
+ property?.comment?.summary ?? property?.signatures?.[0]?.comment?.summary;
110
+
111
+ if (summary) {
112
+ return summary
113
+ .filter((summary: any) => summary.kind === 'text')
114
+ .map((summary: any) => summary.text)
115
+ .join('');
116
+ }
117
+
118
+ return '';
119
+ }
120
+
121
+ function processProperties(child: any): TypeDocProperty[] {
122
+ const properties: TypeDocProperty[] = [];
123
+
124
+ if (!child?.children) {
125
+ return properties;
126
+ }
127
+
128
+ for (const property of child.children) {
129
+ const type = getPropertyType(property);
130
+ const tags = extractCommentTags(property);
131
+ const comment = getCommentSummary(property);
132
+
133
+ properties.push({
134
+ name: property.name,
135
+ defaultValue: property.defaultValue,
136
+ type,
137
+ comment,
138
+ tags,
139
+ });
140
+ }
141
+
142
+ return properties;
143
+ }
144
+
145
+ function processProjectChildren(
146
+ project: any,
147
+ rootPath: string
148
+ ): TypeDocTarget[] {
149
+ const types: TypeDocTarget[] = [];
150
+
151
+ if (!project.children) {
152
+ return types;
153
+ }
154
+
155
+ for (const child of project.children) {
156
+ const source = path.relative(rootPath, child.sources![0].fullFileName);
157
+
158
+ const properties = processProperties(child);
159
+
160
+ types.push({
161
+ name: child.name,
162
+ properties,
163
+ type: 'Type',
164
+ source,
165
+ });
166
+ }
167
+
168
+ return types;
169
+ }
170
+
171
+ function determineFramework(source: string): string | undefined {
172
+ const frameworks = ['react', 'angular', 'vue'];
173
+
174
+ for (const framework of frameworks) {
175
+ if (source.includes(framework)) {
176
+ return framework;
177
+ }
178
+ }
179
+
180
+ return undefined;
181
+ }
182
+
183
+ async function generateTypeDocs(
184
+ types: TypeDocTarget[],
185
+ targetPath: string,
186
+ templatesPath: string
187
+ ) {
188
+ const utilsPath = path.join(targetPath, 'utils');
189
+ await fs.ensureDir(utilsPath);
190
+
191
+ for (const typedoc of types) {
192
+ const current_framework = determineFramework(typedoc.source);
193
+ const mdxContent = generateStructuredMDX(
194
+ typedoc,
195
+ templatesPath,
196
+ current_framework
197
+ );
198
+
199
+ const outputPath = path.join(utilsPath, `${toKebabCase(typedoc.name)}.mdx`);
200
+ console.log(`Generating TypeDoc: ${outputPath}`);
201
+ await fs.writeFile(outputPath, mdxContent);
202
+
203
+ if (global.gc) {
204
+ global.gc();
205
+ }
206
+ }
207
+ }
208
+
209
+ function convertTagsToTSXElements(tags: Array<{ tag: string; text?: string }>) {
210
+ return tags
211
+ .map((tag) => {
212
+ if (tag.tag === 'deprecated') {
213
+ return {
214
+ rTag: `<DeprecatedTag message={\`${escapeBackticks(
215
+ tag.text || ''
216
+ )}\`} />`,
217
+ };
218
+ } else if (tag.tag === 'since') {
219
+ return {
220
+ rTag: `<SinceTag version={\`${escapeBackticks(tag.text || '')}\`} />`,
221
+ };
222
+ }
223
+ return null;
224
+ })
225
+ .filter(Boolean);
226
+ }
227
+
228
+ function generateStructuredMDX(
229
+ typedoc: TypeDocTarget,
230
+ templatesPath: string,
231
+ framework?: string
232
+ ): string {
233
+ const propertyTemplate = fs.readFileSync(
234
+ path.join(templatesPath, 'property-table.mustache'),
235
+ 'utf-8'
236
+ );
237
+ const apiTemplate = fs.readFileSync(
238
+ path.join(templatesPath, 'api.mustache'),
239
+ 'utf-8'
240
+ );
241
+
242
+ const kebabName = `ix-${toKebabCase(typedoc.name)}`;
243
+
244
+ const formattedProps = typedoc.properties.map((prop) => {
245
+ return {
246
+ name: prop.name,
247
+ singleFramework: framework,
248
+ docs: escapeBackticks(prop.comment),
249
+ type: escapeBackticks(prop.type),
250
+ default: prop.defaultValue
251
+ ? escapeBackticks(prop.defaultValue)
252
+ : undefined,
253
+ attr: prop.name,
254
+ docsTags: convertTagsToTSXElements(prop.tags),
255
+ };
256
+ });
257
+
258
+ const propertyOutput = Mustache.render(propertyTemplate, {
259
+ tag: kebabName,
260
+ props: formattedProps,
261
+ });
262
+
263
+ return Mustache.render(apiTemplate, {
264
+ tag: kebabName,
265
+ hasProps: typedoc.properties.length > 0,
266
+ hasEvents: false,
267
+ hasSlots: false,
268
+ singleFramework: framework,
269
+ framework: framework?.charAt(0).toUpperCase()! + framework?.slice(1),
270
+ properties: propertyOutput,
271
+ events: '',
272
+ slots: '',
273
+ });
274
+ }
275
+
276
+ function escapeBackticks(str: string): string {
277
+ if (!str) return '';
278
+ return str.replace(/`/g, '\\`');
279
+ }
280
+
281
+ export async function generateTypeScriptDocs(
282
+ classPaths: string[],
283
+ targetPath: string
284
+ ) {
285
+ console.log('Generating TypeScript API docs');
286
+
287
+ const BATCH_SIZE = 5;
288
+ const batches = [];
289
+
290
+ for (let i = 0; i < classPaths.length; i += BATCH_SIZE) {
291
+ batches.push(classPaths.slice(i, i + BATCH_SIZE));
292
+ }
293
+
294
+ for (let i = 0; i < batches.length; i++) {
295
+ const batch = batches[i];
296
+ console.log(
297
+ `Processing batch ${i + 1}/${batches.length} (${batch.length} files)`
298
+ );
299
+ for (const classPath of batch) {
300
+ await generateDocsForEntrypoint(classPath, targetPath);
301
+
302
+ if (global.gc) {
303
+ global.gc();
304
+ }
305
+ }
306
+ }
307
+ }
@@ -0,0 +1,43 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2024 Siemens AG
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+ import { escapeMarkdown } from './escape';
11
+
12
+ export function convertDocsTagsToTSXElement(
13
+ tagName: string,
14
+ docsTags: {
15
+ name: string;
16
+ text?: string;
17
+ }[]
18
+ ) {
19
+ return docsTags.map((tag) => {
20
+ const { name, text } = tag;
21
+ const escapedText = escapeMarkdown(text ?? '').replace(/`/g, '\\`');
22
+ let template = '';
23
+ if (name === 'since') {
24
+ template = `<SinceTag message={\`${escapedText}\`} />`;
25
+ }
26
+
27
+ if (name === 'deprecated') {
28
+ template = `<DeprecatedTag message={\`${escapedText}\`} />`;
29
+ }
30
+
31
+ if (name === 'form-ready') {
32
+ template = `<FormReady message={\`${escapedText}\`} />`;
33
+ }
34
+
35
+ if (template === '') {
36
+ console.warn(`Unknown tag: ${name} within ${tagName}`);
37
+ }
38
+
39
+ return {
40
+ rTag: template,
41
+ };
42
+ });
43
+ }
@@ -0,0 +1,54 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2024 Siemens AG
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ export function escapeMarkdown(markdown: string) {
10
+ let replacedMarkdown = markdown;
11
+ const replacemenets: [RegExp, string][] = [
12
+ [/`/g, '\\`'],
13
+ [/\*/g, '\\*'],
14
+ [/#/g, '\\#'],
15
+ [/\//g, '\\/'],
16
+ [/\(/g, '\\('],
17
+ [/\)/g, '\\)'],
18
+ [/\[/g, '\\['],
19
+ [/\]/g, '\\]'],
20
+ [/</g, '&lt;'],
21
+ [/>/g, '&gt;'],
22
+ [/_/g, '\\_'],
23
+ [/`/g, '\\`'],
24
+ [/\|/g, '\uff5c'],
25
+ ];
26
+
27
+ replacemenets.forEach((replace) => {
28
+ const [source, target] = replace;
29
+ replacedMarkdown = markdown.replace(source, target);
30
+ });
31
+
32
+ return replacedMarkdown;
33
+ }
34
+
35
+ export function removeTypescriptHeaderComments(str: string) {
36
+ return str.replaceAll(/\/\*[\s\S]*?\*\//g, '');
37
+ }
38
+
39
+ export function removeHTMLComments(str: string) {
40
+ return str.replaceAll(/<!--[\s\S]*?-->/g, '');
41
+ }
42
+
43
+ export function parseJSDocsToMarkdown(str: string) {
44
+ const linkRegex = /{\@link (.*?)}/g;
45
+ const markdown = str.replaceAll(linkRegex, (_, url) => {
46
+ return `[${url}](${url})`;
47
+ });
48
+
49
+ return markdown;
50
+ }
51
+
52
+ export function escapeBackticks(str: string) {
53
+ return str.replaceAll(/`/g, '\\`');
54
+ }
@@ -0,0 +1,7 @@
1
+ export function toKebabCase(input: string) {
2
+ return input
3
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
4
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
5
+ .trim()
6
+ .toLowerCase();
7
+ }