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
3 changes: 3 additions & 0 deletions .bpmnlintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "bpmnlint:recommended"
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ composer.lock

# npm
node_modules/

# generated lint config bundle (rebuilt by build:vendor)
/build/generated/

8 changes: 8 additions & 0 deletions .phpstan/dokuwiki.stub
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ namespace dokuwiki\Extension {
{
return '';
}

/**
* @return mixed
*/
public function getConf(string $key)
{
return '';
}
}

class ActionPlugin extends Plugin
Expand Down
70 changes: 65 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,59 @@

Renders using the bpmn.io js libraries within dokuwiki:

* BPMN v2.0 diagrams
* DMN v1.3 decision requirement diagrams, decision tables and literal expressions
- BPMN v2.0 diagrams
- DMN v1.3 decision requirement diagrams, decision tables and literal expressions

Refer to this page for details: <https://www.dokuwiki.org/plugin:bpmnio>

## Usage

Embed a diagram by wrapping inline XML (or referencing a media file) in a
`<bpmnio>` block:

```
<bpmnio type="bpmn">
...BPMN 2.0 XML...
</bpmnio>

<bpmnio type="bpmn" src="wiki:diagrams:zoning-map-amendment.bpmn" zoom="0.8" />
```

### Attributes

| Attribute | Values | Description |
| --------- | ----------------------- | ---------------------------------------------------------------------------------- |
| `type` | `bpmn` (default), `dmn` | Diagram kind. |
| `src` | media id | Render a stored media file instead of inline XML. |
| `zoom` | positive number | Scale factor applied to the rendered diagram. |
| `lint` | `on`, `off`, `inactive` | Per-diagram [bpmnlint](https://github.com/bpmn-io/bpmnlint) behaviour (BPMN only). |

### Linting BPMN diagrams

BPMN diagrams ship with an embedded [bpmnlint](https://github.com/bpmn-io/bpmnlint)
linter via [bpmn-js-bpmnlint](https://github.com/bpmn-io/bpmn-js-bpmnlint). A
toggle button appears in the corner of the canvas; clicking it overlays
clickable error/warning badges on the offending elements, each opening the list
of issues for that element. This works on rendered wiki pages (read-only viewer)
as well as in the editor.

The `lint` attribute controls the default state per diagram:

- `lint="on"` — overlays are shown immediately.
- `lint="inactive"` — the toggle button is present but overlays start hidden.
- `lint="off"` — the linter is not loaded for that diagram (no button).
- omitted — falls back to the global plugin setting `lint` (configurable in the
DokuWiki admin under _Configuration Settings → Plugins → bpmnio_). The
shipped default is `off`, which applies to both rendered pages and the
editor.

## Development

### Prerequisites

* PHP 8.1+
* [Composer](https://getcomposer.org/)
* Node.js 20+ and npm
- PHP
- [Composer](https://getcomposer.org/)
- Node.js and npm

### Setup

Expand Down Expand Up @@ -54,6 +95,25 @@ cd /path/to/dokuwiki
php vendor/bin/phpunit --group plugin_bpmnio
```

### Customising the BPMN linter

The lint rules are defined in [`.bpmnlintrc`](.bpmnlintrc) at the repo root and
are compiled into the committed viewer/modeler bundles at build time (bpmnlint
cannot resolve rules in the browser). The default config extends
[`bpmnlint:recommended`](https://github.com/bpmn-io/bpmnlint#built-in-rules).
After editing `.bpmnlintrc`, rebuild the bundles:

```bash
npm run build:vendor # or ./update-vendor.sh
```

To add project-specific rules, create a local
[bpmnlint plugin](https://github.com/bpmn-io/bpmnlint#plugins) (a
`bpmnlint-plugin-<name>` package exporting `rules` and a `recommended` config),
add it to `package.json` (e.g. as a `file:` dependency), reference it from
`.bpmnlintrc` via `plugin:<name>/recommended`, and rebuild. The packing step
inlines the resolved rules so no resolver is needed at runtime.

### Updating vendor libraries

The committed `vendor/` bundles are generated locally from the npm packages
Expand Down
113 changes: 112 additions & 1 deletion _test/syntax_plugin_bpmnio_bpmnio.test.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ class syntax_plugin_bpmnio_test extends DokuWikiTest
{
protected $pluginsEnabled = array('bpmnio');

public function setUp(): void
{
parent::setUp();
global $conf;
$conf['plugin']['bpmnio']['lint'] = 'inactive';
}

public function test_syntax_bpmn()
{
$info = array();
Expand All @@ -18,7 +25,7 @@ public function test_syntax_bpmn()
<div class="bpmn_js_links">
W10=
</div><div class="bpmn_js_canvas sectionedit1">
<div class="bpmn_js_container"></div>
<div class="bpmn_js_container" data-lint="inactive"></div>
</div><!-- EDIT{&quot;target&quot;:&quot;plugin_bpmnio_bpmn&quot;,&quot;secid&quot;:1,&quot;range&quot;:&quot;21-29&quot;} --></div>
OUT;

Expand Down Expand Up @@ -223,6 +230,110 @@ public function test_syntax_ignores_invalid_zoom_attribute()
$this->assertStringNotContainsString('data-zoom=', $xhtml);
}

public function test_syntax_lint_attribute()
{
$info = array();

$input = <<<IN
<bpmnio type="bpmn" lint="on">
XML...
</bpmnio>
IN;

$instructions = p_get_instructions($input);
$xhtml = p_render('xhtml', $instructions, $info);

$this->assertStringContainsString('data-lint="on"', $xhtml);
}

public function test_syntax_lint_off_attribute()
{
$info = array();

$input = <<<IN
<bpmnio type="bpmn" lint="off">
XML...
</bpmnio>
IN;

$instructions = p_get_instructions($input);
$xhtml = p_render('xhtml', $instructions, $info);

$this->assertStringContainsString('data-lint="off"', $xhtml);
}

public function test_syntax_invalid_lint_attribute_falls_back_to_default()
{
global $conf;
$conf['plugin']['bpmnio']['lint'] = 'inactive';

$info = array();

$input = <<<IN
<bpmnio type="bpmn" lint="bogus">
XML...
</bpmnio>
IN;

$instructions = p_get_instructions($input);
$xhtml = p_render('xhtml', $instructions, $info);

// Invalid attribute -> fall back to the global plugin default.
$this->assertStringContainsString('data-lint="inactive"', $xhtml);
}

public function test_syntax_missing_lint_attribute_uses_plugin_default()
{
global $conf;
$conf['plugin']['bpmnio']['lint'] = 'inactive';

$info = array();

$input = <<<IN
<bpmnio type="bpmn">
XML...
</bpmnio>
IN;

$instructions = p_get_instructions($input);
$xhtml = p_render('xhtml', $instructions, $info);

$this->assertStringContainsString('data-lint="inactive"', $xhtml);
}

public function test_syntax_lint_attribute_is_ignored_for_dmn()
{
$info = array();

$input = <<<IN
<bpmnio type="dmn" lint="on">
XML...
</bpmnio>
IN;

$instructions = p_get_instructions($input);
$xhtml = p_render('xhtml', $instructions, $info);

$this->assertStringNotContainsString('data-lint=', $xhtml);
}

public function test_syntax_lint_and_zoom_coexist()
{
$info = array();

$input = <<<IN
<bpmnio type="bpmn" zoom="1.5" lint="inactive">
XML...
</bpmnio>
IN;

$instructions = p_get_instructions($input);
$xhtml = p_render('xhtml', $instructions, $info);

$this->assertStringContainsString('data-zoom="1.5"', $xhtml);
$this->assertStringContainsString('data-lint="inactive"', $xhtml);
}

public function test_syntax_builds_link_payload_for_named_elements()
{
$info = array();
Expand Down
19 changes: 18 additions & 1 deletion action/editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,29 @@ public function handleForm(Event $event)

$form->setHiddenField('plugin_bpmnio_data', $data);
$form->setHiddenField('plugin_bpmnio_links', $linkData);

$lintAttr = '';
if ($type === 'bpmn') {
$allowed = ['on', 'off', 'inactive'];
$lint = strtolower((string) $this->getConf('lint'));
if (!in_array($lint, $allowed, true)) {
$lint = 'inactive';
}
// In the editor the linter toggle must always be available so authors
// can inspect issues. "off" is promoted to "inactive" (button present
// but overlays start hidden); "inactive" and "on" pass through as-is.
if ($lint === 'off') {
$lint = 'inactive';
}
$lintAttr = " data-lint=\"{$lint}\"";
}

$form->addHTML(<<<HTML
<div class="plugin-bpmnio" id="plugin_bpmnio__{$type}_editor">
<div class="{$type}_js_data">{$renderData}</div>
<div class="{$type}_js_links">{$linkData}</div>
<div class="{$type}_js_canvas">
<div class="{$type}_js_container"></div>
<div class="{$type}_js_container"{$lintAttr}></div>
</div>
</div>
HTML);
Expand Down
1 change: 1 addition & 0 deletions all.less
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@import "vendor/bpmn-js/dist/assets/diagram-js.less";
@import "vendor/bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.less";
@import "vendor/bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.less";
@import "vendor/bpmn-js-bpmnlint/dist/assets/css/bpmn-js-bpmnlint.less";

@import "vendor/dmn-js/dist/assets/diagram-js.less";
@import "vendor/dmn-js/dist/assets/dmn-js-shared.less";
Expand Down
67 changes: 64 additions & 3 deletions build/build-vendor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@ import { build } from 'esbuild';
import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import packConfigModule from 'bpmnlint-pack-config';

const { packConfig } = packConfigModule;

const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const nodeModulesDir = path.join(rootDir, 'node_modules');
const vendorDir = path.join(rootDir, 'vendor');
const fontDir = path.join(rootDir, 'font');
const generatedDir = path.join(rootDir, 'build', 'generated');

// .bpmnlintrc is resolved into a self-contained { config, resolver } module at
// build time. bpmnlint cannot resolve rule references in the browser, so the
// packed module is what the bpmn-viewer / bpmn-modeler entry points import.
const bpmnlintConfigPath = path.join(rootDir, '.bpmnlintrc');
const packedLintConfigPath = path.join(generatedDir, 'bpmnlintrc.packed.js');

const packages = [
{
Expand Down Expand Up @@ -153,7 +163,55 @@ async function buildBundle({ entryPoint, outFile, metadata, packageName }) {
console.log(`Built ${packageName} bundle: ${path.relative(rootDir, outFile)}`);
}

async function packLintConfig() {
if (!(await pathExists(bpmnlintConfigPath))) {
throw new Error(
`Missing .bpmnlintrc at repo root. It is required to build the linter bundle.`
);
}

await mkdir(generatedDir, { recursive: true });

// Produces an ES module exporting { config, resolver, ... } with every rule
// implementation inlined, so it can run in the browser without a resolver.
const output = await packConfig(bpmnlintConfigPath, 'es');
const banner = '/*! generated from .bpmnlintrc for dokuwiki-plugin-bpmnio — do not edit by hand */\n';

await writeFile(packedLintConfigPath, `${banner}${output.code}`);

console.log(`Packed lint config: ${path.relative(rootDir, packedLintConfigPath)}`);
}

async function copyLintAssets() {
const packageName = 'bpmn-js-bpmnlint';
const sourceDir = path.join(nodeModulesDir, packageName);

await ensureInstalled(packageName);
await cleanPackageOutput(packageName);
await copyMetadata(packageName);

// Only the stylesheet is needed as a committed asset; the JS is bundled into
// the viewer/modeler entry points. The .css is renamed to .less so DokuWiki's
// LESS pipeline (all.less) can @import it like the other vendor stylesheets.
const cssSource = path.join(sourceDir, 'dist', 'assets', 'css', 'bpmn-js-bpmnlint.css');
const cssTarget = path.join(
vendorDir,
packageName,
'dist',
'assets',
'css',
'bpmn-js-bpmnlint.less'
);

await copyFileEnsuringDir(cssSource, cssTarget);

console.log(`Copied ${packageName} stylesheet: ${path.relative(rootDir, cssTarget)}`);
}

async function main() {
await packLintConfig();
await copyLintAssets();

for (const pkg of packages) {
const metadata = await readPackageMetadata(pkg.name);
const sourceDir = path.join(nodeModulesDir, pkg.name);
Expand All @@ -176,12 +234,15 @@ async function main() {
}
}

const lintPackages = ['bpmn-js-bpmnlint', 'bpmnlint', 'bpmnlint-pack-config'];

const generatedMetadata = {
generatedAt: new Date().toISOString(),
packages: Object.fromEntries(
await Promise.all(
packages.map(async (pkg) => [pkg.name, (await readPackageMetadata(pkg.name)).version])
)
await Promise.all([
...packages.map(async (pkg) => [pkg.name, (await readPackageMetadata(pkg.name)).version]),
...lintPackages.map(async (name) => [name, (await readPackageMetadata(name)).version]),
])
),
};

Expand Down
Loading
Loading