papercraft 1.4 → 2.13

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,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './compiler'
4
+
5
+ # Extensions to the Proc class.
6
+ class ::Proc
7
+ # Returns the compiled form code for the proc.
8
+ #
9
+ # @return [String] compiled proc code
10
+ def compiled_code
11
+ Papercraft::Compiler.compile_to_code(self).last
12
+ end
13
+
14
+ # Returns the source map for the compiled proc.
15
+ #
16
+ # @return [Array<String>] source map
17
+ def source_map
18
+ loc = source_location
19
+ fn = compiled? ? loc.first : Papercraft::Compiler.source_location_to_fn(loc)
20
+ Papercraft::Compiler.source_map_store[fn]
21
+ end
22
+
23
+ # Returns the AST for the proc.
24
+ #
25
+ # @return [Prism::Node] AST root
26
+ def ast
27
+ Sirop.to_ast(self)
28
+ end
29
+
30
+ # Returns true if proc is marked as compiled.
31
+ #
32
+ # @return [bool] is the proc marked as compiled
33
+ def compiled?
34
+ @is_compiled
35
+ end
36
+
37
+ # Marks the proc as compiled, i.e. can render directly and takes a string
38
+ # buffer as first argument.
39
+ #
40
+ # @return [self]
41
+ def compiled!
42
+ @is_compiled = true
43
+ self
44
+ end
45
+
46
+ # Returns the compiled proc for the given proc. If marked as compiled, returns
47
+ # self.
48
+ #
49
+ # @param mode [Symbol] compilation mode (:html, :xml)
50
+ # @return [Proc] compiled proc or self
51
+ def compiled_proc(mode: :html)
52
+ @compiled_proc ||= @is_compiled ? self : compile(mode:)
53
+ end
54
+
55
+ # Compiles the proc into the compiled form.
56
+ #
57
+ # @param mode [Symbol] compilation mode (:html, :xml)
58
+ # @return [Proc] compiled proc
59
+ def compile(mode: :html)
60
+ Papercraft::Compiler.compile(self, mode:).compiled!
61
+ rescue Sirop::Error
62
+ raise Papercraft::Error, "Dynamically defined procs cannot be compiled"
63
+ end
64
+
65
+ # Renders the proc to HTML with the given arguments.
66
+ #
67
+ # @return [String] HTML string
68
+ def render(*a, **b, &c)
69
+ compiled_proc.(+'', *a, **b, &c)
70
+ rescue Exception => e
71
+ e.is_a?(Papercraft::Error) ? raise : raise(Papercraft.translate_backtrace(e))
72
+ end
73
+
74
+ # Renders the proc to XML with the given arguments.
75
+ #
76
+ # @return [String] XML string
77
+ def render_xml(*a, **b, &c)
78
+ compiled_proc(mode: :xml).(+'', *a, **b, &c)
79
+ rescue Exception => e
80
+ e.is_a?(Papercraft::Error) ? raise : raise(Papercraft.translate_backtrace(e))
81
+ end
82
+
83
+ # Renders the proc to HTML with the given arguments into the given buffer.
84
+ #
85
+ # @param buf [String] buffer
86
+ # @return [String] HTML string
87
+ def render_to_buffer(buf, *a, **b, &c)
88
+ compiled_proc.(buf, *a, **b, &c)
89
+ rescue Exception => e
90
+ raise Papercraft.translate_backtrace(e)
91
+ end
92
+
93
+ # Returns a proc that applies the given arguments to the original proc.
94
+ #
95
+ # @return [Proc] applied proc
96
+ def apply(*a, **b, &c)
97
+ compiled = compiled_proc
98
+ c_compiled = c&.compiled_proc
99
+
100
+ ->(__buffer__, *x, **y, &z) {
101
+ c_proc = c_compiled && ->(__buffer__, *d, **e) {
102
+ c_compiled.(__buffer__, *a, *d, **b, **e, &z)
103
+ }.compiled!
104
+
105
+ compiled.(__buffer__, *a, *x, **b, **y, &c_proc)
106
+ }.compiled!
107
+ end
108
+
109
+ # Caches and returns the rendered HTML for the template with the given
110
+ # arguments.
111
+ #
112
+ # @return [String] HTML string
113
+ def render_cached(*args, **kargs, &block)
114
+ @render_cache ||= {}
115
+ key = args.empty? && kargs.empty? && !block ? nil : [args, kargs, block&.source_location]
116
+ @render_cache[key] ||= render(*args, **kargs, &block)
117
+ end
118
+ end
@@ -1,207 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './html'
4
-
5
3
  module Papercraft
