biskuteri-cafe-JKomasto2/BasicHTMLParser.java

377 lines
7.8 KiB
Java
Raw Normal View History

import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Locale;
import java.text.BreakIterator;
import cafe.biskuteri.hinoki.Tree;
interface
BasicHTMLParser {
public static Tree<String>
parse(String html)
{
List<String> segments;
segments = distinguishTagsFromPcdata(html);
Tree<String> document;
document = toNodes(segments);
2022-05-16 18:52:00 +02:00
document = splitText(document);
document = evaluateHtmlEscapes(document);
document = hierarchise(document);
return document;
}
// - -%- -
private static List<String>
distinguishTagsFromPcdata(String html)
{
List<String> returnee = new ArrayList<>();
StringBuilder segment = new StringBuilder();
boolean inTag = false;
for (char c: html.toCharArray())
{
if (c == '<')
{
String addee = empty(segment);
if (!addee.isEmpty()) returnee.add(addee);
inTag = true;
segment.append(c);
}
else if (c == '>')
{
assert inTag;
assert segment.length() > 0;
segment.append(c);
returnee.add(empty(segment));
inTag = false;
}
else
{
segment.append(c);
}
}
String addee = empty(segment);
if (!addee.isEmpty()) returnee.add(addee);
return returnee;
}
private static Tree<String>
toNodes(List<String> segments)
{
Tree<String> returnee = new Tree<String>();
for (String segment: segments)
{
boolean isTag = segment.startsWith("<");
Tree<String> node = new Tree<String>();
if (!isTag)
{
node.key = "text";
node.value = segment;
returnee.add(node);
continue;
}
node.key = "tag";
String key = null, value = null;
StringBuilder b = new StringBuilder();
boolean inQuotes = false, inValue = false;
char[] chars = segment.toCharArray();
for (int o = 1; o < chars.length - 1; ++o)
{
char c = chars[o];
if (c == '"')
{
inQuotes = !inQuotes;
}
else if (inQuotes)
{
b.append(c);
}
else if (c == '=')
{
assert b.length() > 0;
key = empty(b);
inValue = true;
}
else if (Character.isWhitespace(c))
{
if (b.length() > 0)
{
if (inValue) value = empty(b);
else key = empty(b);
Tree<String> attr = new Tree<String>();
attr.key = key;
attr.value = value;
node.add(attr);
}
inValue = false;
}
else
{
b.append(c);
}
}
if (b.length() > 0)
{
if (inValue) value = empty(b);
else key = empty(b);
Tree<String> attr = new Tree<String>();
attr.key = key;
attr.value = value;
node.add(attr);
}
returnee.add(node);
}
return returnee;
}
private static Tree<String>
2022-05-16 18:52:00 +02:00
splitText(Tree<String> nodes)
{
2022-05-16 18:52:00 +02:00
Tree<String> returnee = new Tree<>();
for (Tree<String> node: nodes)
{
2022-05-16 18:52:00 +02:00
if (node.key.equals("tag"))
{
returnee.add(node);
continue;
}
2022-05-16 18:52:00 +02:00
assert node.key.equals("text");
StringBuilder b = new StringBuilder();
2022-05-16 18:52:00 +02:00
boolean letter = false, cletter;
boolean space = false, cspace;
boolean emoji = false;
for (char c: node.value.toCharArray())
{
2022-05-16 18:52:00 +02:00
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)
{
2022-05-16 18:52:00 +02:00
assert b.length() > 0;
Tree<String> addee = new Tree<>();
addee.key = "space";
addee.value = empty(b);
returnee.add(addee);
b.append(c);
}
else
{
2022-05-16 18:52:00 +02:00
b.append(c);
}
2022-05-16 18:52:00 +02:00
/*
* We can specially handle special
* characters like \n, but I'll opt not to.
*/
letter = cletter;
space = cspace;
}
if (b.length() > 0)
{
2022-05-16 18:52:00 +02:00
Tree<String> addee = new Tree<>();
addee.key = space ? "space" : "text";
addee.value = empty(b);
returnee.add(addee);
}
}
return returnee;
}
2022-05-16 18:52:00 +02:00
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>
hierarchise(Tree<String> nodes)
{
Tree<String> root = new Tree<String>();
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)
{
if (node.key.equals("tag"))
{
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;
selfClosing |= tagName.equals("br");
if (isClosing)
{
assert parents.size() > 1;
Tree<String> parent, grandparent;
parent = parents.pop();
grandparent = parents.peek();
String pTagName = parent.get(0).key;
assert tagName.equals("/" + pTagName);
grandparent.get("children").add(parent);
}
else if (selfClosing)
{
parents.peek().get("children").add(node);
}
else
{
parents.push(node);
}
}
else
{
parents.peek().get("children").add(node);
}
}
assert parents.size() == 1;
return parents.pop();
}
private static String
empty(StringBuilder b)
{
String s = b.toString();
b.delete(0, b.length());
return s;
}
2022-05-16 18:52:00 +02:00
private static boolean
isPunctuation(char c)
{
2022-05-16 18:52:00 +02:00
switch (Character.getType(c))
{
2022-05-16 18:52:00 +02:00
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;
}
2022-05-16 18:52:00 +02:00
}
2022-05-16 18:52:00 +02:00
private static boolean
isLetter(char c)
{
return Character.isLetter(c) || isPunctuation(c);
}
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();
}
}