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 }.compact) 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