Compare commits

..

2 Commits

Author SHA1 Message Date
Florian Rival
65bc9ef4a8 Add missing ability to select text when a fatal error is displayed on screen 2025-10-12 17:15:49 +02:00
Florian Rival
073268160f Improve documentation pages generation [ci skip] 2025-10-12 14:23:50 +02:00
6 changed files with 222 additions and 123 deletions

View File

@@ -12,6 +12,7 @@ const {
getHelpLink,
getExtensionFolderName,
improperlyFormattedHelpPaths,
generateSvgImageIcon,
} = require('./lib/WikiHelpLink');
const {
convertCommonMarkdownToPythonMarkdown,
@@ -23,6 +24,8 @@ const {
rawTextsToString,
} = require('./lib/ExtensionReferenceGenerator');
const { mapVector, mapFor } = require('./lib/MapFor');
const { groupBy, sortKeys } = require('./lib/ArrayHelpers');
const { generateAllExtensionsSections } = require('./lib/WikiExtensionTable');
/** @typedef {import("./lib/ExtensionReferenceGenerator.js").RawText} RawText */
@@ -35,10 +38,6 @@ const outputRootPath = path.join(gdRootPath, 'docs-wiki');
const extensionsRootPath = path.join(outputRootPath, 'extensions');
const extensionsMainFilePath = path.join(extensionsRootPath, 'index.md');
const generateSvgImageIcon = iconUrl => {
return `<img src="${iconUrl}" class="extension-icon"></img>`;
};
/**
* @param {{id: string, username: string}[]} authors
*/
@@ -146,28 +145,6 @@ const getAllExtensionShortHeaders = async () => {
return extensionShortHeaders;
};
const groupBy = (array, getKey) => {
const table = {};
for (const element of array) {
const key = getKey(element);
let group = table[key];
if (!group) {
group = [];
table[key] = group;
}
group.push(element);
}
return table;
};
const sortKeys = table => {
const sortedTable = {};
for (const key of Object.keys(table).sort()) {
sortedTable[key] = table[key];
}
return sortedTable;
};
/**
* Create a page for an extension.
* @param {any} gd
@@ -339,46 +316,6 @@ const generateEventsFunctionExtensionMetadata = (
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`;
};
/**
* @param {Array<any>} extensions The extension (gdEventsFunctionsExtension)
*/
const generateAllExtensionsSections = extensions => {
let extensionSectionsContent = '';
const extensionsByCategory = sortKeys(
groupBy(extensions, pair => pair.getCategory() || 'General')
);
for (const category in extensionsByCategory) {
const extensions = extensionsByCategory[category];
extensionSectionsContent += `### ${category}\n\n`;
extensionSectionsContent += '||Name|Description||\n';
extensionSectionsContent += '|---|---|---|---|\n';
for (const extension of extensions) {
extensionSectionsContent += generateExtensionSection(extension);
}
extensionSectionsContent += '\n';
}
return extensionSectionsContent;
};
/**
* @param {Array<any>} extensions The extension (gdEventsFunctionsExtension)
*/
@@ -417,14 +354,14 @@ const generateExtensionsMkDocsDotPagesFile = async (
- index.md
- search.md
- tiers.md
- Create your own extensions:
- Create a new extension : create.md
- best-practices.md
- share-extension.md
${generateExtensionsPageList(reviewedExtensions, 1)}
- Experimental extensions:
${generateExtensionsPageList(experimentalExtensions, 2)}
- ...
- Create your own extensions:
- Create a new extension : create.md
- best-practices.md
- share-extension.md
`;
const extensionsDotPagesFilePath = path.join(extensionsRootPath, '.pages');
@@ -433,14 +370,7 @@ ${generateExtensionsPageList(experimentalExtensions, 2)}
};
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/):
- [Reviewed extensions](#reviewed-extensions)
- [Experimental extensions](#experimental-extensions)
`;
let content = '';
const project = new gd.ProjectHelper.createNewGDJSProject();
await addAllExtensionsToProject(gd, project);
const extensionShortHeaders = await getAllExtensionShortHeaders();
@@ -477,7 +407,10 @@ Here are listed all the extensions available in GDevelop. The list is divided in
false
);
}
content += generateAllExtensionsSections(reviewedExtensions);
content += generateAllExtensionsSections({
extensions: reviewedExtensions,
baseFolder: 'extensions',
});
content += `## Experimental extensions
@@ -503,7 +436,10 @@ guarantee they meet all the quality standards of fully reviewed extensions.
true
);
}
content += generateAllExtensionsSections(experimentalExtensions);
content += generateAllExtensionsSections({
extensions: experimentalExtensions,
baseFolder: 'extensions',
});
await generateExtensionsMkDocsDotPagesFile(
reviewedExtensions,
@@ -519,7 +455,7 @@ initializeGDevelopJs().then(async gd => {
console.info(` Loading all community extensions...`);
let indexPageContent = `---
icon: material/puzzle
icon: material/star-plus
---
# Extensions