6
-
7
- # Template represents a distinct, reusable HTML template. A template can
8
- # include other templates, and also be nested inside other templates.
9
- #
10
- # Since in Papercraft HTML is expressed using blocks (or procs,) the Template
11
- # class is simply a special kind of Proc, which has some enhanced
12
- # capabilities, allowing it to be easily composed in a variety of ways.
13
- #
14
- # Templates are usually created using the class methods `html`, `xml` or
15
- # `json`, for HTML, XML or JSON templates, respectively:
16
- #
17
- # greeter = Papercraft.html { |name| h1 "Hello, #{name}!" }
18
- # greeter.render('world') #=> "<h1>Hello, world!</h1>"
19
- #
20
- # Templates can also be created using the normal constructor:
21
- #
22
- # greeter = Papercraft::Template.new(mode: :html) { |name| h1 "Hello, #{name}!" }
23
- # greeter.render('world') #=> "<h1>Hello, world!</h1>"
24
- #
25
- # The different methods for creating templates can also take a custom MIME
26
- # type, by passing a `mime_type` named argument:
27
- #
28
- # json = Papercraft.json(mime_type: 'application/feed+json') { ... }
29
- #
30
- # In the template block, HTML elements are created by simply calling
31
- # unqualified methods:
32
- #
33
- # page_layout = Papercraft.html {
34
- # html5 {
35
- # head {
36
- # title 'foo'
37
- # }
38
- # body {
39
- # h1 "Hello, world!"
40
- # }
41
- # }
42
- # }
43
- #
44
- # Papercraft templates can take explicit parameters in order to render
45
- # dynamic content. This can be in the form of regular or named parameters. The
46
- # `greeter` template shown above takes a single `name` parameter. Here's how a
47
- # anchor template could be implemented with named parameters:
48
- #
49
- # anchor = Papercraft.html { |uri: , text: | a(text, href: uri) }
50
- #
51
- # The above template could later be rendered by passing the needed arguments:
52
- #
53
- # anchor.render(uri: 'https://example.com', text: 'Example')
54
- #
55
- # ## Template Composition
56
- #
57
- # A template can be included in another template using the `emit` method:
58
- #
59
- # links = Papercraft.html {
60
- # emit anchor, uri: '/posts', text: 'Posts'
61
- # emit anchor, uri: '/archive', text: 'Archive'
62
- # emit anchor, uri: '/about', text: 'About'
63
- # }
64
- #
65
- # Another way of composing templates is to pass the templates themselves as
66
- # parameters:
67
- #
68
- # links = Papercraft.html { |anchors|
69
- # anchors.each { |a| emit a }
70
- # }
71
- # links.render([
72
- # anchor.apply(uri: '/posts', text: 'Posts'),
73
- # anchor.apply(uri: '/archive', text: 'Archive'),
74
- # anchor.apply(uri: '/about', text: 'About')
75
- # ])
76
- #
77
- # The `#apply` method creates a new template, applying the given parameters
78
- # such that the template can be rendered without parameters:
79
- #
80
- # links_with_anchors = links.apply([
81
- # anchor.apply(uri: '/posts', text: 'Posts'),
82
- # anchor.apply(uri: '/archive', text: 'Archive'),
83
- # anchor.apply(uri: '/about', text: 'About')
84
- # ])
85
- # links_with_anchors.render
86
- #
87
- class Template < Proc
88
-
89
- # Determines the rendering mode: `:html` or `:xml`.
90
- attr_accessor :mode
91
-
92
- STOCK_MIME_TYPE = {
93
- html: 'text/html',
94
- xml: 'application/xml',
95
- json: 'application/json'
96
- }.freeze
97
-
98
- # Initializes a template with the given block. The rendering mode (HTML or
99
- # XML) can be passed in the `mode:` parameter. If `mode:` is not specified,
100
- # the template defaults to HTML.
101
- #
102
- # @param mode [:html, :xml] rendering mode
103
- # @param mime_type [String, nil] the template's mime type (nil for default)
104
- # @param block [Proc] nested HTML block
105
- def initialize(mode: :html, mime_type: nil, &block)
4
+ # Template wrapper class. This class can be used to distinguish between Papercraft
5
+ # templates and other kinds of procs.
6
+ class Template
7
+ attr_reader :proc, :mode
8
+
9
+ # @param proc [Proc] template proc
10
+ # @param mode [Symbol] mode (:html, :xml)
11
+ def initialize(proc, mode: :html)
12
+ @proc = proc
106
13
  @mode = mode
