mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Add a listing of actions and conditions in community extension reference pages (#5464)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
* Launch this script to generate a list (in markdown format) of all custom extensions.
|
||||
*/
|
||||
|
||||
const initializeGDevelopJs = require('../public/libGD.js');
|
||||
const fs = require('fs').promises;
|
||||
const { default: axios } = require('axios');
|
||||
const path = require('path');
|
||||
@@ -16,8 +17,16 @@ const {
|
||||
convertCommonMarkdownToPythonMarkdown,
|
||||
} = require('./lib/PythonMarkdownHelper');
|
||||
const shell = require('shelljs');
|
||||
const {
|
||||
generateExtensionReference,
|
||||
generateExtensionRawText,
|
||||
rawTextsToString,
|
||||
} = require('./lib/ExtensionReferenceGenerator');
|
||||
const { mapVector, mapFor } = require('./lib/MapFor');
|
||||
|
||||
/** @typedef {{ tier: 'community' | 'reviewed', shortDescription: string, authorIds: Array<string>, authors?: Array<{id: string, username: string}>, extensionNamespace: string, fullName: string, name: string, version: string, gdevelopVersion?: string, url: string, headerUrl: string, tags: Array<string>, category: string, previewIconUrl: string, eventsBasedBehaviorsCount: number, eventsFunctionsCount: number, helpPath: string, description: string, iconUrl: string}} ExtensionHeader */
|
||||
/** @typedef {import("./lib/ExtensionReferenceGenerator.js").RawText} RawText */
|
||||
|
||||
/** @typedef {{ tier: 'community' | 'reviewed', shortDescription: string, authorIds: Array<string>, authors?: Array<{id: string, username: string}>, extensionNamespace: string, fullName: string, name: string, version: string, gdevelopVersion?: string, url: string, headerUrl: string, tags: Array<string>, category: string, previewIconUrl: string, eventsBasedBehaviorsCount: number, eventsFunctionsCount: number}} ExtensionShortHeader */
|
||||
|
||||
const extensionShortHeadersUrl =
|
||||
'https://api.gdevelop-app.com/asset/extension-short-header';
|
||||
@@ -30,22 +39,6 @@ const generateSvgImageIcon = iconUrl => {
|
||||
return `<img src="${iconUrl}" class="extension-icon"></img>`;
|
||||
};
|
||||
|
||||
/** @returns {string} */
|
||||
const generateExtensionFooterText = fullName => {
|
||||
return (
|
||||
`
|
||||
---
|
||||
|
||||
!!! tip
|
||||
|
||||
Learn [how to install new extensions](/gdevelop5/extensions/search) by following a step-by-step guide.
|
||||
|
||||
*This page is an auto-generated reference page about the **${fullName}** extension, made by the community of [GDevelop, the open-source, cross-platform game engine designed for everyone](https://gdevelop.io/).*` +
|
||||
' ' +
|
||||
'Learn more about [all GDevelop community-made extensions here](/gdevelop5/extensions).'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{id: string, username: string}[]} authors
|
||||
*/
|
||||
@@ -63,33 +56,94 @@ const generateAuthorNamesWithLinks = authors => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the list of all extensions and their associated short headers
|
||||
* Add a serialized (JS object) events function extension to the project.
|
||||
*
|
||||
* (useful as containing author public profiles information).
|
||||
* @returns {Promise<Array<ExtensionHeader>>} A promise to all extension headers
|
||||
*
|
||||
* @param {any} gd
|
||||
* @param {any} project (gdProject)
|
||||
*
|
||||
* @returns {Promise<any>} A promise to all extensions (gdEventsFunctionsExtension)
|
||||
*/
|
||||
const getAllExtensionHeaders = async () => {
|
||||
const addAllExtensionsToProject = async (gd, project) => {
|
||||
const response = await axios.get(extensionShortHeadersUrl);
|
||||
const extensionShortHeaders = response.data;
|
||||
if (!extensionShortHeaders.length) {
|
||||
throw new Error('Unexpected response from the extension endpoint.');
|
||||
}
|
||||
|
||||
const extensionHeaders = await Promise.all(
|
||||
return await Promise.all(
|
||||
extensionShortHeaders.map(async extensionShortHeader => {
|
||||
const response = await axios.get(extensionShortHeader.headerUrl);
|
||||
const extensionHeader = response.data;
|
||||
if (!extensionHeader) {
|
||||
const response = await axios.get(extensionShortHeader.url);
|
||||
const serializedExtension = response.data;
|
||||
if (!serializedExtension) {
|
||||
throw new Error(
|
||||
`Unexpected response when fetching an extension header (${
|
||||
extensionShortHeader.headerUrl
|
||||
`Unexpected response when fetching an extension (${
|
||||
extensionShortHeader.url
|
||||
}).`
|
||||
);
|
||||
}
|
||||
return { ...extensionHeader, ...extensionShortHeader };
|
||||
|
||||
const { name } = serializedExtension;
|
||||
if (!name)
|
||||
return Promise.reject(new Error('Malformed extension (missing name).'));
|
||||
|
||||
const newEventsFunctionsExtension = project.insertNewEventsFunctionsExtension(
|
||||
name,
|
||||
0
|
||||
);
|
||||
unserializeFromJSObject(
|
||||
gd,
|
||||
newEventsFunctionsExtension,
|
||||
serializedExtension,
|
||||
'unserializeFrom',
|
||||
project
|
||||
);
|
||||
|
||||
return newEventsFunctionsExtension;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return extensionHeaders;
|
||||
/**
|
||||
* Tool function to restore a serializable object from a JS object.
|
||||
* Most gd.* objects are "serializable", meaning they have a serializeTo
|
||||
* and unserializeFrom method.
|
||||
* @param {any} serializable A gd.* object to restore (gdSerializable)
|
||||
* @param {Object} object The JS object to be used to restore the serializable.
|
||||
* @param {string} methodName The name of the unserialization method. "unserializeFrom" by default
|
||||
* @param {?any} optionalProject The project to pass as argument for unserialization (gdProject)
|
||||
*/
|
||||
function unserializeFromJSObject(
|
||||
gd,
|
||||
serializable,
|
||||
object,
|
||||
methodName = 'unserializeFrom',
|
||||
optionalProject = undefined
|
||||
) {
|
||||
const serializedElement = gd.Serializer.fromJSObject(object);
|
||||
if (!optionalProject) {
|
||||
serializable[methodName](serializedElement);
|
||||
} else {
|
||||
// It's not uncommon for unserializeFrom methods of gd.* classes
|
||||
// to require the project to be passed as first argument.
|
||||
serializable[methodName](optionalProject, serializedElement);
|
||||
}
|
||||
serializedElement.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of all extensions and their associated short headers
|
||||
* (useful as containing author public profiles information).
|
||||
* @returns {Promise<Array<ExtensionShortHeader>>} A promise to all extension headers
|
||||
*/
|
||||
const getAllExtensionShortHeaders = async () => {
|
||||
const response = await axios.get(extensionShortHeadersUrl);
|
||||
const extensionShortHeaders = response.data;
|
||||
if (!extensionShortHeaders.length) {
|
||||
throw new Error('Unexpected response from the extension endpoint.');
|
||||
}
|
||||
return extensionShortHeaders;
|
||||
};
|
||||
|
||||
const groupBy = (array, getKey) => {
|
||||
@@ -116,42 +170,39 @@ const sortKeys = table => {
|
||||
|
||||
/**
|
||||
* Create a page for an extension.
|
||||
* @param {ExtensionHeader} extensionHeader The extension header
|
||||
* @param {any} gd
|
||||
* @param {any} project (gdProject)
|
||||
* @param {any} extension The extension (gdEventsFunctionsExtension)
|
||||
* @param {ExtensionShortHeader} extensionShortHeader
|
||||
* @param {boolean} isCommunity The tier
|
||||
*/
|
||||
const createExtensionReferencePage = async (extensionHeader, isCommunity) => {
|
||||
const folderName = getExtensionFolderName(extensionHeader.name);
|
||||
const referencePageUrl = `${gdevelopWikiUrlRoot}/extensions/${folderName}`;
|
||||
const helpPageUrl = getHelpLink(extensionHeader.helpPath) || referencePageUrl;
|
||||
const authorNamesWithLinks = generateAuthorNamesWithLinks(
|
||||
extensionHeader.authors || []
|
||||
const createExtensionReferencePage = async (
|
||||
gd,
|
||||
project,
|
||||
extension,
|
||||
extensionShortHeader,
|
||||
isCommunity
|
||||
) => {
|
||||
const extensionMetadata = generateEventsFunctionExtensionMetadata(
|
||||
gd,
|
||||
project,
|
||||
extension
|
||||
);
|
||||
const extensionReference = generateExtensionReference(extensionMetadata);
|
||||
const referencePageContent = rawTextsToString(
|
||||
generateExtensionRawText(
|
||||
extensionReference,
|
||||
reference =>
|
||||
generateExtensionHeaderText(
|
||||
reference,
|
||||
extensionShortHeader,
|
||||
isCommunity
|
||||
),
|
||||
generateExtensionFooterText
|
||||
)
|
||||
);
|
||||
|
||||
const referencePageContent =
|
||||
`# ${extensionHeader.fullName}` +
|
||||
'\n\n' +
|
||||
generateSvgImageIcon(extensionHeader.previewIconUrl) +
|
||||
'\n' +
|
||||
`${extensionHeader.shortDescription}\n` +
|
||||
'\n' +
|
||||
`**Authors and contributors** to this community extension: ${authorNamesWithLinks}.\n` +
|
||||
'\n' +
|
||||
(isCommunity
|
||||
? `!!! warning
|
||||
This is an extension made by a community member — but not reviewed
|
||||
by the GDevelop extension team. As such, we can't guarantee it
|
||||
meets all the quality standards of official extensions. In case of
|
||||
doubt, contact the author to know more about what the extension
|
||||
does or inspect its content before using it.
|
||||
\n\n`
|
||||
: '') +
|
||||
'---\n' +
|
||||
'\n' +
|
||||
convertCommonMarkdownToPythonMarkdown(extensionHeader.description) +
|
||||
'\n' +
|
||||
(extensionHeader.helpPath ? `\n[Read more...](${helpPageUrl})\n` : ``) +
|
||||
generateExtensionFooterText(extensionHeader.fullName);
|
||||
|
||||
const folderName = getExtensionFolderName(extension.getName());
|
||||
const extensionReferenceFilePath = path.join(
|
||||
extensionsRootPath,
|
||||
folderName,
|
||||
@@ -165,26 +216,155 @@ const createExtensionReferencePage = async (extensionHeader, isCommunity) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a section for an extension.
|
||||
* @param {ExtensionHeader} extensionHeader The extension header
|
||||
*
|
||||
* @param {{ extension: any }} extension (gdPlatformExtension)
|
||||
* @param {ExtensionShortHeader} extensionShortHeader
|
||||
* @param {boolean} isCommunity
|
||||
* @returns {RawText}
|
||||
*/
|
||||
const generateExtensionSection = extensionHeader => {
|
||||
const folderName = getExtensionFolderName(extensionHeader.name);
|
||||
const generateExtensionHeaderText = (
|
||||
{ extension },
|
||||
extensionShortHeader,
|
||||
isCommunity
|
||||
) => {
|
||||
const folderName = getExtensionFolderName(extension.getName());
|
||||
const referencePageUrl = `${gdevelopWikiUrlRoot}/extensions/${folderName}`;
|
||||
const helpPageUrl = getHelpLink(extensionHeader.helpPath) || referencePageUrl;
|
||||
const helpPageUrl = getHelpLink(extension.getHelpPath()) || referencePageUrl;
|
||||
const authorNamesWithLinks = generateAuthorNamesWithLinks(
|
||||
extensionShortHeader.authors || []
|
||||
);
|
||||
|
||||
return `|${generateSvgImageIcon(extensionHeader.previewIconUrl)}|**${
|
||||
extensionHeader.fullName
|
||||
}**|${extensionHeader.shortDescription}|${`[Read more...](${helpPageUrl})` +
|
||||
return {
|
||||
text:
|
||||
`# ${extension.getFullName()}` +
|
||||
'\n\n' +
|
||||
generateSvgImageIcon(extensionShortHeader.previewIconUrl) +
|
||||
'\n' +
|
||||
`${extensionShortHeader.shortDescription}\n` +
|
||||
'\n' +
|
||||
`**Authors and contributors** to this community extension: ${authorNamesWithLinks}.\n` +
|
||||
'\n' +
|
||||
(isCommunity
|
||||
? `!!! warning
|
||||
This is an extension made by a community member — but not reviewed
|
||||
by the GDevelop extension team. As such, we can't guarantee it
|
||||
meets all the quality standards of official extensions. In case of
|
||||
doubt, contact the author to know more about what the extension
|
||||
does or inspect its content before using it.
|
||||
\n\n`
|
||||
: '') +
|
||||
'---\n' +
|
||||
'\n' +
|
||||
convertCommonMarkdownToPythonMarkdown(extension.getDescription()) +
|
||||
'\n' +
|
||||
(extension.getHelpPath() ? `\n[Read more...](${helpPageUrl})\n` : ``) +
|
||||
'\n' +
|
||||
`!!! tip
|
||||
Learn [how to install new extensions](/gdevelop5/extensions/search) by following a step-by-step guide.` +
|
||||
'\n',
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateExtensionFooterText = ({ extension }) => {
|
||||
return {
|
||||
text:
|
||||
`
|
||||
---
|
||||
|
||||
*This page is an auto-generated reference page about the **${extension.getFullName()}** extension, made by the community of [GDevelop, the open-source, cross-platform game engine designed for everyone](https://gdevelop.io/).*` +
|
||||
' ' +
|
||||
'Learn more about [all GDevelop community-made extensions here](/gdevelop5/extensions).',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the metadata for the events based extension
|
||||
* @param {any} project A project containing of the extensions (gdProject)
|
||||
* @param {any} eventsFunctionsExtension An extension (gdEventsFunctionsExtension)
|
||||
* @returns {any} the extension metadata (gdPlatformExtension)
|
||||
*/
|
||||
const generateEventsFunctionExtensionMetadata = (
|
||||
gd,
|
||||
project,
|
||||
eventsFunctionsExtension
|
||||
) => {
|
||||
const extension = new gd.PlatformExtension();
|
||||
gd.MetadataDeclarationHelper.declareExtension(
|
||||
extension,
|
||||
eventsFunctionsExtension
|
||||
);
|
||||
|
||||
// Generate all behaviors and their functions
|
||||
mapVector(
|
||||
eventsFunctionsExtension.getEventsBasedBehaviors(),
|
||||
eventsBasedBehavior => {
|
||||
const behaviorMethodMangledNames = new gd.MapStringString();
|
||||
gd.MetadataDeclarationHelper.generateBehaviorMetadata(
|
||||
project,
|
||||
extension,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedBehavior,
|
||||
behaviorMethodMangledNames
|
||||
);
|
||||
behaviorMethodMangledNames.delete();
|
||||
}
|
||||
);
|
||||
// Generate all objects and their functions
|
||||
mapVector(
|
||||
eventsFunctionsExtension.getEventsBasedObjects(),
|
||||
eventsBasedObject => {
|
||||
const objectMethodMangledNames = new gd.MapStringString();
|
||||
gd.MetadataDeclarationHelper.generateObjectMetadata(
|
||||
project,
|
||||
extension,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
objectMethodMangledNames
|
||||
);
|
||||
objectMethodMangledNames.delete();
|
||||
}
|
||||
);
|
||||
// Generate all free functions
|
||||
const metadataDeclarationHelper = new gd.MetadataDeclarationHelper();
|
||||
mapFor(0, eventsFunctionsExtension.getEventsFunctionsCount(), i => {
|
||||
const eventsFunction = eventsFunctionsExtension.getEventsFunctionAt(i);
|
||||
metadataDeclarationHelper.generateFreeFunctionMetadata(
|
||||
project,
|
||||
extension,
|
||||
eventsFunctionsExtension,
|
||||
eventsFunction
|
||||
);
|
||||
});
|
||||
metadataDeclarationHelper.delete();
|
||||
|
||||
return extension;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a section for an extension.
|
||||
* @param {any} extension The extension (gdEventsFunctionsExtension)
|
||||
*/
|
||||
const generateExtensionSection = extension => {
|
||||
const folderName = getExtensionFolderName(extension.getName());
|
||||
const referencePageUrl = `${gdevelopWikiUrlRoot}/extensions/${folderName}`;
|
||||
const helpPageUrl = getHelpLink(extension.getHelpPath()) || referencePageUrl;
|
||||
|
||||
return `|${generateSvgImageIcon(
|
||||
extension.getPreviewIconUrl()
|
||||
)}|**${extension.getFullName()}**|${extension.getShortDescription()}|${`[Read more...](${helpPageUrl})` +
|
||||
(helpPageUrl !== referencePageUrl
|
||||
? ` ([reference](${referencePageUrl}))`
|
||||
: '')}|\n`;
|
||||
};
|
||||
|
||||
const generateAllExtensionsSections = extensionShortHeaders => {
|
||||
/**
|
||||
* @param {Array<any>} extensions The extension (gdEventsFunctionsExtension)
|
||||
*/
|
||||
const generateAllExtensionsSections = extensions => {
|
||||
let extensionSectionsContent = '';
|
||||
const extensionsByCategory = sortKeys(
|
||||
groupBy(extensionShortHeaders, pair => pair.category || 'General')
|
||||
groupBy(extensions, pair => pair.getCategory() || 'General')
|
||||
);
|
||||
for (const category in extensionsByCategory) {
|
||||
const extensions = extensionsByCategory[category];
|
||||
@@ -193,15 +373,15 @@ const generateAllExtensionsSections = extensionShortHeaders => {
|
||||
extensionSectionsContent += '||Name|Description||\n';
|
||||
extensionSectionsContent += '|---|---|---|---|\n';
|
||||
|
||||
for (const extensionHeader of extensions) {
|
||||
extensionSectionsContent += generateExtensionSection(extensionHeader);
|
||||
for (const extension of extensions) {
|
||||
extensionSectionsContent += generateExtensionSection(extension);
|
||||
}
|
||||
extensionSectionsContent += '\n';
|
||||
}
|
||||
return extensionSectionsContent;
|
||||
};
|
||||
|
||||
const generateExtensionsList = async extensionShortHeaders => {
|
||||
const generateExtensionsList = async gd => {
|
||||
let content = `## Extensions list
|
||||
|
||||
Here are listed all the extensions available in GDevelop. The list is divided in [two tiers](/gdevelop5/extensions/tiers/):
|
||||
@@ -210,19 +390,36 @@ Here are listed all the extensions available in GDevelop. The list is divided in
|
||||
- [Community extensions](#community-extensions)
|
||||
|
||||
`;
|
||||
const extensionHeaders = await getAllExtensionHeaders();
|
||||
const reviewedExtensionHeaders = extensionHeaders.filter(
|
||||
const project = new gd.ProjectHelper.createNewGDJSProject();
|
||||
const extensions = await addAllExtensionsToProject(gd, project);
|
||||
const extensionShortHeaders = await getAllExtensionShortHeaders();
|
||||
|
||||
const reviewedExtensions = extensions.filter(
|
||||
pair => pair.tier !== 'community'
|
||||
);
|
||||
const communityExtensionHeaders = extensionHeaders.filter(
|
||||
const communityExtensions = extensions.filter(
|
||||
pair => pair.tier === 'community'
|
||||
);
|
||||
|
||||
content += '## Reviewed extensions\n\n';
|
||||
for (const extensionHeader of reviewedExtensionHeaders) {
|
||||
await createExtensionReferencePage(extensionHeader, false);
|
||||
for (const extension of reviewedExtensions) {
|
||||
const extensionShortHeader = extensionShortHeaders.find(
|
||||
header => header.name === extension.getName()
|
||||
);
|
||||
if (!extensionShortHeader) {
|
||||
throw new Error(
|
||||
`Could not find header for extension: ${extension.getName()}`
|
||||
);
|
||||
}
|
||||
await createExtensionReferencePage(
|
||||
gd,
|
||||
project,
|
||||
extension,
|
||||
extensionShortHeader,
|
||||
false
|
||||
);
|
||||
}
|
||||
content += generateAllExtensionsSections(reviewedExtensionHeaders);
|
||||
content += generateAllExtensionsSections(reviewedExtensions);
|
||||
|
||||
content += `## Community extensions
|
||||
|
||||
@@ -233,14 +430,29 @@ doubt, contact the author to know more about what the extension
|
||||
does or inspect its content before using it.
|
||||
|
||||
`;
|
||||
for (const extensionHeader of communityExtensionHeaders) {
|
||||
await createExtensionReferencePage(extensionHeader, true);
|
||||
for (const extension of communityExtensions) {
|
||||
const extensionShortHeader = extensionShortHeaders.find(
|
||||
header => header.name === extension.getName()
|
||||
);
|
||||
if (!extensionShortHeader) {
|
||||
throw new Error(
|
||||
`Could not find header for extension: ${extension.getName()}`
|
||||
);
|
||||
}
|
||||
await createExtensionReferencePage(
|
||||
gd,
|
||||
project,
|
||||
extension,
|
||||
extensionShortHeader,
|
||||
true
|
||||
);
|
||||
}
|
||||
content += generateAllExtensionsSections(communityExtensionHeaders);
|
||||
content += generateAllExtensionsSections(communityExtensions);
|
||||
project.delete();
|
||||
return content;
|
||||
};
|
||||
|
||||
(async () => {
|
||||
initializeGDevelopJs().then(async gd => {
|
||||
try {
|
||||
console.info(`ℹ️ Loading all community extensions...`);
|
||||
|
||||
@@ -259,7 +471,7 @@ Read more about this:
|
||||
|
||||
`;
|
||||
|
||||
indexPageContent += await generateExtensionsList();
|
||||
indexPageContent += await generateExtensionsList(gd);
|
||||
|
||||
try {
|
||||
await fs.mkdir(path.dirname(extensionsMainFilePath), { recursive: true });
|
||||
@@ -282,4 +494,4 @@ Read more about this:
|
||||
console.error('❌ Error while fetching data', err);
|
||||
shell.exit(1);
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
@@ -27,66 +27,22 @@ const expressionsFilePath = path.join(
|
||||
allFeaturesRootPath,
|
||||
'expressions-reference.md'
|
||||
);
|
||||
const {
|
||||
generateExtensionReference,
|
||||
generateExtensionRawText,
|
||||
rawTextsToString,
|
||||
generateExpressionsTableHeader,
|
||||
generateObjectHeaderText,
|
||||
generateObjectNoExpressionsText,
|
||||
generateBehaviorHeaderText,
|
||||
generateBehaviorNoExpressionsText,
|
||||
generateHeader,
|
||||
} = require('./lib/ExtensionReferenceGenerator');
|
||||
|
||||
// Types definitions used in this script:
|
||||
|
||||
/**
|
||||
* @typedef {Object} RawText A text to be shown on a page
|
||||
* @prop {string} text The text to render (in Markdown/Dokuwiki syntax)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ReferenceText A text with metadata to format/manipulate/order it.
|
||||
* @prop {string} orderKey The type of the expression, instruction, or anything that help to uniquely order this text.
|
||||
* @prop {string} text The text to render (in Markdown/Dokuwiki syntax)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ObjectReference
|
||||
* @prop {any} objectMetadata The object.
|
||||
* @prop {Array<ReferenceText>} actionsReferenceTexts Reference texts for the object actions.
|
||||
* @prop {Array<ReferenceText>} conditionsReferenceTexts Reference texts for the object conditions.
|
||||
* @prop {Array<ReferenceText>} expressionsReferenceTexts Reference texts for the object expressions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BehaviorReference
|
||||
* @prop {any} behaviorMetadata The behavior.
|
||||
* @prop {Array<ReferenceText>} actionsReferenceTexts Reference texts for the behavior actions.
|
||||
* @prop {Array<ReferenceText>} conditionsReferenceTexts Reference texts for the behavior conditions.
|
||||
* @prop {Array<ReferenceText>} expressionsReferenceTexts Reference texts for the behavior expressions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ExtensionReference
|
||||
* @prop {any} extension The extension.
|
||||
* @prop {Array<ReferenceText>} freeExpressionsReferenceTexts Reference texts for free expressions.
|
||||
* @prop {Array<ReferenceText>} freeActionsReferenceTexts Reference texts for free actions.
|
||||
* @prop {Array<ReferenceText>} freeConditionsReferenceTexts Reference texts for free conditions.
|
||||
* @prop {Array<ObjectReference>} objectReferences Reference of all extension objects.
|
||||
* @prop {Array<BehaviorReference>} behaviorReferences Reference of all extension behaviors.
|
||||
*/
|
||||
|
||||
const sanitizeExpressionDescription = str => {
|
||||
return (
|
||||
str
|
||||
// Disallow new lines in descriptions:
|
||||
.replace(/\n/g, '')
|
||||
// Replace a few description parts that can conflict with DokuWiki/Markdown:
|
||||
.replace(/\)\*x/, ') * x')
|
||||
.replace(/x\^n/, '"x to the power n"')
|
||||
);
|
||||
};
|
||||
|
||||
const translateTypeToHumanReadableType = type => {
|
||||
if (type === 'expression') return 'number';
|
||||
if (type === 'objectList') return 'object';
|
||||
if (type === 'objectPtr') return 'object';
|
||||
if (type === 'stringWithSelector') return 'string';
|
||||
|
||||
return type;
|
||||
};
|
||||
/** @typedef {import("./lib/ExtensionReferenceGenerator.js").RawText} RawText */
|
||||
/** @typedef {import("./lib/ExtensionReferenceGenerator.js").ExtensionReference} ExtensionReference */
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateFileHeaderText = () => {
|
||||
return {
|
||||
text: `# Expressions reference
|
||||
@@ -108,29 +64,6 @@ object or behavior they belong too. When \`Object\` is written, you should enter
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateExtensionHeaderText = ({ extension, depth }) => {
|
||||
return {
|
||||
text:
|
||||
generateHeader({ headerName: extension.getFullName(), depth }).text +
|
||||
`
|
||||
${extension.getDescription()} ${generateReadMoreLink(extension.getHelpPath())}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateExtensionFooterText = ({ extension }) => {
|
||||
return {
|
||||
text:
|
||||
`
|
||||
---
|
||||
*This page is an auto-generated reference page about the **${extension.getFullName()}** feature of [GDevelop, the open-source, cross-platform game engine designed for everyone](https://gdevelop.io/).*` +
|
||||
' ' +
|
||||
'Learn more about [all GDevelop features here](/gdevelop5/all-features).',
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateExtensionSeparatorText = () => {
|
||||
return {
|
||||
@@ -138,258 +71,6 @@ const generateExtensionSeparatorText = () => {
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateObjectHeaderText = ({
|
||||
extension,
|
||||
objectMetadata,
|
||||
showExtensionName,
|
||||
showHelpLink,
|
||||
}) => {
|
||||
// Skip the header for the base object. The "Base object" extension
|
||||
// will already have an header and explanation.
|
||||
if (objectMetadata.getName() === '') {
|
||||
return { text: '' };
|
||||
}
|
||||
|
||||
const additionalText =
|
||||
showExtensionName &&
|
||||
extension.getFullName() !== objectMetadata.getFullName()
|
||||
? `(from extension ${extension.getFullName()})`
|
||||
: '';
|
||||
|
||||
const helpPath = showHelpLink
|
||||
? objectMetadata.getHelpPath() || extension.getHelpPath()
|
||||
: '';
|
||||
|
||||
return {
|
||||
text: `
|
||||
## ${objectMetadata.getFullName()} ${additionalText}
|
||||
|
||||
${objectMetadata.getDescription()} ${generateReadMoreLink(helpPath)}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateBehaviorHeaderText = ({
|
||||
extension,
|
||||
behaviorMetadata,
|
||||
showExtensionName,
|
||||
showHelpLink,
|
||||
}) => {
|
||||
const additionalText =
|
||||
showExtensionName &&
|
||||
extension.getFullName() !== behaviorMetadata.getFullName()
|
||||
? `(from extension ${extension.getFullName()})`
|
||||
: '';
|
||||
|
||||
const helpPath = showHelpLink
|
||||
? behaviorMetadata.getHelpPath() || extension.getHelpPath()
|
||||
: '';
|
||||
|
||||
return {
|
||||
text: `
|
||||
## ${behaviorMetadata.getFullName()} ${additionalText}
|
||||
|
||||
${behaviorMetadata.getDescription()} ${generateReadMoreLink(helpPath)}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateObjectNoExpressionsText = () => {
|
||||
return {
|
||||
text: `_No expressions for this object._\n`,
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateBehaviorNoExpressionsText = () => {
|
||||
return {
|
||||
text: `_No expressions for this behavior._\n`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {?{ headerName: string, depth: number }} headerOptions
|
||||
* @returns {RawText}
|
||||
*/
|
||||
const generateExpressionsTableHeader = headerOptions => {
|
||||
// We don't put a header for the last column
|
||||
const text =
|
||||
(headerOptions ? generateHeader(headerOptions).text + '\n' : '') +
|
||||
`| Expression | Description | |
|
||||
|-----|-----|-----|`;
|
||||
return {
|
||||
text,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{ headerName: string, depth: number }} headerOptions
|
||||
* @returns {RawText}
|
||||
*/
|
||||
const generateHeader = ({ headerName, depth }) => {
|
||||
const markdownHeaderMark = Array(depth)
|
||||
.fill('#')
|
||||
.join('');
|
||||
return {
|
||||
text: `${markdownHeaderMark} ${headerName}\n`,
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {ReferenceText} */
|
||||
const generateInstructionReferenceRowsText = ({
|
||||
instructionType,
|
||||
instructionMetadata,
|
||||
isCondition,
|
||||
objectMetadata,
|
||||
behaviorMetadata,
|
||||
}) => {
|
||||
return {
|
||||
orderKey: instructionType,
|
||||
text:
|
||||
'**' +
|
||||
instructionMetadata.getFullName() +
|
||||
'** ' +
|
||||
'\n' +
|
||||
instructionMetadata.getDescription().replace(/\n/, ' \n') +
|
||||
'\n',
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {ReferenceText} */
|
||||
const generateExpressionReferenceRowsText = ({
|
||||
expressionType,
|
||||
expressionMetadata,
|
||||
objectMetadata,
|
||||
behaviorMetadata,
|
||||
}) => {
|
||||
let parameterRows = [];
|
||||
let parameterStrings = [];
|
||||
mapVector(expressionMetadata.getParameters(), (parameterMetadata, index) => {
|
||||
if ((!!objectMetadata && index < 1) || (!!behaviorMetadata && index < 2)) {
|
||||
return; // Skip the first (or first twos) parameters by convention.
|
||||
}
|
||||
if (parameterMetadata.isCodeOnly()) return;
|
||||
|
||||
const sanitizedDescription = sanitizeExpressionDescription(
|
||||
[
|
||||
parameterMetadata.getDescription(),
|
||||
parameterMetadata.getLongDescription(),
|
||||
parameterMetadata.isOptional() ? '_Optional_.' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
);
|
||||
|
||||
const humanReadableType = translateTypeToHumanReadableType(
|
||||
parameterMetadata.getType()
|
||||
);
|
||||
|
||||
parameterRows.push(
|
||||
`| | _${humanReadableType}_ | ${sanitizedDescription} |`
|
||||
);
|
||||
parameterStrings.push(humanReadableType);
|
||||
});
|
||||
|
||||
let expressionUsage = '';
|
||||
if (objectMetadata) {
|
||||
expressionUsage = 'Object.' + expressionType;
|
||||
} else if (behaviorMetadata) {
|
||||
expressionUsage =
|
||||
'Object.' + behaviorMetadata.getDefaultName() + '::' + expressionType;
|
||||
} else {
|
||||
expressionUsage = expressionType;
|
||||
}
|
||||
expressionUsage += '(' + parameterStrings.join(', ') + ')';
|
||||
|
||||
const sanitizedExpression = sanitizeExpressionDescription(
|
||||
expressionMetadata.getDescription()
|
||||
);
|
||||
|
||||
let text = `| \`${expressionUsage}\` | ${sanitizedExpression} ||`;
|
||||
if (parameterRows.length) {
|
||||
text += '\n' + parameterRows.join('\n');
|
||||
}
|
||||
|
||||
return {
|
||||
orderKey: expressionType,
|
||||
text,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{ instructionsMetadata?: any, areConditions: boolean, objectMetadata?: any, behaviorMetadata?: any }} metadata
|
||||
* @returns {Array<ReferenceText>}
|
||||
*/
|
||||
const generateInstructionsReferenceRowsTexts = ({
|
||||
instructionsMetadata,
|
||||
areConditions,
|
||||
objectMetadata,
|
||||
behaviorMetadata,
|
||||
}) => {
|
||||
/** @type {Array<string>} */
|
||||
const instructionTypes = instructionsMetadata.keys().toJSArray();
|
||||
// @ts-ignore
|
||||
return instructionTypes
|
||||
.map(instructionType => {
|
||||
const instructionMetadata = instructionsMetadata.get(instructionType);
|
||||
|
||||
if (instructionMetadata.isHidden()) return null;
|
||||
|
||||
return generateInstructionReferenceRowsText({
|
||||
instructionType,
|
||||
instructionMetadata,
|
||||
isCondition: areConditions,
|
||||
objectMetadata,
|
||||
behaviorMetadata,
|
||||
});
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{ expressionsMetadata?: any, objectMetadata?: any, behaviorMetadata?: any }} metadata
|
||||
* @returns {Array<ReferenceText>}
|
||||
*/
|
||||
const generateExpressionsReferenceRowsTexts = ({
|
||||
expressionsMetadata,
|
||||
objectMetadata,
|
||||
behaviorMetadata,
|
||||
}) => {
|
||||
/** @type {Array<string>} */
|
||||
const expressionTypes = expressionsMetadata.keys().toJSArray();
|
||||
// @ts-ignore
|
||||
return expressionTypes
|
||||
.map(expressionType => {
|
||||
const expressionMetadata = expressionsMetadata.get(expressionType);
|
||||
|
||||
if (!expressionMetadata.isShown()) return null;
|
||||
|
||||
return generateExpressionReferenceRowsText({
|
||||
expressionType,
|
||||
expressionMetadata,
|
||||
objectMetadata,
|
||||
behaviorMetadata,
|
||||
});
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {ReferenceText} referenceText1
|
||||
* @param {ReferenceText} referenceText2
|
||||
*/
|
||||
const sortReferenceTexts = (referenceText1, referenceText2) => {
|
||||
if (referenceText1.orderKey > referenceText2.orderKey) {
|
||||
return 1;
|
||||
} else if (referenceText1.orderKey < referenceText2.orderKey) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {any} gd
|
||||
* @returns {Array<ExtensionReference>}
|
||||
@@ -400,132 +81,7 @@ const generateAllExtensionReferences = gd => {
|
||||
/** @type {Array<ExtensionReference>} */
|
||||
const extensionReferences = mapVector(
|
||||
platformExtensions,
|
||||
/** @returns {ExtensionReference} */
|
||||
extension => {
|
||||
const extensionExpressions = extension.getAllExpressions();
|
||||
const extensionStrExpressions = extension.getAllStrExpressions();
|
||||
|
||||
/** @type {Array<string>} */
|
||||
const objectTypes = extension.getExtensionObjectsTypes().toJSArray();
|
||||
/** @type {Array<string>} */
|
||||
const behaviorTypes = extension.getBehaviorsTypes().toJSArray();
|
||||
|
||||
// Object expressions
|
||||
/** @type {Array<ObjectReference>} */
|
||||
let objectReferences = objectTypes.map(objectType => {
|
||||
const objectMetadata = extension.getObjectMetadata(objectType);
|
||||
const actionsReferenceTexts = generateInstructionsReferenceRowsTexts({
|
||||
areConditions: false,
|
||||
instructionsMetadata: extension.getAllActionsForObject(objectType),
|
||||
objectMetadata,
|
||||
});
|
||||
const conditionsReferenceTexts = generateInstructionsReferenceRowsTexts(
|
||||
{
|
||||
areConditions: true,
|
||||
instructionsMetadata: extension.getAllConditionsForObject(
|
||||
objectType
|
||||
),
|
||||
objectMetadata,
|
||||
}
|
||||
);
|
||||
const expressionsReferenceTexts = [
|
||||
...generateExpressionsReferenceRowsTexts({
|
||||
expressionsMetadata: extension.getAllExpressionsForObject(
|
||||
objectType
|
||||
),
|
||||
objectMetadata,
|
||||
}),
|
||||
...generateExpressionsReferenceRowsTexts({
|
||||
expressionsMetadata: extension.getAllStrExpressionsForObject(
|
||||
objectType
|
||||
),
|
||||
objectMetadata,
|
||||
}),
|
||||
];
|
||||
expressionsReferenceTexts.sort(sortReferenceTexts);
|
||||
|
||||
return {
|
||||
objectMetadata,
|
||||
actionsReferenceTexts,
|
||||
conditionsReferenceTexts,
|
||||
expressionsReferenceTexts,
|
||||
};
|
||||
});
|
||||
|
||||
// Behavior expressions
|
||||
/** @type {Array<BehaviorReference>} */
|
||||
let behaviorReferences = behaviorTypes.map(behaviorType => {
|
||||
const behaviorMetadata = extension.getBehaviorMetadata(behaviorType);
|
||||
const actionsReferenceTexts = generateInstructionsReferenceRowsTexts({
|
||||
areConditions: false,
|
||||
instructionsMetadata: extension.getAllActionsForBehavior(
|
||||
behaviorType
|
||||
),
|
||||
behaviorMetadata,
|
||||
});
|
||||
const conditionsReferenceTexts = generateInstructionsReferenceRowsTexts(
|
||||
{
|
||||
areConditions: true,
|
||||
instructionsMetadata: extension.getAllConditionsForBehavior(
|
||||
behaviorType
|
||||
),
|
||||
behaviorMetadata,
|
||||
}
|
||||
);
|
||||
const expressionsReferenceTexts = [
|
||||
...generateExpressionsReferenceRowsTexts({
|
||||
expressionsMetadata: extension.getAllExpressionsForBehavior(
|
||||
behaviorType
|
||||
),
|
||||
behaviorMetadata,
|
||||
}),
|
||||
...generateExpressionsReferenceRowsTexts({
|
||||
expressionsMetadata: extension.getAllStrExpressionsForBehavior(
|
||||
behaviorType
|
||||
),
|
||||
behaviorMetadata,
|
||||
}),
|
||||
];
|
||||
expressionsReferenceTexts.sort(sortReferenceTexts);
|
||||
|
||||
return {
|
||||
behaviorMetadata,
|
||||
actionsReferenceTexts,
|
||||
conditionsReferenceTexts,
|
||||
expressionsReferenceTexts,
|
||||
};
|
||||
});
|
||||
|
||||
// Free (non objects/non behaviors) actions/conditions/expressions
|
||||
const freeActionsReferenceTexts = generateInstructionsReferenceRowsTexts({
|
||||
areConditions: false,
|
||||
instructionsMetadata: extension.getAllActions(),
|
||||
});
|
||||
const freeConditionsReferenceTexts = generateInstructionsReferenceRowsTexts(
|
||||
{
|
||||
areConditions: true,
|
||||
instructionsMetadata: extension.getAllConditions(),
|
||||
}
|
||||
);
|
||||
const freeExpressionsReferenceTexts = [
|
||||
...generateExpressionsReferenceRowsTexts({
|
||||
expressionsMetadata: extensionStrExpressions,
|
||||
}),
|
||||
...generateExpressionsReferenceRowsTexts({
|
||||
expressionsMetadata: extensionExpressions,
|
||||
}),
|
||||
];
|
||||
freeExpressionsReferenceTexts.sort(sortReferenceTexts);
|
||||
|
||||
return {
|
||||
extension,
|
||||
freeActionsReferenceTexts,
|
||||
freeConditionsReferenceTexts,
|
||||
freeExpressionsReferenceTexts,
|
||||
objectReferences,
|
||||
behaviorReferences,
|
||||
};
|
||||
}
|
||||
generateExtensionReference
|
||||
);
|
||||
|
||||
return extensionReferences;
|
||||
@@ -588,6 +144,29 @@ Remember that you can also [search for new features in the community extensions]
|
||||
];
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateExtensionHeaderText = ({ extension, depth }) => {
|
||||
return {
|
||||
text:
|
||||
generateHeader({ headerName: extension.getFullName(), depth }).text +
|
||||
`
|
||||
${extension.getDescription()} ${generateReadMoreLink(extension.getHelpPath())}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateExtensionFooterText = ({ extension }) => {
|
||||
return {
|
||||
text:
|
||||
`
|
||||
---
|
||||
*This page is an auto-generated reference page about the **${extension.getFullName()}** feature of [GDevelop, the open-source, cross-platform game engine designed for everyone](https://gdevelop.io/).*` +
|
||||
' ' +
|
||||
'Learn more about [all GDevelop features here](/gdevelop5/all-features).',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array<ExtensionReference>} extensionReferences
|
||||
* @returns {{allExtensionRawTexts: Array<{folderName: string, texts: Array<RawText>}>}}
|
||||
@@ -600,106 +179,15 @@ const generateExtensionRawTexts = extensionReferences => {
|
||||
);
|
||||
})
|
||||
.map(extensionReference => {
|
||||
const {
|
||||
extension,
|
||||
freeActionsReferenceTexts,
|
||||
freeConditionsReferenceTexts,
|
||||
freeExpressionsReferenceTexts,
|
||||
objectReferences,
|
||||
behaviorReferences,
|
||||
} = extensionReference;
|
||||
|
||||
const withHeaderIfNotEmpty = (texts, { headerName, depth }) => {
|
||||
if (!texts.length) return [];
|
||||
|
||||
return [generateHeader({ headerName, depth }), ...texts];
|
||||
};
|
||||
|
||||
return {
|
||||
folderName: getExtensionFolderName(
|
||||
extensionReference.extension.getName()
|
||||
),
|
||||
texts: [
|
||||
generateExtensionHeaderText({ extension, depth: 1 }),
|
||||
...withHeaderIfNotEmpty(freeActionsReferenceTexts, {
|
||||
headerName: 'Actions',
|
||||
depth: 2,
|
||||
}),
|
||||
...withHeaderIfNotEmpty(freeConditionsReferenceTexts, {
|
||||
headerName: 'Conditions',
|
||||
depth: 2,
|
||||
}),
|
||||
freeExpressionsReferenceTexts.length
|
||||
? generateExpressionsTableHeader({
|
||||
headerName: 'Expressions',
|
||||
depth: 2,
|
||||
})
|
||||
: { text: '' },
|
||||
...freeExpressionsReferenceTexts,
|
||||
...objectReferences.flatMap(objectReference => {
|
||||
const {
|
||||
objectMetadata,
|
||||
actionsReferenceTexts,
|
||||
conditionsReferenceTexts,
|
||||
expressionsReferenceTexts,
|
||||
} = objectReference;
|
||||
return [
|
||||
generateObjectHeaderText({
|
||||
extension,
|
||||
objectMetadata,
|
||||
showExtensionName: false,
|
||||
showHelpLink: false,
|
||||
}),
|
||||
...withHeaderIfNotEmpty(actionsReferenceTexts, {
|
||||
headerName: 'Object actions',
|
||||
depth: 3,
|
||||
}),
|
||||
...withHeaderIfNotEmpty(conditionsReferenceTexts, {
|
||||
headerName: 'Object conditions',
|
||||
depth: 3,
|
||||
}),
|
||||
expressionsReferenceTexts.length
|
||||
? generateExpressionsTableHeader({
|
||||
headerName: 'Object expressions',
|
||||
depth: 3,
|
||||
})
|
||||
: generateObjectNoExpressionsText(),
|
||||
...expressionsReferenceTexts,
|
||||
];
|
||||
}),
|
||||
...behaviorReferences.flatMap(behaviorReference => {
|
||||
const {
|
||||
behaviorMetadata,
|
||||
actionsReferenceTexts,
|
||||
conditionsReferenceTexts,
|
||||
expressionsReferenceTexts,
|
||||
} = behaviorReference;
|
||||
return [
|
||||
generateBehaviorHeaderText({
|
||||
extension,
|
||||
behaviorMetadata,
|
||||
showExtensionName: false,
|
||||
showHelpLink: false,
|
||||
}),
|
||||
...withHeaderIfNotEmpty(actionsReferenceTexts, {
|
||||
headerName: 'Behavior actions',
|
||||
depth: 3,
|
||||
}),
|
||||
...withHeaderIfNotEmpty(conditionsReferenceTexts, {
|
||||
headerName: 'Behavior conditions',
|
||||
depth: 3,
|
||||
}),
|
||||
expressionsReferenceTexts.length
|
||||
? generateExpressionsTableHeader({
|
||||
headerName: 'Behavior expressions',
|
||||
depth: 3,
|
||||
})
|
||||
: generateBehaviorNoExpressionsText(),
|
||||
...expressionsReferenceTexts,
|
||||
];
|
||||
}),
|
||||
generateExtensionFooterText({ extension }),
|
||||
].filter(Boolean),
|
||||
texts: generateExtensionRawText(
|
||||
extensionReference,
|
||||
generateExtensionHeaderText,
|
||||
generateExtensionFooterText
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -788,13 +276,6 @@ const generateAllExpressionsRawTexts = extensionReferences => {
|
||||
|
||||
const noopTranslationFunction = str => str;
|
||||
|
||||
const rawTextsToString = rawTexts =>
|
||||
rawTexts
|
||||
.map(({ text }) => {
|
||||
return text;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
initializeGDevelopJs().then(async gd => {
|
||||
try {
|
||||
// @ts-ignore - not passing onFindGDJS - is it still useful?
|
||||
|
558
newIDE/app/scripts/lib/ExtensionReferenceGenerator.js
Normal file
558
newIDE/app/scripts/lib/ExtensionReferenceGenerator.js
Normal file
@@ -0,0 +1,558 @@
|
||||
// @ts-check
|
||||
const { mapVector } = require('./MapFor');
|
||||
const { generateReadMoreLink } = require('./WikiHelpLink');
|
||||
|
||||
// Types definitions used in this script:
|
||||
|
||||
/**
|
||||
* @typedef {Object} RawText A text to be shown on a page
|
||||
* @prop {string} text The text to render (in Markdown/Dokuwiki syntax)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ReferenceText A text with metadata to format/manipulate/order it.
|
||||
* @prop {string} orderKey The type of the expression, instruction, or anything that help to uniquely order this text.
|
||||
* @prop {string} text The text to render (in Markdown/Dokuwiki syntax)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ObjectReference
|
||||
* @prop {any} objectMetadata The object.
|
||||
* @prop {Array<ReferenceText>} actionsReferenceTexts Reference texts for the object actions.
|
||||
* @prop {Array<ReferenceText>} conditionsReferenceTexts Reference texts for the object conditions.
|
||||
* @prop {Array<ReferenceText>} expressionsReferenceTexts Reference texts for the object expressions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BehaviorReference
|
||||
* @prop {any} behaviorMetadata The behavior.
|
||||
* @prop {Array<ReferenceText>} actionsReferenceTexts Reference texts for the behavior actions.
|
||||
* @prop {Array<ReferenceText>} conditionsReferenceTexts Reference texts for the behavior conditions.
|
||||
* @prop {Array<ReferenceText>} expressionsReferenceTexts Reference texts for the behavior expressions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ExtensionReference
|
||||
* @prop {any} extension The extension.
|
||||
* @prop {Array<ReferenceText>} freeExpressionsReferenceTexts Reference texts for free expressions.
|
||||
* @prop {Array<ReferenceText>} freeActionsReferenceTexts Reference texts for free actions.
|
||||
* @prop {Array<ReferenceText>} freeConditionsReferenceTexts Reference texts for free conditions.
|
||||
* @prop {Array<ObjectReference>} objectReferences Reference of all extension objects.
|
||||
* @prop {Array<BehaviorReference>} behaviorReferences Reference of all extension behaviors.
|
||||
*/
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateObjectNoExpressionsText = () => {
|
||||
return {
|
||||
text: `_No expressions for this object._\n`,
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateBehaviorNoExpressionsText = () => {
|
||||
return {
|
||||
text: `_No expressions for this behavior._\n`,
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateBehaviorHeaderText = ({
|
||||
extension,
|
||||
behaviorMetadata,
|
||||
showExtensionName,
|
||||
showHelpLink,
|
||||
}) => {
|
||||
const additionalText =
|
||||
showExtensionName &&
|
||||
extension.getFullName() !== behaviorMetadata.getFullName()
|
||||
? `(from extension ${extension.getFullName()})`
|
||||
: '';
|
||||
|
||||
const helpPath = showHelpLink
|
||||
? behaviorMetadata.getHelpPath() || extension.getHelpPath()
|
||||
: '';
|
||||
|
||||
return {
|
||||
text: `
|
||||
## ${behaviorMetadata.getFullName()} ${additionalText}
|
||||
|
||||
${behaviorMetadata.getDescription()} ${generateReadMoreLink(helpPath)}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {RawText} */
|
||||
const generateObjectHeaderText = ({
|
||||
extension,
|
||||
objectMetadata,
|
||||
showExtensionName,
|
||||
showHelpLink,
|
||||
}) => {
|
||||
// Skip the header for the base object. The "Base object" extension
|
||||
// will already have an header and explanation.
|
||||
if (objectMetadata.getName() === '') {
|
||||
return { text: '' };
|
||||
}
|
||||
|
||||
const additionalText =
|
||||
showExtensionName &&
|
||||
extension.getFullName() !== objectMetadata.getFullName()
|
||||
? `(from extension ${extension.getFullName()})`
|
||||
: '';
|
||||
|
||||
const helpPath = showHelpLink
|
||||
? objectMetadata.getHelpPath() || extension.getHelpPath()
|
||||
: '';
|
||||
|
||||
return {
|
||||
text: `
|
||||
## ${objectMetadata.getFullName()} ${additionalText}
|
||||
|
||||
${objectMetadata.getDescription()} ${generateReadMoreLink(helpPath)}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {?{ headerName: string, depth: number }} headerOptions
|
||||
* @returns {RawText}
|
||||
*/
|
||||
const generateExpressionsTableHeader = headerOptions => {
|
||||
// We don't put a header for the last column
|
||||
const text =
|
||||
(headerOptions ? generateHeader(headerOptions).text + '\n' : '') +
|
||||
`| Expression | Description | |
|
||||
|-----|-----|-----|`;
|
||||
return {
|
||||
text,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{ headerName: string, depth: number }} headerOptions
|
||||
* @returns {RawText}
|
||||
*/
|
||||
const generateHeader = ({ headerName, depth }) => {
|
||||
const markdownHeaderMark = Array(depth)
|
||||
.fill('#')
|
||||
.join('');
|
||||
return {
|
||||
text: `${markdownHeaderMark} ${headerName}\n`,
|
||||
};
|
||||
};
|
||||
|
||||
const rawTextsToString = rawTexts =>
|
||||
rawTexts
|
||||
.map(({ text }) => {
|
||||
return text;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const sanitizeExpressionDescription = str => {
|
||||
return (
|
||||
str
|
||||
// Disallow new lines in descriptions:
|
||||
.replace(/\n/g, '')
|
||||
// Replace a few description parts that can conflict with DokuWiki/Markdown:
|
||||
.replace(/\)\*x/, ') * x')
|
||||
.replace(/x\^n/, '"x to the power n"')
|
||||
);
|
||||
};
|
||||
|
||||
const translateTypeToHumanReadableType = type => {
|
||||
if (type === 'expression') return 'number';
|
||||
if (type === 'objectList') return 'object';
|
||||
if (type === 'objectPtr') return 'object';
|
||||
if (type === 'stringWithSelector') return 'string';
|
||||
|
||||
return type;
|
||||
};
|
||||
|
||||
/** @returns {ReferenceText} */
|
||||
const generateInstructionReferenceRowsText = ({
|
||||
instructionType,
|
||||
instructionMetadata,
|
||||
isCondition,
|
||||
objectMetadata,
|
||||
behaviorMetadata,
|
||||
}) => {
|
||||
return {
|
||||
orderKey: instructionType,
|
||||
text:
|
||||
'**' +
|
||||
instructionMetadata.getFullName() +
|
||||
'** ' +
|
||||
'\n' +
|
||||
instructionMetadata.getDescription().replace(/\n/, ' \n') +
|
||||
'\n',
|
||||
};
|
||||
};
|
||||
|
||||
/** @returns {ReferenceText} */
|
||||
const generateExpressionReferenceRowsText = ({
|
||||
expressionType,
|
||||
expressionMetadata,
|
||||
objectMetadata,
|
||||
behaviorMetadata,
|
||||
}) => {
|
||||
let parameterRows = [];
|
||||
let parameterStrings = [];
|
||||
mapVector(expressionMetadata.getParameters(), (parameterMetadata, index) => {
|
||||
if ((!!objectMetadata && index < 1) || (!!behaviorMetadata && index < 2)) {
|
||||
return; // Skip the first (or first twos) parameters by convention.
|
||||
}
|
||||
if (parameterMetadata.isCodeOnly()) return;
|
||||
|
||||
const sanitizedDescription = sanitizeExpressionDescription(
|
||||
[
|
||||
parameterMetadata.getDescription(),
|
||||
parameterMetadata.getLongDescription(),
|
||||
parameterMetadata.isOptional() ? '_Optional_.' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
);
|
||||
|
||||
const humanReadableType = translateTypeToHumanReadableType(
|
||||
parameterMetadata.getType()
|
||||
);
|
||||
|
||||
parameterRows.push(
|
||||
`| | _${humanReadableType}_ | ${sanitizedDescription} |`
|
||||
);
|
||||
parameterStrings.push(humanReadableType);
|
||||
});
|
||||
|
||||
let expressionUsage = '';
|
||||
if (objectMetadata) {
|
||||
expressionUsage = 'Object.' + expressionType;
|
||||
} else if (behaviorMetadata) {
|
||||
expressionUsage =
|
||||
'Object.' + behaviorMetadata.getDefaultName() + '::' + expressionType;
|
||||
} else {
|
||||
expressionUsage = expressionType;
|
||||
}
|
||||
expressionUsage += '(' + parameterStrings.join(', ') + ')';
|
||||
|
||||
const sanitizedExpression = sanitizeExpressionDescription(
|
||||
expressionMetadata.getDescription()
|
||||
);
|
||||
|
||||
let text = `| \`${expressionUsage}\` | ${sanitizedExpression} ||`;
|
||||
if (parameterRows.length) {
|
||||
text += '\n' + parameterRows.join('\n');
|
||||
}
|
||||
|
||||
return {
|
||||
orderKey: expressionType,
|
||||
text,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{ instructionsMetadata?: any, areConditions: boolean, objectMetadata?: any, behaviorMetadata?: any }} metadata
|
||||
* @returns {Array<ReferenceText>}
|
||||
*/
|
||||
const generateInstructionsReferenceRowsTexts = ({
|
||||
instructionsMetadata,
|
||||
areConditions,
|
||||
objectMetadata,
|
||||
behaviorMetadata,
|
||||
}) => {
|
||||
/** @type {Array<string>} */
|
||||
const instructionTypes = instructionsMetadata.keys().toJSArray();
|
||||
// @ts-ignore
|
||||
return instructionTypes
|
||||
.map(instructionType => {
|
||||
const instructionMetadata = instructionsMetadata.get(instructionType);
|
||||
|
||||
if (instructionMetadata.isHidden()) return null;
|
||||
|
||||
return generateInstructionReferenceRowsText({
|
||||
instructionType,
|
||||
instructionMetadata,
|
||||
isCondition: areConditions,
|
||||
objectMetadata,
|
||||
behaviorMetadata,
|
||||
});
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {{ expressionsMetadata?: any, objectMetadata?: any, behaviorMetadata?: any }} metadata
|
||||
* @returns {Array<ReferenceText>}
|
||||
*/
|
||||
const generateExpressionsReferenceRowsTexts = ({
|
||||
expressionsMetadata,
|
||||
objectMetadata,
|
||||
behaviorMetadata,
|
||||
}) => {
|
||||
/** @type {Array<string>} */
|
||||
const expressionTypes = expressionsMetadata.keys().toJSArray();
|
||||
// @ts-ignore
|
||||
return expressionTypes
|
||||
.map(expressionType => {
|
||||
const expressionMetadata = expressionsMetadata.get(expressionType);
|
||||
|
||||
if (!expressionMetadata.isShown()) return null;
|
||||
|
||||
return generateExpressionReferenceRowsText({
|
||||
expressionType,
|
||||
expressionMetadata,
|
||||
objectMetadata,
|
||||
behaviorMetadata,
|
||||
});
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {ReferenceText} referenceText1
|
||||
* @param {ReferenceText} referenceText2
|
||||
*/
|
||||
const sortReferenceTexts = (referenceText1, referenceText2) => {
|
||||
if (referenceText1.orderKey > referenceText2.orderKey) {
|
||||
return 1;
|
||||
} else if (referenceText1.orderKey < referenceText2.orderKey) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {any} platformExtension
|
||||
* @returns {ExtensionReference}
|
||||
*/
|
||||
const generateExtensionReference = extension => {
|
||||
const extensionExpressions = extension.getAllExpressions();
|
||||
const extensionStrExpressions = extension.getAllStrExpressions();
|
||||
|
||||
/** @type {Array<string>} */
|
||||
const objectTypes = extension.getExtensionObjectsTypes().toJSArray();
|
||||
/** @type {Array<string>} */
|
||||
const behaviorTypes = extension.getBehaviorsTypes().toJSArray();
|
||||
|
||||
// Object expressions
|
||||
/** @type {Array<ObjectReference>} */
|
||||
let objectReferences = objectTypes.map(objectType => {
|
||||
const objectMetadata = extension.getObjectMetadata(objectType);
|
||||
const actionsReferenceTexts = generateInstructionsReferenceRowsTexts({
|
||||
areConditions: false,
|
||||
instructionsMetadata: extension.getAllActionsForObject(objectType),
|
||||
objectMetadata,
|
||||
});
|
||||
const conditionsReferenceTexts = generateInstructionsReferenceRowsTexts({
|
||||
areConditions: true,
|
||||
instructionsMetadata: extension.getAllConditionsForObject(objectType),
|
||||
objectMetadata,
|
||||
});
|
||||
const expressionsReferenceTexts = [
|
||||
...generateExpressionsReferenceRowsTexts({
|
||||
expressionsMetadata: extension.getAllExpressionsForObject(objectType),
|
||||
objectMetadata,
|
||||
}),
|
||||
...generateExpressionsReferenceRowsTexts({
|
||||
expressionsMetadata: extension.getAllStrExpressionsForObject(
|
||||
objectType
|
||||
),
|
||||
objectMetadata,
|
||||
}),
|
||||
];
|
||||
expressionsReferenceTexts.sort(sortReferenceTexts);
|
||||
|
||||
return {
|
||||
objectMetadata,
|
||||
actionsReferenceTexts,
|
||||
conditionsReferenceTexts,
|
||||
expressionsReferenceTexts,
|
||||
};
|
||||
});
|
||||
|
||||
// Behavior expressions
|
||||
/** @type {Array<BehaviorReference>} */
|
||||
let behaviorReferences = behaviorTypes.map(behaviorType => {
|
||||
const behaviorMetadata = extension.getBehaviorMetadata(behaviorType);
|
||||
const actionsReferenceTexts = generateInstructionsReferenceRowsTexts({
|
||||
areConditions: false,
|
||||
instructionsMetadata: extension.getAllActionsForBehavior(behaviorType),
|
||||
behaviorMetadata,
|
||||
});
|
||||
const conditionsReferenceTexts = generateInstructionsReferenceRowsTexts({
|
||||
areConditions: true,
|
||||
instructionsMetadata: extension.getAllConditionsForBehavior(behaviorType),
|
||||
behaviorMetadata,
|
||||
});
|
||||
const expressionsReferenceTexts = [
|
||||
...generateExpressionsReferenceRowsTexts({
|
||||
expressionsMetadata: extension.getAllExpressionsForBehavior(
|
||||
behaviorType
|
||||
),
|
||||
behaviorMetadata,
|
||||
}),
|
||||
...generateExpressionsReferenceRowsTexts({
|
||||
expressionsMetadata: extension.getAllStrExpressionsForBehavior(
|
||||
behaviorType
|
||||
),
|
||||
behaviorMetadata,
|
||||
}),
|
||||
];
|
||||
expressionsReferenceTexts.sort(sortReferenceTexts);
|
||||
|
||||
return {
|
||||
behaviorMetadata,
|
||||
actionsReferenceTexts,
|
||||
conditionsReferenceTexts,
|
||||
expressionsReferenceTexts,
|
||||
};
|
||||
});
|
||||
|
||||
// Free (non objects/non behaviors) actions/conditions/expressions
|
||||
const freeActionsReferenceTexts = generateInstructionsReferenceRowsTexts({
|
||||
areConditions: false,
|
||||
instructionsMetadata: extension.getAllActions(),
|
||||
});
|
||||
const freeConditionsReferenceTexts = generateInstructionsReferenceRowsTexts({
|
||||
areConditions: true,
|
||||
instructionsMetadata: extension.getAllConditions(),
|
||||
});
|
||||
const freeExpressionsReferenceTexts = [
|
||||
...generateExpressionsReferenceRowsTexts({
|
||||
expressionsMetadata: extensionStrExpressions,
|
||||
}),
|
||||
...generateExpressionsReferenceRowsTexts({
|
||||
expressionsMetadata: extensionExpressions,
|
||||
}),
|
||||
];
|
||||
freeExpressionsReferenceTexts.sort(sortReferenceTexts);
|
||||
|
||||
return {
|
||||
extension,
|
||||
freeActionsReferenceTexts,
|
||||
freeConditionsReferenceTexts,
|
||||
freeExpressionsReferenceTexts,
|
||||
objectReferences,
|
||||
behaviorReferences,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {ExtensionReference} extensionReference
|
||||
* @param {({extension: gdPlatformExtension, depth: number}) => RawText} generateExtensionHeaderText
|
||||
* @param {({extension: gdPlatformExtension}) => RawText} generateExtensionFooterText
|
||||
* @returns {Array<RawText>}}}
|
||||
*/
|
||||
const generateExtensionRawText = (
|
||||
extensionReference,
|
||||
generateExtensionHeaderText,
|
||||
generateExtensionFooterText
|
||||
) => {
|
||||
const {
|
||||
extension,
|
||||
freeActionsReferenceTexts,
|
||||
freeConditionsReferenceTexts,
|
||||
freeExpressionsReferenceTexts,
|
||||
objectReferences,
|
||||
behaviorReferences,
|
||||
} = extensionReference;
|
||||
|
||||
const withHeaderIfNotEmpty = (texts, { headerName, depth }) => {
|
||||
if (!texts.length) return [];
|
||||
|
||||
return [generateHeader({ headerName, depth }), ...texts];
|
||||
};
|
||||
|
||||
return [
|
||||
generateExtensionHeaderText({ extension, depth: 1 }),
|
||||
...withHeaderIfNotEmpty(freeActionsReferenceTexts, {
|
||||
headerName: 'Actions',
|
||||
depth: 2,
|
||||
}),
|
||||
...withHeaderIfNotEmpty(freeConditionsReferenceTexts, {
|
||||
headerName: 'Conditions',
|
||||
depth: 2,
|
||||
}),
|
||||
freeExpressionsReferenceTexts.length
|
||||
? generateExpressionsTableHeader({
|
||||
headerName: 'Expressions',
|
||||
depth: 2,
|
||||
})
|
||||
: { text: '' },
|
||||
...freeExpressionsReferenceTexts,
|
||||
...objectReferences.flatMap(objectReference => {
|
||||
const {
|
||||
objectMetadata,
|
||||
actionsReferenceTexts,
|
||||
conditionsReferenceTexts,
|
||||
expressionsReferenceTexts,
|
||||
} = objectReference;
|
||||
return [
|
||||
generateObjectHeaderText({
|
||||
extension,
|
||||
objectMetadata,
|
||||
showExtensionName: false,
|
||||
showHelpLink: false,
|
||||
}),
|
||||
...withHeaderIfNotEmpty(actionsReferenceTexts, {
|
||||
headerName: 'Object actions',
|
||||
depth: 3,
|
||||
}),
|
||||
...withHeaderIfNotEmpty(conditionsReferenceTexts, {
|
||||
headerName: 'Object conditions',
|
||||
depth: 3,
|
||||
}),
|
||||
expressionsReferenceTexts.length
|
||||
? generateExpressionsTableHeader({
|
||||
headerName: 'Object expressions',
|
||||
depth: 3,
|
||||
})
|
||||
: generateObjectNoExpressionsText(),
|
||||
...expressionsReferenceTexts,
|
||||
];
|
||||
}),
|
||||
...behaviorReferences.flatMap(behaviorReference => {
|
||||
const {
|
||||
behaviorMetadata,
|
||||
actionsReferenceTexts,
|
||||
conditionsReferenceTexts,
|
||||
expressionsReferenceTexts,
|
||||
} = behaviorReference;
|
||||
return [
|
||||
generateBehaviorHeaderText({
|
||||
extension,
|
||||
behaviorMetadata,
|
||||
showExtensionName: false,
|
||||
showHelpLink: false,
|
||||
}),
|
||||
...withHeaderIfNotEmpty(actionsReferenceTexts, {
|
||||
headerName: 'Behavior actions',
|
||||
depth: 3,
|
||||
}),
|
||||
...withHeaderIfNotEmpty(conditionsReferenceTexts, {
|
||||
headerName: 'Behavior conditions',
|
||||
depth: 3,
|
||||
}),
|
||||
expressionsReferenceTexts.length
|
||||
? generateExpressionsTableHeader({
|
||||
headerName: 'Behavior expressions',
|
||||
depth: 3,
|
||||
})
|
||||
: generateBehaviorNoExpressionsText(),
|
||||
...expressionsReferenceTexts,
|
||||
];
|
||||
}),
|
||||
generateExtensionFooterText({ extension }),
|
||||
].filter(Boolean);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
rawTextsToString,
|
||||
generateExtensionReference,
|
||||
generateExtensionRawText,
|
||||
generateExpressionsTableHeader,
|
||||
generateObjectHeaderText,
|
||||
generateObjectNoExpressionsText,
|
||||
generateBehaviorHeaderText,
|
||||
generateBehaviorNoExpressionsText,
|
||||
generateHeader,
|
||||
};
|
Reference in New Issue
Block a user