Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ If you do not have a "composer.local.json" file yet, create one and add the foll
```
{
"require": {
"mediawiki/mermaid": "~6.0.0"
"mediawiki/mermaid": "~6.0.1"
}
}
```

If you already have a "composer.local.json" file add the following line to the end of the "require"
section in your file:

"mediawiki/mermaid": "~6.0.0"
"mediawiki/mermaid": "~6.0.1"

Remember to add a comma to the end of the preceding line in this section.

Expand Down
17 changes: 17 additions & 0 deletions docs/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
This file contains the *release notes* of the **Mermaid** extension. See also the
[readme], the [installation and configuration information] and [usage examples].

### 6.0.1

Released July 9, 2025.

The `$configMap` is now automatically generated from the `Mermaid.js` source, grouped, and type-aware.

* Fetches and parses `config.type.ts` from Mermaid.js v10.9.3
* Recursively extracts all config properties including nested interfaces into a flat dot-notation map
* Detects boolean properties and assigns FILTER_VALIDATE_BOOLEAN for correct PHP typing
* Groups configuration keys by diagram prefix (e.g. flowchart, gantt, sequence) with comment headers in the PHP array
* Automatically updates `src/MermaidConfigExtractor.php` by replacing the `$configMap` block in-place
* Eliminates manual syncing and reduces risk of human error or outdated configs
* Improves clarity and maintainability of the config structure
* Adds more test cases for different types of config keys
* Simplifies future Mermaid.js upgrades — just update the version in `scripts/generateConfigMap.ts` and regenerate
* steps how to update `configMap` added to `docs/UPDATEMERMAID.md`

### 6.0.0

Released July 7, 2025.
Expand Down
7 changes: 5 additions & 2 deletions docs/UPDATEMERMAID.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ After running `npm install`, the following occurs:

This approach ensures that ResourceLoader can treat the final file as a single script without requiring multiple scripts or worrying about load order.

### To Update `mermaid`
### To Update `mermaid` and `configMap`

1. Open `package.json` and change the version under `"mermaid"`
(e.g., `"mermaid": "latest"` or a specific version like `"10.9.3"`).

