| /* | |
| * Copyright (c) 2019, 2024, 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.fixuppandoc; | |
| import java.io.BufferedReader; | |
| import java.io.BufferedWriter; | |
| import java.io.IOException; | |
| import java.io.InputStreamReader; | |
| import java.io.OutputStreamWriter; | |
| import java.io.PrintStream; | |
| import java.io.PrintWriter; | |
| import java.io.Reader; | |
| import java.io.Writer; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.LinkedHashMap; | |
| import java.util.LinkedHashSet; | |
| import java.util.List; | |
| import java.util.Locale; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.Set; | |
| import java.util.regex.Matcher; | |
| import java.util.regex.Pattern; | |
| import java.util.stream.Collectors; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| /** | |
| * Fixup HTML generated by pandoc. | |
| * | |
| * <h2>{@code <html>}</h2> | |
| * | |
| * Replace the existing element with {@code <html lang="en">}, removing references to XML. | |
| * | |
| * <h2>{@code <main>}</h2> | |
| * | |
| * {@code <main>} is inserted if palpable content is found that is not with a | |
| * section such as {@code header}, {@code footer}, {@code aside}. | |
| * | |
| * {@code </main>} is inserted if {@code <main>} was inserted and a section | |
| * is started that should not be included in the main section. | |
| * | |
| * <h2>Tables: row headings</h2> | |
| * | |
| * For simple tables, as typically generated by _pandoc_, determine the column | |
| * whose contents are unique, and convert the cells in that column to be header | |
| * cells with {@code scope="row"}. In case of ambiguity, a column containing a | |
| * {@code <th>} whose contents begin with <em>name</em> is preferred. | |
| * When converting the cell, the {@code style} attribute will be updated to | |
| * specify {@code font-weight: normal}, and if there is not already an explicit | |
| * setting for {@code text-align}, then the style will be updated to include | |
| * {@code text-align:left;}. | |
| * | |
| * These rules do not apply if the table contains any cells that include | |
| * a setting for the {@code scope} attribute, or if the table contains | |
| * spanning cells or nested tables. | |
| * | |
| * <h2>{@code <meta name="generator">}</h2> | |
| * | |
| * Update the content string, to indicate it has been processed by this program. | |
| * | |
| * <h2>{@code <nav id="TOC">}</h2> | |
| * | |
| * Set attribute {@code title="Table Of Contents"} | |
| * | |
| */ | |
| public class Main { | |
| /** | |
| * Runs the program. | |
| * | |
| * <pre> | |
| * java build.tools.fixuppandoc.Main [-o output-file] [input-file] | |
| * </pre> | |
| * | |
| * If no input file is specified, the program will read from standard input. | |
| * If no output file is specified, the program will write to standard output. | |
| * Any error messages will be written to the standard error stream. | |
| * | |
| * Consistent with the | |
| * <a href="https://pandoc.org/MANUAL.html#character-encoding">pandoc tool</a>, | |
| * input and output text is encoded as UTF-8. | |
| * | |
| * @param args the command-line arguments | |
| */ | |
| public static void main(String... args) { | |
| try { | |
| new Main().run(args); | |
| } catch (IOException | IllegalArgumentException e) { | |
| System.err.println(e); | |
| System.exit(1); | |
| } catch (Throwable t) { | |
| t.printStackTrace(System.err); | |
| System.exit(1); | |
| } | |
| } | |
| private void run(String... args) throws IOException { | |
| Path inFile = null; | |
| Path outFile = null; | |
| NavBar navbar = null; | |
| for (int i = 0; i < args.length; i++) { | |
| String arg = args[i]; | |
| if (arg.equals("-o") && i + 1 < args.length) { | |
| outFile = Path.of(args[++i]); | |
| } else if (arg.equals("--insert-nav")) { | |
| navbar = new NavBar(); | |
| } else if (arg.equals("--nav-right-info") && i + 1 < args.length) { | |
| navbar.rightSideInfo(args[++i]); | |
| } else if (arg.equals("--nav-subdirs") && i + 1 < args.length) { | |
| navbar.subdirs(Integer.parseInt(args[++i])); | |
| } else if (arg.equals("--nav-link-guides")) { | |
| navbar.linkGuides(true); | |
| } else if (arg.startsWith("-")) { | |
| throw new IllegalArgumentException(arg); | |
| } else if (inFile == null) { | |
| inFile = Path.of(arg); | |
| } else { | |
| System.err.println("ARGV: " + Arrays.toString(args)); | |
| throw new IllegalArgumentException(arg); | |
| } | |
| } | |
| new Fixup(navbar).run(inFile, outFile); | |
| } | |
| /** | |
| * A class to read HTML, copying input to output, modifying | |
| * fragments as needed. | |
| */ | |
| class Fixup extends HtmlParser { | |
| /** The output stream. */ | |
| PrintWriter out; | |
| /** A stream for reporting errors. */ | |
| PrintStream err = System.err; | |
| /** A manager for the navigation bar, null if not required. */ | |
| NavBar navbar; | |
| /** | |
| * Flag to indicate when {@code <main>} is permitted around palpable content. | |
| * Set within {@code <body>}; disabled within elements in which {@code <main>} | |
| * is not permitted. | |
| */ | |
| boolean allowMain = false; | |
| /** | |
| * Flag to indicate that {@code <main>} is required. | |
| * Set on {@code <body>}; reset when {@code <main>} is either found or generated. | |
| */ | |
| boolean needMain = false; | |
| /** | |
| * Flag to indicate that {@code </main>} is required. | |
| * Set if {@code <main>} is generated. | |
| * Reset when a start or end element is found that requires that {@code </main>} | |
| * needs to be generated if necessary. | |
| */ | |
| boolean needEndMain = false; | |
| /** | |
| * Handler for {@code <table>} elements. | |
| */ | |
| Table table; | |
| Fixup(NavBar navbar) { | |
| this.navbar = navbar; | |
| } | |
| /** | |
| * Run the program, copying an input file to an output file. | |
| * If the input file is {@code null}, input is read from the standard input. | |
| * If the output file is {@code null}, output is written to the standard output. | |
| * | |
| * @param inFile the input file | |
| * @param outFile the output file | |
| * @throws IOException if an IO error occurs | |
| */ | |
| void run(Path inFile, Path outFile) throws IOException { | |
| try (Writer out = openWriter(outFile)) { | |
| this.out = new PrintWriter(out); | |
| if (inFile != null) { | |
| read(inFile); | |
| } else { | |
| read(new BufferedReader(new InputStreamReader(System.in, UTF_8))); | |
| } | |
| } | |
| } | |
| /** | |
| * Returns a writer for a file, or for the standard output if the file is {@code null}. | |
| * | |
| * @param file the file | |
| * @return the writer | |
| * @throws IOException if an IO error occurs | |
| */ | |
| private Writer openWriter(Path file) throws IOException { | |
| if (file != null) { | |
| return Files.newBufferedWriter(file, UTF_8); | |
| } else { | |
| return new BufferedWriter(new OutputStreamWriter(System.out, UTF_8) { | |
| @Override | |
| public void close() throws IOException { | |
| flush(); | |
| } | |
| }); | |
| } | |
| } | |
| @Override | |
| protected void error(Path file, int lineNumber, String message) { | |
| err.print(file == null ? "<stdin>" : file); | |
| if (lineNumber > 0) { | |
| err.print(":"); | |
| err.print(lineNumber); | |
| } | |
| err.print(": "); | |
| err.println(message); | |
| } | |
| @Override | |
| protected void error(Path file, int lineNumber, Throwable t) { | |
| error(file, lineNumber, t.toString()); | |
| t.printStackTrace(err); | |
| } | |
| /** | |
| * The buffer in which input is stored until an appropriate action can be determined. | |
| * Using the buffer ensures that the output exactly matches the input, except where | |
| * it is intentionally modified. | |
| */ | |
| private StringBuilder buffer = new StringBuilder(); | |
| @Override | |
| public int nextChar() throws IOException { | |
| if (ch > 0) { | |
| buffer.append((char) ch); | |
| } | |
| return super.nextChar(); | |
| } | |
| @Override | |
| protected void doctype(String s) { | |
| flushBuffer(); | |
| } | |
| @Override | |
| protected void startElement(String name, Map<String,String> attrs, boolean selfClosing) { | |
| switch (name) { | |
| case "html": | |
| // replace the existing <html> fragment | |
| out.write("<html lang=\"en\">"); | |
| buffer.setLength(0); | |
| break; | |
| case "meta": | |
| // update the meta-data for the generator | |
| if (Objects.equals(attrs.get("name"), "generator")) { | |
| out.write(buffer.toString() | |
| .replaceAll("(content=\"[^\"]*)(\")", "$1,fixuphtml$2")); | |
| buffer.setLength(0); | |
| } | |
| break; | |
| case "article": | |
| case "aside": | |
| case "footer": | |
| case "header": | |
| case "nav": | |
| // starting one of these elements will terminate <main> if one is being | |
| // inserted | |
| if (needEndMain) { | |
| out.write("</main>"); | |
| needEndMain = false; | |
| } | |
| // <main> is not permitted within these elements | |
| allowMain = false; | |
| if (navbar != null && name.equals("header") && Objects.equals(attrs.get("id"), "title-block-header")) { | |
| flushBuffer(); | |
| navbar.write(out); | |
| } else if (name.equals("nav") && Objects.equals(attrs.get("id"), "TOC")) { | |
| out.write(buffer.toString() | |
| .replaceAll(">$", " title=\"Table Of Contents\">")); | |
| buffer.setLength(0); | |
| } | |
| break; | |
| case "body": | |
| // within <body>, <main> is both permitted and required | |
| allowMain = true; | |
| needMain = true; | |
| break; | |
| case "main": | |
| // an explicit <main> found in the input; no need to add one | |
| needMain = false; | |
| break; | |
| case "table": | |
| // The entire content of a <table> is buffered, until it can be | |
| // determined in which column of the table contains the cells | |
| // that can be used to identify the row. | |
| if (table == null) { | |
| table = new Table(); | |
| } else { | |
| // tables containing nested tables are not updated | |
| table.simple = false; | |
| } | |
| table.nestDepth++; | |
| break; | |
| case "thead": | |
| case "tbody": | |
| if (table != null) { | |
| table.endCell(); | |
| } | |
| break; | |
| case "tr": | |
| if (table != null) { | |
| table.endCell(); | |
| table.nextCellColumnIndex = 0; | |
| } | |
| break; | |
| case "td": | |
| case "th": | |
| if (table != null) { | |
| if (attrs.containsKey("rowspan") | |
| || attrs.containsKey("colspan") | |
| || attrs.containsKey("scope")) { | |
| // tables containing spanning cells and tables that already | |
| // contain scope attributes are not updated | |
| table.simple = false; | |
| } | |
| table.startCell(name); | |
| } | |
| break; | |
| } | |
| // by default, the content is deemed to be palpable content, and so | |
| // insert <main> if it is permitted and one is still required, | |
| // while also ensuring that it does not appear before <body> | |
| if (allowMain && needMain && !name.equals("body")) { | |
| out.write("<main>"); | |
| needMain = false; | |
| needEndMain = true; | |
| } | |
| flushBuffer(); | |
| } | |
| @Override | |
| protected void endElement(String name) { | |
| switch (name) { | |
| case "article": | |
| case "aside": | |
| case "footer": | |
| case "header": | |
| case "nav": | |
| // The code does not handle nested elements of these kinds, but could. | |
| // So, assuming they are not nested, ending these elements implies | |
| // that <main> is once again permitted. | |
| allowMain = true; | |
| break; | |
| case "body": | |
| // The document is nearly done; insert <main> if needed | |
| if (needEndMain) { | |
| out.write("</main>"); | |
| needEndMain = false; | |
| } | |
| break; | |
| case "table": | |
| // if the table is finished, analyze it and write it out | |
| if (table != null) { | |
| if (--table.nestDepth == 0) { | |
| table.add(buffer.toString()); | |
| table.write(out); | |
| table = null; | |
| buffer.setLength(0); | |
| } | |
| } | |
| break; | |
| case "thead": | |
| case "tbody": | |
| case "tr": | |
| case "td": | |
| case "th": | |
| // ending any of these elements implicitly or explicitly ends the | |
| // current cell | |
| table.endCell(); | |
| break; | |
| } | |
| flushBuffer(); | |
| } | |
| @Override | |
| protected void content(String content) { | |
| if (table != null) { | |
| table.content(content); | |
| } else if (allowMain && needMain && !content.isBlank()) { | |
| // insert <main> if required and if we have palpable content | |
| out.write("<main>"); | |
| needMain = false; | |
| needEndMain = true; | |
| } | |
| flushBuffer(); | |
| } | |
| @Override | |
| protected void comment(String comment) { | |
| flushBuffer(); | |
| } | |
| /** | |
| * Flushes the buffer, either by adding it into a table, if one is | |
| * in progress, or by writing it out. | |
| */ | |
| private void flushBuffer() { | |
| String s = buffer.toString(); | |
| if (table != null) { | |
| table.add(s); | |
| } else { | |
| out.write(s); | |
| } | |
| buffer.setLength(0); | |
| } | |
| } | |
| class NavBar { | |
| private int subdirs = 0; | |
| private boolean linkGuides = false; | |
| private String rightSideInfo = ""; | |
| void subdirs(int subdirs) { | |
| this.subdirs = subdirs; | |
| } | |
| void linkGuides(boolean linkGuides) { | |
| this.linkGuides = linkGuides; | |
| } | |
| void rightSideInfo(String rightSideInfo) { | |
| this.rightSideInfo = rightSideInfo; | |
| } | |
| void write(PrintWriter out) { | |
| get().lines().forEach(out::println); | |
| } | |
| String get() { | |
| String pathToSpecs = "../".repeat(subdirs); | |
| String api = pathToSpecs + "../api/index.html"; | |
| String specs = pathToSpecs + "index.html"; | |
| String guides = pathToSpecs + "man/index.html"; | |
| StringBuilder sb = new StringBuilder(); | |
| sb.append("\n"); | |
| sb.append("<div class=\"navbar\">"); // full enclosing banner | |
| if (rightSideInfo != null) { | |
| sb.append("<div>").append(rightSideInfo).append("</div>"); | |
| } | |
| sb.append("<nav>"); // nav links | |
| var links = new ArrayList<>(List.of( | |
| link(api, "API"), | |
| link(specs, "OTHER SPECIFICATIONS") | |
| )); | |
| if (linkGuides) { | |
| links.add(link(guides, "TOOL GUIDES")); | |
| } | |
| sb.append(list(links)); | |
| sb.append("</nav>"); | |
| sb.append("</div>"); | |
| sb.append("\n"); | |
| return sb.toString(); | |
| } | |
| String list(List<String> items) { | |
| return items.stream() | |
| .map(i -> "<li>" + i) | |
| .collect(Collectors.joining("\n", "<ul>", "</ul>")); | |
| } | |
| private String link(String href, String label) { | |
| return "<a href=\"" + href + "\">" + label + "</a>"; | |
| } | |
| } | |
| /** | |
| * Storage for the content of a {@code <table>} element} until we can determine | |
| * whether we should add {@code scope="row"} to the cells in a given column, | |
| * and if so, which column. | |
| * | |
| * The column with the highest number of unique entries is selected; | |
| * in case of ambiguity, a column whose heading begins "name" is chosen. | |
| * | |
| * Only "simple" tables are supported. Tables with any of the following | |
| * features are not considered "simple" and will not be modified: | |
| * <ul> | |
| * <li>Tables containing nested tables</li> | |
| * <li>Tables containing cells that use "rowspan" and "colspan" attributes</li> | |
| * <li>Tables containing cells that already use "scope" attributes</li> | |
| * </ul> | |
| */ | |
| class Table { | |
| /** | |
| * A fragment of HTML in this table. | |
| */ | |
| class Entry { | |
| /** The fragment. */ | |
| final String html; | |
| /** The column for a {@code <td>} fragment, or -1. */ | |
| final int column; | |
| Entry(String html, int column) { | |
| this.html = html; | |
| this.column = column; | |
| } | |
| } | |
| /** Whether or not this is a "simple" table. */ | |
| boolean simple = true; | |
| /** The nesting depth of the current table, within enclosing tables. */ | |
| int nestDepth; | |
| /** A list of the HTML fragments that make up this table. */ | |
| List<Entry> entries; | |
| /** The plain text contents of each column, used to determine the primary column. */ | |
| List<Set<String>> columnContents; | |
| /** The column index of the next cell to be found. */ | |
| int nextCellColumnIndex; | |
| /** A flag to mark the start of a {@code <td>} cell. */ | |
| boolean startTDCell; | |
| /** The column index of the current cell, or -1 if not in a cell. */ | |
| int currCellColumnIndex; | |
| /** The plain text contents of the current column. */ | |
| Set<String> currColumnContents; | |
| /** The plain text content of the current cell. */ | |
| StringBuilder currCellContent; | |
| /** The kind ({@code th} or {@code td}) of the current cell. */ | |
| String currCellKind; | |
| /** | |
| * The index of the column, if any, containing a heading beginning "name". | |
| * This column is given preferential treatment when deciding the primary column. | |
| */ | |
| int nameColumn; | |
| Table() { | |
| entries = new ArrayList<>(); | |
| columnContents = new ArrayList<>(); | |
| } | |
| void startCell(String name) { | |
| endCell(); | |
| startTDCell = name.equals("td"); | |
| currCellColumnIndex = nextCellColumnIndex++; | |
| currColumnContents = getColumn(currCellColumnIndex); | |
| currCellContent = new StringBuilder(); | |
| currCellKind = name; | |
| } | |
| void endCell() { | |
| if (currCellContent != null) { | |
| String c = currCellContent.toString().trim(); | |
| if (Objects.equals(currCellKind, "th") | |
| && c.toLowerCase(Locale.US).startsWith("name")) { | |
| nameColumn = currCellColumnIndex; | |
| } | |
| currColumnContents.add(c); | |
| currCellContent = null; | |
| currCellColumnIndex = -1; | |
| currColumnContents = null; | |
| } | |
| } | |
| void content(String content) { | |
| if (currCellContent != null) { | |
| currCellContent.append(content); | |
| } | |
| } | |
| void add(String html) { | |
| int index = startTDCell ? currCellColumnIndex : -1; | |
| entries.add(new Entry(html, index)); | |
| startTDCell = false; | |
| } | |
| void write(PrintWriter out) { | |
| int max = -1; | |
| int maxIndex = -1; | |
| int index = 0; | |
| for (Set<String> c : columnContents) { | |
| if (c.size() > max || c.size() == max && index == nameColumn) { | |
| max = c.size(); | |
| maxIndex = index; | |
| } | |
| index++; | |
| } | |
| boolean updateEndTd = false; | |
| Pattern styleAttr = Pattern.compile("(?s)(?<before>.*style=\")(?<style>[^\"]*)(?<after>\".*)"); | |
| for (Entry e : entries) { | |
| if (simple && e.column == maxIndex) { | |
| String attrs = e.html.substring(3, e.html.length() - 1); | |
| out.write("<th"); | |
| Matcher m = styleAttr.matcher(attrs); | |
| if (m.matches()) { | |
| out.write(m.group("before")); | |
| out.write("font-weight: normal; "); | |
| String style = m.group("style"); | |
| if (!style.contains("text-align")) { | |
| out.write("text-align: left; "); | |
| } | |
| out.write(style); | |
| out.write(m.group("after")); | |
| } else { | |
| out.write(" style=\"font-weight: normal; text-align:left;\" "); | |
| out.write(attrs); | |
| } | |
| out.write(" scope=\"row\""); | |
| out.write(">"); | |
| updateEndTd = true; | |
| } else if (updateEndTd && e.html.equalsIgnoreCase("</td>")) { | |
| out.write("</th>"); | |
| updateEndTd = false; | |
| } else { | |
| out.write(e.html); | |
| if (updateEndTd && e.html.regionMatches(true, 0, "<td", 0, 3)) { | |
| // a new cell has been started without explicitly closing the | |
| // cell that was being updated | |
| updateEndTd = false; | |
| } | |
| } | |
| } | |
| } | |
| private Set<String> getColumn(int index) { | |
| while (columnContents.size() <= index) { | |
| columnContents.add(new LinkedHashSet<>()); | |
| } | |
| return columnContents.get(index); | |
| } | |
| } | |
| /** | |
| * A basic HTML parser. | |
| * Override the protected methods as needed to get notified of significant items | |
| * in any file that is read. | |
| */ | |
| abstract class HtmlParser { | |
| private Path file; | |
| private Reader in; | |
| protected int ch; | |
| private int lineNumber; | |
| private boolean inScript; | |
| private boolean xml; | |
| /** | |
| * Read a file. | |
| * @param file the file | |
| */ | |
| void read(Path file) { | |
| try (Reader r = Files.newBufferedReader(file, UTF_8)) { | |
| this.file = file; | |
| read(r); | |
| } catch (IOException e) { | |
| error(file, -1, e); | |
| } | |
| } | |
| HtmlParser() { } | |
| /** | |
| * Read a stream. | |
| * @param r the stream | |
| */ | |
| void read(Reader r) { | |
| try { | |
| this.in = r; | |
| StringBuilder content = new StringBuilder(); | |
| startFile(file); | |
| try { | |
| lineNumber = 1; | |
| xml = false; | |
| nextChar(); | |
| while (ch != -1) { | |
| if (ch == '<') { | |
| content(content.toString()); | |
| content.setLength(0); | |
| html(); | |
| } else { | |
| content.append((char) ch); | |
| if (ch == '\n') { | |
| content(content.toString()); | |
| content.setLength(0); | |
| } | |
| nextChar(); | |
| } | |
| } | |
| } finally { | |
| endFile(); | |
| } | |
| } catch (IOException e) { | |
| error(file, lineNumber, e); | |
| } catch (Throwable t) { | |
| error(file, lineNumber, t); | |
| t.printStackTrace(System.err); | |
| } | |
| } | |
| protected int getLineNumber() { | |
| return lineNumber; | |
| } | |
| /** | |
| * Called when a file has been opened, before parsing begins. | |
| * This is always the first notification when reading a file. | |
| * This implementation does nothing. | |
| * | |
| * @param file the file | |
| */ | |
| protected void startFile(Path file) { } | |
| /** | |
| * Called when the parser has finished reading a file. | |
| * This is always the last notification when reading a file, | |
| * unless any errors occur while closing the file. | |
| * This implementation does nothing. | |
| */ | |
| protected void endFile() { } | |
| /** | |
| * Called when a doctype declaration is found, at the beginning of the file. | |
| * This implementation does nothing. | |
| * @param s the doctype declaration | |
| */ | |
| protected void doctype(String s) { } | |
| /** | |
| * Called when the opening tag of an HTML element is encountered. | |
| * This implementation does nothing. | |
| * @param name the name of the tag | |
| * @param attrs the attribute | |
| * @param selfClosing whether or not this is a self-closing tag | |
| */ | |
| protected void startElement(String name, Map<String,String> attrs, boolean selfClosing) { } | |
| /** | |
| * Called when the closing tag of an HTML tag is encountered. | |
| * This implementation does nothing. | |
| * @param name the name of the tag | |
| */ | |
| protected void endElement(String name) { } | |
| /** | |
| * Called for sequences of character content. | |
| * @param content the character content | |
| */ | |
| protected void content(String content) { } | |
| /** | |
| * Called for sequences of comment. | |
| * @param comment the comment | |
| */ | |
| protected void comment(String comment) { } | |
| /** | |
| * Called when an error has been encountered. | |
| * @param file the file being read | |
| * @param lineNumber the line number of line containing the error | |
| * @param message a description of the error | |
| */ | |
| protected abstract void error(Path file, int lineNumber, String message); | |
| /** | |
| * Called when an exception has been encountered. | |
| * @param file the file being read | |
| * @param lineNumber the line number of the line being read when the exception was found | |
| * @param t the exception | |
| */ | |
| protected abstract void error(Path file, int lineNumber, Throwable t); | |
| protected int nextChar() throws IOException { | |
| ch = in.read(); | |
| if (ch == '\n') | |
| lineNumber++; | |
| return ch; | |
| } | |
| /** | |
| * Read the start or end of an HTML tag, or an HTML comment | |
| * {@literal <identifier attrs> } or {@literal </identifier> } | |
| * @throws java.io.IOException if there is a problem reading the file | |
| */ | |
| protected void html() throws IOException { | |
| nextChar(); | |
| if (isIdentifierStart((char) ch)) { | |
| String name = readIdentifier().toLowerCase(Locale.US); | |
| Map<String,String> attrs = htmlAttrs(); | |
| if (attrs != null) { | |
| boolean selfClosing = false; | |
| if (ch == '/') { | |
| nextChar(); | |
| selfClosing = true; | |
| } | |
| if (ch == '>') { | |
| nextChar(); | |
| startElement(name, attrs, selfClosing); | |
| if (name.equals("script")) { | |
| inScript = true; | |
| } | |
| return; | |
| } | |
| } | |
| } else if (ch == '/') { | |
| nextChar(); | |
| if (isIdentifierStart((char) ch)) { | |
| String name = readIdentifier().toLowerCase(Locale.US); | |
| skipWhitespace(); | |
| if (ch == '>') { | |
| nextChar(); | |
| endElement(name); | |
| if (name.equals("script")) { | |
| inScript = false; | |
| } | |
| return; | |
| } | |
| } | |
| } else if (ch == '!') { | |
| nextChar(); | |
| if (ch == '-') { | |
| nextChar(); | |
| if (ch == '-') { | |
| nextChar(); | |
| StringBuilder comment = new StringBuilder(); | |
| while (ch != -1) { | |
| int dash = 0; | |
| while (ch == '-') { | |
| dash++; | |
| comment.append(ch); | |
| nextChar(); | |
| } | |
| // Strictly speaking, a comment should not contain "--" | |
| // so dash > 2 is an error, dash == 2 implies ch == '>' | |
| // See http://www.w3.org/TR/html-markup/syntax.html#syntax-comments | |
| // for more details. | |
| if (dash >= 2 && ch == '>') { | |
| comment.setLength(comment.length() - 2); | |
| comment(comment.toString()); | |
| nextChar(); | |
| return; | |
| } | |
| comment.append(ch); | |
| nextChar(); | |
| } | |
| } | |
| } else if (ch == '[') { | |
| nextChar(); | |
| if (ch == 'C') { | |
| nextChar(); | |
| if (ch == 'D') { | |
| nextChar(); | |
| if (ch == 'A') { | |
| nextChar(); | |
| if (ch == 'T') { | |
| nextChar(); | |
| if (ch == 'A') { | |
| nextChar(); | |
| if (ch == '[') { | |
| while (true) { | |
| nextChar(); | |
| if (ch == ']') { | |
| nextChar(); | |
| if (ch == ']') { | |
| nextChar(); | |
| if (ch == '>') { | |
| nextChar(); | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } else { | |
| StringBuilder sb = new StringBuilder(); | |
| while (ch != -1 && ch != '>') { | |
| sb.append((char) ch); | |
| nextChar(); | |
| } | |
| Pattern p = Pattern.compile("(?is)doctype\\s+html\\s?.*"); | |
| String s = sb.toString(); | |
| if (p.matcher(s).matches()) { | |
| doctype(s); | |
| return; | |
| } | |
| } | |
| } else if (ch == '?') { | |
| nextChar(); | |
| if (ch == 'x') { | |
| nextChar(); | |
| if (ch == 'm') { | |
| nextChar(); | |
| if (ch == 'l') { | |
| Map<String,String> attrs = htmlAttrs(); | |
| if (ch == '?') { | |
| nextChar(); | |
| if (ch == '>') { | |
| nextChar(); | |
| xml = true; | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| if (!inScript) { | |
| error(file, lineNumber, "bad html"); | |
| } | |
| } | |
| /** | |
| * Read a series of HTML attributes, terminated by {@literal > }. | |
| * Each attribute is of the form {@literal identifier[=value] }. | |
| * "value" may be unquoted, single-quoted, or double-quoted. | |
| */ | |
| private Map<String,String> htmlAttrs() throws IOException { | |
| Map<String, String> map = new LinkedHashMap<>(); | |
| skipWhitespace(); | |
| while (isIdentifierStart((char) ch)) { | |
| String name = readAttributeName().toLowerCase(Locale.US); | |
| skipWhitespace(); | |
| String value = null; | |
| if (ch == '=') { | |
| nextChar(); | |
| skipWhitespace(); | |
| if (ch == '\'' || ch == '"') { | |
| char quote = (char) ch; | |
| nextChar(); | |
| StringBuilder sb = new StringBuilder(); | |
| while (ch != -1 && ch != quote) { | |
| sb.append((char) ch); | |
| nextChar(); | |
| } | |
| value = sb.toString() // hack to replace common entities | |
| .replace("<", "<") | |
| .replace(">", ">") | |
| .replace("&", "&"); | |
| nextChar(); | |
| } else { | |
| StringBuilder sb = new StringBuilder(); | |
| while (ch != -1 && !isUnquotedAttrValueTerminator((char) ch)) { | |
| sb.append((char) ch); | |
| nextChar(); | |
| } | |
| value = sb.toString(); | |
| } | |
| skipWhitespace(); | |
| } | |
| map.put(name, value); | |
| } | |
| return map; | |
| } | |
| private boolean isIdentifierStart(char ch) { | |
| return Character.isUnicodeIdentifierStart(ch); | |
| } | |
| private String readIdentifier() throws IOException { | |
| StringBuilder sb = new StringBuilder(); | |
| sb.append((char) ch); | |
| nextChar(); | |
| while (ch != -1 && Character.isUnicodeIdentifierPart(ch)) { | |
| sb.append((char) ch); | |
| nextChar(); | |
| } | |
| return sb.toString(); | |
| } | |
| private String readAttributeName() throws IOException { | |
| StringBuilder sb = new StringBuilder(); | |
| sb.append((char) ch); | |
| nextChar(); | |
| while (ch != -1 && Character.isUnicodeIdentifierPart(ch) | |
| || ch == '-' | |
| || (xml || sb.toString().startsWith("xml")) && ch == ':') { | |
| sb.append((char) ch); | |
| nextChar(); | |
| } | |
| return sb.toString(); | |
| } | |
| private boolean isWhitespace(char ch) { | |
| return Character.isWhitespace(ch); | |
| } | |
| private void skipWhitespace() throws IOException { | |
| while (isWhitespace((char) ch)) { | |
| nextChar(); | |
| } | |
| } | |
| private boolean isUnquotedAttrValueTerminator(char ch) { | |
| switch (ch) { | |
| case '\f': case '\n': case '\r': case '\t': | |
| case ' ': | |
| case '"': case '\'': case '`': | |
| case '=': case '<': case '>': | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| } | |
| } |