statique 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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