chore(ckeditor5-mermaid): integrate the rest of the files

This commit is contained in:
Elian Doran
2025-05-04 18:19:26 +03:00
parent 89f95f6187
commit 07c2f1805e
31 changed files with 44 additions and 540 deletions

View File

@@ -0,0 +1,93 @@
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import {
_setModelData as setModelData,
_getModelData as getModelData
} from '@ckeditor/ckeditor5-engine';
import InsertMermaidCommand from '../../src/commands/insertMermaidCommand.js';
import MermaidEditing from '../../src/mermaidediting.js';
/* global document */
describe( 'InsertMermaidCommand', () => {
let domElement, editor, model, command;
beforeEach( async () => {
domElement = document.createElement( 'div' );
document.body.appendChild( domElement );
editor = await ClassicEditor.create( domElement, {
plugins: [
MermaidEditing,
Paragraph
]
} );
model = editor.model;
command = new InsertMermaidCommand( editor );
} );
afterEach( () => {
domElement.remove();
return editor.destroy();
} );
describe( '#isEnabled', () => {
describe( 'should be false', () => {
it( 'when selection is inside mermaid', () => {
setModelData( model,
'<paragraph>foo</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C">[]</mermaid>'
);
expect( command.isEnabled ).to.be.false;
} );
it( 'when mermaid is selected', () => {
setModelData( model,
'<paragraph>foo</paragraph>' +
'[<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>]'
);
expect( command.isEnabled ).to.be.false;
} );
} );
describe( 'should be true', () => {
it( 'when text is selected', () => {
setModelData( model,
'<paragraph>[foo]</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>'
);
expect( command.isEnabled ).to.be.true;
} );
it( 'when mermaid is part of the selection', () => {
setModelData( model,
'<paragraph>[foo</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>' +
'<paragraph>b]az</paragraph>'
);
expect( command.isEnabled ).to.be.true;
} );
} );
} );
describe( 'execute()', () => {
it( 'should add sample mermaid', () => {
setModelData( model,
'<paragraph>[foo]</paragraph>'
);
command.execute();
expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
'<mermaid displayMode="split" source="flowchart TB\nA --> B\nB --> C"></mermaid>'
);
} );
} );
} );

View File

@@ -0,0 +1,113 @@
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import {
_setModelData as setModelData,
_getModelData as getModelData
} from '@ckeditor/ckeditor5-engine';
import MermaidPreviewCommand from '../../src/commands/mermaidPreviewCommand.js';
import MermaidEditing from '../../src/mermaidediting.js';
/* global document */
describe( 'MermaidPreviewCommand', () => {
let domElement, editor, model, command;
beforeEach( async () => {
domElement = document.createElement( 'div' );
document.body.appendChild( domElement );
editor = await ClassicEditor.create( domElement, {
plugins: [
MermaidEditing,
Paragraph
]
} );
model = editor.model;
command = new MermaidPreviewCommand( editor );
} );
afterEach( () => {
domElement.remove();
return editor.destroy();
} );
describe( '#value', () => {
it( 'should be true when mermaid element has displayMode attribute equal to "preview"', () => {
setModelData( model, '<mermaid displayMode="preview" source="foo"></mermaid>' );
expect( command.value ).to.equal( true );
} );
it( 'should be false when mermaid element has displayMode attribute equal to "split"', () => {
setModelData( model, '<mermaid displayMode="split" source="foo"></mermaid>' );
expect( command.value ).to.equal( false );
} );
it( 'should be false when mermaid element has displayMode attribute equal to "source"', () => {
setModelData( model, '<mermaid displayMode="source" source="foo"></mermaid>' );
expect( command.value ).to.equal( false );
} );
} );
describe( '#isEnabled', () => {
describe( 'should be false', () => {
it( 'when text is selected', () => {
setModelData( model,
'<paragraph>[foo]</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>'
);
expect( command.isEnabled ).to.be.false;
} );
it( 'when mermaid is part of the selection', () => {
setModelData( model,
'<paragraph>[foo</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>' +
'<paragraph>b]az</paragraph>'
);
expect( command.isEnabled ).to.be.false;
} );
} );
describe( 'should be true', () => {
it( 'when selection is inside mermaid', () => {
setModelData( model,
'<paragraph>foo</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C">[]</mermaid>'
);
expect( command.isEnabled ).to.be.true;
} );
it( 'when mermaid is selected', () => {
setModelData( model,
'<paragraph>foo</paragraph>' +
'[<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>]'
);
expect( command.isEnabled ).to.be.true;
} );
} );
} );
describe( 'execute()', () => {
it( 'should change displayMode to "preview" for mermaid', () => {
setModelData( model,
'[<mermaid displayMode="source" source="foo"></mermaid>]'
);
command.execute();
expect( getModelData( model ) ).to.equal(
'[<mermaid displayMode="preview" source="foo"></mermaid>]'
);
} );
} );
} );

