From e4f13ad8c8d365a456d8d7cf659c91fa13a6fcff Mon Sep 17 00:00:00 2001 From: Snowyfox Date: Sat, 14 May 2022 18:04:46 -0400 Subject: [PATCH] Added attempt at using AttributedString. Somewhat works now, but I think we'll abandon it.. --- BasicHTMLParser.java | 132 +++++----- ClipboardApi.java | 0 ComposeWindow.java | 0 ImageApi.java | 0 ImageWindow.java | 0 JKomasto.java | 0 KDE_Dialog_Appear.wav | Bin LoginWindow.java | 0 MastodonApi.java | 12 +- NotificationsWindow.java | 0 PostWindow.java | 27 ++- ProfileWindow.java | 0 RepliesWindow.java | 0 RequestListener.java | 0 RichTextPane.java | 0 RichTextPane2.java | 480 +++++++++++++++++++++++++++++++++++++ RudimentaryHTMLParser.java | 0 TimelineWindow.java | 0 TwoToggleButton.java | 0 WindowUpdater.java | 0 notifOptions.txt | 0 notifOptions.txt~ | 0 run | 7 +- 23 files changed, 585 insertions(+), 73 deletions(-) mode change 100755 => 100644 ClipboardApi.java mode change 100755 => 100644 ComposeWindow.java mode change 100755 => 100644 ImageApi.java mode change 100755 => 100644 ImageWindow.java mode change 100755 => 100644 JKomasto.java mode change 100755 => 100644 KDE_Dialog_Appear.wav mode change 100755 => 100644 LoginWindow.java mode change 100755 => 100644 MastodonApi.java mode change 100755 => 100644 NotificationsWindow.java mode change 100755 => 100644 PostWindow.java mode change 100755 => 100644 ProfileWindow.java mode change 100755 => 100644 RepliesWindow.java mode change 100755 => 100644 RequestListener.java mode change 100755 => 100644 RichTextPane.java create mode 100644 RichTextPane2.java mode change 100755 => 100644 RudimentaryHTMLParser.java mode change 100755 => 100644 TimelineWindow.java mode change 100755 => 100644 TwoToggleButton.java mode change 100755 => 100644 WindowUpdater.java mode change 100755 => 100644 notifOptions.txt mode change 100755 => 100644 notifOptions.txt~ diff --git a/BasicHTMLParser.java b/BasicHTMLParser.java index d55c2eb..fc5009e 100644 --- a/BasicHTMLParser.java +++ b/BasicHTMLParser.java @@ -15,10 +15,10 @@ BasicHTMLParser { { List segments; segments = distinguishTagsFromPcdata(html); - segments = evaluateHtmlEscapes(segments); - + Tree document; document = toNodes(segments); + document = evaluateHtmlEscapes(document); document = distinguishEmojisFromText(document); document = hierarchise(document); @@ -61,51 +61,6 @@ BasicHTMLParser { return returnee; } - private static List - evaluateHtmlEscapes(List strings) - { - List returnee = new ArrayList<>(); - - for (String string: strings) - { - StringBuilder whole = new StringBuilder(); - StringBuilder part = new StringBuilder(); - boolean inEscape = false; - for (char c: string.toCharArray()) - { - if (inEscape && c == ';') - { - part.append(c); - inEscape = false; - String v = empty(part); - if (v.equals("<")) part.append('<'); - if (v.equals(">")) part.append('>'); - if (v.equals("&")) part.append('&'); - if (v.equals(""")) part.append('"'); - if (v.equals("'")) part.append('\''); - if (v.equals("'")) part.append('\''); - } - else if (!inEscape && c == '&') - { - String v = empty(part); - if (!v.isEmpty()) whole.append(v); - part.append(c); - inEscape = true; - } - else - { - part.append(c); - } - } - String v = empty(part); - if (!v.isEmpty()) whole.append(v); - - returnee.add(empty(whole)); - } - - return returnee; - } - private static Tree toNodes(List segments) { @@ -181,6 +136,22 @@ BasicHTMLParser { return returnee; } + private static Tree + evaluateHtmlEscapes(Tree nodes) + { + for (Tree node: nodes) + { + node.value = evaluateHtmlEscapes(node.value); + for (Tree attr: node) + { + attr.key = evaluateHtmlEscapes(attr.key); + attr.value = evaluateHtmlEscapes(attr.value); + } + } + + return nodes; + } + private static Tree distinguishEmojisFromText(Tree nodes) { @@ -233,10 +204,10 @@ BasicHTMLParser { hierarchise(Tree nodes) { Tree root = new Tree(); - root.add(new Tree<>("attributes", null)); - root.get(0).add(new Tree<>("html", null)); + root.key = "tag"; + root.add(new Tree<>("html", null)); root.add(new Tree<>("children", null)); - + Deque> parents = new LinkedList<>(); parents.push(root); for (Tree node: nodes) @@ -246,6 +217,9 @@ BasicHTMLParser { assert node.size() > 0; String tagName = node.get(0).key; + assert node.get("children") == null; + node.add(new Tree<>("children", null)); + boolean isClosing, selfClosing; isClosing = tagName.startsWith("/"); selfClosing = node.get("/") != null; @@ -258,30 +232,18 @@ BasicHTMLParser { parent = parents.pop(); grandparent = parents.peek(); - assert tagName.equals( - "/" - + parent.get("attributes").get(0).key - ); + String pTagName = parent.get(0).key; + assert tagName.equals("/" + pTagName); grandparent.get("children").add(parent); } else if (selfClosing) { - Tree elem = new Tree(); - node.key = "attributes"; - elem.add(node); - elem.add(new Tree<>("children", null)); - - parents.peek().get("children").add(elem); + parents.peek().get("children").add(node); } else { - Tree elem = new Tree(); - node.key = "attributes"; - elem.add(node); - elem.add(new Tree<>("children", null)); - - parents.push(elem); + parents.push(node); } } else @@ -325,4 +287,42 @@ BasicHTMLParser { return returnee; } + private static String + evaluateHtmlEscapes(String string) + { + if (string == null) return string; + + StringBuilder whole = new StringBuilder(); + StringBuilder part = new StringBuilder(); + boolean inEscape = false; + for (char c: string.toCharArray()) + { + if (inEscape && c == ';') + { + part.append(c); + inEscape = false; + String v = empty(part); + if (v.equals("<")) part.append('<'); + if (v.equals(">")) part.append('>'); + if (v.equals("&")) part.append('&'); + if (v.equals(""")) part.append('"'); + if (v.equals("'")) part.append('\''); + if (v.equals("'")) part.append('\''); + } + else if (!inEscape && c == '&') + { + String v = empty(part); + if (!v.isEmpty()) whole.append(v); + part.append(c); + inEscape = true; + } + else + { + part.append(c); + } + } + String v = empty(part); + if (!v.isEmpty()) whole.append(v); + return whole.toString(); + } } \ No newline at end of file diff --git a/ClipboardApi.java b/ClipboardApi.java old mode 100755 new mode 100644 diff --git a/ComposeWindow.java b/ComposeWindow.java old mode 100755 new mode 100644 diff --git a/ImageApi.java b/ImageApi.java old mode 100755 new mode 100644 diff --git a/ImageWindow.java b/ImageWindow.java old mode 100755 new mode 100644 diff --git a/JKomasto.java b/JKomasto.java old mode 100755 new mode 100644 diff --git a/KDE_Dialog_Appear.wav b/KDE_Dialog_Appear.wav old mode 100755 new mode 100644 diff --git a/LoginWindow.java b/LoginWindow.java old mode 100755 new mode 100644 diff --git a/MastodonApi.java b/MastodonApi.java old mode 100755 new mode 100644 index 137af65..3810092 --- a/MastodonApi.java +++ b/MastodonApi.java @@ -581,15 +581,23 @@ MastodonApi { debugPrint(Tree tree, String prefix) { System.err.print(prefix); - System.err.print(tree.key); + System.err.print(deescape(tree.key)); System.err.print(": "); - System.err.println(tree.value); + System.err.println(deescape(tree.value)); for (Tree child: tree) debugPrint(child, prefix + " "); } // - -%- - + private static String + deescape(String string) + { + if (string == null) return string; + string = string.replaceAll("\n", "\\\\n"); + return string; + } + private static Tree fromPlain(Reader r) throws IOException diff --git a/NotificationsWindow.java b/NotificationsWindow.java old mode 100755 new mode 100644 diff --git a/PostWindow.java b/PostWindow.java old mode 100755 new mode 100644 index 19e08fb..5a8d501 --- a/PostWindow.java +++ b/PostWindow.java @@ -58,6 +58,9 @@ PostWindow extends JFrame { private PostComponent display; + private static JFrame + temp; + // - -%- - private static final DateTimeFormatter @@ -98,6 +101,28 @@ 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); @@ -428,8 +453,6 @@ implements ActionListener { public void setHtml(String n) { - BasicHTMLParser.parse(n); - RichTextPane.Builder b = new RichTextPane.Builder(); Tree nodes = RudimentaryHTMLParser.depthlessRead(n); for (Tree node: nodes) diff --git a/ProfileWindow.java b/ProfileWindow.java old mode 100755 new mode 100644 diff --git a/RepliesWindow.java b/RepliesWindow.java old mode 100755 new mode 100644 diff --git a/RequestListener.java b/RequestListener.java old mode 100755 new mode 100644 diff --git a/RichTextPane.java b/RichTextPane.java old mode 100755 new mode 100644 diff --git a/RichTextPane2.java b/RichTextPane2.java new file mode 100644 index 0000000..64571ac --- /dev/null +++ b/RichTextPane2.java @@ -0,0 +1,480 @@ + +import javax.swing.JComponent; +import java.text.AttributedString; +import java.text.AttributedCharacterIterator; +import java.awt.Graphics; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Image; +import java.awt.geom.Rectangle2D; +import java.awt.font.TextAttribute; +import java.awt.event.ComponentListener; +import java.awt.event.ComponentEvent; +import java.util.List; +import java.util.ArrayList; +import cafe.biskuteri.hinoki.Tree; + +class +RichTextPane2 extends JComponent +implements ComponentListener { + + private AttributedString + text; + +// ---%-@-%--- + + public void + setText(Tree html, Tree emojiMap) + { + Tree commands = turnIntoCommands(html); + + class AStrSegment { + String text; + int offset; + Object[] values = new Object[Attribute.COUNT]; + /* + { + values[3] = (Boolean)true; + values[4] = (Integer)0; + values[5] = (Boolean)true; + values[6] = (Boolean)false; + } + */ + } + List segments = new ArrayList<>(); + + int offset = 0; + for (Tree command: commands) + { + if (command.key.equals("text")) + { + StringBuilder b = new StringBuilder(); + Boolean cibl = null; + Boolean cwhi = null; + for (char c: command.value.toCharArray()) + { + Boolean ibl = isBasicLatin(c); + Boolean whi = Character.isWhitespace(c); + if (!ibl.equals(cibl) || !whi.equals(cwhi)) + { + if (b.length() > 0) + { + assert cibl != null && cwhi != null; + AStrSegment s = new AStrSegment(); + s.offset = offset; + s.text = b.toString(); + s.values[3] = cibl; + s.values[6] = cwhi; + segments.add(s); + offset += s.text.length(); + b.delete(0, b.length()); + } + cibl = ibl; + cwhi = whi; + } + + b.append(c); + } + if (b.length() > 0) + { + AStrSegment s = new AStrSegment(); + s.offset = offset; + s.text = b.toString(); + s.values[3] = cibl; + s.values[6] = cwhi; + segments.add(s); + offset += s.text.length(); + } + } + else if (command.key.equals("emoji")) + { + AStrSegment s = new AStrSegment(); + s.offset = offset; + s.values[3] = true; + s.values[6] = false; + + String shortcode = command.value; + String url = null; + Tree m = emojiMap.get(shortcode); + if (m != null) url = m.value; + Image img = ImageApi.remote(url); + if (img != null) + { + s.text = " "; + s.values[0] = img; + s.values[1] = shortcode; + segments.add(s); + offset += 1; + } + else + { + s.text = shortcode; + s.values[0] = null; + s.values[1] = null; + segments.add(s); + offset += shortcode.length(); + } + } + else if (command.key.equals("link")) + { + AStrSegment s = new AStrSegment(); + s.offset = offset; + s.text = command.value; + s.values[2] = command.get("url").value; + s.values[3] = true; + s.values[6] = false; + /* + * Technically we're supposed to treat + * the anchor text like a text node. + * As in, it could be non-Basic-Latin.. + * I'll be Mastodon-specific again, and + * assume it's a URL or some @ string. + */ + } + } + + AttributedString astr; + StringBuilder b = new StringBuilder(); + for (AStrSegment segment: segments) + { + b.append(segment.text); + } + astr = new AttributedString(b.toString()); + for (AStrSegment segment: segments) + { + Object[] v = segment.values; + astr.addAttribute( + Attribute.IMAGE, segment.values[0], + segment.offset, + segment.offset + segment.text.length() + ); + astr.addAttribute( + Attribute.ALT, segment.values[1], + segment.offset, + segment.offset + segment.text.length() + ); + astr.addAttribute( + Attribute.LINK, segment.values[2], + segment.offset, + segment.offset + segment.text.length() + ); + astr.addAttribute( + Attribute.BASICLATIN, segment.values[3], + segment.offset, + segment.offset + segment.text.length() + ); + astr.addAttribute( + Attribute.Y, segment.values[4], + segment.offset, + segment.offset + segment.text.length() + ); + astr.addAttribute( + Attribute.OFFSCREEN, segment.values[5], + segment.offset, + segment.offset + segment.text.length() + ); + astr.addAttribute( + Attribute.WHITESPACE, segment.values[6], + segment.offset, + segment.offset + segment.text.length() + ); + } + + this.text = astr; + componentResized(null); + } + +// - -%- - + + public void + componentResized(ComponentEvent eC) + { + int w = getWidth(), h = getHeight(); + + // We're going to evaluate the + // line and off-screen attributes. + + FontMetrics fm = getFontMetrics(getFont()); + Graphics g = getGraphics(); + int x = 0, y = fm.getAscent(); + + AttributedCharacterIterator it; + it = text.getIterator(); + + while (it.getIndex() < it.getEndIndex()) + { + int start = it.getIndex(); + int end = it.getRunLimit(); + + Image img = (Image) + it.getAttribute(Attribute.IMAGE); + Boolean ibl = (Boolean) + it.getAttribute(Attribute.BASICLATIN); + Boolean whi = (Boolean) + it.getAttribute(Attribute.WHITESPACE); + + assert ibl != null; + assert whi != null; + + if (img != null) + { + int ow = img.getWidth(this); + int oh = img.getHeight(this); + int nh = fm.getAscent() + fm.getDescent(); + int nw = ow * nh/oh; + if (x + nw > w) + { + y += fm.getAscent() + fm.getDescent(); + x = nw; + } + text.addAttribute( + Attribute.Y, (Integer)y, + start, end + ); + text.addAttribute( + Attribute.OFFSCREEN, (Boolean)(y > h), + start, end + ); + it.setIndex(end); + } + else + { + int p, xOff = 0; + for (p = end; p > start; --p) + { + Rectangle2D r; + r = fm.getStringBounds(it, start, p, g); + xOff = (int)r.getWidth(); + if (x + xOff < w) break; + } + if (p == end || whi) + { + x += xOff; + text.addAttribute( + Attribute.Y, (Integer)y, + start, end + ); + text.addAttribute( + Attribute.OFFSCREEN, (Boolean)(y > h), + start, end + ); + it.setIndex(end); + } + else if (p <= start) + { + y += fm.getAscent() + fm.getDescent(); + x = xOff; + text.addAttribute( + Attribute.Y, (Integer)y, + start, end + ); + text.addAttribute( + Attribute.OFFSCREEN, (Boolean)(y > h), + start, end + ); + it.setIndex(end); + } + else + { + text.addAttribute( + Attribute.Y, (Integer)y, + start, p + ); + text.addAttribute( + Attribute.OFFSCREEN, (Boolean)(y > h), + start, p + ); + y += fm.getAscent() + fm.getDescent(); + x = 0; + it.setIndex(p); + } + } + } + + text.addAttribute(TextAttribute.FONT, getFont()); + + repaint(); + } + + protected void + paintComponent(Graphics g) + { + int w = getWidth(), h = getHeight(); + g.clearRect(0, 0, w, h); + + FontMetrics fm = g.getFontMetrics(); + + AttributedCharacterIterator it; + it = text.getIterator(); + + ((java.awt.Graphics2D)g).setRenderingHint( + java.awt.RenderingHints.KEY_ANTIALIASING, + java.awt.RenderingHints.VALUE_ANTIALIAS_ON + ); + + int x = 0, y = fm.getAscent(); + while (it.getIndex() < it.getEndIndex()) + { + int start = it.getIndex(); + int end = it.getRunLimit(); + + Image img = (Image) + it.getAttribute(Attribute.IMAGE); + Boolean ibl = (Boolean) + it.getAttribute(Attribute.BASICLATIN); + Integer ny = (Integer) + it.getAttribute(Attribute.Y); + + if (ny > y) + { + y = ny; + x = 0; + } + + if (img != null) + { + int ow = img.getWidth(this); + int oh = img.getHeight(this); + int nh = fm.getAscent() + fm.getDescent(); + int nw = ow * nh/oh; + int iy = y + fm.getDescent() - nh; + g.drawImage(img, x, iy, nw, nh, this); + x += nw; + } + else + { + Rectangle2D r; + r = fm.getStringBounds(it, start, end, g); + AttributedCharacterIterator sit; + sit = text.getIterator(null, start, end); + g.drawString(sit, x, y); + x += (int)r.getWidth(); + } + it.setIndex(end); + } + } + + public void + componentMoved(ComponentEvent eC) { } + + public void + componentShown(ComponentEvent eC) { } + + public void + componentHidden(ComponentEvent eC) { } + +// - -%- - + + private static Boolean + isBasicLatin(char c) + { + return true; + } + + private static String + toText(Tree node) + { + Tree children = node.get("children"); + if (children == null) + { + boolean text = node.key.equals("text"); + boolean emoji = node.key.equals("emoji"); + assert text || emoji; + return node.value; + } + + StringBuilder b = new StringBuilder(); + for (Tree child: children) + { + b.append(toText(child)); + } + return b.toString(); + } + + private static Tree + turnIntoCommands(Tree tag) + { + assert tag.key.equals("tag"); + Tree returnee = new Tree(); + + String tagName = tag.get(0).key; + Tree children = tag.get("children"); + + if (tagName.equals("a")) + { + String url = tag.get("href").value; + Tree addee = new Tree<>(); + addee.key = "link"; + addee.value = toText(tag); + addee.add(new Tree<>("url", url)); + returnee.add(addee); + } + else if (tagName.equals("span")) + { + Tree addee = new Tree<>(); + addee.key = "text"; + addee.value = toText(tag); + returnee.add(addee); + } + else if (tagName.equals("br")) + { + returnee.add(new Tree<>("text", "\n")); + } + else + { + for (Tree child: children) + { + if (!child.key.equals("tag")) + { + returnee.add(child); + continue; + } + child = turnIntoCommands(child); + for (Tree command: child) + { + returnee.add(command); + } + } + if (tagName.equals("p")) + { + returnee.add(new Tree<>("text", "\n")); + returnee.add(new Tree<>("text", "\n")); + } + } + + return returnee; + } + +// ---%-@-%--- + + public static class + Attribute extends AttributedCharacterIterator.Attribute { + + public static final Attribute + IMAGE = new Attribute("IMAGE"), + ALT = new Attribute("ALT"), + LINK = new Attribute("LINK"), + BASICLATIN = new Attribute("BASICLATIN"), + Y = new Attribute("Y"), + OFFSCREEN = new Attribute("OFFSCREEN"), + WHITESPACE = new Attribute("WHITESPACE"); + + public static final int + COUNT = 7; + +// -=%=- + + private + Attribute(String name) { super(name); } + + } + +// ---%-@-%--- + + RichTextPane2() + { + this.addComponentListener(this); + text = new AttributedString(""); + } + +} \ No newline at end of file diff --git a/RudimentaryHTMLParser.java b/RudimentaryHTMLParser.java old mode 100755 new mode 100644 diff --git a/TimelineWindow.java b/TimelineWindow.java old mode 100755 new mode 100644 diff --git a/TwoToggleButton.java b/TwoToggleButton.java old mode 100755 new mode 100644 diff --git a/WindowUpdater.java b/WindowUpdater.java old mode 100755 new mode 100644 diff --git a/notifOptions.txt b/notifOptions.txt old mode 100755 new mode 100644 diff --git a/notifOptions.txt~ b/notifOptions.txt~ old mode 100755 new mode 100644 diff --git a/run b/run index 5920a19..3cf3ccb 100755 --- a/run +++ b/run @@ -1,13 +1,14 @@ #!/usr/bin/make -f CLASSPATH=.:../Hinoki:/usr/share/java/javax.json.jar -OPTIONS= +COMPILE_OPTIONS=-Xlint:deprecation +RUNTIME_OPTIONS=-ea c: - javac -cp $(CLASSPATH) $(OPTIONS) *.java + javac -cp $(CLASSPATH) $(COMPILE_OPTIONS) *.java r: - java -cp $(CLASSPATH) $(OPTIONS) -ea JKomasto + java -cp $(CLASSPATH) $(RUNTIME_OPTIONS) JKomasto cr: c r