/* * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package build.tools.jigsaw; import java.io.IOException; import java.io.PrintStream; import java.lang.module.Configuration; import java.lang.module.ModuleDescriptor; import java.lang.module.ModuleFinder; import java.lang.module.ModuleReference; import java.lang.module.ResolvedModule; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import static java.lang.module.ModuleDescriptor.*; import static build.tools.jigsaw.ModuleSummary.HtmlDocument.Selector.*; import static build.tools.jigsaw.ModuleSummary.HtmlDocument.Division.*; public class ModuleSummary { private static final String USAGE = "Usage: ModuleSummary --module-path <dir> -o <outfile> [--root mn]*"; public static void main(String[] args) throws Exception { int i=0; Path modpath = null; Path outfile = null; Set<String> roots = new HashSet<>(); while (i < args.length && args[i].startsWith("-")) { String arg = args[i++]; switch (arg) { case "--module-path": modpath = Paths.get(args[i++]); break; case "-o": outfile = Paths.get(args[i++]); break; case "--root": roots.add(args[i++]); default: System.err.println(USAGE); System.exit(-1); } } if (outfile == null || modpath == null) { System.err.println(USAGE); System.exit(1); } Path dir = outfile.getParent() != null ? outfile.getParent() : Paths.get("."); Files.createDirectories(dir); Map<String, ModuleSummary> modules = new HashMap<>(); Set<ModuleReference> mrefs = ModuleFinder.ofSystem().findAll(); for (ModuleReference mref : mrefs) { String mn = mref.descriptor().name(); Path jmod = modpath.resolve(mn + ".jmod"); modules.put(mn, new ModuleSummary(mref, jmod)); } if (roots.isEmpty()) { roots.addAll(modules.keySet()); } genReport(outfile, modules, roots, "JDK Module Summary"); } static void genReport(Path outfile, Map<String, ModuleSummary> modules, Set<String> roots, String title) throws IOException { Configuration cf = resolve(roots); try (PrintStream out = new PrintStream(Files.newOutputStream(outfile))) { HtmlDocument doc = new HtmlDocument(title, modules); Set<ModuleDescriptor> descriptors = cf.modules().stream() .map(ResolvedModule::reference) .map(ModuleReference::descriptor) .collect(Collectors.toSet()); doc.writeTo(out, descriptors); } } private final String name; private final ModuleDescriptor descriptor; private final JmodInfo jmodInfo; ModuleSummary(ModuleReference mref, Path jmod) throws IOException { this.name = mref.descriptor().name(); this.descriptor = mref.descriptor(); this.jmodInfo = new JmodInfo(jmod); } String name() { return name; } long uncompressedSize() { return jmodInfo.size; } long jmodFileSize() { return jmodInfo.filesize; // estimated compressed size } ModuleDescriptor descriptor() { return descriptor; } int numClasses() { return jmodInfo.classCount; } long classBytes() { return jmodInfo.classBytes; } int numResources() { return jmodInfo.resourceCount; } long resourceBytes() { return jmodInfo.resourceBytes; } int numConfigs() { return jmodInfo.configCount; } long configBytes() { return jmodInfo.configBytes; } int numCommands() { return jmodInfo.nativeCmds.size(); } long commandBytes() { return jmodInfo.nativeCmds.values().stream() .mapToLong(l -> l.longValue()).sum() - jmodInfo.debugInfoCmdBytes; } int numCommandsDebug() { return jmodInfo.debugInfoCmdCount; } long commandDebugBytes() { return jmodInfo.debugInfoCmdBytes; } int numNativeLibraries() { return jmodInfo.nativeLibs.size(); } long nativeLibrariesBytes() { return jmodInfo.nativeLibs.values().stream() .mapToLong(l -> l.longValue()).sum() - jmodInfo.debugInfoLibBytes; } int numNativeLibrariesDebug() { return jmodInfo.debugInfoLibCount; } long nativeLibrariesDebugBytes() { return jmodInfo.debugInfoLibBytes; } Map<String,Long> commands() { return jmodInfo.nativeCmds; } Map<String,Long> nativeLibs() { return jmodInfo.nativeLibs; } Map<String,Long> configFiles() { return jmodInfo.configFiles; } static class JmodInfo { final long size; final long filesize; final int classCount; final long classBytes; final int resourceCount; final long resourceBytes; final int configCount; final long configBytes; final int debugInfoLibCount; final long debugInfoLibBytes; final int debugInfoCmdCount; final long debugInfoCmdBytes; final Map<String,Long> configFiles = new HashMap<>(); final Map<String,Long> nativeCmds = new HashMap<>(); final Map<String,Long> nativeLibs = new HashMap<>(); JmodInfo(Path jmod) throws IOException { long total = 0; long cBytes = 0, rBytes = 0, cfBytes = 0, dizLibBytes = 0, dizCmdBytes = 0; int cCount = 0, rCount = 0, cfCount = 0, dizLibCount = 0, dizCmdCount = 0; try (ZipFile zf = new ZipFile(jmod.toFile())) { for (Enumeration<? extends ZipEntry> e = zf.entries(); e.hasMoreElements(); ) { ZipEntry ze = e.nextElement(); String fn = ze.getName(); int pos = fn.indexOf('/'); String dir = fn.substring(0, pos); String filename = fn.substring(fn.lastIndexOf('/') + 1); // name shown in the column String name = filename; long len = ze.getSize(); total += len; switch (dir) { case NATIVE_LIBS: nativeLibs.put(name, len); if (filename.endsWith(".diz")) { dizLibCount++; dizLibBytes += len; } break; case NATIVE_CMDS: nativeCmds.put(name, len); if (filename.endsWith(".diz")) { dizCmdCount++; dizCmdBytes += len; } break; case CLASSES: if (filename.endsWith(".class")) { cCount++; cBytes += len; } else { rCount++; rBytes += len; } break; case CONFIG: configFiles.put(name, len); cfCount++; cfBytes += len; break; default: break; } } this.filesize = jmod.toFile().length(); this.classCount = cCount; this.classBytes = cBytes; this.resourceCount = rCount; this.resourceBytes = rBytes; this.configCount = cfCount; this.configBytes = cfBytes; this.size = total; this.debugInfoLibCount = dizLibCount; this.debugInfoLibBytes = dizLibBytes; this.debugInfoCmdCount = dizCmdCount; this.debugInfoCmdBytes = dizCmdBytes; } } static final String NATIVE_LIBS = "native"; static final String NATIVE_CMDS = "bin"; static final String CLASSES = "classes"; static final String CONFIG = "conf"; static final String MODULE_ID = "module/id"; static final String MODULE_MAIN_CLASS = "module/main-class"; } static Configuration resolve(Set<String> roots) { return Configuration.empty() .resolveRequires(ModuleFinder.ofSystem(), ModuleFinder.of(), roots); } static class HtmlDocument { final String title; final Map<String, ModuleSummary> modules; boolean requiresTransitiveNote = false; boolean aggregatorNote = false; boolean totalBytesNote = false; HtmlDocument(String title, Map<String, ModuleSummary> modules) { this.title = title; this.modules = modules; } void writeTo(PrintStream out, Set<ModuleDescriptor> selectedModules) { out.format("<html><head>%n"); out.format("<title>%s</title>%n", title); // stylesheet Arrays.stream(HtmlDocument.STYLES).forEach(out::println); out.format("</head>%n"); // body begins out.format("<body>%n"); // title and date out.println(DOCTITLE.toString(title)); out.println(VERSION.toString(String.format("%tc", new Date()))); // total modules and sizes long totalBytes = selectedModules.stream() .map(ModuleDescriptor::name) .map(modules::get) .mapToLong(ModuleSummary::uncompressedSize) .sum(); String[] sections = new String[] { String.format("%s: %d", "Total modules", selectedModules.size()), String.format("%s: %,d bytes (%s %s)", "Total size", totalBytes, System.getProperty("os.name"), System.getProperty("os.arch")) }; out.println(SECTION.toString(sections)); // write table and header out.println(String.format("<table class=\"%s\">", MODULES)); out.println(header("Module", "Requires", "Exports", "Services", "Commands/Native Libraries/Configs")); // write contents - one row per module selectedModules.stream() .sorted(Comparator.comparing(ModuleDescriptor::name)) .map(m -> modules.get(m.name())) .map(ModuleTableRow::new) .forEach(table -> table.writeTo(out)); out.format("</table>"); // end table out.format("</body>"); out.println("</html>"); } String header(String... columns) { StringBuilder sb = new StringBuilder(); sb.append("<tr>"); Arrays.stream(columns) .forEach(cn -> sb.append(" <th>").append(cn).append("</th>").append("\n")); sb.append("</tr>"); return sb.toString(); } static enum Selector { MODULES("modules"), MODULE("module"), MODULE_DEF("code name def"), AGGREGATOR("code name def agg"), REQUIRES("code"), REQUIRES_PUBLIC("code reexp"), BR("br"), CODE("code"), NUMBER("number"),; final String name; Selector(String name) { this.name = name; } @Override public String toString() { return name; } } static enum Division { DOCTITLE("doctitle"), VERSION("versions"), SECTION("section"); final String name; Division(String name) { this.name = name; } public String toString(String... lines) { String value = Arrays.stream(lines).collect(Collectors.joining("<br>\n")); return "<div class=\"" + name + "\">" + value + "</div>"; } } class ModuleTableRow { private final ModuleSummary ms; private final Set<ModuleDescriptor> deps; private final int maxRows; private final boolean aggregator; ModuleTableRow(ModuleSummary ms) { this.ms = ms; Configuration cf = resolve(Set.of(ms.name())); this.deps = cf.modules().stream() .map(ResolvedModule::reference) .map(ModuleReference::descriptor) .collect(Collectors.toSet()); int count = (ms.numClasses() > 0 ? 1 : 0) + (ms.numResources() > 0 ? 1 : 0) + (ms.numConfigs() > 0 ? 1 : 0) + (ms.numNativeLibraries() > 0 ? 1 : 0) + (ms.numNativeLibrariesDebug() > 0 ? 1 : 0) + (ms.numCommands() > 0 ? 1 : 0) + (ms.numCommandsDebug() > 0 ? 1 : 0); this.aggregator = ms.numClasses() == 1 && count == 1; // only module-info.class // 5 fixed rows (name + 2 transitive count/size + 2 blank rows) this.maxRows = 5 + count + (aggregator && !aggregatorNote ? 2 : 0); } public void writeTo(PrintStream out) { out.println(String.format("<tr id=\"%s\" class=\"%s\">", ms.name(), MODULE)); out.println(moduleColumn()); out.println(requiresColumn()); out.println(exportsColumn()); out.println(servicesColumn()); out.println(otherSectionColumn()); out.println("</td>"); out.println("</tr>"); } public String moduleColumn() { // module name StringBuilder sb = new StringBuilder(" "); sb.append("<td>"); sb.append(String.format("<table class=\"%s\">", MODULE)).append("\n"); sb.append(moduleName(ms.name())); sb.append(blankRow()); // metadata sb.append(toTableRow("class", "classes", ms.numClasses(), ms.classBytes())); sb.append(toTableRow("resource", "resources", ms.numResources(), ms.resourceBytes())); sb.append(toTableRow("config", "configs", ms.numConfigs(), ms.configBytes())); sb.append(toTableRow("native library", "native libraries", ms.numNativeLibraries(), ms.nativeLibrariesBytes())); sb.append(toTableRow("native library debug", "native libraries debug", ms.numNativeLibrariesDebug(), ms.nativeLibrariesDebugBytes())); sb.append(toTableRow("command", "commands", ms.numCommands(), ms.commandBytes())); sb.append(toTableRow("command debug", "commands debug", ms.numCommandsDebug(), ms.commandDebugBytes())); sb.append(blankRow()); // transitive dependencies long reqBytes = deps.stream() .filter(d -> !d.name().equals(ms.name())) .mapToLong(d -> modules.get(d.name()).uncompressedSize()) .sum(); long reqJmodFileSize = deps.stream() .mapToLong(d -> modules.get(d.name()).jmodFileSize()) .sum(); // size if (totalBytesNote) { sb.append(toTableRow("Total bytes", ms.uncompressedSize())); sb.append(toTableRow("Total bytes of dependencies", reqBytes)); } else { // print footnote sb.append(toTableRow("Total bytes<sup>1</sup>", ms.uncompressedSize())); sb.append(toTableRow("Total bytes of dependencies<sup>2</sup>", reqBytes)); } String files = deps.size() == 1 ? "file" : "files"; sb.append(toTableRow(String.format("Total jmod bytes (%d %s)", deps.size(), files), reqJmodFileSize)); if (aggregator && !aggregatorNote) { aggregatorNote = true; sb.append(blankRow()); sb.append(toTableRow("<i>* aggregator is a module with module-info.class only</i>", BR)); } if (!totalBytesNote) { totalBytesNote = true; sb.append(blankRow()); sb.append(toTableRow("<i><sup>1</sup>sum of all files including debug files</i>", BR)); sb.append(toTableRow("<i><sup>2</sup>sum of direct and indirect dependencies</i>", BR)); } sb.append("</table>").append("</td>"); return sb.toString(); } private String moduleName(String mn) { if (aggregator) { StringBuilder sb = new StringBuilder(); sb.append(String.format("<tr><td colspan=\"2\"><span class=\"%s\">", AGGREGATOR)) .append(mn) .append("</span>").append("  "); if (!aggregatorNote) { sb.append("(aggregator<sup>*</sup>)"); } else { sb.append("(aggregator)"); } sb.append("</td></tr>"); return sb.toString(); } else { return toTableRow(mn, MODULE_DEF); } } public String requiresColumn() { StringBuilder sb = new StringBuilder(); sb.append(String.format("<td>")); boolean footnote = requiresTransitiveNote; ms.descriptor().requires().stream() .sorted(Comparator.comparing(Requires::name)) .forEach(r -> { boolean requiresTransitive = r.modifiers().contains(Requires.Modifier.TRANSITIVE); Selector sel = requiresTransitive ? REQUIRES_PUBLIC : REQUIRES; String req = String.format("<a class=\"%s\" href=\"#%s\">%s</a>", sel, r.name(), r.name()); if (!requiresTransitiveNote && requiresTransitive) { requiresTransitiveNote = true; req += "<sup>*</sup>"; } sb.append(req).append("\n").append("<br>"); }); if (!ms.name().equals("java.base")) { int directDeps = ms.descriptor().requires().size(); int indirectDeps = deps.size()-directDeps-1; for (int i=directDeps; i< (maxRows-1); i++) { sb.append("<br>"); } sb.append("<br>"); sb.append("<i>+").append(indirectDeps).append(" transitive dependencies</i>"); } if (footnote != requiresTransitiveNote) { sb.append("<br><br>").append("<i>* bold denotes requires transitive</i>"); } sb.append("</td>"); return sb.toString(); } public String exportsColumn() { StringBuilder sb = new StringBuilder(); sb.append(String.format(" <td class=\"%s\">", CODE)); ms.descriptor().exports().stream() .sorted(Comparator.comparing(Exports::source)) .filter(e -> !e.isQualified()) .forEach(e -> sb.append(e.source()).append("<br>").append("\n")); sb.append("</td>"); return sb.toString(); } public String servicesColumn() { StringBuilder sb = new StringBuilder(); sb.append(String.format(" <td class=\"%s\">", CODE)); ms.descriptor().uses().stream() .sorted() .forEach(s -> sb.append("uses ").append(s).append("<br>").append("\n")); ms.descriptor().provides().stream() .sorted(Comparator.comparing(Provides::service)) .map(p -> String.format("provides %s<br>    with %s", p.service(), p.providers())) .forEach(p -> sb.append(p).append("<br>").append("\n")); sb.append("</td>"); return sb.toString(); } public String otherSectionColumn() { StringBuilder sb = new StringBuilder(); sb.append("<td>"); sb.append(String.format("<table class=\"%s\">", MODULE)).append("\n"); // commands if (ms.numCommands() > 0) { sb.append(toTableRow("bin/", CODE)); ms.commands().entrySet().stream() .sorted(Map.Entry.comparingByKey()) .forEach(e -> sb.append(toTableRow(e.getKey(), e.getValue(), CODE))); sb.append(blankRow()); } // native libraries if (ms.numNativeLibraries() > 0) { sb.append(toTableRow("lib/", CODE)); ms.nativeLibs().entrySet().stream() .sorted(Map.Entry.comparingByKey()) .forEach(e -> sb.append(toTableRow(e.getKey(), e.getValue(), CODE))); sb.append(blankRow()); } // config files if (ms.numConfigs() > 0) { sb.append(toTableRow("conf/", CODE)); ms.configFiles().entrySet().stream() .sorted(Map.Entry.comparingByKey()) .forEach(e -> sb.append(toTableRow(e.getKey(), e.getValue(), CODE))); } // totals sb.append("</table>").append("</td>"); return sb.toString(); } private String blankRow() { return toTableRow(" ", BR); } private String toTableRow(String col, Selector selector) { TableDataBuilder builder = new TableDataBuilder(); builder.colspan(selector, 2, col); return builder.build(); } private String toTableRow(String col1, long col2) { return toTableRow(col1, col2, BR); } private String toTableRow(String col1, long col2, Selector selector) { TableDataBuilder builder = new TableDataBuilder(); builder.data(selector, col1); builder.data(col2); return builder.build(); } private String toTableRow(String singular, String plural, int count, long bytes) { if (count == 0) { return ""; } TableDataBuilder builder = new TableDataBuilder(); if (count == 1) { builder.data(count + " " + singular); } else { builder.data(count + " " + plural); } builder.data(bytes); return builder.build(); } class TableDataBuilder { private final StringBuilder sb; TableDataBuilder() { this.sb = new StringBuilder("<tr>"); } TableDataBuilder data(String s) { data(BR, s); return this; } TableDataBuilder data(long num) { data(NUMBER, String.format("%,d", num)); return this; } TableDataBuilder colspan(Selector selector, int columns, String data) { sb.append("<td colspan=\"").append(columns).append("\">"); sb.append("<span class=\"").append(selector).append("\">"); sb.append(data).append("</span></td>"); return this; } TableDataBuilder data(Selector selector, String data) { sb.append("<td class=\"").append(selector).append("\">"); sb.append(data).append("</td>"); return this; } String build() { sb.append("</tr>"); return sb.toString(); } } } private static final String[] STYLES = new String[]{ "<link rel=\"stylesheet\" type=\"text/css\" href=\"/.fonts/dejavu.css\"/>", "<style type=\"text/css\">", " HTML, BODY, DIV, SPAN, APPLET, OBJECT, IFRAME, H1, H2, H3, H4, H5, H6, P,", " BLOCKQUOTE, PRE, A, ABBR, ACRONYM, ADDRESS, BIG, CITE, CODE, DEL, DFN, EM,", " IMG, INS, KBD, Q, S, SAMP, SMALL, STRIKE, STRONG, SUB, SUP, TT, VAR, B, U,", " I, CENTER, DL, DT, DD, OL, UL, LI, FIELDSET, FORM, LABEL, LEGEND, TABLE,", " CAPTION, TBODY, TFOOT, THEAD, TR, TH, TD, ARTICLE, ASIDE, CANVAS, DETAILS,", " EMBED, FIGURE, FIGCAPTION, FOOTER, HEADER, HGROUP, MENU, NAV, OUTPUT, RUBY,", " SECTION, SUMMARY, TIME, MARK, AUDIO, VIDEO {", " margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit;", " vertical-align: baseline; }", " ARTICLE, ASIDE, DETAILS, FIGCAPTION, FIGURE, ", " FOOTER, HEADER, HGROUP, MENU, NAV, SECTION { display: block; }", " BLOCKQUOTE, Q { quotes: none; }", " BLOCKQUOTE:before, BLOCKQUOTE:after, Q:before, Q:after {", " content: ''; content: none; }", " TABLE { border-collapse: collapse; border-spacing: 0; }", " A { text-decoration: none; }", " A:link { color: #437291; }", " A:visited { color: #666666; }", " A.anchor:link, A.anchor:visited { color: black; }", " A[href]:hover { color: #e76f00; }", " A IMG { border-width: 0px; }", " HTML { font-size: 20px; } /* baseline grid */", " HTML > BODY { font-size: 14px; }", " BODY {", " background: white;", " margin: 40px;", " margin-bottom: 150%;", " line-height: 20px;", " -webkit-text-size-adjust: 100%; /* iOS */", " color: #222;", " }", " BODY { font-family: \"DejaVu Serif\", \"Lucida Bright\", \"Bookman Old Style\",", " Georgia, serif; }", " CODE, TT, .jref, DIV.spec .open, TABLE.profiles {", " font-family: \"DejaVu Sans\", \"Lucida Sans\", Helvetica, sans-serif; }", " PRE, .code { font-family: \"DejaVu Sans Mono\", \"Bitstream Vera Sans Mono\",", " Monaco, \"Courier New\", monospace; }", " H1, H2, H3, H4 { color: green; font-weight: bold; }", " I { font-style: italic; }", " TH { font-weight: bold; }", " P { text-indent: 40px; }", " P:first-child, UL + P, OL + P, BLOCKQUOTE + P, TABLE + P, P.subsection,", " P.break, DIV.profiles-table + P { text-indent: 0; }", " P.break { margin-top: 10px; }", " P.subsection { margin-top: 20px; }", " P.subsection SPAN.title { font-weight: bold; padding-right: 20px; }", " UL, OL { margin: 10px 0; padding-left: 40px; }", " LI { margin-bottom: 10px; }", " UL.compact LI { margin-bottom: 0; }", " PRE { padding: 0; margin: 10px 0 10px 20px; background: #eee; width: 45em; }", " BLOCKQUOTE { margin: 10px 0; margin-left: 20px; }", " LI BLOCKQUOTE { margin-left: 0; }", " UL LI { list-style-type: square; }", " .todo { color: darkred; text-align: right; }", " .error { color: red; font-weight: bold; }", " .warn { color: #ee0000; font-weight: bold; }", " DIV.doctitle { margin-top: -13px;", " font-size: 22px; line-height: 40px; font-weight: bold; }", " DIV.twarn { color: #cc0000; font-weight: bold; margin-bottom: 9px; }", " DIV.subtitle { margin-top: 2px; font-size: 18px; font-weight: bold; }", " DIV.authors { margin-top: 10px; margin-bottom: 10px; font-size: 16px; }", " DIV.author A { font-style: italic; }", " DIV.version { margin-top: 10px; font-size: 12px; }", " DIV.version, DIV.legal-notice { font-size: 12px; line-height: 15px; }", " SPAN.hash { font-size: 9px; }", " DIV.version SPAN.modified { color: green; font-weight: bold; }", " DIV.head { margin-bottom: 20px; }", " DIV.section > DIV.title, DIV.section DIV.number SPAN {", " font-size: 15px; font-weight: bold; }", " TABLE { border-collapse: collapse; border: none; }", " TD.number { text-align: right; }", " TD, TH { text-align: left; white-space: nowrap; }", " TD.name, SPAN.name { font-weight: bold; }", " ", " TABLE.module { width: 100%; }", " TABLE.module TD:first-child { padding-right: 10px; }", " TR.module > TD { padding: 10px 0; border-top: 1px solid black; }", " TR > TH { padding-bottom: 10px; }", " TR.br TD { padding-top: 20px; }", " TABLE.modules { margin-top: 20px; }", " TABLE.modules > TBODY > TR > TD:nth-child(even) { background: #eee; }", " TABLE.modules > TBODY > TR > TD, TABLE.modules > TBODY > TR > TH {", " padding-left: 10px; padding-right: 10px; }", " .reexp, .def { font-weight: bold; }", " .agg { font-style: italic; }", " SUP { height: 0; line-height: 1; position: relative;", " vertical-align: baseline; bottom: 1ex; font-size: 11px; }", "</style>", }; } }