View File

@@ -0,0 +1,113 @@
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import {
_setModelData as setModelData,
_getModelData as getModelData
} from '@ckeditor/ckeditor5-engine';
import MermaidSourceViewCommand from '../../src/commands/mermaidSourceViewCommand.js';
import MermaidEditing from '../../src/mermaidediting.js';
/* global document */
describe( 'MermaidSourceViewCommand', () => {
let domElement, editor, model, command;
beforeEach( async () => {
domElement = document.createElement( 'div' );
document.body.appendChild( domElement );
editor = await ClassicEditor.create( domElement, {
plugins: [
MermaidEditing,
Paragraph
]
} );
model = editor.model;
command = new MermaidSourceViewCommand( editor );
} );
afterEach( () => {
domElement.remove();
return editor.destroy();
} );
describe( '#value', () => {
it( 'should be true when mermaid element has displayMode attribute equal to "preview"', () => {
setModelData( model, '<mermaid displayMode="preview" source="foo"></mermaid>' );
expect( command.value ).to.equal( false );
} );
it( 'should be false when mermaid element has displayMode attribute equal to "split"', () => {
setModelData( model, '<mermaid displayMode="split" source="foo"></mermaid>' );
expect( command.value ).to.equal( false );
} );
it( 'should be false when mermaid element has displayMode attribute equal to "source"', () => {
setModelData( model, '<mermaid displayMode="source" source="foo"></mermaid>' );
expect( command.value ).to.equal( true );
} );
} );
describe( '#isEnabled', () => {
describe( 'should be false', () => {
it( 'when text is selected', () => {
setModelData( model,
'<paragraph>[foo]</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>'
);
expect( command.isEnabled ).to.be.false;
} );
it( 'when mermaid is part of the selection', () => {
setModelData( model,
'<paragraph>[foo</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>' +
'<paragraph>b]az</paragraph>'
);
expect( command.isEnabled ).to.be.false;
} );
} );
describe( 'should be true', () => {
it( 'when selection is inside mermaid', () => {
setModelData( model,
'<paragraph>foo</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C">[]</mermaid>'
);
expect( command.isEnabled ).to.be.true;
} );
it( 'when mermaid is selected', () => {
setModelData( model,
'<paragraph>foo</paragraph>' +
'[<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>]'
);
expect( command.isEnabled ).to.be.true;
} );
} );
} );
describe( 'execute()', () => {
it( 'should add text', () => {
setModelData( model,
'[<mermaid displayMode="preview" source="foo"></mermaid>]'
);
command.execute();
expect( getModelData( model ) ).to.equal(
'[<mermaid displayMode="source" source="foo"></mermaid>]'
);
} );
} );
} );

View File

@@ -0,0 +1,113 @@
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import {
_setModelData as setModelData,
_getModelData as getModelData
} from '@ckeditor/ckeditor5-engine';
import MermaidSplitViewCommand from '../../src/commands/mermaidSplitViewCommand.js';
import MermaidEditing from '../../src/mermaidediting.js';
/* global document */
describe( 'MermaidSplitViewCommand', () => {
let domElement, editor, model, command;
beforeEach( async () => {
domElement = document.createElement( 'div' );
document.body.appendChild( domElement );
editor = await ClassicEditor.create( domElement, {
plugins: [
MermaidEditing,
Paragraph
]
} );
model = editor.model;
command = new MermaidSplitViewCommand( editor );
} );
afterEach( () => {
domElement.remove();
return editor.destroy();
} );
describe( '#value', () => {
it( 'should be true when mermaid element has displayMode attribute equal to "preview"', () => {
setModelData( model, '<mermaid displayMode="preview" source="foo"></mermaid>' );
expect( command.value ).to.equal( false );
} );
it( 'should be false when mermaid element has displayMode attribute equal to "split"', () => {
setModelData( model, '<mermaid displayMode="split" source="foo"></mermaid>' );
expect( command.value ).to.equal( true );
} );
it( 'should be false when mermaid element has source attribute equal to "source"', () => {
setModelData( model, '<mermaid displayMode="source" source="foo"></mermaid>' );
expect( command.value ).to.equal( false );
} );
} );
describe( '#isEnabled', () => {
describe( 'should be false', () => {
it( 'when text is selected', () => {
setModelData( model,
'<paragraph>[foo]</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>'
);
expect( command.isEnabled ).to.be.false;
} );
it( 'when mermaid is part of the selection', () => {
setModelData( model,
'<paragraph>[foo</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>' +
'<paragraph>b]az</paragraph>'
);
expect( command.isEnabled ).to.be.false;
} );
} );
describe( 'should be true', () => {
it( 'when selection is inside mermaid', () => {
setModelData( model,
'<paragraph>foo</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C">[]</mermaid>'
);
expect( command.isEnabled ).to.be.true;
} );
it( 'when mermaid is selected', () => {
setModelData( model,
'<paragraph>foo</paragraph>' +
'[<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>]'
);
expect( command.isEnabled ).to.be.true;
} );
} );
} );
describe( 'execute()', () => {
it( 'should change displayMode to "source" for mermaid', () => {
setModelData( model,
'[<mermaid displayMode="source" source="foo"></mermaid>]'
);
command.execute();
expect( getModelData( model ) ).to.equal(
'[<mermaid displayMode="split" source="foo"></mermaid>]'
);
} );
} );
} );

