diff --git a/docs/src/api.md b/docs/src/api.md index d0f8450..0272d78 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -13,7 +13,9 @@ Diagram(::Symbol, ::AbstractString) Diagram(::Symbol; ::Union{Nothing,AbstractString}, ::Union{Nothing,AbstractString}) SUPPORTED_TEXT_PLAIN_SHOW_MIME_TYPES TEXT_PLAIN_SHOW_MIME_TYPE +overrideShowable render +resetShowableOverrides ``` ### Service Management @@ -41,8 +43,10 @@ DiagramTypeMetadata DIAGRAM_TYPE_METADATA LIMITED_DIAGRAM_SUPPORT MIME_TO_RENDER_ARGUMENT_MAP +SHOWABLE_OVERRIDES UriSafeBase64Payload getDiagramTypeMetadata +normalizeDiagramType ``` ### Documentation diff --git a/docs/src/examples.md b/docs/src/examples.md index ac6ecb7..521f48b 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -269,15 +269,54 @@ using `write`. write("mermaid_diagram.png", mermaid_diagram_as_png) ``` -![Mermaid diagram as PNG example](mermaid_diagram.png) - -Note the difference in file size and fonts when rendering to SVG. +Note the difference in file size when rendering to SVG. ```@example diagrams write("mermaid_diagram.svg", render(mermaid_diagram, "svg")) ``` -![Mermaid diagram as SVG example](mermaid_diagram.svg) +### Overriding `Base.show` behavior + +In some cases the output format that is used may affect how the final diagram +renders. Although it is always possible to explicitly fall back to calling +`render` in these cases, this may be cumbersome when writing documentation, +etc. and having to explicitly do that for every diagram of a specific type. As +an alternative, it is possible to instruct `Kroki` to ignore certain output +formats when rendering a specific diagram type using `Base.show`. + +For instance, by default (in most cases) a Mermaid diagram will be rendered as +an SVG. Due to the way these diagrams are rendered by Kroki this may result in +text getting cut off, see +[yuzutech/kroki#1345](https://github.com/yuzutech/kroki/issues/1345) for +details. + +```@example diagrams +mermaid_diagram +``` + +!!! tip "Inspecting the output format of an image" + + The most straightforward way of inspecting the output format is to open the + image in a new tab in your browser and checking its extension, e.g. by + right clicking on it and selecting _Open image in new tab_ in Chrome or a + similar browser. Alternatively the type of the rendered image can be seen + by inspecting the source of this page. + +Using [`Kroki.overrideShowable`](@ref), `Kroki` can be instructed to not render +to SVG and pick the next most suitable output format with a fallback of +rendering the diagram to text if none are available. + +```@example diagrams +Kroki.overrideShowable(MIME"image/svg+xml"(), :mermaid, false) +``` + +For Mermaid diagrams in the context of `Documenter` this means rendering the +diagram as a PNG. When rendering to PNG the previously mentioned font issues do +not arise. + +```@example diagrams +mermaid_diagram +``` ## Controlling text rendering diff --git a/src/Kroki.jl b/src/Kroki.jl index fd6b852..8013233 100644 --- a/src/Kroki.jl +++ b/src/Kroki.jl @@ -140,7 +140,9 @@ they occur. _SVG output is supported for all [`Diagram`](@ref) types_. See the [support table](@ref diagram-support) for an overview of other supported output formats -per diagram type. +per diagram type and [`overrideShowable`](@ref) in case it is necessary to +override default `Base.show` behavior, e.g. to disable a specific output +format. """ render( diagram::Diagram, @@ -354,6 +356,48 @@ const MIME_TO_RENDER_ARGUMENT_MAP = Dict{MIME, String}( MIME"text/plain; charset=utf-8"() => "utxt", ) +""" +Normalizes the `type` of the [`Diagram`](@ref) enabling consistent comparisons, +etc. while enabling user-facing case insensitivity. +""" +normalizeDiagramType(diagram::Diagram) = normalizeDiagramType(diagram.type) +normalizeDiagramType(diagram_type::Symbol) = Symbol(lowercase(String(diagram_type))) + +""" +Overrides the behavior of `Base.showable` for a specific `output_format` and +`diagram_type` relative to what is recorded in +[`LIMITED_DIAGRAM_SUPPORT`](@ref). The overrides are tracked through +[`SHOWABLE_OVERRIDES`](@ref). + +Note that overriding whether a diagram type is `showable` for a specific output +format, specifically enabling one, requires the Kroki service to support it for +a diagram to properly render! +""" +function overrideShowable(output_format::MIME, diagram_type::Symbol, supported::Bool) + SHOWABLE_OVERRIDES[(normalizeDiagramType(diagram_type), output_format)] = supported +end + +""" +Resets [`SHOWABLE_OVERRIDES`](@ref) so that the default `showable` support is +enabled for all diagram types. +""" +resetShowableOverrides() = empty!(SHOWABLE_OVERRIDES) + +""" +Tracks overrides that should be applied to the output format support registered +in [`LIMITED_DIAGRAM_SUPPORT`](@ref) for specific diagram types. + +Typically used to disable an output format that might selected by `Base.show` +to render a specific diagram type to for a given display in case that produces +undesired results. Can also be used to enable output formats in addition to SVG +for new diagram types that support them and that may not have been added to +this package's [`LIMITED_DIAGRAM_SUPPORT`](@ref) yet. + +Should be manipulated using [`overrideShowable`](@ref) and +[`resetShowableOverrides`](@ref). +""" +const SHOWABLE_OVERRIDES = Dict{Tuple{Symbol, MIME}, Bool}() + # `Base.show` methods should only be defined for diagram types that actually # support the desired output format. This would make sure incompatible formats # are not accidentally rendered on compatible `AbstractDisplay`s causing @@ -369,14 +413,22 @@ Base.show(io::IO, ::T, diagram::Diagram) where {T <: MIME} = # to render this MIME type, it is simply forwarded to that method Base.show(io::IO, ::MIME"text/plain", diagram::Diagram) = show(io, diagram) -# SVG output is supported by _all_ diagram types. An additional `showable` -# method is necessary as `LIMITED_DIAGRAM_SUPPORT` documents only those diagram -# types that _only_ support SVG. This makes sure SVG output also works for new -# diagram types if they get added to the Kroki service, but not yet to this -# package -Base.showable(::MIME"image/svg+xml", ::Diagram) = true -Base.showable(::T, diagram::Diagram) where {T <: MIME} = - Symbol(lowercase(String(diagram.type))) ∈ get(LIMITED_DIAGRAM_SUPPORT, T(), Tuple([])) +function Base.showable(output_format::T, diagram::Diagram) where {T <: MIME} + diagram_type = normalizeDiagramType(diagram) + + override_index = (diagram_type, output_format) + if haskey(SHOWABLE_OVERRIDES, override_index) + return SHOWABLE_OVERRIDES[override_index] + elseif T === MIME"image/svg+xml" + # SVG output is supported by _all_ diagram types. Instead of encoding + # this for all types in `LIMITED_DIAGRAM_SUPPORT` this is tracked here + # so that SVG output also works for new diagram types if they get added + # to the Kroki service, but not yet to this package + return true + else + return diagram_type ∈ get(LIMITED_DIAGRAM_SUPPORT, output_format, Tuple([])) + end +end """ Defines the MIME type to be used when `show` gets called on a [`Diagram`](@ref) diff --git a/test/kroki/rendering_test.jl b/test/kroki/rendering_test.jl index dbda2d5..b12c777 100644 --- a/test/kroki/rendering_test.jl +++ b/test/kroki/rendering_test.jl @@ -2,9 +2,13 @@ module RenderingTest using Test: @test, @testset, @test_throws -using Kroki: Diagram, Kroki, render +using Kroki: Diagram, Kroki, overrideShowable, render, resetShowableOverrides using Kroki.Exceptions: InvalidOutputFormatError, UnsupportedMIMETypeError +PDF_MIME_TYPE = MIME"application/pdf"() +PNG_MIME_TYPE = MIME"image/png"() +SVG_MIME_TYPE = MIME"image/svg+xml"() + function testShowMethodRenders( diagram::Diagram, mime_type::MIME, @@ -121,23 +125,20 @@ end # should be overridden to indicate the diagram cannot be rendered in the # specified MIME type svgbob_diagram = Diagram(:svgbob, "-->[_...__... ]") - @test_throws( - InvalidOutputFormatError, - show(IOBuffer(), MIME"application/pdf"(), svgbob_diagram) - ) - @test !showable("application/pdf", svgbob_diagram) - @test_throws(InvalidOutputFormatError, sprint(show, MIME"image/png"(), svgbob_diagram)) - @test !showable(MIME"image/png"(), svgbob_diagram) + @test_throws(InvalidOutputFormatError, show(IOBuffer(), PDF_MIME_TYPE, svgbob_diagram)) + @test !showable(PDF_MIME_TYPE, svgbob_diagram) + @test_throws(InvalidOutputFormatError, sprint(show, PNG_MIME_TYPE, svgbob_diagram)) + @test !showable(PNG_MIME_TYPE, svgbob_diagram) @test_throws( InvalidOutputFormatError, show(IOBuffer(), MIME"image/jpeg"(), svgbob_diagram) ) @test !showable("image/jpeg", svgbob_diagram) - testShowMethodRenders(svgbob_diagram, MIME"image/svg+xml"(), "svg") + testShowMethodRenders(svgbob_diagram, SVG_MIME_TYPE, "svg") @test !showable("non-existent/mime-type", svgbob_diagram) plantuml_diagram = Diagram(:PlantUML, "A -> B: C") - testShowMethodRenders(plantuml_diagram, MIME"image/png"(), "png") + testShowMethodRenders(plantuml_diagram, PNG_MIME_TYPE, "png") # PlantUML diagrams support SVG, but are not part of the # `LIMITED_DIAGRAM_SUPPORT` as they support more output formats. # @@ -145,13 +146,15 @@ end # is necessary to make sure a `showable` method is available to indicate # SVG is always supported to those enviroments that need to query that # information - @test showable(MIME"image/svg+xml"(), plantuml_diagram) - testShowMethodRenders(plantuml_diagram, MIME"image/svg+xml"(), "svg") + @test showable(SVG_MIME_TYPE, plantuml_diagram) + testShowMethodRenders(plantuml_diagram, SVG_MIME_TYPE, "svg") @testset "`text/plain`" begin + plain_text_mime_type = MIME"text/plain"() + @testset "without ASCII/Unicode rendering support" begin # These diagram types should simply display their `specification` - @test sprint(show, MIME"text/plain"(), svgbob_diagram) == + @test sprint(show, plain_text_mime_type, svgbob_diagram) == svgbob_diagram.specification end @@ -166,21 +169,56 @@ end Kroki.TEXT_PLAIN_SHOW_MIME_TYPE[] = MIME"text/plain; charset=utf-8"() testShowMethodRenders(plantuml_diagram, MIME"text/plain"(), "utxt") - Kroki.TEXT_PLAIN_SHOW_MIME_TYPE[] = MIME"text/plain"() + Kroki.TEXT_PLAIN_SHOW_MIME_TYPE[] = plain_text_mime_type testShowMethodRenders(plantuml_diagram, MIME"text/plain"(), "txt") @testset "generates an error if an invalid `text/plain` MIME type is configured" begin - Kroki.TEXT_PLAIN_SHOW_MIME_TYPE[] = MIME"image/png"() + Kroki.TEXT_PLAIN_SHOW_MIME_TYPE[] = PNG_MIME_TYPE @test_throws( UnsupportedMIMETypeError, - show(IOBuffer(), MIME"text/plain"(), plantuml_diagram) + show(IOBuffer(), plain_text_mime_type, plantuml_diagram) ) end Kroki.TEXT_PLAIN_SHOW_MIME_TYPE[] = original_text_plain_mimetype end end + + @testset "overrides" begin + diagram = Diagram(:plantuml, "A -> B: C") + + @testset "for `image/svg+xml`" begin + # SVG is supported by default + @test showable(SVG_MIME_TYPE, diagram) + overrideShowable(SVG_MIME_TYPE, :plantuml, false) + @test !showable(SVG_MIME_TYPE, diagram) + end + + @testset "for other MIME types" begin + # PDF is supported explicitly for PlantUML through the + # `LIMITED_DIAGRAM_SUPPORT` + @test showable(PDF_MIME_TYPE, diagram) + overrideShowable(PDF_MIME_TYPE, :plantuml, false) + @test !showable(PDF_MIME_TYPE, diagram) + end + + resetShowableOverrides() + @test showable(SVG_MIME_TYPE, diagram) + @test showable(PDF_MIME_TYPE, diagram) + + @testset "diagram types are case insensitive" begin + overrideShowable(SVG_MIME_TYPE, :PlAnTuMl, false) + @test !showable(SVG_MIME_TYPE, diagram) + + overrideShowable(PDF_MIME_TYPE, :PlantUML, false) + @test !showable(PDF_MIME_TYPE, diagram) + end + + resetShowableOverrides() + @test showable(SVG_MIME_TYPE, diagram) + @test showable(PDF_MIME_TYPE, diagram) + end end end