Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial import.

  • Loading branch information...
commit 4ff86c548ccff4c0c42183aabd70a0d320436689 0 parents
@bluesliverx bluesliverx authored
Showing with 5,172 additions and 0 deletions.
  1. +42 −0 .classpath
  2. +11 −0 .gitignore
  3. +32 −0 .project
  4. +4 −0 .settings/com.springsource.sts.grails.core.prefs
  5. +3 −0  .settings/org.codehaus.groovy.eclipse.preferences.prefs
  6. +4 −0 .settings/org.eclipse.wst.common.project.facet.core.xml
  7. +56 −0 NewDocGrailsPlugin.groovy
  8. +28 −0 README.md
  9. +6 −0 application.properties
  10. +44 −0 grails-app/conf/BuildConfig.groovy
  11. +24 −0 grails-app/conf/Config.groovy
  12. +32 −0 grails-app/conf/DataSource.groovy
  13. +13 −0 grails-app/conf/UrlMappings.groovy
  14. +54 −0 grails-app/views/error.gsp
  15. +30 −0 scripts/MigrateDoc.groovy
  16. +28 −0 scripts/NewDoc.groovy
  17. +10 −0 scripts/_Install.groovy
  18. +410 −0 scripts/_NewDocs.groovy
  19. +5 −0 scripts/_Uninstall.groovy
  20. +10 −0 scripts/_Upgrade.groovy
  21. +375 −0 src/groovy/grails/plugins/newdoc/DocEngine.groovy
  22. +616 −0 src/groovy/grails/plugins/newdoc/DocPublisher.groovy
  23. +127 −0 src/groovy/grails/plugins/newdoc/LegacyDocMigrator.groovy
  24. +17 −0 src/groovy/grails/plugins/newdoc/internal/FileResourceChecker.groovy
  25. +57 −0 src/groovy/grails/plugins/newdoc/internal/LegacyTocStrategy.groovy
  26. +32 −0 src/groovy/grails/plugins/newdoc/internal/StringEscapeCategory.groovy
  27. +16 −0 src/groovy/grails/plugins/newdoc/internal/UserGuideNode.groovy
  28. +102 −0 src/groovy/grails/plugins/newdoc/internal/YamlTocStrategy.groovy
  29. +4 −0 src/template/css/custom.css
  30. +723 −0 src/template/css/main.css
  31. +17 −0 src/template/css/menu.css
  32. +148 −0 src/template/css/pdf.css
  33. +431 −0 src/template/css/ref.css
  34. +139 −0 src/template/css/skin.css
  35. +123 −0 src/template/css/tools.css
  36. BIN  src/template/img/default/bullet.gif
  37. BIN  src/template/img/default/linear-gradient-green.png
  38. BIN  src/template/img/default/linear-gradient.png
  39. BIN  src/template/img/default/separator-horizontal.gif
  40. BIN  src/template/img/default/separator-menu.png
  41. BIN  src/template/img/default/separator-vertical.gif
  42. BIN  src/template/img/favicon.ico
  43. BIN  src/template/img/grails-icon.png
  44. BIN  src/template/img/grails.png
  45. BIN  src/template/img/groovy.png
  46. BIN  src/template/img/note.gif
  47. BIN  src/template/img/springsource-logo.png
  48. BIN  src/template/img/warning.gif
  49. +57 −0 src/template/js/docs.js
  50. +10 −0 src/template/log4j.properties
  51. +142 −0 src/template/style/guideItem.html
  52. +17 −0 src/template/style/index.html
  53. +140 −0 src/template/style/layout.html
  54. +13 −0 src/template/style/menu.html
  55. +98 −0 src/template/style/referenceItem.html
  56. +4 −0 src/template/style/section.html
  57. +1 −0  web-app/WEB-INF/.gitignore
  58. +42 −0 web-app/WEB-INF/applicationContext.xml
  59. +14 −0 web-app/WEB-INF/sitemesh.xml
  60. +550 −0 web-app/WEB-INF/tld/grails.tld
  61. +311 −0 web-app/WEB-INF/tld/spring.tld