107
- @mime_type = mime_type || STOCK_MIME_TYPE[mode]
108
- super(&block)
109
14
  end
110
15
 
111
- H_EMPTY = {}.freeze
112
-
113
- # Renders the template with the given parameters and or block, and returns
114
- # the string result.
115
- #
116
- # @param *params [any] unnamed parameters
117
- # @param **named_params [any] named parameters
118
- # @return [String] rendered string
119
- def render(*a, **b, &block)
120
- template = self
121
- Renderer.verify_proc_parameters(template, a, b)
122
- renderer_class.new do
123
- push_emit_yield_block(block) if block
124
- instance_exec(*a, **b, &template)
125
- end.to_s
16
+ def render(*, **, &)
17
+ (mode == :xml) ? @proc.render_xml(*, **, &) : @proc.render(*, **, &)
126
18
  end
127
19
 
128
- # Renders a template fragment. Any given parameters are passed to the
129
- # template just like with {Template#render}. See also
130
- # {https://htmx.org/essays/template-fragments/ HTMX template fragments}.
131
- #
132
- # form = Papercraft.html { |action|
133
- # h1 'Hello'
134
- # fragment(:buttons) {
135
- # button action
136
- # button 'Cancel'
137
- # }
138
- # }
139
- # form.render_fragment(:buttons, 'foo') #=> "<button>foo</button><button>Cancel</buttons>"
140
- #
141
- # @param name [Symbol, String] fragment name
142
- # @param *params [any] unnamed parameters
143
- # @param **named_params [any] named parameters
144
- # @return [String] rendered string
145
- def render_fragment(name, *a, **b, &block)
146
- template = self
147
- Renderer.verify_proc_parameters(template, a, b)
148
- renderer_class.new(name) do
149
- push_emit_yield_block(block) if block
150
- instance_exec(*a, **b, &template)
151
- end.to_s
20
+ def apply(*, **, &)
21
+ Template.new(@proc.apply(*, **, &), mode: @mode)
152
22
  end
153
-
154
- # Creates a new template, applying the given parameters and or block to the
155
- # current one. Application is one of the principal methods of composing
156
- # templates, particularly when passing inner templates as blocks:
157
- #
158
- # article_wrapper = Papercraft.html {
159
- # article {
160
- # emit_yield
161
- # }
162
- # }
163
- # wrapped_article = article_wrapper.apply {
164
- # h1 'Article title'
165
- # }
166
- # wrapped_article.render #=> "<article><h1>Article title</h1></article>"
167
- #
168
- # @param *a [<any>] normal parameters
169
- # @param **b [Hash] named parameters
170
- # @return [Papercraft::Template] applied template
171
- def apply(*a, **b, &block)
172
- template = self
173
- Template.new(mode: @mode, mime_type: @mime_type, &proc do |*x, **y|
174
- push_emit_yield_block(block) if block
175
- instance_exec(*a, *x, **b, **y, &template)
176
- end)
177
- end
178
-
179
- # Returns the Renderer class used for rendering the templates, according to
180
- # the template's mode.
181
- #
182
- # @return [Papercraft::Renderer] Renderer used for rendering the template
183
- def renderer_class
184
- case @mode
185
- when :html
186
- HTMLRenderer
187
- when :xml
188
- XMLRenderer
189
- when :json
190
- JSONRenderer
191
- else
192
- raise "Invalid mode #{@mode.inspect}"
193
- end
194
- end
195
-
196
- # Returns the template's associated MIME type.
197
- #
198
- # @return [String] MIME type
199
- def mime_type
200
- @mime_type
201
- end
202
-
203
- def compile(*args)
204
- Papercraft::Compiler.new.compile(self, *args)
23
+
24
+ def compiled_proc
25
+ @proc.compiled_proc(mode: @mode)
205
26
  end
206
27
  end
207
28
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Papercraft
4
- VERSION = '1.4'
4
+ VERSION = '2.13'
5
5
  end
data/lib/papercraft.rb CHANGED
@@ -1,109 +1,133 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'papercraft/template'
4
- require_relative 'papercraft/renderer'
5
- # require_relative 'papercraft/compiler'
4
+ require_relative 'papercraft/compiler'
5
+ require_relative 'papercraft/proc_ext'
6
6
 
