diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1f01cc9..898e9d8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,6 +18,10 @@ jobs: analyze: name: Analyze runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write strategy: fail-fast: false @@ -30,11 +34,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -43,7 +47,7 @@ jobs: # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Setup Java JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' @@ -52,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -66,4 +70,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 868a967..1793071 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -9,14 +9,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check w/o SNAPSHOT when "bump version" if: ${{ contains(github.event.head_commit.message, 'bump version') }} run: grep "" pom.xml | head -1 | grep -v SNAPSHOT - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' diff --git a/.gitignore b/.gitignore index 08bf4b2..c945f72 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ lib/ tmp/ codavaj-*.jar java8.properties +AGENTS.md diff --git a/README.md b/README.md index 5b1ff24..9d4764f 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,40 @@ [![Releases](https://jitpack.io/v/umjammer/codavaj.svg)](https://jitpack.io/#umjammer/codavaj) -[![Actions Status](https://github.com/umjammer/codavaj/actions/workflows/maven.yml/badge.svg)](https://github.com/umjammer/codavaj/actions) +[![Java CI](https://github.com/umjammer/codavaj/actions/workflows/maven.yml/badge.svg)](https://github.com/umjammer/codavaj/actions/workflows/maven.yml) [![CodeQL](https://github.com/umjammer/codavaj/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/umjammer/codavaj/actions/workflows/codeql-analysis.yml) -![Java](https://img.shields.io/badge/Java-8-b07219) +![Java](https://img.shields.io/badge/Java-17-b07219) -# CODAVAJ - ( javadoc in reverse ) README +# CODAVAJ -## Convert Javadoc to Java Source +logo        codavaj is anadrome of javadoc -to convert javadoc tree into java source (external references to Sun's -standard javadocs are automatically resolved -i.e. `http://java.sun.com/j2se/1.5.0/docs/api/`) +## Install -``` -codavaj.cmd codavaj {}* -``` +* https://jitpack.io/#umjammer/codavaj -i.e. +## Usage -``` -codavaj.cmd codavaj tmp/jumpi/javadoc tmp/jumpi/src -``` +### Convert Javadoc to Java Source -or +to convert javadoc tree into java source
+(external references to Sun's standard javadocs are automatically resolved +i.e. `http://java.sun.com/j2se/1.5.0/docs/api/`) ``` -codavaj.cmd codavaj tmp/jumpi/javadoc tmp/jumpi/src http://external.link.com/api +codavaj.cmd codavaj {}* ``` -or +#### i.e. -``` -codavaj.cmd codavaj http://jumpi.sourceforge.net/javadoc/j2se tmp/jumpi/src +```shell + $ # from LOCAL javadoc + $ codavaj.cmd codavaj tmp/jumpi/javadoc tmp/jumpi/src + $ # or from local javadoc w/ EXTERNAL LINK + $ codavaj.cmd codavaj tmp/jumpi/javadoc tmp/jumpi/src http://external.link.com/api + $ # or from REMOTE javadoc + $ codavaj.cmd codavaj http://jumpi.sourceforge.net/javadoc/j2se tmp/jumpi/src ``` -## Cooperate with Java Parser +### Cooperate with Java Parser | **parser** | **set javadoc to (decompiled) source** | **rename argument names as javadoc documented** | **code** | **output** | |:-----------|:--------------------------------------:|:-----------------------------------------------:|------------------------------------------------------------------------------------------------------------|:--------------------------------:| @@ -42,18 +43,56 @@ codavaj.cmd codavaj http://jumpi.sourceforge.net/javadoc/j2se tmp/jumpi/src | [JDT](https://www.eclipse.org/jdt/) | ✅ | 🚫 | [📄](https://github.com/umjammer/codavaj/blob/master/src/test/java/commentator/JgtCommentator.java) | little bit worse than JavaParser | | [spoon](https://github.com/INRIA/spoon) | ✅ | 🚫 | [📄](https://github.com/umjammer/codavaj/blob/master/src/test/java/commentator/SpoonCommentator.java) | formats are gone | -## known issues +#### Example + +```shell + $ # install codavaj + $ cd $HOME/src/java + $ git clone https://github.com/umjammer/codavaj + + $ # decompile by fernflower + $ cd $HOME/src/java/javax-speech + $ curl -o tmp/speech-1.0.0.jar https://github.com/umjammer/umjammer/raw/mvn-repo/javax/speech/speech/1.0.0/speech-1.0.0.jar + $ mkdir -p tmp/classes + $ java -jar fernflower.jar tmp/speech-1.0.0.jar tmp/classes + $ pushd src/main/java; jar xvf $HOME/src/java/javax-speech/tmp/classes/speech-1.0.0.jar; popd + + $ # add comment from javadoc on the net to decompiled source + $ cd $HOME/src/java/javax-speech + $ mkdir -p src/main/java + $ mkdir -p tmp/src + $ (pushd $HOME/src/java/codavaj; mvn -P comment test-compile antrun:run@javaParser \ + -Dcomment.javadoc=https://docs.oracle.com/cd/E17802_01/products/products/java-media/speech/forDevelopers/jsapi-doc \ + -Dcomment.source=$HOME/src/java/javax-speech/src/main/java \ + -Dcomment.out=$HOME/src/java/javax-speech/tmp/src \ + ; popd) + $ diff -rBw src/main/java tmp/src | less -R + $ diff -rBw src/main/java tmp/src > tmp/c.patch + $ pushd src/main/java; patch -p 2 < ../../../tmp/c.patch; popd +``` + +### known issues -* codavaj does not introduce default constructor's if they weren't found -in the javadoc. This leads to compile problems if there are subclasses -which use the class's default constructor through the implicit super(). + * codavaj does not introduce default constructor's if they weren't found + in the javadoc. This leads to compile problems if there are subclasses + which use the class's default constructor through the implicit super(). -* nekohtml ~1.19.22 + * nekohtml ~1.19.22 * https://mvnrepository.com/artifact/net.sourceforge.nekohtml/nekohtml/1.9.22 * https://mvnrepository.com/artifact/xerces/xercesImpl/2.11.0 * but 1.19.22 doesn't work with this project currently * so i excluded xerces from dependencies, and add xerces 2.12.2 individually. idk how codeql detect those. * https://sourceforge.net/p/nekohtml/bugs/167/#fdcc + + * @see split too much by arguments' separator "," + * override methods after 2nd.'s comment are gone + * h hags + * code tag + * ol, ul, li tag + +## References + + * https://docs.openrewrite.org/ ## TODO @@ -62,4 +101,8 @@ which use the class's default constructor through the implicit super(). * ~~javadoc 11~~ * ~~en test case~~ * https://github.com/HtmlUnit/htmlunit-neko - * https://github.com/HtmlUnit/htmlunit-neko/security/advisories/GHSA-6jmm-mp6w-4rrg \ No newline at end of file + * https://github.com/HtmlUnit/htmlunit-neko/security/advisories/GHSA-6jmm-mp6w-4rrg + TODO + * use vavi-util-screenscraping? + * interface default + * ~~Spoon~~ \ No newline at end of file diff --git a/jitpack.yml b/jitpack.yml index 4a7f508..efde7bf 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,5 +1,2 @@ jdk: - openjdk17 -before_install: - - sdk install java 17.0.1-open - - sdk use java 17.0.1-open diff --git a/pom.xml b/pom.xml index 9fc7bc7..e1de8a3 100644 --- a/pom.xml +++ b/pom.xml @@ -3,15 +3,10 @@ org.codavaj codavaj - 1.4.6 + 1.4.7 -TODO - - use vavi-util-screenscraping? - interface default - - Spoon + https://github.com/umjammer/codavaj https://github.com/umjammer/codavaj @@ -110,7 +105,7 @@ TODO org.codehaus.mojo exec-maven-plugin - 3.0.0 + 3.1.1 retrieve-config @@ -129,7 +124,7 @@ TODO org.codehaus.mojo properties-maven-plugin - 1.0.0 + 1.2.1 read-properties @@ -149,18 +144,24 @@ TODO + comment + + https://docs.oracle.com/javase/jp/8/docs/api/ + (\w+)+ + @@ -176,6 +177,28 @@ TODO + + + + + + + + + + + + + + + + rewrite + + run + + + + @@ -186,11 +209,33 @@ TODO - - - - - + + + + + + + + + + + jdt + + run + + + + + + + + + + + + + + @@ -207,12 +252,13 @@ TODO + - - - - - + + + + + @@ -230,10 +276,9 @@ TODO org.apache.maven.plugins maven-compiler-plugin - 3.10.1 + 3.12.1 - 17 - 17 + 17 @@ -258,7 +303,7 @@ TODO org.apache.maven.plugins maven-javadoc-plugin - 3.1.0 + 3.5.0 -J-Duser.language=en_US en_US @@ -271,9 +316,9 @@ TODO org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M7 + 3.2.5 - once + 1 -Xms2048m -Xmx4096m -Djava.util.logging.config.file=${project.build.testOutputDirectory}/logging.properties @@ -296,7 +341,7 @@ TODO org.junit junit-bom - 5.9.1 + 6.0.3 pom import @@ -310,49 +355,25 @@ TODO 2.12.2 - org.httpunit + com.github.hazendaz.httpunit httpunit - 1.7.3 - - - nekohtml - nekohtml - - - xerces - xmlParserAPIs - - - xerces - xercesImpl - - + 2.5.0 org.dom4j dom4j - 2.1.3 - - - net.sourceforge.nekohtml - nekohtml - 1.9.16 - - - xerces - xercesImpl - - + 2.2.0 + org.apache.ant ant - 1.10.12 + 1.10.15 - com.github.jaxen-xpath + jaxen jaxen - 1.2.0 + 2.0.6 com.rainerhahnekamp @@ -362,7 +383,7 @@ TODO com.github.umjammer vavi-commons - 1.1.8 + 1.1.17 @@ -383,46 +404,59 @@ TODO com.github.javaparser - javaparser-symbol-solver-core - 3.24.7 + javaparser-core + 3.28.0 test - com.netflix.devinsight.rewrite + org.openrewrite rewrite-core - 1.4.0 + 8.78.4 test - - - com.fasterxml.jackson.core - jackson-annotations - - - org.slf4j - slf4j-api - - + + + org.openrewrite + rewrite-java + 8.72.6 + test + + + com.google.guava + guava + 33.5.0-jre org.slf4j slf4j-jdk14 - 2.0.3 + 2.0.18 test org.eclipse.jdt org.eclipse.jdt.core - 3.29.0 + 3.46.0 test fr.inria.gforge.spoon spoon-core - 10.2.0 + 11.4.1-beta-1 test + + + org.codehaus.plexus + plexus-utils + + + + org.codehaus.plexus + plexus-utils + 4.0.3 + test + diff --git a/src/main/java/org/codavaj/Main.java b/src/main/java/org/codavaj/Main.java index 7ba2935..80c481b 100644 --- a/src/main/java/org/codavaj/Main.java +++ b/src/main/java/org/codavaj/Main.java @@ -16,10 +16,11 @@ package org.codavaj; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.logging.Logger; import org.codavaj.process.docparser.DocParser; import org.codavaj.process.srcwriter.SrcWriter; @@ -38,7 +39,7 @@ */ public class Main { - private static final Logger logger = Logger.getLogger(Main.class.getName()); + private static final Logger logger = System.getLogger(Main.class.getName()); private static final String usage_wget = "wget "; private static final String usage_parse = "codavaj {}*"; @@ -86,14 +87,14 @@ public static void main(String[] args) throws Exception { // "exec-maven-plugin" doesn't have "fork" option. // usually, use a jvmarg like "-Dcodavaj.file.encoding=MS932" System.setProperty("codavaj.file.encoding", args[3]); - logger.info("encoding: " + System.getProperty("codavaj.file.encoding")); + logger.log(Level.INFO, "encoding: " + System.getProperty("codavaj.file.encoding")); } if (args.length > 4) { // reluctantly, cause "exec-maven-plugin" cannot pass system properties as jvm arguments. // "exec-maven-plugin" doesn't have "fork" option. // usually, use a jvmarg like "-Dcodavaj.language=JAPANESE" System.setProperty("codavaj.language", args[4]); - logger.info("language: " + System.getProperty("codavaj.language")); + logger.log(Level.INFO, "language: " + System.getProperty("codavaj.language")); } // all subsequent arguments become external javadoc URL references diff --git a/src/main/java/org/codavaj/process/ProgressEvent.java b/src/main/java/org/codavaj/process/ProgressEvent.java index 9f00609..1818ecb 100644 --- a/src/main/java/org/codavaj/process/ProgressEvent.java +++ b/src/main/java/org/codavaj/process/ProgressEvent.java @@ -23,7 +23,7 @@ public class ProgressEvent { private int done; private int todo; - private String message; + private final String message; /** * Creates a new ProgressEvent object. diff --git a/src/main/java/org/codavaj/process/docparser/DocParser.java b/src/main/java/org/codavaj/process/docparser/DocParser.java index b012ce4..a57dc3f 100644 --- a/src/main/java/org/codavaj/process/docparser/DocParser.java +++ b/src/main/java/org/codavaj/process/docparser/DocParser.java @@ -16,11 +16,11 @@ package org.codavaj.process.docparser; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.regex.Pattern; import org.codavaj.ProcessException; @@ -37,7 +37,7 @@ */ public class DocParser implements Progressive { - private static final Logger logger = Logger.getLogger(DocParser.class.getName()); + private static final Logger logger = System.getLogger(DocParser.class.getName()); /** * directory to find javadoc root. @@ -77,7 +77,7 @@ public TypeFactory process() throws ProcessException { } } } catch (Exception e) { - logger.log(Level.SEVERE, "All class determination failed!", e); + logger.log(Level.ERROR, "All class determination failed!", e); throw new ProcessException(e); } @@ -92,7 +92,7 @@ public TypeFactory process() throws ProcessException { try { parserUtil.processType(type); } catch (Exception e) { - logger.severe("Class parsing failed on " + type.getTypeName()); + logger.log(Level.ERROR, "Class parsing failed on " + type.getTypeName()); errors.put(type, e); // throw new ProcessException(e); } @@ -109,7 +109,7 @@ public TypeFactory process() throws ProcessException { //info( parserUtil.prettyPrint(allconstants)); parserUtil.processConstant(typeFactory.getTypeMap(), javadocClassName != null); } catch (Exception e) { - logger.log(Level.SEVERE, "All constant determination failed!", e); + logger.log(Level.ERROR, "All constant determination failed!", e); throw new ProcessException(e); } diff --git a/src/main/java/org/codavaj/process/docparser/FullyQualifiedNameMap.java b/src/main/java/org/codavaj/process/docparser/FullyQualifiedNameMap.java index 66af38d..5110edb 100644 --- a/src/main/java/org/codavaj/process/docparser/FullyQualifiedNameMap.java +++ b/src/main/java/org/codavaj/process/docparser/FullyQualifiedNameMap.java @@ -6,13 +6,14 @@ package org.codavaj.process.docparser; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.HashMap; import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; import org.codavaj.type.Type; + /** * FullyQualifiedNameMap. * @@ -21,7 +22,7 @@ */ public class FullyQualifiedNameMap extends HashMap { - private static final Logger logger = Logger.getLogger(FullyQualifiedNameMap.class.getName()); + private static final Logger logger = System.getLogger(FullyQualifiedNameMap.class.getName()); /** { short name, fully qualified name } */ public FullyQualifiedNameMap() { @@ -101,5 +102,3 @@ public String guess(String typeName) { } } } - -/* */ diff --git a/src/main/java/org/codavaj/process/docparser/ParseException.java b/src/main/java/org/codavaj/process/docparser/ParseException.java index c20a65b..ac650a3 100644 --- a/src/main/java/org/codavaj/process/docparser/ParseException.java +++ b/src/main/java/org/codavaj/process/docparser/ParseException.java @@ -49,5 +49,3 @@ public ParseException(String m, Throwable t) { super(m, t); } } - -/* */ diff --git a/src/main/java/org/codavaj/process/docparser/ParserUtils.java b/src/main/java/org/codavaj/process/docparser/ParserUtils.java index 0fc8b3b..a3b9a6e 100644 --- a/src/main/java/org/codavaj/process/docparser/ParserUtils.java +++ b/src/main/java/org/codavaj/process/docparser/ParserUtils.java @@ -18,8 +18,11 @@ import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -35,10 +38,9 @@ import java.util.ResourceBundle; import java.util.ServiceLoader; import java.util.StringTokenizer; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; import org.codavaj.type.EnumConst; import org.codavaj.type.Field; @@ -56,15 +58,17 @@ import org.dom4j.tree.DefaultText; import org.xml.sax.InputSource; import org.xml.sax.SAXException; +import vavi.util.Debug; import static com.rainerhahnekamp.sneakythrow.Sneaky.sneaked; + /** * for version ~ 1.6.x */ public class ParserUtils implements Parser { - private static final Logger logger = Logger.getLogger(ParserUtils.class.getName()); + private static final Logger logger = System.getLogger(ParserUtils.class.getName()); /** * Return the classname from a filename. @@ -126,7 +130,7 @@ protected String getTag(String text) { } else if (text.contains(rb.getString("token.default"))) { return "ignore"; } else { -logger.fine("unhandled tag: " + text); +logger.log(Level.DEBUG, "unhandled tag: " + text); return text; } } @@ -187,12 +191,49 @@ protected void processDD(Type t, Node dd,String tag, List commentText) { case "see": if (text.contains(rb.getString("token.see.exclude.1")) || text.contains(rb.getString("token.see.exclude.2"))) { -logger.fine("ignore 3: " + dd.asXML()); +logger.log(Level.DEBUG, "ignore 3: " + dd.asXML()); return; } - replaceA(((Element) dd), true); // TODO no need to replace? + replaceAforSee(((Element) dd)); text = tidyText(dd, true); - break; +Debug.printf("--- %s", text); + +//--- somehow works +// String[] lines = text.split(", "); +// Arrays.stream(lines).forEach(l -> commentText.add("@see " + l)); +//--- + +//--- problems --- + StringBuilder stack = new StringBuilder(); + AtomicBoolean methodArgumentsStart = new AtomicBoolean(); + String[] lines = text.split(", "); // TODO split method argument separator also + Arrays.stream(lines).forEach(l -> { +Debug.printf("@@@: %s, %s, %s", l, methodArgumentsStart.get(), stack); + if (l.contains("(")) { + if (!l.contains(")")) { + assert stack.isEmpty() : "stack is not empty"; + assert !methodArgumentsStart.get() : "flag is true"; + methodArgumentsStart.set(true); + stack.append(l); + return; + } + } + if (methodArgumentsStart.get()) { + assert !stack.isEmpty() : "stack is empty"; + stack.append(l); + return; + } + if (l.contains(")")) { + stack.append(l); + commentText.add("@see " + stack); + stack.setLength(0); + methodArgumentsStart.set(false); + return; + } + commentText.add("@see " + l); + }); +//--- problems --- + return; case "return": replaceA(((Element) dd), true); text = tidyText(dd, true); @@ -222,9 +263,9 @@ private int processDT(Type t, List nodes, int j, String tag, List return j; } - /** Processes A */ + /** A tag to "@link" */ private String processA(Node a) { -logger.fine("A: " + a.asXML()); +logger.log(Level.DEBUG, "A: " + a.asXML()); String href = a.valueOf("@href"); if ((href.startsWith("http") || href.startsWith("../")) && href.replace(".html#", ".").contains(a.getText().replaceAll("\\([\\w$_\\.,\\s\\[\\]]*\\)", ""))) { @@ -235,6 +276,19 @@ private String processA(Node a) { } } + /** A tag to for "@see" */ + private String processAforSee(Node a) { +logger.log(Level.DEBUG, "A: " + a.asXML()); + String href = a.valueOf("@href"); + if ((href.startsWith("http") || href.startsWith("../")) && + href.replace(".html#", ".").contains(a.getText().replaceAll("\\([\\w$_\\.,\\s\\[\\]]*\\)", ""))) { + String link = hrefToLink(href); + return link.replace("$", "."); + } else { + return href; + } + } + /** * @param url javadoc link * @return a class name with field or method @@ -285,7 +339,7 @@ private String hrefToLink(String url) { protected void determineComment(Type t, List allNodes, List commentText) { for (int i = 0; (allNodes != null) && (i < allNodes.size()); i++) { Node node = allNodes.get(i); -logger.finer("node: " + (node.getName() == null ? "TEXT" : node.getName()) + ": " + node.asXML()); +logger.log(Level.TRACE, "node: " + (node.getName() == null ? "TEXT" : node.getName()) + ": " + node.asXML()); if (node.getNodeType() == Node.ELEMENT_NODE) { if ("DT".equals(node.getName())) { @@ -312,7 +366,7 @@ protected void determineComment(Type t, List allNodes, List commen // TODO this makes unexpected new lines commentText.add(processA(node)); } else { -logger.fine("unhandled node: " + node.getName()); +logger.log(Level.DEBUG, "unhandled node: " + node.getName()); replaceA(((Element) node), true); String text = node.asXML(); String[] lines = text.split("\\n"); @@ -325,10 +379,10 @@ protected void determineComment(Type t, List allNodes, List commen if (text.contains(rb.getString("token.comment.exclude.1")) || text.contains(rb.getString("token.comment.exclude.2"))) { if (i + 1 < allNodes.size() && "A".equals(allNodes.get(i + 1).getName())) { -logger.fine("ignore 1.1: " + text + allNodes.get(i + 1).asXML()); +logger.log(Level.DEBUG, "ignore 1.1: " + text + allNodes.get(i + 1).asXML()); i++; } else { -logger.fine("ignore 1.2: " + text); +logger.log(Level.DEBUG, "ignore 1.2: " + text); } } else { if (!text.isEmpty()) { @@ -339,7 +393,7 @@ protected void determineComment(Type t, List allNodes, List commen } } } else { -logger.fine("ignore 5: " + node.asXML()); +logger.log(Level.DEBUG, "ignore 5: " + node.asXML()); } } } @@ -352,15 +406,16 @@ protected List replaceA(Element element, boolean recursive) { boolean ignore = false; if ("A".equals(node.getName())) { int index = nodes.indexOf(node); -logger.finer("index: " + index); +logger.log(Level.TRACE, "index: " + index); if (index > 0) { Node before = nodes.get(index - 1); -logger.finer("before: " + before.asXML()); +logger.log(Level.TRACE, "before: " + before.asXML()); if (before.getNodeType() == Node.TEXT_NODE && ( before.getText().contains(rb.getString("token.comment.exclude.1")) || - before.getText().contains(rb.getString("token.comment.exclude.2")))) { + before.getText().contains(rb.getString("token.comment.exclude.2")) + )) { ignore = true; -logger.finer("ignore 1.0: " + before.getText() + node.asXML()); +logger.log(Level.TRACE, "ignore 1.0: " + before.getText() + node.asXML()); } } if (!ignore) { @@ -371,8 +426,40 @@ protected List replaceA(Element element, boolean recursive) { } } else { if (recursive) { - replaceA(((Element) node), true); + replaceA((Element) node, true); + } + } + } + } + return nodes; + } + + /** replace a tag fot @see tag */ + protected List replaceAforSee(Element element) { + List nodes = element.content(); + for (Node node : nodes) { + if (node.getNodeType() == Node.ELEMENT_NODE) { + boolean ignore = false; + if ("A".equals(node.getName())) { + int index = nodes.indexOf(node); + logger.log(Level.TRACE, "index: " + index); + if (index > 0) { + Node before = nodes.get(index - 1); + logger.log(Level.TRACE, "before: " + before.asXML()); + if (before.getNodeType() == Node.TEXT_NODE && ( + before.getText().contains(rb.getString("token.comment.exclude.1")) || + before.getText().contains(rb.getString("token.comment.exclude.2")) + )) { + ignore = true; + logger.log(Level.TRACE, "ignore 1.0: " + before.getText() + node.asXML()); + } + } + if (!ignore) { + String string = processAforSee(node); + nodes.set(nodes.indexOf(node), new DefaultText(string)); } + } else { + replaceAforSee((Element) node); } } } @@ -533,7 +620,7 @@ then one with the type ( href ) and field name text() String fieldName = constantNode.valueOf(xpaths[1]); String constantValue = constantNode.valueOf(xpaths[2]); - //logger.fine( typeName + "#" + fieldName +"=" +constantValue ); + //logger.log(Level.DEBUG, typeName + "#" + fieldName +"=" +constantValue ); Type type = types.get(typeName); if (type == null) { @@ -564,7 +651,7 @@ then one with the type ( href ) and field name text() * * @return DOCUMENT ME! */ - private Object determineConstantValue(String typeName, String constantvalue) { + private static Object determineConstantValue(String typeName, String constantvalue) { //( Boolean, Byte, Char, Double, Float, Integer, Long, Short ) Object value = null; @@ -828,7 +915,7 @@ protected List tokenizeWordListWithTypeParameters(String text, String de currentWord += ch; } else if(inTypeParamStackCount == 0 && delimeters.indexOf(ch) >= 0) { // at a word delimeter - if(!"".equals(currentWord)) { + if(!currentWord.isEmpty()) { words.add(currentWord); currentWord = ""; } @@ -837,7 +924,7 @@ protected List tokenizeWordListWithTypeParameters(String text, String de } } - if(!"".equals(currentWord)) + if(!currentWord.isEmpty()) words.add(currentWord); return(words); @@ -916,10 +1003,10 @@ protected void determineFields(Type type, Document typeXml, String nameXpath) { // if jdk supported xpath2.0, we could use regex for xpath if (rb.getLocale().equals(Locale.JAPANESE)) { if (!fieldsXpath.equals(fieldsXpath.replace("サマリー", "概要"))) { - logger.warning("no fields: " + fieldsXpath); + logger.log(Level.WARNING, "no fields: " + fieldsXpath); fieldsXpath = fieldsXpath.replace("サマリー", "概要"); fieldList = typeXml.selectNodes(fieldsXpath); - logger.fine("fields: " + fieldList.size()); + logger.log(Level.DEBUG, "fields: " + fieldList.size()); } } } @@ -934,7 +1021,7 @@ protected void determineFields(Type type, Document typeXml, String nameXpath) { String fieldtypeParam = convertNodesToString(fieldtypeNode); -// logger.fine("fieldtype: " + fieldtypeParam); +// logger.log(Level.DEBUG, "fieldtype: " + fieldtypeParam); Parameter temp = determineParameter(type, fieldtypeParam, false); field.setType(temp.getType()); field.setArray(temp.isArray()); @@ -946,7 +1033,7 @@ protected void determineFields(Type type, Document typeXml, String nameXpath) { Element fieldNameNode = (Element) fieldNode.selectSingleNode(nameXpath); String fieldName = fieldNameNode.getText(); -// logger.fine("fieldname: " + fieldName); +// logger.log(Level.DEBUG, "fieldname: " + fieldName); field.setName(fieldName); } } @@ -971,7 +1058,7 @@ protected void determineInnerTypes(Type type, Document typeXml, String nameXpath Node innerTypeNode = innerTypeNodes.get(i); String innerTypeName = convertNodesToString(innerTypeNode.selectSingleNode(nameXpath)); -logger.fine(innerTypeName); +logger.log(Level.DEBUG, innerTypeName); Type innerType = type.createInnerType(); innerType.setTypeName(innerTypeName); @@ -1004,7 +1091,7 @@ protected void determineEnumConsts(String enumConstsXpath, Type type, Document t Element enumConstNameNode = (Element) enumConstNode.selectSingleNode(nameXpath); String enumConstName = enumConstNameNode.getText(); - //logger.fine( "enumConstName: " + enumConstName ); + //logger.log(Level.DEBUG, "enumConstName: " + enumConstName ); enumConst.setName(enumConstName); } } @@ -1065,10 +1152,10 @@ protected void determineConstructors(String constructorsXpath, Type type, Docume // if jdk supported xpath2.0, we could use regex for xpath if (rb.getLocale().equals(Locale.JAPANESE)) { if (!constructorsXpath.equals(constructorsXpath.replace("サマリー", "概要"))) { - logger.warning("no constructors: " + constructorsXpath); + logger.log(Level.WARNING, "no constructors: " + constructorsXpath); constructorsXpath = constructorsXpath.replace("サマリー", "概要"); methodList = typeXml.selectNodes(constructorsXpath); - logger.fine("constructors: " + methodList.size()); + logger.log(Level.DEBUG, "constructors: " + methodList.size()); } } } @@ -1088,14 +1175,14 @@ private void determineMethodParameters(Type t, Method method, List paramli Element methodNameElement = (Element) paramlistNodes.get(0); // link in the same file method.setName(methodNameElement.getText()); - //logger.fine(" methodname: " + method.getMethodName()); + //logger.log(Level.DEBUG, " methodname: " + method.getMethodName()); StringBuilder methodParams = new StringBuilder(); for (int paramIdx = 1; paramIdx < paramlistNodes.size(); paramIdx++) { //expect the methodName in the first parameter, so skip it Node paramNode = paramlistNodes.get(paramIdx); - // logger.fine( paramNode.getNodeTypeName()+" "+paramNode.getStringValue() ); + // logger.log(Level.DEBUG, paramNode.getNodeTypeName()+" "+paramNode.getStringValue() ); // need to combine method description into a single text which can then // be parsed easily. TypeVariables tend to have length 1 ( E, V, K etc. ) which can easily match one character of the link to the generic parent if (paramNode.getNodeType() == Node.ELEMENT_NODE && "A".equals(paramNode.getName()) && paramNode.getText().length() > 1 && paramNode.valueOf("@href").contains(paramNode.getText())) { @@ -1352,10 +1439,10 @@ protected void determineMethods(String methodXpath, Type type, Document typeXml) // if jdk supported xpath2.0, we could use regex for xpath if (rb.getLocale().equals(Locale.JAPANESE)) { if (!methodXpath.equals(methodXpath.replace("サマリー", "概要"))) { - logger.warning("no methods: " + methodXpath); + logger.log(Level.WARNING, "no methods: " + methodXpath); methodXpath = methodXpath.replace("サマリー", "概要"); methodList = typeXml.selectNodes(methodXpath); - logger.fine("methods: " + methodList.size()); + logger.log(Level.DEBUG, "methods: " + methodList.size()); } } } @@ -1525,7 +1612,7 @@ private String javadocLinkToTypename(String link) { }; /** */ - private boolean isDefaultJavadocUrl(String url) { + private static boolean isDefaultJavadocUrl(String url) { return Arrays.stream(defaultJavadocUrls).anyMatch(url::startsWith); } @@ -1816,7 +1903,7 @@ protected void determineImplementsList(Type t, Document typeXml) { if (!combinedText.isEmpty()) { List words = tokenizeWordListWithTypeParameters(combinedText, " ,\t\n\r\f"); for (String typeName : words) { -logger.finest("token: " + typeName); +logger.log(Level.TRACE, "token: " + typeName); t.addImplementsType(typeName); } } @@ -1846,12 +1933,12 @@ protected String getLabelString(Type type) { /** determines language */ protected boolean isLanguageOf(Locale locale) { // umm... -logger.finer("lang: " + rb.getLocale().getLanguage()); +logger.log(Level.TRACE, "lang: " + rb.getLocale().getLanguage()); return rb.getLocale().getLanguage().equals(locale.getLanguage()); } /** version comparator like 1.8.3 */ - protected static VersionComparator versionComparator = new VersionComparator(); + protected static final VersionComparator versionComparator = new VersionComparator(); /** */ public boolean isSuitableVersion(String version) { @@ -1919,7 +2006,7 @@ public void setExternalLinks(List externalLinks) { /** check an url (including local path) is exist or not */ private static boolean exists(String url) { URI uri; - if (!url.startsWith("http:")) { + if (!url.startsWith("http:") && !url.startsWith("https:")) { return Files.exists(Paths.get(url)); } else { try { @@ -1939,14 +2026,14 @@ private static boolean exists(String url) { */ private static InputSource getInputSource(String url) throws IOException { URI uri = null; - if (!url.startsWith("http:")) { + if (!url.startsWith("http:") && !url.startsWith("https:")) { uri = Paths.get(url).toUri(); } else { uri = URI.create(url); } InputSource is = new InputSource(uri.toURL().openStream()); String encoding = System.getProperty("codavaj.file.encoding", System.getProperty("file.encoding")); -logger.finer("encoding: " + encoding); +logger.log(Level.TRACE, "encoding: " + encoding); is.setEncoding(encoding); return is; } @@ -1974,7 +2061,7 @@ public static ParserUtils factory(String dir) throws IOException { String lang = langNode.getText(); rb = ResourceBundle.getBundle("token", new Locale(lang)); -logger.info("lang >6: " + rb.getLocale().getLanguage()); +logger.log(Level.INFO, "lang >6: " + rb.getLocale().getLanguage()); String versionText = document.selectSingleNode("//comment()[contains(., \"Generated by javadoc\")]").getText(); int firstBracket = versionText.indexOf('('); @@ -1984,13 +2071,13 @@ public static ParserUtils factory(String dir) throws IOException { } else { // w/o lang maybe v6 String language = System.getProperty("codavaj.language"); -logger.info("language: " + language); +logger.log(Level.INFO, "language: " + language); if (language != null) { rb = ResourceBundle.getBundle("token", new Locale(language)); } else { rb = ResourceBundle.getBundle("token"); } -logger.info("lang =6: " + rb.getLocale().getLanguage()); +logger.log(Level.INFO, "lang =6: " + rb.getLocale().getLanguage()); parserUtil = new ParserUtils(); } @@ -2003,7 +2090,7 @@ public static ParserUtils factory(String dir) throws IOException { parserUtil.classes = parserUtil.getAllFqTypenames(allClasses); parserUtil.javadocDirName = dir; -logger.fine("parser: " + parserUtil.getClass().getName()); +logger.log(Level.DEBUG, "parser: " + parserUtil.getClass().getName()); return parserUtil; } catch (SAXException e) { throw new IllegalStateException(e); @@ -2059,7 +2146,7 @@ public void processType(Type type) throws IOException { } catch (SAXException e) { throw new IllegalStateException(e); } catch (Exception e) { -e.printStackTrace(); + logger.log(Level.ERROR, e.getMessage(), e); throw new ParseException(typeXml.asXML(), e); } } @@ -2077,6 +2164,8 @@ public void processConstant(Map maps, boolean lenient) throws IOEx Document allConstants = loadHtmlAsDom(getInputSource(allConstantsFilename)); determineConstants(allConstants, maps, lenient); + } catch (FileNotFoundException e) { +Debug.println("accept not found???"); // TODO } catch (SAXException e) { throw new IllegalStateException(e); } @@ -2125,7 +2214,7 @@ private static Document loadHtmlMetadataAsDom(InputSource html) throws SAXExcept DOMReader xmlReader = new DOMReader(); Document result = xmlReader.read(parser.getDocument()); -// logger.info("XML " + prettyPrint(result)); +// logger.log(Level.INFO, "XML " + prettyPrint(result)); return result; } @@ -2159,11 +2248,11 @@ private Document loadHtmlAsDom(InputSource html) throws SAXException, IOExceptio parser.parse(html); //String html = new String(baos.toByteArray(), "UTF-8"); - //logger.info(html); + //logger.log(Level.INFO, html); DOMReader xmlReader = new DOMReader(); Document result = xmlReader.read(parser.getDocument()); - //logger.info("XML " + prettyPrint(result)); + //logger.log(Level.INFO, "XML " + prettyPrint(result)); return result; } @@ -2196,7 +2285,7 @@ protected ElementRemover getRemover() { } /** */ - protected FullyQualifiedNameMap fqnm = new FullyQualifiedNameMap(); + protected final FullyQualifiedNameMap fqnm = new FullyQualifiedNameMap(); /** */ public FullyQualifiedNameMap getFullyQualifiedNameMap() { diff --git a/src/main/java/org/codavaj/process/docparser/ParserUtils10.java b/src/main/java/org/codavaj/process/docparser/ParserUtils10.java index b8f7da5..87ca5c9 100644 --- a/src/main/java/org/codavaj/process/docparser/ParserUtils10.java +++ b/src/main/java/org/codavaj/process/docparser/ParserUtils10.java @@ -85,5 +85,3 @@ public boolean isSuitableVersion(String version) { return versionComparator.compare(version, "10.0.0") >= 0 && versionComparator.compare(version, "11.0.0") < 0; } } - -/* */ diff --git a/src/main/java/org/codavaj/process/docparser/ParserUtils11.java b/src/main/java/org/codavaj/process/docparser/ParserUtils11.java index cefad58..e8bf284 100644 --- a/src/main/java/org/codavaj/process/docparser/ParserUtils11.java +++ b/src/main/java/org/codavaj/process/docparser/ParserUtils11.java @@ -96,5 +96,3 @@ public String getFirstIndexFileName() { return "allclasses-index.html"; } } - -/* */ diff --git a/src/main/java/org/codavaj/process/docparser/ParserUtils12.java b/src/main/java/org/codavaj/process/docparser/ParserUtils12.java index 90ccc6a..11e0062 100644 --- a/src/main/java/org/codavaj/process/docparser/ParserUtils12.java +++ b/src/main/java/org/codavaj/process/docparser/ParserUtils12.java @@ -43,5 +43,3 @@ public boolean isSuitableVersion(String version) { return versionComparator.compare(version, "12.0.0") >= 0 && versionComparator.compare(version, "13.0.0") < 0; } } - -/* */ diff --git a/src/main/java/org/codavaj/process/docparser/ParserUtils13.java b/src/main/java/org/codavaj/process/docparser/ParserUtils13.java index 495a496..beea4b5 100644 --- a/src/main/java/org/codavaj/process/docparser/ParserUtils13.java +++ b/src/main/java/org/codavaj/process/docparser/ParserUtils13.java @@ -31,5 +31,3 @@ public boolean isSuitableVersion(String version) { return versionComparator.compare(version, "13.0.0") >= 0; } } - -/* */ diff --git a/src/main/java/org/codavaj/process/docparser/ParserUtils8.java b/src/main/java/org/codavaj/process/docparser/ParserUtils8.java index 3a7e2c6..58afb22 100644 --- a/src/main/java/org/codavaj/process/docparser/ParserUtils8.java +++ b/src/main/java/org/codavaj/process/docparser/ParserUtils8.java @@ -6,12 +6,12 @@ package org.codavaj.process.docparser; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; import org.codavaj.type.Type; import org.cyberneko.html.filters.ElementRemover; @@ -21,6 +21,7 @@ import static com.rainerhahnekamp.sneakythrow.Sneaky.sneaked; + /** * for version 1.8.x * @@ -31,7 +32,7 @@ */ public class ParserUtils8 extends ParserUtils { - private static final Logger logger = Logger.getLogger(ParserUtils8.class.getName()); + private static final Logger logger = System.getLogger(ParserUtils8.class.getName()); /* details */ @Override @@ -50,7 +51,7 @@ protected void determineComment(Type t, List allNodes, List commen commentText.add(line.trim()); } } else { -logger.fine("ignore 1: " + text); +logger.log(Level.DEBUG, "ignore 1: " + text); } } else if ("DL".equals(node.getName())) { List nodes = node.selectNodes("*[name()='DT' or name()='DD']"); @@ -277,7 +278,7 @@ protected void extendedType(Type t, Document typeXml) { combinedText.append(convertNodesToString(n)); } String text = combinedText.toString().trim().replaceFirst("^.+\\s*" + keyword + "\\s+([\\w_\\$\\.\\<\\>]+)\\s*.*$", "$1"); - if (text.length() > 0) { + if (!text.isEmpty()) { String typeName = fqnm.toFullyQualifiedName(t, text); t.setSuperType(typeName); } @@ -311,7 +312,7 @@ public void determineImplementsList(Type t, Document typeXml) { String extension = t.isInterface() ? "extends" : "implements"; List implementsTypeAs = typeXml.selectNodes("//LI/text()[contains(.,'" + extension + "')]/following-sibling::A"); - if (implementsTypeAs != null && implementsTypeAs.size() > 0) { + if (implementsTypeAs != null && !implementsTypeAs.isEmpty()) { for (Object implementsTypeA : implementsTypeAs) { Node node = (Node) implementsTypeA; diff --git a/src/main/java/org/codavaj/process/loader/JarLoader.java b/src/main/java/org/codavaj/process/loader/JarLoader.java index a137ab4..2d4b011 100644 --- a/src/main/java/org/codavaj/process/loader/JarLoader.java +++ b/src/main/java/org/codavaj/process/loader/JarLoader.java @@ -17,20 +17,21 @@ package org.codavaj.process.loader; import java.io.File; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.jar.JarFile; -import java.util.logging.Level; -import java.util.logging.Logger; import org.codavaj.ProcessException; import org.codavaj.process.Progressive; import org.codavaj.type.TypeFactory; + /** * Load the complete contents of a Jar file into a TypeFactory. */ public class JarLoader implements Progressive { - private static final Logger logger = Logger.getLogger(JarLoader.class.getName()); + private static final Logger logger = System.getLogger(JarLoader.class.getName()); private String jarFileName; private TypeFactory typeFactory; diff --git a/src/main/java/org/codavaj/process/search/Search.java b/src/main/java/org/codavaj/process/search/Search.java index 3576b0e..ae1b928 100644 --- a/src/main/java/org/codavaj/process/search/Search.java +++ b/src/main/java/org/codavaj/process/search/Search.java @@ -16,19 +16,20 @@ package org.codavaj.process.search; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import org.codavaj.ProcessException; import org.codavaj.process.Progressive; import org.codavaj.type.TypeFactory; + /** * DOCUMENT ME! */ public class Search implements Progressive { - private static final Logger logger = Logger.getLogger(Search.class.getName()); + private static final Logger logger = System.getLogger(Search.class.getName()); private TypeFactory javadocTypeFactory; private TypeFactory jarTypeFactory; diff --git a/src/main/java/org/codavaj/process/search/SearchAlgorithm.java b/src/main/java/org/codavaj/process/search/SearchAlgorithm.java index b28aeeb..8ba046f 100644 --- a/src/main/java/org/codavaj/process/search/SearchAlgorithm.java +++ b/src/main/java/org/codavaj/process/search/SearchAlgorithm.java @@ -1,18 +1,20 @@ package org.codavaj.process.search; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.List; -import java.util.logging.Logger; import org.codavaj.type.Package; import org.codavaj.type.TypeFactory; + public class SearchAlgorithm { - private static final Logger logger = Logger.getLogger(SearchAlgorithm.class.getName()); + private static final Logger logger = System.getLogger(SearchAlgorithm.class.getName()); - private TypeFactory docFactory; - private TypeFactory jarFactory; - private SearchContext ctx; + private final TypeFactory docFactory; + private final TypeFactory jarFactory; + private final SearchContext ctx; public SearchAlgorithm( TypeFactory javadoc, TypeFactory jar, SearchContext ctx ) { this.docFactory = javadoc; @@ -24,13 +26,13 @@ public void search() { Package javadocRoot = docFactory.getDefaultPackage(); Package jarRoot = jarFactory.getDefaultPackage(); - logger.info("Attempting " + javadocRoot.getName() + " -> " + jarRoot.getName()); + logger.log(Level.INFO, "Attempting " + javadocRoot.getName() + " -> " + jarRoot.getName()); ctx.setMap(javadocRoot, jarRoot); if ( matchAll(javadocRoot, jarRoot )) { - logger.info( "Matched All " + javadocRoot.getName() +" -> " + jarRoot.getName()); + logger.log(Level.INFO, "Matched All " + javadocRoot.getName() +" -> " + jarRoot.getName()); } else { - logger.info("NOT Matched All " + javadocRoot.getName() +" -> " + jarRoot.getName()); + logger.log(Level.INFO, "NOT Matched All " + javadocRoot.getName() +" -> " + jarRoot.getName()); ctx.removeMap(javadocRoot, jarRoot); } @@ -45,7 +47,7 @@ private boolean matchAll( Package javadocPackage, Package jarPackage ) { boolean matchesAllSubPackages = true; for (Package docsubPackage : docsubPackages) { if (!matchAny(docsubPackage, jarsubPackages)) { - logger.info("Package " + docsubPackage.getName() + " doesn't match any sub packages of " + jarPackage.getName()); + logger.log(Level.INFO, "Package " + docsubPackage.getName() + " doesn't match any sub packages of " + jarPackage.getName()); matchesAllSubPackages = false; break; } @@ -57,14 +59,14 @@ private boolean matchAll( Package javadocPackage, Package jarPackage ) { private boolean matchAny( Package javadocPackage, List jarPackages ) { for (Package jarPackage : jarPackages) { if (ctx.getMap(jarPackage) != null) { - logger.info("Already mapped " + jarPackage.getName()); + logger.log(Level.INFO, "Already mapped " + jarPackage.getName()); continue; } - logger.info("Attempting " + javadocPackage.getName() + " -> " + jarPackage.getName()); + logger.log(Level.INFO, "Attempting " + javadocPackage.getName() + " -> " + jarPackage.getName()); ctx.setMap(javadocPackage, jarPackage); if (matchAll(javadocPackage, jarPackage)) { - logger.info("Matched ALL " + javadocPackage.getName() + " -> " + jarPackage.getName()); + logger.log(Level.INFO, "Matched ALL " + javadocPackage.getName() + " -> " + jarPackage.getName()); return true; } ctx.removeMap(javadocPackage, jarPackage); diff --git a/src/main/java/org/codavaj/process/search/SearchContext.java b/src/main/java/org/codavaj/process/search/SearchContext.java index 7131d2f..5843bd4 100644 --- a/src/main/java/org/codavaj/process/search/SearchContext.java +++ b/src/main/java/org/codavaj/process/search/SearchContext.java @@ -8,8 +8,8 @@ public class SearchContext { - private Map types = new HashMap<>(); - private Map packages = new HashMap<>(); + private final Map types = new HashMap<>(); + private final Map packages = new HashMap<>(); public void setMap( Package javadocPackage, Package jarPackage ) { if ( packages.containsKey(jarPackage.getName())) { diff --git a/src/main/java/org/codavaj/process/srcwriter/SrcWriter.java b/src/main/java/org/codavaj/process/srcwriter/SrcWriter.java index d672d60..006c1b8 100644 --- a/src/main/java/org/codavaj/process/srcwriter/SrcWriter.java +++ b/src/main/java/org/codavaj/process/srcwriter/SrcWriter.java @@ -20,9 +20,9 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; import org.codavaj.MissingParameterException; import org.codavaj.ProcessException; @@ -31,12 +31,13 @@ import org.codavaj.type.Type; import org.codavaj.type.TypeFactory; + /** * DOCUMENT ME! */ public class SrcWriter implements Progressive { - private static final Logger logger = Logger.getLogger(SrcWriter.class.getName()); + private static final Logger logger = System.getLogger(SrcWriter.class.getName()); /** * DOCUMENT ME! @@ -59,7 +60,7 @@ public Void process() throws ProcessException { srcDir.mkdirs(); } if (!srcDir.isDirectory()) { - throw new ProcessException("" + srcDir + " must be a directory."); + throw new ProcessException(srcDir + " must be a directory."); } List alltypes = typeFactory.getTypes(); @@ -97,7 +98,7 @@ public Void process() throws ProcessException { bw.flush(); bw.close(); } catch (IOException iox) { - logger.log(Level.SEVERE, "Error processing " + type.getTypeName(), iox); + logger.log(Level.ERROR, "Error processing " + type.getTypeName(), iox); } } return null; diff --git a/src/main/java/org/codavaj/process/srcwriter/WriterUtils.java b/src/main/java/org/codavaj/process/srcwriter/WriterUtils.java index 6208161..02dc75a 100644 --- a/src/main/java/org/codavaj/process/srcwriter/WriterUtils.java +++ b/src/main/java/org/codavaj/process/srcwriter/WriterUtils.java @@ -35,7 +35,7 @@ */ public class WriterUtils { - private static String LINEFEED = System.getProperty("line.separator"); + private static final String LINEFEED = System.getProperty("line.separator"); /** * Formatted representation of the Type. @@ -78,7 +78,7 @@ public static void print(Type t, Writer w) throws IOException { } /** no need to be public method at interface or annotation */ - private boolean isCondition1(Type type, Modifiable t) { + private static boolean isCondition1(Type type, Modifiable t) { if (t instanceof Method && (type.isAnnotation() || type.isInterface())) { return false; } @@ -86,7 +86,7 @@ private boolean isCondition1(Type type, Modifiable t) { } /** no need to be abstract method at interface or annotation */ - private boolean isCondition2(Type type, Modifiable t) { + private static boolean isCondition2(Type type, Modifiable t) { if (t instanceof Method && (type.isAnnotation() || type.isInterface())) { return false; } @@ -139,7 +139,7 @@ protected void printLineFeed() throws IOException { } protected void printComment(List commentText, int indentation) throws IOException { - if (commentText == null || commentText.size() == 0) { + if (commentText == null || commentText.isEmpty()) { return; } @@ -228,11 +228,11 @@ protected void print(Object value) if (value instanceof String) { w.write("\"" + value + "\""); } else if (value instanceof Float) { - w.write("" + value + "f"); + w.write(value + "f"); } else if (value instanceof Double) { - w.write("" + value + "d"); + w.write(value + "d"); } else if (value instanceof Long) { - w.write("" + value + "l"); + w.write(value + "l"); } else if (value instanceof Character) { char c = (Character) value; w.write("(char)" + (int)c ); @@ -399,7 +399,7 @@ protected void print(Type t, int indentation) } if ((t.getImplementsList() != null) - && (t.getImplementsList().size() > 0)) { + && (!t.getImplementsList().isEmpty())) { if (t.isInterface()) { w.write(" extends "); } else { diff --git a/src/main/java/org/codavaj/process/wget/LinkUtils.java b/src/main/java/org/codavaj/process/wget/LinkUtils.java index faf00de..22d6a5f 100644 --- a/src/main/java/org/codavaj/process/wget/LinkUtils.java +++ b/src/main/java/org/codavaj/process/wget/LinkUtils.java @@ -16,14 +16,16 @@ package org.codavaj.process.wget; -import java.util.logging.Logger; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; + /** * A utility set for working with URL's from javadocs. */ public class LinkUtils { - private static final Logger logger = Logger.getLogger(LinkUtils.class.getName()); + private static final Logger logger = System.getLogger(LinkUtils.class.getName()); /** Separator in URL's within javadocs. */ protected static final String URL_SEPARATOR = "/"; @@ -58,12 +60,12 @@ public String relativeUrl(String rootUrl, String url, String link) { } if (!rootUrl.endsWith(URL_SEPARATOR)) { - logger.warning("Root url " + rootUrl + " is not normalized"); + logger.log(Level.WARNING, "Root url " + rootUrl + " is not normalized"); return null; } - if ("".equals(link.trim())) { + if (link.trim().isEmpty()) { return null; } @@ -92,7 +94,7 @@ public String relativeUrl(String rootUrl, String url, String link) { // : ./f/g // : ../../f/g -> http://a/b/c/d/e/../../f/g if (link.startsWith(URL_SEPARATOR)) { - logger.fine("link absolute " + link); + logger.log(Level.DEBUG, "link absolute " + link); return null; } @@ -115,19 +117,19 @@ public String relativeUrl(String rootUrl, String url, String link) { if (link.startsWith(path)) { // a String rel = link.substring(path.length()); - logger.fine("(a)link: " + link + " -> " + rel); + logger.log(Level.DEBUG, "(a)link: " + link + " -> " + rel); link = rel; } if (link.startsWith("http") || link.startsWith("ftp") || link.startsWith("email")) { // link is not relative - logger.fine("link " + link + " is not relative - skipping."); + logger.log(Level.DEBUG, "link " + link + " is not relative - skipping."); return null; } if (link.contains("../")) { - logger.fine("cannot handle relative links with ../ - " + link); + logger.log(Level.DEBUG, "cannot handle relative links with ../ - " + link); return null; } @@ -135,7 +137,7 @@ public String relativeUrl(String rootUrl, String url, String link) { String absoluteUrl = path + link; String relativeUrl = absoluteUrl.substring(rootUrl.length()); - logger.info("relativeUrl " + relativeUrl); + logger.log(Level.INFO, "relativeUrl " + relativeUrl); return relativeUrl; } diff --git a/src/main/java/org/codavaj/process/wget/Wget.java b/src/main/java/org/codavaj/process/wget/Wget.java index 2ac9736..0f2c443 100644 --- a/src/main/java/org/codavaj/process/wget/Wget.java +++ b/src/main/java/org/codavaj/process/wget/Wget.java @@ -19,9 +19,9 @@ import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.Stack; -import java.util.logging.Level; -import java.util.logging.Logger; import org.codavaj.ProcessException; import org.codavaj.process.ProgressEvent; @@ -35,12 +35,13 @@ import com.meterware.httpunit.WebRequest; import com.meterware.httpunit.WebResponse; + /** * DOCUMENT ME! */ public class Wget implements Progressive { - private static final Logger logger = Logger.getLogger(LinkUtils.class.getName()); + private static final Logger logger = System.getLogger(Wget.class.getName()); private static final String CONST_INDEX_HTML = "index.html"; private static final String CONST_INDEX_ALL_HTML = "index-all.html"; @@ -55,7 +56,7 @@ public class Wget implements Progressive { private boolean overwriteFiles = false; private int retryCount = 5; private long retryWait = 10000; - private LinkUtils linkUtil = new LinkUtils(); + private final LinkUtils linkUtil = new LinkUtils(); /** * Wget processes the project by downloading the javadoc tree and saving @@ -94,7 +95,7 @@ public Void process() throws ProcessException { fetch.push(CONST_CONSTANTS_HTML); fetch.push(CONST_SERIALIZED_FORM_HTML); - while (fetch.size() > 0) { + while (!fetch.isEmpty()) { getPage(wc, got, fetch); } } catch (Exception e) { @@ -119,19 +120,19 @@ private void getPage(WebConversation wc, Stack got, Stack fetch) saveContent(relativePath, response); if (!HTML_CONTENT.equalsIgnoreCase(response.getContentType())) { - logger.fine("not html " + url); + logger.log(Level.DEBUG, "not html " + url); return; } - logger.fine("The page " + relativePath + " contains " + logger.log(Level.DEBUG, "The page " + relativePath + " contains " + response.getLinks().length + " links"); if (response.getFrameNames() != null) { // we have frames, so we need to put the link to each frame onto the for (int i = 0; i < response.getFrameNames().length; i++) { String framename = response.getFrameNames()[i]; - logger.fine("Frame " + framename); + logger.log(Level.DEBUG, "Frame " + framename); HTMLElement[] frame = response.getElementsWithName(framename); @@ -141,7 +142,7 @@ private void getPage(WebConversation wc, Stack got, Stack fetch) addRelativeUrl(url, src, got, fetch); } - logger.info(frame[0].toString()); + logger.log(Level.INFO, frame[0].toString()); } } @@ -183,7 +184,7 @@ private WebResponse fetchPage(WebConversation wc, String url) lastException = e; if (i < retryCount) { - logger.fine("Failed to get page " + url + " ... retrying" + logger.log(Level.DEBUG, "Failed to get page " + url + " ... retrying" + e.getMessage()); Thread.sleep(retryWait); } @@ -202,18 +203,18 @@ private void addRelativeUrl(String url, String link, Stack got, Stack getCommentAsString() { - if (getComment() != null && getComment().size() > 0) { + if (getComment() != null && !getComment().isEmpty()) { StringBuilder sb = new StringBuilder("/**\n"); for (String line : getComment()) { sb.append(" * "); @@ -50,7 +50,7 @@ default Optional getCommentAsString() { /** @return without javadoc comment marker */ default Optional getInnerCommentAsString() { - if (getComment() != null && getComment().size() > 0) { + if (getComment() != null && !getComment().isEmpty()) { StringBuilder sb = new StringBuilder(); for (String line : getComment()) { sb.append(line); @@ -62,5 +62,3 @@ default Optional getInnerCommentAsString() { } } } - -/* */ diff --git a/src/main/java/org/codavaj/type/Method.java b/src/main/java/org/codavaj/type/Method.java index ab79024..1eac28d 100644 --- a/src/main/java/org/codavaj/type/Method.java +++ b/src/main/java/org/codavaj/type/Method.java @@ -26,8 +26,8 @@ public class Method extends Modifiable implements Commentable { private String name; private String typeParameters; // generics, see http://java.sun.com/docs/books/jls/third_edition/html/classes.html#8.4.1 private Parameter returnParameter; // ignore name - private List parameterList = new ArrayList<>(); - private List throwsList = new ArrayList<>(); + private final List parameterList = new ArrayList<>(); + private final List throwsList = new ArrayList<>(); private List comment = null; private String defaultValue = null; @@ -41,7 +41,7 @@ public String toString() { sb.append(p.toString()); sb.append(", "); } - if (parameterList.size() > 0) { + if (!parameterList.isEmpty()) { sb.setLength(sb.length() - 2); } sb.append(")"); diff --git a/src/main/java/org/codavaj/type/Package.java b/src/main/java/org/codavaj/type/Package.java index 2c83c61..23ee362 100644 --- a/src/main/java/org/codavaj/type/Package.java +++ b/src/main/java/org/codavaj/type/Package.java @@ -26,10 +26,10 @@ */ public class Package { static final String defaultPackageName = ""; - private String name; + private final String name; private Package parentPackage; - private Map types = new HashMap<>(); - private Map packages = new HashMap<>(); + private final Map types = new HashMap<>(); + private final Map packages = new HashMap<>(); Package(String packagename) { name = packagename; diff --git a/src/main/java/org/codavaj/type/Type.java b/src/main/java/org/codavaj/type/Type.java index f4eeda1..9ae3ceb 100644 --- a/src/main/java/org/codavaj/type/Type.java +++ b/src/main/java/org/codavaj/type/Type.java @@ -17,27 +17,29 @@ package org.codavaj.type; import java.io.File; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.logging.Logger; + /** * A reflection-like representation of java.lang.Class. */ public class Type extends Modifiable implements Commentable { - private static final Logger logger = Logger.getLogger(Type.class.getName()); + private static final Logger logger = System.getLogger(Type.class.getName()); private String superType = null; - private List implementsList = new ArrayList<>(); - private List methodList = new ArrayList<>(); - private List fieldList = new ArrayList<>(); - private List enumConstList = new ArrayList<>(); - private List constructorList = new ArrayList<>(); + private final List implementsList = new ArrayList<>(); + private final List methodList = new ArrayList<>(); + private final List fieldList = new ArrayList<>(); + private final List enumConstList = new ArrayList<>(); + private final List constructorList = new ArrayList<>(); private List comment = null; - private List innerTypeList = new ArrayList<>(); + private final List innerTypeList = new ArrayList<>(); private String typeName; private String typeParameters; // generics private Package pckg; @@ -380,7 +382,7 @@ public Method lookupConstructor(List params) { * @return a method if found, otherwise null. */ public Method lookupMethodByName(String name, List params) { -logger.finer(methodList.toString()); +logger.log(Level.TRACE, methodList.toString()); for (Method m : methodList) { if (name.equals(m.getName())) { @@ -432,7 +434,7 @@ public String getTypeParameters() { /** */ public boolean isTypeParameter(String typeParameter) { - return typeParameters == null ? false : Arrays.stream(typeParameters.split("[\\s,<>]")).anyMatch(typeParameter::equals); + return typeParameters == null ? false : Arrays.asList(typeParameters.split("[\\s,<>]")).contains(typeParameter); } /** diff --git a/src/main/java/org/codavaj/type/TypeFactory.java b/src/main/java/org/codavaj/type/TypeFactory.java index 97f7fd0..0eba6c2 100644 --- a/src/main/java/org/codavaj/type/TypeFactory.java +++ b/src/main/java/org/codavaj/type/TypeFactory.java @@ -16,6 +16,8 @@ package org.codavaj.type; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; @@ -23,23 +25,22 @@ import java.util.TreeMap; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.zip.ZipEntry; import org.codavaj.process.docparser.FullyQualifiedNameMap; import org.codavaj.type.reflection.ReflectionUtils; import org.codavaj.type.reflection.SingleJarClassLoader; + /** * A container for Types. */ public class TypeFactory { - private static final Logger logger = Logger.getLogger(TypeFactory.class.getName()); + private static final Logger logger = System.getLogger(TypeFactory.class.getName()); - private Map types = new TreeMap<>(); - private Map packages = new TreeMap<>(); + private final Map types = new TreeMap<>(); + private final Map packages = new TreeMap<>(); /** * Creates a new TypeFactory object. @@ -171,10 +172,10 @@ public void link() { try { enclosingType.addInnerType(type); } catch (IllegalArgumentException e) { - logger.info("type " + type.getShortName() + " was already defined in type " + enclosingType.getTypeName()); + logger.log(Level.INFO, "type " + type.getShortName() + " was already defined in type " + enclosingType.getTypeName()); } } else { - logger.warning("enclosing type " + type.getEnclosingType() + " was not found in type " + type.getTypeName()); + logger.log(Level.WARNING, "enclosing type " + type.getEnclosingType() + " was not found in type " + type.getTypeName()); } } diff --git a/src/main/java/org/codavaj/type/reflection/ReflectionUtils.java b/src/main/java/org/codavaj/type/reflection/ReflectionUtils.java index c1465fd..2f2c011 100644 --- a/src/main/java/org/codavaj/type/reflection/ReflectionUtils.java +++ b/src/main/java/org/codavaj/type/reflection/ReflectionUtils.java @@ -255,27 +255,18 @@ private static String getTypeName(String classname) { String strippedClassname = classname.substring(classname.lastIndexOf("[") + 1); - switch (strippedClassname.charAt(0)) { - case 'L': - return strippedClassname.substring(1, strippedClassname.length() - 1); // cut ; off end - case 'Z': - return "boolean"; - case 'B': - return "byte"; - case 'C': - return "char"; - case 'D': - return "double"; - case 'F': - return "float"; - case 'I': - return "int"; - case 'J': - return "long"; - case 'S': - return "short"; - } + return switch (strippedClassname.charAt(0)) { + case 'L' -> strippedClassname.substring(1, strippedClassname.length() - 1); // cut ; off end + case 'Z' -> "boolean"; + case 'B' -> "byte"; + case 'C' -> "char"; + case 'D' -> "double"; + case 'F' -> "float"; + case 'I' -> "int"; + case 'J' -> "long"; + case 'S' -> "short"; + default -> classname; + }; - return classname; // should never happen! } } diff --git a/src/main/java/org/codavaj/type/reflection/SingleJarClassLoader.java b/src/main/java/org/codavaj/type/reflection/SingleJarClassLoader.java index 5848357..37437f7 100644 --- a/src/main/java/org/codavaj/type/reflection/SingleJarClassLoader.java +++ b/src/main/java/org/codavaj/type/reflection/SingleJarClassLoader.java @@ -9,7 +9,7 @@ public class SingleJarClassLoader extends ClassLoader { - JarFile jarFile; + final JarFile jarFile; public SingleJarClassLoader(JarFile jarFile) { this.jarFile = jarFile; diff --git a/src/main/java/org/cyberneko/html/HTMLAugmentations.java b/src/main/java/org/cyberneko/html/HTMLAugmentations.java new file mode 100644 index 0000000..1d9acb7 --- /dev/null +++ b/src/main/java/org/cyberneko/html/HTMLAugmentations.java @@ -0,0 +1,134 @@ +/* + * Copyright 2004-2008 Andy Clark, Marc Guillemot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyberneko.html; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import org.apache.xerces.xni.Augmentations; + + +/** + * This class is here to overcome the XNI changes to the + * Augmentations interface. In early versions of XNI, the + * augmentations interface contained a clear() method to + * remove all of the items from the augmentations instance. A later + * version of XNI changed this method to removeAllItems(). + * Therefore, this class extends the augmentations interface and + * explicitly implements both of these methods. + *

+ * Note: + * This code is inspired by performance enhancements submitted by + * Marc-André Morissette. + * + * @author Andy Clark + */ +public class HTMLAugmentations implements Augmentations { + + // + // Data + // + + /** Augmentation items. */ + protected final Map fItems = new HashMap<>(); + + // + // Public methods + // + public HTMLAugmentations() { + // nothing + } + + /** + * Copy constructor + * + * @param augs the object to copy + */ + HTMLAugmentations(Augmentations augs) { + for (Enumeration keys = augs.keys(); keys.hasMoreElements(); ) { + String key = (String) keys.nextElement(); + Object value = augs.getItem(key); + if (value instanceof HTMLScanner.LocationItem) { + value = new HTMLScanner.LocationItem((HTMLScanner.LocationItem) value); + } + fItems.put(key, value); + } + } + + // since Xerces 2.3.0 + + /** Removes all of the elements in this augmentations object. */ + public void removeAllItems() { + fItems.clear(); + } + + // from Xerces 2.0.0 (beta4) until 2.3.0 + + /** Removes all of the elements in this augmentations object. */ + public void clear() { + fItems.clear(); + } + + // + // Augmentations methods + // + + /** + * Add additional information identified by a key to the Augmentations + * structure. + * + * @param key Identifier, can't be null + * @param item Additional information + * @return The previous value of the specified key in the Augmentations + * structure, or null if it did not have one. + */ + public Object putItem(String key, Object item) { + return fItems.put(key, item); + } + + /** + * Get information identified by a key from the Augmentations structure. + * + * @param key Identifier, can't be null + * @return The value to which the key is mapped in the Augmentations + * structure; null if the key is not mapped to any + * value. + */ + public Object getItem(String key) { + return fItems.get(key); + } + + /** + * Remove additional info from the Augmentations structure + * + * @param key Identifier, can't be null + * @return The previous value of the specified key in the Augmentations + * structure, or null if it did not have one. + */ + public Object removeItem(String key) { + return fItems.remove(key); + } + + /** + * Returns an enumeration of the keys in the Augmentations structure. + */ + public Enumeration keys() { + return Collections.enumeration(fItems.keySet()); + } +} diff --git a/src/main/java/org/cyberneko/html/HTMLComponent.java b/src/main/java/org/cyberneko/html/HTMLComponent.java new file mode 100644 index 0000000..8e4b9ff --- /dev/null +++ b/src/main/java/org/cyberneko/html/HTMLComponent.java @@ -0,0 +1,49 @@ +/* + * Copyright 2004-2008 Andy Clark, Marc Guillemot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyberneko.html; + +import org.apache.xerces.xni.parser.XMLComponent; + + +/** + * This interface extends the XNI XMLComponent interface + * to add methods that allow the preferred default values for features + * and properties to be queried. + * + * @author Andy Clark + * @version $Id: HTMLComponent.java,v 1.4 2005/02/14 03:56:54 andyc Exp $ + */ +public interface HTMLComponent extends XMLComponent { + + // + // HTMLComponent methods + // + + /** + * Returns the default state for a feature, or null if this + * component does not want to report a default value for this + * feature. + */ + Boolean getFeatureDefault(String featureId); + + /** + * Returns the default state for a property, or null if this + * component does not want to report a default value for this + * property. + */ + Object getPropertyDefault(String propertyId); +} diff --git a/src/main/java/org/cyberneko/html/HTMLConfiguration.java b/src/main/java/org/cyberneko/html/HTMLConfiguration.java new file mode 100644 index 0000000..fe7c6d1 --- /dev/null +++ b/src/main/java/org/cyberneko/html/HTMLConfiguration.java @@ -0,0 +1,666 @@ +/* + * Copyright 2002-2009 Andy Clark, Marc Guillemot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyberneko.html; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import org.apache.xerces.util.DefaultErrorHandler; +import org.apache.xerces.util.ParserConfigurationSettings; +import org.apache.xerces.xni.XMLDTDContentModelHandler; +import org.apache.xerces.xni.XMLDTDHandler; +import org.apache.xerces.xni.XMLDocumentHandler; +import org.apache.xerces.xni.XNIException; +import org.apache.xerces.xni.parser.XMLConfigurationException; +import org.apache.xerces.xni.parser.XMLDocumentFilter; +import org.apache.xerces.xni.parser.XMLDocumentSource; +import org.apache.xerces.xni.parser.XMLEntityResolver; +import org.apache.xerces.xni.parser.XMLErrorHandler; +import org.apache.xerces.xni.parser.XMLInputSource; +import org.apache.xerces.xni.parser.XMLParseException; +import org.apache.xerces.xni.parser.XMLPullParserConfiguration; +import org.cyberneko.html.filters.NamespaceBinder; +import org.cyberneko.html.xercesbridge.XercesBridge; + + +/** + * An XNI-based parser configuration that can be used to parse HTML + * documents. This configuration can be used directly in order to + * parse HTML documents or can be used in conjunction with any XNI + * based tools, such as the Xerces2 implementation. + *

+ * This configuration recognizes the following features: + *

    + *
  • http://cyberneko.org/html/features/augmentations + *
  • http://cyberneko.org/html/features/report-errors + *
  • http://cyberneko.org/html/features/report-errors/simple + *
  • http://cyberneko.org/html/features/balance-tags + *
  • and + *
  • the features supported by the scanner and tag balancer components. + *
+ *

+ * This configuration recognizes the following properties: + *

    + *
  • http://cyberneko.org/html/properties/names/elems + *
  • http://cyberneko.org/html/properties/names/attrs + *
  • http://cyberneko.org/html/properties/filters + *
  • http://cyberneko.org/html/properties/error-reporter + *
  • and + *
  • the properties supported by the scanner and tag balancer. + *
+ *

+ * For complete usage information, refer to the documentation. + * + * @author Andy Clark + * @version $Id: HTMLConfiguration.java,v 1.9 2005/02/14 03:56:54 andyc Exp $ + * @see HTMLScanner + * @see HTMLTagBalancer + * @see HTMLErrorReporter + */ +public class HTMLConfiguration extends ParserConfigurationSettings implements XMLPullParserConfiguration { + + // + // Constants + // + + // features + + /** Namespaces. */ + protected static final String NAMESPACES = "http://xml.org/sax/features/namespaces"; + + /** Include infoset augmentations. */ + protected static final String AUGMENTATIONS = "http://cyberneko.org/html/features/augmentations"; + + /** Report errors. */ + protected static final String REPORT_ERRORS = "http://cyberneko.org/html/features/report-errors"; + + /** Simple report format. */ + protected static final String SIMPLE_ERROR_FORMAT = "http://cyberneko.org/html/features/report-errors/simple"; + + /** Balance tags. */ + protected static final String BALANCE_TAGS = "http://cyberneko.org/html/features/balance-tags"; + + // properties + + /** Modify HTML element names: { "upper", "lower", "default" }. */ + protected static final String NAMES_ELEMS = "http://cyberneko.org/html/properties/names/elems"; + + /** Modify HTML attribute names: { "upper", "lower", "default" }. */ + protected static final String NAMES_ATTRS = "http://cyberneko.org/html/properties/names/attrs"; + + /** Pipeline filters. */ + protected static final String FILTERS = "http://cyberneko.org/html/properties/filters"; + + /** Error reporter. */ + protected static final String ERROR_REPORTER = "http://cyberneko.org/html/properties/error-reporter"; + + // other + + /** Error domain. */ + protected static final String ERROR_DOMAIN = "http://cyberneko.org/html"; + + // private + + /** Document source class array. */ + private static final Class[] DOCSOURCE = {XMLDocumentSource.class}; + + // + // Data + // + + // handlers + + /** Document handler. */ + protected XMLDocumentHandler fDocumentHandler; + + /** DTD handler. */ + protected XMLDTDHandler fDTDHandler; + + /** DTD content model handler. */ + protected XMLDTDContentModelHandler fDTDContentModelHandler; + + /** Error handler. */ + protected XMLErrorHandler fErrorHandler = new DefaultErrorHandler(); + + // other settings + + /** Entity resolver. */ + protected XMLEntityResolver fEntityResolver; + + /** Locale. */ + protected Locale fLocale = Locale.getDefault(); + + // state + + /** + * Stream opened by parser. Therefore, must close stream manually upon + * termination of parsing. + */ + protected boolean fCloseStream; + + // components + + /** Components. */ + protected final List fHTMLComponents = new ArrayList<>(2); + + // pipeline + + /** Document scanner. */ + protected final HTMLScanner fDocumentScanner = createDocumentScanner(); + + /** HTML tag balancer. */ + protected final HTMLTagBalancer fTagBalancer = new HTMLTagBalancer(); + + /** Namespace binder. */ + protected final NamespaceBinder fNamespaceBinder = new NamespaceBinder(); + + // other components + + /** Error reporter. */ + protected final HTMLErrorReporter fErrorReporter = new ErrorReporter(); + + // HACK: workarounds Xerces 2.0.x problems + + /** Parser version is Xerces 2.0.0. */ + protected static boolean XERCES_2_0_0 = false; + + /** Parser version is Xerces 2.0.1. */ + protected static boolean XERCES_2_0_1 = false; + + /** Parser version is XML4J 4.0.x. */ + protected static boolean XML4J_4_0_x = false; + + // + // Static initializer + // + + static { + try { + String VERSION = "org.apache.xerces.impl.Version"; + Object version = ObjectFactory.createObject(VERSION, VERSION); + java.lang.reflect.Field field = version.getClass().getField("fVersion"); + String versionStr = String.valueOf(field.get(version)); + XERCES_2_0_0 = versionStr.equals("Xerces-J 2.0.0"); + XERCES_2_0_1 = versionStr.equals("Xerces-J 2.0.1"); + XML4J_4_0_x = versionStr.startsWith("XML4J 4.0."); + } catch (Throwable e) { + // ignore + } + } + + // + // Constructors + // + + /** Default constructor. */ + public HTMLConfiguration() { + + // add components + addComponent(fDocumentScanner); + addComponent(fTagBalancer); + addComponent(fNamespaceBinder); + + // + // features + // + + // recognized features + String VALIDATION = "http://xml.org/sax/features/validation"; + String[] recognizedFeatures = { + AUGMENTATIONS, + NAMESPACES, + VALIDATION, + REPORT_ERRORS, + SIMPLE_ERROR_FORMAT, + BALANCE_TAGS, + }; + addRecognizedFeatures(recognizedFeatures); + setFeature(AUGMENTATIONS, false); + setFeature(NAMESPACES, true); + setFeature(VALIDATION, false); + setFeature(REPORT_ERRORS, false); + setFeature(SIMPLE_ERROR_FORMAT, false); + setFeature(BALANCE_TAGS, true); + + // HACK: Xerces 2.0.0 + if (XERCES_2_0_0) { + // NOTE: These features should not be required but it causes a + // problem if they're not there. This will be fixed in + // subsequent releases of Xerces. + recognizedFeatures = new String[] { + "http://apache.org/xml/features/scanner/notify-builtin-refs", + }; + addRecognizedFeatures(recognizedFeatures); + } + + // HACK: Xerces 2.0.1 + if (XERCES_2_0_0 || XERCES_2_0_1 || XML4J_4_0_x) { + // NOTE: These features should not be required but it causes a + // problem if they're not there. This should be fixed in + // subsequent releases of Xerces. + recognizedFeatures = new String[] { + "http://apache.org/xml/features/validation/schema/normalized-value", + "http://apache.org/xml/features/scanner/notify-char-refs", + }; + addRecognizedFeatures(recognizedFeatures); + } + + // + // properties + // + + // recognized properties + String[] recognizedProperties = { + NAMES_ELEMS, + NAMES_ATTRS, + FILTERS, + ERROR_REPORTER, + }; + addRecognizedProperties(recognizedProperties); + setProperty(NAMES_ELEMS, "upper"); + setProperty(NAMES_ATTRS, "lower"); + setProperty(ERROR_REPORTER, fErrorReporter); + + // HACK: Xerces 2.0.0 + if (XERCES_2_0_0) { + // NOTE: This is a hack to get around a problem in the Xerces 2.0.0 + // AbstractSAXParser. If it uses a parser configuration that + // does not have a SymbolTable, then it will remove *all* + // attributes. This will be fixed in subsequent releases of + // Xerces. + String SYMBOL_TABLE = "http://apache.org/xml/properties/internal/symbol-table"; + recognizedProperties = new String[] { + SYMBOL_TABLE, + }; + addRecognizedProperties(recognizedProperties); + Object symbolTable = ObjectFactory.createObject("org.apache.xerces.util.SymbolTable", + "org.apache.xerces.util.SymbolTable"); + setProperty(SYMBOL_TABLE, symbolTable); + } + } + + protected HTMLScanner createDocumentScanner() { + return new HTMLScanner(); + } + + // + // Public methods + // + + /** + * Pushes an input source onto the current entity stack. This + * enables the scanner to transparently scan new content (e.g. + * the output written by an embedded script). At the end of the + * current entity, the scanner returns where it left off at the + * time this entity source was pushed. + *

+ * Hint: + * To use this feature to insert the output of <SCRIPT> + * tags, remember to buffer the entire output of the + * processed instructions before pushing a new input source. + * Otherwise, events may appear out of sequence. + * + * @param inputSource The new input source to start scanning. + * @see #evaluateInputSource(XMLInputSource) + */ + public void pushInputSource(XMLInputSource inputSource) { + fDocumentScanner.pushInputSource(inputSource); + } + + /** + * EXPERIMENTAL: may change in next release
+ * Immediately evaluates an input source and add the new content (e.g. + * the output written by an embedded script). + * + * @param inputSource The new input source to start scanning. + * @see #pushInputSource(XMLInputSource) + */ + public void evaluateInputSource(XMLInputSource inputSource) { + fDocumentScanner.evaluateInputSource(inputSource); + } + + // XMLParserConfiguration methods + // + + /** Sets a feature. */ + public void setFeature(String featureId, boolean state) throws XMLConfigurationException { + super.setFeature(featureId, state); + int size = fHTMLComponents.size(); + for (HTMLComponent component : fHTMLComponents) { + component.setFeature(featureId, state); + } + } + + /** Sets a property. */ + public void setProperty(String propertyId, Object value) throws XMLConfigurationException { + super.setProperty(propertyId, value); + + if (propertyId.equals(FILTERS)) { + XMLDocumentFilter[] filters = (XMLDocumentFilter[]) getProperty(FILTERS); + if (filters != null) { + for (XMLDocumentFilter filter : filters) { + if (filter instanceof HTMLComponent) { + addComponent((HTMLComponent) filter); + } + } + } + } + + for (HTMLComponent component : fHTMLComponents) { + component.setProperty(propertyId, value); + } + } + + /** Sets the document handler. */ + public void setDocumentHandler(XMLDocumentHandler handler) { + fDocumentHandler = handler; + if (handler instanceof HTMLTagBalancingListener) { + fTagBalancer.setTagBalancingListener((HTMLTagBalancingListener) handler); + } + } + + /** Returns the document handler. */ + public XMLDocumentHandler getDocumentHandler() { + return fDocumentHandler; + } + + /** Sets the DTD handler. */ + public void setDTDHandler(XMLDTDHandler handler) { + fDTDHandler = handler; + } + + /** Returns the DTD handler. */ + public XMLDTDHandler getDTDHandler() { + return fDTDHandler; + } + + /** Sets the DTD content model handler. */ + public void setDTDContentModelHandler(XMLDTDContentModelHandler handler) { + fDTDContentModelHandler = handler; + } + + /** Returns the DTD content model handler. */ + public XMLDTDContentModelHandler getDTDContentModelHandler() { + return fDTDContentModelHandler; + } + + /** Sets the error handler. */ + public void setErrorHandler(XMLErrorHandler handler) { + fErrorHandler = handler; + } + + /** Returns the error handler. */ + public XMLErrorHandler getErrorHandler() { + return fErrorHandler; + } + + /** Sets the entity resolver. */ + public void setEntityResolver(XMLEntityResolver resolver) { + fEntityResolver = resolver; + } + + /** Returns the entity resolver. */ + public XMLEntityResolver getEntityResolver() { + return fEntityResolver; + } + + /** Sets the locale. */ + public void setLocale(Locale locale) { + if (locale == null) { + locale = Locale.getDefault(); + } + fLocale = locale; + } + + /** Returns the locale. */ + public Locale getLocale() { + return fLocale; + } + + /** Parses a document. */ + public void parse(XMLInputSource source) throws XNIException, IOException { + setInputSource(source); + parse(true); + } + + // + // XMLPullParserConfiguration methods + // + + // parsing + + /** + * Sets the input source for the document to parse. + * + * @param inputSource The document's input source. + * @throws XMLConfigurationException Thrown if there is a configuration error when initializing the parser. + * @throws IOException Thrown on I/O error. + * @see #parse(boolean) + */ + public void setInputSource(XMLInputSource inputSource) throws XMLConfigurationException, IOException { + reset(); + fCloseStream = inputSource.getByteStream() == null && inputSource.getCharacterStream() == null; + fDocumentScanner.setInputSource(inputSource); + } + + /** + * Parses the document in a pull parsing fashion. + * + * @param complete True if the pull parser should parse the + * remaining document completely. + * @return True if there is more document to parse. + * @throws XNIException Any XNI exception, possibly wrapping another exception. + * @throws IOException An IO exception from the parser, possibly + * from a byte stream or character stream + * supplied by the parser. + * @see #setInputSource + */ + public boolean parse(boolean complete) throws XNIException, IOException { + try { + boolean more = fDocumentScanner.scanDocument(complete); + if (!more) { + cleanup(); + } + return more; + } catch (XNIException | IOException e) { + cleanup(); + throw e; + } + } + + /** + * If the application decides to terminate parsing before the xml document + * is fully parsed, the application should call this method to free any + * resource allocated during parsing. For example, close all opened streams. + */ + public void cleanup() { + fDocumentScanner.cleanup(fCloseStream); + } + + // + // Protected methods + // + + /** Adds a component. */ + protected void addComponent(HTMLComponent component) { + + // add component to list + fHTMLComponents.add(component); + + // add recognized features and set default states + String[] features = component.getRecognizedFeatures(); + addRecognizedFeatures(features); + int featureCount = features != null ? features.length : 0; + for (int i = 0; i < featureCount; i++) { + Boolean state = component.getFeatureDefault(features[i]); + if (state != null) { + setFeature(features[i], state); + } + } + + // add recognized properties and set default values + String[] properties = component.getRecognizedProperties(); + addRecognizedProperties(properties); + int propertyCount = properties != null ? properties.length : 0; + for (int i = 0; i < propertyCount; i++) { + Object value = component.getPropertyDefault(properties[i]); + if (value != null) { + setProperty(properties[i], value); + } + } + } + + /** Resets the parser configuration. */ + protected void reset() throws XMLConfigurationException { + + // reset components + int size = fHTMLComponents.size(); + for (HTMLComponent component : fHTMLComponents) { + component.reset(this); + } + + // configure pipeline + XMLDocumentSource lastSource = fDocumentScanner; + if (getFeature(NAMESPACES)) { + lastSource.setDocumentHandler(fNamespaceBinder); + fNamespaceBinder.setDocumentSource(fTagBalancer); + lastSource = fNamespaceBinder; + } + if (getFeature(BALANCE_TAGS)) { + lastSource.setDocumentHandler(fTagBalancer); + fTagBalancer.setDocumentSource(fDocumentScanner); + lastSource = fTagBalancer; + } + XMLDocumentFilter[] filters = (XMLDocumentFilter[]) getProperty(FILTERS); + if (filters != null) { + for (XMLDocumentFilter filter : filters) { + XercesBridge.getInstance().XMLDocumentFilter_setDocumentSource(filter, lastSource); + lastSource.setDocumentHandler(filter); + lastSource = filter; + } + } + lastSource.setDocumentHandler(fDocumentHandler); + + } + + // + // Interfaces + // + + /** + * Defines an error reporter for reporting HTML errors. There is no such + * thing as a fatal error in parsing HTML. I/O errors are fatal but should + * throw an IOException directly instead of reporting an error. + *

+ * When used in a configuration, the error reporter instance should be + * set as a property with the following property identifier: + *

+     * "http://cyberneko.org/html/internal/error-reporter" in the
+     * 
+ * Components in the configuration can query the error reporter using this + * property identifier. + *

+ * Note: + * All reported errors are within the domain "http://cyberneko.org/html". + * + * @author Andy Clark + */ + protected class ErrorReporter implements HTMLErrorReporter { + + // + // Data + // + + /** Last locale. */ + protected Locale fLastLocale; + + /** Error messages. */ + protected ResourceBundle fErrorMessages; + + // + // HTMLErrorReporter methods + // + + /** Format message without reporting error. */ + public String formatMessage(String key, Object[] args) { + if (!getFeature(SIMPLE_ERROR_FORMAT)) { + if (!fLocale.equals(fLastLocale)) { + fErrorMessages = null; + fLastLocale = fLocale; + } + if (fErrorMessages == null) { + fErrorMessages = ResourceBundle.getBundle("org/cyberneko/html/res/ErrorMessages", fLocale); + } + try { + String value = fErrorMessages.getString(key); + String message = MessageFormat.format(value, args); + return message; + } catch (MissingResourceException e) { + // ignore and return a simple format + } + } + return formatSimpleMessage(key, args); + } + + /** Reports a warning. */ + public void reportWarning(String key, Object[] args) throws XMLParseException { + if (fErrorHandler != null) { + fErrorHandler.warning(ERROR_DOMAIN, key, createException(key, args)); + } + } + + /** Reports an error. */ + public void reportError(String key, Object[] args) throws XMLParseException { + if (fErrorHandler != null) { + fErrorHandler.error(ERROR_DOMAIN, key, createException(key, args)); + } + } + + // + // Protected methods + // + + /** Creates parse exception. */ + protected XMLParseException createException(String key, Object[] args) { + String message = formatMessage(key, args); + return new XMLParseException(fDocumentScanner, message); + } + + /** Format simple message. */ + protected String formatSimpleMessage(String key, Object[] args) { + StringBuilder str = new StringBuilder(); + str.append(ERROR_DOMAIN); + str.append('#'); + str.append(key); + if (args != null && args.length > 0) { + str.append('\t'); + for (int i = 0; i < args.length; i++) { + if (i > 0) { + str.append('\t'); + } + str.append(args[i]); + } + } + return str.toString(); + } + } +} diff --git a/src/main/java/org/cyberneko/html/HTMLElements.java b/src/main/java/org/cyberneko/html/HTMLElements.java new file mode 100644 index 0000000..1293f1b --- /dev/null +++ b/src/main/java/org/cyberneko/html/HTMLElements.java @@ -0,0 +1,785 @@ +/* + * Copyright 2002-2009 Andy Clark, Marc Guillemot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyberneko.html; + + +/** + * Collection of HTML element information. + * + * @author Andy Clark + * @author Ahmed Ashour + * @author Marc Guillemot + * @version $Id: HTMLElements.java,v 1.12 2005/02/14 07:16:59 andyc Exp $ + */ +public class HTMLElements { + + // + // Constants + // + + // element codes + + // NOTE: The element codes *must* start with 0 and increment in + // sequence. The parent and closes references depends on + // this assumption. -Ac + + public static final short A = 0; + public static final short ABBR = A + 1; + public static final short ACRONYM = ABBR + 1; + public static final short ADDRESS = ACRONYM + 1; + public static final short APPLET = ADDRESS + 1; + public static final short AREA = APPLET + 1; + public static final short B = AREA + 1; + public static final short BASE = B + 1; + public static final short BASEFONT = BASE + 1; + public static final short BDO = BASEFONT + 1; + public static final short BGSOUND = BDO + 1; + public static final short BIG = BGSOUND + 1; + public static final short BLINK = BIG + 1; + public static final short BLOCKQUOTE = BLINK + 1; + public static final short BODY = BLOCKQUOTE + 1; + public static final short BR = BODY + 1; + public static final short BUTTON = BR + 1; + public static final short CAPTION = BUTTON + 1; + public static final short CENTER = CAPTION + 1; + public static final short CITE = CENTER + 1; + public static final short CODE = CITE + 1; + public static final short COL = CODE + 1; + public static final short COLGROUP = COL + 1; + public static final short COMMENT = COLGROUP + 1; + public static final short DEL = COMMENT + 1; + public static final short DFN = DEL + 1; + public static final short DIR = DFN + 1; + public static final short DIV = DIR + 1; + public static final short DD = DIV + 1; + public static final short DL = DD + 1; + public static final short DT = DL + 1; + public static final short EM = DT + 1; + public static final short EMBED = EM + 1; + public static final short FIELDSET = EMBED + 1; + public static final short FONT = FIELDSET + 1; + public static final short FORM = FONT + 1; + public static final short FRAME = FORM + 1; + public static final short FRAMESET = FRAME + 1; + public static final short H1 = FRAMESET + 1; + public static final short H2 = H1 + 1; + public static final short H3 = H2 + 1; + public static final short H4 = H3 + 1; + public static final short H5 = H4 + 1; + public static final short H6 = H5 + 1; + public static final short HEAD = H6 + 1; + public static final short HR = HEAD + 1; + public static final short HTML = HR + 1; + public static final short I = HTML + 1; + public static final short IFRAME = I + 1; + public static final short ILAYER = IFRAME + 1; + public static final short IMG = ILAYER + 1; + public static final short INPUT = IMG + 1; + public static final short INS = INPUT + 1; + public static final short ISINDEX = INS + 1; + public static final short KBD = ISINDEX + 1; + public static final short KEYGEN = KBD + 1; + public static final short LABEL = KEYGEN + 1; + public static final short LAYER = LABEL + 1; + public static final short LEGEND = LAYER + 1; + public static final short LI = LEGEND + 1; + public static final short LINK = LI + 1; + public static final short LISTING = LINK + 1; + public static final short MAP = LISTING + 1; + public static final short MARQUEE = MAP + 1; + public static final short MENU = MARQUEE + 1; + public static final short META = MENU + 1; + public static final short MULTICOL = META + 1; + public static final short NEXTID = MULTICOL + 1; + public static final short NOBR = NEXTID + 1; + public static final short NOEMBED = NOBR + 1; + public static final short NOFRAMES = NOEMBED + 1; + public static final short NOLAYER = NOFRAMES + 1; + public static final short NOSCRIPT = NOLAYER + 1; + public static final short OBJECT = NOSCRIPT + 1; + public static final short OL = OBJECT + 1; + public static final short OPTION = OL + 1; + public static final short OPTGROUP = OPTION + 1; + public static final short P = OPTGROUP + 1; + public static final short PARAM = P + 1; + public static final short PLAINTEXT = PARAM + 1; + public static final short PRE = PLAINTEXT + 1; + public static final short Q = PRE + 1; + public static final short RB = Q + 1; + public static final short RBC = RB + 1; + public static final short RP = RBC + 1; + public static final short RT = RP + 1; + public static final short RTC = RT + 1; + public static final short RUBY = RTC + 1; + public static final short S = RUBY + 1; + public static final short SAMP = S + 1; + public static final short SCRIPT = SAMP + 1; + public static final short SECTION = SCRIPT + 1; + public static final short SELECT = SECTION + 1; + public static final short SMALL = SELECT + 1; + public static final short SOUND = SMALL + 1; + public static final short SPACER = SOUND + 1; + public static final short SPAN = SPACER + 1; + public static final short STRIKE = SPAN + 1; + public static final short STRONG = STRIKE + 1; + public static final short STYLE = STRONG + 1; + public static final short SUB = STYLE + 1; + public static final short SUP = SUB + 1; + public static final short TABLE = SUP + 1; + public static final short TBODY = TABLE + 1; + public static final short TD = TBODY + 1; + public static final short TEXTAREA = TD + 1; + public static final short TFOOT = TEXTAREA + 1; + public static final short TH = TFOOT + 1; + public static final short THEAD = TH + 1; + public static final short TITLE = THEAD + 1; + public static final short TR = TITLE + 1; + public static final short TT = TR + 1; + public static final short U = TT + 1; + public static final short UL = U + 1; + public static final short VAR = UL + 1; + public static final short WBR = VAR + 1; + public static final short XML = WBR + 1; + public static final short XMP = XML + 1; + public static final short UNKNOWN = XMP + 1; + + // information + + /** Element information organized by first letter. */ + protected static final Element[][] ELEMENTS_ARRAY = new Element[26][]; + + /** Element information as a contiguous list. */ + protected static final ElementList ELEMENTS = new ElementList(); + + /** No such element. */ + public static final Element NO_SUCH_ELEMENT = new Element(UNKNOWN, "", Element.CONTAINER, new short[] {BODY, HEAD}/*HTML*/, null); + + // + // Static initializer + // + + /** + * Initializes the element information. + *

+ * Note: + * The getElement method requires that the HTML elements + * are added to the list in alphabetical order. If new elements are + * added, then they must be inserted in alphabetical order. + */ + static { + // + // + // + // + // + // + // + // + + // initialize array of element information + ELEMENTS_ARRAY['A' - 'A'] = new Element[] { + // A - - (%inline;)* -(A) + new Element(A, "A", Element.CONTAINER, BODY, new short[] {A}), + // ABBR - - (%inline;)* + new Element(ABBR, "ABBR", Element.INLINE, BODY, null), + // ACRONYM - - (%inline;)* + new Element(ACRONYM, "ACRONYM", Element.INLINE, BODY, null), + // ADDRESS - - (%inline;)* + new Element(ADDRESS, "ADDRESS", Element.BLOCK, BODY, new short[] {P}), + // APPLET + new Element(APPLET, "APPLET", Element.CONTAINER, BODY, null), + // AREA - O EMPTY + new Element(AREA, "AREA", Element.EMPTY, MAP, null), + }; + ELEMENTS_ARRAY['B' - 'A'] = new Element[] { + // B - - (%inline;)* + new Element(B, "B", Element.INLINE, BODY, null), + // BASE - O EMPTY + new Element(BASE, "BASE", Element.EMPTY, HEAD, null), + // BASEFONT + new Element(BASEFONT, "BASEFONT", Element.EMPTY, HEAD, null), + // BDO - - (%inline;)* + new Element(BDO, "BDO", Element.INLINE, BODY, null), + // BGSOUND + new Element(BGSOUND, "BGSOUND", Element.EMPTY, HEAD, null), + // BIG - - (%inline;)* + new Element(BIG, "BIG", Element.INLINE, BODY, null), + // BLINK + new Element(BLINK, "BLINK", Element.INLINE, BODY, null), + // BLOCKQUOTE - - (%block;|SCRIPT)+ + new Element(BLOCKQUOTE, "BLOCKQUOTE", Element.BLOCK, BODY, new short[] {P}), + // BODY O O (%block;|SCRIPT)+ +(INS|DEL) + new Element(BODY, "BODY", Element.CONTAINER, HTML, new short[] {HEAD}), + // BR - O EMPTY + new Element(BR, "BR", Element.EMPTY, BODY, null), + // BUTTON - - (%flow;)* -(A|%formctrl;|FORM|FIELDSET) + new Element(BUTTON, "BUTTON", Element.INLINE | Element.BLOCK, BODY, new short[] {BUTTON}), + }; + ELEMENTS_ARRAY['C' - 'A'] = new Element[] { + // CAPTION - - (%inline;)* + new Element(CAPTION, "CAPTION", Element.INLINE, TABLE, null), + // CENTER, + new Element(CENTER, "CENTER", Element.CONTAINER, BODY, new short[] {P}), + // CITE - - (%inline;)* + new Element(CITE, "CITE", Element.INLINE, BODY, null), + // CODE - - (%inline;)* + new Element(CODE, "CODE", Element.INLINE, BODY, null), + // COL - O EMPTY + new Element(COL, "COL", Element.EMPTY, TABLE, null), + // COLGROUP - O (COL)* + new Element(COLGROUP, "COLGROUP", Element.CONTAINER, TABLE, new short[] {COL, COLGROUP}), + // COMMENT + new Element(COMMENT, "COMMENT", Element.SPECIAL, HTML, null), + }; + ELEMENTS_ARRAY['D' - 'A'] = new Element[] { + // DEL - - (%flow;)* + new Element(DEL, "DEL", Element.INLINE, BODY, null), + // DFN - - (%inline;)* + new Element(DFN, "DFN", Element.INLINE, BODY, null), + // DIR + new Element(DIR, "DIR", Element.CONTAINER, BODY, new short[] {P}), + // DIV - - (%flow;)* + new Element(DIV, "DIV", Element.CONTAINER, BODY, new short[] {P}), + // DD - O (%flow;)* + // codavaj: reverted to pre-1.9.18 definition (parent DL, not a block of BODY); + // javadoc 6 pages rely on stray

/
being re-parented into a synthesized
+ new Element(DD, "DD", 0, DL, new short[] {DT, DD}), + // DL - - (DT|DD)+ + // codavaj: reverted to pre-1.9.18 definition (does not close P), see DD above + new Element(DL, "DL", Element.BLOCK, BODY, null), + // DT - O (%inline;)* + // codavaj: reverted to pre-1.9.18 definition, see DD above + new Element(DT, "DT", 0, DL, new short[] {DT, DD}), + }; + ELEMENTS_ARRAY['E' - 'A'] = new Element[] { + // EM - - (%inline;)* + new Element(EM, "EM", Element.INLINE, BODY, null), + // EMBED + new Element(EMBED, "EMBED", Element.EMPTY, BODY, null), + }; + ELEMENTS_ARRAY['F' - 'A'] = new Element[] { + // FIELDSET - - (#PCDATA,LEGEND,(%flow;)*) + new Element(FIELDSET, "FIELDSET", Element.CONTAINER, BODY, new short[] {P}), + // FONT + new Element(FONT, "FONT", Element.CONTAINER, BODY, null), + // FORM - - (%block;|SCRIPT)+ -(FORM) + new Element(FORM, "FORM", Element.CONTAINER, new short[] {BODY, TD, DIV}, new short[] {BUTTON, P}), + // FRAME - O EMPTY + new Element(FRAME, "FRAME", Element.EMPTY, FRAMESET, null), + // FRAMESET - - ((FRAMESET|FRAME)+ & NOFRAMES?) + new Element(FRAMESET, "FRAMESET", Element.CONTAINER, HTML, null), + }; + ELEMENTS_ARRAY['H' - 'A'] = new Element[] { + // (H1|H2|H3|H4|H5|H6) - - (%inline;)* + new Element(H1, "H1", Element.BLOCK, new short[] {BODY, A}, new short[] {H1, H2, H3, H4, H5, H6, P}), + new Element(H2, "H2", Element.BLOCK, new short[] {BODY, A}, new short[] {H1, H2, H3, H4, H5, H6, P}), + new Element(H3, "H3", Element.BLOCK, new short[] {BODY, A}, new short[] {H1, H2, H3, H4, H5, H6, P}), + new Element(H4, "H4", Element.BLOCK, new short[] {BODY, A}, new short[] {H1, H2, H3, H4, H5, H6, P}), + new Element(H5, "H5", Element.BLOCK, new short[] {BODY, A}, new short[] {H1, H2, H3, H4, H5, H6, P}), + new Element(H6, "H6", Element.BLOCK, new short[] {BODY, A}, new short[] {H1, H2, H3, H4, H5, H6, P}), + // HEAD O O (%head.content;) +(%head.misc;) + new Element(HEAD, "HEAD", 0, HTML, null), + // HR - O EMPTY + new Element(HR, "HR", Element.EMPTY, BODY, new short[] {P}), + // HTML O O (%html.content;) + new Element(HTML, "HTML", 0, null, null), + }; + ELEMENTS_ARRAY['I' - 'A'] = new Element[] { + // I - - (%inline;)* + new Element(I, "I", Element.INLINE, BODY, null), + // IFRAME + new Element(IFRAME, "IFRAME", Element.BLOCK, BODY, null), + // ILAYER + new Element(ILAYER, "ILAYER", Element.BLOCK, BODY, null), + // IMG - O EMPTY + new Element(IMG, "IMG", Element.EMPTY, BODY, null), + // INPUT - O EMPTY + new Element(INPUT, "INPUT", Element.EMPTY, BODY, null), + // INS - - (%flow;)* + new Element(INS, "INS", Element.INLINE, BODY, null), + // ISINDEX + new Element(ISINDEX, "ISINDEX", Element.INLINE, HEAD, null), + }; + ELEMENTS_ARRAY['K' - 'A'] = new Element[] { + // KBD - - (%inline;)* + new Element(KBD, "KBD", Element.INLINE, BODY, null), + // KEYGEN + new Element(KEYGEN, "KEYGEN", Element.EMPTY, BODY, null), + }; + ELEMENTS_ARRAY['L' - 'A'] = new Element[] { + // LABEL - - (%inline;)* -(LABEL) + new Element(LABEL, "LABEL", Element.INLINE, BODY, null), + // LAYER + new Element(LAYER, "LAYER", Element.BLOCK, BODY, null), + // LEGEND - - (%inline;)* + new Element(LEGEND, "LEGEND", Element.INLINE, FIELDSET, null), + // LI - O (%flow;)* + new Element(LI, "LI", Element.CONTAINER, new short[] {BODY, UL, OL}, new short[] {LI, P}), + // LINK - O EMPTY + new Element(LINK, "LINK", Element.EMPTY, HEAD, null), + // LISTING + new Element(LISTING, "LISTING", Element.BLOCK, BODY, new short[] {P}), + }; + ELEMENTS_ARRAY['M' - 'A'] = new Element[] { + // MAP - - ((%block;) | AREA)+ + new Element(MAP, "MAP", Element.INLINE, BODY, null), + // MARQUEE + new Element(MARQUEE, "MARQUEE", Element.CONTAINER, BODY, null), + // MENU + new Element(MENU, "MENU", Element.CONTAINER, BODY, new short[] {P}), + // META - O EMPTY + new Element(META, "META", Element.EMPTY, HEAD, new short[] {STYLE, TITLE}), + // MULTICOL + new Element(MULTICOL, "MULTICOL", Element.CONTAINER, BODY, null), + }; + ELEMENTS_ARRAY['N' - 'A'] = new Element[] { + // NEXTID + new Element(NEXTID, "NEXTID", Element.EMPTY, BODY, null), + // NOBR + new Element(NOBR, "NOBR", Element.INLINE, BODY, new short[] {NOBR}), + // NOEMBED + new Element(NOEMBED, "NOEMBED", Element.CONTAINER, BODY, null), + // NOFRAMES - - (BODY) -(NOFRAMES) + new Element(NOFRAMES, "NOFRAMES", Element.CONTAINER, null, null), + // NOLAYER + new Element(NOLAYER, "NOLAYER", Element.CONTAINER, BODY, null), + // NOSCRIPT - - (%block;)+ + new Element(NOSCRIPT, "NOSCRIPT", Element.CONTAINER, new short[] {BODY}, null), + }; + ELEMENTS_ARRAY['O' - 'A'] = new Element[] { + // OBJECT - - (PARAM | %flow;)* + new Element(OBJECT, "OBJECT", Element.CONTAINER, BODY, null), + // OL - - (LI)+ + new Element(OL, "OL", Element.BLOCK, BODY, new short[] {P}), + // OPTGROUP - - (OPTION)+ + new Element(OPTGROUP, "OPTGROUP", 0, SELECT, new short[] {OPTION}), + // OPTION - O (#PCDATA) + new Element(OPTION, "OPTION", 0, SELECT, new short[] {OPTION}), + }; + ELEMENTS_ARRAY['P' - 'A'] = new Element[] { + // P - O (%inline;)* + new Element(P, "P", Element.CONTAINER, BODY, new short[] {P}), + // PARAM - O EMPTY + new Element(PARAM, "PARAM", Element.EMPTY, new short[] {OBJECT, APPLET}, null), + // PLAINTEXT + new Element(PLAINTEXT, "PLAINTEXT", Element.SPECIAL, BODY, null), + // PRE - - (%inline;)* -(%pre.exclusion;) + // codavaj: reverted to pre-1.9.18 definition (does not close P); javadoc 6 + // signature
 blocks live inside 

and must stay there + new Element(PRE, "PRE", 0, BODY, null), + }; + ELEMENTS_ARRAY['Q' - 'A'] = new Element[] { + // Q - - (%inline;)* + new Element(Q, "Q", Element.INLINE, BODY, null), + }; + ELEMENTS_ARRAY['R' - 'A'] = new Element[] { + // RB + new Element(RB, "RB", Element.INLINE, RUBY, new short[] {RB}), + // RBC + new Element(RBC, "RBC", 0, RUBY, null), + // RP + new Element(RP, "RP", Element.INLINE, RUBY, new short[] {RB}), + // RT + new Element(RT, "RT", Element.INLINE, RUBY, new short[] {RB, RP}), + // RTC + new Element(RTC, "RTC", 0, RUBY, new short[] {RBC}), + // RUBY + new Element(RUBY, "RUBY", Element.CONTAINER, BODY, new short[] {RUBY}), + }; + ELEMENTS_ARRAY['S' - 'A'] = new Element[] { + // S + new Element(S, "S", Element.INLINE, BODY, null), + // SAMP - - (%inline;)* + new Element(SAMP, "SAMP", Element.INLINE, BODY, null), + // SCRIPT - - %Script; + new Element(SCRIPT, "SCRIPT", Element.SPECIAL, new short[] {HEAD, BODY}, null), + + new Element(SECTION, "SECTION", Element.CONTAINER, BODY, new short[] {SELECT}), + // SELECT - - (OPTGROUP|OPTION)+ + new Element(SELECT, "SELECT", Element.CONTAINER, BODY, new short[] {SELECT}), + // SMALL - - (%inline;)* + new Element(SMALL, "SMALL", Element.INLINE, BODY, null), + // SOUND + new Element(SOUND, "SOUND", Element.EMPTY, HEAD, null), + // SPACER + new Element(SPACER, "SPACER", Element.EMPTY, BODY, null), + // SPAN - - (%inline;)* + new Element(SPAN, "SPAN", Element.CONTAINER, BODY, null), + // STRIKE + new Element(STRIKE, "STRIKE", Element.INLINE, BODY, null), + // STRONG - - (%inline;)* + new Element(STRONG, "STRONG", Element.INLINE, BODY, null), + // STYLE - - %StyleSheet; + new Element(STYLE, "STYLE", Element.SPECIAL, new short[] {HEAD, BODY}, new short[] {STYLE, TITLE, META}), + // SUB - - (%inline;)* + new Element(SUB, "SUB", Element.INLINE, BODY, null), + // SUP - - (%inline;)* + new Element(SUP, "SUP", Element.INLINE, BODY, null), + }; + ELEMENTS_ARRAY['T' - 'A'] = new Element[] { + // TABLE - - (CAPTION?, (COL*|COLGROUP*), THEAD?, TFOOT?, TBODY+) + new Element(TABLE, "TABLE", Element.BLOCK | Element.CONTAINER, BODY, null), + // TBODY O O (TR)+ + new Element(TBODY, "TBODY", 0, TABLE, new short[] {THEAD, TBODY, TFOOT, TD, TH, TR, COLGROUP}), + // TD - O (%flow;)* + new Element(TD, "TD", Element.CONTAINER, TR, TABLE, new short[] {TD, TH}), + // TEXTAREA - - (#PCDATA) + new Element(TEXTAREA, "TEXTAREA", Element.SPECIAL, BODY, null), + // TFOOT - O (TR)+ + new Element(TFOOT, "TFOOT", 0, TABLE, new short[] {THEAD, TBODY, TFOOT, TD, TH, TR}), + // TH - O (%flow;)* + new Element(TH, "TH", Element.CONTAINER, TR, TABLE, new short[] {TD, TH}), + // THEAD - O (TR)+ + new Element(THEAD, "THEAD", 0, TABLE, new short[] {THEAD, TBODY, TFOOT, TD, TH, TR, COLGROUP}), + // TITLE - - (#PCDATA) -(%head.misc;) + new Element(TITLE, "TITLE", Element.SPECIAL, new short[] {HEAD, BODY}, null), + // TR - O (TH|TD)+ + new Element(TR, "TR", Element.BLOCK, new short[] {TBODY, THEAD, TFOOT}, TABLE, new short[] {TD, TH, TR, COLGROUP, DIV}), + // TT - - (%inline;)* + new Element(TT, "TT", Element.INLINE, BODY, null), + }; + ELEMENTS_ARRAY['U' - 'A'] = new Element[] { + // U, + new Element(U, "U", Element.INLINE, BODY, null), + // UL - - (LI)+ + new Element(UL, "UL", Element.CONTAINER, BODY, new short[] {P}), + }; + ELEMENTS_ARRAY['V' - 'A'] = new Element[] { + // VAR - - (%inline;)* + new Element(VAR, "VAR", Element.INLINE, BODY, null), + }; + ELEMENTS_ARRAY['W' - 'A'] = new Element[] { + // WBR + new Element(WBR, "WBR", Element.EMPTY, BODY, null), + }; + ELEMENTS_ARRAY['X' - 'A'] = new Element[] { + // XML + new Element(XML, "XML", 0, BODY, null), + // XMP + new Element(XMP, "XMP", Element.SPECIAL, BODY, new short[] {P}), + }; + + // keep contiguous list of elements for lookups by code + for (Element[] elements : ELEMENTS_ARRAY) { + if (elements != null) { + for (Element element : elements) { + ELEMENTS.addElement(element); + } + } + } + ELEMENTS.addElement(NO_SUCH_ELEMENT); + + // initialize cross references to parent elements + for (int i = 0; i < ELEMENTS.size; i++) { + Element element = ELEMENTS.data[i]; + if (element.parentCodes != null) { + element.parent = new Element[element.parentCodes.length]; + for (int j = 0; j < element.parentCodes.length; j++) { + element.parent[j] = ELEMENTS.data[element.parentCodes[j]]; + } + element.parentCodes = null; + } + } + } + + // + // Public static methods + // + + /** + * Returns the element information for the specified element code. + * + * @param code The element code. + */ + public static Element getElement(short code) { + return ELEMENTS.data[code]; + } + + /** + * Returns the element information for the specified element name. + * + * @param ename The element name. + */ + public static Element getElement(String ename) { + Element element = getElement(ename, NO_SUCH_ELEMENT); + if (element == NO_SUCH_ELEMENT) { + element = new Element(UNKNOWN, ename.toUpperCase(), Element.CONTAINER, new short[] {BODY, HEAD}/*HTML*/, null); + element.parent = NO_SUCH_ELEMENT.parent; + element.parentCodes = NO_SUCH_ELEMENT.parentCodes; + } + return element; + } + + /** + * Returns the element information for the specified element name. + * + * @param ename The element name. + * @param element The default element to return if not found. + */ + public static Element getElement(String ename, Element element) { + + if (!ename.isEmpty()) { + int c = ename.charAt(0); + if (c >= 'a' && c <= 'z') { + c = 'A' + c - 'a'; + } + if (c >= 'A' && c <= 'Z') { + Element[] elements = ELEMENTS_ARRAY[c - 'A']; + if (elements != null) { + for (Element elem : elements) { + if (elem.name.equalsIgnoreCase(ename)) { + return elem; + } + } + } + } + } + return element; + } + + // + // Classes + // + + /** + * Element information. + * + * @author Andy Clark + */ + public static class Element { + + // + // Constants + // + + /** Inline element. */ + public static final int INLINE = 0x01; + + /** Block element. */ + public static final int BLOCK = 0x02; + + /** Empty element. */ + public static final int EMPTY = 0x04; + + /** Container element. */ + public static final int CONTAINER = 0x08; + + /** Special element. */ + public static final int SPECIAL = 0x10; + + // + // Data + // + + /** The element code. */ + public final short code; + + /** The element name. */ + public final String name; + + /** Informational flags. */ + public final int flags; + + /** Parent elements. */ + public short[] parentCodes; + + /** Parent elements. */ + public Element[] parent; + + /** The bounding element code. */ + public final short bounds; + + /** List of elements this element can close. */ + public final short[] closes; + + // + // Constructors + // + + /** + * Constructs an element object. + * + * @param code The element code. + * @param name The element name. + * @param flags Informational flags + * @param parent Natural closing parent name. + * @param closes List of elements this element can close. + */ + public Element(short code, String name, int flags, short parent, short[] closes) { + this(code, name, flags, new short[] {parent}, (short) -1, closes); + }; + + /** + * Constructs an element object. + * + * @param code The element code. + * @param name The element name. + * @param flags Informational flags + * @param parent Natural closing parent name. + * @param closes List of elements this element can close. + */ + public Element(short code, String name, int flags, short parent, short bounds, short[] closes) { + this(code, name, flags, new short[] {parent}, bounds, closes); + } + + /** + * Constructs an element object. + * + * @param code The element code. + * @param name The element name. + * @param flags Informational flags + * @param parents Natural closing parent names. + * @param closes List of elements this element can close. + */ + public Element(short code, String name, int flags, short[] parents, short[] closes) { + this(code, name, flags, parents, (short) -1, closes); + } + + /** + * Constructs an element object. + * + * @param code The element code. + * @param name The element name. + * @param flags Informational flags + * @param parents Natural closing parent names. + * @param closes List of elements this element can close. + */ + public Element(short code, String name, int flags, short[] parents, short bounds, short[] closes) { + this.code = code; + this.name = name; + this.flags = flags; + this.parentCodes = parents; + this.parent = null; + this.bounds = bounds; + this.closes = closes; + } + + // + // Public methods + // + + /** Returns true if this element is an inline element. */ + public final boolean isInline() { + return (flags & INLINE) != 0; + } + + /** Returns true if this element is a block element. */ + public final boolean isBlock() { + return (flags & BLOCK) != 0; + } + + /** Returns true if this element is an empty element. */ + public final boolean isEmpty() { + return (flags & EMPTY) != 0; + } + + /** Returns true if this element is a container element. */ + public final boolean isContainer() { + return (flags & CONTAINER) != 0; + } + + /** + * Returns true if this element is special -- if its content + * should be parsed ignoring markup. + */ + public final boolean isSpecial() { + return (flags & SPECIAL) != 0; + } + + /** + * Returns true if this element can close the specified Element. + * + * @param tag The element. + */ + public boolean closes(short tag) { + if (closes != null) { + for (short close : closes) { + if (close == tag) { + return true; + } + } + } + return false; + + } + + // + // Object methods + // + + /** Returns a hash code for this object. */ + public int hashCode() { + return name.hashCode(); + } + + /** Returns true if the objects are equal. */ + public boolean equals(Object o) { + return name.equals(o); + } + + /** + * Provides a simple representation to make debugging easier + */ + public String toString() { + return super.toString() + "(name=" + name + ")"; + } + + /** + * Indicates if the provided element is an accepted parent of current element + * + * @param element the element to test for "paternity" + * @return true if element belongs to the {@link #parent} + */ + public boolean isParent(Element element) { + if (parent == null) + return false; + for (Element value : parent) { + if (element.code == value.code) + return true; + } + return false; + } + } + + /** Unsynchronized list of elements. */ + public static class ElementList { + + // + // Data + // + + /** The size of the list. */ + public int size; + + /** The data in the list. */ + public Element[] data = new Element[120]; + + // + // Public methods + // + + /** Adds an element to list, resizing if necessary. */ + public void addElement(Element element) { + if (size == data.length) { + Element[] newarray = new Element[size + 20]; + System.arraycopy(data, 0, newarray, 0, size); + data = newarray; + } + data[size++] = element; + } + } +} diff --git a/src/main/java/org/cyberneko/html/HTMLEntities.java b/src/main/java/org/cyberneko/html/HTMLEntities.java new file mode 100644 index 0000000..3c19c06 --- /dev/null +++ b/src/main/java/org/cyberneko/html/HTMLEntities.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2009 Andy Clark, Marc Guillemot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyberneko.html; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + + +/** + * Pre-defined HTML entities. + * + * @author Andy Clark + * @version $Id: HTMLEntities.java,v 1.5 2005/02/14 03:56:54 andyc Exp $ + */ +public class HTMLEntities { + + // + // Constants + // + + /** Entities. */ + protected static final Map ENTITIES; + + /** Reverse mapping from characters to names. */ + protected static final IntProperties SEITITNE = new IntProperties(); + + // + // Static initialization + // + + static { + Properties props = new Properties(); + // load entities + load0(props, "res/HTMLlat1.properties"); + load0(props, "res/HTMLspecial.properties"); + load0(props, "res/HTMLsymbol.properties"); + load0(props, "res/XMLbuiltin.properties"); + + // store reverse mappings + Enumeration keys = props.propertyNames(); + while (keys.hasMoreElements()) { + String key = (String) keys.nextElement(); + String value = props.getProperty(key); + if (value.length() == 1) { + int ivalue = value.charAt(0); + SEITITNE.put(ivalue, key); + } + } + + ENTITIES = Collections.unmodifiableMap(new HashMap<>(props)); + } + + // + // Public static methods + // + + /** + * Returns the character associated to the given entity name, or + * -1 if the name is not known. + */ + public static int get(String name) { + String value = (String) ENTITIES.get(name); + return value != null ? value.charAt(0) : -1; + } + + /** + * Returns the name associated to the given character or null if + * the character is not known. + */ + public static String get(int c) { + return SEITITNE.get(c); + } + + // + // Private static methods + // + + /** Loads the entity values in the specified resource. */ + private static void load0(Properties props, String filename) { + try { + InputStream stream = HTMLEntities.class.getResourceAsStream(filename); + props.load(stream); + stream.close(); + } catch (IOException e) { + System.err.println("error: unable to load resource \"" + filename + "\""); + } + } + + // + // Classes + // + + static class IntProperties { + + private final Entry[] entries = new Entry[101]; + + public void put(int key, String value) { + int hash = key % entries.length; + Entry entry = new Entry(key, value, entries[hash]); + entries[hash] = entry; + } + + public String get(int key) { + int hash = key % entries.length; + Entry entry = entries[hash]; + while (entry != null) { + if (entry.key == key) { + return entry.value; + } + entry = entry.next; + } + return null; + } + + static class Entry { + + public final int key; + public final String value; + public final Entry next; + + public Entry(int key, String value, Entry next) { + this.key = key; + this.value = value; + this.next = next; + } + } + } +} diff --git a/src/main/java/org/cyberneko/html/HTMLErrorReporter.java b/src/main/java/org/cyberneko/html/HTMLErrorReporter.java new file mode 100644 index 0000000..a79708d --- /dev/null +++ b/src/main/java/org/cyberneko/html/HTMLErrorReporter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2009 Andy Clark, Marc Guillemot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyberneko.html; + +import org.apache.xerces.xni.parser.XMLParseException; + + +/** + * Defines an error reporter for reporting HTML errors. There is no such + * thing as a fatal error in parsing HTML. I/O errors are fatal but should + * throw an IOException directly instead of reporting an error. + *

+ * When used in a configuration, the error reporter instance should be + * set as a property with the following property identifier: + *

+ * "http://cyberneko.org/html/internal/error-reporter" in the
+ * 
+ * Components in the configuration can query the error reporter using this + * property identifier. + *

+ * Note: + * All reported errors are within the domain "http://cyberneko.org/html". + * + * @author Andy Clark + * @version $Id: HTMLErrorReporter.java,v 1.4 2005/02/14 03:56:54 andyc Exp $ + */ +public interface HTMLErrorReporter { + + // + // HTMLErrorReporter methods + // + + /** Format message without reporting error. */ + String formatMessage(String key, Object[] args); + + /** Reports a warning. */ + void reportWarning(String key, Object[] args) throws XMLParseException; + + /** Reports an error. */ + void reportError(String key, Object[] args) throws XMLParseException; +} diff --git a/src/main/java/org/cyberneko/html/HTMLEventInfo.java b/src/main/java/org/cyberneko/html/HTMLEventInfo.java new file mode 100644 index 0000000..c90baad --- /dev/null +++ b/src/main/java/org/cyberneko/html/HTMLEventInfo.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2009 Andy Clark, Marc Guillemot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyberneko.html; + +/** + * This interface is used to pass augmentated information to the + * application through the XNI pipeline. + * + * @author Andy Clark + * @version $Id: HTMLEventInfo.java,v 1.4 2005/02/14 03:56:54 andyc Exp $ + */ +public interface HTMLEventInfo { + + // + // HTMLEventInfo methods + // + + // location information + + /** Returns the line number of the beginning of this event. */ + int getBeginLineNumber(); + + /** Returns the column number of the beginning of this event. */ + int getBeginColumnNumber(); + + /** Returns the character offset of the beginning of this event. */ + int getBeginCharacterOffset(); + + /** Returns the line number of the end of this event. */ + int getEndLineNumber(); + + /** Returns the column number of the end of this event. */ + int getEndColumnNumber(); + + /** Returns the character offset of the end of this event. */ + int getEndCharacterOffset(); + + // other information + + /** Returns true if this corresponding event was synthesized. */ + boolean isSynthesized(); + + /** + * Synthesized infoset item. + * + * @author Andy Clark + */ + class SynthesizedItem implements HTMLEventInfo { + + // + // HTMLEventInfo methods + // + + // location information + + /** Returns the line number of the beginning of this event. */ + public int getBeginLineNumber() { + return -1; + } + + /** Returns the column number of the beginning of this event. */ + public int getBeginColumnNumber() { + return -1; + } + + /** Returns the character offset of the beginning of this event. */ + public int getBeginCharacterOffset() { + return -1; + } + + /** Returns the line number of the end of this event. */ + public int getEndLineNumber() { + return -1; + } + + /** Returns the column number of the end of this event. */ + public int getEndColumnNumber() { + return -1; + } + + /** Returns the character offset of the end of this event. */ + public int getEndCharacterOffset() { + return -1; + } + + // other information + + /** Returns true if this corresponding event was synthesized. */ + public boolean isSynthesized() { + return true; + } + + // + // Object methods + // + + /** Returns a string representation of this object. */ + public String toString() { + return "synthesized"; + } + } +} diff --git a/src/main/java/org/cyberneko/html/HTMLScanner.java b/src/main/java/org/cyberneko/html/HTMLScanner.java new file mode 100644 index 0000000..1d9aff7 --- /dev/null +++ b/src/main/java/org/cyberneko/html/HTMLScanner.java @@ -0,0 +1,3687 @@ +/* + * Copyright 2002-2009 Andy Clark, Marc Guillemot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyberneko.html; + +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.util.BitSet; +import java.util.Locale; +import java.util.Stack; + +import org.apache.xerces.util.EncodingMap; +import org.apache.xerces.util.NamespaceSupport; +import org.apache.xerces.util.URI; +import org.apache.xerces.util.XMLAttributesImpl; +import org.apache.xerces.util.XMLResourceIdentifierImpl; +import org.apache.xerces.util.XMLStringBuffer; +import org.apache.xerces.xni.Augmentations; +import org.apache.xerces.xni.NamespaceContext; +import org.apache.xerces.xni.QName; +import org.apache.xerces.xni.XMLAttributes; +import org.apache.xerces.xni.XMLDocumentHandler; +import org.apache.xerces.xni.XMLLocator; +import org.apache.xerces.xni.XMLResourceIdentifier; +import org.apache.xerces.xni.XMLString; +import org.apache.xerces.xni.XNIException; +import org.apache.xerces.xni.parser.XMLComponentManager; +import org.apache.xerces.xni.parser.XMLConfigurationException; +import org.apache.xerces.xni.parser.XMLDocumentScanner; +import org.apache.xerces.xni.parser.XMLInputSource; +import org.cyberneko.html.xercesbridge.XercesBridge; + + +/** + * A simple HTML scanner. This scanner makes no attempt to balance tags + * or fix other problems in the source document — it just scans what + * it can and generates XNI document "events", ignoring errors of all + * kinds. + *

+ * This component recognizes the following features: + *

    + *
  • http://cyberneko.org/html/features/augmentations + *
  • http://cyberneko.org/html/features/report-errors + *
  • http://apache.org/xml/features/scanner/notify-char-refs + *
  • http://apache.org/xml/features/scanner/notify-builtin-refs + *
  • http://cyberneko.org/html/features/scanner/notify-builtin-refs + *
  • http://cyberneko.org/html/features/scanner/fix-mswindows-refs + *
  • http://cyberneko.org/html/features/scanner/script/strip-cdata-delims + *
  • http://cyberneko.org/html/features/scanner/script/strip-comment-delims + *
  • http://cyberneko.org/html/features/scanner/style/strip-cdata-delims + *
  • http://cyberneko.org/html/features/scanner/style/strip-comment-delims + *
  • http://cyberneko.org/html/features/scanner/ignore-specified-charset + *
  • http://cyberneko.org/html/features/scanner/cdata-sections + *
  • http://cyberneko.org/html/features/override-doctype + *
  • http://cyberneko.org/html/features/insert-doctype + *
  • http://cyberneko.org/html/features/parse-noscript-content + *
  • http://cyberneko.org/html/features/scanner/allow-selfclosing-iframe + *
  • http://cyberneko.org/html/features/scanner/allow-selfclosing-tags + *
+ *

+ * This component recognizes the following properties: + *

    + *
  • http://cyberneko.org/html/properties/names/elems + *
  • http://cyberneko.org/html/properties/names/attrs + *
  • http://cyberneko.org/html/properties/default-encoding + *
  • http://cyberneko.org/html/properties/error-reporter + *
  • http://cyberneko.org/html/properties/doctype/pubid + *
  • http://cyberneko.org/html/properties/doctype/sysid + *
+ * + * @author Andy Clark + * @author Marc Guillemot + * @author Ahmed Ashour + * @version $Id: HTMLScanner.java,v 1.19 2005/06/14 05:52:37 andyc Exp $ + * @see HTMLElements + * @see HTMLEntities + */ +public class HTMLScanner implements XMLDocumentScanner, XMLLocator, HTMLComponent { + + // + // Constants + // + + // doctype info: HTML 4.01 strict + + /** HTML 4.01 strict public identifier ("-//W3C//DTD HTML 4.01//EN"). */ + public static final String HTML_4_01_STRICT_PUBID = "-//W3C//DTD HTML 4.01//EN"; + + /** HTML 4.01 strict system identifier ("http://www.w3.org/TR/html4/strict.dtd"). */ + public static final String HTML_4_01_STRICT_SYSID = "http://www.w3.org/TR/html4/strict.dtd"; + + // doctype info: HTML 4.01 loose + + /** HTML 4.01 transitional public identifier ("-//W3C//DTD HTML 4.01 Transitional//EN"). */ + public static final String HTML_4_01_TRANSITIONAL_PUBID = "-//W3C//DTD HTML 4.01 Transitional//EN"; + + /** HTML 4.01 transitional system identifier ("http://www.w3.org/TR/html4/loose.dtd"). */ + public static final String HTML_4_01_TRANSITIONAL_SYSID = "http://www.w3.org/TR/html4/loose.dtd"; + + // doctype info: HTML 4.01 frameset + + /** HTML 4.01 frameset public identifier ("-//W3C//DTD HTML 4.01 Frameset//EN"). */ + public static final String HTML_4_01_FRAMESET_PUBID = "-//W3C//DTD HTML 4.01 Frameset//EN"; + + /** HTML 4.01 frameset system identifier ("http://www.w3.org/TR/html4/frameset.dtd"). */ + public static final String HTML_4_01_FRAMESET_SYSID = "http://www.w3.org/TR/html4/frameset.dtd"; + + // features + + /** Include infoset augmentations. */ + protected static final String AUGMENTATIONS = "http://cyberneko.org/html/features/augmentations"; + + /** Report errors. */ + protected static final String REPORT_ERRORS = "http://cyberneko.org/html/features/report-errors"; + + /** Notify character entity references (e.g. &#32;, &#x20;, etc). */ + public static final String NOTIFY_CHAR_REFS = "http://apache.org/xml/features/scanner/notify-char-refs"; + + /** + * Notify handler of built-in entity references (e.g. &amp;, + * &lt;, etc). + *

+ * Note: + * This only applies to the five pre-defined XML general entities. + * Specifically, "amp", "lt", "gt", "quot", and "apos". This is done + * for compatibility with the Xerces feature. + *

+ * To be notified of the built-in entity references in HTML, set the + * http://cyberneko.org/html/features/scanner/notify-builtin-refs + * feature to true. + */ + public static final String NOTIFY_XML_BUILTIN_REFS = "http://apache.org/xml/features/scanner/notify-builtin-refs"; + + /** + * Notify handler of built-in entity references (e.g. &nobr;, + * &copy;, etc). + *

+ * Note: + * This includes the five pre-defined XML general entities. + */ + public static final String NOTIFY_HTML_BUILTIN_REFS = "http://cyberneko.org/html/features/scanner/notify-builtin-refs"; + + /** Fix Microsoft Windows® character entity references. */ + public static final String FIX_MSWINDOWS_REFS = "http://cyberneko.org/html/features/scanner/fix-mswindows-refs"; + + /** + * Strip HTML comment delimiters ("<!−−" and + * "−−>") from SCRIPT tag contents. + */ + public static final String SCRIPT_STRIP_COMMENT_DELIMS = "http://cyberneko.org/html/features/scanner/script/strip-comment-delims"; + + /** + * Strip XHTML CDATA delimiters ("<![CDATA[" and "]]>") from + * SCRIPT tag contents. + */ + public static final String SCRIPT_STRIP_CDATA_DELIMS = "http://cyberneko.org/html/features/scanner/script/strip-cdata-delims"; + + /** + * Strip HTML comment delimiters ("<!−−" and + * "−−>") from STYLE tag contents. + */ + public static final String STYLE_STRIP_COMMENT_DELIMS = "http://cyberneko.org/html/features/scanner/style/strip-comment-delims"; + + /** + * Strip XHTML CDATA delimiters ("<![CDATA[" and "]]>") from + * STYLE tag contents. + */ + public static final String STYLE_STRIP_CDATA_DELIMS = "http://cyberneko.org/html/features/scanner/style/strip-cdata-delims"; + + /** + * Ignore specified charset found in the <meta equiv='Content-Type' + * content='text/html;charset=…'> tag or in the <?xml … encoding='…'> processing instruction + */ + public static final String IGNORE_SPECIFIED_CHARSET = "http://cyberneko.org/html/features/scanner/ignore-specified-charset"; + + /** Scan CDATA sections. */ + public static final String CDATA_SECTIONS = "http://cyberneko.org/html/features/scanner/cdata-sections"; + + /** Override doctype declaration public and system identifiers. */ + public static final String OVERRIDE_DOCTYPE = "http://cyberneko.org/html/features/override-doctype"; + + /** Insert document type declaration. */ + public static final String INSERT_DOCTYPE = "http://cyberneko.org/html/features/insert-doctype"; + + /** Parse <noscript>...</noscript> content */ + public static final String PARSE_NOSCRIPT_CONTENT = "http://cyberneko.org/html/features/parse-noscript-content"; + + /** Allows self closing <iframe/> tag */ + public static final String ALLOW_SELFCLOSING_IFRAME = "http://cyberneko.org/html/features/scanner/allow-selfclosing-iframe"; + + /** Allows self closing tags e.g. <div/> (XHTML) */ + public static final String ALLOW_SELFCLOSING_TAGS = "http://cyberneko.org/html/features/scanner/allow-selfclosing-tags"; + + /** Normalize attribute values. */ + protected static final String NORMALIZE_ATTRIBUTES = "http://cyberneko.org/html/features/scanner/normalize-attrs"; + + /** Recognized features. */ + private static final String[] RECOGNIZED_FEATURES = { + AUGMENTATIONS, + REPORT_ERRORS, + NOTIFY_CHAR_REFS, + NOTIFY_XML_BUILTIN_REFS, + NOTIFY_HTML_BUILTIN_REFS, + FIX_MSWINDOWS_REFS, + SCRIPT_STRIP_CDATA_DELIMS, + SCRIPT_STRIP_COMMENT_DELIMS, + STYLE_STRIP_CDATA_DELIMS, + STYLE_STRIP_COMMENT_DELIMS, + IGNORE_SPECIFIED_CHARSET, + CDATA_SECTIONS, + OVERRIDE_DOCTYPE, + INSERT_DOCTYPE, + NORMALIZE_ATTRIBUTES, + PARSE_NOSCRIPT_CONTENT, + ALLOW_SELFCLOSING_IFRAME, + ALLOW_SELFCLOSING_TAGS, + }; + + /** Recognized features defaults. */ + private static final Boolean[] RECOGNIZED_FEATURES_DEFAULTS = { + null, + null, + Boolean.FALSE, + Boolean.FALSE, + Boolean.FALSE, + Boolean.FALSE, + Boolean.FALSE, + Boolean.FALSE, + Boolean.FALSE, + Boolean.FALSE, + Boolean.FALSE, + Boolean.FALSE, + Boolean.FALSE, + Boolean.FALSE, + Boolean.FALSE, + Boolean.TRUE, + Boolean.FALSE, + Boolean.FALSE, + }; + + // properties + + /** Modify HTML element names: { "upper", "lower", "default" }. */ + protected static final String NAMES_ELEMS = "http://cyberneko.org/html/properties/names/elems"; + + /** Modify HTML attribute names: { "upper", "lower", "default" }. */ + protected static final String NAMES_ATTRS = "http://cyberneko.org/html/properties/names/attrs"; + + /** Default encoding. */ + protected static final String DEFAULT_ENCODING = "http://cyberneko.org/html/properties/default-encoding"; + + /** Error reporter. */ + protected static final String ERROR_REPORTER = "http://cyberneko.org/html/properties/error-reporter"; + + /** Doctype declaration public identifier. */ + protected static final String DOCTYPE_PUBID = "http://cyberneko.org/html/properties/doctype/pubid"; + + /** Doctype declaration system identifier. */ + protected static final String DOCTYPE_SYSID = "http://cyberneko.org/html/properties/doctype/sysid"; + + /** Recognized properties. */ + private static final String[] RECOGNIZED_PROPERTIES = { + NAMES_ELEMS, + NAMES_ATTRS, + DEFAULT_ENCODING, + ERROR_REPORTER, + DOCTYPE_PUBID, + DOCTYPE_SYSID, + }; + + /** Recognized properties defaults. */ + private static final Object[] RECOGNIZED_PROPERTIES_DEFAULTS = { + null, + null, + "Windows-1252", + null, + HTML_4_01_TRANSITIONAL_PUBID, + HTML_4_01_TRANSITIONAL_SYSID, + }; + + // states + + /** State: content. */ + protected static final short STATE_CONTENT = 0; + + /** State: markup bracket. */ + protected static final short STATE_MARKUP_BRACKET = 1; + + /** State: start document. */ + protected static final short STATE_START_DOCUMENT = 10; + + /** State: end document. */ + protected static final short STATE_END_DOCUMENT = 11; + + // modify HTML names + + /** Don't modify HTML names. */ + protected static final short NAMES_NO_CHANGE = 0; + + /** Uppercase HTML names. */ + protected static final short NAMES_UPPERCASE = 1; + + /** Lowercase HTML names. */ + protected static final short NAMES_LOWERCASE = 2; + + // defaults + + /** Default buffer size. */ + protected static final int DEFAULT_BUFFER_SIZE = 2048; + + // debugging + + /** Set to true to debug changes in the scanner. */ + private static final boolean DEBUG_SCANNER = false; + + /** Set to true to debug changes in the scanner state. */ + private static final boolean DEBUG_SCANNER_STATE = false; + + /** Set to true to debug the buffer. */ + private static final boolean DEBUG_BUFFER = false; + + /** Set to true to debug character encoding handling. */ + private static final boolean DEBUG_CHARSET = false; + + /** Set to true to debug callbacks. */ + protected static final boolean DEBUG_CALLBACKS = false; + + // static vars + + /** Synthesized event info item. */ + protected static final HTMLEventInfo SYNTHESIZED_ITEM = new HTMLEventInfo.SynthesizedItem(); + + private final static BitSet ENTITY_CHARS = new BitSet(); + + static { + final String str = "-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; + for (int i = 0; i < str.length(); ++i) { + char c = str.charAt(i); + ENTITY_CHARS.set(c); + } + } + // + // Data + // + + // features + + /** Augmentations. */ + protected boolean fAugmentations; + + /** Report errors. */ + protected boolean fReportErrors; + + /** Notify character entity references. */ + protected boolean fNotifyCharRefs; + + /** Notify XML built-in general entity references. */ + protected boolean fNotifyXmlBuiltinRefs; + + /** Notify HTML built-in general entity references. */ + protected boolean fNotifyHtmlBuiltinRefs; + + /** Fix Microsoft Windows® character entity references. */ + protected boolean fFixWindowsCharRefs; + + /** Strip CDATA delimiters from SCRIPT tags. */ + protected boolean fScriptStripCDATADelims; + + /** Strip comment delimiters from SCRIPT tags. */ + protected boolean fScriptStripCommentDelims; + + /** Strip CDATA delimiters from STYLE tags. */ + protected boolean fStyleStripCDATADelims; + + /** Strip comment delimiters from STYLE tags. */ + protected boolean fStyleStripCommentDelims; + + /** Ignore specified character set. */ + protected boolean fIgnoreSpecifiedCharset; + + /** CDATA sections. */ + protected boolean fCDATASections; + + /** Override doctype declaration public and system identifiers. */ + protected boolean fOverrideDoctype; + + /** Insert document type declaration. */ + protected boolean fInsertDoctype; + + /** Normalize attribute values. */ + protected boolean fNormalizeAttributes; + + /** Parse noscript content. */ + protected boolean fParseNoScriptContent; + + /** Parse noframes content. */ + protected boolean fParseNoFramesContent; + + /** Allows self closing iframe tags. */ + protected boolean fAllowSelfclosingIframe; + + /** Allows self closing tags. */ + protected boolean fAllowSelfclosingTags; + + // properties + + /** Modify HTML element names. */ + protected short fNamesElems; + + /** Modify HTML attribute names. */ + protected short fNamesAttrs; + + /** Default encoding. */ + protected String fDefaultIANAEncoding; + + /** Error reporter. */ + protected HTMLErrorReporter fErrorReporter; + + /** Doctype declaration public identifier. */ + protected String fDoctypePubid; + + /** Doctype declaration system identifier. */ + protected String fDoctypeSysid; + + // boundary locator information + + /** Beginning line number. */ + protected int fBeginLineNumber; + + /** Beginning column number. */ + protected int fBeginColumnNumber; + + /** Beginning character offset in the file. */ + protected int fBeginCharacterOffset; + + /** Ending line number. */ + protected int fEndLineNumber; + + /** Ending column number. */ + protected int fEndColumnNumber; + + /** Ending character offset in the file. */ + protected int fEndCharacterOffset; + + // state + + /** The playback byte stream. */ + protected PlaybackInputStream fByteStream; + + /** Current entity. */ + protected CurrentEntity fCurrentEntity; + + /** The current entity stack. */ + protected final Stack fCurrentEntityStack = new Stack<>(); + + /** The current scanner. */ + protected Scanner fScanner; + + /** The current scanner state. */ + protected short fScannerState; + + /** The document handler. */ + protected XMLDocumentHandler fDocumentHandler; + + /** Auto-detected IANA encoding. */ + protected String fIANAEncoding; + + /** Auto-detected Java encoding. */ + protected String fJavaEncoding; + + /** True if the encoding matches "ISO-8859-*". */ + protected boolean fIso8859Encoding; + + /** Element count. */ + protected int fElementCount; + + /** Element depth. */ + protected int fElementDepth; + + // scanners + + /** Content scanner. */ + protected final Scanner fContentScanner = new ContentScanner(); + + /** + * Special scanner used for elements whose content needs to be scanned + * as plain text, ignoring markup such as elements and entity references. + * For example: <SCRIPT> and <COMMENT>. + */ + protected final SpecialScanner fSpecialScanner = new SpecialScanner(); + + // temp vars + + /** String buffer. */ + protected final XMLStringBuffer fStringBuffer = new XMLStringBuffer(1024); + + /** String buffer. */ + private final XMLStringBuffer fStringBuffer2 = new XMLStringBuffer(1024); + + /** Non-normalized attribute string buffer. */ + private final XMLStringBuffer fNonNormAttr = new XMLStringBuffer(128); + + /** Augmentations. */ + private final HTMLAugmentations fInfosetAugs = new HTMLAugmentations(); + + /** Location infoset item. */ + private final LocationItem fLocationItem = new LocationItem(); + + /** Single boolean array. */ + private final boolean[] fSingleBoolean = {false}; + + /** Resource identifier. */ + private final XMLResourceIdentifierImpl fResourceId = new XMLResourceIdentifierImpl(); + + private static final char REPLACEMENT_CHARACTER = '\uFFFD'; // the � character + + // + // Public methods + // + + /** + * Pushes an input source onto the current entity stack. This + * enables the scanner to transparently scan new content (e.g. + * the output written by an embedded script). At the end of the + * current entity, the scanner returns where it left off at the + * time this entity source was pushed. + *

+ * Note: + * This functionality is experimental at this time and is + * subject to change in future releases of NekoHTML. + * + * @param inputSource The new input source to start scanning. + * @see #evaluateInputSource(XMLInputSource) + */ + public void pushInputSource(XMLInputSource inputSource) { + Reader reader = getReader(inputSource); + + fCurrentEntityStack.push(fCurrentEntity); + String encoding = inputSource.getEncoding(); + String publicId = inputSource.getPublicId(); + String baseSystemId = inputSource.getBaseSystemId(); + String literalSystemId = inputSource.getSystemId(); + String expandedSystemId = expandSystemId(literalSystemId, baseSystemId); + fCurrentEntity = new CurrentEntity(reader, encoding, publicId, baseSystemId, literalSystemId, expandedSystemId); + } + + private Reader getReader(XMLInputSource inputSource) { + Reader reader = inputSource.getCharacterStream(); + if (reader == null) { + try { + return new InputStreamReader(inputSource.getByteStream(), fJavaEncoding); + } catch (UnsupportedEncodingException e) { + // should not happen as this encoding is already used to parse the "main" source + } + } + return reader; + } + + /** + * Immediately evaluates an input source and add the new content (e.g. + * the output written by an embedded script). + * + * @param inputSource The new input source to start evaluating. + * @see #pushInputSource(XMLInputSource) + */ + public void evaluateInputSource(XMLInputSource inputSource) { + Scanner previousScanner = fScanner; + short previousScannerState = fScannerState; + CurrentEntity previousEntity = fCurrentEntity; + Reader reader = getReader(inputSource); + + String encoding = inputSource.getEncoding(); + String publicId = inputSource.getPublicId(); + String baseSystemId = inputSource.getBaseSystemId(); + String literalSystemId = inputSource.getSystemId(); + String expandedSystemId = expandSystemId(literalSystemId, baseSystemId); + fCurrentEntity = new CurrentEntity(reader, encoding, publicId, baseSystemId, literalSystemId, expandedSystemId); + setScanner(fContentScanner); + setScannerState(STATE_CONTENT); + try { + do { + fScanner.scan(false); + } while (fScannerState != STATE_END_DOCUMENT); + } catch (IOException e) { + // ignore + } + setScanner(previousScanner); + setScannerState(previousScannerState); + fCurrentEntity = previousEntity; + } + + /** + * Cleans up used resources. For example, if scanning is terminated + * early, then this method ensures all remaining open streams are + * closed. + * + * @param closeall Close all streams, including the original. + * This is used in cases when the application has + * opened the original document stream and should + * be responsible for closing it. + */ + public void cleanup(boolean closeall) { + int size = fCurrentEntityStack.size(); + if (size > 0) { + // current entity is not the original, so close it + if (fCurrentEntity != null) { + fCurrentEntity.closeQuietly(); + } + // close remaining streams + for (int i = closeall ? 0 : 1; i < size; i++) { + fCurrentEntity = fCurrentEntityStack.pop(); + fCurrentEntity.closeQuietly(); + } + } else if (closeall && fCurrentEntity != null) { + fCurrentEntity.closeQuietly(); + } + } + + // + // XMLLocator methods + // + + /** Returns the encoding. */ + public String getEncoding() { + return fCurrentEntity != null ? fCurrentEntity.encoding : null; + } + + /** Returns the public identifier. */ + public String getPublicId() { + return fCurrentEntity != null ? fCurrentEntity.publicId : null; + } + + /** Returns the base system identifier. */ + public String getBaseSystemId() { + return fCurrentEntity != null ? fCurrentEntity.baseSystemId : null; + } + + /** Returns the literal system identifier. */ + public String getLiteralSystemId() { + return fCurrentEntity != null ? fCurrentEntity.literalSystemId : null; + } + + /** Returns the expanded system identifier. */ + public String getExpandedSystemId() { + return fCurrentEntity != null ? fCurrentEntity.expandedSystemId : null; + } + + /** Returns the current line number. */ + public int getLineNumber() { + return fCurrentEntity != null ? fCurrentEntity.getLineNumber() : -1; + } + + /** Returns the current column number. */ + public int getColumnNumber() { + return fCurrentEntity != null ? fCurrentEntity.getColumnNumber() : -1; + } + + /** Returns the XML version. */ + public String getXMLVersion() { + return fCurrentEntity != null ? CurrentEntity.version : null; + } + + /** Returns the character offset. */ + public int getCharacterOffset() { + return fCurrentEntity != null ? fCurrentEntity.getCharacterOffset() : -1; + } + + // + // HTMLComponent methods + // + + /** Returns the default state for a feature. */ + public Boolean getFeatureDefault(String featureId) { + int length = RECOGNIZED_FEATURES != null ? RECOGNIZED_FEATURES.length : 0; + for (int i = 0; i < length; i++) { + if (RECOGNIZED_FEATURES[i].equals(featureId)) { + return RECOGNIZED_FEATURES_DEFAULTS[i]; + } + } + return null; + } + + /** Returns the default state for a property. */ + public Object getPropertyDefault(String propertyId) { + int length = RECOGNIZED_PROPERTIES != null ? RECOGNIZED_PROPERTIES.length : 0; + for (int i = 0; i < length; i++) { + if (RECOGNIZED_PROPERTIES[i].equals(propertyId)) { + return RECOGNIZED_PROPERTIES_DEFAULTS[i]; + } + } + return null; + } + + // + // XMLComponent methods + // + + /** Returns recognized features. */ + public String[] getRecognizedFeatures() { + return RECOGNIZED_FEATURES; + } + + /** Returns recognized properties. */ + public String[] getRecognizedProperties() { + return RECOGNIZED_PROPERTIES; + } + + /** Resets the component. */ + public void reset(XMLComponentManager manager) throws XMLConfigurationException { + + // get features + fAugmentations = manager.getFeature(AUGMENTATIONS); + fReportErrors = manager.getFeature(REPORT_ERRORS); + fNotifyCharRefs = manager.getFeature(NOTIFY_CHAR_REFS); + fNotifyXmlBuiltinRefs = manager.getFeature(NOTIFY_XML_BUILTIN_REFS); + fNotifyHtmlBuiltinRefs = manager.getFeature(NOTIFY_HTML_BUILTIN_REFS); + fFixWindowsCharRefs = manager.getFeature(FIX_MSWINDOWS_REFS); + fScriptStripCDATADelims = manager.getFeature(SCRIPT_STRIP_CDATA_DELIMS); + fScriptStripCommentDelims = manager.getFeature(SCRIPT_STRIP_COMMENT_DELIMS); + fStyleStripCDATADelims = manager.getFeature(STYLE_STRIP_CDATA_DELIMS); + fStyleStripCommentDelims = manager.getFeature(STYLE_STRIP_COMMENT_DELIMS); + fIgnoreSpecifiedCharset = manager.getFeature(IGNORE_SPECIFIED_CHARSET); + fCDATASections = manager.getFeature(CDATA_SECTIONS); + fOverrideDoctype = manager.getFeature(OVERRIDE_DOCTYPE); + fInsertDoctype = manager.getFeature(INSERT_DOCTYPE); + fNormalizeAttributes = manager.getFeature(NORMALIZE_ATTRIBUTES); + fParseNoScriptContent = manager.getFeature(PARSE_NOSCRIPT_CONTENT); + fAllowSelfclosingIframe = manager.getFeature(ALLOW_SELFCLOSING_IFRAME); + fAllowSelfclosingTags = manager.getFeature(ALLOW_SELFCLOSING_TAGS); + + // get properties + fNamesElems = getNamesValue(String.valueOf(manager.getProperty(NAMES_ELEMS))); + fNamesAttrs = getNamesValue(String.valueOf(manager.getProperty(NAMES_ATTRS))); + fDefaultIANAEncoding = String.valueOf(manager.getProperty(DEFAULT_ENCODING)); + fErrorReporter = (HTMLErrorReporter) manager.getProperty(ERROR_REPORTER); + fDoctypePubid = String.valueOf(manager.getProperty(DOCTYPE_PUBID)); + fDoctypeSysid = String.valueOf(manager.getProperty(DOCTYPE_SYSID)); + } + + /** Sets a feature. */ + public void setFeature(String featureId, boolean state) { + + switch (featureId) { + case AUGMENTATIONS -> fAugmentations = state; + case IGNORE_SPECIFIED_CHARSET -> fIgnoreSpecifiedCharset = state; + case NOTIFY_CHAR_REFS -> fNotifyCharRefs = state; + case NOTIFY_XML_BUILTIN_REFS -> fNotifyXmlBuiltinRefs = state; + case NOTIFY_HTML_BUILTIN_REFS -> fNotifyHtmlBuiltinRefs = state; + case FIX_MSWINDOWS_REFS -> fFixWindowsCharRefs = state; + case SCRIPT_STRIP_CDATA_DELIMS -> fScriptStripCDATADelims = state; + case SCRIPT_STRIP_COMMENT_DELIMS -> fScriptStripCommentDelims = state; + case STYLE_STRIP_CDATA_DELIMS -> fStyleStripCDATADelims = state; + case STYLE_STRIP_COMMENT_DELIMS -> fStyleStripCommentDelims = state; + case PARSE_NOSCRIPT_CONTENT -> fParseNoScriptContent = state; + case ALLOW_SELFCLOSING_IFRAME -> fAllowSelfclosingIframe = state; + case ALLOW_SELFCLOSING_TAGS -> fAllowSelfclosingTags = state; + } + } + + /** Sets a property. */ + public void setProperty(String propertyId, Object value) throws XMLConfigurationException { + + switch (propertyId) { + case NAMES_ELEMS -> { + fNamesElems = getNamesValue(String.valueOf(value)); + } + case NAMES_ATTRS -> { + fNamesAttrs = getNamesValue(String.valueOf(value)); + } + case DEFAULT_ENCODING -> { + fDefaultIANAEncoding = String.valueOf(value); + } + } + } + + // + // XMLDocumentScanner methods + // + + /** Sets the input source. */ + public void setInputSource(XMLInputSource source) throws IOException { + + // reset state + fElementCount = 0; + fElementDepth = -1; + fByteStream = null; + fCurrentEntityStack.removeAllElements(); + + fBeginLineNumber = 1; + fBeginColumnNumber = 1; + fBeginCharacterOffset = 0; + fEndLineNumber = fBeginLineNumber; + fEndColumnNumber = fBeginColumnNumber; + fEndCharacterOffset = fBeginCharacterOffset; + + // reset encoding information + fIANAEncoding = fDefaultIANAEncoding; + fJavaEncoding = fIANAEncoding; + + // get location information + String encoding = source.getEncoding(); + String publicId = source.getPublicId(); + String baseSystemId = source.getBaseSystemId(); + String literalSystemId = source.getSystemId(); + String expandedSystemId = expandSystemId(literalSystemId, baseSystemId); + + // open stream + Reader reader = source.getCharacterStream(); + if (reader == null) { + InputStream inputStream = source.getByteStream(); + if (inputStream == null) { + URL url = new URL(expandedSystemId); + inputStream = url.openStream(); + } + fByteStream = new PlaybackInputStream(inputStream); + String[] encodings = new String[2]; + if (encoding == null) { + fByteStream.detectEncoding(encodings); + } else { + encodings[0] = encoding; + } + if (encodings[0] == null) { + encodings[0] = fDefaultIANAEncoding; + if (fReportErrors) { + fErrorReporter.reportWarning("HTML1000", null); + } + } + if (encodings[1] == null) { + encodings[1] = EncodingMap.getIANA2JavaMapping(encodings[0].toUpperCase(Locale.ENGLISH)); + if (encodings[1] == null) { + encodings[1] = encodings[0]; + if (fReportErrors) { + fErrorReporter.reportWarning("HTML1001", new Object[] {encodings[0]}); + } + } + } + fIANAEncoding = encodings[0]; + fJavaEncoding = encodings[1]; + /* PATCH: Asgeir Asgeirsson */ + fIso8859Encoding = fIANAEncoding == null + || fIANAEncoding.toUpperCase(Locale.ENGLISH).startsWith("ISO-8859") + || fIANAEncoding.equalsIgnoreCase(fDefaultIANAEncoding); + encoding = fIANAEncoding; + reader = new InputStreamReader(fByteStream, fJavaEncoding); + } + fCurrentEntity = new CurrentEntity(reader, encoding, publicId, baseSystemId, literalSystemId, expandedSystemId); + + // set scanner and state + setScanner(fContentScanner); + setScannerState(STATE_START_DOCUMENT); + + } + + /** Scans the document. */ + public boolean scanDocument(boolean complete) throws XNIException, IOException { + do { + if (!fScanner.scan(complete)) { + return false; + } + } while (complete); + return true; + } + + /** Sets the document handler. */ + public void setDocumentHandler(XMLDocumentHandler handler) { + fDocumentHandler = handler; + } + + // @since Xerces 2.1.0 + + /** Returns the document handler. */ + public XMLDocumentHandler getDocumentHandler() { + return fDocumentHandler; + } + + // + // Protected static methods + // + + /** Returns the value of the specified attribute, ignoring case. */ + protected static String getValue(XMLAttributes attrs, String aname) { + int length = attrs != null ? attrs.getLength() : 0; + for (int i = 0; i < length; i++) { + if (attrs.getQName(i).equalsIgnoreCase(aname)) { + return attrs.getValue(i); + } + } + return null; + } + + /** + * Expands a system id and returns the system id as a URI, if + * it can be expanded. A return value of null means that the + * identifier is already expanded. An exception thrown + * indicates a failure to expand the id. + * + * @param systemId The systemId to be expanded. + * @return Returns the URI string representing the expanded system + * identifier. A null value indicates that the given + * system identifier is already expanded. + * + */ + public static String expandSystemId(String systemId, String baseSystemId) { + + // check for bad parameters id + if (systemId == null || systemId.isEmpty()) { + return systemId; + } + // if id already expanded, return + try { + URI uri = new URI(systemId); + if (uri != null) { + return systemId; + } + } catch (URI.MalformedURIException e) { + // continue on... + } + // normalize id + String id = fixURI(systemId); + + // normalize base + URI base; + URI uri = null; + try { + if (baseSystemId == null || baseSystemId.isEmpty() || baseSystemId.equals(systemId)) { + String dir; + try { + dir = fixURI(System.getProperty("user.dir")); + } catch (SecurityException se) { + dir = ""; + } + if (!dir.endsWith("/")) { + dir = dir + "/"; + } + base = new URI("file", "", dir, null, null); + } else { + try { + base = new URI(fixURI(baseSystemId)); + } catch (URI.MalformedURIException e) { + String dir; + try { + dir = fixURI(System.getProperty("user.dir")); + } catch (SecurityException se) { + dir = ""; + } + if (baseSystemId.indexOf(':') != -1) { + // for xml schemas we might have baseURI with + // a specified drive + base = new URI("file", "", fixURI(baseSystemId), null, null); + } else { + if (!dir.endsWith("/")) { + dir = dir + "/"; + } + dir = dir + fixURI(baseSystemId); + base = new URI("file", "", dir, null, null); + } + } + } + // expand id + uri = new URI(base, id); + } catch (URI.MalformedURIException e) { + // let it go through + } + + if (uri == null) { + return systemId; + } + return uri.toString(); + + } + + /** + * Fixes a platform dependent filename to standard URI form. + * + * @param str The string to fix. + * @return Returns the fixed URI string. + */ + protected static String fixURI(String str) { + + // handle platform dependent strings + str = str.replace(java.io.File.separatorChar, '/'); + + // Windows fix + if (str.length() >= 2) { + char ch1 = str.charAt(1); + // change "C:blah" to "/C:blah" + if (ch1 == ':') { + char ch0 = String.valueOf(str.charAt(0)).toUpperCase(Locale.ENGLISH).charAt(0); + if (ch0 >= 'A' && ch0 <= 'Z') { + str = "/" + str; + } + } + // change "//blah" to "file://blah" + else if (ch1 == '/' && str.charAt(0) == '/') { + str = "file:" + str; + } + } + + // done + return str; + + } + + /** Modifies the given name based on the specified mode. */ + protected static String modifyName(String name, short mode) { + return switch (mode) { + case NAMES_UPPERCASE -> name.toUpperCase(Locale.ENGLISH); + case NAMES_LOWERCASE -> name.toLowerCase(Locale.ENGLISH); + default -> name; + }; + } + + /** + * Converts HTML names string value to constant value. + * + * @see #NAMES_NO_CHANGE + * @see #NAMES_LOWERCASE + * @see #NAMES_UPPERCASE + */ + protected static short getNamesValue(String value) { + if (value.equals("lower")) { + return NAMES_LOWERCASE; + } + if (value.equals("upper")) { + return NAMES_UPPERCASE; + } + return NAMES_NO_CHANGE; + } + + /** + * Fixes Microsoft Windows® specific characters. + *

+ * Details about this common problem can be found at + * http://www.cs.tut.fi/~jkorpela/www/windows-chars.html + */ + protected int fixWindowsCharacter(int origChar) { + /* PATCH: Asgeir Asgeirsson */ + return switch (origChar) { + case 130 -> 8218; + case 131 -> 402; + case 132 -> 8222; + case 133 -> 8230; + case 134 -> 8224; + case 135 -> 8225; + case 136 -> 710; + case 137 -> 8240; + case 138 -> 352; + case 139 -> 8249; + case 140 -> 338; + case 145 -> 8216; + case 146 -> 8217; + case 147 -> 8220; + case 148 -> 8221; + case 149 -> 8226; + case 150 -> 8211; + case 151 -> 8212; + case 152 -> 732; + case 153 -> 8482; + case 154 -> 353; + case 155 -> 8250; + case 156 -> 339; + case 159 -> 376; + default -> origChar; + }; + } + + // + // Protected methods + // + + // i/o + + /** Reads a single character. */ + protected int read() throws IOException { + return fCurrentEntity.read(); + } + + + // debugging + + /** Sets the scanner. */ + protected void setScanner(Scanner scanner) { + fScanner = scanner; + if (DEBUG_SCANNER) { + System.out.print("$$$ setScanner("); + System.out.print(scanner != null ? scanner.getClass().getName() : "null"); + System.out.println(");"); + } + } + + /** Sets the scanner state. */ + protected void setScannerState(short state) { + fScannerState = state; + if (DEBUG_SCANNER_STATE) { + System.out.print("$$$ setScannerState("); + switch (fScannerState) { + case STATE_CONTENT: { + System.out.print("STATE_CONTENT"); + break; + } + case STATE_MARKUP_BRACKET: { + System.out.print("STATE_MARKUP_BRACKET"); + break; + } + case STATE_START_DOCUMENT: { + System.out.print("STATE_START_DOCUMENT"); + break; + } + case STATE_END_DOCUMENT: { + System.out.print("STATE_END_DOCUMENT"); + break; + } + } + System.out.println(");"); + } + } + + // scanning + + /** Scans a DOCTYPE line. */ + protected void scanDoctype() throws IOException { + String root = null; + String pubid = null; + String sysid = null; + + if (skipSpaces()) { + root = scanName(true); + if (root == null) { + if (fReportErrors) { + fErrorReporter.reportError("HTML1014", null); + } + } else { + root = modifyName(root, fNamesElems); + } + if (skipSpaces()) { + if (skip("PUBLIC", false)) { + skipSpaces(); + pubid = scanLiteral(); + if (skipSpaces()) { + sysid = scanLiteral(); + } + } else if (skip("SYSTEM", false)) { + skipSpaces(); + sysid = scanLiteral(); + } + } + } + int c; + while ((c = fCurrentEntity.read()) != -1) { + if (c == '<') { + fCurrentEntity.rewind(); + break; + } + if (c == '>') { + break; + } + if (c == '[') { + skipMarkup(true); + break; + } + } + + if (fDocumentHandler != null) { + if (fOverrideDoctype) { + pubid = fDoctypePubid; + sysid = fDoctypeSysid; + } + fEndLineNumber = fCurrentEntity.getLineNumber(); + fEndColumnNumber = fCurrentEntity.getColumnNumber(); + fEndCharacterOffset = fCurrentEntity.getCharacterOffset(); + fDocumentHandler.doctypeDecl(root, pubid, sysid, locationAugs()); + } + } + + /** Scans a quoted literal. */ + protected String scanLiteral() throws IOException { + int quote = fCurrentEntity.read(); + if (quote == '\'' || quote == '"') { + StringBuffer str = new StringBuffer(); + int c; + while ((c = fCurrentEntity.read()) != -1) { + if (c == quote) { + break; + } + if (c == '\r' || c == '\n') { + fCurrentEntity.rewind(); + // NOTE: This collapses newlines to a single space. + // [Q] Is this the right thing to do here? -Ac + skipNewlines(); + str.append(' '); + } else if (c == '<') { + fCurrentEntity.rewind(); + break; + } else { + appendChar(str, c); + } + } + if (c == -1) { + if (fReportErrors) { + fErrorReporter.reportError("HTML1007", null); + } + throw new EOFException(); + } + return str.toString(); + } + fCurrentEntity.rewind(); + return null; + } + + /** Scans a name. */ + protected String scanName(boolean strict) throws IOException { + if (DEBUG_BUFFER) { + fCurrentEntity.debugBufferIfNeeded("(scanName: "); + } + if (fCurrentEntity.offset == fCurrentEntity.length) { + if (fCurrentEntity.load(0) == -1) { + if (DEBUG_BUFFER) { + fCurrentEntity.debugBufferIfNeeded(")scanName: "); + } + return null; + } + } + int offset = fCurrentEntity.offset; + while (true) { + while (fCurrentEntity.hasNext()) { + char c = fCurrentEntity.getNextChar(); + if ((strict && (!Character.isLetterOrDigit(c) && c != '-' && c != '.' && c != ':' && c != '_')) + || (!strict && (Character.isWhitespace(c) || c == '=' || c == '/' || c == '>'))) { + fCurrentEntity.rewind(); + break; + } + } + if (fCurrentEntity.offset == fCurrentEntity.length) { + int length = fCurrentEntity.length - offset; + System.arraycopy(fCurrentEntity.buffer, offset, fCurrentEntity.buffer, 0, length); + int count = fCurrentEntity.load(length); + offset = 0; + if (count == -1) { + break; + } + } else { + break; + } + } + int length = fCurrentEntity.offset - offset; + String name = length > 0 ? new String(fCurrentEntity.buffer, offset, length) : null; + if (DEBUG_BUFFER) { + fCurrentEntity.debugBufferIfNeeded(")scanName: ", " -> \"" + name + '"'); + } + return name; + } + + /** Scans an entity reference. */ + protected int scanEntityRef(XMLStringBuffer str, boolean content) + throws IOException { + str.clear(); + str.append('&'); + boolean endsWithSemicolon = false; + while (true) { + int c = fCurrentEntity.read(); + if (c == ';') { + str.append(';'); + endsWithSemicolon = true; + break; + } else if (c == -1) { + break; + } else if (!ENTITY_CHARS.get(c) && c != '#') { + fCurrentEntity.rewind(); + break; + } + appendChar(str, c); + } + + if (!endsWithSemicolon) { + if (fReportErrors) { + fErrorReporter.reportWarning("HTML1004", null); + } + } + if (str.length == 1) { + if (content && fDocumentHandler != null && fElementCount >= fElementDepth) { + fEndLineNumber = fCurrentEntity.getLineNumber(); + fEndColumnNumber = fCurrentEntity.getColumnNumber(); + fEndCharacterOffset = fCurrentEntity.getCharacterOffset(); + fDocumentHandler.characters(str, locationAugs()); + } + return -1; + } + + String name; + if (endsWithSemicolon) + name = str.toString().substring(1, str.length - 1); + else + name = str.toString().substring(1); + + if (name.startsWith("#")) { + int value = -1; + try { + if (name.startsWith("#x") || name.startsWith("#X")) { + value = Integer.parseInt(name.substring(2), 16); + } else { + value = Integer.parseInt(name.substring(1)); + } + /* PATCH: Asgeir Asgeirsson */ + if (fFixWindowsCharRefs && fIso8859Encoding) { + value = fixWindowsCharacter(value); + } + if (content && fDocumentHandler != null && fElementCount >= fElementDepth) { + fEndLineNumber = fCurrentEntity.getLineNumber(); + fEndColumnNumber = fCurrentEntity.getColumnNumber(); + fEndCharacterOffset = fCurrentEntity.getCharacterOffset(); + if (fNotifyCharRefs) { + XMLResourceIdentifier id = resourceId(); + String encoding = null; + fDocumentHandler.startGeneralEntity(name, id, encoding, locationAugs()); + } + str.clear(); + try { + appendChar(str, value); + } catch (IllegalArgumentException e) { // when value is not valid as UTF-16 + if (fReportErrors) { + fErrorReporter.reportError("HTML1005", new Object[] {name}); + } + str.append(REPLACEMENT_CHARACTER); + } + fDocumentHandler.characters(str, locationAugs()); + if (fNotifyCharRefs) { + fDocumentHandler.endGeneralEntity(name, locationAugs()); + } + } + } catch (NumberFormatException e) { + if (fReportErrors) { + fErrorReporter.reportError("HTML1005", new Object[] {name}); + } + if (content && fDocumentHandler != null && fElementCount >= fElementDepth) { + fEndLineNumber = fCurrentEntity.getLineNumber(); + fEndColumnNumber = fCurrentEntity.getColumnNumber(); + fEndCharacterOffset = fCurrentEntity.getCharacterOffset(); + fDocumentHandler.characters(str, locationAugs()); + } + } + return value; + } + + int c = HTMLEntities.get(name); + // in attributes, some incomplete entities should be recognized, not all + // TODO: investigate to find which ones (there are differences between browsers) + // in a first time, consider only those that behave the same in FF and IE + boolean invalidEntityInAttribute = !content && !endsWithSemicolon && c > 256; + if (c == -1 || invalidEntityInAttribute) { + if (fReportErrors) { + fErrorReporter.reportWarning("HTML1006", new Object[] {name}); + } + if (content && fDocumentHandler != null && fElementCount >= fElementDepth) { + fEndLineNumber = fCurrentEntity.getLineNumber(); + fEndColumnNumber = fCurrentEntity.getColumnNumber(); + fEndCharacterOffset = fCurrentEntity.getCharacterOffset(); + fDocumentHandler.characters(str, locationAugs()); + } + return -1; + } + if (content && fDocumentHandler != null && fElementCount >= fElementDepth) { + fEndLineNumber = fCurrentEntity.getLineNumber(); + fEndColumnNumber = fCurrentEntity.getColumnNumber(); + fEndCharacterOffset = fCurrentEntity.getCharacterOffset(); + boolean notify = fNotifyHtmlBuiltinRefs || (fNotifyXmlBuiltinRefs && builtinXmlRef(name)); + if (notify) { + XMLResourceIdentifier id = resourceId(); + String encoding = null; + fDocumentHandler.startGeneralEntity(name, id, encoding, locationAugs()); + } + str.clear(); + appendChar(str, c); + fDocumentHandler.characters(str, locationAugs()); + if (notify) { + fDocumentHandler.endGeneralEntity(name, locationAugs()); + } + } + return c; + } + + /** Returns true if the specified text is present and is skipped. */ + protected boolean skip(String s, boolean caseSensitive) throws IOException { + int length = s != null ? s.length() : 0; + for (int i = 0; i < length; i++) { + if (fCurrentEntity.offset == fCurrentEntity.length) { + System.arraycopy(fCurrentEntity.buffer, fCurrentEntity.offset - i, fCurrentEntity.buffer, 0, i); + if (fCurrentEntity.load(i) == -1) { + fCurrentEntity.offset = 0; + return false; + } + } + char c0 = s.charAt(i); + char c1 = fCurrentEntity.getNextChar(); + if (!caseSensitive) { + c0 = String.valueOf(c0).toUpperCase(Locale.ENGLISH).charAt(0); + c1 = String.valueOf(c1).toUpperCase(Locale.ENGLISH).charAt(0); + } + if (c0 != c1) { + fCurrentEntity.rewind(i + 1); + return false; + } + } + return true; + } + + /** Skips markup. */ + protected boolean skipMarkup(boolean balance) throws IOException { + if (DEBUG_BUFFER) { + fCurrentEntity.debugBufferIfNeeded("(skipMarkup: "); + } + int depth = 1; + boolean slashgt = false; +OUTER: + while (true) { + if (fCurrentEntity.offset == fCurrentEntity.length) { + if (fCurrentEntity.load(0) == -1) { + break; + } + } + while (fCurrentEntity.hasNext()) { + char c = fCurrentEntity.getNextChar(); + if (balance && c == '<') { + depth++; + } else if (c == '>') { + depth--; + if (depth == 0) { + break OUTER; + } + } else if (c == '/') { + if (fCurrentEntity.offset == fCurrentEntity.length) { + if (fCurrentEntity.load(0) == -1) { + break OUTER; + } + } + c = fCurrentEntity.getNextChar(); + if (c == '>') { + slashgt = true; + depth--; + if (depth == 0) { + break OUTER; + } + } else { + fCurrentEntity.rewind(); + } + } else if (c == '\r' || c == '\n') { + fCurrentEntity.rewind(); + skipNewlines(); + } + } + } + if (DEBUG_BUFFER) { + fCurrentEntity.debugBufferIfNeeded(")skipMarkup: ", " -> " + slashgt); + } + return slashgt; + } + + /** Skips whitespace. */ + protected boolean skipSpaces() throws IOException { + if (DEBUG_BUFFER) { + fCurrentEntity.debugBufferIfNeeded("(skipSpaces: "); + } + boolean spaces = false; + while (true) { + if (fCurrentEntity.offset == fCurrentEntity.length) { + if (fCurrentEntity.load(0) == -1) { + break; + } + } + char c = fCurrentEntity.getNextChar(); + if (!Character.isWhitespace(c)) { + fCurrentEntity.rewind(); + break; + } + spaces = true; + if (c == '\r' || c == '\n') { + fCurrentEntity.rewind(); + skipNewlines(); + } + } + if (DEBUG_BUFFER) { + fCurrentEntity.debugBufferIfNeeded(")skipSpaces: ", " -> " + spaces); + } + return spaces; + } + + /** Skips newlines and returns the number of newlines skipped. */ + protected int skipNewlines() throws IOException { + if (DEBUG_BUFFER) { + fCurrentEntity.debugBufferIfNeeded("(skipNewlines: "); + } + + if (!fCurrentEntity.hasNext()) { + if (fCurrentEntity.load(0) == -1) { + if (DEBUG_BUFFER) { + fCurrentEntity.debugBufferIfNeeded(")skipNewlines: "); + } + return 0; + } + } + char c = fCurrentEntity.getCurrentChar(); + int newlines = 0; + int offset = fCurrentEntity.offset; + if (c == '\n' || c == '\r') { + do { + c = fCurrentEntity.getNextChar(); + if (c == '\r') { + newlines++; + if (fCurrentEntity.offset == fCurrentEntity.length) { + offset = 0; + fCurrentEntity.offset = newlines; + if (fCurrentEntity.load(newlines) == -1) { + break; + } + } + if (fCurrentEntity.getCurrentChar() == '\n') { + fCurrentEntity.offset++; + fCurrentEntity.characterOffset_++; + offset++; + } + } else if (c == '\n') { + newlines++; + if (fCurrentEntity.offset == fCurrentEntity.length) { + offset = 0; + fCurrentEntity.offset = newlines; + if (fCurrentEntity.load(newlines) == -1) { + break; + } + } + } else { + fCurrentEntity.rewind(); + break; + } + } while (fCurrentEntity.offset < fCurrentEntity.length - 1); + fCurrentEntity.incLine(newlines); + } + if (DEBUG_BUFFER) { + fCurrentEntity.debugBufferIfNeeded(")skipNewlines: ", " -> " + newlines); + } + return newlines; + } + + // infoset utility methods + + /** Returns an augmentations object with a location item added. */ + protected final Augmentations locationAugs() { + HTMLAugmentations augs = null; + if (fAugmentations) { + fLocationItem.setValues(fBeginLineNumber, fBeginColumnNumber, + fBeginCharacterOffset, fEndLineNumber, fEndColumnNumber, fEndCharacterOffset); + augs = fInfosetAugs; + augs.removeAllItems(); + augs.putItem(AUGMENTATIONS, fLocationItem); + } + return augs; + } + + /** Returns an augmentations object with a synthesized item added. */ + protected final Augmentations synthesizedAugs() { + HTMLAugmentations augs = null; + if (fAugmentations) { + augs = fInfosetAugs; + augs.removeAllItems(); + augs.putItem(AUGMENTATIONS, SYNTHESIZED_ITEM); + } + return augs; + } + + /** Returns an empty resource identifier. */ + protected final XMLResourceIdentifier resourceId() { + // + fResourceId.clear(); + return fResourceId; + // NOTE: Unfortunately, the Xerces DOM parser classes expect a + // non-null resource identifier object to be passed to + // startGeneralEntity. -Ac +// return null; + } + + // + // Protected static methods + // + + /** Returns true if the name is a built-in XML general entity reference. */ + protected static boolean builtinXmlRef(String name) { + return name.equals("amp") || name.equals("lt") || name.equals("gt") || + name.equals("quot") || name.equals("apos"); + } + + // + // Private methods + // + + /** + * Append a character to an XMLStringBuffer. The character is an int value, and can either be a + * single UTF-16 character or a supplementary character represented by two UTF-16 code points. + * + * @param str The XMLStringBuffer to append to. + * @param value The character value. + */ + private static void appendChar(XMLStringBuffer str, int value) { + if (value > Character.MAX_VALUE) { + char[] chars = Character.toChars(value); + + str.append(chars, 0, chars.length); + } else { + str.append((char) value); + } + } + + /** + * Append a character to a StringBuffer. The character is an int value, and can either be a + * single UTF-16 character or a supplementary character represented by two UTF-16 code points. + * + * @param str The StringBuffer to append to. + * @param value The character value. + */ + private static void appendChar(StringBuffer str, int value) { + if (value > Character.MAX_VALUE) { + char[] chars = Character.toChars(value); + + str.append(chars, 0, chars.length); + } else { + str.append((char) value); + } + } + + // + // Interfaces + // + + /** + * Basic scanner interface. + * + * @author Andy Clark + */ + public interface Scanner { + + // + // Scanner methods + // + + /** + * Scans part of the document. This interface allows scanning to + * be performed in a pulling manner. + * + * @param complete True if the scanner should not return until + * scanning is complete. + * @return True if additional scanning is required. + * @throws IOException Thrown if I/O error occurs. + */ + boolean scan(boolean complete) throws IOException; + } + + // + // Classes + // + + /** + * Current entity. + * + * @author Andy Clark + */ + public static class CurrentEntity { + + // + // Data + // + + /** Character stream. */ + private Reader stream_; + + /** Encoding. */ + private String encoding; + + /** Public identifier. */ + public final String publicId; + + /** Base system identifier. */ + public final String baseSystemId; + + /** Literal system identifier. */ + public final String literalSystemId; + + /** Expanded system identifier. */ + public final String expandedSystemId; + + /** XML version. */ + public static final String version = "1.0"; + + /** Line number. */ + private int lineNumber_ = 1; + + /** Column number. */ + private int columnNumber_ = 1; + + /** Character offset in the file. */ + public int characterOffset_ = 0; + + // buffer + + /** Character buffer. */ + public char[] buffer = new char[DEFAULT_BUFFER_SIZE]; + + /** Offset into character buffer. */ + public int offset = 0; + + /** Length of characters read into character buffer. */ + public int length = 0; + + private boolean endReached_ = false; + + // + // Constructors + // + + /** Constructs an entity from the specified stream. */ + public CurrentEntity(Reader stream, String encoding, String publicId, String baseSystemId, + String literalSystemId, String expandedSystemId) { + stream_ = stream; + this.encoding = encoding; + this.publicId = publicId; + this.baseSystemId = baseSystemId; + this.literalSystemId = literalSystemId; + this.expandedSystemId = expandedSystemId; + } + + private char getCurrentChar() { + return buffer[offset]; + } + + /** + * Gets the current character and moves to next one. + * + * @return + */ + private char getNextChar() { + characterOffset_++; + columnNumber_++; + return buffer[offset++]; + } + + private void closeQuietly() { + try { + stream_.close(); + } catch (IOException e) { + // ignore + } + } + + /** + * Indicates if there are characters left. + */ + boolean hasNext() { + return offset < length; + } + + /** + * Loads a new chunk of data into the buffer and returns the number of + * characters loaded or -1 if no additional characters were loaded. + * + * @param offset The offset at which new characters should be loaded. + */ + protected int load(int offset) throws IOException { + if (DEBUG_BUFFER) { + debugBufferIfNeeded("(load: "); + } + // resize buffer, if needed + if (offset == buffer.length) { + int adjust = buffer.length / 4; + char[] array = new char[buffer.length + adjust]; + System.arraycopy(buffer, 0, array, 0, length); + buffer = array; + } + // read a block of characters + int count = stream_.read(buffer, offset, buffer.length - offset); + if (count == -1) { + endReached_ = true; + } + length = count != -1 ? count + offset : offset; + this.offset = offset; + if (DEBUG_BUFFER) { + debugBufferIfNeeded(")load: ", " -> " + count); + } + return count; + } + + /** Reads a single character. */ + protected int read() throws IOException { + if (DEBUG_BUFFER) { + debugBufferIfNeeded("(read: "); + } + if (offset == length) { + if (endReached_) { + return -1; + } + if (load(0) == -1) { + if (DEBUG_BUFFER) { + System.out.println(")read: -> -1"); + } + return -1; + } + } + char c = buffer[offset++]; + characterOffset_++; + columnNumber_++; + + if (DEBUG_BUFFER) { + debugBufferIfNeeded(")read: ", " -> " + c); + } + return c; + } + + /** Prints the contents of the character buffer to standard out. */ + private void debugBufferIfNeeded(String prefix) { + debugBufferIfNeeded(prefix, ""); + } + + /** Prints the contents of the character buffer to standard out. */ + private void debugBufferIfNeeded(String prefix, String suffix) { + if (DEBUG_BUFFER) { + System.out.print(prefix); + System.out.print('['); + System.out.print(length); + System.out.print(' '); + System.out.print(offset); + if (length > 0) { + System.out.print(" \""); + for (int i = 0; i < length; i++) { + if (i == offset) { + System.out.print('^'); + } + char c = buffer[i]; + switch (c) { + case '\r': { + System.out.print("\\r"); + break; + } + case '\n': { + System.out.print("\\n"); + break; + } + case '\t': { + System.out.print("\\t"); + break; + } + case '"': { + System.out.print("\\\""); + break; + } + default: { + System.out.print(c); + } + } + } + if (offset == length) { + System.out.print('^'); + } + System.out.print('"'); + } + System.out.print(']'); + System.out.print(suffix); + System.out.println(); + } + } + + private void setStream(InputStreamReader inputStreamReader) { + stream_ = inputStreamReader; + offset = length = characterOffset_ = 0; + lineNumber_ = columnNumber_ = 1; + encoding = inputStreamReader.getEncoding(); + } + + /** + * Goes back, cancelling the effect of the previous read() call. + */ + private void rewind() { + offset--; + characterOffset_--; + columnNumber_--; + } + + private void rewind(int i) { + offset -= i; + characterOffset_ -= i; + columnNumber_ -= i; + } + + private void incLine() { + lineNumber_++; + columnNumber_ = 1; + } + + private void incLine(int nbLines) { + lineNumber_ += nbLines; + columnNumber_ = 1; + } + + public int getLineNumber() { + return lineNumber_; + } + + private void resetBuffer(XMLStringBuffer buffer, int lineNumber, + int columnNumber, int characterOffset) { + lineNumber_ = lineNumber; + columnNumber_ = columnNumber; + this.characterOffset_ = characterOffset; + this.buffer = buffer.ch; + this.offset = buffer.offset; + this.length = buffer.length; + } + + private int getColumnNumber() { + return columnNumber_; + } + + private void restorePosition(int originalOffset, + int originalColumnNumber, int originalCharacterOffset) { + this.offset = originalOffset; + this.columnNumber_ = originalColumnNumber; + this.characterOffset_ = originalCharacterOffset; + } + + private int getCharacterOffset() { + return characterOffset_; + } + } + + /** + * The primary HTML document scanner. + * + * @author Andy Clark + */ + public class ContentScanner implements Scanner { + + // + // Data + // + + // temp vars + + /** A qualified name. */ + private final QName fQName = new QName(); + + /** Attributes. */ + private final XMLAttributesImpl fAttributes = new XMLAttributesImpl(); + + // + // Scanner methods + // + + /** Scan. */ + public boolean scan(boolean complete) throws IOException { + boolean next; + do { + try { + next = false; + switch (fScannerState) { + case STATE_CONTENT: { + fBeginLineNumber = fCurrentEntity.getLineNumber(); + fBeginColumnNumber = fCurrentEntity.getColumnNumber(); + fBeginCharacterOffset = fCurrentEntity.getCharacterOffset(); + int c = fCurrentEntity.read(); + if (c == '<') { + setScannerState(STATE_MARKUP_BRACKET); + next = true; + } else if (c == '&') { + scanEntityRef(fStringBuffer, true); + } else if (c == -1) { + throw new EOFException(); + } else { + fCurrentEntity.rewind(); + scanCharacters(); + } + break; + } + case STATE_MARKUP_BRACKET: { + int c = fCurrentEntity.read(); + if (c == '!') { + if (skip("--", false)) { + scanComment(); + } else if (skip("[CDATA[", false)) { + scanCDATA(); + } else if (skip("DOCTYPE", false)) { + scanDoctype(); + } else { + if (fReportErrors) { + fErrorReporter.reportError("HTML1002", null); + } + skipMarkup(true); + } + } else if (c == '?') { + scanPI(); + } else if (c == '/') { + scanEndElement(); + } else if (c == -1) { + if (fReportErrors) { + fErrorReporter.reportError("HTML1003", null); + } + if (fDocumentHandler != null && fElementCount >= fElementDepth) { + fStringBuffer.clear(); + fStringBuffer.append('<'); + fDocumentHandler.characters(fStringBuffer, null); + } + throw new EOFException(); + } else { + fCurrentEntity.rewind(); + fElementCount++; + fSingleBoolean[0] = false; + String ename = scanStartElement(fSingleBoolean); + String enameLC = ename == null ? null : ename.toLowerCase(); + fBeginLineNumber = fCurrentEntity.getLineNumber(); + fBeginColumnNumber = fCurrentEntity.getColumnNumber(); + fBeginCharacterOffset = fCurrentEntity.getCharacterOffset(); + if ("script".equals(enameLC)) { + scanScriptContent(); + } else if (!fAllowSelfclosingTags && !fAllowSelfclosingIframe && "iframe".equals(enameLC)) { + scanUntilEndTag("iframe"); + } else if (!fParseNoScriptContent && "noscript".equals(enameLC)) { + scanUntilEndTag("noscript"); + } else if (!fParseNoFramesContent && "noframes".equals(enameLC)) { + scanUntilEndTag("noframes"); + } else if (ename != null && !fSingleBoolean[0] + && HTMLElements.getElement(enameLC).isSpecial() + && (!ename.equalsIgnoreCase("TITLE") || isEnded(enameLC))) { + setScanner(fSpecialScanner.setElementName(ename)); + setScannerState(STATE_CONTENT); + return true; + } + } + setScannerState(STATE_CONTENT); + break; + } + case STATE_START_DOCUMENT: { + if (fDocumentHandler != null && fElementCount >= fElementDepth) { + if (DEBUG_CALLBACKS) { + System.out.println("startDocument()"); + } + XMLLocator locator = HTMLScanner.this; + String encoding = fIANAEncoding; + Augmentations augs = locationAugs(); + NamespaceContext nscontext = new NamespaceSupport(); + XercesBridge.getInstance().XMLDocumentHandler_startDocument(fDocumentHandler, locator, encoding, nscontext, augs); + } + if (fInsertDoctype && fDocumentHandler != null) { + String root = HTMLElements.getElement(HTMLElements.HTML).name; + root = modifyName(root, fNamesElems); + String pubid = fDoctypePubid; + String sysid = fDoctypeSysid; + fDocumentHandler.doctypeDecl(root, pubid, sysid, + synthesizedAugs()); + } + setScannerState(STATE_CONTENT); + break; + } + case STATE_END_DOCUMENT: { + if (fDocumentHandler != null && fElementCount >= fElementDepth && complete) { + if (DEBUG_CALLBACKS) { + System.out.println("endDocument()"); + } + fEndLineNumber = fCurrentEntity.getLineNumber(); + fEndColumnNumber = fCurrentEntity.getColumnNumber(); + fEndCharacterOffset = fCurrentEntity.getCharacterOffset(); + fDocumentHandler.endDocument(locationAugs()); + } + return false; + } + default: { + throw new RuntimeException("unknown scanner state: " + fScannerState); + } + } + } catch (EOFException e) { + if (fCurrentEntityStack.empty()) { + setScannerState(STATE_END_DOCUMENT); + } else { + fCurrentEntity = fCurrentEntityStack.pop(); + } + next = true; + } + } while (next || complete); + return true; + } + + /** + * Scans the content of

+ * If the encoding is changed, then the scanner calls the + * playback method and re-scans the beginning of the HTML + * document again. This should not be too much of a performance problem + * because the <meta> tag appears at the beginning of the document. + *

+ * If the <body> tag is reached without playing back the bytes, + * then the buffer can be cleared by calling the clear + * method. This stops the buffering of bytes and allows the memory used + * by the buffer to be reclaimed. + *

+ * Note: + * If the buffer is never played back or cleared, this input stream + * will continue to buffer the entire stream. Therefore, it is very + * important to use this stream correctly. + * + * @author Andy Clark + */ + public static class PlaybackInputStream extends FilterInputStream { + + // + // Constants + // + + /** Set to true to debug playback. */ + private static final boolean DEBUG_PLAYBACK = false; + + // + // Data + // + + // state + + /** Playback mode. */ + protected boolean fPlayback = false; + + /** Buffer cleared. */ + protected boolean fCleared = false; + + /** Encoding detected. */ + protected boolean fDetected = false; + + // buffer info + + /** Byte buffer. */ + protected byte[] fByteBuffer = new byte[1024]; + + /** Offset into byte buffer during playback. */ + protected int fByteOffset = 0; + + /** Length of bytes read into byte buffer. */ + protected int fByteLength = 0; + + /** Pushback offset. */ + public int fPushbackOffset = 0; + + /** Pushback length. */ + public int fPushbackLength = 0; + + // + // Constructors + // + + /** Constructor. */ + public PlaybackInputStream(InputStream in) { + super(in); + } + + // + // Public methods + // + + /** Detect encoding. */ + public void detectEncoding(String[] encodings) throws IOException { + if (fDetected) { + throw new IOException("Should not detect encoding twice."); + } + fDetected = true; + int b1 = read(); + if (b1 == -1) { + return; + } + int b2 = read(); + if (b2 == -1) { + fPushbackLength = 1; + return; + } + // UTF-8 BOM: 0xEFBBBF + if (b1 == 0xEF && b2 == 0xBB) { + int b3 = read(); + if (b3 == 0xBF) { + fPushbackOffset = 3; + encodings[0] = "UTF-8"; + encodings[1] = "UTF8"; + return; + } + fPushbackLength = 3; + } + // UTF-16 LE BOM: 0xFFFE + if (b1 == 0xFF && b2 == 0xFE) { + encodings[0] = "UTF-16"; + encodings[1] = "UnicodeLittleUnmarked"; + return; + } + // UTF-16 BE BOM: 0xFEFF + else if (b1 == 0xFE && b2 == 0xFF) { + encodings[0] = "UTF-16"; + encodings[1] = "UnicodeBigUnmarked"; + return; + } + // unknown + fPushbackLength = 2; + } + + /** Playback buffer contents. */ + public void playback() { + fPlayback = true; + } + + /** + * Clears the buffer. + *

+ * Note: + * The buffer cannot be cleared during playback. Therefore, calling + * this method during playback will not do anything. However, the + * buffer will be cleared automatically at the end of playback. + */ + public void clear() { + if (!fPlayback) { + fCleared = true; + fByteBuffer = null; + } + } + + // + // InputStream methods + // + + /** Read a byte. */ + public int read() throws IOException { + if (DEBUG_PLAYBACK) { + System.out.println("(read"); + } + if (fPushbackOffset < fPushbackLength) { + return fByteBuffer[fPushbackOffset++] & 0xFF; + } + if (fCleared) { + return in.read(); + } + if (fPlayback) { + int c = fByteBuffer[fByteOffset++]; + if (fByteOffset == fByteLength) { + fCleared = true; + fByteBuffer = null; + } + if (DEBUG_PLAYBACK) { + System.out.println(")read -> " + (char) c); + } + return c; + } + int c = in.read(); + if (c != -1) { + if (fByteLength == fByteBuffer.length) { + byte[] newarray = new byte[fByteLength + 1024]; + System.arraycopy(fByteBuffer, 0, newarray, 0, fByteLength); + fByteBuffer = newarray; + } + fByteBuffer[fByteLength++] = (byte) c; + } + if (DEBUG_PLAYBACK) { + System.out.println(")read -> " + (char) c); + } + return c; + } + + /** Read an array of bytes. */ + public int read(byte[] array) throws IOException { + return read(array, 0, array.length); + } + + /** Read an array of bytes. */ + public int read(byte[] array, int offset, int length) throws IOException { + if (DEBUG_PLAYBACK) { + System.out.println(")read(" + offset + ',' + length + ')'); + } + if (fPushbackOffset < fPushbackLength) { + int count = fPushbackLength - fPushbackOffset; + if (count > length) { + count = length; + } + System.arraycopy(fByteBuffer, fPushbackOffset, array, offset, count); + fPushbackOffset += count; + return count; + } + if (fCleared) { + return in.read(array, offset, length); + } + if (fPlayback) { + if (fByteOffset + length > fByteLength) { + length = fByteLength - fByteOffset; + } + System.arraycopy(fByteBuffer, fByteOffset, array, offset, length); + fByteOffset += length; + if (fByteOffset == fByteLength) { + fCleared = true; + fByteBuffer = null; + } + return length; + } + int count = in.read(array, offset, length); + if (count != -1) { + if (fByteLength + count > fByteBuffer.length) { + byte[] newarray = new byte[fByteLength + count + 512]; + System.arraycopy(fByteBuffer, 0, newarray, 0, fByteLength); + fByteBuffer = newarray; + } + System.arraycopy(array, offset, fByteBuffer, fByteLength, count); + fByteLength += count; + } + if (DEBUG_PLAYBACK) { + System.out.println(")read(" + offset + ',' + length + ") -> " + count); + } + return count; + } + } + + /** + * Location infoset item. + * + * @author Andy Clark + */ + protected static class LocationItem implements HTMLEventInfo, Cloneable { + + // + // Data + // + + /** Beginning line number. */ + protected int fBeginLineNumber; + + /** Beginning column number. */ + protected int fBeginColumnNumber; + + /** Beginning character offset. */ + protected int fBeginCharacterOffset; + + /** Ending line number. */ + protected int fEndLineNumber; + + /** Ending column number. */ + protected int fEndColumnNumber; + + /** Ending character offset. */ + protected int fEndCharacterOffset; + + // + // Public methods + // + public LocationItem() { + // nothing + } + + LocationItem(LocationItem other) { + setValues(other.fBeginLineNumber, other.fBeginColumnNumber, other.fBeginCharacterOffset, + other.fEndLineNumber, other.fEndColumnNumber, other.fEndCharacterOffset); + } + + /** Sets the values of this item. */ + public void setValues(int beginLine, int beginColumn, int beginOffset, + int endLine, int endColumn, int endOffset) { + fBeginLineNumber = beginLine; + fBeginColumnNumber = beginColumn; + fBeginCharacterOffset = beginOffset; + fEndLineNumber = endLine; + fEndColumnNumber = endColumn; + fEndCharacterOffset = endOffset; + } + + // + // HTMLEventInfo methods + // + + // location information + + /** Returns the line number of the beginning of this event. */ + public int getBeginLineNumber() { + return fBeginLineNumber; + } + + /** Returns the column number of the beginning of this event. */ + public int getBeginColumnNumber() { + return fBeginColumnNumber; + } + + /** Returns the character offset of the beginning of this event. */ + public int getBeginCharacterOffset() { + return fBeginCharacterOffset; + } + + /** Returns the line number of the end of this event. */ + public int getEndLineNumber() { + return fEndLineNumber; + } + + /** Returns the column number of the end of this event. */ + public int getEndColumnNumber() { + return fEndColumnNumber; + } + + /** Returns the character offset of the end of this event. */ + public int getEndCharacterOffset() { + return fEndCharacterOffset; + } + + // other information + + /** Returns true if this corresponding event was synthesized. */ + public boolean isSynthesized() { + return false; + } + + // + // Object methods + // + + /** Returns a string representation of this object. */ + public String toString() { + String str = String.valueOf(fBeginLineNumber) + + ':' + + fBeginColumnNumber + + ':' + + fBeginCharacterOffset + + ':' + + fEndLineNumber + + ':' + + fEndColumnNumber + + ':' + + fEndCharacterOffset; + return str; + } + + } + + /** + * To detect if 2 encoding are compatible, both must be able to read the meta tag specifying + * the new encoding. This means that the byte representation of some minimal html markup must + * be the same in both encodings + */ + boolean isEncodingCompatible(String encoding1, String encoding2) { + try { + try { + return canRoundtrip(encoding1, encoding2); + } catch (UnsupportedOperationException e) { + // if encoding1 only supports decode, we can test it the other way to only decode with it + try { + return canRoundtrip(encoding2, encoding1); + } catch (UnsupportedOperationException e1) { + // encoding2 only supports decode too. Time to give up. + return false; + } + } + } catch (UnsupportedEncodingException e) { + return false; + } + } + + private static boolean canRoundtrip(String encodeCharset, String decodeCharset) throws UnsupportedEncodingException { + final String reference = " -1"); + } + return -1; + } + } + char c = fCurrentEntity.getNextChar(); + if (DEBUG_BUFFER) { + fCurrentEntity.debugBufferIfNeeded(")read: ", " -> " + c); + } + return c; + } + + /** + * Indicates if the end comment --> is available, loading further data if needed, without to reset the buffer + */ + private boolean endCommentAvailable() throws IOException { + int nbCaret = 0; + int originalOffset = fCurrentEntity.offset; + int originalColumnNumber = fCurrentEntity.getColumnNumber(); + int originalCharacterOffset = fCurrentEntity.getCharacterOffset(); + + while (true) { + int c = readPreservingBufferContent(); + if (c == -1) { + fCurrentEntity.restorePosition(originalOffset, originalColumnNumber, originalCharacterOffset); + return false; + } else if (c == '>' && nbCaret >= 2) { + fCurrentEntity.restorePosition(originalOffset, originalColumnNumber, originalCharacterOffset); + return true; + } else if (c == '-') { + nbCaret++; + } else { + nbCaret = 0; + } + } + } + + /** + * Reduces the buffer to the content between start and end marker when + * only whitespaces are found before the startMarker as well as after the end marker + */ + static void reduceToContent(XMLStringBuffer buffer, String startMarker, String endMarker) { + int i = 0; + int startContent = -1; + int l1 = startMarker.length(); + int l2 = endMarker.length(); + while (i < buffer.length - l1 - l2) { + char c = buffer.ch[buffer.offset + i]; + if (Character.isWhitespace(c)) { + ++i; + } else if (c == startMarker.charAt(0) + && startMarker.equals(new String(buffer.ch, buffer.offset + i, l1))) { + startContent = buffer.offset + i + l1; + break; + } else { + return; // start marker not found + } + } + if (startContent == -1) { // start marker not found + return; + } + + i = buffer.length - 1; + while (i > startContent + l2) { + char c = buffer.ch[buffer.offset + i]; + if (Character.isWhitespace(c)) { + --i; + } else if (c == endMarker.charAt(l2 - 1) + && endMarker.equals(new String(buffer.ch, buffer.offset + i - l2 + 1, l2))) { + + buffer.length = buffer.offset + i - startContent - 2; + buffer.offset = startContent; + return; + } else { + return; // start marker not found + } + } + } +} diff --git a/src/main/java/org/cyberneko/html/HTMLTagBalancer.java b/src/main/java/org/cyberneko/html/HTMLTagBalancer.java new file mode 100644 index 0000000..fa5f665 --- /dev/null +++ b/src/main/java/org/cyberneko/html/HTMLTagBalancer.java @@ -0,0 +1,1401 @@ +/* + * Copyright 2002-2009 Andy Clark, Marc Guillemot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyberneko.html; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.apache.xerces.util.XMLAttributesImpl; +import org.apache.xerces.xni.Augmentations; +import org.apache.xerces.xni.NamespaceContext; +import org.apache.xerces.xni.QName; +import org.apache.xerces.xni.XMLAttributes; +import org.apache.xerces.xni.XMLDocumentHandler; +import org.apache.xerces.xni.XMLLocator; +import org.apache.xerces.xni.XMLResourceIdentifier; +import org.apache.xerces.xni.XMLString; +import org.apache.xerces.xni.XNIException; +import org.apache.xerces.xni.parser.XMLComponentManager; +import org.apache.xerces.xni.parser.XMLConfigurationException; +import org.apache.xerces.xni.parser.XMLDocumentFilter; +import org.apache.xerces.xni.parser.XMLDocumentSource; +import org.cyberneko.html.HTMLElements.Element; +import org.cyberneko.html.filters.NamespaceBinder; +import org.cyberneko.html.xercesbridge.XercesBridge; + + +/** + * Balances tags in an HTML document. This component receives document events + * and tries to correct many common mistakes that human (and computer) HTML + * document authors make. This tag balancer can: + *

    + *
  • add missing parent elements; + *
  • automatically close elements with optional end tags; and + *
  • handle mis-matched inline element tags. + *
+ *

+ * This component recognizes the following features: + *

    + *
  • http://cyberneko.org/html/features/augmentations + *
  • http://cyberneko.org/html/features/report-errors + *
  • http://cyberneko.org/html/features/balance-tags/document-fragment + *
  • http://cyberneko.org/html/features/balance-tags/ignore-outside-content + *
+ *

+ * This component recognizes the following properties: + *

    + *
  • http://cyberneko.org/html/properties/names/elems + *
  • http://cyberneko.org/html/properties/names/attrs + *
  • http://cyberneko.org/html/properties/error-reporter + *
  • http://cyberneko.org/html/properties/balance-tags/current-stack + *
+ * + * @author Andy Clark + * @author Marc Guillemot + * @version $Id: HTMLTagBalancer.java,v 1.20 2005/02/14 04:06:22 andyc Exp $ + * @see HTMLElements + */ +public class HTMLTagBalancer implements XMLDocumentFilter, HTMLComponent { + + // + // Constants + // + + // features + + /** Namespaces. */ + protected static final String NAMESPACES = "http://xml.org/sax/features/namespaces"; + + /** Include infoset augmentations. */ + protected static final String AUGMENTATIONS = "http://cyberneko.org/html/features/augmentations"; + + /** Report errors. */ + protected static final String REPORT_ERRORS = "http://cyberneko.org/html/features/report-errors"; + + /** Document fragment balancing only (deprecated). */ + protected static final String DOCUMENT_FRAGMENT_DEPRECATED = "http://cyberneko.org/html/features/document-fragment"; + + /** Document fragment balancing only. */ + protected static final String DOCUMENT_FRAGMENT = "http://cyberneko.org/html/features/balance-tags/document-fragment"; + + /** Ignore outside content. */ + protected static final String IGNORE_OUTSIDE_CONTENT = "http://cyberneko.org/html/features/balance-tags/ignore-outside-content"; + + /** Recognized features. */ + private static final String[] RECOGNIZED_FEATURES = { + NAMESPACES, + AUGMENTATIONS, + REPORT_ERRORS, + DOCUMENT_FRAGMENT_DEPRECATED, + DOCUMENT_FRAGMENT, + IGNORE_OUTSIDE_CONTENT, + }; + + /** Recognized features defaults. */ + private static final Boolean[] RECOGNIZED_FEATURES_DEFAULTS = { + null, + null, + null, + null, + Boolean.FALSE, + Boolean.FALSE, + }; + + // properties + + /** Modify HTML element names: { "upper", "lower", "default" }. */ + protected static final String NAMES_ELEMS = "http://cyberneko.org/html/properties/names/elems"; + + /** Modify HTML attribute names: { "upper", "lower", "default" }. */ + protected static final String NAMES_ATTRS = "http://cyberneko.org/html/properties/names/attrs"; + + /** Error reporter. */ + protected static final String ERROR_REPORTER = "http://cyberneko.org/html/properties/error-reporter"; + + /** + * EXPERIMENTAL: may change in next release
+ * Name of the property holding the stack of elements in which context a document fragment should be parsed. + **/ + public static final String FRAGMENT_CONTEXT_STACK = "http://cyberneko.org/html/properties/balance-tags/fragment-context-stack"; + + /** Recognized properties. */ + private static final String[] RECOGNIZED_PROPERTIES = { + NAMES_ELEMS, + NAMES_ATTRS, + ERROR_REPORTER, + FRAGMENT_CONTEXT_STACK, + }; + + /** Recognized properties defaults. */ + private static final Object[] RECOGNIZED_PROPERTIES_DEFAULTS = { + null, + null, + null, + null, + }; + + // modify HTML names + + /** Don't modify HTML names. */ + protected static final short NAMES_NO_CHANGE = 0; + + /** Match HTML element names. */ + protected static final short NAMES_MATCH = 0; + + /** Uppercase HTML names. */ + protected static final short NAMES_UPPERCASE = 1; + + /** Lowercase HTML names. */ + protected static final short NAMES_LOWERCASE = 2; + + // static vars + + /** Synthesized event info item. */ + protected static final HTMLEventInfo SYNTHESIZED_ITEM = new HTMLEventInfo.SynthesizedItem(); + + // + // Data + // + + // features + + /** Namespaces. */ + protected boolean fNamespaces; + + /** Include infoset augmentations. */ + protected boolean fAugmentations; + + /** Report errors. */ + protected boolean fReportErrors; + + /** Document fragment balancing only. */ + protected boolean fDocumentFragment; + + /** Ignore outside content. */ + protected boolean fIgnoreOutsideContent; + + /** Allows self closing iframe tags. */ + protected boolean fAllowSelfclosingIframe; + + /** Allows self closing tags. */ + protected boolean fAllowSelfclosingTags; + + // properties + + /** Modify HTML element names. */ + protected short fNamesElems; + + /** Modify HTML attribute names. */ + protected short fNamesAttrs; + + /** Error reporter. */ + protected HTMLErrorReporter fErrorReporter; + + // connections + + /** The document source. */ + protected XMLDocumentSource fDocumentSource; + + /** The document handler. */ + protected XMLDocumentHandler fDocumentHandler; + + // state + + /** The element stack. */ + protected final InfoStack fElementStack = new InfoStack(); + + /** The inline stack. */ + protected final InfoStack fInlineStack = new InfoStack(); + + /** True if seen anything. Important for xml declaration. */ + protected boolean fSeenAnything; + + /** True if root element has been seen. */ + protected boolean fSeenDoctype; + + /** True if root element has been seen. */ + protected boolean fSeenRootElement; + + /** + * True if seen the end of the document element. In other words, + * this variable is set to false until the end </HTML> + * tag is seen (or synthesized). This is used to ensure that + * extraneous events after the end of the document element do not + * make the document stream ill-formed. + */ + protected boolean fSeenRootElementEnd; + + /** True if seen <head< element. */ + protected boolean fSeenHeadElement; + + /** True if seen <body< element. */ + protected boolean fSeenBodyElement; + private boolean fSeenBodyElementEnd; + + /** True if seen <frameset< element. */ + private boolean fSeenFramesetElement; + + /** True if a form is in the stack (allow to discard opening of nested forms) */ + protected boolean fOpenedForm; + + // temp vars + + /** A qualified name. */ + private final QName fQName = new QName(); + + /** Empty attributes. */ + private final XMLAttributes fEmptyAttrs = new XMLAttributesImpl(); + + /** Augmentations. */ + private final HTMLAugmentations fInfosetAugs = new HTMLAugmentations(); + + protected HTMLTagBalancingListener tagBalancingListener; + private final LostText lostText_ = new LostText(); + + private boolean forcedStartElement_ = false; + private boolean forcedEndElement_ = false; + + /** + * Stack of elements determining the context in which a document fragment should be parsed + */ + private QName[] fragmentContextStack_ = null; + private int fragmentContextStackSize_ = 0; // not 0 only when a fragment is parsed and fragmentContextStack_ is set + + private final List endElementsBuffer_ = new ArrayList<>(); + + // + // HTMLComponent methods + // + + /** Returns the default state for a feature. */ + public Boolean getFeatureDefault(String featureId) { + int length = RECOGNIZED_FEATURES != null ? RECOGNIZED_FEATURES.length : 0; + for (int i = 0; i < length; i++) { + if (RECOGNIZED_FEATURES[i].equals(featureId)) { + return RECOGNIZED_FEATURES_DEFAULTS[i]; + } + } + return null; + } + + /** Returns the default state for a property. */ + public Object getPropertyDefault(String propertyId) { + int length = RECOGNIZED_PROPERTIES != null ? RECOGNIZED_PROPERTIES.length : 0; + for (int i = 0; i < length; i++) { + if (RECOGNIZED_PROPERTIES[i].equals(propertyId)) { + return RECOGNIZED_PROPERTIES_DEFAULTS[i]; + } + } + return null; + } + + // + // XMLComponent methods + // + + /** Returns recognized features. */ + public String[] getRecognizedFeatures() { + return RECOGNIZED_FEATURES; + } + + /** Returns recognized properties. */ + public String[] getRecognizedProperties() { + return RECOGNIZED_PROPERTIES; + } + + /** Resets the component. */ + public void reset(XMLComponentManager manager) throws XMLConfigurationException { + + // get features + fNamespaces = manager.getFeature(NAMESPACES); + fAugmentations = manager.getFeature(AUGMENTATIONS); + fReportErrors = manager.getFeature(REPORT_ERRORS); + fDocumentFragment = manager.getFeature(DOCUMENT_FRAGMENT) || + manager.getFeature(DOCUMENT_FRAGMENT_DEPRECATED); + fIgnoreOutsideContent = manager.getFeature(IGNORE_OUTSIDE_CONTENT); + fAllowSelfclosingIframe = manager.getFeature(HTMLScanner.ALLOW_SELFCLOSING_IFRAME); + fAllowSelfclosingTags = manager.getFeature(HTMLScanner.ALLOW_SELFCLOSING_TAGS); + + // get properties + fNamesElems = getNamesValue(String.valueOf(manager.getProperty(NAMES_ELEMS))); + fNamesAttrs = getNamesValue(String.valueOf(manager.getProperty(NAMES_ATTRS))); + fErrorReporter = (HTMLErrorReporter) manager.getProperty(ERROR_REPORTER); + + fragmentContextStack_ = (QName[]) manager.getProperty(FRAGMENT_CONTEXT_STACK); + fSeenAnything = false; + fSeenDoctype = false; + fSeenRootElement = false; + fSeenRootElementEnd = false; + fSeenHeadElement = false; + fSeenBodyElement = false; + fSeenBodyElementEnd = false; + fSeenFramesetElement = false; + + } + + /** Sets a feature. */ + public void setFeature(String featureId, boolean state) throws XMLConfigurationException { + + switch (featureId) { + case AUGMENTATIONS -> { + fAugmentations = state; + } + case REPORT_ERRORS -> { + fReportErrors = state; + } + case IGNORE_OUTSIDE_CONTENT -> { + fIgnoreOutsideContent = state; + } + } + + } + + /** Sets a property. */ + public void setProperty(String propertyId, Object value) throws XMLConfigurationException { + + if (propertyId.equals(NAMES_ELEMS)) { + fNamesElems = getNamesValue(String.valueOf(value)); + return; + } + + if (propertyId.equals(NAMES_ATTRS)) { + fNamesAttrs = getNamesValue(String.valueOf(value)); + } + } + + // + // XMLDocumentSource methods + // + + /** Sets the document handler. */ + public void setDocumentHandler(XMLDocumentHandler handler) { + fDocumentHandler = handler; + } + + // @since Xerces 2.1.0 + + /** Returns the document handler. */ + public XMLDocumentHandler getDocumentHandler() { + return fDocumentHandler; + } + + // + // XMLDocumentHandler methods + // + + // since Xerces-J 2.2.0 + + /** Start document. */ + public void startDocument(XMLLocator locator, String encoding, NamespaceContext nscontext, Augmentations augs) + throws XNIException { + + // reset state + fElementStack.top = 0; + if (fragmentContextStack_ != null) { + fragmentContextStackSize_ = fragmentContextStack_.length; + for (QName name : fragmentContextStack_) { + Element elt = HTMLElements.getElement(name.localpart); + fElementStack.push(new Info(elt, name)); + } + + } else { + fragmentContextStackSize_ = 0; + } + + // pass on event + if (fDocumentHandler != null) { + XercesBridge.getInstance().XMLDocumentHandler_startDocument(fDocumentHandler, locator, encoding, nscontext, augs); + } + } + + // old methods + + /** XML declaration. */ + public void xmlDecl(String version, String encoding, String standalone, Augmentations augs) throws XNIException { + if (!fSeenAnything && fDocumentHandler != null) { + fDocumentHandler.xmlDecl(version, encoding, standalone, augs); + } + } + + /** Doctype declaration. */ + public void doctypeDecl(String rootElementName, String publicId, String systemId, Augmentations augs) + throws XNIException { + + fSeenAnything = true; + if (fReportErrors) { + if (fSeenRootElement) { + fErrorReporter.reportError("HTML2010", null); + } else if (fSeenDoctype) { + fErrorReporter.reportError("HTML2011", null); + } + } + if (!fSeenRootElement && !fSeenDoctype) { + fSeenDoctype = true; + if (fDocumentHandler != null) { + fDocumentHandler.doctypeDecl(rootElementName, publicId, systemId, augs); + } + } + } + + /** End document. */ + public void endDocument(Augmentations augs) throws XNIException { + + // and have been buffered to consider outside content + fIgnoreOutsideContent = true; // endElement should not ignore the elements passed from buffer + consumeBufferedEndElements(); + + // handle empty document + if (!fSeenRootElement && !fDocumentFragment) { + if (fReportErrors) { + fErrorReporter.reportError("HTML2000", null); + } + if (fDocumentHandler != null) { + fSeenRootElementEnd = false; + forceStartBody(); // will force and + String body = modifyName("body", fNamesElems); + fQName.setValues(null, body, body, null); + callEndElement(fQName, synthesizedAugs()); + + String ename = modifyName("html", fNamesElems); + fQName.setValues(null, ename, ename, null); + callEndElement(fQName, synthesizedAugs()); + } + } + + // pop all remaining elements + else { + int length = fElementStack.top - fragmentContextStackSize_; + for (int i = 0; i < length; i++) { + Info info = fElementStack.pop(); + if (fReportErrors) { + String ename = info.qname.rawname; + fErrorReporter.reportWarning("HTML2001", new Object[] {ename}); + } + if (fDocumentHandler != null) { + callEndElement(info.qname, synthesizedAugs()); + } + } + } + + // call handler + if (fDocumentHandler != null) { + fDocumentHandler.endDocument(augs); + } + } + + /** + * Consume elements that have been buffered, like that are first consumed + * at the end of document + */ + private void consumeBufferedEndElements() { + List toConsume = endElementsBuffer_; + endElementsBuffer_.clear(); + for (ElementEntry entry : toConsume) { + forcedEndElement_ = true; + endElement(entry.name_, entry.augs_); + } + endElementsBuffer_.clear(); + } + + /** Comment. */ + public void comment(XMLString text, Augmentations augs) throws XNIException { + fSeenAnything = true; + consumeEarlyTextIfNeeded(); + if (fDocumentHandler != null) { + fDocumentHandler.comment(text, augs); + } + } + + private void consumeEarlyTextIfNeeded() { + if (!lostText_.isEmpty()) { + if (!fSeenBodyElement) { + forceStartBody(); + } + lostText_.refeed(this); + } + } + + /** Processing instruction. */ + public void processingInstruction(String target, XMLString data, Augmentations augs) throws XNIException { + fSeenAnything = true; + consumeEarlyTextIfNeeded(); + if (fDocumentHandler != null) { + fDocumentHandler.processingInstruction(target, data, augs); + } + } + + /** Start element. */ + public void startElement(QName elem, XMLAttributes attrs, Augmentations augs) throws XNIException { + fSeenAnything = true; + + boolean isForcedCreation = forcedStartElement_; + forcedStartElement_ = false; + + // check for end of document + if (fSeenRootElementEnd) { + notifyDiscardedStartElement(elem, attrs, augs); + return; + } + + // get element information + HTMLElements.Element element = getElement(elem); + short elementCode = element.code; + + // the creation of some elements like TABLE or SELECT can't be forced. Any others? + if (isForcedCreation && (elementCode == HTMLElements.TABLE || elementCode == HTMLElements.SELECT)) { + return; // don't accept creation + } + + // ignore multiple html, head, body elements + if (fSeenRootElement && elementCode == HTMLElements.HTML) { + notifyDiscardedStartElement(elem, attrs, augs); + return; + } + // accept only frame and frameset within frameset + if (fSeenFramesetElement && elementCode != HTMLElements.FRAME && elementCode != HTMLElements.FRAMESET && elementCode != HTMLElements.NOFRAMES) { + notifyDiscardedStartElement(elem, attrs, augs); + return; + } + + if (elementCode == HTMLElements.HEAD) { + if (fSeenHeadElement) { + notifyDiscardedStartElement(elem, attrs, augs); + return; + } + fSeenHeadElement = true; + } else if (elementCode == HTMLElements.FRAMESET) { + // create if none was present + if (!fSeenHeadElement) { + QName head = createQName("head"); + forceStartElement(head, null, synthesizedAugs()); + endElement(head, synthesizedAugs()); + } + consumeBufferedEndElements(); // (if any) has been buffered + fSeenFramesetElement = true; + } else if (elementCode == HTMLElements.BODY) { + // create if none was present + if (!fSeenHeadElement) { + QName head = createQName("head"); + forceStartElement(head, null, synthesizedAugs()); + endElement(head, synthesizedAugs()); + } + consumeBufferedEndElements(); // (if any) has been buffered + + if (fSeenBodyElement) { + notifyDiscardedStartElement(elem, attrs, augs); + return; + } + fSeenBodyElement = true; + } else if (elementCode == HTMLElements.FORM) { + if (fOpenedForm) { + notifyDiscardedStartElement(elem, attrs, augs); + return; + } + fOpenedForm = true; + } else if (elementCode == HTMLElements.UNKNOWN) { + consumeBufferedEndElements(); + } + + // check proper parent + if (element.parent != null) { + HTMLElements.Element preferedParent = element.parent[0]; + if (fDocumentFragment && (preferedParent.code == HTMLElements.HEAD || preferedParent.code == HTMLElements.BODY)) { + // nothing, don't force HEAD or BODY creation for a document fragment + } else if (!fSeenRootElement && !fDocumentFragment) { + String pname = preferedParent.name; + pname = modifyName(pname, fNamesElems); + if (fReportErrors) { + String ename = elem.rawname; + fErrorReporter.reportWarning("HTML2002", new Object[] {ename, pname}); + } + QName qname = new QName(null, pname, pname, null); + boolean parentCreated = forceStartElement(qname, null, synthesizedAugs()); + if (!parentCreated) { + if (!isForcedCreation) { + notifyDiscardedStartElement(elem, attrs, augs); + } + return; + } + } else { + if (preferedParent.code != HTMLElements.HEAD || (!fSeenBodyElement && !fDocumentFragment)) { + int depth = getParentDepth(element.parent, element.bounds); + if (depth == -1) { // no parent found + String pname = modifyName(preferedParent.name, fNamesElems); + QName qname = new QName(null, pname, pname, null); + if (fReportErrors) { + String ename = elem.rawname; + fErrorReporter.reportWarning("HTML2004", new Object[] {ename, pname}); + } + boolean parentCreated = forceStartElement(qname, null, synthesizedAugs()); + if (!parentCreated) { + if (!isForcedCreation) { + notifyDiscardedStartElement(elem, attrs, augs); + } + return; + } + } + } + } + } + + // if block element, save immediate parent inline elements + int depth = 0; + if (element.flags == 0) { + int length = fElementStack.top; + fInlineStack.top = 0; + for (int i = length - 1; i >= 0; i--) { + Info info = fElementStack.data[i]; + if (!info.element.isInline()) { + break; + } + fInlineStack.push(info); + endElement(info.qname, synthesizedAugs()); + } + depth = fInlineStack.top; + } + + // close previous elements + // all elements close a