@@ -12,11 +12,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
1212
1313import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element' ;
1414import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position' ;
15- import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range' ;
16- import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element' ;
17- import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range' ;
1815
19- import modelWriter from '@ckeditor/ckeditor5-engine/src/model/writer' ;
2016import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter' ;
2117import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter' ;
2218
@@ -43,6 +39,8 @@ export default class Paragraph extends Plugin {
4339 const data = editor . data ;
4440 const editing = editor . editing ;
4541
42+ editor . commands . set ( 'paragraph' , new ParagraphCommand ( editor ) ) ;
43+
4644 // Schema.
4745 doc . schema . registerItem ( 'paragraph' , '$block' ) ;
4846
@@ -56,25 +54,29 @@ export default class Paragraph extends Plugin {
5654 . fromElement ( 'p' )
5755 . toElement ( 'paragraph' ) ;
5856
59- // Autoparagraph text.
60- data . viewToModel . on ( 'text' , ( evt , data , consumable , conversionApi ) => {
61- autoparagraphText ( doc , evt , data , consumable , conversionApi ) ;
62- } , { priority : 'lowest' } ) ;
57+ // Content autoparagraphing. --------------------------------------------------
6358
64- // Post-fix potential subsequent paragraphs created by autoparagraphText().
65- data . viewToModel . on ( 'element' , mergeSubsequentParagraphs , { priority : 'lowest' } ) ;
66- data . viewToModel . on ( 'documentFragment' , mergeSubsequentParagraphs , { priority : 'lowest' } ) ;
59+ // Step 1.
60+ // "Second chance" converters for elements and texts which were not allowed in their original locations.
61+ // They check if this element/text could be converted if it was in a paragraph.
62+ // Forcefully converted items will be temporarily in an invalid context. It's going to be fixed in step 2.
6763
68- // Convert paragraph-like elements to paragraphs if they weren't consumed.
69- // It's a 'low' priority in order to hook in before the default 'element' converter
70- // which would then convert children before handling this element.
71- data . viewToModel . on ( 'element' , ( evt , data , consumable , conversionApi ) => {
72- autoparagraphParagraphLikeElements ( doc , evt , data , consumable , conversionApi ) ;
73- } , { priority : 'low' } ) ;
64+ // Executed after converter added by a feature, but before "default" to-model-fragment converter.
65+ data . viewToModel . on ( 'element' , convertAutoparagraphableItem , { priority : 'low' } ) ;
66+ // Executed after default text converter.
67+ data . viewToModel . on ( 'text' , convertAutoparagraphableItem , { priority : 'lowest' } ) ;
7468
75- editor . commands . set ( 'paragraph' , new ParagraphCommand ( editor ) ) ;
69+ // Step 2.
70+ // After an item is "forced" to be converted by `convertAutoparagraphableItem`, we need to actually take
71+ // care of adding the paragraph (assumed in `convertAutoparagraphableItem`) and wrap that item in it.
72+
73+ // Executed after all converters (even default ones).
74+ data . viewToModel . on ( 'element' , autoparagraphItems , { priority : 'lowest' } ) ;
75+ data . viewToModel . on ( 'documentFragment' , autoparagraphItems , { priority : 'lowest' } ) ;
76+
77+ // Empty roots autoparagraphing. -----------------------------------------------
7678
77- // Post-fixer that takes care of adding empty paragraph elements to empty roots.
79+ // Post-fixer which takes care of adding empty paragraph elements to empty roots.
7880 // Besides fixing content on #changesDone we also need to handle #dataReady because
7981 // if initial data is empty or setData() wasn't even called there will be no #change fired.
8082 doc . on ( 'change' , ( evt , type , changes , batch ) => findEmptyRoots ( doc , batch ) ) ;
@@ -133,108 +135,124 @@ Paragraph.paragraphLikeElements = new Set( [
133135 'td'
134136] ) ;
135137
136- const paragraphsToMerge = new WeakSet ( ) ;
137-
138- function autoparagraphText ( doc , evt , data , consumable , conversionApi ) {
139- // If text wasn't consumed by the default converter...
140- if ( ! consumable . test ( data . input ) ) {
138+ // This converter forces a conversion of a non-consumed view item, if that item would be allowed by schema and converted it if was
139+ // inside a paragraph element. The converter checks whether conversion would be possible if there was a paragraph element
140+ // between `data.input` item and its parent. If the conversion would be allowed, the converter adds `"paragraph"` to the
141+ // context and fires conversion for `data.input` again.
142+ function convertAutoparagraphableItem ( evt , data , consumable , conversionApi ) {
143+ // If the item wasn't consumed by some ot the dedicated converters...
144+ if ( ! consumable . test ( data . input , { name : data . input . name } ) ) {
141145 return ;
142146 }
143147
144- // And paragraph is allowed in this context ...
145- if ( ! doc . schema . check ( { name : 'paragraph' , inside : data . context } ) ) {
148+ // But would be allowed if it was in a paragraph ...
149+ if ( ! isParagraphable ( data . input , data . context , conversionApi . schema , false ) ) {
146150 return ;
147151 }
148152
149- // Let's do autoparagraphing.
150-
151- const paragraph = new ModelElement ( 'paragraph' ) ;
152-
153- paragraphsToMerge . add ( paragraph ) ;
154-
155- data . context . push ( paragraph ) ;
156-
157- const text = conversionApi . convertItem ( data . input , consumable , data ) ;
158-
159- if ( text ) {
160- data . output = paragraph ;
161- paragraph . appendChildren ( text ) ;
162- }
163-
153+ // Convert that item in a paragraph context.
154+ data . context . push ( 'paragraph' ) ;
155+ const item = conversionApi . convertItem ( data . input , consumable , data ) ;
164156 data . context . pop ( ) ;
157+
158+ data . output = item ;
165159}
166160
167- function autoparagraphParagraphLikeElements ( doc , evt , data , consumable , conversionApi ) {
168- // If this is a paragraph-like element...
169- if ( ! Paragraph . paragraphLikeElements . has ( data . input . name ) ) {
161+ // This converter checks all children of an element or document fragment that has been converted and wraps
162+ // children in a paragraph element if it is allowed by schema.
163+ //
164+ // Basically, after an item is "forced" to be converted by `convertAutoparagraphableItem`, we need to actually take
165+ // care of adding the paragraph (assumed in `convertAutoparagraphableItem`) and wrap that item in it.
166+ function autoparagraphItems ( evt , data , consumable , conversionApi ) {
167+ // Autoparagraph only if the element has been converted.
168+ if ( ! data . output ) {
170169 return ;
171170 }
172171
173- // Which wasn't consumed by its own converter...
174- if ( ! consumable . test ( data . input , { name : true } ) ) {
172+ const isParagraphLike = Paragraph . paragraphLikeElements . has ( data . input . name ) && ! data . output . is ( 'element' ) ;
173+
174+ // Keep in mind that this converter is added to all elements and document fragments.
175+ // This means that we have to make a smart decision in which elements (at what level) auto-paragraph should be inserted.
176+ // There are three situations when it is correct to add paragraph:
177+ // - we are converting a view document fragment: this means that we are at the top level of conversion and we should
178+ // add paragraph elements for "bare" texts (unless converting in $clipboardHolder, but this is covered by schema),
179+ // - we are converting an element that was converted to model element: this means that it will be represented in model
180+ // and has added its context when converting children - we should add paragraph for those items that passed
181+ // in `convertAutoparagraphableItem`, because it is correct for them to be autoparagraphed,
182+ // - we are converting "paragraph-like" element, which children should always be autoparagraphed (if it is allowed by schema,
183+ // so we won't end up with, i.e., paragraph inside paragraph, if paragraph was in paragraph-like element).
184+ const shouldAutoparagraph =
185+ ( data . input . is ( 'documentFragment' ) ) ||
186+ ( data . input . is ( 'element' ) && data . output . is ( 'element' ) ) ||
187+ isParagraphLike ;
188+
189+ if ( ! shouldAutoparagraph ) {
175190 return ;
176191 }
177192
178- // And there are no other paragraph-like elements inside this tree...
179- if ( hasParagraphLikeContent ( data . input ) ) {
180- return ;
181- }
193+ // Take care of proper context. This is important for `isParagraphable` checks.
194+ const needsNewContext = data . output . is ( 'element' ) ;
182195
183- // And paragraph is allowed in this context...
184- if ( ! doc . schema . check ( { name : 'paragraph' , inside : data . context } ) ) {
185- return ;
196+ if ( needsNewContext ) {
197+ data . context . push ( data . output ) ;
186198 }
187199
188- // Let's convert this element to a paragraph and then all its children.
189-
190- consumable . consume ( data . input , { name : true } ) ;
191-
192- const paragraph = new ModelElement ( 'paragraph' ) ;
193-
194- data . context . push ( paragraph ) ;
195-
196- const convertedChildren = conversionApi . convertChildren ( data . input , consumable , data ) ;
197-
198- paragraph . appendChildren ( modelWriter . normalizeNodes ( convertedChildren ) ) ;
199-
200- // Remove the created paragraph from the stack for other converters.
201- // See https://github.com/ckeditor/ckeditor5-engine/issues/736
202- data . context . pop ( ) ;
203-
204- data . output = paragraph ;
205- }
200+ // `paragraph` element that will wrap auto-paragraphable children.
201+ let autoParagraph = null ;
202+
203+ // Check children and wrap them in a `paragraph` element if they need to be wrapped.
204+ // Be smart when wrapping children and put all auto-paragraphable siblings in one `paragraph` parent:
205+ // foo<$text bold="true">bar</$text><paragraph>xxx</paragraph>baz --->
206+ // <paragraph>foo<$text bold="true">bar</$text></paragraph><paragraph>xxx</paragraph><paragraph>baz</paragraph>
207+ for ( let i = 0 ; i < data . output . childCount ; i ++ ) {
208+ const child = data . output . getChild ( i ) ;
209+
210+ if ( isParagraphable ( child , data . context , conversionApi . schema , isParagraphLike ) ) {
211+ // If there is no wrapping `paragraph` element, create it.
212+ if ( ! autoParagraph ) {
213+ autoParagraph = new ModelElement ( 'paragraph' ) ;
214+ data . output . insertChildren ( child . index , autoParagraph ) ;
215+ }
216+ // Otherwise, use existing `paragraph` and just fix iterator.
217+ // Thanks to reusing `paragraph` element, multiple siblings ends up in same container.
218+ else {
219+ i -- ;
220+ }
206221
207- // Merges subsequent paragraphs if they should be merged (see shouldMerge).
208- function mergeSubsequentParagraphs ( evt , data ) {
209- if ( ! data . output ) {
210- return ;
222+ child . remove ( ) ;
223+ autoParagraph . appendChildren ( child ) ;
224+ } else {
225+ // That was not a paragraphable children, reset `paragraph` wrapper - following auto-paragraphable children
226+ // need to be placed in a new `paragraph` element.
227+ autoParagraph = null ;
228+ }
211229 }
212230
213- let node = data . output . getChild ( 0 ) ;
231+ if ( needsNewContext ) {
232+ data . context . pop ( ) ;
233+ }
234+ }
214235
215- while ( node && node . nextSibling ) {
216- const nextSibling = node . nextSibling ;
236+ function isParagraphable ( node , context , schema , insideParagraphLikeElement ) {
237+ const name = node . name || '$text' ;
217238
218- if ( paragraphsToMerge . has ( node ) && paragraphsToMerge . has ( nextSibling ) ) {
219- modelWriter . insert ( ModelPosition . createAt ( node , 'end' ) , Array . from ( nextSibling . getChildren ( ) ) ) ;
220- modelWriter . remove ( ModelRange . createOn ( nextSibling ) ) ;
221- } else {
222- node = node . nextSibling ;
223- }
239+ // Node is paragraphable if it is inside paragraph like element, or...
240+ // It is not allowed at this context...
241+ if ( ! insideParagraphLikeElement && schema . check ( { name : name , inside : context } ) ) {
242+ return false ;
224243 }
225- }
226244
227- // Checks whether an element has paragraph-like descendant.
228- function hasParagraphLikeContent ( element ) {
229- const range = ViewRange . createIn ( element ) ;
245+ // And paragraph is allowed in this context...
246+ if ( ! schema . check ( { name : 'paragraph' , inside : context } ) ) {
247+ return false ;
248+ }
230249
231- for ( const value of range ) {
232- if ( value . item instanceof ViewElement && Paragraph . paragraphLikeElements . has ( value . item . name ) ) {
233- return true ;
234- }
250+ // And a node would be allowed in this paragraph...
251+ if ( ! schema . check ( { name : name , inside : context . concat ( 'paragraph' ) } ) ) {
252+ return false ;
235253 }
236254
237- return false ;
255+ return true ;
238256}
239257
240258// Looks through all roots created in document and marks every empty root, saving which batch made it empty.
0 commit comments