7
- # Papercraft is a composable templating library
7
+ # Papercraft is a functional templating library. In Papercraft, templates are expressed as plain
8
+ # Ruby procs.
8
9
  module Papercraft
9
10
  # Exception class used to signal templating-related errors
10
11
  class Error < RuntimeError; end
11
12
 
12
- class << self
13
-
14
- # Installs one or more extensions. Extensions enhance templating capabilities
15
- # by adding namespaced methods to emplates. An extension is implemented as a
16
- # Ruby module containing one or more methods. Each method in the extension
17
- # module can be used to render a specific HTML element or a set of elements.
18
- #
19
- # This is a convenience method. For more information on using Papercraft
20
- # extensions, see `Papercraft::Renderer::extension`
21
- #
22
- # @param map [Hash] hash mapping methods to extension modules
23
- # @return [void]
24
- def extension(map)
25
- Renderer.extension(map)
26
- end
13
+ extend self
27
14
 
28
- # Creates a new papercraft template. `Papercraft.html` can take either a proc
29
- # argument or a block. In both cases, the proc is converted to a
30
- # `Papercraft::Template`.
31
- #
32
- # Papercraft.html(proc { h1 'hi' }).render #=> "<h1>hi</h1>"
33
- # Papercraft.html { h1 'hi' }.render #=> "<h1>hi</h1>"
34
- #
35
- # @param template [Proc] template block
36
- # @return [Papercraft::Template] Papercraft template
37
- def html(o = nil, mime_type: nil, &template)
38
- return o if o.is_a?(Papercraft::Template)
39
- template ||= o
40
- Papercraft::Template.new(mode: :html, mime_type: mime_type, &template)
41
- end
15
+ # Registry of Papercraft exgtensions
16
+ Extensions = {}
17
+
18
+ # Registers extensions to the Papercraft syntax.
19
+ #
20
+ # @param spec [Hash] hash mapping symbols to procs
21
+ # @return [self]
22
+ def extension(spec)
23
+ Extensions.merge!(spec)
24
+ self
25
+ end
26
+
27
+ # Formats the given string, converting underscores to dashes.
28
+ #
29
+ # @param tag [String, Symbol] input string
30
+ # @return [String] output string
31
+ def underscores_to_dashes(tag)
32
+ tag.to_s.gsub('_', '-')
33
+ end
42
34
 
43
- # Creates a new Papercraft template in XML mode. `Papercraft.xml` can take
44
- # either a proc argument or a block. In both cases, the proc is converted to a
45
- # `Papercraft::Template`.
46
- #
47
- # Papercraft.xml(proc { item 'foo' }).render #=> "<item>foo</item>"
48
- # Papercraft.xml { item 'foo' }.render #=> "<item>foo</item>"
49
- #
50
- # @param template [Proc] template block
51
- # @return [Papercraft::Template] Papercraft template
52
- def xml(o = nil, mime_type: nil, &template)
53
- return o if o.is_a?(Papercraft::Template)
54
- template ||= o
55
- Papercraft::Template.new(mode: :xml, mime_type: mime_type, &template)
35
+ # Formats the given hash as tag attributes.
36
+ #
37
+ # @param attrs [Hash] input hash
38
+ # @return [String] formatted attributes
39
+ def format_tag_attrs(attrs)
40
+ attrs.each_with_object(+'') do |(k, v), html|
41
+ case v
42
+ when nil, false
43
+ when true
44
+ html << ' ' if !html.empty?
45
+ html << underscores_to_dashes(k)
46
+ else
47
+ html << ' ' if !html.empty?
48
+ v = v.join(' ') if v.is_a?(Array)
49
+ html << "#{underscores_to_dashes(k)}=\"#{v}\""
50
+ end
56
51
  end
52
+ end
53
+
54
+ # Translates entries in exception's backtrace to point to original source code.
55
+ #
56
+ # @param err [Exception] raised exception
57
+ # @return [Exception] raised exception
58
+ def translate_backtrace(err)
59
+ cache = {}
60
+ is_argument_error = err.is_a?(ArgumentError) && err.backtrace[0] =~ /^\:\:/
61
+ backtrace = err.backtrace.map { |e| compute_backtrace_entry(e, cache) }
62
+
63
+ return make_argument_error(err, backtrace) if is_argument_error
64
+
65
+ err.set_backtrace(backtrace)
66
+ err
67
+ end
57
68
 
