isola 0.1.0 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d66e9eb2d33f8ab15d77fc1d71ed2c962d977b004c3eb340cd09c932a513a65e
4
- data.tar.gz: b6d134d39802939a0728b9ab59300968286da39f36b9eb13e79ebac380192efd
3
+ metadata.gz: ae141ebf3510995229e7bc854c3c43a566deb7f74b47f5c28c64840c1f25db18
4
+ data.tar.gz: 458948286fb8f457a77e5a7d9e9ed7805258ea53a9c6cb5ee3e4640a35d9f41e
5
5
  SHA512:
6
- metadata.gz: ebeba2d27bb183e2a1ebe19b1dc899d3686bfb918d5d9cbebb6fa5cd1354c81ba85dfa950051fe192cfec00c7c132712b765b566385312dad15be79cf7f333cb
7
- data.tar.gz: 568589f57d81020434011aa145c01c33452bb04b4f8fee604b0b9e5a89447ba4dc3172168746afb5827f77e526961dbbf559e70db883d45a945e0c5a2f888b8a
6
+ metadata.gz: 7909154b135a3667a4289d5e9e91ddcf3e79029801e17b103ddebe906789561ea6a11499de3cb7e090bb83e38339c166786f8b05f1053a536464f3ec6f6843fa
7
+ data.tar.gz: '083de5dffb0186058bd30f4edf7d41e7b9a3472ea09b5b661654a4456585b74062cbf5ab11069af43c4994cf16e80626b5b5a90834effa2f59854c6091e0b246'
data/README.md CHANGED
@@ -19,6 +19,8 @@ gem install isola
19
19
  1. place markdown files in your directory
20
20
  2. `isola build`
21
21
 
22
+ See [Usage Guide](USAGE.md) / [使い方ガイド](USAGE.ja.md) for details.
23
+
22
24
  ## Development Policy
23
25
 
24
26
  This project does not use Agentic Coding at all. AI assistance in code reviews is not avoided.
