diff --git a/.gitignore b/.gitignore index 4654a1b..77fff54 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ /javadoc.xml *.class /*.jardesc -*.jar *.war *.ear /local.properties @@ -17,5 +16,9 @@ *.iml *.iws -# Maven +# Gradle +**/.gradle /build/ +/buildSrc/build/ + +/CHANGES.txt diff --git a/LICENSE.txt b/LICENSE.txt index dfe0da1..f2dd46d 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005-2019, Carlos Amengual + * Copyright (c) 2005-2022, Carlos Amengual * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 464d7e2..9318a11 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,102 @@ # css4j - dom4j module -Subclasses several [dom4j](https://dom4j.github.io/) classes and provides CSS functionality to it. Licence is BSD 3-clause. +Subclasses several [dom4j](https://dom4j.github.io/) classes and provides CSS functionality to it. -Please refer to the `css4j-dist` repository for build instructions. +[License](LICENSE.txt) is BSD 3-clause. + +See the [latest Release Notes](RELEASE_NOTES.md). + +## Java™ Runtime Environment requirements +All the classes in the binary package have been compiled with a [Java compiler](https://adoptium.net/) +set to 1.7 compiler compliance level, except the `module-info.java` file. + +Building the library requires JDK 11 or higher. + +
+ +## Build from source +To build css4j-dom4j from the code that is currently at the Git repository, Java +11 or later is needed, although the resulting jar files can be run with a 1.7 JRE. + +You can run a variety of Gradle tasks with the Gradle wrapper (on Windows shells you can omit the `./`): + +- `./gradlew build` (normal build) +- `./gradlew build publishToMavenLocal` (to install in local Maven repository) +- `./gradlew copyJars` (to copy jar files into a top-level _jar_ directory) +- `./gradlew lineEndingConversion` (to convert line endings of top-level text files to CRLF) +- `./gradlew publish` (to deploy to a Maven repository, as described in the `publishing.repositories.maven` block of +[build.gradle](https://github.com/css4j/css4j-dom4j/blob/1-stable/build.gradle)) + +
+ +## Usage from a Gradle project +If your Gradle project depends on css4j-dom4j, you can use this project's own Maven repository in a `repositories` section of +your build file: +```groovy +repositories { + maven { + url "https://css4j.github.io/maven/" + mavenContent { + releasesOnly() + } + content { + includeGroup 'io.sf.carte' + includeGroup 'io.sf.jclf' + includeGroup 'xmlpull' + includeGroup 'xpp3' + } + } +} +``` +please use this repository **only** for the artifact groups listed in the `includeGroup` statements. + +Then, in your `build.gradle` file: +```groovy +dependencies { + api "io.sf.carte:css4j-dom4j:${css4jDom4jVersion}" +} +``` +where `css4jDom4jVersion` would be defined in a `gradle.properties` file. + +
+ +## Software dependencies + +In case that you do not use a Gradle or Maven build (which would manage the +dependencies according to the relevant `.module` or `.pom` files), the required +and optional library packages are the following: + +### Compile-time dependencies + +- The [css4j](https://github.com/css4j/css4j/releases) library (and its transitive + dependencies); version 1.3.1 or higher (but below 2.0) is recommended. + +- The [css4j-agent](https://github.com/css4j/css4j-agent/releases) library; + version 1.3.0 or higher (but below 2.0) is recommended. + **It is optional at runtime.** + +- The [dom4j](https://github.com/dom4j/dom4j) JAR package (tested with 2.1.1). + Requires at least version 2.0 to compile and 2.1.1 to run the tests, but you + should be able to run the resulting jar file with dom4j 1.6 if you are stuck with it. + +- The [XPP3 Pull Parser](https://github.com/xmlpull-xpp3/xmlpull-xpp3) (which + can be used with this library but beware that it does not support [character + entities](https://dev.w3.org/html5/html-author/charref)). + **It is optional at runtime.** + +### Test dependencies + +- A recent version of [JUnit 4](https://junit.org/junit4/). + +- The `batik-css`, `batik-util`and `batik-i18n` artifacts from + [Apache Batik](https://xmlgraphics.apache.org/batik/). + +- [Jaxen](https://github.com/jaxen-xpath/jaxen), this software was tested with + version 1.2.0. + +- [SLF4J](http://www.slf4j.org/), which is a logging package. + +
+ +## Website +For more information please visit https://css4j.github.io/ diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..41fa65a --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,27 @@ +# css4j-dom4j version 1.3.0 Release Notes + +### February 15, 2022 + +
+ +## Release Highlights + +### `:dir()` pseudo-class + +The `:dir()` pseudo-class is now partially supported in computed styles. + +Full support could not be implemented due to dom4j's limited DOM support. + +Users are advised to switch to css4j's native DOM if they need a full `:dir` +implementation. + +### Build + +The build system was switched from Maven to Gradle. + + +## Project Sites + +Project home: https://css4j.github.io/ + +Development site: https://github.com/css4j/css4j-dom4j diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..87854d7 --- /dev/null +++ b/build.gradle @@ -0,0 +1,288 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'org.gradlex.extra-java-module-info' version '1.2' +} + +group = 'io.sf.carte' +version = '1.3.1' + +description = 'css4j-dom4j' + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + withJavadocJar() + withSourcesJar() + registerFeature('xmlpull') { + usingSourceSet(sourceSets.main) + } + registerFeature('useragent') { + usingSourceSet(sourceSets.main) + } +} + +dependencies { + api('io.sf.carte:css4j') { + version { + strictly '[1.3.0,2.0[' + prefer '1.3.1' + } + } + useragentImplementation('io.sf.carte:css4j-agent') { + version { + strictly '[1.2.0,2.0[' + prefer '1.3.0' + } + } + /* + * Any dom4j version can be used, but tests may not pass with versions prior + * to 2.1.1, due to changes in dom4j's defaults. + */ + api('org.dom4j:dom4j') { + version { + require '[1.6.0,)' + prefer '2.1.4' + } + } + xmlpullImplementation 'xmlpull:xmlpull:1.2.0' + xmlpullImplementation 'xpp3:xpp3_min:1.2.0' + testImplementation(group: 'io.sf.carte', name: 'css4j', classifier: 'tests') { + version { + strictly '[1.3.0,2.0[' + prefer '1.3.1' + } + } + testImplementation 'org.apache.xmlgraphics:batik-css:1.15' + testImplementation 'org.apache.xmlgraphics:batik-util:1.15' + testImplementation 'org.apache.xmlgraphics:batik-i18n:1.15' + testImplementation 'jaxen:jaxen:1.2.0' + testImplementation('org.slf4j:slf4j-api') { + version { + require '[1.7.28,)' + prefer '2.0.6' + } + } + testImplementation 'junit:junit:4.13.2' +} + +extraJavaModuleInfo { + failOnMissingModuleInfo.set(false) + automaticModule('org.dom4j:dom4j', 'org.dom4j') + automaticModule('nu.validator:htmlparser', 'htmlparser') + automaticModule('org.w3c.css:sac', 'sac') +} + +repositories { + maven { + url = uri('https://repo.maven.apache.org/maven2/') + } + maven { + url "https://css4j.github.io/maven/" + mavenContent { + releasesOnly() + } + content { + includeGroup 'io.sf.carte' + includeGroup 'io.sf.jclf' + includeGroup 'xmlpull' + includeGroup 'xpp3' + } + } +} + +sourceSets { + main { + java { + srcDirs = ['src'] + includes += ["**/*.java"] + } + resources { + srcDirs = ['src'] + excludes += ["**/*.java"] + } + } + test { + java { + srcDirs = ['junit'] + includes += ["**/*.java"] + } + resources { + srcDirs = ['junit'] + excludes += ["**/*.java"] + } + } +} + +tasks.register('testsJar', Jar) { + archiveClassifier = 'tests' + from(sourceSets.test.output) +} + +configurations { + tests +} + +artifacts { + tests testsJar +} + +tasks.compileJava { + excludes += ['module-info.java'] + modularity.inferModulePath = false +} + +tasks.register('compileModuleInfo', JavaCompile) { + description = 'Compile module-info to Java 11 bytecode' + dependsOn tasks.compileJava + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + source = sourceSets.main.java + classpath = sourceSets.main.compileClasspath + destinationDirectory = sourceSets.main.java.destinationDirectory + modularity.inferModulePath = true + includes = ['module-info.java'] +} + +classes.dependsOn compileModuleInfo + +// Check bytecode version, in case some other task screws it +tasks.register('checkLegacyJava') { + description = 'Check that classes are Java 7 bytecode (except module-info)' + def classdir = sourceSets.main.output.classesDirs.files.stream().findAny().get() + def classfiles = fileTree(classdir).matching({it.exclude('module-info.class')}).files + doFirst() { + if (!classfiles.isEmpty()) { + def classfile = classfiles.stream().findAny().get() + if (classfile != null) { + def classbytes = classfile.bytes + def bcversion = classbytes[6] * 128 + classbytes[7] + if (bcversion != 51) { + throw new GradleException("Bytecode on " + classfile + + " is not valid Java 7. Version should be 51, instead is " + bcversion) + } + } + } + } +} + +classes.finalizedBy checkLegacyJava + +// Copy jar files to 'jar' directory +tasks.register('copyJars', Copy) { + description = 'Copy jar files to \'jar\' directory' + dependsOn tasks.build + dependsOn 'testsJar' + include '**/*.jar' + excludes = ['slf4j*.jar'] + from layout.buildDirectory.dir("libs") + from configurations.runtimeClasspath + into "${rootDir}/jar" +} + +tasks.register('lineEndingConversion', CRLFConvert) { + file "$rootDir/LICENSE.txt" + file "$rootDir/CHANGES.txt" + file "$rootDir/RELEASE_NOTES.md" +} + +tasks.register('cleanBuildSrc') { + description = 'Clean the buildSrc directory' + doLast { + delete("$rootDir/buildSrc/build") + } +} + +tasks.named('clean') { + finalizedBy('cleanBuildSrc') +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +tasks.withType(Javadoc) { + options.addStringOption('Xdoclint:none', '-quiet') + options.addStringOption('encoding', 'UTF-8') + options.addStringOption('charset', 'UTF-8') + options.overview = 'src/overview.html' + options.links 'https://docs.oracle.com/en/java/javase/11/docs/api/' + options.links 'https://css4j.github.io/api/1.0/' + options.links 'https://dom4j.github.io/javadoc/2.1.1/' +} + +tasks.withType(AbstractArchiveTask).configureEach { + // Reproducible build + preserveFileTimestamps = false + reproducibleFileOrder = true + // Copy license file + dependsOn lineEndingConversion + from ('LICENSE.txt') { + into 'META-INF' + } +} + +tasks.withType(PublishToMavenRepository) { task -> + doFirst { + if (repository == publishing.repositories.getByName('mavenRepo')) { + logger.lifecycle "Deploying artifacts to \"${it.repository.url}\"" + } + } +} + +publishing { + publications { + maven(MavenPublication) { + description = 'css4j DOM4J module' + from(components.java) + suppressAllPomMetadataWarnings() + artifact(testsJar) + pom { + description = 'css4j DOM4J module' + url = "https://github.com/css4j/css4j-dom4j/" + licenses { + license { + name = "BSD 3-clause license" + url = "https://css4j.github.io/LICENSE.txt" + } + } + } + } + } + repositories { + maven { + name = 'mavenRepo' + /* + * The following section applies to the 'publish' task: + * + * If you plan to deploy to a repository, please configure the + * 'mavenReleaseRepoUrl' and/or 'mavenSnapshotRepoUrl' properties + * (for example in GRADLE_USER_HOME/gradle.properties). + * + * Otherwise, Gradle shall create a 'build/repository' subdirectory + * at ${rootDir} and deploy there. + * + * Properties 'mavenRepoUsername' and 'mavenRepoPassword' can also + * be set (generally from command line). + */ + def releasesUrl + def snapshotsUrl + if (project.hasProperty('mavenReleaseRepoUrl') && project.mavenReleaseRepoUrl) { + releasesUrl = mavenReleaseRepoUrl + } else { + releasesUrl = "${buildDir}/repository/releases" + } + if (project.hasProperty('mavenSnapshotRepoUrl') && project.mavenSnapshotRepoUrl) { + snapshotsUrl = mavenSnapshotRepoUrl + } else { + snapshotsUrl = "${buildDir}/repository/snapshots" + } + url = version.endsWith('-SNAPSHOT') ? snapshotsUrl : releasesUrl + if (project.hasProperty('mavenRepoUsername') && + project.hasProperty('mavenRepoPassword')) { + credentials.username = mavenRepoUsername + credentials.password = mavenRepoPassword + } + } + } +} diff --git a/buildSrc/src/main/groovy/CRLFConvert.groovy b/buildSrc/src/main/groovy/CRLFConvert.groovy new file mode 100644 index 0000000..d6b941d --- /dev/null +++ b/buildSrc/src/main/groovy/CRLFConvert.groovy @@ -0,0 +1,43 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction + +/** + * Converts line endings to CRLF (Windows) + *

+ * Usage: + *

