sirena 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/.github/workflows/build_deploy.yml +59 -0
- data/.github/workflows/links.yml +85 -0
- data/.github/workflows/rake.yml +15 -0
- data/.github/workflows/release.yml +27 -0
- data/.gitignore +68 -0
- data/.rspec +3 -0
- data/.rubocop.yml +14 -0
- data/.rubocop_todo.yml +70 -0
- data/ARCHITECTURE.md +744 -0
- data/Gemfile +12 -0
- data/LICENSE +25 -0
- data/README.adoc +357 -0
- data/Rakefile +11 -0
- data/docs/.gitignore +1 -0
- data/docs/Gemfile +13 -0
- data/docs/_config.yml +182 -0
- data/docs/_diagram_types/architecture-diagram.adoc +314 -0
- data/docs/_diagram_types/block-diagram.adoc +345 -0
- data/docs/_diagram_types/c4-diagram.adoc +559 -0
- data/docs/_diagram_types/class-diagram.adoc +816 -0
- data/docs/_diagram_types/er-diagram.adoc +719 -0
- data/docs/_diagram_types/error-diagram.adoc +114 -0
- data/docs/_diagram_types/examples/flowchart-examples.adoc +29 -0
- data/docs/_diagram_types/flowchart.adoc +488 -0
- data/docs/_diagram_types/gantt-chart.adoc +502 -0
- data/docs/_diagram_types/git-graph.adoc +600 -0
- data/docs/_diagram_types/index.adoc +192 -0
- data/docs/_diagram_types/info-diagram.adoc +103 -0
- data/docs/_diagram_types/kanban-diagram.adoc +262 -0
- data/docs/_diagram_types/mindmap.adoc +603 -0
- data/docs/_diagram_types/packet-diagram.adoc +378 -0
- data/docs/_diagram_types/pie-chart.adoc +335 -0
- data/docs/_diagram_types/quadrant-chart.adoc +406 -0
- data/docs/_diagram_types/radar-chart.adoc +528 -0
- data/docs/_diagram_types/requirement-diagram.adoc +416 -0
- data/docs/_diagram_types/sankey-diagram.adoc +357 -0
- data/docs/_diagram_types/sequence-diagram.adoc +664 -0
- data/docs/_diagram_types/state-diagram.adoc +658 -0
- data/docs/_diagram_types/timeline.adoc +352 -0
- data/docs/_diagram_types/treemap-diagram.adoc +462 -0
- data/docs/_diagram_types/user-journey.adoc +602 -0
- data/docs/_features/index.adoc +129 -0
- data/docs/_guides/cli-reference.adoc +203 -0
- data/docs/_guides/index.adoc +56 -0
- data/docs/_guides/installation.adoc +100 -0
- data/docs/_guides/quick-start.adoc +132 -0
- data/docs/_pages/comparison.adoc +441 -0
- data/docs/_pages/compatibility.adoc +300 -0
- data/docs/_pages/index.adoc +39 -0
- data/docs/_references/index.adoc +103 -0
- data/docs/_tutorials/index.adoc +57 -0
- data/docs/index.adoc +166 -0
- data/docs/lychee.toml +54 -0
- data/examples/.gitignore +10 -0
- data/examples/README.adoc +196 -0
- data/examples/README.md +64 -0
- data/examples/architecture/01-basic-services.mmd +9 -0
- data/examples/architecture/01-basic-services.svg +37 -0
- data/examples/architecture/02-service-groups.mmd +16 -0
- data/examples/architecture/02-service-groups.svg +55 -0
- data/examples/architecture/README.adoc +79 -0
- data/examples/block/01-basic-blocks.mmd +13 -0
- data/examples/block/01-basic-blocks.svg +44 -0
- data/examples/block/02-block-shapes.mmd +13 -0
- data/examples/block/02-block-shapes.svg +47 -0
- data/examples/block/README.adoc +85 -0
- data/examples/c4/01-context-diagram.mmd +10 -0
- data/examples/c4/01-context-diagram.svg +45 -0
- data/examples/c4/02-container-diagram.mmd +24 -0
- data/examples/c4/02-container-diagram.svg +105 -0
- data/examples/c4/README.adoc +92 -0
- data/examples/class_diagram/01-basic-classes.mmd +61 -0
- data/examples/class_diagram/01-basic-classes.svg +117 -0
- data/examples/class_diagram/02-relationships.mmd +61 -0
- data/examples/class_diagram/02-relationships.svg +129 -0
- data/examples/class_diagram/README.adoc +93 -0
- data/examples/er_diagram/01-basic-entities.mmd +64 -0
- data/examples/er_diagram/01-basic-entities.svg +5 -0
- data/examples/er_diagram/02-cardinality.mmd +57 -0
- data/examples/er_diagram/02-cardinality.svg +125 -0
- data/examples/er_diagram/README.adoc +88 -0
- data/examples/error/01-basic-error.mmd +1 -0
- data/examples/error/01-basic-error.svg +13 -0
- data/examples/error/02-error-display.mmd +1 -0
- data/examples/error/02-error-display.svg +13 -0
- data/examples/error/README.adoc +71 -0
- data/examples/error_message_example.svg +13 -0
- data/examples/flowchart/00-original.mmd +13 -0
- data/examples/flowchart/00-original.svg +5 -0
- data/examples/flowchart/01-basic-flow.mmd +7 -0
- data/examples/flowchart/01-basic-flow.svg +52 -0
- data/examples/flowchart/01-basic-flow.yml +13 -0
- data/examples/flowchart/02*.svg +87 -0
- data/examples/flowchart/02-node-shapes.mmd +9 -0
- data/examples/flowchart/02-node-shapes.svg +33 -0
- data/examples/flowchart/03-edge-types.mmd +7 -0
- data/examples/flowchart/03-edge-types.svg +53 -0
- data/examples/flowchart/04-subgraphs.mmd +9 -0
- data/examples/flowchart/04-subgraphs.svg +33 -0
- data/examples/flowchart/05-styling.mmd +9 -0
- data/examples/flowchart/05-styling.svg +33 -0
- data/examples/flowchart/06-complex-flow.mmd +8 -0
- data/examples/flowchart/06-complex-flow.svg +59 -0
- data/examples/flowchart/README.adoc +167 -0
- data/examples/gantt/01-simple-timeline.* +14 -0
- data/examples/gantt/01-simple-timeline.mmd +6 -0
- data/examples/gantt/01-simple-timeline.svg +26 -0
- data/examples/gantt/02-task-dependencies.mmd +6 -0
- data/examples/gantt/02-task-dependencies.svg +26 -0
- data/examples/gantt/README.adoc +86 -0
- data/examples/git_graph/01-linear-history.mmd +12 -0
- data/examples/git_graph/01-linear-history.svg +26 -0
- data/examples/git_graph/02-branching.mmd +12 -0
- data/examples/git_graph/02-branching.svg +26 -0
- data/examples/git_graph/README.adoc +73 -0
- data/examples/info/02-showinfo.mmd +1 -0
- data/examples/info/02-showinfo.svg +10 -0
- data/examples/info/README.adoc +58 -0
- data/examples/info_showinfo_example.svg +10 -0
- data/examples/kanban/01-simple-board.mmd +8 -0
- data/examples/kanban/01-simple-board.svg +43 -0
- data/examples/kanban/02-workflow.mmd +8 -0
- data/examples/kanban/02-workflow.svg +43 -0
- data/examples/kanban/README.adoc +79 -0
- data/examples/mindmap/01-simple-tree.mmd +19 -0
- data/examples/mindmap/01-simple-tree.svg +61 -0
- data/examples/mindmap/02-knowledge-map.mmd +19 -0
- data/examples/mindmap/02-knowledge-map.svg +61 -0
- data/examples/mindmap/README.adoc +77 -0
- data/examples/packet/01-basic-packet.* +17 -0
- data/examples/packet/01-basic-packet.mmd +4 -0
- data/examples/packet/01-basic-packet.svg +82 -0
- data/examples/packet/README.adoc +58 -0
- data/examples/pie/01-simple-chart.mmd +5 -0
- data/examples/pie/01-simple-chart.svg +17 -0
- data/examples/pie/02-labeled-slices.mmd +6 -0
- data/examples/pie/02-labeled-slices.svg +19 -0
- data/examples/pie/README.adoc +75 -0
- data/examples/quadrant/01-basic-quadrant.mmd +13 -0
- data/examples/quadrant/01-basic-quadrant.svg +33 -0
- data/examples/quadrant/02-positioned-items.mmd +14 -0
- data/examples/quadrant/02-positioned-items.svg +35 -0
- data/examples/quadrant/README.adoc +84 -0
- data/examples/radar/01-simple-radar.* +5 -0
- data/examples/radar/01-simple-radar.mmd +3 -0
- data/examples/radar/01-simple-radar.svg +25 -0
- data/examples/radar/02-multiple-curves.mmd +4 -0
- data/examples/radar/02-multiple-curves.svg +43 -0
- data/examples/radar/README.adoc +75 -0
- data/examples/requirement/01-basic-requirements.mmd +23 -0
- data/examples/requirement/01-basic-requirements.svg +49 -0
- data/examples/requirement/02-risk-levels.mmd +23 -0
- data/examples/requirement/02-risk-levels.svg +49 -0
- data/examples/requirement/README.adoc +85 -0
- data/examples/sankey/01-simple-flow.mmd +7 -0
- data/examples/sankey/01-simple-flow.svg +34 -0
- data/examples/sankey/02-multi-stage.mmd +11 -0
- data/examples/sankey/02-multi-stage.svg +44 -0
- data/examples/sankey/README.adoc +74 -0
- data/examples/sequence/01-basic-sequence.mmd +27 -0
- data/examples/sequence/01-basic-sequence.svg +5 -0
- data/examples/sequence/02-activations.mmd +17 -0
- data/examples/sequence/02-activations.svg +78 -0
- data/examples/sequence/README.adoc +86 -0
- data/examples/state_diagram/01-simple-states.mmd +29 -0
- data/examples/state_diagram/01-simple-states.svg +5 -0
- data/examples/state_diagram/02-composite.mmd +19 -0
- data/examples/state_diagram/02-composite.svg +81 -0
- data/examples/state_diagram/README.adoc +90 -0
- data/examples/timeline/01-simple-timeline.mmd +11 -0
- data/examples/timeline/01-simple-timeline.svg +36 -0
- data/examples/timeline/02-periods.mmd +15 -0
- data/examples/timeline/02-periods.svg +47 -0
- data/examples/timeline/README.adoc +78 -0
- data/examples/treemap/01-basic-treemap.mmd +12 -0
- data/examples/treemap/01-basic-treemap.svg +59 -0
- data/examples/treemap/README.adoc +59 -0
- data/examples/user_journey/01-simple-journey.mmd +23 -0
- data/examples/user_journey/01-simple-journey.svg +5 -0
- data/examples/user_journey/02-multi-actor.mmd +18 -0
- data/examples/user_journey/02-multi-actor.svg +129 -0
- data/examples/user_journey/README.adoc +81 -0
- data/examples/xychart/01-line-chart.mmd +5 -0
- data/examples/xychart/01-line-chart.svg +43 -0
- data/examples/xychart/02-bar-chart.mmd +7 -0
- data/examples/xychart/02-bar-chart.svg +48 -0
- data/examples/xychart/README.adoc +80 -0
- data/exe/sirena +7 -0
- data/lib/sirena/cli.rb +138 -0
- data/lib/sirena/commands/batch.rb +117 -0
- data/lib/sirena/commands/render.rb +80 -0
- data/lib/sirena/commands/types.rb +29 -0
- data/lib/sirena/commands/version.rb +24 -0
- data/lib/sirena/diagram/architecture.rb +46 -0
- data/lib/sirena/diagram/base.rb +61 -0
- data/lib/sirena/diagram/block.rb +81 -0
- data/lib/sirena/diagram/c4.rb +328 -0
- data/lib/sirena/diagram/class_diagram.rb +385 -0
- data/lib/sirena/diagram/er_diagram.rb +238 -0
- data/lib/sirena/diagram/error.rb +38 -0
- data/lib/sirena/diagram/flowchart.rb +160 -0
- data/lib/sirena/diagram/gantt.rb +71 -0
- data/lib/sirena/diagram/git_graph.rb +36 -0
- data/lib/sirena/diagram/info.rb +38 -0
- data/lib/sirena/diagram/kanban.rb +178 -0
- data/lib/sirena/diagram/mindmap.rb +54 -0
- data/lib/sirena/diagram/packet.rb +79 -0
- data/lib/sirena/diagram/pie.rb +115 -0
- data/lib/sirena/diagram/quadrant.rb +138 -0
- data/lib/sirena/diagram/radar.rb +52 -0
- data/lib/sirena/diagram/requirement.rb +133 -0
- data/lib/sirena/diagram/sankey.rb +217 -0
- data/lib/sirena/diagram/sequence.rb +242 -0
- data/lib/sirena/diagram/state_diagram.rb +237 -0
- data/lib/sirena/diagram/timeline.rb +171 -0
- data/lib/sirena/diagram/treemap.rb +84 -0
- data/lib/sirena/diagram/user_journey.rb +149 -0
- data/lib/sirena/diagram/xy_chart.rb +76 -0
- data/lib/sirena/diagram.rb +8 -0
- data/lib/sirena/diagram_registry.rb +101 -0
- data/lib/sirena/engine.rb +292 -0
- data/lib/sirena/parser/architecture.rb +41 -0
- data/lib/sirena/parser/base.rb +41 -0
- data/lib/sirena/parser/block.rb +72 -0
- data/lib/sirena/parser/c4.rb +53 -0
- data/lib/sirena/parser/class_diagram.rb +63 -0
- data/lib/sirena/parser/er_diagram.rb +40 -0
- data/lib/sirena/parser/error.rb +49 -0
- data/lib/sirena/parser/flowchart.rb +71 -0
- data/lib/sirena/parser/gantt.rb +60 -0
- data/lib/sirena/parser/git_graph.rb +95 -0
- data/lib/sirena/parser/grammars/architecture.rb +145 -0
- data/lib/sirena/parser/grammars/block.rb +190 -0
- data/lib/sirena/parser/grammars/c4.rb +226 -0
- data/lib/sirena/parser/grammars/class_diagram.rb +284 -0
- data/lib/sirena/parser/grammars/common.rb +84 -0
- data/lib/sirena/parser/grammars/er_diagram.rb +114 -0
- data/lib/sirena/parser/grammars/error.rb +40 -0
- data/lib/sirena/parser/grammars/flowchart.rb +298 -0
- data/lib/sirena/parser/grammars/gantt.rb +252 -0
- data/lib/sirena/parser/grammars/git_graph.rb +167 -0
- data/lib/sirena/parser/grammars/info.rb +58 -0
- data/lib/sirena/parser/grammars/kanban.rb +83 -0
- data/lib/sirena/parser/grammars/mindmap.rb +115 -0
- data/lib/sirena/parser/grammars/packet.rb +73 -0
- data/lib/sirena/parser/grammars/pie.rb +128 -0
- data/lib/sirena/parser/grammars/quadrant.rb +199 -0
- data/lib/sirena/parser/grammars/radar.rb +150 -0
- data/lib/sirena/parser/grammars/requirement.rb +188 -0
- data/lib/sirena/parser/grammars/sankey.rb +104 -0
- data/lib/sirena/parser/grammars/sequence.rb +247 -0
- data/lib/sirena/parser/grammars/state_diagram.rb +172 -0
- data/lib/sirena/parser/grammars/timeline.rb +142 -0
- data/lib/sirena/parser/grammars/treemap.rb +120 -0
- data/lib/sirena/parser/grammars/xy_chart.rb +120 -0
- data/lib/sirena/parser/info.rb +49 -0
- data/lib/sirena/parser/kanban.rb +97 -0
- data/lib/sirena/parser/mindmap.rb +106 -0
- data/lib/sirena/parser/packet.rb +76 -0
- data/lib/sirena/parser/pie.rb +49 -0
- data/lib/sirena/parser/quadrant.rb +57 -0
- data/lib/sirena/parser/radar.rb +104 -0
- data/lib/sirena/parser/requirement.rb +70 -0
- data/lib/sirena/parser/sankey.rb +64 -0
- data/lib/sirena/parser/sequence.rb +51 -0
- data/lib/sirena/parser/state_diagram.rb +69 -0
- data/lib/sirena/parser/timeline.rb +57 -0
- data/lib/sirena/parser/transforms/architecture.rb +97 -0
- data/lib/sirena/parser/transforms/block.rb +254 -0
- data/lib/sirena/parser/transforms/c4.rb +347 -0
- data/lib/sirena/parser/transforms/class_diagram.rb +352 -0
- data/lib/sirena/parser/transforms/er_diagram.rb +169 -0
- data/lib/sirena/parser/transforms/error.rb +58 -0
- data/lib/sirena/parser/transforms/flowchart.rb +293 -0
- data/lib/sirena/parser/transforms/gantt.rb +215 -0
- data/lib/sirena/parser/transforms/git_graph.rb +160 -0
- data/lib/sirena/parser/transforms/info.rb +58 -0
- data/lib/sirena/parser/transforms/kanban.rb +176 -0
- data/lib/sirena/parser/transforms/mindmap.rb +227 -0
- data/lib/sirena/parser/transforms/packet.rb +63 -0
- data/lib/sirena/parser/transforms/pie.rb +143 -0
- data/lib/sirena/parser/transforms/quadrant.rb +177 -0
- data/lib/sirena/parser/transforms/radar.rb +126 -0
- data/lib/sirena/parser/transforms/requirement.rb +272 -0
- data/lib/sirena/parser/transforms/sankey.rb +122 -0
- data/lib/sirena/parser/transforms/sequence.rb +342 -0
- data/lib/sirena/parser/transforms/state_diagram.rb +292 -0
- data/lib/sirena/parser/transforms/timeline.rb +177 -0
- data/lib/sirena/parser/transforms/treemap.rb +81 -0
- data/lib/sirena/parser/transforms/xy_chart.rb +132 -0
- data/lib/sirena/parser/treemap.rb +98 -0
- data/lib/sirena/parser/user_journey.rb +120 -0
- data/lib/sirena/parser/xy_chart.rb +114 -0
- data/lib/sirena/parser.rb +8 -0
- data/lib/sirena/renderer/architecture.rb +251 -0
- data/lib/sirena/renderer/base.rb +251 -0
- data/lib/sirena/renderer/block.rb +286 -0
- data/lib/sirena/renderer/c4.rb +490 -0
- data/lib/sirena/renderer/class_diagram.rb +499 -0
- data/lib/sirena/renderer/er_diagram.rb +417 -0
- data/lib/sirena/renderer/error.rb +131 -0
- data/lib/sirena/renderer/flowchart.rb +301 -0
- data/lib/sirena/renderer/gantt.rb +331 -0
- data/lib/sirena/renderer/git_graph.rb +368 -0
- data/lib/sirena/renderer/info.rb +93 -0
- data/lib/sirena/renderer/kanban.rb +295 -0
- data/lib/sirena/renderer/mindmap.rb +396 -0
- data/lib/sirena/renderer/packet.rb +239 -0
- data/lib/sirena/renderer/pie.rb +235 -0
- data/lib/sirena/renderer/quadrant.rb +292 -0
- data/lib/sirena/renderer/radar.rb +323 -0
- data/lib/sirena/renderer/requirement.rb +371 -0
- data/lib/sirena/renderer/sankey.rb +255 -0
- data/lib/sirena/renderer/sequence.rb +424 -0
- data/lib/sirena/renderer/state_diagram.rb +328 -0
- data/lib/sirena/renderer/timeline.rb +304 -0
- data/lib/sirena/renderer/treemap.rb +152 -0
- data/lib/sirena/renderer/user_journey.rb +331 -0
- data/lib/sirena/renderer/xy_chart.rb +452 -0
- data/lib/sirena/renderer.rb +8 -0
- data/lib/sirena/svg/circle.rb +41 -0
- data/lib/sirena/svg/document.rb +103 -0
- data/lib/sirena/svg/element.rb +65 -0
- data/lib/sirena/svg/ellipse.rb +33 -0
- data/lib/sirena/svg/group.rb +71 -0
- data/lib/sirena/svg/line.rb +49 -0
- data/lib/sirena/svg/path.rb +76 -0
- data/lib/sirena/svg/polygon.rb +43 -0
- data/lib/sirena/svg/polyline.rb +35 -0
- data/lib/sirena/svg/rect.rb +57 -0
- data/lib/sirena/svg/style.rb +44 -0
- data/lib/sirena/svg/text.rb +72 -0
- data/lib/sirena/svg.rb +19 -0
- data/lib/sirena/text_measurement.rb +71 -0
- data/lib/sirena/theme/builtin/dark.yml +70 -0
- data/lib/sirena/theme/builtin/default.yml +80 -0
- data/lib/sirena/theme/builtin/high_contrast.yml +70 -0
- data/lib/sirena/theme/builtin/light.yml +70 -0
- data/lib/sirena/theme/color_palette.rb +48 -0
- data/lib/sirena/theme/effect_styles.rb +28 -0
- data/lib/sirena/theme/registry.rb +41 -0
- data/lib/sirena/theme/shape_styles.rb +28 -0
- data/lib/sirena/theme/spacing_config.rb +24 -0
- data/lib/sirena/theme/typography.rb +30 -0
- data/lib/sirena/theme.rb +69 -0
- data/lib/sirena/transform/architecture.rb +273 -0
- data/lib/sirena/transform/base.rb +199 -0
- data/lib/sirena/transform/block.rb +215 -0
- data/lib/sirena/transform/c4.rb +288 -0
- data/lib/sirena/transform/class_diagram.rb +296 -0
- data/lib/sirena/transform/er_diagram.rb +204 -0
- data/lib/sirena/transform/error.rb +39 -0
- data/lib/sirena/transform/flowchart.rb +161 -0
- data/lib/sirena/transform/gantt.rb +253 -0
- data/lib/sirena/transform/git_graph.rb +283 -0
- data/lib/sirena/transform/info.rb +39 -0
- data/lib/sirena/transform/kanban.rb +180 -0
- data/lib/sirena/transform/mindmap.rb +251 -0
- data/lib/sirena/transform/packet.rb +185 -0
- data/lib/sirena/transform/pie.rb +62 -0
- data/lib/sirena/transform/quadrant.rb +167 -0
- data/lib/sirena/transform/radar.rb +227 -0
- data/lib/sirena/transform/requirement.rb +233 -0
- data/lib/sirena/transform/sankey.rb +212 -0
- data/lib/sirena/transform/sequence.rb +143 -0
- data/lib/sirena/transform/state_diagram.rb +228 -0
- data/lib/sirena/transform/timeline.rb +139 -0
- data/lib/sirena/transform/treemap.rb +120 -0
- data/lib/sirena/transform/user_journey.rb +207 -0
- data/lib/sirena/transform/xy_chart.rb +273 -0
- data/lib/sirena/transform.rb +8 -0
- data/lib/sirena/version.rb +5 -0
- data/lib/sirena.rb +328 -0
- data/lib/tasks/benchmark.rake +532 -0
- data/lib/tasks/examples.rake +468 -0
- data/lib/tasks/generate_mermaid_fixtures.rake +363 -0
- data/lib/tasks/mermaid_fixtures.rake +46 -0
- data/scripts/extract_mermaid_tests.rb +493 -0
- data/scripts/rename_to_sirena.rb +73 -0
- data/sirena.gemspec +47 -0
- metadata +529 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../diagram/c4'
|
|
4
|
+
|
|
5
|
+
module Sirena
|
|
6
|
+
module Parser
|
|
7
|
+
module Transforms
|
|
8
|
+
# Transform for converting Parslet parse tree to C4 diagram model.
|
|
9
|
+
#
|
|
10
|
+
# Converts the parse tree output from Grammars::C4 into a
|
|
11
|
+
# fully-formed Diagram::C4 object with elements, relationships, and
|
|
12
|
+
# boundaries.
|
|
13
|
+
class C4
|
|
14
|
+
def initialize
|
|
15
|
+
@boundary_stack = []
|
|
16
|
+
@current_boundary = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Transform parse tree into C4 diagram.
|
|
20
|
+
#
|
|
21
|
+
# @param tree [Array, Hash] Parslet parse tree
|
|
22
|
+
# @return [Diagram::C4] the C4 diagram model
|
|
23
|
+
def apply(tree)
|
|
24
|
+
diagram = Diagram::C4.new
|
|
25
|
+
@boundary_stack = []
|
|
26
|
+
@current_boundary = nil
|
|
27
|
+
|
|
28
|
+
# Extract level from header
|
|
29
|
+
extract_level(diagram, tree)
|
|
30
|
+
|
|
31
|
+
# Process statements - collect scattered attributes
|
|
32
|
+
if tree.is_a?(Array)
|
|
33
|
+
# Merge scattered attribute hashes with their parent element/boundary
|
|
34
|
+
merged_tree = merge_scattered_attributes(tree)
|
|
35
|
+
merged_tree.each do |item|
|
|
36
|
+
process_statement(diagram, item) if item.is_a?(Hash)
|
|
37
|
+
end
|
|
38
|
+
elsif tree.is_a?(Hash)
|
|
39
|
+
process_statement(diagram, tree)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
diagram
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Merge scattered attribute hashes into their parent statements
|
|
48
|
+
def merge_scattered_attributes(tree)
|
|
49
|
+
result = []
|
|
50
|
+
i = 0
|
|
51
|
+
while i < tree.length
|
|
52
|
+
item = tree[i]
|
|
53
|
+
|
|
54
|
+
# Check if this is an element/boundary statement starter
|
|
55
|
+
if item.is_a?(Hash) && (item[:element_type] || item[:boundary_type])
|
|
56
|
+
# Don't merge if this has a body (it's a complete boundary statement)
|
|
57
|
+
if item[:body]
|
|
58
|
+
result << item
|
|
59
|
+
i += 1
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Look ahead and merge all related hashes
|
|
64
|
+
j = i + 1
|
|
65
|
+
while j < tree.length && tree[j].is_a?(Hash)
|
|
66
|
+
next_item = tree[j]
|
|
67
|
+
# Stop if we hit another element/boundary/relationship/title/config
|
|
68
|
+
break if next_item[:element_type] || next_item[:boundary_type] ||
|
|
69
|
+
next_item[:rel_type] || next_item[:title] ||
|
|
70
|
+
next_item[:config_params] || next_item[:header]
|
|
71
|
+
# Merge this hash into the current statement
|
|
72
|
+
item = item.merge(next_item)
|
|
73
|
+
j += 1
|
|
74
|
+
end
|
|
75
|
+
result << item
|
|
76
|
+
i = j
|
|
77
|
+
else
|
|
78
|
+
result << item
|
|
79
|
+
i += 1
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
result
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def extract_level(diagram, tree)
|
|
86
|
+
header = if tree.is_a?(Array)
|
|
87
|
+
tree.find { |item| item.is_a?(Hash) && item[:header] }
|
|
88
|
+
elsif tree.is_a?(Hash) && tree[:header]
|
|
89
|
+
tree
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if header && header[:header]
|
|
93
|
+
header_str = header[:header].to_s
|
|
94
|
+
diagram.level = case header_str
|
|
95
|
+
when 'C4Context'
|
|
96
|
+
'Context'
|
|
97
|
+
when 'C4Container'
|
|
98
|
+
'Container'
|
|
99
|
+
when 'C4Component'
|
|
100
|
+
'Component'
|
|
101
|
+
when 'C4Dynamic'
|
|
102
|
+
'Dynamic'
|
|
103
|
+
when 'C4Deployment'
|
|
104
|
+
'Deployment'
|
|
105
|
+
when 'C4 diagram'
|
|
106
|
+
'Context' # Default
|
|
107
|
+
else
|
|
108
|
+
'Context'
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def process_statement(diagram, stmt)
|
|
114
|
+
return unless stmt.is_a?(Hash)
|
|
115
|
+
|
|
116
|
+
if stmt[:header]
|
|
117
|
+
# Already processed
|
|
118
|
+
nil
|
|
119
|
+
elsif stmt[:title]
|
|
120
|
+
diagram.title = extract_text(stmt[:title])
|
|
121
|
+
elsif stmt[:config_params]
|
|
122
|
+
process_layout_config(diagram, stmt)
|
|
123
|
+
elsif stmt[:boundary_type]
|
|
124
|
+
process_boundary(diagram, stmt)
|
|
125
|
+
elsif stmt[:rel_type]
|
|
126
|
+
process_relationship(diagram, stmt)
|
|
127
|
+
elsif stmt[:element_type]
|
|
128
|
+
process_element(diagram, stmt)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def process_layout_config(diagram, stmt)
|
|
133
|
+
# Extract layout config as a simple string
|
|
134
|
+
config_parts = []
|
|
135
|
+
if stmt[:config_params].is_a?(Array)
|
|
136
|
+
stmt[:config_params].each do |param|
|
|
137
|
+
key = extract_text(param[:key]) if param[:key]
|
|
138
|
+
value = extract_text(param[:value]) if param[:value]
|
|
139
|
+
config_parts << "#{key}=#{value}" if key && value
|
|
140
|
+
end
|
|
141
|
+
elsif stmt[:config_params].is_a?(Hash)
|
|
142
|
+
key = extract_text(stmt[:config_params][:key])
|
|
143
|
+
value = extract_text(stmt[:config_params][:value])
|
|
144
|
+
config_parts << "#{key}=#{value}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
diagram.layout_config = config_parts.join(', ')
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def process_boundary(diagram, stmt)
|
|
151
|
+
boundary = Diagram::C4Boundary.new
|
|
152
|
+
|
|
153
|
+
# Handle boundary type (can be a variable reference)
|
|
154
|
+
boundary_type = stmt[:boundary_type]
|
|
155
|
+
if boundary_type.is_a?(Hash) && boundary_type[:variable]
|
|
156
|
+
# Variable reference like ${macroName}
|
|
157
|
+
boundary.boundary_type = extract_text(boundary_type[:variable][:var])
|
|
158
|
+
else
|
|
159
|
+
boundary.boundary_type = boundary_type.to_s
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
boundary.id = extract_text(stmt[:id]) if stmt[:id]
|
|
163
|
+
boundary.label = extract_text(stmt[:label]) if stmt[:label]
|
|
164
|
+
boundary.type_param = extract_text(stmt[:type]) if stmt[:type]
|
|
165
|
+
boundary.link = extract_text(stmt[:link]) if stmt[:link]
|
|
166
|
+
boundary.tags = extract_text(stmt[:tags]) if stmt[:tags]
|
|
167
|
+
|
|
168
|
+
# Set parent if we're inside another boundary
|
|
169
|
+
boundary.parent_id = @current_boundary if @current_boundary
|
|
170
|
+
|
|
171
|
+
# Add boundary to diagram
|
|
172
|
+
diagram.boundaries << boundary
|
|
173
|
+
|
|
174
|
+
# Process nested content
|
|
175
|
+
if stmt[:body]
|
|
176
|
+
old_boundary = @current_boundary
|
|
177
|
+
@current_boundary = boundary.id
|
|
178
|
+
|
|
179
|
+
# Normalize body to array of items
|
|
180
|
+
body_array = stmt[:body].is_a?(Array) ? stmt[:body] : [stmt[:body]]
|
|
181
|
+
|
|
182
|
+
# Extract items from the body structure
|
|
183
|
+
body_items = body_array.flat_map do |item|
|
|
184
|
+
if item.is_a?(Hash) && item[:item]
|
|
185
|
+
[item[:item]]
|
|
186
|
+
else
|
|
187
|
+
[]
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Merge scattered attributes in the body
|
|
192
|
+
merged_body = merge_scattered_attributes(body_items)
|
|
193
|
+
|
|
194
|
+
# Process each merged item
|
|
195
|
+
merged_body.each do |nested_item|
|
|
196
|
+
if nested_item[:boundary_type]
|
|
197
|
+
# Nested boundary
|
|
198
|
+
nested_boundary_id = process_nested_boundary(diagram,
|
|
199
|
+
nested_item)
|
|
200
|
+
boundary.boundary_ids << nested_boundary_id if
|
|
201
|
+
nested_boundary_id
|
|
202
|
+
elsif nested_item[:element_type]
|
|
203
|
+
# Element inside boundary
|
|
204
|
+
element_id = process_nested_element(diagram, nested_item)
|
|
205
|
+
boundary.element_ids << element_id if element_id
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
@current_boundary = old_boundary
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def process_nested_boundary(diagram, stmt)
|
|
214
|
+
boundary = Diagram::C4Boundary.new
|
|
215
|
+
|
|
216
|
+
# Handle boundary type (can be a variable reference)
|
|
217
|
+
boundary_type = stmt[:boundary_type]
|
|
218
|
+
if boundary_type.is_a?(Hash) && boundary_type[:variable]
|
|
219
|
+
# Variable reference like ${macroName}
|
|
220
|
+
boundary.boundary_type = extract_text(boundary_type[:variable][:var])
|
|
221
|
+
else
|
|
222
|
+
boundary.boundary_type = boundary_type.to_s
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
boundary.id = extract_text(stmt[:id]) if stmt[:id]
|
|
226
|
+
boundary.label = extract_text(stmt[:label]) if stmt[:label]
|
|
227
|
+
boundary.type_param = extract_text(stmt[:type]) if stmt[:type]
|
|
228
|
+
boundary.link = extract_text(stmt[:link]) if stmt[:link]
|
|
229
|
+
boundary.tags = extract_text(stmt[:tags]) if stmt[:tags]
|
|
230
|
+
boundary.parent_id = @current_boundary
|
|
231
|
+
|
|
232
|
+
diagram.boundaries << boundary
|
|
233
|
+
|
|
234
|
+
# Process nested content recursively
|
|
235
|
+
if stmt[:body]
|
|
236
|
+
old_boundary = @current_boundary
|
|
237
|
+
@current_boundary = boundary.id
|
|
238
|
+
|
|
239
|
+
# Convert body to array and extract items
|
|
240
|
+
body_items = Array(stmt[:body]).flat_map do |item|
|
|
241
|
+
if item.is_a?(Hash) && item[:item]
|
|
242
|
+
[item[:item]]
|
|
243
|
+
else
|
|
244
|
+
[]
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Merge scattered attributes in the body
|
|
249
|
+
merged_body = merge_scattered_attributes(body_items)
|
|
250
|
+
|
|
251
|
+
# Process each merged item
|
|
252
|
+
merged_body.each do |nested_item|
|
|
253
|
+
if nested_item[:boundary_type]
|
|
254
|
+
nested_boundary_id = process_nested_boundary(diagram,
|
|
255
|
+
nested_item)
|
|
256
|
+
boundary.boundary_ids << nested_boundary_id if
|
|
257
|
+
nested_boundary_id
|
|
258
|
+
elsif nested_item[:element_type]
|
|
259
|
+
element_id = process_nested_element(diagram, nested_item)
|
|
260
|
+
boundary.element_ids << element_id if element_id
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
@current_boundary = old_boundary
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
boundary.id
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def process_element(diagram, stmt)
|
|
271
|
+
element = create_element(stmt)
|
|
272
|
+
element.boundary_id = @current_boundary if @current_boundary
|
|
273
|
+
diagram.elements << element
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def process_nested_element(diagram, stmt)
|
|
277
|
+
element = create_element(stmt)
|
|
278
|
+
element.boundary_id = @current_boundary if @current_boundary
|
|
279
|
+
diagram.elements << element
|
|
280
|
+
element.id
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def create_element(stmt)
|
|
284
|
+
element = Diagram::C4Element.new
|
|
285
|
+
|
|
286
|
+
# Extract element type
|
|
287
|
+
element_type = stmt[:element_type]
|
|
288
|
+
if element_type.is_a?(Hash) && element_type[:variable]
|
|
289
|
+
# Handle ${macroName} variable references (used in tests)
|
|
290
|
+
element.element_type = extract_text(element_type[:variable][:var])
|
|
291
|
+
else
|
|
292
|
+
element.element_type = element_type.to_s
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Extract parameters
|
|
296
|
+
element.id = extract_text(stmt[:id]) if stmt[:id]
|
|
297
|
+
element.label = extract_text(stmt[:label]) if stmt[:label]
|
|
298
|
+
element.description = extract_text(stmt[:description]) if
|
|
299
|
+
stmt[:description]
|
|
300
|
+
element.technology = extract_text(stmt[:technology]) if
|
|
301
|
+
stmt[:technology]
|
|
302
|
+
|
|
303
|
+
# Extract attributes
|
|
304
|
+
element.sprite = extract_text(stmt[:sprite]) if stmt[:sprite]
|
|
305
|
+
element.link = extract_text(stmt[:link]) if stmt[:link]
|
|
306
|
+
element.tags = extract_text(stmt[:tags]) if stmt[:tags]
|
|
307
|
+
|
|
308
|
+
# Set external flag based on element type
|
|
309
|
+
element.external = element.element_type&.end_with?('_Ext') || false
|
|
310
|
+
|
|
311
|
+
element
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def process_relationship(diagram, stmt)
|
|
315
|
+
relationship = Diagram::C4Relationship.new
|
|
316
|
+
|
|
317
|
+
relationship.rel_type = stmt[:rel_type].to_s
|
|
318
|
+
relationship.from_id = extract_text(stmt[:from]) if stmt[:from]
|
|
319
|
+
relationship.to_id = extract_text(stmt[:to]) if stmt[:to]
|
|
320
|
+
relationship.label = extract_text(stmt[:label]) if stmt[:label]
|
|
321
|
+
relationship.technology = extract_text(stmt[:technology]) if
|
|
322
|
+
stmt[:technology]
|
|
323
|
+
|
|
324
|
+
diagram.relationships << relationship
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def extract_text(value)
|
|
328
|
+
case value
|
|
329
|
+
when Hash
|
|
330
|
+
if value[:string]
|
|
331
|
+
value[:string].to_s
|
|
332
|
+
elsif value[:var]
|
|
333
|
+
# Variable reference like ${macroName}
|
|
334
|
+
value[:var].to_s
|
|
335
|
+
else
|
|
336
|
+
value.values.first.to_s
|
|
337
|
+
end
|
|
338
|
+
when String
|
|
339
|
+
value
|
|
340
|
+
else
|
|
341
|
+
value.to_s
|
|
342
|
+
end.strip
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../diagram/class_diagram'
|
|
4
|
+
|
|
5
|
+
module Sirena
|
|
6
|
+
module Parser
|
|
7
|
+
module Transforms
|
|
8
|
+
# Transform for converting Parslet parse tree to Class diagram model.
|
|
9
|
+
#
|
|
10
|
+
# Converts the parse tree output from Grammars::ClassDiagram into a
|
|
11
|
+
# fully-formed Diagram::ClassDiagram object with entities and
|
|
12
|
+
# relationships.
|
|
13
|
+
class ClassDiagram
|
|
14
|
+
# Relationship type mappings from operators
|
|
15
|
+
RELATIONSHIP_TYPES = {
|
|
16
|
+
'<|--' => 'inheritance',
|
|
17
|
+
'--|>' => 'inheritance',
|
|
18
|
+
'*--' => 'composition',
|
|
19
|
+
'--*' => 'composition',
|
|
20
|
+
'o--' => 'aggregation',
|
|
21
|
+
'--o' => 'aggregation',
|
|
22
|
+
'-->' => 'association',
|
|
23
|
+
'<--' => 'association',
|
|
24
|
+
'--' => 'association',
|
|
25
|
+
'..|>' => 'realization',
|
|
26
|
+
'<|..' => 'realization',
|
|
27
|
+
'..>' => 'dependency',
|
|
28
|
+
'<..' => 'dependency',
|
|
29
|
+
'..' => 'association'
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
# Operators where arrow points left (reverse direction)
|
|
33
|
+
LEFT_POINTING = ['<|--', '<--', '<|..', '<..'].freeze
|
|
34
|
+
|
|
35
|
+
# Visibility symbol mappings
|
|
36
|
+
VISIBILITY_SYMBOLS = {
|
|
37
|
+
'+' => 'public',
|
|
38
|
+
'-' => 'private',
|
|
39
|
+
'#' => 'protected',
|
|
40
|
+
'~' => 'package'
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# Transform parse tree into Class diagram.
|
|
44
|
+
#
|
|
45
|
+
# @param tree [Array, Hash] Parslet parse tree
|
|
46
|
+
# @return [Diagram::ClassDiagram] the Class diagram model
|
|
47
|
+
def apply(tree)
|
|
48
|
+
@diagram = Diagram::ClassDiagram.new
|
|
49
|
+
@current_namespace = nil
|
|
50
|
+
|
|
51
|
+
# Tree is an array: [header, direction, ...statements]
|
|
52
|
+
if tree.is_a?(Array)
|
|
53
|
+
tree.each do |item|
|
|
54
|
+
process_item(item) if item.is_a?(Hash)
|
|
55
|
+
end
|
|
56
|
+
elsif tree.is_a?(Hash)
|
|
57
|
+
process_item(tree)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@diagram
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def process_item(item)
|
|
66
|
+
return unless item.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
# Process header to get direction
|
|
69
|
+
if item[:direction] && item[:direction][:dir_value]
|
|
70
|
+
@diagram.direction = extract_text(item[:direction][:dir_value])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Process statements
|
|
74
|
+
process_statement(item) unless item[:header] || item[:direction]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def process_statement(stmt)
|
|
78
|
+
return unless stmt.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
if stmt[:namespace_keyword]
|
|
81
|
+
# Namespace block
|
|
82
|
+
process_namespace(stmt)
|
|
83
|
+
elsif stmt[:keyword] == 'class' && stmt[:class_id]
|
|
84
|
+
# Class declaration
|
|
85
|
+
process_class_declaration(stmt)
|
|
86
|
+
elsif stmt[:stereotype] && stmt[:class_id] && !stmt[:keyword]
|
|
87
|
+
# Standalone stereotype
|
|
88
|
+
process_standalone_stereotype(stmt)
|
|
89
|
+
elsif stmt[:class_id] && stmt[:member]
|
|
90
|
+
# Colon member definition
|
|
91
|
+
process_colon_member(stmt)
|
|
92
|
+
elsif stmt[:from_id] && stmt[:to_id] && stmt[:operator]
|
|
93
|
+
# Relationship
|
|
94
|
+
process_relationship(stmt)
|
|
95
|
+
elsif stmt[:link_keyword] || stmt[:callback_keyword]
|
|
96
|
+
# Link or callback - ignore for now
|
|
97
|
+
nil
|
|
98
|
+
elsif stmt[:class_id] && !stmt[:keyword]
|
|
99
|
+
# Standalone class
|
|
100
|
+
ensure_entity_exists(extract_text(stmt[:class_id]))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def process_namespace(stmt)
|
|
105
|
+
namespace_name = extract_text(stmt[:namespace_name])
|
|
106
|
+
old_namespace = @current_namespace
|
|
107
|
+
@current_namespace = namespace_name
|
|
108
|
+
|
|
109
|
+
# Process namespace body
|
|
110
|
+
if stmt[:namespace_body]
|
|
111
|
+
statements = Array(stmt[:namespace_body])
|
|
112
|
+
statements.each do |s|
|
|
113
|
+
process_statement(s) if s.is_a?(Hash)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@current_namespace = old_namespace
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def process_class_declaration(stmt)
|
|
121
|
+
class_id = extract_text(stmt[:class_id])
|
|
122
|
+
class_id = qualify_name(class_id)
|
|
123
|
+
|
|
124
|
+
entity = find_or_create_entity(class_id)
|
|
125
|
+
|
|
126
|
+
# Handle stereotype
|
|
127
|
+
if stmt[:stereotype] && stmt[:stereotype][:stereotype_value]
|
|
128
|
+
entity.stereotype = extract_text(stmt[:stereotype][:stereotype_value])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Handle generic parameters
|
|
132
|
+
if stmt[:generic] && stmt[:generic][:generic_type]
|
|
133
|
+
generic_type = extract_text(stmt[:generic][:generic_type])
|
|
134
|
+
# Append generic to class name for display
|
|
135
|
+
entity.name = "#{entity.name}~#{generic_type}~"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Handle class body
|
|
139
|
+
process_class_body(entity, stmt[:body]) if stmt[:body]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def process_standalone_stereotype(stmt)
|
|
143
|
+
class_id = extract_text(stmt[:class_id])
|
|
144
|
+
class_id = qualify_name(class_id)
|
|
145
|
+
|
|
146
|
+
entity = find_or_create_entity(class_id)
|
|
147
|
+
|
|
148
|
+
if stmt[:stereotype][:stereotype_value]
|
|
149
|
+
entity.stereotype = extract_text(stmt[:stereotype][:stereotype_value])
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def process_colon_member(stmt)
|
|
154
|
+
class_id = extract_text(stmt[:class_id])
|
|
155
|
+
class_id = qualify_name(class_id)
|
|
156
|
+
|
|
157
|
+
entity = find_or_create_entity(class_id)
|
|
158
|
+
|
|
159
|
+
# Parse visibility
|
|
160
|
+
visibility = parse_visibility(stmt[:visibility])
|
|
161
|
+
|
|
162
|
+
# Parse member
|
|
163
|
+
member_data = stmt[:member]
|
|
164
|
+
if member_data[:method_name]
|
|
165
|
+
# It's a method
|
|
166
|
+
add_method_to_entity(entity, member_data, visibility)
|
|
167
|
+
else
|
|
168
|
+
# It's an attribute
|
|
169
|
+
add_attribute_to_entity(entity, member_data, visibility)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def process_class_body(entity, body_data)
|
|
174
|
+
return unless body_data.is_a?(Array)
|
|
175
|
+
|
|
176
|
+
body_data.each do |member_item|
|
|
177
|
+
next unless member_item.is_a?(Hash)
|
|
178
|
+
next unless member_item[:member]
|
|
179
|
+
|
|
180
|
+
visibility = parse_visibility(member_item[:visibility])
|
|
181
|
+
member_data = member_item[:member]
|
|
182
|
+
|
|
183
|
+
if member_data[:method_name]
|
|
184
|
+
add_method_to_entity(entity, member_data, visibility)
|
|
185
|
+
else
|
|
186
|
+
add_attribute_to_entity(entity, member_data, visibility)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def add_method_to_entity(entity, method_data, visibility)
|
|
192
|
+
method_name = extract_text(method_data[:method_name])
|
|
193
|
+
parameters = method_data[:parameters] ? extract_text(method_data[:parameters]) : ''
|
|
194
|
+
return_type = nil
|
|
195
|
+
|
|
196
|
+
if method_data[:return_type] && method_data[:return_type][:type]
|
|
197
|
+
return_type = extract_text(method_data[:return_type][:type])
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
method = Diagram::ClassMethod.new.tap do |m|
|
|
201
|
+
m.name = method_name
|
|
202
|
+
m.parameters = parameters
|
|
203
|
+
m.return_type = return_type
|
|
204
|
+
m.visibility = visibility
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
entity.class_methods << method
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def add_attribute_to_entity(entity, attr_data, visibility)
|
|
211
|
+
attr_name = extract_text(attr_data[:attr_name])
|
|
212
|
+
attr_type = attr_data[:type] ? extract_text(attr_data[:type]) : nil
|
|
213
|
+
|
|
214
|
+
attribute = Diagram::ClassAttribute.new.tap do |attr|
|
|
215
|
+
attr.name = attr_name
|
|
216
|
+
attr.type = attr_type
|
|
217
|
+
attr.visibility = visibility
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
entity.attributes << attribute
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def process_relationship(stmt)
|
|
224
|
+
from_id = extract_text(stmt[:from_id])
|
|
225
|
+
to_id = extract_text(stmt[:to_id])
|
|
226
|
+
operator = extract_text(stmt[:operator][:arrow])
|
|
227
|
+
|
|
228
|
+
# Qualify names if in namespace
|
|
229
|
+
from_id = qualify_name(from_id)
|
|
230
|
+
to_id = qualify_name(to_id)
|
|
231
|
+
|
|
232
|
+
# Ensure both entities exist
|
|
233
|
+
ensure_entity_exists(from_id)
|
|
234
|
+
ensure_entity_exists(to_id)
|
|
235
|
+
|
|
236
|
+
# Get relationship type
|
|
237
|
+
relationship_type = RELATIONSHIP_TYPES[operator]
|
|
238
|
+
relationship_type ||= 'association'
|
|
239
|
+
|
|
240
|
+
# Parse cardinality
|
|
241
|
+
source_card = nil
|
|
242
|
+
target_card = nil
|
|
243
|
+
|
|
244
|
+
if stmt[:source_card]
|
|
245
|
+
source_card = extract_text(stmt[:source_card][:string])
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
if stmt[:target_card]
|
|
249
|
+
target_card = extract_text(stmt[:target_card][:string])
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Parse label
|
|
253
|
+
label = nil
|
|
254
|
+
if stmt[:pipe_label] && stmt[:pipe_label][:label_text]
|
|
255
|
+
label = extract_text(stmt[:pipe_label][:label_text])
|
|
256
|
+
# Strip surrounding quotes if present
|
|
257
|
+
label = label.gsub(/^["']|["']$/, '') if label
|
|
258
|
+
elsif stmt[:colon_label] && stmt[:colon_label][:label_text]
|
|
259
|
+
label = extract_text(stmt[:colon_label][:label_text]).strip
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Determine direction
|
|
263
|
+
# For left-pointing arrows, reverse the relationship
|
|
264
|
+
if LEFT_POINTING.include?(operator)
|
|
265
|
+
actual_from = to_id
|
|
266
|
+
actual_to = from_id
|
|
267
|
+
# Swap cardinalities
|
|
268
|
+
actual_source_card = target_card
|
|
269
|
+
actual_target_card = source_card
|
|
270
|
+
else
|
|
271
|
+
actual_from = from_id
|
|
272
|
+
actual_to = to_id
|
|
273
|
+
actual_source_card = source_card
|
|
274
|
+
actual_target_card = target_card
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
relationship = Diagram::ClassRelationship.new.tap do |rel|
|
|
278
|
+
rel.from_id = actual_from
|
|
279
|
+
rel.to_id = actual_to
|
|
280
|
+
rel.relationship_type = relationship_type
|
|
281
|
+
rel.label = label
|
|
282
|
+
rel.source_cardinality = actual_source_card
|
|
283
|
+
rel.target_cardinality = actual_target_card
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
@diagram.relationships << relationship
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def find_or_create_entity(class_id)
|
|
290
|
+
existing = @diagram.find_entity(class_id)
|
|
291
|
+
return existing if existing
|
|
292
|
+
|
|
293
|
+
entity = Diagram::ClassEntity.new.tap do |e|
|
|
294
|
+
e.id = class_id
|
|
295
|
+
e.name = class_id
|
|
296
|
+
end
|
|
297
|
+
@diagram.entities << entity
|
|
298
|
+
entity
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def ensure_entity_exists(class_id)
|
|
302
|
+
find_or_create_entity(class_id)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def qualify_name(name)
|
|
306
|
+
return name unless @current_namespace
|
|
307
|
+
return name if name.include?('.')
|
|
308
|
+
|
|
309
|
+
"#{@current_namespace}.#{name}"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def parse_visibility(vis_data)
|
|
313
|
+
return 'public' unless vis_data
|
|
314
|
+
|
|
315
|
+
symbol = extract_text(vis_data[:vis_symbol])
|
|
316
|
+
VISIBILITY_SYMBOLS[symbol] || 'public'
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def extract_text(value)
|
|
320
|
+
case value
|
|
321
|
+
when Hash
|
|
322
|
+
if value[:string]
|
|
323
|
+
value[:string].to_s
|
|
324
|
+
elsif value[:arrow]
|
|
325
|
+
value[:arrow].to_s
|
|
326
|
+
elsif value[:stereotype_value]
|
|
327
|
+
value[:stereotype_value].to_s
|
|
328
|
+
elsif value[:dir_value]
|
|
329
|
+
value[:dir_value].to_s
|
|
330
|
+
elsif value[:label_text]
|
|
331
|
+
value[:label_text].to_s
|
|
332
|
+
elsif value[:vis_symbol]
|
|
333
|
+
value[:vis_symbol].to_s
|
|
334
|
+
elsif value[:type]
|
|
335
|
+
value[:type].to_s
|
|
336
|
+
elsif value[:attr_name]
|
|
337
|
+
value[:attr_name].to_s
|
|
338
|
+
elsif value[:method_name]
|
|
339
|
+
value[:method_name].to_s
|
|
340
|
+
else
|
|
341
|
+
value.values.first.to_s
|
|
342
|
+
end
|
|
343
|
+
when String
|
|
344
|
+
value
|
|
345
|
+
else
|
|
346
|
+
value.to_s
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|