diff --git a/plugin.js b/plugin.js index 07580a8..0f49182 100644 --- a/plugin.js +++ b/plugin.js @@ -7,424 +7,396 @@ * @fileOverview Rich code snippets for CKEditor. */ -'use strict'; - -( function() { - CKEDITOR.plugins.add( 'codesnippet', { - requires: 'widget,dialog', - // jscs:disable maximumLineLength - lang: 'ar,az,bg,ca,cs,da,de,de-ch,el,en,en-au,en-gb,eo,es,es-mx,et,eu,fa,fi,fr,fr-ca,gl,he,hr,hu,id,it,ja,km,ko,ku,lt,lv,nb,nl,no,oc,pl,pt,pt-br,ro,ru,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE% - // jscs:enable maximumLineLength - icons: 'codesnippet', // %REMOVE_LINE_CORE% - hidpi: true, // %REMOVE_LINE_CORE% - - isSupportedEnvironment: function() { - return !CKEDITOR.env.ie || CKEDITOR.env.version > 8; - }, - - beforeInit: function( editor ) { - editor._.codesnippet = {}; - - /** - * Sets the custom syntax highlighter. See {@link CKEDITOR.plugins.codesnippet.highlighter} - * to learn how to register a custom highlighter. - * - * **Note**: - * - * * This method can only be called while initialising plugins (in one of - * the three callbacks). - * * This method is accessible through the `editor.plugins.codesnippet` namespace only. - * - * @since 4.4.0 - * @member CKEDITOR.plugins.codesnippet - * @param {CKEDITOR.plugins.codesnippet.highlighter} highlighter - */ - this.setHighlighter = function( highlighter ) { - editor._.codesnippet.highlighter = highlighter; - - var langs = editor._.codesnippet.langs = - editor.config.codeSnippet_languages || highlighter.languages; - - // We might escape special regex chars below, but we expect that there - // should be no crazy values used as lang keys. - editor._.codesnippet.langsRegex = new RegExp( '(?:^|\\s)language-(' + - CKEDITOR.tools.object.keys( langs ).join( '|' ) + ')(?:\\s|$)' ); - }; - - editor.once( 'pluginsLoaded', function() { - // Remove the method once it cannot be used, because it leaks the editor reference (#589). - this.setHighlighter = null; - }, this ); - }, - - onLoad: function() { - CKEDITOR.dialog.add( 'codeSnippet', this.path + 'dialogs/codesnippet.js' ); - }, - - init: function( editor ) { - editor.ui.addButton && editor.ui.addButton( 'CodeSnippet', { - label: editor.lang.codesnippet.button, - command: 'codeSnippet', - toolbar: 'insert,10' - } ); - }, - - afterInit: function( editor ) { - var path = this.path; - - registerWidget( editor ); - - // At the very end, if no custom highlighter was set so far (by plugin#setHighlighter) - // we will set default one. - if ( !editor._.codesnippet.highlighter ) { - var hljsHighlighter = new CKEDITOR.plugins.codesnippet.highlighter( { - languages: { - apache: 'Apache', - bash: 'Bash', - coffeescript: 'CoffeeScript', - cpp: 'C++', - cs: 'C#', - css: 'CSS', - diff: 'Diff', - html: 'HTML', - http: 'HTTP', - ini: 'INI', - java: 'Java', - javascript: 'JavaScript', - json: 'JSON', - makefile: 'Makefile', - markdown: 'Markdown', - nginx: 'Nginx', - objectivec: 'Objective-C', - perl: 'Perl', - php: 'PHP', - python: 'Python', - ruby: 'Ruby', - sql: 'SQL', - vbscript: 'VBScript', - xhtml: 'XHTML', - xml: 'XML' - }, - - init: function( callback ) { - var that = this; - - if ( editor.plugins.codesnippet.isSupportedEnvironment() ) { - CKEDITOR.scriptLoader.load( path + 'lib/highlight/highlight.pack.js', function() { - that.hljs = window.hljs; - callback(); - } ); - } - - // Method is available only if wysiwygarea exists. - if ( editor.addContentsCss ) { - editor.addContentsCss( path + 'lib/highlight/styles/' + editor.config.codeSnippet_theme + '.css' ); - } - }, - - highlighter: function( code, language, callback ) { - var highlighted = this.hljs.highlightAuto( code, - this.hljs.getLanguage( language ) ? [ language ] : undefined ); - - if ( highlighted ) - callback( highlighted.value ); - } - } ); - - this.setHighlighter( hljsHighlighter ); - } - } - } ); - - /** - * Global helpers and classes of the Code Snippet plugin. - * - * For more information see the {@glink features/codesnippet Code Snippet Guide}. - * - * @class - * @singleton - */ - CKEDITOR.plugins.codesnippet = { - highlighter: Highlighter - }; - - /** - * A Code Snippet highlighter. It can be set as a default highlighter - * using {@link CKEDITOR.plugins.codesnippet#setHighlighter}, for example: - * - * // Create a new plugin which registers a custom code highlighter - * // based on customEngine in order to replace the one that comes - * // with the Code Snippet plugin. - * CKEDITOR.plugins.add( 'myCustomHighlighter', { - * afterInit: function( editor ) { - * // Create a new instance of the highlighter. - * var myHighlighter = new CKEDITOR.plugins.codesnippet.highlighter( { - * init: function( ready ) { - * // Asynchronous code to load resources and libraries for customEngine. - * customEngine.loadResources( function() { - * // Let the editor know that everything is ready. - * ready(); - * } ); - * }, - * highlighter: function( code, language, callback ) { - * // Let the customEngine highlight the code. - * customEngine.highlight( code, language, function() { - * callback( highlightedCode ); - * } ); - * } - * } ); - * - * // Check how it performs. - * myHighlighter.highlight( 'foo()', 'javascript', function( highlightedCode ) { - * console.log( highlightedCode ); // -> foo() - * } ); - * - * // From now on, myHighlighter will be used as a Code Snippet - * // highlighter, overwriting the default engine. - * editor.plugins.codesnippet.setHighlighter( myHighlighter ); - * } - * } ); - * - * @since 4.4.0 - * @class CKEDITOR.plugins.codesnippet.highlighter - * @extends CKEDITOR.plugins.codesnippet - * @param {Object} def Highlighter definition. See {@link #highlighter}, {@link #init} and {@link #languages}. - */ - function Highlighter( def ) { - CKEDITOR.tools.extend( this, def ); - - /** - * A queue of {@link #highlight} jobs to be - * done once the highlighter is {@link #ready}. - * - * @readonly - * @property {Array} [=[]] - */ - this.queue = []; - - // Async init – execute jobs when ready. - if ( this.init ) { - this.init( CKEDITOR.tools.bind( function() { - // Execute pending jobs. - var job; - - while ( ( job = this.queue.pop() ) ) - job.call( this ); - - this.ready = true; - }, this ) ); - } else { - this.ready = true; - } - - /** - * If specified, this function should asynchronously load highlighter-specific - * resources and execute `ready` when the highlighter is ready. - * - * @property {Function} [init] - * @param {Function} ready The function to be called once - * the highlighter is {@link #ready}. - */ - - /** - * A function which highlights given plain text `code` in a given `language` and, once done, - * calls the `callback` function with highlighted markup as an argument. - * - * @property {Function} [highlighter] - * @param {String} code Code to be formatted. - * @param {String} lang Language to be used ({@link CKEDITOR.config#codeSnippet_languages}). - * @param {Function} callback Function which accepts highlighted String as an argument. - */ - - /** - * Defines languages supported by the highlighter. - * They can be restricted with the {@link CKEDITOR.config#codeSnippet_languages} configuration option. - * - * **Note**: If {@link CKEDITOR.config#codeSnippet_languages} is set, **it will - * overwrite** the languages listed in `languages`. - * - * languages: { - * coffeescript: 'CoffeeScript', - * cpp: 'C++', - * cs: 'C#', - * css: 'CSS' - * } - * - * More information on how to change the list of languages is available - * in the {@glink features/codesnippet#changing-supported-languages Code Snippet documentation}. - * - * @property {Object} languages - */ - - /** - * A flag which indicates whether the highlighter is ready to do jobs - * from the {@link #queue}. - * - * @readonly - * @property {Boolean} ready - */ - } - - /** - * Executes the {@link #highlighter}. If the highlighter is not ready, it defers the job ({@link #queue}) - * and executes it when the highlighter is {@link #ready}. - * - * @param {String} code Code to be formatted. - * @param {String} lang Language to be used ({@link CKEDITOR.config#codeSnippet_languages}). - * @param {Function} callback Function which accepts highlighted String as an argument. - */ - Highlighter.prototype.highlight = function() { - var arg = arguments; - - // Highlighter is ready – do it now. - if ( this.ready ) - this.highlighter.apply( this, arg ); - // Queue the job. It will be done once ready. - else { - this.queue.push( function() { - this.highlighter.apply( this, arg ); - } ); - } - }; - - // Encapsulates snippet widget registration code. - // @param {CKEDITOR.editor} editor - function registerWidget( editor ) { - var codeClass = editor.config.codeSnippet_codeClass, - newLineRegex = /\r?\n/g, - textarea = new CKEDITOR.dom.element( 'textarea' ), - lang = editor.lang.codesnippet; - - editor.widgets.add( 'codeSnippet', { - allowedContent: 'pre; code(language-*)', - // Actually we need both - pre and code, but ACF does not make it possible - // to defire required content with "and" operator. - requiredContent: 'pre', - styleableElements: 'pre', - template: '
', - dialog: 'codeSnippet', - pathName: lang.pathName, - mask: true, - - parts: { - pre: 'pre', - code: 'code' - }, - - highlight: function() { - var that = this, - widgetData = this.data, - callback = function( formatted ) { - // IE8 (not supported browser) have issue with new line chars, when using innerHTML. - // It will simply strip it. - that.parts.code.setHtml( editor.plugins.codesnippet.isSupportedEnvironment() ? - formatted : formatted.replace( newLineRegex, '
' ) ); - }; - - // Set plain code first, so even if custom handler will not call it the code will be there. - callback( CKEDITOR.tools.htmlEncode( widgetData.code ) ); - - // Call higlighter to apply its custom highlighting. - editor._.codesnippet.highlighter.highlight( widgetData.code, widgetData.lang, function( formatted ) { - editor.fire( 'lockSnapshot' ); - callback( formatted ); - editor.fire( 'unlockSnapshot' ); - } ); - }, - - data: function() { - var newData = this.data, - oldData = this.oldData; - - if ( newData.code ) - this.parts.code.setHtml( CKEDITOR.tools.htmlEncode( newData.code ) ); - - // Remove old .language-* class. - if ( oldData && newData.lang != oldData.lang ) - this.parts.code.removeClass( 'language-' + oldData.lang ); - - // Lang needs to be specified in order to apply formatting. - if ( newData.lang ) { - // Apply new .language-* class. - this.parts.code.addClass( 'language-' + newData.lang ); - - this.highlight(); - } - - // Save oldData. - this.oldData = CKEDITOR.tools.copy( newData ); - }, - - // Upcasts
...
- upcast: function( el, data ) { - if ( el.name != 'pre' ) - return; - - var childrenArray = getNonEmptyChildren( el ), - code; - - if ( childrenArray.length != 1 || ( code = childrenArray[ 0 ] ).name != 'code' ) - return; - - // Upcast with text only: https://dev.ckeditor.com/ticket/11926#comment:4 - if ( code.children.length != 1 || code.children[ 0 ].type != CKEDITOR.NODE_TEXT ) - return; - - // Read language-* from class attribute. - var matchResult = editor._.codesnippet.langsRegex.exec( code.attributes[ 'class' ] ); - - if ( matchResult ) - data.lang = matchResult[ 1 ]; - - // Use textarea to decode HTML entities (https://dev.ckeditor.com/ticket/11926). - textarea.setHtml( code.getHtml() ); - data.code = textarea.getValue(); - - code.addClass( codeClass ); - - return el; - }, - - // Downcasts to
...
- downcast: function( el ) { - var code = el.getFirst( 'code' ); - - // Remove pretty formatting from .... - code.children.length = 0; - - // Remove config#codeSnippet_codeClass. - code.removeClass( codeClass ); - - // Set raw text inside .... - code.add( new CKEDITOR.htmlParser.text( CKEDITOR.tools.htmlEncode( this.data.code ) ) ); - - return el; - } - } ); - - // Returns an **array** of child elements, with whitespace-only text nodes - // filtered out. - // @param {CKEDITOR.htmlParser.element} parentElement - // @return Array - array of CKEDITOR.htmlParser.node - var whitespaceOnlyRegex = /^[\s\n\r]*$/; - - function getNonEmptyChildren( parentElement ) { - var ret = [], - preChildrenList = parentElement.children, - curNode; - - // Filter out empty text nodes. - for ( var i = preChildrenList.length - 1; i >= 0; i-- ) { - curNode = preChildrenList[ i ]; - - if ( curNode.type != CKEDITOR.NODE_TEXT || !curNode.value.match( whitespaceOnlyRegex ) ) - ret.push( curNode ); - } - - return ret; - } - } -} )(); +"use strict"; + +(function () { + CKEDITOR.plugins.add("codesnippet", { + requires: "widget,dialog", + // jscs:disable maximumLineLength + lang: "ar,az,bg,ca,cs,da,de,de-ch,el,en,en-au,en-gb,eo,es,es-mx,et,eu,fa,fi,fr,fr-ca,gl,he,hr,hu,id,it,ja,km,ko,ku,lt,lv,nb,nl,no,oc,pl,pt,pt-br,ro,ru,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn", // %REMOVE_LINE_CORE% + // jscs:enable maximumLineLength + icons: "codesnippet", // %REMOVE_LINE_CORE% + hidpi: true, // %REMOVE_LINE_CORE% + defaultLanguages: { + apache: "Apache", + bash: "Bash", + coffeescript: "CoffeeScript", + cpp: "C++", + cs: "C#", + css: "CSS", + diff: "Diff", + html: "HTML", + http: "HTTP", + ini: "INI", + java: "Java", + javascript: "JavaScript", + json: "JSON", + makefile: "Makefile", + markdown: "Markdown", + nginx: "Nginx", + objectivec: "Objective-C", + perl: "Perl", + php: "PHP", + python: "Python", + ruby: "Ruby", + sql: "SQL", + vbscript: "VBScript", + xhtml: "XHTML", + xml: "XML", + }, + highlighterType: 'highlight', // highlight or prism + configs: { + highlight: { + globalName: 'hljs', + jsPath: 'lib/highlight/highlight.pack.js', + cssPath: 'lib/highlight/styles/', + codeClass: 'hljs', + }, + prism: { + globalName: 'Prism', + codeClass: 'prism', + } + }, + isSupportedEnvironment() { + return !CKEDITOR.env.ie || CKEDITOR.env.version > 8; + }, + + beforeInit(editor) { + editor._.codesnippet = {}; + const configHighlighter = editor.config.codeSnippet_highlighter.toLowerCase(); + if (configHighlighter.indexOf('prism') > -1) this.highlighterType = 'prism'; + if (configHighlighter.indexOf('highlight') > -1) this.highlighterType = 'highlight'; + + /** + * Sets the custom syntax highlighter. See {@link CKEDITOR.plugins.codesnippet.highlighter} + * to learn how to register a custom highlighter. + * + * **Note**: + * + * * This method can only be called while initialising plugins (in one of + * the three callbacks). + * * This method is accessible through the `editor.plugins.codesnippet` namespace only. + * + * @since 4.4.0 + * @member CKEDITOR.plugins.codesnippet + * @param {CKEDITOR.plugins.codesnippet.highlighter} highlighter + */ + this.setHighlighter = function setHighlighter(highlighter) { + editor._.codesnippet.highlighter = highlighter; + + var langs = (editor._.codesnippet.langs = + editor.config.codeSnippet_languages || highlighter.languages); + + // We might escape special regex chars below, but we expect that there + // should be no crazy values used as lang keys. + // this should optionally support lang-, language-, and + editor._.codesnippet.langsRegex = new RegExp( + "(?:^|\\s)(lang(uage)?-)?(" + + CKEDITOR.tools.object.keys(langs).join("|") + + ")(?:\\s|$)" + ); + }; + + editor.once( + "pluginsLoaded", + function () { + // Remove the method once it cannot be used, because it leaks the editor reference (#589). + // note: determine if this gets added back in the afterInit + this.setHighlighter = null; + }, + this + ); + }, + + onLoad() { + CKEDITOR.dialog.add("codeSnippet", this.path + "dialogs/codesnippet.js"); + }, + + init(editor) { + if (editor.ui.addButton) { + editor.ui.addButton("CodeSnippet", { + label: editor.lang.codesnippet.button, + command: "codeSnippet", + toolbar: "insert,10", + }); + } + }, + /** + * @typedef CodeHighlighter + * @property {Object} languages A map of language names and their aliases. + * @property {Function} init A function that initializes the highlighter. + * @property {Function} loadJs A function that loads the highlighter's JS file. + * @property {Function} loadCss A function that loads the highlighter's CSS file. + * @property {Function} highlight A function that highlights the code snippet. + */ + /** + * + * @param {string} highlighterType [prism, higlight] + * @param {Object} editor The editor instance + * @returns {CodeHighlighter} + */ + getHighlighter(highlighterType, editor) { + const { path, defaultLanguages } = this; + let jsPath; + let cssPath; + let globalName = this.configs[highlighterType].globalName; + // note: consider moving this outside this function. Right now it's here just b/c... + // ...it's useful to keep it in the same scope as editor. But that isn't really necessary. + // TODO: Make this a class? + function CodeHighlighter(editor, nameOnWindow, pluginPath, cssPath) { + return { + languages: defaultLanguages, + init(callback) { + if (editor.plugins.codesnippet.isSupportedEnvironment()) { + this.loadJs(pluginPath, nameOnWindow, callback); + this.loadCss(cssPath); + } + }, + loadJs(pluginPath, globalname, callback) { + try { + if (pluginPath) { + CKEDITOR.scriptLoader.load(pluginPath, () => { + this[nameOnWindow] = window[nameOnWindow]; + callback(); + }); + } else { + this[nameOnWindow] = window[nameOnWindow]; + callback(); + } + } catch (loadJsError) { + console.error(loadJsError); + } + }, + loadCss(cssPath) { + if (!editor.addContentsCss || !cssPath) return; + editor.addContentsCss(cssPath); + }, + highlighter(code, language, callback) { + const highlighted = this[nameOnWindow].highlightAuto( + code, + this[nameOnWindow].getLanguage(language) ? [language] : undefined + ); + + if (highlighted) callback(highlighted.value); + }, + }; + } + + if (highlighterType === "highlight") { + // highlight depends on the js that comes with the library, and needs CSS added in + jsPath = path + this.configs.highlight.jsPath; + cssPath = + path + + this.configs.highlight.cssPath + + editor.config.codeSnippet_theme + + ".css"; + } + + const codeHighlighter = new CodeHighlighter( + editor, + globalName, + jsPath, + cssPath + ); + + if (highlighterType === "prism") { + // Prism's highlight function takes different arguments that highlight's. It needs to be sent the grammar and language name + // Would be more useful to make the highlighter function more globally useful, but this is a quick fix + codeHighlighter.highlighter = function highlighter( + code, + language, + callback + ) { + const highlighted = this[globalName].highlight( + code, + this[globalName].languages[language], + language + ); + + if (highlighted) callback(highlighted); + }; + } + + return codeHighlighter; + }, + afterInit(editor) { + const {highlighterType} = this; + registerWidget(editor); + // At the very end, if no custom highlighter was set so far (by plugin#setHighlighter) + // we will set default one. + // we get a highlighter just to keep functions a bit smaller. + if (!editor._.codesnippet.highlighter) { + const codesnippetHighlighter = new CKEDITOR.plugins.codesnippet.highlighter( + this.getHighlighter(highlighterType, editor) + ); + this.setHighlighter(codesnippetHighlighter); + } + }, + }); + + /** + * Global helpers and classes of the Code Snippet plugin. + * + * For more information see the {@glink features/codesnippet Code Snippet Guide}. + * + * @class + * @singleton + */ + CKEDITOR.plugins.codesnippet = { + highlighter: Highlighter, + }; + + /** + * A Code Snippet highlighter. It can be set as a default highlighter + * using {@link CKEDITOR.plugins.codesnippet#setHighlighter}, for example: + * + * // Create a new plugin which registers a custom code highlighter + * // based on customEngine in order to replace the one that comes + * // with the Code Snippet plugin. + * CKEDITOR.plugins.add( 'myCustomHighlighter', { + * afterInit: function( editor ) { + * // Create a new instance of the highlighter. + * var myHighlighter = new CKEDITOR.plugins.codesnippet.highlighter( { + * init: function( ready ) { + * // Asynchronous code to load resources and libraries for customEngine. + * customEngine.loadResources( function() { + * // Let the editor know that everything is ready. + * ready(); + * } ); + * }, + * highlighter: function( code, language, callback ) { + * // Let the customEngine highlight the code. + * customEngine.highlight( code, language, function() { + * callback( highlightedCode ); + * } ); + * } + * } ); + * + * // Check how it performs. + * myHighlighter.highlight( 'foo()', 'javascript', function( highlightedCode ) { + * console.log( highlightedCode ); // -> foo() + * } ); + * + * // From now on, myHighlighter will be used as a Code Snippet + * // highlighter, overwriting the default engine. + * editor.plugins.codesnippet.setHighlighter( myHighlighter ); + * } + * } ); + * + * @since 4.4.0 + * @class CKEDITOR.plugins.codesnippet.highlighter + * @extends CKEDITOR.plugins.codesnippet + * @param {Object} def Highlighter definition. See {@link #highlighter}, {@link #init} and {@link #languages}. + */ + function Highlighter(def) { + CKEDITOR.tools.extend(this, def); + + /** + * A queue of {@link #highlight} jobs to be + * done once the highlighter is {@link #ready}. + * + * @readonly + * @property {Array} [=[]] + */ + this.queue = []; + + // Async init – execute jobs when ready. + if (this.init) { + this.init( + CKEDITOR.tools.bind(function () { + // Execute pending jobs. + var job; + + while ((job = this.queue.pop())) job.call(this); + + this.ready = true; + }, this) + ); + } else { + this.ready = true; + } + + /** + * If specified, this function should asynchronously load highlighter-specific + * resources and execute `ready` when the highlighter is ready. + * + * @property {Function} [init] + * @param {Function} ready The function to be called once + * the highlighter is {@link #ready}. + */ + + /** + * A function which highlights given plain text `code` in a given `language` and, once done, + * calls the `callback` function with highlighted markup as an argument. + * + * @property {Function} [highlighter] + * @param {String} code Code to be formatted. + * @param {String} lang Language to be used ({@link CKEDITOR.config#codeSnippet_languages}). + * @param {Function} callback Function which accepts highlighted String as an argument. + */ + + /** + * Defines languages supported by the highlighter. + * They can be restricted with the {@link CKEDITOR.config#codeSnippet_languages} configuration option. + * + * **Note**: If {@link CKEDITOR.config#codeSnippet_languages} is set, **it will + * overwrite** the languages listed in `languages`. + * + * languages: { + * coffeescript: 'CoffeeScript', + * cpp: 'C++', + * cs: 'C#', + * css: 'CSS' + * } + * + * More information on how to change the list of languages is available + * in the {@glink features/codesnippet#changing-supported-languages Code Snippet documentation}. + * + * @property {Object} languages + */ + + /** + * A flag which indicates whether the highlighter is ready to do jobs + * from the {@link #queue}. + * + * @readonly + * @property {Boolean} ready + */ + } + + /** + * Executes the {@link #highlighter}. If the highlighter is not ready, it defers the job ({@link #queue}) + * and executes it when the highlighter is {@link #ready}. + * + * @param {String} code Code to be formatted. + * @param {String} lang Language to be used ({@link CKEDITOR.config#codeSnippet_languages}). + * @param {Function} callback Function which accepts highlighted String as an argument. + */ + Highlighter.prototype.highlight = function () { + var arg = arguments; + + // Highlighter is ready – do it now. + if (this.ready) this.highlighter.apply(this, arg); + // Queue the job. It will be done once ready. + else { + this.queue.push(function () { + this.highlighter.apply(this, arg); + }); + } + }; + /** + * Encapsulates snippet widget registration code. + * @param {CKEDITOR.editor} editor + */ + function registerWidget(editor) { + var textarea = new CKEDITOR.dom.element("textarea"); + const codeSnippetWidget = editor.config.getCodeSnippetWidget( + editor, + textarea + ); + editor.widgets.add("codeSnippet", codeSnippetWidget); + } +})(); /** * A CSS class of the `` element used internally for styling @@ -432,8 +404,8 @@ * {@link CKEDITOR.config#codeSnippet_theme config.codeSnippet_theme}), * which means that it is **not present** in the editor output data. * - * // Changes the class to "myCustomClass". - * config.codeSnippet_codeClass = 'myCustomClass'; + * // Changes the class to "myCustomClass". + * config.codeSnippet_codeClass = 'myCustomClass'; * * **Note**: The class might need to be changed when you are using a custom * highlighter (the default is [highlight.js](https://highlightjs.org)). @@ -442,12 +414,195 @@ * Read more in the {@glink features/codesnippet documentation} * and see the {@glink examples/codesnippet example}. * - * @since 4.4.0 + * @since 4.4.1 * @cfg {String} [codeSnippet_codeClass='hljs'] * @member CKEDITOR.config */ CKEDITOR.config.codeSnippet_codeClass = 'hljs'; +/* NOTES +* 1. The codeSnippet_codeClass should be variably set based on CKEDITOR.config.codeSnippet_highlighter +* But this didn't work. So don't rely on it. logically set it in the codeSnippetWidget function +* 2. The getCodeSnippetWidget function has an object mapping highlighterType to codeClass. +* I tried to set that on CKEDITOR.config, but that didn't work (never saw the property display). + But that object should be moved _somewhere_ more visible. +* 3. The object that codeSnippetWidget returns should really be a class. +*/ + +/** + * This gets the widget definition for the code snippet widget + * @param {Object} editor the ckeditor instance + * @param {Node} textarea ckeditor.dom element + * @returns {Object} a complex widget definition that includes highlight functionality + */ +CKEDITOR.config.getCodeSnippetWidget = function getCodeSnippetWidget( + editor, + textarea +) { + const { lang, config } = editor; + const codesnippet = lang.codesnippet; + const codeClasses = { // @TODO: find out why this can't be set on CKEDITOR.config + 'hljs': 'hljs', + 'highlightjs': 'hljs', + 'prism': 'prism', + 'prismjs': 'prism', + }; + const highlighterType = config.codeSnippet_highlighter; + const codeClass = codeClasses[highlighterType]; + const whitespaceOnlyRegex = /^[\s\n\r]*$/; + const newLineRegex = /\r?\n/g; + /** + * Returns an **array** of child elements, with whitespace-only text nodes + * @TODO consider moving this out of here? maybe to a utils or something? + * @param {CKEDITOR.htmlParser.element} parentElement + * @returns {Array} array of CKEDITOR.htmlParser.node + */ + + function getNonEmptyChildren(parentElement) { + var ret = [], + preChildrenList = parentElement.children, + curNode; + + // Filter out empty text nodes. + for (var i = preChildrenList.length - 1; i >= 0; i--) { + curNode = preChildrenList[i]; + + if ( + curNode.type != CKEDITOR.NODE_TEXT || + !curNode.value.match(whitespaceOnlyRegex) + ) + ret.push(curNode); + } + + return ret; + } + // @TODO make this a class? + return { + allowedContent: "pre; code(language-*)", + // Actually we need both - pre and code, but ACF does not make it possible + // to defire required content with "and" operator. + requiredContent: "pre", + styleableElements: "pre", + template: `
`, + dialog: "codeSnippet", + pathName: codesnippet.pathName, + mask: true, + parts: { + pre: "pre", + code: "code", + }, + highlight() { + const that = this; + let widgetData = this.data; + const callback = function (formatted) { + // @TODO: do we still care about IE8? + // IE8 (not supported browser) have issue with new line chars, when using innerHTML. + // It will simply strip it. + that.parts.code.setHtml( + editor.plugins.codesnippet.isSupportedEnvironment() + ? formatted + : formatted.replace(newLineRegex, "
") + ); + }; + + // Set plain code first, so even if custom handler will not call it the code will be there. + callback(CKEDITOR.tools.htmlEncode(widgetData.code)); + + // Call higlighter to apply its custom highlighting. + // wrap in a try-catch because if we don't, and it fails, then the editor stops working, too. + try { + editor._.codesnippet.highlighter.highlight( + widgetData.code, + widgetData.lang, + function (formatted) { + editor.fire("lockSnapshot"); + callback(formatted); + editor.fire("unlockSnapshot"); + } + ); + } catch (highlightErr) { + console.error(highlightErr, widgetData); + } + }, + data() { + const newData = this.data; + const { oldData } = this; + + if (newData.code) + this.parts.code.setHtml(CKEDITOR.tools.htmlEncode(newData.code)); + + // @TODO: what about lang- classes or instances where just the language is the class? find a solution for those. + // Remove old .language-* class. + if (oldData && newData.lang != oldData.lang) + this.parts.code.removeClass("language-" + oldData.lang); + + // Lang needs to be specified in order to apply formatting. + if (newData.lang) { + // Apply new .language-* class. + this.parts.code.addClass("language-" + newData.lang); + + this.highlight(); + } + + // Save oldData. + this.oldData = CKEDITOR.tools.copy(newData); + }, + // Upcasts
...
+ upcast(el, data) { + if (el.name != "pre") return; + + const childrenArray = getNonEmptyChildren(el); + let code; + + if ( + childrenArray.length != 1 || + (code = childrenArray[0]).name != "code" + ) { + return; + } + + // Upcast with text only: https://dev.ckeditor.com/ticket/11926#comment:4 + if ( + code.children.length != 1 || + code.children[0].type != CKEDITOR.NODE_TEXT + ) { + return; + } + + // Read language-* from class attribute. + var matchResult = editor._.codesnippet.langsRegex.exec( + code.attributes["class"] + ); + + // because the regex can match lang- or language-, or , we get multiple results. so get the last one. + if (matchResult) data.lang = matchResult[matchResult.length - 1]; + // Use textarea to decode HTML entities (https://dev.ckeditor.com/ticket/11926). + textarea.setHtml(code.getHtml()); + data.code = textarea.getValue(); + + code.addClass(codeClass); + + return el; + }, + // Downcasts to
...
+ downcast(el) { + var code = el.getFirst("code"); + + // Remove pretty formatting from .... + code.children.length = 0; + + // Remove config#codeSnippet_codeClass. + code.removeClass(codeClass); + + // Set raw text inside .... + code.add( + new CKEDITOR.htmlParser.text(CKEDITOR.tools.htmlEncode(this.data.code)) + ); + + return el; + }, + }; +}; /** * Restricts languages available in the "Code Snippet" dialog window. * An empty value is always added to the list. @@ -458,11 +613,11 @@ CKEDITOR.config.codeSnippet_codeClass = 'hljs'; * Read more in the {@glink features/codesnippet#changing-supported-languages documentation} * and see the {@glink examples/codesnippet example}. * - * // Restricts languages to JavaScript and PHP. - * config.codeSnippet_languages = { - * javascript: 'JavaScript', - * php: 'PHP' - * }; + * // Restricts languages to JavaScript and PHP. + * config.codeSnippet_languages = { + * javascript: 'JavaScript', + * php: 'PHP' + * }; * * @since 4.4.0 * @cfg {Object} [codeSnippet_languages=null] @@ -478,11 +633,11 @@ CKEDITOR.config.codeSnippet_codeClass = 'hljs'; * Read more in the {@glink features/codesnippet#changing-highlighter-theme documentation} * and see the {@glink examples/codesnippet example}. * - * // Changes the theme to "pojoaque". - * config.codeSnippet_theme = 'pojoaque'; + * // Changes the theme to "pojoaque". + * config.codeSnippet_theme = 'pojoaque'; * * @since 4.4.0 * @cfg {String} [codeSnippet_theme='default'] * @member CKEDITOR.config */ -CKEDITOR.config.codeSnippet_theme = 'default'; +CKEDITOR.config.codeSnippet_theme = "default";