data/USAGE.ja.md ADDED
@@ -0,0 +1,118 @@
1
+ # Isola 使い方ガイド
2
+
3
+ ERBを使ったシンプルなStatic Site Generator(SSG)です。
4
+
5
+ ## インストール
6
+
7
+ ```bash
8
+ gem install isola
9
+ ```
10
+
11
+ ## 基本的な使い方
12
+
13
+ ### 1. サイトディレクトリを作る
14
+
15
+ ```
16
+ my-site/
17
+ ├── _config.yaml
18
+ ├── _layouts/
19
+ │ └── default.html.erb
20
+ ├── _includes/
21
+ │ └── head.html.erb
22
+ ├── index.md
23
+ └── css/
24
+ └── main.css
25
+ ```
26
+
27
+ ### 2. 設定ファイル(`_config.yaml`)
28
+
29
+ ```yaml
30
+ url: https://example.com
31
+ title: My Site
32
+ destination: _site
33
+ default_language: ja
34
+ excludes:
35
+ - README.md
36
+ ```
37
+
38
+ すべて省略可能です。`destination`のデフォルトは`_site`です。
39
+
40
+ ### 3. ページを書く
41
+
42
+ Markdownファイルの先頭にYAML front-matterを記述します。
43
+
44
+ ```markdown
45
+ ---
46
+ layout: default
47
+ title: トップページ
48
+ ---
49
+
50
+ 本文をMarkdownで書きます。
51
+ ```
52
+
53
+ 拡張子`.md.erb`にすると、Markdown内でERBが使えます。
54
+
55
+ ### 4. レイアウトを作る
56
+
57
+ `_layouts/default.html.erb`:
58
+
59
+ ```erb
60
+ <html>
61
+ <head>
62
+ <title><%= page[:title] %></title>
63
+ </head>
64
+ <body>
65
+ <%= content %>
66
+ </body>
67
+ </html>
68
+ ```
69
+
70
+ テンプレート内では以下が使えます:
71
+
72
+ - `content` — ページ本文
73
+ - `page[:title]`, `page[:lang]` など — front-matterの値(`lang`はサイト設定から自動的に含まれますが、設定すれば上書きされます)
74
+ - `site[:title]`, `site[:url]` など — サイト設定
75
+ - `include 'head', key: value` — インクルードの挿入
76
+
77
+ ### 5. ビルド
78
+
79
+ ```bash
80
+ cd my-site
81
+ isola build
82
+ ```
83
+
84
+ `_site/`に生成されます。
85
+
86
+ ### 6. 開発サーバー
87
+
88
+ ```bash
89
+ cd my-site
90
+ isola serve
91
+ ```
92
+
93
+ サイトをビルドし、ローカル開発サーバーを起動します。デフォルトでは `http://127.0.0.1:4444` で待ち受けます。ホストとポートは `_config.yaml` で変更できます:
94
+
95
+ ```yaml
96
+ host: 127.0.0.1
97
+ port: 4444
98
+ ```
99
+
100
+ サーバーはファイルの変更を監視し、自動的にサイトを再ビルドします。`</body>` タグを含むHTMLページにはライブリロード用のスクリプトが挿入され、再ビルド後にブラウザが自動的にリロードされます。ライブリロードが機能するのは `</body>` タグを含むHTMLファイルのみです。
101
+
102
+ ## ファイルの処理
103
+
104
+ Isolaが処理するテンプレートエンジンは現在 **ERB**(`.erb`)と **Markdown**(`.md`, `.markdown`, `.mkd`)のみです。
105
+
106
+ 拡張子の末尾から順に処理します。例えば `page.md.erb` の場合、まずERBを処理し、次にMarkdownを処理します。
107
+
108
+ 処理後にまだ拡張子が残っている場合はそのまま使います。残っていない場合は `.html` が付与されます。
109
+
110
+ | ソース | 処理 | 出力 |
111
+ |---|---|---|
112
+ | `page.md` | Markdown → HTML | `page.html` |
113
+ | `page.md.erb` | ERB → Markdown → HTML | `page.html` |
114
+ | `style.css.erb` | ERB | `style.css` |
115
+ | `index.html.erb` | ERB | `index.html` |
116
+ | `*.css`, `*.js` など | なし | そのままコピー |
117
+
118
+ `_`や`.`で始まるファイル・ディレクトリは自動的に除外されます(`_layouts/`と`_includes/`を除く)。
data/USAGE.md ADDED
@@ -0,0 +1,118 @@
1
+ # Isola Usage Guide
2
+
3
+ A simple Static Site Generator (SSG) using ERB.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ gem install isola
9
+ ```
10
+
11
+ ## Basic Usage
12
+
13
+ ### 1. Create a site directory
14
+
15
+ ```
16
+ my-site/
17
+ ├── _config.yaml
18
+ ├── _layouts/
19
+ │ └── default.html.erb
20
+ ├── _includes/
21
+ │ └── head.html.erb
22
+ ├── index.md
23
+ └── css/
24
+ └── main.css
25
+ ```
26
+
27
+ ### 2. Configuration (`_config.yaml`)
28
+
29
+ ```yaml
30
+ url: https://example.com
31
+ title: My Site
32
+ destination: _site
33
+ default_language: en
34
+ excludes:
35
+ - README.md
36
+ ```
37
+
38
+ All fields are optional. The default `destination` is `_site`.
39
+
40
+ ### 3. Write pages
41
+
42
+ Add YAML front-matter at the top of your Markdown files.
43
+
44
+ ```markdown
45
+ ---
46
+ layout: default
47
+ title: Top Page
48
+ ---
49
+
50
+ Write your content in Markdown.
51
+ ```
52
+
53
+ Use the `.md.erb` extension to enable ERB inside Markdown.
54
+
55
+ ### 4. Create layouts
56
+
57
+ `_layouts/default.html.erb`:
58
+
59
+ ```erb
60
+ <html>
61
+ <head>
62
+ <title><%= page[:title] %></title>
63
+ </head>
64
+ <body>
65
+ <%= content %>
66
+ </body>
67
+ </html>
68
+ ```
69
+
70
+ The following are available in templates:
71
+
72
+ - `content` — page body
73
+ - `page[:title]`, `page[:lang]` etc. — front-matter values (including `lang` from site config; you can overwrite in the page front-matter)
74
+ - `site[:title]`, `site[:url]` etc. — site configuration
75
+ - `include 'head', key: value` — insert an include
76
+
77
+ ### 5. Build
78
+
79
+ ```bash
80
+ cd my-site
81
+ isola build
82
+ ```
83
+
84
+ Output is generated in `_site/`.
85
+
86
+ ### 6. Development Server
87
+
88
+ ```bash
89
+ cd my-site
90
+ isola serve
91
+ ```
92
+
93
+ Builds the site and starts a local development server. By default, the server listens on `http://127.0.0.1:4444`. You can change the host and port in `_config.yaml`:
94
+
95
+ ```yaml
96
+ host: 127.0.0.1
97
+ port: 4444
98
+ ```
99
+
100
+ The server watches for file changes and automatically rebuilds the site. HTML pages that contain a `</body>` tag are injected with a live-reload script, so the browser refreshes automatically after each rebuild. Live reload only works for HTML files with a `</body>` tag.
101
+
102
+ ## File Processing
103
+
104
+ Isola currently supports only **ERB** (`.erb`) and **Markdown** (`.md`, `.markdown`, `.mkd`) as template engines.
105
+
106
+ Extensions are processed from right to left. For example, `page.md.erb` is first processed as ERB, then as Markdown.
107
+
108
+ If an extension remains after processing, it is kept as-is. If no extension remains, `.html` is used.
109
+
110
+ | Source | Processing | Output |
111
+ |---|---|---|
112
+ | `page.md` | Markdown → HTML | `page.html` |
113
+ | `page.md.erb` | ERB → Markdown → HTML | `page.html` |
114
+ | `style.css.erb` | ERB | `style.css` |
115
+ | `index.html.erb` | ERB | `index.html` |
116
+ | `*.css`, `*.js` etc. | None | Copied as-is |
117
+
118
+ Files and directories starting with `_` or `.` are excluded automatically (except `_layouts/` and `_includes/`).
data/exe/isola CHANGED
@@ -3,20 +3,45 @@
3
3
 
