Skip to content

Commit

Permalink
Merge pull request #9440 from ckeditor/i/9402
Browse files Browse the repository at this point in the history
Fix (code-block): Markers created in/on code block element are now preserved after the document is loaded. Closes #9402.
  • Loading branch information
scofalik authored Apr 13, 2021
2 parents aefc6a2 + df84fe3 commit 2616f8b
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 62 deletions.
8 changes: 6 additions & 2 deletions packages/ckeditor5-code-block/src/codeblockediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
import {
modelToViewCodeBlockInsertion,
modelToDataViewSoftBreakInsertion,
dataViewToModelCodeBlockInsertion
dataViewToModelCodeBlockInsertion,
dataViewToModelTextNewlinesInsertion
} from './converters';

const DEFAULT_ELEMENT = 'paragraph';
Expand Down Expand Up @@ -85,6 +86,7 @@ export default class CodeBlockEditing extends Plugin {
const editor = this.editor;
const schema = editor.model.schema;
const model = editor.model;
const view = editor.editing.view;

const normalizedLanguagesDefs = getNormalizedAndLocalizedLanguageDefinitions( editor );

Expand Down Expand Up @@ -130,7 +132,9 @@ export default class CodeBlockEditing extends Plugin {
editor.editing.downcastDispatcher.on( 'insert:codeBlock', modelToViewCodeBlockInsertion( model, normalizedLanguagesDefs, true ) );
editor.data.downcastDispatcher.on( 'insert:codeBlock', modelToViewCodeBlockInsertion( model, normalizedLanguagesDefs ) );
editor.data.downcastDispatcher.on( 'insert:softBreak', modelToDataViewSoftBreakInsertion( model ), { priority: 'high' } );
editor.data.upcastDispatcher.on( 'element:pre', dataViewToModelCodeBlockInsertion( editor.editing.view, normalizedLanguagesDefs ) );

editor.data.upcastDispatcher.on( 'element:code', dataViewToModelCodeBlockInsertion( view, normalizedLanguagesDefs ) );
editor.data.upcastDispatcher.on( 'text', dataViewToModelTextNewlinesInsertion() );

// Intercept the clipboard input (paste) when the selection is anchored in the code block and force the clipboard
// data to be pasted as a single plain text. Otherwise, the code lines will split the code block and
Expand Down
90 changes: 68 additions & 22 deletions packages/ckeditor5-code-block/src/converters.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
* @module code-block/converters
*/

import {
rawSnippetTextToModelDocumentFragment,
getPropertyAssociation
} from './utils';
import { getPropertyAssociation } from './utils';

/**
* A model-to-view (both editing and data) converter for the `codeBlock` element.
Expand Down Expand Up @@ -120,11 +117,11 @@ export function modelToDataViewSoftBreakInsertion( model ) {
*
* Sample input:
*
* <pre><code class="language-javascript">foo();\nbar();</code></pre>
* <pre><code class="language-javascript">foo();bar();</code></pre>
*
* Sample output:
*
* <codeBlock language="javascript">foo();<softBreak></softBreak>bar();</codeBlock>
* <codeBlock language="javascript">foo();bar();</codeBlock>
*
* @param {module:engine/view/view~View} editingView
* @param {Array.<module:code-block/codeblock~CodeBlockLanguageDefinition>} languageDefs The normalized language
Expand All @@ -144,21 +141,26 @@ export function dataViewToModelCodeBlockInsertion( editingView, languageDefs ) {
const defaultLanguageName = languageDefs[ 0 ].language;

return ( evt, data, conversionApi ) => {
const viewItem = data.viewItem;
const viewChild = viewItem.getChild( 0 );
const viewCodeElement = data.viewItem;
const viewPreElement = viewCodeElement.parent;

if ( !viewPreElement || !viewPreElement.is( 'element', 'pre' ) ) {
return;
}

if ( !viewChild || !viewChild.is( 'element', 'code' ) ) {
// In case of nested code blocks we don't want to convert to another code block.
if ( data.modelCursor.findAncestor( 'codeBlock' ) ) {
return;
}

const { consumable, writer } = conversionApi;

if ( !consumable.test( viewItem, { name: true } ) || !consumable.test( viewChild, { name: true } ) ) {
if ( !consumable.test( viewCodeElement, { name: true } ) ) {
return;
}

const codeBlock = writer.createElement( 'codeBlock' );
const viewChildClasses = [ ...viewChild.getClassNames() ];
const viewChildClasses = [ ...viewCodeElement.getClassNames() ];

// As we're to associate each class with a model language, a lack of class (empty class) can be
// also associated with a language if the language definition was configured so. Pushing an empty
Expand All @@ -183,24 +185,68 @@ export function dataViewToModelCodeBlockInsertion( editingView, languageDefs ) {
writer.setAttribute( 'language', defaultLanguageName, codeBlock );
}

// HTML elements are invalid content for `<code>`.
// Read only text nodes.
const textData = [ ...editingView.createRangeIn( viewChild ) ]
.filter( current => current.type === 'text' )
.map( ( { item } ) => item.data )
.join( '' );
const fragment = rawSnippetTextToModelDocumentFragment( writer, textData );

writer.append( fragment, codeBlock );
conversionApi.convertChildren( viewCodeElement, codeBlock );

// Let's try to insert code block.
if ( !conversionApi.safeInsert( codeBlock, data.modelCursor ) ) {
return;
}

consumable.consume( viewItem, { name: true } );
consumable.consume( viewChild, { name: true } );
consumable.consume( viewCodeElement, { name: true } );

conversionApi.updateConversionResult( codeBlock, data );
};
}

/**
* A view-to-model converter for new line characters in `<pre>`.
*
* Sample input:
*
* <pre><code class="language-javascript">foo();\nbar();</code></pre>
*
* Sample output:
*
* <codeBlock language="javascript">foo();<softBreak></softBreak>bar();</codeBlock>
*
* @returns {Function} Returns a conversion callback.
*/
export function dataViewToModelTextNewlinesInsertion() {
return ( evt, data, { consumable, writer } ) => {
let position = data.modelCursor;

// When node is already converted then do nothing.
if ( !consumable.test( data.viewItem ) ) {
return;
}

// When not inside `codeBlock` then do nothing.
if ( !position.findAncestor( 'codeBlock' ) ) {
return;
}

consumable.consume( data.viewItem );

const text = data.viewItem.data;
const textLines = text.split( '\n' ).map( data => writer.createText( data ) );
const lastLine = textLines[ textLines.length - 1 ];

for ( const node of textLines ) {
writer.insert( node, position );
position = position.getShiftedBy( node.offsetSize );

if ( node !== lastLine ) {
const softBreak = writer.createElement( 'softBreak' );

writer.insert( softBreak, position );
position = writer.createPositionAfter( softBreak );
}
}

data.modelRange = writer.createRange(
data.modelCursor,
position
);
data.modelCursor = position;
};
}
37 changes: 0 additions & 37 deletions packages/ckeditor5-code-block/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,43 +99,6 @@ export function getLeadingWhiteSpaces( textNode ) {
return textNode.data.match( /^(\s*)/ )[ 0 ];
}

/**
* For plain text containing the code (a snippet), it returns a document fragment containing
* model text nodes separated by soft breaks (in place of new line characters "\n"), for instance:
*
* Input:
*
* "foo()\n
* bar()"
*
* Output:
*
* <DocumentFragment>
* "foo()"
* <softBreak></softBreak>
* "bar()"
* </DocumentFragment>
*
* @param {module:engine/model/writer~Writer} writer
* @param {String} text The raw code text to be converted.
* @returns {module:engine/model/documentfragment~DocumentFragment}
*/
export function rawSnippetTextToModelDocumentFragment( writer, text ) {
const fragment = writer.createDocumentFragment();
const textLines = text.split( '\n' ).map( data => writer.createText( data ) );
const lastLine = textLines[ textLines.length - 1 ];

for ( const node of textLines ) {
writer.append( node, fragment );

if ( node !== lastLine ) {
writer.appendElement( 'softBreak', fragment );
}
}

return fragment;
}

/**
* For plain text containing the code (a snippet), it returns a document fragment containing
* view text nodes separated by `<br>` elements (in place of new line characters "\n"), for instance:
Expand Down
Loading

0 comments on commit 2616f8b

Please sign in to comment.