diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml
index 3f86c15..e138c66 100644
--- a/.github/workflows/ubuntu.yml
+++ b/.github/workflows/ubuntu.yml
@@ -41,6 +41,7 @@ jobs:
- name: Install requirements on ubuntu
run: |
+ sudo apt update
sudo apt install -y --no-install-recommends \
libczmq-dev \
python3 \
diff --git a/lib/iruby/display.rb b/lib/iruby/display.rb
index 74b966a..67a6ae6 100644
--- a/lib/iruby/display.rb
+++ b/lib/iruby/display.rb
@@ -2,6 +2,22 @@
module IRuby
module Display
+ DEFAULT_MIME_TYPE_FORMAT_METHODS = {
+ "text/html" => :to_html,
+ "text/markdown" => :to_markdown,
+ "image/svg+xml" => :to_svg,
+ "image/png" => :to_png,
+ "appliation/pdf" => :to_pdf,
+ "image/jpeg" => :to_jpeg,
+ "text/latex" => [:to_latex, :to_tex],
+ # NOTE: Do not include the entry of "application/json" because
+ # all objects can respond to `to_json` due to json library
+ # "application/json" => :to_json,
+ "application/javascript" => :to_javascript,
+ nil => :to_iruby,
+ "text/plain" => :inspect
+ }.freeze
+
class << self
# @private
def convert(obj, options)
@@ -24,17 +40,38 @@ def display(obj, options = {})
raise 'Invalid mime type' unless exact_mime.include?('/')
end
- data = {}
+ data = render_mimebundle(obj, exact_mime, fuzzy_mime)
+
+ # Render by additional formatters
+ render_by_registry(data, obj, exact_mime, fuzzy_mime)
+
+ # Render by to_xxx methods
+ DEFAULT_MIME_TYPE_FORMAT_METHODS.each do |mime, methods|
+ next if mime.nil? && !data.empty? # for to_iruby
- # Render additional representation
- render(data, obj, exact_mime, fuzzy_mime)
+ next if mime && data.key?(mime) # do not overwrite
- # IPython always requires a text representation
- render(data, obj, 'text/plain', nil) unless data['text/plain']
+ method = Array(methods).find {|m| obj.respond_to?(m) }
+ next if method.nil?
+
+ result = obj.send(method)
+ case mime
+ when nil # to_iruby
+ case result
+ when Array
+ mime, result = result
+ else
+ warn(("Ignore the result of to_iruby method of %p because " +
+ "it does not return a pair of mime-type and formatted representation") % obj)
+ next
+ end
+ end
+ data[mime] = result
+ end
# As a last resort, interpret string representation of the object
# as the given mime type.
- if exact_mime && data.none? { |m, _| exact_mime == m }
+ if exact_mime && !data.key?(exact_mime)
data[exact_mime] = protect(exact_mime, obj)
end
@@ -67,7 +104,21 @@ def ascii?(mime)
end
end
- def render(data, obj, exact_mime, fuzzy_mime)
+ private def render_mimebundle(obj, exact_mime, fuzzy_mime)
+ data = {}
+ if obj.respond_to?(:to_iruby_mimebundle)
+ include_mime = [exact_mime].compact
+ formats, metadata = obj.to_iruby_mimebundle(include: include_mime)
+ formats.each do |mime, value|
+ if fuzzy_mime.nil? || mime.include?(fuzzy_mime)
+ data[mime] = value
+ end
+ end
+ end
+ data
+ end
+
+ private def render_by_registry(data, obj, exact_mime, fuzzy_mime)
# Filter matching renderer by object type
renderer = Registry.renderer.select { |r| r.match?(obj) }
@@ -88,6 +139,8 @@ def render(data, obj, exact_mime, fuzzy_mime)
# Return first render result which has the right mime type
renderer.each do |r|
mime, result = r.render(obj)
+ next if data.key?(mime)
+
if mime && result && (!exact_mime || exact_mime == mime) && (!fuzzy_mime || mime.include?(fuzzy_mime))
data[mime] = protect(mime, result)
break
@@ -98,6 +151,19 @@ def render(data, obj, exact_mime, fuzzy_mime)
end
end
+ private def render_by_to_iruby(data, obj)
+ if obj.respond_to?(:to_iruby)
+ result = obj.to_iruby
+ mime, rep = case result
+ when Array
+ result
+ else
+ [nil, result]
+ end
+ data[mime] = rep
+ end
+ end
+
class Representation
attr_reader :object, :options
@@ -120,6 +186,60 @@ def new(obj, options)
end
end
+ class FormatMatcher
+ def initialize(&block)
+ @block = block
+ end
+
+ def call(obj)
+ @block.(obj)
+ end
+
+ def inspect
+ "#{self.class.name}[%p]" % @block
+ end
+ end
+
+ class RespondToFormatMatcher < FormatMatcher
+ def initialize(name)
+ super() {|obj| obj.respond_to?(name) }
+ @name = name
+ end
+
+ attr_reader :name
+
+ def inspect
+ "#{self.class.name}[respond_to?(%p)]" % name
+ end
+ end
+
+ class TypeFormatMatcher < FormatMatcher
+ def initialize(class_block)
+ super() do |obj|
+ begin
+ self.klass === obj
+ # We have to rescue all exceptions since constant autoloading could fail with a different error
+ rescue Exception
+ false
+ end
+ end
+ @class_block = class_block
+ end
+
+ def klass
+ @class_block.()
+ end
+
+ def inspect
+ klass = begin
+ @class_block.()
+ rescue Exception
+ @class_block
+ end
+ "#{self.class.name}[%p]" % klass
+ end
+ end
+
class Renderer
attr_reader :match, :mime, :priority
@@ -159,25 +279,21 @@ def renderer
]
def match(&block)
- @match = block
+ @match = FormatMatcher.new(&block)
priority 0
nil
end
def respond_to(name)
- match { |obj| obj.respond_to?(name) }
+ @match = RespondToFormatMatcher.new(name)
+ priority 0
+ nil
end
def type(&block)
- match do |obj|
- begin
- block.call === obj
- # We have to rescue all exceptions since constant autoloading could fail with a different error
- rescue Exception
- rescue #NameError
- false
- end
- end
+ @match = TypeFormatMatcher.new(block)
+ priority 0
+ nil
end
def priority(p)
@@ -301,36 +417,17 @@ def format(mime = nil, &block)
type { Gruff::Base }
format 'image/png', &:to_blob
- respond_to :to_html
- format 'text/html', &:to_html
-
- respond_to :to_latex
- format 'text/latex', &:to_latex
-
- respond_to :to_tex
- format 'text/latex', &:to_tex
-
- respond_to :to_javascript
- format 'text/javascript', &:to_javascript
-
- respond_to :to_svg
+ type { Rubyvis::Mark }
format 'image/svg+xml' do |obj|
- obj.render if defined?(Rubyvis) && Rubyvis::Mark === obj
+ obj.render
obj.to_svg
end
- respond_to :to_iruby
- format(&:to_iruby)
-
match { |obj| obj.respond_to?(:path) && obj.method(:path).arity == 0 && File.readable?(obj.path) }
format do |obj|
mime = MIME::Types.of(obj.path).first.to_s
[mime, File.read(obj.path)] if SUPPORTED_MIMES.include?(mime)
end
-
- type { Object }
- priority(-1000)
- format 'text/plain', &:inspect
end
end
end
diff --git a/test/iruby/display_test.rb b/test/iruby/display_test.rb
new file mode 100644
index 0000000..7bdd788
--- /dev/null
+++ b/test/iruby/display_test.rb
@@ -0,0 +1,188 @@
+module IRubyTest
+ class DisplayTest < TestBase
+ def setup
+ @object = Object.new
+ @object.instance_variable_set(:@to_html_called, false)
+ @object.instance_variable_set(:@to_markdown_called, false)
+ @object.instance_variable_set(:@to_iruby_called, false)
+ @object.instance_variable_set(:@to_iruby_mimebundle_called, false)
+
+ class << @object
+ attr_reader :to_html_called
+ attr_reader :to_markdown_called
+ attr_reader :to_iruby_called
+ attr_reader :to_iruby_mimebundle_called
+
+ def html
+ "html"
+ end
+
+ def markdown
+ "*markdown*"
+ end
+
+ def inspect
+ "!!! inspect !!!"
+ end
+ end
+ end
+
+ def define_to_html
+ class << @object
+ def to_html
+ @to_html_called = true
+ html
+ end
+ end
+ end
+
+ def define_to_markdown
+ class << @object
+ def to_markdown
+ @to_markdown_called = true
+ markdown
+ end
+ end
+ end
+
+ def define_to_iruby
+ class << @object
+ def to_iruby
+ @to_iruby_called = true
+ ["text/html", "to_iruby"]
+ end
+ end
+ end
+
+ def define_to_iruby_mimebundle
+ class << @object
+ def to_iruby_mimebundle(include: [])
+ @to_iruby_mimebundle_called = true
+ mimes = if include.empty?
+ ["text/html", "text/markdown", "application/json"]
+ else
+ include
+ end
+ formats = mimes.map { |mime|
+ result = case mime
+ when "text/html"
+ "html"
+ when "text/markdown"
+ "**markdown**"
+ when "application/json"
+ %Q[{"mimebundle": "json"}]
+ end
+ [mime, result]
+ }.to_h
+ metadata = {}
+ return formats, metadata
+ end
+ end
+ end
+
+ def assert_iruby_display(expected)
+ assert_equal(expected,
+ {
+ result: IRuby::Display.display(@object),
+ to_html_called: @object.to_html_called,
+ to_markdown_called: @object.to_markdown_called,
+ to_iruby_called: @object.to_iruby_called,
+ to_iruby_mimebundle_called: @object.to_iruby_mimebundle_called
+ })
+ end
+
+ sub_test_case("the object cannot handle all the mime types") do
+ def test_display
+ assert_iruby_display({
+ result: {"text/plain" => "!!! inspect !!!"},
+ to_html_called: false,
+ to_markdown_called: false,
+ to_iruby_called: false,
+ to_iruby_mimebundle_called: false
+ })
+ end
+ end
+
+ sub_test_case("the object can respond to to_iruby") do
+ def setup
+ super
+ define_to_iruby
+ end
+
+ def test_display
+ assert_iruby_display({
+ result: {
+ "text/html" => "to_iruby",
+ "text/plain" => "!!! inspect !!!"
+ },
+ to_html_called: false,
+ to_markdown_called: false,
+ to_iruby_called: true,
+ to_iruby_mimebundle_called: false
+ })
+ end
+
+ sub_test_case("the object can respond to to_markdown") do
+ def setup
+ super
+ define_to_markdown
+ end
+
+ def test_display
+ assert_iruby_display({
+ result: {
+ "text/markdown" => "*markdown*",
+ "text/plain" => "!!! inspect !!!"
+ },
+ to_html_called: false,
+ to_markdown_called: true,
+ to_iruby_called: false,
+ to_iruby_mimebundle_called: false
+ })
+ end
+ end
+
+ sub_test_case("the object can respond to to_html") do
+ def setup
+ super
+ define_to_html
+ end
+
+ def test_display
+ assert_iruby_display({
+ result: {
+ "text/html" => "html",
+ "text/plain" => "!!! inspect !!!"
+ },
+ to_html_called: true,
+ to_markdown_called: false,
+ to_iruby_called: false,
+ to_iruby_mimebundle_called: false
+ })
+ end
+
+ sub_test_case("the object can respond to to_iruby_mimebundle") do
+ def setup
+ super
+ define_to_iruby_mimebundle
+ end
+
+ def test_display
+ assert_iruby_display({
+ result: {
+ "text/html" => "html",
+ "text/markdown" => "**markdown**",
+ "application/json" => %Q[{"mimebundle": "json"}],
+ "text/plain" => "!!! inspect !!!"
+ },
+ to_html_called: false,
+ to_markdown_called: false,
+ to_iruby_called: false,
+ to_iruby_mimebundle_called: true
+ })
+ end
+ end
+ end
+ end
+ end
+end