1# frozen_string_literal: true
2
3require "blather"
4
5class FormTemplate
6 def initialize(template, filename="template", **kwargs)
7 @args = kwargs
8 @template = template
9 @filename = filename
10 freeze
11 end
12
13 def self.for(path, **kwargs)
14 if path.is_a?(FormTemplate)
15 raise "Sent args and a FormTemplate" unless kwargs.empty?
16
17 return path
18 end
19
20 full_path = File.dirname(__dir__) + "/forms/#{path}.rb"
21 new(File.read(full_path), full_path, **kwargs)
22 end
23
24 def self.render(path, context=OneRender.new, **kwargs)
25 self.for(path).render(context, **kwargs)
26 end
27
28 def render(context=OneRender.new, **kwargs)
29 one = context.merge(**@args).merge(**kwargs)
30 one.instance_eval(@template, @filename)
31 one.form
32 end
33
34 class OneRender
35 def initialize(**kwargs)
36 kwargs.each do |k, v|
37 instance_variable_set("@#{k}", v)
38 end
39 @__form = Blather::Stanza::X.new
40 @__builder = Nokogiri::XML::Builder.with(@__form)
41 end
42
43 def merge(**kwargs)
44 OneRender.new(**to_h.merge(kwargs))
45 end
46
47 def form!
48 @__type_set = true
49 @__form.type = :form
50 end
51
52 def result!
53 @__type_set = true
54 @__form.type = :result
55 end
56
57 def title(s)
58 @__form.title = s
59 end
60
61 def instructions(s)
62 @__form.instructions = s
63 end
64
65 def validate(field, datatype: nil, **kwargs)
66 Nokogiri::XML::Builder.with(field) do |xml|
67 xml.validate(
68 xmlns: "http://jabber.org/protocol/xdata-validate",
69 datatype: datatype || "xs:string"
70 ) do
71 xml.basic unless validation_type(xml, **kwargs)
72 end
73 end
74 end
75
76 def validation_type(xml, open: false, regex: nil, range: nil)
77 xml.open if open
78 xml.range(min: range.first, max: range.last) if range
79 xml.regex(regex.source) if regex
80 open || regex || range
81 end
82
83 def add_media(field, media)
84 Nokogiri::XML::Builder.with(field) do |xml|
85 xml.media(xmlns: "urn:xmpp:media-element") do
86 media.each do |item|
87 xml.uri(item[:uri], type: item[:type])
88 end
89 end
90 end
91 end
92
93 # Given a map of fields to labels, and a list of objects this will
94 # produce a table from calling each field's method on every object in the
95 # list. So, this list is value_semantics / OpenStruct style
96 def table(list, **fields)
97 keys = fields.keys
98 FormTable.new(
99 list.map { |x| keys.map { |k| x.public_send(k) } },
100 **fields
101 ).add_to_form(@__form)
102 end
103
104 def simple_child(field, name, xmlns, content)
105 return unless content
106
107 Nokogiri::XML::Builder.with(field) do |xml|
108 xml.public_send(name, content, xmlns: xmlns)
109 end
110 end
111
112 def field(
113 datatype: nil, open: false, regex: nil, range: nil,
114 suffix: nil, prefix: nil, media: [],
115 **kwargs
116 )
117 f = Blather::Stanza::X::Field.new(kwargs)
118 add_media(f, media) unless media.empty?
119 if datatype || open || regex || range
120 validate(f, datatype: datatype, open: open, regex: regex, range: range)
121 end
122 simple_child(f, :x, "https://ns.cheogram.com/suffix-label", suffix)
123 simple_child(f, :x, "https://ns.cheogram.com/prefix-label", prefix)
124 @__form.fields += [f]
125 end
126
127 def context
128 PartialRender.new(@__form, @__builder, **to_h)
129 end
130
131 def render(path, **kwargs)
132 FormTemplate.render(path, context, **kwargs)
133 end
134
135 def to_h
136 instance_variables
137 .reject { |sym| sym.to_s.start_with?("@__") }
138 .each_with_object({}) { |var, acc|
139 name = var.to_s[1..-1]
140 acc[name.to_sym] = instance_variable_get(var)
141 }
142 end
143
144 def xml
145 @__builder
146 end
147
148 def form
149 raise "Type never set" unless @__type_set
150
151 @__form
152 end
153
154 class PartialRender < OneRender
155 def initialize(form, builder, **kwargs)
156 kwargs.each do |k, v|
157 instance_variable_set("@#{k}", v)
158 end
159 @__form = form
160 @__builder = builder
161 end
162
163 def merge(**kwargs)
164 PartialRender.new(@__form, @__builder, **to_h.merge(kwargs))
165 end
166
167 # As a partial, we are not a complete form
168 def form; end
169
170 def form!
171 raise "Invalid 'form!' in Partial"
172 end
173
174 def result!
175 raise "Invalid 'result!' in Partial"
176 end
177
178 def title(_)
179 raise "Invalid 'title' in Partial"
180 end
181
182 def instructions(_)
183 raise "Invalid 'instructions' in Partial"
184 end
185 end
186 end
187end