Class: Graphomaton::Exporters::Mermaid

Inherits:
Object
  • Object
show all
Includes:
Graphomaton::ExporterIntrospection
Defined in:
lib/graphomaton/exporters/mermaid.rb

Constant Summary collapse

DEFAULT_DIRECTION =
:lr
DEFAULT_THEME =
:default
DEFAULT_CDN =
'https://cdn.jsdelivr.net/npm/mermaid@10.9.8/dist/mermaid.esm.min.mjs'
DEFAULT_MATHJAX =
false
DEFAULT_MATHJAX_CDN =
'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js'
DEFAULT_LANG =
'en'
DEFAULT_SHOW_SOURCE =
false
DEFAULT_PAN_ZOOM =
false
DEFAULT_NOTES =
false
DEFAULT_CLASS_DEFS =
false
DIRECTION_OPTIONS =
%i[lr tb rl bt].freeze
PSEUDOSTATE_TYPES =
%i[choice fork join].freeze
RESERVED_IDENTIFIERS =
%w[state note direction class classDef hide as of].freeze
UI_TEXT =
{
  'en' => {
    default_title: 'Automaton diagram',
    notice_label: 'Note:',
    online_notice: 'This diagram is rendered in the browser with Mermaid.js and requires network access.',
    offline_notice: 'Mermaid.js is loaded from a classic script asset.',
    controls_label: 'Diagram zoom controls',
    viewer_label: 'Zoomable automaton diagram',
    zoom_out: 'Zoom out',
    zoom_reset: 'Reset zoom',
    zoom_in: 'Zoom in',
    reset: 'Reset'
  }.freeze,
  'ja' => {
    default_title: 'オートマトン図',
    notice_label: '注意:',
    online_notice: 'この図は Mermaid.js を使用してブラウザ上で描画されるため、ネットワーク接続が必要です。',
    offline_notice: 'Mermaid.js は classic script アセットから読み込まれます。',
    controls_label: '図のズーム操作',
    viewer_label: 'ズーム可能なオートマトン図',
    zoom_out: '縮小',
    zoom_reset: 'ズームをリセット',
    zoom_in: '拡大',
    reset: 'リセット'
  }.freeze
}.freeze

Instance Method Summary collapse

Methods included from Graphomaton::ExporterIntrospection

#capabilities, #losses_for

Constructor Details

#initialize(automaton, direction: DEFAULT_DIRECTION, notes: DEFAULT_NOTES, class_defs: DEFAULT_CLASS_DEFS) ⇒ Mermaid

Returns a new instance of Mermaid.



49
50
51
52
53
54
55
56
57
58
59
# File 'lib/graphomaton/exporters/mermaid.rb', line 49

def initialize(automaton, direction: DEFAULT_DIRECTION, notes: DEFAULT_NOTES, class_defs: DEFAULT_CLASS_DEFS)
  @automaton = automaton
  @direction = resolve_direction(direction)
  @notes = notes
  @class_defs = class_defs
  @identifiers = IdentifierAllocator.new(reserved: RESERVED_IDENTIFIERS)
  @state_names = allocate_state_names
  @hierarchy_usable = @automaton.validation_diagnostics.none? do |diagnostic|
    diagnostic.code == 'invalid-state-hierarchy'
  end
end

Instance Method Details

#exportObject



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/graphomaton/exporters/mermaid.rb', line 61

def export
  lines = ['stateDiagram-v2']
  lines << "    direction #{direction_keyword}"
  lines.concat(state_alias_lines)
  lines.concat(pseudostate_lines)
  lines.concat(composite_state_lines)
  lines.concat(state_group_lines)

  lines << "    [*] --> #{state_name(@automaton.initial_state)}" if @automaton.initial_state

  @automaton.transition_records.each do |trans|
    from = state_name(trans[:from])
    to = state_name(trans[:to])
    label = format_label(trans[:label])
    lines << "    #{from} --> #{to} : #{label}"
  end

  @automaton.final_states.each do |state|
    lines << "    #{state_name(state)} --> [*]"
  end

  lines.concat(state_note_lines) if @notes
  lines.concat(class_definition_lines) if @class_defs

  "#{lines.join("\n")}\n"