4
4
  require "isola"
5
5
  require "thor"
6
+ require "listen"
6
7
 
7
8
  class IsolaCLI < Thor
8
9
  package_name "Isola"
10
+
9
11
  desc "build site", "build site on current directory"
10
12
  def build
13
+ site = construct_site
14
+ site.build
15
+ end
16
+
17
+ desc "serve", "build site and serve"
18
+ def serve
19
+ site = construct_site
20
+ site.build
21
+
22
+ site_dir = File.join(site[:root_dir], site[:destination])
23
+ host = site[:host]
24
+ port = site[:port]
25
+ server = Isola::DevServer.new(site_dir, host, port)
26
+
27
+ watcher = Isola::Watcher.new(site) { server.notify_reload }
28
+ listener = Listen.to(File.expand_path(site[:root_dir]), &watcher.method(:handle_changes))
29
+ listener.start
30
+
31
+ puts "serving at http://#{host}:#{port}"
32
+ server.start
33
+ end
34
+
35
+ private
36
+
37
+ def construct_site
11
38
  config =
12
39
  if File.exist? "_config.yaml"
13
40
  File.read("_config.yaml")
14
41
  else
15
42
  ""
16
43
  end
17
- site = Isola::Site.new(config)
18
- site.collect_files
19
- site.process
44
+ Isola::Site.new(config)
20
45
  end
21
46
  end
22
47
 
data/lib/isola/context.rb CHANGED
@@ -5,14 +5,14 @@ module Isola
5
5
  attr_reader :site, :content, :layout
6
6
  def initialize(page, site)
7
7
  @page_source = page
8
- @page_meta = Data.define(*page.meta.keys).new(**page.meta)
8
+ @meta = {lang: site[:lang]}.merge(page.meta).freeze
9
9
  @site = site
10
10
  @content = ""
11
11
  @layout = {}
12
12
  end
13
13
 
14
14
  def page
15
- @page_meta
15
+ @meta
16
16
  end
17
17
 
18
18
  def include name, params = {}
