├── .detekt └── config.yml ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── example_aggregated_report.png ├── jacocoaggregatecoverageplugin ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── com │ │ └── azizutku │ │ └── jacocoaggregatecoverageplugin │ │ ├── AggregateJacocoReportsTask.kt │ │ ├── CopyJacocoReportsTask.kt │ │ ├── JacocoAggregateCoveragePlugin.kt │ │ ├── extensions │ │ └── JacocoAggregateCoveragePluginExtension.kt │ │ ├── models │ │ ├── CoverageMetrics.kt │ │ └── ModuleCoverageRow.kt │ │ └── utils │ │ └── HtmlCodeGenerator.kt │ └── resources │ ├── html │ └── index.html │ └── jacoco-resources │ ├── branchfc.gif │ ├── branchnc.gif │ ├── branchpc.gif │ ├── bundle.gif │ ├── class.gif │ ├── down.gif │ ├── greenbar.gif │ ├── group.gif │ ├── method.gif │ ├── package.gif │ ├── prettify.css │ ├── prettify.js │ ├── redbar.gif │ ├── report.css │ ├── report.gif │ ├── session.gif │ ├── sort.gif │ ├── sort.js │ ├── source.gif │ └── up.gif └── settings.gradle.kts /.detekt/config.yml: -------------------------------------------------------------------------------- 1 | build: 2 | maxIssues: 0 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout Project 19 | uses: actions/checkout@v3.5.2 20 | 21 | - name: Validate Gradle Wrapper 22 | uses: gradle/wrapper-validation-action@v1.0.6 23 | 24 | - name: Configure JDK 25 | uses: actions/setup-java@v3.11.0 26 | with: 27 | distribution: 'temurin' 28 | java-version: '11' 29 | cache: gradle 30 | 31 | - name: Grant execute permission for gradlew 32 | run: chmod +x gradlew 33 | 34 | - name: Run build with caching enabled 35 | uses: gradle/gradle-build-action@v2.4.2 36 | with: 37 | arguments: clean build -s 38 | 39 | - name: Run Detekt 40 | run: ./gradlew detektMain 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release CI 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 45 12 | 13 | steps: 14 | - name: Checkout Project 15 | uses: actions/checkout@v4.0.0 16 | 17 | - name: Validate Gradle Wrapper 18 | uses: gradle/wrapper-validation-action@v1.1.0 19 | 20 | - name: Configure JDK 21 | uses: actions/setup-java@v3.12.0 22 | with: 23 | distribution: 'temurin' 24 | java-version: '17' 25 | cache: gradle 26 | 27 | - name: Grant execute permission for gradlew 28 | run: chmod +x gradlew 29 | 30 | - name: Create Release 31 | id: create_release 32 | uses: actions/create-release@v1.1.4 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | with: 36 | tag_name: ${{ github.ref }} 37 | release_name: ${{ github.ref }} 38 | draft: true 39 | prerelease: false 40 | 41 | - name: Publish Gradle Plugin 42 | env: 43 | GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} 44 | GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} 45 | run: ./gradlew -Dgradle.publish.key=$GRADLE_PUBLISH_KEY -Dgradle.publish.secret=$GRADLE_PUBLISH_SECRET publishPlugins 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.aab 3 | *.apk 4 | *.ap_ 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | **/build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # IntelliJ 37 | *.iml 38 | .idea/ 39 | 40 | # Keystore files 41 | *.jks 42 | 43 | # External native build folder generated in Android Studio 2.2 and later 44 | .externalNativeBuild 45 | 46 | # C++ source code files 47 | .cxx 48 | 49 | # macOS 50 | .DS_Store 51 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 2 | 3 | ### Added 4 | - Initial release of the plugin 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (c) 2023 Aziz Utku Kagitci 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | --- 25 | 26 | Third-Party Software Licenses 27 | ============================= 28 | 29 | JaCoCo License 30 | -------------- 31 | 32 | Copyright (c) 2009, 2023 Mountainminds GmbH & Co. KG and Contributors 33 | 34 | The JaCoCo Java Code Coverage Library and all included documentation is made 35 | available by Mountainminds GmbH & Co. KG, Munich. Except indicated below, the 36 | Content is provided to you under the terms and conditions of the Eclipse Public 37 | License Version 2.0 ("EPL"). A copy of the EPL is available at 38 | [https://www.eclipse.org/legal/epl-2.0/](https://www.eclipse.org/legal/epl-2.0/). 39 | 40 | Please visit 41 | [http://www.jacoco.org/jacoco/trunk/doc/license.html](http://www.jacoco.org/jacoco/trunk/doc/license.html) 42 | for the complete license information including third party licenses and trademarks. 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Android Weekly #604](https://androidweekly.net/issues/issue-604/badge)](https://androidweekly.net/issues/issue-604) 2 | [![Kotlin Weekly #388](https://img.shields.io/badge/Featured%20in%20kotlinweekly.net-Issue%20%23388-orange)](https://mailchi.mp/kotlinweekly/kotlin-weekly-388) 3 | 4 | ## JaCoCo Aggregate Coverage Plugin 5 | The plugin streamlines the generation of a unified code coverage report across multi-module Android projects. It collects and combines coverage data generated by [JaCoCo](https://github.com/jacoco/jacoco) from all subprojects, providing a detailed and comprehensive view of your project's test coverage. 6 | 7 | ## Features 8 | - Aggregates JaCoCo coverage reports from multiple modules. 9 | - Generates a unified report for overall test coverage visualization. 10 | - Ideal for multi-module Android projects. 11 | - Easy to apply. 12 | 13 | > Note: This plugin does not configure the JaCoCo plugin automatically. You must configure the JaCoCo plugin manually within your project. The JaCoCo Aggregate Coverage Plugin requires the JaCoCo report task to be set up since it utilizes these tasks to aggregate reports. 14 | 15 | # 1) Getting started 16 | Integrate the JaCoCo Aggregate Coverage Plugin into your project with these steps: 17 | 18 | ### Step 1: Update the root `build.gradle[.kts]` file 19 | In your root `build.gradle[.kts]` file, add the following plugin configuration: 20 | 21 | ```gradle 22 | plugins { 23 | ... 24 | id "com.azizutku.jacocoaggregatecoverageplugin" version "[write the latest version]" apply true 25 | } 26 | ``` 27 | 28 | ### Step 2: Configure the plugin in the root `build.gradle[.kts]` file 29 | ```kotlin 30 | jacocoAggregateCoverage { 31 | jacocoTestReportTask.set("YOUR_JACOCO_TEST_REPORT_TASK") 32 | // Other optional configurations 33 | } 34 | ``` 35 | 36 | # 2) Usage 37 | After applying and configuring the plugin, you can generate the aggregated report by running the following command: 38 | 39 | ```bash 40 | ./gradlew aggregateJacocoReports 41 | ``` 42 | The unified report will be generated at **`build/reports/jacocoAggregated/index.html`** in the root project. 43 | 44 | # Configuration 45 | Configure the plugin in your root-level `build.gradle[.kts]` file: 46 | ```kotlin 47 | jacocoAggregateCoverage { 48 | jacocoTestReportTask.set("jacocoTestDebugUnitTestReport") 49 | // Add the report directory only if you have a custom directory set 50 | configuredCustomReportsDirectory.set("customJacocoReportDir") 51 | // Specify the HTML output location if you have a custom one 52 | configuredCustomHtmlOutputLocation.set("jacocoCustomHtmlFolder") 53 | } 54 | ``` 55 | - **jacocoTestReportTask**: Required. Specify the task used to generate the JaCoCo report. 56 | - **configuredCustomReportsDirectory**: Optional. Set this if you have a custom reports directory. 57 | - **configuredCustomHtmlOutputLocation**: Optional. Set this if you have a custom directory for the HTML report output. 58 | 59 | ## Example Aggregated Report 60 | ![Example Aggregated Report](images/example_aggregated_report.png) 61 | 62 | # Contribution 63 | Contributions are welcome! Please feel free to submit a pull request or open an issue on the GitHub repository. 64 | 65 | # License 66 | This project is licensed under the MIT License, allowing modification, distribution, and use in your own projects. Additionally, it incorporates resources and code from JaCoCo, which is covered by the Eclipse Public License v2.0. For detailed license terms, please refer to the [LICENSE](https://github.com/azizutku/jacoco-aggregate-coverage-plugin/blob/main/LICENSE) file. 67 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.jvm).apply(false) 3 | alias(libs.plugins.gradle.publish).apply(false) 4 | alias(libs.plugins.detekt).apply(false) 5 | } 6 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "1.9.10" 3 | androidGradlePlugin = "8.1.2" 4 | gradlePublish = "1.2.0" 5 | detekt = "1.22.0" 6 | 7 | [libraries] 8 | # Android Gradle Api Plugin 9 | android-gradle-api = { group = "com.android.tools.build", name = "gradle-api", version.ref = "androidGradlePlugin" } 10 | 11 | # Detekt plugins 12 | detekt-formating = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" } 13 | detekt-rules = { group = "io.gitlab.arturbosch.detekt", name = "detekt-rules", version.ref = "detekt" } 14 | 15 | [plugins] 16 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 17 | gradle-publish = { id = "com.gradle.plugin-publish", version.ref = "gradlePublish" } 18 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } 19 | 20 | [bundles] 21 | detekt = ["detekt.formating", "detekt.rules"] -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /images/example_aggregated_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/images/example_aggregated_report.png -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | import io.gitlab.arturbosch.detekt.extensions.DetektExtension 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | 5 | plugins { 6 | id("com.gradle.plugin-publish") 7 | id("io.gitlab.arturbosch.detekt") 8 | `kotlin-dsl` 9 | `maven-publish` 10 | } 11 | 12 | dependencies { 13 | compileOnly(libs.android.gradle.api) 14 | detektPlugins(libs.bundles.detekt) 15 | implementation("org.jsoup:jsoup:1.16.2") 16 | } 17 | 18 | java { 19 | sourceCompatibility = JavaVersion.VERSION_11 20 | targetCompatibility = JavaVersion.VERSION_11 21 | } 22 | 23 | tasks.withType().configureEach { 24 | compilerOptions { 25 | jvmTarget.set(JvmTarget.JVM_11) 26 | } 27 | } 28 | 29 | gradlePlugin { 30 | website.set("https://github.com/azizutku/jacoco-aggregate-coverage-plugin") 31 | vcsUrl.set("https://github.com/azizutku/jacoco-aggregate-coverage-plugin.git") 32 | plugins { 33 | create("JacocoAggregateCoveragePlugin") { 34 | id = "com.azizutku.jacocoaggregatecoverageplugin" 35 | displayName = "Jacoco Aggregate Coverage Plugin" 36 | description = "The JaCoCo Aggregate Coverage Plugin simplifies the process of " + 37 | "generating a unified code coverage report for multi-module Gradle projects. " + 38 | "Leveraging the power of JaCoCo, it seamlessly aggregates coverage data " + 39 | "across all subprojects, creating a comprehensive overview of your project's " + 40 | "test coverage. This plugin is ideal for large-scale projects where insight " + 41 | "into overall code quality is essential." 42 | implementationClass = 43 | "com.azizutku.jacocoaggregatecoverageplugin.JacocoAggregateCoveragePlugin" 44 | tags.set( 45 | listOf( 46 | "jacoco", "coverage", "code-coverage", "report", "aggregation", 47 | "unified-report", "multi-module", "test-coverage", "aggregated-test-coverage", 48 | "unified-test-coverage", "android", "kotlin" 49 | ) 50 | ) 51 | } 52 | } 53 | } 54 | 55 | configure { 56 | source = project.files("src/main/kotlin") 57 | buildUponDefaultConfig = true 58 | allRules = false 59 | config = files("$rootDir/.detekt/config.yml") 60 | baseline = file("$rootDir/.detekt/baseline.xml") 61 | } 62 | 63 | group = "com.azizutku.jacocoaggregatecoverageplugin" 64 | version = "0.1.0" 65 | -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/kotlin/com/azizutku/jacocoaggregatecoverageplugin/AggregateJacocoReportsTask.kt: -------------------------------------------------------------------------------- 1 | package com.azizutku.jacocoaggregatecoverageplugin 2 | 3 | import com.azizutku.jacocoaggregatecoverageplugin.extensions.JacocoAggregateCoveragePluginExtension 4 | import com.azizutku.jacocoaggregatecoverageplugin.models.CoverageMetrics 5 | import com.azizutku.jacocoaggregatecoverageplugin.models.ModuleCoverageRow 6 | import com.azizutku.jacocoaggregatecoverageplugin.utils.HtmlCodeGenerator 7 | import org.gradle.api.DefaultTask 8 | import org.gradle.api.Project 9 | import org.gradle.api.file.ConfigurableFileCollection 10 | import org.gradle.api.file.DirectoryProperty 11 | import org.gradle.api.provider.Property 12 | import org.gradle.api.tasks.CacheableTask 13 | import org.gradle.api.tasks.InputDirectory 14 | import org.gradle.api.tasks.InputFiles 15 | import org.gradle.api.tasks.OutputDirectory 16 | import org.gradle.api.tasks.PathSensitive 17 | import org.gradle.api.tasks.PathSensitivity 18 | import org.gradle.api.tasks.TaskAction 19 | import org.jsoup.Jsoup 20 | import java.io.File 21 | import java.io.IOException 22 | 23 | private const val TOTAL_COVERAGE_PLACEHOLDER = "TOTAL_COVERAGE_PLACEHOLDER" 24 | private const val LINKED_MODULES_PLACEHOLDER = "LINKED_MODULES_PLACEHOLDER" 25 | 26 | /** 27 | * A Gradle task that generates a unified HTML report from individual JaCoCo report files. 28 | * This task is responsible for collating the coverage metrics from multiple subprojects 29 | * and presenting them in a single, easily navigable HTML document. 30 | */ 31 | @CacheableTask 32 | internal abstract class AggregateJacocoReportsTask : DefaultTask() { 33 | /** 34 | * The source folder that contains the plugin's resources. 35 | */ 36 | @get:InputDirectory 37 | @get:PathSensitive(PathSensitivity.RELATIVE) 38 | abstract val pluginSourceFolder: Property 39 | 40 | /** 41 | * The set of JaCoCo report files for Gradle's incremental build checks. 42 | */ 43 | @get:InputFiles 44 | @get:PathSensitive(PathSensitivity.RELATIVE) 45 | abstract val jacocoReportsFileCollection: ConfigurableFileCollection 46 | 47 | /** 48 | * The directory where the aggregated JaCoCo reports will be generated and stored. 49 | */ 50 | @get:OutputDirectory 51 | abstract val outputDirectory: DirectoryProperty 52 | 53 | /** 54 | * Lazily instantiated [HtmlCodeGenerator] for generating report HTML. 55 | */ 56 | private val htmlCodeGenerator by lazy { HtmlCodeGenerator() } 57 | 58 | /** 59 | * A lazy-initialized map of subproject paths to their respective coverage metrics. 60 | * This map is used for generating the aggregated report. 61 | */ 62 | private val subprojectToCoverageMap: Map by lazy { 63 | project.subprojects.associate { 64 | it.path to parseCoverageMetrics(it) 65 | }.filterValues { it != null }.mapValues { it.value!! } 66 | } 67 | 68 | /** 69 | * Executes the task action to create the unified index HTML. 70 | * This function orchestrates the reading of individual coverage reports, 71 | * aggregates the coverage data, and produces a single index HTML file 72 | * that represents the aggregated coverage information. 73 | */ 74 | @TaskAction 75 | fun createUnifiedIndexHtml() { 76 | val maximumInstructionTotal = subprojectToCoverageMap.values.maxOfOrNull { 77 | it.instructionsTotal 78 | } ?: 0 79 | val maximumBranchesTotal = subprojectToCoverageMap.values.maxOfOrNull { 80 | it.branchesTotal 81 | } ?: 0 82 | 83 | val tableBodyForModules = 84 | subprojectToCoverageMap.entries 85 | .joinToString(separator = "\n") { (moduleName, moduleCoverage) -> 86 | createModuleCoverageRow( 87 | moduleName = moduleName, 88 | moduleCoverage = moduleCoverage, 89 | maxInstructionTotal = maximumInstructionTotal, 90 | maxBranchesTotal = maximumBranchesTotal, 91 | ) 92 | } 93 | 94 | val unifiedMetrics = 95 | subprojectToCoverageMap.values.fold(CoverageMetrics()) { acc, metrics -> 96 | acc + metrics 97 | } 98 | 99 | updateBreadcrumbs() 100 | buildUnifiedIndexHtml(unifiedMetrics, tableBodyForModules) 101 | } 102 | 103 | /** 104 | * Updates the breadcrumbs for report navigation in the generated HTML files. 105 | * This method modifies the individual index HTML files of the subprojects 106 | * to include a link back to the root unified report. 107 | */ 108 | private fun updateBreadcrumbs() { 109 | project.subprojects.forEach { subproject -> 110 | val indexHtmlFile = outputDirectory.dir(subproject.path) 111 | .get().file("index.html").asFile 112 | if (indexHtmlFile.exists().not()) { 113 | return@forEach 114 | } 115 | val document = Jsoup.parse(indexHtmlFile, Charsets.UTF_8.name()) 116 | val spanElement = document.select("span.el_report").first() 117 | 118 | if (spanElement != null) { 119 | val newAnchor = document.createElement("a") 120 | newAnchor.attr("href", "../index.html") 121 | newAnchor.addClass("el_report") 122 | newAnchor.text("root") 123 | 124 | val newSpan = document.createElement("span") 125 | newSpan.addClass("el_package") 126 | newSpan.text(subproject.path) 127 | 128 | spanElement.before(newAnchor) 129 | newAnchor.after(newSpan).after(" > ") 130 | 131 | spanElement.remove() 132 | } 133 | 134 | indexHtmlFile.writeText(document.outerHtml()) 135 | } 136 | } 137 | 138 | /** 139 | * Parses coverage metrics from a subproject's JaCoCo report. 140 | * 141 | * @param subproject The subproject from which to parse coverage metrics. 142 | * @return A [CoverageMetrics] containing the parsed coverage data or null 143 | * if no data is found. 144 | */ 145 | private fun parseCoverageMetrics(subproject: Project): CoverageMetrics? { 146 | val pluginExtension = 147 | project.extensions.getByType(JacocoAggregateCoveragePluginExtension::class.java) 148 | val generatedReportDirectory = pluginExtension.getReportDirectory() 149 | val indexHtmlFileProvider = subproject.layout.buildDirectory 150 | .file("$generatedReportDirectory/index.html") 151 | 152 | return CoverageMetrics.parseModuleCoverageMetrics(indexHtmlFileProvider) 153 | } 154 | 155 | /** 156 | * Creates an HTML table row representing the coverage data for a module. 157 | * This row includes progress bars and percentages for different coverage metrics. 158 | * 159 | * @param moduleName The name of the module. 160 | * @param moduleCoverage The coverage metrics for the module. 161 | * @param maxInstructionTotal The maximum total instructions for scaling the progress bar. 162 | * @param maxBranchesTotal The maximum total branches for scaling the progress bar. 163 | * @return An HTML string representing the table row. 164 | */ 165 | private fun createModuleCoverageRow( 166 | moduleName: String, 167 | moduleCoverage: CoverageMetrics, 168 | maxInstructionTotal: Int, 169 | maxBranchesTotal: Int, 170 | ): String { 171 | val moduleCoverageRow = ModuleCoverageRow.generateModuleCoverageRow( 172 | moduleName = moduleName, 173 | moduleCoverage = moduleCoverage, 174 | maxInstructionTotal = maxInstructionTotal, 175 | maxBranchesTotal = maxBranchesTotal, 176 | subprojectToCoverageMap = subprojectToCoverageMap, 177 | ) 178 | return htmlCodeGenerator.generateModuleCoverageTableRowHtml( 179 | moduleName = moduleName, 180 | coverageMetrics = moduleCoverage, 181 | moduleCoverageRow = moduleCoverageRow, 182 | ) 183 | } 184 | 185 | /** 186 | * Builds the final unified index HTML file using a template and the coverage data. 187 | * Replaces placeholders in the template with actual coverage data and module links. 188 | * 189 | * @param metrics The aggregated coverage metrics. 190 | * @param linksHtml The HTML string containing links to the individual module reports. 191 | */ 192 | private fun buildUnifiedIndexHtml(metrics: CoverageMetrics, linksHtml: String) { 193 | try { 194 | val templateFile = project.file("${pluginSourceFolder.get().path}/html/index.html") 195 | val templateText = templateFile.readText(Charsets.UTF_8) 196 | 197 | val totalCoverageString = htmlCodeGenerator.createTotalCoverageString(metrics) 198 | val newText = templateText 199 | .replace(TOTAL_COVERAGE_PLACEHOLDER, totalCoverageString) 200 | .replace(LINKED_MODULES_PLACEHOLDER, linksHtml) 201 | 202 | val destination = outputDirectory.file("index.html").get().asFile 203 | destination.writeText(newText, Charsets.UTF_8) 204 | logger.lifecycle("Aggregated report is generated at: ${destination.absolutePath}") 205 | } catch (exception: IOException) { 206 | logger.error("Error occurred while aggregating reports: ${exception.message}") 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/kotlin/com/azizutku/jacocoaggregatecoverageplugin/CopyJacocoReportsTask.kt: -------------------------------------------------------------------------------- 1 | package com.azizutku.jacocoaggregatecoverageplugin 2 | 3 | import com.azizutku.jacocoaggregatecoverageplugin.extensions.JacocoAggregateCoveragePluginExtension 4 | import org.gradle.api.DefaultTask 5 | import org.gradle.api.file.ConfigurableFileCollection 6 | import org.gradle.api.file.DirectoryProperty 7 | import org.gradle.api.provider.Property 8 | import org.gradle.api.tasks.CacheableTask 9 | import org.gradle.api.tasks.InputDirectory 10 | import org.gradle.api.tasks.InputFiles 11 | import org.gradle.api.tasks.Internal 12 | import org.gradle.api.tasks.OutputDirectory 13 | import org.gradle.api.tasks.PathSensitive 14 | import org.gradle.api.tasks.PathSensitivity 15 | import org.gradle.api.tasks.TaskAction 16 | import java.io.File 17 | 18 | /** 19 | * A Gradle task to copy JaCoCo reports from all subprojects into a single directory. 20 | * It aggregates all the coverage data into one place for easier access and management. 21 | * This task is essential for creating a unified view of test coverage across multiple modules. 22 | */ 23 | @CacheableTask 24 | internal abstract class CopyJacocoReportsTask : DefaultTask() { 25 | 26 | /** 27 | * The source folder containing plugin resources. 28 | */ 29 | @get:InputDirectory 30 | @get:PathSensitive(PathSensitivity.RELATIVE) 31 | abstract val pluginSourceFolder: Property 32 | 33 | /** 34 | * The set of JaCoCo report files for Gradle's incremental build checks. 35 | */ 36 | @get:InputFiles 37 | @get:PathSensitive(PathSensitivity.RELATIVE) 38 | abstract val jacocoReportsFileCollection: ConfigurableFileCollection 39 | 40 | /** 41 | * The directory where the required resources will be stored. 42 | */ 43 | @get:OutputDirectory 44 | abstract val outputDirectoryResources: DirectoryProperty 45 | 46 | /** 47 | * The directory for the generated aggregated coverage report. 48 | */ 49 | @get:Internal 50 | abstract val aggregatedReportDir: DirectoryProperty 51 | 52 | /** 53 | * Performs the action of copying required resources and JaCoCo reports from all subprojects. 54 | * This method is invoked when the task executes. It checks for the existence of 55 | * JaCoCo reports in each subproject and copies them into a unified directory. 56 | */ 57 | @TaskAction 58 | fun copyJacocoReports() { 59 | val pluginExtension = 60 | project.extensions.getByType(JacocoAggregateCoveragePluginExtension::class.java) 61 | val reportDirectory = pluginExtension.getReportDirectory() 62 | if (reportDirectory == null) { 63 | logger.error( 64 | "You need to specify jacocoTestReportTask property of " + 65 | "jacocoAggregateCoverage extension block in your root build gradle" 66 | ) 67 | return 68 | } 69 | copyRequiredResources(outputDirectoryResources.get().asFile) 70 | 71 | project.subprojects.forEach { subproject -> 72 | val jacocoReportDir = subproject.layout.buildDirectory 73 | .dir(reportDirectory) 74 | .get() 75 | .asFile 76 | 77 | if (jacocoReportDir.exists()) { 78 | project.copy { 79 | from(jacocoReportDir) 80 | into(aggregatedReportDir.get().dir(subproject.path)) 81 | } 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * Copies JaCoCo-related resources to the specified unified report directory. 88 | * 89 | * @param unifiedReportDir The target directory to copy resources into. 90 | */ 91 | private fun copyRequiredResources(unifiedReportDir: File) { 92 | project.copy { 93 | from(project.file("${pluginSourceFolder.get().path}/jacoco-resources/")) 94 | into(unifiedReportDir) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/kotlin/com/azizutku/jacocoaggregatecoverageplugin/JacocoAggregateCoveragePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.azizutku.jacocoaggregatecoverageplugin 2 | 3 | import com.azizutku.jacocoaggregatecoverageplugin.extensions.JacocoAggregateCoveragePluginExtension 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | import org.gradle.api.Task 7 | import org.gradle.api.file.Directory 8 | import org.gradle.api.tasks.Copy 9 | 10 | // Tasks 11 | private const val TASK_GROUP = "jacoco aggregate coverage plugin" 12 | private const val TASK_COPY_JACOCO_REPORTS = "copyJacocoReports" 13 | private const val TASK_AGGREGATE_JACOCO_REPORTS = "aggregateJacocoReports" 14 | private const val TASK_UNZIP_PLUGIN = "unzipJacocoAggregateCoveragePlugin" 15 | 16 | private const val EXTENSION_NAME_PLUGIN = "jacocoAggregateCoverage" 17 | 18 | private const val PLUGIN_OUTPUT_PATH = "reports/jacocoAggregated" 19 | 20 | /** 21 | * A Gradle plugin to aggregate JaCoCo coverage reports from multiple subprojects. 22 | * This plugin facilitates the collection and unification of code coverage metrics 23 | * across different modules of a multi-module project. 24 | */ 25 | internal class JacocoAggregateCoveragePlugin : Plugin { 26 | 27 | /** 28 | * Applies the plugin to the given project. 29 | * This method sets up the plugin extension and registers tasks required 30 | * for aggregating JaCoCo reports. 31 | * 32 | * @param project The Gradle project to which the plugin is applied. 33 | */ 34 | override fun apply(project: Project) { 35 | val pluginExtension = createPluginExtension(project) 36 | 37 | val unzipTaskProvider = project.tasks.register(TASK_UNZIP_PLUGIN, Copy::class.java) { 38 | configureUnzipTask(this, project) 39 | } 40 | 41 | val copyReportsTaskProvider = project.tasks.register( 42 | TASK_COPY_JACOCO_REPORTS, 43 | CopyJacocoReportsTask::class.java, 44 | ) { 45 | group = TASK_GROUP 46 | description = "Copy required resources and JaCoCo reports from all subprojects " + 47 | "into a single directory" 48 | pluginSourceFolder.set(unzipTaskProvider.map { task -> task.destinationDir }) 49 | jacocoReportsFileCollection.setFrom(getJacocoGeneratedDirs(project, pluginExtension)) 50 | outputDirectoryResources.set( 51 | project.layout.buildDirectory.dir("$PLUGIN_OUTPUT_PATH/jacoco-resources") 52 | ) 53 | aggregatedReportDir.set( 54 | project.layout.buildDirectory.dir(PLUGIN_OUTPUT_PATH) 55 | ) 56 | } 57 | 58 | project.tasks.register( 59 | TASK_AGGREGATE_JACOCO_REPORTS, 60 | AggregateJacocoReportsTask::class.java, 61 | ).configure { 62 | group = TASK_GROUP 63 | description = "Generate a aggregated report for JaCoCo coverage reports" 64 | if (isJacocoTestReportTaskValueSet(pluginExtension, project).not()) { 65 | return@configure 66 | } 67 | pluginSourceFolder.set(unzipTaskProvider.map { task -> task.destinationDir }) 68 | jacocoReportsFileCollection.setFrom(getJacocoGeneratedDirs(project, pluginExtension)) 69 | outputDirectory.set( 70 | project.layout.buildDirectory.dir(PLUGIN_OUTPUT_PATH) 71 | ) 72 | val jacocoTestReportTasks = getJacocoTestReportTasks(project, pluginExtension) 73 | if (jacocoTestReportTasks.isEmpty()) { 74 | val jacocoTestReportTask = pluginExtension.jacocoTestReportTask.get() 75 | project.logger.error( 76 | """ 77 | There are no tasks named '$jacocoTestReportTask' in your project. Please 78 | ensure that you set the `jacocoTestReportTask` property in the plugin 79 | extension to the name of the task you use to generate JaCoCo test reports. 80 | """.trimIndent() 81 | ) 82 | return@configure 83 | } 84 | copyReportsTaskProvider.get().mustRunAfter(jacocoTestReportTasks) 85 | dependsOn(copyReportsTaskProvider) 86 | dependsOn(jacocoTestReportTasks) 87 | } 88 | } 89 | 90 | /** 91 | * Checks if the 'jacocoTestReportTask' property is set in the plugin extension. 92 | * 93 | * @param pluginExtension The plugin extension with configuration. 94 | * @param project The Gradle project to log errors to. 95 | * @return True if the property is set, false otherwise. 96 | */ 97 | private fun isJacocoTestReportTaskValueSet( 98 | pluginExtension: JacocoAggregateCoveragePluginExtension, 99 | project: Project 100 | ): Boolean { 101 | val jacocoTestReportTask = pluginExtension.jacocoTestReportTask.orNull 102 | if (jacocoTestReportTask == null) { 103 | project.logger.error( 104 | """ 105 | The 'jacocoTestReportTask' property has not been specified in the 106 | JacocoAggregateCoveragePluginExtension extension. Please ensure this property 107 | is set in the build gradle file of the root project. 108 | """.trimIndent() 109 | ) 110 | return false 111 | } 112 | return true 113 | } 114 | 115 | /** 116 | * Retrieves a list of directories containing JaCoCo reports from all subprojects. 117 | * 118 | * @param project The root project. 119 | * @param pluginExtension The plugin extension containing the configuration. 120 | * @return List of JaCoCo report directories. 121 | */ 122 | private fun getJacocoGeneratedDirs( 123 | project: Project, 124 | pluginExtension: JacocoAggregateCoveragePluginExtension 125 | ): List { 126 | val reportDirectory = pluginExtension.getReportDirectory() ?: return emptyList() 127 | return project.subprojects.map { subproject -> 128 | subproject.layout.buildDirectory 129 | .dir(reportDirectory) 130 | .get() 131 | } 132 | } 133 | 134 | /** 135 | * Collects JaCoCo test report tasks from subprojects. 136 | * 137 | * @param project The root project. 138 | * @param pluginExtension The plugin extension with configuration. 139 | * @return List of configured JaCoCo report tasks, or empty if none set. 140 | */ 141 | private fun getJacocoTestReportTasks( 142 | project: Project, 143 | pluginExtension: JacocoAggregateCoveragePluginExtension, 144 | ): List { 145 | val jacocoTestReportTask = pluginExtension.jacocoTestReportTask.orNull ?: return emptyList() 146 | return project.subprojects.mapNotNull { subproject -> 147 | subproject.tasks.findByName(jacocoTestReportTask) 148 | } 149 | } 150 | 151 | /** 152 | * Configures the task to unzip plugin resources. 153 | * This task extracts necessary resources from the plugin to the build directory. 154 | * 155 | * @param task The task instance that needs to be configured. 156 | * @param project The project within which the task is being configured. 157 | */ 158 | private fun configureUnzipTask(task: Copy, project: Project) { 159 | task.apply { 160 | group = TASK_GROUP 161 | description = "Unzip plugin resources into the build directory" 162 | val codeLocation = this@JacocoAggregateCoveragePlugin.javaClass.protectionDomain 163 | .codeSource.location.toExternalForm() 164 | from(project.zipTree(codeLocation)) { 165 | include("jacoco-resources/**") 166 | include("html/index.html") 167 | } 168 | into( 169 | project.layout.buildDirectory.dir( 170 | "intermediates/jacoco_aggregate_coverage_plugin" 171 | ) 172 | ) 173 | } 174 | } 175 | 176 | /** 177 | * Creates and configures the plugin extension object. 178 | * The extension is used for configuring the plugin through the Gradle build script. 179 | * 180 | * @param project The project to which this extension will be added. 181 | * @return The created and configured [JacocoAggregateCoveragePluginExtension] instance. 182 | */ 183 | private fun createPluginExtension(project: Project): JacocoAggregateCoveragePluginExtension { 184 | return project.extensions.create( 185 | EXTENSION_NAME_PLUGIN, 186 | JacocoAggregateCoveragePluginExtension::class.java 187 | ).apply { 188 | jacocoTestReportTask.convention(null) 189 | configuredCustomReportsDirectory.convention(null) 190 | configuredCustomHtmlOutputLocation.convention(null) 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/kotlin/com/azizutku/jacocoaggregatecoverageplugin/extensions/JacocoAggregateCoveragePluginExtension.kt: -------------------------------------------------------------------------------- 1 | package com.azizutku.jacocoaggregatecoverageplugin.extensions 2 | 3 | import org.gradle.api.provider.Property 4 | 5 | /** 6 | * Extension for configuring the JaCoCo Aggregate Coverage Plugin. 7 | * 8 | * @property jacocoTestReportTask Specifies the JaCoCo test report task that needs to be executed 9 | * before aggregation. 10 | * @property configuredCustomReportsDirectory If set, the plugin uses this directory to locate 11 | * JaCoCo reports. 12 | * @property configuredCustomHtmlOutputLocation If set, the plugin uses this directory for the 13 | * output of the generated HTML report. 14 | */ 15 | interface JacocoAggregateCoveragePluginExtension { 16 | val jacocoTestReportTask: Property 17 | val configuredCustomReportsDirectory: Property 18 | val configuredCustomHtmlOutputLocation: Property 19 | 20 | /** 21 | * Determines the JaCoCo reports directory path. 22 | * 23 | * @return The path to the report directory or `null` if task name is not configured. 24 | */ 25 | fun getReportDirectory(): String? { 26 | return when { 27 | jacocoTestReportTask.orNull == null -> null 28 | configuredCustomHtmlOutputLocation.orNull != null -> 29 | configuredCustomHtmlOutputLocation.get() 30 | configuredCustomReportsDirectory.orNull != null -> 31 | "${configuredCustomReportsDirectory.get()}/${jacocoTestReportTask.get()}/html" 32 | else -> "reports/jacoco/${jacocoTestReportTask.get()}/html" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/kotlin/com/azizutku/jacocoaggregatecoverageplugin/models/CoverageMetrics.kt: -------------------------------------------------------------------------------- 1 | package com.azizutku.jacocoaggregatecoverageplugin.models 2 | 3 | import org.gradle.api.file.RegularFile 4 | import org.gradle.api.provider.Provider 5 | import org.jsoup.Jsoup 6 | import org.jsoup.nodes.Element 7 | 8 | // Indexes 9 | private const val INDEX_INSTRUCTIONS = 1 10 | private const val INDEX_BRANCHES = 3 11 | private const val INDEX_COMPLEXITIES_MISSED = 5 12 | private const val INDEX_COMPLEXITIES_TOTAL = 6 13 | private const val INDEX_LINES_MISSED = 7 14 | private const val INDEX_LINES_TOTAL = 8 15 | private const val INDEX_METHODS_MISSED = 9 16 | private const val INDEX_METHODS_TOTAL = 10 17 | private const val INDEX_CLASSES_MISSED = 11 18 | private const val INDEX_CLASSES_TOTAL = 12 19 | 20 | private const val SPACE = " " 21 | private const val SELECTOR_TD = "td" 22 | 23 | private const val NUMBER_HUNDRED = 100 24 | 25 | /** 26 | * Data class representing the metrics of code coverage. 27 | * 28 | * @property instructionsMissed The number of missed instructions. 29 | * @property instructionsTotal The total number of instructions. 30 | * @property branchesMissed The number of missed branches. 31 | * @property branchesTotal The total number of branches. 32 | * @property complexityMissed The number of missed complexities. 33 | * @property complexityTotal The total number of complexities. 34 | * @property linesMissed The number of missed lines. 35 | * @property linesTotal The total number of lines. 36 | * @property methodsMissed The number of missed methods. 37 | * @property methodsTotal The total number of methods. 38 | * @property classesMissed The number of missed classes. 39 | * @property classesTotal The total number of classes. 40 | */ 41 | data class CoverageMetrics( 42 | val instructionsMissed: Int = 0, 43 | val instructionsTotal: Int = 0, 44 | val branchesMissed: Int = 0, 45 | val branchesTotal: Int = 0, 46 | val complexityMissed: Int = 0, 47 | val complexityTotal: Int = 0, 48 | val linesMissed: Int = 0, 49 | val linesTotal: Int = 0, 50 | val methodsMissed: Int = 0, 51 | val methodsTotal: Int = 0, 52 | val classesMissed: Int = 0, 53 | val classesTotal: Int = 0 54 | ) { 55 | 56 | /** 57 | * The coverage percentage of instructions. 58 | */ 59 | val instructionsCoverage: Int 60 | get() = calculateCoverage(instructionsMissed, instructionsTotal) 61 | 62 | /** 63 | * The coverage percentage of branches. 64 | */ 65 | val branchesCoverage: Int 66 | get() = calculateCoverage(branchesMissed, branchesTotal) 67 | 68 | /** 69 | * Calculates the coverage percentage based on missed and total metrics. 70 | * 71 | * @param missed The number of missed elements (instructions, branches, etc.). 72 | * @param total The total number of elements. 73 | * @return The coverage percentage as an integer. 74 | */ 75 | private fun calculateCoverage(missed: Int, total: Int): Int = if (total != 0) { 76 | ((total - missed) / total.toDouble() * NUMBER_HUNDRED).toInt() 77 | } else { 78 | -1 79 | } 80 | 81 | /** 82 | * Aggregates coverage metrics from another [CoverageMetrics] instance. 83 | * This is useful for combining coverage data from multiple modules or tests. 84 | * 85 | * @param other Another [CoverageMetrics] instance to combine with this one. 86 | * @return A new [CoverageMetrics] instance representing the aggregated metrics. 87 | */ 88 | operator fun plus(other: CoverageMetrics): CoverageMetrics = CoverageMetrics( 89 | instructionsMissed + other.instructionsMissed, 90 | instructionsTotal + other.instructionsTotal, 91 | branchesMissed + other.branchesMissed, 92 | branchesTotal + other.branchesTotal, 93 | complexityMissed + other.complexityMissed, 94 | complexityTotal + other.complexityTotal, 95 | linesMissed + other.linesMissed, 96 | linesTotal + other.linesTotal, 97 | methodsMissed + other.methodsMissed, 98 | methodsTotal + other.methodsTotal, 99 | classesMissed + other.classesMissed, 100 | classesTotal + other.classesTotal 101 | ) 102 | 103 | companion object { 104 | /** 105 | * Calculates and formats the coverage percentage as a human-readable string. 106 | * 107 | * @param missed The number of missed elements (instructions, branches, etc.). 108 | * @param total The total number of elements. 109 | * @return The formatted coverage percentage string. 110 | */ 111 | fun calculateCoveragePercentage(missed: Int, total: Int): String { 112 | if (total == 0) return "n/a" 113 | val coverage = (((total - missed) / total.toDouble()) * NUMBER_HUNDRED).toInt() 114 | return "$coverage%" 115 | } 116 | 117 | /** 118 | * Parses JaCoCo coverage metrics from a module's HTML report. 119 | * 120 | * @param indexHtmlFileProvider Provider for the module's index.html file. 121 | * @return [CoverageMetrics] with parsed data or `null` if the file is missing. 122 | */ 123 | fun parseModuleCoverageMetrics( 124 | indexHtmlFileProvider: Provider, 125 | ): CoverageMetrics? { 126 | val indexHtmlFile = indexHtmlFileProvider.get().asFile 127 | if (indexHtmlFile.exists().not()) { 128 | return null 129 | } 130 | return Jsoup.parse( 131 | indexHtmlFile, 132 | Charsets.UTF_8.name(), 133 | ).select("tfoot tr").firstOrNull()?.let { footer -> 134 | CoverageMetrics( 135 | instructionsMissed = extractMissedValue(footer, INDEX_INSTRUCTIONS), 136 | instructionsTotal = extractTotalValue(footer, INDEX_INSTRUCTIONS), 137 | branchesMissed = extractMissedValue(footer, INDEX_BRANCHES), 138 | branchesTotal = extractTotalValue(footer, INDEX_BRANCHES), 139 | complexityMissed = extractSingleValue(footer, INDEX_COMPLEXITIES_MISSED), 140 | complexityTotal = extractSingleValue(footer, INDEX_COMPLEXITIES_TOTAL), 141 | linesMissed = extractSingleValue(footer, INDEX_LINES_MISSED), 142 | linesTotal = extractSingleValue(footer, INDEX_LINES_TOTAL), 143 | methodsMissed = extractSingleValue(footer, INDEX_METHODS_MISSED), 144 | methodsTotal = extractSingleValue(footer, INDEX_METHODS_TOTAL), 145 | classesMissed = extractSingleValue(footer, INDEX_CLASSES_MISSED), 146 | classesTotal = extractSingleValue(footer, INDEX_CLASSES_TOTAL), 147 | ) 148 | } 149 | } 150 | 151 | /** 152 | * Extracts a numeric value from a table cell in an HTML document. 153 | * Used for parsing coverage data from HTML reports. 154 | * 155 | * @param element The HTML element representing a table row. 156 | * @param index The index of the table cell from which to extract the value. 157 | * @return The extracted value as an integer. 158 | */ 159 | private fun extractSingleValue(element: Element, index: Int): Int { 160 | return element.select(SELECTOR_TD).eq(index).text().toIntOrNull() ?: 0 161 | } 162 | 163 | /** 164 | * Extracts the missed value from a coverage data cell in an HTML document. 165 | * Used for parsing the number of missed elements (instructions, branches, etc.) from coverage reports. 166 | * 167 | * @param element The HTML element representing a table row. 168 | * @param index The index of the table cell from which to extract the missed value. 169 | * @return The number of missed elements as an integer. 170 | */ 171 | private fun extractMissedValue(element: Element, index: Int): Int { 172 | val text = element.select(SELECTOR_TD).eq(index).text() 173 | val values = text.split(SPACE) 174 | return values[0].filter { it.isDigit() }.toIntOrNull() ?: 0 175 | } 176 | 177 | /** 178 | * Extracts the total value from a coverage data cell in an HTML document. 179 | * Used for parsing the total number of elements (instructions, branches, etc.) from coverage reports. 180 | * 181 | * @param element The HTML element representing a table row. 182 | * @param index The index of the table cell from which to extract the total value. 183 | * @return The total number of elements as an integer. 184 | */ 185 | private fun extractTotalValue(element: Element, index: Int): Int { 186 | val text = element.select(SELECTOR_TD).eq(index).text() 187 | val values = text.split(SPACE) 188 | return values[2].filter { it.isDigit() }.toIntOrNull() ?: 0 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/kotlin/com/azizutku/jacocoaggregatecoverageplugin/models/ModuleCoverageRow.kt: -------------------------------------------------------------------------------- 1 | package com.azizutku.jacocoaggregatecoverageplugin.models 2 | 3 | private const val MAXIMUM_WIDTH_FOR_PROGRESS_BARS = 120 4 | 5 | /** 6 | * Represents a row in the aggregated coverage report, detailing module coverage metrics. 7 | */ 8 | internal data class ModuleCoverageRow( 9 | val instructionsCoverage: String, 10 | val branchesCoverage: String, 11 | val moduleNameOrder: Int, 12 | val instructionsMissedOrder: Int, 13 | val instructionMissedRedProgressBar: String, 14 | val instructionMissedGreenProgressBar: String, 15 | val branchesMissedRedProgressBar: String, 16 | val branchesMissedGreenProgressBar: String, 17 | val instructionsCoverageOrder: Int, 18 | val branchesMissedOrder: Int, 19 | val branchesCoverageOrder: Int, 20 | val complexityMissedOrder: Int, 21 | val complexityTotalOrder: Int, 22 | val linesMissedOrder: Int, 23 | val linesTotalOrder: Int, 24 | val methodsMissedOrder: Int, 25 | val methodsTotalOrder: Int, 26 | val classesMissedOrder: Int, 27 | val classesTotalOrder: Int, 28 | ) { 29 | companion object { 30 | 31 | /** 32 | * Generates a coverage row for a given module, including progress bars. 33 | * 34 | * @param moduleName Name of the module. 35 | * @param moduleCoverage Coverage metrics for the module. 36 | * @param maxInstructionTotal The highest number of total instructions across all modules. 37 | * @param maxBranchesTotal The highest number of total branches across all modules. 38 | * @param subprojectToCoverageMap Map of module names to their coverage metrics. 39 | * @return A [ModuleCoverageRow] containing coverage data and HTML progress bar elements. 40 | */ 41 | @Suppress("LongMethod") 42 | fun generateModuleCoverageRow( 43 | moduleName: String, 44 | moduleCoverage: CoverageMetrics, 45 | maxInstructionTotal: Int, 46 | maxBranchesTotal: Int, 47 | subprojectToCoverageMap: Map, 48 | ): ModuleCoverageRow = ModuleCoverageRow( 49 | instructionsCoverage = CoverageMetrics.calculateCoveragePercentage( 50 | missed = moduleCoverage.instructionsMissed, 51 | total = moduleCoverage.instructionsTotal, 52 | ), 53 | branchesCoverage = CoverageMetrics.calculateCoveragePercentage( 54 | missed = moduleCoverage.branchesMissed, 55 | total = moduleCoverage.branchesTotal, 56 | ), 57 | moduleNameOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { 58 | moduleName 59 | }, 60 | instructionsMissedOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { 61 | it.instructionsMissed 62 | }, 63 | instructionMissedRedProgressBar = getProgressBarHtml( 64 | value = moduleCoverage.instructionsMissed, 65 | maxValue = maxInstructionTotal, 66 | color = "red", 67 | ), 68 | instructionMissedGreenProgressBar = getProgressBarHtml( 69 | value = moduleCoverage.instructionsTotal - moduleCoverage.instructionsMissed, 70 | maxValue = maxInstructionTotal, 71 | color = "green", 72 | ), 73 | instructionsCoverageOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { 74 | it.instructionsCoverage 75 | }, 76 | branchesMissedOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { metrics -> 77 | metrics.branchesMissed 78 | }, 79 | branchesMissedRedProgressBar = getProgressBarHtml( 80 | value = moduleCoverage.branchesMissed, 81 | maxValue = maxBranchesTotal, 82 | color = "red", 83 | ), 84 | branchesMissedGreenProgressBar = getProgressBarHtml( 85 | value = moduleCoverage.branchesTotal - moduleCoverage.branchesMissed, 86 | maxValue = maxBranchesTotal, 87 | color = "green", 88 | ), 89 | branchesCoverageOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { 90 | it.branchesCoverage 91 | }, 92 | complexityMissedOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { 93 | it.complexityMissed 94 | }, 95 | complexityTotalOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { 96 | it.complexityTotal 97 | }, 98 | linesMissedOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { 99 | it.linesMissed 100 | }, 101 | linesTotalOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { 102 | it.linesTotal 103 | }, 104 | methodsMissedOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { 105 | it.methodsMissed 106 | }, 107 | methodsTotalOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { 108 | it.methodsTotal 109 | }, 110 | classesMissedOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { 111 | it.classesMissed 112 | }, 113 | classesTotalOrder = subprojectToCoverageMap.findOrderOfProperty(moduleName) { 114 | it.classesTotal 115 | }, 116 | ) 117 | 118 | /** 119 | * Finds the ordinal index of a property in a sorted list of coverage metrics. 120 | * Used to determine the order of modules based on a specific coverage metric. 121 | * 122 | * @param key The key of the coverage metric to find. 123 | * @param selector A lambda to select the property used for sorting. 124 | * @return The index of the property in the sorted list. 125 | */ 126 | private fun > Map.findOrderOfProperty( 127 | key: String, 128 | selector: (CoverageMetrics) -> T 129 | ): Int { 130 | val sortedEntries = entries.sortedBy { selector(it.value) } 131 | return sortedEntries.indexOfFirst { it.key == key } 132 | } 133 | 134 | /** 135 | * Generates an HTML snippet for a progress bar. 136 | * This snippet represents the coverage as a visual bar in the report. 137 | * 138 | * @param value The value represented by the progress bar. 139 | * @param maxValue The maximum possible value for scaling the progress bar width. 140 | * @param color The color of the progress bar (e.g., "red", "green"). 141 | * @return An HTML string representing the progress bar. 142 | */ 143 | private fun getProgressBarHtml(value: Int, maxValue: Int, color: String): String { 144 | val widthPercentage = 145 | (value.toFloat() / maxValue * MAXIMUM_WIDTH_FOR_PROGRESS_BARS).toInt() 146 | return "$value" 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/kotlin/com/azizutku/jacocoaggregatecoverageplugin/utils/HtmlCodeGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.azizutku.jacocoaggregatecoverageplugin.utils 2 | 3 | import com.azizutku.jacocoaggregatecoverageplugin.models.CoverageMetrics 4 | import com.azizutku.jacocoaggregatecoverageplugin.models.ModuleCoverageRow 5 | 6 | /** 7 | * Utility class for generating HTML code for coverage reporting. 8 | */ 9 | internal class HtmlCodeGenerator { 10 | 11 | /** 12 | * Generates HTML for a table row representing a module's coverage metrics. 13 | * 14 | * @param moduleName Name of the module. 15 | * @param coverageMetrics Coverage metrics associated with the module. 16 | * @param moduleCoverageRow Detailed coverage data for the module. 17 | * @return HTML string for the module's coverage table row. 18 | */ 19 | fun generateModuleCoverageTableRowHtml( 20 | moduleName: String, 21 | coverageMetrics: CoverageMetrics, 22 | moduleCoverageRow: ModuleCoverageRow, 23 | ): String = with(moduleCoverageRow) { 24 | return """ 25 | 26 | $moduleName 27 | 28 | $instructionMissedRedProgressBar$instructionMissedGreenProgressBar 29 | 30 | $instructionsCoverage 31 | 32 | $branchesMissedRedProgressBar$branchesMissedGreenProgressBar 33 | 34 | $branchesCoverage 35 | 36 | ${coverageMetrics.complexityMissed} 37 | 38 | ${coverageMetrics.complexityTotal} 39 | ${coverageMetrics.linesMissed} 40 | ${coverageMetrics.linesTotal} 41 | ${coverageMetrics.methodsMissed} 42 | ${coverageMetrics.methodsTotal} 43 | ${coverageMetrics.classesMissed} 44 | ${coverageMetrics.classesTotal} 45 | 46 | """.trimIndent() 47 | } 48 | 49 | /** 50 | * Creates an HTML string representing the total coverage metrics. 51 | * This string is used to populate the summary section of the unified report. 52 | * 53 | * @param metrics The aggregated coverage metrics. 54 | * @return An HTML string representing the total coverage. 55 | */ 56 | fun createTotalCoverageString(metrics: CoverageMetrics): String = with(metrics) { 57 | // Constructing the total coverage string to replace in the HTML template 58 | val instructionsCoveragePercentage = CoverageMetrics.calculateCoveragePercentage( 59 | instructionsMissed, 60 | instructionsTotal, 61 | ) 62 | val branchesCoveragePercentage = CoverageMetrics.calculateCoveragePercentage( 63 | branchesMissed, 64 | branchesTotal, 65 | ) 66 | """ 67 | $instructionsMissed of $instructionsTotal 68 | $instructionsCoveragePercentage 69 | $branchesMissed of $branchesTotal 70 | $branchesCoveragePercentage 71 | $complexityMissed 72 | $complexityTotal 73 | $linesMissed 74 | $linesTotal 75 | $methodsMissed 76 | $methodsTotal 77 | $classesMissed 78 | $classesTotal 79 | """.trimIndent() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Aggregated JaCoco Report 9 | 10 | 11 | 12 | 16 |