View File

@@ -0,0 +1,32 @@
import { Mermaid as MermaidDll, icons } from '../src/index.js';
import Mermaid from '../src/mermaid.js';
import infoIcon from './../theme/icons/info.svg';
import insertMermaidIcon from './../theme/icons/insert.svg';
import previewModeIcon from './../theme/icons/preview-mode.svg';
import splitModeIcon from './../theme/icons/split-mode.svg';
import sourceModeIcon from './../theme/icons/source-mode.svg';
describe( 'CKEditor5 Mermaid DLL', () => {
it( 'exports MermaidWidget', () => {
expect( MermaidDll ).to.equal( Mermaid );
} );
describe( 'icons', () => {
it( 'exports the "insertMermaidIcon" icon', () => {
expect( icons.insertMermaidIcon ).to.equal( insertMermaidIcon );
} );
it( 'exports the "infoIcon" icon', () => {
expect( icons.infoIcon ).to.equal( infoIcon );
} );
it( 'exports the "previewModeIcon" icon', () => {
expect( icons.previewModeIcon ).to.equal( previewModeIcon );
} );
it( 'exports the "splitModeIcon" icon', () => {
expect( icons.splitModeIcon ).to.equal( splitModeIcon );
} );
it( 'exports the "sourceModeIcon" icon', () => {
expect( icons.sourceModeIcon ).to.equal( sourceModeIcon );
} );
} );
} );

View File

@@ -1,17 +0,0 @@
import { describe, expect, it } from 'vitest';
import { Mermaid as MermaidDll, icons } from '../src/index.js';
import Mermaid from '../src/mermaid.js';
import ckeditor from './../theme/icons/ckeditor.svg';
describe( 'CKEditor5 Mermaid DLL', () => {
it( 'exports Mermaid', () => {
expect( MermaidDll ).to.equal( Mermaid );
} );
describe( 'icons', () => {
it( 'exports the "ckeditor" icon', () => {
expect( icons.ckeditor ).to.equal( ckeditor );
} );
} );
} );

View File

@@ -0,0 +1,67 @@
<style type="text/css">
#editor, .ck-editor {
/* Adjust width to the typical width in GH. */
width: 820px !important;
}
pre.markdown-output {
background: hsl(70, 7%, 16%);
color: hsl(0, 0%, 100%);
display: block;
font-size: 1em;
font-family: Monaco, Menlo, Consolas, "Roboto Mono", "Courier New", "Ubuntu Mono", monospace;
padding: 1.333em;
}
.editor-container {
display:flex;
}
.output-container {
padding-left: 20px;
}
</style>
<div class="editor-container">
<textarea id="editor">
Mermaid snippet:
```mermaid
flowchart TB
A --> C
A --> D
B --> C
B --> D
```
More complex case:
```mermaid
sequenceDiagram
participant Alice
participant Bob
Alice->>John: Hello John, how are you?
loop Healthcheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts <br/>prevail!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
```
Javascript snippet:
```Javascript
var foo = 'bar';
alert( foo );
```
</textarea>
<div class="output-container">
<p>Output:</p>
<pre class="markdown-output"><code id="markdown-output"></code></pre>
</div>
</div>

View File