@@ -0,0 +1,104 @@
1
+ require "webrick"
2
+
3
+ module Isola
4
+ class ReloadStream
5
+ def initialize
6
+ @queue = Queue.new
7
+ end
8
+
9
+ def readpartial(maxlen, buf = +"")
10
+ if !@data
11
+ @data = @queue.pop.dup
12
+ @data.force_encoding(Encoding::ASCII_8BIT)
13
+ end
14
+
15
+ if @data.bytesize <= maxlen
16
+ buf.replace(@data)
17
+ @data = nil
18
+ else
19
+ buf.replace(@data.byteslice(0, maxlen))
20
+ @data = @data.byteslice(maxlen..-1) || ""
21
+ end
22
+
23
+ buf
24
+ rescue
25
+ raise EOFError
26
+ end
27
+
28
+ def notify
29
+ @queue.push("data: reload\n\n")
30
+ end
31
+
32
+ def close
33
+ @queue.close
34
+ end
35
+
36
+ def closed?
37
+ @queue.closed?
38
+ end
39
+ end
40
+
41
+ class DevServer
42
+ def initialize(root_dir, host, port)
43
+ @root_dir = File.expand_path(root_dir)
44
+ @host = host
45
+ @port = port
46
+ @streams = []
47
+ @mutex = Mutex.new
48
+ end
49
+
50
+ def start
51
+ @server = WEBrick::HTTPServer.new(BindAddress: @host, Port: @port)
52
+ @server.mount("/", LiveFileHandler, @root_dir)
53
+ @server.mount_proc("/_reload") do |req, res|
54
+ stream = ReloadStream.new
55
+ @mutex.synchronize { @streams << stream }
56
+ res["Content-Type"] = "text/event-stream"
57
+ res["Cache-Control"] = "no-cache"
58
+ res["Connection"] = "keep-alive"
59
+ res.chunked = true
60
+ res.body = stream
61
+ end
62
+
63
+ trap("INT") do
64
+ @streams.each(&:close)
65
+ @server.shutdown
66
+ end
67
+
68
+ @server.start
69
+ end
70
+
71
+ def remove_stream stream
72
+ @mutex.synchronize { @streams.delete(stream) }
73
+ end
74
+
75
+ def notify_reload
76
+ @mutex.synchronize {
77
+ @streams.reject!(&:closed?)
78
+ @streams.each(&:notify)
79
+ }
80
+ end
81
+
82
+ def shutdown
83
+ @mutex.synchronize {
84
+ @streams.each(&:close)
85
+ }
86
+ @server&.shutdown
87
+ end
88
+ end
89
+
90
+ # for live reload
91
+ class LiveFileHandler < WEBrick::HTTPServlet::FileHandler
92
+ RELOAD_SCRIPT = <<~HTML
93
+ <script>new EventSource("/_reload").onmessage=()=>location.reload()</script>
94
+ HTML
95
+ def do_GET(req, res)
96
+ super
97
+ if res["Content-Type"]&.include?("text/html")
98
+ html = res.body.is_a?(IO) ? res.body.read : res.body
99
+ res.body = html.sub(%r{</body>}i, "#{RELOAD_SCRIPT}</body>")
100
+ res["Content-Length"] = res.body.bytesize
101
+ end
102
+ end
103
+ end
104
+ end
@@ -17,6 +17,10 @@ module Isola
17
17
  collect(@root_dir)
18
18
  end
19
19
 
20
+ def ignore?(absolute_path)
21
+ !process_path?(absolute_path.delete_prefix("#{@root_dir}/"))
22
+ end
23
+
20
24
  private
21
25
 
22
26
  def collect dir
data/lib/isola/site.rb CHANGED
@@ -3,31 +3,26 @@ require "fileutils"
3
3
  module Isola
4
4
  class Site
5
5
  attr_accessor :config
6
- DEFAULT_CONFIG = {url: "http://example.com", title: "my awesome site", destination: "_site", default_language: "en"}.freeze
6
+ DEFAULT_CONFIG = {url: "http://example.com",
7
+ title: "my awesome site",
8
+ destination: "_site",
9
+ default_language: "en",
10
+ host: "127.0.0.1",
11
+ port: 4444}.freeze
7
12
  SUPPORTED_TILT_EXT = [".erb", ".md", ".markdown", ".mkd"]
8
13
  EXT_MAP = {".md" => ".html", ".mkd" => ".html", ".markdown" => ".html", "" => ".html"}
9
14
  def initialize(config)
10
15
  @config = DEFAULT_CONFIG.merge(YAML.safe_load(config, symbolize_names: true) || {})
11
16
  @config[:root_dir] ||= Dir.pwd
12
17
  @config[:excludes] ||= []
13
- @parsed_layouts = {}
14
- @parsed_includes = {}
15
- end
16
-
17
- def title
18
- @config[:title]
19
- end
20
-
21
- def url
22
- @config[:url]
18
+ collect_files
23
19
  end
24
20
 
25
- def lang
26
- @config[:default_language]
27
- end
28
-
29
- def root_dir
30
- @config[:root_dir]
21
+ def [] key
22
+ if key == :lang
23
+ key = :default_language
24
+ end
25
+ @config[key]
31
26
  end
32
27
 
33
28
  def supported_ext? ext
@@ -38,19 +33,27 @@ module Isola
38
33
  EXT_MAP[ext]
39
34
  end
40
35
 