+ * + * tasks.register('lineEndingConversion', CRLFConvert) { + * file "path/to/file1.txt" + * file "path/to/fileN.txt" + * } + * + */ +class CRLFConvert extends DefaultTask { + + private static final String CRLF = "\r\n" + private static final String LF = "\n" + + private files = [] + + @TaskAction + def action() { + files.each { path -> + File file = new File(path) + if (file.exists()) { + String content = file.text + String newContent = content.replaceAll(/\r\n/, LF) + newContent = newContent.replaceAll(/\n|\r/, CRLF) + if (content != newContent) { + file.write(newContent) + } + } else { + logger.warn('File ' + path + ' does not exist.') + } + } + } + + def file(String path) { + this.files << path + } +} diff --git a/changes.sh b/changes.sh new file mode 100755 index 0000000..9562880 --- /dev/null +++ b/changes.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# +# This script writes the changelog in a human-readable format. +# +# You'll probably want to edit manually the result of executing the script. +# +if [[ $# -eq 0 ]] ; then + echo "No version supplied (e.g. '1.3.1')" + exit 1 +fi +OLDTAG=`git tag -l --merged 1-stable --sort=-taggerdate|head -1` +echo "Writing changes from tag $OLDTAG" +TITLE="CSS4J-DOM4J Module changes" +VERHDR="Version ${1}" +OUTFILE="CHANGES.txt" +echo -en "${TITLE}\\r\\n${TITLE//?/=}\\r\\n\\r\\n${VERHDR}\\r\\n${VERHDR//?/-}\\r\\n\\r\\n">${OUTFILE} +git log --reverse --pretty=format:%s ${OLDTAG}..|sed -e 's/^/- /'|fold -s|sed -r 's/^([^-])/ \1/'|sed -e 's/$/\r/'>>${OUTFILE} +echo -en "\\n">>${OUTFILE} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/junit/io/sf/carte/doc/dom4j/BaseURLElementTest.java b/junit/io/sf/carte/doc/dom4j/BaseURLElementTest.java index f8d1df3..2095ad5 100644 --- a/junit/io/sf/carte/doc/dom4j/BaseURLElementTest.java +++ b/junit/io/sf/carte/doc/dom4j/BaseURLElementTest.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -75,10 +75,10 @@ public void childAddedNode() throws Exception { Reader re = DOMCSSStyleSheetFactoryTest.sampleHTMLReader(); InputSource isrc = new InputSource(re); CSSDocument xhtmlDoc = XHTMLDocumentFactoryTest.parseXML(isrc); + re.close(); assertNotNull(xhtmlDoc.getBaseURL()); assertEquals("http://www.example.com/", xhtmlDoc.getBaseURL().toExternalForm()); - re.close(); } @Test diff --git a/junit/io/sf/carte/doc/dom4j/CSSStylableElementTest.java b/junit/io/sf/carte/doc/dom4j/CSSStylableElementTest.java index 2254766..e69ee32 100644 --- a/junit/io/sf/carte/doc/dom4j/CSSStylableElementTest.java +++ b/junit/io/sf/carte/doc/dom4j/CSSStylableElementTest.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -67,13 +67,14 @@ public void getStyle2() { assertNull(body.getStyle()); assertFalse(body.hasAttributes()); xhtmlDoc.getDocumentElement().appendChild(body); - body.setAttribute("style", "font-family: Arial"); + body.setAttribute("style", "font-family:Arial"); assertTrue(body.hasAttributes()); assertTrue(body.hasAttribute("style")); - assertEquals("font-family: Arial; ", body.getAttribute("style")); + assertEquals("font-family:Arial", body.getAttribute("style")); CSSStyleDeclaration style = body.getStyle(); assertNotNull(style); assertEquals(1, style.getLength()); + assertEquals("font-family: Arial; ", body.getAttribute("style")); assertEquals("font-family: Arial; ", style.getCssText()); style.setCssText("font-family: Helvetica"); assertEquals("font-family: Helvetica; ", style.getCssText()); diff --git a/junit/io/sf/carte/doc/dom4j/DOM4JCSSStyleDeclarationTest.java b/junit/io/sf/carte/doc/dom4j/DOM4JCSSStyleDeclarationTest.java index d651b76..c70c833 100644 --- a/junit/io/sf/carte/doc/dom4j/DOM4JCSSStyleDeclarationTest.java +++ b/junit/io/sf/carte/doc/dom4j/DOM4JCSSStyleDeclarationTest.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause diff --git a/junit/io/sf/carte/doc/dom4j/DOM4JCSSStyleSheetTest.java b/junit/io/sf/carte/doc/dom4j/DOM4JCSSStyleSheetTest.java index f5b99ea..fcc83d0 100644 --- a/junit/io/sf/carte/doc/dom4j/DOM4JCSSStyleSheetTest.java +++ b/junit/io/sf/carte/doc/dom4j/DOM4JCSSStyleSheetTest.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause diff --git a/junit/io/sf/carte/doc/dom4j/DOM4JSelectorMatcherTest.java b/junit/io/sf/carte/doc/dom4j/DOM4JSelectorMatcherTest.java index ca7b1cc..60ec4a9 100644 --- a/junit/io/sf/carte/doc/dom4j/DOM4JSelectorMatcherTest.java +++ b/junit/io/sf/carte/doc/dom4j/DOM4JSelectorMatcherTest.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -11,6 +11,7 @@ package io.sf.carte.doc.dom4j; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.IOException; @@ -19,6 +20,7 @@ import java.util.LinkedList; import java.util.List; +import org.dom4j.DocumentType; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -157,6 +159,42 @@ public void testMatchSelector1Class() throws Exception { document.getDocumentElement().add(elm); SelectorMatcher matcher = elm.getSelectorMatcher(); assertTrue(matcher.matches(selist) >= 0); + // Case insensitive + elm.setAttribute("class", "exampleClass"); + assertEquals(0, matcher.matches(selist)); + } + + @Test + public void testMatchSelector1ClassCI() throws Exception { + AbstractCSSStyleSheet css = parseStyle(".exampleClass {color: blue;}"); + CSSStyleDeclarationRule rule = (CSSStyleDeclarationRule) css.getCssRules().item(0); + SelectorList selist = CSSOMBridge.getSelectorList(rule); + CSSStylableElement elm = createElement("p"); + elm.addAttribute("class", "exampleclass"); + document.getDocumentElement().add(elm); + SelectorMatcher matcher = elm.getSelectorMatcher(); + assertTrue(matcher.matches(selist) >= 0); + // Case insensitive + elm.setAttribute("class", "exampleClass"); + assertEquals(0, matcher.matches(selist)); + } + + @Test + public void testMatchSelector1ClassStrict() throws Exception { + DocumentType docType = factory.createDocType("html", null, null); + document.setDocType(docType); + AbstractCSSStyleSheet css = parseStyle(".exampleclass {color: blue;}"); + CSSStyleDeclarationRule rule = (CSSStyleDeclarationRule) css.getCssRules().item(0); + SelectorList selist = CSSOMBridge.getSelectorList(rule); + CSSStylableElement elm = createElement("p"); + elm.addAttribute("class", "exampleclass"); + document.getDocumentElement().add(elm); + SelectorMatcher matcher = elm.getSelectorMatcher(); + assertTrue(matcher.matches(selist) >= 0); + // Case sensitive + elm.setAttribute("class", "exampleClass"); + matcher = elm.getSelectorMatcher(); + assertEquals(-1, matcher.matches(selist)); } @Test @@ -287,6 +325,44 @@ public void testMatchSelector1Id() throws Exception { document.getDocumentElement().add(elm); SelectorMatcher matcher = elm.getSelectorMatcher(); assertTrue(matcher.matches(selist) >= 0); + // Case insensitive + elm.addAttribute("id", "exampleId"); + assertTrue(matcher.matches(selist) >= 0); + } + + @Test + public void testMatchSelector1IdCI() throws Exception { + AbstractCSSStyleSheet css = parseStyle("#exampleId {color: blue;}"); + CSSStyleDeclarationRule rule = (CSSStyleDeclarationRule) css.getCssRules().item(0); + SelectorList selist = CSSOMBridge.getSelectorList(rule); + CSSStylableElement elm = createElement("p"); + elm.addAttribute("class", "exampleclass"); + elm.addAttribute("id", "exampleid"); + document.getDocumentElement().add(elm); + SelectorMatcher matcher = elm.getSelectorMatcher(); + assertTrue(matcher.matches(selist) >= 0); + // Case insensitive + elm.addAttribute("id", "exampleId"); + assertTrue(matcher.matches(selist) >= 0); + } + + @Test + public void testMatchSelector1IdCIStrict() throws Exception { + DocumentType docType = factory.createDocType("html", null, null); + document.setDocType(docType); + AbstractCSSStyleSheet css = parseStyle("#exampleId {color: blue;}"); + CSSStyleDeclarationRule rule = (CSSStyleDeclarationRule) css.getCssRules().item(0); + SelectorList selist = CSSOMBridge.getSelectorList(rule); + CSSStylableElement elm = createElement("p"); + elm.addAttribute("class", "exampleclass"); + elm.addAttribute("id", "exampleid"); + document.getDocumentElement().add(elm); + SelectorMatcher matcher = elm.getSelectorMatcher(); + assertEquals(-1, matcher.matches(selist)); + // Case insensitive + elm.addAttribute("id", "exampleId"); + matcher = elm.getSelectorMatcher(); + assertEquals(0, matcher.matches(selist)); } @Test @@ -486,6 +562,50 @@ public void testMatchSelectorPseudoOfType() throws Exception { assertTrue(matcher.matches(selist) < 0); } + @Test + public void testMatchSelectorPseudoAnyLink() throws Exception { + CSSStylableElement a = createElement("a"); + a.setAttribute("href", "foo"); + CSSStylableElement elm = document.getDocumentElement(); + elm.appendChild(a); + SelectorMatcher matcher = a.getSelectorMatcher(); + AbstractCSSStyleSheet css = parseStyle(":any-link {color: blue;}"); + CSSStyleDeclarationRule rule = (CSSStyleDeclarationRule) css.getCssRules().item(0); + SelectorList selist = CSSOMBridge.getSelectorList(rule); + assertEquals(0, matcher.matches(selist)); + // + matcher = elm.getSelectorMatcher(); + assertEquals(-1, matcher.matches(selist)); + elm.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "bar"); + assertEquals(0, matcher.matches(selist)); + // + a.removeAttribute("href"); + matcher = a.getSelectorMatcher(); + assertEquals(-1, matcher.matches(selist)); + } + + @Test + public void testMatchSelectorPseudoLink() throws Exception { + CSSStylableElement a = createElement("a"); + a.setAttribute("href", "foo"); + CSSStylableElement elm = document.getDocumentElement(); + elm.appendChild(a); + SelectorMatcher matcher = a.getSelectorMatcher(); + AbstractCSSStyleSheet css = parseStyle(":link {color: blue;}"); + CSSStyleDeclarationRule rule = (CSSStyleDeclarationRule) css.getCssRules().item(0); + SelectorList selist = CSSOMBridge.getSelectorList(rule); + assertEquals(0, matcher.matches(selist)); + // + matcher = elm.getSelectorMatcher(); + assertEquals(-1, matcher.matches(selist)); + elm.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "bar"); + assertEquals(0, matcher.matches(selist)); + // + a.removeAttribute("href"); + matcher = a.getSelectorMatcher(); + assertEquals(-1, matcher.matches(selist)); + } + @Test public void testMatchSelectorPseudoRoot() throws Exception { CSSStylableElement root = document.getDocumentElement(); diff --git a/junit/io/sf/carte/doc/dom4j/DOM4JUserAgentTest.java b/junit/io/sf/carte/doc/dom4j/DOM4JUserAgentTest.java index d6b9f22..bdc4e42 100644 --- a/junit/io/sf/carte/doc/dom4j/DOM4JUserAgentTest.java +++ b/junit/io/sf/carte/doc/dom4j/DOM4JUserAgentTest.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause diff --git a/junit/io/sf/carte/doc/dom4j/DirMatcherTest.java b/junit/io/sf/carte/doc/dom4j/DirMatcherTest.java new file mode 100644 index 0000000..12dbae8 --- /dev/null +++ b/junit/io/sf/carte/doc/dom4j/DirMatcherTest.java @@ -0,0 +1,110 @@ +/* + + Copyright (c) 2005-2022, Carlos Amengual. + + SPDX-License-Identifier: BSD-3-Clause + + Licensed under a BSD-style License. You can find the license here: + https://css4j.github.io/LICENSE.txt + + */ + +package io.sf.carte.doc.dom4j; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; + +import org.dom4j.DocumentException; +import org.junit.Before; +import org.junit.Test; +import org.w3c.css.sac.SelectorList; + +import io.sf.carte.doc.style.css.om.DOMCSSStyleSheetFactoryTest; +import io.sf.carte.doc.style.css.parser.CSSParser; + +public class DirMatcherTest { + + XHTMLDocument document; + + @Before + public void setUp() throws IOException, DocumentException { + document = TestDocumentFactory.loadDocument(DOMCSSStyleSheetFactoryTest.directionalityHTMLReader()); + } + + @Test + public void testMatchDirectionality() { + // Prepare selectors + CSSParser parser = new CSSParser(); + SelectorList ltr = parser.parseSelectors(":dir(ltr)"); + SelectorList rtl = parser.parseSelectors(":dir(rtl)"); + // + CSSStylableElement elm = document.getElementById("head"); + assertTrue(elm.matches(rtl, null)); + assertFalse(elm.matches(ltr, null)); + elm = document.getElementById("h1"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + elm = document.getElementById("para1"); + assertTrue(elm.matches(rtl, null)); + assertFalse(elm.matches(ltr, null)); + elm = document.getElementById("para2"); + assertTrue(elm.matches(rtl, null)); + assertFalse(elm.matches(ltr, null)); + elm = document.getElementById("h2"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + elm = document.getElementById("tableid"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + elm = document.getElementById("cell12"); + assertTrue(elm.matches(rtl, null)); + assertFalse(elm.matches(ltr, null)); + elm = document.getElementById("tr1"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + elm = document.getElementById("form1"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + elm = document.getElementById("username"); + assertTrue(elm.matches(rtl, null)); + assertFalse(elm.matches(ltr, null)); + elm = document.getElementById("phonelabel"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + elm = document.getElementById("telephone"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + /* FAIL + elm = document.getElementById("textareartl"); + assertTrue(elm.matches(rtl, null)); + assertFalse(elm.matches(ltr, null)); + elm = document.getElementById("textarealtr"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + elm = document.getElementById("textareaempty"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + */ + elm = document.getElementById("bdirtl"); + assertTrue(elm.matches(rtl, null)); + assertFalse(elm.matches(ltr, null)); + elm = document.getElementById("bdiltr"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + elm = document.getElementById("bdiauto"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + elm = document.getElementById("bdiempty"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + elm = document.getElementById("bdiautoempty"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + elm = document.getElementById("bdibadempty"); + assertFalse(elm.matches(rtl, null)); + assertTrue(elm.matches(ltr, null)); + } + +} diff --git a/junit/io/sf/carte/doc/dom4j/LinkElementTest.java b/junit/io/sf/carte/doc/dom4j/LinkElementTest.java index 12daea5..58f54c2 100644 --- a/junit/io/sf/carte/doc/dom4j/LinkElementTest.java +++ b/junit/io/sf/carte/doc/dom4j/LinkElementTest.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -17,11 +17,15 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import java.net.MalformedURLException; + import org.dom4j.Namespace; import org.dom4j.QName; import org.junit.Before; import org.junit.Test; +import io.sf.carte.doc.style.css.om.AbstractCSSStyleSheet; + public class LinkElementTest { XHTMLDocument xDoc = null; @@ -30,12 +34,10 @@ public class LinkElementTest { QName link_qname = null; - LinkElement linkElement = null; - @Before public void setUp() { XHTMLDocumentFactory factory = new TestDocumentFactory(); - headElement = (HeadElement) factory.createElement("head"); + headElement = (HeadElement) factory.createElement("head", XHTMLDocument.XHTML_NAMESPACE_URI); xDoc = factory.createDocument(headElement); link_qname = new QName("link", new Namespace("", XHTMLDocument.XHTML_NAMESPACE_URI)); link_qname.setDocumentFactory(factory); @@ -44,7 +46,7 @@ public void setUp() { @Test public void addRemove() { assertEquals(0, xDoc.linkedStyle.size()); - linkElement = (LinkElement) headElement.addElement(link_qname); + LinkElement linkElement = (LinkElement) headElement.addElement(link_qname); assertEquals(1, xDoc.linkedStyle.size()); headElement.remove(linkElement); assertEquals(0, xDoc.linkedStyle.size()); @@ -52,7 +54,7 @@ public void addRemove() { @Test public void addRemoveAttribute() { - linkElement = (LinkElement) headElement.addElement(link_qname); + LinkElement linkElement = (LinkElement) headElement.addElement(link_qname); xDoc.getStyleSheet(); int iniSerial = xDoc.getStyleCacheSerial(); linkElement.addAttribute("href", "http://www.example.com/css/example.css"); @@ -74,7 +76,7 @@ public void addRemoveAttribute() { @Test public void attributeSetValue() { - linkElement = (LinkElement) headElement.addElement(link_qname); + LinkElement linkElement = (LinkElement) headElement.addElement(link_qname); linkElement.addAttribute("rel", "stylesheet"); linkElement.addAttribute("href", "http://www.example.com/css/example.css"); xDoc.getStyleSheet(); @@ -85,4 +87,47 @@ public void attributeSetValue() { assertEquals(iniSerial, xDoc.getStyleCacheSerial()); } + @Test + public void getSheet() throws MalformedURLException { + LinkElement linkElement = (LinkElement) headElement.addElement(link_qname); + linkElement.addAttribute("rel", "stylesheet"); + linkElement.addAttribute("href", "http://www.example.com/css/common.css"); + xDoc.getStyleSheet(); + assertFalse(xDoc.getErrorHandler().hasErrors()); + AbstractCSSStyleSheet sheet = linkElement.getSheet(); + assertNotNull(sheet); + assertEquals(3, sheet.getCssRules().getLength()); + // + int iniSerial = xDoc.getStyleCacheSerial(); + linkElement.attribute("href").setValue("jar:http://www.example.com/evil.jar!/file"); + sheet = linkElement.getSheet(); + assertNotNull(sheet); + assertEquals(0, sheet.getCssRules().getLength()); + assertTrue(xDoc.getErrorHandler().hasErrors()); + assertTrue(xDoc.getErrorHandler().hasPolicyErrors()); + iniSerial++; + assertEquals(iniSerial, xDoc.getStyleCacheSerial()); + // Setting BASE does not change things + xDoc.getErrorHandler().reset(); + CSSStylableElement base = xDoc.getDocumentFactory().createElement("base"); + base.setAttribute("href", "jar:http://www.example.com/evil.jar!/dir/file"); + headElement.appendChild(base); + sheet = linkElement.getSheet(); + assertNotNull(sheet); + assertEquals(0, sheet.getCssRules().getLength()); + assertTrue(xDoc.getErrorHandler().hasErrors()); + assertTrue(xDoc.getErrorHandler().hasPolicyErrors()); + // Set document URI to enable policy enforcement + xDoc.setDocumentURI("http://www.example.com/example.html"); + // + xDoc.getErrorHandler().reset(); + linkElement.attribute("href").setValue("file:/dev/zero"); + sheet = linkElement.getSheet(); + assertNotNull(sheet); + assertEquals(0, sheet.getCssRules().getLength()); + assertTrue(xDoc.getErrorHandler().hasErrors()); + assertTrue(xDoc.getErrorHandler().hasPolicyErrors()); + assertEquals(iniSerial, xDoc.getStyleCacheSerial()); + } + } diff --git a/junit/io/sf/carte/doc/dom4j/PermissiveErrorHandler.java b/junit/io/sf/carte/doc/dom4j/PermissiveErrorHandler.java new file mode 100644 index 0000000..06d8a1e --- /dev/null +++ b/junit/io/sf/carte/doc/dom4j/PermissiveErrorHandler.java @@ -0,0 +1,37 @@ +/* + + Copyright (c) 2020-2022, Carlos Amengual. + + SPDX-License-Identifier: BSD-3-Clause + + Licensed under a BSD-style License. You can find the license here: + https://css4j.github.io/LICENSE.txt + + */ + +package io.sf.carte.doc.dom4j; + +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +class PermissiveErrorHandler implements ErrorHandler { + + public PermissiveErrorHandler() { + super(); + } + + @Override + public void warning(SAXParseException exception) throws SAXException { + } + + @Override + public void error(SAXParseException exception) throws SAXException { + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + throw exception; + } + +} diff --git a/junit/io/sf/carte/doc/dom4j/PseudoClassTest.java b/junit/io/sf/carte/doc/dom4j/PseudoClassTest.java new file mode 100644 index 0000000..59bbdf0 --- /dev/null +++ b/junit/io/sf/carte/doc/dom4j/PseudoClassTest.java @@ -0,0 +1,145 @@ +/* + + Copyright (c) 2005-2022, Carlos Amengual. + + SPDX-License-Identifier: BSD-3-Clause + + Licensed under a BSD-style License. You can find the license here: + https://css4j.github.io/LICENSE.txt + + */ + +package io.sf.carte.doc.dom4j; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +import java.io.IOException; +import java.io.StringReader; + +import org.dom4j.DocumentException; +import org.dom4j.io.SAXReader; +import org.junit.BeforeClass; +import org.junit.Test; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import io.sf.carte.doc.style.css.CSSComputedProperties; +import io.sf.carte.doc.style.css.CSSDocument; +import io.sf.carte.doc.style.css.CSSElement; +import io.sf.carte.doc.xml.dtd.DefaultEntityResolver; + +public class PseudoClassTest { + + private static XHTMLDocument htmlDoc; + + @BeforeClass + public static void setUpBeforeClass() throws DocumentException, SAXException, IOException { + TestDocumentFactory factory = new TestDocumentFactory(); + factory.getDefaultStyleSheet(CSSDocument.ComplianceMode.STRICT).getCssRules().clear(); + SAXReader reader = new SAXReader(factory); + reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", true); + reader.setEntityResolver(new DefaultEntityResolver()); + String str = "
Test.
..
. .
..
..
"; + InputSource source = new InputSource(new StringReader(str)); + htmlDoc = (XHTMLDocument) reader.read(source); + htmlDoc.setDocumentURI("http://www.example.com/xhtml/pseudoclass.html"); + } + + public void setUp() { + htmlDoc.getErrorHandler().reset(); + } + + @Test + public void getElementgetStyle1() { + CSSElement elm = htmlDoc.getElementById("tr1"); + assertNotNull(elm); + CSSComputedProperties styledecl = elm.getComputedStyle(null); + assertEquals(3, styledecl.getLength()); + assertEquals("#faf", styledecl.getPropertyValue("color")); + assertEquals("#002a55", styledecl.getPropertyValue("background-color")); + assertEquals("bold", styledecl.getPropertyValue("font-weight")); + + assertEquals("background-color:#002a55;color:#faf;font-weight:bold;", + styledecl.getMinifiedCssText()); + + assertFalse(htmlDoc.getErrorHandler().hasComputedStyleErrors(elm)); + assertFalse(htmlDoc.getErrorHandler().hasComputedStyleErrors()); + assertFalse(htmlDoc.getErrorHandler().hasErrors()); + assertFalse(htmlDoc.getErrorHandler().hasIOErrors()); + } + + @Test + public void getElementgetStyle2() { + CSSElement elm = htmlDoc.getElementById("tr2"); + assertNotNull(elm); + CSSComputedProperties styledecl = elm.getComputedStyle(null); + assertEquals(2, styledecl.getLength()); + assertEquals("#100", styledecl.getPropertyValue("color")); + assertEquals("#fbf", styledecl.getPropertyValue("background-color")); + assertEquals("normal", styledecl.getPropertyValue("font-weight")); + + assertEquals("background-color:#fbf;color:#100;", styledecl.getMinifiedCssText()); + + assertFalse(htmlDoc.getErrorHandler().hasComputedStyleErrors(elm)); + assertFalse(htmlDoc.getErrorHandler().hasComputedStyleErrors()); + assertFalse(htmlDoc.getErrorHandler().hasErrors()); + assertFalse(htmlDoc.getErrorHandler().hasIOErrors()); + } + + @Test + public void getElementgetStyle3() { + CSSElement elm = htmlDoc.getElementById("tr3"); + assertNotNull(elm); + CSSComputedProperties styledecl = elm.getComputedStyle(null); + assertEquals(2, styledecl.getLength()); + assertEquals("#001", styledecl.getPropertyValue("color")); + assertEquals("#f5f5f5", styledecl.getPropertyValue("background-color")); + assertEquals("normal", styledecl.getPropertyValue("font-weight")); + + assertEquals("background-color:#f5f5f5;color:#001;", styledecl.getMinifiedCssText()); + + assertFalse(htmlDoc.getErrorHandler().hasComputedStyleErrors(elm)); + assertFalse(htmlDoc.getErrorHandler().hasComputedStyleErrors()); + assertFalse(htmlDoc.getErrorHandler().hasErrors()); + assertFalse(htmlDoc.getErrorHandler().hasIOErrors()); + } + + @Test + public void getElementgetStyle4() { + CSSElement elm = htmlDoc.getElementById("tr4"); + assertNotNull(elm); + CSSComputedProperties styledecl = elm.getComputedStyle(null); + assertEquals(2, styledecl.getLength()); + assertEquals("#100", styledecl.getPropertyValue("color")); + assertEquals("#fbf", styledecl.getPropertyValue("background-color")); + assertEquals("normal", styledecl.getPropertyValue("font-weight")); + + assertEquals("background-color:#fbf;color:#100;", styledecl.getMinifiedCssText()); + + assertFalse(htmlDoc.getErrorHandler().hasComputedStyleErrors(elm)); + assertFalse(htmlDoc.getErrorHandler().hasComputedStyleErrors()); + assertFalse(htmlDoc.getErrorHandler().hasErrors()); + assertFalse(htmlDoc.getErrorHandler().hasIOErrors()); + } + + @Test + public void getElementgetStyle5() { + CSSElement elm = htmlDoc.getElementById("tr5"); + assertNotNull(elm); + CSSComputedProperties styledecl = elm.getComputedStyle(null); + assertEquals(2, styledecl.getLength()); + assertEquals("#001", styledecl.getPropertyValue("color")); + assertEquals("#f5f5f5", styledecl.getPropertyValue("background-color")); + assertEquals("normal", styledecl.getPropertyValue("font-weight")); + + assertEquals("background-color:#f5f5f5;color:#001;", styledecl.getMinifiedCssText()); + + assertFalse(htmlDoc.getErrorHandler().hasComputedStyleErrors(elm)); + assertFalse(htmlDoc.getErrorHandler().hasComputedStyleErrors()); + assertFalse(htmlDoc.getErrorHandler().hasErrors()); + assertFalse(htmlDoc.getErrorHandler().hasIOErrors()); + } + +} diff --git a/junit/io/sf/carte/doc/dom4j/StyleElementTest.java b/junit/io/sf/carte/doc/dom4j/StyleElementTest.java index 9a835d8..46edc60 100644 --- a/junit/io/sf/carte/doc/dom4j/StyleElementTest.java +++ b/junit/io/sf/carte/doc/dom4j/StyleElementTest.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -12,12 +12,17 @@ package io.sf.carte.doc.dom4j; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import org.dom4j.Namespace; import org.dom4j.QName; import org.junit.Before; import org.junit.Test; +import io.sf.carte.doc.style.css.StyleFormattingFactory; +import io.sf.carte.doc.style.css.om.TestStyleFormattingFactory; + public class StyleElementTest { XHTMLDocument xDoc = null; @@ -31,7 +36,7 @@ public class StyleElementTest { @Before public void setUp() { XHTMLDocumentFactory factory = XHTMLDocumentFactory.getInstance(); - headElement = (HeadElement) factory.createElement("head"); + headElement = (HeadElement) factory.createElement("head", XHTMLDocument.XHTML_NAMESPACE_URI); xDoc = factory.createDocument(headElement); style_qname = new QName("style", new Namespace("", XHTMLDocument.XHTML_NAMESPACE_URI)); @@ -43,6 +48,7 @@ public void addRemove() { assertEquals(0, xDoc.embeddedStyle.size()); styleElement = (StyleElement) headElement.addElement(style_qname); assertEquals(1, xDoc.embeddedStyle.size()); + styleElement.setAttribute("type", "foo/bar"); assertEquals(0, xDoc.getStyleSheets().getLength()); styleElement.setAttribute("type", "text/css"); assertEquals(1, xDoc.getStyleSheets().getLength()); @@ -63,6 +69,50 @@ public void change() { assertEquals(iniSerial, xDoc.getStyleCacheSerial()); } + @Test + public void getText() { + styleElement = (StyleElement) headElement.addElement(style_qname); + styleElement.setAttribute("type", "text/css"); + styleElement.setText("p {font-size: large; font-style: italic;}"); + assertEquals("p {font-size: large; font-style: italic;}", styleElement.getText()); + // + styleElement.normalize(); + assertEquals("p {font-size: large; font-style: italic;}", styleElement.getText()); + // + xDoc.getStyleSheet(); + StyleFormattingFactory formattingf = new TestStyleFormattingFactory(); + xDoc.getDocumentFactory().getStyleSheetFactory().setStyleFormattingFactory(formattingf); + styleElement.normalize(); + assertEquals("p {font-size: large; font-style: italic; }", styleElement.getText()); + // Empty type + styleElement.setAttribute("type", ""); + assertNotNull(styleElement.getSheet()); + styleElement.normalize(); + assertEquals("p {font-size: large; font-style: italic; }", styleElement.getText()); + // + styleElement.setText("not style-related element"); + assertEquals("not style-related element", styleElement.getText()); + styleElement.normalize(); + assertEquals("not style-related element", styleElement.getText()); + // Remove attribute + styleElement.removeAttribute("type"); + styleElement.normalize(); + assertEquals("not style-related element", styleElement.getText()); + // Other type + styleElement.setAttribute("type", "text/xsl"); + styleElement.setText( + "" + + "bar" + + ""); + assertNull(styleElement.getSheet()); + styleElement.normalize(); + assertEquals( + "" + + "bar" + + "", + styleElement.getText()); + } + @Test public void getNamespaceURI() { styleElement = (StyleElement) headElement.addElement(style_qname); diff --git a/junit/io/sf/carte/doc/dom4j/TestDocumentFactory.java b/junit/io/sf/carte/doc/dom4j/TestDocumentFactory.java index 01c512c..a4ba6ac 100644 --- a/junit/io/sf/carte/doc/dom4j/TestDocumentFactory.java +++ b/junit/io/sf/carte/doc/dom4j/TestDocumentFactory.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -12,16 +12,22 @@ package io.sf.carte.doc.dom4j; import java.io.IOException; +import java.io.Reader; import java.net.URL; import java.net.URLConnection; +import org.dom4j.DocumentException; import org.dom4j.dom.DOMDocumentType; import org.dom4j.dom.DOMElement; +import org.dom4j.io.SAXReader; +import org.xml.sax.ErrorHandler; import io.sf.carte.doc.agent.MockURLConnectionFactory; import io.sf.carte.doc.style.css.StyleDatabase; import io.sf.carte.doc.style.css.om.DummyDeviceFactory; import io.sf.carte.doc.style.css.om.TestStyleDatabase; +import nu.validator.htmlparser.common.XmlViolationPolicy; +import nu.validator.htmlparser.sax.HtmlParser; /** * A document factory for test purposes. @@ -53,6 +59,13 @@ public XHTMLDocument createDocument() { return mydoc; } + @Override + protected XHTMLDocument createDocument(DOMDocumentType documentType) { + XHTMLDocument document = new MyXHTMLDocument(documentType); + document.setDocumentFactory(this); + return document; + } + class MyXHTMLDocument extends XHTMLDocument { private static final long serialVersionUID = 4L; @@ -99,4 +112,17 @@ public StyleDatabase getStyleDatabase(String targetMedium) { } } + public static XHTMLDocument loadDocument(Reader re) throws DocumentException, IOException { + HtmlParser parser = new HtmlParser(XmlViolationPolicy.ALTER_INFOSET); + parser.setCommentPolicy(XmlViolationPolicy.ALLOW); + parser.setXmlnsPolicy(XmlViolationPolicy.ALLOW); + TestDocumentFactory factory = new TestDocumentFactory(); + factory.getStyleSheetFactory().setDefaultHTMLUserAgentSheet(); + SAXReader reader = new SAXReader(factory); + reader.setXMLReader(parser); + ErrorHandler errorHandler = new PermissiveErrorHandler(); + reader.setErrorHandler(errorHandler); + return (XHTMLDocument) reader.read(re); + } + } diff --git a/junit/io/sf/carte/doc/dom4j/XHTMLDocumentFactoryTest.java b/junit/io/sf/carte/doc/dom4j/XHTMLDocumentFactoryTest.java index 8d3efa3..67a7c40 100644 --- a/junit/io/sf/carte/doc/dom4j/XHTMLDocumentFactoryTest.java +++ b/junit/io/sf/carte/doc/dom4j/XHTMLDocumentFactoryTest.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause diff --git a/junit/io/sf/carte/doc/dom4j/XHTMLDocumentTest.java b/junit/io/sf/carte/doc/dom4j/XHTMLDocumentTest.java index 061ba7b..33c0130 100644 --- a/junit/io/sf/carte/doc/dom4j/XHTMLDocumentTest.java +++ b/junit/io/sf/carte/doc/dom4j/XHTMLDocumentTest.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -50,6 +50,7 @@ import io.sf.carte.doc.style.css.om.MediaRule; public class XHTMLDocumentTest { + XHTMLDocument xhtmlDoc; @Before @@ -96,7 +97,7 @@ public void getStyleSheet() throws Exception { assertEquals(3, sheet.getCssRules().getLength()); assertEquals("background-color: red;\n", ((CSSStyleRule) sheet.getCssRules().item(0)).getStyle().getCssText()); AbstractCSSStyleDeclaration fontface = ((BaseCSSDeclarationRule) sheet.getCssRules().item(1)).getStyle(); - assertEquals("url('http://www.example.com/css/font/MechanicalBd.otf')", fontface.getPropertyValue("src")); + assertEquals("url('http://www.example.com/fonts/OpenSans-Regular.ttf')", fontface.getPropertyValue("src")); CSSValue ffval = fontface.getPropertyCSSValue("src"); assertEquals(CSSValue.CSS_PRIMITIVE_VALUE, ffval.getCssValueType()); assertEquals(CSSPrimitiveValue.CSS_URI, ((CSSPrimitiveValue) ffval).getPrimitiveType()); @@ -205,7 +206,7 @@ public void getElementgetStyle() { assertFalse(xhtmlDoc.getErrorHandler().hasComputedStyleErrors(elm)); assertFalse(xhtmlDoc.getErrorHandler().hasComputedStyleErrors()); assertFalse(xhtmlDoc.getErrorHandler().hasErrors()); - assertTrue(xhtmlDoc.getErrorHandler().hasIOErrors()); + assertFalse(xhtmlDoc.getErrorHandler().hasIOErrors()); xhtmlDoc.getErrorHandler().reset(); // Check for non-existing property assertNull(styledecl.getPropertyCSSValue("does-not-exist")); @@ -833,11 +834,62 @@ public void testLinkElement() { } @Test - public void testBaseElement() { + public void testBaseElement() throws MalformedURLException { assertEquals("http://www.example.com/", xhtmlDoc.getBaseURI()); CSSElement base = (CSSElement) xhtmlDoc.getElementsByTagName("base").item(0); base.setAttribute("href", "http://www.example.com/newbase/"); assertEquals("http://www.example.com/newbase/", xhtmlDoc.getBaseURI()); + // Wrong URL + base.setAttribute("href", "http//"); + assertNull(xhtmlDoc.getBaseURI()); + assertEquals("http//", base.getAttribute("href")); + assertTrue(xhtmlDoc.getErrorHandler().hasIOErrors()); + // Relative URL + base.setAttribute("href", "foo"); + assertEquals("foo", base.getAttribute("href")); + assertNull(xhtmlDoc.getBaseURI()); + // Remove attribute + base.removeAttribute("href"); + assertNull(xhtmlDoc.getBaseURI()); + // Unsafe base assignment + xhtmlDoc.setDocumentURI("http://www.example.com/document.html"); + assertEquals("http://www.example.com/document.html", xhtmlDoc.getDocumentURI()); + base.setAttribute("href", "jar:http://www.example.com/evil.jar!/file"); + assertTrue(xhtmlDoc.getErrorHandler().hasPolicyErrors()); + assertEquals("http://www.example.com/document.html", xhtmlDoc.getBaseURI()); + assertEquals("jar:http://www.example.com/evil.jar!/file", base.getAttribute("href")); + } + + @Test + public void testBaseElement2() throws MalformedURLException { + assertEquals("http://www.example.com/", xhtmlDoc.getBaseURI()); + // Set documentURI and then unsafe attribute + xhtmlDoc.setDocumentURI("http://www.example.com/doc-uri"); + CSSElement base = (CSSElement) xhtmlDoc.getElementsByTagName("base").item(0); + // Relative URL + base.setAttribute("href", "foo"); + assertEquals("foo", base.getAttribute("href")); + assertEquals("http://www.example.com/foo", xhtmlDoc.getBaseURI()); + // Wrong URL + base.setAttribute("href", "foo://"); + assertEquals("foo://", base.getAttribute("href")); + assertEquals("http://www.example.com/doc-uri", xhtmlDoc.getBaseURI()); + assertTrue(xhtmlDoc.getErrorHandler().hasIOErrors()); + // Remove attribute + base.removeAttribute("href"); + assertEquals("http://www.example.com/doc-uri", xhtmlDoc.getBaseURI()); + // Unsafe base assignment + base.setAttribute("href", "jar:http://www.example.com/evil.jar!/file"); + assertEquals("http://www.example.com/doc-uri", xhtmlDoc.getBaseURI()); + assertEquals("jar:http://www.example.com/evil.jar!/file", base.getAttribute("href")); + assertTrue(xhtmlDoc.getErrorHandler().hasPolicyErrors()); + } + + @Test + public void testSetBaseURL() throws MalformedURLException { + assertEquals("http://www.example.com/", xhtmlDoc.getBaseURI()); + xhtmlDoc.setBaseURL(new URL("http://www.example.com/newbase/")); + assertEquals("http://www.example.com/newbase/", xhtmlDoc.getBaseURI()); } @Test diff --git a/junit/io/sf/carte/doc/dom4j/XHTMLElementTest.java b/junit/io/sf/carte/doc/dom4j/XHTMLElementTest.java index 4eea3c3..ffef80a 100644 --- a/junit/io/sf/carte/doc/dom4j/XHTMLElementTest.java +++ b/junit/io/sf/carte/doc/dom4j/XHTMLElementTest.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause diff --git a/junit/io/sf/carte/doc/dom4j/XMLDocumentBuilderTest.java b/junit/io/sf/carte/doc/dom4j/XMLDocumentBuilderTest.java index 77daf46..9027374 100644 --- a/junit/io/sf/carte/doc/dom4j/XMLDocumentBuilderTest.java +++ b/junit/io/sf/carte/doc/dom4j/XMLDocumentBuilderTest.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -164,6 +164,7 @@ private void testPlainXML(Document document) { private Document parseDocument(Reader re) throws SAXException, IOException { XMLDocumentBuilder builder = new XMLDocumentBuilder(factory); builder.setIgnoreElementContentWhitespace(true); + builder.setHTMLProcessing(true); builder.setEntityResolver(resolver); InputSource is = new InputSource(re); Document document = builder.parse(is); diff --git a/junit/io/sf/carte/doc/dom4j/XPP3Test.java b/junit/io/sf/carte/doc/dom4j/XPP3Test.java index ce4bb77..d9f0ac3 100644 --- a/junit/io/sf/carte/doc/dom4j/XPP3Test.java +++ b/junit/io/sf/carte/doc/dom4j/XPP3Test.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 511e498..0000000 --- a/pom.xml +++ /dev/null @@ -1,194 +0,0 @@ - - 4.0.0 - - - io.sf.carte - css4j-dist - 1.0.0 - - io.sf.carte - css4j-dom4j - jar - - - io.sf.carte - css4j - ${project.parent.version} - jar - compile - false - - - io.sf.carte - css4j - ${project.parent.version} - test-jar - test - - - io.sf.carte - css4j-agent - ${project.parent.version} - jar - compile - false - - - org.dom4j - dom4j - 2.1.1 - jar - compile - false - - - xpp3 - xpp3 - 1.1.4c - compile - false - - - jaxen - jaxen - 1.2.0 - test - false - - - - - ${project.basedir}/build - ${project.build.directory}/bin - ${project.artifactId}-${project.version} - ${project.build.directory}/testbin - ${project.basedir}/src - ${project.basedir}/junit - - - org.apache.maven.plugins - maven-source-plugin - 3.1.0 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - - default-compile - - - --add-reads - io.sf.carte.css4j.dom4j=ALL-UNNAMED - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.1.0 - - 7 - true - - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.1.2 - - - - io.sf.carte.css4j.dom4j - - false - - - - - - test-jar - - - - - - - - ${project.build.sourceDirectory} - - **/*.java - overview.html - - - - ${project.basedir} - - LICENSE.txt - - - - - - ${project.basedir}/junit - - **/*.java - - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.1.0 - - ${project.build.directory}/doc - api - - - - default - - javadoc - - - - - - - - scm:git:https://github.com/css4j/css4j-dom4j.git - https://github.com/css4j/css4j-dom4j - - - - github - GitHub css4j Apache Maven Packages - https://maven.pkg.github.com/css4j/css4j-dom4j - - - diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..f6fcefb --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'css4j-dom4j' diff --git a/src/io/sf/carte/doc/dom4j/BaseHrefAttribute.java b/src/io/sf/carte/doc/dom4j/BaseHrefAttribute.java deleted file mode 100644 index 5fdc886..0000000 --- a/src/io/sf/carte/doc/dom4j/BaseHrefAttribute.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - - Copyright (c) 2005-2019, Carlos Amengual. - - SPDX-License-Identifier: BSD-3-Clause - - Licensed under a BSD-style License. You can find the license here: - https://css4j.github.io/LICENSE.txt - - */ - -package io.sf.carte.doc.dom4j; - -import java.net.MalformedURLException; -import java.net.URL; - -import org.dom4j.Element; -import org.dom4j.QName; -import org.dom4j.dom.DOMAttribute; - -/** - * Href attribute of Base element. - * - * @author Carlos Amengual - * - */ -class BaseHrefAttribute extends DOMAttribute { - - private static final long serialVersionUID = 3L; - - BaseHrefAttribute(QName qname) { - super(qname); - } - - BaseHrefAttribute(QName qname, String value) { - super(qname, value); - } - - BaseHrefAttribute(Element parent, QName qname, String value) { - super(parent, qname, value); - } - - @Override - public void setValue(String value) { - super.setValue(value); - if (value != null) { - URL base; - try { - base = new URL(value); - } catch (MalformedURLException e) { - XHTMLDocument doc = (XHTMLDocument) getDocument(); - if(doc != null) { - doc.setBaseURL(null); - } - return; - } - XHTMLDocument doc = (XHTMLDocument) getDocument(); - if(doc != null) { - doc.setBaseURL(base); - } - } else { - XHTMLDocument doc = (XHTMLDocument) getDocument(); - if(doc != null) { - doc.setBaseURL(null); - } - } - } - -} diff --git a/src/io/sf/carte/doc/dom4j/BaseURLElement.java b/src/io/sf/carte/doc/dom4j/BaseURLElement.java index 543393d..f5526af 100644 --- a/src/io/sf/carte/doc/dom4j/BaseURLElement.java +++ b/src/io/sf/carte/doc/dom4j/BaseURLElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -11,9 +11,6 @@ package io.sf.carte.doc.dom4j; -import java.net.MalformedURLException; -import java.net.URL; - import org.dom4j.Attribute; import org.dom4j.Node; import org.dom4j.QName; @@ -26,9 +23,7 @@ */ class BaseURLElement extends XHTMLElement { - transient URL base = null; - - private static final long serialVersionUID = 2L; + private static final long serialVersionUID = 3L; BaseURLElement(String name) { super(name); @@ -38,29 +33,21 @@ class BaseURLElement extends XHTMLElement { super(qname); } - BaseURLElement(QName qname, int attributeCount) { - super(qname, attributeCount); - } + BaseURLElement(QName qname, int attributeCount) { + super(qname, attributeCount); + } @Override protected void childAdded(Node node) { super.childAdded(node); - if(node instanceof Attribute){ - if(node.getName().equals("href")){ + if (node instanceof Attribute) { + if (node.getName().equalsIgnoreCase("href")) { String href = ((Attribute) node).getValue(); if (href != null) { - try { - base = new URL(href); - } catch (MalformedURLException e) { - XHTMLDocument doc = getOwnerDocument(); - if(doc != null) { - doc.setBaseURL(null); - } - return; - } XHTMLDocument doc = getOwnerDocument(); - if(doc != null) { - doc.setBaseURL(base); + if (doc != null) { + doc.setBaseURL(this, href); + HrefAttribute.onBaseModify(doc); } } } @@ -69,11 +56,14 @@ protected void childAdded(Node node) { @Override protected void childRemoved(Node node) { - super.childRemoved(node); - if(node instanceof Attribute && node.getName().equals("href")){ - base = null; - getOwnerDocument().setBaseURL(null); + if (node instanceof Attribute && node.getName().equalsIgnoreCase("href")) { + XHTMLDocument doc = getOwnerDocument(); + if (doc != null) { + doc.setBaseURL(null); + HrefAttribute.onBaseModify(doc); + } } + super.childRemoved(node); } } diff --git a/src/io/sf/carte/doc/dom4j/CSSStylableElement.java b/src/io/sf/carte/doc/dom4j/CSSStylableElement.java index ec3febd..7a2f3c4 100644 --- a/src/io/sf/carte/doc/dom4j/CSSStylableElement.java +++ b/src/io/sf/carte/doc/dom4j/CSSStylableElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -18,11 +18,13 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import org.dom4j.Attribute; import org.dom4j.Element; import org.dom4j.QName; import org.dom4j.dom.DOMElement; +import org.dom4j.tree.DefaultAttribute; import org.w3c.css.sac.DescendantSelector; import org.w3c.css.sac.InputSource; import org.w3c.css.sac.Parser; @@ -35,6 +37,8 @@ import org.w3c.dom.NodeList; import org.w3c.dom.css.CSSStyleDeclaration; +import io.sf.carte.doc.DirectionalityHelper; +import io.sf.carte.doc.DirectionalityHelper.Directionality; import io.sf.carte.doc.agent.CSSCanvas; import io.sf.carte.doc.style.css.CSSDocument; import io.sf.carte.doc.style.css.CSSElement; @@ -51,8 +55,7 @@ * @author Carlos Amengual * */ -abstract public class CSSStylableElement extends DOMElement implements - CSSElement { +abstract public class CSSStylableElement extends DOMElement implements CSSElement { private static final long serialVersionUID = 8L; @@ -69,9 +72,20 @@ protected CSSStylableElement(QName qname) { } protected CSSStylableElement(QName qname, int attributeCount) { - super(qname, attributeCount); - } + super(qname, attributeCount); + } + /** + * The {@code XHTMLDocument} object which is the root ancestor of this node. + *

