diff --git a/cli/src/main/groovy/com/jessebrault/ssg/AbstractBuildCommand.groovy b/cli/src/main/groovy/com/jessebrault/ssg/AbstractBuildCommand.groovy new file mode 100644 index 0000000..cc087f7 --- /dev/null +++ b/cli/src/main/groovy/com/jessebrault/ssg/AbstractBuildCommand.groovy @@ -0,0 +1,89 @@ +package com.jessebrault.ssg + +import com.jessebrault.ssg.buildscript.GroovyBuildScriptRunner +import com.jessebrault.ssg.part.GspPartRenderer +import com.jessebrault.ssg.part.PartFilePartsProvider +import com.jessebrault.ssg.part.PartType +import com.jessebrault.ssg.specialpage.GspSpecialPageRenderer +import com.jessebrault.ssg.specialpage.SpecialPageFileSpecialPagesProvider +import com.jessebrault.ssg.specialpage.SpecialPageType +import com.jessebrault.ssg.template.GspTemplateRenderer +import com.jessebrault.ssg.template.TemplateFileTemplatesProvider +import com.jessebrault.ssg.template.TemplateType +import com.jessebrault.ssg.text.MarkdownExcerptGetter +import com.jessebrault.ssg.text.MarkdownFrontMatterGetter +import com.jessebrault.ssg.text.MarkdownTextRenderer +import com.jessebrault.ssg.text.TextFileTextsProvider +import com.jessebrault.ssg.text.TextType +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger + +abstract class AbstractBuildCommand extends AbstractSubCommand { + + private static final Logger logger = LogManager.getLogger(AbstractBuildCommand) + + protected final Collection builds = [] + protected final StaticSiteGenerator ssg + + AbstractBuildCommand() { + // Configure + def markdownText = new TextType(['.md'], new MarkdownTextRenderer(), new MarkdownFrontMatterGetter(), new MarkdownExcerptGetter()) + def gspTemplate = new TemplateType(['.gsp'], new GspTemplateRenderer()) + def gspPart = new PartType(['.gsp'], new GspPartRenderer()) + def gspSpecialPage = new SpecialPageType(['.gsp'], new GspSpecialPageRenderer()) + + def defaultTextsProvider = new TextFileTextsProvider([markdownText], new File('texts')) + def defaultTemplatesProvider = new TemplateFileTemplatesProvider([gspTemplate], new File('templates')) + def defaultPartsProvider = new PartFilePartsProvider([gspPart], new File('parts')) + def defaultSpecialPagesProvider = new SpecialPageFileSpecialPagesProvider([gspSpecialPage], new File('specialPages')) + + def defaultConfig = new Config( + textProviders: [defaultTextsProvider], + templatesProviders: [defaultTemplatesProvider], + partsProviders: [defaultPartsProvider], + specialPagesProviders: [defaultSpecialPagesProvider] + ) + def defaultGlobals = [:] + + // Run build script, if applicable + if (new File('ssgBuilds.groovy').exists()) { + logger.info('found buildScript: ssgBuilds.groovy') + def buildScriptRunner = new GroovyBuildScriptRunner() + this.builds.addAll(buildScriptRunner.runBuildScript('ssgBuilds.groovy', defaultConfig, defaultGlobals)) + logger.debug('after running ssgBuilds.groovy, builds: {}', this.builds) + } + + if (this.builds.empty) { + // Add default build + builds << new Build('default', defaultConfig, defaultGlobals, new File('build')) + } + + // Get ssg object + this.ssg = new SimpleStaticSiteGenerator() + } + + protected final Integer doBuild() { + logger.traceEntry('builds: {}, ssg: {}', this.builds, this.ssg) + + def hadDiagnostics = false + // Do each build + this.builds.each { + def result = this.ssg.generate(it) + if (result.v1.size() > 0) { + hadDiagnostics = true + result.v1.each { + logger.error(it.message) + } + } else { + result.v2.each { GeneratedPage generatedPage -> + def target = new File(it.outDir, generatedPage.path + '.html') + target.createParentDirectories() + target.write(generatedPage.html) + } + } + } + + logger.traceExit(hadDiagnostics ? 1 : 0) + } + +} diff --git a/cli/src/main/groovy/com/jessebrault/ssg/AbstractSubCommand.groovy b/cli/src/main/groovy/com/jessebrault/ssg/AbstractSubCommand.groovy new file mode 100644 index 0000000..be780df --- /dev/null +++ b/cli/src/main/groovy/com/jessebrault/ssg/AbstractSubCommand.groovy @@ -0,0 +1,45 @@ +package com.jessebrault.ssg + +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import org.apache.logging.log4j.core.LoggerContext +import picocli.CommandLine + +import java.util.concurrent.Callable + +abstract class AbstractSubCommand implements Callable { + + private static final Logger logger = LogManager.getLogger(AbstractSubCommand) + + @CommandLine.ParentCommand + StaticSiteGeneratorCli cli + + abstract Integer doSubCommand() + + @Override + Integer call() { + logger.traceEntry() + + // Setup Loggers + def context = (LoggerContext) LogManager.getContext(false) + def configuration = context.getConfiguration() + def rootLoggerConfig = configuration.getRootLogger() + + if (this.cli.logLevel?.info) { + rootLoggerConfig.level = Level.INFO + } else if (this.cli.logLevel?.debug) { + rootLoggerConfig.level = Level.DEBUG + } else if (this.cli.logLevel?.trace) { + rootLoggerConfig.level = Level.TRACE + } else { + rootLoggerConfig.level = Level.WARN + } + + context.updateLoggers() + + // Run SubCommand + logger.traceExit(this.doSubCommand()) + } + +} diff --git a/cli/src/main/groovy/com/jessebrault/ssg/SsgBuild.groovy b/cli/src/main/groovy/com/jessebrault/ssg/SsgBuild.groovy new file mode 100644 index 0000000..8a7fd2d --- /dev/null +++ b/cli/src/main/groovy/com/jessebrault/ssg/SsgBuild.groovy @@ -0,0 +1,21 @@ +package com.jessebrault.ssg + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import picocli.CommandLine + +@CommandLine.Command( + name = 'build', + mixinStandardHelpOptions = true, + description = 'Builds the project.' +) +class SsgBuild extends AbstractBuildCommand { + + private static final Logger logger = LogManager.getLogger(SsgBuild) + + @Override + Integer doSubCommand() { + this.doBuild() + } + +} diff --git a/cli/src/main/groovy/com/jessebrault/ssg/SsgInit.groovy b/cli/src/main/groovy/com/jessebrault/ssg/SsgInit.groovy new file mode 100644 index 0000000..731ea6e --- /dev/null +++ b/cli/src/main/groovy/com/jessebrault/ssg/SsgInit.groovy @@ -0,0 +1,54 @@ +package com.jessebrault.ssg + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import picocli.CommandLine + +@CommandLine.Command( + name = 'init', + mixinStandardHelpOptions = true, + description = 'Generates a blank project, optionally with some basic files.' +) +class SsgInit extends AbstractSubCommand { + + private static final Logger logger = LogManager.getLogger(SsgInit) + + @CommandLine.Option(names = ['-s', '--skeleton'], description = 'Include some basic files in the generated project.') + boolean withSkeletonFiles + + @Override + Integer doSubCommand() { + logger.traceEntry() + new FileTreeBuilder().with { + // Generate dirs + dir('texts') { + if (this.withSkeletonFiles) { + file('hello.md', this.getClass().getResource('/hello.md').text) + } + } + dir('templates') { + if (this.withSkeletonFiles) { + file('hello.gsp', this.getClass().getResource('/hello.gsp').text) + } + } + dir('parts') { + if (this.withSkeletonFiles) { + file('head.gsp', this.getClass().getResource('/head.gsp').text) + } + } + dir('specialPages') { + if (this.withSkeletonFiles) { + file('specialPage.gsp', this.getClass().getResource('/specialPage.gsp').text) + } + } + + // Generate ssgBuilds.groovy + if (this.withSkeletonFiles) { + file('ssgBuilds.groovy', this.getClass().getResource('/ssgBuilds.groovy').text) + } else { + file('ssgBuilds.groovy', this.getClass().getResource('/ssgBuildsBasic.groovy').text) + } + } + logger.traceExit(0) + } +} diff --git a/cli/src/main/groovy/com/jessebrault/ssg/SsgWatch.groovy b/cli/src/main/groovy/com/jessebrault/ssg/SsgWatch.groovy new file mode 100644 index 0000000..21942be --- /dev/null +++ b/cli/src/main/groovy/com/jessebrault/ssg/SsgWatch.groovy @@ -0,0 +1,116 @@ +package com.jessebrault.ssg + +import com.jessebrault.ssg.provider.WithWatchableDir +import groovy.io.FileType +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import picocli.CommandLine + +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds +import java.nio.file.WatchEvent +import java.nio.file.WatchKey + +@CommandLine.Command( + name = 'watch', + mixinStandardHelpOptions = true, + description = 'Run in watch mode, rebuilding the project whenever files are created/updated/deleted.' +) +class SsgWatch extends AbstractBuildCommand { + + private static final Logger logger = LogManager.getLogger(SsgWatch) + + @Override + Integer doSubCommand() { + logger.traceEntry() + + // Setup watchService and watchKeys + def watchService = FileSystems.getDefault().newWatchService() + Map watchKeys = [:] + + // Our Closure to register a directory path + def registerPath = { Path path -> + if (!Files.isDirectory(path)) { + throw new IllegalArgumentException('path must be a directory, given: ' + path) + } + logger.debug('registering dir with path: {}', path) + def watchKey = path.register( + watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, + StandardWatchEventKinds.ENTRY_MODIFY + ) + watchKeys[watchKey] = path + logger.debug('watchKeys: {}', watchKeys) + } + + // Get all base watchableDirs + Collection watchableProviders = [] + this.builds.each { + it.config.textProviders.each { + if (it instanceof WithWatchableDir) { + watchableProviders << it + } + } + it.config.templatesProviders.each { + if (it instanceof WithWatchableDir) { + watchableProviders << it + } + } + it.config.partsProviders.each { + if (it instanceof WithWatchableDir) { + watchableProviders << it + } + } + it.config.specialPagesProviders.each { + if (it instanceof WithWatchableDir) { + watchableProviders << it + } + } + } + // register them and their child directories using the Closure above + watchableProviders.each { + def baseDirFile = it.watchableDir + registerPath(baseDirFile.toPath()) + baseDirFile.eachFile(FileType.DIRECTORIES) { + registerPath(it.toPath()) + } + } + + //noinspection GroovyInfiniteLoopStatement + while (true) { + def watchKey = watchService.take() + def path = watchKeys[watchKey] + if (path == null) { + logger.warn('unexpected watchKey: {}', watchKey) + } else { + watchKey.pollEvents().each { + assert it instanceof WatchEvent + def childName = it.context() + def childPath = path.resolve(childName) + if (it.kind() == StandardWatchEventKinds.ENTRY_CREATE && Files.isDirectory(childPath)) { + registerPath(childPath) + } else if (Files.isRegularFile(childPath)) { + logger.debug('detected {} for regularFile with path {}', it.kind(), childPath) + def t = new Thread({ + this.doBuild() + }) + t.setName('workerThread') + t.start() + } + } + } + def valid = watchKey.reset() + if (!valid) { + def removedPath = watchKeys.remove(watchKey) + logger.debug('removed path: {}', removedPath) + } + } + + //noinspection GroovyUnreachableStatement + logger.traceExit(0) + } + +} diff --git a/cli/src/main/groovy/com/jessebrault/ssg/StaticSiteGeneratorCli.groovy b/cli/src/main/groovy/com/jessebrault/ssg/StaticSiteGeneratorCli.groovy index af42ba2..75e8f32 100644 --- a/cli/src/main/groovy/com/jessebrault/ssg/StaticSiteGeneratorCli.groovy +++ b/cli/src/main/groovy/com/jessebrault/ssg/StaticSiteGeneratorCli.groovy @@ -1,45 +1,15 @@ package com.jessebrault.ssg -import com.jessebrault.ssg.buildscript.GroovyBuildScriptRunner -import com.jessebrault.ssg.part.GspPartRenderer -import com.jessebrault.ssg.part.PartFilePartsProvider -import com.jessebrault.ssg.part.PartType -import com.jessebrault.ssg.provider.WithWatchableDir -import com.jessebrault.ssg.specialpage.GspSpecialPageRenderer -import com.jessebrault.ssg.specialpage.SpecialPageFileSpecialPagesProvider -import com.jessebrault.ssg.specialpage.SpecialPageType -import com.jessebrault.ssg.template.GspTemplateRenderer -import com.jessebrault.ssg.template.TemplateFileTemplatesProvider -import com.jessebrault.ssg.template.TemplateType -import com.jessebrault.ssg.text.MarkdownExcerptGetter -import com.jessebrault.ssg.text.MarkdownFrontMatterGetter -import com.jessebrault.ssg.text.MarkdownTextRenderer -import com.jessebrault.ssg.text.TextFileTextsProvider -import com.jessebrault.ssg.text.TextType -import groovy.io.FileType -import org.apache.logging.log4j.Level -import org.apache.logging.log4j.LogManager -import org.apache.logging.log4j.Logger -import org.apache.logging.log4j.core.LoggerContext import picocli.CommandLine -import java.nio.file.FileSystems -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardWatchEventKinds -import java.nio.file.WatchEvent -import java.nio.file.WatchKey -import java.util.concurrent.Callable - @CommandLine.Command( name = 'ssg', mixinStandardHelpOptions = true, version = '0.0.1-SNAPSHOT', - description = 'Generates a set of html files from a given configuration.' + description = 'Generates a set of html files from a given configuration.', + subcommands = [SsgInit, SsgBuild, SsgWatch] ) -class StaticSiteGeneratorCli implements Callable { - - private static final Logger logger = LogManager.getLogger(StaticSiteGeneratorCli) +class StaticSiteGeneratorCli { static void main(String[] args) { System.exit(new CommandLine(StaticSiteGeneratorCli).execute(args)) @@ -61,187 +31,4 @@ class StaticSiteGeneratorCli implements Callable { @CommandLine.ArgGroup(exclusive = true, heading = 'Log Level') LogLevel logLevel - @CommandLine.Option(names = ['-w', '--watch'], description = 'Run in watch mode.') - boolean watch - - @Override - Integer call() { - logger.traceEntry() - - // Setup Loggers - def context = (LoggerContext) LogManager.getContext(false) - def configuration = context.getConfiguration() - def rootLoggerConfig = configuration.getRootLogger() - - if (this.logLevel?.info) { - rootLoggerConfig.level = Level.INFO - } else if (this.logLevel?.debug) { - rootLoggerConfig.level = Level.DEBUG - } else if (this.logLevel?.trace) { - rootLoggerConfig.level = Level.TRACE - } else { - rootLoggerConfig.level = Level.WARN - } - - context.updateLoggers() - - // Configure - def markdownText = new TextType(['.md'], new MarkdownTextRenderer(), new MarkdownFrontMatterGetter(), new MarkdownExcerptGetter()) - def gspTemplate = new TemplateType(['.gsp'], new GspTemplateRenderer()) - def gspPart = new PartType(['.gsp'], new GspPartRenderer()) - def gspSpecialPage = new SpecialPageType(['.gsp'], new GspSpecialPageRenderer()) - - def defaultTextsProvider = new TextFileTextsProvider([markdownText], new File('texts')) - def defaultTemplatesProvider = new TemplateFileTemplatesProvider([gspTemplate], new File('templates')) - def defaultPartsProvider = new PartFilePartsProvider([gspPart], new File('parts')) - def defaultSpecialPagesProvider = new SpecialPageFileSpecialPagesProvider([gspSpecialPage], new File('specialPages')) - - def defaultConfig = new Config( - textProviders: [defaultTextsProvider], - templatesProviders: [defaultTemplatesProvider], - partsProviders: [defaultPartsProvider], - specialPagesProviders: [defaultSpecialPagesProvider] - ) - def defaultGlobals = [:] - - Collection builds = [] - - // Run build script, if applicable - if (new File('ssgBuilds.groovy').exists()) { - logger.info('found buildScript: ssgBuilds.groovy') - def buildScriptRunner = new GroovyBuildScriptRunner() - builds.addAll(buildScriptRunner.runBuildScript('ssgBuilds.groovy', defaultConfig, defaultGlobals)) - logger.debug('after running ssgBuilds.groovy, builds: {}', builds) - } - - if (builds.empty) { - // Add default build - builds << new Build('default', defaultConfig, defaultGlobals, new File('build')) - } - - // Get ssg object - def ssg = new SimpleStaticSiteGenerator() - - if (this.watch) { - generate(builds, ssg) - watch(builds, ssg) - } else { - generate(builds, ssg) - } - } - - private static Integer generate(Collection builds, StaticSiteGenerator ssg) { - logger.traceEntry('builds: {}, ssg: {}', builds, ssg) - - def hadDiagnostics = false - // Do each build - builds.each { - def result = ssg.generate(it) - if (result.v1.size() > 0) { - hadDiagnostics = true - result.v1.each { - logger.error(it.message) - } - } else { - result.v2.each { GeneratedPage generatedPage -> - def target = new File(it.outDir, generatedPage.path + '.html') - target.createParentDirectories() - target.write(generatedPage.html) - } - } - } - - logger.traceExit(hadDiagnostics ? 1 : 0) - } - - private static Integer watch(Collection builds, StaticSiteGenerator ssg) { - logger.traceEntry('builds: {}, ssg: {}', builds, ssg) - - // Setup watchService and watchKeys - def watchService = FileSystems.getDefault().newWatchService() - Map watchKeys = [:] - - // Our Closure to register a directory path - def registerPath = { Path path -> - if (!Files.isDirectory(path)) { - throw new IllegalArgumentException('path must be a directory, given: ' + path) - } - logger.debug('registering dir with path: {}', path) - def watchKey = path.register( - watchService, - StandardWatchEventKinds.ENTRY_CREATE, - StandardWatchEventKinds.ENTRY_DELETE, - StandardWatchEventKinds.ENTRY_MODIFY - ) - watchKeys[watchKey] = path - logger.debug('watchKeys: {}', watchKeys) - } - - // Get all base watchableDirs - Collection watchableProviders = [] - builds.each { - it.config.textProviders.each { - if (it instanceof WithWatchableDir) { - watchableProviders << it - } - } - it.config.templatesProviders.each { - if (it instanceof WithWatchableDir) { - watchableProviders << it - } - } - it.config.partsProviders.each { - if (it instanceof WithWatchableDir) { - watchableProviders << it - } - } - it.config.specialPagesProviders.each { - if (it instanceof WithWatchableDir) { - watchableProviders << it - } - } - } - // register them and their child directories using the Closure above - watchableProviders.each { - def baseDirFile = it.watchableDir - registerPath(baseDirFile.toPath()) - baseDirFile.eachFile(FileType.DIRECTORIES) { - registerPath(it.toPath()) - } - } - - //noinspection GroovyInfiniteLoopStatement - while (true) { - def watchKey = watchService.take() - def path = watchKeys[watchKey] - if (path == null) { - logger.warn('unexpected watchKey: {}', watchKey) - } else { - watchKey.pollEvents().each { - assert it instanceof WatchEvent - def childName = it.context() - def childPath = path.resolve(childName) - if (it.kind() == StandardWatchEventKinds.ENTRY_CREATE && Files.isDirectory(childPath)) { - registerPath(childPath) - } else if (Files.isRegularFile(childPath)) { - logger.debug('detected {} for regularFile with path {}', it.kind(), childPath) - def t = new Thread({ - generate(builds, ssg) - }) - t.setName('workerThread') - t.start() - } - } - } - def valid = watchKey.reset() - if (!valid) { - def removedPath = watchKeys.remove(watchKey) - logger.debug('removed path: {}', removedPath) - } - } - - //noinspection GroovyUnreachableStatement - logger.traceExit(0) - } - } diff --git a/cli/src/main/resources/head.gsp b/cli/src/main/resources/head.gsp new file mode 100644 index 0000000..df47909 --- /dev/null +++ b/cli/src/main/resources/head.gsp @@ -0,0 +1,3 @@ + + <%= binding.title %> + \ No newline at end of file diff --git a/cli/src/main/resources/hello.gsp b/cli/src/main/resources/hello.gsp new file mode 100644 index 0000000..58997a9 --- /dev/null +++ b/cli/src/main/resources/hello.gsp @@ -0,0 +1,10 @@ + + <% + out << parts['head.gsp'].render([ + title: "${ globals.siteTitle }: ${ frontMatter.title }" + ]) + %> + + <%= text %> + + \ No newline at end of file diff --git a/cli/src/main/resources/hello.md b/cli/src/main/resources/hello.md new file mode 100644 index 0000000..62f97d8 --- /dev/null +++ b/cli/src/main/resources/hello.md @@ -0,0 +1,5 @@ +--- +template: hello.gsp +title: Greeting +--- +# Hello, World! \ No newline at end of file diff --git a/cli/src/main/resources/specialPage.gsp b/cli/src/main/resources/specialPage.gsp new file mode 100644 index 0000000..71a3adb --- /dev/null +++ b/cli/src/main/resources/specialPage.gsp @@ -0,0 +1,8 @@ + + + ${ globals.siteTitle }: Special Page + + + <%= texts.find { it.path == 'hello' }.render() %> + + \ No newline at end of file diff --git a/cli/src/main/resources/ssgBuilds.groovy b/cli/src/main/resources/ssgBuilds.groovy new file mode 100644 index 0000000..6113e08 --- /dev/null +++ b/cli/src/main/resources/ssgBuilds.groovy @@ -0,0 +1,14 @@ +// This file was auto-generated by the ssg init command. + +build { + name = 'My Simple Build' + outDir = new File('mySimpleBuild') + + config { + // Config options here + } + + globals { + siteTitle = 'My Simple Site' + } +} \ No newline at end of file diff --git a/cli/src/main/resources/ssgBuildsBasic.groovy b/cli/src/main/resources/ssgBuildsBasic.groovy new file mode 100644 index 0000000..f859f2f --- /dev/null +++ b/cli/src/main/resources/ssgBuildsBasic.groovy @@ -0,0 +1 @@ +// This file was auto-generated by the ssg init command. \ No newline at end of file