41
- def collect_files
42
- @file_handler = FileHandler.new(root_dir, excludes: @config[:excludes])
43
- end
44
-
45
- def process
36
+ def build
37
+ dest_dir = File.join(@file_handler.root_dir, @config[:destination])
38
+ FileUtils.rm_rf(dest_dir)
46
39
  @file_handler.pages.each do |name, path|
47
40
  page = Source.new(path, read_in_site(path))
48
- puts "processing #{path}..."
41
+ puts "building #{path}..."
49
42
  rendered, path = Context.new(page, self).render
50
- dest_path = File.join(@file_handler.root_dir, @config[:destination], path)
43
+ dest_path = File.join(dest_dir, path)
51
44
  FileUtils.mkdir_p(File.dirname(dest_path))
52
45
  File.write(dest_path, rendered)
53
46
  end
47
+ puts "done."
48
+ end
49
+
50
+ def rebuild
51
+ collect_files
52
+ build
53
+ end
54
+
55
+ def ignore?(path)
56
+ @file_handler.ignore?(path)
54
57
  end
55
58
 
56
59
  def layout name
@@ -63,6 +66,12 @@ module Isola
63
66
 
64
67
  private
65
68
 
69
+ def collect_files
70
+ @file_handler = FileHandler.new(config[:root_dir], excludes: @config[:excludes])
71
+ @parsed_layouts = {}
72
+ @parsed_includes = {}
73
+ end
74
+
66
75
  def find_source(name, cache, store)
67
76
  cache[name] ||=
68
77
  begin
@@ -73,7 +82,7 @@ module Isola
73
82
  end
74
83
 
75
84
  def read_in_site(p)
76
- File.read(File.join(root_dir, p))
85
+ File.read(File.join(config[:root_dir], p))
77
86
  end
78
87
  end
79
88
  end
data/lib/isola/source.rb CHANGED
@@ -12,6 +12,10 @@ module Isola
12
12
  end
13
13
  end
14
14
 
15
+ def [] key
16
+ @meta[key]
17
+ end
18
+
15
19
  def render(context, site, params = {})
16
20
  path = @filepath.dup
17
21
  rendered = @content.dup
data/lib/isola/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Isola
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
@@ -0,0 +1,15 @@
1
+ module Isola
2
+ class Watcher
3
+ def initialize(site, &on_rebuild)
4
+ @site = site
5
+ @on_rebuild = on_rebuild
6
+ end
7
+
8
+ def handle_changes(modified, added, removed)
9
+ paths = [*modified, *added, *removed]
10
+ return if paths.all? { |path| @site.ignore?(path) }
11
+ @site.rebuild
12
+ @on_rebuild&.call
13
+ end
14
+ end
15
+ end
data/lib/isola.rb CHANGED
@@ -5,6 +5,8 @@ require_relative "isola/site"
5
5
  require_relative "isola/file_handler"
6
6
  require_relative "isola/source"
7
7
  require_relative "isola/context"
8
+ require_relative "isola/watcher"
9
+ require_relative "isola/dev_server"
8
10
 
9
11
  module Isola
10
12
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: isola
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Satoshi Kojima
@@ -51,6 +51,34 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '1.5'
54
+ - !ruby/object:Gem::Dependency
55
+ name: webrick
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.9'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.9'
68
+ - !ruby/object:Gem::Dependency
69
+ name: listen
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.10'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.10'
54
82
  description: ''
55
83
  email:
56
84
  - skoji@skoji.jp
@@ -62,14 +90,17 @@ files:
62
90
  - LICENSE
63
91
  - README.md
64
92
  - Rakefile
93
+ - USAGE.ja.md
94
+ - USAGE.md
65
95
  - exe/isola
66
96
  - lib/isola.rb
67
97
  - lib/isola/context.rb
98
+ - lib/isola/dev_server.rb
68
99
  - lib/isola/file_handler.rb
69
100
  - lib/isola/site.rb
70
101
  - lib/isola/source.rb
71
102
  - lib/isola/version.rb
72
- - sig/isola.rbs
103
+ - lib/isola/watcher.rb
73
104
  homepage: https://github.com/skoji/isola
74
105
  licenses: []
75
106
  metadata:
@@ -82,7 +113,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
82
113
  requirements:
83
114
  - - ">="
84
115
  - !ruby/object:Gem::Version
85
- version: 4.0.0
116
+ version: 3.4.0
86
117
  required_rubygems_version: !ruby/object:Gem::Requirement
87
118
  requirements:
88
119
  - - ">="
data/sig/isola.rbs DELETED
@@ -1,3 +0,0 @@
1
- module Isola
2
- VERSION: String
3
- end