From cb547a8c2eeb40f3c19e976b129a5fea2d317f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Fri, 10 Feb 2017 11:50:16 +0100 Subject: [PATCH 1/3] Initial implementation. --- CHANGELOG.md | 6 + LICENSE.md | 2 +- README.md | 4 +- lang/contexts.json | 3 + package.json | 19 +- src/blockquote.js | 86 ++++++ src/blockquotecommand.js | 224 +++++++++++++++ src/blockquoteengine.js | 63 +++++ tests/.jshintrc | 22 ++ tests/blockquote.js | 238 ++++++++++++++++ tests/blockquotecommand.js | 532 +++++++++++++++++++++++++++++++++++ tests/blockquoteengine.js | 71 +++++ tests/manual/blockquote.html | 18 ++ tests/manual/blockquote.js | 24 ++ tests/manual/blockquote.md | 11 + tests/manual/logo.png | Bin 0 -> 23712 bytes theme/theme.scss | 8 + 17 files changed, 1323 insertions(+), 8 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 lang/contexts.json create mode 100644 src/blockquote.js create mode 100644 src/blockquotecommand.js create mode 100644 src/blockquoteengine.js create mode 100644 tests/.jshintrc create mode 100644 tests/blockquote.js create mode 100644 tests/blockquotecommand.js create mode 100644 tests/blockquoteengine.js create mode 100644 tests/manual/blockquote.html create mode 100644 tests/manual/blockquote.js create mode 100644 tests/manual/blockquote.md create mode 100644 tests/manual/logo.png create mode 100644 theme/theme.scss diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b5541e2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +Changelog +========= + +## 0.0.1 (2017-03-06) + +Internal changes only (updated dependencies, documentation, etc.). diff --git a/LICENSE.md b/LICENSE.md index ee70020..e60faa5 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ Software License Agreement ========================== -**CKEditor 5 Message Quote Feature** – https://github.com/ckeditor/ckeditor5-paragraph
+**CKEditor 5 Block Quote Feature** – https://github.com/ckeditor/ckeditor5-paragraph
Copyright (c) 2003-2017, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved. Licensed under the terms of any of the following licenses at your choice: diff --git a/README.md b/README.md index c3c683e..d3211b2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -CKEditor 5 Message Quote Feature +CKEditor 5 Block Quote Feature ======================================== [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-quote.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-quote) @@ -7,7 +7,7 @@ CKEditor 5 Message Quote Feature [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-quote/status.svg)](https://david-dm.org/ckeditor/ckeditor5-quote) [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-quote/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-quote?type=dev) -The message quote feature for CKEditor 5 project. More information about the project can be found at the following url: . +The block quote feature for CKEditor 5 project. More information about the project can be found at the following url: . ## License diff --git a/lang/contexts.json b/lang/contexts.json new file mode 100644 index 0000000..163affa --- /dev/null +++ b/lang/contexts.json @@ -0,0 +1,3 @@ +{ + "Block quote": "Toolbar button tooltip for the Block quote feature." +} diff --git a/package.json b/package.json index 38fd37b..facfa51 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,23 @@ { - "name": "@ckeditor/ckeditor5-quote", + "name": "@ckeditor/ckeditor5-block-quote", "version": "0.0.0", - "description": "Message quote feature for CKEditor 5.", + "description": "Block quote feature for CKEditor 5.", "keywords": [ "CKEditor" ], "dependencies": { + "@ckeditor/ckeditor5-core": "*", + "@ckeditor/ckeditor5-engine": "*", + "@ckeditor/ckeditor5-ui": "*", + "@ckeditor/ckeditor5-utils": "*" }, "devDependencies": { - "@ckeditor/ckeditor5-dev-lint": "^2.0.0", + "@ckeditor/ckeditor5-dev-lint": "^2.0.2", + "@ckeditor/ckeditor5-editor-classic": "*", + "@ckeditor/ckeditor5-enter": "*", + "@ckeditor/ckeditor5-list": "*", + "@ckeditor/ckeditor5-paragraph": "*", + "@ckeditor/ckeditor5-presets": "*", "gulp": "^3.9.0", "guppy-pre-commit": "^0.4.0" }, @@ -19,9 +28,9 @@ "author": "CKSource (http://cksource.com/)", "license": "(GPL-2.0 OR LGPL-2.1 OR MPL-1.1)", "homepage": "https://ckeditor5.github.io", - "bugs": "https://github.com/ckeditor/ckeditor5-quote/issues", + "bugs": "https://github.com/ckeditor/ckeditor5-block-quote/issues", "repository": { "type": "git", - "url": "https://github.com/ckeditor/ckeditor5-quote.git" + "url": "https://github.com/ckeditor/ckeditor5-block-quote.git" } } diff --git a/src/blockquote.js b/src/blockquote.js new file mode 100644 index 0000000..53cc154 --- /dev/null +++ b/src/blockquote.js @@ -0,0 +1,86 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module block-quote/blockquote + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import BlockQuoteEngine from './blockquoteengine'; + +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; + +import quoteIcon from '@ckeditor/ckeditor5-core/theme/icons/quote.svg'; +import '../theme/theme.scss'; + +/** + * The block quote plugin. + * + * It introduces the `'blockQuote'` button and requires {@link module:block-quote/blockquoteengine~BlockQuoteEngine} + * plugin. It also changes Enter key behavior so it escapes block quotes when pressed in an + * empty quoted block. + * + * @extends module:core/plugin~Plugin + */ +export default class BlockQuote extends Plugin { + /** + * @inheritDoc + */ + static get requires() { + return [ BlockQuoteEngine ]; + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const t = editor.t; + const command = editor.commands.get( 'blockQuote' ); + + editor.ui.componentFactory.add( 'blockQuote', ( locale ) => { + const buttonView = new ButtonView( locale ); + + buttonView.set( { + label: t( 'Block quote' ), + icon: quoteIcon, + tooltip: true + } ); + + // Bind button model to command. + buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' ); + + // Execute command. + this.listenTo( buttonView, 'execute', () => editor.execute( 'blockQuote' ) ); + + return buttonView; + } ); + } + + /** + * @inheritDoc + */ + afterInit() { + const editor = this.editor; + const command = editor.commands.get( 'blockQuote' ); + + // Overwrite default enter key behavior. + // If enter key is pressed with selection collapsed in empty block inside a quote, break the quote. + // This listener is added in afterInit in order to register it after list's feature listener. + // We can't use a priority for this, because 'low' is already used by the enter feature, unless + // we'd use numeric priority in this case. + this.listenTo( this.editor.editing.view, 'enter', ( evt, data ) => { + const doc = this.editor.document; + const positionParent = doc.selection.getLastPosition().parent; + + if ( doc.selection.isCollapsed && positionParent.isEmpty && command.value ) { + this.editor.execute( 'blockQuote' ); + + data.preventDefault(); + evt.stop(); + } + } ); + } +} diff --git a/src/blockquotecommand.js b/src/blockquotecommand.js new file mode 100644 index 0000000..82be4d9 --- /dev/null +++ b/src/blockquotecommand.js @@ -0,0 +1,224 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module block-quote/blockquotecommand + */ + +import Command from '@ckeditor/ckeditor5-core/src/command/command'; +import Position from '@ckeditor/ckeditor5-engine/src/model/position'; +import Element from '@ckeditor/ckeditor5-engine/src/model/element'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; +import first from '@ckeditor/ckeditor5-utils/src/first'; + +/** + * The block quote command. + * + * @extends module:core/command/command~Command + */ +export default class BlockQuoteCommand extends Command { + /** + * @inheritDoc + */ + constructor( editor ) { + super( editor ); + + /** + * Flag indicating whether the command is active. It's on when the selection starts + * in a quoted block. + * + * @readonly + * @observable + * @member {Boolean} #value + */ + this.set( 'value', false ); + + // Update current value each time changes are done to the document. + this.listenTo( editor.document, 'changesDone', () => { + this.refreshValue(); + this.refreshState(); + } ); + } + + /** + * Updates command's {@link #value} based on the current selection. + */ + refreshValue() { + const firstBlock = this.editor.document.selection.getSelectedBlocks().next().value; + + // In the current implementation, the block quote must be an immediate parent of a block element. + this.value = !!( firstBlock && findQuote( firstBlock ) ); + } + + /** + * Executes the command. When the command {@link #value is on}, then all block quotes within + * the selection will be removed. If it's off, then all selected blocks will be wrapped with + * a block quote. + * + * @protected + * @param {Object} [options] Options for executed command. + * @param {module:engine/model/batch~Batch} [options.batch] Batch to collect all the change steps. + * New batch will be created if this option is not set. + */ + _doExecute( options = {} ) { + const doc = this.editor.document; + const batch = options.batch || doc.batch(); + const blocks = Array.from( doc.selection.getSelectedBlocks() ); + + doc.enqueueChanges( () => { + if ( this.value ) { + this._removeQuote( batch, blocks.filter( findQuote ) ); + } else { + this._applyQuote( batch, blocks ); + } + } ); + } + + /** + * @inheritDoc + */ + _checkEnabled() { + if ( this.value ) { + return true; + } + + const selection = this.editor.document.selection; + const schema = this.editor.document.schema; + + const firstBlock = first( selection.getSelectedBlocks() ); + + if ( !firstBlock ) { + return false; + } + + const isMQAllowed = schema.check( { + name: 'blockQuote', + inside: Position.createBefore( firstBlock ) + } ); + const isBlockAllowed = schema.check( { + name: firstBlock.name, + attributes: Array.from( firstBlock.getAttributeKeys() ), + inside: 'blockQuote' + } ); + + // Whether can wrap the block. + return isMQAllowed && isBlockAllowed; + } + + /** + * Removes the quote from the given blocks. + * + * If blocks which are supposed to be "unquoted" are in the middle of a quote, + * start it or end it, then the quote will be split (if needed) and the blocks + * will be moved out of it, so other quoted blocks remained quoted. + * + * @param {module:engine/model/batch~Batch} batch + * @param {Array.} blocks + */ + _removeQuote( batch, blocks ) { + // Unquote all groups of block. Iterate in the reverse order to not break following ranges. + getRangesOfBlockGroups( blocks ).reverse().forEach( ( groupRange ) => { + if ( groupRange.start.isAtStart && groupRange.end.isAtEnd ) { + batch.unwrap( groupRange.start.parent ); + + return; + } + + // The group of blocks are at the beginning of an so let's move them left (out of the ). + if ( groupRange.start.isAtStart ) { + const positionBefore = Position.createBefore( groupRange.start.parent ); + + batch.move( groupRange, positionBefore ); + + return; + } + + // The blocks are in the middle of an so we need to split the after the last block + // so we move the items there. + if ( !groupRange.end.isAtEnd ) { + batch.split( groupRange.end ); + } + + // Now we are sure that groupRange.end.isAtEnd is true, so let's move the blocks right. + + const positionAfter = Position.createAfter( groupRange.end.parent ); + + batch.move( groupRange, positionAfter ); + } ); + } + + /** + * Applies the quote to the given blocks. + * + * @param {module:engine/model/batch~Batch} batch + * @param {Array.} blocks + */ + _applyQuote( batch, blocks ) { + const quotesToMerge = []; + + // Quote all groups of block. Iterate in the reverse order to not break following ranges. + getRangesOfBlockGroups( blocks ).reverse().forEach( ( groupRange ) => { + let quote = findQuote( groupRange.start ); + + if ( !quote ) { + quote = new Element( 'blockQuote' ); + + batch.wrap( groupRange, quote ); + } + + quotesToMerge.push( quote ); + } ); + + // Merge subsequent elements. Reverse the order again because this time we want to go through + // the elements in the source order (due to how merge works – it moves the right element's content + // to the first element and removes the right one. Since we may need to merge a couple of subsequent `` elements + // we want to keep the reference to the first (furthest left) one. + quotesToMerge.reverse().reduce( ( currentQuote, nextQuote ) => { + if ( currentQuote.nextSibling == nextQuote ) { + batch.merge( Position.createAfter( currentQuote ) ); + + return currentQuote; + } + + return nextQuote; + } ); + } +} + +function findQuote( elementOrPosition ) { + return elementOrPosition.parent.name == 'blockQuote' ? elementOrPosition.parent : null; +} + +// Returns a minimal array of ranges containing groups of subsequent blocks. +// +// content: abcdefgh +// blocks: [ a, b, d , f, g, h ] +// output ranges: [ab]c[d]e[fgh] +// +// @param {Array.} blocks +// @returns {Array.} +function getRangesOfBlockGroups( blocks ) { + let startPosition; + let i = 0; + const ranges = []; + + while ( i < blocks.length ) { + const block = blocks[ i ]; + const nextBlock = blocks[ i + 1 ]; + + if ( !startPosition ) { + startPosition = Position.createBefore( block ); + } + + if ( !nextBlock || block.nextSibling != nextBlock ) { + ranges.push( new Range( startPosition, Position.createAfter( block ) ) ); + startPosition = null; + } + + i++; + } + + return ranges; +} diff --git a/src/blockquoteengine.js b/src/blockquoteengine.js new file mode 100644 index 0000000..20147b1 --- /dev/null +++ b/src/blockquoteengine.js @@ -0,0 +1,63 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module block-quote/blockquoteengine + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; + +import BlockQuoteCommand from './blockquotecommand'; + +import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter'; +import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter'; + +/** + * The block quote engine. + * + * Introduces the `'blockQuote'` command and `'blockQuote'` model element. + * + * @extends module:core/plugin~Plugin + */ +export default class BlockQuoteEngine extends Plugin { + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const schema = editor.document.schema; + + editor.commands.set( 'blockQuote', new BlockQuoteCommand( editor ) ); + + schema.registerItem( 'blockQuote' ); + schema.allow( { name: 'blockQuote', inside: '$root' } ); + schema.allow( { name: '$block', inside: 'blockQuote' } ); + + buildViewConverter().for( editor.data.viewToModel ) + .fromElement( 'blockquote' ) + .toElement( 'blockQuote' ); + + buildModelConverter().for( editor.data.modelToView, editor.editing.modelToView ) + .fromElement( 'blockQuote' ) + .toElement( 'blockquote' ); + } + + /** + * @inheritDoc + */ + afterInit() { + const schema = this.editor.document.schema; + + // TODO + // Workaround for https://github.com/ckeditor/ckeditor5-engine/issues/532#issuecomment-280924650. + if ( schema.hasItem( 'listItem' ) ) { + schema.allow( { + name: 'listItem', + inside: 'blockQuote', + attributes: [ 'type', 'indent' ] + } ); + } + } +} diff --git a/tests/.jshintrc b/tests/.jshintrc new file mode 100644 index 0000000..912ffcd --- /dev/null +++ b/tests/.jshintrc @@ -0,0 +1,22 @@ +{ + "esnext": true, + "expr": true, + "immed": true, + "loopfunc": true, + "noarg": true, + "nonbsp": true, + "strict": "implied", + "undef": true, + "unused": true, + "varstmt": true, + "globals": { + "after": false, + "afterEach": false, + "before": false, + "beforeEach": false, + "describe": false, + "expect": false, + "it": false, + "sinon": false + } +} diff --git a/tests/blockquote.js b/tests/blockquote.js new file mode 100644 index 0000000..9986735 --- /dev/null +++ b/tests/blockquote.js @@ -0,0 +1,238 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* global document */ + +import BlockQuote from '../src/blockquote'; +import BlockQuoteEngine from '../src/blockquoteengine'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import List from '@ckeditor/ckeditor5-list/src/list'; +import Enter from '@ckeditor/ckeditor5-enter/src/enter'; + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +describe( 'BlockQuote', () => { + let editor, doc, command, element; + + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return ClassicTestEditor.create( element, { + plugins: [ BlockQuote, Paragraph, List, Enter ] + } ) + .then( newEditor => { + editor = newEditor; + doc = editor.document; + command = editor.commands.get( 'blockQuote' ); + } ); + } ); + + afterEach( () => { + element.remove(); + + return editor.destroy(); + } ); + + it( 'requires BlockQuoteEngine', () => { + expect( BlockQuote.requires ).to.deep.equal( [ BlockQuoteEngine ] ); + } ); + + describe( 'blockQuote button', () => { + it( 'has the base properties', () => { + const button = editor.ui.componentFactory.create( 'blockQuote' ); + + expect( button ).to.have.property( 'label', 'Block quote' ); + expect( button ).to.have.property( 'icon' ); + expect( button ).to.have.property( 'tooltip', true ); + } ); + + it( 'has isOn bound to command\'s value', () => { + const button = editor.ui.componentFactory.create( 'blockQuote' ); + + command.value = false; + expect( button ).to.have.property( 'isOn', false ); + + command.value = true; + expect( button ).to.have.property( 'isOn', true ); + } ); + + it( 'has isEnabled bound to command\'s isEnabled', () => { + const button = editor.ui.componentFactory.create( 'blockQuote' ); + + command.isEnabled = true; + expect( button ).to.have.property( 'isEnabled', true ); + + command.isEnabled = false; + expect( button ).to.have.property( 'isEnabled', false ); + } ); + + it( 'executes command when it\'s executed', () => { + const button = editor.ui.componentFactory.create( 'blockQuote' ); + + const spy = sinon.stub( editor, 'execute' ); + + button.fire( 'execute' ); + + expect( spy.calledOnce ).to.be.true; + expect( spy.args[ 0 ][ 0 ] ).to.equal( 'blockQuote' ); + } ); + } ); + + describe( 'enter key support', () => { + function fakeEventData() { + return { + preventDefault: sinon.spy() + }; + } + + it( 'does nothing if selection is in an empty block but not in a block quote', () => { + const data = fakeEventData(); + const execSpy = sinon.spy( editor, 'execute' ); + + setModelData( doc, 'x[]x' ); + + editor.editing.view.fire( 'enter', data ); + + // Only enter command should be executed. + expect( data.preventDefault.called ).to.be.true; + expect( execSpy.calledOnce ).to.be.true; + expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'enter' ); + } ); + + it( 'does nothing if selection is in a non-empty block (at the end) in a block quote', () => { + const data = fakeEventData(); + const execSpy = sinon.spy( editor, 'execute' ); + + setModelData( doc, '
xx[]
' ); + + editor.editing.view.fire( 'enter', data ); + + // Only enter command should be executed. + expect( data.preventDefault.called ).to.be.true; + expect( execSpy.calledOnce ).to.be.true; + expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'enter' ); + } ); + + it( 'does nothing if selection is in a non-empty block (at the beginning) in a block quote', () => { + const data = fakeEventData(); + const execSpy = sinon.spy( editor, 'execute' ); + + setModelData( doc, '
[]xx
' ); + + editor.editing.view.fire( 'enter', data ); + + // Only enter command should be executed. + expect( data.preventDefault.called ).to.be.true; + expect( execSpy.calledOnce ).to.be.true; + expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'enter' ); + } ); + + it( 'does nothing if selection is not collapsed', () => { + const data = fakeEventData(); + const execSpy = sinon.spy( editor, 'execute' ); + + setModelData( doc, '
[]
' ); + + editor.editing.view.fire( 'enter', data ); + + // Only enter command should be executed. + expect( data.preventDefault.called ).to.be.true; + expect( execSpy.calledOnce ).to.be.true; + expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'enter' ); + } ); + + it( 'does not interfere with a similar handler in the list feature', () => { + const data = fakeEventData(); + + setModelData( doc, + 'x' + + '
' + + 'a' + + '[]' + + '
' + + 'x' + ); + + editor.editing.view.fire( 'enter', data ); + + expect( data.preventDefault.called ).to.be.true; + + expect( getModelData( doc ) ).to.equal( + 'x' + + '
' + + 'a' + + '[]' + + '
' + + 'x' + ); + } ); + + it( 'escapes block quote if selection is in an empty block in an empty block quote', () => { + const data = fakeEventData(); + const execSpy = sinon.spy( editor, 'execute' ); + + setModelData( doc, 'x
[]
x' ); + + editor.editing.view.fire( 'enter', data ); + + expect( data.preventDefault.called ).to.be.true; + expect( execSpy.calledOnce ).to.be.true; + expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( 'x[]x' ); + } ); + + it( 'escapes block quote if selection is in an empty block in the middle of a block quote', () => { + const data = fakeEventData(); + const execSpy = sinon.spy( editor, 'execute' ); + + setModelData( doc, + 'x' + + '
a[]b
' + + 'x' + ); + + editor.editing.view.fire( 'enter', data ); + + expect( data.preventDefault.called ).to.be.true; + expect( execSpy.calledOnce ).to.be.true; + expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + 'x' + + '
a
' + + '[]' + + '
b
' + + 'x' + ); + } ); + + it( 'escapes block quote if selection is in an empty block at the end of a block quote', () => { + const data = fakeEventData(); + const execSpy = sinon.spy( editor, 'execute' ); + + setModelData( doc, + 'x' + + '
a[]
' + + 'x' + ); + + editor.editing.view.fire( 'enter', data ); + + expect( data.preventDefault.called ).to.be.true; + expect( execSpy.calledOnce ).to.be.true; + expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + 'x' + + '
a
' + + '[]' + + 'x' + ); + } ); + } ); +} ); diff --git a/tests/blockquotecommand.js b/tests/blockquotecommand.js new file mode 100644 index 0000000..2ce2aa5 --- /dev/null +++ b/tests/blockquotecommand.js @@ -0,0 +1,532 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import BlockQuoteEngine from '../src/blockquoteengine'; +import BlockQuoteCommand from '../src/blockquotecommand'; + +import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter'; + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; + +import Command from '@ckeditor/ckeditor5-core/src/command/command'; + +describe( 'BlockQuoteCommand', () => { + let editor, doc, command; + + beforeEach( () => { + return VirtualTestEditor.create( { + plugins: [ BlockQuoteEngine ] + } ) + .then( newEditor => { + editor = newEditor; + + doc = editor.document; + + doc.schema.registerItem( 'paragraph', '$block' ); + doc.schema.registerItem( 'heading', '$block' ); + doc.schema.registerItem( 'widget' ); + + doc.schema.allow( { name: 'widget', inside: '$root' } ); + doc.schema.allow( { name: '$text', inside: 'widget' } ); + + doc.schema.limits.add( 'widget' ); + + buildModelConverter().for( editor.editing.modelToView ) + .fromElement( 'paragraph' ) + .toElement( 'p' ); + + buildModelConverter().for( editor.editing.modelToView ) + .fromElement( 'heading' ) + .toElement( 'h' ); + + buildModelConverter().for( editor.editing.modelToView ) + .fromElement( 'widget' ) + .toElement( 'widget' ); + + command = editor.commands.get( 'blockQuote' ); + } ); + } ); + + afterEach( () => { + editor.destroy(); + } ); + + it( 'is a command', () => { + expect( BlockQuoteCommand.prototype ).to.be.instanceOf( Command ); + expect( command ).to.be.instanceOf( Command ); + } ); + + describe( 'value', () => { + it( 'is false when selection is not in a block quote', () => { + setModelData( doc, 'x[]x' ); + + expect( command ).to.have.property( 'value', false ); + } ); + + it( 'is false when start of the selection is not in a block quote', () => { + setModelData( doc, 'x[x
y]y
' ); + + expect( command ).to.have.property( 'value', false ); + } ); + + it( 'is false when selection starts in a blockless space', () => { + doc.schema.allow( { name: '$text', inside: '$root' } ); + + setModelData( doc, 'x[]x' ); + + expect( command ).to.have.property( 'value', false ); + } ); + + it( 'is true when selection is in a block quote', () => { + setModelData( doc, '
x[]x
' ); + + expect( command ).to.have.property( 'value', true ); + } ); + + it( 'is true when selection starts in a block quote', () => { + setModelData( doc, '
x[x
y]y' ); + + expect( command ).to.have.property( 'value', true ); + } ); + } ); + + describe( 'isEnabled', () => { + it( 'is true when selection is in a block which can be wrapped with blockQuote', () => { + setModelData( doc, 'x[]x' ); + + expect( command ).to.have.property( 'isEnabled', true ); + } ); + + it( 'is true when selection is in a block which is already in blockQuote', () => { + setModelData( doc, '
x[]x
' ); + + expect( command ).to.have.property( 'isEnabled', true ); + } ); + + it( 'is true when selection starts in a block which can be wrapped with blockQuote', () => { + setModelData( doc, 'x[xy]y' ); + + expect( command ).to.have.property( 'isEnabled', true ); + } ); + + it( 'is false when selection is in an element which cannot be wrapped with blockQuote (because it cannot be its child)', () => { + setModelData( doc, 'x[]x' ); + + expect( command ).to.have.property( 'isEnabled', false ); + } ); + + it( + 'is false when selection is in an element which cannot be wrapped with blockQuote' + + '(because mQ is not allowed in its parent)', + () => { + doc.schema.disallow( { name: 'blockQuote', inside: '$root' } ); + + setModelData( doc, 'x[]x' ); + + expect( command ).to.have.property( 'isEnabled', false ); + } + ); + + // https://github.com/ckeditor/ckeditor5-engine/issues/826 + // it( 'is false when selection starts in an element which cannot be wrapped with blockQuote', () => { + // setModelData( doc, 'x[xy]y' ); + + // expect( command ).to.have.property( 'isEnabled', false ); + // } ); + } ); + + describe( '_doExecute()', () => { + describe( 'applying quote', () => { + it( 'should wrap a single block', () => { + setModelData( + doc, + 'abc' + + 'x[]x' + + 'def' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + 'abc' + + '
x[]x
' + + 'def' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '

abc

x{}x

def

' + ); + } ); + + it( 'should wrap multiple blocks', () => { + setModelData( + doc, + 'a[bc' + + 'xx' + + 'de]f' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + '
' + + 'a[bc' + + 'xx' + + 'de]f' + + '
' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '
a{bc

xx

de}f

' + ); + } ); + + it( 'should merge with an existing quote', () => { + setModelData( + doc, + 'a[bc' + + '
x]xyy
' + + 'def' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + '
' + + 'a[bc' + + 'x]x' + + 'yy' + + '
' + + 'def' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '
a{bc

x}x

yy

def

' + ); + } ); + + it( 'should not merge with a quote preceding the current block', () => { + setModelData( + doc, + '
abc
' + + 'x[]x' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + '
abc
' + + '
x[]x
' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '

abc

' + + '

x{}x

' + ); + } ); + + it( 'should not merge with a quote following the current block', () => { + setModelData( + doc, + 'x[]x' + + '
abc
' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + '
x[]x
' + + '
abc
' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '

x{}x

' + + '

abc

' + ); + } ); + + it( 'should merge with an existing quote (more blocks)', () => { + setModelData( + doc, + 'a[bc' + + 'def' + + '
x]x
' + + 'ghi' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + '
' + + 'a[bc' + + 'def' + + 'x]x' + + '
' + + 'ghi' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '
a{bc

def

x}x

ghi

' + ); + } ); + + it( 'should not wrap non-block content', () => { + setModelData( + doc, + 'a[bc' + + 'xx' + + 'de]f' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + '
' + + 'a[bc' + + '
' + + 'xx' + + '
' + + 'de]f' + + '
' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '

a{bc

xx

de}f

' + ); + } ); + + it( 'should correctly wrap and merge groups of blocks', () => { + setModelData( + doc, + 'a[bc' + + 'xx' + + 'def' + + '
ghi
' + + 'yy' + + 'jk]l' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + '
a[bc
' + + 'xx' + + '
defghi
' + + 'yy' + + '
jk]l
' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '

a{bc

' + + 'xx' + + '

def

ghi

' + + 'yy' + + '

jk}l

' + ); + } ); + + it( 'should correctly merge a couple of subsequent quotes', () => { + setModelData( + doc, + 'x' + + 'a[bc' + + '
def
' + + 'ghi' + + '
jkl
' + + 'mn]o' + + 'y' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + 'x' + + '
' + + 'a[bc' + + 'def' + + 'ghi' + + 'jkl' + + 'mn]o' + + '
' + + 'y' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '

x

' + + '
' + + '

a{bc

' + + '

def

' + + '

ghi

' + + '

jkl

' + + '

mn}o

' + + '
' + + '

y

' + ); + } ); + } ); + + describe( 'removing quote', () => { + it( 'should unwrap a single block', () => { + setModelData( + doc, + 'abc' + + '
x[]x
' + + 'def' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + 'abc' + + 'x[]x' + + 'def' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '

abc

x{}x

def

' + ); + } ); + + it( 'should unwrap multiple blocks', () => { + setModelData( + doc, + '
' + + 'a[bc' + + 'xx' + + 'de]f' + + '
' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + 'a[bc' + + 'xx' + + 'de]f' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '

a{bc

xx

de}f

' + ); + } ); + + it( 'should unwrap only the selected blocks - at the beginning', () => { + setModelData( + doc, + 'xx' + + '
' + + 'a[b]c' + + 'xx' + + '
' + + 'yy' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + 'xx' + + 'a[b]c' + + '
' + + 'xx' + + '
' + + 'yy' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '

xx

a{b}c

xx

yy

' + ); + } ); + + it( 'should unwrap only the selected blocks - at the end', () => { + setModelData( + doc, + '
' + + 'abc' + + 'x[x' + + '
' + + 'de]f' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + '
' + + 'abc' + + '
' + + 'x[x' + + 'de]f' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '

abc

x{x

de}f

' + ); + } ); + + it( 'should unwrap only the selected blocks - in the middle', () => { + setModelData( + doc, + 'xx' + + '
' + + 'abc' + + 'c[]de' + + 'fgh' + + '
' + + 'xx' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + 'xx' + + '
abc
' + + 'c[]de' + + '
fgh
' + + 'xx' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '

xx

' + + '

abc

' + + '

c{}de

' + + '

fgh

' + + '

xx

' + ); + } ); + + it( 'should remove multiple quotes', () => { + setModelData( + doc, + '
a[bc
' + + 'xx' + + '
defghi
' + + 'yy' + + '
de]fghi
' + ); + + editor.execute( 'blockQuote' ); + + expect( getModelData( doc ) ).to.equal( + 'a[bc' + + 'xx' + + 'defghi' + + 'yy' + + 'de]f' + + '
ghi
' + ); + + expect( getViewData( editor.editing.view ) ).to.equal( + '

a{bc

' + + '

xx

' + + '

def

ghi

' + + '

yy

' + + '

de}f

' + + '

ghi

' + ); + } ); + } ); + } ); +} ); diff --git a/tests/blockquoteengine.js b/tests/blockquoteengine.js new file mode 100644 index 0000000..cb991fb --- /dev/null +++ b/tests/blockquoteengine.js @@ -0,0 +1,71 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import BlockQuoteEngine from '../src/blockquoteengine'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import ListEngine from '@ckeditor/ckeditor5-list/src/listengine'; + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import BlockQuoteCommand from '../src/blockquotecommand'; + +describe( 'BlockQuoteEngine', () => { + let editor, doc; + + beforeEach( () => { + return VirtualTestEditor.create( { + plugins: [ BlockQuoteEngine, Paragraph ] + } ) + .then( newEditor => { + editor = newEditor; + + doc = editor.document; + } ); + } ); + + afterEach( () => { + editor.destroy(); + } ); + + it( 'adds a blockQuote command', () => { + expect( editor.commands.get( 'blockQuote' ) ).to.be.instanceOf( BlockQuoteCommand ); + } ); + + it( 'allows for blockQuote in the $root', () => { + expect( doc.schema.check( { name: 'blockQuote', inside: '$root' } ) ).to.be.true; + } ); + + it( 'allows for $block in blockQuote', () => { + expect( doc.schema.check( { name: '$block', inside: 'blockQuote' } ) ).to.be.true; + expect( doc.schema.check( { name: 'paragraph', inside: 'blockQuote' } ) ).to.be.true; + } ); + + it( 'adds converters to the data pipeline', () => { + const data = '

x

'; + + editor.setData( data ); + + expect( getModelData( doc ) ).to.equal( '
[]x
' ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'adds a converter to the view pipeline', () => { + setModelData( doc, '
x
' ); + + expect( editor.getData() ).to.equal( '

x

' ); + } ); + + it( 'allows list items inside blockQuote', () => { + return VirtualTestEditor.create( { + plugins: [ BlockQuoteEngine, Paragraph, ListEngine ] + } ) + .then( editor => { + editor.setData( '
  • xx
' ); + + expect( editor.getData() ).to.equal( '
  • xx
' ); + } ); + } ); +} ); diff --git a/tests/manual/blockquote.html b/tests/manual/blockquote.html new file mode 100644 index 0000000..5bde4c1 --- /dev/null +++ b/tests/manual/blockquote.html @@ -0,0 +1,18 @@ +
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

+
+

Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat.

+
+ CKEditor logo +
+

Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

+
+

Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris.

+
    +
  • Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.
  • +
+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum.

+
diff --git a/tests/manual/blockquote.js b/tests/manual/blockquote.js new file mode 100644 index 0000000..0b5bcda --- /dev/null +++ b/tests/manual/blockquote.js @@ -0,0 +1,24 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* global document, console, window */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic'; +import ArticlePreset from '@ckeditor/ckeditor5-presets/src/article'; +import BlockQuote from '../../src/blockquote'; + +ClassicEditor.create( document.querySelector( '#editor' ), { + plugins: [ + ArticlePreset, + BlockQuote + ], + toolbar: [ 'headings', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ] +} ) +.then( editor => { + window.editor = editor; +} ) +.catch( err => { + console.error( err.stack ); +} ); diff --git a/tests/manual/blockquote.md b/tests/manual/blockquote.md new file mode 100644 index 0000000..d36ddc8 --- /dev/null +++ b/tests/manual/blockquote.md @@ -0,0 +1,11 @@ +## Block quote feature + +Check block quote related behaviors: + +* applying quotes to multiple blocks, +* removing quotes, +* Enter (should leave quote when pressed in an empty block), +* Backspace, +* undo/redo, +* applying headings and lists, +* stability when used with nested lists. diff --git a/tests/manual/logo.png b/tests/manual/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c9a13d628da130fe4bb24eef85fe39551360625b GIT binary patch literal 23712 zcmV+8Kpek`P)4Tx0C?Jsl+SMzWf;dlyYtSh2{;E+0gJ(jKY)-zwqTG3L3g{$wouDv+hW_8 znCoMPiX1$ccrw9X5iY<%0|`MxY)DKvl=ve=5-x_+!%TMv zXvO#R%=3Pq@AvyW?~{Sd=T+Z#(-zEnp{kVf*7(GvmAOHhXIMiwYpQnO=gQ?FfU57i zJaX>+ObX!I_LcAd^Q@`Kz=qTlK(iLuvyeIlx@-F?1mhO4=V<7Mka-s9QsWbokjVmF z^(Za?T~kp!4s@x}No`@q40Cbh4CzWGcl~Ud+tLC)h%7op= zT>sBA>$aBlHUVpcnUO=lR!ALdsDVlxe_~gQBfyK0y6lJf3a}Ycf3#-Ca={irt|ajcdi^v~yrE#?Sk1c_{KG&Z<^rEXG`|s$wbP5r0c> zEaucV)T)I;zz)#&b0k$o(qsyWjmLc~DwN1$;iG6Wje|=GiHAhtq@H93XBAu-iHhS6 zqK?F45ru=j3MNvx9b=key~v7PA}jWaA+b;F5q((VHSxOGFA5?n`o!LLZ8@QDQRjA> z=UrMzEGir!f4aN1wWD`Zz3tdnPThH+y7`D_x$Gx70Rm>)$nDAgFU^0qp~G=qoO|oq z+Dr2se%yTZTl%7YUB9K@&=)!g{a1ZKU(j!m!^NeJWEP2oha^ZiNcM06myjwhzv0nL z;2uz%_D*+qZtN?k?#TCs?+tUZr<2Y8lKnG#J^N|)Z1(quro7^mw2!o}wC}aAwQsZ^ zv9$Br1?>mzqV}csZQ`v~-;eg&lS_0f@m8F*#5MEgM)L);U_NKQWDa%s%=KokIbd!v z3+Bf5yCwCKy*NzZwx_zXR)ibF!{I0rML-o7kK+&Re_Sm#iC!`I(AMq|dqvx&Q8Wrh z4$IhP^clUzK_l1kXKXPFMxU|8C_b=t_9`971S;9*cBYU7%%E_XL*f!3LwPI&=x_SR zl~Zqo*3NA96)WfauCxX{dq2&&G3Cz8o!d}IfG_UnBZ0WM?|pB1=KP-XoO7O| zL`1l7dzak=Q2zh-c$9!ro99!pE&$_QgKfnBQ~3~AfFnZHc3E;XuC1`Lipi{^Y#5k6 z(gi_@>V0Kg0Q-g8Ie~K@q)LH^u<8m=F4uWzwax>pb&lBv%e4l|(C8M-EI9K3@E}LX$!FpHY{Q}s}18|ez){Y2^wI+{j*gUY>~E9-frS64S*g6dy>lvu&Ut~_fGSPonwfIcN$y(`-R)t<|eilUa7P= zxYp#pwK@k^TdX=RXpIFJ2$rF6HMo%zKoW2kX(PPf7o9~|QyKo?s&PIzH+CViU$~tK za0{YT4BxH@Cz>wLR2&{!tMbTVlUMB)xL|6*P!g&Mc0f&%AeK1PhXH!~2Vf$fP*S5cY^YRuUV*a23enDJy2s*b}8b%%#HTpn4d@N}a^({?d57(fB*6s~~l13d}up@Ghj^d<2qQ^E1N=FobL8>UAufc*lv6MCQz z^opx_x$5#{wa%lfO}@I;q^@0*QXaT{Fu4Mb@*}uJE|^5JnG$sS16>c#(_?oj1-8vS zb>RlyJ+%D-*oRb1!tdP&x}!BG8ZDk~DIQv^@K~+MV#^H)wxxukU)i)3XlYXviG=GY z?SK?jrQ2!JJ4g=`(8Ig5fMF;eKDmLrV>E0lxB&Lef$@k2F08mNFV<`xTeo? zp7P-6JsAZRS9rPFx&Zcf6*Y-e3xiIHY6=HRKHV>$fW0K2>~I(Duy*c0Z{M!5#Vb zHkuAEJ6=it@rupiMibZ3hJU``{JjT@BJDs&}I-$%`ZH&(s|A zG@^Ri3E=`J!lNrS>RKom=Ra?M_k%rA%<)=c6)nf%s0+{5Y@Vz(dA?z@T(ePtAzn#7 z18BdQ$`L__^dv$zB78jjQ%U%^IE~S90ptwOm!W0Ger1*RFTw&|t=qg-v)NnDUI6>~ zl32PTEV-J)O^0WyHcvM+M=CW$Yd}yMiYPQ*wNi@*O%X4m>m;BPd_2Umk{Fod^d%AH zW1yF=Iis90^+g!$$Gv+^1&)U2S8Er*es1yZ4%wTE2uGR@&sJMJU9)+yTE`ZztyO6u z>;Hg8dF6Pd4eT_E5sAbSjmi-q>cr!sVD3d((f}xZkUhNmGTdVvft-TBI#J^v?ifGs zdAoJ~f;-N;m4cQl%)6Qw>Mfpa*gRWpvhHXP+~W0kX9IBi1)X@rmN@T@+?Pf-BqiD- zo%BVg@buGGUxsb+Vz{Ut98@OoAy^7b5%HhVe-FR!X6@uvE2T$+TNm?QiS`vaY0rc48BD5t6Y@eqmfhy4>A#owaT5?m=7kL}i zmu^pFDOhoY7dD#QGEuky_P65}R4r>-v)~Gc8a6LD!V8reP22Sz;|it{OkeAE0`Nuw zmXrcR{wwj?;`!S0B# ztc6!wHZL@64%J#LJFW*%S1^>Xn)tmY;vc^{C1QJ=S~@|Tpeo6z9wpk6dLeYu7t+)h z-G#HCg8HJ)g8GvB`D-u@#ly!pa4sFgI*(fi-Z8L;ZQhCTFA-rw;7!}+CEMjtqs8%- zi>tNwSb-ek^_Upmmk~y9|Nbb@ga1ydDdA&8I=Oe+eQD}TATxHUlBQCgp?JFY--ZIi z_knS>cy!Evh@m{y#PF3QEvK%&bh}P0;EDAnCmJ?83fT)_9}4KviltICwC1FE(fj$P z&9SCKQv~hzyP5)~cwe$3L1O6xIf&%Rs2lt}$;)HFmrkubE%MXJ*FAnNp&}xQyirnk zIs@?ZWC%AD)EuFqT?9qZP!ueKlHq}W-PKeaq2am|41==e-LLhj^Yr9DnFipvJh|Sw z0QO;6V-UmFUCm35!cF*Y%Qvwv^xzY#;=o9 zfi(4{3kB(D^6K{ZF8sWcuX`7B!xi2;nq%KYfvZMx>@H`R$ygLk?{`*Q;mwB4!PN#| zn6L8qYJ&obqN#A?)ags~Zmk7N!Qal;_~=~uyru1L+oV$P0nNKEcUPM{*=!U)0%ctnp!;SdK&mu)QI{LO-{X&A(8A# zpB{DM33PDf)V1*GD`xmVca9CDaSQOJg(^S!!UC^UT1;k49Fd`uQc2M`MPOSg$KTz& zlaghgmy!MKgWX4YQwyIzcAQ6xCA4jmQHo%kSBDX#4P>_i=mCg3FN6@-KG<{&)b0Bu z@;jn{jGh=e1?Un5v=8)H@hJAE_Iqa9pptP%DdoQ@mw0$Y|UM> zCI0BzZT@?cn5|v%rV$K3XskOf-}Tf9{_bRj>5Li9uAVB;0||1&HTe5mcJSWO^HYK} z-VUJGwdS*x8jlxCSdK&1?@@6hKo5Y~p`?TnPdZf)Nh&Dq2uzyNqKS!pXQmZqwOtZ6Wpx0uLUv;-DgF0K|#<&Ei0X1xe%iwAUtpNuB{ zClCVN6&STl{@}W8eBE4`Wn0G;lz#N33zt#;mHy~T^StHkXFamlT$ews+dNmPl0$G) z$eyG{aXO>AM!Xr#@G;3KF5wJkSNbBlZ%`dHIIc&=8^K zdU?o5#^j1pmfI(b+%j2UXThRq8ff1}W(I7C`K%UzuC8wxqRD}&J|(Z z(NtZbY$!%E21d~K5DaYlf37<&AAR^JFIQR=O%+L6WdM~j-b$$i98ugfljpzPaM5|m z+s`W4MT9@B)Oe=aAn%vw({5U~ft#c+5(T;hkB!h5I_XUCGXdz^K$h6gMb#HAz2(P{ z>ZI^=kDhq4s4Y-)g@)^y7;Vd0Ts@lOj>!W1CJJ0!$n=zAU#hnF#FNK4R(EhkxMnQR zKfidK#g@Y(D-8};TCCYFO)caM#Z=DZ#_H=qccjC8S-Vy2Sq; zy)Vk=gNp=G6rY679^9wIlTAfDS+M2`ErBu1NJ=K>a zT}h}f?F%7ZB~8*7ov559>5J~r-P^q{N<8-J_yDZCUPRxLv$%F7$4wIjZW=FealwiW zDQiiwa#ZbvN&A#Q;hhW_1;6*^8b9*fJhNF3pbFK|CLC~S3(wWSG!>uUzmvTqx$xpO z6uew%@!<#G^q;36ebHV2%>t@g`2NeM`Qbg&7r-7y^`^}qtX0T}M)#mE-DSqX)RzwW z61gu9{yy|2T5*j;IwcB?rlmb|QCozPX>e&F%S~f>Zl1_<^=OVU%Oq<4r6u7Z;{z{x z-?HtZP)uYDlEz^*SMv`ZI>M2vP0lYMb=29j1cse(E@X zc67ZXy7%Jgq+D1UVOKH77jL_Sj8f;7&BhroDh2QTP_r4gk4>d7$p|X(|3+f!OpK=| zC`gi)#CUd}Drxi-H+*T(0qU+$cU@30leO44p5^B80{h1DTvp5yom2~cw@Q5TM#5_P zrAmtfD|PN&tn+-Oi5B5owvO=sTt0>6`#-t@MbqGa-BRY?ytGWwG(x?+8~<=b7_$r> zS+4Ovk8kiT+eTxp1OIUADF5TwdLI==dPIEhv-D1iCPy1CmlmvuzuzVVdoZy4mhE#SGVv=D2yhz%?T| zCNlA2iW{K{;YpB)-W!hQiM0mzF4cKxrOu0$78OU6H57UOvH$kvB?_j&4_-DI@_B%d z%$51g*H+Vko&rwo%bb67WSwu>HX6IoZkjBxyOiNb%?VG6&ZyoU=)uhtXo~RgN}Wp! zBj**3IVyjz#ZIKq~?n&b#+W-R8m7I`=Krd3?3Ov4)MKA#W)1hGHgb zcnPUrWZIgw_`@S>e86dzTSlVEtMhYp1*=pA{3%I`J= zXlmiPN{a)_H6B>5@!Up>CELXmD42?I(*RT9`WLLDBfabbmH{Ul4hNQNe00l52x?Qo zM`p|1vsh=$?`rHKpTuZb)z(%dA`CIPHv#^^nWW>SQ;dvcRp`;Av&4|)_ky^CpqfbiR?Na>y*6k6O8cHmCwOS3&fgrbuq|ieqGCZqg}S}+FJ8eG?e(`SyxtfEtq#&JLq+wr zsst8_FDt$l%Oj{vwU&?&d5Su56tV1=KG#L!CxHTpx`IED$*0v5~1mMZ54S_F_|?OF%)$T z3oVzrs~NXUzw$aHeUbDA14}6m+_{@A=ZP2OS$mZ`41=6u(sG@+T0(OAM^XowlVfdZ zTDgHY1TtoteEW`ZJ~CONV5)fZ4!E~!KbH{!xubNPz;HVSmbQ!Vhh9@mhHWg2;Hu=M&>wIG8SXk~CV8k-`57%zxqg%@S@zHe- zS6WmZ?{$_kipz6Zt{Tg+x18nbu{@U+Gvp0LOW=*V&7U1x=MUdpqp5|W>AertM`rBB zbTYtd3l3EpY{{25cVq|n{k1C3)EZcbZ%#xgxy1)7x-@x7Jc>uv7lp8DYi=CL^Qj%< z%w(;$x44RH^2y5Os;zmd(%^yB29K^ac)ey*)0!-bydS*-&Ky)9Fcwlx53cI%maBskB1hQ)>J2`(S|uZGum=rW@fO~cCa+{t8v(z8 zu;OSwGFjpWc1(mI2|(>804@S=Hf$bSZ}8w+oo6;$oM<_?8uCh!RSKorU6*OY7$JD| zkb~3GnUwZ&@-h`v9L?3^dH(cRdkRnY3OJg% zti|_jANM|fzk4t`@$@%~RX+K`3BL2tNj`gYjpsI6C=n(sgAvQX#4C=tuGdy8DMm-# zoV-Xz>$6})>IjTy3?5jh@sWp*aQ9+0HlY->`z9xrOka61l?QBZ>Z$!JEuY_geCIg3 zM{?9%oq*?1etKBSYaMuMsXhSte-|gH0D043Pd38?%@$UoN*d+q9bIIvnLXDAAWXFcKi-IWeqEbJ1X>@Yqe&C;$DzwN&7AW7EY zikW`&r33KV1N>Vxn}7V+F>aeK^37XExN)Mu*1R|26x=F~W})fua<#=%YYkqiws>K^ ziHPEpmyGj?i^pS=PG!sB8|KRVyO+G494E!#y! zlUKr@F+k^r*dTRgAYt|yBsPtgfR?}qYc^d;N8p(voZZ?VRg=BtcmO}6AL zN|q6tOl{aMi!GN8TVso7dy!M##qhmPouFVUzIDf#-(=>&@$0vg`JGpneHt?WPw%Aj zxOl@P2Nu>@ZOt*BH8yW#2e>Gk0cyB@NxU0R^1V99?df5m*NBZqJUpL z;+4mzG6qfEDGw$A9kaqn@ZAoBqiFO_`{Ml*X2TIm8G}!38{^`<#p?~5hn5>0t~AM@ zD0ges?IlV2f0liX+!f(i&1)>fZ;3F0jG-9Mm|o9$Bq7xz{HNDf_|_d`vHqT`M)O=+ z%JO=(Mb7BCPmB~n8ASD%fy^tHyHt&Q){lEM&rq zvFQq5TB!1CudZ-#wa!Gw3}-~U1HF?{1VbzUS*6HXN!sOI2+?cF79nqXbFHsbTU=Jk zhM#-RG}t#;;Dz-D1z+*$NncX0MZcFG&0~uV_K%(0#;0ff+%c8neT5vYX!Mpu=OR&X zONVW=jsR@d?`u(!`yO3yP<6FeIMQ)$o#qmj{ImU?>cP~P#Kq(I!xq1?cb1)njJJ}; zx9iKAijQn5^ZETd_}MFG(0C(s-3>zf8<7f~bGY3eY$>m4?cwEG8(D=F$rn#_pL(<`oK-$b7Gl(V?vC8$=Ulb7 zBRT%^#_jy0oue$+-kM3J=-Hz_NYL+=RBG`$AP+9rShL-*IHd(z0kY>_EAG|c_HT~Riv~6MAVlQt{oU7x!Ut%N#2Sf=TwP)o6dEO>2&7Q z)R$0#1IGNJkKpdbmdkfOeS(Kq>M`RYEnZRU*WWwG&s{Ois;d#*`9%!BFd#jf6hHDF zYd9QSsfR!h-t!&PC2XCXo$W_oIxkAY5x%rmJ*VaNw@oX0Z!U|g6e`-vn__bpT^OKa zv!@;pUbJ<`hri9w)B#Wy@iGdEzw7FVXKDM}2KL=K6G6olmk3W!7I#vu<$zZryfFAo_m;9`{lSDJ9}gUj!YV1r1UAK7 zy@$-$i9f+WsQJfM@R}wwCVR?Ra=_2NxXAZCbJF*0^hfdhmWKar>j-~*{Wda&qV9TS z{!`!fn2u77kT(nJUcb}ycuhd%MhZ}rhmlv~a%~>?G?z~VW?nXdz+q863U%Ko&N&$j_ z;*EymgWVhEunfhnQii$~wq{NK=*T)Bf8s4xZ8tO^wUp5Q?GK{&;_2O2E@3L6rG;nL>YE$vB0|nIxT=t+ z<%jR6cVfG8Dk&F2+sP}d6tC4Cjzyxo>sJ}CDQ82QxQ;5Rq-~q-`&y=u~H={cYeTn{VM|-)!UmxGt++YtTYA(;4;V~^K3JfIw z9&ZdYm14zld9um%@nf89I$^@C z{)s$W3mKZOpaxg`pbksK42XS^RgHU@<*>Ps?WcU{-mAz4aH?Kn|)c(Z25c>9$jd3KdDAw{r7n9LX) zT5s~LkF^0G=f5Def5?|3-2ICr2S_}wFpI1@ziRa`GynonViU4+%R77dAsVX zzQpg2NUgwnL-9hrx{1N=fBVy>!Ja|}Taq1xDXUyWcZ~75;@LDmRcTU*L_PtwJ!!nbMg`(qYO?q93BfL$adm?=S(>?K0Otn2j8=!T@Eg`HZj;k z-l({?oWu4lFFIyZyY;n476xQc9B;ZjS82sGdw~68d0K9}8#1jp)Imk0x6C-mEU9Sk zdVX}JGdcUlNuz-KN0yzU-t+jaBPYBx_S@HhSye~nj z#Fryyph}vaB#O`5>#pX$rCJ9RKje28ruEpgv@n}B`S-7{^3}CQ2=w6gWAB-voH4x9 zYB-%Fm3A3QQFAo+_!6X!oQ-$Pl*svmCd1H|SR$+i?YH~}vh$_{dvH2lU&>)eDTTzl z4xvj^7ld7W8;}EuQyvE-el9V9( zo*t2e#sPAs;>*WZsW|RAQ=fyxp;835rQTB!bWn; zWldT-p}usXFQ=luq|K6Qzo-4#^(Kca&5%a9B9ttHJ7-E%9i7?^;A$Apn0$G@!hb!w z=66W8%lF@P=_FT;<*7OvLk(wHm;7f>f;4z$wZV~E%U}8EFF>#i_D>aK>QP_%k`gv7 zrFf&#WFRA*t1X;hqbCo6wt%zof*OT(Ta?X<=RbtrU zl<3Rgt+tj@tUH>&o3BNZXAd<0Wak*AjM;T8&lRC;8vN2ri!3&sm=#9BH2CQ&X0ZJ! z-eGup+6%W(EVW!7@+ntz4)wN~ayaTZ(3_Ud6Vd?0fJcv4H#gV?ui|*^NZywqyYD+n z_O?f>mKg))TP_FJ+tt+I%ilj;A=*5 zeB$D9R=Oy#oWmNKDBFt-6Pqyyq6I3y$Y+z@4vaqqbv1xvZ{h) zb(+bXMN3Ox%Zgqjc3Qj(>#^lJRmTk#Edh2Gv%GJ-EkViv<~SXrdVVhO-l6b%X1Om;q-p+LLB&mCF_U55?-=Pxd1`M-Bhv1)5f)IjP> zx4@q>6>rp9Jn82>-pp*9$~41eX#IG)q)Pv$g7uceGv|;5`Hq2o!ZNsYEE8EJlUUc7 z0D9~=)!-^m{dlA9@N6ZnZ3Emm=1Gu7vQPe0oxJoAG|f<~G+jP(cqx2cL6rXHts{JB zw(J2`rEzx{wM_0?tnnY;SPkzB9{Yb?GQka#1+NID1_iowyncnErGZ+c0(%Z%M-Mg!Z8tWq5Ck76T2W!^WEV|&gDhp|u9h;*Q;Jn8v>1$_GOBF#t( zfh%yuNRIEibb?jejcwzLUd%be;8?xIzd5`ROIBTh+ontWqn%@{wj$d5fL0hWx@9Py zUu$wWG0>4Q6#J)(G_($*eK(o0BVmes3I_1fdXqOQ=SE}LJL*?q<0t0rGvZSp&>t%R>Ph?M_w z*92FN<>K9&5{>Sf$*jSDKeERCOVtqQ!DD~qJu}P|GBmaB9W}f4`iQ?3+jeL1sEC)TT*8|)%N$uQVAQKaR6Vew_Y32+9pqzTGnDaESohCS|4 zz}!Arh|!0%k-DDUj6(xmI_SuF#^5(!Tj8}@JXtN820y-MCY1QnibKK0`?H4@aDqM{ zUz6XOxA@fM(^MSYW#4RnCocg^6!-YNT}3=)Zk#HywU7zFn_lWmH_|j<)rrR^I=`s!+5P2UqGW`-3C_NT+fZS5FkEMFu*0sV`~2+x2TIUs+gZt>wPsy!{=dG2UCh zvYZPg$TamOW<;7|RO*ViBk0NXW;o0oJmjK6hWC`)108gg8Bfhez3Y(?`SBoG{qwgf zeBopzcM|y4aKp# z&0{OI5awq*>q=1g>~OEE4p z#_Tp?3XNx!no)}54JVuw(vg9VTP6xLwWr)jSFz&s)R%sD1lJ-zqyy8=oy z4==ag@pj*H!ae@_kt~iP(efL&!bk!&s;;;Q1*Ld+wGm2mp=LaiV`pMgsF(UO6wqaG z=sSKZ!0)`a%%Ms%9L8|{$o-@5nNHSZI-b`ugwMP~oSb2B|H)O3zr(Eq=LFa<%V!xg6?Sr5 zNJ+|6f4l=Sa0Sl!21SKzLh z5+B_w#8M-ht}rh>Zd@YGuK0@$Z>27Al-cArd&lUF>t>v&;Ex4*FampjYT zw@wy1WJXS#r^|p*Um{1k7v2BnM1}u(d?V!D0d@J|J=1K-XTq_<?~~nYzuJBO zJIve9nI$+kj^#XAEk%7vI(emi<{HQ8AOWxwD(JVEsJF9{WX|X&6PfxqK8cL?Y;f*H0`tnjNSrxoS zubZcetaVXm0vnFczqQU^zO^1X9lUq^Lwjb}Gm;D4$$DeG{r?$5G2d`_WT_gyrqEMn zZUn8PI3J9@MDH<7gE!{ac)jUd0Q)6{EL$xjoJ}3X3M0zrwFs603oVz&*BWij*niON zlf}><{!FSb!}zF60grHCN44q5%~M7G$wgyrN3cXP z9Y~pn)*Jll;l;@5;QiY_+%d+7w~nw9qcgo32bPlNq~y27$z?4DMc7Wx4LYBMS@8 z4I1OW@vF)?ECn{{gVt@%k^(SkR~wJiZJtV)6`6)&|3n}`s?%0qhF$}c@?ecnFciOZ zXpxnc8>1t$Ig5Y2XNGl0Qlhi?lAh-Q^XcbLCX!XyJDTS^cTce#X(R48VrM9!=IU@Z zRY$6+x6hXSbv`l_eNnMNY+Pwz#?wC^17Qnksi#FAC-QUCO!aqgTA8ogAUy!(AY6B(1gKe54| z99ivP}xXw&9tz94*Yy#iK2bMb}$m_@RY>n9CoeupTCLf7ES=nl95 zT8|~G;5B-6xOByqmw8o3RC!^e#j902 z{KB+&Mnu<)=EMC#XPdsrfLBgno%d-!Syho__1f_Q-+oEp2u5c=mJiU^H=6wNiwiL+ zQww}#>nPu}V~mwniv6`j)qn41DaBIL;emxp2V`%b8$nd31UYCgj;FT^kE~QU`VLGl zol`UhJTv|YrErptyF;vNG?ik_)*OgbQ-jCcF<-}0R)Si_2A75MM;7Y9{S^bHq?ae8z7_mfl3K%SU1=!EI@t+GuWWunRC{8GOx17B|p-<1{s*>1p zF0}_bLQk2=0wZDD>Tozey)AaVZ!SW~^eU@yM=%e_KXSzk)z~0K{FfOD99Q_6=T3H< z4wsd4eD9uVRy&p(bn52bK!?HO%Qe<*Z`d2(JAPX+%e4~)>aN>$vtI8=?{9KaaQBJz zkiKq8um_*t*Uc7b>UQfur%!bH2^!_?1yk|WwI(YqH#Ug9yPRe3NH)F-@l4Z~L8hRR z{8k`u8oXI)@=Kw1-kwVO*!FQgyrtdfm2C0~d};2PukeR&taO|X-@SX9_fPt5#B`79 zUct)X^;(mI%k2#v0mr_5Zp3$6Q+?H!&d)&n)@0$~r3Q7|d0UN7&p8?cu(zB=DbpLo zPP4C0s*ci`?0Q?BZ4O4Nslhzz%@f5?Gd}w=qaH}-4(UkNk$BWPz+{MAEWT zAuFf9t}*!r0_jw1;b&er86Npj?fiRodD^*GS?#Ap?dPgirM;{Jt-zwjZtB-kJ5aN?V+y91q$owCZm0v}thZf>xPK+#g%Fp|N> z2&<{mg+zBA!;ylGsd#9mj-%sW*wtfscBc7|55m)jD3K4m1||`mM=~b&FIM^e*Ow!7 z#QXQ}xqON%$MVz?Q%&LNAi_k}sb6e}wW%^zH;bSdB@%P8p-d5gz&VfAuSkpS2NKl$7V z?=$ps7Dt4OOIiN)6>}c<@6cc478@HaHM#G^U}b)T=xnY%7;d4E8O#4DTDw;rP2bhc+VhFOsZ+1552{Y7j+k zn85M{Dt9)l!O| z`kJkz?X#AM1X(Z@Pb}A1XxK4NnTedmwUec=J91F^lKfpR@X)dKxD>bP!0tX}hrPHzoN^vYHK6a4h^^C52!wDaBN z9N&NWv^SMxsE#JDz!PSn?(p-^p9s0J>Hqw#Gi5%0$t24yzcQN;j{?=?c)i7=DFYoJ z+%n?HkLA{{G}fcemh80e1hxzil*+Mxjo_M?GV}Qq$$ZNHsP1Y;T<` z`W7Ci4s;o?O4{)p{b{c6S!wdfQkCC(bvcaKp>}@R6xU4_sI<~|mpa-@R{!LU6~26O z14F6U&;RJ&Id&AYG+j-KxZ_EXBHVpqBXXbjvmcl#F_G=IHb?rUFySJ&Cs(Sxx#4VL zu=}@Ng$$EsdtpHex$Wzt9yF=O2Ri(VX{tyeZ9w^y#78;RAME*t0!Of4KD@xOdYq2r zOoN};JJ&fulvF;H@N>@|_m?)PP~q89$nZmZXW4N4nnTJ4z`UV2uvqn%8Mg;I_KX(T zJC^sCjrHN_UEa8%00*92Tix7X7huvdxPG#T;}2pdSI`XN$_SKPJphYNICXX%G_^p=~ zV#%ri-@a>_+vi4DZzZRi#P^gjcy+zOv#Yh31PO4`uVDN1n{}J7uE(pXSwpdZvOrDiPWkJM^7MY+y`;6NMCgcSB4hF= zM^?FeKANZR-Eg_)1~7s*Ixv8|NJ_Yx50JboTf1V?-|XLQ(()fJ9&94Rp8#GdIywl zn<}DEU6%h2Dl_(5*EsmTqptFu@=mv(dww1}VPCzonB@oFGs}j*_C0M4OvZ2B{q(aZ z!e198yEjgi`1V~>{^`(3Wh_I5U65YR<2~roa@OPR+S|T2Fqw-45({{v-sB~Jp~R*J z`$c(+OLA71-JF9(Qg3CW;(njE2fP*F#)$%3@+P+KZavqVrwhZz@j>WI$9wRT)kl|W z{PxR>9dzV7FPq}V=^`7UeYNh8GAFVYe|~h8&mUV0Rhb}J{a07av8|M)spC?wD^Riw z9$TvL#B$XyCVBsOaH;02If8Wc;^}?r83y1853f`&fIV+0_DvPAgBP1rUk0$k(EdQj ziyJKtNBXF>KkIq*Sl;uLQJlRhsSHnFl1~^6S^vi2MP946LZFLpa{7~Bv(+C~NpJEJ zgp2{7e(rdtR0JPpDKC1BBF+*F9V(~ zJ+@8vqc0uGilJC&IAO9HwXeQ)ro_iDnPAy=;`Y@MBdM}!@XTtR-#R=WCa}Rd_wh@n zn9XId-K2e;P%sQ$t~B}R-7j(L7Z33r55LAnOJf-+=Ud-(H1W0$3wUI)%6z?f0qnbq zS<0rz+fP~T6OSL>w^$8%e5e^u7E)~|2Ic8JkAMNTO?Upa`ww~Axxwd-tn#-f);s9P zj|AFzbUjRjpE)9oWeon~D@);!i1I;qNjb;uvm?HBwu*@bS|D#KN|s5(6~;0amLWZ& z@qogtK*m(8*6TdIW)J_xzT3b)n>Dz4JnQ#j4^CgAAE*|gXeypuYqAs!bol>##YmPt zX#-vQ@$_!dUdLD#56rJ6w7AU%cAt;lGEu-q`K|*DZG{n( z&yP189t~Ei&^GnId9wQ|#CMv$B!9NOiLcIYq-y6=+&(kHYAeMN%$FQZgK*(*^pv}9 zq8K^ldNq0JfiKd2&cCjPtP;L_a-*v$)aC%Y|6P6UR5@G(CVB{T7d%bpa%g;UMY3D9NQ1!oRyufyU zPipFLw6CxPg{0sidDA2tv{3YE^6Dou_EM&je=(IpS}hJPH#Rre1sJtVZk)*C=yvPC z09k2AK+PM9SE@G8LdW(Y}`t&YiwxE4b$b25(O_|wdGK4IT$IcZc=%AUumg-#Vop8>fVzZ zthJo8lA>CiN4J|MOPIzIeY+@ngoVJ*@V>=5-+vk5A}@fOr;3dGW*VQp=_0x|A%#b5zjrz|nLyGx-ev^pYt*;ssSv#nUS_mYXgU zK5y^Ov4%+6b?+0x1m3FKJilDwrY+-}m$L(G&zbBh^llyK$F{XU(DBTAlQ(O2jGyl+ zWw|ogTA|LM`Xa+ebs2yIM|=D&6yv7BiH41!DOp~#SUf=o%YYTz;fKCv3p-2M*f2N1 zU;2(@10|)yEQyu=_Zte7);zp;c3KC{E3jvk;?4oR8$#r{GPd-1;r+_g;x|RL93Bb> zIwXu@w@wvlhLiI{XzoMM7a8=*=^06-Lr!%4q%|l41rIZn_nwu46Lp(^b;TV2;_}(h zgejPBeR`$F-yUCMBvf%uM}6tP_B~BuGDNud#Ku`^9XPMR4sh3G9tYLqh4-)Ad8-d4 zB0;wO3~!tAmIUjRZ%BsTIwq%7CGAIFQk$++Jl>m1Vfs?*hOIf#bXd1te(LIN{K7Rm z!Zw4b7V|StAM2be9YTGPfq1%fJ$p6YMfvz*mE*NDzcbZ3-@ty^Xr2iRn(lCkUOcH2 zN~XbMt93RU&4^`$qk8Wf%X4ul%duLMtf2;kPd~mMJ2e*vH3Q&#+8KwBQ&mQM*kF7Hu?oJwBed+&lrC${CR(!e+Pb}8h zR?2Q}u+L@;-dE1?@LB__4<(QuTsLPZj@E3RSgmvWbSYd@Q?d+hoXGQEHX7vpO%rby zPw!7(qATn@Wk~_H2o+n?)S7~+*j3K4Z>r2)b7gLtDKnSPv~$FWC5OfX`oDi^p3gk{ z7L!S!_pFi*puP-9Uy_N1cHp&oAPvhI^OZU8zz%TNT!{m#4RQ)^KJ^rwyc7ys3->P8 zxP7`5d643c=@NhR=4ubtK2)#5z&yPleKC~w2TeRhrDnTm1(R8eo2N^BV7APib0b_c zULa>0F%d+Imxrjx(6xo)m!Chur=L1TIb+6o^6B#QZuF(!xS~MLfT!j+;EK7;9ofNK z7vr)N_p}}iX)y^hZx}qVR11&KK*QfRSztP6Vfz~^hKs{8m|8gieNnzF=rvwxs*ccb zT`WU!Q6b9>TZ`PeWrPpRl(@K^|K=;z$IV%*!%(d30nlPgDZ;iI+}GF&~D z=e~ssWy?q}Fr6j!C7>q&Emu=_HICMlG6q+T7r1q%#2vFEynmv|Xx5DVbYK^#lnO@; z#gF1a(phmdUpThPPd;^wx2i2>a+yv$i~$cWITQ3Hb&gUPxAL*XdsBj41WKmKjgwja z;;2ndNvy~u1IFJV>TR@K9$2h#f}*6rk%tkUb!O@2$Rc`z#z6-Zxte>E@;dd+=S~F;(VsM=L1? zZiAhk!G6_@Qrxpx)}Xell!=eT9M z#GSJv?3*kyowqu)=rno~1-u};qG4W3+WaI)brm$$+b_^Poy@1H2} z&|;O*j1ev}I92t<2!JSFUrNn&5nwuNa@}l+J7&tH^rq7p@fh#xp##UMH(S z%V1Hx^W@f;z@NXh!N<0bMW_IL%!ukh4LjT3bn@s|e_EkzmA zlNz<)Br2_{8~r$?DrtXJVDKp|Z?>SRg?(GfeDvb!vqOPJ-hu6_XXtj%N|kSW>)A~yjE-Ry-yzJ{)Gybp~x9+fVNz3V!vn^>?vot zX{y8>vt@3aEHIh1;@Ytvy$wGR){);IeT}bHn>?^k;Y%kscw)KETeTLp7V@T|V0w{W z1KO4JrUe;%FKJYLk+Ti-l&{8A-ig&x@cFOZ%gu8m{`wGg0qoI_P640z>T&LwuaY53 zWn=(NxKoL!YvF(FzlfWsiedjq0C-yqe|dbJzj|wfSE?zpU4;zqpD1wKOo_dt z`54zsN`p!69o(vp=9$$xUzy+FzWEBzuQga{c}u(Y9V^nqq#pJ(E(ycQg^>!w6*4f1Q zy94Y`FE#mwCytWSf*vR-?QTSBDaDHIa_3x$|8o6KvW5x)FCnj1v9*e>e}Sn;^bVqV zl%v~Pcy6uE{R>s@nXmBG)jEq!Z~V-Q-kx%!eXHnhMT34(T?R@B&p=e~LthklCaFqr zv{xu{1Y9LtRLt|??Ir%%l{>g{Eblja!P(w5bKZl!u7wXBIKrFt7REr^rh5-gTMCw1 zE_cqB`MvA5Gm$kzt=s!LQha4dPcE064v($Wxcg*<1B*3Y-e^#Bw3l3(hSx$IMsM9i z-yRI;!_t@GczWWOq}*>PFht?_7p$#4;b2@DT(hmjUE9X_y4f)<8qHJkYYBqy0Q@#= z%sa2a9-MlgdiDf=cC^ybIxq-*Ndu0h6ss+l9mOm^e&r0`xOH?O-PaZ2l}eNQ7i-)* zzrn$kI>+k{uJ$%qPKI?1AS=qC5?`Bqcy<<6ksax`*J+FRV%PG z?3pNV$F@;Ev}Ke%Q)Ol{R?Jc_d_!;JI`{KFvIBf!zQT7LJi&-1*m`*Sk_H}ALERN< zu5i^@p0C?dX8%NiZN&^zmRF73(85B);mul$Cs*nmT&eN=dXqKV^Eoe?23h6p^l;O_ zC#M7S(^Fqgg{OO6nBM;!?KPfhyngz*OqN@=jdI)cIJaC>VrM=}&QP7trR0qEyKFkJ zA8$D9zwdRjB4O-49iWG&hF>JAxn6nSLQymgN~ZU9)iu;zO~Z9jz7<5lPg;ZMt-B@7 z5r=>?OJB|`PdAlU@NvWgxa%rfXo{xE_0vT@uw|5w>=@_La*h$pOb^^=Iq@9yX7Mh# z&E-t4AIo!arA|hXVzn`x6-IOv&qXMhP%8#sAa zfZb4v{Zn}!U8$2(o>693AP+ra=j-K>egre65<195)S1@yqAvry-eGupmlD1T5=(y} znXSQfyw^6BGr3{=2;a1IoU3O`TvEuBGdo|PC?eLVysbQZ(}5k}w&^mz{_@IMFd~&R zVno`HAIBr$U_5=`ODpkWOn*OwfMI$1Q0pbTZxL1qmR}rm1Z=I?aJ~C? zj~2OMw!lZXPjbyff!TbfqkSRDyU#UhvgyDs2%{O3o5%8eVZkPc->h|7^rc^q`l$k4 zhV(QaB(QhB0TXXIm!thW!}ZWQowIn~bdhh`KF$r(-t1~Ikyu7QtZ=SXo&Pv_SAab@ zNN?I&=C4m~U|13xP#8pg83b^H(3e3j%znR6`edCQJe_vMQVTc=nvTYGJZiFMI>$%1 zPI1$0i7UnmOj<^4I9npzL@9O7a_$RYkLUpVM{}4;aP+CEFN5)PPK&+_^rX7~g{gw* zt-NB6s})V<6$rK)8TRiQ=Y!K_ZkQ>uBWqz9NpQFGVRb%plg$Tq?@PKQZ*uuqo>$iD zVL!_$uWRgzNjXKJr_h(SR@XnTkTx~3eQ(FA);JobvRSU$n&Tf#k8#UniQSX!a(wbN zD%z$)?`~+nixX5q$Xf>cCW;(ht$D`$!{wfX$Qq|{@(d_5CZo5}_AjrCVB1ho2A51^ z_|V)qcWo`RXC%i=Hj_e4f~XzUy*C@g--Tcgc>9N@vizqvEUzSTrj1C?WKycqdAjNU zldHhhnx-QtF_WIJl$AN!&7S^8)-`3HIQ2`B;&nAvo?Z ztUXpSSJC^}GRXn#Zk6t+ce|uNcE7ws(Pu0x(%KPxk z!v+T=gfJ2c2q`84ynq!(5rSXB9`SpCAHbGn#D)!s1!92^5<~&= z(6bIq3+vL<6|$oySERZ(q3Y4U{`lw#zWeLHPTflzsxMovVmC)$vLu)rv2&ew0t;h* z3A}M}jJH00iZ|~boFDjfytJ9gc6i=-_H|tR%V!7p{_Ruz@*j_KVc0VUdP(S% zrCu(R;G;!e>+s&mAs*hng>T+D!u^v&+&x;ZNyp!ujOEU=Kd0B9A7A3(uigR4(@Z90 z+vL%_`m%sbQ%UFlo``^jaDK@6`lpWZ)~l!Z#_c0~@oW!Ai$!;h+={kC^$zS;_U^eG z<`;keH-7ZH_i)MyPle)ok`29j$yTnW4xLNM743UV;PqPv_~B>H@aDxU_{!-aE{4=Z zZfq)k2lf|EXb3-h=Y9O-5C6hJy2Kunoo-{~)ujcNki5FbWl9jqliPjvWFOzVbA|_Z z4sieU1fM(h>G+RVhtBSG_C6;1ZA10zv?%gfE@!fqRu@Us8u0Bwu9lmeC zi(i^w>gG%oQR#Ohz^K`>WNK<7;3e@1JI}s*?Or$G^#U@er4+R!dIWm0(IQ9t_oG)z zDX&&>Mg3Z@q^u{}{N($BtPtg&QPiTg?*wm(qZk#7 zE(T@x7yVQgqb7|WNOqohDzS;Vl>QExdt+Wq|Mu}UN5Hk)=Z)<;!G9&N+mh26KpO)X zdCXwNhHgYR`bpYAUIRIT>?M5U~>I zQ~+Exf9kErY^(pL$Y97BA9aQ}{@fos2W0yj*BF5IQqL3YoA2wrv`l5N#glpfYJiGc zIm0edDyrwy16|s7Y1cKQfO>C60rwqsbZjUZ-F3WIN738@@tU^<)5i%J%PEr-Df~AR zAY~9#ChKeMd1SzCTlOrlsW%P{=T-MI8jVs zz`hB%=ehM9;CrBoJD>quEbS1fD^eT4iC6%mH9SD?pU$J@M*COHwF+28>@^y#YCulX z#{oUBk$}%{8eXagT>(XH>SqsZ>H%EW!d9N2%5s2ZFKL6&*Rdl(F%oZF)_C->?tbb$ z7O&4k(9d~!|Gp5mxga+L`#Pm!PQPxfLbmqM9)KQfqRwf)I{{qO{nqW;TcdoB?0$1T zdrmzHiRS!bjoaR%SZ7|BUTKq{QXn-6W~vWJfx~#~wtoolX>HZ|{c?`!nChL2ht}t* zb2&EfS|fnl-j5pjblhSH^0X-$2`tUg=j^~9#2jHvkWyOb!ow+rJVzBn3oIZ#MUgTg zegs@NKx>dy4{g1@{*1C@OSyC^09Qt#!H>2Lk_@f!=Pt?vx)Dm+3XA0LMa3eUOf6X( zs67w24b{ziaw98Do#!sb(V9Xi_q8ez6LaT{o42<{V7>gnXsY2+H5d(8W0b_sC>%Xf zBSE--3(h|a_L0oyjX-RqXgqLqOz)xX=ZKONBk5?0G{AOVeOku6dS*t+eenrhtn>jKN22J)(_~ z7H)uUDGV7vHL&Ge4>u}83iR1Bs-X}Qn7E$bdU|z^0(wPT@+sH=AL@FfKy17{yfHXq zR53=2FZAz>r|z>#ku`wjoMIBNmr&a_+*PFej)*mmp1(_@91utnB_%eB(Ktk2;X-@t z(psIt8hB3e1*A0)%^()6Ktwr7mr?^gO45kIlprY^i4cQHGHIo}@QV&mr3SUROK_TUV=7+bGRg^oIUywmFIrNsJ+=gzK$#?g!3{V{ zIu#-?sn|(T6r(oPYDYjQl73Z%PNZ|MNSRc_4mkHy-^M2>^!_uEu1_^nR!f4 zg6r>w5LgAUtb$J_uJoe3hoV1HIk^DBlLsfew2ZoSA}5n-ty}*Fs<~d3GtjjeE|(An zpMiWu0|9qa&fZfM1X)VEJqk=?iP3ZR!i^iRx7(U*sMQrwl%a8WsGu@c4?pDWMedX| zy-zj1X!{B+azSZToop^35M~EM<%q9qYGZ-aIC(LK#<6W+O4$e|Ff&_2VOq2RkXZ%3 zj-zu4G`$Q-fuduLMsstk;#j)IF4!0QC3=x(+MX*60RU-6#BGKvSNntq8NYuCr#keqnkYGrr_rwlYfMcqXBFI`sV^E}Woo^=PX&am0m*ioQdYF4 zU#`uhlSyRMq)v^A0#jNCS_))IboD=5sWDz>Cu~xH6$PH;5<@+Q?d8X@{vK=WS{1R| zT7cX1_b} zfn4S__NaU|0&=Xs+qxE$EOH?3<;>(VpsG={QjEHDhThlV2 z{y49vk20zQs)C-^i(s=tu+57gC7l1fuO^Ui1h8J)sWnAiNfy|11z&M|5RDf7J!sEd z>e_vL(ynLaLt82^E=7)(gp>h87gOmvmm;5N-k<=>b6j0NV~j#k0_ygU6FWMv?25^P z%xVPSx#~+Za?NPyzQlZ^+L-^ZoL7T&n?QF=g_?2`XJk81+a<1|G6h@t7$t(FtAnRu z$petNzwWfsN{c9sv&^{RXS@?rnyYN0fH0}5-daasjbeUCM@JI`=thBYma+%3ImRhO zaTe1xh)};jq##ONiYp$0O**X1^{&>bw7&ahqb7}@aiaHkTAD24Jl^0v1 z2s)tV>e`zNh?kY#s`QZpy~Y5K3Pf7Gy+7c~tFzQ6|Yc<7S9;r&(dg~}hvu(@XbDLdY{be>^jk?ek zqWHUdiVPWyB+QGdf~1tNTCE&B5U43SbL;Ob=s>;FnAxPr*u#mD<1}zPe^2r7zFaot zd9_-pz)k{?_h8o2b5~9qOyt^k(`^RU>*_;&AyaAS?()$n&~J~&{!JQ9qE}K$CuXX; z@wNU=M`5;kXcDb5ea+7xr6&9@7K>;0ynX9j+`IhjCpg~C8?$@8G_U^wEsu}tC$$+( P00000NkvXXu0mjf%p3nA literal 0 HcmV?d00001 diff --git a/theme/theme.scss b/theme/theme.scss new file mode 100644 index 0000000..13dd2f2 --- /dev/null +++ b/theme/theme.scss @@ -0,0 +1,8 @@ +// Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. +// For licensing, see LICENSE.md or http://ckeditor.com/license + +blockquote { + border-left: solid 5px #CCC; + padding-left: 20px; + font-style: italic; +} From 86da33290aef8a5b0620083aae6989ff2ffbf446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Mon, 27 Mar 2017 15:05:34 +0200 Subject: [PATCH 2/3] The left margin doesn't seem to be necessary. --- theme/theme.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/theme/theme.scss b/theme/theme.scss index 13dd2f2..89c1756 100644 --- a/theme/theme.scss +++ b/theme/theme.scss @@ -4,5 +4,6 @@ blockquote { border-left: solid 5px #CCC; padding-left: 20px; + margin-left: 0; font-style: italic; } From 3d94a93d431a56d7bade37ef0d4d6ea418722218 Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Thu, 30 Mar 2017 09:00:41 +0200 Subject: [PATCH 3/3] Changed: used `first` util instead of manually obtaining first item from iterator. --- src/blockquotecommand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blockquotecommand.js b/src/blockquotecommand.js index 82be4d9..77b90b2 100644 --- a/src/blockquotecommand.js +++ b/src/blockquotecommand.js @@ -46,7 +46,7 @@ export default class BlockQuoteCommand extends Command { * Updates command's {@link #value} based on the current selection. */ refreshValue() { - const firstBlock = this.editor.document.selection.getSelectedBlocks().next().value; + const firstBlock = first( this.editor.document.selection.getSelectedBlocks() ); // In the current implementation, the block quote must be an immediate parent of a block element. this.value = !!( firstBlock && findQuote( firstBlock ) );