Code style
CKEditor 5 development environment has ESLint enabled both as a pre-commit hook and on CI. This means that code style issues are detected automatically. Additionally, .editorconfig
files are present in every repository to automatically adjust your IDEs settings (if it is configured to read them).
Here comes a quick summary of these rules.
# General
LF for line endings. Never use CRLF.
The recommended maximum line length is 120 characters. It cannot exceed 140 characters.
# Whitespace
No trailing spaces. Empty lines should not contain any spaces.
Whitespace inside parenthesis and before and after operators:
No whitespace for an empty parenthesis:
No whitespace before colon and semicolon:
# Indentation
Indentation with tab, for both code and comments. Never use spaces.
If you want to have the code readable, set tab to 4 spaces in your IDE.
Multiple lines condition. Use one tab for each line:
We do our best to avoid complex conditions. As a rule of thumb, we first recommend finding a way to move the complexity out of the condition, for example, to a separate function with early returns for each “sentence” in such a condition.
However, overdoing things is not good as well and sometimes such a condition can be perfectly readable (which is the ultimate goal here).
# Braces
Braces start at the same line as the head statement and end aligned with it:
# Blank lines
The code should read like a book, so put blank lines between “paragraphs” of code. This is an open and contextual rule, but some recommendations would be to separate the following sections:
- variable, class and function declarations,
if()
,for()
and similar blocks,- steps of an algorithm,
return
statements,- comment sections (comments should be preceded with a blank line, but if they are the “paragraph” themselves, they should also be followed with one),
- etc.
Example:
# Multi-line statements and calls
Whenever there is a multi-line function call:
- Put the first parameter in a new line.
- Put every parameter in a separate line indented by one tab.
- Put the last closing parenthesis in a new line, at the same indendation level as the beginning of the call.
Examples:
Note that the examples above are just showcasing how such function calls can be structured. However, it is best to avoid them.
It is generally recommended to avoid having functions that accept more than 3 arguments. Instead, it is better to wrap them in an object so all parameters can be named.
It is also recommended to split such long statements into multiple shorter ones, for example, by extracting some longer parameters to separate variables.
# Strings
Use single quotes:
Long strings can be concatenated with plus (+
):
or template strings can be used (note that the 2nd and 3rd line will be indented in this case):
Strings of HTML should use indentation for readability:
# Comments
- Comments are always preceded by a blank line.
- Comments start with a capital first letter and require a period at the end (since they are sentences).
- There must be a single space at the start of the text, right after the comment token.
Block comments (/** ... */
) are used for documentation only. Asterisks are aligned with space:
All other comments use line comments (//
):
Comments related to tickets or issues should not describe the whole issue fully. A short description should be used instead, together with the ticket number in parenthesis:
# Linting
CKEditor 5 development environment uses ESLint and stylelint.
A couple of useful links:
- Disabling ESLint with inline comments.
- CKEditor 5 ESLint preset (npm:
eslint-config-ckeditor5
). - CKEditor 5 stylelint preset (npm:
stylelint-config-ckeditor5
).
Avoid using automatic code formatters on existing code. It is fine to automatically format code that you are editing, but you should not be changing the formatting of the code that is already written to not pollute your PRs. You should also not rely solely on automatic corrections.
# Visibility levels
Each class property (including methods, symbols, getters or setters) can be public, protected or private. The default visibility is public, so you should not document that a property is public — there is no need to do this.
Additional rules apply to private properties:
- The names of private and protected properties that are exposed in a class prototype (or in any other way) should be prefixed with an underscore.
- When documenting a private variable that is not added to a class prototype (or exposed in any other way),
//
comments should be used and using@private
is not necessary. - A symbol property (e.g.
this[ Symbol( 'symbolName' ) ]
) should be documented as@property {Type} _symbolName
.
Example:
# Accessibility
The table below shows the accessibility of properties:
Class | Package | Subclass | World | |
---|---|---|---|---|
@public |
yes | yes | yes | yes |
@protected |
yes | yes | yes | no |
@private |
yes | no | no | no |
(yes – accessible, no – not accessible)
For instance, a protected property is accessible from its own class in which it was defined, from its whole package, and from its subclasses (even if they are not in the same package).
Protected properties and methods are often used for testability. Since tests are located in the same package as the code, they can access these properties.
# Getters
You can use ES6 getters to simplify class API:
A getter should feel like a natural property. There are several recommendations to follow when creating getters:
- They should be fast.
- They should not throw.
- They should not change the object state.
- They should not return new instances of an object every time (so
foo.bar == foo.bar
is true). It is OK to create a new instance for the first call and cache it if it is possible.
# Order within class definition
Within class definition the methods and properties should be ordered as follows:
- Constructor.
- Getters and setters.
- Iterators.
- Public instance methods.
- Public static methods.
- Protected instance methods.
- Protected static methods.
- Private instance methods.
- Private static methods.
The order within each group is left for the implementor.
# Tests
There are some special rules and tips for tests.
# Test organization
-
Always use an outer
describe()
in a test file. Do not allow any globals, especially hooks (beforeEach()
,after()
, etc.) outside the outermostdescribe()
. -
The outermost
describe()
calls should create meaningful groups, so when all tests are run together a failing TC can be identified within the code base. For example:Using titles like “utils” is not fine as there are multiple utils in the entire project. “Table utils” would be better.
-
Test descriptions (
it()
) should be written like documentation (what you do and what should happen), e.g. “the foo dialog closes when the X button is clicked”. Also, “…case 1”, “…case 2” in test descriptions are not helpful. -
Avoid test descriptions like “does not crash when two ranges get merged” — instead explain what is actually expected to happen. For instance: “leaves 1 range when two ranges get merged”.
-
Most often, using words like “correctly”, “works fine” is a code smell. Thing about the requirements — when writing them you do not say that feature X should “work fine”. You document how it should work.
-
Ideally, it should be possible to recreate an algorithm just by reading the test descriptions.
-
Avoid covering multiple cases under one
it()
. It is OK to have multiple assertions in one test, but not to test e.g. how methodfoo()
works when it is called with 1, then with 2, then 3, etc. There should be a separate test for each case. -
Every test should clean after itself, including destroying all editors and removing all elements that have been added.
# Test implementation
-
Avoid using real timeouts. Use fake timers instead when possible. Timeouts make tests really slow.
-
However — do not overoptimize (especially that performance is not a priority in tests). In most cases it is completely fine (and hence recommended) to create a separate editor for every
it()
. -
We aim at having 100% coverage of all distinctive scenarios. Covering 100% branches in the code is not the goal here — it is a byproduct of covering real scenarios.
Think about this — when you fix a bug by adding a parameter to an existing function call you do not affect code coverage (that line was called anyway). However, you had a bug, meaning that your test suite did not cover it. Therefore, a test must be created for that code change.
-
It should be
expect( x ).to.equal( y )
. NOT:.expect( x ).to.be.equal( y )
-
When using Sinon spies, pay attention to the readability of assertions and failure messages.
-
Use named spies, for example:
-
# Naming
# JavaScript code names
Variables, functions, namespaces, parameters and all undocumented cases must be named in lowerCamelCase:
Classes must be named in UpperCamelCase:
Mixins must be named in UpperCamelCase, postfixed with “Mixin”:
Global namespacing variables must be named in ALLCAPS:
# Private properties and methods
Private properties and methods are prefixed with an underscore:
# Methods and functions
Methods and functions are almost always verbs or actions:
# Properties and variables
Properties and variables are almost always nouns:
Boolean properties and variables are always prefixed by an auxiliary verb:
# Buttons, Commands and Plugins
# Buttons
All buttons should follow the verb + noun or the noun convention. Examples:
- The verb + noun convention:
insertTable
selectAll
uploadImage
- The noun convention:
bold
mediaEmbed
restrictedEditing
# Commands
As for commands it is trickier, because there are many more possible combinations of their names than there are for buttons. Examples:
- The feature-related convention:
- noun-based case:
codeBlock
fontColor
shiftEnter
- verb-based case:
indent
removeFormat
selectAll
- noun-based case:
- The feature + sub-feature convention:
imageStyle
imageTextAlternative
tableAlignment
For commands, the noun + verb (or the feature + action) naming conventions should not be used, because it does not sound natural (what do vs. do what). In most cases the proper name should start with the action followed by the feature name:
checkTodoList
insertTable
uploadImage
# Plugins
Plugins should follow the feature or the feature + sub-feature convention. Examples:
- The feature convention:
Bold
Paragraph
SpecialCharacters
- The feature + sub-feature convention:
ImageResize
ListProperties
TableClipboard
Plugins must be named in UpperCamelCase.
# Shortcuts
For local variables commonly accepted short versions for long names are fine:
The following are the only short versions accepted for property names:
# Acronyms and proper names
Acronyms and, partially, proper names are naturally written in uppercase. This may stand against code style rules described above — especially when there is a need to include an acronym or a proper name in a variable or class name. In such case, one should follow the following rules:
- Acronyms:
- All lowercase if at the beginning of the variable name:
let domError
. - Default camel case at the beginning of the class name:
class DomError
. - Default camel case inside the variable or class name:
function getDomError()
.
- All lowercase if at the beginning of the variable name:
- Proper names:
- All lowercase if at the beginning of the variable:
let ckeditorError
. - Original case if at the beginning of the class name:
class CKEditorError
. - Original case inside the variable or class name:
function getCKEditorError()
.
- All lowercase if at the beginning of the variable:
However, two-letter acronyms and proper names (if originally written uppercase) should be uppercase. So e.g. getUI
(not getUi
).
Two most frequently used acronyms which cause problems:
- DOM – It should be e.g.
getDomNode()
, - HTML – It should be e.g.
toHtml()
.
# CSS classes
CSS class naming pattern is based on BEM methodology and code style. All names are in lowercase with an optional dash (-
) between the words.
Top–level building blocks begin with a mandatory ck-
prefix:
Elements belonging to the block namespace are delimited by double underscore (__
):
Modifiers are delimited by a single underscore (_
). Key-value modifiers
follow the block-or-element_key_value
naming pattern:
In HTML:
# ID attributes
HTML ID attribute naming pattern follows CSS classes naming guidelines. Each ID must begin with the ck-
prefix and consist of dash–separated (-
) words in lowercase:
# File names
File and directory names must follow a standard that makes their syntax easy to predict:
- All lowercase.
- Only alphanumeric characters are accepted.
- Words are separated by dashes (
-
) (kebab-case).- Code entities are considered single words, so the
DataProcessor
class is defined in thedataprocessor.js
file. - However, a test file covering for “mutations in multi-root editors”:
mutations-in-multi-root-editors.js
.
- Code entities are considered single words, so the
- HTML files have the
.html
extension.
# Examples
ckeditor.js
tools.js
editor.js
dataprocessor.js
build-all.js
andbuild-min.js
test-core-style-system.html
# Standard files
Widely used standard files do not obey the above rules:
README.md
,LICENSE.md
,CONTRIBUTING.md
,CHANGES.md
.gitignore
and all standard “dot-files”node_modules
# CKEditor 5 custom ESLint rules
In addition to the rules provided by ESLint, CKEditor 5 uses a few custom rules described below.
# Importing between packages: ckeditor5-rules/no-relative-imports
While importing modules from the same package, it is allowed to use relative paths, like this:
While importing modules from other packages, it is not allowed to use relative paths, and the import must be done using the package name, like this:
👎 Examples of incorrect code for this rule:
Even if the import statement works locally, it will throw an error when developers install packages from npm.
👍 Examples of correct code for this rule:
# Description of an error: ckeditor5-rules/ckeditor-error-message
Each time a new error is created, it needs a description to be displayed on the error codes page, like this:
👎 Examples of incorrect code for this rule:
👍 Examples of correct code for this rule:
# DLL Builds: ckeditor5-rules/ckeditor-imports
To make CKEditor 5 plugins compatible with each other, we needed to introduce limitations when importing files from packages.
Packages marked as “Base DLL build” can import between themselves without any restrictions. Names of these packages are specified in the DLL builds guide.
The other CKEditor 5 features (non-DLL) can import “Base DLL” packages using the ckeditor5
package.
When importing modules from the ckeditor5
package, all imports must come from the src/
directory. Other directories are not published on npm, so such imports will not work.
👎 Examples of incorrect code for this rule:
👍 Examples of correct code for this rule:
Also, non-DLL packages should not import between non-DLL packages to avoid code duplications when building DLL builds.
👎 Examples of incorrect code for this rule:
To use the createImageViewElement()
function, consider implementing a utils plugin that will expose the required function in the ckeditor5-image
package.
When importing a DLL package from another DLL package, an import statement must use the full name of the imported package instead of using the ckeditor5
notation.
👎 Examples of incorrect code for this rule:
👍 Examples of correct code for this rule:
History of changes:
- Force importing using the
ckeditor5
package. - Imports from the
ckeditor5
package must use thesrc/
directory. - Imports between DLL packages must use full names of packages.
# Cross package imports: ckeditor5-rules/no-cross-package-imports
It is allowed to import modules from other packages:
However, some packages cannot import modules from CKEditor 5 as it could lead to code duplication and errors in runtime. Hence, the rule disables this kind of import.
Currently, it applies to the @ckeditor/ckeditor5-watchdog
package.
👎 Examples of an incorrect code for this rule:
Every day, we work hard to keep our documentation complete. Have you spotted an outdated information? Is something missing? Please report it via our issue tracker.