@@ -0,0 +1,67 @@
/**
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/* globals console, window, document */
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
import { Typing } from '@ckeditor/ckeditor5-typing';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { Undo } from '@ckeditor/ckeditor5-undo';
import { Enter } from '@ckeditor/ckeditor5-enter';
import { Clipboard } from '@ckeditor/ckeditor5-clipboard';
import { Link } from '@ckeditor/ckeditor5-link';
import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles';
import { Markdown } from '@ckeditor/ckeditor5-markdown-gfm';
import { CodeBlock } from '@ckeditor/ckeditor5-code-block';
import Mermaid from '../../src/mermaid.js';
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [
Markdown,
Typing,
Paragraph,
Undo,
Enter,
Clipboard,
Link,
Bold,
Italic,
CodeBlock,
Mermaid
],
toolbar: [ 'bold', 'italic', 'link', 'undo', 'redo', 'codeBlock', 'mermaid' ],
codeBlock: {
languages: [
{ language: 'plaintext', label: 'Plain text', class: '' },
{ language: 'javascript', label: 'JavaScript' },
{ language: 'python', label: 'Python' },
{ language: 'mermaid', label: 'Mermaid' }
]
}
} )
.then( editor => {
window.editor = editor;
setupMarkdownOutputPreview( editor );
} )
.catch( err => {
console.error( err.stack );
} );
function setupMarkdownOutputPreview( editor ) {
const outputElement = document.querySelector( '#markdown-output' );
editor.model.document.on( 'change', () => {
outputElement.innerText = editor.getData();
} );
// Set the initial data with delay so hightlight.js doesn't catch them.
window.setTimeout( () => {
outputElement.innerText = editor.getData();
}, 500 );
}

View File

@@ -0,0 +1 @@
## Mermaid widget

View File

@@ -0,0 +1,38 @@
<style type="text/css">
#editor, .ck-editor {
/* Adjust width to the typical width in GH. */
width: 820px !important;
}
</style>
<div id="editor">
<p>Mermaid snippet:</p>
<pre>
<code class="mermaid language-mermaid">
flowchart TB
A --> C
A --> D
B --> C
B --> D
</code>
</pre>
<p>More complex case:</p>
<pre>
<code class="mermaid language-mermaid">
sequenceDiagram
participant Alice
participant Bob
Alice->>John: Hello John, how are you?
loop Healthcheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts <br/>prevail!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
</code>
</pre>
</div>

View File

@@ -0,0 +1,50 @@
/**
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/* globals console, window, document */
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
import { Typing } from '@ckeditor/ckeditor5-typing';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { Undo } from '@ckeditor/ckeditor5-undo';
import { Enter } from '@ckeditor/ckeditor5-enter';
import { Clipboard } from '@ckeditor/ckeditor5-clipboard';
import { Link } from '@ckeditor/ckeditor5-link';
import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles';
import { CodeBlock } from '@ckeditor/ckeditor5-code-block';
import Mermaid from '../../src/mermaid.js';
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [
Typing,
Paragraph,
Undo,
Enter,
Clipboard,
Link,
Bold,
Italic,
CodeBlock,
Mermaid
],
toolbar: [ 'bold', 'italic', 'link', 'undo', 'redo', 'codeBlock', 'mermaid' ],
codeBlock: {
languages: [
{ language: 'plaintext', label: 'Plain text', class: '' },
{ language: 'javascript', label: 'JavaScript' },
{ language: 'python', label: 'Python' },
{ language: 'mermaid', label: 'Mermaid' }
]
}
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

View File

@@ -0,0 +1 @@
## Mermaid widget

View File

@@ -0,0 +1,47 @@
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
import { Essentials } from '@ckeditor/ckeditor5-essentials';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { Heading } from '@ckeditor/ckeditor5-heading';
import { _setModelData as setModelData } from '@ckeditor/ckeditor5-engine';
import Mermaid from '../src/mermaid.js';
/* global document */
describe( 'Mermaid', () => {
it( 'should be named', () => {
expect( Mermaid.pluginName ).to.equal( 'Mermaid' );
} );
describe( 'init()', () => {
let domElement, editor;
beforeEach( async () => {
domElement = document.createElement( 'div' );
document.body.appendChild( domElement );
editor = await ClassicEditor.create( domElement, {
plugins: [
Paragraph,
Heading,
Essentials,
Mermaid
],
toolbar: [
'mermaid'
]
} );
setModelData( editor.model, '<paragraph>[]</paragraph>' );
} );
afterEach( () => {
domElement.remove();
return editor.destroy();
} );
it( 'should add an icon to the toolbar', () => {
expect( editor.ui.componentFactory.has( 'Mermaid' ) ).to.equal( true );
} );
} );
} );

View File

