aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Duncan <pabs@pablotron.org>2016-07-18 21:16:59 -0400
committerPaul Duncan <pabs@pablotron.org>2016-07-18 21:16:59 -0400
commitf442f7a57b3b6b988f1d914b4ef5c53993b1fa90 (patch)
tree6941249bada8065816b96a55fcee115416e5d86f
parenta8b773addc615f85c5e6fbb1835581c25c75504e (diff)
downloadguff-f442f7a57b3b6b988f1d914b4ef5c53993b1fa90.tar.bz2
guff-f442f7a57b3b6b988f1d914b4ef5c53993b1fa90.zip
add theme packer
-rw-r--r--data/themes/sample-theme/files/css/style.css1
-rw-r--r--data/themes/sample-theme/files/js/script.js1
-rw-r--r--data/themes/sample-theme/guff-manifest.json23
-rw-r--r--data/themes/sample-theme/templates/blog-list-item.html9
-rw-r--r--shard.yml2
-rw-r--r--src/guff.cr1
-rw-r--r--src/guff/cli.cr25
-rw-r--r--src/guff/config.cr71
-rw-r--r--src/guff/theme/assets.cr15
-rw-r--r--src/guff/theme/file-info.cr18
-rw-r--r--src/guff/theme/source-manifest.cr135
11 files changed, 287 insertions, 14 deletions
diff --git a/data/themes/sample-theme/files/css/style.css b/data/themes/sample-theme/files/css/style.css
new file mode 100644
index 0000000..913c182
--- /dev/null
+++ b/data/themes/sample-theme/files/css/style.css
@@ -0,0 +1 @@
+/* sample style */
diff --git a/data/themes/sample-theme/files/js/script.js b/data/themes/sample-theme/files/js/script.js
new file mode 100644
index 0000000..aebb122
--- /dev/null
+++ b/data/themes/sample-theme/files/js/script.js
@@ -0,0 +1 @@
+/* sample script */
diff --git a/data/themes/sample-theme/guff-manifest.json b/data/themes/sample-theme/guff-manifest.json
new file mode 100644
index 0000000..407ac40
--- /dev/null
+++ b/data/themes/sample-theme/guff-manifest.json
@@ -0,0 +1,23 @@
+{
+ "format": 1,
+ "metadata": {
+ "name": "Sample Theme",
+ "version": "1.0",
+ "date": "2016-07-18"
+ },
+
+ "files": [
+ "css/style.css",
+ "js/script.js"
+ ],
+
+ "assets": {
+ "scripts": [
+ "js/script.js"
+ ],
+
+ "styles": [
+ "css/style.css"
+ ]
+ }
+}
diff --git a/data/themes/sample-theme/templates/blog-list-item.html b/data/themes/sample-theme/templates/blog-list-item.html
new file mode 100644
index 0000000..7e9dddf
--- /dev/null
+++ b/data/themes/sample-theme/templates/blog-list-item.html
@@ -0,0 +1,9 @@
+<div id='post-%{post_id}' class='post'>
+ <div class='title'>%{name|h}</div>
+ <div class='sub-title'>
+ By <span class='created-by'>%{user_name|h}</span>
+ at <span class='posted-at'>%{created_at|h}</span>
+ </div>
+
+ <div class='body'>%{body}</div>
+</div>
diff --git a/shard.yml b/shard.yml
index e5c3b72..e9b7cd0 100644
--- a/shard.yml
+++ b/shard.yml
@@ -9,3 +9,5 @@ license: MIT
dependencies:
sqlite3:
github: manastech/crystal-sqlite3
+ libzip-crystal:
+ github: pablotron/libzip-crystal
diff --git a/src/guff.cr b/src/guff.cr
index cfd020c..f0573a3 100644
--- a/src/guff.cr
+++ b/src/guff.cr
@@ -4,6 +4,7 @@ require "json"
require "yaml"
require "secure_random"
require "sqlite3"
+require "libzip-crystal/zip"
module Guff
# TODO: think about versioning a bit (semantic, datestamp, etc)
diff --git a/src/guff/cli.cr b/src/guff/cli.cr
index c3fb4d6..15ff5a4 100644
--- a/src/guff/cli.cr
+++ b/src/guff/cli.cr
@@ -103,6 +103,21 @@ module Guff::CLI
end
end
end
+
+ class ThemeListAction < Action
+ def run
+ puts "TODO: not implemented"
+ end
+ end
+
+ class ThemePackAction < Action
+ def run
+ Theme::SourceManifest.save(
+ src_dir: @config.theme_src_dir.not_nil!,
+ dst_path: @config.theme_dst_path.not_nil!,
+ )
+ end
+ end
end
def self.run(app : String, args : Array(String))
@@ -119,6 +134,16 @@ module Guff::CLI
Actions::InitAction.run(config)
when "run"
Actions::RunAction.run(config)
+ when "theme"
+ case config.theme_action
+ when "list"
+ Actions::ThemeListAction.run(config)
+ when "pack"
+ Actions::ThemePackAction.run(config)
+ else
+ # never reached
+ raise "unknown theme action: #{config.theme_action}"
+ end
when "help"
# do nothing
else
diff --git a/src/guff/config.cr b/src/guff/config.cr
index 534e796..205c65d 100644
--- a/src/guff/config.cr
+++ b/src/guff/config.cr
@@ -1,27 +1,36 @@
require "option_parser"
class Guff::Config
- property :mode, :env, :host, :port, :data_dir, :system_dir
+ property :mode, :env, :host, :port, :data_dir, :system_dir,
+ :theme_action, :theme_src_dir, :theme_dst_path
DEFAULTS = {
- mode: "help",
- env: "production",
- host: "127.0.0.1",
- port: "8989",
- data_dir: "./data",
- system_dir: "/usr/local/share/guff",
+ mode: "help",
+ env: "production",
+ host: "127.0.0.1",
+ port: "8989",
+ data_dir: "./data",
+ system_dir: "/usr/local/share/guff",
+ theme_action: "list"
}
+ @theme_action : String
+ @theme_src_dir : String?
+ @theme_dst_path : String?
+
def initialize
- @mode = DEFAULTS[:mode] as String
- @env = (ENV["GUFF_ENVIRONMENT"]? || DEFAULTS[:env]) as String
- @host = (ENV["GUFF_HOST"]? || DEFAULTS[:host]) as String
- @port = (ENV["GUFF_PORT"]? || DEFAULTS[:port]) as String
- @data_dir = (ENV["GUFF_DATA_DIR"]? || DEFAULTS[:data_dir]) as String
- @system_dir = (ENV["GUFF_SYSTEM_DIR"]? || DEFAULTS[:system_dir]) as String
+ @mode = DEFAULTS[:mode] as String
+ @env = (ENV["GUFF_ENVIRONMENT"]? || DEFAULTS[:env]) as String
+ @host = (ENV["GUFF_HOST"]? || DEFAULTS[:host]) as String
+ @port = (ENV["GUFF_PORT"]? || DEFAULTS[:port]) as String
+ @data_dir = (ENV["GUFF_DATA_DIR"]? || DEFAULTS[:data_dir]) as String
+ @system_dir = (ENV["GUFF_SYSTEM_DIR"]? || DEFAULTS[:system_dir]) as String
+ @theme_action = DEFAULTS[:theme_action]
end
- VALID_MODES = %w{init run help}
+ VALID_MODES = %w{init run help theme}
+
+ VALID_THEME_ACTIONS = %w{list pack}
def mode=(mode : String)
raise "unknown mode: \"#{mode}\"" unless VALID_MODES.includes?(mode)
@@ -120,12 +129,46 @@ class Guff::Config
p.on("-h", "--help", "Print usage.") do
r.mode = "help"
end
+
+ p.separator
+ p.separator("
+Examples:
+ # init new data directory
+ guff init some-app
+
+ # run using directory some-app
+ guff run -D some-app
+
+ # pack theme in directory /path/to/some-theme as ./some-theme.zip
+ guff theme pack /path/to/some-theme
+
+ # pack theme in directory /path/to/some-theme as /other/path/output.zip
+ guff theme pack /path/to/some-theme /other/path/output.zip
+ ")
end
case r.mode
when "init"
# shortcut for -D parameter
r.data_dir = args.shift if args.size > 0
+ when "theme"
+ # get theme action
+ r.theme_action = args.shift if args.size > 0
+ unless VALID_THEME_ACTIONS.includes?(r.theme_action)
+ raise "unknown theme action: \"#{r.theme_action}\""
+ end
+
+ case r.theme_action
+ when "pack"
+ raise "missing theme directory" unless args.size > 0
+ r.theme_src_dir = args.shift
+
+ if args.size > 0
+ r.theme_dst_path = args.shift
+ else
+ r.theme_dst_path = File.basename(r.theme_src_dir.not_nil!) + ".zip"
+ end
+ end
when "help"
# print help
puts p
diff --git a/src/guff/theme/assets.cr b/src/guff/theme/assets.cr
new file mode 100644
index 0000000..5f15106
--- /dev/null
+++ b/src/guff/theme/assets.cr
@@ -0,0 +1,15 @@
+class Guff::Theme::Assets
+ getter :scripts, :styles
+
+ JSON.mapping(
+ scripts: Array(String),
+ styles: Array(String),
+ )
+
+ def to_json(io)
+ {
+ scripts: @scripts,
+ styles: @styles,
+ }.to_json(io)
+ end
+end
diff --git a/src/guff/theme/file-info.cr b/src/guff/theme/file-info.cr
new file mode 100644
index 0000000..914be75
--- /dev/null
+++ b/src/guff/theme/file-info.cr
@@ -0,0 +1,18 @@
+class Guff::Theme::FileInfo
+ getter :size, :hash
+
+ def initialize(abs_path : String, @path : String)
+ @size = File.size(abs_path)
+
+ d = OpenSSL::Digest.new("SHA1")
+ @hash = d.file(abs_path).hexdigest as String
+ end
+
+ def to_json(io)
+ {
+ name: @path,
+ size: @size,
+ hash: @hash,
+ }.to_json(io)
+ end
+end
diff --git a/src/guff/theme/source-manifest.cr b/src/guff/theme/source-manifest.cr
new file mode 100644
index 0000000..5dc1065
--- /dev/null
+++ b/src/guff/theme/source-manifest.cr
@@ -0,0 +1,135 @@
+class Guff::Theme::SourceManifest
+ getter :format, :metadata, :assets, :files
+
+ JSON.mapping(
+ format: Int32,
+ metadata: Hash(String, String),
+ assets: Assets,
+ files: Array(String),
+ )
+
+ REQUIRED_METADATA_FIELDS = {
+ "name" => /\S+/,
+ "version" => /\S+/,
+ "date" => /^\d{4}-\d{2}-\d{2}$/,
+ }
+
+ def self.save(src_dir : String, dst_path : String)
+ load(src_dir).save(dst_path, src_dir)
+ end
+
+ def self.load(src_dir : String) : SourceManifest
+ r = from_json(File.read(File.join(src_dir, "guff-manifest.json")))
+
+ # check format version
+ unless r.format == 1
+ raise "unknown theme format: #{r.format}"
+ end
+
+ # check for required manifest files
+ REQUIRED_METADATA_FIELDS.each do |key, re|
+ unless r.metadata[key]?
+ # missing field
+ raise "missing required metadata field: #{key}"
+ end
+
+ unless r.metadata[key].match(re)
+ # invalid field format
+ raise "metadata field format mismatch: #{key}"
+ end
+ end
+
+ # build path to files dir
+ base_dir = File.join(src_dir, "files")
+
+ # check manifest for missing files
+ missing_files = r.files.select do |path|
+ !File.exists?(File.join(base_dir, path))
+ end
+
+ # raise error if there are missing files
+ if missing_files.size > 0
+ raise "Missing files: #{missing_files.join(", ")}"
+ end
+
+ # build path lut
+ lut = r.files.reduce({} of String => Bool) do |rl, path|
+ rl[path] = true
+ rl
+ end
+
+ # check assets
+ {
+ scripts: r.assets.scripts,
+ styles: r.assets.styles,
+ }.each do |type, paths|
+ # find assets specified in manifest but not in
+ missing = paths.select { |path| !lut[path]? }
+
+ if missing.size > 0
+ raise "Missing #{type} in manifest: #{missing.join(", ")}"
+ end
+ end
+
+ # check files directory for entries missing from manifest
+ Dir.glob(File.join(base_dir, "**")).each do |path|
+ unless File.directory?(path)
+ # build relative path
+ rel_path = path.gsub(base_dir, "")
+ raise "File missing from manifest: #{path}" unless lut[rel_path]?
+ end
+ end
+
+ # return result
+ r
+ end
+
+ def to_json(io : IO, src_dir : String)
+ # build directory paths
+ files_dir = File.join(src_dir, "files")
+ templates_dir = File.join(src_dir, "templates")
+
+ # return output json
+ {
+ # format version
+ format: 1,
+
+ # theme metadata
+ metadata: @metadata,
+
+ # theme files
+ files: @files.map { |path|
+ FileInfo.new(File.join(files_dir, path), path)
+ },
+
+ # theme templates
+ templates: Dir.glob(File.join(templates_dir, "**")).select { |path|
+ !File.directory?(path)
+ }.reduce({} of String => String) do |r, path|
+ r[path.gsub(templates_dir + "/", "")] = File.read(path)
+ r
+ end,
+
+ # theme assets (css and js files)
+ assets: @assets,
+ }.to_json(io)
+ end
+
+ def save(dst_path : String, src_dir : String)
+ Zip::Archive.create(dst_path) do |zip|
+ # add manifest
+ zip.add("guff-manifest.json", String.build do |io|
+ to_json(io, src_dir)
+ end)
+
+ # add files
+ @files.each do |path|
+ # build path
+ dst_path = File.join("files", path)
+
+ # add file to archive
+ zip.add_file(dst_path, File.join(src_dir, dst_path))
+ end
+ end
+ end
+end