diff --git a/BasicHTMLParser.java b/BasicHTMLParser.java
index fc5009e..3d70e0e 100644
--- a/BasicHTMLParser.java
+++ b/BasicHTMLParser.java
@@ -18,8 +18,8 @@ BasicHTMLParser {
Tree document;
document = toNodes(segments);
+ document = splitText(document);
document = evaluateHtmlEscapes(document);
- document = distinguishEmojisFromText(document);
document = hierarchise(document);
return document;
@@ -136,6 +136,104 @@ BasicHTMLParser {
return returnee;
}
+ private static Tree
+ splitText(Tree nodes)
+ {
+ Tree returnee = new Tree<>();
+
+ for (Tree node: nodes)
+ {
+ if (node.key.equals("tag"))
+ {
+ returnee.add(node);
+ continue;
+ }
+ assert node.key.equals("text");
+
+ StringBuilder b = new StringBuilder();
+ boolean letter = false, cletter;
+ boolean space = false, cspace;
+ boolean emoji = false;
+ for (char c: node.value.toCharArray())
+ {
+ cletter = isLetter(c);
+ cspace = Character.isWhitespace(c);
+
+ if (c == ':' && !emoji && !letter)
+ {
+ if (b.length() > 0)
+ {
+ Tree addee = new Tree<>();
+ addee.key = space ? "space" : "text";
+ addee.value = empty(b);
+ returnee.add(addee);
+ }
+ emoji = true;
+ b.append(c);
+ }
+ else if (c == ':' && emoji)
+ {
+ assert letter;
+ b.append(c);
+ Tree addee = new Tree<>();
+ addee.key = "emoji";
+ addee.value = empty(b);
+ returnee.add(addee);
+ /*
+ * Technically, addee.value.length()
+ * could be zero, which probably means
+ * someone just put two colons in a row,
+ * maybe for Haskell source code. I'd
+ * be surprised if Mastodon didn't escape
+ * it. (If they did, the next step will
+ * handle them.) Anyways treating it as
+ * an empty emoji is the correct action.
+ */
+ emoji = false;
+ cletter = false;
+ }
+ else if (cspace && letter)
+ {
+ assert b.length() > 0;
+ Tree addee = new Tree<>();
+ addee.key = "text";
+ addee.value = empty(b);
+ returnee.add(addee);
+ b.append(c);
+ }
+ else if (cletter && space)
+ {
+ assert b.length() > 0;
+ Tree addee = new Tree<>();
+ addee.key = "space";
+ addee.value = empty(b);
+ returnee.add(addee);
+ b.append(c);
+ }
+ else
+ {
+ b.append(c);
+ }
+ /*
+ * We can specially handle special
+ * characters like \n, but I'll opt not to.
+ */
+
+ letter = cletter;
+ space = cspace;
+ }
+ if (b.length() > 0)
+ {
+ Tree addee = new Tree<>();
+ addee.key = space ? "space" : "text";
+ addee.value = empty(b);
+ returnee.add(addee);
+ }
+ }
+
+ return returnee;
+ }
+
private static Tree
evaluateHtmlEscapes(Tree nodes)
{
@@ -152,54 +250,6 @@ BasicHTMLParser {
return nodes;
}
- private static Tree
- distinguishEmojisFromText(Tree nodes)
- {
- Tree returnee = new Tree();
-
- for (Tree node: nodes)
- {
- if (!node.key.equals("text"))
- {
- returnee.add(node);
- continue;
- }
-
- List segments;
- segments = distinguishWhitespaceFromText(node.value);
- StringBuilder b = new StringBuilder();
- for (String segment: segments)
- {
- boolean starts = segment.startsWith(":");
- boolean ends = segment.endsWith(":");
- if (starts && ends)
- {
- Tree text = new Tree();
- text.key = "text";
- text.value = empty(b);
- returnee.add(text);
- Tree emoji = new Tree();
- emoji.key = "emoji";
- emoji.value = segment;
- returnee.add(emoji);
- }
- else
- {
- b.append(segment);
- }
- }
- if (b.length() > 0)
- {
- Tree text = new Tree();
- text.key = "text";
- text.value = empty(b);
- returnee.add(text);
- }
- }
-
- return returnee;
- }
-
private static Tree
hierarchise(Tree nodes)
{
@@ -264,27 +314,26 @@ BasicHTMLParser {
return s;
}
- private static List
- distinguishWhitespaceFromText(String text)
+ private static boolean
+ isPunctuation(char c)
{
- List returnee = new ArrayList<>();
-
- StringBuilder segment = new StringBuilder();
- boolean inWhitespace = false;
- for (char c: text.toCharArray())
+ switch (Character.getType(c))
{
- boolean w = Character.isWhitespace(c);
- boolean change = w ^ inWhitespace;
- if (change)
- {
- returnee.add(empty(segment));
- inWhitespace = !inWhitespace;
- }
- segment.append(c);
+ case Character.START_PUNCTUATION:
+ case Character.END_PUNCTUATION:
+ case Character.DASH_PUNCTUATION:
+ case Character.CONNECTOR_PUNCTUATION:
+ case Character.INITIAL_QUOTE_PUNCTUATION:
+ case Character.FINAL_QUOTE_PUNCTUATION:
+ case Character.OTHER_PUNCTUATION: return true;
+ default: return false;
}
- returnee.add(empty(segment));
+ }
- return returnee;
+ private static boolean
+ isLetter(char c)
+ {
+ return Character.isLetter(c) || isPunctuation(c);
}
private static String
diff --git a/PostWindow.java b/PostWindow.java
index 5a8d501..0166e2d 100644
--- a/PostWindow.java
+++ b/PostWindow.java
@@ -39,6 +39,9 @@ import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
+import java.util.Map;
+import java.util.HashMap;
+
class
PostWindow extends JFrame {
@@ -59,7 +62,10 @@ PostWindow extends JFrame {
display;
private static JFrame
- temp;
+ test;
+
+ private static RichTextPane3
+ test2;
// - -%- -
@@ -101,28 +107,6 @@ PostWindow extends JFrame {
display.setEmojiUrls(post.emojiUrls);
- {
- Tree html = BasicHTMLParser.parse(post.text);
- Tree emojiMap = new Tree<>();
- for (String[] m: post.emojiUrls)
- {
- emojiMap.add(new Tree<>(m[0], m[1]));
- }
-
- if (temp == null)
- {
- temp = new JFrame();
- temp.setSize(256, 256);
- temp.setLocationByPlatform(true);
- temp.setVisible(true);
- RichTextPane2 pane = new RichTextPane2();
- pane.setFont(new Font("Dialog", Font.PLAIN, 18));
- temp.setContentPane(pane);
- }
- ((RichTextPane2)temp.getContentPane())
- .setText(html, emojiMap);
- }
-
display.setHtml(post.text);
display.setFavourited(post.favourited);
display.setBoosted(post.boosted);
@@ -139,6 +123,23 @@ PostWindow extends JFrame {
display.resetFocus();
repaint();
+
+ if (test == null)
+ {
+ test = new JFrame();
+ test.setSize(340, 256);
+ test2 = new RichTextPane3();
+ test2.setFont(new Font("Dialog", Font.PLAIN, 18));
+ test.setContentPane(test2);
+ test.setVisible(true);
+ }
+ test2.setText(BasicHTMLParser.parse(post.text));
+ Map emojis = new HashMap<>();
+ for (String[] entry: post.emojiUrls)
+ {
+ emojis.put(entry[0], ImageApi.remote(entry[1]));
+ }
+ test2.setEmojis(emojis);
}
public void
diff --git a/RichTextPane3.java b/RichTextPane3.java
new file mode 100644
index 0000000..9cda6f5
--- /dev/null
+++ b/RichTextPane3.java
@@ -0,0 +1,267 @@
+
+import javax.swing.JComponent;
+import java.awt.Graphics;
+import java.awt.Point;
+import java.awt.FontMetrics;
+import java.awt.Font;
+import java.awt.Image;
+import java.awt.event.ComponentListener;
+import java.awt.event.ComponentEvent;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.List;
+import java.util.LinkedList;
+import cafe.biskuteri.hinoki.Tree;
+
+class
+RichTextPane3 extends JComponent
+implements ComponentListener {
+
+ private Tree
+ html;
+
+ private Map
+ emojis;
+
+ private Map, Point>
+ layout;
+
+// ---%-@-%---
+
+ public void
+ setText(Tree html)
+ {
+ assert html != null;
+
+ this.html = html;
+
+ if (!isValid()) return;
+
+ FontMetrics fm = getFontMetrics(getFont());
+ int iy = fm.getAscent();
+ int fph = (fm.getAscent() + fm.getDescent()) * 3/2;
+ Point cursor = new Point(0, iy - fph);
+ layout.clear();
+ layout(html, fm, cursor);
+ repaint();
+ }
+
+ public void
+ setEmojis(Map emojis)
+ {
+ assert emojis != null;
+ this.emojis = emojis;
+ setText(html);
+ }
+
+// - -%- -
+
+ private void
+ layout(Tree node, FontMetrics fm, Point cursor)
+ {
+ int lh = fm.getAscent() + fm.getDescent();
+
+ if (node.key.equals("space"))
+ {
+ int w = fm.stringWidth(node.value);
+ if (cursor.x + w < getWidth())
+ {
+ layout.put(node, new Point(cursor));
+ cursor.x += w;
+ }
+ else
+ {
+ layout.put(node, new Point(cursor));
+ cursor.y += lh;
+ cursor.x = 0;
+ }
+ }
+ else if (node.key.equals("text"))
+ {
+ int w = fm.stringWidth(node.value);
+ if (cursor.x + w < getWidth())
+ {
+ layout.put(node, new Point(cursor));
+ cursor.x += w;
+ }
+ else if (w < getWidth())
+ {
+ cursor.y += lh;
+ cursor.x = 0;
+ layout.put(node, new Point(cursor));
+ cursor.x += w;
+ }
+ else
+ {
+ StringBuilder rem = new StringBuilder();
+ rem.append(node.value);
+ int mw = getWidth();
+ int aw = mw - cursor.x;
+
+ w = fm.charWidth(node.value.charAt(0));
+ if (w >= aw)
+ {
+ cursor.y += lh;
+ cursor.x = 0;
+ }
+
+ while (rem.length() > 0)
+ {
+ int l = 2;
+ for (; l <= rem.length(); ++l)
+ {
+ String substr = rem.substring(0, l);
+ w = fm.stringWidth(substr);
+ if (w >= aw) break;
+ }
+ String substr = rem.substring(0, --l);
+ w = fm.stringWidth(substr);
+
+ Tree temp = new Tree<>();
+ temp.key = node.key;
+ temp.value = substr;
+ layout.put(temp, new Point(cursor));
+
+ rem.delete(0, l);
+ if (rem.length() == 0)
+ {
+ cursor.x = w;
+ }
+ else
+ {
+ cursor.y += lh;
+ cursor.x = 0;
+ }
+ aw = mw;
+ }
+ }
+ }
+ else if (node.key.equals("emoji"))
+ {
+ Image image = emojis.get(node.value);
+ int w;
+ if (image != null)
+ {
+ int ow = image.getWidth(this);
+ int oh = image.getHeight(this);
+ int h = lh;
+ w = ow * h/oh;
+ }
+ else
+ {
+ w = fm.stringWidth(node.value);
+ }
+ if (cursor.x + w < getWidth())
+ {
+ layout.put(node, new Point(cursor));
+ cursor.x += w;
+ }
+ else
+ {
+ cursor.y += lh;
+ cursor.x = 0;
+ layout.put(node, new Point(cursor));
+ cursor.x += w;
+ }
+ }
+ else if (node.key.equals("tag"))
+ {
+ String tagName = node.get(0).key;
+ Tree children = node.get("children");
+
+ if (tagName.equals("br"))
+ {
+ layout.put(node, new Point(cursor));
+ cursor.y += lh;
+ cursor.x = 0;
+ }
+ else if (tagName.equals("p"))
+ {
+ layout.put(node, new Point(cursor));
+ cursor.y += lh * 3/2;
+ cursor.x = 0;
+ }
+ else if (tagName.equals("a"))
+ {
+ layout.put(node, new Point(cursor));
+ // For now we ignore the link.
+ }
+ else if (tagName.equals("span"))
+ {
+ layout.put(node, new Point(cursor));
+ }
+
+ for (Tree child: children)
+ {
+ layout(child, fm, cursor);
+ }
+ }
+ else assert false;
+ }
+
+ protected void
+ paintComponent(Graphics g)
+ {
+ g.setFont(getFont());
+ FontMetrics fm = g.getFontMetrics();
+
+ ((java.awt.Graphics2D)g).setRenderingHint(
+ java.awt.RenderingHints.KEY_ANTIALIASING,
+ java.awt.RenderingHints.VALUE_ANTIALIAS_ON
+ );
+
+ for (Tree node: layout.keySet())
+ {
+ if (node.key.equals("text"))
+ {
+ Point point = layout.get(node);
+ g.drawString(node.value, point.x, point.y);
+ }
+ else if (node.key.equals("emoji"))
+ {
+ Point point = layout.get(node);
+ Image image = emojis.get(node.value);
+ if (image != null)
+ {
+ int ow = image.getWidth(this);
+ int oh = image.getHeight(this);
+ int nh = fm.getAscent() + fm.getDescent();
+ int nw = ow * nh/oh;
+ int x = point.x;
+ int y = point.y - fm.getAscent();
+ g.drawImage(image, x, y, nw, nh, this);
+ }
+ else
+ {
+ int x = point.x;
+ int y = point.y;
+ g.drawString(node.value, x, y);
+ }
+ }
+ else continue;
+ }
+ }
+
+ public void
+ componentResized(ComponentEvent eC) { setText(html); }
+
+ public void
+ componentMoved(ComponentEvent eC) { }
+
+ public void
+ componentShown(ComponentEvent eC) { }
+
+ public void
+ componentHidden(ComponentEvent eC) { }
+
+// ---%-@-%---
+
+ RichTextPane3()
+ {
+ layout = new HashMap<>();
+ emojis = new HashMap<>();
+ setText(new Tree());
+ this.addComponentListener(this);
+ }
+
+}