@@ -1,56 +0,0 @@
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { ClassicEditor, Essentials, Paragraph, Heading } from 'ckeditor5';
import Mermaid from '../src/mermaid.js';
describe( 'Mermaid', () => {
it( 'should be named', () => {
expect( Mermaid.pluginName ).to.equal( 'Mermaid' );
} );
describe( 'init()', () => {
let domElement: HTMLElement, editor: ClassicEditor;
beforeEach( async () => {
domElement = document.createElement( 'div' );
document.body.appendChild( domElement );
editor = await ClassicEditor.create( domElement, {
licenseKey: 'GPL',
plugins: [
Paragraph,
Heading,
Essentials,
Mermaid
],
toolbar: [
'mermaid'
]
} );
} );
afterEach( () => {
domElement.remove();
return editor.destroy();
} );
it( 'should load Mermaid', () => {
const myPlugin = editor.plugins.get( 'Mermaid' );
expect( myPlugin ).to.be.an.instanceof( Mermaid );
} );
it( 'should add an icon to the toolbar', () => {
expect( editor.ui.componentFactory.has( 'mermaid' ) ).to.equal( true );
} );
it( 'should add a text into the editor after clicking the icon', () => {
const icon = editor.ui.componentFactory.create( 'mermaid' );
expect( editor.getData() ).to.equal( '' );
icon.fire( 'execute' );
expect( editor.getData() ).to.equal( '<p>Hello CKEditor 5!</p>' );
} );
} );
} );

View File