+ * This is also the {@code XHTMLDocument} object used to create new nodes. + *

+ * + * @return the {@code XHTMLDocument} object which is the root ancestor of this + * node, or {@code null} if this node is an {@code XHTMLDocument}, a + * {@code DocumentType} which is not used inside any + * {@code XHTMLDocument} yet, or this node is not part of a document. + */ @Override public XHTMLDocument getOwnerDocument() { return (XHTMLDocument) super.getDocument(); @@ -87,10 +101,40 @@ protected XHTMLDocumentFactory getDocumentFactory() { return (XHTMLDocumentFactory) super.getDocumentFactory(); } + @Override + public Attr setAttributeNode(Attr newAttr) throws DOMException { + Attribute attribute = attribute(newAttr); + if (attribute != newAttr) { + if (newAttr.getOwnerElement() != null) { + throw new DOMException(DOMException.INUSE_ATTRIBUTE_ERR, "Attribute is already in use"); + } + if (attribute != null) { + attribute.detach(); + add((Attribute) newAttr); + if (attribute instanceof Attr) { + return (Attr) attribute; + } + } else { + add((Attribute) newAttr); + } + } + return null; + } + + @Override + public Attr setAttributeNodeNS(Attr newAttr) throws DOMException { + return setAttributeNode(newAttr); + } + + @Override + protected Attribute attribute(Attr attr) { + return attribute(((DefaultAttribute) attr).getQName()); + } + @Override public String getBaseURI() { URL baseURL = getOwnerDocument().getBaseURL(); - if(baseURL != null) { + if (baseURL != null) { return baseURL.toExternalForm(); } else { return null; @@ -110,11 +154,11 @@ public void setIdAttributeNode(Attr idAttr, boolean isId) { * @return the id attribute, or the empty string if has no ID. */ @Override - abstract public String getId(); + abstract public String getId(); /** - * Gets the inline style declaration from the current contents of the style - * XHTML attribute. + * Gets the inline style declaration from the current contents of the + * style XHTML attribute. * * @return the style declaration, or null if the element has no * style attribute. @@ -149,7 +193,7 @@ public void exportHintsToStyle(CSSStyleDeclaration style) { @Override public boolean hasOverrideStyle(String pseudoElt) { - if(overrideStyleSet == null) { + if (overrideStyleSet == null) { return false; } return overrideStyleSet.containsKey(pseudoElt); @@ -158,12 +202,12 @@ public boolean hasOverrideStyle(String pseudoElt) { @Override public ExtendedCSSStyleDeclaration getOverrideStyle(String pseudoElt) { ExtendedCSSStyleDeclaration overrideStyle = null; - if(overrideStyleSet == null) { + if (overrideStyleSet == null) { overrideStyleSet = new HashMap(1); } else { overrideStyle = overrideStyleSet.get(pseudoElt); } - if(overrideStyle == null) { + if (overrideStyle == null) { overrideStyle = getDocumentFactory().createInlineStyle(this); overrideStyleSet.put(pseudoElt, overrideStyle); } @@ -173,8 +217,7 @@ public ExtendedCSSStyleDeclaration getOverrideStyle(String pseudoElt) { /** * Gets the computed style declaration that applies to this element. * - * @param pseudoElt - * the pseudo-element name. + * @param pseudoElt the pseudo-element name. * @return the computed style declaration. */ @Override @@ -205,6 +248,51 @@ public ComputedCSSStyle getComputedStyle() { } } + String getAttributeValue(String attrName) { + String value = null; + Attribute attr = attribute(attrName); + if (attr == null) { + /* + * There could be 3 reasons for this: + * 1) There is no attribute with attrName. + * 2) attrName contains a prefix and DOM4J does not like that. + * 3) We are in HTML and attrName is a valid case-insensitive match. + */ + String prefix = ""; + String localName; + int idx = attrName.indexOf(':'); + if (idx != -1) { + prefix = attrName.substring(0, idx); + idx++; + if (idx < attrName.length()) { + localName = attrName.substring(idx); + } else { + return ""; + } + } else { + localName = attrName; + } + List list = attributeList(); + for (Attribute item : list) { + String nsuri; // In some configurations, nsuri could be null + if (localName.equalsIgnoreCase(item.getName())) { + // Aren't we in HTML? Then the problem is the prefix? Check CS match + if ((nsuri = item.getNamespaceURI()) != null && nsuri.length() != 0 + && !XHTMLDocument.XHTML_NAMESPACE_URI.equals(nsuri) && !localName.equals(item.getName())) { + continue; + } + if (Objects.equals(prefix, item.getNamespacePrefix())) { + value = item.getValue(); + break; + } + } + } + } else { + value = attr.getValue(); + } + return value != null ? value : ""; + } + /** * DOM4J CSS Selector matcher. * @@ -221,8 +309,8 @@ class DOM4JSelectorMatcher extends AbstractSelectorMatcher { @Override protected AbstractSelectorMatcher getParentSelectorMatcher() { Element parent = getParent(); - if(parent instanceof CSSStylableElement) { - return (AbstractSelectorMatcher) ((CSSStylableElement)parent).getSelectorMatcher(); + if (parent instanceof CSSStylableElement) { + return (AbstractSelectorMatcher) ((CSSStylableElement) parent).getSelectorMatcher(); } else { return null; } @@ -231,7 +319,7 @@ protected AbstractSelectorMatcher getParentSelectorMatcher() { @Override protected AbstractSelectorMatcher getPreviousSiblingSelectorMatcher() { Element parent = getParent(); - if(parent == null) { + if (parent == null) { return null; } @SuppressWarnings("rawtypes") @@ -240,8 +328,8 @@ protected AbstractSelectorMatcher getPreviousSiblingSelectorMatcher() { int sibindex = elements.indexOf(CSSStylableElement.this) - 1; if (sibindex != -1) { Object sibling = elements.get(sibindex); - if(sibling instanceof CSSStylableElement) { - return (AbstractSelectorMatcher) ((CSSStylableElement)sibling).getSelectorMatcher(); + if (sibling instanceof CSSStylableElement) { + return (AbstractSelectorMatcher) ((CSSStylableElement) sibling).getSelectorMatcher(); } } return null; @@ -250,36 +338,36 @@ protected AbstractSelectorMatcher getPreviousSiblingSelectorMatcher() { @Override protected int indexOf(SelectorList selectors) { Element parent = getParent(); - if(parent == null) { + if (parent == null) { return 1; // root element } NodeList list = getParentNode().getChildNodes(); int sz = list.getLength(); int idx = 0; - for (int i=0; i< sz; i++) { Node node = list.item(i); - if (node.getNodeType() == Node.ELEMENT_NODE && matchSelectors(selectors, (CSSStylableElement)node)) { + if (node.getNodeType() == Node.ELEMENT_NODE && matchSelectors(selectors, (CSSStylableElement) node)) { idx++; if (node == CSSStylableElement.this) { - break; + return idx; } } } - return idx == sz ? -1 : idx; + return -1; } @Override protected int reverseIndexOf(SelectorList selectors) { Element parent = getParent(); - if(parent == null) { + if (parent == null) { return 1; // root element } NodeList list = getParentNode().getChildNodes(); int sz = list.getLength(); int idx = 0; - for (int i=sz-1; i>=0; i--) { + for (int i = sz - 1; i >= 0; i--) { Node node = list.item(i); - if (node.getNodeType() == Node.ELEMENT_NODE && matchSelectors(selectors, (CSSStylableElement)node)) { + if (node.getNodeType() == Node.ELEMENT_NODE && matchSelectors(selectors, (CSSStylableElement) node)) { idx++; if (node == CSSStylableElement.this) { break; @@ -294,7 +382,7 @@ private boolean matchSelectors(SelectorList selectors, CSSStylableElement node) return true; } int sz = selectors.getLength(); - for (int i=0; i< sz; i++) { if (node.getSelectorMatcher().matches(selectors.item(i))) { return true; } @@ -315,7 +403,7 @@ protected boolean isActivePseudoClass(String pseudoclassName) { @Override protected boolean isFirstChild() { Element parent = getParent(); - if(parent == null) { + if (parent == null) { return true; // root element } return parent.elements().indexOf(CSSStylableElement.this) == 0; @@ -324,7 +412,7 @@ protected boolean isFirstChild() { @Override protected boolean isLastChild() { Element parent = getParent(); - if(parent == null) { + if (parent == null) { return true; // root element } @SuppressWarnings("rawtypes") @@ -360,10 +448,10 @@ protected boolean isLastOfType() { protected boolean isNthOfType(int step, int offset) { int idx = 0; Element parent = getParent(); - if(parent != null) { + if (parent != null) { NodeList list = getParentNode().getChildNodes(); int sz = list.getLength(); - for (int i=0; i< sz; i++) { Node node = list.item(i); if (node.getNodeType() == Node.ELEMENT_NODE && getLocalName().equals(node.getNodeName())) { idx++; @@ -383,10 +471,10 @@ protected boolean isNthOfType(int step, int offset) { protected boolean isNthLastOfType(int step, int offset) { int idx = 0; Element parent = getParent(); - if(parent != null) { + if (parent != null) { NodeList list = getParentNode().getChildNodes(); int sz = list.getLength(); - for (int i=sz-1; i>=0; i--) { + for (int i = sz - 1; i >= 0; i--) { Node node = list.item(i); if (node.getNodeType() == Node.ELEMENT_NODE && getLocalName().equals(node.getNodeName())) { idx++; @@ -402,28 +490,15 @@ protected boolean isNthLastOfType(int step, int offset) { return step == 0 ? idx == 0 : Math.floorMod(idx, step) == 0; } - @Override - protected boolean isNotVisitedLink() { - String href = getAttribute("href"); - if (href != null && href.length() != 0) { - return !getOwnerDocument().isVisitedURI(href); - } else { - return false; - } - } - - @Override - protected boolean isVisitedLink() { - String href = getAttribute("href"); - if (href != null && href.length() != 0) { - return getOwnerDocument().isVisitedURI(href); - } else { - return false; - } - } - @Override protected boolean isTarget() { + String uri = getOwnerDocument().getDocumentURI(); + int idx; + if (uri != null && (idx = uri.lastIndexOf('#')) != -1) { + idx++; + int len = uri.length(); + return idx < len && getId().equals(uri.subSequence(idx, len)); + } return false; } @@ -437,7 +512,7 @@ protected boolean isEmpty() { if (hasChildNodes()) { NodeList list = getChildNodes(); int sz = list.getLength(); - for (int i=0; i< sz; i++) { Node node = list.item(i); short type = node.getNodeType(); if (type == Node.ELEMENT_NODE) { @@ -460,7 +535,7 @@ protected boolean isBlank() { if (hasChildNodes()) { NodeList list = getChildNodes(); int sz = list.getLength(); - for (int i=0; i< sz; i++) { Node node = list.item(i); short type = node.getNodeType(); if (type == Node.ELEMENT_NODE) { @@ -481,9 +556,9 @@ protected boolean isBlank() { @Override protected boolean isDisabled() { /* - * A form control is disabled if its disabled attribute is set, or if it is a descendant - * of a fieldset element whose disabled attribute is set and is not a descendant of that - * fieldset element's first legend element child, if any. + * A form control is disabled if its disabled attribute is set, or if it is a + * descendant of a fieldset element whose disabled attribute is set and is not a + * descendant of that fieldset element's first legend element child, if any. */ if (hasAttribute("disabled")) { return true; @@ -498,7 +573,7 @@ protected boolean isDisabled() { @Override protected boolean isDefaultButton() { // "A form element's default button is the first submit button - // in tree order whose form owner is that form element." + // in tree order whose form owner is that form element." CSSStylableElement parent = (CSSStylableElement) getParent(); if (parent == null) { return false; @@ -528,7 +603,7 @@ protected boolean isDefaultButton() { for (int i = 0; i < sz; i++) { Node node = list.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { - if (!defaultButtonCheck((CSSStylableElement)node, formid)) { + if (!defaultButtonCheck((CSSStylableElement) node, formid)) { return false; } } @@ -567,13 +642,24 @@ protected String getNamespaceURI() { @Override protected String getAttributeValue(String attrName) { - String value = attributeValue(attrName); - return value != null? value : ""; + return CSSStylableElement.this.getAttributeValue(attrName); } @Override protected boolean hasAttribute(String attrName) { - return attribute(attrName) != null; + Attribute attr = attribute(attrName); + if (attr == null) { + List list = attributeList(); + for (Attribute item : list) { + String nsuri; // In some configurations, nsuri could be null + if (attrName.equalsIgnoreCase(item.getName()) && ((nsuri = item.getNamespaceURI()) == null + || nsuri.length() == 0 || XHTMLDocument.XHTML_NAMESPACE_URI.equals(nsuri))) { + return true; + } + } + return false; + } + return attr != null; } @Override @@ -582,44 +668,58 @@ protected String getId() { } @Override - protected CSSDocument.ComplianceMode getComplianceMode() { - return CSSStylableElement.this.getOwnerDocument().getComplianceMode(); + protected CSSDocument getOwnerDocument() { + return CSSStylableElement.this.getOwnerDocument(); } @Override protected String getLanguage() { /* - * In (X)HTML, the lang attribute contains the language, - * but that may not be true for other XML. + * In (X)HTML, the lang attribute contains the language, but that may not be + * true for other XML. */ String lang = attributeValue("lang"); - if(lang == null) { + if (lang == null) { lang = attributeValue("LANG"); } Element parent = CSSStylableElement.this; - while(lang == null || lang.length() == 0) { + while (lang == null || lang.length() == 0) { parent = parent.getParent(); - if(parent == null) { + if (parent == null) { break; } else { lang = parent.attributeValue("lang"); - if(lang == null) { + if (lang == null) { lang = attributeValue("LANG"); } } } - if(lang == null) { + if (lang == null) { lang = ""; } return lang; } + @Override + protected boolean isDir(String argument) { + try { + return super.isDir(argument); + } catch (RuntimeException e) { + return false; + } + } + + @Override + protected Directionality getDirectionality() { + return DirectionalityHelper.getDirectionality(CSSStylableElement.this); + } + @Override protected boolean scopeMatchChild(DescendantSelector selector) { SimpleSelector desc = selector.getSimpleSelector(); NodeList list = getChildNodes(); int sz = list.getLength(); - for (int i=0; i< sz; i++) { Node node = list.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { SelectorMatcher childSM = ((CSSStylableElement) node).getSelectorMatcher(); @@ -656,7 +756,7 @@ protected boolean scopeMatchDirectAdjacent(SiblingSelector selector) { */ @Override public SelectorMatcher getSelectorMatcher() { - if(selectorMatcher == null) { + if (selectorMatcher == null) { selectorMatcher = new DOM4JSelectorMatcher(); } return selectorMatcher; diff --git a/src/io/sf/carte/doc/dom4j/CachedTableCellElement.java b/src/io/sf/carte/doc/dom4j/CachedTableCellElement.java index 419a0f2..25dcaf2 100644 --- a/src/io/sf/carte/doc/dom4j/CachedTableCellElement.java +++ b/src/io/sf/carte/doc/dom4j/CachedTableCellElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -34,13 +34,14 @@ protected CachedTableCellElement(QName qname) { super(qname); } - CachedTableCellElement(QName qname, int attributeCount) { - super(qname, attributeCount); - } + CachedTableCellElement(QName qname, int attributeCount) { + super(qname, attributeCount); + } @Override public boolean hasPresentationalHints() { - return hasAttribute("bgcolor") || hasAttribute("width") || hasAttribute("height"); + return hasAttribute("bgcolor") || hasAttribute("width") || hasAttribute("height") || hasAttribute("background") + || hasAttribute("align"); } @Override @@ -48,6 +49,8 @@ public void exportHintsToStyle(CSSStyleDeclaration style) { AttributeToStyle.bgcolor(getAttribute("bgcolor"), style); AttributeToStyle.width(getAttribute("width"), style); AttributeToStyle.height(getAttribute("height"), style); + AttributeToStyle.background(getAttribute("background"), style); + AttributeToStyle.align(getAttribute("align"), style); } } diff --git a/src/io/sf/carte/doc/dom4j/CachedTableElement.java b/src/io/sf/carte/doc/dom4j/CachedTableElement.java index 80bcfbd..dcbc3ed 100644 --- a/src/io/sf/carte/doc/dom4j/CachedTableElement.java +++ b/src/io/sf/carte/doc/dom4j/CachedTableElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -34,18 +34,25 @@ protected CachedTableElement(QName qname) { super(qname); } - CachedTableElement(QName qname, int attributeCount) { - super(qname, attributeCount); - } + CachedTableElement(QName qname, int attributeCount) { + super(qname, attributeCount); + } @Override public boolean hasPresentationalHints() { - return hasAttribute("bgcolor"); + return hasAttribute("width") || hasAttribute("height") || hasAttribute("cellspacing") || hasAttribute("border") + || hasAttribute("bordercolor") || hasAttribute("bgcolor") || hasAttribute("background"); } @Override public void exportHintsToStyle(CSSStyleDeclaration style) { AttributeToStyle.bgcolor(getAttribute("bgcolor"), style); + AttributeToStyle.cellSpacing(getAttribute("cellspacing"), style); + AttributeToStyle.width(getAttribute("width"), style); + AttributeToStyle.height(getAttribute("height"), style); + AttributeToStyle.border(getAttribute("border"), style); + AttributeToStyle.borderColor(getAttribute("bordercolor"), style); + AttributeToStyle.background(getAttribute("background"), style); } } diff --git a/src/io/sf/carte/doc/dom4j/CachedTableRowElement.java b/src/io/sf/carte/doc/dom4j/CachedTableRowElement.java index 8a5d45c..c34e2f8 100644 --- a/src/io/sf/carte/doc/dom4j/CachedTableRowElement.java +++ b/src/io/sf/carte/doc/dom4j/CachedTableRowElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -34,18 +34,21 @@ protected CachedTableRowElement(QName qname) { super(qname); } - CachedTableRowElement(QName qname, int attributeCount) { - super(qname, attributeCount); - } + CachedTableRowElement(QName qname, int attributeCount) { + super(qname, attributeCount); + } @Override public boolean hasPresentationalHints() { - return hasAttribute("bgcolor"); + return hasAttribute("bgcolor") || hasAttribute("height") || hasAttribute("background") || hasAttribute("align"); } @Override public void exportHintsToStyle(CSSStyleDeclaration style) { AttributeToStyle.bgcolor(getAttribute("bgcolor"), style); + AttributeToStyle.height(getAttribute("height"), style); + AttributeToStyle.background(getAttribute("background"), style); + AttributeToStyle.align(getAttribute("align"), style); } } diff --git a/src/io/sf/carte/doc/dom4j/CachedXHTMLElement.java b/src/io/sf/carte/doc/dom4j/CachedXHTMLElement.java index 776619f..1bc6b01 100644 --- a/src/io/sf/carte/doc/dom4j/CachedXHTMLElement.java +++ b/src/io/sf/carte/doc/dom4j/CachedXHTMLElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -40,14 +40,14 @@ class CachedXHTMLElement extends XHTMLElement { super(qname); } - CachedXHTMLElement(QName qname, int attributeCount) { - super(qname, attributeCount); - } + CachedXHTMLElement(QName qname, int attributeCount) { + super(qname, attributeCount); + } @Override public ComputedCSSStyle getComputedStyle() { int documentCacheSerial = getOwnerDocument().getStyleCacheSerial(); - if(cachedComputedStyle == null || cacheSerial != documentCacheSerial){ + if (cachedComputedStyle == null || cacheSerial != documentCacheSerial) { cacheSerial = documentCacheSerial; cachedComputedStyle = super.getComputedStyle(); } @@ -64,10 +64,10 @@ public void onStyleModify() { // Cascade notification @SuppressWarnings("rawtypes") Iterator elements = elementIterator(); - while(elements.hasNext()) { + while (elements.hasNext()) { Element element = (Element) elements.next(); - if(element instanceof CachedXHTMLElement) { - ((CachedXHTMLElement)element).onStyleModify(); + if (element instanceof CachedXHTMLElement) { + ((CachedXHTMLElement) element).onStyleModify(); } } getDocument().onStyleModify(); diff --git a/src/io/sf/carte/doc/dom4j/DOM4JCSSStyleSheet.java b/src/io/sf/carte/doc/dom4j/DOM4JCSSStyleSheet.java index 944d294..dee859d 100644 --- a/src/io/sf/carte/doc/dom4j/DOM4JCSSStyleSheet.java +++ b/src/io/sf/carte/doc/dom4j/DOM4JCSSStyleSheet.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -77,8 +77,8 @@ protected String getTargetMedium() { /** * Creates and returns a copy of this style sheet. *

- * The copy is a shallow copy (the rule list is new, but the referenced rules are the same - * as in the cloned object. + * The copy is a shallow copy (the rule list is new, but the referenced rules + * are the same as in the cloned object. * * @return a clone of this instance. */ diff --git a/src/io/sf/carte/doc/dom4j/DOM4JComputedStyle.java b/src/io/sf/carte/doc/dom4j/DOM4JComputedStyle.java index 8b8bba5..3396433 100644 --- a/src/io/sf/carte/doc/dom4j/DOM4JComputedStyle.java +++ b/src/io/sf/carte/doc/dom4j/DOM4JComputedStyle.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -58,20 +58,19 @@ public ComputedCSSStyle getParentComputedStyle() { } /** - * Gets the (whitespace-trimmed) text content of the - * node associated to this style. + * Gets the (whitespace-trimmed) text content of the node associated to this + * style. * - * @return the text content, or the empty string if the box - * has no text. + * @return the text content, or the empty string if the box has no text. */ @Override public String getText() { String text; Node node = getOwnerNode(); - if(node instanceof DOMElement) { - text = ((DOMElement)node).getTextTrim(); - } else if(node instanceof org.dom4j.Node) { - text = BoxModelHelper.contractSpaces(((org.dom4j.Node)node).getText()); + if (node instanceof DOMElement) { + text = ((DOMElement) node).getTextTrim(); + } else if (node instanceof org.dom4j.Node) { + text = BoxModelHelper.contractSpaces(((org.dom4j.Node) node).getText()); } else { text = ""; } @@ -86,7 +85,7 @@ public String getText() { @Override public StyleDatabase getStyleDatabase() { Node node = getOwnerNode(); - if(node != null) { + if (node != null) { CSSDocument doc = (CSSDocument) node.getOwnerDocument(); return doc.getStyleDatabase(); } diff --git a/src/io/sf/carte/doc/dom4j/DOM4JDocumentCSSStyleSheet.java b/src/io/sf/carte/doc/dom4j/DOM4JDocumentCSSStyleSheet.java index e808abf..96340c9 100644 --- a/src/io/sf/carte/doc/dom4j/DOM4JDocumentCSSStyleSheet.java +++ b/src/io/sf/carte/doc/dom4j/DOM4JDocumentCSSStyleSheet.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -57,10 +57,8 @@ public void setOwnerDocument(CSSDocument owner) { /** * Gets the computed style for the given DOM4J element and pseudo-element. * - * @param elm - * the element. - * @param pseudoElt - * the pseudo-element. + * @param elm the element. + * @param pseudoElt the pseudo-element. * @return the computed style declaration. */ @Override @@ -76,8 +74,8 @@ public ComputedCSSStyle getComputedStyle(CSSElement elm, String pseudoElt) { /** * Creates and returns a copy of this style sheet. *

- * The copy is a shallow copy (the rule list is new, but the referenced rules are the same - * as in the cloned object. + * The copy is a shallow copy (the rule list is new, but the referenced rules + * are the same as in the cloned object. * * @return a clone of this instance. */ diff --git a/src/io/sf/carte/doc/dom4j/DOM4JUserAgent.java b/src/io/sf/carte/doc/dom4j/DOM4JUserAgent.java index 2da37e8..550ec61 100644 --- a/src/io/sf/carte/doc/dom4j/DOM4JUserAgent.java +++ b/src/io/sf/carte/doc/dom4j/DOM4JUserAgent.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -25,8 +25,6 @@ import org.dom4j.dom.DOMElement; import org.dom4j.io.SAXReader; import org.dom4j.io.XPP3Reader; -import org.w3c.dom.DOMException; -import org.w3c.dom.DocumentType; import org.w3c.dom.NodeList; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; @@ -92,12 +90,13 @@ public void setEntityResolver(EntityResolver resolver) { * * @param url the URL that points to the document. * @return the XHTMLDocument. - * @throws IOException if there is an I/O problem reading the URL. - * @throws io.sf.carte.doc.DocumentException if there is a problem parsing the document. + * @throws IOException if there is an I/O problem reading + * the URL. + * @throws io.sf.carte.doc.DocumentException if there is a problem parsing the + * document. */ @Override - public XHTMLDocument readURL(URL url) - throws IOException, io.sf.carte.doc.DocumentException { + public XHTMLDocument readURL(URL url) throws IOException, io.sf.carte.doc.DocumentException { long time = System.currentTimeMillis(); URLConnection con = openConnection(url, time); con.connect(); @@ -108,38 +107,33 @@ public XHTMLDocument readURL(URL url) try { is = openInputStream(con); xdoc = parseDocument(AgentUtil.inputStreamToReader(is, conType, contentEncoding, "utf-8")); - xdoc.setLoadingTime(time); - if (xdoc.getBaseURL() == null) { - xdoc.setBaseURL(url); - } } catch (IOException e) { throw e; } catch (io.sf.carte.doc.DocumentException e) { - throw new io.sf.carte.doc.DocumentException( - "Error parsing document " + url.toExternalForm(), e.getCause()); + throw new io.sf.carte.doc.DocumentException("Error parsing document " + url.toExternalForm(), e.getCause()); } finally { - if(is != null) { + if (is != null) { is.close(); } } + xdoc.setLoadingTime(time); + xdoc.setDocumentURI(url.toExternalForm()); // Check for preferred style String defStyle = con.getHeaderField("Default-Style"); NodeList list = xdoc.getElementsByTagName("meta"); int listL = list.getLength(); - for(int i = listL - 1; i>=0; i--) { - if("Default-Style".equalsIgnoreCase( - ((DOMElement)list.item(i)).attributeValue("http-equiv"))) { - String metaDefStyle = ((DOMElement)list.item(i)) - .attributeValue("content"); - if(metaDefStyle != null) { + for (int i = listL - 1; i >= 0; i--) { + if ("Default-Style".equalsIgnoreCase(((DOMElement) list.item(i)).attributeValue("http-equiv"))) { + String metaDefStyle = ((DOMElement) list.item(i)).attributeValue("content"); + if (metaDefStyle != null) { // Per HTML4 spec § 14.3.2: - // "If two or more META declarations or HTTP headers specify - // the preferred style sheet, the last one takes precedence." + // "If two or more META declarations or HTTP headers specify + // the preferred style sheet, the last one takes precedence." defStyle = metaDefStyle; } } } - if(defStyle != null) { + if (defStyle != null) { xdoc.setSelectedStyleSheetSet(defStyle); } // Referrer Policy @@ -148,8 +142,8 @@ public XHTMLDocument readURL(URL url) xdoc.setReferrerPolicyHeader(referrerPolicy); } // Read cookies and close connection, if appropriate - if(con instanceof HttpURLConnection) { - HttpURLConnection hcon = (HttpURLConnection)con; + if (con instanceof HttpURLConnection) { + HttpURLConnection hcon = (HttpURLConnection) con; readCookies(hcon, time); hcon.disconnect(); } @@ -162,19 +156,17 @@ protected InputStream openInputStream(URLConnection con) throws IOException { protected AgentXHTMLDocument parseDocument(Reader re) throws io.sf.carte.doc.DocumentException, IOException { try { - if(useXPP3) { + if (useXPP3) { return parseWithXPP3Reader(re); } else { return parseWithSAXReader(re); } } catch (DocumentException e) { - throw new io.sf.carte.doc.DocumentException( - "Error parsing document", e); + throw new io.sf.carte.doc.DocumentException("Error parsing document", e); } } - private AgentXHTMLDocument parseWithSAXReader(Reader re) - throws DocumentException { + private AgentXHTMLDocument parseWithSAXReader(Reader re) throws DocumentException { InputSource isrc = new InputSource(re); XHTMLDocumentFactory factory = getXHTMLDocumentFactory(); SAXReader reader = new SAXReader(factory); @@ -182,8 +174,7 @@ private AgentXHTMLDocument parseWithSAXReader(Reader re) return (AgentXHTMLDocument) reader.read(isrc); } - private AgentXHTMLDocument parseWithXPP3Reader(Reader re) - throws DocumentException, IOException { + private AgentXHTMLDocument parseWithXPP3Reader(Reader re) throws DocumentException, IOException { XHTMLDocumentFactory factory = getXHTMLDocumentFactory(); XPP3Reader reader = new XPP3Reader(factory); try { @@ -194,8 +185,6 @@ private AgentXHTMLDocument parseWithXPP3Reader(Reader re) } public XHTMLDocumentFactory getXHTMLDocumentFactory() { - // Uncomment the next line to use PDF. - //factory.getDeviceFactory().setStyleDatabase("pdf", new PDFStyleDatabase()); return factory; } @@ -215,26 +204,16 @@ public XHTMLDocument createDocument() { } @Override - public XHTMLDocument createDocument(String namespaceURI, String qualifiedName, DocumentType docType) - throws DOMException { - XHTMLDocument document; - if (docType != null) { - DOMDocumentType documentType = asDocumentType(docType); - document = new AgentXHTMLDocument(documentType); - } else { - document = new AgentXHTMLDocument(); - } + protected XHTMLDocument createDocument(DOMDocumentType documentType) { + AgentXHTMLDocument document = new AgentXHTMLDocument(documentType); document.setDocumentFactory(this); - if (qualifiedName != null) { - document.add(createElement(createQName(qualifiedName, namespaceURI))); - } return document; } public class AgentXHTMLDocument extends XHTMLDocument { private static final long serialVersionUID = 3L; - + private long loadingTime; AgentXHTMLDocument() { @@ -265,7 +244,7 @@ public class AgentXHTMLDocument extends XHTMLDocument { public URLConnection openConnection(URL url) throws IOException { return DOM4JUserAgent.this.openConnection(url, loadingTime); } - + @Override public boolean isVisitedURI(String href) { try { diff --git a/src/io/sf/carte/doc/dom4j/DocumentStyleEventAttribute.java b/src/io/sf/carte/doc/dom4j/DocumentStyleEventAttribute.java index 1365301..2f58073 100644 --- a/src/io/sf/carte/doc/dom4j/DocumentStyleEventAttribute.java +++ b/src/io/sf/carte/doc/dom4j/DocumentStyleEventAttribute.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -16,7 +16,8 @@ import org.dom4j.dom.DOMAttribute; /** - * An attribute that fires a parent and document-wide style modify event when changed. + * An attribute that fires a parent and document-wide style modify event when + * changed. * * @author Carlos Amengual * @@ -40,9 +41,13 @@ class DocumentStyleEventAttribute extends DOMAttribute { @Override public void setValue(String value) { super.setValue(value); - StyleDefinerElement element = (StyleDefinerElement) getParent(); - if (element != null) { - element.resetLinkedSheet(); + Element owner = getParent(); + if (owner != null) { + if (owner instanceof StyleDefinerElement) { + ((StyleDefinerElement) owner).resetLinkedSheet(); + } else if (owner instanceof CachedXHTMLElement) { + ((CachedXHTMLElement) owner).onStyleModify(); + } } } diff --git a/src/io/sf/carte/doc/dom4j/FontElement.java b/src/io/sf/carte/doc/dom4j/FontElement.java index 02dfd6a..ea14d74 100644 --- a/src/io/sf/carte/doc/dom4j/FontElement.java +++ b/src/io/sf/carte/doc/dom4j/FontElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -17,7 +17,8 @@ import io.sf.carte.doc.style.css.property.AttributeToStyle; /** - * Font element.

+ * Font element. + *

* Provides equivalent CSS style for this deprecated element. * * @author Carlos Amengual @@ -35,9 +36,9 @@ class FontElement extends XHTMLElement { super(qname); } - FontElement(QName qname, int attributeCount) { - super(qname, attributeCount); - } + FontElement(QName qname, int attributeCount) { + super(qname, attributeCount); + } @Override public boolean hasPresentationalHints() { diff --git a/src/io/sf/carte/doc/dom4j/HeadElement.java b/src/io/sf/carte/doc/dom4j/HeadElement.java index feaadbb..95c0d4c 100644 --- a/src/io/sf/carte/doc/dom4j/HeadElement.java +++ b/src/io/sf/carte/doc/dom4j/HeadElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -33,23 +33,31 @@ class HeadElement extends XHTMLElement { super(qname); } - HeadElement(QName qname, int attributeCount) { - super(qname, attributeCount); - } + HeadElement(QName qname, int attributeCount) { + super(qname, attributeCount); + } @Override protected void childAdded(Node node) { super.childAdded(node); if (node instanceof BaseURLElement) { - getOwnerDocument().setBaseURL(((BaseURLElement) node).base); - ((BaseURLElement) node).base = null; // help GC + XHTMLDocument doc = getOwnerDocument(); + if (doc != null) { + String href = ((BaseURLElement) node).getAttribute("href").trim(); + if (href.length() != 0) { + doc.setBaseURL(this, href); + } + } } else if ("meta".equalsIgnoreCase(node.getName())) { - Element elt = (Element) node; - String name = elt.getAttribute("http-equiv"); - if (name.length() == 0) { - name = elt.getAttribute("name"); + XHTMLDocument doc = getOwnerDocument(); + if (doc != null) { + Element elt = (Element) node; + String name = elt.getAttribute("http-equiv"); + if (name.length() == 0) { + name = elt.getAttribute("name"); + } + doc.onMetaAdded(name, elt.getAttribute("content")); } - getOwnerDocument().onMetaAdded(name, elt.getAttribute("content")); } } diff --git a/src/io/sf/carte/doc/dom4j/HrefAttribute.java b/src/io/sf/carte/doc/dom4j/HrefAttribute.java new file mode 100644 index 0000000..a2150b2 --- /dev/null +++ b/src/io/sf/carte/doc/dom4j/HrefAttribute.java @@ -0,0 +1,74 @@ +/* + + Copyright (c) 2005-2022, Carlos Amengual. + + SPDX-License-Identifier: BSD-3-Clause + + Licensed under a BSD-style License. You can find the license here: + https://css4j.github.io/LICENSE.txt + + */ + +package io.sf.carte.doc.dom4j; + +import java.util.Iterator; + +import org.dom4j.Element; +import org.dom4j.QName; +import org.dom4j.dom.DOMAttribute; + +/** + * Href attribute. + * + * @author Carlos Amengual + * + */ +class HrefAttribute extends DOMAttribute { + + private static final long serialVersionUID = 3L; + + HrefAttribute(QName qname) { + super(qname); + } + + HrefAttribute(QName qname, String value) { + super(qname, value); + } + + HrefAttribute(Element parent, QName qname, String value) { + super(parent, qname, value); + } + + @Override + public void setValue(String value) { + super.setValue(value); + XHTMLDocument doc = (XHTMLDocument) getDocument(); + org.w3c.dom.Element owner; + if (doc != null && (owner = getOwnerElement()) != null) { + if (owner instanceof StyleDefinerElement) { + StyleDefinerElement element = (StyleDefinerElement) owner; + element.resetLinkedSheet(); + } else if ("base".equalsIgnoreCase(owner.getLocalName())) { + if (value != null && value.length() != 0) { + // We set base to null first, in case setBaseURL(owner, base) fails + doc.setBaseURL(null); + doc.setBaseURL(owner, value); + } else { + doc.setBaseURL(null); + } + onBaseModify(doc); + } + if (owner instanceof CachedXHTMLElement) { + ((CachedXHTMLElement) owner).onStyleModify(); + } + } + } + + static void onBaseModify(XHTMLDocument doc) { + Iterator links = doc.linkedStyle.iterator(); + while (links.hasNext()) { + links.next().resetLinkedSheet(); + } + } + +} diff --git a/src/io/sf/carte/doc/dom4j/ImgElement.java b/src/io/sf/carte/doc/dom4j/ImgElement.java index cd9a82e..a6f8ba8 100644 --- a/src/io/sf/carte/doc/dom4j/ImgElement.java +++ b/src/io/sf/carte/doc/dom4j/ImgElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -35,8 +35,8 @@ class ImgElement extends XHTMLElement { } ImgElement(QName qname, int attributeCount) { - super(qname, attributeCount); - } + super(qname, attributeCount); + } @Override public boolean hasPresentationalHints() { diff --git a/src/io/sf/carte/doc/dom4j/LinkElement.java b/src/io/sf/carte/doc/dom4j/LinkElement.java index 078a4eb..51c5876 100644 --- a/src/io/sf/carte/doc/dom4j/LinkElement.java +++ b/src/io/sf/carte/doc/dom4j/LinkElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -11,10 +11,12 @@ package io.sf.carte.doc.dom4j; +import java.io.IOException; import java.net.URL; import org.dom4j.QName; import org.w3c.css.sac.CSSException; +import org.w3c.dom.DOMException; import io.sf.carte.doc.style.css.MediaQueryList; import io.sf.carte.doc.style.css.om.AbstractCSSStyleSheet; @@ -45,9 +47,10 @@ class LinkElement extends StyleDefinerElement { /** * Gets the associated style sheet for the node. * - * @return the associated style sheet for the node, or null if the sheet is - * not CSS or the media attribute was not understood. If the URL is invalid or the - * sheet could not be parsed, the returned sheet will be empty. + * @return the associated style sheet for the node, or null if the + * sheet is not CSS or the media attribute was not understood. If the + * URL is invalid or the sheet could not be parsed, the returned sheet + * will be empty. */ @Override public AbstractCSSStyleSheet getSheet() { @@ -110,7 +113,13 @@ private void loadStyleSheet(String href, String title) { if (media == null || media.trim().length() == 0) { mediaList = MediaList.createMediaList(); } else { - mediaList = getDocumentFactory().getStyleSheetFactory().createMediaQueryList(media, this); + try { + mediaList = getDocumentFactory().getStyleSheetFactory().createImmutableMediaQueryList(media, this); + } catch (CSSException e) { + getErrorHandler().linkedStyleError(this, e.getMessage()); + linkedSheet = null; + return; + } if (mediaList.isNotAllMedia() && mediaList.hasErrors()) { linkedSheet = null; return; @@ -130,10 +139,18 @@ private void loadStyleSheet(String href, String title) { } try { URL url = getOwnerDocument().getURL(href); - linkedSheet.setHref(url.toExternalForm()); - linkedSheet.loadStyleSheet(url, referrerPolicy); + if (getOwnerDocument().isAuthorizedOrigin(url)) { + linkedSheet.setHref(url.toExternalForm()); + linkedSheet.loadStyleSheet(url, referrerPolicy); + } else { + getErrorHandler().policyError(this, "Unauthorized URL: " + url.toExternalForm()); + } } catch (CSSException e) { getErrorHandler().linkedSheetError(e, linkedSheet); + } catch (IOException e) { + getErrorHandler().ioError(href, e); + } catch (DOMException e) { + // Already logged } catch (Exception e) { getErrorHandler().linkedSheetError(e, linkedSheet); } diff --git a/src/io/sf/carte/doc/dom4j/StyleAttribute.java b/src/io/sf/carte/doc/dom4j/StyleAttribute.java index d285674..b31ee61 100644 --- a/src/io/sf/carte/doc/dom4j/StyleAttribute.java +++ b/src/io/sf/carte/doc/dom4j/StyleAttribute.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -54,6 +54,9 @@ public CSSStylableElement getOwnerElement() { @Override public String getValue() { + if (inlineStyle == null || inlineStyle.getLength() == 0) { + return super.getValue(); + } return getStyle().getCssText(); } diff --git a/src/io/sf/carte/doc/dom4j/StyleDefinerElement.java b/src/io/sf/carte/doc/dom4j/StyleDefinerElement.java index 7ff9d65..0b1f496 100644 --- a/src/io/sf/carte/doc/dom4j/StyleDefinerElement.java +++ b/src/io/sf/carte/doc/dom4j/StyleDefinerElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause diff --git a/src/io/sf/carte/doc/dom4j/StyleElement.java b/src/io/sf/carte/doc/dom4j/StyleElement.java index 0ef8060..a8055be 100644 --- a/src/io/sf/carte/doc/dom4j/StyleElement.java +++ b/src/io/sf/carte/doc/dom4j/StyleElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -16,6 +16,7 @@ import java.io.StringReader; import org.dom4j.QName; +import org.w3c.css.sac.CSSException; import org.w3c.css.sac.InputSource; import org.w3c.dom.DOMException; @@ -45,12 +46,44 @@ class StyleElement extends StyleDefinerElement { super(qname, attributeCount); } + @Override + public void normalize() { + if (!containsCSS()) { + super.normalize(); + } else { + // Local reference to sheet, to avoid race conditions. + final AbstractCSSStyleSheet sheet = getSheet(); + if (sheet == null) { + super.normalize(); + } else { + super.setText(sheet.toString()); + } + } + } + + private boolean containsCSS() { + if (linkedSheet != null) { + // Local reference to sheet, to avoid race conditions. + final AbstractCSSStyleSheet sheet; + String type = attributeValue("type"); + if ("text/css".equalsIgnoreCase(type) || ((type == null || type.length() == 0) + && (sheet = getSheet()) != null && sheet.getCssRules().getLength() != 0)) { + return true; + } + } + /* + * If the sheet has not been processed yet, we return false as well. + */ + return false; + } + /** * Gets the associated style sheet for the node. * - * @return the associated style sheet for the node, or null if the sheet is - * not CSS or the media attribute was not understood. If the element is empty or - * the sheet could not be parsed, the returned sheet will be empty. + * @return the associated style sheet for the node, or null if the + * sheet is not CSS or the media attribute was not understood. If the + * element is empty or the sheet could not be parsed, the returned sheet + * will be empty. */ @Override public AbstractCSSStyleSheet getSheet() { @@ -60,7 +93,7 @@ public AbstractCSSStyleSheet getSheet() { return null; } String type = attributeValue("type"); - if (!"text/css".equalsIgnoreCase(type)) { + if (type != null && !"text/css".equalsIgnoreCase(type) && type.length() != 0) { return null; } String media = attributeValue("media"); @@ -68,7 +101,12 @@ public AbstractCSSStyleSheet getSheet() { if (media == null || media.trim().length() == 0) { mediaList = MediaList.createMediaList(); } else { - mediaList = getDocumentFactory().getStyleSheetFactory().createMediaQueryList(media, this); + try { + mediaList = getDocumentFactory().getStyleSheetFactory().createImmutableMediaQueryList(media, this); + } catch (CSSException e) { + getErrorHandler().linkedStyleError(this, e.getMessage()); + return null; + } if (mediaList.isNotAllMedia() && mediaList.hasErrors()) { return null; } diff --git a/src/io/sf/carte/doc/dom4j/TableCellElement.java b/src/io/sf/carte/doc/dom4j/TableCellElement.java index edb657d..2afc507 100644 --- a/src/io/sf/carte/doc/dom4j/TableCellElement.java +++ b/src/io/sf/carte/doc/dom4j/TableCellElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -34,14 +34,14 @@ protected TableCellElement(QName qname) { super(qname); } - TableCellElement(QName qname, int attributeCount) { - super(qname, attributeCount); - } + TableCellElement(QName qname, int attributeCount) { + super(qname, attributeCount); + } @Override public boolean hasPresentationalHints() { - return hasAttribute("bgcolor") || hasAttribute("width") || hasAttribute("height") - || hasAttribute("background") || hasAttribute("align"); + return hasAttribute("bgcolor") || hasAttribute("width") || hasAttribute("height") || hasAttribute("background") + || hasAttribute("align"); } @Override diff --git a/src/io/sf/carte/doc/dom4j/TableElement.java b/src/io/sf/carte/doc/dom4j/TableElement.java index 3ab4d7a..aaf1b3c 100644 --- a/src/io/sf/carte/doc/dom4j/TableElement.java +++ b/src/io/sf/carte/doc/dom4j/TableElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -34,15 +34,14 @@ protected TableElement(QName qname) { super(qname); } - TableElement(QName qname, int attributeCount) { - super(qname, attributeCount); - } + TableElement(QName qname, int attributeCount) { + super(qname, attributeCount); + } @Override public boolean hasPresentationalHints() { - return hasAttribute("width") || hasAttribute("height") || hasAttribute("cellspacing") - || hasAttribute("border") || hasAttribute("bordercolor") || hasAttribute("bgcolor") - || hasAttribute("background"); + return hasAttribute("width") || hasAttribute("height") || hasAttribute("cellspacing") || hasAttribute("border") + || hasAttribute("bordercolor") || hasAttribute("bgcolor") || hasAttribute("background"); } @Override diff --git a/src/io/sf/carte/doc/dom4j/TableRowElement.java b/src/io/sf/carte/doc/dom4j/TableRowElement.java index 4d29c85..1f4ff69 100644 --- a/src/io/sf/carte/doc/dom4j/TableRowElement.java +++ b/src/io/sf/carte/doc/dom4j/TableRowElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -34,14 +34,13 @@ protected TableRowElement(QName qname) { super(qname); } - TableRowElement(QName qname, int attributeCount) { - super(qname, attributeCount); - } + TableRowElement(QName qname, int attributeCount) { + super(qname, attributeCount); + } @Override public boolean hasPresentationalHints() { - return hasAttribute("bgcolor") || hasAttribute("height") || hasAttribute("background") - || hasAttribute("align"); + return hasAttribute("bgcolor") || hasAttribute("height") || hasAttribute("background") || hasAttribute("align"); } @Override diff --git a/src/io/sf/carte/doc/dom4j/XHTMLDocument.java b/src/io/sf/carte/doc/dom4j/XHTMLDocument.java index ff7b669..cd75b6d 100644 --- a/src/io/sf/carte/doc/dom4j/XHTMLDocument.java +++ b/src/io/sf/carte/doc/dom4j/XHTMLDocument.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -62,12 +62,14 @@ public class XHTMLDocument extends DOMDocument implements CSSDocument { public final static String XHTML_NAMESPACE_URI = "http://www.w3.org/1999/xhtml"; - private static final long serialVersionUID = 6L; + private static final long serialVersionUID = 7L; private DocumentCSSStyleSheet mergedStyleSheet = null; private int styleCacheSerial = Integer.MIN_VALUE; + private String documentURI = null; + private URL baseURL = null; Set linkedStyle = new LinkedHashSet(4); @@ -163,11 +165,11 @@ public Object setUserData(String key, Object data, UserDataHandler handler) { } /** - * A list containing all the style sheets explicitly linked into or embedded - * in a document. For HTML documents, this includes external style sheets, - * included via the HTML LINK element, and inline STYLE elements. In XML, - * this includes external style sheets, included via style sheet processing - * instructions (see [XML StyleSheet]). + * A list containing all the style sheets explicitly linked into or embedded in + * a document. For HTML documents, this includes external style sheets, included + * via the HTML LINK element, and inline STYLE elements. In XML, this includes + * external style sheets, included via style sheet processing instructions (see + * [XML StyleSheet]). */ @Override public StyleSheetList getStyleSheets() { @@ -177,10 +179,10 @@ public StyleSheetList getStyleSheets() { return sheets; } - protected void updateStyleLists() { + private void updateStyleLists() { /* - * Add the linked and embedded styles. Must be added in this order, as - * mandated by the CSS spec. + * Add the linked and embedded styles. Must be added in this order, as mandated + * by the CSS spec. */ sheets.clear(); // Add styles referenced by links @@ -215,11 +217,11 @@ private void addLinkedSheet(AbstractCSSStyleSheet linkedSheet) { } /** - * Gets the merged style sheet that applies to this document, resulting from - * the merge of the document's default style sheet, the document linked or - * embedded style sheets, and the non-important part of the user style - * sheet. Does not include overriden styles nor the 'important' part of the - * user-defined style sheet. + * Gets the merged style sheet that applies to this document, resulting from the + * merge of the document's default style sheet, the document linked or embedded + * style sheets, and the non-important part of the user style sheet. Does not + * include overriden styles nor the 'important' part of the user-defined style + * sheet. *

* The style sheet is lazily built. * @@ -250,22 +252,18 @@ private void mergeStyleSheets() { } /** - * Adds a style sheet (contained by the given InputSource) to the global - * style sheet defined by the document's default style sheet and all the - * linked and embedded styles. + * Adds a style sheet (contained by the given InputSource) to the global style + * sheet defined by the document's default style sheet and all the linked and + * embedded styles. * - * @param cssSrc - * the document's InputSource. - * @return true if the parsing reported no errors or fatal errors, false - * otherwise. - * @throws DOMException - * if a DOM problem is found parsing the sheet. - * @throws CSSException - * if a non-DOM problem is found parsing the sheet. - * @throws IOException - * if a problem is found reading the sheet. + * @param cssSrc the document's InputSource. + * @return true if the parsing reported no errors or fatal errors, + * false otherwise. + * @throws DOMException if a DOM problem is found parsing the sheet. + * @throws CSSException if a non-DOM problem is found parsing the sheet. + * @throws IOException if a problem is found reading the sheet. */ - public boolean addStyleSheet(InputSource cssSrc) throws DOMException, IOException { + public boolean addStyleSheet(InputSource cssSrc) throws DOMException, CSSException, IOException { String media = cssSrc.getMedia(); if (media != null && !"all".equalsIgnoreCase(media)) { // handle as media rule @@ -303,10 +301,10 @@ public DOMStringList getStyleSheetSets() { /** * Gets the title of the currently selected style sheet set. * - * @return the title of the currently selected style sheet, the empty string - * if none is selected, or null if there are style - * sheets from different style sheet sets that have their style - * sheet disabled flag unset. + * @return the title of the currently selected style sheet, the empty string if + * none is selected, or null if there are style sheets from + * different style sheet sets that have their style sheet disabled flag + * unset. */ @Override public String getSelectedStyleSheetSet() { @@ -334,13 +332,11 @@ public String getSelectedStyleSheetSet() { } /** - * Selects a style sheet set, disabling the other non-persistent sheet sets. - * If the name is the empty string, all non-persistent sheets will be - * disabled. Otherwise, if the name does not match any of the sets, does - * nothing. + * Selects a style sheet set, disabling the other non-persistent sheet sets. If + * the name is the empty string, all non-persistent sheets will be disabled. + * Otherwise, if the name does not match any of the sets, does nothing. * - * @param name - * the case-sensitive name of the set to select. + * @param name the case-sensitive name of the set to select. */ @Override public void setSelectedStyleSheetSet(String name) { @@ -373,11 +369,10 @@ public String getLastStyleSheetSet() { } /** - * Enables a style sheet set. If the name does not match any of the sets, - * does nothing. + * Enables a style sheet set. If the name does not match any of the sets, does + * nothing. * - * @param name - * the case-sensitive name of the set to enable. + * @param name the case-sensitive name of the set to enable. */ @Override public void enableStyleSheetsForSet(String name) { @@ -396,10 +391,7 @@ public void enableStyleSheetsForSet(String name) { } } - /* - * This method should only be called from HeadElement. - */ - void onEmbeddedStyleAdd(LinkStyle element) { + void onLinkStyleAdd(LinkStyle element) { if (element instanceof LinkElement) { linkedStyle.add((LinkElement) element); } else if (element instanceof StyleElement) { @@ -408,25 +400,27 @@ void onEmbeddedStyleAdd(LinkStyle element) { onStyleModify(); } - /* - * This method should only be called from HeadElement. - */ - void onEmbeddedStyleRemove(LinkStyle element) { + void onLinkStyleRemove(LinkStyle element) { + boolean removed; if (element instanceof LinkElement) { - linkedStyle.remove(element); + removed = linkedStyle.remove(element); } else if (element instanceof StyleElement) { - embeddedStyle.remove(element); - } - CSSStyleSheet sheet = (CSSStyleSheet) element.getSheet(); - if (sheet != null) { - String title = sheet.getTitle(); - if (title != null) { - sheets.remove(title); - } else { - sheets.remove(sheet); + removed = embeddedStyle.remove(element); + } else { + removed = false; + } + if (removed) { + CSSStyleSheet sheet = (CSSStyleSheet) element.getSheet(); + if (sheet != null) { + String title = sheet.getTitle(); + if (title != null) { + sheets.remove(title); + } else { + sheets.remove(sheet); + } } + onStyleModify(); } - onStyleModify(); } /** @@ -444,8 +438,8 @@ void onStyleModify() { /** * Gets the serial number for the document-wide merged style sheet. *

- * The serial number will be increased by one each time that any of the - * sheets that conform the style is changed. + * The serial number will be increased by one each time that any of the sheets + * that conform the style is changed. * * @return the serial number for the merged style sheet. */ @@ -456,22 +450,19 @@ int getStyleCacheSerial() { /** * Gets the override style sheet for an element and pseudo-element. *

- * The getOverrideStyle method provides a mechanism through which a DOM - * author could effect immediate change to the style of an element without - * modifying the explicitly linked style sheets of a document or the inline - * style of elements in the style sheets. + * The getOverrideStyle method provides a mechanism through which a DOM author + * could effect immediate change to the style of an element without modifying + * the explicitly linked style sheets of a document or the inline style of + * elements in the style sheets. *

*

- * The override style sheet comes after the author style sheet in the - * cascade algorithm. DOM Level 2. + * The override style sheet comes after the author style sheet in the cascade + * algorithm. DOM Level 2. *

* - * @param elt - * the element. - * @param pseudoElt - * the pseudo-element, or null if none. - * @return the override style sheet for the given element and - * pseudo-element. + * @param elt the element. + * @param pseudoElt the pseudo-element, or null if none. + * @return the override style sheet for the given element and pseudo-element. */ @Override public ExtendedCSSStyleDeclaration getOverrideStyle(Element elt, String pseudoElt) { @@ -486,8 +477,7 @@ public ExtendedCSSStyleDeclaration getOverrideStyle(Element elt, String pseudoEl * Gets the style database currently used to apply specific styles to this * document. * - * @return the style database, or null if no style database has been - * selected. + * @return the style database, or null if no style database has been selected. */ @Override public StyleDatabase getStyleDatabase() { @@ -514,10 +504,9 @@ public String getTargetMedium() { /** * Set the medium that will be used to compute the styles of this document. * - * @param medium - * the name of the target medium, like 'screen' or 'print'. - * @throws CSSMediaException - * if the document is unable to target the given medium. + * @param medium the name of the target medium, like 'screen' or 'print'. + * @throws CSSMediaException if the document is unable to target the given + * medium. */ @Override public void setTargetMedium(String medium) throws CSSMediaException { @@ -581,8 +570,8 @@ public ErrorHandler getErrorHandler() { /** * Has any of the linked or embedded style sheets any error or warning ? * - * @return true if any of the linked or embedded style sheets has any SAC or rule error - * or warning, false otherwise. + * @return true if any of the linked or embedded style sheets has + * any SAC or rule error or warning, false otherwise. */ @Override public boolean hasStyleIssues() { @@ -590,13 +579,14 @@ public boolean hasStyleIssues() { } public void onMetaAdded(String name, String attribute) { - if ("Default-Style".equalsIgnoreCase(name)) { + if ("default-style".equalsIgnoreCase(name)) { metaDefaultStyleSet = attribute; + onStyleModify(); } } public void onMetaRemoved(String name, String attribute) { - if ("Default-Style".equalsIgnoreCase(name)) { + if ("default-style".equalsIgnoreCase(name)) { metaDefaultStyleSet = ""; } } @@ -605,25 +595,52 @@ public void onMetaRemoved(String name, String attribute) { * Gets the base URL of this Document. *

* If the Document's head element has a base child - * element, the base URI is computed using the value of the href attribute - * of the base element. It can also be set with the + * element, the base URI is computed using the value of the href attribute of + * the base element. It can also be set with the * setBaseURL method. - *

- * In dom4j, the getDocumentURI method cannot be trusted to - * find the base URL. + *

* * @return the base URL, or null if no base URL could be found. */ @Override public URL getBaseURL() { + if (baseURL == null) { + String buri = documentURI; + XHTMLElement elm = getDocumentElement(); + if (elm != null) { + String attr = elm.getAttribute("xml:base"); + if (attr.length() != 0) { + if (setBaseURL(elm, attr)) { + return baseURL; + } + } else { + // BASE element + NodeList headnl = getElementsByTagName("head"); + if (headnl.getLength() != 0) { + NodeList nl = ((DOMElement) headnl.item(0)).getElementsByTagName("base"); + if (nl.getLength() != 0) { + CSSStylableElement base = (CSSStylableElement) nl.item(0); + String href = base.getAttribute("href"); + if (href.length() != 0 && setBaseURL(base, href)) { + return baseURL; + } + } + } + } + } + try { + baseURL = new URL(buri); + } catch (MalformedURLException e) { + } + } return baseURL; } /** * Gets the absolute base URI of this node. * - * @return the absolute base URI of this node, or null if an absolute URI - * could not be obtained. + * @return the absolute base URI of this node, or null if an absolute URI could + * not be obtained. */ @Override public String getBaseURI() { @@ -637,22 +654,78 @@ public String getBaseURI() { /** * Sets the Base URL of this Document. * - * @param baseURL - * the base URL. + * @param baseURL the base URL. */ public void setBaseURL(URL baseURL) { this.baseURL = baseURL; + HrefAttribute.onBaseModify(this); + } + + /** + * Set the {@code BASE} URL obtained from the {@code href} attribute of the + * given <base> element. + * + * @param element the <base> element. + * @param base the value of the {@code href} attribute. + * @return {@code true} if the {@code BASE} URL was set. + */ + boolean setBaseURL(Element element, String base) { + String docUri = getDocumentURI(); + if (docUri != null) { + URL docUrl; + try { + docUrl = new URL(docUri); + } catch (MalformedURLException e) { + return setBaseForNullDocumentURI(base, element); + } + URL urlBase; + try { + urlBase = new URL(docUrl, base); + } catch (MalformedURLException e) { + getErrorHandler().ioError(base, e); + return false; + } + String docscheme = docUrl.getProtocol(); + String bscheme = urlBase.getProtocol(); + if (!docscheme.equals(bscheme)) { + if (!bscheme.equals("https") && !bscheme.equals("http") && !docscheme.equals("file") + && !docscheme.equals("jar")) { + // Remote document wants to set a non-http base URI + getErrorHandler().policyError(element, + "Remote document wants to set a non-http base URL: " + urlBase.toExternalForm()); + return false; + } + } + baseURL = urlBase; + return true; + } else { + return setBaseForNullDocumentURI(base, element); + } + } + + private boolean setBaseForNullDocumentURI(String base, Element baseElement) { + try { + URL urlBase = new URL(base); + String scheme = urlBase.getProtocol(); + if (scheme.equals("https") || scheme.equals("http")) { + baseURL = urlBase; + return true; + } + // Remote document wants to set a non-http base URL + getErrorHandler().policyError(baseElement, "Untrusted document wants to set a non-http base URL: " + base); + } catch (MalformedURLException e) { + getErrorHandler().ioError(base, e); + } + return false; } /** * Gets an URL for the given URI, taking into account the Base URL if * appropriate. * - * @param uri - * the uri. + * @param uri the uri. * @return the absolute URL. - * @throws MalformedURLException - * if the uri was wrong. + * @throws MalformedURLException if the uri was wrong. */ @Override public URL getURL(String uri) throws MalformedURLException { @@ -671,8 +744,7 @@ public URL getURL(String uri) throws MalformedURLException { /** * Is the provided URL a safe origin to load certain external resources? * - * @param linkedURL - * the URL of the external resource. + * @param linkedURL the URL of the external resource. * * @return true if is a safe origin, false otherwise. */ @@ -693,8 +765,36 @@ public boolean isSafeOrigin(URL linkedURL) { } /** - * Get the referrer policy obtained through the 'Referrer-Policy' header or a meta - * element. + * Determine whether the retrieval of the given URL is authorized. + *

+ * If the URL's protocol is not {@code http} nor {@code https} and document's + * base URL's scheme is neither {@code file} nor {@code jar}, it is denied. + *

+ * + * @param url the URL to check. + * @return {@code true} if allowed. + */ + @Override + public boolean isAuthorizedOrigin(URL url) { + String scheme = url.getProtocol(); + if (documentURI != null) { + URL base = getBaseURL(); + String baseScheme = base.getProtocol(); + // To try to speed things up, only the parameter's scheme is compared + // case-insensitively + if (!scheme.equalsIgnoreCase("https") && !scheme.equalsIgnoreCase("http") && !baseScheme.equals("file") + && !baseScheme.equals("jar")) { + return false; + } + } else if (!scheme.equalsIgnoreCase("https") && !scheme.equalsIgnoreCase("http")) { + return false; + } + return true; + } + + /** + * Get the referrer policy obtained through the 'Referrer-Policy' header or a + * meta element. * * @return the referrer policy, or the empty string if none was specified. */ @@ -721,19 +821,23 @@ protected void setReferrerPolicyHeader(String policy) { } } + @Override + public void setDocumentURI(String documentURI) { + this.documentURI = documentURI; + setBaseURL(null); + } + @Override public String getDocumentURI() { - return null; + return documentURI; } /** * Opens a connection for the given URL. * - * @param url - * the URL to open a connection to. + * @param url the URL to open a connection to. * @return the URL connection. - * @throws IOException - * if the connection could not be opened. + * @throws IOException if the connection could not be opened. */ @Override public URLConnection openConnection(URL url) throws IOException { @@ -741,14 +845,12 @@ public URLConnection openConnection(URL url) throws IOException { } /** - * Opens an InputStream for the given URI, taking into account the Base URL - * if needed. + * Opens an InputStream for the given URI, taking into account the Base URL if + * needed. * - * @param uri - * the uri to open a connection. + * @param uri the uri to open a connection. * @return the InputStream. - * @throws IOException - * if the uri was wrong, or the stream could not be opened. + * @throws IOException if the uri was wrong, or the stream could not be opened. */ public InputStream openStream(String uri) throws IOException { return openConnection(getURL(uri)).getInputStream(); diff --git a/src/io/sf/carte/doc/dom4j/XHTMLDocumentFactory.java b/src/io/sf/carte/doc/dom4j/XHTMLDocumentFactory.java index 74e224e..ee22306 100644 --- a/src/io/sf/carte/doc/dom4j/XHTMLDocumentFactory.java +++ b/src/io/sf/carte/doc/dom4j/XHTMLDocumentFactory.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -68,7 +68,7 @@ public class XHTMLDocumentFactory extends DOMDocumentFactory { private boolean styleCacheOn = false; - private static transient XHTMLDocumentFactory singleton = new XHTMLDocumentFactory(); + private static final XHTMLDocumentFactory singleton = new XHTMLDocumentFactory(); protected XHTMLDocumentFactory() { this(EnumSet.noneOf(Parser2.Flag.class)); @@ -111,26 +111,26 @@ BaseDocumentCSSStyleSheet getDefaultStyleSheet(CSSDocument.ComplianceMode mode) } /** - * Indicates whether the stylable elements currently produced by this - * factory are cache-enabled or not. + * Indicates whether the stylable elements currently produced by this factory + * are cache-enabled or not. * - * @return true if the per-element cache is enabled, false otherwise. + * @return true if the per-element cache is enabled, + * false otherwise. */ public boolean isStyleCacheOn() { return styleCacheOn; } /** - * Can turn on or off the per-Element style caching capability (by default - * is off). + * Can turn on or off the per-Element style caching capability (by default is + * off). *

* Only applications that repeatedly call the * {@link CSSStylableElement#getComputedStyle()} method on the same Element * should turn it on. * - * @param onOff - * set to true to turn on the cache capability, to false to turn - * it off. + * @param onOff set to true to turn on the cache capability, to false to turn it + * off. */ public void setStyleCache(boolean onOff) { this.styleCacheOn = onOff; @@ -239,17 +239,22 @@ public XHTMLDocument createDocument(Element rootElement) { return mydoc; } + protected XHTMLDocument createDocument(DOMDocumentType documentType) { + XHTMLDocument document = new XHTMLDocument(documentType); + document.setDocumentFactory(this); + return document; + } + @Override public XHTMLDocument createDocument(String namespaceURI, String qualifiedName, DocumentType docType) throws DOMException { XHTMLDocument document; if (docType != null) { DOMDocumentType documentType = asDocumentType(docType); - document = new XHTMLDocument(documentType); + document = createDocument(documentType); } else { - document = new XHTMLDocument(); + document = createDocument(); } - document.setDocumentFactory(this); if (qualifiedName != null) { if (namespaceURI == null) { namespaceURI = ""; @@ -262,12 +267,13 @@ public XHTMLDocument createDocument(String namespaceURI, String qualifiedName, D @Override public Attribute createAttribute(Element owner, QName qname, String value) { String name = qname.getName(); - if (owner instanceof StyleDefinerElement) { - return new DocumentStyleEventAttribute(qname, value); - } else if ((name = qname.getName()).equals("href") && owner instanceof BaseURLElement) { - return new BaseHrefAttribute(qname, value); + if ((name = qname.getName()).equals("href")) { + return new HrefAttribute(qname, value); } else if (name.equalsIgnoreCase("style")) { return new StyleAttribute(qname, value); + } else if (name.equalsIgnoreCase("type") || name.equalsIgnoreCase("media") || name.equalsIgnoreCase("rel") + || name.equalsIgnoreCase("title")) { + return new DocumentStyleEventAttribute(qname, value); } else { return super.createAttribute(owner, qname, value); } @@ -325,8 +331,7 @@ protected BaseCSSStyleSheet createRuleStyleSheet(AbstractCSSRule ownerRule, Stri } @Override - protected BaseCSSStyleSheet createLinkedStyleSheet(Node ownerNode, String title, - MediaQueryList mediaList) { + protected BaseCSSStyleSheet createLinkedStyleSheet(Node ownerNode, String title, MediaQueryList mediaList) { return new MyDOM4JCSSStyleSheet(title, ownerNode, mediaList, null, CSSStyleSheetFactory.ORIGIN_AUTHOR); } @@ -499,11 +504,10 @@ private void mergeUserSheets() { } /** - * Sets a default HTML default style sheet as the user agent style - * sheet. + * Sets a default HTML default style sheet as the user agent style sheet. *

- * This is not necessary in the DOM4J backend, as that style sheet is - * loaded by default. + * This is not necessary in the DOM4J backend, as that style sheet is loaded by + * default. */ @Override public void setDefaultHTMLUserAgentSheet() { diff --git a/src/io/sf/carte/doc/dom4j/XHTMLElement.java b/src/io/sf/carte/doc/dom4j/XHTMLElement.java index ef0010f..c24774e 100644 --- a/src/io/sf/carte/doc/dom4j/XHTMLElement.java +++ b/src/io/sf/carte/doc/dom4j/XHTMLElement.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -86,15 +86,20 @@ public void setAttributeNS(String namespaceURI, String qualifiedName, String val @Override protected void childAdded(Node node) { super.childAdded(node); - if (node instanceof LinkStyle) { - getOwnerDocument().onEmbeddedStyleAdd((LinkStyle) node); + String nsUri; + XHTMLDocument doc; + if (node instanceof LinkStyle && (doc = getOwnerDocument()) != null + && ((nsUri = ((CSSStylableElement) node).getNamespaceURI()) == null || nsUri.length() == 0 + || nsUri.equals(doc.getDocumentElement().getNamespaceURI()))) { + doc.onLinkStyleAdd((LinkStyle) node); } } @Override protected void childRemoved(Node node) { - if (node instanceof LinkStyle) { - getOwnerDocument().onEmbeddedStyleRemove((LinkStyle) node); + XHTMLDocument doc; + if (node instanceof LinkStyle && (doc = getOwnerDocument()) != null) { + doc.onLinkStyleRemove((LinkStyle) node); } super.childRemoved(node); } diff --git a/src/io/sf/carte/doc/dom4j/package-info.java b/src/io/sf/carte/doc/dom4j/package-info.java index fdc64b7..dcf18bf 100644 --- a/src/io/sf/carte/doc/dom4j/package-info.java +++ b/src/io/sf/carte/doc/dom4j/package-info.java @@ -1,6 +1,5 @@ /** - * Built on top of the DOM4J package, provides XHTML parsing with built-in - * support for CSS style sheets. + * Built on top of the DOM4J package, provides it with built-in support for CSS style sheets. *

* DOM4J is an XML DOM-like software package with support for Java * language constructs, like Collections. See @@ -8,49 +7,9 @@ * for more information. *

*

- * This implementation integrates DOM4J with the CSS DOM implementation found in + * This implementation integrates DOM4J with the CSSOM implementation found in * the io.sf.carte.doc.style.css package and subpackages. *

- *

Short example

- *

- * This is the easiest way to use this package: - *

- * - *
- Reader re = ...  [reader for XHTML document]
- InputSource source = new InputSource(re);
- SAXReader reader = new SAXReader(XHTMLDocumentFactory.getInstance());
- reader.setEntityResolver(new DefaultEntityResolver());
- Document document = reader.read(source);
- * 
- *

- * And once you got the element you want style for (see, for example, the - * DOM4J Quick Start Guide), just get it: - *

- * - *
- * CSSComputedProperties style = ((CSSStylableElement) element).getComputedStyle();
- * String propertyValue = style.getPropertyValue("display");
- * 
- * - *

Non-standard interfaces

- *

- * The CSSComputedProperties interface is a convenient extension to W3C's - * CSSStyleDeclaration. - *

- *

- * Some CSS default values are system-dependent, and there are other processes that rely - * on device-specific information. This information can be provided by a user-supplied - * DeviceFactory object. You can set the appropriate - * DeviceFactory at the XHTMLDocumentFactory with the - * setDeviceFactory method. - *

- * If you use the AWT, you may consider using the the AWT module. For example: - * - *

- * Color color = AWTHelper.getAWTColor(style.getCSSColor());
- * 
* * Read the documentation of the individual classes for information on * additional capabilities, like caching or the use of customized style sheets. diff --git a/src/module-info.java b/src/module-info.java index 840082f..a1c5663 100644 --- a/src/module-info.java +++ b/src/module-info.java @@ -1,6 +1,6 @@ /* - Copyright (c) 2005-2019, Carlos Amengual. + Copyright (c) 2005-2022, Carlos Amengual. SPDX-License-Identifier: BSD-3-Clause @@ -13,5 +13,8 @@ exports io.sf.carte.doc.dom4j; requires transitive io.sf.carte.css4j.agent.net; - requires transitive dom4j; + requires transitive org.dom4j; + requires static org.xmlpull.mxp1; + requires static org.xmlpull.v1; + requires java.xml; } diff --git a/src/overview.html b/src/overview.html index a8b6d13..3036fb3 100644 --- a/src/overview.html +++ b/src/overview.html @@ -3,16 +3,79 @@ CSS4J-DOM4J Overview -This module implements the W3C's -CSS - Object Model API for DOM4J. -

Just use the XHTMLDocumentFactory as you would use DOMDocumentFactory, -and the produced documents (and elements) will be style-aware.

-

There is more information in the description of the io.sf.carte.doc.dom4j subpackage.

-

Media handling

-

By default, computed styles only take into account generic styles that are -common to all media. If you want to target a specific medium, you have to use -the setTargetMedium() method of the document, which in DOM4J can -be done with the XHTMLDocument.setTargetMedium() method.

+

Built on top of the DOM4J package, gives DOM4J built-in support for CSS style +sheets. You can use the XHTMLDocumentFactory as you would use a +DOMDocumentFactory, and the produced documents (and elements) will +be style-aware.

+

To use the library with dom4j (using dom4j-like documents, elements and factory) you need the css4j-dom4j module in addition to the core +module. The -dom4j module optionally depends on the -agent module, you do not need it unless you plan to use the DOM4J agent.

+
+

Example with DOM4J

+

This is the easiest way to use this package with DOM4J, using that library's SAXReader:

+
+Reader re = ... [reader for XHTML document]
+InputSource source = new InputSource(re);
+SAXReader reader = new SAXReader(XHTMLDocumentFactory.getInstance());
+reader.setEntityResolver(new DefaultEntityResolver());
+XHTMLDocument document = (XHTMLDocument) reader.read(source);
+
+

And once you got the element you want the computed style for (see, for example, +the DOM4J Quick Start Guide), +just get it with a procedure analogous to the +ViewCSS +interface:

+
+CSSComputedProperties style = ((CSSStylableElement) element).getComputedStyle(null);
+String display = style.getPropertyValue("display");
+
+

It is also possible to parse an HTML5 document into a css4j-dom4j tree with the validator.nu HTML5 parser:

+
+XHTMLDocumentFactory factory = XHTMLDocumentFactory.getInstance();
+// Next line is optional: default is TRUE, and is probably what you want
+// factory.setLenientSystemValues(false);
+HtmlDocumentBuilder builder = new HtmlDocumentBuilder(factory);
+// We do not set the EntityResolver, the HtmlDocumentBuilder does not need it
+Reader re = ... [reader for HTML document]
+InputSource source = new InputSource(re);
+XHTMLDocument document = (XHTMLDocument) builder.parse(source);
+
+

Or use a SAX parser to parse an XML document into a css4j-dom4j tree, with +XMLDocumentBuilder +instead of dom4j's SAXReader:

+
+XHTMLDocumentFactory factory = XHTMLDocumentFactory.getInstance();
+// Next line is optional: default is TRUE, and is probably what you want
+// factory.setLenientSystemValues(false);
+XMLDocumentBuilder builder = new XMLDocumentBuilder(factory);
+builder.setEntityResolver(new DefaultEntityResolver());
+Reader re = ... [reader for XML document]
+InputSource source = new InputSource(re);
+XHTMLDocument document = (XHTMLDocument) builder.parse(source);
+re.close();
+
+

The code above uses the default JAXP SAX parser. You could use a different SAXParserFactory:

+
+SAXParserFactory parserFactory = ...
+XMLDocumentBuilder builder = new XMLDocumentBuilder(factory, parserFactory);
+
+
+
+

Clearing the User Agent sheet

+

A default user agent sheet is automatically loaded by the style sheet factory in +css4j-dom4j, something that does not happen with the other back-ends where you have to do +that explicitly (the css4j-dom4j behaviour is kept for backwards compatibility). To clear +that sheet, first obtain an instance of the factory that you are using: +

+
+XHTMLDocumentFactory docFactory = XHTMLDocumentFactory.getInstance();
+
+

and then, get the user agent sheet and clear the rule list:

+
+docFactory.getStyleSheetFactory().getUserAgentStyleSheet(CSSDocument.ComplianceMode.STRICT).getCssRules().clear();
+
+

This is assuming the STRICT mode, i.e. that you use a DOCTYPE, +otherwise use QUIRKS. +

+