diff --git a/apps/docs-generator/src/UserDocCategoryBuilder.ts b/apps/docs-generator/src/UserDocCategoryBuilder.ts new file mode 100644 index 000000000..3eab32628 --- /dev/null +++ b/apps/docs-generator/src/UserDocCategoryBuilder.ts @@ -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 + } +} diff --git a/apps/docs-generator/src/UserDocMarkdownBuilder.ts b/apps/docs-generator/src/UserDocMarkdownBuilder.ts new file mode 100644 index 000000000..f6c8d5416 --- /dev/null +++ b/apps/docs-generator/src/UserDocMarkdownBuilder.ts @@ -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(); + } +} diff --git a/apps/docs-generator/src/main.ts b/apps/docs-generator/src/main.ts index a0a35cd68..59bad8aa2 100644 --- a/apps/docs-generator/src/main.ts +++ b/apps/docs-generator/src/main.ts @@ -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 @@ -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); @@ -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}`); } } diff --git a/apps/docs-generator/src/user-doc-generator.ts b/apps/docs-generator/src/user-doc-generator.ts index 771aa31dd..50fb9a4e3 100644 --- a/apps/docs-generator/src/user-doc-generator.ts +++ b/apps/docs-generator/src/user-doc-generator.ts @@ -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 @@ -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, @@ -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 @@ -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(); - } -} diff --git a/apps/docs-generator/src/util.ts b/apps/docs-generator/src/util.ts new file mode 100644 index 000000000..505f3e0b2 --- /dev/null +++ b/apps/docs-generator/src/util.ts @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +import path from 'node:path'; + +import { type BlockTypeWrapper } from '@jvalue/jayvee-language-server'; + +/** + * Returns the domain of a block type or undefined if it does not come from a domain extension in the stdlib. + */ +export function getBlockTypeDomain( + blockType: BlockTypeWrapper, +): string | undefined { + const filePath = blockType.astNode.$container?.$document?.uri.path; + + if (filePath === undefined) { + return undefined; + } + + const pathElements = filePath.split(path.sep); + + // pathElements[0] is an empty string since the path starts with / + // pathElements[1] is the stdlib directory + if (pathElements[2] === 'domain') { + return pathElements[3] ?? undefined; + } +} diff --git a/apps/docs/docs/user/block-types/.gitignore b/apps/docs/docs/user/block-types/.gitignore index 277e9bb17..85fd9530f 100644 --- a/apps/docs/docs/user/block-types/.gitignore +++ b/apps/docs/docs/user/block-types/.gitignore @@ -2,4 +2,5 @@ # # SPDX-License-Identifier: AGPL-3.0-only -*.md \ No newline at end of file +*.md +*/* \ No newline at end of file diff --git a/apps/docs/docs/user/composite-block-types.md b/apps/docs/docs/user/composite-block-types.md index e0e9df159..dcf526a66 100644 --- a/apps/docs/docs/user/composite-block-types.md +++ b/apps/docs/docs/user/composite-block-types.md @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 7 --- # Composite Block Types diff --git a/apps/docs/docs/user/expressions.md b/apps/docs/docs/user/expressions.md index a2cceb49c..5fa547f6a 100644 --- a/apps/docs/docs/user/expressions.md +++ b/apps/docs/docs/user/expressions.md @@ -1,5 +1,5 @@ --- -sidebar_position: 7 +sidebar_position: 8 --- # Expressions diff --git a/apps/docs/docs/user/transforms.md b/apps/docs/docs/user/transforms.md index 468a22cb9..002b6303d 100644 --- a/apps/docs/docs/user/transforms.md +++ b/apps/docs/docs/user/transforms.md @@ -74,4 +74,4 @@ transform StatusToBoolean { ## Applying transforms to table columns Transforms can be applied to columns of a table. -Please refer to the documentation of the [`TableTransformer` block type](./block-types/TableTransformer.md) to find out how. +Please refer to the documentation of the [`TableTransformer` block type](./block-types/builtin/TableTransformer.md) to find out how. diff --git a/libs/interpreter-lib/src/std-extension.spec.ts b/libs/interpreter-lib/src/std-extension.spec.ts index aa30d9654..1e3bba282 100644 --- a/libs/interpreter-lib/src/std-extension.spec.ts +++ b/libs/interpreter-lib/src/std-extension.spec.ts @@ -9,17 +9,19 @@ import { StdExecExtension } from '@jvalue/jayvee-extensions/std/exec'; import { type BlockTypeWrapper, createJayveeServices, - getAllBuiltinBlockTypes, + getAllReferenceableBlockTypes, initializeWorkspace, + isBuiltinBlockTypeDefinition, } from '@jvalue/jayvee-language-server'; import { NodeFileSystem } from 'langium/node'; async function loadAllBuiltinBlockTypes(): Promise { const services = createJayveeServices(NodeFileSystem).Jayvee; await initializeWorkspace(services); - return getAllBuiltinBlockTypes( + return getAllReferenceableBlockTypes( services.shared.workspace.LangiumDocuments, services.WrapperFactories, + (blockType) => isBuiltinBlockTypeDefinition(blockType), ); } diff --git a/libs/language-server/src/lib/ast/model-util.ts b/libs/language-server/src/lib/ast/model-util.ts index d243423b9..755481ed8 100644 --- a/libs/language-server/src/lib/ast/model-util.ts +++ b/libs/language-server/src/lib/ast/model-util.ts @@ -8,17 +8,17 @@ import { strict as assert } from 'assert'; import { type AstNode, AstUtils, type LangiumDocuments } from 'langium'; import { - type BuiltinBlockTypeDefinition, type BuiltinConstrainttypeDefinition, type ExportDefinition, type ExportableElement, type JayveeModel, - isBuiltinBlockTypeDefinition, + type ReferenceableBlockTypeDefinition, isBuiltinConstrainttypeDefinition, isExportDefinition, isExportableElement, isExportableElementDefinition, isJayveeModel, + isReferenceableBlockTypeDefinition, } from './generated/ast'; import { type BlockTypeWrapper, @@ -51,17 +51,20 @@ export function getNextAstNodeContainer( } /** - * Utility function that gets all builtin block types. + * Utility function that gets all referenceable block types, optionally filtered by a provided filter function. * Duplicates are only added once. * Make sure to call {@link initializeWorkspace} first so that the file system is initialized. */ -export function getAllBuiltinBlockTypes( +export function getAllReferenceableBlockTypes( documentService: LangiumDocuments, wrapperFactories: WrapperFactoryProvider, + filter: ( + blockTypeDefinition: ReferenceableBlockTypeDefinition, + ) => boolean = () => true, ): BlockTypeWrapper[] { - const allBuiltinBlockTypes: BlockTypeWrapper[] = []; - const visitedBuiltinBlockTypeDefinitions = - new Set(); + const allBlockTypes: BlockTypeWrapper[] = []; + const visitedBlockTypeDefinitions = + new Set(); documentService.all .map((document) => document.parseResult.value) @@ -69,25 +72,27 @@ export function getAllBuiltinBlockTypes( if (!isJayveeModel(parsedDocument)) { throw new Error('Expected parsed document to be a JayveeModel'); } - const allBlockTypes = AstUtils.streamAllContents(parsedDocument).filter( - isBuiltinBlockTypeDefinition, - ); - allBlockTypes.forEach((blockTypeDefinition) => { + const allReferenceableBlockTypes = AstUtils.streamAllContents( + parsedDocument, + ) + .filter(isReferenceableBlockTypeDefinition) + .filter(filter); + allReferenceableBlockTypes.forEach((blockTypeDefinition) => { const wasAlreadyVisited = - visitedBuiltinBlockTypeDefinitions.has(blockTypeDefinition); + visitedBlockTypeDefinitions.has(blockTypeDefinition); if (wasAlreadyVisited) { return; } if (wrapperFactories.BlockType.canWrap(blockTypeDefinition)) { - allBuiltinBlockTypes.push( + allBlockTypes.push( wrapperFactories.BlockType.wrap(blockTypeDefinition), ); - visitedBuiltinBlockTypeDefinitions.add(blockTypeDefinition); + visitedBlockTypeDefinitions.add(blockTypeDefinition); } }); }); - return allBuiltinBlockTypes; + return allBlockTypes; } /** diff --git a/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts b/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts index 7264c26c0..afaa7a71c 100644 --- a/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts +++ b/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts @@ -39,8 +39,8 @@ import { isValuetypeDefinition, } from '../ast/generated/ast'; import { - getAllBuiltinBlockTypes, getAllBuiltinConstraintTypes, + getAllReferenceableBlockTypes, getExportedElements, } from '../ast/model-util'; import { LspDocGenerator } from '../docs/lsp-doc-generator'; @@ -112,7 +112,7 @@ export class JayveeCompletionProvider extends DefaultCompletionProvider { context: CompletionContext, acceptor: CompletionAcceptor, ): MaybePromise { - const blockTypes = getAllBuiltinBlockTypes( + const blockTypes = getAllReferenceableBlockTypes( this.langiumDocuments, this.wrapperFactories, );