@@ -0,0 +1,296 @@
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
import { Essentials } from '@ckeditor/ckeditor5-essentials';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { Heading } from '@ckeditor/ckeditor5-heading';
import { CodeBlockEditing } from '@ckeditor/ckeditor5-code-block';
import {
_setModelData as setModelData,
_getModelData as getModelData,
_getViewData as getViewData
} from '@ckeditor/ckeditor5-engine';
import MermaidEditing from '../src/mermaidediting.js';
/* global document */
describe( 'MermaidEditing', () => {
it( 'should be named', () => {
expect( MermaidEditing.pluginName ).to.equal( 'MermaidEditing' );
} );
describe( 'conversion', () => {
let domElement, editor, model;
beforeEach( async () => {
domElement = document.createElement( 'div' );
document.body.appendChild( domElement );
editor = await ClassicEditor.create( domElement, {
plugins: [
Paragraph,
Heading,
Essentials,
CodeBlockEditing,
MermaidEditing
]
} );
model = editor.model;
} );
afterEach( () => {
domElement.remove();
return editor.destroy();
} );
describe( 'conversion', () => {
describe( 'upcast', () => {
it( 'works correctly', () => {
editor.setData(
'<pre spellcheck="false">' +
'<code class="language-mermaid">flowchart TB\nA --> B\nB --> C</code>' +
'</pre>'
);
expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
'<mermaid displayMode="split" source="flowchart TB\nA --> B\nB --> C">' +
'</mermaid>'
);
} );
it( 'works correctly when empty', () => {
editor.setData(
'<pre spellcheck="false">' +
'<code class="language-mermaid"></code>' +
'</pre>'
);
expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
'<mermaid displayMode="split" source=""></mermaid>'
);
} );
} );
describe( 'data downcast', () => {
it( 'works correctly', () => {
// Using editor.setData() instead of setModelData helper because of #11365.
editor.setData(
'<pre spellcheck="false">' +
'<code class="language-mermaid">flowchart TB\nA --> B\nB --> C</code>' +
'</pre>'
);
expect( editor.getData() ).to.equal(
'<pre spellcheck="false">' +
'<code class="language-mermaid">flowchart TB\nA --&gt; B\nB --&gt; C</code>' +
'</pre>'
);
} );
it( 'works correctly when empty ', () => {
// Using editor.setData() instead of setModelData helper because of #11365.
editor.setData(
'<pre spellcheck="false">' +
'<code class="language-mermaid"></code>' +
'</pre>'
);
expect( editor.getData() ).to.equal(
'<pre spellcheck="false">' +
'<code class="language-mermaid"></code>' +
'</pre>'
);
} );
} );
describe( 'editing downcast', () => {
it( 'works correctly without displayMode attribute', () => {
// Using editor.setData() instead of setModelData helper because of #11365.
editor.setData(
'<pre spellcheck="false">' +
'<code class="language-mermaid">flowchart TB\nA --> B\nB --> C</code>' +
'</pre>'
);
expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
'<div class="ck-mermaid__split-mode ck-mermaid__wrapper ck-widget ck-widget_selected' +
' ck-widget_with-selection-handle" contenteditable="false">' +
'<div class="ck ck-widget__selection-handle"></div>' +
// New lines replaced with space, same issue in getViewData as in #11365.
'<textarea class="ck-mermaid__editing-view" data-cke-ignore-events="true"' +
' placeholder="Insert mermaid source code"></textarea>' +
'<div class="ck-mermaid__preview"></div>' +
'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
'</div>'
);
} );
it( 'works correctly with displayMode attribute', () => {
setModelData( editor.model,
'<mermaid source="foo" displayMode="preview"></mermaid>'
);
expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
'<div class="ck-mermaid__preview-mode ck-mermaid__wrapper ck-widget ck-widget_selected ' +
'ck-widget_with-selection-handle" contenteditable="false">' +
'<div class="ck ck-widget__selection-handle"></div>' +
'<textarea class="ck-mermaid__editing-view" data-cke-ignore-events="true"' +
' placeholder="Insert mermaid source code"></textarea>' +
'<div class="ck-mermaid__preview"></div>' +
'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
'</div>'
);
} );
it( 'works correctly with empty source', () => {
setModelData( editor.model,
'<mermaid source="" displayMode="preview"></mermaid>'
);
expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal(
'<div class="ck-mermaid__preview-mode ck-mermaid__wrapper ck-widget ck-widget_selected ' +
'ck-widget_with-selection-handle" contenteditable="false">' +
'<div class="ck ck-widget__selection-handle"></div>' +
'<textarea class="ck-mermaid__editing-view" data-cke-ignore-events="true"' +
' placeholder="Insert mermaid source code"></textarea>' +
'<div class="ck-mermaid__preview"></div>' +
'<div class="ck ck-reset_all ck-widget__type-around"></div>' +
'</div>'
);
} );
describe( 'textarea value', () => {
let domTextarea = null;
beforeEach( () => {
// Using editor.setData() instead of setModelData helper because of #11365.
editor.setData(
'<pre spellcheck="false">' +
'<code class="language-mermaid">flowchart TB\nA --> B\nB --> C</code>' +
'</pre>'
);
const textareaView = editor.editing.view.document.getRoot().getChild( 0 ).getChild( 1 );
domTextarea = editor.editing.view.domConverter.viewToDom( textareaView );
} );
it( 'is properly set during the initial conversion', () => {
expect( domTextarea.value ).to.equal( 'flowchart TB\nA --> B\nB --> C' );
} );
it( 'is properly updated after model\'s attribute change', () => {
const { model } = editor;
const mermaidModel = model.document.getRoot().getChild( 0 );
model.change( writer => {
writer.setAttribute( 'source', 'abc', mermaidModel );
} );
expect( domTextarea.value ).to.equal( 'abc' );
} );
it( 'doesn\'t loop if model attribute changes to the same value', () => {
const { model } = editor;
const mermaidModel = model.document.getRoot().getChild( 0 );
model.change( writer => {
writer.setAttribute( 'source', 'flowchart TB\nA --> B\nB --> C', mermaidModel );
} );
expect( domTextarea.value ).to.equal( 'flowchart TB\nA --> B\nB --> C' );
} );
} );
describe( 'preview div', () => {
let domPreviewContainer, renderMermaidStub;
beforeEach( () => {
// Using editor.setData() instead of setModelData helper because of #11365.
editor.setData(
'<pre spellcheck="false">' +
'<code class="language-mermaid">flowchart TB\nA --> B\nB --> C</code>' +
'</pre>'
);
const previewContainerView = editor.editing.view.document.getRoot().getChild( 0 ).getChild( 2 );
domPreviewContainer = editor.editing.view.domConverter.viewToDom( previewContainerView );
renderMermaidStub = sinon.stub( editor.plugins.get( 'MermaidEditing' ), '_renderMermaid' );
} );
afterEach( () => {
renderMermaidStub.restore();
} );
it( 'has proper inner text set during the initial conversion', () => {
expect( domPreviewContainer.textContent ).to.equal( 'flowchart TB\nA --> B\nB --> C' );
} );
it( 'has proper inner text set after a model\'s attribute change', () => {
const { model } = editor;
const mermaidModel = model.document.getRoot().getChild( 0 );
model.change( writer => {
writer.setAttribute( 'source', 'abc', mermaidModel );
} );
expect( domPreviewContainer.textContent ).to.equal( 'abc' );
} );
it( 'calls mermaid render function after a model\'s attribute change', () => {
const { model } = editor;
const mermaidModel = model.document.getRoot().getChild( 0 );
model.change( writer => {
writer.setAttribute( 'source', 'abc', mermaidModel );
} );
expect( renderMermaidStub.callCount ).to.equal( 1 );
sinon.assert.calledWithExactly( renderMermaidStub, domPreviewContainer );
} );
} );
} );
it( 'adds a editing pipeline converter that has a precedence over code block', () => {
setModelData( editor.model, '<mermaid source="foo"></mermaid>' );
const firstViewChild = editor.editing.view.document.getRoot().getChild( 0 );
expect( firstViewChild.name ).to.equal( 'div' );
expect( firstViewChild.hasClass( 'ck-mermaid__wrapper' ), 'has ck-mermaid__wrapper class' ).to.be.true;
} );
it( 'does not convert code blocks other than mermaid language', () => {
setModelData( editor.model, '<codeBlock language="javascript">foo</codeBlock>' );
const firstViewChild = editor.editing.view.document.getRoot().getChild( 0 );
expect( firstViewChild.name ).not.to.equal( 'div' );
expect( firstViewChild.hasClass( 'ck-mermaid__wrapper' ), 'has ck-mermaid__wrapper class' ).to.be.false;
} );
it( 'adds a preview element', () => {
setModelData( editor.model, '<mermaid source="foo"></mermaid>' );
const widgetChildren = [ ...editor.editing.view.document.getRoot().getChild( 0 ).getChildren() ];
const previewView = widgetChildren.filter( item => item.name === 'div' && item.hasClass( 'ck-mermaid__preview' ) );
expect( previewView.length ).to.equal( 1 );
} );
it( 'adds an editing element', () => {
setModelData( editor.model, '<mermaid source="foo"></mermaid>' );
const widgetChildren = [ ...editor.editing.view.document.getRoot().getChild( 0 ).getChildren() ];
const previewView = widgetChildren.filter(
item => item.name === 'textarea' && item.hasClass( 'ck-mermaid__editing-view' )
);
expect( previewView.length ).to.equal( 1 );
} );
} );
} );
} );

