diff --git a/README.adoc b/README.adoc index 1dc22b1d..a2128cec 100644 --- a/README.adoc +++ b/README.adoc @@ -1,77 +1 @@ -= Groovy website -The Groovy development team -:revdate: 24-02-2014 -:build-icon: http://ci.groovy-lang.org:8111/app/rest/builds/buildType:(id:Groovy_Website)/statusIcon -:noheader: -:groovy-www: http://groovy-lang.org/ -:groovy-ci: http://ci.groovy-lang.org/viewType.html?buildTypeId=Groovy_Website&guest=1 -:gradle: http://www.gradle.org -:markupte: http://docs.groovy-lang.org/latest/html/documentation/markup-template-engine.html - -[.left.text-left] -image::http://groovy-lang.org/img/groovy-logo.png[] -{groovy-www}[Groovy] is an agile and dynamic language for the Java Virtual Machine. It builds upon the strengths of Java, but has additional power features inspired by languages like Python, Ruby and Smalltalk. - -Groovy makes modern programming features available to Java developers with almost-zero learning curve as well as supports Domain-Specific Languages and other compact syntax so your code becomes easy to read and maintain. - -Groovy makes writing shell and build scripts easy with its powerful processing primitives, OO abilities and an Ant DSL. - -It also increases developer productivity by reducing scaffolding code when developing web, GUI, database or console applications. Groovy simplifies testing by supporting unit testing and mocking out-of-the-box. Groovy also seamlessly integrates with all existing Java classes and libraries and compiles straight to Java bytecode so you can use it anywhere you can use Java. - -== Sources for the Groovy website - -This project builds the Groovy website. It is using {gradle}[Gradle] and is fully statically generated. - -Build is image:{build-icon}[build status, link={groovy-ci}]. - -== Generating the site - ----- -git clone https://github.com/groovy/groovy-website -cd groovy-website -./gradlew webzip ----- - -The output can be found in the `build` directory: - ----- -build - |---- site : the generated static website - |---- reports : deadlinks report - |---- distributions : zip of the website ----- - -== Contributing - -The website is generated thanks to Gradle and makes use of the {markupte}[Markup Template Engine]. The structure of the -project consists of two modules: - ----- -generator : utility classes and model for generating the website -site : the website itself ----- - -The website subproject consists of: - ----- -src/main/site : sources for the static website - |--- assets : static resources such as images, CSS files, ... - |--- html : elements that templates include as raw HTML contents - |--- includes : includes used by templates - |--- layouts : layouts for the various pages - |--- pages : individual pages -build.gradle : website weaving logic ----- - -Additional details can be found in this http://melix.github.io/blog/2014/07/new-groovy-website.html[blog post]. - -== Continuous Integration - -The official CI server runs {groovy-ci}[here] (login as user guest and leave the password blank) and is sponsored by http://www.jetbrains.com[JetBrains]. - -WARNING: The website is continuously updated from the _master_ branch. This means that *every merge on master is immediately published*. Changes that need to be -applied on a specific date need to be done on a dedicated branch. - -== License - -Groovy is licensed under the terms of the http://www.apache.org/licenses/LICENSE-2.0.html[Apache License, Version 2.0] +Moved to: https://github.com/apache/groovy-website/ diff --git a/archive/LICENSE b/archive/LICENSE new file mode 100644 index 00000000..ad410e11 --- /dev/null +++ b/archive/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/archive/README.adoc b/archive/README.adoc new file mode 100644 index 00000000..1dc22b1d --- /dev/null +++ b/archive/README.adoc @@ -0,0 +1,77 @@ += Groovy website +The Groovy development team +:revdate: 24-02-2014 +:build-icon: http://ci.groovy-lang.org:8111/app/rest/builds/buildType:(id:Groovy_Website)/statusIcon +:noheader: +:groovy-www: http://groovy-lang.org/ +:groovy-ci: http://ci.groovy-lang.org/viewType.html?buildTypeId=Groovy_Website&guest=1 +:gradle: http://www.gradle.org +:markupte: http://docs.groovy-lang.org/latest/html/documentation/markup-template-engine.html + +[.left.text-left] +image::http://groovy-lang.org/img/groovy-logo.png[] +{groovy-www}[Groovy] is an agile and dynamic language for the Java Virtual Machine. It builds upon the strengths of Java, but has additional power features inspired by languages like Python, Ruby and Smalltalk. + +Groovy makes modern programming features available to Java developers with almost-zero learning curve as well as supports Domain-Specific Languages and other compact syntax so your code becomes easy to read and maintain. + +Groovy makes writing shell and build scripts easy with its powerful processing primitives, OO abilities and an Ant DSL. + +It also increases developer productivity by reducing scaffolding code when developing web, GUI, database or console applications. Groovy simplifies testing by supporting unit testing and mocking out-of-the-box. Groovy also seamlessly integrates with all existing Java classes and libraries and compiles straight to Java bytecode so you can use it anywhere you can use Java. + +== Sources for the Groovy website + +This project builds the Groovy website. It is using {gradle}[Gradle] and is fully statically generated. + +Build is image:{build-icon}[build status, link={groovy-ci}]. + +== Generating the site + +---- +git clone https://github.com/groovy/groovy-website +cd groovy-website +./gradlew webzip +---- + +The output can be found in the `build` directory: + +---- +build + |---- site : the generated static website + |---- reports : deadlinks report + |---- distributions : zip of the website +---- + +== Contributing + +The website is generated thanks to Gradle and makes use of the {markupte}[Markup Template Engine]. The structure of the +project consists of two modules: + +---- +generator : utility classes and model for generating the website +site : the website itself +---- + +The website subproject consists of: + +---- +src/main/site : sources for the static website + |--- assets : static resources such as images, CSS files, ... + |--- html : elements that templates include as raw HTML contents + |--- includes : includes used by templates + |--- layouts : layouts for the various pages + |--- pages : individual pages +build.gradle : website weaving logic +---- + +Additional details can be found in this http://melix.github.io/blog/2014/07/new-groovy-website.html[blog post]. + +== Continuous Integration + +The official CI server runs {groovy-ci}[here] (login as user guest and leave the password blank) and is sponsored by http://www.jetbrains.com[JetBrains]. + +WARNING: The website is continuously updated from the _master_ branch. This means that *every merge on master is immediately published*. Changes that need to be +applied on a specific date need to be done on a dedicated branch. + +== License + +Groovy is licensed under the terms of the http://www.apache.org/licenses/LICENSE-2.0.html[Apache License, Version 2.0] diff --git a/archive/build.gradle b/archive/build.gradle new file mode 100644 index 00000000..d56e185e --- /dev/null +++ b/archive/build.gradle @@ -0,0 +1,23 @@ +// Grab the plugin from a Maven Repo automatically +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.yahoo.platform.yui:yuicompressor:2.4.7' + } +} + +plugins { + id 'com.gradle.build-scan' version '1.0' +} + +buildScan { + licenseAgreementUrl = 'https://gradle.com/terms-of-service' + licenseAgree = 'yes' +} + +allprojects { + apply plugin: 'idea' +} + diff --git a/archive/buildSrc/build.gradle b/archive/buildSrc/build.gradle new file mode 100644 index 00000000..ce168b0b --- /dev/null +++ b/archive/buildSrc/build.gradle @@ -0,0 +1,8 @@ +repositories { + jcenter() +} + +dependencies { + compile 'org.apache.httpcomponents:httpclient:4.5.1' + compile 'com.yahoo.platform.yui:yuicompressor:2.4.7' +} diff --git a/archive/buildSrc/src/main/groovy/util/CheckLinks.groovy b/archive/buildSrc/src/main/groovy/util/CheckLinks.groovy new file mode 100644 index 00000000..3ac9eb26 --- /dev/null +++ b/archive/buildSrc/src/main/groovy/util/CheckLinks.groovy @@ -0,0 +1,95 @@ +package util + +import org.apache.http.client.config.CookieSpecs +import org.apache.http.client.config.RequestConfig +import org.apache.http.client.methods.CloseableHttpResponse +import org.apache.http.client.methods.HttpGet +import org.apache.http.impl.client.CloseableHttpClient +import org.apache.http.impl.client.HttpClients +import org.gradle.api.logging.Logger +import org.gradle.util.GFileUtils + +class CheckLinks { + Logger logger = null + List excludeFromChecks = [] + final Map deadLinks = [:] + Map isDead = [:] + def baseDir + + boolean checkIsDead(link, currentPath) { + if (excludeFromChecks.any { link.startsWith(it) }) { + // skip checking those links because they dramatically increase build time + // while being most likely ok because generated through changelog parsing + return false + } + + try { + URL url + boolean rejected = false + try { + url = URI.create(link).toURL() + } catch (e) { + if (e.message.contains('URI is not absolute')) { + rejected = true + } + } + if (rejected || !url) { + def path = "file:///${new File("$baseDir/${currentPath ? currentPath + '/' : ''}$link").canonicalPath.replace('\\', '/')}" + url = URI.create(path).toURL() + } + logger?.debug("Checking URL: $url") + def cx = url.openConnection() + if (cx instanceof HttpURLConnection) { + CloseableHttpClient httpclient = HttpClients.createDefault() + RequestConfig requestConfig = RequestConfig.custom() + .setSocketTimeout(5_000) + .setConnectTimeout(5_000) + .setConnectionRequestTimeout(5_000) + .setCookieSpec(CookieSpecs.STANDARD) + .build() + HttpGet httpget = new HttpGet(link) + httpget.config = requestConfig + CloseableHttpResponse response + try { + response = httpclient.execute(httpget) + if (response.statusLine.statusCode == 404) { + return true + } + } finally { + response.close() + } + } + } catch (e) { + logger?.debug e.message + return true + } + return false + } + + def checkLink(List dead, int line, String link, currentPath) { + if (!isDead.containsKey(link)) isDead[link] = checkIsDead(link, currentPath) + if (isDead[link]) { + dead << [line:line, link:link] + } + } + + def checkPage(File f) { + def currentPath = GFileUtils.relativePath(baseDir, f.parentFile) + f.eachLine('utf-8') { String line, int nb -> + def dead = [] + [/\shref=['"](.+?)['"]/, /src=['"](.+?)['"]/].each { regex -> + def matcher = line =~ regex + if (matcher) { + matcher.each { + def linkpath = it[1] + checkLink(dead, nb, linkpath, currentPath) + } + } + } + if (dead) { + deadLinks[f] = dead + } + } + } + +} diff --git a/archive/buildSrc/src/main/groovy/util/CssFilter.groovy b/archive/buildSrc/src/main/groovy/util/CssFilter.groovy new file mode 100644 index 00000000..b83d5cce --- /dev/null +++ b/archive/buildSrc/src/main/groovy/util/CssFilter.groovy @@ -0,0 +1,26 @@ +package util + +import com.yahoo.platform.yui.compressor.CssCompressor +import org.apache.tools.ant.filters.BaseFilterReader + +class CssFilter extends BaseFilterReader { + Writer writer + Thread worker + + CssFilter(Reader reader) { + super(new PipedReader()) + writer = new PipedWriter(this.@in) + def compressor = new CssCompressor(reader) + reader.close() + worker = Thread.start { + compressor.compress(writer, -1) + writer.close() + } + } + + void close() { + worker.join() + super.close() + } + +} diff --git a/archive/buildSrc/src/main/groovy/util/JsFilter.groovy b/archive/buildSrc/src/main/groovy/util/JsFilter.groovy new file mode 100644 index 00000000..ce19b2c3 --- /dev/null +++ b/archive/buildSrc/src/main/groovy/util/JsFilter.groovy @@ -0,0 +1,26 @@ +package util + +import com.yahoo.platform.yui.compressor.JavaScriptCompressor +import org.apache.tools.ant.filters.BaseFilterReader + +class JsFilter extends BaseFilterReader { + Writer writer + Thread worker + + JsFilter(Reader reader) { + super(new PipedReader()) + writer = new PipedWriter(this.@in) + def compressor = new JavaScriptCompressor(reader, null) + reader.close() + worker = Thread.start { + compressor.compress(writer, -1, true, false, false, false) + writer.close() + } + } + + void close() { + worker.join() + super.close() + } + +} diff --git a/archive/generator/build.gradle b/archive/generator/build.gradle new file mode 100644 index 00000000..5f9a5d6a --- /dev/null +++ b/archive/generator/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'groovy' + +repositories { + jcenter() +} + +dependencies { + ext.groovyVersion = '2.4.15' + ext.asciidocVersion = '1.5.8.1' + compile "org.codehaus.groovy:groovy:$groovyVersion" + compile "org.codehaus.groovy:groovy-json:$groovyVersion" + compile "org.codehaus.groovy:groovy-templates:$groovyVersion" + compile("org.asciidoctor:asciidoctorj:$asciidocVersion") { + // to fix intermittent errors, e.g. as described here: + // https://github.com/asciidoctor/asciidoctorj/issues/680 + // you might need to use a custom jruby version + //exclude(group: 'org.jruby', module: 'jruby-complete') + } + //compile "org.jruby:jruby-complete:9.1.17.0" +} + +compileGroovy.sourceCompatibility = '1.7' +compileGroovy.targetCompatibility = '1.7' diff --git a/archive/generator/src/main/groovy/generator/AsciidoctorFactory.groovy b/archive/generator/src/main/groovy/generator/AsciidoctorFactory.groovy new file mode 100644 index 00000000..72d48dd6 --- /dev/null +++ b/archive/generator/src/main/groovy/generator/AsciidoctorFactory.groovy @@ -0,0 +1,9 @@ +package generator + +import groovy.transform.CompileStatic +import org.asciidoctor.Asciidoctor + +@CompileStatic +class AsciidoctorFactory { + @Lazy static Asciidoctor instance = Asciidoctor.Factory.create() +} diff --git a/archive/generator/src/main/groovy/generator/ChangelogParser.groovy b/archive/generator/src/main/groovy/generator/ChangelogParser.groovy new file mode 100644 index 00000000..ee84faad --- /dev/null +++ b/archive/generator/src/main/groovy/generator/ChangelogParser.groovy @@ -0,0 +1,104 @@ +package generator + +import groovy.json.JsonSlurper +import model.Changelog +import model.Issue + +import java.util.regex.Pattern + +class ChangelogParser { + private static final String JIRA_SERVER = 'https://issues.apache.org/jira' + private static final String PROJECT_NAME = 'GROOVY' + private static final String PROJECT_ID = '12318123' + + private static final String LOGNOTES_INTRO = /Release Notes - Groovy/ + private static final String LOGNOTES_END = /<\/textarea>/ + private static final String BUGTYPE_MARK = '** ' + private static final String ITEM_MARK = ' * ' + private static final Pattern ITEM_PATTERN = ~/\[(GROOVY-[0-9]+)\] - (.+)/ + private static final String VERSION_PATTERN = /^((1\.)|[23]\.)/ + + static List fetchReleaseNotes(File cacheDirectory) { + def slurper = new JsonSlurper() + def versions = slurper.parse("$JIRA_SERVER/rest/api/2/project/$PROJECT_NAME/versions".toURL()) + def versionMap = versions.findAll { + it.name =~ VERSION_PATTERN && + it.released == true + }.collectEntries { + [fixName(it.name), it.id] + } + + def raw = versionMap.collect { name, id -> + println "Fetching changelog for version $name" + new Changelog(groovyVersion: name, issues: changelogHTML(id, cacheDirectory)) + } + createAggregates(raw, versionMap.keySet()) + } + + private static String fixName(String name) { + String id = name + String classifier = '' + + int idx = name.indexOf('-') + if (idx>0) { + classifier = name.substring(idx) + id = name - classifier + } + if (id.count('.')<2) { + // groovy 2.0 instead of 2.0.0 + id = "${id}.0" + } + "$id$classifier" + } + + private static List createAggregates(final List changelogs, final Set releasedVersions) { + def allMajor = changelogs.groupBy { + def v = it.groovyVersion + v.contains('-')?v-v.substring(v.indexOf('-')):v + }.findAll { ver, logs -> ver in releasedVersions } + allMajor.collect { k,v -> + def changelog = changelogs.find { it.groovyVersion == k } + if (!changelog) { + println "Not found: $k" + changelog = new Changelog(groovyVersion: k, issues:[]) + changelogs << changelog + } + v.each { + changelog.issues = [*changelog.issues, *it.issues].unique().sort { it.id } + } + } + changelogs + } + + private static List changelogHTML(String id, File cacheDir) { + def cache = new File(cacheDir, "changelog-${id}.html") + def log + if (cache.exists()) { + log = cache.getText('UTF-8') + } else { + log = new URL("$JIRA_SERVER/secure/ReleaseNote.jspa?version=$id&styleName=Text&projectId=$PROJECT_ID").getText('UTF-8') + cache.write(log, 'UTF-8') + } + boolean inNotes = false + String type = null + List issues = [] + + log.eachLine { line -> + if (line.startsWith(LOGNOTES_INTRO)) { + inNotes = true + } else if (line.startsWith(LOGNOTES_END)) { + inNotes = false + } else if (inNotes) { + if (line.startsWith(BUGTYPE_MARK)) { + type = line - BUGTYPE_MARK + } else if (line.startsWith(ITEM_MARK)) { + def m = ITEM_PATTERN.matcher(line) + m.find() + issues << new Issue(id: m.group(1), description: m.group(2), type: type) + } + } + } + + issues + } +} diff --git a/archive/generator/src/main/groovy/generator/DocUtils.groovy b/archive/generator/src/main/groovy/generator/DocUtils.groovy new file mode 100644 index 00000000..49d8609e --- /dev/null +++ b/archive/generator/src/main/groovy/generator/DocUtils.groovy @@ -0,0 +1,5 @@ +package generator + +class DocUtils { + @Lazy public static final String DOCS_BASEURL = System.getProperty('docs_baseurl') +} diff --git a/archive/generator/src/main/groovy/generator/DocumentationHTMLCleaner.groovy b/archive/generator/src/main/groovy/generator/DocumentationHTMLCleaner.groovy new file mode 100644 index 00000000..fafa9db9 --- /dev/null +++ b/archive/generator/src/main/groovy/generator/DocumentationHTMLCleaner.groovy @@ -0,0 +1,101 @@ +package generator + +import groovy.transform.CompileStatic + +/** + * This class is responsible for downloading a documentation page as generated through the Asciidoctor task + * of the Groovy build, then filter its contents in order to return only the body of the documentation, as HTML. + * + * @author Cédric Champeau + */ +@CompileStatic +class DocumentationHTMLCleaner { + private final static String BODY_START = / KNOWN_REPLACEMENTS = [ + /docs\.groovy-lang\.org\/(latest|next)\/html\/documentation\/gdk\.html/: 'groovy-lang.org/gdk.html', + /\/maven\/groovy-/: '/maven/apache-groovy-' + ] + + private static String cleanupPage(String location) { + def url = location.toURL() + try { + def fullHTML = url.getText('utf-8') + return extractBetween(fullHTML, BODY_START, BODY_END) + } catch (FileNotFoundException e) { + // 404 not found + } + + null + } + + private static String extractBetween(String html, String startString, String endString) { + def start = html.indexOf(startString) + if (start > 0) { + start = html.indexOf('>', start) + 1 + } + if (start > 1) { + def end = html.indexOf(endString, start) + if (end > start) { + return html.substring(start, end) + } + } + null + } + + public static DocPage parsePage(String location) { + String contents = cleanupPage(location) + if (contents==null) { + return new DocPage(content: "Contents not found for $location, most likely because this section has not yet been written.") + } + String toc = extractTOC(contents)?:'' + String main = extractBetween(contents, MAIN_START, MAIN_END)?:"Main body not found for $location" + main = replaceInternalLinks(main) + new DocPage(toc: toc, content: main) + } + + private static String replaceInternalLinks(String html) { + def replacer = { List it -> + def (String tag, String attr, String url) = [it[1], it[2], it[3]] + url = url.replaceAll(/x(.+)\.(?:pagespeed.+)/, '$1') + if (!url.startsWith('http') && !url.startsWith('#') && 'target.html'!=url) { + "$tag $attr'${DocUtils.DOCS_BASEURL}/html/documentation/$url'" + } else { + it[0] + } + } + html = html.replaceAll(/(a)\s+(href=)["'](.+?)["']/,replacer) + html = html.replaceAll(/(img)\s+(src=)["'](.+?)["']/,replacer) + KNOWN_REPLACEMENTS.each { link, repl -> + html = html.replaceAll(link, repl) + } + html + } + + private static String extractTOC(final String html) { + int start = html.indexOf(TOC_START) + if (start > 0) { + int end = html.indexOf(MAIN_START) + if (end>0) { + def out = html.substring(start, end).replace("
Table of Contents
", "") + end = out.size()-1 + while (!out.substring(end, out.size()).startsWith('')) end-- + return out.substring(0, end) + } + } + null + } + + static class DocPage { + String toc = '' + String content + } + +} diff --git a/archive/generator/src/main/groovy/generator/PageTemplate.groovy b/archive/generator/src/main/groovy/generator/PageTemplate.groovy new file mode 100644 index 00000000..8801d8db --- /dev/null +++ b/archive/generator/src/main/groovy/generator/PageTemplate.groovy @@ -0,0 +1,59 @@ +package generator + +import groovy.text.markup.BaseTemplate +import groovy.text.markup.MarkupTemplateEngine +import groovy.text.markup.TemplateConfiguration +import groovy.transform.CompileStatic + +@CompileStatic +abstract class PageTemplate extends BaseTemplate { + public static final String BASEDIR = "basePath" + + private final Map model + + PageTemplate( + final MarkupTemplateEngine templateEngine, + final Map model, final Map modelTypes, final TemplateConfiguration configuration) { + super(templateEngine, model, modelTypes, configuration) + this.model = model + } + + String relative(String path) { + String base = (String) model.get(BASEDIR) + if (base && !path.startsWith('http') && !path.startsWith(File.separator)) { + String up = "..${File.separator}" + "${up*(1+base.count(File.separator))}$path" + } else { + path + } + } + + /** + * Converts and outputs asciidoctor markup into HTML + * @param body the asciidoctor markup + */ + void asciidoc(String body, Map options=[:]) { + yieldUnescaped asciidocText(body, options) + } + + /** + * Converts and returns asciidoctor markup into HTML. This method + * does *not* automatically render the result so it is possible + * to post-process the generated HTML. + * @param body the asciidoctor markup + */ + String asciidocText(String body, Map options=[:]) { + def asciidoctor = AsciidoctorFactory.instance + def attributes = options.attributes + if (!attributes) { + attributes = [:] + options.put('attributes', attributes) + } + attributes['source-highlighter'] = 'prettify' + asciidoctor.convert(body,options) + } + + String latestDocURL(String target) { + "${DocUtils.DOCS_BASEURL}/html/$target" + } +} diff --git a/archive/generator/src/main/groovy/generator/SiteGenerator.groovy b/archive/generator/src/main/groovy/generator/SiteGenerator.groovy new file mode 100644 index 00000000..65a92c8c --- /dev/null +++ b/archive/generator/src/main/groovy/generator/SiteGenerator.groovy @@ -0,0 +1,277 @@ +package generator + +import groovy.io.FileType +import groovy.text.markup.MarkupTemplateEngine +import groovy.text.markup.TemplateConfiguration +import groovy.transform.CompileStatic +import model.Changelog +import model.Page +import model.Section +import model.SectionItem +import model.SiteMap + +import java.nio.file.FileSystems +import java.nio.file.Path +import java.nio.file.WatchEvent + +import static generator.DocumentationHTMLCleaner.parsePage +import static java.nio.file.StandardWatchEventKinds.* + +@CompileStatic +class SiteGenerator { + + private final static Closure SEMANTIC_SORT = { String v1, String v2 -> + List items1 = decomposeVersion(v1) + List items2 = decomposeVersion(v2) + for (int i=0; i=items2.size()) { + return 1 + } + if (i>=items1.size()) { + return -1 + } + def p1 = items1[i] + def p2 = items2[i] + if (p1.isNumber()) { + if (p2.isNumber()) { + def pi1 = p1.toInteger() + def pi2 = p2.toInteger() + + if (pi1 < pi2) { + return 1 + } else if (pi1 > pi2) { + return -1 + } + } else { + return -1 + } + } else if (p2.isNumber()) { + return 1 + } else { + return p2 <=> p1 + } + } + 0 + } + + File sourcesDir + File outputDir + String sitemapFilename + + private MarkupTemplateEngine tplEngine + private SiteMap siteMap + + void setup() { + + println "Generating website using Groovy ${GroovySystem.version}" + + def tplConf = new TemplateConfiguration() + tplConf.autoIndent = true + tplConf.autoNewLine = true + tplConf.baseTemplateClass = PageTemplate + + def classLoader = new URLClassLoader([sourcesDir.toURI().toURL()] as URL[], this.class.classLoader) + tplEngine = new MarkupTemplateEngine(classLoader, tplConf, new MarkupTemplateEngine.CachingTemplateResolver()) + + siteMap = SiteMap.from(new File(sourcesDir, sitemapFilename)) + + } + + void render(String page, String target = null, Map model = [:], String baseDir=null) { + model.menu = siteMap.menu + model.currentPage = target + target = target ?: page + File root + if (baseDir) { + root = new File(outputDir, baseDir) + model[PageTemplate.BASEDIR] = baseDir + root.mkdirs() + } else { + root = outputDir + } + + new File(root,"${target}.html").write(tplEngine.createTemplateByPath("pages/${page}.groovy").make(model).toString(), 'utf-8') + } + + void generateSite() { + long sd = System.currentTimeMillis() + setup() + + List changelogs = [] + if (siteMap.changelogs) { + def cacheDir = new File(new File('build'), 'cache') + cacheDir.mkdirs() + println "Cache directory: $cacheDir" + changelogs = ChangelogParser.fetchReleaseNotes(cacheDir) + } + + renderDocumentation() + + renderPages(changelogs) + + renderChangelogs(changelogs) + + if (siteMap.releaseNotes) { + renderReleaseNotes() + } + + if (siteMap.wiki) { + renderWiki() + } + + long dur = System.currentTimeMillis() - sd + println "Generated site into $outputDir in ${dur}ms" + } + + private List
renderDocumentation() { + siteMap.documentationSections.each { Section section -> + section.items.each { SectionItem item -> + if (item.generate) { + println "Generating documentation page [$item.name]" + render 'docpage', item.targetFilename, [ + category: 'Learn', + title : item.name, + page : parsePage("${DocUtils.DOCS_BASEURL}/html/documentation/${item.sourceFilename}.html")] + } + } + } + } + + private List renderPages(List changelogs) { + siteMap.pages.each { Page page -> + println "Rendering individual page [$page.source]" + if ('changelogs' == page.source) { + page.model.versions = changelogs.groovyVersion.sort(SEMANTIC_SORT) + } + render page.source, page.target, page.model + } + } + + private List renderChangelogs(List changelogs) { + changelogs.each { + println "Rendering changelog for Groovy $it.groovyVersion" + render 'changelog', "changelog-$it.groovyVersion", [groovyVersion: it.groovyVersion, issues: it.issues], 'changelogs' + } + } + + private void renderReleaseNotes() { + def releaseNotesVersions = new TreeSet(new Comparator() { + @Override + int compare(final String v1, final String v2) { + v2.toDouble() <=> v1.toDouble() + } + }) + new File(sourcesDir, 'releasenotes').eachFile(FileType.FILES) { File file -> + def name = file.name.substring(0, file.name.lastIndexOf('.adoc')) + def version = name - 'groovy-' + releaseNotesVersions << version + println "Rendering release notes for Groovy $version" + render 'release-notes', name, [notes: file.getText('utf-8'), groovyVersion: version], 'releasenotes' + } + render 'releases', 'releases', [versions: releaseNotesVersions] + } + + private void renderWiki() { + def asciidoctor = AsciidoctorFactory.instance + println "Rendering wiki" + + def wikiDir = new File(sourcesDir, "wiki") + def gepList = [:] + wikiDir.eachFileRecurse { f-> + if (f.name.endsWith('.adoc')) { + def header = asciidoctor.readDocumentHeader(f) + def bn = f.name.substring(0, f.name.lastIndexOf('.adoc')) + def author = header.author?.fullName + if (!author) { + author = header.authors*.fullName.join(', ') + } + println "Rendering $header.documentTitle.combined${author ? ' by ' + author : ''}" + def relativePath = [] + def p = f.parentFile + while (p != wikiDir) { + relativePath << p.name + p = p.parentFile + } + String baseDir = relativePath ? "wiki${File.separator}${relativePath.join(File.separator)}" : 'wiki' + render 'wiki', bn, [notes:f.getText('utf-8'), header: header], baseDir + if (f.name.startsWith('GEP-')) { + gepList[bn] = header.documentTitle.subtitle + } + } + } + render 'geps', "geps", [list: gepList], 'wiki' + } + + static void main(String... args) { + def sourcesDir = args[0] as File + def outputDir = args[1] as File + def sitemapFilename = args[2] + def generator = new SiteGenerator(sourcesDir: sourcesDir, outputDir: outputDir, sitemapFilename: sitemapFilename) + boolean watchMode = args.length > 3 ? Boolean.valueOf(args[3]) : false + generator.generateSite() + + if (watchMode) { + println "Started watch mode" + def watcher = FileSystems.default.newWatchService() + + sourcesDir.toPath().register(watcher, + ENTRY_CREATE, + ENTRY_DELETE, + ENTRY_MODIFY) + + sourcesDir.eachDirRecurse { File f -> + f.toPath().register(watcher, + ENTRY_CREATE, + ENTRY_DELETE, + ENTRY_MODIFY) + } + + def existingDirectories = ['pages', 'layouts', 'includes', 'html', 'assets', 'css', 'fonts', 'img', 'js', 'vendor'] + + while (true) { + def key = watcher.take() + def pollEvents = (List>) key.pollEvents() + + def changed = pollEvents.collect { "${it.context()}".toString() }.join(', ') + + // only generate when the event refers to the actual file modified / created / added + // as otherwise the watcher service generates two events: + // 1) one for directory containing the modified file, and + // 2) one for the actual file being modified + // this checks avoid getting two events for one change + if (existingDirectories.every { !changed.contains(it) }) { + try { + println "Regenerating site due to changes in: ${changed}" + // todo: selective regeneration + generator.generateSite() + } finally { + key.reset() + } + } + } + } + } + + static List decomposeVersion(String version) { + String qualifier = '' + if (version.indexOf('-')>0) { + qualifier = version.substring(version.indexOf('-')) + version = version - qualifier + } + List parts = version.split(/\./).toList() + if (qualifier) { + parts << qualifier + } + parts + } + + static boolean exists(String u) { + def url = new URL(u) + HttpURLConnection.setFollowRedirects(false) + HttpURLConnection connection = (HttpURLConnection) url.openConnection() + connection.setRequestMethod("HEAD") + // pretend to be a browser to keep fussy websites a little happier + connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 (.NET CLR 3.5.30729)") + return connection.responseCode == HttpURLConnection.HTTP_OK + } +} diff --git a/archive/generator/src/main/groovy/model/Book.groovy b/archive/generator/src/main/groovy/model/Book.groovy new file mode 100644 index 00000000..4537ace1 --- /dev/null +++ b/archive/generator/src/main/groovy/model/Book.groovy @@ -0,0 +1,17 @@ +package model + +import groovy.transform.CompileStatic + +@CompileStatic +class Book { + String title + String authors + String cover + String url + String description + + void authors (String authors) { this.authors = authors } + void cover (String cover) { this.cover = cover } + void url (String url) { this.url = url } + void description(String description) { this.description = description } +} diff --git a/archive/generator/src/main/groovy/model/Changelog.groovy b/archive/generator/src/main/groovy/model/Changelog.groovy new file mode 100644 index 00000000..b7ba9107 --- /dev/null +++ b/archive/generator/src/main/groovy/model/Changelog.groovy @@ -0,0 +1,11 @@ +package model + +import groovy.transform.CompileStatic +import groovy.transform.ToString + +@CompileStatic +@ToString +class Changelog { + String groovyVersion + List issues +} diff --git a/archive/generator/src/main/groovy/model/Course.groovy b/archive/generator/src/main/groovy/model/Course.groovy new file mode 100644 index 00000000..68f86183 --- /dev/null +++ b/archive/generator/src/main/groovy/model/Course.groovy @@ -0,0 +1,34 @@ +package model + +import groovy.transform.CompileStatic + +@CompileStatic +class Course { + + String title + String instructor + String url + String description + String cover + + void title(String title) { + this.title = title + } + + void instructor(String instructor) { + this.instructor = instructor + } + + void url(String url){ + this.url = url + } + + void description(String description){ + this.description = description + } + + void cover(String cover){ + this.cover = cover + } + +} diff --git a/archive/generator/src/main/groovy/model/Distribution.groovy b/archive/generator/src/main/groovy/model/Distribution.groovy new file mode 100644 index 00000000..55ef17c5 --- /dev/null +++ b/archive/generator/src/main/groovy/model/Distribution.groovy @@ -0,0 +1,19 @@ +package model + +import groovy.transform.CompileStatic + +@CompileStatic +class Distribution { + String name + Closure description + List packages = [] + + void description(Closure cl) { this.description = cl } + + void version(String name, Closure versionSpec) { + DownloadPackage pkg = new DownloadPackage(version:name) + def clone = versionSpec.rehydrate(pkg,pkg,pkg) + clone() + packages.add(pkg) + } +} diff --git a/archive/generator/src/main/groovy/model/DownloadPackage.groovy b/archive/generator/src/main/groovy/model/DownloadPackage.groovy new file mode 100644 index 00000000..074a38c4 --- /dev/null +++ b/archive/generator/src/main/groovy/model/DownloadPackage.groovy @@ -0,0 +1,28 @@ +package model + +import groovy.transform.CompileStatic + +@CompileStatic +class DownloadPackage { + String version + String releaseNotes + String windowsInstaller + boolean stable = false + boolean archive = false + + void releaseNotes(String notes) { + releaseNotes = notes + } + + void windowsInstaller(String installer) { + windowsInstaller = installer + } + + void stable(boolean b) { + stable = b + } + + void archive(boolean b) { + archive = b + } +} diff --git a/archive/generator/src/main/groovy/model/Ecosystem.groovy b/archive/generator/src/main/groovy/model/Ecosystem.groovy new file mode 100644 index 00000000..17a102f7 --- /dev/null +++ b/archive/generator/src/main/groovy/model/Ecosystem.groovy @@ -0,0 +1,13 @@ +package model + +import groovy.transform.CompileStatic + +@CompileStatic +class Ecosystem extends LinkedHashMap { + void project(String name, Closure entrySpec) { + def entry = new EcosystemEntry(name:name) + def clone = entrySpec.rehydrate(entry,entry,entry) + clone() + put(name, entry) + } +} diff --git a/archive/generator/src/main/groovy/model/EcosystemEntry.groovy b/archive/generator/src/main/groovy/model/EcosystemEntry.groovy new file mode 100644 index 00000000..55dc9fd0 --- /dev/null +++ b/archive/generator/src/main/groovy/model/EcosystemEntry.groovy @@ -0,0 +1,18 @@ +package model + +import groovy.transform.ToString + +import groovy.transform.CompileStatic + +@CompileStatic +@ToString(includeNames=true) +class EcosystemEntry { + String name + String url + String description + String logo + + void url(String url) { this.url = url } + void description(String description) { this.description = description } + void logo(String logo) { this.logo = logo } +} diff --git a/archive/generator/src/main/groovy/model/Event.groovy b/archive/generator/src/main/groovy/model/Event.groovy new file mode 100644 index 00000000..6eaf7d7e --- /dev/null +++ b/archive/generator/src/main/groovy/model/Event.groovy @@ -0,0 +1,19 @@ +package model + +import groovy.transform.CompileStatic + +@CompileStatic +class Event { + String name + String location + String date + String description + String url + String logo + + void location (String location) { this.location = location } + void date (String date) { this.date = date } + void description(String description) { this.description = description } + void url (String url) { this.url = url } + void logo (String logo) { this.logo = logo } +} diff --git a/archive/generator/src/main/groovy/model/Events.groovy b/archive/generator/src/main/groovy/model/Events.groovy new file mode 100644 index 00000000..8767dece --- /dev/null +++ b/archive/generator/src/main/groovy/model/Events.groovy @@ -0,0 +1,13 @@ +package model + +import groovy.transform.CompileStatic + +@CompileStatic +class Events extends LinkedHashMap { + void event(String name, Closure eventClosure) { + def entry = new Event(name: name) + def clone = eventClosure.rehydrate(entry, entry, entry) + clone() + put(name, entry) + } +} diff --git a/archive/generator/src/main/groovy/model/Issue.groovy b/archive/generator/src/main/groovy/model/Issue.groovy new file mode 100644 index 00000000..db4b1ee7 --- /dev/null +++ b/archive/generator/src/main/groovy/model/Issue.groovy @@ -0,0 +1,14 @@ +package model + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +@CompileStatic +@ToString +@EqualsAndHashCode +class Issue { + String id + String type + String description +} diff --git a/archive/generator/src/main/groovy/model/Library.groovy b/archive/generator/src/main/groovy/model/Library.groovy new file mode 100644 index 00000000..879cd98f --- /dev/null +++ b/archive/generator/src/main/groovy/model/Library.groovy @@ -0,0 +1,13 @@ +package model + +import groovy.transform.CompileStatic + +@CompileStatic +class Library extends LinkedHashMap { + void book(String title, Closure bookClosure) { + def book = new Book(title: title) + def clone = bookClosure.rehydrate(book, book, book) + clone() + put(title, book) + } +} diff --git a/archive/generator/src/main/groovy/model/Menu.groovy b/archive/generator/src/main/groovy/model/Menu.groovy new file mode 100644 index 00000000..9e495eda --- /dev/null +++ b/archive/generator/src/main/groovy/model/Menu.groovy @@ -0,0 +1,21 @@ +package model + +import groovy.transform.CompileStatic + +@CompileStatic +class Menu extends LinkedHashMap> { + + void group(String name, Closure groupSpec) { + Group g = new Group() + def clone = groupSpec.rehydrate(g,g,g) + clone() + put(name, g.items) + } + + private static class Group { + List items = [] + void item(String name, String link, String style=null) { + items << new MenuItem(name:name, link: link, style:style) + } + } +} diff --git a/archive/generator/src/main/groovy/model/MenuItem.groovy b/archive/generator/src/main/groovy/model/MenuItem.groovy new file mode 100644 index 00000000..6e580744 --- /dev/null +++ b/archive/generator/src/main/groovy/model/MenuItem.groovy @@ -0,0 +1,13 @@ +package model + +import groovy.transform.ToString + +import groovy.transform.CompileStatic + +@CompileStatic +@ToString(includeNames=true) +class MenuItem { + String name + String link + String style +} diff --git a/archive/generator/src/main/groovy/model/Page.groovy b/archive/generator/src/main/groovy/model/Page.groovy new file mode 100644 index 00000000..5b29679c --- /dev/null +++ b/archive/generator/src/main/groovy/model/Page.groovy @@ -0,0 +1,10 @@ +package model + +import groovy.transform.CompileStatic + +@CompileStatic +class Page { + String source + String target + Map model = [:] +} diff --git a/archive/generator/src/main/groovy/model/Section.groovy b/archive/generator/src/main/groovy/model/Section.groovy new file mode 100644 index 00000000..b0b14fdf --- /dev/null +++ b/archive/generator/src/main/groovy/model/Section.groovy @@ -0,0 +1,21 @@ +package model + +import groovy.transform.ToString + +@ToString(includeNames=true) +import groovy.transform.CompileStatic + +@CompileStatic +class Section { + String name + String icon + List items = [] + + void item(String name, String targetFile, String sourceFile, boolean generate = true) { + items.add(new SectionItem(name: name, sourceFilename: sourceFile, targetFilename: targetFile, generate:generate)) + } + + String getAnchor() { + name.replaceAll('[^a-zA-Z0-9]','').toLowerCase() + } +} diff --git a/archive/generator/src/main/groovy/model/SectionItem.groovy b/archive/generator/src/main/groovy/model/SectionItem.groovy new file mode 100644 index 00000000..7379b237 --- /dev/null +++ b/archive/generator/src/main/groovy/model/SectionItem.groovy @@ -0,0 +1,14 @@ +package model + +import groovy.transform.ToString + +import groovy.transform.CompileStatic + +@CompileStatic +@ToString(includeNames=true) +class SectionItem { + String name + String targetFilename + String sourceFilename + boolean generate = true +} diff --git a/archive/generator/src/main/groovy/model/SiteMap.groovy b/archive/generator/src/main/groovy/model/SiteMap.groovy new file mode 100644 index 00000000..89ad7996 --- /dev/null +++ b/archive/generator/src/main/groovy/model/SiteMap.groovy @@ -0,0 +1,137 @@ +package model + +import groovy.transform.ToString +import org.codehaus.groovy.control.CompilerConfiguration + +import groovy.transform.CompileStatic +import org.codehaus.groovy.control.customizers.ImportCustomizer + +@CompileStatic +@ToString(includeNames=true) +class SiteMap { + final List
documentationSections = [] + final List distributions = [] + final Menu menu = new Menu() + final Ecosystem ecosystem = new Ecosystem() + final Events allEvents = new Events() + final Library library = new Library() + final List allDocVersions = [] + final List pages = [] + final List usergroups = [] + final List