Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #1 from ckeditor/t/ckeditor5/436
Browse files Browse the repository at this point in the history
Feature: Initial implementation of the code block feature. Closes ckeditor/ckeditor5#436. Closes ckeditor/ckeditor5#5664. Closes ckeditor/ckeditor5#5666.
  • Loading branch information
jodator authored Nov 20, 2019
2 parents 69ec195 + 4f3b521 commit eb9ef5e
Show file tree
Hide file tree
Showing 25 changed files with 3,950 additions and 3 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
CKEditor 5 code block feature
========================================

This is an initial package for development purposes. It does not contain code yet.
[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-code-block.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-code-block)
[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-code-block.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-code-block)
[![Build Status](https://travis-ci.org/ckeditor/ckeditor5-code-block.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-code-block)
[![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-code-block/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-code-block?branch=master)
<br>
[![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-code-block/status.svg)](https://david-dm.org/ckeditor/ckeditor5-code-block)
[![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-code-block/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-code-block?type=dev)

This package implements the code block feature for CKEditor 5.

## Documentation

See the [`@ckeditor/ckeditor5-code-block` package](https://ckeditor.com/docs/ckeditor5/latest/api/code-block.html) page as well as the [Code block feature guide](https://ckeditor.com/docs/ckeditor5/latest/features/code-block.html) in [CKEditor 5 documentation](https://ckeditor.com/docs/ckeditor5/latest/).

## License

Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file or [https://ckeditor.com/legal/ckeditor-oss-license](https://ckeditor.com/legal/ckeditor-oss-license).
4 changes: 4 additions & 0 deletions lang/contexts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"Insert code block": "A label of the button that allows inserting a new code block into the editor content.",
"Plain text": "A language of the code block in the editor content when no specific programming language is associated with it."
}
15 changes: 13 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,22 @@
"ckeditor5-plugin"
],
"dependencies": {
"@ckeditor/ckeditor5-core": "^12.3.0",
"@ckeditor/ckeditor5-utils": "^14.0.0",
"@ckeditor/ckeditor5-core": "^15.0.0",
"@ckeditor/ckeditor5-enter": "^15.0.0",
"@ckeditor/ckeditor5-ui": "^15.0.0",
"@ckeditor/ckeditor5-utils": "^15.0.0",
"lodash-es": "^4.17.10"
},
"devDependencies": {
"@ckeditor/ckeditor5-alignment": "^15.0.0",
"@ckeditor/ckeditor5-autoformat": "^15.0.0",
"@ckeditor/ckeditor5-basic-styles": "^15.0.0",
"@ckeditor/ckeditor5-block-quote": "^15.0.0",
"@ckeditor/ckeditor5-editor-classic": "^15.0.0",
"@ckeditor/ckeditor5-engine": "^15.0.0",
"@ckeditor/ckeditor5-indent": "^15.0.0",
"@ckeditor/ckeditor5-paragraph": "^15.0.0",
"@ckeditor/ckeditor5-undo": "^15.0.0",
"eslint": "^5.5.0",
"eslint-config-ckeditor5": "^2.0.0",
"husky": "^1.3.1",
Expand Down
166 changes: 166 additions & 0 deletions src/codeblock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module code-block/codeblock
*/

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import CodeBlockEditing from './codeblockediting';
import CodeBlockUI from './codeblockui';

/**
* The code block plugin.
*
* For more information about this feature check the package page.
*
* This is a "glue" plugin which loads the {@link module:code-block/codeblockediting~CodeBlockEditing code block editing feature}
* and {@link module:code-block/codeblockui~CodeBlockUI code block UI feature}.
*
* @extends module:core/plugin~Plugin
*/
export default class CodeBlock extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [ CodeBlockEditing, CodeBlockUI ];
}

/**
* @inheritDoc
*/
static get pluginName() {
return 'CodeBlock';
}
}

/**
* The configuration of the {@link module:code-block/codeblock~CodeBlock} feature.
*
* Read more in {@link module:code-block/codeblock~CodeBlockConfig}.
*
* @member {module:code-block/codeblock~CodeBlockConfig} module:core/editor/editorconfig~EditorConfig#codeBlock
*/

/**
* The configuration of the {@link module:code-block/codeblock~CodeBlock code block feature}.
*
* ClassicEditor
* .create( editorElement, {
* codeBlock: ... // Code block feature configuration.
* } )
* .then( ... )
* .catch( ... );
*
* See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
*
* @interface CodeBlockConfig
*/

/**
* The code block language descriptor. See {@link module:code-block/codeblock~CodeBlockConfig#languages} to learn more.
*
* {
* language: 'javascript',
* label: 'JavaScript'
* }
*
* @typedef {Object} module:code-block/codeblock~CodeBlockLanguageDefinition
* @property {String} language The name of the language that will be stored in the model attribute. Also, when `class`
* is not specified, it will also be used to create the CSS class associated with the language (prefixed by "language-").
* @property {String} label The human–readable label associated with the language and displayed in the UI.
* @property {String} [class] The CSS class associated with the language. When not specified the `language`
* property is used to create a class prefixed by "language-".
*/

/**
* The list of code languages available in the user interface to choose for a particular code block.
*
* The language of the code block is represented as a CSS class (by default prefixed by "language-") set on the
* `<code>` element, both when editing and in the editor data. The CSS class associated with the language
* can be used by third–party code syntax highlighters to detect and apply the correct highlighting.
*
* For instance, this language configuration:
*
* ClassicEditor
* .create( editorElement, {
* codeBlock: {
* languages: [
* // ...
* { language: 'javascript', label: 'JavaScript' },
* // ...
* ]
* }
* } )
* .then( ... )
* .catch( ... );
*
* will result in the following structure of JavaScript code blocks in the editor editing and data:
*
* <pre><code class="language-javascript">window.alert( 'Hello world!' )</code></pre>
*
* You can customize the CSS class by specifying an optional `class` property in a language definition:
*
* ClassicEditor
* .create( editorElement, {
* codeBlock: {
* languages: [
* // Do not render CSS class for the plain text code blocks.
* { language: 'plaintext', label: 'Plain text', class: '' },
*
* // Use the "php-code" class for PHP code blocks.
* { language: 'php', label: 'PHP', class: 'php-code' },
*
* // Use the "js" class for JavaScript code blocks.
* { language: 'javascript', label: 'JavaScript', class: 'js' },
*
* // Python code blocks will have the default "language-python" CSS class.
* { language: 'python', label: 'Python' }
* ]
* }
* } )
* .then( ... )
* .catch( ... );
*
* The default value of the language configuration is as follows:
*
* languages: [
* { language: 'plaintext', label: 'Plain text' }, // The default language.
* { language: 'c', label: 'C' },
* { language: 'cs', label: 'C#' },
* { language: 'cpp', label: 'C++' },
* { language: 'css', label: 'CSS' },
* { language: 'diff', label: 'Diff' },
* { language: 'xml', label: 'HTML/XML' },
* { language: 'java', label: 'Java' },
* { language: 'javascript', label: 'JavaScript' },
* { language: 'php', label: 'PHP' },
* { language: 'python', label: 'Python' },
* { language: 'ruby', label: 'Ruby' },
* { language: 'typescript', label: 'TypeScript' },
* ]
*
* **Note**: The first language defined in the configuration is considered the default one. This means it will be
* applied to code blocks loaded from data that have no CSS `class` specified (or no matching `class` in the config).
* It will also be used when creating new code blocks using the main UI button. By default it is "Plain text".
*
* @member {Array.<module:code-block/codeblock~CodeBlockLanguageDefinition>} module:code-block/codeblock~CodeBlockConfig#languages
*/

/**
* A sequence of characters inserted or removed from the code block lines when its indentation
* is changed by the user, for instance, using <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd> keys.
*
* The default value is a single tab character (" ", `\u0009` in Unicode).
*
* This configuration is used by `indentCodeBlock` and `outdentCodeBlock` commands (instances of
* {@link module:code-block/indentcodeblockcommand~IndentCodeBlockCommand}).
*
* **Note**: Setting this configuration to `false` will disable the code block indentation commands
* and associated keystrokes.
*
* @member {String} module:code-block/codeblock~CodeBlockConfig#indentSequence
*/
162 changes: 162 additions & 0 deletions src/codeblockcommand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module code-block/codeblockcommand
*/

import Command from '@ckeditor/ckeditor5-core/src/command';
import first from '@ckeditor/ckeditor5-utils/src/first';
import { getNormalizedAndLocalizedLanguageDefinitions } from './utils';

/**
* The code block command plugin.
*
* @extends module:core/command~Command
*/
export default class CodeBlockCommand extends Command {
/**
* Whether the selection starts in a code block.
*
* @observable
* @readonly
* @member {Boolean} #value
*/

/**
* @inheritDoc
*/
refresh() {
this.value = this._getValue();
this.isEnabled = this._checkEnabled();
}

/**
* Executes the command. When the command {@link #value is on}, all top-most code blocks within
* the selection will be removed. If it is off, all selected blocks will be flattened and
* wrapped by a code block.
*
* @fires execute
* @param {Object} [options] Command options.
* @param {Boolean} [options.forceValue] If set, it will force the command behavior. If `true`, the command will apply a code block,
* otherwise the command will remove the code block. If not set, the command will act basing on its current value.
*/
execute( options = {} ) {
const editor = this.editor;
const model = editor.model;
const selection = model.document.selection;
const normalizedLanguagesDefs = getNormalizedAndLocalizedLanguageDefinitions( editor );
const firstLanguageInConfig = normalizedLanguagesDefs[ 0 ];

const blocks = Array.from( selection.getSelectedBlocks() );
const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue;
const language = options.language || firstLanguageInConfig.language;

model.change( writer => {
if ( value ) {
this._applyCodeBlock( writer, blocks, language );
} else {
this._removeCodeBlock( writer, blocks );
}
} );
}

/**
* Checks the command's {@link #value}.
*
* @private
* @returns {Boolean} The current value.
*/
_getValue() {
const selection = this.editor.model.document.selection;
const firstBlock = first( selection.getSelectedBlocks() );
const isCodeBlock = !!( firstBlock && firstBlock.is( 'codeBlock' ) );

return isCodeBlock ? firstBlock.getAttribute( 'language' ) : false;
}

/**
* Checks whether the command can be enabled in the current context.
*
* @private
* @returns {Boolean} Whether the command should be enabled.
*/
_checkEnabled() {
if ( this.value ) {
return true;
}

const selection = this.editor.model.document.selection;
const schema = this.editor.model.schema;

const firstBlock = first( selection.getSelectedBlocks() );

if ( !firstBlock ) {
return false;
}

return canBeCodeBlock( schema, firstBlock );
}

/**
* @private
* @param {module:engine/model/writer~Writer} writer
* @param {Array.<module:engine/model/element~Element>} blocks
* @param {String} [language]
*/
_applyCodeBlock( writer, blocks, language ) {
const schema = this.editor.model.schema;
const allowedBlocks = blocks.filter( block => canBeCodeBlock( schema, block ) );

for ( const block of allowedBlocks ) {
writer.rename( block, 'codeBlock' );
writer.setAttribute( 'language', language, block );
schema.removeDisallowedAttributes( [ block ], writer );
}

allowedBlocks.reverse().forEach( ( currentBlock, i ) => {
const nextBlock = allowedBlocks[ i + 1 ];

if ( currentBlock.previousSibling === nextBlock ) {
writer.appendElement( 'softBreak', nextBlock );
writer.merge( writer.createPositionBefore( currentBlock ) );
}
} );
}

/**
* @private
* @param {module:engine/model/writer~Writer} writer
* @param {Array.<module:engine/model/element~Element>} blocks
*/
_removeCodeBlock( writer, blocks ) {
const codeBlocks = blocks.filter( block => block.is( 'codeBlock' ) );

for ( const block of codeBlocks ) {
const range = writer.createRangeOn( block );

for ( const item of Array.from( range.getItems() ).reverse() ) {
if ( item.is( 'softBreak' ) && item.parent.is( 'codeBlock' ) ) {
const { position } = writer.split( writer.createPositionBefore( item ) );

writer.rename( position.nodeAfter, 'paragraph' );
writer.removeAttribute( 'language', position.nodeAfter );
writer.remove( item );
}
}

writer.rename( block, 'paragraph' );
writer.removeAttribute( 'language', block );
}
}
}

function canBeCodeBlock( schema, element ) {
if ( element.is( 'rootElement' ) || schema.isLimit( element ) ) {
return false;
}

return schema.checkChild( element.parent, 'codeBlock' );
}
Loading

0 comments on commit eb9ef5e

Please sign in to comment.