View File

@@ -0,0 +1,154 @@
import { ClassicEditor as ClassicTestEditor } from '@ckeditor/ckeditor5-editor-classic';
import { Essentials } from '@ckeditor/ckeditor5-essentials';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { WidgetToolbarRepository } from '@ckeditor/ckeditor5-widget';
import { _setModelData as setData } from '@ckeditor/ckeditor5-engine';
import Mermaid from '../src/mermaid.js';
/* global document */
describe( 'MermaidToolbar', () => {
let editor, domElement, widgetToolbarRepository, balloon, toolbar, model;
beforeEach( () => {
domElement = document.createElement( 'div' );
document.body.appendChild( domElement );
return ClassicTestEditor.create( domElement, {
plugins: [ Essentials, Paragraph, Mermaid ],
mermaid: {
toolbar: [ 'fake_button' ]
}
} ).then( newEditor => {
editor = newEditor;
model = newEditor.model;
widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
toolbar = widgetToolbarRepository._toolbarDefinitions.get( 'mermaidToolbar' ).view;
balloon = editor.plugins.get( 'ContextualBalloon' );
} );
} );
afterEach( () => {
domElement.remove();
return editor.destroy();
} );
describe( 'toolbar', () => {
it( 'should be initialized with expected buttons', () => {
editor.ui.focusTracker.isFocused = true;
setData( model, '<mermaid displayMode="split" source="">[]</mermaid>' );
expect( toolbar.items ).to.have.length( 5 );
expect( toolbar.items.get( 0 ).label ).to.equal( 'Source view' );
expect( toolbar.items.get( 1 ).label ).to.equal( 'Split view' );
expect( toolbar.items.get( 2 ).label ).to.equal( 'Preview' );
expect( toolbar.items.get( 4 ).label ).to.equal( 'Read more about Mermaid diagram syntax' );
} );
} );
describe( 'integration with the editor focus', () => {
it( 'should show the toolbar when the editor gains focus and the mermaid widget is selected', () => {
setData( model,
'[<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>]'
);
expect( balloon.visibleView ).to.be.null;
// @todo: remove me
// expect( balloon.visibleView === null, 'balloon.visibleView === null' ).to.be.true;
editor.ui.focusTracker.isFocused = true;
expect( balloon.visibleView ).to.equal( toolbar );
// @todo: remove me
// expect( balloon.visibleView === toolbar, 'balloon.visibleView === toolbar' ).to.be.true;
} );
it( 'should hide the toolbar when the editor loses focus and the mermaid widget is selected', () => {
setData( model,
'[<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>]'
);
editor.ui.focusTracker.isFocused = true;
expect( balloon.visibleView ).to.equal( toolbar );
editor.ui.focusTracker.isFocused = false;
expect( balloon.visibleView ).to.be.null;
} );
} );
describe( 'integration with the editor selection', () => {
beforeEach( () => {
editor.ui.focusTracker.isFocused = true;
} );
it( 'should show the toolbar on ui#update when the mermaid widget is selected', () => {
setData( model,
'<paragraph>[foo]</paragraph>' +
'<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>'
);
expect( balloon.visibleView ).to.be.null;
editor.ui.fire( 'update' );
expect( balloon.visibleView ).to.be.null;
model.change( writer => {
// Set selection to the [<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>]
writer.setSelection( model.document.getRoot().getChild( 1 ), 'on' );
} );
expect( balloon.visibleView ).to.equal( toolbar );
// Make sure successive change does not throw, e.g. attempting
// to insert the toolbar twice.
editor.ui.fire( 'update' );
expect( balloon.visibleView ).to.equal( toolbar );
} );
it( 'should hide the toolbar on ui#update if the mermaid widget is deselected', () => {
setData( model,
'<paragraph>foo</paragraph>' +
'[<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>]'
);
expect( balloon.visibleView ).to.equal( toolbar );
model.change( writer => {
// Select the <paragraph>[foo]</paragraph>
writer.setSelection( model.document.getRoot().getChild( 0 ), 'in' );
} );
expect( balloon.visibleView ).to.be.null;
// Make sure successive change does not throw, e.g. attempting
// to remove the toolbar twice.
editor.ui.fire( 'update' );
expect( balloon.visibleView ).to.be.null;
} );
it( 'should not hide the toolbar on ui#update when the selection is being moved from one mermaid widget to another', () => {
setData( model,
'[<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>]' +
'<mermaid source="flowchart TB\nA --> B\nB --> C"></mermaid>'
);
expect( balloon.visibleView ).to.equal( toolbar );
model.change( writer => {
// Set selection to the second <mermaid></mermaid>
writer.setSelection( model.document.selection.getSelectedElement().nextSibling, 'on' );
} );
expect( balloon.visibleView ).to.equal( toolbar );
// Make sure successive change does not throw, e.g. attempting
// to insert the toolbar twice.
editor.ui.fire( 'update' );
expect( balloon.visibleView ).to.equal( toolbar );
} );
} );
} );

