diff --git a/README.md b/README.md index f39a9df3b..8158053b8 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,7 @@ Add it to builtinPlugins ```js InlineEditor.builtinPlugins = [ // ... - Mathematics, - // ... + Mathematics ]; ``` @@ -35,8 +34,7 @@ InlineEditor.defaultConfig = { toolbar: { items: [ // ... - 'math', - // ... + 'math' ] }, // ... @@ -44,14 +42,56 @@ InlineEditor.defaultConfig = { engine: 'mathjax', outputType: 'script', forceOutputType: false - }, - // ... + } }; ``` ### Copying plugin's theme for Lark Copy __theme/ckeditor5-math__ folder from [https://github.com/isaul32/ckeditor5-theme-lark](https://github.com/isaul32/ckeditor5-theme-lark) to your lark theme repository fork. -## Supported input and output formats +### Styles +Styles requires PostCSS like offical CKEditor plugins. + +## Configuration & Usage + +### Available typesetting engines +__MathJax__ +- Tested by using version __2.7.5__ and __TeX-MML-AM_HTMLorMML__ configuration. It works also with version __3.0.0__ or newer! +- Use __\\( \\)__ delimiters for inline and __\\[ \\]__ delimiters for display + +__KaTeX__ +- Tested by using version __0.11.0__ +- Doesn't support preview feature because __.ck-reset_all *__ css rules from ckeditor5-ui and ckeditor5-theme-lark breaks it. + +__Custom__ +It's possible to implement with engine config. For example, this feature can be used when use back end rendering. +```js +InlineEditor.defaultConfig = { + // ... + math: { + engine: (equation, element, display) => { + // ... + } + } +} +``` +- __equation__ is equation in TeX format without delimiters +- __element__ is DOM element reserved for rendering +- __display__ is boolean. Typesetting should be inline when false + + +### Plugin options +```js +InlineEditor.defaultConfig = { + // ... + math: { + engine: 'mathjax', // or katex or function (equation, element, display) => { ... } + outputType: 'script', // or span or math + forceOutputType: false // forces output to use outputType + } +} +``` + +### Supported input and output formats Supported input and output formats are: ```html @@ -63,38 +103,13 @@ Supported input and output formats are: \[ \sqrt{\frac{a}{b}} \] ``` -## Available typesetting engines -### MathJax -- Tested by using version __2.7.5__ and __TeX-MML-AM_HTMLorMML__ configuration. It works also with version __3.0.0__ or newer! -- Use __\\( \\)__ delimiters for inline and __\\[ \\]__ delimiters for display -### KaTeX -- Tested by using version __0.11.0__ -- Doesn't support preview feature because __.ck-reset_all *__ css rules from ckeditor5-ui and ckeditor5-theme-lark breaks it. -### Custom -It's possible to implement with _engine: (__equation__, __element__, __display__) => { ... }_ math config. -- __equation__ is equation in TeX format without delimiters -- __element__ is DOM element reserved for rendering -- __display__ is boolean. Typesetting should be inline when false - - -## Plugin options -```js -InlineEditor.defaultConfig = { - // ... - math: { - engine: 'mathjax', // or katex or function (equation, element, display) => { ... } - outputType: 'script', // or span or math - forceOutputType: false // forces output to use outputType - } - // ... -} -``` -## Styles -- Styles requires PostCSS like offical CKEditor plugins +### Math paste support +It's possible to paste equations in mathtex by using __delimiters__. For example: +- __\\(__ x=\frac{-b\pm\sqrt{b^2-4ac}}{2a} __\\)__ (inline mode) +- __\\[__ x=\frac{-b\pm\sqrt{b^2-4ac}}{2a} __\\]__ (display mode) ## Todo -- AutoMath like AutoMediaEmbed -- Convert equation to mathtex when paste from word +- Convert equations to mathtex when paste document from word - Fix KaTex preview - Make better way to import lark theme for plugin - MathML input and output when using MathJax version 3 diff --git a/src/automath.js b/src/automath.js new file mode 100644 index 000000000..5f8a7eeb7 --- /dev/null +++ b/src/automath.js @@ -0,0 +1,125 @@ +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; +import Undo from '@ckeditor/ckeditor5-undo/src/undo'; +import LiveRange from '@ckeditor/ckeditor5-engine/src/model/liverange'; +import LivePosition from '@ckeditor/ckeditor5-engine/src/model/liveposition'; + +import { defaultConfig, removeDelimiters, EQUATION_REGEXP } from './utils'; + +export default class AutoMath extends Plugin { + static get requires() { + return [ Clipboard, Undo ]; + } + + static get pluginName() { + return 'AutoMath'; + } + + constructor( editor ) { + super( editor ); + + this._timeoutId = null; + + this._positionToInsert = null; + } + + init() { + const editor = this.editor; + const modelDocument = editor.model.document; + + this.listenTo( editor.plugins.get( Clipboard ), 'inputTransformation', () => { + const firstRange = modelDocument.selection.getFirstRange(); + + const leftLivePosition = LivePosition.fromPosition( firstRange.start ); + leftLivePosition.stickiness = 'toPrevious'; + + const rightLivePosition = LivePosition.fromPosition( firstRange.end ); + rightLivePosition.stickiness = 'toNext'; + + modelDocument.once( 'change:data', () => { + this._mathBetweenPositions( leftLivePosition, rightLivePosition ); + + leftLivePosition.detach(); + rightLivePosition.detach(); + }, { priority: 'high' } ); + } ); + + editor.commands.get( 'undo' ).on( 'execute', () => { + if ( this._timeoutId ) { + global.window.clearTimeout( this._timeoutId ); // eslint-disable-line + this._positionToInsert.detach(); + + this._timeoutId = null; + this._positionToInsert = null; + } + }, { priority: 'high' } ); + } + + _mathBetweenPositions( leftPosition, rightPosition ) { + const editor = this.editor; + + const mathConfig = { + ...defaultConfig, + ...this.editor.config.get( 'math' ) + }; + + const equationRange = new LiveRange( leftPosition, rightPosition ); + const walker = equationRange.getWalker( { ignoreElementEnd: true } ); + + let equation = ''; + + // Get equation text + for ( const node of walker ) { + if ( node.item.is( 'textProxy' ) ) { + equation += node.item.data; + } + } + + equation = equation.trim(); + + // Check if equation + if ( !equation.match( EQUATION_REGEXP ) ) { + return; + } + + const mathCommand = editor.commands.get( 'math' ); + + // Do not anything if math element cannot be inserted at the current position + if ( !mathCommand.isEnabled ) { + return; + } + + this._positionToInsert = LivePosition.fromPosition( leftPosition ); + + // With timeout user can undo conversation if want use plain text + this._timeoutId = global.window.setTimeout( () => { // eslint-disable-line + editor.model.change( writer => { + this._timeoutId = null; + + writer.remove( equationRange ); + + let insertPosition; + + // Check if position where the math element should be inserted is still valid. + if ( this._positionToInsert.root.rootName !== '$graveyard' ) { + insertPosition = this._positionToInsert; + } + + editor.model.change( writer => { + const params = { + ...removeDelimiters( equation ), + type: mathConfig.outputType, + }; + const mathElement = writer.createElement( 'mathtex', params ); + + editor.model.insertContent( mathElement, insertPosition ); + + writer.setSelection( mathElement, 'on' ); + } ); + + this._positionToInsert.detach(); + this._positionToInsert = null; + } ); + }, 100 ); + } +} diff --git a/src/math.js b/src/math.js index 8c6ba6934..73f753082 100644 --- a/src/math.js +++ b/src/math.js @@ -3,10 +3,11 @@ import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import MathUI from './mathui'; import MathEditing from './mathediting'; +import AutoMath from './automath'; export default class Math extends Plugin { static get requires() { - return [ MathEditing, MathUI, Widget ]; + return [ MathEditing, MathUI, Widget, AutoMath ]; } static get pluginName() { diff --git a/src/mathediting.js b/src/mathediting.js index 34e26a12b..480e76717 100644 --- a/src/mathediting.js +++ b/src/mathediting.js @@ -5,7 +5,7 @@ import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import MathCommand from './mathcommand'; -import { defaultConfig, renderEquation } from './utils'; +import { defaultConfig, renderEquation, removeDelimiters } from './utils'; export default class MathEditing extends Plugin { static get requires() { @@ -105,20 +105,14 @@ export default class MathEditing extends Plugin { classes: [ 'math-tex' ] }, model: ( viewElement, modelWriter ) => { - let equation = viewElement.getChild( 0 ).data.trim(); + const equation = viewElement.getChild( 0 ).data.trim(); - // Remove delimiters (e.g. \( \) or \[ \]) - const hasInlineDelimiters = equation.includes( '\\(' ) && equation.includes( '\\)' ); - const hasDisplayDelimiters = equation.includes( '\\[' ) && equation.includes( '\\]' ); - if ( hasInlineDelimiters || hasDisplayDelimiters ) { - equation = equation.substring( 2, equation.length - 2 ).trim(); - } + const params = { + ...removeDelimiters( equation ), + type: mathConfig.forceOutputType ? mathConfig.outputType : 'span' + }; - return modelWriter.createElement( 'mathtex', { - equation, - type: mathConfig.forceOutputType ? mathConfig.outputType : 'span', - display: hasDisplayDelimiters - } ); + return modelWriter.createElement( 'mathtex', params ); } } ); diff --git a/src/mathui.js b/src/mathui.js index 76f2842e1..aa6ad87d5 100644 --- a/src/mathui.js +++ b/src/mathui.js @@ -67,7 +67,7 @@ export default class MathUI extends Plugin { const formView = new MainFormView( editor.locale, mathConfig.engine ); formView.mathInputView.bind( 'value' ).to( mathCommand, 'value' ); - formView.displayButtonView.bind( 'displayIsOn' ).to( mathCommand, 'display' ); + formView.displayButtonView.bind( 'isOn' ).to( mathCommand, 'display' ); // Listen to submit button click this.listenTo( formView, 'submit', () => { diff --git a/src/ui/mainformview.js b/src/ui/mainformview.js index 1d4a5f546..ac7f92322 100644 --- a/src/ui/mainformview.js +++ b/src/ui/mainformview.js @@ -15,6 +15,8 @@ import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg'; import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler'; +import { removeDelimiters, EQUATION_REGEXP } from '../utils'; + import MathView from './mathview'; import '../../theme/mathform.css'; @@ -53,6 +55,7 @@ export default class MainFormView extends View { // Math element this.mathView = new MathView( engine, locale ); + this.mathView.bind( 'display' ).to( this.displayButtonView, 'isOn' ); children = [ this.mathInputView, @@ -160,7 +163,25 @@ export default class MainFormView extends View { mathInput.infoText = t( 'Insert equation in TeX format.' ); inputView.on( 'input', () => { if ( this.previewEnabled ) { - this.mathView.value = inputView.element.value; + const equationInput = inputView.element.value.trim(); + + // If input has delimiters + if ( equationInput.match( EQUATION_REGEXP ) ) { + // Get equation without delimiters + const params = removeDelimiters( equationInput ); + + // Remove delimiters from input field + inputView.element.value = params.equation; + + // update display button and preview + this.displayButtonView.isOn = params.display; + if ( this.previewEnabled ) { + // Update preview view + this.mathView.value = params.equation; + } + } else { + this.mathView.value = equationInput; + } } } ); @@ -205,15 +226,13 @@ export default class MainFormView extends View { } } ); - switchButton.bind( 'isOn' ).to( this, 'displayIsOn' ); - switchButton.on( 'execute', () => { // Toggle state - this.set( 'displayIsOn', !this.displayIsOn ); + switchButton.isOn = !switchButton.isOn; if ( this.previewEnabled ) { // Update preview view - this.mathView.display = this.displayIsOn; + this.mathView.display = switchButton.isOn; } } ); diff --git a/src/utils.js b/src/utils.js index 54c6dcec9..fcc907571 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,15 @@ +export const EQUATION_REGEXP = /^\\(\[|\().*?\\(\]|\))$/; + +export const defaultConfig = { + engine: 'mathjax', + outputType: 'script', + forceOutputType: false +}; + export function renderEquation( equation, element, engine = 'katex', display = false ) { + if ( !element ) { + return; + } /* eslint-disable */ if ( engine === 'mathjax' && typeof MathJax !== 'undefined' ) { const version = MathJax.version; @@ -22,7 +33,10 @@ export function renderEquation( equation, element, engine = 'katex', display = f } else { element.innerHTML = '\\(' + equation + '\\)'; } - MathJax.Hub.Queue( [ 'Typeset', MathJax.Hub, element ] ); + // Fixme: MathJax render occasionally math processing error without timeout + setTimeout( () => { + MathJax.Hub.Queue( [ 'Typeset', MathJax.Hub, element ] ); + }, 100); } } else if ( engine === 'katex' && typeof katex !== 'undefined' ) { katex.render( equation, element, { @@ -38,6 +52,7 @@ export function renderEquation( equation, element, engine = 'katex', display = f /* eslint-enable */ } +// Simple MathJax 3 version check export function isMathJaxVersion3( version ) { return version && typeof version === 'string' && version.split( '.' ).length === 3 && version.split( '.' )[ 0 ] === '3'; } @@ -52,8 +67,19 @@ export function getSelectedMathModelWidget( selection ) { return null; } -export const defaultConfig = { - engine: 'mathjax', - outputType: 'script', - forceOutputType: false -}; +// Remove delimiters and figure display mode for the model +export function removeDelimiters( equation ) { + equation = equation.trim(); + + // Remove delimiters (e.g. \( \) or \[ \]) + const hasInlineDelimiters = equation.includes( '\\(' ) && equation.includes( '\\)' ); + const hasDisplayDelimiters = equation.includes( '\\[' ) && equation.includes( '\\]' ); + if ( hasInlineDelimiters || hasDisplayDelimiters ) { + equation = equation.substring( 2, equation.length - 2 ).trim(); + } + + return { + equation, + display: hasDisplayDelimiters + }; +}