wytch 0.2.0 → 0.3.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/lib/wytch/builder.rb +27 -0
  4. data/lib/wytch/cli.rb +35 -2
  5. data/lib/wytch/content_loader.rb +24 -0
  6. data/lib/wytch/once.rb +20 -0
  7. data/lib/wytch/page.rb +76 -1
  8. data/lib/wytch/reload_coordinator.rb +19 -0
  9. data/lib/wytch/server.rb +28 -0
  10. data/lib/wytch/site.rb +49 -0
  11. data/lib/wytch/site_code_loader_middleware.rb +19 -0
  12. data/lib/wytch/templates/src/site/layout.rb.tt +1 -0
  13. data/lib/wytch/version.rb +2 -1
  14. data/lib/wytch.rb +29 -0
  15. data/website/.gitignore +4 -0
  16. data/website/Gemfile +8 -0
  17. data/website/Gemfile.lock +63 -0
  18. data/website/assets/Main.res +2 -0
  19. data/website/assets/main.css +1 -0
  20. data/website/bin/generate_api_docs +44 -0
  21. data/website/config.rb +6 -0
  22. data/website/content/api/wytch/builder.rb +6 -0
  23. data/website/content/api/wytch/cli.rb +6 -0
  24. data/website/content/api/wytch/contentloader.rb +6 -0
  25. data/website/content/api/wytch/error.rb +6 -0
  26. data/website/content/api/wytch/once.rb +6 -0
  27. data/website/content/api/wytch/page.rb +6 -0
  28. data/website/content/api/wytch/reloadcoordinator.rb +6 -0
  29. data/website/content/api/wytch/server.rb +6 -0
  30. data/website/content/api/wytch/site.rb +6 -0
  31. data/website/content/api/wytch/sitecodeloadermiddleware.rb +6 -0
  32. data/website/content/index.rb +6 -0
  33. data/website/content/sitemap.rb +4 -0
  34. data/website/package.json +19 -0
  35. data/website/public/robots.txt +2 -0
  36. data/website/rescript.json +18 -0
  37. data/website/src/wytch_site/api_class_view.rb +90 -0
  38. data/website/src/wytch_site/home_view.rb +33 -0
  39. data/website/src/wytch_site/layout.rb +34 -0
  40. data/website/src/wytch_site/page.rb +18 -0
  41. data/website/src/wytch_site/sitemap_helper.rb +17 -0
  42. data/website/src/wytch_site/sitemap_view.rb +27 -0
  43. data/website/vite.config.js +26 -0
  44. metadata +30 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 662c7632994e850e71fce70842f153b11d7e56f8877df6156094c070213a68a6
4
- data.tar.gz: 546b863eb417cd7ada91d725a1def5662aae4182e9c53762ab98758c574f103b
3
+ metadata.gz: 7aa017b3a2d6ed1b64cde7b4a73f312222d64a630a365ad71db68f4babc28ce5
4
+ data.tar.gz: fba837a626f3c4dfcc3525d03127ada6b62b30b367e31da4c5eef3721c7b264a
5
5
  SHA512:
6
- metadata.gz: 9a8a7bc87f19981aa54f33132f5cb5e9adf7799c380187f2b5db8a0756feab7f2c6e158eeaeeaef2d6bccb95188f8ff5e561e66b5bf4c8dda70df56859f8c137
7
- data.tar.gz: ac6aae6cf98f82c74dd1b11ba1d814d03cbbb9b9ab85c6c2f738fc300632ff99602c571e436554e58b11aa7d47fb94c99f9532e41eb77116ccd7fe37a9a56dca
6
+ metadata.gz: b70258ec6436a644dee4ecb637dc0bcad929428c7803d4272b7eef34b8bb0620b024c683a2a77f41e1235c05f5c1713b188a374bc7d050bb77a571bd924db7f0
7
+ data.tar.gz: 98a84928a3cf7bd5aff50e99225d6b944da43ca69a44c2bb94a10ee8bc5ba424365ca228378f2f2472757b25632f86b7b5bea0aaa458e075100e2b96f1f87985
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2025-12-7
4
+
5
+ - Add YARD documentation to all classes and methods
6
+ - Add UTF-8 charset meta tag to generated site layouts
7
+ - Fix index page building to `build/index.html` instead of `build/index/index.html`
8
+ - Update CLI output to reflect single-command build process
9
+
10
+ ## [0.2.0] - 2025-12-7
11
+
12
+ - Add Vite integration for asset pipeline (CSS, ReScript)
13
+ - Add blog scaffolding with posts, sitemap, and Atom feed
14
+ - Add Layout component for consistent page structure
15
+ - `wytch build` now automatically runs `npm run build` for assets
16
+ - Add static file serving from `public/` directory
17
+ - Add hot-reloading for site code and content during development
18
+ - Add documentation (README usage guide)
19
+
3
20
  ## [0.1.0] - 2025-10-29
4
21
 
5
22
  - Initial release
data/lib/wytch/builder.rb CHANGED
@@ -3,9 +3,29 @@
3
3
  require "fileutils"
4
4
 
5
5
  module Wytch
6
+ # Builds the static site by rendering all pages to the output directory.
7
+ #
8
+ # The Builder is responsible for the production build process:
9
+ # 1. Loading site configuration and content
10
+ # 2. Rendering each page to HTML
11
+ # 3. Copying static files from public/
12
+ # 4. Integrating Vite-built assets
13
+ #
14
+ # @example Building via CLI
15
+ # $ wytch build
16
+ #
17
+ # @example Building programmatically
18
+ # Wytch::Builder.new.build
6
19
  class Builder