View File

@@ -9,12 +9,12 @@ const fs = require('fs').promises;
const path = require('path');
const shell = require('shelljs');
const {
gdevelopWikiUrlRoot,
getHelpLink,
generateReadMoreLink,
improperlyFormattedHelpPaths,
getExtensionFolderName,
} = require('./lib/WikiHelpLink');
const { groupBy, sortKeys } = require('./lib/ArrayHelpers');
const { generateAllExtensionsSections } = require('./lib/WikiExtensionTable');
shell.exec('node import-GDJS-Runtime.js');
@@ -27,6 +27,7 @@ const expressionsFilePath = path.join(
allFeaturesRootPath,
'expressions-reference.md'
);
const {
generateExtensionReference,
generateExtensionRawText,
@@ -94,57 +95,41 @@ const generateAllExtensionReferences = gd => {
const generateAllFeaturesStartPageRawTexts = extensionReferences => {
const headerText = {
text: `---
icon: material/star-circle
icon: material/star
---
# All features
# All GDevelop core features
This page lists **all the features** that are provided in GDevelop. These can be objects, behaviors but also features that can be used directly using actions, conditions or expressions (without requiring an object to be existing on the scene).
This page lists **all the core features** that are provided in GDevelop. These can be objects, visual effects, behaviors, actions, conditions or expressions.
Note that GDevelop can also be extended with extensions: take a look at [the list of community extensions](/gdevelop5/extensions) or learn how to create your [own set of features (behaviors, actions, conditions or expressions)](/gdevelop5/extensions/create).
GDevelop can also be extended with extensions: take a look at [the list of extended features](/gdevelop5/extensions) or learn how to create your [own extensions](/gdevelop5/extensions/create).
`,
};
const footerText = {
text: `
You can also find a **reference sheet of all expressions**:
You can also find a **reference sheet of all base expressions**:
* [Expressions reference](/gdevelop5/all-features/expressions-reference)
## More features as extensions
Remember that you can also [search for new features in the community extensions](/gdevelop5/extensions), or create your [own set of features (behaviors, actions, conditions or expressions)](/gdevelop5/extensions/create).`,
You can also [search for new features in list of extensions](/gdevelop5/extensions), or create your [own objects, behaviors, actions, conditions or expressions](/gdevelop5/extensions/create).`,
};
return [
headerText,
...extensionReferences
.filter(extensionReference => {
return !ignoredExtensionNames.includes(
extensionReference.extension.getName()
);
})
.flatMap(extensionReferences => {
const folderName = getExtensionFolderName(
extensionReferences.extension.getName()
);
const helpPagePath = extensionReferences.extension.getHelpPath();
const referencePageUrl = `${gdevelopWikiUrlRoot}/all-features/${folderName}/reference`;
const helpPageUrl = getHelpLink(helpPagePath) || referencePageUrl;
const filteredExtensions = extensionReferences
.filter(extensionReference => {
return !ignoredExtensionNames.includes(
extensionReference.extension.getName()
);
})
.map(extensionReference => extensionReference.extension);
return [
{
text:
'* ' +
// Link to help page or to reference if none.
`[${extensionReferences.extension.getFullName()}](${helpPageUrl})` +
(helpPageUrl !== referencePageUrl
? ` ([reference](${referencePageUrl}))`
: ''),
},
];
}),
footerText,
];
const groupedContent = generateAllExtensionsSections({
extensions: filteredExtensions,
baseFolder: 'all-features',
});
return [headerText, { text: groupedContent }, footerText];
};
/** @returns {RawText} */
@@ -205,6 +190,61 @@ const generateExtensionRawTexts = extensionReferences => {
return { allExtensionRawTexts };
};
/**
* Generate the .pages nav list for All features, grouped by category.
* @param {Array<ExtensionReference>} extensionReferences
* @param {number} indentationLevel
*/
const generateAllFeaturesPageList = (extensionReferences, indentationLevel) => {
const filteredExtensions = extensionReferences.filter(extensionReference => {
return !ignoredExtensionNames.includes(
extensionReference.extension.getName()
);
});
const extensionsByCategory = sortKeys(
groupBy(filteredExtensions, ref => ref.extension.getCategory() || 'General')
);
const baseIndentation = ' '.repeat(4 * indentationLevel);
let pagesList = '';
for (const category in extensionsByCategory) {
pagesList += `${baseIndentation}- ${category}:\n`;
const extensionReferences = extensionsByCategory[category]
.slice()
.sort((a, b) =>
a.extension.getFullName().localeCompare(b.extension.getFullName())
);
for (const { extension } of extensionReferences) {
const folderName = getExtensionFolderName(extension.getName());
pagesList += `${baseIndentation} - ${extension.getFullName()}: ${folderName}\n`;
}
}
return pagesList.length === 0
? pagesList
: pagesList.substring(0, pagesList.length - 1);
};
/**
* Write the .pages file for All features
* @param {Array<ExtensionReference>} extensionReferences
*/
const generateAllFeaturesMkDocsDotPagesFile = async extensionReferences => {
const dotPagesContent = `nav:
- index.md
${generateAllFeaturesPageList(extensionReferences, 1)}
- ...
- expressions-reference.md
`;
const allFeaturesDotPagesFilePath = path.join(allFeaturesRootPath, '.pages');
await fs.writeFile(allFeaturesDotPagesFilePath, dotPagesContent);
console.info(` File generated: ${allFeaturesDotPagesFilePath}`);
};
/**
* @param {Array<ExtensionReference>} extensionReferences
* @returns {Array<RawText>}
@@ -340,6 +380,9 @@ initializeGDevelopJs().then(async gd => {
);
console.info(` File generated: ${allFeaturesFilePath}`);
// Generate .pages to organize navigation by categories
await generateAllFeaturesMkDocsDotPagesFile(extensionReferences);
if (improperlyFormattedHelpPaths.size > 0) {
console.info(
`⚠️ Reference documents generated, but some help paths are invalid:`,

View File

@@ -0,0 +1,41 @@
// @ts-check
/**
* @template T
* @param {Array<T>} array
* @param {(T) => string} getKey
* @returns {Record<string, Array<T>>}
*/
const groupBy = (array, getKey) => {
/** @type {Record<string, Array<T>>} */
const table = {};
for (const element of array) {
const key = getKey(element);
let group = table[key];
if (!group) {
group = [];
table[key] = group;
}
group.push(element);
}
return table;
};
/**
* @template T
* @param {Record<string, Array<T>>} table
* @returns {Record<string, Array<T>>}
*/
const sortKeys = table => {
/** @type {Record<string, Array<T>>} */
const sortedTable = {};
for (const key of Object.keys(table).sort()) {
sortedTable[key] = table[key];
}
return sortedTable;
};
module.exports = {
groupBy,
sortKeys,
};

View File

@@ -0,0 +1,72 @@
// @ts-check
const { sortKeys, groupBy } = require('./ArrayHelpers');
const {
gdevelopWikiUrlRoot,
generateSvgImageIcon,
getExtensionFolderName,
getHelpLink,
} = require('./WikiHelpLink');
/** @typedef {import('../../../../GDevelop.js/types').EventsFunctionsExtension} EventsFunctionsExtension */
/** @typedef {import('../../../../GDevelop.js/types').PlatformExtension} PlatformExtension */
/**
* Generate a section for an extension.
* @param {EventsFunctionsExtension | PlatformExtension} extension The extension
* @param {string} baseFolder The base folder for the extension pages.
*/
const generateExtensionSection = (extension, baseFolder) => {
const folderName = getExtensionFolderName(extension.getName());
const referencePageUrl = `${gdevelopWikiUrlRoot}/${baseFolder}/${folderName}`;
const helpPageUrl = getHelpLink(extension.getHelpPath()) || referencePageUrl;
// @ts-ignore
const icon = extension.getPreviewIconUrl
? // @ts-ignore
extension.getPreviewIconUrl()
: extension.getIconUrl();
// @ts-ignore
const shortDescription = extension.getShortDescription
? // @ts-ignore
extension.getShortDescription()
: extension.getDescription().slice(0, 100) + '...';
return `|${generateSvgImageIcon(
icon
)}|**${extension.getFullName()}**|${shortDescription}|${`[Read more...](${helpPageUrl})` +
(helpPageUrl !== referencePageUrl
? ` ([reference](${referencePageUrl}))`
: '')}|\n`;
};
/**
* @param {{extensions: Array<EventsFunctionsExtension | PlatformExtension>, baseFolder: string}} options
*/
const generateAllExtensionsSections = ({ extensions, baseFolder }) => {
let extensionSectionsContent = '';
/** @type {Record<string, Array<EventsFunctionsExtension | PlatformExtension>>} */
const extensionsByCategory = sortKeys(
groupBy(extensions, pair => pair.getCategory() || 'General')
);
for (const category in extensionsByCategory) {
const extensions = extensionsByCategory[category];
extensionSectionsContent += `### ${category}\n\n`;
extensionSectionsContent += '||Name|Description||\n';
extensionSectionsContent += '|---|---|---|---|\n';
for (const extension of extensions) {
extensionSectionsContent += generateExtensionSection(
extension,
baseFolder
);
}
extensionSectionsContent += '\n';
}
return extensionSectionsContent;
};
module.exports = {
generateAllExtensionsSections,
};

View File

@@ -67,10 +67,15 @@ const getExtensionFolderName = extensionName => {
);
};
const generateSvgImageIcon = iconUrl => {
return `<img src="${iconUrl}" class="extension-icon"></img>`;
};
module.exports = {
gdevelopWikiUrlRoot,
improperlyFormattedHelpPaths,
getHelpLink,
generateReadMoreLink,
getExtensionFolderName,
generateSvgImageIcon,
};

View File

@@ -213,15 +213,17 @@ export const ErrorFallbackComponent = ({
or restart GDevelop.
</Trans>
</AlertMessage>
<BackgroundText>Error ID: {uniqueErrorId}</BackgroundText>
<BackgroundText allowSelection>
Unique Error ID for debugging: {uniqueErrorId}
</BackgroundText>
{error && error.stack && (
<BackgroundText style={styles.errorMessage}>
{error.stack.slice(0, 200)}...
<BackgroundText allowSelection style={styles.errorMessage}>
{error.stack.slice(0, 400)}...
</BackgroundText>
)}
{componentStack && (
<BackgroundText style={styles.errorMessage}>
{componentStack.slice(0, 200)}...
<BackgroundText allowSelection style={styles.errorMessage}>
{componentStack.slice(0, 300)}...
</BackgroundText>
)}
</ColumnStackLayout>