/*
* jMemorize - Learning made easy (and fun) - A Leitner flashcards tool
* Copyright(C) 2004-2008 Riad Djemili and contributors
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 1, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package jmemorize.core;
import java.awt.Dimension;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
/**
* This class handles encoding/decoding and displaying formatted/
* unformatted text. The text is immutable.
*
* Styled document
* Unformatted String <--> FormattedText class
* Encoding
*
* @author djemili
*/
public class FormattedText implements Cloneable
{
public class ParseException extends Exception
{
public ParseException(String message)
{
super(message);
}
}
// TODO add trimming at end
// TODO check if reg exp that breaks at new lines is suffice
// TODO replace direct StyledDocument reference by eclipse-style IAdapter pattern
// TODO optimze the reg expr
/**
* An empty formatted text (immutable).
*/
public static final FormattedText EMPTY = FormattedText.unformatted("");
private static final String TAGS = "<(/?(b|i|u|sub|sup)?)>";
private static final Pattern TEXT_PATTERN = Pattern.compile(
"(.*?)<(/?(b|i|u|sub|sup)?)>", Pattern.DOTALL);
// private static final Pattern IMG_PATTERN = Pattern.compile(
// "<img id=\"(.*?)\"/>", Pattern.DOTALL);
private static final String CONTENT_ELEMENT_NAME = "content";
private String m_formattedText;
private String m_unformattedText;
private static Map<String, Object> stylesMap = new HashMap<String, Object>();
static
{
setupStylesMap();
}
public static FormattedText formatted(String formatted)
{
FormattedText text = new FormattedText();
text.m_formattedText = formatted;
text.m_unformattedText = unescape(formatted.replaceAll(TAGS, "").replaceAll("<img .*?/>", ""));
return text;
}
public static FormattedText formatted(StyledDocument document)
{
Element root = document.getDefaultRootElement();
String fText = removeRedundantTags(getFormattedText(
root, 0, document.getLength()));
return FormattedText.formatted(fText);
}
public static FormattedText formatted(StyledDocument document,
int start, int end)
{
Element root = document.getDefaultRootElement();
String fText = removeRedundantTags(getFormattedText(
root, start, end));
return FormattedText.formatted(fText);
}
public static FormattedText unformatted(String unformatted)
{
FormattedText text = new FormattedText();
text.m_formattedText = unformatted;
text.m_unformattedText = unformatted;
return text;
}
public static void insertImage(Document doc, ImageIcon icon, int offset)
throws BadLocationException
{
int iconWidth = icon.getIconWidth();
int iconHeight = icon.getIconHeight();
Dimension dim = new Dimension(iconWidth, iconHeight);
SimpleAttributeSet sa = new SimpleAttributeSet();
JLabel label = new JLabel(icon);
label.setMinimumSize(dim);
label.setPreferredSize(dim);
label.setMaximumSize(dim);
label.setSize(dim);
StyleConstants.setComponent(sa, label);
doc.insertString(offset, " ", sa);
}
public String getFormatted()
{
return m_formattedText;
}
public String getUnformatted()
{
return m_unformattedText;
}
// TODO rename to toStyledDocument
public StyledDocument getDocument()
{
DefaultStyledDocument doc = new DefaultStyledDocument();
doc.setCharacterAttributes(0, doc.getLength() + 1, // HACK
SimpleAttributeSet.EMPTY, true);
try
{
decode(doc, m_formattedText, 0);
}
catch (Exception e)
{
Main.logThrowable("Error formatting card", e);
}
return doc;
}
public void insertIntoDocument(StyledDocument doc, int offset)
{
try
{
decode(doc, m_formattedText, offset);
}
catch (Exception e)
{
Main.logThrowable("Error formatting card", e);
}
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString()
{
return m_unformattedText;
}
/* (non-Javadoc)
* @see java.lang.Object#clone()
*/
public Object clone()
{
try
{
return super.clone();
}
catch (CloneNotSupportedException e)
{
assert false;
}
return null;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object obj)
{
if (obj instanceof FormattedText)
{
FormattedText other = (FormattedText)obj;
return m_formattedText.equals(other.m_formattedText);
}
return false;
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
public int hashCode()
{
return m_formattedText.hashCode();
}
private static String removeRedundantTags(String formattedText)
{
/*
* StyledDocument merges styles in certain situations. To avoid that
* this results in getting a different encoding after decoding to a
* StyledDocument and back to an encoding again, we remove redundant
* tags by ourself.
*/
for (String key : stylesMap.keySet())
{
StringBuffer sb = new StringBuffer();
sb.append("</").append(key).append("><").append(key).append(">");
formattedText = formattedText.replaceAll(sb.toString(), "");
}
return formattedText;
}
private static String getFormattedText(Element e, int startSelection,
int endSelection)
{
StringBuffer sb = new StringBuffer();
if (e.getName().equals(CONTENT_ELEMENT_NAME))
{
Document doc = e.getDocument();
int start = e.getStartOffset();
int end = Math.min(e.getEndOffset(), doc.getLength());
if (start > endSelection || end < startSelection)
return sb.toString();
try
{
start = Math.max(start, startSelection);
end = Math.min(end, endSelection);
String text = doc.getText(start, end - start);
sb.append(escape(text));
}
catch (BadLocationException e1)
{
e1.printStackTrace();
Main.logThrowable("Error formatting text", e1);
}
}
// else if (e.getName().equals(StyleConstants.ParagraphConstants.ComponentElementName))
// {
// AttributeSet attr = e.getAttributes();
// JLabel label = (JLabel)attr.getAttribute(
// StyleConstants.ParagraphConstants.ComponentAttribute);
//
// ImageIcon icon = (ImageIcon)label.getIcon();
// String description = icon.getDescription();
//
// try
// {
// String id = "";
// if (description.startsWith(ImageRepository.IMG_ID_PREFIX))
// {
// id = description.substring(2);
// }
// else
// {
// File file = new File(description);
// FileInputStream in = new FileInputStream(file);
// id = ImageRepository.getInstance().addImage(in, file.getName());
// }
//
// sb.insert(0, "<img id=\""+ id +"\"/>");
// }
// catch (IOException e1)
// {
// e1.printStackTrace();
// Main.logThrowable("Error formatting image", e1);
// }
// }
else
{
for (int i = 0; i < e.getElementCount(); i++)
{
sb.append(getFormattedText(e.getElement(i),
startSelection, endSelection));
}
}
for (String name : stylesMap.keySet())
{
Object styleId = stylesMap.get(name);
if (hasStyle(e.getAttributes(), styleId))
{
sb.insert(0, "<"+name+">");
sb.append("</"+name+">");
}
}
return sb.toString();
}
private static String escape(String text)
{
return text.replaceAll("<", "<").replaceAll(">", ">");
}
private static String unescape(String text)
{
return text.replaceAll("<", "<").replaceAll(">", ">");
}
private static void setupStylesMap()
{
stylesMap.put("b", StyleConstants.Bold);
stylesMap.put("i", StyleConstants.Italic);
stylesMap.put("u", StyleConstants.Underline);
stylesMap.put("sub", StyleConstants.Subscript);
stylesMap.put("sup", StyleConstants.Superscript);
}
private void decode(StyledDocument doc, String text, int offset)
throws BadLocationException, ParseException
{
StringBuffer sb = new StringBuffer(text);
// Map<Integer, ImageIcon> images = decodeImages(doc, sb);
/*
* problem we need to decode the images first and remove the strings
* from the overall string, because pattern searching takes too long.
* this is problematic though, because decodeImages expects to be called
* afterwards.
*/
Matcher m = TEXT_PATTERN.matcher(sb);
int end = 0;
SimpleAttributeSet attr = new SimpleAttributeSet();
while (m.find())
{
String pretext = m.group(1);
String tag = m.group(2);
String unescapedPretext = unescape(pretext);
doc.insertString(offset, unescapedPretext, attr);
offset += unescapedPretext.length();
boolean style = true;
if (tag.startsWith("/"))
{
tag = tag.substring(1);
style = false;
}
Object styleId = stylesMap.get(tag);
attr.addAttribute(styleId, Boolean.valueOf(style));
end = m.end();
}
String restText = unescape(sb.substring(end));
doc.insertString(offset, restText, new SimpleAttributeSet());
// for (Entry<Integer, ImageIcon> entry : images.entrySet())
// {
// ImageIcon icon = entry.getValue();
// Integer iconOffset = entry.getKey();
// insertImage(doc, icon, iconOffset);
// }
}
// TODO move this back into decode
// private Map<Integer, ImageIcon> decodeImages(StyledDocument doc, StringBuffer text)
// throws BadLocationException, ParseException
// {
// Map<Integer, ImageIcon> images = new HashMap<Integer, ImageIcon>();
//
// Matcher m = IMG_PATTERN.matcher(text);
//
// while (m.find())
// {
// String id = m.group(1);
//
// int offset = m.start(0);
// text.replace(offset, m.end(0), "");
//
// ImageIcon img = ImageRepository.getInstance().getImage(id);
//
// if (img == null)
// throw new ParseException("Image with id "+id+" wasn't found in image repository.");
//
// images.put(offset, img);
// }
//
// return images;
// }
private static boolean hasStyle(AttributeSet attr, Object styleId)
{
Boolean style = (Boolean)attr.getAttribute(styleId);
return style != null && style.booleanValue();
}
}