20
+ # @return [String] the output directory for built files
7
21
  OUTPUT_DIR = "build"
8
22
 
23
+ # Builds the entire site.
24
+ #
25
+ # Sets RACK_ENV to "production", loads the site configuration,
26
+ # renders all pages to HTML files, and copies static assets.
27
+ #
28
+ # @return [void]
9
29
  def build
10
30
  ENV["RACK_ENV"] = "production"
11
31
  Site.load!
@@ -31,12 +51,19 @@ module Wytch
31
51
 
32
52
  private
33
53
 
54
+ # Copies files from public/ to the output directory.
55
+ #
56
+ # @return [void]
34
57
  def copy_public_files
35
58
  return unless Dir.exist?("public")
36
59
 
37
60
  FileUtils.cp_r "public/.", OUTPUT_DIR, verbose: true
38
61
  end
39
62
 
63
+ # Reports on Vite assets in the output directory.
64
+ # Vite builds directly to build/assets, so no copying is needed.
65
+ #
66
+ # @return [void]
40
67
  def copy_vite_assets
41
68
  vite_output = File.join(OUTPUT_DIR, "assets")
42
69
  return unless Dir.exist?(vite_output)
data/lib/wytch/cli.rb CHANGED
@@ -3,15 +3,36 @@
3
3
  require "thor"
4
4
 
5
5
  module Wytch
6
+ # Command-line interface for Wytch.
7
+ #
8
+ # Provides commands for creating, developing, and building Wytch sites.
9
+ #
10
+ # @example
11
+ # $ wytch new my-site # Create a new site
12
+ # $ wytch server # Start development server
13
+ # $ wytch build # Build for production
6
14
  class CLI < Thor
7
15
  include Thor::Actions
8
16
 
17
+ # Returns the path to template files used by the generator.
18
+ #
19
+ # @return [String] path to templates directory
9
20
  def self.source_root
10
21
  File.expand_path("templates", __dir__)
11
22
  end
12
23
 
13
24
  desc "new NAME", "Create a new Wytch site"
14
25
  method_option :local, type: :boolean, default: false, desc: "Use local Wytch gem for development"
26
+ # Creates a new Wytch site with the given name.
27
+ #
28
+ # Generates a complete site structure including:
29
+ # - Configuration files (Gemfile, config.rb, package.json, etc.)
30
+ # - Content directory with sample pages
31
+ # - Source directory with views and layouts
32
+ # - Asset pipeline setup (Vite, ReScript, Tailwind)
33
+ #
34
+ # @param name [String] the name/directory for the new site
35
+ # @return [void]
15
36
  def new(name)
16
37
  @local_wytch_path = File.expand_path("../..", __dir__) if options[:local]
17
38
 
@@ -68,18 +89,26 @@ module Wytch
68
89
  say " npm run dev # Start Vite dev server (Terminal 1)"
69
90
  say " wytch server # Start Wytch dev server (Terminal 2)"
70
91
  say "\nTo build for production:"
71
- say " npm run build # Build assets with Vite"
72
- say " wytch build # Build static site"
92
+ say " wytch build # Build assets and static site to build/"
73
93
  end
74
94
 
75
95
  desc "server", "Start a development server"
76
96
  method_option :port, type: :numeric, default: 6969, aliases: "-p", desc: "Port to run the server on"
77
97
  method_option :host, type: :string, default: "localhost", aliases: "-h", desc: "Host to bind the server to"
98
+ # Starts the development server with hot reloading.
99
+ #
100
+ # @return [void]
78
101
  def server
79
102
  Server.new(options).start
80
103
  end
81
104
 
82
105
  desc "build", "Build the static site"
106
+ # Builds the site for production.
107
+ #
108
+ # First runs `npm run build` to compile assets with Vite,
109
+ # then renders all pages to static HTML in the build/ directory.
110
+ #
111
+ # @return [void]
83
112
  def build
84
113
  system("npm run build") || abort("Asset build failed")
85
114
  Builder.new.build
@@ -87,6 +116,10 @@ module Wytch
87
116
 
88
117
  private
89
118
 
119
+ # Converts a snake_case name to PascalCase.
120
+ #
121
+ # @param name [String] the name to classify
122
+ # @return [String] the PascalCase version
90
123
  def classify(name)
91
124
  name.split("_").map(&:capitalize).join
92
125
  end
@@ -1,7 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wytch
4
+ # Discovers and loads content files into Page objects.
5
+ #
6
+ # The ContentLoader scans the content directory for Ruby files and
7
+ # instantiates Page objects for each one. It uses the configured
8
+ # page_class from the Site.
9
+ #
10
+ # @see Wytch::Site#content_dir
11
+ # @see Wytch::Site#page_class
4
12
  class ContentLoader
13
+ # Loads all content files and returns a hash of pages.
14
+ #
15
+ # Scans the content directory recursively for .rb files,
16
+ # creates a Page instance for each, and returns them keyed by path.
17
+ #
18
+ # @return [Hash{String => Page}] pages keyed by their URL path
5
19
  def load_content
6
20
  pages = {}
7
21
 
@@ -15,14 +29,24 @@ module Wytch
15
29
 
16
30
  private
17
31
 
