Added attempt at using AttributedString.

Somewhat works now, but I think we'll abandon it..
This commit is contained in:
Snowyfox 2022-05-14 18:04:46 -04:00
parent 71b9c496c4
commit e4f13ad8c8
23 changed files with 585 additions and 73 deletions

View File

@ -15,10 +15,10 @@ BasicHTMLParser {
{
List<String> segments;
segments = distinguishTagsFromPcdata(html);
segments = evaluateHtmlEscapes(segments);
Tree<String> document;
document = toNodes(segments);
document = evaluateHtmlEscapes(document);
document = distinguishEmojisFromText(document);
document = hierarchise(document);
@ -61,51 +61,6 @@ BasicHTMLParser {
return returnee;
}
private static List<String>
evaluateHtmlEscapes(List<String> strings)
{
List<String> 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("&lt;")) part.append('<');
if (v.equals("&gt;")) part.append('>');
if (v.equals("&amp;")) part.append('&');
if (v.equals("&quot;")) part.append('"');
if (v.equals("&apos;")) part.append('\'');
if (v.equals("&#39;")) 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<String>
toNodes(List<String> segments)
{
@ -181,6 +136,22 @@ BasicHTMLParser {
return returnee;
}
private static Tree<String>
evaluateHtmlEscapes(Tree<String> nodes)
{
for (Tree<String> node: nodes)
{
node.value = evaluateHtmlEscapes(node.value);
for (Tree<String> attr: node)
{
attr.key = evaluateHtmlEscapes(attr.key);
attr.value = evaluateHtmlEscapes(attr.value);
}
}
return nodes;
}
private static Tree<String>
distinguishEmojisFromText(Tree<String> nodes)
{
@ -233,10 +204,10 @@ BasicHTMLParser {
hierarchise(Tree<String> nodes)
{
Tree<String> root = new Tree<String>();
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<Tree<String>> parents = new LinkedList<>();
parents.push(root);
for (Tree<String> 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<String> elem = new Tree<String>();
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<String> elem = new Tree<String>();
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("&lt;")) part.append('<');
if (v.equals("&gt;")) part.append('>');
if (v.equals("&amp;")) part.append('&');
if (v.equals("&quot;")) part.append('"');
if (v.equals("&apos;")) part.append('\'');
if (v.equals("&#39;")) 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();
}
}

0
ClipboardApi.java Executable file → Normal file
View File

0
ComposeWindow.java Executable file → Normal file
View File

0
ImageApi.java Executable file → Normal file
View File

0
ImageWindow.java Executable file → Normal file
View File

0
JKomasto.java Executable file → Normal file
View File

0
KDE_Dialog_Appear.wav Executable file → Normal file
View File

0
LoginWindow.java Executable file → Normal file
View File

12
MastodonApi.java Executable file → Normal file
View File

@ -581,15 +581,23 @@ MastodonApi {
debugPrint(Tree<String> 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<String> 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<String>
fromPlain(Reader r)
throws IOException

0
NotificationsWindow.java Executable file → Normal file
View File

27
PostWindow.java Executable file → Normal file
View File

@ -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<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.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<String> nodes = RudimentaryHTMLParser.depthlessRead(n);
for (Tree<String> node: nodes)

0
ProfileWindow.java Executable file → Normal file
View File

0
RepliesWindow.java Executable file → Normal file
View File

0
RequestListener.java Executable file → Normal file
View File

0
RichTextPane.java Executable file → Normal file
View File

480
RichTextPane2.java Normal file
View File

@ -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<String> html, Tree<String> emojiMap)
{
Tree<String> 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<AStrSegment> segments = new ArrayList<>();
int offset = 0;
for (Tree<String> 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<String> 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<String> node)
{
Tree<String> 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<String> child: children)
{
b.append(toText(child));
}
return b.toString();
}
private static Tree<String>
turnIntoCommands(Tree<String> tag)
{
assert tag.key.equals("tag");
Tree<String> returnee = new Tree<String>();
String tagName = tag.get(0).key;
Tree<String> children = tag.get("children");
if (tagName.equals("a"))
{
String url = tag.get("href").value;
Tree<String> 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<String> 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<String> child: children)
{
if (!child.key.equals("tag"))
{
returnee.add(child);
continue;
}
child = turnIntoCommands(child);
for (Tree<String> 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("");
}
}

0
RudimentaryHTMLParser.java Executable file → Normal file
View File

0
TimelineWindow.java Executable file → Normal file
View File

0
TwoToggleButton.java Executable file → Normal file
View File

0
WindowUpdater.java Executable file → Normal file
View File

0
notifOptions.txt Executable file → Normal file
View File

0
notifOptions.txt~ Executable file → Normal file
View File

7
run
View File

@ -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