Last active
December 11, 2020 17:13
-
-
Save stanio/b79ce9348946aa6b3395328ec4c59d14 to your computer and use it in GitHub Desktop.
HTMLEditorKit: CSS relative font-size bug
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
* <div style="font-size: 120%">120%</div></pre> | |
* <p> | |
* but doesn't correct:</p> | |
* <pre> | |
* <h3 style="font-size: 120%">120%</h3></pre> | |
* <p> | |
* To correct inheritance with other nested constructs like:</p> | |
* <pre> | |
* <ol style="font-size: 120%"><li>120%</li></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(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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