From 44efddbd5c6c9d8cd811d4ae149cf1adc24b9dac Mon Sep 17 00:00:00 2001 From: Snowyfox Date: Mon, 16 May 2022 12:52:00 -0400 Subject: [PATCH] Added basic version of HTML text pane. --- BasicHTMLParser.java | 181 ++++++++++++++++++----------- PostWindow.java | 47 ++++---- RichTextPane3.java | 267 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 406 insertions(+), 89 deletions(-) create mode 100644 RichTextPane3.java 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); + } + +}