/* ****************************************************************************** * Copyright (c) 2006-2012 XMind Ltd. and others. * * This file is a part of XMind 3. XMind releases 3 and * above are dual-licensed under the Eclipse Public License (EPL), * which is available at http://www.eclipse.org/legal/epl-v10.html * and the GNU Lesser General Public License (LGPL), * which is available at http://www.gnu.org/licenses/lgpl.html * See http://www.xmind.net/license.html for details. * * Contributors: * XMind Ltd. - initial API and implementation *******************************************************************************/ package org.xmind.ui.internal.notes; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IRegion; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.Image; import org.xmind.core.IHtmlNotesContent; import org.xmind.core.IHyperlinkSpan; import org.xmind.core.IImageSpan; import org.xmind.core.INotes; import org.xmind.core.IParagraph; import org.xmind.core.ISpan; import org.xmind.core.ITextSpan; import org.xmind.core.style.IStyle; import org.xmind.core.style.IStyleSheet; import org.xmind.core.util.Property; import org.xmind.ui.resources.ColorUtils; import org.xmind.ui.resources.FontUtils; import org.xmind.ui.richtext.Hyperlink; import org.xmind.ui.richtext.IRichDocument; import org.xmind.ui.richtext.ImagePlaceHolder; import org.xmind.ui.richtext.LineStyle; import org.xmind.ui.richtext.RichTextUtils; import org.xmind.ui.style.StyleUtils; import org.xmind.ui.style.Styles; import org.xmind.ui.util.Logger; public class HtmlNotesContentBuilder { private static final Map<String, String> EMPTY_CONTENTS = Collections .emptyMap(); private RichDocumentNotesAdapter adapter; private IStyleSheet styleSheet; private IRichDocument document; private Iterator<StyleRange> textStyles; private Iterator<LineStyle> lineStyles; private Iterator<Hyperlink> hyperlinks; private StyleRange textStyle; private LineStyle lineStyle; private Hyperlink hyperlink; private IHtmlNotesContent result; private IParagraph p; private IHyperlinkSpan h = null; private int totalLines; private int offset = 0; private int lineEnd = 0; private Map<String, Map<String, String>> contentsCache = new HashMap<String, Map<String, String>>(); public HtmlNotesContentBuilder(RichDocumentNotesAdapter adapter) { this.adapter = adapter; } public void build(IRichDocument document) { this.document = document; if ("".equals(document.get())) { //$NON-NLS-1$ this.result = null; return; } this.textStyles = Arrays.asList(document.getTextStyles()).iterator(); this.lineStyles = Arrays.asList(document.getLineStyles()).iterator(); this.hyperlinks = Arrays.asList(document.getHyperlinks()).iterator(); this.textStyle = textStyles.hasNext() ? textStyles.next() : null; this.lineStyle = lineStyles.hasNext() ? lineStyles.next() : null; this.hyperlink = hyperlinks.hasNext() ? hyperlinks.next() : null; this.totalLines = document.getNumberOfLines(); this.styleSheet = adapter.getWorkbook().getStyleSheet(); this.result = (IHtmlNotesContent) adapter.getWorkbook() .createNotesContent(INotes.HTML); for (int lineIndex = 0; lineIndex < totalLines; lineIndex++) { buildParagraph(lineIndex); } } private void buildParagraph(int lineIndex) { p = result.createParagraph(); result.addParagraph(p); if (lineStyle != null && lineStyle.lineIndex == lineIndex) { applyStyleToParagraph(p, lineStyle); lineStyle = lineStyles.hasNext() ? lineStyles.next() : null; } IRegion lineRange; try { lineRange = document.getLineInformation(lineIndex); } catch (BadLocationException e) { Logger.log(e, "Failed to obtain line information"); //$NON-NLS-1$ return; } int lineStart = lineRange.getOffset(); int lineLength = lineRange.getLength(); if (lineIndex < totalLines - 1) { lineLength += NotesConstants.LENGTH_DELIMITER; } lineEnd = lineStart + lineLength; while (offset < lineEnd) { buildLineContent(); } } private void buildLineContent() { int next; StyleRange appliedStyle = null; if (isInHyperlink()) { next = Math.min(lineEnd, getNextHyperlinkEnd()); } else { next = Math.min(lineEnd, getNextHyperlinkStart()); } if (isInStyle()) { next = Math.min(next, Math.min(lineEnd, getNextStyleEnd())); appliedStyle = textStyle; } else { next = Math.min(next, Math.min(lineEnd, getNextStyleStart())); } if (isInHyperlink() && isHyperlinkStarting()) { startBuildingHyperlink(); } ISpan span = createSpan(next, appliedStyle); if (span != null) { addSpan(span); } if (isInHyperlink() && isHyperlinkEnding(next)) { finishCurrentHyperlink(); } if (isInStyle() && isStyleEnding(next)) { finishCurrentStyle(); } offset = next; } private void finishCurrentStyle() { textStyle = textStyles.hasNext() ? textStyles.next() : null; } private boolean isStyleEnding(int next) { return textStyle != null && next == textStyle.start + textStyle.length; } private void finishCurrentHyperlink() { hyperlink = hyperlinks.hasNext() ? hyperlinks.next() : null; h = null; } private boolean isHyperlinkEnding(int next) { return hyperlink != null && next == hyperlink.end(); } private void addSpan(ISpan span) { if (h != null) { h.addSpan(span); } else { p.addSpan(span); } } private ISpan createSpan(int next, StyleRange style) { String content = getTrimmedContent(next - offset); if (content != null) { if (style != null && style.metrics != null && ImagePlaceHolder.PLACE_HOLDER.equals(content)) { return createImage(style); } else { return createText(content, style); } } return null; } private void startBuildingHyperlink() { h = result.createHyperlinkSpan(hyperlink.href); p.addSpan(h); } private boolean isHyperlinkStarting() { return hyperlink != null && offset == hyperlink.start; } private int getNextStyleStart() { return textStyle == null ? Integer.MAX_VALUE : textStyle.start; } private int getNextStyleEnd() { StyleRange style = textStyle; int endOfStyle = style.start + style.length; return style == null ? Integer.MAX_VALUE : endOfStyle; } private int getNextHyperlinkStart() { return hyperlink == null ? Integer.MAX_VALUE : hyperlink.start; } private int getNextHyperlinkEnd() { return hyperlink == null ? Integer.MAX_VALUE : hyperlink.end(); } private boolean isInStyle() { if (textStyle != null) { int endOfStyle = textStyle.start + textStyle.length; return offset >= textStyle.start && offset < endOfStyle; } return false; } private boolean isInHyperlink() { return hyperlink != null && offset >= hyperlink.start && offset < hyperlink.end(); } private ITextSpan createText(String content, StyleRange style) { ITextSpan text = result.createTextSpan(content); applyStyleToSpan(text, style); return text; } private IImageSpan createImage(StyleRange style) { Image image = document.findImage(style.start); // Image image = (Image) style.data; if (image != null) { String url = adapter.getImageUrl(image); if (url != null) { IImageSpan img = result.createImageSpan(url); return img; } } return null; } private String getTrimmedContent(int textLength) { String content; try { content = document.get(offset, textLength); } catch (BadLocationException e) { Logger.log(e, "Failed to obtain text contet"); //$NON-NLS-1$ return null; } return trimContent(content); } public String trimContent(String content) { // return content.replaceAll(ImagePlaceHolder.PLACE_HOLDER // + "|\\r\\n|\\r|\\n", NotesConstants.EMPTY); //$NON-NLS-1$ return content.replaceAll("\\r\\n|\\r|\\n", NotesConstants.EMPTY); //$NON-NLS-1$ } private void applyStyleToParagraph(IParagraph p, LineStyle lineStyle) { if (lineStyle == null) return; Map<String, String> contents = new HashMap<String, String>(); String align = toModelAlign(lineStyle.alignment); // if (align == null) // return; if (align != null) contents.put(Styles.TextAlign, align); // boolean bullet = lineStyle.bullet; // if (bullet) // contents.put(Styles.TextBullet, Styles.TEXT_STYLE_BULLET); // boolean number = lineStyle.number; // if (number) // contents.put(Styles.TextBullet, Styles.TEXT_STYLE_NUMBER); String bulletStyle = getBulletStyle(lineStyle.bulletStyle); if (bulletStyle != null) contents.put(Styles.TextBullet, bulletStyle); String styleId = getNewStyleId(contents, IStyle.PARAGRAPH); if (styleId == null) return; p.setStyleId(styleId); // p.setAttribute(DOMConstants.ATTR_STYLE_ID, styleId); } private String getBulletStyle(String bulletStyle) { if (LineStyle.BULLET.equals(bulletStyle)) return Styles.TEXT_STYLE_BULLET; else if (LineStyle.NUMBER.equals(bulletStyle)) return Styles.TEXT_STYLE_NUMBER; return null; } private void applyStyleToSpan(ISpan span, StyleRange style) { if (style == null) return; String fontName; int size; if (style.font != null) { FontData fontData = style.font.getFontData()[0]; fontName = fontData.getName(); if (RichTextUtils.DEFAULT_FONT_DATA.getName().equals(fontName)) fontName = null; size = fontData.getHeight(); if (RichTextUtils.DEFAULT_FONT_DATA.getHeight() == size) size = -1; } else { fontName = null; size = -1; } String availableFontName = FontUtils.getAAvailableFontNameFor(fontName); fontName = availableFontName != null ? availableFontName : fontName; boolean bold = RichTextUtils.isBold(style); boolean italic = RichTextUtils.isItalic(style); boolean underline = style.underline; boolean strikeout = style.strikeout; String foreground = style.foreground == null ? null : ColorUtils.toString(style.foreground); String background = style.background == null ? null : ColorUtils.toString(style.background); if (fontName == null && size < 0 && !bold && !italic && !underline && !strikeout && foreground == null && background == null) return; Map<String, String> contents = new HashMap<String, String>(); if (fontName != null) contents.put(Styles.FontFamily, fontName); if (size > 0) contents.put(Styles.FontSize, StyleUtils.addUnitPoint(size)); if (bold) contents.put(Styles.FontWeight, Styles.FONT_WEIGHT_BOLD); if (italic) contents.put(Styles.FontStyle, Styles.FONT_STYLE_ITALIC); if (underline || strikeout) { if (!underline) { contents.put(Styles.TextDecoration, Styles.TEXT_DECORATION_LINE_THROUGH); } else if (!strikeout) { contents.put(Styles.TextDecoration, Styles.TEXT_DECORATION_UNDERLINE); } else { contents.put(Styles.TextDecoration, Styles.TEXT_UNDERLINE_AND_LINE_THROUGH); } } if (foreground != null) contents.put(Styles.TextColor, foreground); if (background != null) contents.put(Styles.BackgroundColor, background); String styleId = getNewStyleId(contents, IStyle.TEXT); if (styleId == null) return; // span.setAttribute(DOMConstants.ATTR_STYLE_ID, styleId); span.setStyleId(styleId); } private String getNewStyleId(Map<String, String> sourceContents, String styleType) { if (sourceContents.isEmpty()) return null; IStyle similar = findSimilarStyle(sourceContents); if (similar != null) return similar.getId(); IStyle newStyle = createStyle(styleType, sourceContents); if (newStyle != null) return newStyle.getId(); return null; } private Map<String, String> getContents(IStyle style) { if (style == null) return EMPTY_CONTENTS; String id = style.getId(); Map<String, String> map = contentsCache.get(id); if (map != null) return map; map = new HashMap<String, String>(); contentsCache.put(id, map); Iterator<Property> it = style.properties(); while (it.hasNext()) { Property p = it.next(); map.put(p.key, p.value); } return map; } private IStyle findSimilarStyle(Map<String, String> sourceCotents) { Set<IStyle> styles = styleSheet.getStyles(IStyleSheet.NORMAL_STYLES); for (IStyle style : styles) { Map<String, String> contents = getContents(style); if (contents.equals(sourceCotents)) return style; } return null; } private IStyle createStyle(String styleType, Map<String, String> contents) { IStyle newStyle = styleSheet.createStyle(styleType); for (Entry<String, String> en : contents.entrySet()) { newStyle.setProperty(en.getKey(), en.getValue()); } styleSheet.addStyle(newStyle, IStyleSheet.NORMAL_STYLES); return newStyle; } private static String toModelAlign(int alignment) { if (alignment == SWT.CENTER) return Styles.ALIGN_CENTER; if (alignment == SWT.RIGHT) return Styles.ALIGN_RIGHT; return null; } public IHtmlNotesContent getResult() { return result; } }