end

#export_html(theme: DEFAULT_THEME, cdn: DEFAULT_CDN, inline_mermaid: false, offline: false, title: nil, lang: DEFAULT_LANG, show_source: DEFAULT_SHOW_SOURCE, pan_zoom: DEFAULT_PAN_ZOOM, mathjax: DEFAULT_MATHJAX, mathjax_cdn: DEFAULT_MATHJAX_CDN, inline_mathjax: false, self_contained: false, nonce: nil, csp: false, mermaid_sha256: nil, mathjax_sha256: nil) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/graphomaton/exporters/mermaid.rb', line 88

def export_html(theme: DEFAULT_THEME, cdn: DEFAULT_CDN, inline_mermaid: false, offline: false, title: nil, lang: DEFAULT_LANG,
                show_source: DEFAULT_SHOW_SOURCE, pan_zoom: DEFAULT_PAN_ZOOM,
                mathjax: DEFAULT_MATHJAX, mathjax_cdn: DEFAULT_MATHJAX_CDN,
                inline_mathjax: false, self_contained: false, nonce: nil, csp: false,
                mermaid_sha256: nil, mathjax_sha256: nil)
  if self_contained
    inline_mermaid = true
    offline = true
    inline_mathjax = true if mathjax
  end
  resolved_nonce = resolve_nonce(nonce)
  mermaid_code = export
  language = resolve_language(lang)
  ui = UI_TEXT.fetch(language)
  title_text = title || ui[:default_title]
  InputPolicy.text!(title_text.to_s, context: 'HTML title', max_bytes: Graphomaton::DEFAULT_MAX_LABEL_LENGTH)

  <<~HTML
    <!DOCTYPE html>
    <html lang="#{escape_attribute(language)}">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        #{csp_meta(csp, resolved_nonce)}
        <title>#{escape_text(title_text)}</title>
        #{script_block(cdn: cdn, theme: theme, inline_mermaid: inline_mermaid, offline: offline, nonce: resolved_nonce, sha256: mermaid_sha256)}
        #{mathjax_block(enabled: mathjax, cdn: mathjax_cdn, inline: inline_mathjax, nonce: resolved_nonce, sha256: mathjax_sha256)}
        <style#{nonce_attribute(resolved_nonce)}>
            body {
                font-family: Arial, sans-serif;
                max-width: 1200px;
                margin: 0 auto;
                padding: 20px;
            }
            .mermaid {
                text-align: center;
                background: white;
                border: 1px solid #ddd;
                border-radius: 8px;
                padding: 20px;
                margin: 20px 0;
            }
            h1 {
                color: #333;
                text-align: center;
            }
            .info {
                background: #f5f5f5;
                padding: 10px;
                border-radius: 4px;
                margin-bottom: 20px;
            }
            pre.mermaid-source {
                background: #f8fafc;
                border: 1px solid #ddd;
                border-radius: 8px;
                overflow-x: auto;
                padding: 16px;
            }
            #{pan_zoom_css(pan_zoom)}
            #{auto_theme_css(theme)}
        </style>
    </head>
    <body>
        <h1>#{escape_text(title_text)}</h1>
        <div class="info">
            <p><strong>#{ui[:notice_label]}</strong> #{offline ? ui[:offline_notice] : ui[:online_notice]}</p>
        </div>
        #{pan_zoom_controls(pan_zoom, ui)}
        <div class="mermaid#{pan_zoom ? ' pan-zoom-content' : ''}"#{pan_zoom ? %( data-pan-zoom-viewer tabindex="0" role="region" aria-label="#{escape_attribute(ui[:viewer_label])}") : ''}>
    #{escape_text(mermaid_code)}
        </div>
        #{source_block(mermaid_code, show_source: show_source)}
        #{pan_zoom_script(pan_zoom, nonce: resolved_nonce)}
    </body>
    </html>
  HTML
end