diff --git a/docs/devGuide/development/workflow.md b/docs/devGuide/development/workflow.md
index 1c22eb41f8..4127c6110b 100644
--- a/docs/devGuide/development/workflow.md
+++ b/docs/devGuide/development/workflow.md
@@ -375,15 +375,7 @@ To update PlantUML to a newer version:
1. Download the JAR file from [PlantUML's website](https://plantuml.com/download).
1. Rename the file to `plantuml.jar` (if required), and replace the existing JAR file located in `packages/core/src/plugins/default`.
-1. Generate the image files for the `.puml` files listed in `docs/userGuide/diagrams`.
-
-
-
-1. Add a new `.md` file in `userGuide`, e.g. `plantuml.md`, containing `` tags of all diagrams to be generated.
-1. Serve the documentation site using `markbind serve -d`.
-1. Access the corresponding HTML page with the generated diagrams, i.e. `/userGuide/plantuml.html`.
-1. Right-click on each image and save the image in `docs/userGuide/diagrams`.
-
+1. Check the HTML pages that contain PlantUML diagrams, i.e. `/userGuide/components/imagesAndDiagrams.html`.
### Updating Bootstrap and Bootswatch
diff --git a/docs/userGuide/diagrams/activity.png b/docs/userGuide/diagrams/activity.png
deleted file mode 100644
index 46e7c97c0a..0000000000
Binary files a/docs/userGuide/diagrams/activity.png and /dev/null differ
diff --git a/docs/userGuide/diagrams/archimate.png b/docs/userGuide/diagrams/archimate.png
deleted file mode 100644
index 2969980b94..0000000000
Binary files a/docs/userGuide/diagrams/archimate.png and /dev/null differ
diff --git a/docs/userGuide/diagrams/class.png b/docs/userGuide/diagrams/class.png
deleted file mode 100644
index 01d071e63b..0000000000
Binary files a/docs/userGuide/diagrams/class.png and /dev/null differ
diff --git a/docs/userGuide/diagrams/component.png b/docs/userGuide/diagrams/component.png
deleted file mode 100644
index baabac4164..0000000000
Binary files a/docs/userGuide/diagrams/component.png and /dev/null differ
diff --git a/docs/userGuide/diagrams/ditaa.png b/docs/userGuide/diagrams/ditaa.png
deleted file mode 100644
index b01ae874c6..0000000000
Binary files a/docs/userGuide/diagrams/ditaa.png and /dev/null differ
diff --git a/docs/userGuide/diagrams/entityrelation.png b/docs/userGuide/diagrams/entityrelation.png
deleted file mode 100644
index 52d3e8d4a4..0000000000
Binary files a/docs/userGuide/diagrams/entityrelation.png and /dev/null differ
diff --git a/docs/userGuide/diagrams/gantt.png b/docs/userGuide/diagrams/gantt.png
deleted file mode 100644
index 3614bbab9b..0000000000
Binary files a/docs/userGuide/diagrams/gantt.png and /dev/null differ
diff --git a/docs/userGuide/diagrams/object.png b/docs/userGuide/diagrams/object.png
deleted file mode 100644
index 6ac23d0df8..0000000000
Binary files a/docs/userGuide/diagrams/object.png and /dev/null differ
diff --git a/docs/userGuide/diagrams/sequence.png b/docs/userGuide/diagrams/sequence.png
deleted file mode 100644
index e0256b622d..0000000000
Binary files a/docs/userGuide/diagrams/sequence.png and /dev/null differ
diff --git a/docs/userGuide/diagrams/state.png b/docs/userGuide/diagrams/state.png
deleted file mode 100644
index 721659c56f..0000000000
Binary files a/docs/userGuide/diagrams/state.png and /dev/null differ
diff --git a/docs/userGuide/diagrams/usecase.png b/docs/userGuide/diagrams/usecase.png
deleted file mode 100644
index 73d8b62881..0000000000
Binary files a/docs/userGuide/diagrams/usecase.png and /dev/null differ
diff --git a/docs/userGuide/syntax/diagrams.md b/docs/userGuide/syntax/diagrams.md
index fcaf41778f..08f0a9afec 100644
--- a/docs/userGuide/syntax/diagrams.md
+++ b/docs/userGuide/syntax/diagrams.md
@@ -36,10 +36,9 @@ See [Deploying via Github Actions](../deployingTheSite.html#deploying-via-github
-
+
-```
@startuml
alice -> bob ++ : hello
@@ -52,11 +51,6 @@ bob -> george !! : delete
return success
@enduml
-```
-
-
-
-
@@ -88,7 +82,7 @@ in another file:
-
+
@@ -103,50 +97,50 @@ The full PlantUML syntax reference can be found at plantuml.com/guide
**Sequence Diagram**:
-
+
**Use Case Diagram**:
-
+
**Class Diagram**:
-
+
**Activity Diagram**:
-
+
**Component Diagram**:
-
+
**State Diagram**:
-
+
**Object Diagram**:
-
+
**Gantt Diagram**:
-
+
**Entity Relation Diagram**:
-
+
**Ditaa Diagram**:
-
+
**Archimate Diagram**:
-
+
****Options****
-Name | Type | Description
---- | --- | ---
-alt | `string` | The alternative text of the diagram.
+Name | Type | Description
+-----|----------|-------------------------------------
+alt | `string` | The alternative text of the diagram.
height | `string` | The height of the diagram in pixels.
-name | `string` | The name of the output file.
-src | `string` | The URL of the diagram if your diagram is in another `.puml` file.
The URL can be specified as absolute or relative references. More info in: _[Intra-Site Links]({{baseUrl}}/userGuide/formattingContents.html#intraSiteLinks)_
-width | `string` | The width of the diagram in pixels.
If both width and height are specified, width takes priority over height. It is to maintain the diagram's aspect ratio.
+name | `string` | The name of the output file.
Avoid using the same name for different diagrams to prevent overwriting.
+src | `string` | The URL of the diagram if your diagram is in another `.puml` file.
The URL can be specified as absolute or relative references. More info in: _[Intra-Site Links]({{baseUrl}}/userGuide/formattingContents.html#intraSiteLinks)_
+width | `string` | The width of the diagram in pixels.
If both width and height are specified, width takes priority over height. It is to maintain the diagram's aspect ratio.
diff --git a/packages/core/src/Page/index.ts b/packages/core/src/Page/index.ts
index 30e4a06940..f836a6cc81 100644
--- a/packages/core/src/Page/index.ts
+++ b/packages/core/src/Page/index.ts
@@ -23,6 +23,8 @@ require('../patches/htmlparser2');
const _ = { cloneDeep, isObject, isArray };
+const LockManager = require('../utils/LockManager');
+
const PACKAGE_VERSION = require('../../package.json').version;
const {
@@ -534,6 +536,9 @@ export class Page {
// Each source path will only contain 1 copy of build/re-build page (the latest one)
pageVueServerRenderer.pageEntries[this.pageConfig.sourcePath] = builtPage;
+ // Wait for all pages resources to be generated before writing to disk
+ await LockManager.waitForLockRelease();
+
/*
* Server-side render Vue page app into actual html.
*
diff --git a/packages/core/src/plugins/default/markbind-plugin-plantuml.ts b/packages/core/src/plugins/default/markbind-plugin-plantuml.ts
index 72739e6c82..5b709af4e0 100644
--- a/packages/core/src/plugins/default/markbind-plugin-plantuml.ts
+++ b/packages/core/src/plugins/default/markbind-plugin-plantuml.ts
@@ -15,10 +15,24 @@ import * as urlUtil from '../../utils/urlUtil';
import { PluginContext } from '../Plugin';
import { NodeProcessorConfig } from '../../html/NodeProcessor';
import { MbNode } from '../../utils/node';
+import LockManager from '../../utils/LockManager';
+
+interface DiagramStatus {
+ hashKey: string;
+}
const JAR_PATH = path.resolve(__dirname, 'plantuml.jar');
-const processedDiagrams = new Set();
+const PUML_EXT = '.png';
+
+/**
+* This Map maintains a record of processed diagrams. When a diagram is generated or regenerated,
+* it's added to this map. Subsequently, if a PUML or non-PUML file is edited, leading to a hot reload,
+* the generateDiagram function can avoid redundant regeneration by checking this map.
+* If the diagram's identifier is present in the map,
+* the generation process is bypassed, thus preventing duplicates.
+ */
+const processedDiagrams = new Map();
let graphvizCheckCompleted = false;
@@ -28,15 +42,22 @@ let graphvizCheckCompleted = false;
* @param content puml dsl used to generate the puml diagram
*/
function generateDiagram(imageOutputPath: string, content: string) {
+ const hashKey = cryptoJS.MD5(imageOutputPath + content).toString();
+
// Avoid generating twice
- if (processedDiagrams.has(imageOutputPath)) { return; }
- processedDiagrams.add(imageOutputPath);
+ if (processedDiagrams.has(imageOutputPath) && processedDiagrams.get(imageOutputPath)?.hashKey === hashKey) {
+ return;
+ }
// Creates output dir if it doesn't exist
const outputDir = path.dirname(imageOutputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
+ const lockId = LockManager.createLock();
+
+ // Add new diagram to the map
+ processedDiagrams.set(imageOutputPath, { hashKey });
// Java command to launch PlantUML jar
const cmd = `java -jar "${JAR_PATH}" -nometadata -pipe > "${imageOutputPath}"`;
@@ -59,6 +80,7 @@ function generateDiagram(imageOutputPath: string, content: string) {
childProcess.on('error', (error) => {
logger.debug(error as unknown as string);
logger.error(`Error generating ${imageOutputPath}`);
+ LockManager.deleteLock(lockId);
});
childProcess.stderr?.on('data', (errorMsg) => {
@@ -68,6 +90,7 @@ function generateDiagram(imageOutputPath: string, content: string) {
childProcess.on('exit', () => {
// This goes to the log file, but not shown on the console
logger.debug(errorLog);
+ LockManager.deleteLock(lockId);
});
}
@@ -90,7 +113,6 @@ export = {
},
beforeSiteGenerate: () => {
- processedDiagrams.clear();
graphvizCheckCompleted = false;
},
@@ -111,6 +133,7 @@ export = {
let pumlContent;
let pathFromRootToImage;
+
if (node.attribs.src) {
const srcWithoutBaseUrl = urlUtil.stripBaseUrl(node.attribs.src, config.baseUrl);
const srcWithoutLeadingSlash = srcWithoutBaseUrl.startsWith('/')
@@ -126,8 +149,8 @@ export = {
return;
}
- pathFromRootToImage = fsUtil.setExtension(srcWithoutLeadingSlash, '.png');
- node.attribs.src = fsUtil.ensurePosix(fsUtil.setExtension(node.attribs.src, '.png'));
+ pathFromRootToImage = fsUtil.setExtension(srcWithoutLeadingSlash, PUML_EXT);
+ node.attribs.src = fsUtil.ensurePosix(fsUtil.setExtension(node.attribs.src, PUML_EXT));
} else {
pumlContent = cheerio(node).text();
@@ -136,13 +159,13 @@ export = {
const nameWithoutLeadingSlash = nameWithoutBaseUrl.startsWith('/')
? nameWithoutBaseUrl.substring(1)
: nameWithoutBaseUrl;
- pathFromRootToImage = fsUtil.ensurePosix(fsUtil.setExtension(nameWithoutLeadingSlash, '.png'));
+ pathFromRootToImage = fsUtil.ensurePosix(fsUtil.setExtension(nameWithoutLeadingSlash, PUML_EXT));
delete node.attribs.name;
} else {
const normalizedContent = pumlContent.replace(/\r\n/g, '\n');
const hashedContent = cryptoJS.MD5(normalizedContent).toString();
- pathFromRootToImage = `${hashedContent}.png`;
+ pathFromRootToImage = `${hashedContent}${PUML_EXT}`;
}
node.attribs.src = `${config.baseUrl}/${pathFromRootToImage}`;
diff --git a/packages/core/src/utils/LockManager.ts b/packages/core/src/utils/LockManager.ts
new file mode 100644
index 0000000000..7e3781d86f
--- /dev/null
+++ b/packages/core/src/utils/LockManager.ts
@@ -0,0 +1,86 @@
+import { v4 as uuidv4 } from 'uuid';
+
+/**
+ * The `LockManager` is a singleton class designed to help wait for required async
+ * promised operations to complete
+ * before the page is generated. It provides functionalities to create, delete, and wait
+ * for the release of locks.
+ * The locks are stored in a Map with a unique ID (either provided or auto-generated) as
+ * the key.
+ * The class provides an instance property to get the singleton instance of `LockManager`.
+ */
+
+class LockManager {
+ // Holds the single instance of LockManager.
+ private static _instance: LockManager;
+
+ // A Map to keep track of the active locks.
+ private locks: Map;
+
+ /**
+ * Private constructor to prevent direct instantiation from outside.
+ * Initializes the locks Map.
+ */
+ private constructor() {
+ this.locks = new Map();
+ }
+
+ /**
+ * Provides a way to access the single instance of the LockManager.
+ * If it doesn't exist, it creates one.
+ * @returns {LockManager} The single instance of LockManager.
+ */
+ public static get instance() {
+ if (!LockManager._instance) {
+ LockManager._instance = new LockManager();
+ }
+
+ return LockManager._instance;
+ }
+
+ /**
+ * Creates a new lock.
+ * @param {string} [id] - An optional ID to use for the lock. If not provided, a UUID will be generated.
+ * @returns {string} The ID of the created lock.
+ */
+ createLock(id?: string): string {
+ const lockId = id ?? uuidv4();
+ this.locks.set(lockId, true);
+ return lockId;
+ }
+
+ /**
+ * Deletes a lock by its ID.
+ * @param {string} lockId - The ID of the lock to be deleted.
+ */
+ deleteLock(lockId: string): void {
+ this.locks.delete(lockId);
+ }
+
+ /**
+ * Deletes all locks, clearing the locks Map.
+ */
+ deleteAllLocks(): void {
+ this.locks.clear();
+ }
+
+ /**
+ * Waits until all locks are released and then resolves.
+ * @returns {Promise} A promise that resolves when all locks are released.
+ */
+ waitForLockRelease(): Promise {
+ return new Promise((resolve) => {
+ const checkLocks = () => {
+ if (this.locks.size === 0) {
+ resolve();
+ } else {
+ setTimeout(checkLocks, 100);
+ }
+ };
+ checkLocks();
+ });
+ }
+}
+
+// Export the singleton instance of LockManager.
+export = LockManager.instance;
diff --git a/packages/core/test/unit/utils/LockManager.test.ts b/packages/core/test/unit/utils/LockManager.test.ts
new file mode 100644
index 0000000000..cc4b8bac83
--- /dev/null
+++ b/packages/core/test/unit/utils/LockManager.test.ts
@@ -0,0 +1,42 @@
+import LockManager from '../../../src/utils/LockManager';
+
+describe('LockManager', () => {
+ let lockManager: typeof LockManager;
+
+ beforeEach(() => {
+ lockManager = LockManager;
+ });
+
+ afterEach(() => {
+ lockManager.deleteAllLocks();
+ });
+
+ it('should create a new lock', () => {
+ const lockId = lockManager.createLock();
+ expect(lockId).toBeDefined();
+ });
+
+ it('should use the provided ID when creating a lock', () => {
+ const lockId = 'customId';
+ const createdLockId = lockManager.createLock(lockId);
+ expect(createdLockId).toEqual(lockId);
+ });
+
+ it('should delete all locks', () => {
+ lockManager.createLock();
+ lockManager.createLock();
+ lockManager.deleteAllLocks();
+ });
+
+ it('should wait until all locks are released and resolve', async () => {
+ const lockId1 = lockManager.createLock();
+ const lockId2 = lockManager.createLock();
+
+ const waitForLockReleasePromise = lockManager.waitForLockRelease();
+
+ setTimeout(() => lockManager.deleteLock(lockId1), 100);
+ setTimeout(() => lockManager.deleteLock(lockId2), 200);
+
+ await expect(waitForLockReleasePromise).resolves.toBeUndefined();
+ });
+});