32
+ # Creates a Page instance from a content file.
33
+ #
34
+ # @param file_path [String] path to the content file, relative to content_dir
35
+ # @return [Page] the loaded page
18
36
  def load_page(file_path)
19
37
  page_class.new(file_path:)
20
38
  end
21
39
 
40
+ # Returns the configured page class.
41
+ #
42
+ # @return [Class] the page class to instantiate
22
43
  def page_class
23
44
  Object.const_get(Wytch.site.page_class)
24
45
  end
25
46
 
47
+ # Returns the content directory path.
48
+ #
49
+ # @return [String] the content directory
26
50
  def content_dir
27
51
  Wytch.site.content_dir
28
52
  end
data/lib/wytch/once.rb CHANGED
@@ -1,12 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wytch
4
+ # A thread-safe utility for executing a block exactly once.
5
+ #
6
+ # Once ensures that a given block of code is executed only one time,
7
+ # even when called from multiple threads. After the first execution,
8
+ # subsequent calls are no-ops.
9
+ #
10
+ # @example
11
+ # setup = Once.new { puts "Initializing..." }
12
+ # setup.call # prints "Initializing..."
13
+ # setup.call # does nothing
14
+ # setup.call # does nothing
4
15
  class Once
16
+ # Creates a new Once instance with the given block.
17
+ #
18
+ # @yield the block to execute once
5
19
  def initialize(&block)
6
20
  @block = block
7
21
  @mutex = Mutex.new
8
22
  end
9
23
 
24
+ # Executes the block if it hasn't been executed yet.
25
+ #
26
+ # Thread-safe: if multiple threads call this simultaneously,
27
+ # only one will execute the block.
28
+ #
29
+ # @return [void]
10
30
  def call
11
31
  @mutex&.synchronize do
12
32
  return unless @mutex
data/lib/wytch/page.rb CHANGED
@@ -1,7 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wytch
4
+ # Represents a single page in a Wytch site.
5
+ #
6
+ # Pages are defined by Ruby content files in the content directory. Each content
7
+ # file is evaluated in the context of a Page instance, allowing it to set metadata
8
+ # and configure the view class.
9
+ #
10
+ # @example A typical content file (content/about.rb)
11
+ # view MySite::AboutView
12
+ # add MySite::PageHelpers
13
+ #
14
+ # @metadata[:title] = "About Us"
15
+ # @metadata[:description] = "Learn more about our company"
16
+ #
17
+ # @example Accessing page data in a view
18
+ # class MySite::AboutView < Phlex::HTML
19
+ # def initialize(page)
20
+ # @page = page
21
+ # end
22
+ #
23
+ # def view_template
24
+ # h1 { @page.metadata[:title] }
25
+ # end
26
+ # end
4
27
  class Page
28
+ # Creates a new page by loading and evaluating a content file.
29
+ #
30
+ # @param file_path [String] path to the content file, relative to the content directory
5
31
  def initialize(file_path:)
6
32
  @file_path = file_path
7
33
  @metadata = {}
@@ -12,12 +38,19 @@ module Wytch
12
38
  instance_eval File.read(source_path), source_path
13
39
  end
14
40
 
41
+ # @return [Hash] arbitrary metadata set by the content file
15
42
  attr_reader :metadata
16
43
 
44
+ # Renders the page using its configured view class.
45
+ #
46
+ # @return [String] the rendered HTML
17
47
  def render
18
48
  @view_class.new(self).call
19
49
  end
20
50
 
51
+ # Returns the URL path for this page.
52
+ #
53
+ # @return [String] the URL path (e.g., "/" for index, "/about" for about.rb)
21
54
  def path
22
55
  if virtual_path == "index"
23
56
  "/"
@@ -26,34 +59,76 @@ module Wytch
26
59
  end
27
60
  end
28
61
 
62
+ # Returns the virtual path derived from the file path.
63
+ #
64
+ # @return [String] the path without directory prefix or .rb suffix
29
65
  def virtual_path
30
66
  @file_path.delete_prefix("#{Wytch.site.content_dir}/").delete_suffix(".rb")
31
67
  end
32
68
 
69
+ # Returns the output file path for the built page.
70
+ #
71
+ # @return [String] the build path (e.g., "about/index.html")
33
72
  def build_path
34
- "#{virtual_path}/index.html"
73
+ if virtual_path == "index"
74
+ "index.html"
75
+ else
76
+ "#{virtual_path}/index.html"
77
+ end
35
78
  end
36
79
 
80
+ # Extends this page instance with a helper module.
81
+ # Called from content files to add helper methods.
82
+ #
83
+ # @param helper_module [Module] the module to extend this page with
84
+ # @return [void]
85
+ #
86
+ # @example
87
+ # add MySite::PostHelpers
37
88
  def add(helper_module)
38
89
  extend helper_module
39
90
  end
40
91
 
92
+ # Sets the view class for rendering this page.
93
+ # Called from content files to specify which Phlex view to use.
94
+ #
95
+ # @param view_class [Class] a Phlex view class that accepts the page in its initializer
96
+ # @return [void]
97
+ #
98
+ # @example
99
+ # view MySite::PostView
41
100
  def view(view_class)
42
101
  @view_class = view_class
43
102
  end
44
103
 
104
+ # Whether this page should be included in the sitemap.
105
+ # Override in subclasses to exclude pages.
106
+ #
107
+ # @return [Boolean] true by default
45
108
  def include_in_sitemap?
