lib/syntax_tree/mermaid.rb
# frozen_string_literal: true require "cgi" require "stringio" module SyntaxTree # This module is responsible for rendering mermaid (https://mermaid.js.org/) # flow charts. module Mermaid # This is the main class that handles rendering a flowchart. It keeps track # of its nodes and links and renders them according to the mermaid syntax. class FlowChart attr_reader :output, :prefix, :nodes, :links def initialize @output = StringIO.new @output.puts("flowchart TD") @prefix = " " @nodes = {} @links = [] end # Retrieve a node that has already been added to the flowchart by its id. def fetch(id) nodes.fetch(id) end # Add a link to the flowchart between two nodes with an optional label. def link(from, to, label = nil, type: :directed, color: nil) link = Link.new(from, to, label, type, color) links << link output.puts("#{prefix}#{link.render}") link end # Add a node to the flowchart with an optional label. def node(id, label = " ", shape: :rectangle) node = Node.new(id, label, shape) nodes[id] = node output.puts("#{prefix}#{nodes[id].render}") node end # Add a subgraph to the flowchart. Within the given block, all of the # nodes will be rendered within the subgraph. def subgraph(label) output.puts("#{prefix}subgraph #{Mermaid.escape(label)}") previous = prefix @prefix = "#{prefix} " begin yield ensure @prefix = previous output.puts("#{prefix}end") end end # Return the rendered flowchart. def render links.each_with_index do |link, index| if link.color output.puts("#{prefix}linkStyle #{index} stroke:#{link.color}") end end output.string end end # This class represents a link between two nodes in a flowchart. It is not # meant to be interacted with directly, but rather used as a data structure # by the FlowChart class. class Link TYPES = %i[directed dotted].freeze COLORS = %i[green red].freeze attr_reader :from, :to, :label, :type, :color def initialize(from, to, label, type, color) raise unless TYPES.include?(type) raise if color && !COLORS.include?(color) @from = from @to = to @label = label @type = type @color = color end def render left_side, right_side, full_side = sides if label escaped = Mermaid.escape(label) "#{from.id} #{left_side} #{escaped} #{right_side} #{to.id}" else "#{from.id} #{full_side} #{to.id}" end end private def sides case type when :directed %w[-- --> -->] when :dotted %w[-. .-> -.->] end end end # This class represents a node in a flowchart. Unlike the Link class, it can # be used directly. It is the return value of the #node method, and is meant # to be passed around to #link methods to create links between nodes. class Node SHAPES = %i[circle rectangle rounded stadium].freeze attr_reader :id, :label, :shape def initialize(id, label, shape) raise unless SHAPES.include?(shape) @id = id @label = label @shape = shape end def render left_bound, right_bound = bounds "#{id}#{left_bound}#{Mermaid.escape(label)}#{right_bound}" end private def bounds case shape when :circle %w[(( ))] when :rectangle ["[", "]"] when :rounded %w[( )] when :stadium ["([", "])"] end end end class << self # Escape a label to be used in the mermaid syntax. This is used to escape # HTML entities such that they render properly within the quotes. def escape(label) "\"#{CGI.escapeHTML(label)}\"" end # Create a new flowchart. If a block is given, it will be yielded to and # the flowchart will be rendered. Otherwise, the flowchart will be # returned. def flowchart flowchart = FlowChart.new if block_given? yield flowchart flowchart.render else flowchart end end end end end