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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liquidbook
|
|
4
|
+
module Tags
|
|
5
|
+
# Handles {% render 'snippet' %} and {% render 'snippet' with object %}
|
|
6
|
+
# Also handles {% render 'snippet' for collection %}
|
|
7
|
+
class RenderTag < Liquid::Tag
|
|
8
|
+
SYNTAX = /['"]([^'"]+)['"](?:\s+(?:with|for)\s+(\S+)(?:\s+as\s+(\w+))?)?/
|
|
9
|
+
|
|
10
|
+
def initialize(tag_name, markup, tokens)
|
|
11
|
+
super
|
|
12
|
+
if markup.strip =~ SYNTAX
|
|
13
|
+
@snippet_name = Regexp.last_match(1)
|
|
14
|
+
@variable_expr = Regexp.last_match(2)
|
|
15
|
+
@alias_name = Regexp.last_match(3)
|
|
16
|
+
else
|
|
17
|
+
# Try simple variable assignments: {% render 'snippet', var: value %}
|
|
18
|
+
if markup.strip =~ /['"]([^'"]+)['"]\s*,?\s*(.*)/
|
|
19
|
+
@snippet_name = Regexp.last_match(1)
|
|
20
|
+
@inline_vars = parse_inline_vars(Regexp.last_match(2))
|
|
21
|
+
else
|
|
22
|
+
raise Liquid::SyntaxError, "Invalid syntax for render tag: #{markup}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render(context)
|
|
28
|
+
theme_root = context.registers[:theme_root] || Liquidbook.root
|
|
29
|
+
snippet_path = find_snippet(theme_root)
|
|
30
|
+
|
|
31
|
+
unless snippet_path
|
|
32
|
+
return "<!-- snippet '#{@snippet_name}' not found -->"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
source = File.read(snippet_path)
|
|
36
|
+
template = Liquid::Template.parse(source, environment: Liquidbook.environment)
|
|
37
|
+
|
|
38
|
+
# Build isolated scope
|
|
39
|
+
inner = {}
|
|
40
|
+
|
|
41
|
+
if @variable_expr
|
|
42
|
+
value = context[@variable_expr]
|
|
43
|
+
alias_key = @alias_name || @snippet_name
|
|
44
|
+
inner[alias_key] = value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if @inline_vars
|
|
48
|
+
@inline_vars.each do |key, expr|
|
|
49
|
+
inner[key] = context[expr] || expr
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Merge parent assigns with inner scope
|
|
54
|
+
parent_assigns = {}
|
|
55
|
+
context.environments.each { |env| parent_assigns.merge!(env) if env.is_a?(Hash) }
|
|
56
|
+
assigns = parent_assigns.merge(inner)
|
|
57
|
+
|
|
58
|
+
template.render(
|
|
59
|
+
assigns,
|
|
60
|
+
registers: { theme_root: theme_root }
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def find_snippet(theme_root)
|
|
67
|
+
paths = [
|
|
68
|
+
File.join(theme_root, "snippets", "#{@snippet_name}.liquid"),
|
|
69
|
+
File.join(theme_root, "sections", "#{@snippet_name}.liquid")
|
|
70
|
+
]
|
|
71
|
+
paths.find { |p| File.exist?(p) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parse_inline_vars(str)
|
|
75
|
+
return {} if str.nil? || str.strip.empty?
|
|
76
|
+
|
|
77
|
+
vars = {}
|
|
78
|
+
str.scan(/(\w+)\s*:\s*([^,]+)/) do |key, value|
|
|
79
|
+
vars[key.strip] = value.strip.gsub(/\A['"]|['"]\z/, "")
|
|
80
|
+
end
|
|
81
|
+
vars
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liquidbook
|
|
4
|
+
module Tags
|
|
5
|
+
# Handles {% section 'name' %} tags by rendering the referenced section file
|
|
6
|
+
class SectionTag < Liquid::Tag
|
|
7
|
+
SYNTAX = /['"](\w+)['"]/
|
|
8
|
+
|
|
9
|
+
def initialize(tag_name, markup, tokens)
|
|
10
|
+
super
|
|
11
|
+
if markup.strip =~ SYNTAX
|
|
12
|
+
@section_name = Regexp.last_match(1)
|
|
13
|
+
else
|
|
14
|
+
raise Liquid::SyntaxError, "Invalid syntax for section tag: #{markup}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render(context)
|
|
19
|
+
theme_root = context.registers[:theme_root] || Liquidbook.root
|
|
20
|
+
section_path = File.join(theme_root, "sections", "#{@section_name}.liquid")
|
|
21
|
+
|
|
22
|
+
unless File.exist?(section_path)
|
|
23
|
+
return "<!-- section '#{@section_name}' not found -->"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
source = File.read(section_path)
|
|
27
|
+
parser = SchemaParser.new(source)
|
|
28
|
+
template = Liquid::Template.parse(parser.template_without_schema, environment: Liquidbook.environment)
|
|
29
|
+
template.render(context)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liquidbook
|
|
4
|
+
# Renders a Liquid template with mock data and Shopify-compatible tags/filters
|
|
5
|
+
class ThemeRenderer
|
|
6
|
+
def initialize(theme_root: nil)
|
|
7
|
+
@theme_root = theme_root || Liquidbook.root
|
|
8
|
+
@mock_data = MockData.new(theme_root: @theme_root)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Render a section file by name
|
|
12
|
+
def render_section(name, overrides: {})
|
|
13
|
+
path = File.join(@theme_root, "sections", "#{name}.liquid")
|
|
14
|
+
raise Error, "Section not found: #{name}" unless File.exist?(path)
|
|
15
|
+
|
|
16
|
+
source = File.read(path)
|
|
17
|
+
render_source(source, section: true, overrides: overrides)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Render a snippet file by name, with optional parameter overrides
|
|
21
|
+
def render_snippet(name, overrides: {})
|
|
22
|
+
path = File.join(@theme_root, "snippets", "#{name}.liquid")
|
|
23
|
+
raise Error, "Snippet not found: #{name}" unless File.exist?(path)
|
|
24
|
+
|
|
25
|
+
source = File.read(path)
|
|
26
|
+
render_source(source, section: false, snippet_params: snippet_defaults(source).merge(overrides))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Render raw Liquid source
|
|
30
|
+
def render_source(source, section: false, snippet_params: {}, overrides: {})
|
|
31
|
+
parser = SchemaParser.new(source)
|
|
32
|
+
template_source = parser.template_without_schema
|
|
33
|
+
|
|
34
|
+
assigns = if section
|
|
35
|
+
@mock_data.with_section(parser)
|
|
36
|
+
else
|
|
37
|
+
@mock_data.to_assigns
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Merge snippet @param defaults
|
|
41
|
+
assigns.merge!(snippet_params)
|
|
42
|
+
|
|
43
|
+
# Merge any overrides from the UI
|
|
44
|
+
assigns.merge!(overrides)
|
|
45
|
+
|
|
46
|
+
template = Liquid::Template.parse(template_source, environment: Liquidbook.environment)
|
|
47
|
+
template.render(
|
|
48
|
+
assigns,
|
|
49
|
+
registers: { theme_root: @theme_root }
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Extract @param definitions from a snippet
|
|
54
|
+
def snippet_params(name)
|
|
55
|
+
path = File.join(@theme_root, "snippets", "#{name}.liquid")
|
|
56
|
+
return [] unless File.exist?(path)
|
|
57
|
+
|
|
58
|
+
ParamParser.new(File.read(path)).parse
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# List available sections
|
|
62
|
+
def sections
|
|
63
|
+
list_templates("sections")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# List available snippets
|
|
67
|
+
def snippets
|
|
68
|
+
list_templates("snippets")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def snippet_defaults(source)
|
|
74
|
+
ParamParser.new(source).default_assigns
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def list_templates(dir)
|
|
78
|
+
pattern = File.join(@theme_root, dir, "*.liquid")
|
|
79
|
+
Dir.glob(pattern).map { |f| File.basename(f, ".liquid") }.sort
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/liquidbook.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "liquid"
|
|
4
|
+
require_relative "liquidbook/version"
|
|
5
|
+
require_relative "liquidbook/config"
|
|
6
|
+
require_relative "liquidbook/schema_parser"
|
|
7
|
+
require_relative "liquidbook/param_parser"
|
|
8
|
+
require_relative "liquidbook/mock_data"
|
|
9
|
+
require_relative "liquidbook/filters/shopify_filters"
|
|
10
|
+
require_relative "liquidbook/tags/section_tag"
|
|
11
|
+
require_relative "liquidbook/tags/render_tag"
|
|
12
|
+
require_relative "liquidbook/pid_manager"
|
|
13
|
+
require_relative "liquidbook/theme_renderer"
|
|
14
|
+
require_relative "liquidbook/server/app"
|
|
15
|
+
|
|
16
|
+
module Liquidbook
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def root
|
|
21
|
+
@root || Dir.pwd
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_writer :root
|
|
25
|
+
|
|
26
|
+
def environment
|
|
27
|
+
@environment ||= build_environment
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def config
|
|
31
|
+
@config ||= Config.new(theme_root: root)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Reset (useful when reloading)
|
|
35
|
+
def reset!
|
|
36
|
+
@environment = nil
|
|
37
|
+
@config = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def build_environment
|
|
43
|
+
Liquid::Environment.build do |e|
|
|
44
|
+
e.register_filter(Filters::ShopifyFilters)
|
|
45
|
+
e.register_tag("section", Tags::SectionTag)
|
|
46
|
+
e.register_tag("render", Tags::RenderTag)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/sig/liquidbook.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: liquidbook
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- sena-m09
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: liquid
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: sinatra
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '4.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '4.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rackup
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: puma
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '6.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '6.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: thor
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.3'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.3'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: listen
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.9'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.9'
|
|
96
|
+
description: A development tool that renders Shopify Liquid templates locally with
|
|
97
|
+
mock data, providing a browser-based preview for sections and snippets.
|
|
98
|
+
email:
|
|
99
|
+
- sena-murakami@gaji.jp
|
|
100
|
+
executables:
|
|
101
|
+
- liquidbook
|
|
102
|
+
extensions: []
|
|
103
|
+
extra_rdoc_files: []
|
|
104
|
+
files:
|
|
105
|
+
- ".rspec"
|
|
106
|
+
- LICENSE
|
|
107
|
+
- README.md
|
|
108
|
+
- Rakefile
|
|
109
|
+
- docs/README.ja.md
|
|
110
|
+
- exe/liquidbook
|
|
111
|
+
- fixtures/default_mocks.yml
|
|
112
|
+
- lib/liquidbook.rb
|
|
113
|
+
- lib/liquidbook/cli.rb
|
|
114
|
+
- lib/liquidbook/config.rb
|
|
115
|
+
- lib/liquidbook/filters/shopify_filters.rb
|
|
116
|
+
- lib/liquidbook/mock_data.rb
|
|
117
|
+
- lib/liquidbook/param_parser.rb
|
|
118
|
+
- lib/liquidbook/pid_manager.rb
|
|
119
|
+
- lib/liquidbook/schema_parser.rb
|
|
120
|
+
- lib/liquidbook/server/app.rb
|
|
121
|
+
- lib/liquidbook/server/views/index.erb
|
|
122
|
+
- lib/liquidbook/server/views/layout.erb
|
|
123
|
+
- lib/liquidbook/server/views/preview.erb
|
|
124
|
+
- lib/liquidbook/tags/render_tag.rb
|
|
125
|
+
- lib/liquidbook/tags/section_tag.rb
|
|
126
|
+
- lib/liquidbook/theme_renderer.rb
|
|
127
|
+
- lib/liquidbook/version.rb
|
|
128
|
+
- sig/liquidbook.rbs
|
|
129
|
+
homepage: https://github.com/sena-m09/liquidbook
|
|
130
|
+
licenses:
|
|
131
|
+
- MIT
|
|
132
|
+
metadata:
|
|
133
|
+
homepage_uri: https://github.com/sena-m09/liquidbook
|
|
134
|
+
source_code_uri: https://github.com/sena-m09/liquidbook
|
|
135
|
+
changelog_uri: https://github.com/sena-m09/liquidbook/blob/main/CHANGELOG.md
|
|
136
|
+
rdoc_options: []
|
|
137
|
+
require_paths:
|
|
138
|
+
- lib
|
|
139
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - ">="
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: 3.1.0
|
|
144
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
145
|
+
requirements:
|
|
146
|
+
- - ">="
|
|
147
|
+
- !ruby/object:Gem::Version
|
|
148
|
+
version: '0'
|
|
149
|
+
requirements: []
|
|
150
|
+
rubygems_version: 3.6.9
|
|
151
|
+
specification_version: 4
|
|
152
|
+
summary: Storybook-like preview server for Shopify Liquid sections and snippets
|
|
153
|
+
test_files: []
|