46
109
  true
47
110
  end
48
111
 
112
+ # The last modification date for sitemap generation.
113
+ # Override in subclasses to provide actual dates.
114
+ #
115
+ # @return [Date, Time, nil] the last modified date, or nil if unknown
49
116
  def last_modified
50
117
  nil
51
118
  end
52
119
 
120
+ # The change frequency hint for sitemap generation.
121
+ # Override in subclasses to provide hints like "daily", "weekly", etc.
122
+ #
123
+ # @return [String, nil] the change frequency, or nil if unknown
53
124
  def change_frequency
54
125
  nil
55
126
  end
56
127
 
128
+ # The priority hint for sitemap generation.
129
+ # Override in subclasses to provide a value between 0.0 and 1.0.
130
+ #
131
+ # @return [Float, nil] the priority, or nil if unknown
57
132
  def priority
58
133
  nil
59
134
  end
@@ -4,9 +4,21 @@ require "concurrent/atomic/read_write_lock"
4
4
  require "listen"
5
5
 
6
6
  module Wytch
7
+ # Coordinates hot reloading of site code and content during development.
8
+ #
9
+ # The ReloadCoordinator watches the site code and content directories for
10
+ # changes using the Listen gem. When files change, it marks the appropriate
11
+ # component as dirty and reloads it on the next request.
12
+ #
13
+ # It uses a read-write lock to ensure thread-safe reloading while allowing
14
+ # concurrent reads during page rendering.
15
+ #
16
+ # @see Wytch::SiteCodeLoaderMiddleware
7
17
  class ReloadCoordinator
18
+ # @return [Concurrent::ReadWriteLock] lock for coordinating reloads
8
19
  attr_reader :reload_lock
9
20
 
21
+ # Creates a new ReloadCoordinator and sets up file watchers.
10
22
  def initialize
11
23
  @reload_lock = Concurrent::ReadWriteLock.new
12
24
 
@@ -26,6 +38,13 @@ module Wytch
26
38
  end
27
39
  end
28
40
 
41
+ # Reloads site code and/or content if changes have been detected.
42
+ #
43
+ # Starts file listeners on first call. If site code has changed, reloads
44
+ # both site code (via Zeitwerk) and content. If only content has changed,
45
+ # reloads just the content.
46
+ #
47
+ # @return [void]
29
48
  def reload!
30
49
  @start_site_code_listener&.call
31
50
  @start_content_listener&.call
data/lib/wytch/server.rb CHANGED
@@ -3,13 +3,38 @@
3
3
  require "rack"
4
4
 
5
5
  module Wytch
6
+ # Development server for Wytch sites.
7
+ #
8
+ # The Server provides a local development environment with:
9
+ # - Hot reloading of site code and content
10
+ # - Static file serving from public/
11
+ # - Page rendering on each request
12
+ #
13
+ # @example Starting via CLI
14
+ # $ wytch server
15
+ # $ wytch server --port 3000 --host 0.0.0.0
16
+ #
17
+ # @example Starting programmatically
18
+ # Wytch::Server.new(port: 3000).start
6
19
  class Server
20
+ # @return [Hash] server options (port, host)
7
21
  attr_reader :options
8
22
 
23
+ # Creates a new server instance.
24
+ #
25
+ # @param options [Hash] server options
26
+ # @option options [Integer] :port the port to listen on (default: 6969)
27
+ # @option options [String] :host the host to bind to (default: "localhost")
9
28
  def initialize(options = {})
10
29
  @options = options
11
30
  end
12
31
 
32
+ # Starts the development server.
33
+ #
34
+ # Loads the site configuration, starts file watchers for hot reloading,
35
+ # and begins serving requests. This method blocks until the server is stopped.
36
+ #
37
+ # @return [void]
13
38
  def start
14
39
  ENV["RACK_ENV"] = "development"
15
40
  Site.load!
@@ -30,6 +55,9 @@ module Wytch
30
55
 
31
56
  private
32
57
 
58
+ # Builds the Rack application stack.
59
+ #
60
+ # @return [Rack::Builder] the configured Rack app
33
61
  def app
