etassia 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +140 -0
- data/LICENSE +25 -0
- data/README.adoc +436 -0
- data/exe/etassia +7 -0
- data/lib/etassia/cli.rb +187 -0
- data/lib/etassia/commands/batch.rb +210 -0
- data/lib/etassia/commands/notation_loader.rb +22 -0
- data/lib/etassia/commands/render.rb +98 -0
- data/lib/etassia/commands/types.rb +33 -0
- data/lib/etassia/commands/version.rb +24 -0
- data/lib/etassia/corpus_type_names.rb +52 -0
- data/lib/etassia/diagram/architecture.rb +108 -0
- data/lib/etassia/diagram/base.rb +61 -0
- data/lib/etassia/diagram/block.rb +102 -0
- data/lib/etassia/diagram/c4.rb +337 -0
- data/lib/etassia/diagram/class_diagram.rb +466 -0
- data/lib/etassia/diagram/class_namespace.rb +17 -0
- data/lib/etassia/diagram/class_note.rb +17 -0
- data/lib/etassia/diagram/containment.rb +107 -0
- data/lib/etassia/diagram/er_diagram.rb +317 -0
- data/lib/etassia/diagram/error.rb +38 -0
- data/lib/etassia/diagram/flowchart.rb +419 -0
- data/lib/etassia/diagram/flowchart_label_text.rb +63 -0
- data/lib/etassia/diagram/gantt.rb +94 -0
- data/lib/etassia/diagram/generic_text.rb +30 -0
- data/lib/etassia/diagram/git_graph.rb +57 -0
- data/lib/etassia/diagram/info.rb +38 -0
- data/lib/etassia/diagram/kanban.rb +198 -0
- data/lib/etassia/diagram/mindmap.rb +71 -0
- data/lib/etassia/diagram/packet.rb +93 -0
- data/lib/etassia/diagram/pie.rb +115 -0
- data/lib/etassia/diagram/quadrant.rb +135 -0
- data/lib/etassia/diagram/radar.rb +65 -0
- data/lib/etassia/diagram/requirement.rb +168 -0
- data/lib/etassia/diagram/sankey.rb +217 -0
- data/lib/etassia/diagram/sequence.rb +299 -0
- data/lib/etassia/diagram/sequence_box.rb +18 -0
- data/lib/etassia/diagram/sequence_frame.rb +37 -0
- data/lib/etassia/diagram/sequence_frame_section.rb +23 -0
- data/lib/etassia/diagram/sequence_text.rb +69 -0
- data/lib/etassia/diagram/sequence_wrap.rb +23 -0
- data/lib/etassia/diagram/state_diagram.rb +282 -0
- data/lib/etassia/diagram/timeline.rb +183 -0
- data/lib/etassia/diagram/treemap.rb +95 -0
- data/lib/etassia/diagram/user_journey.rb +153 -0
- data/lib/etassia/diagram/xy_chart.rb +90 -0
- data/lib/etassia/diagram.rb +8 -0
- data/lib/etassia/diagram_registry.rb +34 -0
- data/lib/etassia/engine.rb +334 -0
- data/lib/etassia/error/diagram_type_error.rb +10 -0
- data/lib/etassia/error/layout_error.rb +12 -0
- data/lib/etassia/error/notation_registration_error.rb +10 -0
- data/lib/etassia/error/parse_error.rb +11 -0
- data/lib/etassia/error/pipeline_error.rb +10 -0
- data/lib/etassia/error/render_error.rb +12 -0
- data/lib/etassia/error/score_error.rb +12 -0
- data/lib/etassia/error.rb +10 -0
- data/lib/etassia/error_report.rb +69 -0
- data/lib/etassia/ir/data.rb +39 -0
- data/lib/etassia/ir/data_value.rb +35 -0
- data/lib/etassia/ir/dimension.rb +10 -0
- data/lib/etassia/ir/edge.rb +34 -0
- data/lib/etassia/ir/graph.rb +28 -0
- data/lib/etassia/ir/integrity.rb +80 -0
- data/lib/etassia/ir/item.rb +46 -0
- data/lib/etassia/ir/model.rb +56 -0
- data/lib/etassia/ir/node.rb +10 -0
- data/lib/etassia/ir/placement.rb +47 -0
- data/lib/etassia/ir/prepositioned.rb +25 -0
- data/lib/etassia/ir/prepositioned_item.rb +26 -0
- data/lib/etassia/ir/property_set.rb +43 -0
- data/lib/etassia/ir/scalar.rb +25 -0
- data/lib/etassia/ir/series.rb +10 -0
- data/lib/etassia/ir.rb +17 -0
- data/lib/etassia/js_math.rb +511 -0
- data/lib/etassia/js_number.rb +64 -0
- data/lib/etassia/layout/architecture.rb +1472 -0
- data/lib/etassia/layout/base.rb +276 -0
- data/lib/etassia/layout/block.rb +738 -0
- data/lib/etassia/layout/c4.rb +876 -0
- data/lib/etassia/layout/c4_boundary_metrics.rb +35 -0
- data/lib/etassia/layout/c4_bounds.rb +106 -0
- data/lib/etassia/layout/c4_intersection.rb +77 -0
- data/lib/etassia/layout/c4_placement.rb +135 -0
- data/lib/etassia/layout/c4_shape_metrics.rb +97 -0
- data/lib/etassia/layout/c4_shape_outline.rb +67 -0
- data/lib/etassia/layout/c4_stereotype.rb +30 -0
- data/lib/etassia/layout/c4_text.rb +54 -0
- data/lib/etassia/layout/class_cell_placement.rb +148 -0
- data/lib/etassia/layout/class_diagram.rb +1116 -0
- data/lib/etassia/layout/class_diagram_namespaces.rb +26 -0
- data/lib/etassia/layout/class_diagram_notes.rb +26 -0
- data/lib/etassia/layout/class_name_advances.rb +31 -0
- data/lib/etassia/layout/class_namespace_box.rb +21 -0
- data/lib/etassia/layout/class_namespace_nodes.rb +197 -0
- data/lib/etassia/layout/class_namespace_placement.rb +71 -0
- data/lib/etassia/layout/class_note.rb +44 -0
- data/lib/etassia/layout/class_note_nodes.rb +84 -0
- data/lib/etassia/layout/elk_notices.rb +48 -0
- data/lib/etassia/layout/elk_placement.rb +197 -0
- data/lib/etassia/layout/er_diagram.rb +1002 -0
- data/lib/etassia/layout/er_diagram_adjacency.rb +20 -0
- data/lib/etassia/layout/er_diagram_alignment.rb +72 -0
- data/lib/etassia/layout/er_diagram_compaction.rb +85 -0
- data/lib/etassia/layout/er_diagram_conflicts.rb +70 -0
- data/lib/etassia/layout/er_diagram_coordinates.rb +104 -0
- data/lib/etassia/layout/er_diagram_entity_size.rb +82 -0
- data/lib/etassia/layout/er_diagram_layering.rb +109 -0
- data/lib/etassia/layout/er_diagram_ordering.rb +143 -0
- data/lib/etassia/layout/er_diagram_placement.rb +163 -0
- data/lib/etassia/layout/er_diagram_placement_node.rb +22 -0
- data/lib/etassia/layout/error.rb +133 -0
- data/lib/etassia/layout/flowchart/brandes_koepf.rb +208 -0
- data/lib/etassia/layout/flowchart/cluster_reach.rb +70 -0
- data/lib/etassia/layout/flowchart/component_packing.rb +129 -0
- data/lib/etassia/layout/flowchart/directed_placement.rb +428 -0
- data/lib/etassia/layout/flowchart/edge_chains.rb +111 -0
- data/lib/etassia/layout/flowchart/edge_order.rb +34 -0
- data/lib/etassia/layout/flowchart/edge_router.rb +533 -0
- data/lib/etassia/layout/flowchart/label_backdrop_geometry.rb +62 -0
- data/lib/etassia/layout/flowchart/layer_spacing.rb +85 -0
- data/lib/etassia/layout/flowchart/level_edges.rb +74 -0
- data/lib/etassia/layout/flowchart/level_frame.rb +47 -0
- data/lib/etassia/layout/flowchart/minimum_rank_spacing.rb +74 -0
- data/lib/etassia/layout/flowchart/row_centering.rb +229 -0
- data/lib/etassia/layout/flowchart.rb +1290 -0
- data/lib/etassia/layout/gantt.rb +814 -0
- data/lib/etassia/layout/gantt_duration.rb +49 -0
- data/lib/etassia/layout/gantt_exclusions.rb +74 -0
- data/lib/etassia/layout/gantt_ticks.rb +165 -0
- data/lib/etassia/layout/git_graph.rb +1075 -0
- data/lib/etassia/layout/grid.rb +165 -0
- data/lib/etassia/layout/info.rb +113 -0
- data/lib/etassia/layout/kanban.rb +452 -0
- data/lib/etassia/layout/kanban_card_text.rb +95 -0
- data/lib/etassia/layout/mindmap.rb +425 -0
- data/lib/etassia/layout/mindmap_cose.rb +81 -0
- data/lib/etassia/layout/mindmap_cose_body.rb +93 -0
- data/lib/etassia/layout/mindmap_cose_geometry.rb +190 -0
- data/lib/etassia/layout/mindmap_cose_grid.rb +96 -0
- data/lib/etassia/layout/mindmap_cose_link.rb +42 -0
- data/lib/etassia/layout/mindmap_cose_relaxation.rb +166 -0
- data/lib/etassia/layout/mindmap_cose_seed.rb +93 -0
- data/lib/etassia/layout/mindmap_cose_wedge.rb +37 -0
- data/lib/etassia/layout/mindmap_node_size.rb +130 -0
- data/lib/etassia/layout/packet.rb +285 -0
- data/lib/etassia/layout/pie.rb +305 -0
- data/lib/etassia/layout/pie_legend.rb +64 -0
- data/lib/etassia/layout/pie_legend_entry.rb +21 -0
- data/lib/etassia/layout/quadrant.rb +402 -0
- data/lib/etassia/layout/radar.rb +442 -0
- data/lib/etassia/layout/requirement.rb +765 -0
- data/lib/etassia/layout/sankey.rb +501 -0
- data/lib/etassia/layout/scene.rb +18 -0
- data/lib/etassia/layout/sequence/actor_gaps.rb +151 -0
- data/lib/etassia/layout/sequence/box_shape.rb +23 -0
- data/lib/etassia/layout/sequence/creations.rb +73 -0
- data/lib/etassia/layout/sequence/frame_divider.rb +22 -0
- data/lib/etassia/layout/sequence/frame_placement.rb +424 -0
- data/lib/etassia/layout/sequence/frame_reader.rb +75 -0
- data/lib/etassia/layout/sequence/frame_shape.rb +45 -0
- data/lib/etassia/layout/sequence/geometry.rb +24 -0
- data/lib/etassia/layout/sequence/message_rows.rb +165 -0
- data/lib/etassia/layout/sequence/note.rb +16 -0
- data/lib/etassia/layout/sequence/note_box.rb +114 -0
- data/lib/etassia/layout/sequence/note_placement.rb +223 -0
- data/lib/etassia/layout/sequence/text_width.rb +69 -0
- data/lib/etassia/layout/sequence/text_wrap.rb +93 -0
- data/lib/etassia/layout/sequence.rb +936 -0
- data/lib/etassia/layout/state_diagram.rb +1277 -0
- data/lib/etassia/layout/timeline.rb +165 -0
- data/lib/etassia/layout/timeline_card.rb +142 -0
- data/lib/etassia/layout/timeline_flow.rb +164 -0
- data/lib/etassia/layout/timeline_line.rb +33 -0
- data/lib/etassia/layout/timeline_rounding.rb +19 -0
- data/lib/etassia/layout/timeline_text.rb +105 -0
- data/lib/etassia/layout/treemap.rb +303 -0
- data/lib/etassia/layout/treemap_placement.rb +52 -0
- data/lib/etassia/layout/treemap_squarify.rb +108 -0
- data/lib/etassia/layout/user_journey.rb +300 -0
- data/lib/etassia/layout/user_journey_face.rb +33 -0
- data/lib/etassia/layout/user_journey_face_geometry.rb +80 -0
- data/lib/etassia/layout/user_journey_geometry.rb +336 -0
- data/lib/etassia/layout/user_journey_legend.rb +84 -0
- data/lib/etassia/layout/xy_chart.rb +620 -0
- data/lib/etassia/layout.rb +19 -0
- data/lib/etassia/lint_debt_scoreboard.rb +219 -0
- data/lib/etassia/markdown_text/emphasis_simulator.rb +373 -0
- data/lib/etassia/markdown_text.rb +393 -0
- data/lib/etassia/notation/builtin.rb +39 -0
- data/lib/etassia/notation/entry.rb +15 -0
- data/lib/etassia/notation/mermaid/class_diagram_config.rb +42 -0
- data/lib/etassia/notation/mermaid/git_graph_config.rb +46 -0
- data/lib/etassia/notation/mermaid/ir_adapter.rb +27 -0
- data/lib/etassia/notation/mermaid/ir_adapters/architecture.rb +196 -0
- data/lib/etassia/notation/mermaid/ir_adapters/block.rb +138 -0
- data/lib/etassia/notation/mermaid/ir_adapters/c4.rb +258 -0
- data/lib/etassia/notation/mermaid/ir_adapters/class_diagram.rb +257 -0
- data/lib/etassia/notation/mermaid/ir_adapters/er_diagram.rb +220 -0
- data/lib/etassia/notation/mermaid/ir_adapters/error.rb +32 -0
- data/lib/etassia/notation/mermaid/ir_adapters/flowchart.rb +208 -0
- data/lib/etassia/notation/mermaid/ir_adapters/gantt.rb +236 -0
- data/lib/etassia/notation/mermaid/ir_adapters/git_graph.rb +186 -0
- data/lib/etassia/notation/mermaid/ir_adapters/info.rb +34 -0
- data/lib/etassia/notation/mermaid/ir_adapters/kanban.rb +110 -0
- data/lib/etassia/notation/mermaid/ir_adapters/mindmap.rb +76 -0
- data/lib/etassia/notation/mermaid/ir_adapters/packet.rb +51 -0
- data/lib/etassia/notation/mermaid/ir_adapters/pie.rb +71 -0
- data/lib/etassia/notation/mermaid/ir_adapters/quadrant.rb +116 -0
- data/lib/etassia/notation/mermaid/ir_adapters/radar.rb +142 -0
- data/lib/etassia/notation/mermaid/ir_adapters/requirement.rb +237 -0
- data/lib/etassia/notation/mermaid/ir_adapters/sankey.rb +74 -0
- data/lib/etassia/notation/mermaid/ir_adapters/sequence.rb +304 -0
- data/lib/etassia/notation/mermaid/ir_adapters/state_diagram.rb +225 -0
- data/lib/etassia/notation/mermaid/ir_adapters/timeline.rb +93 -0
- data/lib/etassia/notation/mermaid/ir_adapters/treemap.rb +143 -0
- data/lib/etassia/notation/mermaid/ir_adapters/user_journey.rb +127 -0
- data/lib/etassia/notation/mermaid/ir_adapters/xychart.rb +131 -0
- data/lib/etassia/notation/mermaid.rb +399 -0
- data/lib/etassia/notation/parsed.rb +31 -0
- data/lib/etassia/notation/plantuml/activity/arrow_head.rb +45 -0
- data/lib/etassia/notation/plantuml/activity/arrow_style.rb +53 -0
- data/lib/etassia/notation/plantuml/activity/bar_columns.rb +75 -0
- data/lib/etassia/notation/plantuml/activity/box.rb +60 -0
- data/lib/etassia/notation/plantuml/activity/builder.rb +77 -0
- data/lib/etassia/notation/plantuml/activity/canvas.rb +117 -0
- data/lib/etassia/notation/plantuml/activity/column_centres.rb +151 -0
- data/lib/etassia/notation/plantuml/activity/columns.rb +224 -0
- data/lib/etassia/notation/plantuml/activity/diagram.rb +35 -0
- data/lib/etassia/notation/plantuml/activity/edge.rb +35 -0
- data/lib/etassia/notation/plantuml/activity/edge_marks.rb +68 -0
- data/lib/etassia/notation/plantuml/activity/exits.rb +61 -0
- data/lib/etassia/notation/plantuml/activity/frame_marks.rb +116 -0
- data/lib/etassia/notation/plantuml/activity/lane_steps.rb +82 -0
- data/lib/etassia/notation/plantuml/activity/lane_titles.rb +46 -0
- data/lib/etassia/notation/plantuml/activity/layout.rb +50 -0
- data/lib/etassia/notation/plantuml/activity/legacy_parser.rb +181 -0
- data/lib/etassia/notation/plantuml/activity/legacy_row_gaps.rb +65 -0
- data/lib/etassia/notation/plantuml/activity/mark.rb +23 -0
- data/lib/etassia/notation/plantuml/activity/marks.rb +62 -0
- data/lib/etassia/notation/plantuml/activity/metrics.rb +169 -0
- data/lib/etassia/notation/plantuml/activity/new_parser/blocks.rb +166 -0
- data/lib/etassia/notation/plantuml/activity/new_parser.rb +177 -0
- data/lib/etassia/notation/plantuml/activity/node.rb +50 -0
- data/lib/etassia/notation/plantuml/activity/node_marks.rb +174 -0
- data/lib/etassia/notation/plantuml/activity/note.rb +27 -0
- data/lib/etassia/notation/plantuml/activity/parser.rb +51 -0
- data/lib/etassia/notation/plantuml/activity/placement.rb +327 -0
- data/lib/etassia/notation/plantuml/activity/ranking.rb +142 -0
- data/lib/etassia/notation/plantuml/activity/reach.rb +58 -0
- data/lib/etassia/notation/plantuml/activity/renderer.rb +220 -0
- data/lib/etassia/notation/plantuml/activity/route.rb +22 -0
- data/lib/etassia/notation/plantuml/activity/router.rb +308 -0
- data/lib/etassia/notation/plantuml/activity/row_gaps.rb +105 -0
- data/lib/etassia/notation/plantuml/activity/row_spread.rb +59 -0
- data/lib/etassia/notation/plantuml/activity/scene.rb +20 -0
- data/lib/etassia/notation/plantuml/activity/text.rb +36 -0
- data/lib/etassia/notation/plantuml/activity.rb +50 -0
- data/lib/etassia/notation/plantuml/arrow.rb +135 -0
- data/lib/etassia/notation/plantuml/caption.rb +50 -0
- data/lib/etassia/notation/plantuml/caption_layout.rb +97 -0
- data/lib/etassia/notation/plantuml/caption_rows.rb +67 -0
- data/lib/etassia/notation/plantuml/class_header.rb +178 -0
- data/lib/etassia/notation/plantuml/class_name.rb +51 -0
- data/lib/etassia/notation/plantuml/component.rb +37 -0
- data/lib/etassia/notation/plantuml/creole.rb +278 -0
- data/lib/etassia/notation/plantuml/creole_icons.rb +33 -0
- data/lib/etassia/notation/plantuml/diagram.rb +60 -0
- data/lib/etassia/notation/plantuml/diagram_builder.rb +442 -0
- data/lib/etassia/notation/plantuml/diagram_name.rb +51 -0
- data/lib/etassia/notation/plantuml/directives.rb +33 -0
- data/lib/etassia/notation/plantuml/drawing/box.rb +28 -0
- data/lib/etassia/notation/plantuml/drawing/face.rb +39 -0
- data/lib/etassia/notation/plantuml/drawing/pen.rb +105 -0
- data/lib/etassia/notation/plantuml/drawing/renderer.rb +36 -0
- data/lib/etassia/notation/plantuml/drawing/scene.rb +28 -0
- data/lib/etassia/notation/plantuml/drawing.rb +19 -0
- data/lib/etassia/notation/plantuml/ebnf/blocks.rb +272 -0
- data/lib/etassia/notation/plantuml/ebnf/layout.rb +117 -0
- data/lib/etassia/notation/plantuml/ebnf/model.rb +22 -0
- data/lib/etassia/notation/plantuml/ebnf/reader.rb +131 -0
- data/lib/etassia/notation/plantuml/ebnf.rb +34 -0
- data/lib/etassia/notation/plantuml/end_texts.rb +78 -0
- data/lib/etassia/notation/plantuml/entity.rb +54 -0
- data/lib/etassia/notation/plantuml/further.rb +38 -0
- data/lib/etassia/notation/plantuml/gantt/layout.rb +269 -0
- data/lib/etassia/notation/plantuml/gantt/reader.rb +210 -0
- data/lib/etassia/notation/plantuml/gantt/renderer.rb +81 -0
- data/lib/etassia/notation/plantuml/gantt.rb +34 -0
- data/lib/etassia/notation/plantuml/include/blocks.rb +66 -0
- data/lib/etassia/notation/plantuml/include/finder.rb +61 -0
- data/lib/etassia/notation/plantuml/include.rb +78 -0
- data/lib/etassia/notation/plantuml/ir_adapter/annotations.rb +40 -0
- data/lib/etassia/notation/plantuml/ir_adapter/links.rb +91 -0
- data/lib/etassia/notation/plantuml/ir_adapter/sink.rb +49 -0
- data/lib/etassia/notation/plantuml/ir_adapter/structure.rb +86 -0
- data/lib/etassia/notation/plantuml/ir_adapter.rb +32 -0
- data/lib/etassia/notation/plantuml/ir_reader/annotations.rb +51 -0
- data/lib/etassia/notation/plantuml/ir_reader/index.rb +49 -0
- data/lib/etassia/notation/plantuml/ir_reader/links.rb +133 -0
- data/lib/etassia/notation/plantuml/ir_reader/structure.rb +83 -0
- data/lib/etassia/notation/plantuml/ir_reader.rb +42 -0
- data/lib/etassia/notation/plantuml/junction.rb +20 -0
- data/lib/etassia/notation/plantuml/klass.rb +41 -0
- data/lib/etassia/notation/plantuml/label_clearance.rb +67 -0
- data/lib/etassia/notation/plantuml/layout.rb +2806 -0
- data/lib/etassia/notation/plantuml/member.rb +37 -0
- data/lib/etassia/notation/plantuml/member_icon.rb +87 -0
- data/lib/etassia/notation/plantuml/member_icon_elements.rb +91 -0
- data/lib/etassia/notation/plantuml/member_pitch.rb +76 -0
- data/lib/etassia/notation/plantuml/member_rule.rb +48 -0
- data/lib/etassia/notation/plantuml/network/grid.rb +170 -0
- data/lib/etassia/notation/plantuml/network/layout.rb +158 -0
- data/lib/etassia/notation/plantuml/network/model.rb +22 -0
- data/lib/etassia/notation/plantuml/network/placement.rb +64 -0
- data/lib/etassia/notation/plantuml/network/reader.rb +126 -0
- data/lib/etassia/notation/plantuml/network/shapes.rb +109 -0
- data/lib/etassia/notation/plantuml/network.rb +37 -0
- data/lib/etassia/notation/plantuml/node_graph/archimate.rb +49 -0
- data/lib/etassia/notation/plantuml/node_graph/box_reader.rb +86 -0
- data/lib/etassia/notation/plantuml/node_graph/cloud.rb +73 -0
- data/lib/etassia/notation/plantuml/node_graph/crows_foot.rb +85 -0
- data/lib/etassia/notation/plantuml/node_graph/curve.rb +66 -0
- data/lib/etassia/notation/plantuml/node_graph/data_reader.rb +77 -0
- data/lib/etassia/notation/plantuml/node_graph/data_shape.rb +39 -0
- data/lib/etassia/notation/plantuml/node_graph/detour.rb +98 -0
- data/lib/etassia/notation/plantuml/node_graph/edge.rb +26 -0
- data/lib/etassia/notation/plantuml/node_graph/entity_metrics.rb +49 -0
- data/lib/etassia/notation/plantuml/node_graph/entity_reader.rb +65 -0
- data/lib/etassia/notation/plantuml/node_graph/entity_shape.rb +111 -0
- data/lib/etassia/notation/plantuml/node_graph/figures.rb +92 -0
- data/lib/etassia/notation/plantuml/node_graph/frame_ports.rb +73 -0
- data/lib/etassia/notation/plantuml/node_graph/graph.rb +40 -0
- data/lib/etassia/notation/plantuml/node_graph/heads.rb +89 -0
- data/lib/etassia/notation/plantuml/node_graph/label_fan.rb +76 -0
- data/lib/etassia/notation/plantuml/node_graph/label_placer.rb +310 -0
- data/lib/etassia/notation/plantuml/node_graph/label_waypoint.rb +47 -0
- data/lib/etassia/notation/plantuml/node_graph/layout.rb +293 -0
- data/lib/etassia/notation/plantuml/node_graph/link.rb +78 -0
- data/lib/etassia/notation/plantuml/node_graph/macros.rb +39 -0
- data/lib/etassia/notation/plantuml/node_graph/metrics.rb +113 -0
- data/lib/etassia/notation/plantuml/node_graph/node.rb +44 -0
- data/lib/etassia/notation/plantuml/node_graph/note_tail.rb +58 -0
- data/lib/etassia/notation/plantuml/node_graph/packer.rb +126 -0
- data/lib/etassia/notation/plantuml/node_graph/parser.rb +309 -0
- data/lib/etassia/notation/plantuml/node_graph/paths.rb +48 -0
- data/lib/etassia/notation/plantuml/node_graph/ranking.rb +231 -0
- data/lib/etassia/notation/plantuml/node_graph/reader.rb +53 -0
- data/lib/etassia/notation/plantuml/node_graph/references.rb +38 -0
- data/lib/etassia/notation/plantuml/node_graph/renderer.rb +160 -0
- data/lib/etassia/notation/plantuml/node_graph/scene.rb +22 -0
- data/lib/etassia/notation/plantuml/node_graph/shapes.rb +302 -0
- data/lib/etassia/notation/plantuml/node_graph/skipped_ranks.rb +61 -0
- data/lib/etassia/notation/plantuml/node_graph/tab.rb +68 -0
- data/lib/etassia/notation/plantuml/node_graph/usecase_ellipse.rb +100 -0
- data/lib/etassia/notation/plantuml/node_graph.rb +38 -0
- data/lib/etassia/notation/plantuml/note.rb +27 -0
- data/lib/etassia/notation/plantuml/note_layout.rb +257 -0
- data/lib/etassia/notation/plantuml/note_outline.rb +68 -0
- data/lib/etassia/notation/plantuml/objects/map_rows.rb +46 -0
- data/lib/etassia/notation/plantuml/objects/reader.rb +208 -0
- data/lib/etassia/notation/plantuml/objects.rb +39 -0
- data/lib/etassia/notation/plantuml/package.rb +45 -0
- data/lib/etassia/notation/plantuml/package_color.rb +32 -0
- data/lib/etassia/notation/plantuml/package_frames.rb +178 -0
- data/lib/etassia/notation/plantuml/parser.rb +622 -0
- data/lib/etassia/notation/plantuml/portion_visibility.rb +91 -0
- data/lib/etassia/notation/plantuml/qualified_association_layout.rb +332 -0
- data/lib/etassia/notation/plantuml/qualifier_box.rb +78 -0
- data/lib/etassia/notation/plantuml/rank_chain.rb +527 -0
- data/lib/etassia/notation/plantuml/rank_hierarchy.rb +347 -0
- data/lib/etassia/notation/plantuml/relation.rb +103 -0
- data/lib/etassia/notation/plantuml/relation_label_separator.rb +199 -0
- data/lib/etassia/notation/plantuml/relation_path_points.rb +84 -0
- data/lib/etassia/notation/plantuml/renderer.rb +490 -0
- data/lib/etassia/notation/plantuml/scene.rb +134 -0
- data/lib/etassia/notation/plantuml/segment_box.rb +85 -0
- data/lib/etassia/notation/plantuml/sequence/activation.rb +28 -0
- data/lib/etassia/notation/plantuml/sequence/appearance.rb +71 -0
- data/lib/etassia/notation/plantuml/sequence/arrow_end.rb +23 -0
- data/lib/etassia/notation/plantuml/sequence/arrow_marks.rb +111 -0
- data/lib/etassia/notation/plantuml/sequence/arrow_style.rb +42 -0
- data/lib/etassia/notation/plantuml/sequence/arrow_syntax.rb +122 -0
- data/lib/etassia/notation/plantuml/sequence/bar_tracker.rb +59 -0
- data/lib/etassia/notation/plantuml/sequence/box.rb +23 -0
- data/lib/etassia/notation/plantuml/sequence/chrome.rb +60 -0
- data/lib/etassia/notation/plantuml/sequence/chrome_rows.rb +184 -0
- data/lib/etassia/notation/plantuml/sequence/create.rb +34 -0
- data/lib/etassia/notation/plantuml/sequence/delay.rb +19 -0
- data/lib/etassia/notation/plantuml/sequence/destroy.rb +20 -0
- data/lib/etassia/notation/plantuml/sequence/diagram.rb +86 -0
- data/lib/etassia/notation/plantuml/sequence/divider.rb +19 -0
- data/lib/etassia/notation/plantuml/sequence/edge.rb +53 -0
- data/lib/etassia/notation/plantuml/sequence/embedded.rb +77 -0
- data/lib/etassia/notation/plantuml/sequence/fill.rb +56 -0
- data/lib/etassia/notation/plantuml/sequence/fragment.rb +28 -0
- data/lib/etassia/notation/plantuml/sequence/fragment_shape.rb +138 -0
- data/lib/etassia/notation/plantuml/sequence/head_style.rb +43 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter/blocks.rb +73 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter/fills.rb +24 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter/items.rb +48 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter/messages.rb +62 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter/participants.rb +42 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter/settings.rb +54 -0
- data/lib/etassia/notation/plantuml/sequence/ir_adapter.rb +38 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/blocks.rb +77 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/fills.rb +27 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/index.rb +36 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/items.rb +35 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/messages.rb +70 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/participants.rb +42 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader/settings.rb +74 -0
- data/lib/etassia/notation/plantuml/sequence/ir_reader.rb +38 -0
- data/lib/etassia/notation/plantuml/sequence/keyword_defaults.rb +28 -0
- data/lib/etassia/notation/plantuml/sequence/layout.rb +1567 -0
- data/lib/etassia/notation/plantuml/sequence/message.rb +97 -0
- data/lib/etassia/notation/plantuml/sequence/message_wrap.rb +77 -0
- data/lib/etassia/notation/plantuml/sequence/note.rb +61 -0
- data/lib/etassia/notation/plantuml/sequence/note_fill.rb +35 -0
- data/lib/etassia/notation/plantuml/sequence/note_geometry.rb +126 -0
- data/lib/etassia/notation/plantuml/sequence/note_shape.rb +51 -0
- data/lib/etassia/notation/plantuml/sequence/numbered_label.rb +57 -0
- data/lib/etassia/notation/plantuml/sequence/numbering.rb +49 -0
- data/lib/etassia/notation/plantuml/sequence/outline.rb +204 -0
- data/lib/etassia/notation/plantuml/sequence/page_break.rb +17 -0
- data/lib/etassia/notation/plantuml/sequence/paint.rb +79 -0
- data/lib/etassia/notation/plantuml/sequence/parallel_message.rb +19 -0
- data/lib/etassia/notation/plantuml/sequence/parser.rb +844 -0
- data/lib/etassia/notation/plantuml/sequence/participant.rb +27 -0
- data/lib/etassia/notation/plantuml/sequence/picture.rb +29 -0
- data/lib/etassia/notation/plantuml/sequence/ref.rb +24 -0
- data/lib/etassia/notation/plantuml/sequence/ref_shape.rb +74 -0
- data/lib/etassia/notation/plantuml/sequence/refusals.rb +47 -0
- data/lib/etassia/notation/plantuml/sequence/renderer.rb +501 -0
- data/lib/etassia/notation/plantuml/sequence/scene.rb +200 -0
- data/lib/etassia/notation/plantuml/sequence/skin.rb +76 -0
- data/lib/etassia/notation/plantuml/sequence/spacing.rb +28 -0
- data/lib/etassia/notation/plantuml/sequence/style.rb +151 -0
- data/lib/etassia/notation/plantuml/sequence/tab_size.rb +42 -0
- data/lib/etassia/notation/plantuml/sequence/walker.rb +1190 -0
- data/lib/etassia/notation/plantuml/sequence.rb +47 -0
- data/lib/etassia/notation/plantuml/source_lines.rb +33 -0
- data/lib/etassia/notation/plantuml/state/axis_solver.rb +272 -0
- data/lib/etassia/notation/plantuml/state/cluster_padding.rb +75 -0
- data/lib/etassia/notation/plantuml/state/curve.rb +73 -0
- data/lib/etassia/notation/plantuml/state/dot_axis.rb +248 -0
- data/lib/etassia/notation/plantuml/state/extent.rb +42 -0
- data/lib/etassia/notation/plantuml/state/hierarchy.rb +355 -0
- data/lib/etassia/notation/plantuml/state/label_clearance.rb +148 -0
- data/lib/etassia/notation/plantuml/state/label_reach.rb +139 -0
- data/lib/etassia/notation/plantuml/state/layout.rb +173 -0
- data/lib/etassia/notation/plantuml/state/level_edges.rb +113 -0
- data/lib/etassia/notation/plantuml/state/look.rb +36 -0
- data/lib/etassia/notation/plantuml/state/note_shape.rb +73 -0
- data/lib/etassia/notation/plantuml/state/plane.rb +85 -0
- data/lib/etassia/notation/plantuml/state/ranking.rb +181 -0
- data/lib/etassia/notation/plantuml/state/reader.rb +310 -0
- data/lib/etassia/notation/plantuml/state/renderer.rb +253 -0
- data/lib/etassia/notation/plantuml/state/route_detour.rb +115 -0
- data/lib/etassia/notation/plantuml/state/route_geometry.rb +186 -0
- data/lib/etassia/notation/plantuml/state/routes.rb +196 -0
- data/lib/etassia/notation/plantuml/state/row.rb +88 -0
- data/lib/etassia/notation/plantuml/state/rows.rb +50 -0
- data/lib/etassia/notation/plantuml/state/stretch.rb +71 -0
- data/lib/etassia/notation/plantuml/state.rb +62 -0
- data/lib/etassia/notation/plantuml/stereotype_head.rb +58 -0
- data/lib/etassia/notation/plantuml/style/bound.rb +34 -0
- data/lib/etassia/notation/plantuml/style/colour.rb +41 -0
- data/lib/etassia/notation/plantuml/style/colour_names.rb +89 -0
- data/lib/etassia/notation/plantuml/style/defaults.rb +33 -0
- data/lib/etassia/notation/plantuml/style/inline.rb +56 -0
- data/lib/etassia/notation/plantuml/style/reader.rb +242 -0
- data/lib/etassia/notation/plantuml/style/sheet.rb +103 -0
- data/lib/etassia/notation/plantuml/style.rb +32 -0
- data/lib/etassia/notation/plantuml/style_reader.rb +160 -0
- data/lib/etassia/notation/plantuml/style_sheet.rb +27 -0
- data/lib/etassia/notation/plantuml/styled_relation_curve.rb +71 -0
- data/lib/etassia/notation/plantuml/timing/arrows.rb +81 -0
- data/lib/etassia/notation/plantuml/timing/axis.rb +100 -0
- data/lib/etassia/notation/plantuml/timing/layout.rb +172 -0
- data/lib/etassia/notation/plantuml/timing/model.rb +34 -0
- data/lib/etassia/notation/plantuml/timing/reader.rb +173 -0
- data/lib/etassia/notation/plantuml/timing/signals.rb +328 -0
- data/lib/etassia/notation/plantuml/timing.rb +38 -0
- data/lib/etassia/notation/plantuml/tree/layout.rb +276 -0
- data/lib/etassia/notation/plantuml/tree/mindmap_layout.rb +157 -0
- data/lib/etassia/notation/plantuml/tree/reader.rb +186 -0
- data/lib/etassia/notation/plantuml/tree/renderer.rb +67 -0
- data/lib/etassia/notation/plantuml/tree/role.rb +57 -0
- data/lib/etassia/notation/plantuml/tree/rules.rb +156 -0
- data/lib/etassia/notation/plantuml/tree/wbs_layout.rb +164 -0
- data/lib/etassia/notation/plantuml/tree.rb +41 -0
- data/lib/etassia/notation/plantuml/unsupported_construct_error.rb +39 -0
- data/lib/etassia/notation/plantuml/unsupported_constructs.rb +103 -0
- data/lib/etassia/notation/plantuml/usecase.rb +36 -0
- data/lib/etassia/notation/plantuml.rb +132 -0
- data/lib/etassia/notation/plantuml_data/boxes/frames.rb +101 -0
- data/lib/etassia/notation/plantuml_data/boxes/graph_layout.rb +348 -0
- data/lib/etassia/notation/plantuml_data/boxes/heads.rb +65 -0
- data/lib/etassia/notation/plantuml_data/boxes/highlights.rb +70 -0
- data/lib/etassia/notation/plantuml_data/boxes/inline_rows.rb +99 -0
- data/lib/etassia/notation/plantuml_data/boxes/json_reader.rb +24 -0
- data/lib/etassia/notation/plantuml_data/boxes/link_text.rb +94 -0
- data/lib/etassia/notation/plantuml_data/boxes/marks.rb +76 -0
- data/lib/etassia/notation/plantuml_data/boxes/ornaments.rb +130 -0
- data/lib/etassia/notation/plantuml_data/boxes/renderer.rb +95 -0
- data/lib/etassia/notation/plantuml_data/boxes/row_alignment.rb +58 -0
- data/lib/etassia/notation/plantuml_data/boxes/row_bend.rb +43 -0
- data/lib/etassia/notation/plantuml_data/boxes/scene.rb +27 -0
- data/lib/etassia/notation/plantuml_data/boxes/table.rb +56 -0
- data/lib/etassia/notation/plantuml_data/boxes/table_box.rb +355 -0
- data/lib/etassia/notation/plantuml_data/boxes/text.rb +101 -0
- data/lib/etassia/notation/plantuml_data/boxes/tree_builder.rb +82 -0
- data/lib/etassia/notation/plantuml_data/boxes/tree_layout.rb +82 -0
- data/lib/etassia/notation/plantuml_data/boxes/yaml_reader.rb +105 -0
- data/lib/etassia/notation/plantuml_data/openiconic.LICENSE +6 -0
- data/lib/etassia/notation/plantuml_data/openiconic.json +1 -0
- data/lib/etassia/notation/plantuml_data.rb +84 -0
- data/lib/etassia/notation/plantuml_salt/block.rb +80 -0
- data/lib/etassia/notation/plantuml_salt/box.rb +30 -0
- data/lib/etassia/notation/plantuml_salt/font_metrics.rb +28 -0
- data/lib/etassia/notation/plantuml_salt/grid_marks.rb +66 -0
- data/lib/etassia/notation/plantuml_salt/group_marks.rb +77 -0
- data/lib/etassia/notation/plantuml_salt/layout.rb +206 -0
- data/lib/etassia/notation/plantuml_salt/mark.rb +36 -0
- data/lib/etassia/notation/plantuml_salt/markup.rb +57 -0
- data/lib/etassia/notation/plantuml_salt/parser.rb +239 -0
- data/lib/etassia/notation/plantuml_salt/renderer.rb +138 -0
- data/lib/etassia/notation/plantuml_salt/row.rb +19 -0
- data/lib/etassia/notation/plantuml_salt/run.rb +34 -0
- data/lib/etassia/notation/plantuml_salt/scene.rb +23 -0
- data/lib/etassia/notation/plantuml_salt/scroll_marks.rb +94 -0
- data/lib/etassia/notation/plantuml_salt/tab_marks.rb +74 -0
- data/lib/etassia/notation/plantuml_salt/tree_marks.rb +127 -0
- data/lib/etassia/notation/plantuml_salt/widget.rb +40 -0
- data/lib/etassia/notation/plantuml_salt/widget_marks.rb +319 -0
- data/lib/etassia/notation/plantuml_salt.rb +66 -0
- data/lib/etassia/notation/plugin_failure.rb +42 -0
- data/lib/etassia/notation.rb +289 -0
- data/lib/etassia/parser/architecture.rb +25 -0
- data/lib/etassia/parser/atoms/delimited_run.rb +82 -0
- data/lib/etassia/parser/atoms/greedy_run.rb +116 -0
- data/lib/etassia/parser/atoms/joined.rb +47 -0
- data/lib/etassia/parser/atoms/trimmed_run.rb +47 -0
- data/lib/etassia/parser/base.rb +258 -0
- data/lib/etassia/parser/block.rb +32 -0
- data/lib/etassia/parser/builders/architecture.rb +135 -0
- data/lib/etassia/parser/builders/block.rb +288 -0
- data/lib/etassia/parser/builders/c4.rb +275 -0
- data/lib/etassia/parser/builders/capture_string.rb +17 -0
- data/lib/etassia/parser/builders/class_diagram.rb +539 -0
- data/lib/etassia/parser/builders/er_diagram.rb +308 -0
- data/lib/etassia/parser/builders/error.rb +51 -0
- data/lib/etassia/parser/builders/flowchart.rb +1150 -0
- data/lib/etassia/parser/builders/gantt.rb +267 -0
- data/lib/etassia/parser/builders/git_graph.rb +301 -0
- data/lib/etassia/parser/builders/info.rb +57 -0
- data/lib/etassia/parser/builders/kanban.rb +411 -0
- data/lib/etassia/parser/builders/mindmap.rb +271 -0
- data/lib/etassia/parser/builders/packet.rb +85 -0
- data/lib/etassia/parser/builders/pie.rb +105 -0
- data/lib/etassia/parser/builders/quadrant.rb +165 -0
- data/lib/etassia/parser/builders/radar.rb +157 -0
- data/lib/etassia/parser/builders/requirement.rb +230 -0
- data/lib/etassia/parser/builders/sankey.rb +142 -0
- data/lib/etassia/parser/builders/sequence.rb +431 -0
- data/lib/etassia/parser/builders/sequence_blocks.rb +117 -0
- data/lib/etassia/parser/builders/sequence_box_title.rb +57 -0
- data/lib/etassia/parser/builders/state_diagram.rb +364 -0
- data/lib/etassia/parser/builders/timeline.rb +133 -0
- data/lib/etassia/parser/builders/treemap.rb +138 -0
- data/lib/etassia/parser/builders/xy_chart.rb +148 -0
- data/lib/etassia/parser/c4.rb +37 -0
- data/lib/etassia/parser/class_diagram.rb +58 -0
- data/lib/etassia/parser/class_namespaces.rb +62 -0
- data/lib/etassia/parser/class_notes.rb +54 -0
- data/lib/etassia/parser/er_diagram.rb +28 -0
- data/lib/etassia/parser/error.rb +30 -0
- data/lib/etassia/parser/flowchart.rb +87 -0
- data/lib/etassia/parser/gantt.rb +43 -0
- data/lib/etassia/parser/git_graph.rb +106 -0
- data/lib/etassia/parser/grammars/architecture.rb +165 -0
- data/lib/etassia/parser/grammars/block.rb +190 -0
- data/lib/etassia/parser/grammars/c4.rb +229 -0
- data/lib/etassia/parser/grammars/class_diagram.rb +606 -0
- data/lib/etassia/parser/grammars/common.rb +107 -0
- data/lib/etassia/parser/grammars/er_diagram.rb +232 -0
- data/lib/etassia/parser/grammars/error.rb +32 -0
- data/lib/etassia/parser/grammars/flowchart.rb +1722 -0
- data/lib/etassia/parser/grammars/gantt.rb +198 -0
- data/lib/etassia/parser/grammars/git_graph.rb +234 -0
- data/lib/etassia/parser/grammars/info.rb +53 -0
- data/lib/etassia/parser/grammars/kanban.rb +417 -0
- data/lib/etassia/parser/grammars/mermaid_unicode_text.rb +108 -0
- data/lib/etassia/parser/grammars/mindmap.rb +220 -0
- data/lib/etassia/parser/grammars/packet.rb +71 -0
- data/lib/etassia/parser/grammars/pie.rb +128 -0
- data/lib/etassia/parser/grammars/quadrant.rb +202 -0
- data/lib/etassia/parser/grammars/radar.rb +158 -0
- data/lib/etassia/parser/grammars/requirement.rb +258 -0
- data/lib/etassia/parser/grammars/sankey.rb +104 -0
- data/lib/etassia/parser/grammars/sequence.rb +940 -0
- data/lib/etassia/parser/grammars/state_diagram.rb +382 -0
- data/lib/etassia/parser/grammars/timeline.rb +153 -0
- data/lib/etassia/parser/grammars/treemap.rb +122 -0
- data/lib/etassia/parser/grammars/xy_chart.rb +119 -0
- data/lib/etassia/parser/info.rb +32 -0
- data/lib/etassia/parser/kanban.rb +131 -0
- data/lib/etassia/parser/mermaid_shapes.rb +106 -0
- data/lib/etassia/parser/metadata_yaml.rb +452 -0
- data/lib/etassia/parser/mindmap.rb +92 -0
- data/lib/etassia/parser/native/builders/architecture.rb +103 -0
- data/lib/etassia/parser/native/builders/block.rb +68 -0
- data/lib/etassia/parser/native/builders/c4.rb +16 -0
- data/lib/etassia/parser/native/builders/class_diagram.rb +63 -0
- data/lib/etassia/parser/native/builders/er_diagram.rb +47 -0
- data/lib/etassia/parser/native/builders/flowchart.rb +58 -0
- data/lib/etassia/parser/native/builders/gantt.rb +35 -0
- data/lib/etassia/parser/native/builders/gitgraph.rb +35 -0
- data/lib/etassia/parser/native/builders/info.rb +30 -0
- data/lib/etassia/parser/native/builders/kanban.rb +37 -0
- data/lib/etassia/parser/native/builders/mindmap.rb +26 -0
- data/lib/etassia/parser/native/builders/packet.rb +63 -0
- data/lib/etassia/parser/native/builders/pie.rb +82 -0
- data/lib/etassia/parser/native/builders/quadrant.rb +27 -0
- data/lib/etassia/parser/native/builders/radar.rb +84 -0
- data/lib/etassia/parser/native/builders/requirement.rb +25 -0
- data/lib/etassia/parser/native/builders/sankey.rb +18 -0
- data/lib/etassia/parser/native/builders/sequence.rb +48 -0
- data/lib/etassia/parser/native/builders/state_diagram.rb +61 -0
- data/lib/etassia/parser/native/builders/timeline.rb +46 -0
- data/lib/etassia/parser/native/builders/treemap.rb +60 -0
- data/lib/etassia/parser/native/builders/user_journey.rb +83 -0
- data/lib/etassia/parser/native/builders/xy_chart.rb +94 -0
- data/lib/etassia/parser/native/declined.rb +12 -0
- data/lib/etassia/parser/native/mermaid.json +1 -0
- data/lib/etassia/parser/native/text.rb +31 -0
- data/lib/etassia/parser/native.rb +99 -0
- data/lib/etassia/parser/packet.rb +56 -0
- data/lib/etassia/parser/pie.rb +32 -0
- data/lib/etassia/parser/quadrant.rb +77 -0
- data/lib/etassia/parser/radar.rb +99 -0
- data/lib/etassia/parser/requirement.rb +32 -0
- data/lib/etassia/parser/sankey.rb +47 -0
- data/lib/etassia/parser/sequence.rb +34 -0
- data/lib/etassia/parser/state_diagram.rb +29 -0
- data/lib/etassia/parser/timeline.rb +40 -0
- data/lib/etassia/parser/treemap.rb +25 -0
- data/lib/etassia/parser/user_journey.rb +253 -0
- data/lib/etassia/parser/xy_chart.rb +98 -0
- data/lib/etassia/parser.rb +18 -0
- data/lib/etassia/renderer/architecture.rb +232 -0
- data/lib/etassia/renderer/architecture_edge_router.rb +379 -0
- data/lib/etassia/renderer/base.rb +295 -0
- data/lib/etassia/renderer/block.rb +157 -0
- data/lib/etassia/renderer/c4.rb +294 -0
- data/lib/etassia/renderer/class_diagram.rb +671 -0
- data/lib/etassia/renderer/er_diagram.rb +842 -0
- data/lib/etassia/renderer/error.rb +131 -0
- data/lib/etassia/renderer/flowchart.rb +315 -0
- data/lib/etassia/renderer/gantt.rb +177 -0
- data/lib/etassia/renderer/git_graph.rb +224 -0
- data/lib/etassia/renderer/info.rb +59 -0
- data/lib/etassia/renderer/kanban.rb +157 -0
- data/lib/etassia/renderer/line_break_text.rb +56 -0
- data/lib/etassia/renderer/markdown_text.rb +106 -0
- data/lib/etassia/renderer/mindmap.rb +290 -0
- data/lib/etassia/renderer/packet.rb +87 -0
- data/lib/etassia/renderer/pie.rb +113 -0
- data/lib/etassia/renderer/quadrant.rb +189 -0
- data/lib/etassia/renderer/radar.rb +191 -0
- data/lib/etassia/renderer/requirement.rb +209 -0
- data/lib/etassia/renderer/sankey.rb +114 -0
- data/lib/etassia/renderer/sequence/frame_drawing.rb +169 -0
- data/lib/etassia/renderer/sequence/note_drawing.rb +69 -0
- data/lib/etassia/renderer/sequence.rb +490 -0
- data/lib/etassia/renderer/state_diagram.rb +202 -0
- data/lib/etassia/renderer/timeline.rb +134 -0
- data/lib/etassia/renderer/timeline_palette.rb +18 -0
- data/lib/etassia/renderer/treemap/section_total.rb +33 -0
- data/lib/etassia/renderer/treemap.rb +128 -0
- data/lib/etassia/renderer/user_journey.rb +209 -0
- data/lib/etassia/renderer/xy_chart.rb +180 -0
- data/lib/etassia/renderer.rb +22 -0
- data/lib/etassia/runtime.rb +125 -0
- data/lib/etassia/source/frontmatter.rb +501 -0
- data/lib/etassia/source.rb +224 -0
- data/lib/etassia/svg/arrowhead.rb +205 -0
- data/lib/etassia/svg/circle.rb +19 -0
- data/lib/etassia/svg/document.rb +106 -0
- data/lib/etassia/svg/element.rb +209 -0
- data/lib/etassia/svg/ellipse.rb +23 -0
- data/lib/etassia/svg/escaping.rb +149 -0
- data/lib/etassia/svg/group.rb +64 -0
- data/lib/etassia/svg/line.rb +21 -0
- data/lib/etassia/svg/numbers.rb +53 -0
- data/lib/etassia/svg/path.rb +67 -0
- data/lib/etassia/svg/path_geometry.rb +378 -0
- data/lib/etassia/svg/polygon.rb +25 -0
- data/lib/etassia/svg/polyline.rb +28 -0
- data/lib/etassia/svg/rect.rb +34 -0
- data/lib/etassia/svg/style.rb +50 -0
- data/lib/etassia/svg/text.rb +181 -0
- data/lib/etassia/svg/tspan.rb +48 -0
- data/lib/etassia/svg.rb +38 -0
- data/lib/etassia/text_measurement/arial_advances.rb +122 -0
- data/lib/etassia/text_measurement/dejavu_advances.rb +111 -0
- data/lib/etassia/text_measurement/times_advances.rb +64 -0
- data/lib/etassia/text_measurement/trebuchet_advances.rb +53 -0
- data/lib/etassia/text_measurement/trebuchet_kerning.rb +29 -0
- data/lib/etassia/text_measurement.rb +198 -0
- data/lib/etassia/theme/builtin/dark.yml +229 -0
- data/lib/etassia/theme/builtin/default.yml +239 -0
- data/lib/etassia/theme/builtin/high_contrast.yml +231 -0
- data/lib/etassia/theme/builtin/light.yml +229 -0
- data/lib/etassia/theme/color_palette.rb +97 -0
- data/lib/etassia/theme/completion.rb +50 -0
- data/lib/etassia/theme/effect_styles.rb +34 -0
- data/lib/etassia/theme/registry.rb +45 -0
- data/lib/etassia/theme/shape_styles.rb +34 -0
- data/lib/etassia/theme/spacing_config.rb +30 -0
- data/lib/etassia/theme/typography.rb +40 -0
- data/lib/etassia/theme.rb +73 -0
- data/lib/etassia/version.rb +5 -0
- data/lib/etassia.rb +40 -0
- metadata +865 -0
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "parslet"
|
|
4
|
+
require_relative "../base"
|
|
5
|
+
require_relative "../../diagram/flowchart"
|
|
6
|
+
require_relative "../metadata_yaml"
|
|
7
|
+
require_relative "../mermaid_shapes"
|
|
8
|
+
require_relative "../../diagram/containment"
|
|
9
|
+
|
|
10
|
+
module Etassia
|
|
11
|
+
module Parser
|
|
12
|
+
module Builders
|
|
13
|
+
# Transform for converting flowchart parse trees to diagram models.
|
|
14
|
+
#
|
|
15
|
+
# Handles transformation of nodes, edges, subgraphs, and styling
|
|
16
|
+
# directives from Parslet parse trees into Flowchart diagram objects.
|
|
17
|
+
class Flowchart < Parslet::Transform
|
|
18
|
+
# One parse's bookkeeping.
|
|
19
|
+
#
|
|
20
|
+
# It used to sit in `Thread.current` behind a public `.state`, so
|
|
21
|
+
# any code in the host app could read or rewrite the ids the next
|
|
22
|
+
# parse was about to hand out, and a parse that raised had to
|
|
23
|
+
# remember to clear the slot or it kept the whole parse tree
|
|
24
|
+
# alive. Made inside `apply` and passed down instead, it is
|
|
25
|
+
# reachable from nowhere else and goes away with the parse.
|
|
26
|
+
#
|
|
27
|
+
# A gem runs inside somebody else's app, and these used to be
|
|
28
|
+
# shared on the class: 11 of 36 concurrent parses of a 60-box
|
|
29
|
+
# diagram handed out ids that disagreed with the sequential
|
|
30
|
+
# answer. Every parse now counts on its own object, so two
|
|
31
|
+
# threads cannot see each other at all.
|
|
32
|
+
class Context
|
|
33
|
+
# Generated ids, keyed by statement IDENTITY: two subgraphs
|
|
34
|
+
# written the same way are still two boxes, and each needs its
|
|
35
|
+
# own id.
|
|
36
|
+
attr_reader :ids
|
|
37
|
+
|
|
38
|
+
# Who claimed whom, as won rather than as written. Read by the
|
|
39
|
+
# cycle check once the whole diagram has been walked.
|
|
40
|
+
attr_reader :ownership
|
|
41
|
+
|
|
42
|
+
# The box holding each id, so a member lookup is indexed rather
|
|
43
|
+
# than a scan over every box.
|
|
44
|
+
attr_reader :boxes
|
|
45
|
+
|
|
46
|
+
# Ids already spoken for. The first box to name an id keeps it.
|
|
47
|
+
attr_reader :claimed
|
|
48
|
+
|
|
49
|
+
# Ids given to edges with `A e1@--> B`. A later `e1@{ ... }`
|
|
50
|
+
# addresses the edge and is not a node.
|
|
51
|
+
attr_reader :edge_ids
|
|
52
|
+
|
|
53
|
+
def initialize
|
|
54
|
+
@ids = {}.compare_by_identity
|
|
55
|
+
@ownership = {}
|
|
56
|
+
@boxes = {}
|
|
57
|
+
@claimed = {}
|
|
58
|
+
@edge_ids = Set.new
|
|
59
|
+
@counter = -1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# mermaid names a box carrying a free title `subGraph<n>`, and
|
|
63
|
+
# counts from zero within one diagram.
|
|
64
|
+
#
|
|
65
|
+
# @return [String] the next generated subgraph id
|
|
66
|
+
def next_generated_id
|
|
67
|
+
@counter += 1
|
|
68
|
+
"subGraph#{@counter}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Two ways a name can fail, and they are not the same thing.
|
|
73
|
+
# Mermaid does not know `nope`, and says so in words a corpus case
|
|
74
|
+
# quotes. Mermaid does know `bang`, and draws it as a curved
|
|
75
|
+
# outline etassia has no shape for — naming it a rectangle drew a
|
|
76
|
+
# picture the source did not ask for, which is worse than saying no.
|
|
77
|
+
#
|
|
78
|
+
# @raise [Parser::ParseError] on a name etassia will not draw
|
|
79
|
+
def metadata_shape(name)
|
|
80
|
+
return nil if name.nil?
|
|
81
|
+
|
|
82
|
+
MERMAID_SHAPES.fetch(name) do
|
|
83
|
+
raise Parser::ParseError, unsupported_shape(name)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def unsupported_shape(name)
|
|
88
|
+
return "No such shape: #{name}." unless
|
|
89
|
+
UNDRAWABLE_SHAPES.include?(name)
|
|
90
|
+
|
|
91
|
+
"Shape not supported yet: #{name}."
|
|
92
|
+
end
|
|
93
|
+
private :unsupported_shape
|
|
94
|
+
|
|
95
|
+
# mermaid parses the body with js-yaml: a single-line body as a flow
|
|
96
|
+
# mapping, a multiline one as block YAML. Psych does both, so the
|
|
97
|
+
# rules fall out instead of being reimplemented in the grammar.
|
|
98
|
+
#
|
|
99
|
+
# @raise [Parser::ParseError] on YAML mermaid would also refuse
|
|
100
|
+
def metadata_entries(metadata)
|
|
101
|
+
document = metadata_document(metadata)
|
|
102
|
+
# mermaid reads `doc.shape` off whatever `yaml.load` returns, so a
|
|
103
|
+
# node's document that came back null throws there.
|
|
104
|
+
raise Parser::ParseError, "Empty metadata." if document.nil?
|
|
105
|
+
|
|
106
|
+
entries(document)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The parsed YAML of a metadata block, before any node rule reads
|
|
110
|
+
# it. An edge's block stops here: `label` and `shape` mean nothing
|
|
111
|
+
# to an edge, so mermaid never checks them there.
|
|
112
|
+
#
|
|
113
|
+
# Mermaid wraps a single-line body as a flow mapping and uses a
|
|
114
|
+
# multiline body as written. It does not normalize a missing space
|
|
115
|
+
# after a colon: `shape:rect` is an unknown key, so inserting a space
|
|
116
|
+
# would honor something mermaid ignores.
|
|
117
|
+
def metadata_document(metadata)
|
|
118
|
+
body = line_feeds(metadata_body(metadata))
|
|
119
|
+
body = strip_metadata_comments(body)
|
|
120
|
+
body = break_quoted_newlines(body)
|
|
121
|
+
return {} if body.empty?
|
|
122
|
+
|
|
123
|
+
document = body.include?("\n") ? "#{body}\n" : "{\n#{body}\n}"
|
|
124
|
+
MetadataYaml.value(document)
|
|
125
|
+
end
|
|
126
|
+
private :metadata_document
|
|
127
|
+
|
|
128
|
+
# Mermaid turns every carriage return into a line feed before it
|
|
129
|
+
# lexes anything, so a Windows line ending inside the block is an
|
|
130
|
+
# ordinary newline by the time YAML sees it. Reading it as written
|
|
131
|
+
# folded `"one` CR `two"` onto one line, left a `%YAML 1.3` CR
|
|
132
|
+
# unlevelled, and took a body mermaid refuses.
|
|
133
|
+
def line_feeds(body)
|
|
134
|
+
body.gsub(/\r\n?/, "\n")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Mermaid strips comments before lexing, so metadata never sees them.
|
|
138
|
+
# `%%{` is a directive rather than a comment and stays, and a `%%`
|
|
139
|
+
# with nothing after it is not a comment either.
|
|
140
|
+
#
|
|
141
|
+
# The body is a fragment cut from the middle of a line, so its own
|
|
142
|
+
# first character is not the start of a line — only the newlines
|
|
143
|
+
# inside it start one. Anchoring on `^` instead let the leading
|
|
144
|
+
# whitespace run reach back past `@{` and eat the newline that
|
|
145
|
+
# opened the block, which turned `A@{` nl `%% c` nl `}` into an
|
|
146
|
+
# empty body etassia drew as a rectangle. mmdc refuses it, because
|
|
147
|
+
# what is left after the comment goes is `A@{` nl `}`.
|
|
148
|
+
def strip_metadata_comments(body)
|
|
149
|
+
body.gsub(/(?<=\n)#{JS_SPACE}*%%(?!\{)[^\n]+\n?/o, "")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# A newline inside a double-quoted value is a line break to
|
|
153
|
+
# mermaid, which rewrites it before js-yaml ever sees it. Letting
|
|
154
|
+
# YAML fold it turned `"one` newline `two"` into `one two`, where
|
|
155
|
+
# mmdc renders `one<br/>two`.
|
|
156
|
+
def break_quoted_newlines(body)
|
|
157
|
+
body.gsub(/"[^"]*"/) { |run| run.gsub(QUOTED_BREAK, "<br/>") }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# mermaid's lexer runs `/\n\s*/g` over the text between the quotes,
|
|
161
|
+
# and that `\s` is JavaScript's. Ruby's is the five ASCII ones, so
|
|
162
|
+
# a no-break space after the newline stayed in the label and mmdc
|
|
163
|
+
# dropped it. The set below is JavaScript's exactly: it takes the
|
|
164
|
+
# line and paragraph separators and the byte-order mark, and it
|
|
165
|
+
# leaves the next-line character and the zero-width space alone.
|
|
166
|
+
JS_SPACE = '[\t\n\v\f\r \u00a0\u1680\u2000-\u200a' \
|
|
167
|
+
'\u2028\u2029\u202f\u205f\u3000\ufeff]'
|
|
168
|
+
|
|
169
|
+
QUOTED_BREAK = /\n#{JS_SPACE}*/
|
|
170
|
+
|
|
171
|
+
private_constant :JS_SPACE, :QUOTED_BREAK
|
|
172
|
+
|
|
173
|
+
# A key etassia does not read is left alone rather than validated:
|
|
174
|
+
# `A@{ extra: true }` is a node mmdc draws, and a merge key is one
|
|
175
|
+
# of those. A root that is not a mapping has neither key, and mmdc
|
|
176
|
+
# draws the node untouched.
|
|
177
|
+
#
|
|
178
|
+
# Mermaid reads eight more keys off the document. Six of them
|
|
179
|
+
# (`labelType`, `form`, `pos`, `w`, `h`, `constraint`) leave the
|
|
180
|
+
# picture alone on their own, so ignoring them matches mmdc. Two do
|
|
181
|
+
# not: `A@{ icon: "fa:bell" }` and `A@{ img: "x.png" }` each draw a
|
|
182
|
+
# node with an empty label there, and a plain rectangle named `A`
|
|
183
|
+
# here. Both are node kinds etassia does not have yet.
|
|
184
|
+
def entries(document)
|
|
185
|
+
return {} unless document.is_a?(Hash)
|
|
186
|
+
|
|
187
|
+
READ_KEYS.filter_map do |key|
|
|
188
|
+
usable = usable_value(key, document[key])
|
|
189
|
+
[key, usable] if usable
|
|
190
|
+
end.to_h
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
READ_KEYS = %w[shape label].freeze
|
|
194
|
+
|
|
195
|
+
# An empty repeat captures as [], not as an empty slice.
|
|
196
|
+
def metadata_body(metadata)
|
|
197
|
+
return "" unless metadata.is_a?(Hash)
|
|
198
|
+
|
|
199
|
+
value = metadata[:body]
|
|
200
|
+
value.is_a?(Array) ? "" : value.to_s
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Mermaid tests the value for truth before using it, so `null`,
|
|
204
|
+
# `false`, `0` and `""` are all no-ops rather than errors — the
|
|
205
|
+
# node keeps what it had. Reading the raw AST made `shape: null`
|
|
206
|
+
# a lookup for a shape called "null".
|
|
207
|
+
#
|
|
208
|
+
# A sequence renders its first element and nothing else: mmdc draws
|
|
209
|
+
# `label: [one, two]` as `one`, and refuses a shape or a nested
|
|
210
|
+
# sequence, where the value it reaches for is not a scalar.
|
|
211
|
+
def usable_value(key, value)
|
|
212
|
+
return nil unless truthy?(value)
|
|
213
|
+
|
|
214
|
+
value = value.first if value.is_a?(Array) && key == "label"
|
|
215
|
+
return value if value.is_a?(String)
|
|
216
|
+
|
|
217
|
+
raise Parser::ParseError, "Unusable #{key} in metadata."
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# `null`, `false`, `0` and `""` are all falsy to mermaid, which
|
|
221
|
+
# skips the key rather than failing on it. An empty collection is
|
|
222
|
+
# NOT falsy in JavaScript, and mmdc does refuse `label: []`.
|
|
223
|
+
FALSY_VALUES = [nil, false, 0, ""].freeze
|
|
224
|
+
private_constant :FALSY_VALUES
|
|
225
|
+
|
|
226
|
+
def truthy?(value)
|
|
227
|
+
!FALSY_VALUES.include?(value) &&
|
|
228
|
+
!(value.is_a?(Float) && value.nan?)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Shape delimiter to type mapping
|
|
232
|
+
SHAPE_MAP = {
|
|
233
|
+
"[]" => "rect",
|
|
234
|
+
"()" => "rounded",
|
|
235
|
+
"([])" => "stadium",
|
|
236
|
+
"[[]]" => "subroutine",
|
|
237
|
+
"[()]" => "cylindrical",
|
|
238
|
+
"(())" => "circle",
|
|
239
|
+
"((()))" => "double_circle",
|
|
240
|
+
">]" => "asymmetric",
|
|
241
|
+
"{}" => "rhombus",
|
|
242
|
+
"{{}}" => "hexagon",
|
|
243
|
+
"[//]" => "parallelogram",
|
|
244
|
+
'[\\\\]' => "parallelogram_alt",
|
|
245
|
+
'[/\\]' => "trapezoid",
|
|
246
|
+
'[\\/]' => "trapezoid_alt",
|
|
247
|
+
}.freeze
|
|
248
|
+
|
|
249
|
+
# Which marker each character draws. One table serves both ends
|
|
250
|
+
# because the grammar only ever puts `<` at the start and `>` at
|
|
251
|
+
# the end — see the `link_start` and `link_end` rules in
|
|
252
|
+
# `Grammars::Flowchart`, which are the sole producers of this
|
|
253
|
+
# token.
|
|
254
|
+
LINK_MARKERS = {
|
|
255
|
+
">" => "arrow", "<" => "arrow",
|
|
256
|
+
"x" => "cross", "o" => "circle"
|
|
257
|
+
}.freeze
|
|
258
|
+
private_constant :LINK_MARKERS
|
|
259
|
+
|
|
260
|
+
# A comment line inside a label, whitespace as mermaid counts it.
|
|
261
|
+
COMMENT_LINE = Regexp.new(
|
|
262
|
+
"\n[\t\v\f \u00A0\u1680\u2000-\u200A\u2028\u2029\u202F" \
|
|
263
|
+
"\u205F\u3000\uFEFF]*%%[^\n]*",
|
|
264
|
+
)
|
|
265
|
+
private_constant :COMMENT_LINE
|
|
266
|
+
|
|
267
|
+
LABEL_EDGE_SPACE = /\A#{JS_SPACE}+|#{JS_SPACE}+\z/
|
|
268
|
+
private_constant :LABEL_EDGE_SPACE
|
|
269
|
+
|
|
270
|
+
# mermaid resolves the alias lexemes to a direction word before
|
|
271
|
+
# anything reads one, so `graph <` lays out exactly like `graph RL`.
|
|
272
|
+
# Measured against mmdc 11.12.0: `<` RL, `>` LR, `^` BT, `v` and
|
|
273
|
+
# `BR` TB. Keeping the raw lexeme sent the three glyphs down the
|
|
274
|
+
# default branch in the graph transform and drew them top-to-bottom.
|
|
275
|
+
DIRECTION_ALIASES = {
|
|
276
|
+
"<" => "RL",
|
|
277
|
+
">" => "LR",
|
|
278
|
+
"^" => "BT",
|
|
279
|
+
"v" => "TB",
|
|
280
|
+
"BR" => "TB",
|
|
281
|
+
}.freeze
|
|
282
|
+
|
|
283
|
+
# Mermaid honours a leading marker only when the trailing one
|
|
284
|
+
# matches it. `o--x` draws the cross and nothing at the source, and
|
|
285
|
+
# a mismatched pair drops back to normal thickness but keeps its
|
|
286
|
+
# dots. `o==x` is no thicker than `o--x`, while `<-.-x` stays
|
|
287
|
+
# dotted. Both were read as two-ended links here.
|
|
288
|
+
def link_type(token)
|
|
289
|
+
return "invisible" if token.start_with?("~")
|
|
290
|
+
|
|
291
|
+
head = LINK_MARKERS[token[-1]]
|
|
292
|
+
tail = LINK_MARKERS[token[0]]
|
|
293
|
+
matched = !head.nil? && head == tail
|
|
294
|
+
ends = matched ? "#{head}_both" : head || "line"
|
|
295
|
+
|
|
296
|
+
"#{link_weight(token, tail, matched)}#{ends}"
|
|
297
|
+
end
|
|
298
|
+
private :link_type
|
|
299
|
+
|
|
300
|
+
# A thick body keeps its weight only when it carries no leading
|
|
301
|
+
# marker, or a leading marker mermaid actually honours.
|
|
302
|
+
def link_weight(token, tail, matched)
|
|
303
|
+
return "dotted_" if token.include?(".")
|
|
304
|
+
return "thick_" if token.include?("=") && (matched || tail.nil?)
|
|
305
|
+
|
|
306
|
+
""
|
|
307
|
+
end
|
|
308
|
+
private :link_weight
|
|
309
|
+
|
|
310
|
+
# Direction value
|
|
311
|
+
rule(dir_value: simple(:v)) { v.to_s }
|
|
312
|
+
|
|
313
|
+
# Node ID
|
|
314
|
+
rule(node_id: simple(:id)) { id.to_s }
|
|
315
|
+
|
|
316
|
+
# Shape with label
|
|
317
|
+
rule(
|
|
318
|
+
shape: {
|
|
319
|
+
open: simple(:o),
|
|
320
|
+
label: simple(:l),
|
|
321
|
+
close: simple(:c),
|
|
322
|
+
},
|
|
323
|
+
) do
|
|
324
|
+
delims = "#{o}#{c}"
|
|
325
|
+
{
|
|
326
|
+
shape_type: SHAPE_MAP[delims] || "rect",
|
|
327
|
+
label: l.to_s.strip,
|
|
328
|
+
}
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Handle empty labels
|
|
332
|
+
rule(
|
|
333
|
+
shape: {
|
|
334
|
+
open: simple(:o),
|
|
335
|
+
label: sequence(:_),
|
|
336
|
+
close: simple(:c),
|
|
337
|
+
},
|
|
338
|
+
) do
|
|
339
|
+
delims = "#{o}#{c}"
|
|
340
|
+
{
|
|
341
|
+
shape_type: SHAPE_MAP[delims] || "rect",
|
|
342
|
+
label: "",
|
|
343
|
+
}
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Edge label
|
|
347
|
+
rule(label: simple(:l)) { l.to_s.strip }
|
|
348
|
+
rule(label: sequence(:l)) { l.join.strip }
|
|
349
|
+
|
|
350
|
+
# Helper method to create nodes
|
|
351
|
+
def create_node(node_data)
|
|
352
|
+
Diagram::FlowchartNode.new.tap do |n|
|
|
353
|
+
n.id = node_data[:node_id]
|
|
354
|
+
n.label = node_data[:label] || node_data[:node_id]
|
|
355
|
+
n.shape = node_data[:shape_type] || "rect"
|
|
356
|
+
n.classes = node_data[:classes] if node_data[:classes]
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# `link_token` is the raw lexeme the grammar matched — `-->`,
|
|
361
|
+
# `o--x`, `~~~`. `link_type` is what turns it into the edge's
|
|
362
|
+
# arrow type; the two are not the same string.
|
|
363
|
+
def create_edge(source_id, target_data, link_token, label = nil,
|
|
364
|
+
minimum_rank_span = nil)
|
|
365
|
+
Diagram::FlowchartEdge.new.tap do |e|
|
|
366
|
+
e.source_id = source_id
|
|
367
|
+
e.target_id = target_data[:node_id]
|
|
368
|
+
e.arrow_type = link_type(link_token)
|
|
369
|
+
e.minimum_rank_span = minimum_rank_span
|
|
370
|
+
e.label = label
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
private :create_edge
|
|
374
|
+
|
|
375
|
+
# Mermaid derives a labelled link's length from its closing half,
|
|
376
|
+
# not from the display token made by joining both halves. It removes
|
|
377
|
+
# the last character, removes a matching start marker on a double-ended
|
|
378
|
+
# link, then counts the remaining body beyond the ordinary first rank.
|
|
379
|
+
def minimum_rank_span(edge_data)
|
|
380
|
+
token = end_link_token(edge_data).strip
|
|
381
|
+
line = token[0...-1]
|
|
382
|
+
line = line[1..] if matching_end_markers?(token)
|
|
383
|
+
dots = line.count(".")
|
|
384
|
+
span = dots.positive? ? dots : line.length - 1
|
|
385
|
+
span if span > 1
|
|
386
|
+
end
|
|
387
|
+
private :minimum_rank_span
|
|
388
|
+
|
|
389
|
+
def end_link_token(edge_data)
|
|
390
|
+
return edge_data[:arrow][:token].to_s if edge_data[:arrow]
|
|
391
|
+
|
|
392
|
+
edge_data[:close].to_s
|
|
393
|
+
end
|
|
394
|
+
private :end_link_token
|
|
395
|
+
|
|
396
|
+
def matching_end_markers?(token)
|
|
397
|
+
LINK_MARKERS[token[0]] == LINK_MARKERS[token[-1]] &&
|
|
398
|
+
!LINK_MARKERS[token[0]].nil?
|
|
399
|
+
end
|
|
400
|
+
private :matching_end_markers?
|
|
401
|
+
|
|
402
|
+
# Process a parse tree of [header, *statements]. The Context is local
|
|
403
|
+
# to this call, so its ids and retained parse nodes cannot cross parses.
|
|
404
|
+
def apply(tree, diagram = nil)
|
|
405
|
+
diagram ||= Diagram::Flowchart.new
|
|
406
|
+
tree = [tree] unless tree.is_a?(Array)
|
|
407
|
+
apply_header(diagram, tree.first)
|
|
408
|
+
statements = tree.drop(1)
|
|
409
|
+
context = Context.new
|
|
410
|
+
assign_ids(statements, context)
|
|
411
|
+
process_statements(diagram, statements, context)
|
|
412
|
+
reject_ownership_cycles(context)
|
|
413
|
+
promote_referenced_subgraphs(diagram, context)
|
|
414
|
+
|
|
415
|
+
diagram
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def apply_header(diagram, header)
|
|
419
|
+
return unless header.is_a?(Hash) && header[:direction]
|
|
420
|
+
|
|
421
|
+
direction = header[:direction]
|
|
422
|
+
value = direction[:dir_value] || direction
|
|
423
|
+
diagram.direction = canonical_direction(value.to_s)
|
|
424
|
+
end
|
|
425
|
+
private :apply_header
|
|
426
|
+
|
|
427
|
+
def canonical_direction(value)
|
|
428
|
+
DIRECTION_ALIASES.fetch(value, value)
|
|
429
|
+
end
|
|
430
|
+
private :canonical_direction
|
|
431
|
+
|
|
432
|
+
def process_statements(diagram, statements, context, parents = [])
|
|
433
|
+
statements.each do |stmt|
|
|
434
|
+
next unless stmt.is_a?(Hash)
|
|
435
|
+
|
|
436
|
+
process_statement(diagram, stmt, context, parents)
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
STATEMENT_HANDLERS = {
|
|
441
|
+
node: :process_node_statement,
|
|
442
|
+
node_id: :process_standalone_statement,
|
|
443
|
+
subgraph_keyword: :process_subgraph_statement,
|
|
444
|
+
direction_keyword: :process_direction_statement,
|
|
445
|
+
style_keyword: :process_style_statement,
|
|
446
|
+
link_style_keyword: :process_link_style_statement,
|
|
447
|
+
}.freeze
|
|
448
|
+
private_constant :STATEMENT_HANDLERS
|
|
449
|
+
|
|
450
|
+
def process_statement(diagram, stmt, context, parents)
|
|
451
|
+
pair = STATEMENT_HANDLERS.find { |key, _handler| stmt[key] }
|
|
452
|
+
send(pair.last, diagram, stmt, context, parents) if pair
|
|
453
|
+
end
|
|
454
|
+
private :process_statement
|
|
455
|
+
|
|
456
|
+
def process_node_statement(diagram, stmt, context, parents)
|
|
457
|
+
process_node_edge_statement(diagram, stmt, parents.last, context)
|
|
458
|
+
end
|
|
459
|
+
private :process_node_statement
|
|
460
|
+
|
|
461
|
+
def process_standalone_statement(diagram, stmt, context, parents)
|
|
462
|
+
process_standalone_node(diagram, stmt, parents.last, context)
|
|
463
|
+
end
|
|
464
|
+
private :process_standalone_statement
|
|
465
|
+
|
|
466
|
+
def process_subgraph_statement(diagram, stmt, context, parents)
|
|
467
|
+
process_subgraph(diagram, stmt, context, parents)
|
|
468
|
+
end
|
|
469
|
+
private :process_subgraph_statement
|
|
470
|
+
|
|
471
|
+
def process_direction_statement(_diagram, stmt, context, parents)
|
|
472
|
+
set_direction(parents.last, stmt[:dir_value], context)
|
|
473
|
+
end
|
|
474
|
+
private :process_direction_statement
|
|
475
|
+
|
|
476
|
+
def process_style_statement(diagram, stmt, context, _parents)
|
|
477
|
+
declare_styled_node(diagram, stmt[:style_target], context)
|
|
478
|
+
node = diagram.find_node(stmt[:style_target].to_s)
|
|
479
|
+
apply_node_paint(node, stmt[:style_props]) if node
|
|
480
|
+
end
|
|
481
|
+
private :process_style_statement
|
|
482
|
+
|
|
483
|
+
# Commas within paint functions belong to the value. Only a comma
|
|
484
|
+
# introducing another property ends a declaration.
|
|
485
|
+
def apply_node_paint(node, properties)
|
|
486
|
+
declarations = properties.to_s.scan(
|
|
487
|
+
/(?:\A|,)\s*(fill|stroke)\s*:\s*(.*?)(?=,\s*[\w-]+\s*:|\z)/m,
|
|
488
|
+
)
|
|
489
|
+
declarations.each do |property, value|
|
|
490
|
+
paint = Diagram::FlowchartNode.paint_value(value)
|
|
491
|
+
node.public_send("#{property}=", paint) if paint
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
private :apply_node_paint
|
|
495
|
+
|
|
496
|
+
def process_link_style_statement(diagram, stmt, _context, _parents)
|
|
497
|
+
process_link_style(diagram, stmt)
|
|
498
|
+
end
|
|
499
|
+
private :process_link_style_statement
|
|
500
|
+
|
|
501
|
+
def process_standalone_node(diagram, stmt, parent, context)
|
|
502
|
+
node_data = {
|
|
503
|
+
node_id: stmt[:node_id], shape_type: "rect", label: stmt[:node_id]
|
|
504
|
+
}
|
|
505
|
+
add_or_update_node(diagram, node_data)
|
|
506
|
+
claim_member(parent, stmt[:node_id].to_s, context)
|
|
507
|
+
end
|
|
508
|
+
private :process_standalone_node
|
|
509
|
+
|
|
510
|
+
def process_link_style(diagram, stmt)
|
|
511
|
+
check_link_indices(diagram, stmt[:link_targets].to_s)
|
|
512
|
+
check_link_words(stmt)
|
|
513
|
+
end
|
|
514
|
+
private :process_link_style
|
|
515
|
+
|
|
516
|
+
# mmdc refuses `linkStyle 1` when only edge 0 has been drawn so
|
|
517
|
+
# far, so the count is the edges written above the statement.
|
|
518
|
+
def check_link_indices(diagram, targets)
|
|
519
|
+
return if targets == "default"
|
|
520
|
+
|
|
521
|
+
last = diagram.edges.size - 1
|
|
522
|
+
targets.split(",").each do |token|
|
|
523
|
+
index = token.to_i
|
|
524
|
+
# `00` and `01` name no edge: mermaid looks the text up as a key.
|
|
525
|
+
next if index.to_s == token && index <= last
|
|
526
|
+
|
|
527
|
+
raise Parser::ParseError,
|
|
528
|
+
"The index #{token} for linkStyle is out of bounds."
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
private :check_link_indices
|
|
532
|
+
|
|
533
|
+
# mermaid lexes these two as keywords even inside the style text,
|
|
534
|
+
# so `stroke:default`, `stroke:1default` and a bare `interpolate`
|
|
535
|
+
# are refused while `stroke:defaults` and `stroke:x-default` are
|
|
536
|
+
# plain text.
|
|
537
|
+
LINK_STYLE_KEYWORD = Regexp.new(
|
|
538
|
+
"(?:\\A|(?<=[,:;]|#{JS_SPACE}))" \
|
|
539
|
+
"(?:#|[0-9]+)?(interpolate|default)(?![A-Za-z0-9_])",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# mermaid rewrites `#name;` as an entity before it parses, except
|
|
543
|
+
# the last `;` of a lowercase `style ...:#...;` run, then the last
|
|
544
|
+
# `;` of a `classDef ...:#...;` run applied the same way right
|
|
545
|
+
# after. `linkStyle` is neither lowercase nor `classDef`, so
|
|
546
|
+
# `linkStyle 0 stroke:#f00;` holds an entity no style can take.
|
|
547
|
+
STYLE_RUN = /style.*:(?:(?!#{JS_SPACE}).)*#.*;/
|
|
548
|
+
CLASS_DEF_RUN = /classDef.*:(?:(?!#{JS_SPACE}).)*#.*;/
|
|
549
|
+
ENTITY = /#\w+;/
|
|
550
|
+
|
|
551
|
+
private_constant :LINK_STYLE_KEYWORD, :STYLE_RUN, :CLASS_DEF_RUN,
|
|
552
|
+
:ENTITY
|
|
553
|
+
|
|
554
|
+
# Mermaid runs entity stripping over the whole source line, so a curve
|
|
555
|
+
# name and its properties share one entity check even though the
|
|
556
|
+
# grammar splits them into fields. A terminating `;` is part of an
|
|
557
|
+
# entity too.
|
|
558
|
+
def check_link_words(stmt)
|
|
559
|
+
[stmt[:link_curve], stmt[:link_props]].each do |text|
|
|
560
|
+
word = text.to_s[LINK_STYLE_KEYWORD, 1] or next
|
|
561
|
+
raise Parser::ParseError,
|
|
562
|
+
"linkStyle cannot use `#{word}` here: " \
|
|
563
|
+
"write `interpolate <curve>` first, then the styles."
|
|
564
|
+
end
|
|
565
|
+
text = [stmt[:link_curve], stmt[:link_props]].compact.join(" ")
|
|
566
|
+
check_link_entities("#{text}#{stmt[:link_end]}")
|
|
567
|
+
end
|
|
568
|
+
private :check_link_words
|
|
569
|
+
|
|
570
|
+
def check_link_entities(text)
|
|
571
|
+
entity = text.sub(STYLE_RUN, &:chop)
|
|
572
|
+
.sub(CLASS_DEF_RUN, &:chop)[ENTITY] or return
|
|
573
|
+
|
|
574
|
+
raise Parser::ParseError,
|
|
575
|
+
"linkStyle reads `#{entity}` as an HTML entity; " \
|
|
576
|
+
"drop the `;` after the colour."
|
|
577
|
+
end
|
|
578
|
+
private :check_link_entities
|
|
579
|
+
|
|
580
|
+
# `style Q ...` names a vertex, and mermaid draws it even when no
|
|
581
|
+
# other statement mentions it, unless an earlier edge carries that
|
|
582
|
+
# id: then mermaid drops the line and draws no vertex.
|
|
583
|
+
def declare_styled_node(diagram, target, context)
|
|
584
|
+
return if diagram.find_node(target.to_s)
|
|
585
|
+
return if context.edge_ids.include?(target.to_s)
|
|
586
|
+
|
|
587
|
+
node_data = { node_id: target, shape_type: "rect", label: target }
|
|
588
|
+
add_or_update_node(diagram, node_data)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# A subgraph cannot be its own ancestor. mmdc refuses that with
|
|
592
|
+
# "Setting b as parent of a would create a cycle", and the
|
|
593
|
+
# transform was discarding the ids needed to see it.
|
|
594
|
+
#
|
|
595
|
+
# The id is not always the written one. A subgraph declared with a
|
|
596
|
+
# FREE title gets a generated `subGraph<n>` — so `subgraph s Some
|
|
597
|
+
# Title` containing `s` is fine, and containing `subGraph0` is the
|
|
598
|
+
# cycle. Only a bare or bracket-titled declaration keeps its word.
|
|
599
|
+
def process_subgraph(diagram, stmt, context, parents = [])
|
|
600
|
+
id = subgraph_id(stmt, context)
|
|
601
|
+
ancestry = parents + [id]
|
|
602
|
+
record_subgraph(diagram, stmt, id, parents.last, context)
|
|
603
|
+
return unless stmt[:subgraph_statements]
|
|
604
|
+
|
|
605
|
+
sub_stmts = stmt[:subgraph_statements]
|
|
606
|
+
sub_stmts = [sub_stmts] unless sub_stmts.is_a?(Array)
|
|
607
|
+
process_statements(diagram, sub_stmts, context, ancestry)
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# Ids are handed out in one pass before anything reads them.
|
|
611
|
+
# `subgraph_id` is asked for the same statement from several
|
|
612
|
+
# places, and a counter that ticked on every call skipped numbers:
|
|
613
|
+
# a subgraph nested in another came out `subGraph2`.
|
|
614
|
+
#
|
|
615
|
+
# mermaid numbers the innermost box first, so the walk goes down
|
|
616
|
+
# before it hands anything out. Measured on mmdc 11.12.0:
|
|
617
|
+
# `subgraph a One` holding `subgraph b Two` makes b subGraph0 and
|
|
618
|
+
# a subGraph1.
|
|
619
|
+
def assign_ids(statements, context)
|
|
620
|
+
statements = [statements] unless statements.is_a?(Array)
|
|
621
|
+
|
|
622
|
+
statements.each do |stmt|
|
|
623
|
+
next unless stmt.is_a?(Hash) && stmt[:subgraph_keyword]
|
|
624
|
+
|
|
625
|
+
if stmt[:subgraph_statements]
|
|
626
|
+
assign_ids(stmt[:subgraph_statements], context)
|
|
627
|
+
end
|
|
628
|
+
context.ids[stmt] = allocate_id(stmt, context)
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# A quoted id arrives as a Hash, and `to_s` on that gave
|
|
633
|
+
# `{string: "s"}`, so a quoted self-parent slipped through.
|
|
634
|
+
def subgraph_id(stmt, context)
|
|
635
|
+
context.ids[stmt] ||= allocate_id(stmt, context)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def allocate_id(stmt, context)
|
|
639
|
+
generated_id?(stmt) ? context.next_generated_id : written_id(stmt)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# A free title always generates one. So does a bare id carrying
|
|
643
|
+
# whitespace — `subgraph "a b"` is subGraph0 titled "a b". A
|
|
644
|
+
# bracket title keeps the written id even then, so
|
|
645
|
+
# `subgraph "a b" [T]` stays `a b`. All measured on mmdc 11.12.0.
|
|
646
|
+
def generated_id?(stmt)
|
|
647
|
+
return true if free_titled?(stmt)
|
|
648
|
+
|
|
649
|
+
stmt[:subgraph_title].nil? && written_id(stmt).match?(/\s/)
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def written_id(stmt)
|
|
653
|
+
plain_id(stmt[:subgraph_id])
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# A quoted id arrives as `{string: <slice>}`, so a bare `to_s`
|
|
657
|
+
# gives `{string: "a"@21}` and never matches the id it names.
|
|
658
|
+
# Every place that compares an id has to unwrap it, not just the
|
|
659
|
+
# subgraph's own — the cycle walk reads node ids too.
|
|
660
|
+
def plain_id(raw)
|
|
661
|
+
return raw.to_s unless raw.is_a?(Hash)
|
|
662
|
+
|
|
663
|
+
value = raw.values.first
|
|
664
|
+
value.is_a?(Array) ? value.join : value.to_s
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# The grammar captures a bracket title WITHOUT its brackets, so
|
|
668
|
+
# the text cannot tell the two forms apart. They carry different
|
|
669
|
+
# keys for that reason — testing for a leading `[` read every
|
|
670
|
+
# bracketed title as free and generated an id for it.
|
|
671
|
+
def free_titled?(stmt)
|
|
672
|
+
!stmt[:subgraph_free_title].nil?
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# A box cannot end up inside itself. An ancestor chain only sees
|
|
676
|
+
# loops that nest, and two declarations can close one between
|
|
677
|
+
# them — `a` holding `b` while `b` holds `a` — so each edge is
|
|
678
|
+
# recorded as its claim is won, and the whole set is checked
|
|
679
|
+
# once the diagram has been read.
|
|
680
|
+
#
|
|
681
|
+
# Read from the ARBITRATED graph, not from what the source
|
|
682
|
+
# writes. A claim that lost makes no edge, because the box it
|
|
683
|
+
# named already belongs to somebody else and mermaid never hangs
|
|
684
|
+
# it there. Measured on mmdc 11.12.0: `subgraph c` holding `b`,
|
|
685
|
+
# then `b` holding `a`, then `a` holding `b` draws clusters `c`
|
|
686
|
+
# and `b` — the last claim loses, so nothing closes. Walking the
|
|
687
|
+
# written source instead refused a diagram mmdc draws.
|
|
688
|
+
def reject_ownership_cycles(context)
|
|
689
|
+
loop_found = Diagram::Containment.looping_pair(context.ownership)
|
|
690
|
+
return if loop_found.nil?
|
|
691
|
+
|
|
692
|
+
parent, child = loop_found
|
|
693
|
+
raise Parser::ParseError,
|
|
694
|
+
"Setting #{parent} as parent of #{child} would create " \
|
|
695
|
+
"a cycle."
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# Declaration order is kept in the model. Ownership is independent:
|
|
699
|
+
# a parent declared later can adopt an earlier top-level subgraph.
|
|
700
|
+
#
|
|
701
|
+
# Being written inside a box is a claim like any other, so a
|
|
702
|
+
# declaration takes the id only if nothing named it earlier. When
|
|
703
|
+
# it loses, the box is still built and still holds what it
|
|
704
|
+
# contains — it just does not become anybody's child, and the
|
|
705
|
+
# earlier claimant adopts it once the walk is over.
|
|
706
|
+
#
|
|
707
|
+
# The first box wins a duplicated name. When a later declaration is
|
|
708
|
+
# nested, the winning box—not the just-built empty duplicate—is
|
|
709
|
+
# parented, matching mermaid's visible containment.
|
|
710
|
+
def record_subgraph(diagram, stmt, id, parent, context)
|
|
711
|
+
box = Diagram::FlowchartSubgraph.new
|
|
712
|
+
box.id = id
|
|
713
|
+
box.declared_title = subgraph_label(stmt)
|
|
714
|
+
diagram.subgraphs << box
|
|
715
|
+
holding = (context.boxes[id] ||= box)
|
|
716
|
+
holder = claim(parent, id, context)
|
|
717
|
+
return if holder.nil?
|
|
718
|
+
|
|
719
|
+
holding.parent_id = parent
|
|
720
|
+
holder.child_ids << id
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
# A bracketed title stands alone. A free one keeps the word that
|
|
724
|
+
# looks like an id, and the gap after it, because mermaid labels
|
|
725
|
+
# `subgraph s Some Title` with the whole run and generates the id
|
|
726
|
+
# separately. Measured against mmdc 11.12.0.
|
|
727
|
+
def subgraph_label(stmt)
|
|
728
|
+
return stmt[:subgraph_title].to_s if stmt[:subgraph_title]
|
|
729
|
+
|
|
730
|
+
if free_titled?(stmt)
|
|
731
|
+
return [written_id(stmt), stmt[:subgraph_free_gap],
|
|
732
|
+
stmt[:subgraph_free_title]].join
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
# The text that was too spaced to serve as an id becomes the
|
|
736
|
+
# title instead, trimmed: `subgraph " ab"` is titled "ab".
|
|
737
|
+
generated_id?(stmt) ? written_id(stmt).strip : nil
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# The one ownership rule: the first box to name an id keeps it,
|
|
741
|
+
# whether it named a node or another box. Source order decides
|
|
742
|
+
# and nothing else. Measured against mmdc 11.12.0; the shapes
|
|
743
|
+
# are one apiece in flowchart_subgraph_ownership_spec.rb.
|
|
744
|
+
#
|
|
745
|
+
# A declaration at the TOP level claims nothing, which is why
|
|
746
|
+
# `subgraph b / X / end` followed by `subgraph c / b / end` still
|
|
747
|
+
# leaves b inside c.
|
|
748
|
+
#
|
|
749
|
+
# This used to be written twice, once here and once as `adopt`'s
|
|
750
|
+
# "unless the parent is already set". The two disagreed, and a
|
|
751
|
+
# later nested declaration took an id an earlier reference had
|
|
752
|
+
# already claimed.
|
|
753
|
+
#
|
|
754
|
+
# @return [Diagram::FlowchartSubgraph, nil] the box that took it
|
|
755
|
+
def claim(parent, id, context)
|
|
756
|
+
box = enclosing(parent, context)
|
|
757
|
+
return nil if box.nil? || id.empty? || context.claimed[id]
|
|
758
|
+
|
|
759
|
+
context.claimed[id] = true
|
|
760
|
+
# The edge the cycle check reads. Only a WINNING claim makes
|
|
761
|
+
# one, which is what stops a losing claim from closing a loop
|
|
762
|
+
# that mermaid never sees. No `include?` guard: an id is
|
|
763
|
+
# claimed once, so it can be listed only once.
|
|
764
|
+
(context.ownership[box.id] ||= []) << id
|
|
765
|
+
box
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
# `<<`, not `+=`. Each instance gets its own array from the
|
|
769
|
+
# attribute default, so appending touches nobody else, and `+=`
|
|
770
|
+
# went through the model's collection setter once per member —
|
|
771
|
+
# 1,600 nodes in one box took 2.3 seconds instead of half a
|
|
772
|
+
# millisecond.
|
|
773
|
+
def claim_member(parent, node_id, context)
|
|
774
|
+
box = claim(parent, node_id, context)
|
|
775
|
+
box.node_ids << node_id if box
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
# Indexed rather than searched. Scanning every box for every node
|
|
779
|
+
# made a 240-subgraph diagram take four times as long to parse as
|
|
780
|
+
# it did before subgraphs were modelled.
|
|
781
|
+
def enclosing(parent, context)
|
|
782
|
+
parent.nil? ? nil : context.boxes[parent]
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# `direction` inside a box turns that box's contents, and mmdc
|
|
786
|
+
# accepts a top-level one without honouring it — measured on
|
|
787
|
+
# 11.12.0, where `flowchart TD` followed by `direction LR` still
|
|
788
|
+
# stacks its nodes vertically. So there is nothing to do when no
|
|
789
|
+
# box encloses it.
|
|
790
|
+
def set_direction(parent, value, context)
|
|
791
|
+
box = enclosing(parent, context)
|
|
792
|
+
box.direction = value.to_s if box
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# An id that names a subgraph names the BOX, not a node beside it.
|
|
796
|
+
# `one --> two` joins two clusters, and a bare `b` inside
|
|
797
|
+
# `subgraph a` nests box b in a. Measured on mmdc 11.12.0: every
|
|
798
|
+
# form of the reference — bare, shaped, an edge endpoint, written
|
|
799
|
+
# before or after the declaration — draws no node.
|
|
800
|
+
#
|
|
801
|
+
# Left until the walk is over because an EMPTY box is the
|
|
802
|
+
# exception: mmdc draws `subgraph one` / `end` joined by an edge
|
|
803
|
+
# as a plain node, and nothing knows what a box holds until every
|
|
804
|
+
# statement has been read.
|
|
805
|
+
#
|
|
806
|
+
# The ids are collected first and the nodes swept once. Asking
|
|
807
|
+
# `find_node` per box rescans the whole collection each time, and
|
|
808
|
+
# a chain of 2,000 boxes spent 17 seconds in the transform where
|
|
809
|
+
# this takes under one.
|
|
810
|
+
def promote_referenced_subgraphs(diagram, context)
|
|
811
|
+
holders = holders_by_member(diagram)
|
|
812
|
+
promoted = surviving_boxes(context)
|
|
813
|
+
return if promoted.empty?
|
|
814
|
+
|
|
815
|
+
promoted.each_value { |box| adopt(holders[box.id], box) }
|
|
816
|
+
diagram.nodes.reject! { |node| promoted.key?(node.id) }
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
# A box is worth drawing when it holds something, and holding
|
|
820
|
+
# something is exactly what the model already calls `drawable?`.
|
|
821
|
+
#
|
|
822
|
+
# This used to settle by fixed point, re-checking whether taking
|
|
823
|
+
# a reference out of a box had left that box empty. Once `claim`
|
|
824
|
+
# became the single ownership rule that stopped being possible:
|
|
825
|
+
# an id sitting in a box's `node_ids` is an id that box WON, so
|
|
826
|
+
# no declaration parented it elsewhere and the box keeps it.
|
|
827
|
+
# `adopt` is the only later writer and it MOVES an id from
|
|
828
|
+
# `node_ids` to `child_ids` on the same holder, so no pass can
|
|
829
|
+
# leave a box holding less than it holds now.
|
|
830
|
+
#
|
|
831
|
+
# Measured before deleting the loop — 2,312 sources, 301 calls to
|
|
832
|
+
# the old predicate, zero that disagreed with `drawable?`.
|
|
833
|
+
def surviving_boxes(context)
|
|
834
|
+
context.boxes.select { |_id, box| box.drawable? }
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
def holders_by_member(diagram)
|
|
838
|
+
diagram.subgraphs.each_with_object({}) do |box, acc|
|
|
839
|
+
box.node_ids.each { |id| acc[id] = box }
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
# The box that claimed the reference becomes the parent, with
|
|
844
|
+
# nobody to argue with. Two guards used to say so defensively;
|
|
845
|
+
# both were unreachable, because `parent_id` has exactly two
|
|
846
|
+
# writers — `record_subgraph`, which only runs on a WINNING
|
|
847
|
+
# claim, and this method, which runs after the walk. Holding
|
|
848
|
+
# `box.id` in `node_ids` means this holder won that claim, so
|
|
849
|
+
# no declaration can have set a parent or listed the child.
|
|
850
|
+
# `promoted` is keyed by id, so no box is adopted twice either.
|
|
851
|
+
#
|
|
852
|
+
# Checked as well as argued, before the guards came out: 2,312
|
|
853
|
+
# sources, 50 adoptions, none with a parent already set and none
|
|
854
|
+
# where the holder already listed the child.
|
|
855
|
+
def adopt(holder, box)
|
|
856
|
+
return if holder.nil?
|
|
857
|
+
|
|
858
|
+
holder.node_ids.delete(box.id)
|
|
859
|
+
box.parent_id = holder.id
|
|
860
|
+
holder.child_ids << box.id
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
# The last two arguments are new, and both default, so a caller
|
|
864
|
+
# written against the old two-argument form still works: with no
|
|
865
|
+
# enclosing box there is nothing to claim, and the throwaway
|
|
866
|
+
# context is only there to keep the claim path from special-casing
|
|
867
|
+
# its absence.
|
|
868
|
+
def process_node_edge_statement(diagram, stmt, parent = nil,
|
|
869
|
+
context = Context.new)
|
|
870
|
+
edges = stmt[:edges]
|
|
871
|
+
nodes = [stmt[:node], *stmt[:group]]
|
|
872
|
+
sources = declare_group(diagram, nodes, parent, context,
|
|
873
|
+
linkless: edges.nil?)
|
|
874
|
+
return unless edges
|
|
875
|
+
|
|
876
|
+
edge_list(edges).each do |edge_data|
|
|
877
|
+
next unless edge_data.is_a?(Hash)
|
|
878
|
+
|
|
879
|
+
sources = process_edge(diagram, edge_data, sources, parent, context)
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def edge_list(edges)
|
|
884
|
+
edges.is_a?(Array) ? edges : [edges]
|
|
885
|
+
end
|
|
886
|
+
private :edge_list
|
|
887
|
+
|
|
888
|
+
# `arrow` arrives wrapped with the other edge fields, so it cannot be
|
|
889
|
+
# unwrapped by a Parslet rule that matches only that key. Validate its
|
|
890
|
+
# token before the defensive target guard; the grammar currently makes
|
|
891
|
+
# targets mandatory, but the guard protects a future relaxed rule.
|
|
892
|
+
def process_edge(diagram, edge_data, sources, parent, context)
|
|
893
|
+
token = link_token(edge_data)
|
|
894
|
+
label = edge_label(edge_data)
|
|
895
|
+
rank_span = minimum_rank_span(edge_data)
|
|
896
|
+
return sources unless edge_data[:target]
|
|
897
|
+
|
|
898
|
+
nodes = [edge_data[:target], *edge_data[:group]]
|
|
899
|
+
targets = declare_group(diagram, nodes, parent, context)
|
|
900
|
+
record_edge_id(edge_data, context)
|
|
901
|
+
connect_groups(diagram, sources, targets, [token, label, rank_span])
|
|
902
|
+
targets
|
|
903
|
+
end
|
|
904
|
+
private :process_edge
|
|
905
|
+
|
|
906
|
+
def record_edge_id(edge_data, context)
|
|
907
|
+
context.edge_ids << edge_data[:edge_id].to_s if edge_data[:edge_id]
|
|
908
|
+
end
|
|
909
|
+
private :record_edge_id
|
|
910
|
+
|
|
911
|
+
# `A & B --> C & D` links every source to every target.
|
|
912
|
+
def connect_groups(diagram, sources, targets, link)
|
|
913
|
+
sources.product(targets).each do |source, target|
|
|
914
|
+
diagram.edges << create_edge(source[:node_id], target, *link)
|
|
915
|
+
end
|
|
916
|
+
end
|
|
917
|
+
private :connect_groups
|
|
918
|
+
|
|
919
|
+
# The text a link carries: nil when it has no label, or when the
|
|
920
|
+
# pipes hold only space. Written around the link,
|
|
921
|
+
# `A -- text --> B`, it arrives as the run itself; written after it,
|
|
922
|
+
# `A -->|text| B`, as the run between the pipes.
|
|
923
|
+
def edge_label(edge_data)
|
|
924
|
+
label = edge_data[:label]
|
|
925
|
+
return inline_label(label) if edge_data[:open]
|
|
926
|
+
|
|
927
|
+
piped_label(label[:pipe_text]) if label.is_a?(Hash)
|
|
928
|
+
end
|
|
929
|
+
private :edge_label
|
|
930
|
+
|
|
931
|
+
# @raise [Parser::ParseError] on a label that nothing is left of
|
|
932
|
+
def inline_label(label)
|
|
933
|
+
text = unquoted_label(label)
|
|
934
|
+
return text unless text.empty?
|
|
935
|
+
|
|
936
|
+
raise Parser::ParseError, "An inline link label cannot be empty."
|
|
937
|
+
end
|
|
938
|
+
private :inline_label
|
|
939
|
+
|
|
940
|
+
# Mermaid trims the text between pipes, and `| |` is a label of
|
|
941
|
+
# nothing rather than an error. Nothing left is nil, so the link
|
|
942
|
+
# has no label.
|
|
943
|
+
def piped_label(text)
|
|
944
|
+
trimmed = unquoted_label(text).gsub(LABEL_EDGE_SPACE, "")
|
|
945
|
+
trimmed unless trimmed.empty?
|
|
946
|
+
end
|
|
947
|
+
private :piped_label
|
|
948
|
+
|
|
949
|
+
# Mermaid deletes a whole comment line before it lexes, and the
|
|
950
|
+
# quotes of a quoted run are delimiters, not text.
|
|
951
|
+
def unquoted_label(label)
|
|
952
|
+
label.to_s.gsub(COMMENT_LINE, "").delete('"')
|
|
953
|
+
end
|
|
954
|
+
private :unquoted_label
|
|
955
|
+
|
|
956
|
+
# The nodes one side of a link names: the first, and any that
|
|
957
|
+
# `&` joined to it. Returns their node data, in source order.
|
|
958
|
+
#
|
|
959
|
+
# A member that addresses an edge id (`e1@{ ... }`, `e1:::foo`) names
|
|
960
|
+
# no node and changes no node that shares the id, except for its
|
|
961
|
+
# class. Such a shared node still belongs to an enclosing subgraph
|
|
962
|
+
# that names it. In a link, the id is still returned as its endpoint.
|
|
963
|
+
def declare_group(diagram, nodes, parent, context, linkless: false)
|
|
964
|
+
nodes.filter_map do |node_hash|
|
|
965
|
+
declare_node(diagram, node_hash, parent, context, linkless)
|
|
966
|
+
end
|
|
967
|
+
end
|
|
968
|
+
private :declare_group
|
|
969
|
+
|
|
970
|
+
def declare_node(diagram, node_hash, parent, context, linkless)
|
|
971
|
+
if edge_reference?(node_hash, context)
|
|
972
|
+
return declare_edge_reference(
|
|
973
|
+
diagram, node_hash, parent, context, linkless
|
|
974
|
+
)
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
node_data = extract_node_data(node_hash)
|
|
978
|
+
add_or_update_node(diagram, node_data)
|
|
979
|
+
claim_member(parent, node_data[:node_id].to_s, context)
|
|
980
|
+
node_data
|
|
981
|
+
end
|
|
982
|
+
private :declare_node
|
|
983
|
+
|
|
984
|
+
def declare_edge_reference(
|
|
985
|
+
diagram, node_hash, parent, context, linkless
|
|
986
|
+
)
|
|
987
|
+
metadata_document(node_hash[:metadata])
|
|
988
|
+
shared = class_shared_node(diagram, node_hash)
|
|
989
|
+
claim_member(parent, shared.id, context) if shared
|
|
990
|
+
{ node_id: node_hash[:node_id] } unless linkless
|
|
991
|
+
end
|
|
992
|
+
private :declare_edge_reference
|
|
993
|
+
|
|
994
|
+
# Any mention of a known edge id — bare (`e1`), shaped (`e1[x]`),
|
|
995
|
+
# carrying `@{...}`/`:::foo`, or used as a link endpoint — addresses
|
|
996
|
+
# the edge named `e1`, even when a node is also called `e1`: mermaid
|
|
997
|
+
# checks the edges first and stops there, so its shape, label and
|
|
998
|
+
# metadata reach no node.
|
|
999
|
+
def edge_reference?(node_hash, context)
|
|
1000
|
+
context.edge_ids.include?(node_hash[:node_id].to_s)
|
|
1001
|
+
end
|
|
1002
|
+
private :edge_reference?
|
|
1003
|
+
|
|
1004
|
+
# Mermaid gives a `:::foo` class to every node, edge and subgraph
|
|
1005
|
+
# with that id, so a node sharing an edge's id still takes it.
|
|
1006
|
+
def class_shared_node(diagram, node_hash)
|
|
1007
|
+
node = diagram.find_node(node_hash[:node_id].to_s)
|
|
1008
|
+
node.classes = node_hash[:inline_class].to_s if
|
|
1009
|
+
node && node_hash[:inline_class]
|
|
1010
|
+
node
|
|
1011
|
+
end
|
|
1012
|
+
private :class_shared_node
|
|
1013
|
+
# A link written around its label, `A -- text --> B`, arrives in
|
|
1014
|
+
# two halves, and their concatenation reads like the one-piece
|
|
1015
|
+
# link of the same kind: `-- -->` is `---->`.
|
|
1016
|
+
#
|
|
1017
|
+
# Mermaid reads a `<` opening the closing half as the start head
|
|
1018
|
+
# of the whole link, so it moves to the front, and it refuses an
|
|
1019
|
+
# `x` or `o` opening half whose closing half does not end in the
|
|
1020
|
+
# same marker; a `<` needs a `>` to close it. An `x` or `o` at the
|
|
1021
|
+
# very start of the closing half of a link that already opens with
|
|
1022
|
+
# `<` draws nothing of its own: `A <-- t x--> B` and
|
|
1023
|
+
# `A <-- t o--> B` render as a plain double arrow, measured against
|
|
1024
|
+
# mermaid 11.12.0's own parser.
|
|
1025
|
+
# @raise [Parser::ParseError] on an unmatched opening marker
|
|
1026
|
+
def link_token(edge_data)
|
|
1027
|
+
return edge_data[:arrow][:token].to_s if edge_data[:arrow]
|
|
1028
|
+
|
|
1029
|
+
open = edge_data[:open].to_s
|
|
1030
|
+
raw_close = edge_data[:close].to_s
|
|
1031
|
+
reject_unmatched_marker(open, raw_close)
|
|
1032
|
+
close = closing_half(open, raw_close)
|
|
1033
|
+
return "#{open}#{close}" unless lifts_start_head?(open, close)
|
|
1034
|
+
|
|
1035
|
+
"#{close[0]}#{open}#{close[1..]}"
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
def closing_half(open, close)
|
|
1039
|
+
return close unless open.start_with?("<") && close.match?(/\A[ox]/)
|
|
1040
|
+
|
|
1041
|
+
close[1..]
|
|
1042
|
+
end
|
|
1043
|
+
private :closing_half
|
|
1044
|
+
|
|
1045
|
+
# The start head of the closing half, when the opening half has
|
|
1046
|
+
# none, belongs to the whole link: `<` always, `x` or `o` when the
|
|
1047
|
+
# other end carries the same one.
|
|
1048
|
+
def lifts_start_head?(open, close)
|
|
1049
|
+
return false if open.match?(/\A[ox<]/)
|
|
1050
|
+
|
|
1051
|
+
close.start_with?("<") ||
|
|
1052
|
+
(close.match?(/\A[ox]/) && close[-1] == close[0])
|
|
1053
|
+
end
|
|
1054
|
+
private :lifts_start_head?
|
|
1055
|
+
|
|
1056
|
+
def reject_unmatched_marker(open, close)
|
|
1057
|
+
return if marker_closed?(open[0], close)
|
|
1058
|
+
|
|
1059
|
+
raise Parser::ParseError,
|
|
1060
|
+
"The link #{open}#{close} opens with #{open[0]} " \
|
|
1061
|
+
"and does not close with it."
|
|
1062
|
+
end
|
|
1063
|
+
|
|
1064
|
+
# An opening `<` closes on a head and takes no second `<` start
|
|
1065
|
+
# marker on the closing half, but an `x` or `o` there draws
|
|
1066
|
+
# nothing and is dropped by `link_token` above; an opening `x` or
|
|
1067
|
+
# `o` takes none of its own kind again (`x-- t o--x` draws,
|
|
1068
|
+
# `x-- t x--x` does not). A thick closing half that opens with its
|
|
1069
|
+
# own `<` also needs a head: mmdc refuses `A == t <==x B`,
|
|
1070
|
+
# `A == t <=== B` and `A == t x==> B`, and draws the solid and
|
|
1071
|
+
# dotted forms.
|
|
1072
|
+
def marker_closed?(marker, close)
|
|
1073
|
+
return false if invalid_thick_closure?(close)
|
|
1074
|
+
|
|
1075
|
+
case marker
|
|
1076
|
+
when "x", "o" then close[-1] == marker && close[0] != marker
|
|
1077
|
+
when "<" then close[-1] == ">" && !close.start_with?("<")
|
|
1078
|
+
else true
|
|
1079
|
+
end
|
|
1080
|
+
end
|
|
1081
|
+
private :marker_closed?
|
|
1082
|
+
|
|
1083
|
+
def invalid_thick_closure?(close)
|
|
1084
|
+
close.match?(/\A[ox<]=/) && close[-1] != head_for(close[0])
|
|
1085
|
+
end
|
|
1086
|
+
private :invalid_thick_closure?
|
|
1087
|
+
|
|
1088
|
+
def head_for(start)
|
|
1089
|
+
start == "<" ? ">" : start
|
|
1090
|
+
end
|
|
1091
|
+
private :head_for
|
|
1092
|
+
private :reject_unmatched_marker
|
|
1093
|
+
private :link_token
|
|
1094
|
+
|
|
1095
|
+
# A second mention of a node changes only what it actually says.
|
|
1096
|
+
# Treating an absent shape as `rect` and an absent label as the id
|
|
1097
|
+
# meant `A(keep)` followed by `A@{ shape: rect }` kept the round
|
|
1098
|
+
# shape and lost the label — mermaid does the opposite of both.
|
|
1099
|
+
def add_or_update_node(diagram, node_data)
|
|
1100
|
+
return unless node_data
|
|
1101
|
+
|
|
1102
|
+
existing = diagram.find_node(node_data[:node_id])
|
|
1103
|
+
return diagram.nodes << create_node(node_data) unless existing
|
|
1104
|
+
|
|
1105
|
+
update_node(existing, node_data)
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
def update_node(node, data)
|
|
1109
|
+
node.label = data[:label] if data[:label]
|
|
1110
|
+
node.shape = data[:shape_type] if data[:shape_type]
|
|
1111
|
+
node.classes = data[:classes] if data[:classes]
|
|
1112
|
+
end
|
|
1113
|
+
private :update_node
|
|
1114
|
+
|
|
1115
|
+
# Extract node data from a parse tree. Metadata wins over the bracket
|
|
1116
|
+
# form when both provide a shape or label, matching mermaid.
|
|
1117
|
+
def extract_node_data(node_hash)
|
|
1118
|
+
return nil unless node_hash
|
|
1119
|
+
|
|
1120
|
+
node_id = node_hash[:node_id].to_s
|
|
1121
|
+
shape_type, label = bracket_shape(node_hash[:shape])
|
|
1122
|
+
|
|
1123
|
+
entries = metadata_entries(node_hash[:metadata])
|
|
1124
|
+
|
|
1125
|
+
{
|
|
1126
|
+
node_id: node_id,
|
|
1127
|
+
shape_type: metadata_shape(entries["shape"]) || shape_type,
|
|
1128
|
+
label: entries["label"] || label,
|
|
1129
|
+
classes: node_hash[:inline_class]&.to_s,
|
|
1130
|
+
}
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
# nil means the source said nothing, which is not the same as
|
|
1134
|
+
# saying `rect` or naming the node after itself. `create_node`
|
|
1135
|
+
# supplies the defaults, so only a real mention overwrites.
|
|
1136
|
+
def bracket_shape(shape_data)
|
|
1137
|
+
return [nil, nil] unless shape_data
|
|
1138
|
+
|
|
1139
|
+
delims = "#{shape_data[:open]}#{shape_data[:close]}"
|
|
1140
|
+
label = shape_data[:label]
|
|
1141
|
+
# `A[ ]` is an EMPTY label, not an absent one: mmdc draws a node
|
|
1142
|
+
# with nothing in it, and treating it as absent named the node
|
|
1143
|
+
# after itself and kept an older label on a re-mention.
|
|
1144
|
+
label = label.to_s.gsub(COMMENT_LINE, "") if delims == "()"
|
|
1145
|
+
[SHAPE_MAP[delims] || "rect", label&.to_s&.strip]
|
|
1146
|
+
end
|
|
1147
|
+
end
|
|
1148
|
+
end
|
|
1149
|
+
end
|
|
1150
|
+
end
|