lib/prismic/fragments/structured_text.rb
# encoding: utf-8
module Prismic
module Fragments
class StructuredText < Fragment
# Used during the call of {StructuredText#as_html} : blocks are first gathered by groups,
# so that list items of the same list are placed within the same group, allowing to frame
# their serialization with <ul>...</ul> or <ol>...</ol>.
# Images, paragraphs, headings, embed, ... are then placed alone in their own BlockGroup.
class BlockGroup
attr_reader :kind, :blocks
def initialize(kind)
@kind = kind
@blocks = []
end
def <<(block)
blocks << block
end
end
attr_accessor :blocks
def initialize(blocks)
@blocks = blocks
end
# Serializes the current StructuredText fragment into a fully usable HTML code.
# You need to pass a proper link_resolver so that internal links are turned into the proper URL in
# your website. If you use a starter kit, one is provided, that you can still update later.
#
# This method simply executes the as_html methods on blocks;
# it is not advised to override this method if you want to change the HTML output, you should
# override the as_html method at the block level (like {Heading.as_html}, or {Preformatted.as_html},
# for instance).
# @param link_resolver [LinkResolver]
# @param html_serializer [HtmlSerializer]
# @return [String] the resulting html snippet
def as_html(link_resolver, html_serializer=nil)
# Defining blocks that deserve grouping, assigning them "group kind" names
block_group = ->(block){
case block
when Block::ListItem
block.ordered? ? "ol" : "ul"
else
nil
end
}
# Initializing groups, which is an array of BlockGroup objects
groups, last = [], nil
blocks.each {|block|
group = block_group.(block)
groups << BlockGroup.new(group) if !last || group != last
groups.last << block
last = group
}
# HTML-serializing the groups object (delegating the serialization of Block objects),
# without forgetting to frame the BlockGroup objects right if needed
groups.map{|group|
html = group.blocks.map { |b|
b.as_html(link_resolver, html_serializer)
}.join
case group.kind
when "ol"
%(<ol>#{html}</ol>)
when "ul"
%(<ul>#{html}</ul>)
else
html
end
}.join("\n\n")
end
# Returns the StructuredText as plain text, with zero formatting.
# Non-textual blocks (like images and embeds) are simply ignored.
#
# @param separator [String] The string separator inserted between the blocks (a blank space by default)
# @return [String] The complete string representing the textual value of the StructuredText field.
def as_text(separator=' ')
blocks.map{|block| block.as_text }.compact.join(separator)
end
# Finds the first highest title in a structured text
def first_title
max_level = 6 # any title with a higher level kicks the current one out
title = false
@blocks.each do |block|
if block.is_a?(Prismic::Fragments::StructuredText::Block::Heading)
if block.level < max_level
title = block.text
max_level = block.level # new maximum
end
end
end
title
end
class Span
# @return [Number]
attr_accessor :start
# @return [Number]
attr_accessor :end
def initialize(start, finish)
@start = start
@end = finish
end
class Label < Span
# @return [String]
attr_accessor :label
def initialize(start, finish, label)
super(start, finish)
@label = label
end
def serialize(text, link_resolver = nil)
"<span class=\"#{@label}\">#{text}</span>"
end
end
class Em < Span
def serialize(text, link_resolver = nil)
"<em>#{text}</em>"
end
end
class Strong < Span
def serialize(text, link_resolver = nil)
"<strong>#{text}</strong>"
end
end
class Hyperlink < Span
attr_accessor :link
def initialize(start, finish, link)
super(start, finish)
@link = link
end
def serialize(text, link_resolver = nil)
if link.is_a? Prismic::Fragments::DocumentLink and link.broken
"<span>#{text}</span>"
elsif !link.target.nil?
%(<a href="#{link.url(link_resolver)}" target="#{link.target}" rel="noopener">#{text}</a>)
else
%(<a href="#{link.url(link_resolver)}">#{text}</a>)
end
end
end
end
class Block
# Returns nil, as a block is not textual by default.
# This is meant to be overriden by textual blocks (see Prismic::Fragments::StructuredText::Block::Text.as_text, for instance)
#
# @return nil, always.
def as_text
nil
end
class Text
# @return [String]
attr_accessor :text
# @return [Array<Span>]
attr_accessor :spans
# @return [String] may be nil
attr_accessor :label
def initialize(text, spans, label = nil)
@text = text
@spans = spans.select{|span| span.start < span.end}
@label = label
end
def class_code
(@label && %( class="#{label}")) || ''
end
def as_html(link_resolver=nil, html_serializer=nil)
html = ''
# Getting Hashes of spanning tags to insert, sorted by starting position, and by ending position
start_spans, end_spans = prepare_spans
# Open tags
stack = Array.new
(text.length + 1).times do |pos| # Looping to length + 1 to catch closing tags
end_spans[pos].each do |t|
# Close a tag
tag = stack.pop
inner_html = serialize(tag[:span], tag[:html], link_resolver, html_serializer)
if stack.empty?
# The tag was top-level
html += inner_html
else
# Add the content to the parent tag
stack[-1][:html] += inner_html
end
end
start_spans[pos].each do |tag|
# Open a tag
stack.push({
:span => tag,
:html => ''
})
end
if pos < text.length
if stack.empty?
# Top level text
html += cgi_escape_html(text[pos])
else
# Inner text of a span
stack[-1][:html] += cgi_escape_html(text[pos])
end
end
end
html.gsub("\n", '<br>')
end
def cgi_escape_html(string)
# We don't use CGI::escapeHTML because the implementation changed from 1.9 to 2.0 and that break tests
string.gsub(/['&\"<>]/, {
"'" => ''',
'&' => '&',
'"' => '"',
'<' => '<',
'>' => '>'
})
end
# Building two span Hashes:
# * start_spans, with the starting positions as keys, and spans as values
# * end_spans, with the ending positions as keys, and spans as values
def prepare_spans
unless defined?(@prepared_spans)
start_spans = Hash.new{|h,k| h[k] = [] }
end_spans = Hash.new{|h,k| h[k] = [] }
spans.each {|span|
start_spans[span.start] << span
end_spans[span.end] << span
}
# Make sure the spans are sorted bigger first to respect the hierarchy
@start_spans = start_spans.each { |_, spans| spans.sort! { |a, b| b.end - b.start <=> a.end - a.start } }
@end_spans = end_spans
end
[@start_spans, @end_spans]
end
# Zero-formatted textual value of the block.
#
# @return The textual value.
def as_text
@text
end
def serialize(elt, text, link_resolver, html_serializer)
custom_html = html_serializer && html_serializer.serialize(elt, text)
if custom_html.nil?
elt.serialize(text, link_resolver)
else
custom_html
end
end
private :class_code, :cgi_escape_html
end
class Heading < Text
attr_accessor :level
def initialize(text, spans, level, label = nil)
super(text, spans, label)
@level = level
end
def as_html(link_resolver=nil, html_serializer=nil)
custom_html = html_serializer && html_serializer.serialize(self, super)
if custom_html.nil?
%(<h#{level}#{class_code}>#{super}</h#{level}>)
else
custom_html
end
end
end
class Paragraph < Text
def as_html(link_resolver=nil, html_serializer=nil)
custom_html = html_serializer && html_serializer.serialize(self, super)
if custom_html.nil?
%(<p#{class_code}>#{super}</p>)
else
custom_html
end
end
end
class Preformatted < Text
def as_html(link_resolver=nil, html_serializer=nil)
custom_html = html_serializer && html_serializer.serialize(self, super)
if custom_html.nil?
%(<pre#{class_code}>#{super}</pre>)
else
custom_html
end
end
end
class ListItem < Text
attr_accessor :ordered
alias :ordered? :ordered
def initialize(text, spans, ordered, label = nil)
super(text, spans, label)
@ordered = ordered
end
def as_html(link_resolver, html_serializer=nil)
custom_html = html_serializer && html_serializer.serialize(self, super)
if custom_html.nil?
%(<li#{class_code}>#{super}</li>)
else
custom_html
end
end
end
class Image < Block
attr_accessor :view, :label
def initialize(view, label = nil)
@view = view
@label = label
end
def url
@view.url
end
def width
@view.width
end
def height
@view.height
end
def alt
@view.alt
end
def copyright
@view.copyright
end
def link_to
@view.link_to
end
def as_html(link_resolver, html_serializer = nil)
custom = nil
unless html_serializer.nil?
custom = html_serializer.serialize(self, '')
end
if custom.nil?
classes = ['block-img']
unless @label.nil?
classes.push(@label)
end
%(<p class="#{classes.join(' ')}">#{view.as_html(link_resolver)}</p>)
else
custom
end
end
end
class Embed < Block
attr_accessor :embed, :label
def initialize(embed, label)
@embed = embed
@label = label
end
def embed_type
@embed.embed_type
end
def provider
@embed.provider
end
def url
@embed.url
end
def html
@embed.html
end
def as_html(link_resolver, html_serializer = nil)
custom = nil
unless html_serializer.nil?
custom = html_serializer.serialize(self, '')
end
if custom.nil?
embed.as_html(link_resolver)
else
custom
end
end
end
end
end
end
end