42 .classpath
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src/java"/>
+ <classpathentry kind="src" path="src/groovy"/>
+ <classpathentry kind="src" path="grails-app/conf"/>
+ <classpathentry kind="src" path="grails-app/controllers"/>
+ <classpathentry kind="src" path="grails-app/domain"/>
+ <classpathentry kind="src" path="grails-app/services"/>
+ <classpathentry kind="src" path="grails-app/taglib"/>
+ <classpathentry kind="src" path="grails-app/utils"/>
+ <classpathentry kind="src" path="test/integration"/>
+ <classpathentry kind="src" path="test/unit"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry kind="con" path="com.springsource.sts.grails.core.CLASSPATH_CONTAINER"/>
+ <classpathentry exported="true" kind="con" path="GROOVY_DSL_SUPPORT"/>
+ <classpathentry kind="src" path=".link_to_grails_plugins/release-1.0.0.RC3/src/groovy">
+ <attributes>
+ <attribute name="com.springsource.sts.grails.core.SOURCE_FOLDER" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="src" path=".link_to_grails_plugins/release-1.0.0.RC3/src/java">
+ <attributes>
+ <attribute name="com.springsource.sts.grails.core.SOURCE_FOLDER" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry excluding="BuildConfig.groovy|*DataSource.groovy|UrlMappings.groovy|Config.groovy|BootStrap.groovy|spring/resources.groovy" kind="src" path=".link_to_grails_plugins/svn-1.0.0.M1/grails-app/conf">
+ <attributes>
+ <attribute name="com.springsource.sts.grails.core.SOURCE_FOLDER" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="src" path=".link_to_grails_plugins/svn-1.0.0.M1/src/groovy">
+ <attributes>
+ <attribute name="com.springsource.sts.grails.core.SOURCE_FOLDER" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="src" path=".link_to_grails_plugins/tomcat-1.3.7/src/groovy">
+ <attributes>
+ <attribute name="com.springsource.sts.grails.core.SOURCE_FOLDER" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="output" path="web-app/WEB-INF/classes"/>
+</classpath>
11 .gitignore
@@ -0,0 +1,11 @@
+*.class
+target
+*.zip
+plugin.xml
+*.log
+*plugin.xml
+*.pom
+*.md5
+*.sha1
+.classpath
+.link_to_grails_plugins
32 .project
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>NewDoc</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.wst.common.project.facet.core.builder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.springsource.sts.grails.core.nature</nature>
+ <nature>org.eclipse.jdt.groovy.core.groovyNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ <nature>org.eclipse.wst.common.project.facet.core.nature</nature>
+ </natures>
+ <linkedResources>
+ <link>
+ <name>.link_to_grails_plugins</name>
+ <type>2</type>
+ <locationURI>GRAILS_ROOT/1.3.7/projects/NewDoc/plugins</locationURI>
+ </link>
+ </linkedResources>
+</projectDescription>
4 .settings/com.springsource.sts.grails.core.prefs
@@ -0,0 +1,4 @@
+#Thu Sep 01 13:33:53 MDT 2011
+com.springsource.sts.grails.core.com.springsource.sts.grails.core.install.name=Grails 1.3.7
+com.springsource.sts.grails.core.use.default.install=false
+eclipse.preferences.version=1
3  .settings/org.codehaus.groovy.eclipse.preferences.prefs
@@ -0,0 +1,3 @@
+#Created by grails
+eclipse.preferences.version=1
+groovy.dont.generate.class.files=true
4 .settings/org.eclipse.wst.common.project.facet.core.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<faceted-project>
+ <installed facet="grails.app" version="1.0"/>
+</faceted-project>
56 NewDocGrailsPlugin.groovy
@@ -0,0 +1,56 @@
+class NewDocGrailsPlugin {
+ // the plugin version
+ def version = "0.1-SNAPSHOT"
+ // the version or versions of Grails the plugin is designed for
+ def grailsVersion = "1.3.7 > *"
+ // the other plugins this plugin depends on
+ def dependsOn = [:]
+ // resources that are excluded from plugin packaging
+ def pluginExcludes = [
+ "grails-app/views/error.gsp"
+ ]
+
+ def license = "APACHE"
+ def organization = [ name:"Adaptive Computing", url:"http://adaptivecomputing.com" ]
+ def issueManagement = [ system:"GitHub", url:"http://github.com/adaptivecomputing/grails-new-doc/issues" ]
+ def scm = [ url:"http://github.com/adaptivecomputing/grails-new-doc" ]
+
+ def author = "Brian Saville"
+ def authorEmail = "bsaville@adaptivecomputing.com"
+ def title = "New Grails Documentation (2.x docs in 1.x)"
+ def description = '''\
+This plugin is a backport of the additional functionality offered in the grails doc command in Grails 2.0.x. It allows
+YAML syntax to be used with a table of contents. This also fixes the issue with duplicates in groovy doc by specifically
+including the src/groovy, src/java and grails-app folders.
+'''
+
+ // URL to the plugin's documentation
+ def documentation = "http://grails.org/plugin/new-doc"
+
+ def doWithWebDescriptor = { xml ->
+ // TODO Implement additions to web.xml (optional), this event occurs before
+ }
+
+ def doWithSpring = {
+ // TODO Implement runtime spring config (optional)
+ }
+
+ def doWithDynamicMethods = { ctx ->
+ // TODO Implement registering dynamic methods to classes (optional)
+ }
+
+ def doWithApplicationContext = { applicationContext ->
+ // TODO Implement post initialization spring config (optional)
+ }
+
+ def onChange = { event ->
+ // TODO Implement code that is executed when any artefact that this plugin is
+ // watching is modified and reloaded. The event contains: event.source,
+ // event.application, event.manager, event.ctx, and event.plugin.
+ }
+
+ def onConfigChange = { event ->
+ // TODO Implement code that is executed when the project configuration changes.
+ // The event is the same as for 'onChange'.
+ }
+}
28 README.md
@@ -0,0 +1,28 @@
+This plugin is a backport of Grails 2.x documentation (toc.yml, gdoc file structure without section numbers, fixed groovydoc duplicates, etc) to Grails 1.x projects. It provides two scripts.
+
+## Commands
+
+### grails new-doc
+
+This command runs the documentation generation, including groovydoc and generating a guide if it exists at src/docs/guide. See http://grails.org/doc/2.0.0.M2/guide/conf.html#docengine for more information.
+
+### grails migrate-doc
+
+This command migrates the Grails 1.x guide documentation into the 2.x format, including generating a links.yml file for legacy links.
+
+NOTE: This command is untested as of yet!
+
+## Advantages of Grails 2.x Docs
+
+* Use of a single toc.yml file to control titles of pages/sections and their placement in the guide
+* No duplicates of classes in groovydoc (see http://jira.grails.org/browse/GRAILS-6530)
+* Improved look for the guide (just like the grails official docs)
+
+## Caveats
+
+* Duplicates in groovydoc is avoided by using the patch in GRAILS-6530. A more elegant and stable method is actually implemented in Grails 2.x, but it involved another property in the build scripts. For this reason, only src/groovy, src/java, and grails-app are included in groovydoc generation.
+
+## Release Notes
+
+* 0.1
+** Initial release
6 application.properties
@@ -0,0 +1,6 @@
+#Grails Metadata file
+#Thu Sep 01 13:33:52 MDT 2011
+app.grails.version=1.3.7
+app.name=NewGrailsDoc
+plugins.hibernate=1.3.7
+plugins.tomcat=1.3.7
44 grails-app/conf/BuildConfig.groovy
@@ -0,0 +1,44 @@
+grails.project.class.dir = "target/classes"
+grails.project.test.class.dir = "target/test-classes"
+grails.project.test.reports.dir = "target/test-reports"
+//grails.project.war.file = "target/${appName}-${appVersion}.war"
+
+// Disable SCM notification
+grails.release.scm.enabled = false
+
+grails.project.dependency.resolution = {
+ // inherit Grails' default dependencies
+ inherits("global") {
+ // uncomment to disable ehcache
+ // excludes 'ehcache'
+ }
+ log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
+ repositories {
+ grailsPlugins()
+ grailsHome()
+ grailsCentral()
+
+ // uncomment the below to enable remote dependency resolution
+ // from public Maven repositories
+ mavenLocal()
+ mavenCentral()
+ mavenRepo "http://repo.grails.org/grails/libs-releases-local"
+ //mavenRepo "http://snapshots.repository.codehaus.org"
+ //mavenRepo "http://repository.codehaus.org"
+ //mavenRepo "http://download.java.net/maven/2/"
+ //mavenRepo "http://repository.jboss.com/maven2/"
+ }
+ dependencies {
+ // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes eg.
+
+ compile('org.yaml:snakeyaml:1.8')
+ compile("org.grails:grails-gdoc-engine:1.0.1") {
+ excludes "jcl-over-slf4j"
+ }
+ }
+ plugins {
+ build ':release:1.0.0.RC3', {
+ export = false
+ }
+ }
+}
24 grails-app/conf/Config.groovy
@@ -0,0 +1,24 @@
+// configuration for plugin testing - will not be included in the plugin zip
+
+log4j = {
+ // Example of changing the log pattern for the default console
+ // appender:
+ //
+ //appenders {
+ // console name:'stdout', layout:pattern(conversionPattern: '%c{2} %m%n')
+ //}
+
+ error 'org.codehaus.groovy.grails.web.servlet', // controllers
+ 'org.codehaus.groovy.grails.web.pages', // GSP
+ 'org.codehaus.groovy.grails.web.sitemesh', // layouts
+ 'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping
+ 'org.codehaus.groovy.grails.web.mapping', // URL mapping
+ 'org.codehaus.groovy.grails.commons', // core / classloading
+ 'org.codehaus.groovy.grails.plugins', // plugins
+ 'org.codehaus.groovy.grails.orm.hibernate', // hibernate integration
+ 'org.springframework',
+ 'org.hibernate',
+ 'net.sf.ehcache.hibernate'
+
+ warn 'org.mortbay.log'
+}
32 grails-app/conf/DataSource.groovy
@@ -0,0 +1,32 @@
+dataSource {
+ pooled = true
+ driverClassName = "org.hsqldb.jdbcDriver"
+ username = "sa"
+ password = ""
+}
+hibernate {
+ cache.use_second_level_cache = true
+ cache.use_query_cache = true
+ cache.provider_class = 'net.sf.ehcache.hibernate.EhCacheProvider'
+}
+// environment specific settings
+environments {
+ development {
+ dataSource {
+ dbCreate = "create-drop" // one of 'create', 'create-drop','update'
+ url = "jdbc:hsqldb:mem:devDB"
+ }
+ }
+ test {
+ dataSource {
+ dbCreate = "update"
+ url = "jdbc:hsqldb:mem:testDb"
+ }
+ }
+ production {
+ dataSource {
+ dbCreate = "update"
+ url = "jdbc:hsqldb:file:prodDb;shutdown=true"
+ }
+ }
+}
13 grails-app/conf/UrlMappings.groovy
@@ -0,0 +1,13 @@
+class UrlMappings {
+
+ static mappings = {
+ "/$controller/$action?/$id?"{
+ constraints {
+ // apply constraints here
+ }
+ }
+
+ "/"(view:"/index")
+ "500"(view:'/error')
+ }
+}
54 grails-app/views/error.gsp
@@ -0,0 +1,54 @@
+<html>
+ <head>
+ <title>Grails Runtime Exception</title>
+ <style type="text/css">
+ .message {
+ border: 1px solid black;
+ padding: 5px;
+ background-color:#E9E9E9;
+ }
+ .stack {
+ border: 1px solid black;
+ padding: 5px;
+ overflow:auto;
+ height: 300px;
+ }
+ .snippet {
+ padding: 5px;
+ background-color:white;
+ border:1px solid black;
+ margin:3px;
+ font-family:courier;
+ }
+ </style>
+ </head>
+
+ <body>
+ <h1>Grails Runtime Exception</h1>
+ <h2>Error Details</h2>
+
+ <div class="message">
+ <strong>Error ${request.'javax.servlet.error.status_code'}:</strong> ${request.'javax.servlet.error.message'.encodeAsHTML()}<br/>
+ <strong>Servlet:</strong> ${request.'javax.servlet.error.servlet_name'}<br/>
+ <strong>URI:</strong> ${request.'javax.servlet.error.request_uri'}<br/>
+ <g:if test="${exception}">
+ <strong>Exception Message:</strong> ${exception.message?.encodeAsHTML()} <br />
+ <strong>Caused by:</strong> ${exception.cause?.message?.encodeAsHTML()} <br />
+ <strong>Class:</strong> ${exception.className} <br />
+ <strong>At Line:</strong> [${exception.lineNumber}] <br />
+ <strong>Code Snippet:</strong><br />
+ <div class="snippet">
+ <g:each var="cs" in="${exception.codeSnippet}">
+ ${cs?.encodeAsHTML()}<br />
+ </g:each>
+ </div>
+ </g:if>
+ </div>
+ <g:if test="${exception}">
+ <h2>Stack Trace</h2>
+ <div class="stack">
+ <pre><g:each in="${exception.stackTraceLines}">${it.encodeAsHTML()}<br/></g:each></pre>
+ </div>
+ </g:if>
+ </body>
+</html>
30 scripts/MigrateDoc.groovy
@@ -0,0 +1,30 @@
+/*
+* Copyright 2004-2005 the original author or authors.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* 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.
+*/
+
+/**
+ * @author Peter Ledbrook
+ * @since 2.0
+ */
+
+includeTargets << new File("$newDocPluginDir/scripts/_NewDocs.groovy")
+
+USAGE = """
+ migrate-docs
+"""
+
+target(default: "Migrates an old-style user guide to the YAML TOC based one.") {
+ depends parseArguments, migrateDocs
+}
28 scripts/NewDoc.groovy
@@ -0,0 +1,28 @@
+/*
+* Copyright 2004-2005 the original author or authors.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* 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.
+*/
+
+/**
+ * Copied from Doc.groovy
+ * @author Graeme Rocher, Brian Saville
+ * @since 1.0
+ *
+ * Created: Sep 20, 2007
+ * Modified: Sep 1, 2011
+ */
+
+includeTargets << new File("$newDocPluginDir/scripts/_NewDocs.groovy")
+
+setDefaultTarget("newDocs")
10 scripts/_Install.groovy
@@ -0,0 +1,10 @@
+//
+// This script is executed by Grails after plugin was installed to project.
+// This script is a Gant script so you can use all special variables provided
+// by Gant (such as 'baseDir' which points on project base dir). You can
+// use 'ant' to access a global instance of AntBuilder
+//
+// For example you can create directory under project tree:
+//
+// ant.mkdir(dir:"${basedir}/grails-app/jobs")
+//
410 scripts/_NewDocs.groovy
@@ -0,0 +1,410 @@
+/*
+ * Copyright 2004-2005 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.
+ */
+
+import org.apache.tools.ant.types.Path
+import org.apache.tools.ant.Project
+import org.codehaus.groovy.grails.documentation.DocumentationContext
+import org.codehaus.groovy.grails.documentation.DocumentedMethod
+import org.codehaus.groovy.grails.resolve.IvyDependencyManager
+
+import grails.util.GrailsNameUtils
+
+/**
+ * Copied from 2.0 _GrailsDocs.groovy
+ * @author Graeme Rocher, Brian Saville
+ * @since 1.0
+ *
+ * Created: Sep 20, 2007
+ * Modified: Sep 1, 2011
+ */
+
+includeTargets << grailsScript("_GrailsPackage")
+
+javadocDir = "${grailsSettings.docsOutputDir}/api"
+groovydocDir = "${grailsSettings.docsOutputDir}/gapi"
+docEncoding = "UTF-8"
+docSourceLevel = "1.5"
+links = ['http://java.sun.com/j2se/1.5.0/docs/api/']
+
+docsDisabled = { argsMap.nodoc == true }
+pdfEnabled = { argsMap.pdf == true }
+
+createdManual = false
+createdPdf = false
+
+target(newDocs: "Produces documentation for a Grails project") {
+ parseArguments()
+ if (argsMap.init) {
+ ant.mkdir(dir:"${basedir}/src/docs/guide")
+ ant.mkdir(dir:"${basedir}/src/docs/ref/Items")
+ new File("${basedir}/src/docs/guide/1. Introduction.gdoc").write '''
+This is an example documentation template. The syntax format is similar to "Textile":http://textile.thresholdstate.com/.
+
+You can apply formatting such as *bold*, _italic_ and @code@. Bullets are possible too:
+
+* Bullet 1
+* Bullet 2
+
+As well as numbered lists:
+
+# Number 1
+# Number 2
+
+The documentation also handles links to [guide items|guide:1. Introduction] as well as [reference|items]
+ '''
+
+ new File("${basedir}/src/docs/ref/Items/reference.gdoc").write '''
+h1. example
+
+h2. Purpose
+
+This is an example reference item.
+
+h2. Examples
+
+You can use code snippets:
+
+{code}
+def example = new Example()
+{code}
+
+h2. Description
+
+And provide a detailed description
+ '''
+
+ event("StatusUpdate", ["Example documentation created in ${basedir}/src/docs. Use 'grails doc' to publish."])
+ }
+ else {
+ docsInternal()
+ }
+}
+
+target(docsInternal:"Actual documentation task") {
+ depends(compile, javadoc, groovydoc, refdocs, pdf, createIndex)
+}
+
+target(setupDoc:"Sets up the doc directories") {
+ ant.mkdir(dir:grailsSettings.docsOutputDir)
+ ant.mkdir(dir:groovydocDir)
+ ant.mkdir(dir:javadocDir)
+ IvyDependencyManager dependencyManager = grailsSettings.dependencyManager
+ dependencyManager.loadDependencies('docs')
+}
+
+target(groovydoc:"Produces groovydoc documentation") {
+ depends(parseArguments, setupDoc)
+
+ if (docsDisabled()) {
+ event("DocSkip", ['groovydoc'])
+ return
+ }
+
+ ant.taskdef(name:"groovydoc", classname:"org.codehaus.groovy.ant.Groovydoc")
+ event("DocStart", ['groovydoc'])
+
+ final Project project = new Project()
+ final Path sourcePath = new Path(project)
+ File file = new File("./grails-app")
+ file.eachDir{
+ sourcePath.add(new Path(project, it.getAbsolutePath()))
+ }
+ file = new File("./src/groovy")
+ file.eachDir{
+ sourcePath.add(new Path(project, it.getAbsolutePath()))
+ }
+ file = new File("./src/java")
+ file.eachDir{
+ sourcePath.add(new Path(project, it.getAbsolutePath()))
+ }
+
+ if (isPluginProject) {
+ def pluginDescriptor = grailsSettings.baseDir.listFiles().find { it.name.endsWith "GrailsPlugin.groovy" }
+ def tmpDir = new File(grailsSettings.projectWorkDir, "pluginDescForDocs")
+ tmpDir.deleteOnExit()
+
+ // Copy the plugin descriptor to a temporary directory and add that
+ // directory to groovydoc's source path. This is because adding '.'
+ // will cause all Groovy files in the project to be included as source
+ // files (including test cases) and it will also cause duplication
+ // of classes in the generated docs - see
+ //
+ // http://jira.grails.org/browse/GRAILS-6530
+ //
+ // Also, we can't add a single file to the path. Only directories
+ // seem to work. There are quite a few limitations with the GroovyDoc
+ // task currently.
+ ant.copy file: pluginDescriptor, todir: tmpDir, overwrite: true
+
+ sourcePath.add new Path(ant.project, tmpDir.absolutePath)
+ }
+
+ try {
+ ant.groovydoc(destdir:groovydocDir, sourcepath:sourcePath, use:"true",
+ windowtitle:grailsAppName,'private':"true")
+ }
+ catch(Exception e) {
+ event("StatusError", ["Error generating groovydoc: ${e.message}"])
+ }
+ event("DocEnd", ['groovydoc'])
+}
+
+target(javadoc:"Produces javadoc documentation") {
+ depends(parseArguments, setupDoc)
+
+ if (docsDisabled()) {
+ event("DocSkip", ['javadoc'])
+ return
+ }
+
+ event("DocStart", ['javadoc'])
+ File javaDir = new File("${grailsSettings.sourceDir}/java")
+ if (javaDir.listFiles().find{ !it.name.startsWith(".")}) {
+ try {
+ ant.javadoc(access:"protected",
+ destdir:javadocDir,
+ encoding:docEncoding,
+ classpathref:"grails.compile.classpath",
+ use:"yes",
+ windowtitle:grailsAppName,
+ docencoding:docEncoding,
+ charset:docEncoding,
+ source:docSourceLevel,
+ useexternalfile:"yes",
+ breakiterator:"true",
+ linksource:"yes",
+ maxmemory:"128m",
+ failonerror:false,
+ sourcepath:javaDir.absolutePath) {
+ for (i in links) {
+ link(href:i)
+ }
+ }
+ }
+ catch (Exception e) {
+ event("StatusError", ["Error generating javadoc: ${e.message}"])
+ // ignore, empty src/java directory
+ }
+ }
+ event("DocEnd", ['javadoc'])
+}
+
+target(refdocs:"Generates Grails style reference documentation") {
+ depends(parseArguments, createConfig,loadPlugins, setupDoc)
+
+ if (docsDisabled()) return
+
+ def srcDocs = new File("${basedir}/src/docs")
+
+ def context = DocumentationContext.getInstance()
+ if (context?.hasMetadata()) {
+ for (DocumentedMethod m in context.methods) {
+ if (m.artefact && m.artefact != 'Unknown') {
+ String refDir = "${srcDocs}/ref/${GrailsNameUtils.getNaturalName(m.artefact)}"
+ ant.mkdir(dir:refDir)
+ def refFile = new File("${refDir}/${m.name}.gdoc")
+ if (!refFile.exists()) {
+ event("StatusUpdate", ["Generating documentation ${refFile}"])
+ refFile.write """
+h1. ${m.name}
+
+h2. Purpose
+
+${m.text ?: ''}
+
+h2. Examples
+
+{code:java}
+foo.${m.name}(${m.arguments?.collect {GrailsNameUtils.getPropertyName(it)}.join(',')})
+{code}
+
+h2. Description
+
+${m.text ?: ''}
+
+Arguments:
+
+${m.arguments?.collect { '* @'+GrailsNameUtils.getPropertyName(it)+'@\n' }}
+"""
+ }
+ }
+ }
+ }
+
+ if (srcDocs.exists()) {
+ File refDocsDir = grailsSettings.docsOutputDir
+ def publisher = loadClass("grails.plugins.newdoc.DocPublisher").newInstance(srcDocs, refDocsDir, newDocPluginDir)
+ publisher.ant = ant
+ publisher.title = grailsAppName
+ publisher.subtitle = grailsAppName
+ publisher.version = grailsAppVersion
+ publisher.authors = ""
+ publisher.license = ""
+ publisher.copyright = ""
+ publisher.footer = ""
+ publisher.engineProperties = config?.grails?.doc
+ println ">> ${config.grails.doc}"
+ // if this is a plugin obtain additional metadata from the plugin
+ readPluginMetadataForDocs(publisher)
+ readDocProperties(publisher)
+ configureAliases()
+
+ try {
+ publisher.publish()
+
+ createdManual = true
+ event("StatusUpdate", ["Built user manual at ${refDocsDir}/index.html"])
+ }
+ catch (RuntimeException ex) {
+ if (ex.message) {
+ event("StatusError", ["Failed to build user manual: ${ex.message}"])
+ }
+ else {
+ event("StatusError", ["Failed to build user manual."])
+ }
+ exit 1
+ }
+
+ }
+}
+
+target(pdf: "Produces PDF documentation") {
+ depends(parseArguments)
+
+ File refDocsDir = grailsSettings.docsOutputDir
+ File singleHtml = new File(refDocsDir, 'guide/single.html')
+
+ if (docsDisabled() || !pdfEnabled() || !singleHtml.exists()) {
+ event("DocSkip", ['pdf'])
+ return
+ }
+
+ event("DocStart", ['pdf'])
+
+ loadClass("grails.plugins.newdoc.PdfBuilder").build(grailsSettings.docsOutputDir.canonicalPath, grailsHome.toString())
+
+ createdPdf = true
+
+ println "Built user manual PDF at ${refDocsDir}/guide/single.pdf"
+
+ event("DocEnd", ['pdf'])
+}
+
+target(createIndex: "Produces an index.html page in the root directory") {
+ if (docsDisabled()) {
+ return
+ }
+
+ new File("${grailsSettings.docsOutputDir}/all-docs.html").withWriter { writer ->
+ writer.write """\
+<html>
+
+ <head>
+ <title>$grailsAppName Documentation</title>
+ </head>
+
+ <body>
+ <a href="api/index.html">Java API docs</a><br />
+ <a href="gapi/index.html">Groovy API docs</a><br />
+"""
+
+ if (createdManual) {
+ writer.write '\t\t<a href="guide/index.html">Manual (Page per chapter)</a><br />\n'
+ writer.write '\t\t<a href="guide/single.html">Manual (Single page)</a><br />\n'
+ }
+
+ if (createdPdf) {
+ writer.write '\t\t<a href="guide/single.pdf">Manual (PDF)</a><br />\n'
+ }
+
+ writer.write """\
+ </body>
+</html>
+"""
+ }
+}
+
+target(migrateDocs: "Migrates an old-style gdoc user guide to the current approach using a YAML TOC file.") {
+ depends createConfig
+
+ def guideDir = new File(grailsSettings.baseDir, "src/docs/guide")
+ if (guideDir.exists()) {
+ def outDir = new File(guideDir.parentFile, "migratedGuide")
+ def migrator = loadClass("grails.plugins.newdoc.LegacyDocMigrator").newInstance(guideDir, outDir, config.grails.doc.alias)
+ migrator.migrate()
+
+ event("StatusUpdate", ["Migrated user guide at ${outDir.path}"])
+ }
+}
+
+def readPluginMetadataForDocs(publisher) {
+ def basePlugin = loadBasePlugin()?.instance
+ if (basePlugin) {
+ if (basePlugin.hasProperty("title")) {
+ publisher.title = basePlugin.title
+ }
+ if (basePlugin.hasProperty("description")) {
+ publisher.subtitle = basePlugin.description
+ }
+ if (basePlugin.hasProperty("version")) {
+ publisher.version = basePlugin.version
+ }
+ if (basePlugin.hasProperty("license")) {
+ publisher.license = basePlugin.license
+ }
+ if (basePlugin.hasProperty("author")) {
+ publisher.authors = basePlugin.author
+ }
+ }
+}
+
+def readDocProperties(publisher) {
+ ['copyright', 'license', 'authors', 'footer', 'images',
+ 'css', 'style', 'encoding', 'logo', 'sponsorLogo'].each { readIfSet publisher, it }
+}
+
+def configureAliases() {
+ // See http://jira.codehaus.org/browse/GRAILS-6484 for why this is soft loaded
+ def docEngineClassName = "grails.doc.DocEngine"
+ def docEngineClass = classLoader.loadClass(docEngineClassName)
+ if (!docEngineClass) {
+ throw new IllegalStateException("Failed to load $docEngineClassName to configure documentation aliases")
+ }
+ docEngineClass.ALIAS.putAll(config.grails.doc.alias)
+}
+
+private readIfSet(publisher,String prop) {
+ if (config.grails.doc."$prop") {
+ publisher[prop] = config.grails.doc."$prop"
+ }
+}
+
+private loadBasePlugin() {
+ pluginManager?.allPlugins?.find { it.basePlugin }
+}
+
+
+// Workaround for GRAILS-6453
+loadClass = { String clazz ->
+ def doLoad = { -> classLoader.loadClass(clazz) }
+ try {
+ doLoad()
+ } catch (ClassNotFoundException e) {
+ includeTargets << grailsScript("_GrailsCompile")
+ compile()
+ doLoad()
+ }
+}
5 scripts/_Uninstall.groovy
@@ -0,0 +1,5 @@
+//
+// This script is executed by Grails when the plugin is uninstalled from project.
+// Use this script if you intend to do any additional clean-up on uninstall, but
+// beware of messing up SVN directories!
+//
10 scripts/_Upgrade.groovy
@@ -0,0 +1,10 @@
+//
+// This script is executed by Grails during application upgrade ('grails upgrade'
+// command). This script is a Gant script so you can use all special variables
+// provided by Gant (such as 'baseDir' which points on project base dir). You can
+// use 'ant' to access a global instance of AntBuilder
+//
+// For example you can create directory under project tree:
+//
+// ant.mkdir(dir:"${basedir}/grails-app/jobs")
+//
375 src/groovy/grails/plugins/newdoc/DocEngine.groovy
@@ -0,0 +1,375 @@
+/* Copyright 2004-2005 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package grails.plugins.newdoc
+
+import grails.doc.filters.HeaderFilter
+import grails.doc.filters.LinkTestFilter
+import grails.doc.filters.ListFilter
+
+import java.util.regex.Pattern
+
+import org.radeox.api.engine.WikiRenderEngine
+import org.radeox.api.engine.context.InitialRenderContext
+import org.radeox.engine.BaseRenderEngine
+import org.radeox.filter.context.FilterContext
+import org.radeox.filter.regex.RegexFilter
+import org.radeox.filter.regex.RegexTokenFilter
+import org.radeox.macro.BaseMacro
+import org.radeox.macro.CodeMacro
+import org.radeox.macro.MacroLoader
+import org.radeox.macro.parameter.BaseMacroParameter
+import org.radeox.macro.parameter.MacroParameter
+import org.radeox.regex.MatchResult
+import org.radeox.filter.*
+import org.radeox.util.Encoder
+
+/**
+ * A Radeox Wiki engine for generating documentation using a confluence style syntax.
+ *
+ * @author Graeme Rocher
+ * @since 1.2
+ */
+class DocEngine extends BaseRenderEngine implements WikiRenderEngine {
+
+ static final CONTEXT_PATH = "contextPath"
+ static final SOURCE_FILE = "sourceFile"
+ static final BASE_DIR = "base.dir"
+ static final API_BASE_PATH = "apiBasePath"
+ static final API_CONTEXT_PATH = "apiContextPath"
+ static final RESOURCES_CONTEXT_PATH = "resourcesContextPath"
+
+ static EXTERNAL_DOCS = [:]
+ static ALIAS = [:]
+
+ private basedir
+ private macroFilter
+ private macroLoader
+
+ Properties engineProperties = new Properties()
+
+ DocEngine(InitialRenderContext context) {
+ super(context)
+ this.basedir = context.get(BASE_DIR) ?: "."
+ }
+
+ boolean exists(String name) {
+ int barIndex = name.indexOf('|')
+ if (barIndex > -1) {
+ def refItem = name[0..barIndex-1]
+ def refCategory = name[barIndex + 1..-1]
+
+ if (refCategory.startsWith("http://") || refCategory.startsWith("https://")) {
+ return true
+ }
+
+ if (refCategory.startsWith("guide:")) {
+ def alias = refCategory[6..-1]
+
+ if (ALIAS[alias]) {
+ alias = ALIAS[alias]
+ }
+ def ref = "${basedir}/guide/${alias}.gdoc"
+ def file = new File(ref)
+ if (file.exists()) {
+ return true
+ }
+
+ emitWarning(name,ref,"page")
+ }
+ else if (refCategory.startsWith("api:")) {
+ def ref = refCategory[4..-1]
+ if (EXTERNAL_DOCS.keySet().find { ref.startsWith(it) }) {
+ return true
+ }
+
+ ref = ref.replace('.' as char, '/' as char)
+ if (ref.indexOf('#') > -1) {
+ ref = ref[0..ref.indexOf("#")-1]
+ }
+
+ def apiBase = initialContext.get(API_BASE_PATH)
+ if (apiBase) {
+ def apiDocExists = [ "api", "gapi" ].any { dir -> new File("${apiBase}/${dir}/${ref}.html").exists() }
+ if (apiDocExists) return true
+ }
+
+ emitWarning(name,ref,"class")
+ }
+ else {
+ String dir = getNaturalName(refCategory)
+ def ref = "${basedir}/ref/${dir}/${refItem}.gdoc"
+ File file = new File(ref)
+ if (file.exists()) {
+ return true
+ }
+
+ emitWarning(name,ref,"page")
+ }
+ }
+
+ return false
+ }
+
+ private void emitWarning(String name, String ref, String type) {
+ println "WARNING: ${initialContext.get(SOURCE_FILE)}: Link '$name' refers to non-existent $type $ref!"
+ }
+
+ boolean showCreate() { false }
+
+ void addMacro(macro) {
+ macroLoader.add(macroFilter.macroRepository, macro)
+ }
+
+ protected void init() {
+ engineProperties.findAll { it.key.startsWith("api.")}.each {
+ EXTERNAL_DOCS[it.key[4..-1]] = it.value
+ }
+ engineProperties.findAll { it.key.startsWith("alias.")}.each {
+ ALIAS[it.key[6..-1]] = it.value
+ }
+
+ if (null == fp) {
+ fp = new FilterPipe(initialContext)
+
+ def filters = [ParamFilter,
+ MacroFilter,
+ TextileLinkFilter,
+ HeaderFilter,
+ BlockQuoteFilter,
+ ListFilter,
+ LineFilter,
+ StrikeThroughFilter,
+ NewlineFilter,
+ ParagraphFilter,
+ BoldFilter,
+ CodeFilter,
+ ItalicFilter,
+ LinkTestFilter,
+ ImageFilter,
+ MarkFilter,
+ KeyFilter,
+ TypographyFilter,
+ EscapeFilter]
+
+ for (f in filters) {
+ RegexFilter filter = f.newInstance()
+ fp.addFilter(filter)
+
+ if (filter instanceof MacroFilter) {
+ macroFilter = filter
+ macroLoader = new MacroLoader()
+
+ // Add the macros provided by Grails.
+ def repository = filter.macroRepository
+ macroLoader.add(repository, new WarningMacro())
+ macroLoader.add(repository, new NoteMacro())
+ }
+ }
+ fp.init()
+ }
+ }
+
+ void appendLink(StringBuffer buffer, String name, String view, String anchor) {
+ def contextPath = initialContext.get(CONTEXT_PATH)
+
+ if (name.startsWith("guide:")) {
+ def alias = name[6..-1]
+ if (ALIAS[alias]) {
+ alias = ALIAS[alias]
+ }
+
+ // Deal with aliases that include a '/'-separated path.
+ def i = alias.lastIndexOf('/')
+ if (i >= 0) alias = alias[(i + 1)..-1]
+
+ buffer << "<a href=\"$contextPath/guide/single.html#${alias.encodeAsUrlFragment()}\" class=\"guide\">$view</a>"
+ }
+ else if (name.startsWith("api:")) {
+ def link = name[4..-1]
+
+ def externalKey = EXTERNAL_DOCS.keySet().find { link.startsWith(it) }
+ link = link.replace('.' as char, '/' as char) + ".html"
+
+ if (externalKey) {
+ buffer << "<a href=\"${EXTERNAL_DOCS[externalKey]}/$link${anchor ? '#' + anchor : ''}\" class=\"api\">$view</a>"
+ }
+ else {
+ def apiBase = initialContext.get(API_BASE_PATH)
+ contextPath = initialContext.get(API_CONTEXT_PATH)
+
+ def apiDir = [ "api", "gapi" ].find { dir -> new File("${apiBase}/${dir}/${link}").exists() }
+ buffer << "<a href=\"$contextPath/$apiDir/$link${anchor ? '#' + anchor : ''}\" class=\"api\">$view</a>"
+ }
+ }
+ else {
+ String dir = getNaturalName(name)
+ def link = "$contextPath/ref/${dir}/${view}.html"
+ buffer << "<a href=\"$link\" class=\"$name\">$view</a>"
+ }
+ }
+
+ void appendLink(StringBuffer buffer, String name, String view) {
+ appendLink(buffer,name,view,"")
+ }
+
+ void appendCreateLink(StringBuffer buffer, String name, String view) {
+ buffer.append(name)
+ }
+
+ /**
+ * Converts a property name into its natural language equivalent eg ('firstName' becomes 'First Name')
+ * @param name The property name to convert
+ * @return The converted property name
+ */
+ static final nameCache = [:]
+
+ String getNaturalName(String name) {
+ if (nameCache[name]) {
+ return nameCache[name]
+ }
+
+ List words = []
+ int i = 0
+ char[] chars = name.toCharArray()
+ for (int j = 0; j < chars.length; j++) {
+ char c = chars[j]
+ String w
+ if (i >= words.size()) {
+ w = ""
+ words.add(i, w)
+ }
+ else {
+ w = words.get(i)
+ }
+
+ if (Character.isLowerCase(c) || Character.isDigit(c)) {
+ if (Character.isLowerCase(c) && w.length() == 0) {
+ c = Character.toUpperCase(c)
+ }
+ else if (w.length() > 1 && Character.isUpperCase(w.charAt(w.length() - 1))) {
+ w = ""
+ words.add(++i,w)
+ }
+
+ words.set(i, w + c)
+ }
+ else if (Character.isUpperCase(c)) {
+ if ((i == 0 && w.length() == 0) || Character.isUpperCase(w.charAt(w.length() - 1))) {
+ words.set(i, w + c)
+ }
+ else {
+ words.add(++i, String.valueOf(c))
+ }
+ }
+ }
+
+ nameCache[name] = words.join(' ')
+ return nameCache[name]
+ }
+}
+
+class WarningMacro extends BaseMacro {
+ String getName() {"warning"}
+ void execute(Writer writer, MacroParameter params) {
+ writer << '<blockquote class="warning">' << params.content << "</blockquote>"
+ }
+}
+
+class NoteMacro extends BaseMacro {
+ String getName() {"note"}
+ void execute(Writer writer, MacroParameter params) {
+ writer << '<blockquote class="note">' << params.content << "</blockquote>"
+ }
+}
+
+class BlockQuoteFilter extends RegexTokenFilter {
+ BlockQuoteFilter() {
+ super(/(?m)^bc.\s*?(.*?)\n\n/);
+ }
+ void handleMatch(StringBuffer buffer, MatchResult result, FilterContext context) {
+ buffer << "<pre class=\"bq\"><code>${result.group(1)}</code></pre>\n\n"
+ }
+}
+
+class ItalicFilter extends RegexTokenFilter {
+ ItalicFilter() {
+ super(/\b_([^\n]*?)_\b/);
+ }
+ void handleMatch(StringBuffer buffer, MatchResult result, FilterContext context) {
+ buffer << " <em class=\"italic\">${result.group(1)}</em> "
+ }
+}
+
+class BoldFilter extends RegexTokenFilter {
+ BoldFilter() {
+ super(/\*([^\n]*?)\*/);
+ }
+ void handleMatch(StringBuffer buffer, MatchResult result, FilterContext context) {
+ buffer << "<strong class=\"bold\">${result.group(1)}</strong>"
+ }
+}
+
+class CodeFilter extends RegexTokenFilter {
+ CodeFilter() {
+ super(/@([^\n]*?)@/);
+ }
+
+ void handleMatch(StringBuffer buffer, MatchResult result, FilterContext context) {
+ def text = result.group(1)
+ // are we inside a code block?
+ if (text.indexOf('class="code"') > -1) {
+ buffer << "@$text@"
+ }
+ else {
+ buffer << "<code>${text}</code>"
+ }
+ }
+}
+
+class ImageFilter extends RegexTokenFilter {
+ ImageFilter() {
+ super(/!([^\n<>=]*?\.(jpg|png|gif))!/);
+ }
+
+ void handleMatch(StringBuffer buffer, MatchResult result, FilterContext context) {
+ def img = result.group(1)
+ if (img.startsWith("http://") || img.startsWith("https://")) {
+ buffer << "<img border=\"0\" class=\"center\" src=\"$img\"></img>"
+ }
+ else {
+ def path = context.renderContext.get(DocEngine.RESOURCES_CONTEXT_PATH) ?: "."
+ buffer << "<img border=\"0\" class=\"center\" src=\"$path/img/$img\"></img>"
+ }
+ }
+}
+
+class TextileLinkFilter extends RegexTokenFilter {
+ TextileLinkFilter() {
+ super(/"([^"]+?)":(\S+?)(\s)/);
+ }
+
+ void handleMatch(StringBuffer buffer, MatchResult result, FilterContext context) {
+ def text = result.group(1)
+ def link = result.group(2)
+ def space = result.group(3)
+
+ if (link.startsWith("http://") || link.startsWith("https://")) {
+ buffer << "<a href=\"$link\" target=\"blank\">$text</a>$space"
+ }
+ else {
+ buffer << "<a href=\"$link\">$text</a>$space"
+ }
+ }
+}
616 src/groovy/grails/plugins/newdoc/DocPublisher.groovy
@@ -0,0 +1,616 @@
+/* Copyright 2004-2005 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package grails.plugins.newdoc
+
+import grails.plugins.newdoc.internal.*
+import groovy.io.FileType
+import groovy.text.Template
+
+import org.apache.commons.logging.LogFactory
+import org.radeox.engine.context.BaseInitialRenderContext
+import org.yaml.snakeyaml.Yaml
+
+/**
+ * Coordinated the DocEngine the produce documentation based on the gdoc format.
+ *
+ * @see DocEngine
+ *
+ * @author Graeme Rocher
+ * @since 1.2
+ */
+class DocPublisher {
+ static final String TOC_FILENAME = "toc.yml"
+ static final LOG = LogFactory.getLog(this)
+
+ /** The source directory of the documentation */
+ File src
+ /** The target directory to publish to */
+ File target
+ /** The temporary work directory */
+ File workDir
+ /** Directory containing the project's API documentation. */
+ File apiDir
+ /** The directory containing any images to use (will override defaults) **/
+ File images
+ /** The directory containing any CSS to use (will override defaults) **/
+ File css
+ /** The directory containing any Javascript to use (will override defaults) **/
+ File js
+ /** The directory cotnaining any templates to use (will override defaults) **/
+ File style
+ /** The AntBuilder instance to use */
+ AntBuilder ant
+ /** The language we're generating for (gets its own sub-directory). Defaults to '' */
+ String language = ""
+ /** The encoding to use (default is UTF-8) */
+ String encoding = "UTF-8"
+ /** The title of the documentation */
+ String title
+ /** The subtitle of the documentation */
+ String subtitle = ""
+ /** The version of the documentation */
+ String version
+ /** The authors of the documentation */
+ String authors = ""
+ /** The documentation license */
+ String license = ""
+ /** The copyright message */
+ String copyright = ""
+ /** The footer to include */
+ String footer = ""
+ /** HTML markup that renders the left logo */
+ String logo
+ /** HTML markup that renders the right logo */
+ String sponsorLogo
+
+ /** Properties used to configure the DocEngine */
+ Properties engineProperties
+
+ private File baseDir
+ private output
+ private context
+ private engine
+ private customMacros = []
+
+ DocPublisher() {
+ this(null, null)
+ }
+
+ DocPublisher(File src, File target, File baseDir=null, out = [error:System.err.&println,warn:System.err.&println,info:System.out.&println,debug:System.out.&println]) {
+ this.src = src
+ this.target = target
+ this.output = out
+ this.baseDir = baseDir
+
+ try {
+ engineProperties.load(getClass().classLoader.getResourceAsStream("grails/doc/doc.properties"))
+ }
+ catch (e) {
+ // ignore
+ }
+ }
+
+ /** Returns the engine properties. */
+ Properties getEngineProperties() { engineProperties }
+
+ /** Sets the engine properties. Allows clients to override the defaults. */
+ void setEngineProperties(Properties p) {
+ engineProperties = p
+ }
+
+ /**
+ * Registers a custom Radeox macro. If the macro has an 'initialContext'
+ * property, it is set to the render context before first use.
+ */
+ void registerMacro(macro) {
+ customMacros << macro
+ }
+
+ void publish() {
+ // Adds encodeAsUrlPath(), encodeAsUrlFragment() and encodeAsHtml()
+ // methods to String.
+ use(StringEscapeCategory) {
+ catPublish()
+ }
+ }
+
+ private void catPublish() {
+ initialize()
+ if (!src?.exists()) {
+ return
+ }
+
+ // unpack documentation resources
+ String docResources = "${workDir}/doc-resources"
+ ant.mkdir(dir: docResources)
+ //unpack(dest: docResources, src: "grails-doc-files.jar")
+ ant.copy(todir: docResources) {
+ fileset(dir:"${baseDir}/src/template")
+ }
+
+ def refDocsDir = calculateLanguageDir(target?.absolutePath ?: "./docs")
+ def refGuideDir = new File(refDocsDir, "guide")
+ def refPagesDir = "$refGuideDir/pages"
+
+ ant.mkdir(dir: refDocsDir)
+ ant.mkdir(dir: refGuideDir)
+ ant.mkdir(dir: refPagesDir)
+ ant.mkdir(dir: "$refDocsDir/ref")
+
+ String imgsDir = new File(refDocsDir, calculatePathToResources("img")).path
+ ant.mkdir(dir: imgsDir)
+ String cssDir = new File(refDocsDir, calculatePathToResources("css")).path
+ ant.mkdir(dir: cssDir)
+ String jsDir = new File(refDocsDir, calculatePathToResources("js")).path
+ ant.mkdir(dir: jsDir)
+ ant.mkdir(dir: "${refDocsDir}/ref")
+
+ ant.copy(todir: imgsDir) {
+ fileset(dir: "${docResources}/img")
+ }
+
+ if (images && images.exists()) {
+ ant.copy(todir: imgsDir, overwrite: true, failonerror:false) {
+ fileset(dir: images)
+ }
+ }
+ ant.copy(todir: cssDir) {
+ fileset(dir: "${docResources}/css")
+ }
+ if (css && css.exists()) {
+ ant.copy(todir: cssDir, overwrite: true, failonerror:false) {
+ fileset(dir: css)
+ }
+ }
+ if (!new File("${docResources}/js")?.exists())
+ ant.mkdir(dir: "${docResources}/js")
+ ant.copy(todir: jsDir) {
+ fileset(dir: "${docResources}/js")
+ }
+ if (js && js.exists()) {
+ ant.copy(todir: jsDir, overwrite: true, failonerror:false) {
+ fileset(dir: js)
+ }
+ }
+ if (style && style.exists()) {
+ ant.copy(todir: "${docResources}/style", overwrite: true, failonerror:false) {
+ fileset(dir: style)
+ }
+ }
+
+ // Build the table of contents as a tree of nodes. We currently support
+ // two strategies for this:
+ //
+ // 1. From a toc.yml file
+ // 2. From the gdoc filenames
+ //
+ // The first strategy is used if the TOC file exists, otherwise we call
+ // back to the old way of doing it, which means putting the section
+ // numbers in the gdoc filenames.
+ def guideSrcDir = new File(src, "guide")
+ def yamlTocFile = new File(guideSrcDir, TOC_FILENAME)
+ def guide
+ if (yamlTocFile.exists()) {
+ guide = new YamlTocStrategy(new FileResourceChecker(guideSrcDir)).generateToc(yamlTocFile)
+
+ // A set of all gdoc files.
+ def files = []
+ guideSrcDir.traverse(type: FileType.FILES, nameFilter: ~/^.+\.gdoc$/) {
+ files << (it.absolutePath - guideSrcDir.absolutePath)[1..-1]
+ }
+
+ if (!verifyToc(guideSrcDir, files, guide)) {
+ throw new RuntimeException("Encountered errors while building table of contents. Aborting.")
+ }
+
+ for (ch in guide.children) {
+ overrideAliasesFromToc(ch)
+ }
+ }
+ else {
+ def files = guideSrcDir.listFiles()?.findAll { it.name.endsWith(".gdoc") } ?: []
+ guide = new LegacyTocStrategy().generateToc(files)
+ }
+
+ // When migrating from the old style docs to the new style, existing
+ // external links that use URL fragment identifiers will break. To
+ // mitigate against this problem, the user can provide a list of mappings
+ // from the new fragment identifiers to the old ones. The docs will then
+ // include both.
+ def legacyLinksFile = new File(guideSrcDir, "links.yml")
+ def legacyLinks = [:]
+ if (legacyLinksFile.exists()) {
+ legacyLinksFile.withInputStream { input ->
+ legacyLinks = new Yaml().load(input)
+ }
+ }
+
+ def templateEngine = new groovy.text.SimpleTemplateEngine()
+
+ // Reference menu items.
+ def sectionFilter = { it.directory && !it.name.startsWith('.') } as FileFilter
+ def files = new File(src, "ref").listFiles(sectionFilter)?.toList()?.sort() ?: []
+ def refCategories = files.collect { f ->
+ new Expando(
+ name: f.name,
+ usage: new File("${src}/ref/${f.name}.gdoc"),
+ sections: f.listFiles().findAll { it.name.endsWith(".gdoc") }.sort())
+ }
+
+ def fullToc = new StringBuilder()
+
+ def pathToRoot = ".."
+ def vars = [
+ encoding: encoding,
+ title: title,
+ subtitle: subtitle,
+ footer: footer, // TODO - add a way to specify footer
+ authors: authors,
+ version: version,
+ refMenu: refCategories,
+ toc: guide,
+ copyright: copyright,
+ logo: injectPath(logo, pathToRoot),
+ sponsorLogo: injectPath(sponsorLogo, pathToRoot),
+ single: false,
+ path: pathToRoot,
+ resourcesPath: calculatePathToResources(pathToRoot),
+ prev: null,
+ next: null,
+ legacyLinks: legacyLinks
+ ]
+
+ // Build the user guide sections first.
+ def template = templateEngine.createTemplate(new File("${docResources}/style/guideItem.html").newReader(encoding))
+ def sectionTemplate = templateEngine.createTemplate(new File("${docResources}/style/section.html").newReader(encoding))
+ def fullContents = new StringBuilder()
+
+ def chapterVars
+ def chapters = guide.children
+ chapters.eachWithIndex{ chapter, i ->
+ chapterVars = [*:vars, chapterNumber: i + 1]
+ if (i != 0) {
+ chapterVars['prev'] = chapters[i - 1]
+ }
+ if (i != (chapters.size() - 1)) {
+ chapterVars['next'] = chapters[i + 1]
+ }
+ chapterVars.sectionNumber = (i + 1).toString()
+ writeChapter(chapter, template, sectionTemplate, guideSrcDir, refGuideDir.path, fullContents, chapterVars)
+ }
+
+ files = new File("${src}/ref").listFiles()?.toList()?.sort() ?: []
+ def reference = [:]
+ template = templateEngine.createTemplate(new File("${docResources}/style/referenceItem.html").newReader(encoding))
+
+ pathToRoot = "../.."
+ vars.logo = injectPath(logo, pathToRoot)
+ vars.sponsorLogo = injectPath(sponsorLogo, pathToRoot)
+ vars.path = pathToRoot
+ vars.resourcesPath = calculatePathToResources(pathToRoot)
+
+ for (f in files) {
+ if (f.directory && !f.name.startsWith(".")) {
+ def section = f.name
+ vars.section = section
+
+ new File("${refDocsDir}/ref/${section}").mkdirs()
+ def textiles = f.listFiles().findAll { it.name.endsWith(".gdoc")}.sort()
+ def usageFile = new File("${src}/ref/${section}.gdoc")
+ if (usageFile.exists()) {
+ def data = usageFile.text
+ context.set(DocEngine.SOURCE_FILE, usageFile)
+ context.set(DocEngine.CONTEXT_PATH, pathToRoot)
+ vars.content = engine.render(data, context)
+
+ new File("${refDocsDir}/ref/${section}/Usage.html").withWriter(encoding) {out ->
+ template.make(vars).writeTo(out)
+ }
+ }
+ for (txt in textiles) {
+ def name = txt.name[0..-6]
+ def data = txt.text
+ context.set(DocEngine.SOURCE_FILE, txt.name)
+ context.set(DocEngine.CONTEXT_PATH, pathToRoot)
+ vars.content = engine.render(data, context)
+
+ new File("${refDocsDir}/ref/${section}/${name}.html").withWriter(encoding) {out ->
+ template.make(vars).writeTo(out)
+ }
+ }
+ }
+ }
+
+ vars.remove("section")
+ vars.content = fullContents.toString()
+ vars.single = true
+
+ pathToRoot = ".."
+ vars.logo = injectPath(logo, pathToRoot)
+ vars.sponsorLogo = injectPath(sponsorLogo, pathToRoot)
+ vars.path = pathToRoot
+ vars.resourcesPath = calculatePathToResources(pathToRoot)
+
+ template = templateEngine.createTemplate(new File("${docResources}/style/layout.html").newReader(encoding))
+ new File("${refGuideDir}/single.html").withWriter(encoding) {out ->
+ template.make(vars).writeTo(out)
+ }
+
+ vars.content = ""
+ vars.single = false
+ new File("${refGuideDir}/index.html").withWriter(encoding) {out ->
+ template.make(vars).writeTo(out)
+ }
+
+ pathToRoot = "."
+ vars.logo = injectPath(logo, pathToRoot)
+ vars.sponsorLogo = injectPath(sponsorLogo, pathToRoot)
+ vars.path = pathToRoot
+ vars.resourcesPath = calculatePathToResources(pathToRoot)
+
+ new File("${refDocsDir}/index.html").withWriter(encoding) {out ->
+ template.make(vars).writeTo(out)
+ }
+
+ ant.echo "Built user manual at ${refDocsDir}/index.html"
+ }
+
+ void writeChapter(
+ section,
+ Template layoutTemplate,
+ Template sectionTemplate,
+ File guideSrcDir,
+ String targetDir,
+ fullContents,
+ vars) {
+ fullContents << writePage(section, layoutTemplate, sectionTemplate, guideSrcDir, targetDir, "", "..", 0, vars)
+ }
+
+ String writePage(
+ section,
+ Template layoutTemplate,
+ Template sectionTemplate,
+ File guideSrcDir,
+ String targetDir,
+ String subDir,
+ path,
+ level,
+ vars) {
+ def sourceFile = new File(guideSrcDir, section.file)
+ context.set(DocEngine.SOURCE_FILE, sourceFile)
+ context.set(DocEngine.CONTEXT_PATH, path)
+
+ def varsCopy = [*:vars]
+ varsCopy.name = section.name
+ varsCopy.title = section.title
+ varsCopy.path = path
+ varsCopy.level = level
+ varsCopy.sectionToc = section.children
+ varsCopy.content = engine.render(sourceFile.text, context)
+
+ // First create the section content, which usually consists of a header
+ // and the translated gdoc content.
+ def sectionContent = new StringWriter()
+ sectionTemplate.make(varsCopy).writeTo(sectionContent)
+
+ // Aggregate the section content and sub-sections.
+ def accumulatedContent = new StringBuilder()
+ accumulatedContent << sectionContent.toString()
+
+ // Create the sub-section pages.
+ level++
+ final sectionNumber = varsCopy.sectionNumber
+ int subSectionNumber = 1
+ for (s in section.children) {
+ varsCopy.sectionNumber = "$sectionNumber.$subSectionNumber"
+ accumulatedContent << writePage(s, layoutTemplate, sectionTemplate, guideSrcDir, targetDir, "pages", path, level, varsCopy)
+ subSectionNumber++
+ }
+
+ // Reset the section number in the template vars.
+ varsCopy.sectionNumber = sectionNumber
+
+ // TODO PAL - I don't see why these pages are necessary, plus there seems
+ // to be no way to get embedded images to display properly (since the path
+ // passed to the Wiki rendering engine is wrong for pages written to a
+ // 'pages' subdirectory). Keeping them in case someone, somewhere depends
+ // on them.
+ //
+ // Create the HTML page for this section, which includes the content
+ // from all the sub-sections too.
+ if (subDir) {
+ if (subDir.endsWith('/')) subDir = subDir[0..-2]
+ targetDir = "$targetDir/$subDir"
+
+ varsCopy.path = "../${path}"
+ varsCopy.logo = injectPath(logo, varsCopy.path)
+ varsCopy.sponsorLogo = injectPath(sponsorLogo, varsCopy.path)
+ }
+
+ new File("${targetDir}/${section.name}.html").withWriter(encoding) { writer ->
+ varsCopy.content = accumulatedContent.toString()
+ layoutTemplate.make(varsCopy).writeTo(writer)
+ }
+
+ return varsCopy.content
+ }
+
+ protected void initialize() {
+ if (language) {
+ src = new File(src, language)
+ }
+
+ if (!workDir) {
+ workDir = new File(System.getProperty("java.io.tmpdir"))
+ }
+ if (!apiDir) {
+ apiDir = target
+ }
+ if (!ant) {
+ ant = new AntBuilder()
+ }
+ def metaProps = DocPublisher.metaClass.properties
+ def props = engineProperties
+ for (MetaProperty mp in metaProps) {
+ if (mp.type == String) {
+ def value = props[mp.name]
+ if (value) {
+ this[mp.name] = value
+ }
+ }
+ }
+
+ context = new BaseInitialRenderContext()
+ initContext(context, "..")
+
+ engine = new DocEngine(context)
+ engine.engineProperties = engineProperties
+ context.renderEngine = engine
+
+ // Add any custom macros registered with this publisher to the engine.
+ for (m in customMacros) {
+ if (m.metaClass.hasProperty(m, "initialContext")) {
+ m.initialContext = context
+ }
+ engine.addMacro(m)
+ }
+ }
+
+ /**
+ * Checks the table of contents (a tree of {@link UserGuideNode}s) for
+ * duplicate section/alias names and invalid file paths.
+ * @return <code>false</code> if any errors are detected.
+ */
+ protected verifyToc(File baseDir, gdocFiles, toc) {
+ def hasErrors = false
+ def sectionsFound = [] as Set
+ def gdocsNotInToc = gdocFiles as Set
+
+ // Defensive copy
+ if (gdocsNotInToc.is(gdocFiles)) gdocsNotInToc = new HashSet(gdocFiles)
+
+ for (ch in toc.children) {
+ hasErrors |= verifyTocInternal(baseDir, ch, sectionsFound, gdocsNotInToc, [])
+ }
+
+ if (gdocsNotInToc) {
+ for (gdoc in gdocsNotInToc) {
+ output.warn "No TOC entry found for '${gdoc}'"
+ }
+ }
+
+ return !hasErrors
+ }
+
+ private verifyTocInternal(File baseDir, section, existing, gdocFiles, pathElements) {
+ def hasErrors = false
+ def fullName = pathElements ? "${pathElements.join('/')}/${section.name}" : section.name
+
+ // Has this section name already been used?
+ if (section.name in existing) {
+ hasErrors = true
+ output.error "Duplicate section name: ${fullName}"
+ }
+
+ // Does the file path for the gdoc exist?
+ if (!section.file || !new File(baseDir, section.file).exists()) {
+ hasErrors = true
+ output.error "No file found for '${fullName}'"
+ }
+ else {
+ // Found this gdoc file in the TOC.
+ gdocFiles.remove section.file
+ }
+
+ existing << section.name
+
+ for (s in section.children) {
+ hasErrors |= verifyTocInternal(baseDir, s, existing, gdocFiles, pathElements + section.name)
+ }
+
+ return hasErrors
+ }
+
+ private String calculateLanguageDir(startPath, endPath = '') {
+ def elements = [startPath, language, endPath]
+ elements = elements.findAll { it }
+ return elements.join('/')
+ }
+
+ private String injectPath(String source, String path) {
+ if (!source) return source
+
+ def templateEngine = new groovy.text.SimpleTemplateEngine()
+ def out = new StringWriter()
+ templateEngine.createTemplate(source).make(path: calculatePathToResources(path)).writeTo(out)
+ return out.toString()
+ }
+
+ private String calculatePathToResources(String pathToRoot) {
+ return language ? '../' + pathToRoot : pathToRoot
+ }
+
+ private initContext(context, path) {
+ context.set(DocEngine.CONTEXT_PATH, path)
+ context.set(DocEngine.BASE_DIR, src.absolutePath)
+ context.set(DocEngine.API_BASE_PATH, apiDir.absolutePath)
+ context.set(DocEngine.API_CONTEXT_PATH, calculatePathToResources(path))
+ context.set(DocEngine.RESOURCES_CONTEXT_PATH, calculatePathToResources(path))
+ return context
+ }
+
+ private unpack(Map args) {
+
+ def dir = args["dest"] ?: "."
+ def src = args["src"]
+ def overwriteOption = args["overwrite"] == null ? true : args["overwrite"]
+
+ // Can't unjar a file from within a JAR, so we copy it to
+ // the destination directory first.
+ try {
+ URL url = getClass().getClassLoader().getResource(src)
+ if (url) {
+ url.withInputStream { InputStream input ->
+ new File("$dir/$src").withOutputStream { out ->
+ def buffer = new byte[1024]
+ int len
+ while ((len = input.read(buffer)) != -1) {
+ out.write(buffer, 0, len)
+ }
+ }
+ }
+ }
+ // Now unjar it, excluding the META-INF directory.
+ ant.unjar(dest: dir, src: "${dir}/${src}", overwrite: overwriteOption) {
+ patternset {
+ exclude(name: "META-INF/**")
+ }
+ }
+ }
+ finally {
+ // Don't need the JAR file any more, so remove it.
+ ant.delete(file: "${dir}/${src}", failonerror:false)
+ }
+ }
+
+ private overrideAliasesFromToc(node) {
+ engine.engineProperties.setProperty "alias.${node.name}", node.file - ".gdoc"
+
+ for (section in node.children) {
+ overrideAliasesFromToc(section)
+ }
+ }
+}
127 src/groovy/grails/plugins/newdoc/LegacyDocMigrator.groovy
@@ -0,0 +1,127 @@
+package grails.plugins.newdoc
+
+import grails.plugins.newdoc.internal.LegacyTocStrategy
+import grails.plugins.newdoc.internal.StringEscapeCategory
+
+/**
+ * <p>Migrates gdoc-based user guides from the old style, in which the section
+ * numbers are included in the filenames, and the new style which uses a
+ * YAML-based TOC file to organise the sections. It doesn't do a perfect job
+ * but it does a lot of the hard work and you can fine tune the generated gdocs
+ * afterwards.</p>
+ * <p>The migration will not only rename and restructure the gdoc files, but it
+ * will also generate a toc.yml file that reproduces the existing guide structure.
+ * Additional files include:</p>
+ * <ul>
+ * <li><tt>links.yml</tt> - a YAML file mapping new section names to the old ones.
+ * Ensures that URL fragments in old external links continue to work.</li>
+ * <li><tt>rewriteRules.txt</tt> - a simple text file that maps the old chapter
+ * HTML filenames to their new ones. Useful for creating Apache URL rewrite rules.
+ * </li>
+ * </ul>
+ * <p>The names of the new sections are based on the old section names, so they
+ * may not be ideal. Also, the new style requires that every section has a unique
+ * name, although the documentation publishing will pick up and warn of duplicates.
+ * </p>
+ */
+class LegacyDocMigrator {
+ private static final String EOL = System.getProperty("line.separator")
+
+ private guideSrcDir
+ private aliasMap
+ private outDir
+
+ LegacyDocMigrator(File guideSrcDir, aliasMap) {
+ this(guideSrcDir, new File(guideSrcDir.parentFile, "migratedGuide"), aliasMap)
+ }
+
+ LegacyDocMigrator(File guideSrcDir, File outDir, aliasMap) {
+ this.guideSrcDir = guideSrcDir
+ this.outDir = outDir
+ this.aliasMap = aliasMap.collectEntries { key, value -> [value, key] }
+ }
+
+ def migrate() {
+ outDir.mkdirs()
+
+ def files = guideSrcDir.listFiles()?.findAll { it.name.endsWith(".gdoc") } ?: []
+ def guide = new LegacyTocStrategy().generateToc(files)
+
+ def legacyLinkMap = new File(outDir, "links.yml")
+ legacyLinkMap.withWriter { w ->
+ guide.children.each(this.&migrateSection.rcurry([], w))
+ }
+
+ def tocFile = new File(outDir, "toc.yml")
+ tocFile.withWriter { w ->
+ guide.children.each(this.&writeSectionToToc.rcurry(w, 0))
+ }
+
+ // A mapping that can be utilised by Apache HTTPD URL rewriting.
+ def rewriteRulesFile = new File(outDir, "rewriteRules.txt")
+ rewriteRulesFile.withPrintWriter { w ->
+ for (section in guide.children) {
+ w.println "${StringEscapeCategory.encodeAsUrlPath(section.name)}.html -> ${StringEscapeCategory.encodeAsUrlPath(alias(section))}.html"
+ }
+ }
+ }
+
+ private migrateSection(section, pathElements, writer) {
+ def alias = alias(section)
+ def newDir = new File(outDir, pathElements.join('/'))
+ def newFile = new File(newDir, "${alias}.gdoc")
+ def oldFile = new File(guideSrcDir, section.file)
+ newFile.bytes = oldFile.bytes
+
+ writer << alias << ': ' << section.name << EOL
+
+ if (section.children) {
+ newDir = new File(newDir, alias)
+ newDir.mkdirs()
+ for (s in section.children) {
+ migrateSection(s, pathElements + alias, writer)
+ }
+ }
+ }
+
+ private writeSectionToToc(section, writer, indent) {
+ writer << ' ' * indent << alias(section) << ": "
+ if (section.children) {
+ indent++
+ writer << EOL << ' ' * indent << "title: " << section.title << EOL
+
+ for (s in section.children) {
+ writeSectionToToc s, writer, indent
+ }
+ }
+ else {
+ writer << section.title << EOL
+ }
+ }
+
+ private alias(section) {
+ def alias = aliasMap[section.name]
+ if (!alias) {
+ alias = naturalNameToCamelCase(section.title)
+ aliasMap[section.name] = alias
+ }
+ return alias
+ }
+
+ private naturalNameToCamelCase(name) {
+ if (!name) return name
+
+ // Start by breaking the natural name into words.
+ def parts = name.split(/\s+/)
+
+ // Lower case the first letter according to Java Beans rules.
+ parts[0] = java.beans.Introspector.decapitalize(parts[0])
+
+ // The rest of the name parts should have their first letter capitalised.
+ for (int i = 1; i < parts.size(); i++) {
+ parts[i] = parts[i].capitalize()
+ }
+
+ return parts.join('')
+ }
+}
17 src/groovy/grails/plugins/newdoc/internal/FileResourceChecker.groovy
@@ -0,0 +1,17 @@
+package grails.plugins.newdoc.internal
+
+/**
+ * Simple class that checks whether a path relative to a base directory exists
+ * or not. Each instance of the class can have its own base directory.
+ */
+class FileResourceChecker {
+ private final File baseDir
+
+ FileResourceChecker(File baseDir) {
+ this.baseDir = baseDir
+ }
+
+ boolean exists(path) {
+ return new File(baseDir, path).exists()
+ }
+}
57 src/groovy/grails/plugins/newdoc/internal/LegacyTocStrategy.groovy
@@ -0,0 +1,57 @@
+package grails.plugins.newdoc.internal
+
+class LegacyTocStrategy {
+ def generateToc(files) {
+ // Compares two gdoc filenames based on the section number in the
+ // form x.y.z...
+ def sectionNumberComparator = [
+ compare: {o1, o2 ->
+ def idx1 = o1.name[0..o1.name.indexOf(' ') - 1]
+ def idx2 = o2.name[0..o2.name.indexOf(' ') - 1]
+ def nums1 = idx1.split(/\./).findAll { it.trim() != ''}*.toInteger()
+ def nums2 = idx2.split(/\./).findAll { it.trim() != ''}*.toInteger()
+ // pad out with zeros to ensure accurate comparison
+ while (nums1.size() < nums2.size()) {
+ nums1 << 0
+ }
+ while (nums2.size() < nums1.size()) {
+ nums2 << 0
+ }
+ def result = 0
+ for (i in 0..<nums1.size()) {
+ result = nums1[i].compareTo(nums2[i])
+ if (result != 0) break
+ }
+ result
+ },
+ equals: { false }] as Comparator
+
+ // Search the given directory for all gdoc files and order them based
+ // on their section numbers.
+ files = files?.sort(sectionNumberComparator) ?: []
+
+ // A tree of book sections, where 'book' is a list of the top-level
+ // sections and each of those has a list of sub-sections and so on.
+ def book = new UserGuideNode()
+ for (f in files) {
+ // Chapter is filename - '.gdoc' suffix.
+ def chapter = f.name[0..-6]
+ def section = new UserGuideNode(name: chapter, title: chapter, file: f.name)
+
+ def level = 0
+ def matcher = (chapter =~ /^(\S+?)\.?\s(.+)/) // drops last '.' of "xx.yy. "
+ if (matcher) {
+ level = matcher.group(1).split(/\./).size() - 1
+ section.title = matcher.group(2)
+ }
+
+ // This cryptic line finds the appropriate parent section list based
+ // on the current section's level. If the level is 0, then it's 'book'.
+ def parent = (0..<level).inject(book) { node, n -> node.children[-1] }
+ section.parent = parent
+ parent.children << section
+ }
+
+ return book
+ }
+}
32 src/groovy/grails/plugins/newdoc/internal/StringEscapeCategory.groovy
@@ -0,0 +1,32 @@
+package grails.plugins.newdoc.internal;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.apache.commons.lang.StringEscapeUtils;
+
+public class StringEscapeCategory {
+ public static String encodeAsUrlPath(String str) {
+ try {
+ String uri = new URI("http", "localhost", '/' + str, "").toASCIIString();
+ return uri.substring(17, uri.length() - 1);
+ }
+ catch (URISyntaxException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public static String encodeAsUrlFragment(String str) {
+ try {
+ String uri = new URI("http", "localhost", "/", str).toASCIIString();
+ return uri.substring(18, uri.length());
+ }
+ catch (URISyntaxException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public static String encodeAsHtml(String str) {
+ return StringEscapeUtils.escapeHtml(str);
+ }
+}
16 src/groovy/grails/plugins/newdoc/internal/UserGuideNode.groovy
@@ -0,0 +1,16 @@
+package grails.plugins.newdoc.internal
+
+class UserGuideNode {
+ UserGuideNode parent
+ List children = []
+
+ String name
+ String title
+ String file
+
+ @Override
+ // Implement groovy.transform.ToString as simply as possible
+ public String toString() {
+ return "UserGuideNode(${name}, ${title}, ${file})"
+ }
+}
102 src/groovy/grails/plugins/newdoc/internal/YamlTocStrategy.groovy
@@ -0,0 +1,102 @@
+package grails.plugins.newdoc.internal
+
+import org.yaml.snakeyaml.Yaml
+
+/**
+ * Class representing a Grails user guide table of contents defined in YAML.
+ */
+class YamlTocStrategy {
+ private final parser
+ private resourceChecker
+
+ YamlTocStrategy(resourceChecker) {
+ this.parser = new Yaml()
+ this.resourceChecker = resourceChecker
+ }
+
+ UserGuideNode generateToc(yaml) {
+ return load(yaml)
+ }
+
+ protected UserGuideNode load(String yaml) {
+ return process(parser.load(yaml))
+ }
+
+ protected UserGuideNode load(File file) {
+ file.withInputStream { input ->
+ return process(parser.load(input))
+ }
+ }
+
+ protected UserGuideNode load(InputStream input) {
+ return process(parser.load(input))
+ }
+
+ protected UserGuideNode load(Reader input) {
+ return process(parser.load(input))
+ }
+
+ private process(yamlDoc) {
+ def rootNode = new UserGuideNode()
+ processSection(yamlDoc, rootNode)
+ return rootNode
+ }
+
+ private processSection(Map sections, UserGuideNode node) {
+ if (sections.title) {
+ node.title = sections.title
+ sections = sections.clone()
+ sections.remove("title")
+ }
+
+ for (s in sections) {
+ def child = new UserGuideNode(parent: node, name: s.key, file: determineFilePath(s.key, node))
+ node.children << child
+ processSection(s.value, child)
+ }
+ }
+
+ private processSection(String title, UserGuideNode node) {
+ node.title = title
+ }
+
+ private determineFilePath(basename, parent) {
+ // Traverse the parent nodes and build a list of the node names.
+ // The names are stored in reverse order, so the immediate parent
+ // node is first in the list and the root (named) node is last.
+ // The real root node, doesn't have a name and isn't included.
+ def pathElements = []
+ def node = parent
+ while (node.name) {
+ pathElements << node.name
+ node = node.parent
+ }
+
+ // First check whether the gdoc file exists in the root directory.
+ def filePath = "${basename}.gdoc"
+ if (resourceChecker.exists(filePath)) {
+ return filePath
+ }
+ else if (pathElements) {
+ // Now check whether its in any sub-directories named after the
+ // ancestor nodes. First we look in a directory with the same
+ // name as the root (named) node, then in a sub-directory of
+ // that folder named after the next parent, and so on. So if
+ // pathElements is ["changelog", "whatsNew", "intro"], then we
+ // check:
+ //
+ // intro/$basename.gdoc
+ // intro/whatsNew/$basename.gdoc
+ // intro/whatsNew/changelog/$basename.gdoc
+ //
+ for (i in 1..pathElements.size()) {
+ filePath = "${pathElements[-1..-i].join(File.separator)}${File.separator}${basename}.gdoc"
+ if (resourceChecker.exists(filePath)) {
+ return filePath
+ }
+ }