Added basic version of HTML text pane.

This commit is contained in:
Snowyfox 2022-05-16 12:52:00 -04:00
parent e4f13ad8c8
commit 44efddbd5c
3 changed files with 406 additions and 89 deletions

View File

@ -18,8 +18,8 @@ BasicHTMLParser {
Tree<String> document; Tree<String> document;
document = toNodes(segments); document = toNodes(segments);
document = splitText(document);
document = evaluateHtmlEscapes(document); document = evaluateHtmlEscapes(document);
document = distinguishEmojisFromText(document);
document = hierarchise(document); document = hierarchise(document);
return document; return document;
@ -136,6 +136,104 @@ BasicHTMLParser {
return returnee; return returnee;
} }
private static Tree<String>
splitText(Tree<String> nodes)
{
Tree<String> returnee = new Tree<>();
for (Tree<String> 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<String> 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<String> 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<String> 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<String> 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<String> addee = new Tree<>();
addee.key = space ? "space" : "text";
addee.value = empty(b);
returnee.add(addee);
}
}
return returnee;
}
private static Tree<String> private static Tree<String>
evaluateHtmlEscapes(Tree<String> nodes) evaluateHtmlEscapes(Tree<String> nodes)
{ {
@ -152,54 +250,6 @@ BasicHTMLParser {
return nodes; return nodes;
} }
private static Tree<String>
distinguishEmojisFromText(Tree<String> nodes)
{
Tree<String> returnee = new Tree<String>();
for (Tree<String> node: nodes)
{
if (!node.key.equals("text"))
{
returnee.add(node);
continue;
}
List<String> 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<String> text = new Tree<String>();
text.key = "text";
text.value = empty(b);
returnee.add(text);
Tree<String> emoji = new Tree<String>();
emoji.key = "emoji";
emoji.value = segment;
returnee.add(emoji);
}
else
{
b.append(segment);
}
}
if (b.length() > 0)
{
Tree<String> text = new Tree<String>();
text.key = "text";
text.value = empty(b);
returnee.add(text);
}
}
return returnee;
}
private static Tree<String> private static Tree<String>
hierarchise(Tree<String> nodes) hierarchise(Tree<String> nodes)
{ {
@ -264,27 +314,26 @@ BasicHTMLParser {
return s; return s;
} }
private static List<String> private static boolean
distinguishWhitespaceFromText(String text) isPunctuation(char c)
{ {
List<String> returnee = new ArrayList<>(); switch (Character.getType(c))
StringBuilder segment = new StringBuilder();
boolean inWhitespace = false;
for (char c: text.toCharArray())
{ {
boolean w = Character.isWhitespace(c); case Character.START_PUNCTUATION:
boolean change = w ^ inWhitespace; case Character.END_PUNCTUATION:
if (change) case Character.DASH_PUNCTUATION:
{ case Character.CONNECTOR_PUNCTUATION:
returnee.add(empty(segment)); case Character.INITIAL_QUOTE_PUNCTUATION:
inWhitespace = !inWhitespace; case Character.FINAL_QUOTE_PUNCTUATION:
} case Character.OTHER_PUNCTUATION: return true;
segment.append(c); default: return false;
} }
returnee.add(empty(segment)); }
return returnee; private static boolean
isLetter(char c)
{
return Character.isLetter(c) || isPunctuation(c);
} }
private static String private static String

View File

@ -39,6 +39,9 @@ import java.time.ZonedDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.HashMap;
class class
PostWindow extends JFrame { PostWindow extends JFrame {
@ -59,7 +62,10 @@ PostWindow extends JFrame {
display; display;
private static JFrame private static JFrame
temp; test;
private static RichTextPane3
test2;
// - -%- - // - -%- -
@ -101,28 +107,6 @@ PostWindow extends JFrame {
display.setEmojiUrls(post.emojiUrls); display.setEmojiUrls(post.emojiUrls);
{
Tree<String> html = BasicHTMLParser.parse(post.text);
Tree<String> 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.setHtml(post.text);
display.setFavourited(post.favourited); display.setFavourited(post.favourited);
display.setBoosted(post.boosted); display.setBoosted(post.boosted);
@ -139,6 +123,23 @@ PostWindow extends JFrame {
display.resetFocus(); display.resetFocus();
repaint(); 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<String, Image> emojis = new HashMap<>();
for (String[] entry: post.emojiUrls)
{
emojis.put(entry[0], ImageApi.remote(entry[1]));
}
test2.setEmojis(emojis);
} }
public void public void

267
RichTextPane3.java Normal file
View File

@ -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<String>
html;
private Map<String, Image>
emojis;
private Map<Tree<String>, Point>
layout;
// ---%-@-%---
public void
setText(Tree<String> 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<String, Image> emojis)
{
assert emojis != null;
this.emojis = emojis;
setText(html);
}
// - -%- -
private void
layout(Tree<String> 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<String> 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<String> 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<String> 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<String> 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<String>());
this.addComponentListener(this);
}
}