View File

@@ -0,0 +1,88 @@
import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
import { _setModelData as setModelData } from '@ckeditor/ckeditor5-engine';
import Mermaid from '../src/mermaid.js';
import MermaidUI from '../src/mermaidui.js';
/* global document */
describe( 'MermaidUI', () => {
it( 'should be named', () => {
expect( MermaidUI.pluginName ).to.equal( 'MermaidUI' );
} );
describe( 'init()', () => {
let domElement, editor;
beforeEach( async () => {
domElement = document.createElement( 'div' );
document.body.appendChild( domElement );
editor = await ClassicEditor.create( domElement, {
plugins: [
Mermaid
]
} );
} );
afterEach( () => {
domElement.remove();
return editor.destroy();
} );
it( 'should register the UI item', () => {
expect( editor.ui.componentFactory.has( 'mermaid' ) ).to.equal( true );
} );
it( 'has the base properties', () => {
const button = editor.ui.componentFactory.create( 'mermaid' );
expect( button ).to.have.property( 'label', 'Insert Mermaid diagram' );
expect( button ).to.have.property( 'icon' );
expect( button ).to.have.property( 'tooltip', true );
} );
describe( 'UI components', () => {
for ( const buttonName of [
'mermaidPreview',
'mermaidSourceView',
'mermaidSplitView',
'mermaidInfo'
] ) {
it( `should register the ${ buttonName } button`, () => {
expect( editor.ui.componentFactory.has( buttonName ) ).to.equal( true );
} );
it( `should add the base properties for ${ buttonName } button`, () => {
const button = editor.ui.componentFactory.create( buttonName );
expect( button ).to.have.property( 'label' );
expect( button ).to.have.property( 'icon' );
expect( button ).to.have.property( 'tooltip', true );
} );
}
} );
it( 'should set focus inside textarea of a newly created mermaid', () => {
const button = editor.ui.componentFactory.create( 'mermaid' );
button.fire( 'execute' );
expect( document.activeElement.tagName ).to.equal( 'TEXTAREA' );
} );
it( 'should not crash if the button is fired inside model.change()', () => {
const button = editor.ui.componentFactory.create( 'mermaid' );
setModelData( editor.model, '[]' );
editor.model.change( () => {
button.fire( 'execute' );
} );
// As the conversion is to be executed after the model.change(), we don't have access to the fully prepared view and
// despite that, we should still successfully add mermaid widget to the editor, not requiring the selection change
// to the inside of the nonexisting textarea element.
} );
} );
} );