diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..34ddc6b --- /dev/null +++ b/.classpath @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c047dec --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.class +target +*.zip +plugin.xml +*.log +*plugin.xml +*.pom +*.md5 +*.sha1 +.classpath +.link_to_grails_plugins \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 0000000..cc0b69f --- /dev/null +++ b/.project @@ -0,0 +1,32 @@ + + + NewDoc + + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.jdt.core.javabuilder + + + + + + com.springsource.sts.grails.core.nature + org.eclipse.jdt.groovy.core.groovyNature + org.eclipse.jdt.core.javanature + org.eclipse.wst.common.project.facet.core.nature + + + + .link_to_grails_plugins + 2 + GRAILS_ROOT/1.3.7/projects/NewDoc/plugins + + + diff --git a/.settings/com.springsource.sts.grails.core.prefs b/.settings/com.springsource.sts.grails.core.prefs new file mode 100644 index 0000000..4d5772f --- /dev/null +++ b/.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 diff --git a/.settings/org.codehaus.groovy.eclipse.preferences.prefs b/.settings/org.codehaus.groovy.eclipse.preferences.prefs new file mode 100644 index 0000000..bf339c7 --- /dev/null +++ b/.settings/org.codehaus.groovy.eclipse.preferences.prefs @@ -0,0 +1,3 @@ +#Created by grails +eclipse.preferences.version=1 +groovy.dont.generate.class.files=true diff --git a/.settings/org.eclipse.wst.common.project.facet.core.xml b/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100644 index 0000000..13ac95c --- /dev/null +++ b/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,4 @@ + + + + diff --git a/NewDocGrailsPlugin.groovy b/NewDocGrailsPlugin.groovy new file mode 100644 index 0000000..f27c19f --- /dev/null +++ b/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'. + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..214360d --- /dev/null +++ b/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 \ No newline at end of file diff --git a/application.properties b/application.properties new file mode 100644 index 0000000..4e20931 --- /dev/null +++ b/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 diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy new file mode 100644 index 0000000..2a6c140 --- /dev/null +++ b/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 + } + } +} diff --git a/grails-app/conf/Config.groovy b/grails-app/conf/Config.groovy new file mode 100644 index 0000000..2e57c6d --- /dev/null +++ b/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' +} diff --git a/grails-app/conf/DataSource.groovy b/grails-app/conf/DataSource.groovy new file mode 100644 index 0000000..91143e7 --- /dev/null +++ b/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" + } + } +} diff --git a/grails-app/conf/UrlMappings.groovy b/grails-app/conf/UrlMappings.groovy new file mode 100644 index 0000000..8c597d6 --- /dev/null +++ b/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') + } +} diff --git a/grails-app/views/error.gsp b/grails-app/views/error.gsp new file mode 100644 index 0000000..cfc512a --- /dev/null +++ b/grails-app/views/error.gsp @@ -0,0 +1,54 @@ + + + Grails Runtime Exception + + + + +

Grails Runtime Exception

+

Error Details

+ +
+ Error ${request.'javax.servlet.error.status_code'}: ${request.'javax.servlet.error.message'.encodeAsHTML()}
+ Servlet: ${request.'javax.servlet.error.servlet_name'}
+ URI: ${request.'javax.servlet.error.request_uri'}
+ + Exception Message: ${exception.message?.encodeAsHTML()}
+ Caused by: ${exception.cause?.message?.encodeAsHTML()}
+ Class: ${exception.className}
+ At Line: [${exception.lineNumber}]
+ Code Snippet:
+
+ + ${cs?.encodeAsHTML()}
+
+
+
+
+ +

Stack Trace