Project Coverage

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | TOTAL_COVERAGE_PLACEHOLDER 39 | 40 | 41 | 42 | LINKED_MODULES_PLACEHOLDER 43 | 44 |
ModuleMissed InstructionsCov.Missed BranchesCov.MissedCxtyMissedLinesMissedMethodsMissedClasses
Total
45 | 48 | 49 | -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/branchfc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/branchfc.gif -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/branchnc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/branchnc.gif -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/branchpc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/branchpc.gif -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/bundle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/bundle.gif -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/class.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/class.gif -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/down.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/down.gif -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/greenbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/greenbar.gif -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/group.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/group.gif -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/method.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/method.gif -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/package.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/package.gif -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/prettify.css: -------------------------------------------------------------------------------- 1 | /* Pretty printing styles. Used with prettify.js. */ 2 | 3 | .str { color: #2A00FF; } 4 | .kwd { color: #7F0055; font-weight:bold; } 5 | .com { color: #3F5FBF; } 6 | .typ { color: #606; } 7 | .lit { color: #066; } 8 | .pun { color: #660; } 9 | .pln { color: #000; } 10 | .tag { color: #008; } 11 | .atn { color: #606; } 12 | .atv { color: #080; } 13 | .dec { color: #606; } 14 | -------------------------------------------------------------------------------- /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/prettify.js: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2006 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | /** 17 | * @fileoverview 18 | * some functions for browser-side pretty printing of code contained in html. 19 | *

20 | * 21 | * For a fairly comprehensive set of languages see the 22 | * README 23 | * file that came with this source. At a minimum, the lexer should work on a 24 | * number of languages including C and friends, Java, Python, Bash, SQL, HTML, 25 | * XML, CSS, Javascript, and Makefiles. It works passably on Ruby, PHP and Awk 26 | * and a subset of Perl, but, because of commenting conventions, doesn't work on 27 | * Smalltalk, Lisp-like, or CAML-like languages without an explicit lang class. 28 | *

29 | * Usage:

    30 | *
  1. include this source file in an html page via 31 | * {@code } 32 | *
  2. define style rules. See the example page for examples. 33 | *
  3. mark the {@code
    } and {@code } tags in your source with
      34 |  *    {@code class=prettyprint.}
      35 |  *    You can also use the (html deprecated) {@code } tag, but the pretty
      36 |  *    printer needs to do more substantial DOM manipulations to support that, so
      37 |  *    some css styles may not be preserved.
      38 |  * </ol>
      39 |  * That's it.  I wanted to keep the API as simple as possible, so there's no
      40 |  * need to specify which language the code is in, but if you wish, you can add
      41 |  * another class to the {@code <pre>} or {@code <code>} element to specify the
      42 |  * language, as in {@code <pre class="prettyprint lang-java">}.  Any class that
      43 |  * starts with "lang-" followed by a file extension, specifies the file type.
      44 |  * See the "lang-*.js" files in this directory for code that implements
      45 |  * per-language file handlers.
      46 |  * <p>
      47 |  * Change log:<br>
      48 |  * cbeust, 2006/08/22
      49 |  * <blockquote>
      50 |  *   Java annotations (start with "@") are now captured as literals ("lit")
      51 |  * </blockquote>
      52 |  * @requires console
      53 |  */
      54 | 
      55 | // JSLint declarations
      56 | /*global console, document, navigator, setTimeout, window */
      57 | 
      58 | /**
      59 |  * Split {@code prettyPrint} into multiple timeouts so as not to interfere with
      60 |  * UI events.
      61 |  * If set to {@code false}, {@code prettyPrint()} is synchronous.
      62 |  */
      63 | window['PR_SHOULD_USE_CONTINUATION'] = true;
      64 | 
      65 | /** the number of characters between tab columns */
      66 | window['PR_TAB_WIDTH'] = 8;
      67 | 
      68 | /** Walks the DOM returning a properly escaped version of innerHTML.
      69 |   * @param {Node} node
      70 |   * @param {Array.<string>} out output buffer that receives chunks of HTML.
      71 |   */
      72 | window['PR_normalizedHtml']
      73 | 
      74 | /** Contains functions for creating and registering new language handlers.
      75 |   * @type {Object}
      76 |   */
      77 |   = window['PR']
      78 | 
      79 | /** Pretty print a chunk of code.
      80 |   *
      81 |   * @param {string} sourceCodeHtml code as html
      82 |   * @return {string} code as html, but prettier
      83 |   */
      84 |   = window['prettyPrintOne']
      85 | /** Find all the {@code <pre>} and {@code <code>} tags in the DOM with
      86 |   * {@code class=prettyprint} and prettify them.
      87 |   * @param {Function?} opt_whenDone if specified, called when the last entry
      88 |   *     has been finished.
      89 |   */
      90 |   = window['prettyPrint'] = void 0;
      91 | 
      92 | /** browser detection. @extern @returns false if not IE, otherwise the major version. */
      93 | window['_pr_isIE6'] = function () {
      94 |   var ieVersion = navigator && navigator.userAgent &&
      95 |       navigator.userAgent.match(/\bMSIE ([678])\./);
      96 |   ieVersion = ieVersion ? +ieVersion[1] : false;
      97 |   window['_pr_isIE6'] = function () { return ieVersion; };
      98 |   return ieVersion;
      99 | };
     100 | 
     101 | 
     102 | (function () {
     103 |   // Keyword lists for various languages.
     104 |   var FLOW_CONTROL_KEYWORDS =
     105 |       "break continue do else for if return while ";
     106 |   var C_KEYWORDS = FLOW_CONTROL_KEYWORDS + "auto case char const default " +
     107 |       "double enum extern float goto int long register short signed sizeof " +
     108 |       "static struct switch typedef union unsigned void volatile ";
     109 |   var COMMON_KEYWORDS = C_KEYWORDS + "catch class delete false import " +
     110 |       "new operator private protected public this throw true try typeof ";
     111 |   var CPP_KEYWORDS = COMMON_KEYWORDS + "alignof align_union asm axiom bool " +
     112 |       "concept concept_map const_cast constexpr decltype " +
     113 |       "dynamic_cast explicit export friend inline late_check " +
     114 |       "mutable namespace nullptr reinterpret_cast static_assert static_cast " +
     115 |       "template typeid typename using virtual wchar_t where ";
     116 |   var JAVA_KEYWORDS = COMMON_KEYWORDS +
     117 |       "abstract boolean byte extends final finally implements import " +
     118 |       "instanceof null native package strictfp super synchronized throws " +
     119 |       "transient ";
     120 |   var CSHARP_KEYWORDS = JAVA_KEYWORDS +
     121 |       "as base by checked decimal delegate descending event " +
     122 |       "fixed foreach from group implicit in interface internal into is lock " +
     123 |       "object out override orderby params partial readonly ref sbyte sealed " +
     124 |       "stackalloc string select uint ulong unchecked unsafe ushort var ";
     125 |   var JSCRIPT_KEYWORDS = COMMON_KEYWORDS +
     126 |       "debugger eval export function get null set undefined var with " +
     127 |       "Infinity NaN ";
     128 |   var PERL_KEYWORDS = "caller delete die do dump elsif eval exit foreach for " +
     129 |       "goto if import last local my next no our print package redo require " +
     130 |       "sub undef unless until use wantarray while BEGIN END ";
     131 |   var PYTHON_KEYWORDS = FLOW_CONTROL_KEYWORDS + "and as assert class def del " +
     132 |       "elif except exec finally from global import in is lambda " +
     133 |       "nonlocal not or pass print raise try with yield " +
     134 |       "False True None ";
     135 |   var RUBY_KEYWORDS = FLOW_CONTROL_KEYWORDS + "alias and begin case class def" +
     136 |       " defined elsif end ensure false in module next nil not or redo rescue " +
     137 |       "retry self super then true undef unless until when yield BEGIN END ";
     138 |   var SH_KEYWORDS = FLOW_CONTROL_KEYWORDS + "case done elif esac eval fi " +
     139 |       "function in local set then until ";
     140 |   var ALL_KEYWORDS = (
     141 |       CPP_KEYWORDS + CSHARP_KEYWORDS + JSCRIPT_KEYWORDS + PERL_KEYWORDS +
     142 |       PYTHON_KEYWORDS + RUBY_KEYWORDS + SH_KEYWORDS);
     143 | 
     144 |   // token style names.  correspond to css classes
     145 |   /** token style for a string literal */
     146 |   var PR_STRING = 'str';
     147 |   /** token style for a keyword */
     148 |   var PR_KEYWORD = 'kwd';
     149 |   /** token style for a comment */
     150 |   var PR_COMMENT = 'com';
     151 |   /** token style for a type */
     152 |   var PR_TYPE = 'typ';
     153 |   /** token style for a literal value.  e.g. 1, null, true. */
     154 |   var PR_LITERAL = 'lit';
     155 |   /** token style for a punctuation string. */
     156 |   var PR_PUNCTUATION = 'pun';
     157 |   /** token style for a punctuation string. */
     158 |   var PR_PLAIN = 'pln';
     159 | 
     160 |   /** token style for an sgml tag. */
     161 |   var PR_TAG = 'tag';
     162 |   /** token style for a markup declaration such as a DOCTYPE. */
     163 |   var PR_DECLARATION = 'dec';
     164 |   /** token style for embedded source. */
     165 |   var PR_SOURCE = 'src';
     166 |   /** token style for an sgml attribute name. */
     167 |   var PR_ATTRIB_NAME = 'atn';
     168 |   /** token style for an sgml attribute value. */
     169 |   var PR_ATTRIB_VALUE = 'atv';
     170 | 
     171 |   /**
     172 |    * A class that indicates a section of markup that is not code, e.g. to allow
     173 |    * embedding of line numbers within code listings.
     174 |    */
     175 |   var PR_NOCODE = 'nocode';
     176 | 
     177 |   /** A set of tokens that can precede a regular expression literal in
     178 |     * javascript.
     179 |     * http://www.mozilla.org/js/language/js20/rationale/syntax.html has the full
     180 |     * list, but I've removed ones that might be problematic when seen in
     181 |     * languages that don't support regular expression literals.
     182 |     *
     183 |     * <p>Specifically, I've removed any keywords that can't precede a regexp
     184 |     * literal in a syntactically legal javascript program, and I've removed the
     185 |     * "in" keyword since it's not a keyword in many languages, and might be used
     186 |     * as a count of inches.
     187 |     *
     188 |     * <p>The link a above does not accurately describe EcmaScript rules since
     189 |     * it fails to distinguish between (a=++/b/i) and (a++/b/i) but it works
     190 |     * very well in practice.
     191 |     *
     192 |     * @private
     193 |     */
     194 |   var REGEXP_PRECEDER_PATTERN = function () {
     195 |       var preceders = [
     196 |           "!", "!=", "!==", "#", "%", "%=", "&", "&&", "&&=",
     197 |           "&=", "(", "*", "*=", /* "+", */ "+=", ",", /* "-", */ "-=",
     198 |           "->", /*".", "..", "...", handled below */ "/", "/=", ":", "::", ";",
     199 |           "<", "<<", "<<=", "<=", "=", "==", "===", ">",
     200 |           ">=", ">>", ">>=", ">>>", ">>>=", "?", "@", "[",
     201 |           "^", "^=", "^^", "^^=", "{", "|", "|=", "||",
     202 |           "||=", "~" /* handles =~ and !~ */,
     203 |           "break", "case", "continue", "delete",
     204 |           "do", "else", "finally", "instanceof",
     205 |           "return", "throw", "try", "typeof"
     206 |           ];
     207 |       var pattern = '(?:^^|[+-]';
     208 |       for (var i = 0; i < preceders.length; ++i) {
     209 |         pattern += '|' + preceders[i].replace(/([^=<>:&a-z])/g, '\\$1');
     210 |       }
     211 |       pattern += ')\\s*';  // matches at end, and matches empty string
     212 |       return pattern;
     213 |       // CAVEAT: this does not properly handle the case where a regular
     214 |       // expression immediately follows another since a regular expression may
     215 |       // have flags for case-sensitivity and the like.  Having regexp tokens
     216 |       // adjacent is not valid in any language I'm aware of, so I'm punting.
     217 |       // TODO: maybe style special characters inside a regexp as punctuation.
     218 |     }();
     219 | 
     220 |   // Define regexps here so that the interpreter doesn't have to create an
     221 |   // object each time the function containing them is called.
     222 |   // The language spec requires a new object created even if you don't access
     223 |   // the $1 members.
     224 |   var pr_amp = /&/g;
     225 |   var pr_lt = /</g;
     226 |   var pr_gt = />/g;
     227 |   var pr_quot = /\"/g;
     228 |   /** like textToHtml but escapes double quotes to be attribute safe. */
     229 |   function attribToHtml(str) {
     230 |     return str.replace(pr_amp, '&amp;')
     231 |         .replace(pr_lt, '&lt;')
     232 |         .replace(pr_gt, '&gt;')
     233 |         .replace(pr_quot, '&quot;');
     234 |   }
     235 | 
     236 |   /** escapest html special characters to html. */
     237 |   function textToHtml(str) {
     238 |     return str.replace(pr_amp, '&amp;')
     239 |         .replace(pr_lt, '&lt;')
     240 |         .replace(pr_gt, '&gt;');
     241 |   }
     242 | 
     243 | 
     244 |   var pr_ltEnt = /&lt;/g;
     245 |   var pr_gtEnt = /&gt;/g;
     246 |   var pr_aposEnt = /&apos;/g;
     247 |   var pr_quotEnt = /&quot;/g;
     248 |   var pr_ampEnt = /&amp;/g;
     249 |   var pr_nbspEnt = /&nbsp;/g;
     250 |   /** unescapes html to plain text. */
     251 |   function htmlToText(html) {
     252 |     var pos = html.indexOf('&');
     253 |     if (pos < 0) { return html; }
     254 |     // Handle numeric entities specially.  We can't use functional substitution
     255 |     // since that doesn't work in older versions of Safari.
     256 |     // These should be rare since most browsers convert them to normal chars.
     257 |     for (--pos; (pos = html.indexOf('&#', pos + 1)) >= 0;) {
     258 |       var end = html.indexOf(';', pos);
     259 |       if (end >= 0) {
     260 |         var num = html.substring(pos + 3, end);
     261 |         var radix = 10;
     262 |         if (num && num.charAt(0) === 'x') {
     263 |           num = num.substring(1);
     264 |           radix = 16;
     265 |         }
     266 |         var codePoint = parseInt(num, radix);
     267 |         if (!isNaN(codePoint)) {
     268 |           html = (html.substring(0, pos) + String.fromCharCode(codePoint) +
     269 |                   html.substring(end + 1));
     270 |         }
     271 |       }
     272 |     }
     273 | 
     274 |     return html.replace(pr_ltEnt, '<')
     275 |         .replace(pr_gtEnt, '>')
     276 |         .replace(pr_aposEnt, "'")
     277 |         .replace(pr_quotEnt, '"')
     278 |         .replace(pr_nbspEnt, ' ')
     279 |         .replace(pr_ampEnt, '&');
     280 |   }
     281 | 
     282 |   /** is the given node's innerHTML normally unescaped? */
     283 |   function isRawContent(node) {
     284 |     return 'XMP' === node.tagName;
     285 |   }
     286 | 
     287 |   var newlineRe = /[\r\n]/g;
     288 |   /**
     289 |    * Are newlines and adjacent spaces significant in the given node's innerHTML?
     290 |    */
     291 |   function isPreformatted(node, content) {
     292 |     // PRE means preformatted, and is a very common case, so don't create
     293 |     // unnecessary computed style objects.
     294 |     if ('PRE' === node.tagName) { return true; }
     295 |     if (!newlineRe.test(content)) { return true; }  // Don't care
     296 |     var whitespace = '';
     297 |     // For disconnected nodes, IE has no currentStyle.
     298 |     if (node.currentStyle) {
     299 |       whitespace = node.currentStyle.whiteSpace;
     300 |     } else if (window.getComputedStyle) {
     301 |       // Firefox makes a best guess if node is disconnected whereas Safari
     302 |       // returns the empty string.
     303 |       whitespace = window.getComputedStyle(node, null).whiteSpace;
     304 |     }
     305 |     return !whitespace || whitespace === 'pre';
     306 |   }
     307 | 
     308 |   function normalizedHtml(node, out, opt_sortAttrs) {
     309 |     switch (node.nodeType) {
     310 |       case 1:  // an element
     311 |         var name = node.tagName.toLowerCase();
     312 | 
     313 |         out.push('<', name);
     314 |         var attrs = node.attributes;
     315 |         var n = attrs.length;
     316 |         if (n) {
     317 |           if (opt_sortAttrs) {
     318 |             var sortedAttrs = [];
     319 |             for (var i = n; --i >= 0;) { sortedAttrs[i] = attrs[i]; }
     320 |             sortedAttrs.sort(function (a, b) {
     321 |                 return (a.name < b.name) ? -1 : a.name === b.name ? 0 : 1;
     322 |               });
     323 |             attrs = sortedAttrs;
     324 |           }
     325 |           for (var i = 0; i < n; ++i) {
     326 |             var attr = attrs[i];
     327 |             if (!attr.specified) { continue; }
     328 |             out.push(' ', attr.name.toLowerCase(),
     329 |                      '="', attribToHtml(attr.value), '"');
     330 |           }
     331 |         }
     332 |         out.push('>');
     333 |         for (var child = node.firstChild; child; child = child.nextSibling) {
     334 |           normalizedHtml(child, out, opt_sortAttrs);
     335 |         }
     336 |         if (node.firstChild || !/^(?:br|link|img)$/.test(name)) {
     337 |           out.push('<\/', name, '>');
     338 |         }
     339 |         break;
     340 |       case 3: case 4: // text
     341 |         out.push(textToHtml(node.nodeValue));
     342 |         break;
     343 |     }
     344 |   }
     345 | 
     346 |   /**
     347 |    * Given a group of {@link RegExp}s, returns a {@code RegExp} that globally
     348 |    * matches the union o the sets o strings matched d by the input RegExp.
     349 |    * Since it matches globally, if the input strings have a start-of-input
     350 |    * anchor (/^.../), it is ignored for the purposes of unioning.
     351 |    * @param {Array.<RegExp>} regexs non multiline, non-global regexs.
     352 |    * @return {RegExp} a global regex.
     353 |    */
     354 |   function combinePrefixPatterns(regexs) {
     355 |     var capturedGroupIndex = 0;
     356 | 
     357 |     var needToFoldCase = false;
     358 |     var ignoreCase = false;
     359 |     for (var i = 0, n = regexs.length; i < n; ++i) {
     360 |       var regex = regexs[i];
     361 |       if (regex.ignoreCase) {
     362 |         ignoreCase = true;
     363 |       } else if (/[a-z]/i.test(regex.source.replace(
     364 |                      /\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi, ''))) {
     365 |         needToFoldCase = true;
     366 |         ignoreCase = false;
     367 |         break;
     368 |       }
     369 |     }
     370 | 
     371 |     function decodeEscape(charsetPart) {
     372 |       if (charsetPart.charAt(0) !== '\\') { return charsetPart.charCodeAt(0); }
     373 |       switch (charsetPart.charAt(1)) {
     374 |         case 'b': return 8;
     375 |         case 't': return 9;
     376 |         case 'n': return 0xa;
     377 |         case 'v': return 0xb;
     378 |         case 'f': return 0xc;
     379 |         case 'r': return 0xd;
     380 |         case 'u': case 'x':
     381 |           return parseInt(charsetPart.substring(2), 16)
     382 |               || charsetPart.charCodeAt(1);
     383 |         case '0': case '1': case '2': case '3': case '4':
     384 |         case '5': case '6': case '7':
     385 |           return parseInt(charsetPart.substring(1), 8);
     386 |         default: return charsetPart.charCodeAt(1);
     387 |       }
     388 |     }
     389 | 
     390 |     function encodeEscape(charCode) {
     391 |       if (charCode < 0x20) {
     392 |         return (charCode < 0x10 ? '\\x0' : '\\x') + charCode.toString(16);
     393 |       }
     394 |       var ch = String.fromCharCode(charCode);
     395 |       if (ch === '\\' || ch === '-' || ch === '[' || ch === ']') {
     396 |         ch = '\\' + ch;
     397 |       }
     398 |       return ch;
     399 |     }
     400 | 
     401 |     function caseFoldCharset(charSet) {
     402 |       var charsetParts = charSet.substring(1, charSet.length - 1).match(
     403 |           new RegExp(
     404 |               '\\\\u[0-9A-Fa-f]{4}'
     405 |               + '|\\\\x[0-9A-Fa-f]{2}'
     406 |               + '|\\\\[0-3][0-7]{0,2}'
     407 |               + '|\\\\[0-7]{1,2}'
     408 |               + '|\\\\[\\s\\S]'
     409 |               + '|-'
     410 |               + '|[^-\\\\]',
     411 |               'g'));
     412 |       var groups = [];
     413 |       var ranges = [];
     414 |       var inverse = charsetParts[0] === '^';
     415 |       for (var i = inverse ? 1 : 0, n = charsetParts.length; i < n; ++i) {
     416 |         var p = charsetParts[i];
     417 |         switch (p) {
     418 |           case '\\B': case '\\b':
     419 |           case '\\D': case '\\d':
     420 |           case '\\S': case '\\s':
     421 |           case '\\W': case '\\w':
     422 |             groups.push(p);
     423 |             continue;
     424 |         }
     425 |         var start = decodeEscape(p);
     426 |         var end;
     427 |         if (i + 2 < n && '-' === charsetParts[i + 1]) {
     428 |           end = decodeEscape(charsetParts[i + 2]);
     429 |           i += 2;
     430 |         } else {
     431 |           end = start;
     432 |         }
     433 |         ranges.push([start, end]);
     434 |         // If the range might intersect letters, then expand it.
     435 |         if (!(end < 65 || start > 122)) {
     436 |           if (!(end < 65 || start > 90)) {
     437 |             ranges.push([Math.max(65, start) | 32, Math.min(end, 90) | 32]);
     438 |           }
     439 |           if (!(end < 97 || start > 122)) {
     440 |             ranges.push([Math.max(97, start) & ~32, Math.min(end, 122) & ~32]);
     441 |           }
     442 |         }
     443 |       }
     444 | 
     445 |       // [[1, 10], [3, 4], [8, 12], [14, 14], [16, 16], [17, 17]]
     446 |       // -> [[1, 12], [14, 14], [16, 17]]
     447 |       ranges.sort(function (a, b) { return (a[0] - b[0]) || (b[1]  - a[1]); });
     448 |       var consolidatedRanges = [];
     449 |       var lastRange = [NaN, NaN];
     450 |       for (var i = 0; i < ranges.length; ++i) {
     451 |         var range = ranges[i];
     452 |         if (range[0] <= lastRange[1] + 1) {
     453 |           lastRange[1] = Math.max(lastRange[1], range[1]);
     454 |         } else {
     455 |           consolidatedRanges.push(lastRange = range);
     456 |         }
     457 |       }
     458 | 
     459 |       var out = ['['];
     460 |       if (inverse) { out.push('^'); }
     461 |       out.push.apply(out, groups);
     462 |       for (var i = 0; i < consolidatedRanges.length; ++i) {
     463 |         var range = consolidatedRanges[i];
     464 |         out.push(encodeEscape(range[0]));
     465 |         if (range[1] > range[0]) {
     466 |           if (range[1] + 1 > range[0]) { out.push('-'); }
     467 |           out.push(encodeEscape(range[1]));
     468 |         }
     469 |       }
     470 |       out.push(']');
     471 |       return out.join('');
     472 |     }
     473 | 
     474 |     function allowAnywhereFoldCaseAndRenumberGroups(regex) {
     475 |       // Split into character sets, escape sequences, punctuation strings
     476 |       // like ('(', '(?:', ')', '^'), and runs of characters that do not
     477 |       // include any of the above.
     478 |       var parts = regex.source.match(
     479 |           new RegExp(
     480 |               '(?:'
     481 |               + '\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]'  // a character set
     482 |               + '|\\\\u[A-Fa-f0-9]{4}'  // a unicode escape
     483 |               + '|\\\\x[A-Fa-f0-9]{2}'  // a hex escape
     484 |               + '|\\\\[0-9]+'  // a back-reference or octal escape
     485 |               + '|\\\\[^ux0-9]'  // other escape sequence
     486 |               + '|\\(\\?[:!=]'  // start of a non-capturing group
     487 |               + '|[\\(\\)\\^]'  // start/emd of a group, or line start
     488 |               + '|[^\\x5B\\x5C\\(\\)\\^]+'  // run of other characters
     489 |               + ')',
     490 |               'g'));
     491 |       var n = parts.length;
     492 | 
     493 |       // Maps captured group numbers to the number they will occupy in
     494 |       // the output or to -1 if that has not been determined, or to
     495 |       // undefined if they need not be capturing in the output.
     496 |       var capturedGroups = [];
     497 | 
     498 |       // Walk over and identify back references to build the capturedGroups
     499 |       // mapping.
     500 |       for (var i = 0, groupIndex = 0; i < n; ++i) {
     501 |         var p = parts[i];
     502 |         if (p === '(') {
     503 |           // groups are 1-indexed, so max group index is count of '('
     504 |           ++groupIndex;
     505 |         } else if ('\\' === p.charAt(0)) {
     506 |           var decimalValue = +p.substring(1);
     507 |           if (decimalValue && decimalValue <= groupIndex) {
     508 |             capturedGroups[decimalValue] = -1;
     509 |           }
     510 |         }
     511 |       }
     512 | 
     513 |       // Renumber groups and reduce capturing groups to non-capturing groups
     514 |       // where possible.
     515 |       for (var i = 1; i < capturedGroups.length; ++i) {
     516 |         if (-1 === capturedGroups[i]) {
     517 |           capturedGroups[i] = ++capturedGroupIndex;
     518 |         }
     519 |       }
     520 |       for (var i = 0, groupIndex = 0; i < n; ++i) {
     521 |         var p = parts[i];
     522 |         if (p === '(') {
     523 |           ++groupIndex;
     524 |           if (capturedGroups[groupIndex] === undefined) {
     525 |             parts[i] = '(?:';
     526 |           }
     527 |         } else if ('\\' === p.charAt(0)) {
     528 |           var decimalValue = +p.substring(1);
     529 |           if (decimalValue && decimalValue <= groupIndex) {
     530 |             parts[i] = '\\' + capturedGroups[groupIndex];
     531 |           }
     532 |         }
     533 |       }
     534 | 
     535 |       // Remove any prefix anchors so that the output will match anywhere.
     536 |       // ^^ really does mean an anchored match though.
     537 |       for (var i = 0, groupIndex = 0; i < n; ++i) {
     538 |         if ('^' === parts[i] && '^' !== parts[i + 1]) { parts[i] = ''; }
     539 |       }
     540 | 
     541 |       // Expand letters to groupts to handle mixing of case-sensitive and
     542 |       // case-insensitive patterns if necessary.
     543 |       if (regex.ignoreCase && needToFoldCase) {
     544 |         for (var i = 0; i < n; ++i) {
     545 |           var p = parts[i];
     546 |           var ch0 = p.charAt(0);
     547 |           if (p.length >= 2 && ch0 === '[') {
     548 |             parts[i] = caseFoldCharset(p);
     549 |           } else if (ch0 !== '\\') {
     550 |             // TODO: handle letters in numeric escapes.
     551 |             parts[i] = p.replace(
     552 |                 /[a-zA-Z]/g,
     553 |                 function (ch) {
     554 |                   var cc = ch.charCodeAt(0);
     555 |                   return '[' + String.fromCharCode(cc & ~32, cc | 32) + ']';
     556 |                 });
     557 |           }
     558 |         }
     559 |       }
     560 | 
     561 |       return parts.join('');
     562 |     }
     563 | 
     564 |     var rewritten = [];
     565 |     for (var i = 0, n = regexs.length; i < n; ++i) {
     566 |       var regex = regexs[i];
     567 |       if (regex.global || regex.multiline) { throw new Error('' + regex); }
     568 |       rewritten.push(
     569 |           '(?:' + allowAnywhereFoldCaseAndRenumberGroups(regex) + ')');
     570 |     }
     571 | 
     572 |     return new RegExp(rewritten.join('|'), ignoreCase ? 'gi' : 'g');
     573 |   }
     574 | 
     575 |   var PR_innerHtmlWorks = null;
     576 |   function getInnerHtml(node) {
     577 |     // inner html is hopelessly broken in Safari 2.0.4 when the content is
     578 |     // an html description of well formed XML and the containing tag is a PRE
     579 |     // tag, so we detect that case and emulate innerHTML.
     580 |     if (null === PR_innerHtmlWorks) {
     581 |       var testNode = document.createElement('PRE');
     582 |       testNode.appendChild(
     583 |           document.createTextNode('<!DOCTYPE foo PUBLIC "foo bar">\n<foo />'));
     584 |       PR_innerHtmlWorks = !/</.test(testNode.innerHTML);
     585 |     }
     586 | 
     587 |     if (PR_innerHtmlWorks) {
     588 |       var content = node.innerHTML;
     589 |       // XMP tags contain unescaped entities so require special handling.
     590 |       if (isRawContent(node)) {
     591 |         content = textToHtml(content);
     592 |       } else if (!isPreformatted(node, content)) {
     593 |         content = content.replace(/(<br\s*\/?>)[\r\n]+/g, '$1')
     594 |             .replace(/(?:[\r\n]+[ \t]*)+/g, ' ');
     595 |       }
     596 |       return content;
     597 |     }
     598 | 
     599 |     var out = [];
     600 |     for (var child = node.firstChild; child; child = child.nextSibling) {
     601 |       normalizedHtml(child, out);
     602 |     }
     603 |     return out.join('');
     604 |   }
     605 | 
     606 |   /** returns a function that expand tabs to spaces.  This function can be fed
     607 |     * successive chunks of text, and will maintain its own internal state to
     608 |     * keep track of how tabs are expanded.
     609 |     * @return {function (string) : string} a function that takes
     610 |     *   plain text and return the text with tabs expanded.
     611 |     * @private
     612 |     */
     613 |   function makeTabExpander(tabWidth) {
     614 |     var SPACES = '                ';
     615 |     var charInLine = 0;
     616 | 
     617 |     return function (plainText) {
     618 |       // walk over each character looking for tabs and newlines.
     619 |       // On tabs, expand them.  On newlines, reset charInLine.
     620 |       // Otherwise increment charInLine
     621 |       var out = null;
     622 |       var pos = 0;
     623 |       for (var i = 0, n = plainText.length; i < n; ++i) {
     624 |         var ch = plainText.charAt(i);
     625 | 
     626 |         switch (ch) {
     627 |           case '\t':
     628 |             if (!out) { out = []; }
     629 |             out.push(plainText.substring(pos, i));
     630 |             // calculate how much space we need in front of this part
     631 |             // nSpaces is the amount of padding -- the number of spaces needed
     632 |             // to move us to the next column, where columns occur at factors of
     633 |             // tabWidth.
     634 |             var nSpaces = tabWidth - (charInLine % tabWidth);
     635 |             charInLine += nSpaces;
     636 |             for (; nSpaces >= 0; nSpaces -= SPACES.length) {
     637 |               out.push(SPACES.substring(0, nSpaces));
     638 |             }
     639 |             pos = i + 1;
     640 |             break;
     641 |           case '\n':
     642 |             charInLine = 0;
     643 |             break;
     644 |           default:
     645 |             ++charInLine;
     646 |         }
     647 |       }
     648 |       if (!out) { return plainText; }
     649 |       out.push(plainText.substring(pos));
     650 |       return out.join('');
     651 |     };
     652 |   }
     653 | 
     654 |   var pr_chunkPattern = new RegExp(
     655 |       '[^<]+'  // A run of characters other than '<'
     656 |       + '|<\!--[\\s\\S]*?--\>'  // an HTML comment
     657 |       + '|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>'  // a CDATA section
     658 |       // a probable tag that should not be highlighted
     659 |       + '|<\/?[a-zA-Z](?:[^>\"\']|\'[^\']*\'|\"[^\"]*\")*>'
     660 |       + '|<',  // A '<' that does not begin a larger chunk
     661 |       'g');
     662 |   var pr_commentPrefix = /^<\!--/;
     663 |   var pr_cdataPrefix = /^<!\[CDATA\[/;
     664 |   var pr_brPrefix = /^<br\b/i;
     665 |   var pr_tagNameRe = /^<(\/?)([a-zA-Z][a-zA-Z0-9]*)/;
     666 | 
     667 |   /** split markup into chunks of html tags (style null) and
     668 |     * plain text (style {@link #PR_PLAIN}), converting tags which are
     669 |     * significant for tokenization (<br>) into their textual equivalent.
     670 |     *
     671 |     * @param {string} s html where whitespace is considered significant.
     672 |     * @return {Object} source code and extracted tags.
     673 |     * @private
     674 |     */
     675 |   function extractTags(s) {
     676 |     // since the pattern has the 'g' modifier and defines no capturing groups,
     677 |     // this will return a list of all chunks which we then classify and wrap as
     678 |     // PR_Tokens
     679 |     var matches = s.match(pr_chunkPattern);
     680 |     var sourceBuf = [];
     681 |     var sourceBufLen = 0;
     682 |     var extractedTags = [];
     683 |     if (matches) {
     684 |       for (var i = 0, n = matches.length; i < n; ++i) {
     685 |         var match = matches[i];
     686 |         if (match.length > 1 && match.charAt(0) === '<') {
     687 |           if (pr_commentPrefix.test(match)) { continue; }
     688 |           if (pr_cdataPrefix.test(match)) {
     689 |             // strip CDATA prefix and suffix.  Don't unescape since it's CDATA
     690 |             sourceBuf.push(match.substring(9, match.length - 3));
     691 |             sourceBufLen += match.length - 12;
     692 |           } else if (pr_brPrefix.test(match)) {
     693 |             // <br> tags are lexically significant so convert them to text.
     694 |             // This is undone later.
     695 |             sourceBuf.push('\n');
     696 |             ++sourceBufLen;
     697 |           } else {
     698 |             if (match.indexOf(PR_NOCODE) >= 0 && isNoCodeTag(match)) {
     699 |               // A <span class="nocode"> will start a section that should be
     700 |               // ignored.  Continue walking the list until we see a matching end
     701 |               // tag.
     702 |               var name = match.match(pr_tagNameRe)[2];
     703 |               var depth = 1;
     704 |               var j;
     705 |               end_tag_loop:
     706 |               for (j = i + 1; j < n; ++j) {
     707 |                 var name2 = matches[j].match(pr_tagNameRe);
     708 |                 if (name2 && name2[2] === name) {
     709 |                   if (name2[1] === '/') {
     710 |                     if (--depth === 0) { break end_tag_loop; }
     711 |                   } else {
     712 |                     ++depth;
     713 |                   }
     714 |                 }
     715 |               }
     716 |               if (j < n) {
     717 |                 extractedTags.push(
     718 |                     sourceBufLen, matches.slice(i, j + 1).join(''));
     719 |                 i = j;
     720 |               } else {  // Ignore unclosed sections.
     721 |                 extractedTags.push(sourceBufLen, match);
     722 |               }
     723 |             } else {
     724 |               extractedTags.push(sourceBufLen, match);
     725 |             }
     726 |           }
     727 |         } else {
     728 |           var literalText = htmlToText(match);
     729 |           sourceBuf.push(literalText);
     730 |           sourceBufLen += literalText.length;
     731 |         }
     732 |       }
     733 |     }
     734 |     return { source: sourceBuf.join(''), tags: extractedTags };
     735 |   }
     736 | 
     737 |   /** True if the given tag contains a class attribute with the nocode class. */
     738 |   function isNoCodeTag(tag) {
     739 |     return !!tag
     740 |         // First canonicalize the representation of attributes
     741 |         .replace(/\s(\w+)\s*=\s*(?:\"([^\"]*)\"|'([^\']*)'|(\S+))/g,
     742 |                  ' $1="$2$3$4"')
     743 |         // Then look for the attribute we want.
     744 |         .match(/[cC][lL][aA][sS][sS]=\"[^\"]*\bnocode\b/);
     745 |   }
     746 | 
     747 |   /**
     748 |    * Apply the given language handler to sourceCode and add the resulting
     749 |    * decorations to out.
     750 |    * @param {number} basePos the index of sourceCode within the chunk of source
     751 |    *    whose decorations are already present on out.
     752 |    */
     753 |   function appendDecorations(basePos, sourceCode, langHandler, out) {
     754 |     if (!sourceCode) { return; }
     755 |     var job = {
     756 |       source: sourceCode,
     757 |       basePos: basePos
     758 |     };
     759 |     langHandler(job);
     760 |     out.push.apply(out, job.decorations);
     761 |   }
     762 | 
     763 |   /** Given triples of [style, pattern, context] returns a lexing function,
     764 |     * The lexing function interprets the patterns to find token boundaries and
     765 |     * returns a decoration list of the form
     766 |     * [index_0, style_0, index_1, style_1, ..., index_n, style_n]
     767 |     * where index_n is an index into the sourceCode, and style_n is a style
     768 |     * constant like PR_PLAIN.  index_n-1 <= index_n, and style_n-1 applies to
     769 |     * all characters in sourceCode[index_n-1:index_n].
     770 |     *
     771 |     * The stylePatterns is a list whose elements have the form
     772 |     * [style : string, pattern : RegExp, DEPRECATED, shortcut : string].
     773 |     *
     774 |     * Style is a style constant like PR_PLAIN, or can be a string of the
     775 |     * form 'lang-FOO', where FOO is a language extension describing the
     776 |     * language of the portion of the token in $1 after pattern executes.
     777 |     * E.g., if style is 'lang-lisp', and group 1 contains the text
     778 |     * '(hello (world))', then that portion of the token will be passed to the
     779 |     * registered lisp handler for formatting.
     780 |     * The text before and after group 1 will be restyled using this decorator
     781 |     * so decorators should take care that this doesn't result in infinite
     782 |     * recursion.  For example, the HTML lexer rule for SCRIPT elements looks
     783 |     * something like ['lang-js', /<[s]cript>(.+?)<\/script>/].  This may match
     784 |     * '<script>foo()<\/script>', which would cause the current decorator to
     785 |     * be called with '<script>' which would not match the same rule since
     786 |     * group 1 must not be empty, so it would be instead styled as PR_TAG by
     787 |     * the generic tag rule.  The handler registered for the 'js' extension would
     788 |     * then be called with 'foo()', and finally, the current decorator would
     789 |     * be called with '<\/script>' which would not match the original rule and
     790 |     * so the generic tag rule would identify it as a tag.
     791 |     *
     792 |     * Pattern must only match prefixes, and if it matches a prefix, then that
     793 |     * match is considered a token with the same style.
     794 |     *
     795 |     * Context is applied to the last non-whitespace, non-comment token
     796 |     * recognized.
     797 |     *
     798 |     * Shortcut is an optional string of characters, any of which, if the first
     799 |     * character, gurantee that this pattern and only this pattern matches.
     800 |     *
     801 |     * @param {Array} shortcutStylePatterns patterns that always start with
     802 |     *   a known character.  Must have a shortcut string.
     803 |     * @param {Array} fallthroughStylePatterns patterns that will be tried in
     804 |     *   order if the shortcut ones fail.  May have shortcuts.
     805 |     *
     806 |     * @return {function (Object)} a
     807 |     *   function that takes source code and returns a list of decorations.
     808 |     */
     809 |   function createSimpleLexer(shortcutStylePatterns, fallthroughStylePatterns) {
     810 |     var shortcuts = {};
     811 |     var tokenizer;
     812 |     (function () {
     813 |       var allPatterns = shortcutStylePatterns.concat(fallthroughStylePatterns);
     814 |       var allRegexs = [];
     815 |       var regexKeys = {};
     816 |       for (var i = 0, n = allPatterns.length; i < n; ++i) {
     817 |         var patternParts = allPatterns[i];
     818 |         var shortcutChars = patternParts[3];
     819 |         if (shortcutChars) {
     820 |           for (var c = shortcutChars.length; --c >= 0;) {
     821 |             shortcuts[shortcutChars.charAt(c)] = patternParts;
     822 |           }
     823 |         }
     824 |         var regex = patternParts[1];
     825 |         var k = '' + regex;
     826 |         if (!regexKeys.hasOwnProperty(k)) {
     827 |           allRegexs.push(regex);
     828 |           regexKeys[k] = null;
     829 |         }
     830 |       }
     831 |       allRegexs.push(/[\0-\uffff]/);
     832 |       tokenizer = combinePrefixPatterns(allRegexs);
     833 |     })();
     834 | 
     835 |     var nPatterns = fallthroughStylePatterns.length;
     836 |     var notWs = /\S/;
     837 | 
     838 |     /**
     839 |      * Lexes job.source and produces an output array job.decorations of style
     840 |      * classes preceded by the position at which they start in job.source in
     841 |      * order.
     842 |      *
     843 |      * @param {Object} job an object like {@code
     844 |      *    source: {string} sourceText plain text,
     845 |      *    basePos: {int} position of job.source in the larger chunk of
     846 |      *        sourceCode.
     847 |      * }
     848 |      */
     849 |     var decorate = function (job) {
     850 |       var sourceCode = job.source, basePos = job.basePos;
     851 |       /** Even entries are positions in source in ascending order.  Odd enties
     852 |         * are style markers (e.g., PR_COMMENT) that run from that position until
     853 |         * the end.
     854 |         * @type {Array.<number|string>}
     855 |         */
     856 |       var decorations = [basePos, PR_PLAIN];
     857 |       var pos = 0;  // index into sourceCode
     858 |       var tokens = sourceCode.match(tokenizer) || [];
     859 |       var styleCache = {};
     860 | 
     861 |       for (var ti = 0, nTokens = tokens.length; ti < nTokens; ++ti) {
     862 |         var token = tokens[ti];
     863 |         var style = styleCache[token];
     864 |         var match = void 0;
     865 | 
     866 |         var isEmbedded;
     867 |         if (typeof style === 'string') {
     868 |           isEmbedded = false;
     869 |         } else {
     870 |           var patternParts = shortcuts[token.charAt(0)];
     871 |           if (patternParts) {
     872 |             match = token.match(patternParts[1]);
     873 |             style = patternParts[0];
     874 |           } else {
     875 |             for (var i = 0; i < nPatterns; ++i) {
     876 |               patternParts = fallthroughStylePatterns[i];
     877 |               match = token.match(patternParts[1]);
     878 |               if (match) {
     879 |                 style = patternParts[0];
     880 |                 break;
     881 |               }
     882 |             }
     883 | 
     884 |             if (!match) {  // make sure that we make progress
     885 |               style = PR_PLAIN;
     886 |             }
     887 |           }
     888 | 
     889 |           isEmbedded = style.length >= 5 && 'lang-' === style.substring(0, 5);
     890 |           if (isEmbedded && !(match && typeof match[1] === 'string')) {
     891 |             isEmbedded = false;
     892 |             style = PR_SOURCE;
     893 |           }
     894 | 
     895 |           if (!isEmbedded) { styleCache[token] = style; }
     896 |         }
     897 | 
     898 |         var tokenStart = pos;
     899 |         pos += token.length;
     900 | 
     901 |         if (!isEmbedded) {
     902 |           decorations.push(basePos + tokenStart, style);
     903 |         } else {  // Treat group 1 as an embedded block of source code.
     904 |           var embeddedSource = match[1];
     905 |           var embeddedSourceStart = token.indexOf(embeddedSource);
     906 |           var embeddedSourceEnd = embeddedSourceStart + embeddedSource.length;
     907 |           if (match[2]) {
     908 |             // If embeddedSource can be blank, then it would match at the
     909 |             // beginning which would cause us to infinitely recurse on the
     910 |             // entire token, so we catch the right context in match[2].
     911 |             embeddedSourceEnd = token.length - match[2].length;
     912 |             embeddedSourceStart = embeddedSourceEnd - embeddedSource.length;
     913 |           }
     914 |           var lang = style.substring(5);
     915 |           // Decorate the left of the embedded source
     916 |           appendDecorations(
     917 |               basePos + tokenStart,
     918 |               token.substring(0, embeddedSourceStart),
     919 |               decorate, decorations);
     920 |           // Decorate the embedded source
     921 |           appendDecorations(
     922 |               basePos + tokenStart + embeddedSourceStart,
     923 |               embeddedSource,
     924 |               langHandlerForExtension(lang, embeddedSource),
     925 |               decorations);
     926 |           // Decorate the right of the embedded section
     927 |           appendDecorations(
     928 |               basePos + tokenStart + embeddedSourceEnd,
     929 |               token.substring(embeddedSourceEnd),
     930 |               decorate, decorations);
     931 |         }
     932 |       }
     933 |       job.decorations = decorations;
     934 |     };
     935 |     return decorate;
     936 |   }
     937 | 
     938 |   /** returns a function that produces a list of decorations from source text.
     939 |     *
     940 |     * This code treats ", ', and ` as string delimiters, and \ as a string
     941 |     * escape.  It does not recognize perl's qq() style strings.
     942 |     * It has no special handling for double delimiter escapes as in basic, or
     943 |     * the tripled delimiters used in python, but should work on those regardless
     944 |     * although in those cases a single string literal may be broken up into
     945 |     * multiple adjacent string literals.
     946 |     *
     947 |     * It recognizes C, C++, and shell style comments.
     948 |     *
     949 |     * @param {Object} options a set of optional parameters.
     950 |     * @return {function (Object)} a function that examines the source code
     951 |     *     in the input job and builds the decoration list.
     952 |     */
     953 |   function sourceDecorator(options) {
     954 |     var shortcutStylePatterns = [], fallthroughStylePatterns = [];
     955 |     if (options['tripleQuotedStrings']) {
     956 |       // '''multi-line-string''', 'single-line-string', and double-quoted
     957 |       shortcutStylePatterns.push(
     958 |           [PR_STRING,  /^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,
     959 |            null, '\'"']);
     960 |     } else if (options['multiLineStrings']) {
     961 |       // 'multi-line-string', "multi-line-string"
     962 |       shortcutStylePatterns.push(
     963 |           [PR_STRING,  /^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,
     964 |            null, '\'"`']);
     965 |     } else {
     966 |       // 'single-line-string', "single-line-string"
     967 |       shortcutStylePatterns.push(
     968 |           [PR_STRING,
     969 |            /^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,
     970 |            null, '"\'']);
     971 |     }
     972 |     if (options['verbatimStrings']) {
     973 |       // verbatim-string-literal production from the C# grammar.  See issue 93.
     974 |       fallthroughStylePatterns.push(
     975 |           [PR_STRING, /^@\"(?:[^\"]|\"\")*(?:\"|$)/, null]);
     976 |     }
     977 |     if (options['hashComments']) {
     978 |       if (options['cStyleComments']) {
     979 |         // Stop C preprocessor declarations at an unclosed open comment
     980 |         shortcutStylePatterns.push(
     981 |             [PR_COMMENT, /^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,
     982 |              null, '#']);
     983 |         fallthroughStylePatterns.push(
     984 |             [PR_STRING,
     985 |              /^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,
     986 |              null]);
     987 |       } else {
     988 |         shortcutStylePatterns.push([PR_COMMENT, /^#[^\r\n]*/, null, '#']);
     989 |       }
     990 |     }
     991 |     if (options['cStyleComments']) {
     992 |       fallthroughStylePatterns.push([PR_COMMENT, /^\/\/[^\r\n]*/, null]);
     993 |       fallthroughStylePatterns.push(
     994 |           [PR_COMMENT, /^\/\*[\s\S]*?(?:\*\/|$)/, null]);
     995 |     }
     996 |     if (options['regexLiterals']) {
     997 |       var REGEX_LITERAL = (
     998 |           // A regular expression literal starts with a slash that is
     999 |           // not followed by * or / so that it is not confused with
    1000 |           // comments.
    1001 |           '/(?=[^/*])'
    1002 |           // and then contains any number of raw characters,
    1003 |           + '(?:[^/\\x5B\\x5C]'
    1004 |           // escape sequences (\x5C),
    1005 |           +    '|\\x5C[\\s\\S]'
    1006 |           // or non-nesting character sets (\x5B\x5D);
    1007 |           +    '|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+'
    1008 |           // finally closed by a /.
    1009 |           + '/');
    1010 |       fallthroughStylePatterns.push(
    1011 |           ['lang-regex',
    1012 |            new RegExp('^' + REGEXP_PRECEDER_PATTERN + '(' + REGEX_LITERAL + ')')
    1013 |            ]);
    1014 |     }
    1015 | 
    1016 |     var keywords = options['keywords'].replace(/^\s+|\s+$/g, '');
    1017 |     if (keywords.length) {
    1018 |       fallthroughStylePatterns.push(
    1019 |           [PR_KEYWORD,
    1020 |            new RegExp('^(?:' + keywords.replace(/\s+/g, '|') + ')\\b'), null]);
    1021 |     }
    1022 | 
    1023 |     shortcutStylePatterns.push([PR_PLAIN,       /^\s+/, null, ' \r\n\t\xA0']);
    1024 |     fallthroughStylePatterns.push(
    1025 |         // TODO(mikesamuel): recognize non-latin letters and numerals in idents
    1026 |         [PR_LITERAL,     /^@[a-z_$][a-z_$@0-9]*/i, null],
    1027 |         [PR_TYPE,        /^@?[A-Z]+[a-z][A-Za-z_$@0-9]*/, null],
    1028 |         [PR_PLAIN,       /^[a-z_$][a-z_$@0-9]*/i, null],
    1029 |         [PR_LITERAL,
    1030 |          new RegExp(
    1031 |              '^(?:'
    1032 |              // A hex number
    1033 |              + '0x[a-f0-9]+'
    1034 |              // or an octal or decimal number,
    1035 |              + '|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)'
    1036 |              // possibly in scientific notation
    1037 |              + '(?:e[+\\-]?\\d+)?'
    1038 |              + ')'
    1039 |              // with an optional modifier like UL for unsigned long
    1040 |              + '[a-z]*', 'i'),
    1041 |          null, '0123456789'],
    1042 |         [PR_PUNCTUATION, /^.[^\s\w\.$@\'\"\`\/\#]*/, null]);
    1043 | 
    1044 |     return createSimpleLexer(shortcutStylePatterns, fallthroughStylePatterns);
    1045 |   }
    1046 | 
    1047 |   var decorateSource = sourceDecorator({
    1048 |         'keywords': ALL_KEYWORDS,
    1049 |         'hashComments': true,
    1050 |         'cStyleComments': true,
    1051 |         'multiLineStrings': true,
    1052 |         'regexLiterals': true
    1053 |       });
    1054 | 
    1055 |   /** Breaks {@code job.source} around style boundaries in
    1056 |     * {@code job.decorations} while re-interleaving {@code job.extractedTags},
    1057 |     * and leaves the result in {@code job.prettyPrintedHtml}.
    1058 |     * @param {Object} job like {
    1059 |     *    source: {string} source as plain text,
    1060 |     *    extractedTags: {Array.<number|string>} extractedTags chunks of raw
    1061 |     *                   html preceded by their position in {@code job.source}
    1062 |     *                   in order
    1063 |     *    decorations: {Array.<number|string} an array of style classes preceded
    1064 |     *                 by the position at which they start in job.source in order
    1065 |     * }
    1066 |     * @private
    1067 |     */
    1068 |   function recombineTagsAndDecorations(job) {
    1069 |     var sourceText = job.source;
    1070 |     var extractedTags = job.extractedTags;
    1071 |     var decorations = job.decorations;
    1072 | 
    1073 |     var html = [];
    1074 |     // index past the last char in sourceText written to html
    1075 |     var outputIdx = 0;
    1076 | 
    1077 |     var openDecoration = null;
    1078 |     var currentDecoration = null;
    1079 |     var tagPos = 0;  // index into extractedTags
    1080 |     var decPos = 0;  // index into decorations
    1081 |     var tabExpander = makeTabExpander(window['PR_TAB_WIDTH']);
    1082 | 
    1083 |     var adjacentSpaceRe = /([\r\n ]) /g;
    1084 |     var startOrSpaceRe = /(^| ) /gm;
    1085 |     var newlineRe = /\r\n?|\n/g;
    1086 |     var trailingSpaceRe = /[ \r\n]$/;
    1087 |     var lastWasSpace = true;  // the last text chunk emitted ended with a space.
    1088 | 
    1089 |     // See bug 71 and http://stackoverflow.com/questions/136443/why-doesnt-ie7-
    1090 |     var isIE678 = window['_pr_isIE6']();
    1091 |     var lineBreakHtml = (
    1092 |         isIE678
    1093 |         ? (job.sourceNode.tagName === 'PRE'
    1094 |            // Use line feeds instead of <br>s so that copying and pasting works
    1095 |            // on IE.
    1096 |            // Doing this on other browsers breaks lots of stuff since \r\n is
    1097 |            // treated as two newlines on Firefox.
    1098 |            ? (isIE678 === 6 ? '&#160;\r\n' :
    1099 |               isIE678 === 7 ? '&#160;<br>\r' : '&#160;\r')
    1100 |            // IE collapses multiple adjacent <br>s into 1 line break.
    1101 |            // Prefix every newline with '&#160;' to prevent such behavior.
    1102 |            // &nbsp; is the same as &#160; but works in XML as well as HTML.
    1103 |            : '&#160;<br />')
    1104 |         : '<br />');
    1105 | 
    1106 |     // Look for a class like linenums or linenums:<n> where <n> is the 1-indexed
    1107 |     // number of the first line.
    1108 |     var numberLines = job.sourceNode.className.match(/\blinenums\b(?::(\d+))?/);
    1109 |     var lineBreaker;
    1110 |     if (numberLines) {
    1111 |       var lineBreaks = [];
    1112 |       for (var i = 0; i < 10; ++i) {
    1113 |         lineBreaks[i] = lineBreakHtml + '</li><li class="L' + i + '">';
    1114 |       }
    1115 |       var lineNum = numberLines[1] && numberLines[1].length
    1116 |           ? numberLines[1] - 1 : 0;  // Lines are 1-indexed
    1117 |       html.push('<ol class="linenums"><li class="L', (lineNum) % 10, '"');
    1118 |       if (lineNum) {
    1119 |         html.push(' value="', lineNum + 1, '"');
    1120 |       }
    1121 |       html.push('>');
    1122 |       lineBreaker = function () {
    1123 |         var lb = lineBreaks[++lineNum % 10];
    1124 |         // If a decoration is open, we need to close it before closing a list-item
    1125 |         // and reopen it on the other side of the list item.
    1126 |         return openDecoration
    1127 |             ? ('</span>' + lb + '<span class="' + openDecoration + '">') : lb;
    1128 |       };
    1129 |     } else {
    1130 |       lineBreaker = lineBreakHtml;
    1131 |     }
    1132 | 
    1133 |     // A helper function that is responsible for opening sections of decoration
    1134 |     // and outputing properly escaped chunks of source
    1135 |     function emitTextUpTo(sourceIdx) {
    1136 |       if (sourceIdx > outputIdx) {
    1137 |         if (openDecoration && openDecoration !== currentDecoration) {
    1138 |           // Close the current decoration
    1139 |           html.push('</span>');
    1140 |           openDecoration = null;
    1141 |         }
    1142 |         if (!openDecoration && currentDecoration) {
    1143 |           openDecoration = currentDecoration;
    1144 |           html.push('<span class="', openDecoration, '">');
    1145 |         }
    1146 |         // This interacts badly with some wikis which introduces paragraph tags
    1147 |         // into pre blocks for some strange reason.
    1148 |         // It's necessary for IE though which seems to lose the preformattedness
    1149 |         // of <pre> tags when their innerHTML is assigned.
    1150 |         // http://stud3.tuwien.ac.at/~e0226430/innerHtmlQuirk.html
    1151 |         // and it serves to undo the conversion of <br>s to newlines done in
    1152 |         // chunkify.
    1153 |         var htmlChunk = textToHtml(
    1154 |             tabExpander(sourceText.substring(outputIdx, sourceIdx)))
    1155 |             .replace(lastWasSpace
    1156 |                      ? startOrSpaceRe
    1157 |                      : adjacentSpaceRe, '$1&#160;');
    1158 |         // Keep track of whether we need to escape space at the beginning of the
    1159 |         // next chunk.
    1160 |         lastWasSpace = trailingSpaceRe.test(htmlChunk);
    1161 |         html.push(htmlChunk.replace(newlineRe, lineBreaker));
    1162 |         outputIdx = sourceIdx;
    1163 |       }
    1164 |     }
    1165 | 
    1166 |     while (true) {
    1167 |       // Determine if we're going to consume a tag this time around.  Otherwise
    1168 |       // we consume a decoration or exit.
    1169 |       var outputTag;
    1170 |       if (tagPos < extractedTags.length) {
    1171 |         if (decPos < decorations.length) {
    1172 |           // Pick one giving preference to extractedTags since we shouldn't open
    1173 |           // a new style that we're going to have to immediately close in order
    1174 |           // to output a tag.
    1175 |           outputTag = extractedTags[tagPos] <= decorations[decPos];
    1176 |         } else {
    1177 |           outputTag = true;
    1178 |         }
    1179 |       } else {
    1180 |         outputTag = false;
    1181 |       }
    1182 |       // Consume either a decoration or a tag or exit.
    1183 |       if (outputTag) {
    1184 |         emitTextUpTo(extractedTags[tagPos]);
    1185 |         if (openDecoration) {
    1186 |           // Close the current decoration
    1187 |           html.push('</span>');
    1188 |           openDecoration = null;
    1189 |         }
    1190 |         html.push(extractedTags[tagPos + 1]);
    1191 |         tagPos += 2;
    1192 |       } else if (decPos < decorations.length) {
    1193 |         emitTextUpTo(decorations[decPos]);
    1194 |         currentDecoration = decorations[decPos + 1];
    1195 |         decPos += 2;
    1196 |       } else {
    1197 |         break;
    1198 |       }
    1199 |     }
    1200 |     emitTextUpTo(sourceText.length);
    1201 |     if (openDecoration) {
    1202 |       html.push('</span>');
    1203 |     }
    1204 |     if (numberLines) { html.push('</li></ol>'); }
    1205 |     job.prettyPrintedHtml = html.join('');
    1206 |   }
    1207 | 
    1208 |   /** Maps language-specific file extensions to handlers. */
    1209 |   var langHandlerRegistry = {};
    1210 |   /** Register a language handler for the given file extensions.
    1211 |     * @param {function (Object)} handler a function from source code to a list
    1212 |     *      of decorations.  Takes a single argument job which describes the
    1213 |     *      state of the computation.   The single parameter has the form
    1214 |     *      {@code {
    1215 |     *        source: {string} as plain text.
    1216 |     *        decorations: {Array.<number|string>} an array of style classes
    1217 |     *                     preceded by the position at which they start in
    1218 |     *                     job.source in order.
    1219 |     *                     The language handler should assigned this field.
    1220 |     *        basePos: {int} the position of source in the larger source chunk.
    1221 |     *                 All positions in the output decorations array are relative
    1222 |     *                 to the larger source chunk.
    1223 |     *      } }
    1224 |     * @param {Array.<string>} fileExtensions
    1225 |     */
    1226 |   function registerLangHandler(handler, fileExtensions) {
    1227 |     for (var i = fileExtensions.length; --i >= 0;) {
    1228 |       var ext = fileExtensions[i];
    1229 |       if (!langHandlerRegistry.hasOwnProperty(ext)) {
    1230 |         langHandlerRegistry[ext] = handler;
    1231 |       } else if ('console' in window) {
    1232 |         console['warn']('cannot override language handler %s', ext);
    1233 |       }
    1234 |     }
    1235 |   }
    1236 |   function langHandlerForExtension(extension, source) {
    1237 |     if (!(extension && langHandlerRegistry.hasOwnProperty(extension))) {
    1238 |       // Treat it as markup if the first non whitespace character is a < and
    1239 |       // the last non-whitespace character is a >.
    1240 |       extension = /^\s*</.test(source)
    1241 |           ? 'default-markup'
    1242 |           : 'default-code';
    1243 |     }
    1244 |     return langHandlerRegistry[extension];
    1245 |   }
    1246 |   registerLangHandler(decorateSource, ['default-code']);
    1247 |   registerLangHandler(
    1248 |       createSimpleLexer(
    1249 |           [],
    1250 |           [
    1251 |            [PR_PLAIN,       /^[^<?]+/],
    1252 |            [PR_DECLARATION, /^<!\w[^>]*(?:>|$)/],
    1253 |            [PR_COMMENT,     /^<\!--[\s\S]*?(?:-\->|$)/],
    1254 |            // Unescaped content in an unknown language
    1255 |            ['lang-',        /^<\?([\s\S]+?)(?:\?>|$)/],
    1256 |            ['lang-',        /^<%([\s\S]+?)(?:%>|$)/],
    1257 |            [PR_PUNCTUATION, /^(?:<[%?]|[%?]>)/],
    1258 |            ['lang-',        /^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],
    1259 |            // Unescaped content in javascript.  (Or possibly vbscript).
    1260 |            ['lang-js',      /^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],
    1261 |            // Contains unescaped stylesheet content
    1262 |            ['lang-css',     /^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],
    1263 |            ['lang-in.tag',  /^(<\/?[a-z][^<>]*>)/i]
    1264 |           ]),
    1265 |       ['default-markup', 'htm', 'html', 'mxml', 'xhtml', 'xml', 'xsl']);
    1266 |   registerLangHandler(
    1267 |       createSimpleLexer(
    1268 |           [
    1269 |            [PR_PLAIN,        /^[\s]+/, null, ' \t\r\n'],
    1270 |            [PR_ATTRIB_VALUE, /^(?:\"[^\"]*\"?|\'[^\']*\'?)/, null, '\"\'']
    1271 |            ],
    1272 |           [
    1273 |            [PR_TAG,          /^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],
    1274 |            [PR_ATTRIB_NAME,  /^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],
    1275 |            ['lang-uq.val',   /^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],
    1276 |            [PR_PUNCTUATION,  /^[=<>\/]+/],
    1277 |            ['lang-js',       /^on\w+\s*=\s*\"([^\"]+)\"/i],
    1278 |            ['lang-js',       /^on\w+\s*=\s*\'([^\']+)\'/i],
    1279 |            ['lang-js',       /^on\w+\s*=\s*([^\"\'>\s]+)/i],
    1280 |            ['lang-css',      /^style\s*=\s*\"([^\"]+)\"/i],
    1281 |            ['lang-css',      /^style\s*=\s*\'([^\']+)\'/i],
    1282 |            ['lang-css',      /^style\s*=\s*([^\"\'>\s]+)/i]
    1283 |            ]),
    1284 |       ['in.tag']);
    1285 |   registerLangHandler(
    1286 |       createSimpleLexer([], [[PR_ATTRIB_VALUE, /^[\s\S]+/]]), ['uq.val']);
    1287 |   registerLangHandler(sourceDecorator({
    1288 |           'keywords': CPP_KEYWORDS,
    1289 |           'hashComments': true,
    1290 |           'cStyleComments': true
    1291 |         }), ['c', 'cc', 'cpp', 'cxx', 'cyc', 'm']);
    1292 |   registerLangHandler(sourceDecorator({
    1293 |           'keywords': 'null true false'
    1294 |         }), ['json']);
    1295 |   registerLangHandler(sourceDecorator({
    1296 |           'keywords': CSHARP_KEYWORDS,
    1297 |           'hashComments': true,
    1298 |           'cStyleComments': true,
    1299 |           'verbatimStrings': true
    1300 |         }), ['cs']);
    1301 |   registerLangHandler(sourceDecorator({
    1302 |           'keywords': JAVA_KEYWORDS,
    1303 |           'cStyleComments': true
    1304 |         }), ['java']);
    1305 |   registerLangHandler(sourceDecorator({
    1306 |           'keywords': SH_KEYWORDS,
    1307 |           'hashComments': true,
    1308 |           'multiLineStrings': true
    1309 |         }), ['bsh', 'csh', 'sh']);
    1310 |   registerLangHandler(sourceDecorator({
    1311 |           'keywords': PYTHON_KEYWORDS,
    1312 |           'hashComments': true,
    1313 |           'multiLineStrings': true,
    1314 |           'tripleQuotedStrings': true
    1315 |         }), ['cv', 'py']);
    1316 |   registerLangHandler(sourceDecorator({
    1317 |           'keywords': PERL_KEYWORDS,
    1318 |           'hashComments': true,
    1319 |           'multiLineStrings': true,
    1320 |           'regexLiterals': true
    1321 |         }), ['perl', 'pl', 'pm']);
    1322 |   registerLangHandler(sourceDecorator({
    1323 |           'keywords': RUBY_KEYWORDS,
    1324 |           'hashComments': true,
    1325 |           'multiLineStrings': true,
    1326 |           'regexLiterals': true
    1327 |         }), ['rb']);
    1328 |   registerLangHandler(sourceDecorator({
    1329 |           'keywords': JSCRIPT_KEYWORDS,
    1330 |           'cStyleComments': true,
    1331 |           'regexLiterals': true
    1332 |         }), ['js']);
    1333 |   registerLangHandler(
    1334 |       createSimpleLexer([], [[PR_STRING, /^[\s\S]+/]]), ['regex']);
    1335 | 
    1336 |   function applyDecorator(job) {
    1337 |     var sourceCodeHtml = job.sourceCodeHtml;
    1338 |     var opt_langExtension = job.langExtension;
    1339 | 
    1340 |     // Prepopulate output in case processing fails with an exception.
    1341 |     job.prettyPrintedHtml = sourceCodeHtml;
    1342 | 
    1343 |     try {
    1344 |       // Extract tags, and convert the source code to plain text.
    1345 |       var sourceAndExtractedTags = extractTags(sourceCodeHtml);
    1346 |       /** Plain text. @type {string} */
    1347 |       var source = sourceAndExtractedTags.source;
    1348 |       job.source = source;
    1349 |       job.basePos = 0;
    1350 | 
    1351 |       /** Even entries are positions in source in ascending order.  Odd entries
    1352 |         * are tags that were extracted at that position.
    1353 |         * @type {Array.<number|string>}
    1354 |         */
    1355 |       job.extractedTags = sourceAndExtractedTags.tags;
    1356 | 
    1357 |       // Apply the appropriate language handler
    1358 |       langHandlerForExtension(opt_langExtension, source)(job);
    1359 |       // Integrate the decorations and tags back into the source code to produce
    1360 |       // a decorated html string which is left in job.prettyPrintedHtml.
    1361 |       recombineTagsAndDecorations(job);
    1362 |     } catch (e) {
    1363 |       if ('console' in window) {
    1364 |         console['log'](e && e['stack'] ? e['stack'] : e);
    1365 |       }
    1366 |     }
    1367 |   }
    1368 | 
    1369 |   function prettyPrintOne(sourceCodeHtml, opt_langExtension) {
    1370 |     var job = {
    1371 |       sourceCodeHtml: sourceCodeHtml,
    1372 |       langExtension: opt_langExtension
    1373 |     };
    1374 |     applyDecorator(job);
    1375 |     return job.prettyPrintedHtml;
    1376 |   }
    1377 | 
    1378 |   function prettyPrint(opt_whenDone) {
    1379 |     function byTagName(tn) { return document.getElementsByTagName(tn); }
    1380 |     // fetch a list of nodes to rewrite
    1381 |     var codeSegments = [byTagName('pre'), byTagName('code'), byTagName('xmp')];
    1382 |     var elements = [];
    1383 |     for (var i = 0; i < codeSegments.length; ++i) {
    1384 |       for (var j = 0, n = codeSegments[i].length; j < n; ++j) {
    1385 |         elements.push(codeSegments[i][j]);
    1386 |       }
    1387 |     }
    1388 |     codeSegments = null;
    1389 | 
    1390 |     var clock = Date;
    1391 |     if (!clock['now']) {
    1392 |       clock = { 'now': function () { return (new Date).getTime(); } };
    1393 |     }
    1394 | 
    1395 |     // The loop is broken into a series of continuations to make sure that we
    1396 |     // don't make the browser unresponsive when rewriting a large page.
    1397 |     var k = 0;
    1398 |     var prettyPrintingJob;
    1399 | 
    1400 |     function doWork() {
    1401 |       var endTime = (window['PR_SHOULD_USE_CONTINUATION'] ?
    1402 |                      clock.now() + 250 /* ms */ :
    1403 |                      Infinity);
    1404 |       for (; k < elements.length && clock.now() < endTime; k++) {
    1405 |         var cs = elements[k];
    1406 |         // [JACOCO] 'prettyprint' -> 'source'
    1407 |         if (cs.className && cs.className.indexOf('source') >= 0) {
    1408 |           // If the classes includes a language extensions, use it.
    1409 |           // Language extensions can be specified like
    1410 |           //     <pre class="prettyprint lang-cpp">
    1411 |           // the language extension "cpp" is used to find a language handler as
    1412 |           // passed to PR_registerLangHandler.
    1413 |           var langExtension = cs.className.match(/\blang-(\w+)\b/);
    1414 |           if (langExtension) { langExtension = langExtension[1]; }
    1415 | 
    1416 |           // make sure this is not nested in an already prettified element
    1417 |           var nested = false;
    1418 |           for (var p = cs.parentNode; p; p = p.parentNode) {
    1419 |             if ((p.tagName === 'pre' || p.tagName === 'code' ||
    1420 |                  p.tagName === 'xmp') &&
    1421 |                 // [JACOCO] 'prettyprint' -> 'source'
    1422 |                 p.className && p.className.indexOf('source') >= 0) {
    1423 |               nested = true;
    1424 |               break;
    1425 |             }
    1426 |           }
    1427 |           if (!nested) {
    1428 |             // fetch the content as a snippet of properly escaped HTML.
    1429 |             // Firefox adds newlines at the end.
    1430 |             var content = getInnerHtml(cs);
    1431 |             content = content.replace(/(?:\r\n?|\n)$/, '');
    1432 | 
    1433 |             // do the pretty printing
    1434 |             prettyPrintingJob = {
    1435 |               sourceCodeHtml: content,
    1436 |               langExtension: langExtension,
    1437 |               sourceNode: cs
    1438 |             };
    1439 |             applyDecorator(prettyPrintingJob);
    1440 |             replaceWithPrettyPrintedHtml();
    1441 |           }
    1442 |         }
    1443 |       }
    1444 |       if (k < elements.length) {
    1445 |         // finish up in a continuation
    1446 |         setTimeout(doWork, 250);
    1447 |       } else if (opt_whenDone) {
    1448 |         opt_whenDone();
    1449 |       }
    1450 |     }
    1451 | 
    1452 |     function replaceWithPrettyPrintedHtml() {
    1453 |       var newContent = prettyPrintingJob.prettyPrintedHtml;
    1454 |       if (!newContent) { return; }
    1455 |       var cs = prettyPrintingJob.sourceNode;
    1456 | 
    1457 |       // push the prettified html back into the tag.
    1458 |       if (!isRawContent(cs)) {
    1459 |         // just replace the old html with the new
    1460 |         cs.innerHTML = newContent;
    1461 |       } else {
    1462 |         // we need to change the tag to a <pre> since <xmp>s do not allow
    1463 |         // embedded tags such as the span tags used to attach styles to
    1464 |         // sections of source code.
    1465 |         var pre = document.createElement('PRE');
    1466 |         for (var i = 0; i < cs.attributes.length; ++i) {
    1467 |           var a = cs.attributes[i];
    1468 |           if (a.specified) {
    1469 |             var aname = a.name.toLowerCase();
    1470 |             if (aname === 'class') {
    1471 |               pre.className = a.value;  // For IE 6
    1472 |             } else {
    1473 |               pre.setAttribute(a.name, a.value);
    1474 |             }
    1475 |           }
    1476 |         }
    1477 |         pre.innerHTML = newContent;
    1478 | 
    1479 |         // remove the old
    1480 |         cs.parentNode.replaceChild(pre, cs);
    1481 |         cs = pre;
    1482 |       }
    1483 |     }
    1484 | 
    1485 |     doWork();
    1486 |   }
    1487 | 
    1488 |   window['PR_normalizedHtml'] = normalizedHtml;
    1489 |   window['prettyPrintOne'] = prettyPrintOne;
    1490 |   window['prettyPrint'] = prettyPrint;
    1491 |   window['PR'] = {
    1492 |         'combinePrefixPatterns': combinePrefixPatterns,
    1493 |         'createSimpleLexer': createSimpleLexer,
    1494 |         'registerLangHandler': registerLangHandler,
    1495 |         'sourceDecorator': sourceDecorator,
    1496 |         'PR_ATTRIB_NAME': PR_ATTRIB_NAME,
    1497 |         'PR_ATTRIB_VALUE': PR_ATTRIB_VALUE,
    1498 |         'PR_COMMENT': PR_COMMENT,
    1499 |         'PR_DECLARATION': PR_DECLARATION,
    1500 |         'PR_KEYWORD': PR_KEYWORD,
    1501 |         'PR_LITERAL': PR_LITERAL,
    1502 |         'PR_NOCODE': PR_NOCODE,
    1503 |         'PR_PLAIN': PR_PLAIN,
    1504 |         'PR_PUNCTUATION': PR_PUNCTUATION,
    1505 |         'PR_SOURCE': PR_SOURCE,
    1506 |         'PR_STRING': PR_STRING,
    1507 |         'PR_TAG': PR_TAG,
    1508 |         'PR_TYPE': PR_TYPE
    1509 |       };
    1510 | })();
    1511 | 
    
    
    --------------------------------------------------------------------------------
    /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/redbar.gif:
    --------------------------------------------------------------------------------
    https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/redbar.gif
    
    
    --------------------------------------------------------------------------------
    /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/report.css:
    --------------------------------------------------------------------------------
      1 | body, td {
      2 |   font-family:sans-serif;
      3 |   font-size:10pt;
      4 | }
      5 | 
      6 | h1 {
      7 |   font-weight:bold;
      8 |   font-size:18pt;
      9 | }
     10 | 
     11 | .breadcrumb {
     12 |   border:#d6d3ce 1px solid;
     13 |   padding:2px 4px 2px 4px;
     14 | }
     15 | 
     16 | .breadcrumb .info {
     17 |   float:right;
     18 | }
     19 | 
     20 | .breadcrumb .info a {
     21 |   margin-left:8px;
     22 | }
     23 | 
     24 | .el_report {
     25 |   padding-left:18px;
     26 |   background-image:url(report.gif);
     27 |   background-position:left center;
     28 |   background-repeat:no-repeat;
     29 | }
     30 | 
     31 | .el_group {
     32 |   padding-left:18px;
     33 |   background-image:url(group.gif);
     34 |   background-position:left center;
     35 |   background-repeat:no-repeat;
     36 | }
     37 | 
     38 | .el_bundle {
     39 |   padding-left:18px;
     40 |   background-image:url(bundle.gif);
     41 |   background-position:left center;
     42 |   background-repeat:no-repeat;
     43 | }
     44 | 
     45 | .el_package {
     46 |   padding-left:18px;
     47 |   background-image:url(package.gif);
     48 |   background-position:left center;
     49 |   background-repeat:no-repeat;
     50 | }
     51 | 
     52 | .el_class {
     53 |   padding-left:18px;
     54 |   background-image:url(class.gif);
     55 |   background-position:left center;
     56 |   background-repeat:no-repeat;
     57 | }
     58 | 
     59 | .el_source {
     60 |   padding-left:18px;
     61 |   background-image:url(source.gif);
     62 |   background-position:left center;
     63 |   background-repeat:no-repeat;
     64 | }
     65 | 
     66 | .el_method {
     67 |   padding-left:18px;
     68 |   background-image:url(method.gif);
     69 |   background-position:left center;
     70 |   background-repeat:no-repeat;
     71 | }
     72 | 
     73 | .el_session {
     74 |   padding-left:18px;
     75 |   background-image:url(session.gif);
     76 |   background-position:left center;
     77 |   background-repeat:no-repeat;
     78 | }
     79 | 
     80 | pre.source {
     81 |   border:#d6d3ce 1px solid;
     82 |   font-family:monospace;
     83 | }
     84 | 
     85 | pre.source ol {
     86 |   margin-bottom: 0px;
     87 |   margin-top: 0px;
     88 | }
     89 | 
     90 | pre.source li {
     91 |   border-left: 1px solid #D6D3CE;
     92 |   color: #A0A0A0;
     93 |   padding-left: 0px;
     94 | }
     95 | 
     96 | pre.source span.fc {
     97 |   background-color:#ccffcc;
     98 | }
     99 | 
    100 | pre.source span.nc {
    101 |   background-color:#ffaaaa;
    102 | }
    103 | 
    104 | pre.source span.pc {
    105 |   background-color:#ffffcc;
    106 | }
    107 | 
    108 | pre.source span.bfc {
    109 |   background-image: url(branchfc.gif);
    110 |   background-repeat: no-repeat;
    111 |   background-position: 2px center;
    112 | }
    113 | 
    114 | pre.source span.bfc:hover {
    115 |   background-color:#80ff80;
    116 | }
    117 | 
    118 | pre.source span.bnc {
    119 |   background-image: url(branchnc.gif);
    120 |   background-repeat: no-repeat;
    121 |   background-position: 2px center;
    122 | }
    123 | 
    124 | pre.source span.bnc:hover {
    125 |   background-color:#ff8080;
    126 | }
    127 | 
    128 | pre.source span.bpc {
    129 |   background-image: url(branchpc.gif);
    130 |   background-repeat: no-repeat;
    131 |   background-position: 2px center;
    132 | }
    133 | 
    134 | pre.source span.bpc:hover {
    135 |   background-color:#ffff80;
    136 | }
    137 | 
    138 | table.coverage {
    139 |   empty-cells:show;
    140 |   border-collapse:collapse;
    141 | }
    142 | 
    143 | table.coverage thead {
    144 |   background-color:#e0e0e0;
    145 | }
    146 | 
    147 | table.coverage thead td {
    148 |   white-space:nowrap;
    149 |   padding:2px 14px 0px 6px;
    150 |   border-bottom:#b0b0b0 1px solid;
    151 | }
    152 | 
    153 | table.coverage thead td.bar {
    154 |   border-left:#cccccc 1px solid;
    155 | }
    156 | 
    157 | table.coverage thead td.ctr1 {
    158 |   text-align:right;
    159 |   border-left:#cccccc 1px solid;
    160 | }
    161 | 
    162 | table.coverage thead td.ctr2 {
    163 |   text-align:right;
    164 |   padding-left:2px;
    165 | }
    166 | 
    167 | table.coverage thead td.sortable {
    168 |   cursor:pointer;
    169 |   background-image:url(sort.gif);
    170 |   background-position:right center;
    171 |   background-repeat:no-repeat;
    172 | }
    173 | 
    174 | table.coverage thead td.up {
    175 |   background-image:url(up.gif);
    176 | }
    177 | 
    178 | table.coverage thead td.down {
    179 |   background-image:url(down.gif);
    180 | }
    181 | 
    182 | table.coverage tbody td {
    183 |   white-space:nowrap;
    184 |   padding:2px 6px 2px 6px;
    185 |   border-bottom:#d6d3ce 1px solid;
    186 | }
    187 | 
    188 | table.coverage tbody tr:hover {
    189 |   background: #f0f0d0 !important;
    190 | }
    191 | 
    192 | table.coverage tbody td.bar {
    193 |   border-left:#e8e8e8 1px solid;
    194 | }
    195 | 
    196 | table.coverage tbody td.ctr1 {
    197 |   text-align:right;
    198 |   padding-right:14px;
    199 |   border-left:#e8e8e8 1px solid;
    200 | }
    201 | 
    202 | table.coverage tbody td.ctr2 {
    203 |   text-align:right;
    204 |   padding-right:14px;
    205 |   padding-left:2px;
    206 | }
    207 | 
    208 | table.coverage tfoot td {
    209 |   white-space:nowrap;
    210 |   padding:2px 6px 2px 6px;
    211 | }
    212 | 
    213 | table.coverage tfoot td.bar {
    214 |   border-left:#e8e8e8 1px solid;
    215 | }
    216 | 
    217 | table.coverage tfoot td.ctr1 {
    218 |   text-align:right;
    219 |   padding-right:14px;
    220 |   border-left:#e8e8e8 1px solid;
    221 | }
    222 | 
    223 | table.coverage tfoot td.ctr2 {
    224 |   text-align:right;
    225 |   padding-right:14px;
    226 |   padding-left:2px;
    227 | }
    228 | 
    229 | .footer {
    230 |   margin-top:20px;
    231 |   border-top:#d6d3ce 1px solid;
    232 |   padding-top:2px;
    233 |   font-size:8pt;
    234 |   color:#a0a0a0;
    235 | }
    236 | 
    237 | .footer a {
    238 |   color:#a0a0a0;
    239 | }
    240 | 
    241 | .right {
    242 |   float:right;
    243 | }
    244 | 
    
    
    --------------------------------------------------------------------------------
    /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/report.gif:
    --------------------------------------------------------------------------------
    https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/report.gif
    
    
    --------------------------------------------------------------------------------
    /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/session.gif:
    --------------------------------------------------------------------------------
    https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/session.gif
    
    
    --------------------------------------------------------------------------------
    /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/sort.gif:
    --------------------------------------------------------------------------------
    https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/sort.gif
    
    
    --------------------------------------------------------------------------------
    /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/sort.js:
    --------------------------------------------------------------------------------
      1 | /*******************************************************************************
      2 |  * Copyright (c) 2009, 2023 Mountainminds GmbH & Co. KG and Contributors
      3 |  * This program and the accompanying materials are made available under
      4 |  * the terms of the Eclipse Public License 2.0 which is available at
      5 |  * http://www.eclipse.org/legal/epl-2.0
      6 |  *
      7 |  * SPDX-License-Identifier: EPL-2.0
      8 |  *
      9 |  * Contributors:
     10 |  *    Marc R. Hoffmann - initial API and implementation
     11 |  *
     12 |  *******************************************************************************/
     13 | 
     14 | (function () {
     15 | 
     16 |   /**
     17 |    * Sets the initial sorting derived from the hash.
     18 |    *
     19 |    * @param linkelementids
     20 |    *          list of element ids to search for links to add sort inidcator
     21 |    *          hash links
     22 |    */
     23 |   function initialSort(linkelementids) {
     24 |     window.linkelementids = linkelementids;
     25 |     var hash = window.location.hash;
     26 |     if (hash) {
     27 |       var m = hash.match(/up-./);
     28 |       if (m) {
     29 |         var header = window.document.getElementById(m[0].charAt(3));
     30 |         if (header) {
     31 |           sortColumn(header, true);
     32 |         }
     33 |         return;
     34 |       }
     35 |       var m = hash.match(/dn-./);
     36 |       if (m) {
     37 |         var header = window.document.getElementById(m[0].charAt(3));
     38 |         if (header) {
     39 |           sortColumn(header, false);
     40 |         }
     41 |         return
     42 |       }
     43 |     }
     44 |   }
     45 | 
     46 |   /**
     47 |    * Sorts the columns with the given header dependening on the current sort state.
     48 |    */
     49 |   function toggleSort(header) {
     50 |     var sortup = header.className.indexOf('down ') == 0;
     51 |     sortColumn(header, sortup);
     52 |   }
     53 | 
     54 |   /**
     55 |    * Sorts the columns with the given header in the given direction.
     56 |    */
     57 |   function sortColumn(header, sortup) {
     58 |     var table = header.parentNode.parentNode.parentNode;
     59 |     var body = table.tBodies[0];
     60 |     var colidx = getNodePosition(header);
     61 | 
     62 |     resetSortedStyle(table);
     63 | 
     64 |     var rows = body.rows;
     65 |     var sortedrows = [];
     66 |     for (var i = 0; i < rows.length; i++) {
     67 |       r = rows[i];
     68 |       sortedrows[parseInt(r.childNodes[colidx].id.slice(1))] = r;
     69 |     }
     70 | 
     71 |     var hash;
     72 | 
     73 |     if (sortup) {
     74 |       for (var i = sortedrows.length - 1; i >= 0; i--) {
     75 |         body.appendChild(sortedrows[i]);
     76 |       }
     77 |       header.className = 'up ' + header.className;
     78 |       hash = 'up-' + header.id;
     79 |     } else {
     80 |       for (var i = 0; i < sortedrows.length; i++) {
     81 |         body.appendChild(sortedrows[i]);
     82 |       }
     83 |       header.className = 'down ' + header.className;
     84 |       hash = 'dn-' + header.id;
     85 |     }
     86 | 
     87 |     setHash(hash);
     88 |   }
     89 | 
     90 |   /**
     91 |    * Adds the sort indicator as a hash to the document URL and all links.
     92 |    */
     93 |   function setHash(hash) {
     94 |     window.document.location.hash = hash;
     95 |     ids = window.linkelementids;
     96 |     for (var i = 0; i < ids.length; i++) {
     97 |         setHashOnAllLinks(document.getElementById(ids[i]), hash);
     98 |     }
     99 |   }
    100 | 
    101 |   /**
    102 |    * Extend all links within the given tag with the given hash.
    103 |    */
    104 |   function setHashOnAllLinks(tag, hash) {
    105 |     links = tag.getElementsByTagName("a");
    106 |     for (var i = 0; i < links.length; i++) {
    107 |         var a = links[i];
    108 |         var href = a.href;
    109 |         var hashpos = href.indexOf("#");
    110 |         if (hashpos != -1) {
    111 |             href = href.substring(0, hashpos);
    112 |         }
    113 |         a.href = href + "#" + hash;
    114 |     }
    115 |   }
    116 | 
    117 |   /**
    118 |    * Calculates the position of a element within its parent.
    119 |    */
    120 |   function getNodePosition(element) {
    121 |     var pos = -1;
    122 |     while (element) {
    123 |       element = element.previousSibling;
    124 |       pos++;
    125 |     }
    126 |     return pos;
    127 |   }
    128 | 
    129 |   /**
    130 |    * Remove the sorting indicator style from all headers.
    131 |    */
    132 |   function resetSortedStyle(table) {
    133 |     for (var c = table.tHead.firstChild.firstChild; c; c = c.nextSibling) {
    134 |       if (c.className) {
    135 |         if (c.className.indexOf('down ') == 0) {
    136 |           c.className = c.className.slice(5);
    137 |         }
    138 |         if (c.className.indexOf('up ') == 0) {
    139 |           c.className = c.className.slice(3);
    140 |         }
    141 |       }
    142 |     }
    143 |   }
    144 | 
    145 |   window['initialSort'] = initialSort;
    146 |   window['toggleSort'] = toggleSort;
    147 | 
    148 | })();
    149 | 
    
    
    --------------------------------------------------------------------------------
    /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/source.gif:
    --------------------------------------------------------------------------------
    https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/source.gif
    
    
    --------------------------------------------------------------------------------
    /jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/up.gif:
    --------------------------------------------------------------------------------
    https://raw.githubusercontent.com/azizutku/jacoco-aggregate-coverage-plugin/dfa71d68fcb46cb43ed8fbfeae006b705f989f71/jacocoaggregatecoverageplugin/src/main/resources/jacoco-resources/up.gif
    
    
    --------------------------------------------------------------------------------
    /settings.gradle.kts:
    --------------------------------------------------------------------------------
     1 | pluginManagement {
     2 |     repositories {
     3 |         google {
     4 |             content {
     5 |                 includeGroupByRegex("com\\.google.*")
     6 |             }
     7 |         }
     8 |         mavenCentral()
     9 |         gradlePluginPortal()
    10 |     }
    11 | }
    12 | dependencyResolutionManagement {
    13 |     repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    14 |     repositories {
    15 |         google()
    16 |         mavenCentral()
    17 |     }
    18 | }
    19 | 
    20 | rootProject.name = "Jacoco Aggregate Coverage Plugin"
    21 | include(":jacocoaggregatecoverageplugin")
    22 | 
    
    
    --------------------------------------------------------------------------------