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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquidbook
4
+ VERSION = "0.1.0"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module liquidbook
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []