# frozen_string_literal: true if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0") using Overrides::Symbol::Name end module Phlex class HTML DOCTYPE = "" STANDARD_ELEMENTS = { a: "a", abbr: "abbr", address: "address", article: "article", aside: "aside", b: "b", bdi: "bdi", bdo: "bdo", blockquote: "blockquote", body: "body", button: "button", caption: "caption", cite: "cite", code: "code", colgroup: "colgroup", data: "data", datalist: "datalist", dd: "dd", del: "del", details: "details", dfn: "dfn", dialog: "dialog", div: "div", dl: "dl", dt: "dt", em: "em", fieldset: "fieldset", figcaption: "figcaption", figure: "figure", footer: "footer", form: "form", g: "g", h1: "h1", h2: "h2", h3: "h3", h4: "h4", h5: "h5", h6: "h6", head: "head", header: "header", html: "html", i: "i", iframe: "iframe", ins: "ins", kbd: "kbd", label: "label", legend: "legend", li: "li", main: "main", map: "map", mark: "mark", menuitem: "menuitem", meter: "meter", nav: "nav", noscript: "noscript", object: "object", ol: "ol", optgroup: "optgroup", option: "option", output: "output", p: "p", path: "path", picture: "picture", pre: "pre", progress: "progress", q: "q", rp: "rp", rt: "rt", ruby: "ruby", s: "s", samp: "samp", script: "script", section: "section", select: "select", slot: "slot", small: "small", span: "span", strong: "strong", style: "style", sub: "sub", summary: "summary", sup: "sup", svg: "svg", table: "table", tbody: "tbody", td: "td", template_tag: "template", textarea: "textarea", tfoot: "tfoot", th: "th", thead: "thead", time: "time", title: "title", tr: "tr", u: "u", ul: "ul", video: "video", wbr: "wbr", }.freeze VOID_ELEMENTS = { area: "area", br: "br", embed: "embed", hr: "hr", img: "img", input: "input", link: "link", meta: "meta", param: "param", source: "source", track: "track", col: "col", }.freeze EVENT_ATTRIBUTES = %w[onabort onafterprint onbeforeprint onbeforeunload onblur oncanplay oncanplaythrough onchange onclick oncontextmenu oncopy oncuechange oncut ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended onerror onerror onfocus onhashchange oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart onmessage onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onoffline ononline onpagehide onpageshow onpaste onpause onplay onplaying onpopstate onprogress onratechange onreset onresize onscroll onsearch onseeked onseeking onselect onstalled onstorage onsubmit onsuspend ontimeupdate ontoggle onunload onvolumechange onwaiting onwheel].to_h { [_1, true] }.freeze extend Elements include Helpers class << self attr_accessor :rendered_at_least_once def call(...) new(...).call end alias_method :render, :call def new(*args, **kwargs, &block) if block object = super(*args, **kwargs, &nil) object.instance_variable_set(:@_content_block, block) object else super end end end def call(buffer = +"", view_context: nil, parent: nil, &block) @_target = buffer @_view_context = view_context @_parent = parent block ||= @_content_block return buffer unless render? around_template do if block if DeferredRender === self __vanish__(self, &block) template else template do |*args| if args.length > 0 yield_content_with_args(*args, &block) else yield_content(&block) end end end else template end end self.class.rendered_at_least_once = true buffer end def render(renderable, &block) case renderable when Phlex::HTML renderable.call(@_target, view_context: @_view_context, parent: self, &block) when Class if renderable < Phlex::HTML renderable.new.call(@_target, view_context: @_view_context, parent: self, &block) end else raise ArgumentError, "You can't render a #{renderable}." end nil end def format :html end STANDARD_ELEMENTS.each do |method_name, tag| register_element(method_name, tag: tag) end VOID_ELEMENTS.each do |method_name, tag| register_void_element(method_name, tag: tag) end def text(content) @_target << ERB::Util.html_escape( case content when String then content when Symbol then content.name when Integer then content.to_s else format_object(content) || content.to_s end ) nil end def whitespace @_target << " " if block_given? yield @_target << " " end nil end def comment(&block) @_target << "" nil end def doctype @_target << DOCTYPE nil end def unsafe_raw(content = nil) return nil unless content @_target << content end def capture return unless block_given? original_buffer = @_target new_buffer = +"" @_target = new_buffer yield new_buffer ensure @_target = original_buffer end # Like `capture` but the output is vanished into a BlackHole buffer. # Becuase the BlackHole does nothing with the output, this should be faster. def __vanish__(*args) return unless block_given? original_buffer = @_target @_target = BlackHole yield(*args) nil ensure @_target = original_buffer end # Default render predicate can be overridden to prevent rendering private def render? true end private def format_object(object) case object when Float object.to_s end end private def around_template before_template yield after_template end private def before_template nil end private def after_template nil end private def yield_content return unless block_given? original_length = @_target.length content = yield(self) unchanged = (original_length == @_target.length) if unchanged case content when String @_target << ERB::Util.html_escape(content) when Symbol @_target << ERB::Util.html_escape(content.name) when Integer @_target << ERB::Util.html_escape(content.to_s) else if (formatted_object = format_object(content)) @_target << ERB::Util.html_escape(formatted_object) end end end nil end private def yield_content_with_args(*args) return unless block_given? original_length = @_target.length content = yield(*args) unchanged = (original_length == @_target.length) if unchanged case content when String @_target << ERB::Util.html_escape(content) when Symbol @_target << ERB::Util.html_escape(content.name) when Integer, Float @_target << ERB::Util.html_escape(content.to_s) else if (formatted_object = format_object(content)) @_target << ERB::Util.html_escape(formatted_object) end end end nil end private def __attributes__(**attributes) if attributes[:href]&.start_with?(/\s*javascript/) attributes[:href] = attributes[:href].sub(/^\s*(javascript:)+/, "") end buffer = +"" __build_attributes__(attributes, buffer: buffer) unless self.class.rendered_at_least_once Phlex::ATTRIBUTE_CACHE[attributes.hash] = buffer.freeze end buffer end private def __build_attributes__(attributes, buffer:) attributes.each do |k, v| next unless v name = case k when String then k when Symbol then k.name.tr("_", "-") else k.to_s end # Detect unsafe attribute names. Attribute names are considered unsafe if they match an event attribute or include unsafe characters. if HTML::EVENT_ATTRIBUTES[name] || name.match?(/[<>&"']/) raise ArgumentError, "Unsafe attribute name detected: #{k}." end case v when true buffer << " " << name when String buffer << " " << name << '="' << ERB::Util.html_escape(v) << '"' when Symbol buffer << " " << name << '="' << ERB::Util.html_escape(v.name) << '"' when Hash __build_attributes__( v.transform_keys { |subkey| case subkey when Symbol then"#{k}-#{subkey.name.tr('_', '-')}" else "#{k}-#{subkey}" end }, buffer: buffer ) else buffer << " " << name << '="' << ERB::Util.html_escape(v.to_s) << '"' end end buffer end # This should be the last method defined def self.method_added(method_name) if method_name[0] == "_" && Phlex::HTML.instance_methods.include?(method_name) && instance_method(method_name).owner != Phlex::HTML raise NameError, "👋 Redefining the method `#{name}##{method_name}` is not a good idea." end super end end end