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(); + }); +});