/*****************************************************************************
* Copyright (c) 2015 CEA LIST.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Dirk Fauth <dirk.fauth@googlemail.com> - Initial API and implementation
*
*****************************************************************************/
package org.eclipse.nebula.widgets.richtext;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.EntityReference;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import org.eclipse.nebula.widgets.richtext.painter.AlignmentStyle;
import org.eclipse.nebula.widgets.richtext.painter.DefaultEntityReplacer;
import org.eclipse.nebula.widgets.richtext.painter.EntityReplacer;
import org.eclipse.nebula.widgets.richtext.painter.LinePainter;
import org.eclipse.nebula.widgets.richtext.painter.ResourceHelper;
import org.eclipse.nebula.widgets.richtext.painter.SpanElement;
import org.eclipse.nebula.widgets.richtext.painter.SpanElement.SpanType;
import org.eclipse.nebula.widgets.richtext.painter.TagProcessingState;
import org.eclipse.nebula.widgets.richtext.painter.TagProcessingState.TextAlignment;
import org.eclipse.nebula.widgets.richtext.painter.instructions.BoldPaintInstruction;
import org.eclipse.nebula.widgets.richtext.painter.instructions.FontMetricsProvider;
import org.eclipse.nebula.widgets.richtext.painter.instructions.ItalicPaintInstruction;
import org.eclipse.nebula.widgets.richtext.painter.instructions.ListInstruction;
import org.eclipse.nebula.widgets.richtext.painter.instructions.NewLineInstruction;
import org.eclipse.nebula.widgets.richtext.painter.instructions.PaintInstruction;
import org.eclipse.nebula.widgets.richtext.painter.instructions.ParagraphInstruction;
import org.eclipse.nebula.widgets.richtext.painter.instructions.ResetFontPaintInstruction;
import org.eclipse.nebula.widgets.richtext.painter.instructions.ResetParagraphInstruction;
import org.eclipse.nebula.widgets.richtext.painter.instructions.ResetSpanStylePaintInstruction;
import org.eclipse.nebula.widgets.richtext.painter.instructions.SpanStylePaintInstruction;
import org.eclipse.nebula.widgets.richtext.painter.instructions.TextPaintInstruction;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
// TODO 0.2 - Painter: cutting if space is not enough
// TODO 0.2 - Painter: add scrolling support
// TODO 0.2 - Painter: improvements like caching of information
// TODO 0.2 - Extension: add ability to handle custom tags
// TODO 0.2 - Extension: add ability to specify interactions, e.g. links
// TODO 0.2 - Extension: add ability to include images
/**
* The {@link RichTextPainter} is used to parse and render HTML input to a {@link GC}. It works well
* with HTML input generated by ckeditor.
*/
public class RichTextPainter {
public static final String TAG_SPAN = "span";
public static final String TAG_STRONG = "strong";
public static final String TAG_EM = "em";
public static final String TAG_UNDERLINE = "u";
public static final String TAG_STRIKETHROUGH = "s";
public static final String TAG_PARAGRAPH = "p";
public static final String TAG_UNORDERED_LIST = "ul";
public static final String TAG_ORDERED_LIST = "ol";
public static final String TAG_LIST_ITEM = "li";
public static final String TAG_BR = "br";
public static final String ATTRIBUTE_STYLE = "style";
public static final String ATTRIBUTE_STYLE_COLOR = "color";
public static final String ATTRIBUTE_STYLE_BACKGROUND_COLOR = "background-color";
public static final String ATTRIBUTE_STYLE_FONT_SIZE = "font-size";
public static final String ATTRIBUTE_STYLE_FONT_FAMILY = "font-family";
public static final String ATTRIBUTE_PARAGRAPH_MARGIN_LEFT = "margin-left";
public static final String ATTRIBUTE_PARAGRAPH_TEXT_ALIGN = "text-align";
public static final String ATTRIBUTE_PARAGRAPH_TEXT_ALIGN_VALUE_RIGHT = "right";
public static final String CONTROL_CHARACTER_REGEX = "\\n\\r|\\r\\n|\\n|\\r|\\t"; //$NON-NLS-1$
public static final String FAKE_ROOT_TAG_START = "<root>";
public static final String FAKE_ROOT_TAG_END = "</root>";
public static final String[] BULLETS = new String[] { "\u2022", " \u25e6", "\u25aa" };
public static final String SPACE = "\u00a0";
private int paragraphSpace = 5;
private boolean wordWrap;
private final Point preferredSize = new Point(0, 0);
XMLInputFactory factory = XMLInputFactory.newInstance();
{
// as we don't have a well-formed XML document, we need to take care of
// entity references ourself
factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.FALSE);
}
private EntityReplacer entityReplacer = new DefaultEntityReplacer();
/**
* Create a new {@link RichTextPainter} with disabled word wrapping.
*/
public RichTextPainter() {
this(false);
}
/**
* Create a new {@link RichTextPainter}.
*
* @param wordWrap
* <code>true</code> if automatic word wrapping should be enabled, <code>false</code>
* if not.
*/
public RichTextPainter(boolean wordWrap) {
this.wordWrap = wordWrap;
}
/**
* Processes the HTML input to calculate the preferred size. Does not perform rendering.
*
* @param html
* The HTML string to process.
* @param gc
* The {@link GC} to operate on.
* @param bounds
* The available space for painting.
* @param calculateWithWrapping
* <code>true</code> if calculation should be performed with enabled word wrapping,
* <code>false</code> if not
*/
public void preCalculate(String html, GC gc, Rectangle bounds, boolean calculateWithWrapping) {
boolean original = this.wordWrap;
this.wordWrap = calculateWithWrapping;
paintHTML(html, gc, bounds, false);
this.wordWrap = original;
}
/**
* Processes the HTML input and paints the result to the given {@link GC}.
*
* @param html
* The HTML string to process.
* @param gc
* The {@link GC} to operate on.
* @param bounds
* The available space for painting.
*/
public void paintHTML(String html, GC gc, Rectangle bounds) {
paintHTML(html, gc, bounds, true);
}
/**
* Processes the HTML input.
*
* @param html
* The HTML string to process.
* @param gc
* The {@link GC} to operate on.
* @param bounds
* The available space for painting.
* @param render
* <code>true</code> if the processing result should be painted to the {@link GC},
* <code>false</code> if not (in case of pre calculation).
*/
protected void paintHTML(String html, GC gc, Rectangle bounds, boolean render) {
final TagProcessingState state = new TagProcessingState();
state.setStartingPoint(bounds.x, bounds.y);
state.setRendering(render);
Deque<SpanElement> spanStack = new LinkedList<>();
// Collection<PaintInstruction> instructions = new ArrayList<>();
Collection<LinePainter> lines = new ArrayList<>();
// as we only care about html tags, we ignore control character
String cleanedHtml = html.replaceAll(CONTROL_CHARACTER_REGEX, "");
// we need to introduce a fake root tag, because otherwise we will get invalid XML
// exceptions
cleanedHtml = FAKE_ROOT_TAG_START + cleanedHtml + FAKE_ROOT_TAG_END;
gc.setAntialias(SWT.DEFAULT);
gc.setTextAntialias(SWT.DEFAULT);
XMLEventReader parser = null;
int availableWidth = bounds.width;
Deque<Integer> listIndentation = new LinkedList<>();
boolean listOpened = false;
GC tempGC = new GC(gc.getDevice());
tempGC.setFont(gc.getFont());
try {
parser = factory.createXMLEventReader(new StringReader(cleanedHtml));
LinePainter currentLine = null;
while (parser.hasNext()) {
XMLEvent event = parser.nextEvent();
switch (event.getEventType()) {
case XMLStreamConstants.END_DOCUMENT:
parser.close();
break;
case XMLStreamConstants.START_ELEMENT:
final StartElement element = event.asStartElement();
String elementString = element.getName().toString();
if (TAG_PARAGRAPH.equals(elementString)) {
currentLine = createNewLine(lines);
AlignmentStyle alignment = handleAlignmentConfiguration(element);
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state,
new ParagraphInstruction(alignment, getParagraphSpace(), state));
availableWidth -= alignment.marginLeft;
}
else if (TAG_BR.equals(elementString)) {
currentLine = createNewLine(lines);
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new NewLineInstruction(state));
}
else if (TAG_SPAN.equals(elementString)) {
PaintInstruction styleInstruction = handleStyleConfiguration(element, spanStack, state);
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, styleInstruction);
}
else if (TAG_STRONG.equals(elementString)) {
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new BoldPaintInstruction(state));
}
else if (TAG_EM.equals(elementString)) {
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new ItalicPaintInstruction(state));
}
else if (TAG_UNDERLINE.equals(elementString)) {
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new PaintInstruction() {
@Override
public void paint(GC gc, Rectangle area) {
state.setUnderlineActive(true);
}
});
}
else if (TAG_STRIKETHROUGH.equals(elementString)) {
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new PaintInstruction() {
@Override
public void paint(GC gc, Rectangle area) {
state.setStrikethroughActive(true);
}
});
}
else if (TAG_UNORDERED_LIST.equals(elementString)
|| TAG_ORDERED_LIST.equals(elementString)) {
int indent = calculateListIndentation(tempGC);
availableWidth -= indent;
listIndentation.add(indent);
listOpened = true;
AlignmentStyle alignment = handleAlignmentConfiguration(element);
availableWidth -= alignment.marginLeft;
boolean isOrderedList = TAG_ORDERED_LIST.equals(elementString);
currentLine = createNewLine(lines);
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state,
new ListInstruction(indent, isOrderedList, alignment, getParagraphSpace(), state));
// inspect font attributes
PaintInstruction styleInstruction = handleStyleConfiguration(element, spanStack, state);
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, styleInstruction);
}
else if (TAG_LIST_ITEM.equals(elementString)) {
// if a list was opened before, the list tag created a new line
// otherwise we create a new line for the new list item
if (!listOpened) {
currentLine = createNewLine(lines);
currentLine = addInstruction(tempGC, availableWidth, lines,
currentLine, state, new NewLineInstruction(state));
}
else {
listOpened = false;
}
final AlignmentStyle alignment = handleAlignmentConfiguration(element);
// paint number/bullet
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new PaintInstruction() {
@Override
public void paint(GC gc, Rectangle area) {
state.resetX();
String bullet = getBulletCharacter(state.getListDepth()) + "\u00a0";
if (state.isOrderedList()) {
bullet = "" + state.getCurrentListNumber() + ". ";
}
int extend = gc.textExtent(bullet).x;
gc.drawText(bullet, state.getPointer().x - extend, state.getPointer().y, (state.hasPreviousBgColor()));
state.setTextAlignment(alignment.alignment);
state.calculateX(area.width);
}
});
}
break;
case XMLStreamConstants.END_ELEMENT:
String endElementString = event.asEndElement().getName().toString();
if (TAG_PARAGRAPH.equals(endElementString)) {
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state,
new ResetParagraphInstruction(getParagraphSpace(), state));
availableWidth = bounds.width;
}
else if (TAG_SPAN.equals(endElementString)) {
SpanElement span = spanStack.pollLast();
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new ResetSpanStylePaintInstruction(state, span));
}
else if (TAG_STRONG.equals(endElementString)
|| TAG_EM.equals(endElementString)) {
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new ResetFontPaintInstruction(state));
}
else if (TAG_UNDERLINE.equals(endElementString)) {
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new PaintInstruction() {
@Override
public void paint(GC gc, Rectangle area) {
state.setUnderlineActive(false);
}
});
}
else if (TAG_STRIKETHROUGH.equals(endElementString)) {
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new PaintInstruction() {
@Override
public void paint(GC gc, Rectangle area) {
state.setStrikethroughActive(false);
}
});
}
else if (TAG_LIST_ITEM.equals(endElementString)) {
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new PaintInstruction() {
@Override
public void paint(GC gc, Rectangle area) {
state.increaseCurrentListNumber();
state.setTextAlignment(TextAlignment.LEFT);
}
});
}
else if (TAG_ORDERED_LIST.equals(endElementString)
|| TAG_UNORDERED_LIST.equals(endElementString)) {
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new PaintInstruction() {
@Override
public void paint(GC gc, Rectangle area) {
state.resetListConfiguration();
// if the last list layer was finished, increase the line height
// like in paragraph
if (state.getListDepth() == 0) {
state.setMarginLeft(0);
state.increaseY(state.getCurrentLineHeight());
state.increaseY(getParagraphSpace());
}
state.resetX();
state.setTextAlignment(TextAlignment.LEFT);
}
});
availableWidth += listIndentation.pollLast();
}
break;
case XMLStreamConstants.CHARACTERS:
Characters characters = event.asCharacters();
String text = characters.getData();
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new TextPaintInstruction(state, text));
break;
case XMLStreamConstants.ENTITY_REFERENCE:
String value = entityReplacer.getEntityReferenceValue((EntityReference) event);
if (value != null) {
currentLine = addInstruction(tempGC, availableWidth, lines, currentLine, state, new TextPaintInstruction(state, value));
}
break;
case XMLStreamConstants.ATTRIBUTE:
break;
default:
break;
}
}
} catch (XMLStreamException e) {
e.printStackTrace();
} finally {
tempGC.dispose();
if (parser != null) {
try {
parser.close();
} catch (XMLStreamException e) {
e.printStackTrace();
}
}
}
// initialize the state to be able to iterate over the line instructions
state.setLineIterator(lines.iterator());
preferredSize.x = bounds.width;
preferredSize.y = 0;
// perform the painting
for (LinePainter painter : lines) {
painter.paint(gc, bounds);
preferredSize.x = Math.max(preferredSize.x, painter.getContentWidth());
preferredSize.y += painter.getLineHeight();
}
// add paragraphSpace on top and bottom
preferredSize.y += (2 * state.getParagraphCount() * getParagraphSpace());
preferredSize.y = Math.max(preferredSize.y, bounds.height);
}
private LinePainter addInstruction(
GC gc, int availableWidth,
Collection<LinePainter> lines, LinePainter currentLine,
final TagProcessingState state, PaintInstruction instruction) {
if (instruction instanceof FontMetricsProvider) {
// apply the font to the temp GC
((FontMetricsProvider) instruction).getFontMetrics(gc);
}
// if currentLine is null at this point, there is no spanning p tag and we create a new line
// this is for convenience to support also simple text
if (currentLine == null) {
currentLine = createNewLine(lines);
currentLine.addInstruction(new PaintInstruction() {
@Override
public void paint(GC gc, Rectangle area) {
state.activateNextLine();
state.increaseY(getParagraphSpace());
state.increaseParagraphCount();
}
});
}
LinePainter lineToUse = currentLine;
if (instruction instanceof TextPaintInstruction) {
TextPaintInstruction txtInstr = (TextPaintInstruction) instruction;
int textLength = txtInstr.getTextLength(gc);
int trimmedTextLength = txtInstr.getTrimmedTextLength(gc);
if ((currentLine.getContentWidth() + textLength) > availableWidth) {
if (this.wordWrap) {
// if word wrapping is enabled, split the text and create new lines
// by making several TextPaintInstructions with substrings
Deque<String> wordsToProcess = new LinkedList<>(Arrays.asList(txtInstr.getText().split("(?<=\\s)")));
String subString = "";
int subStringLength = 0;
while (!wordsToProcess.isEmpty()) {
String word = wordsToProcess.pollFirst();
int wordLength = gc.textExtent(word).x;
subStringLength += wordLength;
if ((lineToUse.getContentWidth() + subStringLength) > availableWidth) {
boolean newLine = true;
if (!subString.trim().isEmpty()) {
// trim right side
subString = ResourceHelper.rtrim(subString);
txtInstr = new TextPaintInstruction(state, subString);
textLength = txtInstr.getTextLength(gc);
trimmedTextLength = txtInstr.getTrimmedTextLength(gc);
lineToUse.addInstruction(txtInstr);
lineToUse.increaseContentWidth(textLength);
lineToUse.increaseTrimmedContentWidth(trimmedTextLength);
subString = word;
subStringLength = wordLength;
}
else if (lineToUse.getContentWidth() == 0) {
// no content already but text width greater than available width
// TODO 0.2 - modify text to show ...
// TODO 0.2 - add trim to avoid empty lines because of spaces
subString = word;
subStringLength = wordLength;
newLine = false;
}
if (newLine) {
lineToUse = createNewLine(lines);
lineToUse.addInstruction(new NewLineInstruction(state));
}
}
else {
subString += word;
}
}
if (!subString.trim().isEmpty()) {
txtInstr = new TextPaintInstruction(state, subString);
textLength = txtInstr.getTextLength(gc);
trimmedTextLength = txtInstr.getTrimmedTextLength(gc);
instruction = txtInstr;
}
}
}
lineToUse.addInstruction(instruction);
lineToUse.increaseContentWidth(textLength);
lineToUse.increaseTrimmedContentWidth(trimmedTextLength);
}
else {
lineToUse.addInstruction(instruction);
}
return lineToUse;
}
private LinePainter createNewLine(Collection<LinePainter> lines) {
LinePainter currentLine = new LinePainter();
lines.add(currentLine);
return currentLine;
}
private AlignmentStyle handleAlignmentConfiguration(StartElement element) {
AlignmentStyle result = new AlignmentStyle();
for (Iterator<?> attributes = element.getAttributes(); attributes.hasNext();) {
Attribute attribute = (Attribute) attributes.next();
if (ATTRIBUTE_STYLE.equals(attribute.getName().toString())) {
Map<String, String> styleProperties = getStyleProperties(attribute.getValue().toString());
for (Map.Entry<String, String> entry : styleProperties.entrySet()) {
if (ATTRIBUTE_PARAGRAPH_MARGIN_LEFT.equals(entry.getKey())) {
String pixelValue = entry.getValue().replace("px", "");
try {
int pixel = Integer.valueOf(pixelValue.trim());
result.marginLeft = pixel;
} catch (NumberFormatException e) {
// if the value is not a valid number value
// we simply ignore it
e.printStackTrace();
}
}
else if (ATTRIBUTE_PARAGRAPH_TEXT_ALIGN.equals(entry.getKey())) {
try {
TextAlignment alignment = TextAlignment.valueOf(entry.getValue().toUpperCase());
result.alignment = alignment;
} catch (IllegalArgumentException e) {
// if the value doesn't match a valid
// text-aligment value we simply ignore it
e.printStackTrace();
}
}
}
}
}
return result;
}
private PaintInstruction handleStyleConfiguration(StartElement element, Deque<SpanElement> spanStack, TagProcessingState state) {
// create the span element with reset informations on tag close
SpanElement span = new SpanElement();
// create span style paint instruction that should be performed on
// painting
SpanStylePaintInstruction styleInstruction = new SpanStylePaintInstruction(state);
// inspect the attributes
for (Iterator<?> attributes = element.getAttributes(); attributes.hasNext();) {
Attribute attribute = (Attribute) attributes.next();
if (ATTRIBUTE_STYLE.equals(attribute.getName().toString())) {
Map<String, String> styleProperties = getStyleProperties(attribute.getValue().toString());
for (Map.Entry<String, String> entry : styleProperties.entrySet()) {
if (ATTRIBUTE_STYLE_COLOR.equals(entry.getKey())) {
// update the span element to know what to reset on tag
// close
span.types.add(SpanType.COLOR);
// update the style paint instruction
styleInstruction.setForegroundColor(ResourceHelper.getColor(entry.getValue()));
}
else if (ATTRIBUTE_STYLE_BACKGROUND_COLOR.equals(entry.getKey())) {
// update the span element to know what to reset on tag
// close
span.types.add(SpanType.BG_COLOR);
// update the style paint instruction
styleInstruction.setBackgroundColor(ResourceHelper.getColor(entry.getValue()));
}
else if (ATTRIBUTE_STYLE_FONT_SIZE.equals(entry.getKey())) {
// update the span element to know what to reset on tag
// close
span.types.add(SpanType.FONT);
// update the style paint instruction
String pixelValue = entry.getValue().replace("px", "");
try {
int pixel = Integer.valueOf(pixelValue.trim());
// the size in pixels specified in HTML
// so we need to convert it to point
int pointSize = 72 * pixel / Display.getDefault().getDPI().x;
styleInstruction.setFontSize(pointSize);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
else if (ATTRIBUTE_STYLE_FONT_FAMILY.equals(entry.getKey())) {
// update the span element to know what to reset on tag
// close
span.types.add(SpanType.FONT);
// update the style paint instruction
styleInstruction.setFontType(entry.getValue().split(",")[0]);
}
}
}
}
spanStack.add(span);
return styleInstruction;
}
private Map<String, String> getStyleProperties(String styleString) {
Map<String, String> result = new HashMap<>();
String[] configurations = styleString.split(";");
for (String config : configurations) {
String[] keyValuePair = config.split(":");
result.put(keyValuePair[0].trim(), keyValuePair[1].trim());
}
return result;
}
/**
* Calculates the indentation to use for list items.
*
* @param gc
* The current {@link GC} for calculating the text extend.
* @return The indentation to use for list items.
*/
protected int calculateListIndentation(GC gc) {
return gc.textExtent("000. ").x;
}
/**
* @param listDepth
* The list depth of the current list. Needs to be 1 for the top level list.
* @return The bullet character to use for list items of an unordered list.
*/
protected String getBulletCharacter(int listDepth) {
if (listDepth >= BULLETS.length) {
return BULLETS[BULLETS.length - 1];
}
return BULLETS[listDepth - 1];
}
/**
* Set an {@link EntityReplacer} that should be used to replace {@link EntityReference}s in the
* HTML snippet to parse.
*
* @param entityReplacer
* The {@link EntityReplacer} to use.
*/
public void setEntityReplacer(EntityReplacer entityReplacer) {
this.entityReplacer = entityReplacer;
}
/**
* Returns the preferred size of the content. It is calculated after processing the content.
*
* @return The preferred size of the content.
*
* @see RichTextPainter#preCalculate(String, GC, Rectangle, boolean)
*/
public Point getPreferredSize() {
return this.preferredSize;
}
/**
* @return The space that should be used before and after a paragraph.<br>
* <b>Note:</b> Between two paragraphs the paragraphSpace * 2 is added as space.
*/
public int getParagraphSpace() {
return this.paragraphSpace;
}
/**
* @param paragraphSpace
* The space that should be applied before and after a paragraph.<br>
* <b>Note:</b> Between two paragraphs the paragraphSpace * 2 is added as space.
*/
public void setParagraphSpace(int paragraphSpace) {
this.paragraphSpace = paragraphSpace;
}
}