+
+
${it.encodeAsHTML()}
+
+
+ + \ No newline at end of file diff --git a/scripts/MigrateDoc.groovy b/scripts/MigrateDoc.groovy new file mode 100644 index 0000000..1b8d31b --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/scripts/NewDoc.groovy b/scripts/NewDoc.groovy new file mode 100644 index 0000000..fd888b7 --- /dev/null +++ b/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") diff --git a/scripts/_Install.groovy b/scripts/_Install.groovy new file mode 100644 index 0000000..a212160 --- /dev/null +++ b/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") +// diff --git a/scripts/_NewDocs.groovy b/scripts/_NewDocs.groovy new file mode 100644 index 0000000..1d7eee4 --- /dev/null +++ b/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 """\ + + + + $grailsAppName Documentation + + + + Java API docs
+ Groovy API docs
+""" + + if (createdManual) { + writer.write '\t\tManual (Page per chapter)
\n' + writer.write '\t\tManual (Single page)
\n' + } + + if (createdPdf) { + writer.write '\t\tManual (PDF)
\n' + } + + writer.write """\ + + +""" + } +} + +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() + } +} diff --git a/scripts/_Uninstall.groovy b/scripts/_Uninstall.groovy new file mode 100644 index 0000000..7c53169 --- /dev/null +++ b/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! +// diff --git a/scripts/_Upgrade.groovy b/scripts/_Upgrade.groovy new file mode 100644 index 0000000..6a1a4c9 --- /dev/null +++ b/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") +// diff --git a/src/groovy/grails/plugins/newdoc/DocEngine.groovy b/src/groovy/grails/plugins/newdoc/DocEngine.groovy new file mode 100644 index 0000000..2b4bf6f --- /dev/null +++ b/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 << "$view" + } + 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 << "$view" + } + 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 << "$view" + } + } + else { + String dir = getNaturalName(name) + def link = "$contextPath/ref/${dir}/${view}.html" + buffer << "$view" + } + } + + 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 << '
' << params.content << "
" + } +} + +class NoteMacro extends BaseMacro { + String getName() {"note"} + void execute(Writer writer, MacroParameter params) { + writer << '
' << params.content << "
" + } +} + +class BlockQuoteFilter extends RegexTokenFilter { + BlockQuoteFilter() { + super(/(?m)^bc.\s*?(.*?)\n\n/); + } + void handleMatch(StringBuffer buffer, MatchResult result, FilterContext context) { + buffer << "
${result.group(1)}
\n\n" + } +} + +class ItalicFilter extends RegexTokenFilter { + ItalicFilter() { + super(/\b_([^\n]*?)_\b/); + } + void handleMatch(StringBuffer buffer, MatchResult result, FilterContext context) { + buffer << " ${result.group(1)} " + } +} + +class BoldFilter extends RegexTokenFilter { + BoldFilter() { + super(/\*([^\n]*?)\*/); + } + void handleMatch(StringBuffer buffer, MatchResult result, FilterContext context) { + buffer << "${result.group(1)}" + } +} + +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 << "${text}" + } + } +} + +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 << "" + } + else { + def path = context.renderContext.get(DocEngine.RESOURCES_CONTEXT_PATH) ?: "." + buffer << "" + } + } +} + +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 << "$text$space" + } + else { + buffer << "$text$space" + } + } +} \ No newline at end of file diff --git a/src/groovy/grails/plugins/newdoc/DocPublisher.groovy b/src/groovy/grails/plugins/newdoc/DocPublisher.groovy new file mode 100644 index 0000000..bd8e97d --- /dev/null +++ b/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 false 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) + } + } +} \ No newline at end of file diff --git a/src/groovy/grails/plugins/newdoc/LegacyDocMigrator.groovy b/src/groovy/grails/plugins/newdoc/LegacyDocMigrator.groovy new file mode 100644 index 0000000..bbf44a8 --- /dev/null +++ b/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 + +/** + *

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.

+ *

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:

+ * + *

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. + *

+ */ +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('') + } +} \ No newline at end of file diff --git a/src/groovy/grails/plugins/newdoc/internal/FileResourceChecker.groovy b/src/groovy/grails/plugins/newdoc/internal/FileResourceChecker.groovy new file mode 100644 index 0000000..0c5b79b --- /dev/null +++ b/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() + } +} \ No newline at end of file diff --git a/src/groovy/grails/plugins/newdoc/internal/LegacyTocStrategy.groovy b/src/groovy/grails/plugins/newdoc/internal/LegacyTocStrategy.groovy new file mode 100644 index 0000000..01fce6f --- /dev/null +++ b/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.. node.children[-1] } + section.parent = parent + parent.children << section + } + + return book + } +} \ No newline at end of file diff --git a/src/groovy/grails/plugins/newdoc/internal/StringEscapeCategory.groovy b/src/groovy/grails/plugins/newdoc/internal/StringEscapeCategory.groovy new file mode 100644 index 0000000..9ca69c9 --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/src/groovy/grails/plugins/newdoc/internal/UserGuideNode.groovy b/src/groovy/grails/plugins/newdoc/internal/UserGuideNode.groovy new file mode 100644 index 0000000..7ad89cf --- /dev/null +++ b/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})" + } +} \ No newline at end of file diff --git a/src/groovy/grails/plugins/newdoc/internal/YamlTocStrategy.groovy b/src/groovy/grails/plugins/newdoc/internal/YamlTocStrategy.groovy new file mode 100644 index 0000000..caf98b9 --- /dev/null +++ b/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 + } + } + } + + return null + } +} diff --git a/src/template/css/custom.css b/src/template/css/custom.css new file mode 100644 index 0000000..0c95685 --- /dev/null +++ b/src/template/css/custom.css @@ -0,0 +1,4 @@ +/* + Dummy stylesheet allowing for some customisation without having to + copy and modify the other stylesheets. +*/ diff --git a/src/template/css/main.css b/src/template/css/main.css new file mode 100644 index 0000000..4928d3b --- /dev/null +++ b/src/template/css/main.css @@ -0,0 +1,723 @@ +@import "custom.css"; +@import "tools.css"; +@import "skin.css"; + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Body & Html. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +body { + font-family: Arial; +} + +.body { + font-family: Arial; + text-align: justify; + font-size: 85%; + background: #F2F2F2; +} + +/* Monospace should be same size as other fonts: this fix was taken from + * + * http://meyerweb.com/eric/thoughts/2010/02/12/fixed-monospace-sizing/ + */ +pre, code { + font-family: "Courier New", monospace, serif; + font-size: 1em; +} + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Html Tag. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +strong { + font-weight: bold; +} + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Content. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +#iframe { + overflow-x: hidden; +} + +/* +//////////////////////////////////////// +// Colset. +//////////////////////////////////////// +*/ + +#colset { + width: 100%; +} + +#colset #col2, #colset #col1 { + vertical-align: top; +} + +#colset #col2 { + display: table-cell; + width: 250px; +} + +/* Required for IE6 */ +#col2 .local { width: 250px; } + +/* +//////////////////////////////////////// +// Main & Local. +//////////////////////////////////////// +*/ + +#main { + margin: 10px; + border-width: 1px; + padding: 10px 15px; + margin-bottom: 0; +} + +#main.reference h1 { + color: #7C9D00; +} + +.local { + margin-right: 10px; +} + +.local .local-title { + display: block; + padding: 6px 0; + padding-top: 10px; + margin: 0; +} + +.local .local-title a { + text-decoration: none; + font-size: 1.25em; + font-weight: bold; +} + +.local .local-title .toggle { + display: none; +} + +.js .local .local-title .toggle { + display: inline; +} + +.local .local-title .toggle, .local .local-title .toggle a { + font-size: 1.05em; + font-weight: normal; +} + +.local .local-title .toggle a:hover { + text-decoration: underline; +} + +.next-right{ + float:right; +} + +.prev-left{ + float:left; +} + +/* +//////////////////////////////////////// +// Project. +//////////////////////////////////////// +*/ + +#main .project { + clear: left; + padding-top: 10px; +} + +#main .project h1 { + padding: 0; + margin: 5px 0 8px; +} + +#main .project p { + padding: 2px 0; + margin: 0; +} + +/* +//////////////////////////////////////// +// Table of content. +//////////////////////////////////////// +*/ + + +.section-block-right{ + float:right; + clear:right; + width:50%; +} + +.section-block-left{ + float:left; + clear:left; + width:50%; +} + +.toc-item span{ + padding-left:10px; +} + +#table-of-content a { + display: block; + padding: 4px 10px; + text-decoration: none; + color: #444; +} + +#table-of-content a:hover { + text-decoration: none; + border-right: 2px solid #7c9d00; +} + +/* +//////////////////////////////////////// +// Reference menu. +//////////////////////////////////////// +*/ + +.js .menu .menu-sub { + display: none; +} +.js .menu .selected { + display: block; +} + +.menu .menu-block { + width: 210px; + padding: 0 2%; +} + +.menu .menu-block h1 { + padding: 2px 0; + cursor:pointer; + padding-top: 8px; + margin: 0; + font-size: 1.1em; + font-weight: bold; +} + +.menu .menu-sub .menu-item { + margin-left: 10px; + padding: 2px 0; + padding-left: 10px; +} + +.menu .menu-sub .menu-item:first-child { + margin-bottom: 0.3em; + margin-top: 0.3em; +} + +.menu a { + text-decoration: none; +} + + + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Navigation. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +#navigation { + border-width: 0; + border-bottom-width: 1px; +} + +#navigation ul { + margin: 0; + padding: 0; + height: 2.8em; +} + +#navigation ul li { + list-style: none; + padding: 7px 3px; + padding-left: 6px; + margin: 0; + float: left; +} + +#navigation ul li.separator { + display: none; +} + +.js #navigation ul li.separator { + display: block; +} + +#navigation a { + display: block; + padding: 0.3em 10px; + outline: none; + text-decoration: none; +} + +#nav-summary { + position: relative; + margin: 0; + padding: 0; +} + +#nav-summary #nav-summary-childs { + position: absolute; + top: 1.6em; + margin: 0; + border-width: 1px; + padding: 10px; + width: 25em; +} + +#nav-summary #nav-summary-childs { + display: none; +} + +#nav-summary #nav-summary-childs a { + float: none; +} + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Footer. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +#footer { + padding: 10px 10px 10px 20px; +} + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Header. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +#header { + padding: 15px 15px 5px 15px; +} + +#header .images { + width: 100%; + padding: 0; + margin: 0; +} + +#header p { + font-size: 1.1em; + margin: 0 0 0.5em 0; +} + +#header { +} + +#header #logo, #header #sponsor { + display: block; + width: 40%; + float: left; +} + +#header #sponsor { + float: right; + text-align: right; +} + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Default Style. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +.warning { + background-image: url(../img/warning.gif); + background-repeat: no-repeat; + background-position: 10px 10px; + border: 1px solid #CC0000; + margin-top: 1.5em; + margin-bottom: 1.5em; + padding: 10px; + padding-left: 40px; + line-height: 100%; + width: 80%; + background-color: #FFCCCC; +} + +.note { + background-image: url(../img/note.gif); + background-repeat: no-repeat; + background-position: 10px 10px; + border: 1px solid #F0C000; + margin-top: 1.5em; + margin-bottom: 1.5em; + padding: 10px; + padding-left: 40px; + line-height: 100%; + width: 80%; + background-color: #FFFFCE; +} + +.bq { + margin-top: 4px; + margin-bottom: 4px; + padding: 5px 5px 5px 5px; + padding-bottom: 15px; + color: inherit; + background-color: #F0F0F0; + border: 1px dashed black; + font-family: courier, courier new, monospace; + font-size: 1em; + line-height: 1.1em; + white-space: pre; + width: 90%; + overflow: auto; + overflow: scroll -moz-scrollbars-horizontal; + overflow-x: auto; +} + +/** +* Wiki text stylesheet definitions. +* @author Matthias L. Jugel +* @version $Id: SnipSnap-Theme.snip,v 1.1.2.1 2004/01/26 14:07:31 leo Exp $ +*/ + +.bold { + font-weight: bold; +} + +.italic { + font-style: italic; +} + +/************************************************/ +/* image positioning */ +img { + margin-top: 1px; + margin-bottom: 1px; + vertical-align: middle; +} + +img.left { + display: block; + left: 0px; +} + +img.right { + display: block; + right: 0px; +} + +img.center { + text-align: center; +} + +img.float-left { + float: left; + margin-top: 3px; + margin-right: 3px; + margin-bottom: 3px; +} + +img.float-right { + float: right; + margin-top: 3px; + margin-left: 3px; + margin-bottom: 3px; +} + +/* +.graph-image { + position: relative; + width: 100%; +} +*/ + +.heading-1 { + font-weight: bold; + font-size: 1.05em; + font-variant: small-caps; +} + +.heading-1-1 { + font-size: 1em; + font-variant: small-caps; +} + +.heading-1-1-1 { + font-size: 1em; +} + +.quote { + font-style: italic; + color: inherit; + background-color: inherit; + font-family: inherit; + font-size: inherit; +} + +/************************************************/ +/* code formatting */ +.code { + margin: 4px 0; + padding: 5px 15Px; + color: inherit; + border-width: 1px; + font-family: courier, courier new, monospace; + font-size: 1em; + line-height: 1.1em; + white-space: pre; + overflow: auto; + overflow: scroll -moz-scrollbars-horizontal; + overflow-x: auto; +} + +.java-keyword { + font-weight: bold; + background-color: inherit; +} + +.java-object { + background-color: inherit; +} + +.java-quote { + background-color: inherit; +} + +.xml-keyword { + font-weight: bold; +} + +.xml-tag { + color: #0000aa; + background-color: inherit; +} + +/* weblog formatting */ +.blog-date { + display: block; + background-color: #f8f8f8; + color: black; + font-family: verdana, sans-serif; + font-size: 1em; + font-weight: bold; + margin-bottom: 10px; + width: 100%; +} + +/* special formatting of a wiki table */ +.wiki-table { + border-style: solid; + border-color: black; + border-width: 0px 1px 1px 1px; + empty-cells: show; +} + +.wiki-table td { + border-top: 1px solid black; + padding: 4px 4px 4px 4px; +} + +.wiki-table th { + border-top: 1px solid black; + text-align: left; + color: inherit; + font-weight: bold; /* background-color: #DDEEFF; */ + padding: 4px 4px 4px 4px; + font-size: 1em; +} + +.wiki-table .table-odd { + color: inherit; + background-color: #F8F8F8; +} + +.wiki-table .table-even { +} + +/************************************************/ +/* list formatting */ + +.list { +} + +.list-title { + font-weight: bold; +} + +.list ul { + margin-top: 0px; + margin-bottom: 0px; + margin-left: 0px; + padding-left: 0px; + list-style-type: none; +} + +/* wiki lists */ + +ul.minus { + list-style-type: square; +} + +ul.star { + list-style-type: disc; +} + +ol.roman { + list-style-type: lower-roman; +} + +ol.ROMAN { + list-style-type: upper-roman; +} + +ol.alpha { + list-style-type: lower-alpha; +} + +ol.ALPHA { + list-style-type: upper-alpha; +} + +ol.greek { + list-style-type: lower-greek; +} + +ol.GREEK { + list-style-type: upper-greek; +} + +ol.hiragana { + list-style-type: hiragana; +} + +ol.HIRAGANA { + list-style-type: hiragana-iroha; +} + +ol.katakana { + list-style-type: katakana; +} + +ol.KATAKANA { + list-style-type: katakana-iroha; +} + +ol.HEBREW { + list-style-type: hebrew; +} + +/************************************************/ +/* index formatting */ +.index-top { +} + +.index-top th { + padding: 1px 1px 1px 1px; + text-align: left; + color: inherit; + font-weight: bold; + background-color: #d9e4f9; +} + +.index { +} + +.index td { + padding: 1px 1px 1px 1px; +} + +.index th { + padding: 1px 1px 1px 1px; + text-align: left; + color: inherit; + font-weight: bold; + background-color: #d9e4f9; +} + +/************************************************/ +/* calendar display */ +.calendar { + border-spacing: 0px; + font-size: 0.7em; +} + +.calendar td { + text-align: right; + padding: 0px; +} + +.calendar caption { + font-size: 1em; + text-align: left; + font-weight: bold; +} + +.calendar .today { + border: 3px solid #cccccc; +} + +li { + margin-bottom: 2px; + margin-top: 2px; +} + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// CSS 3. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +.corner-all, #nav-summary #nav-summary-childs { + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +#navigation a { + border-radius: 18px; + -moz-border-radius: 18px; + -webkit-border-radius: 18px; +} + +#footer, #table-of-content a:hover, #navigation #nav-summary-childs a { + text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.9); +} + +.wiki-table th strong { + text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.5); +} + +#navigation a, #nav-summary #nav-summary-childs a:hover { + text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.8); +} diff --git a/src/template/css/menu.css b/src/template/css/menu.css new file mode 100644 index 0000000..ace229e --- /dev/null +++ b/src/template/css/menu.css @@ -0,0 +1,17 @@ +.menuItem { + font-family:Tahoma, Verdana, Arial; + font-weight: normal; + font-size:95%; +} +.menuTitle { + font-size:85%; + font-family:Tahoma, Verdana, Arial; +} +.menuUsageItem { + font-size:95%; + font-family:Tahoma, Verdana, Arial; + margin-bottom:10px; +} +.menuUsageItem a { + text-decoration: none; +} \ No newline at end of file diff --git a/src/template/css/pdf.css b/src/template/css/pdf.css new file mode 100644 index 0000000..da847b0 --- /dev/null +++ b/src/template/css/pdf.css @@ -0,0 +1,148 @@ +/* page layout and page numbering */ +@page { + size: a4; + font-family:Arial; +} + +@page :left { + @bottom-left { + content: counter(page); + vertical-align: middle; + margin: 0.8em 0; + font-size: 12pt; + } +} + +@page :right { + @bottom-right { + content: counter(page); + vertical-align: middle; + margin: 0.8em 0; + font-size: 12pt; + } +} + +.body { + margin: 0px; + padding: 0px; + font-size: 12pt; +} + +#main { + margin: 0; +} + +#header { + margin:0px; + padding:0px; + background:url(../img/bg.png) no-repeat; +} + +#header .images .rsalogo{ + display:none; +} + +.body #header .message{ + color:#FFFFFF; + font-size:16px; + margin-left:60px; + margin-top:730px; + border-bottom:none; + text-align:left; +} + +/* style overrides */ + +.paragraph { + text-align:center; + margin: 0.2em 0; + padding: 0.2em 0; +} + +h1 { + font-family: helvetica,arial,sans-serif; + font-size: 1.5em; + string-set: header content(); + page-break-before: always +} + +h1>a { + font-size: 1em; + string-set: header content(); + page-break-before: always +} + +h2 { + font-family: helvetica,arial,sans-serif; + font-size: 1.3em; +} + +h3 { + font-family: helvetica,arial,sans-serif; + font-size: 1.2em; +} + +h4 { + font-family: helvetica,arial,sans-serif; + font-size: 1.1em; +} + +h1, h2, h3, h1>a, h2>a, h3>a { + color:#000000; +} + +#toc div.tocItem { + list-style: none; + margin: 0; + padding: 0; +} + +#toc div.tocItem a::after { + content: leader('.') target-counter(attr(href), page); + font-style: normal; +} + +#col1 { + width: 100%; +} + +#col2 { + width: 0px; +} + +#col2 .local { + display: none; +} + +div.title { + font-size: 20px; +} + +.code pre { + text-align: left; + padding-left:5px; + font-size: 0.85em; + line-height: 100%; + white-space: pre-wrap; + page-break-inside: avoid; +} + +.bq { + white-space: pre-wrap; + page-break-inside: avoid; +} + +.code, .bq { + text-align: left; + margin-bottom: 1em; +} + +.warning, .note{ + background-position:10px 10px; +} + +.warning, .note, table { + margin-top: 1em; + margin-bottom: 1em; + page-break-inside: avoid; +} diff --git a/src/template/css/ref.css b/src/template/css/ref.css new file mode 100644 index 0000000..adf22de --- /dev/null +++ b/src/template/css/ref.css @@ -0,0 +1,431 @@ +@import "tools.css"; +@import "skin.css"; + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Body & Html. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +body { + font-family: Arial; +} + +.body { + margin-left: 30px; + font-family: Tahoma, Verdana, Arial; + font-size:95%; +} + +h1 { + font-family: Arial, Verdana, Tahoma; + border-bottom:1px solid #eeeeee; + font-size:1.2em; + margin-left:30px; + margin-top:50px; +} + +h2 { + font-family: Arial, Verdana, Tahoma; + font-weight:normal; + font-style: italic; + font-size:1.0em; + margin-left:30px; +} + +p { + margin-left:50px; +} + +ul { + + margin-left:50px; +} + +ol { + + margin-left:50px; +} + +blockquote { + margin-left:50px; +} + +/** +* Wiki text stylesheet definitions. +* @author Matthias L. Jugel +* @version $Id: SnipSnap-Theme.snip,v 1.1.2.1 2004/01/26 14:07:31 leo Exp $ +*/ + +.bold { + font-weight: bold; +} + +.italic { + font-style: italic; +} + +/************************************************/ +/* image positioning */ +img { + margin-top: 1px; + margin-bottom: 1px; + vertical-align: middle; +} + +img.left { + display: block; + left: 0px; +} + +img.right { + display: block; + right: 0px; +} + +img.center { + text-align: center; +} + +img.float-left { + float: left; + margin-top: 3px; + margin-right: 3px; + margin-bottom: 3px; +} + +img.float-right { + float: right; + margin-top: 3px; + margin-left: 3px; + margin-bottom: 3px; +} + +/* +.graph-image { + position: relative; + width: 100%; +} +*/ + +.heading-1 { + font-weight: bold; + font-size: 14px; + font-variant: small-caps; +} + +.heading-1-1 { + font-size: 12px; + font-variant: small-caps; +} + +.heading-1-1-1 { + font-size: 12px; +} + +.quote { + font-style: italic; + color: inherit; + background-color: inherit; + font-family: inherit; + font-size: inherit; +} + +.warning { + background-image: url(../img/warning.gif); + background-repeat: no-repeat; + background-position: 7 7; + border: 1px solid #CC0000; + margin-top: 4px; + margin-bottom: 4px; + margin-left:50px; + padding: 10px; + padding-left: 40px; + line-height: 100%; + width: 85%; + background-color: #FFCCCC; +} + +.note { + background-image: url(../img/note.gif); + background-repeat: no-repeat; + background-position: 7 7; + border: 1px solid #F0C000; + margin-top: 4px; + margin-bottom: 4px; + margin-left:50px; + padding: 10px; + padding-left: 40px; + line-height: 100%; + width: 85%; + background-color: #FFFFCE; +} +/************************************************/ +/* code formatting */ +.bq { + margin-top: 4px; + margin-bottom: 4px; + margin-left:40px; + padding: 5px 5px 5px 5px; + padding-bottom: 15px; + color: inherit; + background-color: #F0F0F0; + border: 1px dashed black; + font-family: courier, courier new, monospace; + font-size: 12px; + line-height: 100%; + white-space: pre; + width: 90%; + overflow: auto; + overflow: scroll -moz-scrollbars-horizontal; + overflow-x: auto; +} +.code { + margin-top: 4px; + margin-bottom: 4px; + margin-left:40px; + padding: 5px 5px 5px 5px; + color: inherit; + background-color: #F0F0F0; + border: 1px dashed black; + font-family: courier, courier new, monospace; + font-size: 12px; + line-height: 100%; + white-space: pre; + width: 90%; + overflow: auto; + overflow: scroll -moz-scrollbars-horizontal; + overflow-x: auto; +} +/* Paragraphs are created inside code blocks after blank lines */ +.code p { + margin-left: 0px; +} +table .code { + margin-left:0px; +} + +.java-keyword { + font-weight: bold; + background-color: inherit; +} + +.java-object { + background-color: inherit; +} + +.java-quote { + background-color: inherit; +} + +.xml-keyword { + font-weight: bold; +} + +.xml-tag { + color: #0000aa; + background-color: inherit; +} + +/* weblog formatting */ +.blog-date { + display: block; + background-color: #f8f8f8; + color: black; + font-family: verdana, sans-serif; + font-size: 16px; + font-weight: bold; + margin-bottom: 10px; + width: 100%; +} + +/* special formatting of a wiki table */ +.wiki-table { + border-style: solid; + border-color: black; + border-width: 0px 1px 1px 1px; + empty-cells: show; +} + +.wiki-table td { + border-top: 1px solid black; + padding: 4px 4px 4px 4px; +} + +.wiki-table th { + border-top: 1px solid black; + text-align: left; + color: inherit; + font-weight: bold; /* background-color: #DDEEFF; */ + padding: 4px 4px 4px 4px; + font-size: 12px; +} + +.wiki-table .table-odd { + color: inherit; + background-color: #F8F8F8; +} + +.wiki-table .table-even { +} + +/************************************************/ +/* list formatting */ + +.list { +} + +.list-title { + font-weight: bold; +} + +.list ul { + margin-top: 0px; + margin-bottom: 0px; + margin-left: 0px; + padding-left: 0px; + list-style-type: none; +} + +/* wiki lists */ + +ul.minus { + list-style-type: square; +} + +ul.star { + list-style-type: disc; +} + +ol.roman { + list-style-type: lower-roman; +} + +ol.ROMAN { + list-style-type: upper-roman; +} + +ol.alpha { + list-style-type: lower-alpha; +} + +ol.ALPHA { + list-style-type: upper-alpha; +} + +ol.greek { + list-style-type: lower-greek; +} + +ol.GREEK { + list-style-type: upper-greek; +} + +ol.hiragana { + list-style-type: hiragana; +} + +ol.HIRAGANA { + list-style-type: hiragana-iroha; +} + +ol.katakana { + list-style-type: katakana; +} + +ol.KATAKANA { + list-style-type: katakana-iroha; +} + +ol.HEBREW { + list-style-type: hebrew; +} + +/************************************************/ +/* index formatting */ +.index-top { +} + +.index-top th { + padding: 1px 1px 1px 1px; + text-align: left; + color: inherit; + font-weight: bold; + background-color: #d9e4f9; +} + +.index { +} + +.index td { + padding: 1px 1px 1px 1px; +} + +.index th { + padding: 1px 1px 1px 1px; + text-align: left; + color: inherit; + font-weight: bold; + background-color: #d9e4f9; +} + +/************************************************/ +/* calendar display */ +.calendar { + border-spacing: 0px; + font-size: 0.7em; +} + +.calendar td { + text-align: right; + padding: 0px; +} + +.calendar caption { + font-size: 1em; + text-align: left; + font-weight: bold; +} + +.calendar .today { + border: 3px solid #cccccc; +} + +li { + margin-bottom: 2px; + margin-top: 2px; +} + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// CSS 3. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +.corner-all, #nav-summary #nav-summary-childs { + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +#navigation a { + border-radius: 14px; + -moz-border-radius: 14px; + -webkit-border-radius: 14px; +} + +#footer, #table-of-content a:hover, #navigation #nav-summary-childs a { + text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.9); +} + +.wiki-table th strong { + text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.5); +} + +#navigation a, #nav-summary #nav-summary-childs a:hover { + text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.8); +} diff --git a/src/template/css/skin.css b/src/template/css/skin.css new file mode 100644 index 0000000..7476995 --- /dev/null +++ b/src/template/css/skin.css @@ -0,0 +1,139 @@ +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Color. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +body, .wiki-table th strong { + color: #444; +} + +a, a:hover, a:focus, a:active, a:hover, #main .project strong, #table-of-content a:hover strong, + #table-of-content a strong, .menu .menu-block h1, .menu a:hover, .local .local-title .toggle a, + #navigation #nav-summary-childs a:hover, #main h1, #main h2 { + color: #7c9d00; +} + +#main .project h1, #table-of-content h2 { + color: #444; +} + +#footer { + color: #999; +} + +#footer a, .menu a, .local .local-title a, .local .local-title .toggle, + #navigation #nav-summary-childs a { + color: #333; +} + +.java-keyword, .java-object { + color: #0911ff; +} + +.java-quote { + color: #268000; +} + +#navigation a, #nav-summary #nav-summary-childs a:hover { + color: white; +} + +.toc-item a { + text-decoration: none; +} + +.toc-item a:hover { + text-decoration: underline; +} + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Background Color. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +#main { + background: white; +} + +.code, #navigation #nav-summary-childs { + background: #F2f2F2; +} + + +#navigation .selected a { + background: #5a7103; +} + +#navigation a:hover { + background: #678104; +} + +#navigation .selected a:hover, #navigation .active .button { + background: #526703; +} + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Background Image. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + + +.menu .menu-item { + background: url(../img/default/bullet.gif) no-repeat 2px 8px; +} + +.local .local-title { + background: #F2F2F2 url(../img/default/separator-horizontal.gif) repeat-x 0 bottom; +} + +#table-of-content a:hover { + background: #F2F2F2 url(../img/default/linear-gradient.png) repeat-x 0 0; +} + +.wiki-table th, #navigation { + background: #a4c001 url(../img/default/linear-gradient-green.png) repeat-x 0 0; +} + +#navigation .separator { + background: url(../img/default/separator-menu.png) no-repeat 0 center; +} + + + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Border Color. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +#main { + border-color: #d3d3d3; + border-style: solid; +} + +#navigation { + border-style: solid; + border-color: #344d06; +} + +.code { + border-style: dashed; + border-color: #333; +} + +#navigation #nav-summary-childs { + border-style: solid; + border-color: #888; + border-top-color: #444; +} diff --git a/src/template/css/tools.css b/src/template/css/tools.css new file mode 100644 index 0000000..47e49c5 --- /dev/null +++ b/src/template/css/tools.css @@ -0,0 +1,123 @@ +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Style de base. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +html { + font-size: 100%; +} + +body { + margin: 0; + padding: 0; + font-size: .8em; + line-height: 1.2; + color: black; + background: white; +} + +h1, h2, h3, h4, h5, h6 { + margin: 1em 0 .5em 0; + line-height: 1.1; + font-weight: bold; + font-style: normal; +} +h1 { + font-size: 1.75em; +} +h2 { + font-size: 1.5em; +} +h3 { + font-size: 1.4em; +} +h4 { + font-size: 1.3em; +} + +ul, ol { + margin: .75em 0 .75em 32px; + padding: 0; +} + +p { + margin: 1em 0; +} + +address { + margin: .75em 0; + font-style: normal; +} + +a { + text-decoration: underline; +} + +a:active { + outline: none; +} +a img { + border: none; +} + +em { + font-style: italic; +} + +strong { + font-weight: bold; +} + +form, fieldset { + margin: 0; + padding: 0; + border: none; +} + +input, button, select { + vertical-align: middle; +} + +/* +//////////////////////////////////////////////////////////////////////////////// +// +// Clearfix. +// +//////////////////////////////////////////////////////////////////////////////// +*/ + +.clearfix:after +{ + content: "."; + display: block; + clear: both; + visibility: hidden; + line-height: 0; + font-size: 0; + height: 0; +} + +.clearfix +{ + display: inline-block; +} + +html[xmlns] .clearfix +{ + display: block; +} + +* html .clearfix +{ + height: 1%; +} + + +#toggle-col1{ + display: none; + float: right; + padding-left: 50px; +} diff --git a/src/template/img/default/bullet.gif b/src/template/img/default/bullet.gif new file mode 100644 index 0000000..4ec6a9c Binary files /dev/null and b/src/template/img/default/bullet.gif differ diff --git a/src/template/img/default/linear-gradient-green.png b/src/template/img/default/linear-gradient-green.png new file mode 100644 index 0000000..a746c85 Binary files /dev/null and b/src/template/img/default/linear-gradient-green.png differ diff --git a/src/template/img/default/linear-gradient.png b/src/template/img/default/linear-gradient.png new file mode 100644 index 0000000..ce9cab8 Binary files /dev/null and b/src/template/img/default/linear-gradient.png differ diff --git a/src/template/img/default/separator-horizontal.gif b/src/template/img/default/separator-horizontal.gif new file mode 100644 index 0000000..dc161f9 Binary files /dev/null and b/src/template/img/default/separator-horizontal.gif differ diff --git a/src/template/img/default/separator-menu.png b/src/template/img/default/separator-menu.png new file mode 100644 index 0000000..efbc885 Binary files /dev/null and b/src/template/img/default/separator-menu.png differ diff --git a/src/template/img/default/separator-vertical.gif b/src/template/img/default/separator-vertical.gif new file mode 100644 index 0000000..6a7d0d5 Binary files /dev/null and b/src/template/img/default/separator-vertical.gif differ diff --git a/src/template/img/favicon.ico b/src/template/img/favicon.ico new file mode 100644 index 0000000..3dfcb92 Binary files /dev/null and b/src/template/img/favicon.ico differ diff --git a/src/template/img/grails-icon.png b/src/template/img/grails-icon.png new file mode 100644 index 0000000..68e3678 Binary files /dev/null and b/src/template/img/grails-icon.png differ diff --git a/src/template/img/grails.png b/src/template/img/grails.png new file mode 100644 index 0000000..9cb734d Binary files /dev/null and b/src/template/img/grails.png differ diff --git a/src/template/img/groovy.png b/src/template/img/groovy.png new file mode 100644 index 0000000..2b81b49 Binary files /dev/null and b/src/template/img/groovy.png differ diff --git a/src/template/img/note.gif b/src/template/img/note.gif new file mode 100644 index 0000000..1c9883b Binary files /dev/null and b/src/template/img/note.gif differ diff --git a/src/template/img/springsource-logo.png b/src/template/img/springsource-logo.png new file mode 100644 index 0000000..34e8d8e Binary files /dev/null and b/src/template/img/springsource-logo.png differ diff --git a/src/template/img/warning.gif b/src/template/img/warning.gif new file mode 100644 index 0000000..c6acdec Binary files /dev/null and b/src/template/img/warning.gif differ diff --git a/src/template/js/docs.js b/src/template/js/docs.js new file mode 100644 index 0000000..5f78346 --- /dev/null +++ b/src/template/js/docs.js @@ -0,0 +1,57 @@ +function nextElement(el) { + el = el.nextSibling; + while (el && el.nodeType != 1) { + el = el.nextSibling; + } + return el; +} +function indexOf(arr, o) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] == o) return i; + } + return -1; +} +function contains(arr, o) { return indexOf(arr, o) != -1 } +function getClasses(el) { return el.className.split(" "); } +function pushClass(el, cls) { + var classes = getClasses(el); + classes.push(cls); + el.className = classes.join(" "); + return el.className; +} +function removeClass(el, cls) { + var classes = getClasses(el); + classes.splice(indexOf(classes, "selected"), 1) + el.className = classes.join(" "); + return el.className; +} +function toggleRef(el) { + if (contains(getClasses(el), "selected")) { + removeClass(el, "selected"); + } + else { + pushClass(el, "selected"); + } +} + +var show = true; +function localToggle() { + document.getElementById("col2").style.display = show ? "none" : ""; + document.getElementById("toggle-col1").style.display = show ? "inline" : "none"; + document.getElementById("ref-button").parentNode.className = (show = !show) ? "separator selected" : "separator"; + return false; +} +function toggleNavSummary(hide) { + document.getElementById("nav-summary-childs").style.display = !hide ? "block" : "none"; + document.getElementById("nav-summary").className = hide ? "" : "active"; +} + +var hiddenBlocksShown = false; +function toggleHidden() { + var elements = document.getElementsByClassName("hidden-block"); + for (var i = 0; i < elements.length; i++) { + elements[i].style.display = hiddenBlocksShown ? "none" : "block"; + } + + hiddenBlocksShown = !hiddenBlocksShown +} diff --git a/src/template/log4j.properties b/src/template/log4j.properties new file mode 100644 index 0000000..d5eefe3 --- /dev/null +++ b/src/template/log4j.properties @@ -0,0 +1,10 @@ +# Set root logger level to DEBUG and its only appender to A1. +# TRACE,DEBUG,INFO,WARN,ERROR,FATAL +log4j.rootLogger=ERROR, A1 + +# A1 is set to be a ConsoleAppender. +log4j.appender.A1=org.apache.log4j.ConsoleAppender + +# A1 uses PatternLayout. +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n diff --git a/src/template/style/guideItem.html b/src/template/style/guideItem.html new file mode 100644 index 0000000..e9ee9a9 --- /dev/null +++ b/src/template/style/guideItem.html @@ -0,0 +1,142 @@ + + + + ${sectionNumber} ${title.encodeAsHtml()} ${version} + + + + + + + + + + + + + + + + +
+
+ + <% if(prev){ %> + + <% } %> + + (Quick Reference) + + <% if(next){ %> + + <% } %> + + +
+

${sectionNumber} ${title.encodeAsHtml()} - Reference Documentation

+ +

Authors: ${authors}

+ +

Version: ${version}

+
+ + <% if (sectionToc) { %> +
+

Table of Contents

+ <% + sectionWriter = { lvl, section, prefix -> + %> + + <% + lvl++ + section.children.eachWithIndex { s, j -> + sectionWriter.call(lvl, s, prefix + '.' + (j + 1)) + } + } + + sectionToc.eachWithIndex { s, i -> + sectionWriter.call(0, s, chapterNumber + '.' + (i + 1)) + } + %> +
+ <% } %> + + ${content} + +
+ <% if(prev){ %> + + <% } + if(next){ %> + + <% } %> +
+
+
+
+ + +
+
+ + + + + + + diff --git a/src/template/style/index.html b/src/template/style/index.html new file mode 100644 index 0000000..645ce7d --- /dev/null +++ b/src/template/style/index.html @@ -0,0 +1,17 @@ + + + ${title} ${version} Reference Documentation + + + + + + + + + <h2>Frame Alert</h2> + <p/> + This document is designed to be viewed using the frames feature. If you see this message, you are using a non-frame-capable web client. + + + diff --git a/src/template/style/layout.html b/src/template/style/layout.html new file mode 100644 index 0000000..1d2e1e1 --- /dev/null +++ b/src/template/style/layout.html @@ -0,0 +1,140 @@ + + + + ${title.encodeAsHtml()} ${version} + + + + + + + + + + + + + + + + +
+
+ + (Quick Reference) + +
+

${title.encodeAsHtml()} - Reference Documentation

+

Authors: ${authors}

+

Version: ${version}

+
+ + <% if(toc){ %> +
+

Table of Contents

+ <% + sectionWriter = { lvl, section, topSection, prefix -> + if (single) { + %> + + <% + } + else { + %> + + <% + } + + lvl++ + section.children.eachWithIndex { s, j -> + sectionWriter.call(lvl, s, topSection, prefix + '.' + (j + 1)) + } + } + + toc.children.eachWithIndex { topSection, i -> + sectionWriter.call(0, topSection, topSection, i + 1) + } + %> +
+
+ <% } %> + ${content} +
+
+
+ + +
+
+ + + + + + + + + diff --git a/src/template/style/menu.html b/src/template/style/menu.html new file mode 100644 index 0000000..8d3e593 --- /dev/null +++ b/src/template/style/menu.html @@ -0,0 +1,13 @@ + + + + menu + + + + + ${menu} + + diff --git a/src/template/style/referenceItem.html b/src/template/style/referenceItem.html new file mode 100644 index 0000000..db88fe8 --- /dev/null +++ b/src/template/style/referenceItem.html @@ -0,0 +1,98 @@ + + + + ${title.encodeAsHtml()} ${version} + + + + + + + + + + + + + + + + + +
+
+ + (Quick Reference) + + ${content} +
+
+
+ + +
+
+ + + + + + + + diff --git a/src/template/style/section.html b/src/template/style/section.html new file mode 100644 index 0000000..c627ced --- /dev/null +++ b/src/template/style/section.html @@ -0,0 +1,4 @@ +<% def hLevel = level == 0 ? 1 : 2 %> +<% if (legacyLinks[name]) { %><% } %> +${sectionNumber} ${title} +${content} diff --git a/web-app/WEB-INF/.gitignore b/web-app/WEB-INF/.gitignore new file mode 100644 index 0000000..d366d2e --- /dev/null +++ b/web-app/WEB-INF/.gitignore @@ -0,0 +1 @@ +classes \ No newline at end of file diff --git a/web-app/WEB-INF/applicationContext.xml b/web-app/WEB-INF/applicationContext.xml new file mode 100644 index 0000000..6f42796 --- /dev/null +++ b/web-app/WEB-INF/applicationContext.xml @@ -0,0 +1,42 @@ + + + + + Grails application factory bean + + + + + + A bean that manages Grails plugins + + + + + + + + + + + + + + + + + + classpath*:**/grails-app/**/*.groovy + + + + + + utf-8 + + + \ No newline at end of file diff --git a/web-app/WEB-INF/sitemesh.xml b/web-app/WEB-INF/sitemesh.xml new file mode 100644 index 0000000..a547b41 --- /dev/null +++ b/web-app/WEB-INF/sitemesh.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/web-app/WEB-INF/tld/grails.tld b/web-app/WEB-INF/tld/grails.tld new file mode 100644 index 0000000..9bd036b --- /dev/null +++ b/web-app/WEB-INF/tld/grails.tld @@ -0,0 +1,550 @@ + + + The Grails custom tag library + 0.2 + grails + http://grails.codehaus.org/tags + + + link + org.codehaus.groovy.grails.web.taglib.jsp.JspLinkTag + JSP + + action + false + true + + + controller + false + true + + + id + false + true + + + url + false + true + + + params + false + true + + true + + + form + org.codehaus.groovy.grails.web.taglib.jsp.JspFormTag + JSP + + action + false + true + + + controller + false + true + + + id + false + true + + + url + false + true + + + method + true + true + + true + + + select + org.codehaus.groovy.grails.web.taglib.jsp.JspSelectTag + JSP + + name + true + true + + + value + false + true + + + optionKey + false + true + + + optionValue + false + true + + true + + + datePicker + org.codehaus.groovy.grails.web.taglib.jsp.JspDatePickerTag + empty + + name + true + true + + + value + false + true + + + precision + false + true + + false + + + currencySelect + org.codehaus.groovy.grails.web.taglib.jsp.JspCurrencySelectTag + empty + + name + true + true + + + value + false + true + + true + + + localeSelect + org.codehaus.groovy.grails.web.taglib.jsp.JspLocaleSelectTag + empty + + name + true + true + + + value + false + true + + true + + + timeZoneSelect + org.codehaus.groovy.grails.web.taglib.jsp.JspTimeZoneSelectTag + empty + + name + true + true + + + value + false + true + + true + + + checkBox + org.codehaus.groovy.grails.web.taglib.jsp.JspCheckboxTag + empty + + name + true + true + + + value + true + true + + true + + + hasErrors + org.codehaus.groovy.grails.web.taglib.jsp.JspHasErrorsTag + JSP + + model + false + true + + + bean + false + true + + + field + false + true + + false + + + eachError + org.codehaus.groovy.grails.web.taglib.jsp.JspEachErrorTag + JSP + + model + false + true + + + bean + false + true + + + field + false + true + + false + + + renderErrors + org.codehaus.groovy.grails.web.taglib.jsp.JspEachErrorTag + JSP + + model + false + true + + + bean + false + true + + + field + false + true + + + as + true + true + + false + + + message + org.codehaus.groovy.grails.web.taglib.jsp.JspMessageTag + JSP + + code + false + true + + + error + false + true + + + default + false + true + + false + + + remoteFunction + org.codehaus.groovy.grails.web.taglib.jsp.JspRemoteFunctionTag + empty + + before + false + true + + + after + false + true + + + action + false + true + + + controller + false + true + + + id + false + true + + + url + false + true + + + params + false + true + + + asynchronous + false + true + + + method + false + true + + + update + false + true + + + onSuccess + false + true + + + onFailure + false + true + + + onComplete + false + true + + + onLoading + false + true + + + onLoaded + false + true + + + onInteractive + false + true + + true + + + remoteLink + org.codehaus.groovy.grails.web.taglib.jsp.JspRemoteLinkTag + JSP + + before + false + true + + + after + false + true + + + action + false + true + + + controller + false + true + + + id + false + true + + + url + false + true + + + params + false + true + + + asynchronous + false + true + + + method + false + true + + + update + false + true + + + onSuccess + false + true + + + onFailure + false + true + + + onComplete + false + true + + + onLoading + false + true + + + onLoaded + false + true + + + onInteractive + false + true + + true + + + formRemote + org.codehaus.groovy.grails.web.taglib.jsp.JspFormRemoteTag + JSP + + before + false + true + + + after + false + true + + + action + false + true + + + controller + false + true + + + id + false + true + + + url + false + true + + + params + false + true + + + asynchronous + false + true + + + method + false + true + + + update + false + true + + + onSuccess + false + true + + + onFailure + false + true + + + onComplete + false + true + + + onLoading + false + true + + + onLoaded + false + true + + + onInteractive + false + true + + true + + + invokeTag + org.codehaus.groovy.grails.web.taglib.jsp.JspInvokeGrailsTagLibTag + JSP + + it + java.lang.Object + true + NESTED + + + tagName + true + true + + true + + + diff --git a/web-app/WEB-INF/tld/spring.tld b/web-app/WEB-INF/tld/spring.tld new file mode 100644 index 0000000..1bc7091 --- /dev/null +++ b/web-app/WEB-INF/tld/spring.tld @@ -0,0 +1,311 @@ + + + + + + 1.1.1 + + 1.2 + + Spring + + http://www.springframework.org/tags + + Spring Framework JSP Tag Library. Authors: Rod Johnson, Juergen Hoeller + + + + + htmlEscape + org.springframework.web.servlet.tags.HtmlEscapeTag + JSP + + + Sets default HTML escape value for the current page. + Overrides a "defaultHtmlEscape" context-param in web.xml, if any. + + + + defaultHtmlEscape + true + true + + + + + + + + escapeBody + org.springframework.web.servlet.tags.EscapeBodyTag + JSP + + + Escapes its enclosed body content, applying HTML escaping and/or JavaScript escaping. + The HTML escaping flag participates in a page-wide or application-wide setting + (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). + + + + htmlEscape + false + true + + + + javaScriptEscape + false + true + + + + + + + + message + org.springframework.web.servlet.tags.MessageTag + JSP + + + Retrieves the message with the given code, or text if code isn't resolvable. + The HTML escaping flag participates in a page-wide or application-wide setting + (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). + + + + code + false + true + + + + arguments + false + true + + + + text + false + true + + + + var + false + true + + + + scope + false + true + + + + htmlEscape + false + true + + + + javaScriptEscape + false + true + + + + + + + + theme + org.springframework.web.servlet.tags.ThemeTag + JSP + + + Retrieves the theme message with the given code, or text if code isn't resolvable. + The HTML escaping flag participates in a page-wide or application-wide setting + (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). + + + + code + false + true + + + + arguments + false + true + + + + text + false + true + + + + var + false + true + + + + scope + false + true + + + + htmlEscape + false + true + + + + javaScriptEscape + false + true + + + + + + + + hasBindErrors + org.springframework.web.servlet.tags.BindErrorsTag + JSP + + + Provides Errors instance in case of bind errors. + The HTML escaping flag participates in a page-wide or application-wide setting + (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). + + + + errors + org.springframework.validation.Errors + + + + name + true + true + + + + htmlEscape + false + true + + + + + + + + nestedPath + org.springframework.web.servlet.tags.NestedPathTag + JSP + + + Sets a nested path to be used by the bind tag's path. + + + + nestedPath + java.lang.String + + + + path + true + true + + + + + + + + bind + org.springframework.web.servlet.tags.BindTag + JSP + + + Provides BindStatus object for the given bind path. + The HTML escaping flag participates in a page-wide or application-wide setting + (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). + + + + status + org.springframework.web.servlet.support.BindStatus + + + + path + true + true + + + + ignoreNestedPath + false + true + + + + htmlEscape + false + true + + + + + + + + transform + org.springframework.web.servlet.tags.TransformTag + JSP + + + Provides transformation of variables to Strings, using an appropriate + custom PropertyEditor from BindTag (can only be used inside BindTag). + The HTML escaping flag participates in a page-wide or application-wide setting + (i.e. by HtmlEscapeTag or a "defaultHtmlEscape" context-param in web.xml). + + + + value + true + true + + + + var + false + true + + + + scope + false + true + + + + htmlEscape + false + true + + + + +