Skip to content

Commit

Permalink
Autogenerate docs and autocompletion for composite block types (#632)
Browse files Browse the repository at this point in the history
Autogenerate docs and autocompletion for composite block types
  • Loading branch information
rhazn authored Jan 7, 2025
1 parent e3e6de9 commit 2029836
Show file tree
Hide file tree
Showing 12 changed files with 277 additions and 132 deletions.
70 changes: 70 additions & 0 deletions apps/docs-generator/src/UserDocCategoryBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg
//
// SPDX-License-Identifier: AGPL-3.0-only

import fs from 'node:fs';
import path from 'node:path';

export class UserDocCategoryBuilder {
generateDocsCategory(
rootPath: string,
dirName: string,
label: string,
position: number,
description: string,
): string {
const categoryPath = path.join(rootPath, dirName);

fs.mkdirSync(categoryPath, { recursive: true });

fs.writeFileSync(
path.join(categoryPath, '_category_.json'),
this.getCategoryJSONString(label, position, description),
);

fs.writeFileSync(
path.join(categoryPath, '_category_.json.license'),
this.getCategoryLicenseString(new Date().getFullYear()),
);

fs.writeFileSync(
path.join(categoryPath, '.gitignore'),
this.getCategoryGitignoreString(new Date().getFullYear()),
);

return categoryPath;
}

private getCategoryJSONString(
label: string,
position: number,
description: string,
): string {
return `{
"label": "${label}",
"position": ${position},
"link": {
"type": "generated-index",
"description": "${description}"
}
}`;
}

private getCategoryLicenseString(year: number): string {
// REUSE-IgnoreStart
return `SPDX-FileCopyrightText: ${year} Friedrich-Alexander-Universitat Erlangen-Nurnberg
SPDX-License-Identifier: AGPL-3.0-only`;
// REUSE-IgnoreEnd
}

private getCategoryGitignoreString(year: number): string {
// REUSE-IgnoreStart
return `# SPDX-FileCopyrightText: ${year} Friedrich-Alexander-Universitat Erlangen-Nurnberg
#
# SPDX-License-Identifier: AGPL-3.0-only
*.md`;
// REUSE-IgnoreEnd
}
}
107 changes: 107 additions & 0 deletions apps/docs-generator/src/UserDocMarkdownBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg
//
// SPDX-License-Identifier: AGPL-3.0-only

import {
type ExampleDoc,
type IOType,
MarkdownBuilder,
type PropertySpecification,
} from '@jvalue/jayvee-language-server';

export class UserDocMarkdownBuilder {
private markdownBuilder = new MarkdownBuilder();

docTitle(blockType: string): UserDocMarkdownBuilder {
this.markdownBuilder
.line('---')
.line(`title: ${blockType}`)
.line('---')
.newLine();
return this;
}

generationComment(): UserDocMarkdownBuilder {
this.markdownBuilder
.comment(
'Do NOT change this document as it is auto-generated from the language server',
)
.newLine();
return this;
}

heading(heading: string, depth = 1): UserDocMarkdownBuilder {
this.markdownBuilder.heading(heading, depth);
return this;
}

propertyHeading(propertyName: string, depth = 1): UserDocMarkdownBuilder {
this.markdownBuilder.heading(`\`${propertyName}\``, depth);
return this;
}

propertySpec(propertySpec: PropertySpecification): UserDocMarkdownBuilder {
this.markdownBuilder.line(`Type \`${propertySpec.type.getName()}\``);
if (propertySpec.defaultValue !== undefined) {
this.markdownBuilder
.newLine()
.line(`Default: \`${JSON.stringify(propertySpec.defaultValue)}\``);
}
this.markdownBuilder.newLine();
return this;
}

ioTypes(inputType: IOType, outputType: IOType): UserDocMarkdownBuilder {
this.markdownBuilder
.line(`Input type: \`${inputType}\``)
.newLine()
.line(`Output type: \`${outputType}\``)
.newLine();
return this;
}

compatibleValueType(type: string): UserDocMarkdownBuilder {
this.markdownBuilder.line(`Compatible value type: ${type}`);
this.markdownBuilder.newLine();
return this;
}

description(text?: string, depth = 2): UserDocMarkdownBuilder {
if (text === undefined) {
return this;
}
this.markdownBuilder.heading('Description', depth).line(text).newLine();
return this;
}

propertiesHeading(): UserDocMarkdownBuilder {
this.markdownBuilder.heading('Properties', 2);
return this;
}

validation(text?: string, depth = 2): UserDocMarkdownBuilder {
if (text === undefined) {
return this;
}
this.markdownBuilder.heading('Validation', depth).line(text).newLine();
return this;
}

examples(examples?: ExampleDoc[], depth = 2): UserDocMarkdownBuilder {
if (examples === undefined) {
return this;
}
for (const [index, example] of examples.entries()) {
this.markdownBuilder
.heading(`Example ${index + 1}`, depth)
.code(example.code, 'jayvee')
.line(example.description)
.newLine();
}
return this;
}

build(): string {
return this.markdownBuilder.build();
}
}
44 changes: 37 additions & 7 deletions apps/docs-generator/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg
// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg
//
// SPDX-License-Identifier: AGPL-3.0-only

Expand All @@ -9,13 +9,15 @@ import { fileURLToPath } from 'node:url';
import {
type JayveeServices,
createJayveeServices,
getAllBuiltinBlockTypes,
getAllBuiltinConstraintTypes,
getAllReferenceableBlockTypes,
initializeWorkspace,
} from '@jvalue/jayvee-language-server';
import { NodeFileSystem } from 'langium/node';

import { UserDocGenerator } from './user-doc-generator';
import { getBlockTypeDomain } from './util';
import { UserDocCategoryBuilder } from './UserDocCategoryBuilder';

/** ESM does not know __filename and __dirname, so defined here */
const __filename = fileURLToPath(import.meta.url);
Expand All @@ -38,21 +40,49 @@ function generateBlockTypeDocs(
services: JayveeServices,
docsAppPath: string,
): void {
const blockTypes = getAllBuiltinBlockTypes(
const blockTypes = getAllReferenceableBlockTypes(
services.shared.workspace.LangiumDocuments,
services.WrapperFactories,
);

const docsPath = join(docsAppPath, 'docs', 'user', 'block-types');

const domainsGenerated: string[] = [];

const userCategoryBuilder = new UserDocCategoryBuilder();
userCategoryBuilder.generateDocsCategory(
docsPath,
'builtin',
'Built-in Blocks',
0,
`Built-in Blocks.`,
);

for (const blockType of blockTypes) {
const blockDomain = getBlockTypeDomain(blockType);
if (blockDomain !== undefined) {
if (!domainsGenerated.includes(blockDomain)) {
const userCategoryBuilder = new UserDocCategoryBuilder();
userCategoryBuilder.generateDocsCategory(
docsPath,
blockDomain,
`Domain extension: ${blockDomain}`,
domainsGenerated.length + 1,
`Blocks from the ${blockDomain} domain extension.`,
);
domainsGenerated.push(blockDomain);
}
}
const userDocBuilder = new UserDocGenerator(services);
const blockTypeDoc = userDocBuilder.generateBlockTypeDoc(blockType);

const fileName = `${blockType.type}.md`;
writeFileSync(join(docsPath, fileName), blockTypeDoc, {
flag: 'w',
});
writeFileSync(
join(docsPath, blockDomain ?? 'builtin', fileName),
blockTypeDoc,
{
flag: 'w',
},
);
console.info(`Generated file ${fileName}`);
}
}
Expand Down
106 changes: 4 additions & 102 deletions apps/docs-generator/src/user-doc-generator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg
// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg
//
// SPDX-License-Identifier: AGPL-3.0-only

Expand All @@ -9,16 +9,15 @@ import {
type BlockTypeWrapper,
type ConstraintTypeWrapper,
type ExampleDoc,
type IOType,
type JayveeBlockTypeDocGenerator,
type JayveeConstraintTypeDocGenerator,
type JayveeServices,
type JayveeValueTypesDocGenerator,
MarkdownBuilder,
type PrimitiveValueType,
type PropertySpecification,
} from '@jvalue/jayvee-language-server';

import { UserDocMarkdownBuilder } from './UserDocMarkdownBuilder';

export class UserDocGenerator
implements
JayveeBlockTypeDocGenerator,
Expand All @@ -45,7 +44,7 @@ that fullfil [_constraints_](./primitive-value-types#constraints).`.trim(),
.filter((valueType) => valueType.isReferenceableByUser())
.forEach((valueType) => {
assert(
valueType.getUserDoc(),
valueType.getUserDoc() !== undefined,
`Documentation is missing for user extendable value type: ${valueType.getName()}`,
);
builder
Expand Down Expand Up @@ -181,100 +180,3 @@ block ExampleTableInterpreter oftype TableInterpreter {
};
}
}

class UserDocMarkdownBuilder {
private markdownBuilder = new MarkdownBuilder();

docTitle(blockType: string): UserDocMarkdownBuilder {
this.markdownBuilder
.line('---')
.line(`title: ${blockType}`)
.line('---')
.newLine();
return this;
}

generationComment(): UserDocMarkdownBuilder {
this.markdownBuilder
.comment(
'Do NOT change this document as it is auto-generated from the language server',
)
.newLine();
return this;
}

heading(heading: string, depth = 1): UserDocMarkdownBuilder {
this.markdownBuilder.heading(heading, depth);
return this;
}

propertyHeading(propertyName: string, depth = 1): UserDocMarkdownBuilder {
this.markdownBuilder.heading(`\`${propertyName}\``, depth);
return this;
}

propertySpec(propertySpec: PropertySpecification): UserDocMarkdownBuilder {
this.markdownBuilder.line(`Type \`${propertySpec.type.getName()}\``);
if (propertySpec.defaultValue !== undefined) {
this.markdownBuilder
.newLine()
.line(`Default: \`${JSON.stringify(propertySpec.defaultValue)}\``);
}
this.markdownBuilder.newLine();
return this;
}

ioTypes(inputType: IOType, outputType: IOType): UserDocMarkdownBuilder {
this.markdownBuilder
.line(`Input type: \`${inputType}\``)
.newLine()
.line(`Output type: \`${outputType}\``)
.newLine();
return this;
}

compatibleValueType(type: string): UserDocMarkdownBuilder {
this.markdownBuilder.line(`Compatible value type: ${type}`);
this.markdownBuilder.newLine();
return this;
}

description(text?: string, depth = 2): UserDocMarkdownBuilder {
if (text === undefined) {
return this;
}
this.markdownBuilder.heading('Description', depth).line(text).newLine();
return this;
}

propertiesHeading(): UserDocMarkdownBuilder {
this.markdownBuilder.heading('Properties', 2);
return this;
}

validation(text?: string, depth = 2): UserDocMarkdownBuilder {
if (text === undefined) {
return this;
}
this.markdownBuilder.heading('Validation', depth).line(text).newLine();
return this;
}

examples(examples?: ExampleDoc[], depth = 2): UserDocMarkdownBuilder {
if (examples === undefined) {
return this;
}
for (const [index, example] of examples.entries()) {
this.markdownBuilder
.heading(`Example ${index + 1}`, depth)
.code(example.code, 'jayvee')
.line(example.description)
.newLine();
}
return this;
}

build(): string {
return this.markdownBuilder.build();
}
}
Loading

0 comments on commit 2029836

Please sign in to comment.