34
62
  base_app = lambda { |env|
35
63
  path = env["PATH_INFO"]
data/lib/wytch/site.rb CHANGED
@@ -1,7 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wytch
4
+ # Holds the configuration and state for a Wytch site.
5
+ #
6
+ # The Site class is the central configuration point for Wytch. It manages
7
+ # the content directory, site code loading, page class, and the collection
8
+ # of all pages.
9
+ #
10
+ # @example Configuring a site in config.rb
11
+ # Wytch.configure do |site|
12
+ # site.page_class = "MySite::Page"
13
+ # site.base_url = "https://example.com"
14
+ # site.content_dir = "pages" # default is "content"
15
+ # end
16
+ #
17
+ # @see Wytch.configure
4
18
  class Site
19
+ # Loads the site configuration from config.rb in the current directory.
20
+ #
21
+ # @return [void]
5
22
  def self.load!
6
23
  if File.exist?(config_file = File.join(Dir.pwd, "config.rb"))
7
24
  load config_file
@@ -10,6 +27,7 @@ module Wytch
10
27
  end
11
28
  end
12
29
 
30
+ # Creates a new Site with default configuration.
13
31
  def initialize
14
32
  @inflections = {}
15
33
  @content_dir = "content"
@@ -19,13 +37,38 @@ module Wytch
19
37
  @base_url = nil
20
38
  end
21
39
 
40
+ # @return [Hash] custom inflections for Zeitwerk autoloading
22
41
  attr_reader :inflections
42
+
43
+ # @!attribute content_dir
44
+ # @return [String] directory containing content files (default: "content")
45
+ # @!attribute site_code_path
46
+ # @return [String] directory containing site Ruby code (default: "src")
47
+ # @!attribute page_class
48
+ # @return [String] fully qualified class name for pages (default: "Wytch::Page")
49
+ # @!attribute pages
50
+ # @return [Hash{String => Page}] all loaded pages, keyed by path
51
+ # @!attribute base_url
52
+ # @return [String, nil] base URL for the site (used in sitemaps, feeds, etc.)
23
53
  attr_accessor :content_dir, :site_code_path, :page_class, :pages, :base_url
24
54
 
55
+ # Adds custom inflections for Zeitwerk autoloading.
56
+ #
57
+ # @param inflections_hash [Hash{String => String}] mapping of file names to class names
58
+ # @return [void]
59
+ #
60
+ # @example
61
+ # site.inflect("api" => "API", "html_parser" => "HTMLParser")
25
62
  def inflect(inflections_hash)
26
63
  @inflections.merge!(inflections_hash)
27
64
  end
28
65
 
66
+ # Returns the Zeitwerk loader for site code.
67
+ #
68
+ # The loader is configured to watch the site_code_path directory and
69
+ # supports hot reloading during development.
70
+ #
71
+ # @return [Zeitwerk::Loader] the configured loader
29
72
  def site_code_loader
30
73
  @site_code_loader ||= begin
31
74
  loader = Zeitwerk::Loader.new
@@ -37,10 +80,16 @@ module Wytch
37
80
  end
38
81
  end
39
82
 
83
+ # Returns the content loader instance.
84
+ #
85
+ # @return [ContentLoader] the loader for content files
40
86
  def content_loader
41
87
  @content_loader ||= ContentLoader.new
42
88
  end
43
89
 
90
+ # Loads all content files and populates the pages hash.
91
+ #
92
+ # @return [Hash{String => Page}] all loaded pages
44
93
  def load_content
45
94
  @pages = content_loader.load_content
46
95
  end
@@ -1,12 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wytch
4
+ # Rack middleware that triggers hot reloading before each request.
5
+ #
6
+ # This middleware wraps the base application and coordinates with the
7
+ # ReloadCoordinator to ensure site code and content are reloaded when
8
+ # files change. It acquires a read lock during request processing to
9
+ # prevent reloads from interfering with rendering.
10
+ #
11
+ # @see Wytch::ReloadCoordinator
4
12
  class SiteCodeLoaderMiddleware
13
+ # Creates a new middleware instance.
14
+ #
15
+ # @param app [#call] the Rack application to wrap
16
+ # @param coordinator [ReloadCoordinator] the reload coordinator
5
17
  def initialize(app, coordinator)
6
18
  @app = app
7
19
  @coordinator = coordinator
8
20
  end
9
21
 
22
+ # Handles a Rack request.
23
+ #
24
+ # Triggers a reload check, then processes the request while holding
25
+ # a read lock to prevent concurrent reloads.
26
+ #
27
+ # @param env [Hash] the Rack environment
28
+ # @return [Array] the Rack response tuple
10
29
  def call(env)
11
30
  @coordinator.reload!
12
31
 
@@ -11,6 +11,7 @@ module <%= @site_module %>
11
11
 
12
12
  html do
13
13
  head do
14
+ meta charset: "utf-8"
14
15
  title { @page.metadata[:title] }
15
16
  meta name: "description", content: @page.metadata[:description] if @page.metadata[:description]
16
17
  meta name: "viewport", content: "width=device-width, initial-scale=1.0"
data/lib/wytch/version.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wytch
4
- VERSION = "0.2.0"
4
+ # The current version of Wytch.
5
+ VERSION = "0.3.0"
5
6
  end
data/lib/wytch.rb CHANGED
@@ -7,19 +7,48 @@ loader = Zeitwerk::Loader.for_gem
7
7
  loader.inflector.inflect("cli" => "CLI")
8
8
  loader.setup
9
9
 
10
+ # Wytch is a minimal static site generator that uses Ruby for content
11
+ # and Phlex for views.
12
+ #
13
+ # @example Basic configuration in config.rb
14
+ # Wytch.configure do |site|
15
+ # site.page_class = "MySite::Page"
16
+ # site.base_url = "https://example.com"
17
+ # end
18
+ #
19
+ # @see Wytch::Site
20
+ # @see Wytch::Page
10
21
  module Wytch
22
+ # Base error class for all Wytch errors.
11
23
  class Error < StandardError; end
12
24
 
13
25
  class << self
26
+ # Returns the global site instance.
27
+ #
28
+ # @return [Wytch::Site] the current site configuration
14
29
  def site
15
30
  @site ||= Site.new
16
31
  end
17
32
 
33
+ # Configures the global site instance.
34
+ #
35
+ # @yield [site] Configuration block
36
+ # @yieldparam site [Wytch::Site] the site instance to configure
37
+ # @return [Wytch::Site] the configured site
38
+ #
39
+ # @example
40
+ # Wytch.configure do |site|
41
+ # site.page_class = "MySite::Page"
42
+ # site.content_dir = "pages"
43
+ # end
18
44
  def configure
19
45
  yield(site) if block_given?
20
46
  site
21
47
  end
22
48
 
49
+ # Resets the global site instance. Primarily used for testing.
50
+ #
51
+ # @return [void]
23
52
  def reset_site!
24
53
  @site = nil
25
54
  end
@@ -0,0 +1,4 @@
1
+ build/
2
+ node_modules/
3
+ lib/
4
+ .vite/
data/website/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "wytch", path: ".."
6
+
7
+ gem "builder", "~> 3.3"
8
+ gem "yard", "~> 0.9"
@@ -0,0 +1,63 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ wytch (0.2.0)
5
+ concurrent-ruby (~> 1.3)
6
+ listen (~> 3.9)
7
+ phlex (~> 1.11)
8
+ puma (~> 6.4)
9
+ rack (~> 3.1)
10
+ thor (~> 1.4)
11
+ zeitwerk (~> 2.6)
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ builder (3.3.0)
17
+ concurrent-ruby (1.3.5)
18
+ ffi (1.17.2)
19
+ ffi (1.17.2-aarch64-linux-gnu)
20
+ ffi (1.17.2-aarch64-linux-musl)
21
+ ffi (1.17.2-arm-linux-gnu)
22
+ ffi (1.17.2-arm-linux-musl)
23
+ ffi (1.17.2-arm64-darwin)
24
+ ffi (1.17.2-x86-linux-gnu)
25
+ ffi (1.17.2-x86-linux-musl)
26
+ ffi (1.17.2-x86_64-darwin)
27
+ ffi (1.17.2-x86_64-linux-gnu)
28
+ ffi (1.17.2-x86_64-linux-musl)
29
+ listen (3.9.0)
30
+ rb-fsevent (~> 0.10, >= 0.10.3)
31
+ rb-inotify (~> 0.9, >= 0.9.10)
32
+ nio4r (2.7.5)
33
+ phlex (1.11.0)
34
+ puma (6.6.1)
35
+ nio4r (~> 2.0)
36
+ rack (3.2.4)
37
+ rb-fsevent (0.11.2)
38
+ rb-inotify (0.11.1)
39
+ ffi (~> 1.0)
40
+ thor (1.4.0)
41
+ yard (0.9.38)
42
+ zeitwerk (2.7.3)
43
+
44
+ PLATFORMS
45
+ aarch64-linux-gnu
46
+ aarch64-linux-musl
47
+ arm-linux-gnu
48
+ arm-linux-musl
49
+ arm64-darwin
50
+ ruby
51
+ x86-linux-gnu
52
+ x86-linux-musl
53
+ x86_64-darwin
54
+ x86_64-linux-gnu
55
+ x86_64-linux-musl
56
+
57
+ DEPENDENCIES
58
+ builder (~> 3.3)
59
+ wytch!
60
+ yard (~> 0.9)
61
+
62
+ BUNDLED WITH
63
+ 2.6.9
@@ -0,0 +1,2 @@
1
+ // Main entry point for ReScript code
2
+ Console.log("Hello from Wytch-site!")
@@ -0,0 +1 @@
1
+ @import "tailwindcss";
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'yard'
6
+ require 'fileutils'
7
+
8
+ # Path to the wytch gem lib directory
9
+ WYTCH_LIB_PATH = File.expand_path('../../lib', __dir__)
10
+ API_CONTENT_DIR = File.expand_path('../content/api', __dir__)
11
+
12
+ puts "Cleaning existing API documentation..."
13
+ FileUtils.rm_rf(API_CONTENT_DIR)
14
+ FileUtils.mkdir_p(API_CONTENT_DIR)
15
+
16
+ puts "Parsing YARD documentation from #{WYTCH_LIB_PATH}..."
17
+ YARD.parse("#{WYTCH_LIB_PATH}/**/*.rb")
18
+
19
+ puts "Generating API content files..."
20
+ YARD::Registry.all(:class).each do |klass|
21
+ # Convert Wytch::Page to wytch/page
22
+ path_segments = klass.path.split('::').map(&:downcase)
23
+
24
+ # Create directory structure
25
+ dir_path = File.join(API_CONTENT_DIR, *path_segments[0..-2])
26
+ FileUtils.mkdir_p(dir_path) if path_segments.length > 1
27
+
28
+ # Create content file
29
+ file_path = File.join(API_CONTENT_DIR, *path_segments) + '.rb'
30
+
31
+ File.write(file_path, <<~RUBY)
32
+ # frozen_string_literal: true
33
+
34
+ view WytchSite::ApiClassView
35
+
36
+ @metadata[:title] = "#{klass.path}"
37
+ @metadata[:yard_path] = "#{klass.path}"
38
+ RUBY
39
+
40
+ puts " Created #{file_path.sub(API_CONTENT_DIR + '/', '')}"
41
+ end
42
+
43
+ puts "\nGenerated #{YARD::Registry.all(:class).count} API documentation pages"
44
+ puts "Run 'wytch build' to build the site"
data/website/config.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Wytch.configure do |site|
4
+ site.page_class = "WytchSite::Page"
5
+ # site.base_url = "https://example.com"
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ view WytchSite::ApiClassView
4
+
5
+ @metadata[:title] = "Wytch::Builder"
6
+ @metadata[:yard_path] = "Wytch::Builder"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ view WytchSite::ApiClassView
4
+
5
+ @metadata[:title] = "Wytch::CLI"
6
+ @metadata[:yard_path] = "Wytch::CLI"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ view WytchSite::ApiClassView
4
+
5
+ @metadata[:title] = "Wytch::ContentLoader"
6
+ @metadata[:yard_path] = "Wytch::ContentLoader"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ view WytchSite::ApiClassView
4
+
5
+ @metadata[:title] = "Wytch::Error"
6
+ @metadata[:yard_path] = "Wytch::Error"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ view WytchSite::ApiClassView
4
+
5
+ @metadata[:title] = "Wytch::Once"
6
+ @metadata[:yard_path] = "Wytch::Once"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ view WytchSite::ApiClassView
4
+
5
+ @metadata[:title] = "Wytch::Page"
6
+ @metadata[:yard_path] = "Wytch::Page"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ view WytchSite::ApiClassView
4
+
5
+ @metadata[:title] = "Wytch::ReloadCoordinator"
6
+ @metadata[:yard_path] = "Wytch::ReloadCoordinator"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ view WytchSite::ApiClassView
4
+
5
+ @metadata[:title] = "Wytch::Server"
6
+ @metadata[:yard_path] = "Wytch::Server"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ view WytchSite::ApiClassView
4
+
5
+ @metadata[:title] = "Wytch::Site"
6
+ @metadata[:yard_path] = "Wytch::Site"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ view WytchSite::ApiClassView
4
+
5
+ @metadata[:title] = "Wytch::SiteCodeLoaderMiddleware"
6
+ @metadata[:yard_path] = "Wytch::SiteCodeLoaderMiddleware"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ view WytchSite::HomeView
4
+
5
+ @metadata[:title] = "Wytch"
6
+ @metadata[:description] = "A minimal static site generator"
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ add WytchSite::SitemapHelper
4
+ view WytchSite::SitemapView
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "wytch-site",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build"
9
+ },
10
+ "devDependencies": {
11
+ "@jihchi/vite-plugin-rescript": "^7.1.0",
12
+ "rescript": "^11.1.4",
13
+ "tailwindcss": "^4.1.0",
14
+ "vite": "^7.2.2"
15
+ },
16
+ "dependencies": {
17
+ "@rescript/core": "^1.6.1"
18
+ }
19
+ }
@@ -0,0 +1,2 @@
1
+ User-agent: *
2
+ Allow: /
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "wytch-site",
3
+ "sources": [
4
+ {
5
+ "dir": "assets",
6
+ "subdirs": true
7
+ }
8
+ ],
9
+ "package-specs": [
10
+ {
11
+ "module": "esmodule",
12
+ "in-source": false
13
+ }
14
+ ],
15
+ "suffix": ".res.js",
16
+ "bs-dependencies": ["@rescript/core"],
17
+ "bsc-flags": ["-open RescriptCore"]
18
+ }
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WytchSite
4
+ class ApiClassView < Phlex::HTML
5
+ def initialize(page)
6
+ @page = page
7
+ @klass = @page.yard_object
8
+ end
9
+
10
+ def view_template
11
+ render Layout.new(@page) do
12
+ article do
13
+ header do
14
+ h1 { @klass.path }
15
+ p(class: "summary") { @klass.docstring.summary } unless @klass.docstring.summary.empty?
16
+ end
17
+
18
+ unless @klass.docstring.to_s.empty?
19
+ section(class: "description") do
20
+ h2 { "Description" }
21
+ div { unsafe_raw markdown_to_html(@klass.docstring.to_s) }
22
+ end
23
+ end
24
+
25
+ # Public methods
26
+ public_methods = @klass.meths(visibility: :public).reject { |m| m.name == :initialize }
27
+ if public_methods.any?
28
+ section(class: "methods") do
29
+ h2 { "Methods" }
30
+ public_methods.each do |method|
31
+ render_method(method)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def render_method(method)
42
+ article(class: "method", id: method.name) do
43
+ h3 { code { method.signature } }
44
+
45
+ unless method.docstring.to_s.empty?
46
+ div(class: "method-description") do
47
+ unsafe_raw markdown_to_html(method.docstring.to_s)
48
+ end
49
+ end
50
+
51
+ # Parameters
52
+ param_tags = method.tags.select { |t| t.tag_name == "param" }
53
+ if param_tags.any?
54
+ dl(class: "parameters") do
55
+ dt { "Parameters:" }
56
+ param_tags.each do |tag|
57
+ dd do
58
+ code { tag.name }
59
+ span { " (#{tag.types.join(', ')})" } if tag.types
60
+ plain " — #{tag.text}" if tag.text
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # Returns
67
+ return_tags = method.tags.select { |t| t.tag_name == "return" }
68
+ if return_tags.any?
69
+ dl(class: "returns") do
70
+ dt { "Returns:" }
71
+ return_tags.each do |tag|
72
+ dd do
73
+ span { "(#{tag.types.join(', ')})" } if tag.types
74
+ plain " — #{tag.text}" if tag.text
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def markdown_to_html(text)
83
+ # Simple markdown rendering - just handle basic formatting
84
+ text.gsub(/`([^`]+)`/, '<code>\1</code>')
85
+ .gsub(/\*\*([^*]+)\*\*/, '<strong>\1</strong>')
86
+ .gsub(/\n\n/, '</p><p>')
87
+ .then { |s| "<p>#{s}</p>" }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WytchSite
4
+ class HomeView < Phlex::HTML
5
+ def initialize(page)
6
+ @page = page
7
+ end
8
+
9
+ def view_template
10
+ render Layout.new(@page) do
11
+ h1 { @page.metadata[:title] }
12
+ p { @page.metadata[:description] }
13
+
14
+ section do
15
+ h2 { "API Documentation" }
16
+ ul do
17
+ api_pages.each do |page|
18
+ li { a(href: page.path) { page.metadata[:title] } }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def api_pages
28
+ Wytch.site.pages.values
29
+ .select { |p| p.path.start_with?('/api/') }
30
+ .sort_by { |p| p.metadata[:title] }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WytchSite
4
+ class Layout < Phlex::HTML
5
+ def initialize(page)
6
+ @page = page
7
+ end
8
+
9
+ def view_template(&block)
10
+ doctype
11
+
12
+ html do
13
+ head do
14
+ meta charset: "utf-8"
15
+ title { @page.metadata[:title] }
16
+ meta name: "description", content: @page.metadata[:description] if @page.metadata[:description]
17
+ meta name: "viewport", content: "width=device-width, initial-scale=1.0"
18
+
19
+ # Vite assets
20
+ if ENV["RACK_ENV"] == "development"
21
+ script src: "http://localhost:6970/@vite/client", type: "module"
22
+ link rel: "stylesheet", href: "http://localhost:6970/assets/main.css"
23
+ script src: "http://localhost:6970/assets/Main.res", type: "module"
24
+ else
25
+ link rel: "stylesheet", href: "/assets/main.css"
26
+ script src: "/assets/app.js", type: "module"
27
+ end
28
+ end
29
+
30
+ body(&block)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yard'
4
+
5
+ module WytchSite
6
+ class Page < Wytch::Page
7
+ def yard_object
8
+ return nil unless @metadata[:yard_path]
9
+
10
+ # Parse YARD if registry is empty
11
+ if YARD::Registry.all.empty?
12
+ YARD.parse(File.expand_path('../../../lib/**/*.rb', __dir__))
13
+ end
14
+
15
+ YARD::Registry.at(@metadata[:yard_path])
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WytchSite
4
+ module SitemapHelper
5
+ def path
6
+ "/sitemap.xml"
7
+ end
8
+
9
+ def build_path
10
+ "sitemap.xml"
11
+ end
12
+
13
+ def include_in_sitemap?
14
+ false
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WytchSite
4
+ class SitemapView
5
+ def initialize(page)
6
+ @page = page
7
+ end
8
+
9
+ def call
10
+ require "builder"
11
+
12
+ xml = Builder::XmlMarkup.new(indent: 2)
13
+ xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
14
+ xml.urlset xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9" do
15
+ Wytch.site.pages.values.each do |page|
16
+ next unless page.include_in_sitemap?
17
+ xml.url do
18
+ xml.loc "#{Wytch.site.base_url}#{page.path}"
19
+ xml.lastmod page.last_modified if page.last_modified
20
+ xml.changefreq page.change_frequency if page.change_frequency
21
+ xml.priority page.priority if page.priority
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ import { defineConfig } from "vite";
2
+ import createReScriptPlugin from "@jihchi/vite-plugin-rescript";
3
+
4
+ export default defineConfig({
5
+ plugins: [
6
+ createReScriptPlugin({
7
+ loader: {
8
+ suffix: ".res.js",
9
+ },
10
+ }),
11
+ ],
12
+ build: {
13
+ outDir: "build/assets",
14
+ manifest: true,
15
+ rollupOptions: {
16
+ input: {
17
+ main: "./assets/main.css",
18
+ app: "./assets/Main.res",
19
+ },
20
+ },
21
+ },
22
+ server: {
23
+ port: 6970,
24
+ strictPort: false,
25
+ },
26
+ });
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wytch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jared Norman
@@ -155,6 +155,35 @@ files:
155
155
  - lib/wytch/templates/vite.config.js.tt
156
156
  - lib/wytch/version.rb
157
157
  - sig/wytch.rbs
158
+ - website/.gitignore
159
+ - website/Gemfile
160
+ - website/Gemfile.lock
161
+ - website/assets/Main.res
162
+ - website/assets/main.css
163
+ - website/bin/generate_api_docs
164
+ - website/config.rb
165
+ - website/content/api/wytch/builder.rb
166
+ - website/content/api/wytch/cli.rb
167
+ - website/content/api/wytch/contentloader.rb
168
+ - website/content/api/wytch/error.rb
169
+ - website/content/api/wytch/once.rb
170
+ - website/content/api/wytch/page.rb
171
+ - website/content/api/wytch/reloadcoordinator.rb
172
+ - website/content/api/wytch/server.rb
173
+ - website/content/api/wytch/site.rb
174
+ - website/content/api/wytch/sitecodeloadermiddleware.rb
175
+ - website/content/index.rb
176
+ - website/content/sitemap.rb
177
+ - website/package.json
178
+ - website/public/robots.txt
179
+ - website/rescript.json
180
+ - website/src/wytch_site/api_class_view.rb
181
+ - website/src/wytch_site/home_view.rb
182
+ - website/src/wytch_site/layout.rb
183
+ - website/src/wytch_site/page.rb
184
+ - website/src/wytch_site/sitemap_helper.rb
185
+ - website/src/wytch_site/sitemap_view.rb
186
+ - website/vite.config.js
158
187
  homepage: https://github.com/SuperGoodSoft/wytch
159
188
  licenses:
160
189
  - MIT