liquidbook 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.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/LICENSE +21 -0
- data/README.md +83 -0
- data/Rakefile +8 -0
- data/docs/README.ja.md +81 -0
- data/exe/liquidbook +7 -0
- data/fixtures/default_mocks.yml +88 -0
- data/lib/liquidbook/cli.rb +86 -0
- data/lib/liquidbook/config.rb +104 -0
- data/lib/liquidbook/filters/shopify_filters.rb +146 -0
- data/lib/liquidbook/mock_data.rb +57 -0
- data/lib/liquidbook/param_parser.rb +70 -0
- data/lib/liquidbook/pid_manager.rb +81 -0
- data/lib/liquidbook/schema_parser.rb +58 -0
- data/lib/liquidbook/server/app.rb +158 -0
- data/lib/liquidbook/server/views/index.erb +35 -0
- data/lib/liquidbook/server/views/layout.erb +59 -0
- data/lib/liquidbook/server/views/preview.erb +183 -0
- data/lib/liquidbook/tags/render_tag.rb +85 -0
- data/lib/liquidbook/tags/section_tag.rb +33 -0
- data/lib/liquidbook/theme_renderer.rb +82 -0
- data/lib/liquidbook/version.rb +5 -0
- data/lib/liquidbook.rb +50 -0
- data/sig/liquidbook.rbs +4 -0
- metadata +153 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6c2c697f9bb5b094c752a0798ef24b5bf0265f60c5ed490abe4b9ad2abbb2d60
|
|
4
|
+
data.tar.gz: dda835eaec1bef74610452dfe1596a1b517122ca9e66a852592aa5ca79a43700
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a5ff5a395bdf717724ebec570cb4fb8ff039d4cd9940dbb2df6320e402e0ae18511d5ec2d1ebeca9f598b73f0b81cc96977bae7bcb996117fc0e07e8380d10ff
|
|
7
|
+
data.tar.gz: 731b75e5e253632d37d58033f49d12c4ef74820397212750174bc2e66b9d01c8ad980be48108197effefab90ec4009c6d57becd4b494793f17f0471659665efa
|
data/.rspec
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sena Murakami
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Liquidbook
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sena-m09/liquidbook/actions/workflows/test.yml)
|
|
4
|
+
|
|
5
|
+
A Storybook-like local preview server for Shopify Liquid templates. Browse and preview your sections and snippets in the browser instantly.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Auto-discovers `.liquid` files in `sections/` and `snippets/`
|
|
10
|
+
- Generates default setting values from `{% schema %}` blocks
|
|
11
|
+
- Supports Shopify-compatible filters and tags (`section`, `render`)
|
|
12
|
+
- Load external CSS/JS via `.liquid-preview/config.yml`
|
|
13
|
+
- File watching with live reload
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add to your Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "liquidbook"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or install directly:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem install liquidbook
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Start the preview server
|
|
32
|
+
|
|
33
|
+
Run from your Shopify theme root:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
liquidbook server
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Open `http://127.0.0.1:4567` in your browser to see a list of sections and snippets with live previews.
|
|
40
|
+
|
|
41
|
+
#### Options
|
|
42
|
+
|
|
43
|
+
| Option | Short | Default | Description |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `--port` | `-p` | `4567` | Port number |
|
|
46
|
+
| `--host` | `-H` | `127.0.0.1` | Host to bind |
|
|
47
|
+
| `--root` | `-r` | `.` | Theme root directory |
|
|
48
|
+
|
|
49
|
+
### Render a single template
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
liquidbook render sections/header.liquid
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Outputs the rendered HTML to stdout.
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
Create `.liquid-preview/config.yml` in your theme root to load external assets into the preview:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
head:
|
|
63
|
+
- <script src="https://cdn.tailwindcss.com"></script>
|
|
64
|
+
- ../src/styles/main.css
|
|
65
|
+
- ./src/components.js
|
|
66
|
+
|
|
67
|
+
port: 4567
|
|
68
|
+
host: 127.0.0.1
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`head` entries accept raw HTML tags or file paths. File paths are resolved relative to the theme root.
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
git clone https://github.com/sena-m09/liquidbook.git
|
|
77
|
+
cd liquidbook
|
|
78
|
+
bin/setup
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
[MIT License](https://opensource.org/licenses/MIT)
|
data/Rakefile
ADDED
data/docs/README.ja.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Liquidbook
|
|
2
|
+
|
|
3
|
+
Shopify Liquid テンプレートのローカルプレビューサーバー。Storybook のように、sections や snippets をブラウザ上で即座に確認できます。
|
|
4
|
+
|
|
5
|
+
## 特徴
|
|
6
|
+
|
|
7
|
+
- `sections/` や `snippets/` 内の `.liquid` ファイルを自動検出してプレビュー
|
|
8
|
+
- `{% schema %}` ブロックからデフォルト設定値を自動生成
|
|
9
|
+
- Shopify 互換フィルター・タグ (`section`, `render`) をサポート
|
|
10
|
+
- `.liquid-preview/config.yml` で外部 CSS/JS の読み込みに対応
|
|
11
|
+
- ファイル変更の自動検知(live reload)
|
|
12
|
+
|
|
13
|
+
## インストール
|
|
14
|
+
|
|
15
|
+
Gemfile に追加:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "liquidbook"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
または直接インストール:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install liquidbook
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 使い方
|
|
28
|
+
|
|
29
|
+
### プレビューサーバーの起動
|
|
30
|
+
|
|
31
|
+
Shopify テーマのルートディレクトリで実行:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
liquidbook server
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
ブラウザで `http://127.0.0.1:4567` を開くと、sections / snippets の一覧とプレビューが表示されます。
|
|
38
|
+
|
|
39
|
+
#### オプション
|
|
40
|
+
|
|
41
|
+
| オプション | 短縮 | デフォルト | 説明 |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| `--port` | `-p` | `4567` | ポート番号 |
|
|
44
|
+
| `--host` | `-H` | `127.0.0.1` | バインドするホスト |
|
|
45
|
+
| `--root` | `-r` | `.` | テーマのルートディレクトリ |
|
|
46
|
+
|
|
47
|
+
### 単一テンプレートのレンダリング
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
liquidbook render sections/header.liquid
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
標準出力に HTML が出力されます。
|
|
54
|
+
|
|
55
|
+
## 設定
|
|
56
|
+
|
|
57
|
+
テーマルートに `.liquid-preview/config.yml` を作成すると、プレビューに外部アセットを読み込めます。
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
head:
|
|
61
|
+
- <script src="https://cdn.tailwindcss.com"></script>
|
|
62
|
+
- ../src/styles/main.css
|
|
63
|
+
- ./src/components.js
|
|
64
|
+
|
|
65
|
+
port: 4567
|
|
66
|
+
host: 127.0.0.1
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`head` エントリには生の HTML タグまたはファイルパスを指定できます。ファイルパスはテーマルートからの相対パスで解決されます。
|
|
70
|
+
|
|
71
|
+
## 開発
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
git clone https://github.com/sena-m09/liquidbook.git
|
|
75
|
+
cd liquidbook
|
|
76
|
+
bin/setup
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## ライセンス
|
|
80
|
+
|
|
81
|
+
[MIT License](https://opensource.org/licenses/MIT)
|
data/exe/liquidbook
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
shop:
|
|
2
|
+
name: "Preview Shop"
|
|
3
|
+
url: "https://preview-shop.myshopify.com"
|
|
4
|
+
currency: "JPY"
|
|
5
|
+
money_format: "¥{{amount}}"
|
|
6
|
+
|
|
7
|
+
product:
|
|
8
|
+
title: "サンプル商品"
|
|
9
|
+
handle: "sample-product"
|
|
10
|
+
description: "<p>これはプレビュー用のサンプル商品です。</p>"
|
|
11
|
+
price: 1980
|
|
12
|
+
compare_at_price: 2980
|
|
13
|
+
available: true
|
|
14
|
+
type: "サンプル"
|
|
15
|
+
vendor: "Preview Vendor"
|
|
16
|
+
featured_image:
|
|
17
|
+
src: "https://placehold.co/600x600/EEE/31343C?text=Product"
|
|
18
|
+
alt: "サンプル商品画像"
|
|
19
|
+
width: 600
|
|
20
|
+
height: 600
|
|
21
|
+
images:
|
|
22
|
+
- src: "https://placehold.co/600x600/EEE/31343C?text=Image+1"
|
|
23
|
+
alt: "商品画像 1"
|
|
24
|
+
- src: "https://placehold.co/600x600/EEE/31343C?text=Image+2"
|
|
25
|
+
alt: "商品画像 2"
|
|
26
|
+
variants:
|
|
27
|
+
- title: "デフォルト"
|
|
28
|
+
price: 1980
|
|
29
|
+
available: true
|
|
30
|
+
sku: "SAMPLE-001"
|
|
31
|
+
tags:
|
|
32
|
+
- "sample"
|
|
33
|
+
- "preview"
|
|
34
|
+
|
|
35
|
+
collection:
|
|
36
|
+
title: "サンプルコレクション"
|
|
37
|
+
handle: "sample-collection"
|
|
38
|
+
description: "プレビュー用コレクション"
|
|
39
|
+
products_count: 12
|
|
40
|
+
products:
|
|
41
|
+
- title: "商品 1"
|
|
42
|
+
handle: "product-1"
|
|
43
|
+
price: 1980
|
|
44
|
+
featured_image:
|
|
45
|
+
src: "https://placehold.co/400x400/EEE/31343C?text=1"
|
|
46
|
+
- title: "商品 2"
|
|
47
|
+
handle: "product-2"
|
|
48
|
+
price: 2980
|
|
49
|
+
featured_image:
|
|
50
|
+
src: "https://placehold.co/400x400/EEE/31343C?text=2"
|
|
51
|
+
- title: "商品 3"
|
|
52
|
+
handle: "product-3"
|
|
53
|
+
price: 3980
|
|
54
|
+
featured_image:
|
|
55
|
+
src: "https://placehold.co/400x400/EEE/31343C?text=3"
|
|
56
|
+
|
|
57
|
+
cart:
|
|
58
|
+
item_count: 2
|
|
59
|
+
total_price: 4960
|
|
60
|
+
items:
|
|
61
|
+
- title: "商品 1"
|
|
62
|
+
quantity: 1
|
|
63
|
+
price: 1980
|
|
64
|
+
- title: "商品 2"
|
|
65
|
+
quantity: 1
|
|
66
|
+
price: 2980
|
|
67
|
+
|
|
68
|
+
page:
|
|
69
|
+
title: "サンプルページ"
|
|
70
|
+
handle: "sample-page"
|
|
71
|
+
content: "<p>プレビュー用のページコンテンツです。</p>"
|
|
72
|
+
|
|
73
|
+
customer:
|
|
74
|
+
first_name: "太郎"
|
|
75
|
+
last_name: "テスト"
|
|
76
|
+
email: "test@example.com"
|
|
77
|
+
|
|
78
|
+
request:
|
|
79
|
+
locale:
|
|
80
|
+
iso_code: "ja"
|
|
81
|
+
name: "Japanese"
|
|
82
|
+
host: "localhost:4567"
|
|
83
|
+
|
|
84
|
+
settings:
|
|
85
|
+
type_header_font:
|
|
86
|
+
family: "sans-serif"
|
|
87
|
+
type_body_font:
|
|
88
|
+
family: "sans-serif"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Liquidbook
|
|
7
|
+
class CLI < Thor
|
|
8
|
+
default_command :server
|
|
9
|
+
|
|
10
|
+
desc "server", "Start the preview server"
|
|
11
|
+
option :port, type: :numeric, default: 4567, aliases: "-p", desc: "Port number"
|
|
12
|
+
option :host, type: :string, default: "127.0.0.1", aliases: "-H", desc: "Host to bind"
|
|
13
|
+
option :root, type: :string, default: ".", aliases: "-r", desc: "Theme root directory"
|
|
14
|
+
def server
|
|
15
|
+
theme_root = File.expand_path(options[:root])
|
|
16
|
+
Liquidbook.root = theme_root
|
|
17
|
+
|
|
18
|
+
pid_manager = PidManager.new(theme_root: theme_root)
|
|
19
|
+
result = pid_manager.ensure_can_start!
|
|
20
|
+
warn "Warning: Removed stale PID file." if result == :stale_pid_cleaned
|
|
21
|
+
|
|
22
|
+
puts "Liquidbook v#{VERSION}"
|
|
23
|
+
puts "Theme root: #{theme_root}"
|
|
24
|
+
puts "Server: http://#{options[:host]}:#{options[:port]}"
|
|
25
|
+
puts ""
|
|
26
|
+
|
|
27
|
+
sections = Dir.glob(File.join(theme_root, "sections", "*.liquid")).size
|
|
28
|
+
snippets = Dir.glob(File.join(theme_root, "snippets", "*.liquid")).size
|
|
29
|
+
puts "Found #{sections} sections, #{snippets} snippets"
|
|
30
|
+
puts ""
|
|
31
|
+
|
|
32
|
+
pid_manager.write_pid
|
|
33
|
+
at_exit { pid_manager.remove_pid }
|
|
34
|
+
|
|
35
|
+
Server::App.set :port, options[:port]
|
|
36
|
+
Server::App.set :bind, options[:host]
|
|
37
|
+
Server::App.run!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc "stop", "Stop the running preview server"
|
|
41
|
+
option :root, type: :string, default: ".", aliases: "-r", desc: "Theme root directory"
|
|
42
|
+
def stop
|
|
43
|
+
theme_root = File.expand_path(options[:root])
|
|
44
|
+
pid_manager = PidManager.new(theme_root: theme_root)
|
|
45
|
+
|
|
46
|
+
case pid_manager.stop!
|
|
47
|
+
when :stopped
|
|
48
|
+
puts "Server stopped."
|
|
49
|
+
when :not_running
|
|
50
|
+
puts "Server is not running."
|
|
51
|
+
when :stale_pid_cleaned
|
|
52
|
+
puts "Server is not running (removed stale PID file)."
|
|
53
|
+
end
|
|
54
|
+
rescue Liquidbook::Error => e
|
|
55
|
+
warn "Error: #{e.message}"
|
|
56
|
+
exit 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
desc "render TEMPLATE", "Render a single template to stdout"
|
|
60
|
+
option :root, type: :string, default: ".", aliases: "-r", desc: "Theme root directory"
|
|
61
|
+
def render(template)
|
|
62
|
+
theme_root = File.expand_path(options[:root])
|
|
63
|
+
Liquidbook.root = theme_root
|
|
64
|
+
|
|
65
|
+
renderer = ThemeRenderer.new(theme_root: theme_root)
|
|
66
|
+
|
|
67
|
+
if template.start_with?("sections/") || template.start_with?("snippets/")
|
|
68
|
+
dir, name = template.split("/", 2)
|
|
69
|
+
name = name.sub(/\.liquid$/, "")
|
|
70
|
+
html = dir == "sections" ? renderer.render_section(name) : renderer.render_snippet(name)
|
|
71
|
+
else
|
|
72
|
+
html = renderer.render_section(template.sub(/\.liquid$/, ""))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
puts html
|
|
76
|
+
rescue Liquidbook::Error => e
|
|
77
|
+
warn "Error: #{e.message}"
|
|
78
|
+
exit 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
desc "version", "Show version"
|
|
82
|
+
def version
|
|
83
|
+
puts "liquidbook #{VERSION}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Liquidbook
|
|
6
|
+
# Loads .liquid-preview/config.yml from the theme root
|
|
7
|
+
#
|
|
8
|
+
# head entries can be:
|
|
9
|
+
# - Raw HTML: <script src="https://cdn.tailwindcss.com"></script>
|
|
10
|
+
# - File path: ../src-lit/styles/main.css
|
|
11
|
+
# - File path: ./src/components.js
|
|
12
|
+
#
|
|
13
|
+
# File paths are resolved relative to the theme root and served via /__imports__/
|
|
14
|
+
class Config
|
|
15
|
+
DEFAULT_CONFIG = {
|
|
16
|
+
"head" => [],
|
|
17
|
+
"port" => 4567,
|
|
18
|
+
"host" => "127.0.0.1"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
IMPORT_PATH_PREFIX = "/__imports__"
|
|
22
|
+
|
|
23
|
+
def initialize(theme_root: nil)
|
|
24
|
+
@theme_root = theme_root || Liquidbook.root
|
|
25
|
+
@data = load_config
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns HTML strings ready to inject into <head>
|
|
29
|
+
def head_tags_html
|
|
30
|
+
Array(@data["head"]).map { |entry| entry_to_html(entry) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns a map of serve_path => absolute_file_path for file imports
|
|
34
|
+
def import_files
|
|
35
|
+
@import_files ||= build_import_map
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def port
|
|
39
|
+
@data["port"] || 4567
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def host
|
|
43
|
+
@data["host"] || "127.0.0.1"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def [](key)
|
|
47
|
+
@data[key.to_s]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def load_config
|
|
53
|
+
path = File.join(@theme_root, ".liquid-preview", "config.yml")
|
|
54
|
+
return DEFAULT_CONFIG.dup unless File.exist?(path)
|
|
55
|
+
|
|
56
|
+
loaded = YAML.safe_load(File.read(path), permitted_classes: []) || {}
|
|
57
|
+
DEFAULT_CONFIG.merge(loaded)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def file_path?(entry)
|
|
61
|
+
# Not raw HTML, looks like a path
|
|
62
|
+
!entry.strip.start_with?("<") && entry.match?(/\.\w+$/)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def resolve_path(entry)
|
|
66
|
+
File.expand_path(entry.strip, @theme_root)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def serve_path(entry)
|
|
70
|
+
# Use resolved absolute path for a deterministic, safe URL
|
|
71
|
+
abs = resolve_path(entry)
|
|
72
|
+
safe_name = abs.gsub(/[^a-zA-Z0-9._-]/, "_")
|
|
73
|
+
"#{IMPORT_PATH_PREFIX}/#{safe_name}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def entry_to_html(entry)
|
|
77
|
+
if file_path?(entry)
|
|
78
|
+
url = serve_path(entry)
|
|
79
|
+
ext = File.extname(entry.strip).downcase
|
|
80
|
+
case ext
|
|
81
|
+
when ".css"
|
|
82
|
+
%(<link rel="stylesheet" href="#{url}">)
|
|
83
|
+
when ".js", ".mjs", ".ts"
|
|
84
|
+
%(<script type="module" src="#{url}"></script>)
|
|
85
|
+
else
|
|
86
|
+
%(<link href="#{url}">)
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
entry.strip
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_import_map
|
|
94
|
+
map = {}
|
|
95
|
+
Array(@data["head"]).each do |entry|
|
|
96
|
+
next unless file_path?(entry)
|
|
97
|
+
|
|
98
|
+
abs = resolve_path(entry)
|
|
99
|
+
map[serve_path(entry)] = abs
|
|
100
|
+
end
|
|
101
|
+
map
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liquidbook
|
|
4
|
+
module Filters
|
|
5
|
+
# Common Shopify Liquid filters for local preview
|
|
6
|
+
module ShopifyFilters
|
|
7
|
+
# Asset URL filters
|
|
8
|
+
def asset_url(input)
|
|
9
|
+
"/assets/#{input}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def asset_img_url(input, size = nil)
|
|
13
|
+
return input unless input.is_a?(String)
|
|
14
|
+
|
|
15
|
+
"/assets/#{input}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def image_url(input, width: nil, height: nil, **_opts)
|
|
19
|
+
return "" unless input
|
|
20
|
+
|
|
21
|
+
src = input.is_a?(Hash) ? input["src"] : input.to_s
|
|
22
|
+
params = []
|
|
23
|
+
params << "width=#{width}" if width
|
|
24
|
+
params << "height=#{height}" if height
|
|
25
|
+
params.empty? ? src : "#{src}?#{params.join("&")}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def img_tag(input, alt: "")
|
|
29
|
+
%(<img src="#{input}" alt="#{alt}" loading="lazy">)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Money filters
|
|
33
|
+
def money(input)
|
|
34
|
+
return "" unless input
|
|
35
|
+
|
|
36
|
+
"¥#{format("%d", input.to_i)}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def money_with_currency(input)
|
|
40
|
+
"#{money(input)} JPY"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# String filters (Shopify extensions)
|
|
44
|
+
def handle(input)
|
|
45
|
+
return "" unless input
|
|
46
|
+
|
|
47
|
+
input.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def handleize(input)
|
|
51
|
+
handle(input)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def pluralize(input, singular, plural)
|
|
55
|
+
input.to_i == 1 ? singular : plural
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# URL filters
|
|
59
|
+
def link_to(input, url, title = "")
|
|
60
|
+
title_attr = title.to_s.empty? ? "" : %( title="#{title}")
|
|
61
|
+
%(<a href="#{url}"#{title_attr}>#{input}</a>)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def within(url, collection_url)
|
|
65
|
+
"#{collection_url}#{url}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def stylesheet_tag(url)
|
|
69
|
+
%(<link rel="stylesheet" href="#{url}" type="text/css">)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def script_tag(url)
|
|
73
|
+
%(<script src="#{url}"></script>)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Collection / pagination helpers
|
|
77
|
+
def paginate(input, page_size)
|
|
78
|
+
input
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def default_pagination(_input)
|
|
82
|
+
""
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# JSON / data
|
|
86
|
+
def json(input)
|
|
87
|
+
require "json"
|
|
88
|
+
input.to_json
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Translation stub
|
|
92
|
+
def t(input)
|
|
93
|
+
input.to_s
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Placeholder image
|
|
97
|
+
def placeholder_svg_tag(type)
|
|
98
|
+
%(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" fill="#eee"/><text x="50" y="50" text-anchor="middle" dy=".3em" fill="#999">#{type}</text></svg>)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Color filters
|
|
102
|
+
def color_to_rgb(input)
|
|
103
|
+
input.to_s
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def color_modify(input, _attr, _value)
|
|
107
|
+
input.to_s
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def color_to_hex(input)
|
|
111
|
+
input.to_s
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def color_brightness(input)
|
|
115
|
+
128
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Font filters
|
|
119
|
+
def font_modify(input, _attr, _value)
|
|
120
|
+
input
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def font_url(input)
|
|
124
|
+
""
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def font_face(input)
|
|
128
|
+
""
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Media filters
|
|
132
|
+
def external_video_url(input, **_opts)
|
|
133
|
+
input.to_s
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def media_tag(input, **_opts)
|
|
137
|
+
%(<div class="media-placeholder">#{input}</div>)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Metafield
|
|
141
|
+
def metafield_tag(input)
|
|
142
|
+
input.to_s
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Liquidbook
|
|
6
|
+
# Provides mock Shopify objects for template rendering
|
|
7
|
+
class MockData
|
|
8
|
+
DEFAULT_FIXTURES_PATH = File.expand_path("../../fixtures/default_mocks.yml", __dir__)
|
|
9
|
+
|
|
10
|
+
def initialize(theme_root: nil)
|
|
11
|
+
@theme_root = theme_root || Liquidbook.root
|
|
12
|
+
@data = load_data
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_assigns
|
|
16
|
+
@data.dup
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Merge section schema defaults into the data
|
|
20
|
+
def with_section(schema_parser)
|
|
21
|
+
assigns = to_assigns
|
|
22
|
+
assigns["section"] = {
|
|
23
|
+
"settings" => schema_parser.default_settings,
|
|
24
|
+
"blocks" => schema_parser.default_blocks
|
|
25
|
+
}
|
|
26
|
+
assigns
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def load_data
|
|
32
|
+
data = load_yaml(DEFAULT_FIXTURES_PATH)
|
|
33
|
+
|
|
34
|
+
# Override with user's custom mocks if present
|
|
35
|
+
user_mocks = File.join(@theme_root, ".liquid-preview", "mocks.yml")
|
|
36
|
+
data = deep_merge(data, load_yaml(user_mocks)) if File.exist?(user_mocks)
|
|
37
|
+
|
|
38
|
+
data
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def load_yaml(path)
|
|
42
|
+
return {} unless File.exist?(path)
|
|
43
|
+
|
|
44
|
+
YAML.safe_load(File.read(path), permitted_classes: [Date, Time]) || {}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def deep_merge(base, override)
|
|
48
|
+
base.merge(override) do |_key, old_val, new_val|
|
|
49
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
50
|
+
deep_merge(old_val, new_val)
|
|
51
|
+
else
|
|
52
|
+
new_val
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|