2. Run:
2. Open `scripts/generateConfigMap.ts` and change the version under `"version"`
(e.g., specific version like `"v10.9.3"` or `"mermaid@11.8.1"`, so it is important to be the same as here https://github.com/mermaid-js/mermaid/releases ).

3. Run:

```bash
npm install
Expand Down
2 changes: 1 addition & 1 deletion extension.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Mermaid",
"version": "6.0.0",
"version": "6.0.1",
"author": [
"James Hong Kong",
"Tyler Gibson"
Expand Down
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
],
"scripts": {
"build": "rollup -c",
"postinstall": "npm run build && node resources/utility/inject-nomin.cjs"
"postinstall": "npm run build && node resources/utility/inject-nomin.cjs && npx tsx scripts/generateConfigMap.ts"
},
"repository": {
"type": "git",
Expand All @@ -29,7 +29,9 @@
]
},
"dependencies": {
"mermaid": "10.9.3"
"mermaid": "10.9.3",
"node-fetch": "^2.7.0",
"ts-morph": "^26.0.0"
},
"files": [
"lib",
Expand All @@ -42,8 +44,11 @@
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@types/node": "^24.0.12",
"@types/node-fetch": "^2.6.12",
"copy-files-from-to": "^3.12.1",
"insert-line": "^1.1.0",
"rollup": "^4.0.0"
"rollup": "^4.0.0",
"tsx": "^4.20.3"
}
}
188 changes: 188 additions & 0 deletions scripts/generateConfigMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import fetch from 'node-fetch';
import { Node, SyntaxKind, TypeLiteralNode, InterfaceDeclaration, Project } from "ts-morph";
import { readFileSync, writeFileSync } from 'fs';
import path from 'path';

async function generateConfigMap() {
const version = 'v10.9.3';
const url = `https://raw.githubusercontent.com/mermaid-js/mermaid/${version}/packages/mermaid/src/config.type.ts`;
const response = await fetch(url);
const sourceCode = await response.text();

// Create a ts-morph project and parse the source code
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('config.type.ts', sourceCode);

// Get the main interface MermaidConfig
const mainIface = sourceFile.getInterfaceOrThrow('MermaidConfig');

// Helper function to find InterfaceDeclaration by name
function getInterfaceByName(name: string): InterfaceDeclaration | undefined {
return sourceFile.getInterface(name);
}

// Function to determine if a property is boolean or boolean union
function isBooleanProperty(prop: import("ts-morph").PropertySignature): boolean {
const type = prop.getType();
return type.isBoolean() || (type.isUnion() && type.getUnionTypes().some(t => t.isBoolean()));
}

// Recursive function to extract all properties from an interface, flattening nested properties
// Returns a map of property names (dot notation) to either 'FILTER_VALIDATE_BOOLEAN' or null
function extractProperties(
iface: InterfaceDeclaration,
prefix = ''
): Record<string, string | null> {
const result: Record<string, string | null> = {};

iface.getProperties().forEach(prop => {
const name = prop.getName();
const typeNode = prop.getTypeNode();

if (!typeNode) {
// No explicit type node, assign null
result[prefix + name] = null;
return;
}

if (Node.isTypeLiteral(typeNode)) {
// Inline object type (TypeLiteral) - recurse into it
const nestedProps = extractFromTypeLiteral(typeNode as TypeLiteralNode, prefix + name + '.');
Object.assign(result, nestedProps);
} else if (Node.isTypeReference(typeNode)) {
// Type reference - lookup interface and recurse
const typeName = typeNode.getText();
const refIface = getInterfaceByName(typeName);
if (refIface) {
const nestedProps = extractProperties(refIface, prefix + name + '.');
Object.assign(result, nestedProps);
} else {
// Reference not found - just assign null
result[prefix + name] = null;
}
} else {
// Other types - check if boolean to assign filter
if (isBooleanProperty(prop)) {
result[prefix + name] = 'FILTER_VALIDATE_BOOLEAN';
} else {
result[prefix + name] = null;
}
}
});

return result;
}

// Recursive extraction of properties from TypeLiteralNode, with same filter logic
function extractFromTypeLiteral(
typeLiteralNode: TypeLiteralNode,
prefix = ''
): Record<string, string | null> {
const result: Record<string, string | null> = {};

typeLiteralNode.getMembers().forEach(member => {
if (member.getKind() === SyntaxKind.PropertySignature) {
const propSig = member.asKindOrThrow(SyntaxKind.PropertySignature);
const name = propSig.getName();
const typeNode = propSig.getTypeNode();

if (!typeNode) {
result[prefix + name] = null;
return;
}

if (Node.isTypeLiteral(typeNode)) {
const nestedProps = extractFromTypeLiteral(typeNode as TypeLiteralNode, prefix + name + '.');
Object.assign(result, nestedProps);
} else if (Node.isTypeReference(typeNode)) {
const typeName = typeNode.getText();
const refIface = getInterfaceByName(typeName);
if (refIface) {
const nestedProps = extractProperties(refIface, prefix + name + '.');
Object.assign(result, nestedProps);
} else {
result[prefix + name] = null;
}
} else {
if (isBooleanProperty(propSig)) {
result[prefix + name] = 'FILTER_VALIDATE_BOOLEAN';
} else {
result[prefix + name] = null;
}
}
}
});

return result;
}

// Extract all properties starting from the main interface
const configMap = extractProperties(mainIface);

// Sort keys for nicer output
const sortedKeys = Object.keys(configMap).sort();

// Group keys by their prefix (segment before first dot), global group for keys without dot
const grouped: Record<string, string[]> = {};
for (const key of sortedKeys) {
const group = key.includes('.') ? key.split('.')[0] : 'global';
if (!grouped[group]) grouped[group] = [];
grouped[group].push(key);
}

// Indentation string: 2 tabs
const indent = '\t\t';

// Generate PHP array lines with comments grouping each diagram type
const phpLines: string[] = [];
phpLines.push('private $configMap = [');

// Output global group first without comment
if (grouped['global']) {
phpLines.push(`${indent}// global`);
for (const key of grouped['global']) {
const val = configMap[key];
phpLines.push(`${indent}'${key}' => ${val ?? 'null'},`);
}
delete grouped['global'];
}

// Output other groups with comment headers
for (const group of Object.keys(grouped).sort()) {
phpLines.push(`\n${indent}// ${group}`);
for (const key of grouped[group]) {
const val = configMap[key];
phpLines.push(`${indent}'${key}' => ${val ?? 'null'},`);
}
}

phpLines.push(`\t];`);

const newConfigMapBlock = phpLines.join('\n');

// --- Automatically update PHP file ---

const phpFilePath = path.resolve('src', 'MermaidConfigExtractor.php');
const phpFileContent = readFileSync(phpFilePath, 'utf-8');

// Regex to find the existing $configMap block
const configMapRegex = /private \$configMap = \[[\s\S]*?\];/m;

if (!configMapRegex.test(phpFileContent)) {
console.error('ERROR: Could not find $configMap block in PHP file.');
process.exit(1);
}

// Replace the old block with the new generated block
const newPhpFileContent = phpFileContent.replace(configMapRegex, newConfigMapBlock);

// Write back to the PHP file
writeFileSync(phpFilePath, newPhpFileContent, 'utf-8');

console.log(`ConfigMap updated successfully!`);
}

generateConfigMap().catch(err => {
console.error('Error:', err);
process.exit(1);
});
Loading