Module: Y::RenderRules
- Defined in:
- lib/y/rendering.rb
Overview
Custom render rules for Y::Lexical and Y::ProseMirror.
Both renderers accept a nodes: hash mapping a node type (Lexical's
__type, ProseMirror's node name) to a rule; Y::ProseMirror also accepts
marks:. A rule is consulted before the built-in schema, so it can add a
custom node or override a built-in one.
The usual way in is the block form — one rules.node call per type,
keyword options for markup-as-data, a Ruby block for logic (see Builder
below). contains: says what's inside the node: :inline (formatted
text, the default), :blocks (child block nodes), or :none (a leaf). The
nodes:/marks: keywords take the same rules as plain hashes, for shipping
rule sets as data.
Two kinds of rule:
-
Declarative (keyword options / a Hash): markup as data, rendered natively at full speed. Y::ProseMirror.new(doc) do |rules| rules.node "callout", tag: "aside", attrs: { "class" => ["callout callout--", :kind] }, contains: :blocks end
tagis the element;attrsvalues are templates — a String literal, a Symbol referencing one of the node's stored attributes, or an Array mixing both (an attribute that resolves empty is omitted);textis a template for literal text content;void: trueemits no closing tag;containsdeclares what lives inside the node and renders there: :inline (default) for formatted text, :blocks for child block nodes (a container), :none for a leaf. -
Callback (a block; in the hash form, a callable or
render:pluscontains):Y::Lexical.new(doc) do |rules| rules.node "video_embed" do |node| %(<video src="#{ERB::Util.html_escape(node.attrs["src"])}"></video>) end endThe block runs after the document read has finished (never while the document is locked) and receives a RenderRules::Node with the node's type, stored attributes, children already rendered to HTML, and child_types (its element/block children by type). Its return value is spliced in verbatim — it is trusted HTML, so escape any attribute values you interpolate.
Mark rules (ProseMirror only) are declarative: tag plus attrs
templates whose Symbol refs resolve against the mark's own attributes. A
custom mark wraps outside every built-in mark; several custom marks nest
alphabetically. A rule for a built-in mark's stored name replaces its
wrap (the markup changes, the semantics don't — an overridden code mark
still excludes the other formatting).
Defined Under Namespace
Class Method Summary collapse
-
.compile(nodes, marks) ⇒ Object
Compile the user-facing config into [rules_json, callbacks].
- .compile_attrs(attrs) ⇒ Object
- .compile_declarative_node(rule) ⇒ Object
- .compile_mark(name, rule) ⇒ Object
- .compile_node(type, rule, callbacks) ⇒ Object
-
.compile_parts(template) ⇒ Object
A template: String literal, Symbol attribute reference, or an Array of both.
- .escape_attr(value) ⇒ Object
-
.escape_text(value) ⇒ Object
The escaping the native renderers use, for blocks that build markup from stored values.
-
.splice(segments, callbacks) ⇒ Object
Resolve callback segments depth-first, so a callback's
node.contentis finished HTML even when callback nodes nest.
Class Method Details
.compile(nodes, marks) ⇒ Object
Compile the user-facing config into [rules_json, callbacks]. Structural validation happens in the native parser, which raises ArgumentError.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/y/rendering.rb', line 70 def compile(nodes, marks) callbacks = {} spec = {} unless nodes.empty? spec["nodes"] = nodes.to_h do |type, rule| [type.to_s, compile_node(type.to_s, rule, callbacks)] end end unless marks.empty? spec["marks"] = marks.to_h do |name, rule| [name.to_s, compile_mark(name.to_s, rule)] end end [JSON.generate(spec), callbacks] end |
.compile_attrs(attrs) ⇒ Object
121 122 123 |
# File 'lib/y/rendering.rb', line 121 def compile_attrs(attrs) attrs.map { |name, template| [name.to_s, compile_parts(template)] } end |
.compile_declarative_node(rule) ⇒ Object
102 103 104 105 106 107 108 109 110 |
# File 'lib/y/rendering.rb', line 102 def compile_declarative_node(rule) compiled = {} compiled["tag"] = rule[:tag].to_s if rule[:tag] compiled["void"] = true if rule[:void] compiled["attrs"] = compile_attrs(rule[:attrs]) if rule[:attrs] compiled["text"] = compile_parts(rule[:text]) if rule[:text] compiled["content"] = rule[:contains].to_s if rule[:contains] compiled end |
.compile_mark(name, rule) ⇒ Object
112 113 114 115 116 117 118 119 |
# File 'lib/y/rendering.rb', line 112 def compile_mark(name, rule) raise ArgumentError, "mark rule for #{name.inspect} must be a Hash" unless rule.is_a?(Hash) compiled = {} compiled["tag"] = rule[:tag].to_s if rule[:tag] compiled["attrs"] = compile_attrs(rule[:attrs]) if rule[:attrs] compiled end |
.compile_node(type, rule, callbacks) ⇒ Object
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/y/rendering.rb', line 86 def compile_node(type, rule, callbacks) if rule.respond_to?(:call) callbacks[type] = rule return { "callback" => true } end raise ArgumentError, "rule for #{type.inspect} must be a Hash or a callable" unless rule.is_a?(Hash) if rule[:render] callbacks[type] = rule[:render] compiled = { "callback" => true } compiled["content"] = rule[:contains].to_s if rule[:contains] return compiled end compile_declarative_node(rule) end |
.compile_parts(template) ⇒ Object
A template: String literal, Symbol attribute reference, or an Array of both.
127 128 129 130 131 132 133 134 |
# File 'lib/y/rendering.rb', line 127 def compile_parts(template) Array(template).map do |part| case part when Symbol then { "ref" => part.to_s } else { "lit" => part.to_s } end end end |
.escape_attr(value) ⇒ Object
145 146 147 |
# File 'lib/y/rendering.rb', line 145 def escape_attr(value) escape_text(value).gsub('"', """) end |
.escape_text(value) ⇒ Object
The escaping the native renderers use, for blocks that build markup
from stored values. Text content escapes &, <, > (quotes stay
literal, matching the browser serializer); attribute values also
escape ". Prefer these over ERB::Util.html_escape when byte parity
with editor output matters — html_escape also rewrites apostrophes.
141 142 143 |
# File 'lib/y/rendering.rb', line 141 def escape_text(value) value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">") end |
.splice(segments, callbacks) ⇒ Object
Resolve callback segments depth-first, so a callback's node.content
is finished HTML even when callback nodes nest.
197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/y/rendering.rb', line 197 def splice(segments, callbacks) segments.map do |segment| next segment if segment.is_a?(String) type, attrs_json, content, child_types = segment node = Node.new(type: type, attrs: JSON.parse(attrs_json), content: splice(content, callbacks), child_types: child_types) callbacks.fetch(type).call(node).to_s end.join end |