58
- # Creates a new Papercraft template in JSON mode. `Papercraft.json` can take
59
- # either a proc argument or a block. In both cases, the proc is converted to a
60
- # `Papercraft::Template`.
61
- #
62
- # Papercraft.json(proc { item 42 }).render #=> "[42]"
63
- # Papercraft.json { foo 'bar' }.render #=> "{\"foo\": \"bar\"}"
64
- #
65
- # @param template [Proc] template block
66
- # @return [Papercraft::Template] Papercraft template
67
- def json(o = nil, mime_type: nil, &template)
68
- return o if o.is_a?(Papercraft::Template)
69
- template ||= o
70
- Papercraft::Template.new(mode: :json, mime_type: mime_type, &template)
69
+ # Computes a backtrace entry with caching.
70
+ #
71
+ # @param entry [String] backtrace entry
72
+ # @param cache [Hash] cache store mapping compiled filename to source_map
73
+ def compute_backtrace_entry(entry, cache)
74
+ m = entry.match(/^((\:\:\(.+\:.+\))\:(\d+))/)
75
+ return entry if !m
76
+
77
+ fn = m[2]
78
+ line = m[3].to_i
79
+ source_map = cache[fn] ||= Compiler.source_map_store[fn]
80
+ return entry if !source_map
81
+
82
+ ref = source_map[line] || "?(#{line})"
83
+ entry.sub(m[1], ref)
84
+ end
85
+
86
+ def make_argument_error(err, backtrace)
87
+ m = err.message.match(/(given (\d+), expected (\d+))/)
88
+ if m
89
+ rectified = format('given %d, expected %d', m[2].to_i - 1, m[3].to_i - 1)
90
+ message = err.message.gsub(m[1], rectified)
91
+ else
92
+ message = err.message
71
93
  end
94
+ ArgumentError.new(message).tap { it.set_backtrace(backtrace) }
95
+ end
72
96
 
73
- # Renders Markdown into HTML. The `opts` argument will be merged with the
74
- # default Kramdown options in order to change the rendering behaviour.
75
- #
76
- # @param markdown [String] Markdown
77
- # @param opts [Hash] Kramdown option overrides
78
- # @return [String] HTML
79
- def markdown(markdown, **opts)
80
- # require relevant deps on use
97
+ # Renders Markdown into HTML. The `opts` argument will be merged with the
98
+ # default Kramdown options in order to change the rendering behaviour.
99
+ #
100
+ # @param markdown [String] Markdown
101
+ # @param opts [Hash] Kramdown option overrides
102
+ # @return [String] HTML
103
+ def markdown(markdown, **opts)
104
+ @markdown_deps_loaded ||= true.tap do
81
105
  require 'kramdown'
82
106
  require 'rouge'
83
107
  require 'kramdown-parser-gfm'
84
-
85
- opts = default_kramdown_options.merge(opts)
86
- Kramdown::Document.new(markdown, **opts).to_html
87
108
  end
88
109
 
89
- # Returns the default Kramdown options used for rendering Markdown.
90
- #
91
- # @return [Hash] Kramdown options
92
- def default_kramdown_options
93
- @default_kramdown_options ||= {
94
- entity_output: :numeric,
95
- syntax_highlighter: :rouge,
96
- input: 'GFM',
97
- hard_wrap: false
98
- }
99
- end
110
+ opts = default_kramdown_options.merge(opts)
111
+ Kramdown::Document.new(markdown, **opts).to_html
112
+ end
100
113
 
101
- # Sets the default Kramdown options used for rendering Markdown.
102
- #
103
- # @param opts [Hash] Kramdown options
104
- # @return [void]
105
- def default_kramdown_options=(opts)
106
- @default_kramdown_options = opts
107
- end
114
+ # Returns the default Kramdown options used for rendering Markdown.
115
+ #
116
+ # @return [Hash] Kramdown options
117
+ def default_kramdown_options
118
+ @default_kramdown_options ||= {
119
+ entity_output: :numeric,
120
+ syntax_highlighter: :rouge,
121
+ input: 'GFM',
122
+ hard_wrap: false
123
+ }
124
+ end
125
+
126
+ # Sets the default Kramdown options used for rendering Markdown.
127
+ #
128
+ # @param opts [Hash] Kramdown options
129
+ # @return [Hash] Kramdown options
130
+ def default_kramdown_options=(opts)
131
+ @default_kramdown_options = opts
108
132
  end
109
133
  end