statique 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Statique
4
+ class CLI
5
+ class Server
6
+ class LoggerWrapper
7
+ def <<(msg)
8
+ method, uri, status, time, content_type = msg.chomp.split(":")
9
+ log(:info, "#{method} #{uri}", status: status.to_i, time: time.to_f, content_type:)
10
+ end
11
+
12
+ def log(type, msg, attrs = {})
13
+ Statique.ui.public_send(type, msg, attrs.merge(app: "webrick"))
14
+ end
15
+
16
+ %i[unknown fatal error warn info debug].each do |type|
17
+ define_method(type) do |msg|
18
+ log(type, msg)
19
+ end
20
+
21
+ define_method(:"#{type}?") do
22
+ Statique.ui.compare_levels(Statique.ui.class.config.level, type.to_sym) != :lt
23
+ end
24
+ end
25
+ end
26
+
27
+ def initialize(port: 3000)
28
+ @port = port
29
+ end
30
+
31
+ def run
32
+ Statique.ui.info "Starting server", port: @port
33
+
34
+ logger = LoggerWrapper.new
35
+
36
+ Rack::Handler::WEBrick.run(Statique::App.freeze,
37
+ Port: @port,
38
+ Host: "localhost",
39
+ Logger: logger,
40
+ AccessLog: [[logger, "%m:%U:%s:%T:%{Content-Type}o"]])
41
+ end
42
+
43
+ def stop
44
+ Statique.ui.info "Stopping server"
45
+ Rack::Handler::WEBrick.shutdown
46
+ end
47
+
48
+ class EventStream
49
+ def initialize(type)
50
+ @type = type
51
+ end
52
+
53
+ def puts(message)
54
+ Statique.ui.public_send(@type, message)
55
+ end
56
+
57
+ def write(message)
58
+ puts(message)
59
+ end
60
+
61
+ def sync
62
+ true
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ class Statique
6
+ class CLI < Thor
7
+ autoload :Server, "statique/cli/server"
8
+ autoload :Build, "statique/cli/build"
9
+ autoload :Init, "statique/cli/init"
10
+
11
+ package_name "Statique"
12
+
13
+ COMMAND_ALIASES = {
14
+ "version" => %w[-v --version]
15
+ }.freeze
16
+
17
+ def initialize(*args)
18
+ super
19
+ end
20
+
21
+ def self.exit_on_failure?
22
+ true
23
+ end
24
+
25
+ def self.aliases_for(command_name)
26
+ COMMAND_ALIASES.select { |k, _| k == command_name }.invert
27
+ end
28
+
29
+ desc "init", "Initialize new Statique website"
30
+ argument :name, optional: true, desc: "Name of the directory to initialise the Statique website in"
31
+ def init
32
+ Init.new(name).run
33
+ end
34
+
35
+ desc "server", "Start Statique server"
36
+ def server
37
+ Statique.mode.server!
38
+ Server.new.run
39
+ end
40
+
41
+ desc "build", "Build Statique site"
42
+ def build
43
+ Statique.mode.build!
44
+ Build.new(options.dup).run
45
+ end
46
+
47
+ desc "version", "Prints the statique's version information"
48
+ def version
49
+ Statique.ui.info "Statique v#{Statique::VERSION}"
50
+ end
51
+
52
+ map aliases_for("version")
53
+ end
54
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Statique
4
+ class Discover
5
+ attr_reader :documents, :collections, :files
6
+
7
+ SUPPORTED_EXTENSIONS = %w[
8
+ slim
9
+ md
10
+ builder
11
+ ].freeze
12
+
13
+ GLOB = "**/*.{#{SUPPORTED_EXTENSIONS.join(",")}}"
14
+
15
+ def initialize(root)
16
+ @root = root
17
+ @documents = []
18
+ @collections = Hashie::Mash.new { |hash, key| hash[key] = Set.new }
19
+
20
+ discover_files!
21
+ discover!
22
+
23
+ Statique.mode.build do
24
+ @files.freeze
25
+ @documents.freeze
26
+ @collections.freeze
27
+ end
28
+
29
+ watch_for_changes if Statique.mode.server?
30
+ end
31
+
32
+ private
33
+
34
+ def discover_files!
35
+ @files = @root.glob(GLOB)
36
+ ensure
37
+ Statique.ui.debug "Discovered files", count: @files.size
38
+ end
39
+
40
+ def discover!
41
+ @files.each do |file|
42
+ process(file)
43
+ end
44
+ end
45
+
46
+ def process(file)
47
+ document = Document.new(file)
48
+
49
+ documents << document
50
+
51
+ Array(document.meta.collection).each do |collection|
52
+ collections[collection] << document
53
+ end
54
+ end
55
+
56
+ def watch_for_changes
57
+ require "filewatcher"
58
+
59
+ @filewatcher = Filewatcher.new([Statique.paths.content, Statique.paths.layouts])
60
+ @filewatcher_thread = Thread.new(@filewatcher) do |watcher|
61
+ watcher.watch do |file, event|
62
+ Statique.ui.debug "File change event", file: file, event: event
63
+ discover_files!
64
+ path = Pathname.new(file)
65
+ remove_file!(path)
66
+ process(path) unless event == :deleted
67
+ end
68
+ end
69
+ Statique.ui.debug "Started file watcher", filewatcher: @filewatcher, thread: @filewatcher_thread
70
+
71
+ at_exit do
72
+ Statique.ui.debug "Closing file watcher", thread: @filewatcher_thread
73
+ @filewatcher.stop
74
+ @filewatcher.finalize
75
+ @filewatcher_thread.join
76
+ end
77
+ end
78
+
79
+ def remove_file!(path)
80
+ documents.delete_if { _1.file == path }
81
+ collections.each_value do |collection|
82
+ # TODO: See if set can index by some particular property to avoid looping
83
+ collection.delete_if { _1.file == path }
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hashie"
4
+
5
+ class Statique
6
+ class Document
7
+ attr_reader :file, :meta, :content
8
+
9
+ def initialize(file)
10
+ parsed = FrontMatterParser::Parser.parse_file(file)
11
+ @file, @meta, @content = file.freeze, Hashie::Mash.new(parsed.front_matter).freeze, parsed.content.freeze
12
+ end
13
+
14
+ def path
15
+ case basename
16
+ when "index.slim" then "/"
17
+ when "index.md" then "/"
18
+ else
19
+ "/#{meta.permalink || basename.delete_suffix(extname).delete_prefix(Statique.paths.content.to_s)}"
20
+ end.freeze
21
+ end
22
+
23
+ def view_name
24
+ basename.delete_suffix(extname).freeze
25
+ end
26
+
27
+ def engine_name
28
+ extname.delete_prefix(".").freeze
29
+ end
30
+
31
+ def layout_name
32
+ meta.fetch("layout") { "layout" }.freeze
33
+ end
34
+
35
+ def title
36
+ meta.title.freeze
37
+ end
38
+
39
+ def body
40
+ content
41
+ end
42
+
43
+ def pagination_pages
44
+ return unless Statique.discover.collections.key?(meta.paginates)
45
+ collection = Statique.discover.collections[meta.paginates]
46
+ (collection.size.to_f / Pagy::DEFAULT[:items]).ceil
47
+ end
48
+
49
+ def published_at
50
+ @published_at ||= file.ctime.freeze
51
+ end
52
+
53
+ private
54
+
55
+ def basename
56
+ @basename ||= file.basename.to_s.freeze
57
+ end
58
+
59
+ def extname
60
+ @extname ||= file.extname.to_s.freeze
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Statique
4
+ class Mode
5
+ MODE_BUILD = "build"
6
+ MODE_SERVER = "server"
7
+ MODES = [MODE_BUILD, MODE_SERVER].freeze
8
+
9
+ def initialize(mode = MODE_SERVER)
10
+ raise ArgumentError, "Mode can't be empty" if mode.nil? || mode.empty?
11
+ raise ArgumentError, "Mode must be one of #{MODES}" unless MODES.include?(mode.to_s)
12
+
13
+ @mode = mode == MODE_SERVER ? MODE_SERVER : MODE_BUILD
14
+ end
15
+
16
+ def server!
17
+ @mode = MODE_SERVER
18
+ end
19
+
20
+ def build!
21
+ @mode = MODE_BUILD
22
+ end
23
+
24
+ def server?
25
+ @mode == MODE_SERVER
26
+ end
27
+
28
+ def build?
29
+ @mode == MODE_BUILD
30
+ end
31
+
32
+ def server
33
+ yield if server?
34
+ end
35
+
36
+ def build
37
+ yield if build?
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Statique
4
+ class Paginator
5
+ attr_reader :documents
6
+
7
+ def initialize(pagy, documents, path)
8
+ @pagy, @documents, @path = pagy, documents, path
9
+ end
10
+
11
+ def page
12
+ @pagy.page
13
+ end
14
+
15
+ def total_pages
16
+ @pagy.pages
17
+ end
18
+
19
+ def total_documents
20
+ @pagy.count
21
+ end
22
+
23
+ def previous_page
24
+ @pagy.prev
25
+ end
26
+
27
+ def previous_page_path
28
+ page_path(@pagy.prev)
29
+ end
30
+
31
+ def next_page
32
+ @pagy.next
33
+ end
34
+
35
+ def next_page_path
36
+ page_path(@pagy.next)
37
+ end
38
+
39
+ def per_page
40
+ @pagy.items
41
+ end
42
+
43
+ private
44
+
45
+ def page_path(page)
46
+ return unless page
47
+
48
+ if page == 1
49
+ Statique.url(File.join(@path))
50
+ else
51
+ Statique.url(File.join(@path, "page", page.to_s))
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Statique
4
+ VERSION = "0.1.0"
5
+ end
data/lib/statique.rb ADDED
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "front_matter_parser"
4
+ require "hashie"
5
+ require "pathname"
6
+ require "pagy"
7
+ require "rack"
8
+ require "tty-logger"
9
+ require "dry-configurable"
10
+
11
+ ::FrontMatterParser::SyntaxParser::Builder = FrontMatterParser::SyntaxParser::MultiLineComment["=begin", "=end"]
12
+
13
+ $LOAD_PATH.unshift(File.expand_path("..", __FILE__))
14
+ require "zeitwerk"
15
+
16
+ loader = Zeitwerk::Loader.for_gem
17
+ loader.inflector.inflect(
18
+ "cli" => "CLI"
19
+ )
20
+ loader.setup
21
+
22
+ class Statique
23
+ extend Dry::Configurable
24
+
25
+ class Error < StandardError; end
26
+
27
+ setting :paths, reader: true do
28
+ setting :pwd, default: Pathname.pwd, constructor: -> { Pathname(_1) }
29
+ setting :public, default: Pathname.pwd.join("public"), constructor: -> { Statique.pwd.join(_1) }
30
+ setting :content, default: Pathname.pwd.join("content"), constructor: -> { Statique.pwd.join(_1) }
31
+ setting :layouts, default: Pathname.pwd.join("layouts"), constructor: -> { Statique.pwd.join(_1) }
32
+ setting :assets, default: Pathname.pwd.join("assets"), constructor: -> { Statique.pwd.join(_1) }
33
+ setting :destination, default: Pathname.pwd.join("dist"), constructor: -> { Statique.pwd.join(_1) }
34
+ end
35
+ setting :root_url, default: "/", reader: true
36
+
37
+ class << self
38
+ def discover
39
+ @discover ||= Discover.new(paths.content)
40
+ end
41
+
42
+ def mode
43
+ @mode ||= Mode.new
44
+ end
45
+
46
+ def pwd
47
+ @pwd ||= Pathname.pwd.freeze
48
+ end
49
+
50
+ def version
51
+ VERSION
52
+ end
53
+
54
+ def ui
55
+ @ui ||= TTY::Logger.new(output: $stdout) do |config|
56
+ config.level = :debug if ENV["DEBUG"] == "true"
57
+ end
58
+ end
59
+
60
+ def url(document_or_path)
61
+ File.join(root_url, document_or_path.is_a?(Document) ? document_or_path.path : document_or_path)
62
+ end
63
+
64
+ def build_queue
65
+ @build_queue ||= Queue.new
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,5 @@
1
+ ## Welcome
2
+
3
+ Hello from Statique!
4
+
5
+ Find me in `content/index.md`.
@@ -0,0 +1,13 @@
1
+ doctype html
2
+ html
3
+ head
4
+ meta charset="UTF-8"
5
+ meta name="generator" content="Statique #{Statique.version}"
6
+ title Statique Website
7
+ == assets(:css)
8
+ == assets(:js)
9
+ body
10
+ header
11
+ h1 Statique Website
12
+ main== yield
13
+ footer Made with Statique v#{Statique.version}
@@ -0,0 +1,16 @@
1
+ class Statique
2
+ class Mode
3
+ MODE_BUILD: "build"
4
+ MODE_SERVER: "server"
5
+ MODES: ["build", "server"]
6
+ @mode: "build" | "server"
7
+
8
+ def initialize: (?mode: "build" | "server") -> void
9
+ def server!: -> "server"
10
+ def build!: -> "build"
11
+ def server?: -> bool
12
+ def build?: -> bool
13
+ def server: -> nil
14
+ def build: -> nil
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ class Statique
2
+ VERSION: String
3
+ end
data/sig/statique.rbs ADDED
@@ -0,0 +1,4 @@
1
+ class Statique
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
data/statique.gemspec ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/statique/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "statique"
7
+ spec.version = Statique::VERSION
8
+ spec.authors = ["Piotr Usewicz"]
9
+ spec.email = ["piotr@layer22.com"]
10
+
11
+ spec.summary = "Static website generator"
12
+ spec.description = "Statique is a static website generator written in Ruby using Roda"
13
+ spec.homepage = "https://github.com/pusewicz/statique"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.6.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/pusewicz/statique"
19
+ spec.metadata["changelog_uri"] = "https://raw.githubusercontent.com/pusewicz/statique/v#{spec.version}/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Uncomment to register a new dependency of your gem
33
+ spec.add_dependency "builder", "~> 3.2.4"
34
+ spec.add_dependency "commonmarker", "~> 0.23.2"
35
+ spec.add_dependency "dry-configurable", "~> 0.14.0"
36
+ spec.add_dependency "filewatcher", "~> 1.1.1"
37
+ spec.add_dependency "front_matter_parser", "~> 1.0.1"
38
+ spec.add_dependency "hashie", "~> 5.0.0"
39
+ spec.add_dependency "memo_wise", "~> 1.6.0"
40
+ spec.add_dependency "pagy", "~> 5.9.3"
41
+ spec.add_dependency "rack-rewrite", "~> 1.5.1"
42
+ spec.add_dependency "roda", "~> 3.52"
43
+ spec.add_dependency "sassc", "~> 2.4.0"
44
+ spec.add_dependency "slim", "~> 4.1.0"
45
+ spec.add_dependency "thor", "~> 1.2.1"
46
+ spec.add_dependency "tilt", "~> 2.0.10"
47
+ spec.add_dependency "tty-logger", "~> 0.6.0"
48
+ spec.add_dependency "webrick", "~> 1.7.0"
49
+ spec.add_dependency "zeitwerk", "~> 2.5.4"
50
+
51
+ # For more information and examples about making a new gem, check out our
52
+ # guide at: https://bundler.io/guides/creating_gem.html
53
+ end