Skip to content

Instantly share code, notes, and snippets.

@stanio
Last active December 11, 2020 17:13
Show Gist options
  • Save stanio/b79ce9348946aa6b3395328ec4c59d14 to your computer and use it in GitHub Desktop.
Save stanio/b79ce9348946aa6b3395328ec4c59d14 to your computer and use it in GitHub Desktop.
HTMLEditorKit: CSS relative font-size bug
package net.example.swing;
import java.io.Serializable;
import java.util.Enumeration;
import javax.swing.text.AttributeSet;
import javax.swing.text.Document;
import javax.swing.text.StyleConstants;
import javax.swing.text.View;
import javax.swing.text.html.CSS;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.StyleSheet;
/**
* Fixes CSS {@code font-size} inheritance with percentage values (relative to parent).
* <p>
* <em>TL;DR:</em></p>
* <pre>
* JEditorPane editor = new JEditorPane();
* editor.setEditorKitForContentType("text/html", new HTMLEditorKitWorkaround());</pre>
*
* <blockquote cite="https://www.w3.org/TR/CSS2/cascade.html">
* <h2>6.1 Specified, computed, and actual values</h2>
* <p>
* The final value of a property is the result of a four-step calculation:
* the value is determined through specification (the "specified value"),
* then resolved into a value that is used for inheritance (the "computed
* value"), then converted into an absolute value if necessary (the "used
* value"), and finally transformed according to the limitations of the local
* environment (the "actual value").</p>
*
* <h3>6.1.1 Specified values</h3>
* <p>
* User agents must first assign a specified value to each property based on
* the following mechanisms (in order of precedence):</p>
* <ol>
* <li>If the cascade results in a value, use it.</li>
* <li>Otherwise, if the property is inherited and the element is not the root
* of the document tree, use the computed value of the parent element.</li>
* <li>Otherwise use the property's initial value. The initial value of each
* property is indicated in the property's definition.</li>
* </ol>
*
* <h3>6.1.2 Computed values</h3>
* <p>
* The computed value of a property is determined as specified by the Computed
* Value line in the definition of the property.</p>
*
* <h2>6.2 Inheritance</h2>
* <p>
* When inheritance occurs, elements inherit computed values.
* The computed value from the parent element becomes both
* the specified value and the computed value on the child.</p>
* </blockquote>
* <hr>
* <p>
* Partial workaround (w/o using this {@code EditorKit} modification) is to
* include the following style rule:</p>
* <pre>
* p-implied { font-size: inherit }</pre>
* <p>
* This corrects:</p>
* <pre>
* &lt;div style="font-size: 120%">120%&lt;/div></pre>
* <p>
* but doesn't correct:</p>
* <pre>
* &lt;h3 style="font-size: 120%">120%&lt;/h3></pre>
* <p>
* To correct inheritance with other nested constructs like:</p>
* <pre>
* &lt;ol style="font-size: 120%">&lt;li>120%&lt;/li>&lt;/ol></pre>
* <p>
* specify:</p>
* <pre>
* div, span, ol, ul, li, dl, dt, dd, table, tr, th, td,
* p { font-size: inherit }</pre>
*
* @see <a href="https://www.w3.org/TR/CSS2/cascade.html">Assigning property values, Cascading, and Inheritance</a>
* @see CSS.FontSize
*/
@SuppressWarnings("serial")
public class HTMLEditorKitWorkaround extends HTMLEditorKit {
/** A {@code CSS.FontSize} value: {@code font-size: 100%} */
static final Object FONT_SIZE_INHERIT;
static {
StyleSheet styleSheet = new StyleSheet();
styleSheet.addRule("default { font-size: 100% }");
FONT_SIZE_INHERIT = styleSheet.getStyle("default")
.getAttribute(CSS.Attribute.FONT_SIZE);
}
@Override
public Document createDefaultDocument() {
StyleSheet styles = new StyleSheetWorkaround();
styles.addStyleSheet(getStyleSheet());
HTMLDocument doc = new HTMLDocument(styles);
doc.setParser(getParser());
doc.setAsynchronousLoadPriority(4);
doc.setTokenThreshold(100);
return doc;
}
static class StyleSheetWorkaround extends StyleSheet {
@Override
public AttributeSet getViewAttributes(View v) {
return new ViewAttributesWorkaround(super.getViewAttributes(v));
}
}
static class ViewAttributesWorkaround implements AttributeSet, Serializable {
private AttributeSet attrs;
//private View host;
ViewAttributesWorkaround(AttributeSet attrs) {
this.attrs = attrs;
}
public Object getAttribute(Object key) {
if (key == StyleConstants.FontSize) {
//doGetAttribute(CSS.Attribute.FONT_SIZE)
// .toStyleConstants((StyleConstants) key, host);
if (!isDefined(CSS.Attribute.FONT_SIZE)) {
// Get computed font-size (don't inherit percentages).
AttributeSet resolveParent = getResolveParent();
if (resolveParent != null)
return resolveParent.getAttribute(key);
}
}
return doGetAttribute(key);
}
Object doGetAttribute(Object key) {
// Use "inherit" value, don't inherit percentages.
if (key == CSS.Attribute.FONT_SIZE && !isDefined(key)) {
return FONT_SIZE_INHERIT;
}
return attrs.getAttribute(key);
}
public int getAttributeCount() {
return attrs.getAttributeCount();
}
public boolean isDefined(Object attrName) {
return attrs.isDefined(attrName);
}
public boolean isEqual(AttributeSet attr) {
return attrs.isEqual(attr);
}
public AttributeSet copyAttributes() {
return attrs.copyAttributes();
}
public Enumeration<?> getAttributeNames() {
return attrs.getAttributeNames();
}
public boolean containsAttribute(Object name, Object value) {
return attrs.containsAttribute(name, value);
}
public boolean containsAttributes(AttributeSet attributes) {
return attrs.containsAttributes(attributes);
}
public AttributeSet getResolveParent() {
return attrs.getResolveParent();
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Relative Font-size Test</title>
<style>
/* Partial workaround *
div, ol, ul, li, dl, dt, dd, table, tr, th, td,
p, p-implied { font-size: inherit } */
body { font-size: 12px; font-size: 12;
line-height: 1.3; font-family: sans-serif;
margin-left: 7; margin-right: 7 }
.inherit { font-size: 100% }
h1, .h1, h1.h0 span { font-size: 160% }
h2, .h2, h2.h0 span { font-size: 140% }
h3, .h3, h3.h0 span { font-size: 120% }
h4, .h4, h4.h0 span { font-size: 100% }
p, .h0 { font-size: 100% }
h1, h2, h3, h4, ol, ul, p,
.h1, .h2, .h3, .h4 { font-weight: bold;
margin-top: 0;
margin-bottom: 0;
/*border: 1px dashed silver*/ }
.bug { color: red }
.workaround { color: #1E90FF }
.workaround-xxx { color: purple }
.ok { color: black }
hr { margin-top: 10px; margin-bottom: 10px;
margin-top: 10; margin-bottom: 10 }
</style>
</head>
<body>
<ul>
<li class="bug">Bug</li>
<li class="workaround">Workaround</li>
<li class="workaround-xxx">Workaround (non-standard)</li>
<li>Reference / <span class="ok">Correct</span></li>
</ul>
<hr width="60%" size="1" noshade style="margin: 10">
<div>
<span class="h4">100%</span>
<span class="h3">120%</span>
<span class="h2">140%</span>
<span class="h1">160%</span>
</div>
<h4>100%</h4>
<h3 class="bug">120%</h3>
<h2 class="bug">140%</h2>
<h1 class="bug">160%</h1>
<!-- base-size(12) * x, Use absolute size -->
<h4 style="font-size: 12" class="workaround">100%</h4>
<h3 style="font-size: 14" class="workaround">120%</h3>
<h2 style="font-size: 17" class="workaround">140%</h2>
<h1 style="font-size: 19" class="workaround">160%</h1>
<!-- Error: base-size(12) * x * x, Use sqrt(x)-->
<h4 style="font-size: 100%">100%</h4>
<h3 style="font-size: 109.54%" class="workaround-xxx">120%</h3>
<h2 style="font-size: 118.32%" class="workaround-xxx">140%</h2>
<h1 style="font-size: 126.49%" class="workaround-xxx">160%</h1>
<h4><span class="inherit">100%</span></h4>
<h3 class="workaround"><span class="inherit">120%</span></h3>
<h2 class="workaround"><span class="inherit">140%</span></h2>
<h1 class="workaround"><span class="inherit">160%</span></h1>
<h4 class="h0"><span>100%</span></h4>
<h3 class="h0"><span>120%</span></h3>
<h2 class="h0"><span>140%</span></h2>
<h1 class="h0"><span>160%</span></h1>
<hr width="60%" size="1" noshade style="margin: 10">
<div>
<span class="h4">100%</span>
<span class="h3">120%</span>
<span class="h2">140%</span>
<span class="h1">160%</span>
</div>
<div ><div class="h4">100%</div></div>
<div class="bug"><div class="h3">120%</div></div>
<div class="bug"><div class="h2">140%</div></div>
<div class="bug"><div class="h1">160%</div></div>
<!-- Error: base-size(12) * 1.6 * 1.6 * 1.6, Use cuberoot(x) -->
<div style="font-size: 100%; font-weight: bold" >100%</div>
<div style="font-size: 106.266%; font-weight: bold" class="workaround-xxx">120%</div>
<div style="font-size: 111.869%; font-weight: bold" class="workaround-xxx">140%</div>
<div style="font-size: 116.96%; font-weight: bold" class="workaround-xxx">160%</div>
<div ><div class="h4"><span class="inherit">100%</span></div></div>
<div class="bug"><div class="h3"><span class="inherit">120%</span></div></div>
<div class="bug"><div class="h2"><span class="inherit">140%</span></div></div>
<div class="bug"><div class="h1"><span class="inherit">160%</span></div></div>
<div ><div class="h4"><div class="inherit">100%</div></div></div>
<div class="workaround"><div class="h3"><div class="inherit">120%</div></div></div>
<div class="workaround"><div class="h2"><div class="inherit">140%</div></div></div>
<div class="workaround"><div class="h1"><div class="inherit">160%</div></div></div>
<hr width="60%" size="1" noshade style="margin: 10">
<div>
<span class="h4">100%</span>
<span class="h3">120%</span>
<span class="h2">140%</span>
<span class="h1">160%</span>
</div>
<ol class="bug">
<li class="h4"><span class="ok">100%</span></li>
<li class="h3">120%</li>
<li class="h2">140%</li>
<li class="h1">160%</li>
</ol>
<ol class="bug">
<li class="h4"><span class="inherit"><span class="ok">100%</span></span></li>
<li class="h3"><span class="inherit">120%</span></li>
<li class="h2"><span class="inherit">140%</span></li>
<li class="h1"><span class="inherit">160%</span></li>
</ol>
<ol class="h4" ><li >100%</li></ol>
<ol class="h3" start="2"><li class="bug">120%</li></ol>
<ol class="h2" start="3"><li class="bug">140%</li></ol>
<ol class="h1" start="4"><li class="bug">160%</li></ol>
<div class="workaround">
<ol class="h4" ><li class="inherit"><span class="ok">100%</span></li></ol>
<ol class="h3" start="2"><li class="inherit">120%</li></ol>
<ol class="h2" start="3"><li class="inherit">140%</li></ol>
<ol class="h1" start="4"><li class="inherit">160%</li></ol>
</div>
<hr width="60%" size="1" noshade style="margin: 10">
<div>
<span class="h4">100%</span>
<span class="h3">120%</span>
<span class="h2">140%</span>
<span class="h1">160%</span>
</div>
<div class="h4">
<ol><li>100%</li></ol>
</div>
<div class="h3">
<ol start="2" class="bug"><li>120%</li></ol>
</div>
<div class="h2">
<ol start="3" class="bug"><li>140%</li></ol>
</div>
<div class="h1">
<ol start="4" class="bug"><li>160%</li></ol>
</div>
<div class="h4">
<ol class="inherit"><li>100%</li></ol>
</div>
<div class="h3">
<ol start="2" class="inherit"><li class="workaround">120%</li></ol>
</div>
<div class="h2">
<ol start="3" class="inherit"><li class="workaround">140%</li></ol>
</div>
<div class="h1">
<ol start="4" class="inherit"><li class="workaround">160%</li></ol>
</div>
<div class="h4">
<ol><li class="inherit">100%</li></ol>
</div>
<div class="h3">
<ol start="2" class="bug"><li class="inherit">120%</li></ol>
</div>
<div class="h2">
<ol start="3" class="bug"><li class="inherit">140%</li></ol>
</div>
<div class="h1">
<ol start="4" class="bug"><li class="inherit">160%</li></ol>
</div>
<br>
<table border="1" style="font-size: 140%">
<tr>
<th>A</th>
<th>B</th>
</tr>
<tr>
<td>1</td>
<td>2</td>
</tr>
</table>
<br>
<table border="1">
<thead style="font-size: 140%">
<tr>
<th>A</th>
<th>B</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>2</td>
</tr>
</tbody>
</table>
</body>
</html>
package net.example.swing;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Desktop;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import javax.swing.AbstractAction;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.LookAndFeel;
import javax.swing.SwingUtilities;
import javax.swing.text.Document;
import javax.swing.text.html.HTMLDocument;
/**
* HTMLEditorKit: Wrong CSS relative font sizes
*
* @see <a href="https://bugs.openjdk.java.net/browse/JDK-8257664">JDK-8257664</a>
* @see HTMLEditorKitWorkaround
*/
@SuppressWarnings("serial")
public class RelativeFontSizeTest extends JFrame {
private static boolean useW3CLengthUnits = false; // buggy as well
private URL page = RelativeFontSizeTest.class.getResource("relative-font-size-test.html");
//private String page = "https://www.w3.org/TR/CSS1/#font-size";
public RelativeFontSizeTest() {
super("Relative Font Size Test");
List<JEditorPane> editors = new ArrayList<>();
editors.add(initEditorPane(false, null));
editors.add(initEditorPane(false, "p-implied { font-size: inherit }"));
editors.add(initEditorPane(false,
"div, ol, ul, li, dl, dt, dd, table, tr, th, td, "
+ "p, p-implied { font-size: inherit }"));
editors.add(initEditorPane(true, null));
JSplitPane splitPane = initSplitPane(editors);
// Sync scroll position.
((JScrollPane) splitPane.getLeftComponent())
.getVerticalScrollBar().addAdjustmentListener(evt -> {
double pos = getScrollPosition(editors.get(0));
for (int i = 1; i < editors.size(); i++) {
scrollToPosition(editors.get(i), pos);
}
});
Container contentPane = super.getContentPane();
contentPane.add(splitPane, BorderLayout.CENTER);
if (Desktop.isDesktopSupported()) {
getRootPane().getActionMap().put("Open-In-Desktop-Browser", new AbstractAction("Open-In-Desktop-Browser") {
@Override public void actionPerformed(ActionEvent evt) {
try {
Desktop.getDesktop().browse(URI.create(page.toString()));
} catch (IOException e) {
e.printStackTrace();
}
}
});
getRootPane().getActionMap().put("Reload-All", new AbstractAction("Reload-All") {
@Override public void actionPerformed(ActionEvent evt) {
for (JEditorPane editorPane : editors) {
loadPage(editorPane);
}
}
});
InputMap rootInput = getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
LookAndFeel.loadKeyBindings(rootInput, new Object[] {
"control O", "Open-In-Desktop-Browser",
"control shift R", "Reload-All"
});
}
}
private JEditorPane initEditorPane(boolean workaround, String styleRule) {
JEditorPane editor = new JEditorPane();
editor.setEditable(false);
editor.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true);
editor.putClientProperty(JEditorPane.W3C_LENGTH_UNITS, useW3CLengthUnits);
if (workaround) {
editor.setEditorKitForContentType("text/html", new HTMLEditorKitWorkaround());
}
editor.setFont(editor.getFont().deriveFont(14.0f));
editor.getActionMap().put("Reload-Page", new AbstractAction("Reload-Page") {
@Override public void actionPerformed(ActionEvent evt) { loadPage(editor); }
});
editor.getActionMap().put("Debug-Page", new AbstractAction("Debug-Page") {
@Override public void actionPerformed(ActionEvent evt) {
debugPage(editor, (evt.getModifiers() & ActionEvent.SHIFT_MASK) != 0);
}
});
LookAndFeel.loadKeyBindings(editor.getInputMap(), new Object[] {
"control R", "Reload-Page",
"control D", "Debug-Page",
"control shift D", "Debug-Page"
});
editor.addPropertyChangeListener("document", evt -> {
Object doc = evt.getNewValue();
if (doc instanceof HTMLDocument) {
((HTMLDocument) doc).getStyleSheet().addRule(styleRule);
}
});
loadPage(editor);
return editor;
}
void loadPage(JEditorPane editor) {
try {
editor.setContentType("text/html");
editor.getDocument().putProperty(Document.StreamDescriptionProperty, null); // Reload
editor.setPage(page);
} catch (IOException e) {
editor.setContentType("text/plain");
editor.setText(e.toString());
editor.setForeground(Color.red);
}
}
void debugPage(JEditorPane editor, boolean content) {
if (content) {
System.out.println(editor.getDocument().getDefaultRootElement());
} else {
System.out.println(editor.getUI().getRootView(editor));
}
}
private JSplitPane initSplitPane(List<? extends JComponent> components) {
Function<JComponent, JComponent>
prepareComponent = comp -> new JScrollPane(comp);
JSplitPane splitPane = null;
JComponent rightComponent = null;
int count = 1;
if (components.size() > 1) {
rightComponent = prepareComponent.apply(components.get(components.size() - 1));
count++;
}
for (int i = components.size() - count; i >= 0; i--, count++) {
splitPane = new JSplitPane();
splitPane.setLeftComponent(prepareComponent.apply(components.get(i)));
splitPane.setRightComponent(rightComponent);
splitPane.setResizeWeight(1.0 / count);
splitPane.setOneTouchExpandable(true);
rightComponent = splitPane;
}
if (splitPane == null) {
splitPane = new JSplitPane();
splitPane.setLeftComponent(rightComponent);
splitPane.setResizeWeight(1.0);
}
return splitPane;
}
private static Rectangle visibleRect = new Rectangle();
static double getScrollPosition(JComponent comp) {
int height = comp.getHeight();
comp.computeVisibleRect(visibleRect);
return visibleRect.getY() / (height - visibleRect.getHeight());
}
static void scrollToPosition(JComponent comp, double position) {
int height = comp.getHeight();
comp.computeVisibleRect(visibleRect);
visibleRect.y = (int) Math.round(position * (height - visibleRect.getHeight()));
comp.scrollRectToVisible(visibleRect);
}
public static void main(String[] args) throws Exception {
SwingUtilities.invokeLater(() -> {
RelativeFontSizeTest frame = new RelativeFontSizeTest();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(1000, 800);
frame.setLocation(0, 0);
frame.setVisible(true);
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment